From 358347602cfd43673f4cc64f598440cd5417baeb Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 16 Dec 2021 12:26:52 +0100 Subject: [PATCH 001/256] Switch hermite_spline_xz to index Might help with performance --- include/interpolation_xz.hxx | 2 +- src/mesh/interpolation/hermite_spline_xz.cxx | 86 +++++++++---------- .../monotonic_hermite_spline_xz.cxx | 61 ++++++------- 3 files changed, 65 insertions(+), 84 deletions(-) diff --git a/include/interpolation_xz.hxx b/include/interpolation_xz.hxx index 47474ad39c..04269a830d 100644 --- a/include/interpolation_xz.hxx +++ b/include/interpolation_xz.hxx @@ -95,7 +95,7 @@ protected: /// This is protected rather than private so that it can be /// extended and used by HermiteSplineMonotonic - Tensor i_corner; // x-index of bottom-left grid point + Tensor> i_corner; // index of bottom-left grid point Tensor k_corner; // z-index of bottom-left grid point // Basis functions for cubic Hermite spline interpolation diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index f5ce3357cb..9f14d4b836 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -38,7 +38,6 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) // Initialise in order to avoid 'uninitialized value' errors from Valgrind when using // guard-cell values - i_corner = -1; k_corner = -1; // Allocate Field3D members @@ -55,6 +54,8 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region) { + const int ny = localmesh->LocalNy; + const int nz = localmesh->LocalNz; BOUT_FOR(i, delta_x.getRegion(region)) { const int x = i.x(); const int y = i.y(); @@ -65,29 +66,29 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z // The integer part of xt_prime, zt_prime are the indices of the cell // containing the field line end-point - i_corner(x, y, z) = static_cast(floor(delta_x(x, y, z))); + int i_corn = static_cast(floor(delta_x(x, y, z))); k_corner(x, y, z) = static_cast(floor(delta_z(x, y, z))); // t_x, t_z are the normalised coordinates \in [0,1) within the cell // calculated by taking the remainder of the floating point index - BoutReal t_x = delta_x(x, y, z) - static_cast(i_corner(x, y, z)); + BoutReal t_x = delta_x(x, y, z) - static_cast(i_corn); BoutReal t_z = delta_z(x, y, z) - static_cast(k_corner(x, y, z)); // NOTE: A (small) hack to avoid one-sided differences - if (i_corner(x, y, z) >= localmesh->xend) { - i_corner(x, y, z) = localmesh->xend - 1; + if (i_corn >= localmesh->xend) { + i_corn = localmesh->xend - 1; t_x = 1.0; } - if (i_corner(x, y, z) < localmesh->xstart) { - i_corner(x, y, z) = localmesh->xstart; + if (i_corn < localmesh->xstart) { + i_corn = localmesh->xstart; t_x = 0.0; } // Check that t_x and t_z are in range if ((t_x < 0.0) || (t_x > 1.0)) { throw BoutException( - "t_x={:e} out of range at ({:d},{:d},{:d}) (delta_x={:e}, i_corner={:d})", t_x, - x, y, z, delta_x(x, y, z), i_corner(x, y, z)); + "t_x={:e} out of range at ({:d},{:d},{:d}) (delta_x={:e}, i_corn={:d})", t_x, + x, y, z, delta_x(x, y, z), i_corn); } if ((t_z < 0.0) || (t_z > 1.0)) { @@ -96,17 +97,20 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z x, y, z, delta_z(x, y, z), k_corner(x, y, z)); } - h00_x(x, y, z) = (2. * t_x * t_x * t_x) - (3. * t_x * t_x) + 1.; - h00_z(x, y, z) = (2. * t_z * t_z * t_z) - (3. * t_z * t_z) + 1.; + i_corner[i] = SpecificInd( + (((i_corn * ny) + (y + y_offset)) * nz + k_corner(x, y, z)), ny, nz); - h01_x(x, y, z) = (-2. * t_x * t_x * t_x) + (3. * t_x * t_x); - h01_z(x, y, z) = (-2. * t_z * t_z * t_z) + (3. * t_z * t_z); + h00_x[i] = (2. * t_x * t_x * t_x) - (3. * t_x * t_x) + 1.; + h00_z[i] = (2. * t_z * t_z * t_z) - (3. * t_z * t_z) + 1.; - h10_x(x, y, z) = t_x * (1. - t_x) * (1. - t_x); - h10_z(x, y, z) = t_z * (1. - t_z) * (1. - t_z); + h01_x[i] = (-2. * t_x * t_x * t_x) + (3. * t_x * t_x); + h01_z[i] = (-2. * t_z * t_z * t_z) + (3. * t_z * t_z); - h11_x(x, y, z) = (t_x * t_x * t_x) - (t_x * t_x); - h11_z(x, y, z) = (t_z * t_z * t_z) - (t_z * t_z); + h10_x[i] = t_x * (1. - t_x) * (1. - t_x); + h10_z[i] = t_z * (1. - t_z) * (1. - t_z); + + h11_x[i] = (t_x * t_x * t_x) - (t_x * t_x); + h11_z[i] = (t_z * t_z * t_z) - (t_z * t_z); } } @@ -137,8 +141,8 @@ XZHermiteSpline::getWeightsForYApproximation(int i, int j, int k, int yoffset) { const int ncz = localmesh->LocalNz; const int k_mod = ((k_corner(i, j, k) % ncz) + ncz) % ncz; const int k_mod_m1 = (k_mod > 0) ? (k_mod - 1) : (ncz - 1); - const int k_mod_p1 = (k_mod + 1) % ncz; - const int k_mod_p2 = (k_mod + 2) % ncz; + const int k_mod_p1 = (k_mod == ncz) ? 0 : k_mod + 1; + const int k_mod_p2 = (k_mod_p1 == ncz) ? 0 : k_mod_p1 + 1; return {{i, j + yoffset, k_mod_m1, -0.5 * h10_z(i, j, k)}, {i, j + yoffset, k_mod, h00_z(i, j, k) - 0.5 * h11_z(i, j, k)}, @@ -183,45 +187,35 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region if (skip_mask(x, y, z)) continue; - // Due to lack of guard cells in z-direction, we need to ensure z-index - // wraps around - const int ncz = localmesh->LocalNz; - const int z_mod = ((k_corner(x, y, z) % ncz) + ncz) % ncz; - const int z_mod_p1 = (z_mod + 1) % ncz; + const auto iyp = i.yp(y_offset); - const int y_next = y + y_offset; + const auto ic = i_corner[i]; + const auto iczp = ic.zp(); + const auto icxp = ic.xp(); + const auto icxpzp = iczp.xp(); // Interpolate f in X at Z - const BoutReal f_z = f(i_corner(x, y, z), y_next, z_mod) * h00_x(x, y, z) - + f(i_corner(x, y, z) + 1, y_next, z_mod) * h01_x(x, y, z) - + fx(i_corner(x, y, z), y_next, z_mod) * h10_x(x, y, z) - + fx(i_corner(x, y, z) + 1, y_next, z_mod) * h11_x(x, y, z); + const BoutReal f_z = + f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; // Interpolate f in X at Z+1 - const BoutReal f_zp1 = f(i_corner(x, y, z), y_next, z_mod_p1) * h00_x(x, y, z) - + f(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h01_x(x, y, z) - + fx(i_corner(x, y, z), y_next, z_mod_p1) * h10_x(x, y, z) - + fx(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h11_x(x, y, z); + const BoutReal f_zp1 = f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + fx[iczp] * h10_x[i] + + fx[icxpzp] * h11_x[i]; // Interpolate fz in X at Z - const BoutReal fz_z = fz(i_corner(x, y, z), y_next, z_mod) * h00_x(x, y, z) - + fz(i_corner(x, y, z) + 1, y_next, z_mod) * h01_x(x, y, z) - + fxz(i_corner(x, y, z), y_next, z_mod) * h10_x(x, y, z) - + fxz(i_corner(x, y, z) + 1, y_next, z_mod) * h11_x(x, y, z); + const BoutReal fz_z = fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + fxz[ic] * h10_x[i] + + fxz[icxp] * h11_x[i]; // Interpolate fz in X at Z+1 - const BoutReal fz_zp1 = - fz(i_corner(x, y, z), y_next, z_mod_p1) * h00_x(x, y, z) - + fz(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h01_x(x, y, z) - + fxz(i_corner(x, y, z), y_next, z_mod_p1) * h10_x(x, y, z) - + fxz(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h11_x(x, y, z); + const BoutReal fz_zp1 = fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + + fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; // Interpolate in Z - f_interp(x, y_next, z) = +f_z * h00_z(x, y, z) + f_zp1 * h01_z(x, y, z) - + fz_z * h10_z(x, y, z) + fz_zp1 * h11_z(x, y, z); + f_interp[iyp] = + +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; - ASSERT2(std::isfinite(f_interp(x, y_next, z)) || x < localmesh->xstart - || x > localmesh->xend); + ASSERT2(std::isfinite(f_interp[iyp]) || i.x() < localmesh->xstart + || i.x() > localmesh->xend); } return f_interp; } diff --git a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx index bcf402231b..b2cfdb9515 100644 --- a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx +++ b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx @@ -66,44 +66,35 @@ Field3D XZMonotonicHermiteSpline::interpolate(const Field3D& f, if (skip_mask(x, y, z)) continue; - // Due to lack of guard cells in z-direction, we need to ensure z-index - // wraps around - const int ncz = localmesh->LocalNz; - const int z_mod = ((k_corner(x, y, z) % ncz) + ncz) % ncz; - const int z_mod_p1 = (z_mod + 1) % ncz; + const auto iyp = i.yp(y_offset); - const int y_next = y + y_offset; + const auto ic = i_corner[i]; + const auto iczp = ic.zp(); + const auto icxp = ic.xp(); + const auto icxpzp = iczp.xp(); // Interpolate f in X at Z - const BoutReal f_z = f(i_corner(x, y, z), y_next, z_mod) * h00_x(x, y, z) - + f(i_corner(x, y, z) + 1, y_next, z_mod) * h01_x(x, y, z) - + fx(i_corner(x, y, z), y_next, z_mod) * h10_x(x, y, z) - + fx(i_corner(x, y, z) + 1, y_next, z_mod) * h11_x(x, y, z); + const BoutReal f_z = + f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; // Interpolate f in X at Z+1 - const BoutReal f_zp1 = f(i_corner(x, y, z), y_next, z_mod_p1) * h00_x(x, y, z) - + f(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h01_x(x, y, z) - + fx(i_corner(x, y, z), y_next, z_mod_p1) * h10_x(x, y, z) - + fx(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h11_x(x, y, z); + const BoutReal f_zp1 = f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + fx[iczp] * h10_x[i] + + fx[icxpzp] * h11_x[i]; // Interpolate fz in X at Z - const BoutReal fz_z = fz(i_corner(x, y, z), y_next, z_mod) * h00_x(x, y, z) - + fz(i_corner(x, y, z) + 1, y_next, z_mod) * h01_x(x, y, z) - + fxz(i_corner(x, y, z), y_next, z_mod) * h10_x(x, y, z) - + fxz(i_corner(x, y, z) + 1, y_next, z_mod) * h11_x(x, y, z); + const BoutReal fz_z = fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + fxz[ic] * h10_x[i] + + fxz[icxp] * h11_x[i]; // Interpolate fz in X at Z+1 - const BoutReal fz_zp1 = - fz(i_corner(x, y, z), y_next, z_mod_p1) * h00_x(x, y, z) - + fz(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h01_x(x, y, z) - + fxz(i_corner(x, y, z), y_next, z_mod_p1) * h10_x(x, y, z) - + fxz(i_corner(x, y, z) + 1, y_next, z_mod_p1) * h11_x(x, y, z); + const BoutReal fz_zp1 = fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + + fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; // Interpolate in Z - BoutReal result = +f_z * h00_z(x, y, z) + f_zp1 * h01_z(x, y, z) - + fz_z * h10_z(x, y, z) + fz_zp1 * h11_z(x, y, z); + BoutReal result = + +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; - ASSERT2(std::isfinite(result) || x < localmesh->xstart || x > localmesh->xend); + ASSERT2(std::isfinite(result) || i.x() < localmesh->xstart + || i.x() > localmesh->xend); // Monotonicity // Force the interpolated result to be in the range of the @@ -111,18 +102,14 @@ Field3D XZMonotonicHermiteSpline::interpolate(const Field3D& f, // but also degrades accuracy near maxima and minima. // Perhaps should only impose near boundaries, since that is where // problems most obviously occur. - const BoutReal localmax = BOUTMAX(f(i_corner(x, y, z), y_next, z_mod), - f(i_corner(x, y, z) + 1, y_next, z_mod), - f(i_corner(x, y, z), y_next, z_mod_p1), - f(i_corner(x, y, z) + 1, y_next, z_mod_p1)); + const BoutReal localmax = BOUTMAX(f[ic], f[icxp], f[iczp], f[icxpzp]); - const BoutReal localmin = BOUTMIN(f(i_corner(x, y, z), y_next, z_mod), - f(i_corner(x, y, z) + 1, y_next, z_mod), - f(i_corner(x, y, z), y_next, z_mod_p1), - f(i_corner(x, y, z) + 1, y_next, z_mod_p1)); + const BoutReal localmin = BOUTMIN(f[ic], f[icxp], f[iczp], f[icxpzp]); - ASSERT2(std::isfinite(localmax) || x < localmesh->xstart || x > localmesh->xend); - ASSERT2(std::isfinite(localmin) || x < localmesh->xstart || x > localmesh->xend); + ASSERT2(std::isfinite(localmax) || i.x() < localmesh->xstart + || i.x() > localmesh->xend); + ASSERT2(std::isfinite(localmin) || i.x() < localmesh->xstart + || i.x() > localmesh->xend); if (result > localmax) { result = localmax; @@ -131,7 +118,7 @@ Field3D XZMonotonicHermiteSpline::interpolate(const Field3D& f, result = localmin; } - f_interp(x, y_next, z) = result; + f_interp[iyp] = result; } return f_interp; } From 4ec74efcead5f2aabdac00ea950f166d17c92176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Schw=C3=B6rer?= Date: Wed, 22 Sep 2021 11:28:45 +0200 Subject: [PATCH 002/256] Switch toward regions --- include/interpolation_xz.hxx | 53 +++++++++++++++---- include/mask.hxx | 10 ++++ src/mesh/interpolation/bilinear_xz.cxx | 12 ++--- src/mesh/interpolation/hermite_spline_xz.cxx | 18 ++----- src/mesh/interpolation/lagrange_4pt_xz.cxx | 15 ++---- .../monotonic_hermite_spline_xz.cxx | 7 +-- 6 files changed, 65 insertions(+), 50 deletions(-) diff --git a/include/interpolation_xz.hxx b/include/interpolation_xz.hxx index 04269a830d..7a58ce3346 100644 --- a/include/interpolation_xz.hxx +++ b/include/interpolation_xz.hxx @@ -37,24 +37,56 @@ const Field3D interpolate(const Field2D &f, const Field3D &delta_x, const Field3D interpolate(const Field2D &f, const Field3D &delta_x); class XZInterpolation { +public: + int y_offset; + protected: Mesh* localmesh{nullptr}; - // 3D vector of points to skip (true -> skip this point) - BoutMask skip_mask; + std::string region_name{""}; + std::shared_ptr> region{nullptr}; public: XZInterpolation(int y_offset = 0, Mesh* localmeshIn = nullptr) - : localmesh(localmeshIn == nullptr ? bout::globals::mesh : localmeshIn), - skip_mask(*localmesh, false), y_offset(y_offset) {} + : y_offset(y_offset), + localmesh(localmeshIn == nullptr ? bout::globals::mesh : localmeshIn), + region_name("RGN_ALL") {} XZInterpolation(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZInterpolation(y_offset, mesh) { - skip_mask = mask; + region = regionFromMask(mask, localmesh); } + XZInterpolation(const std::string& region_name, int y_offset = 0, Mesh* mesh = nullptr) + : y_offset(y_offset), localmesh(mesh), region_name(region_name) {} + XZInterpolation(std::shared_ptr> region, int y_offset = 0, + Mesh* mesh = nullptr) + : y_offset(y_offset), localmesh(mesh), region(region) {} virtual ~XZInterpolation() = default; - - void setMask(const BoutMask &mask) { skip_mask = mask; } + void setMask(const BoutMask& mask) { + region = regionFromMask(mask, localmesh); + region_name = ""; + } + void setRegion(const std::string& region_name) { + this->region_name = region_name; + this->region = nullptr; + } + void setRegion(const std::shared_ptr>& region) { + this->region_name = ""; + this->region = region; + } + Region getRegion() const { + if (region_name != "") { + return localmesh->getRegion(region_name); + } + ASSERT1(region != nullptr); + return *region; + } + Region getRegion(const std::string& region) const { + if (region != "" and region != "RGN_ALL") { + return getIntersection(localmesh->getRegion(region), getRegion()); + } + return getRegion(); + } virtual void calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region = "RGN_NOBNDRY") = 0; virtual void calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -71,7 +103,6 @@ public: const std::string& region = "RGN_NOBNDRY") = 0; // Interpolate using the field at (x,y+y_offset,z), rather than (x,y,z) - int y_offset; void setYOffset(int offset) { y_offset = offset; } virtual std::vector @@ -119,7 +150,7 @@ public: XZHermiteSpline(int y_offset = 0, Mesh *mesh = nullptr); XZHermiteSpline(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZHermiteSpline(y_offset, mesh) { - skip_mask = mask; + region = regionFromMask(mask, localmesh); } void calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -177,7 +208,7 @@ public: XZLagrange4pt(int y_offset = 0, Mesh *mesh = nullptr); XZLagrange4pt(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZLagrange4pt(y_offset, mesh) { - skip_mask = mask; + region = regionFromMask(mask, localmesh); } void calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -210,7 +241,7 @@ public: XZBilinear(int y_offset = 0, Mesh *mesh = nullptr); XZBilinear(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZBilinear(y_offset, mesh) { - skip_mask = mask; + region = regionFromMask(mask, localmesh); } void calcWeights(const Field3D& delta_x, const Field3D& delta_z, diff --git a/include/mask.hxx b/include/mask.hxx index 8940edbb16..c26bf31d61 100644 --- a/include/mask.hxx +++ b/include/mask.hxx @@ -72,4 +72,14 @@ public: } }; +inline std::unique_ptr> regionFromMask(const BoutMask& mask, + const Mesh* mesh) { + std::vector indices; + for (auto i : mesh->getRegion("RGN_ALL")) { + if (not mask(i.x(), i.y(), i.z())) { + indices.push_back(i); + } + } + return std::make_unique>(indices); +} #endif //__MASK_H__ diff --git a/src/mesh/interpolation/bilinear_xz.cxx b/src/mesh/interpolation/bilinear_xz.cxx index 7819fafe6f..1869df3218 100644 --- a/src/mesh/interpolation/bilinear_xz.cxx +++ b/src/mesh/interpolation/bilinear_xz.cxx @@ -45,14 +45,11 @@ XZBilinear::XZBilinear(int y_offset, Mesh *mesh) void XZBilinear::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region) { - BOUT_FOR(i, delta_x.getRegion(region)) { + BOUT_FOR(i, getRegion(region)) { const int x = i.x(); const int y = i.y(); const int z = i.z(); - if (skip_mask(x, y, z)) - continue; - // The integer part of xt_prime, zt_prime are the indices of the cell // containing the field line end-point i_corner(x, y, z) = static_cast(floor(delta_x(x, y, z))); @@ -87,7 +84,7 @@ void XZBilinear::calcWeights(const Field3D& delta_x, const Field3D& delta_z, void XZBilinear::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const BoutMask& mask, const std::string& region) { - skip_mask = mask; + setMask(mask); calcWeights(delta_x, delta_z, region); } @@ -95,14 +92,11 @@ Field3D XZBilinear::interpolate(const Field3D& f, const std::string& region) con ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; - BOUT_FOR(i, f.getRegion(region)) { + BOUT_FOR(i, this->getRegion(region)) { const int x = i.x(); const int y = i.y(); const int z = i.z(); - if (skip_mask(x, y, z)) - continue; - const int y_next = y + y_offset; // Due to lack of guard cells in z-direction, we need to ensure z-index // wraps around diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 9f14d4b836..a682c58839 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -56,14 +56,11 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z const int ny = localmesh->LocalNy; const int nz = localmesh->LocalNz; - BOUT_FOR(i, delta_x.getRegion(region)) { + BOUT_FOR(i, getRegion(region)) { const int x = i.x(); const int y = i.y(); const int z = i.z(); - if (skip_mask(x, y, z)) - continue; - // The integer part of xt_prime, zt_prime are the indices of the cell // containing the field line end-point int i_corn = static_cast(floor(delta_x(x, y, z))); @@ -116,7 +113,7 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const BoutMask& mask, const std::string& region) { - skip_mask = mask; + setMask(mask); calcWeights(delta_x, delta_z, region); } @@ -179,16 +176,7 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region localmesh->wait(h); } - BOUT_FOR(i, f.getRegion(region)) { - const int x = i.x(); - const int y = i.y(); - const int z = i.z(); - - if (skip_mask(x, y, z)) - continue; - - const auto iyp = i.yp(y_offset); - + BOUT_FOR(i, getRegion(region)) { const auto ic = i_corner[i]; const auto iczp = ic.zp(); const auto icxp = ic.xp(); diff --git a/src/mesh/interpolation/lagrange_4pt_xz.cxx b/src/mesh/interpolation/lagrange_4pt_xz.cxx index 3a5de28e59..7c79a3f713 100644 --- a/src/mesh/interpolation/lagrange_4pt_xz.cxx +++ b/src/mesh/interpolation/lagrange_4pt_xz.cxx @@ -39,15 +39,12 @@ XZLagrange4pt::XZLagrange4pt(int y_offset, Mesh *mesh) void XZLagrange4pt::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region) { - - BOUT_FOR(i, delta_x.getRegion(region)) { + const auto curregion = getRegion(region); + BOUT_FOR(i, curregion) { const int x = i.x(); const int y = i.y(); const int z = i.z(); - if (skip_mask(x, y, z)) - continue; - // The integer part of xt_prime, zt_prime are the indices of the cell // containing the field line end-point i_corner(x, y, z) = static_cast(floor(delta_x(x, y, z))); @@ -80,7 +77,7 @@ void XZLagrange4pt::calcWeights(const Field3D& delta_x, const Field3D& delta_z, void XZLagrange4pt::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const BoutMask& mask, const std::string& region) { - skip_mask = mask; + setMask(mask); calcWeights(delta_x, delta_z, region); } @@ -89,14 +86,12 @@ Field3D XZLagrange4pt::interpolate(const Field3D& f, const std::string& region) ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; - BOUT_FOR(i, f.getRegion(region)) { + const auto curregion{getRegion(region)}; + BOUT_FOR(i, curregion) { const int x = i.x(); const int y = i.y(); const int z = i.z(); - if (skip_mask(x, y, z)) - continue; - const int jx = i_corner(x, y, z); const int jx2mnew = (jx == 0) ? 0 : (jx - 1); const int jxpnew = jx + 1; diff --git a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx index b2cfdb9515..47eeb2df20 100644 --- a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx +++ b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx @@ -22,7 +22,7 @@ #include "globals.hxx" #include "interpolation_xz.hxx" -#include "output.hxx" +//#include "output.hxx" #include "bout/index_derivs_interface.hxx" #include "bout/mesh.hxx" @@ -58,14 +58,11 @@ Field3D XZMonotonicHermiteSpline::interpolate(const Field3D& f, localmesh->wait(h); } - BOUT_FOR(i, f.getRegion(region)) { + BOUT_FOR(i, getRegion(region)) { const int x = i.x(); const int y = i.y(); const int z = i.z(); - if (skip_mask(x, y, z)) - continue; - const auto iyp = i.yp(y_offset); const auto ic = i_corner[i]; From d0960204c870dc52cfe5315de22a8e5a4607b4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Schw=C3=B6rer?= Date: Fri, 24 Sep 2021 11:19:50 +0200 Subject: [PATCH 003/256] Switch to regions for FCI regions --- include/bout/region.hxx | 23 +++++ include/interpolation_xz.hxx | 14 ++- include/mask.hxx | 1 + include/utils.hxx | 9 ++ src/mesh/interpolation/bilinear_xz.cxx | 6 +- src/mesh/interpolation/lagrange_4pt_xz.cxx | 2 +- .../monotonic_hermite_spline_xz.cxx | 3 +- src/mesh/parallel/fci.cxx | 89 +++++++++---------- src/mesh/parallel/fci.hxx | 7 +- 9 files changed, 98 insertions(+), 56 deletions(-) diff --git a/include/bout/region.hxx b/include/bout/region.hxx index f84c058814..4d0cb51159 100644 --- a/include/bout/region.hxx +++ b/include/bout/region.hxx @@ -51,6 +51,7 @@ #include "bout_types.hxx" #include "bout/assert.hxx" #include "bout/openmpwrap.hxx" +class BoutMask; /// The MAXREGIONBLOCKSIZE value can be tuned to try to optimise /// performance on specific hardware. It determines what the largest @@ -644,6 +645,28 @@ public: return *this; // To allow command chaining }; + /// Return a new region equivalent to *this but with indices contained + /// in mask Region removed + Region mask(const BoutMask& mask) { + // Get the current set of indices that we're going to mask and then + // use to create the result region. + auto currentIndices = getIndices(); + + // Lambda that returns true/false depending if the passed value is in maskIndices + // With C++14 T can be auto instead + auto isInVector = [&](T val) { return mask[val]; }; + + // Erase elements of currentIndices that are in maskIndices + currentIndices.erase( + std::remove_if(std::begin(currentIndices), std::end(currentIndices), isInVector), + std::end(currentIndices)); + + // Update indices + setIndices(currentIndices); + + return *this; // To allow command chaining + }; + /// Returns a new region including only indices contained in both /// this region and the other. Region getIntersection(const Region& otherRegion) { diff --git a/include/interpolation_xz.hxx b/include/interpolation_xz.hxx index 7a58ce3346..915b5a8478 100644 --- a/include/interpolation_xz.hxx +++ b/include/interpolation_xz.hxx @@ -49,8 +49,7 @@ protected: public: XZInterpolation(int y_offset = 0, Mesh* localmeshIn = nullptr) : y_offset(y_offset), - localmesh(localmeshIn == nullptr ? bout::globals::mesh : localmeshIn), - region_name("RGN_ALL") {} + localmesh(localmeshIn == nullptr ? bout::globals::mesh : localmeshIn) {} XZInterpolation(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZInterpolation(y_offset, mesh) { region = regionFromMask(mask, localmesh); @@ -74,6 +73,10 @@ public: this->region_name = ""; this->region = region; } + void setRegion(const Region& region) { + this->region_name = ""; + this->region = std::make_shared>(region); + } Region getRegion() const { if (region_name != "") { return localmesh->getRegion(region_name); @@ -82,9 +85,14 @@ public: return *region; } Region getRegion(const std::string& region) const { + const bool has_region = region_name != "" or this->region != nullptr; if (region != "" and region != "RGN_ALL") { - return getIntersection(localmesh->getRegion(region), getRegion()); + if (has_region) { + return getIntersection(localmesh->getRegion(region), getRegion()); + } + return localmesh->getRegion(region); } + ASSERT1(has_region); return getRegion(); } virtual void calcWeights(const Field3D& delta_x, const Field3D& delta_z, diff --git a/include/mask.hxx b/include/mask.hxx index c26bf31d61..6113e94d63 100644 --- a/include/mask.hxx +++ b/include/mask.hxx @@ -70,6 +70,7 @@ public: inline const bool& operator()(int jx, int jy, int jz) const { return mask(jx, jy, jz); } + inline const bool& operator[](const Ind3D& i) const { return mask[i]; } }; inline std::unique_ptr> regionFromMask(const BoutMask& mask, diff --git a/include/utils.hxx b/include/utils.hxx index 9de4628358..ea293f7bf8 100644 --- a/include/utils.hxx +++ b/include/utils.hxx @@ -38,6 +38,7 @@ #include "bout/array.hxx" #include "bout/assert.hxx" #include "bout/build_config.hxx" +#include "bout/region.hxx" #include #include @@ -348,6 +349,14 @@ public: return data[(i1*n2+i2)*n3 + i3]; } + const T& operator[](Ind3D i) const { + // ny and nz are private :-( + // ASSERT2(i.nz == n3); + // ASSERT2(i.ny == n2); + ASSERT2(0 <= i.ind && i.ind < n1 * n2 * n3); + return data[i.ind]; + } + Tensor& operator=(const T&val){ for(auto &i: data){ i = val; diff --git a/src/mesh/interpolation/bilinear_xz.cxx b/src/mesh/interpolation/bilinear_xz.cxx index 1869df3218..e36527e765 100644 --- a/src/mesh/interpolation/bilinear_xz.cxx +++ b/src/mesh/interpolation/bilinear_xz.cxx @@ -45,7 +45,8 @@ XZBilinear::XZBilinear(int y_offset, Mesh *mesh) void XZBilinear::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region) { - BOUT_FOR(i, getRegion(region)) { + const auto curregion{getRegion(region)}; + BOUT_FOR(i, curregion) { const int x = i.x(); const int y = i.y(); const int z = i.z(); @@ -92,7 +93,8 @@ Field3D XZBilinear::interpolate(const Field3D& f, const std::string& region) con ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; - BOUT_FOR(i, this->getRegion(region)) { + const auto curregion{getRegion(region)}; + BOUT_FOR(i, curregion) { const int x = i.x(); const int y = i.y(); const int z = i.z(); diff --git a/src/mesh/interpolation/lagrange_4pt_xz.cxx b/src/mesh/interpolation/lagrange_4pt_xz.cxx index 7c79a3f713..caf4ce45eb 100644 --- a/src/mesh/interpolation/lagrange_4pt_xz.cxx +++ b/src/mesh/interpolation/lagrange_4pt_xz.cxx @@ -39,7 +39,7 @@ XZLagrange4pt::XZLagrange4pt(int y_offset, Mesh *mesh) void XZLagrange4pt::calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region) { - const auto curregion = getRegion(region); + const auto curregion{getRegion(region)}; BOUT_FOR(i, curregion) { const int x = i.x(); const int y = i.y(); diff --git a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx index 47eeb2df20..fbffc0b1fd 100644 --- a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx +++ b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx @@ -58,7 +58,8 @@ Field3D XZMonotonicHermiteSpline::interpolate(const Field3D& f, localmesh->wait(h); } - BOUT_FOR(i, getRegion(region)) { + const auto curregion{getRegion(region)}; + BOUT_FOR(i, curregion) { const int x = i.x(); const int y = i.y(); const int z = i.z(); diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index c25765852e..77f34fd282 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -50,7 +50,8 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, int offset_, BoundaryRegionPar* inner_boundary, BoundaryRegionPar* outer_boundary, bool zperiodic) - : map_mesh(mesh), offset(offset_), boundary_mask(map_mesh), + : map_mesh(mesh), offset(offset_), + region_no_boundary(map_mesh.getRegion("RGN_NOBNDRY")), corner_boundary_mask(map_mesh) { TRACE("Creating FCIMAP for direction {:d}", offset); @@ -156,6 +157,7 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, const int ncz = map_mesh.LocalNz; + BoutMask to_remove(map_mesh); // Serial loop because call to BoundaryRegionPar::addPoint // (probably?) can't be done in parallel BOUT_FOR_SERIAL(i, xt_prime.getRegion("RGN_NOBNDRY")) { @@ -185,7 +187,7 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, // indices (forward/backward_xt_prime and forward/backward_zt_prime) // are set to -1 - boundary_mask(x, y, z) = true; + to_remove(x, y, z) = true; // Need to specify the index of the boundary intersection, but // this may not be defined in general. @@ -200,13 +202,11 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, // and the gradients dR/dx etc. are evaluated at (x,y,z) // Cache the offsets - const auto i_xp = i.xp(); - const auto i_xm = i.xm(); const auto i_zp = i.zp(); const auto i_zm = i.zm(); - const BoutReal dR_dx = 0.5 * (R[i_xp] - R[i_xm]); - const BoutReal dZ_dx = 0.5 * (Z[i_xp] - Z[i_xm]); + const BoutReal dR_dx = 0.5 * (R[i.xp()] - R[i.xm()]); + const BoutReal dZ_dx = 0.5 * (Z[i.xp()] - Z[i.xm()]); BoutReal dR_dz, dZ_dz; // Handle the edge cases in Z @@ -241,8 +241,17 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, PI // Right-angle intersection ); } - - interp->setMask(boundary_mask); + region_no_boundary = region_no_boundary.mask(to_remove); + + const auto region = fmt::format("RGN_YPAR_{:+d}", offset); + if (not map_mesh.hasRegion3D(region)) { + // The valid region for this slice + map_mesh.addRegion3D(region, + Region(map_mesh.xstart, map_mesh.xend, + map_mesh.ystart+offset, map_mesh.yend+offset, + 0, map_mesh.LocalNz-1, + map_mesh.LocalNy, map_mesh.LocalNz)); + } } Field3D FCIMap::integrate(Field3D &f) const { @@ -265,45 +274,33 @@ Field3D FCIMap::integrate(Field3D &f) const { int nz = map_mesh.LocalNz; - for(int x = map_mesh.xstart; x <= map_mesh.xend; x++) { - for(int y = map_mesh.ystart; y <= map_mesh.yend; y++) { - - int ynext = y+offset; - - for(int z = 0; z < nz; z++) { - if (boundary_mask(x,y,z)) - continue; - - int zm = z - 1; - if (z == 0) { - zm = nz-1; - } - - BoutReal f_c = centre(x,ynext,z); - - if (corner_boundary_mask(x, y, z) || corner_boundary_mask(x - 1, y, z) || - corner_boundary_mask(x, y, zm) || corner_boundary_mask(x - 1, y, zm) || - (x == map_mesh.xstart)) { - // One of the corners leaves the domain. - // Use the cell centre value, since boundary conditions are not - // currently applied to corners. - result(x, ynext, z) = f_c; - - } else { - BoutReal f_pp = corner(x, ynext, z); // (x+1/2, z+1/2) - BoutReal f_mp = corner(x - 1, ynext, z); // (x-1/2, z+1/2) - BoutReal f_pm = corner(x, ynext, zm); // (x+1/2, z-1/2) - BoutReal f_mm = corner(x - 1, ynext, zm); // (x-1/2, z-1/2) - - // This uses a simple weighted average of centre and corners - // A more sophisticated approach might be to use e.g. Gauss-Lobatto points - // which would include cell edges and corners - result(x, ynext, z) = 0.5 * (f_c + 0.25 * (f_pp + f_mp + f_pm + f_mm)); - - ASSERT2(std::isfinite(result(x,ynext,z))); - } - } + BOUT_FOR(i, region_no_boundary) { + const auto inext = i.yp(offset); + BoutReal f_c = centre[inext]; + const auto izm = i.zm(); + const int x = i.x(); + const int y = i.y(); + const int z = i.z(); + const int zm = izm.z(); + if (corner_boundary_mask(x, y, z) || corner_boundary_mask(x - 1, y, z) + || corner_boundary_mask(x, y, zm) || corner_boundary_mask(x - 1, y, zm) + || (x == map_mesh.xstart)) { + // One of the corners leaves the domain. + // Use the cell centre value, since boundary conditions are not + // currently applied to corners. + result[inext] = f_c; + } else { + BoutReal f_pp = corner[inext]; // (x+1/2, z+1/2) + BoutReal f_mp = corner[inext.xm()]; // (x-1/2, z+1/2) + BoutReal f_pm = corner[inext.zm()]; // (x+1/2, z-1/2) + BoutReal f_mm = corner[inext.xm().zm()]; // (x-1/2, z-1/2) + + // This uses a simple weighted average of centre and corners + // A more sophisticated approach might be to use e.g. Gauss-Lobatto points + // which would include cell edges and corners + result[inext] = 0.5 * (f_c + 0.25 * (f_pp + f_mp + f_pm + f_mm)); } + ASSERT2(finite(result[inext])); } return result; } diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index 3ecd964bfa..ef7c98693e 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -54,11 +54,12 @@ public: /// Direction of map const int offset; - /// boundary mask - has the field line left the domain - BoutMask boundary_mask; + /// region containing all points where the field line has not left the + /// domain + Region region_no_boundary; /// If any of the integration area has left the domain BoutMask corner_boundary_mask; - + Field3D interpolate(Field3D& f) const { ASSERT1(&map_mesh == f.getMesh()); return interp->interpolate(f); From d2a81bf142eea4e05fe5bb9c925290586a05fde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Schw=C3=B6rer?= Date: Fri, 19 Nov 2021 13:38:21 +0100 Subject: [PATCH 004/256] Use region_id in interpolation Otherwise the regions aren't cached. --- include/interpolation_xz.hxx | 61 +++++++++++++++++------------------- include/mask.hxx | 4 +-- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/include/interpolation_xz.hxx b/include/interpolation_xz.hxx index 915b5a8478..df58d5b70e 100644 --- a/include/interpolation_xz.hxx +++ b/include/interpolation_xz.hxx @@ -43,8 +43,7 @@ public: protected: Mesh* localmesh{nullptr}; - std::string region_name{""}; - std::shared_ptr> region{nullptr}; + int region_id{-1}; public: XZInterpolation(int y_offset = 0, Mesh* localmeshIn = nullptr) @@ -52,48 +51,44 @@ public: localmesh(localmeshIn == nullptr ? bout::globals::mesh : localmeshIn) {} XZInterpolation(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZInterpolation(y_offset, mesh) { - region = regionFromMask(mask, localmesh); + setMask(mask); } XZInterpolation(const std::string& region_name, int y_offset = 0, Mesh* mesh = nullptr) - : y_offset(y_offset), localmesh(mesh), region_name(region_name) {} - XZInterpolation(std::shared_ptr> region, int y_offset = 0, + : y_offset(y_offset), localmesh(mesh), region_id(localmesh->getRegionID(region_name)) {} + XZInterpolation(const Region& region, int y_offset = 0, Mesh* mesh = nullptr) - : y_offset(y_offset), localmesh(mesh), region(region) {} + : y_offset(y_offset), localmesh(mesh){ + setRegion(region); + } virtual ~XZInterpolation() = default; void setMask(const BoutMask& mask) { - region = regionFromMask(mask, localmesh); - region_name = ""; + setRegion(regionFromMask(mask, localmesh)); } void setRegion(const std::string& region_name) { - this->region_name = region_name; - this->region = nullptr; - } - void setRegion(const std::shared_ptr>& region) { - this->region_name = ""; - this->region = region; + this->region_id = localmesh->getRegionID(region_name); } void setRegion(const Region& region) { - this->region_name = ""; - this->region = std::make_shared>(region); + std::string name; + int i=0; + do { + name = fmt::format("unsec_reg_xz_interp_{:d}",i++); + } while (localmesh->hasRegion3D(name)); + localmesh->addRegion(name, region); + this->region_id = localmesh->getRegionID(name); } - Region getRegion() const { - if (region_name != "") { - return localmesh->getRegion(region_name); - } - ASSERT1(region != nullptr); - return *region; + const Region& getRegion() const { + ASSERT2(region_id != -1); + return localmesh->getRegion(region_id); } - Region getRegion(const std::string& region) const { - const bool has_region = region_name != "" or this->region != nullptr; - if (region != "" and region != "RGN_ALL") { - if (has_region) { - return getIntersection(localmesh->getRegion(region), getRegion()); - } + const Region& getRegion(const std::string& region) const { + if (region_id == -1) { return localmesh->getRegion(region); } - ASSERT1(has_region); - return getRegion(); + if (region == "" or region == "RGN_ALL"){ + return getRegion(); + } + return localmesh->getRegion(localmesh->getCommonRegion(localmesh->getRegionID(region), region_id)); } virtual void calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region = "RGN_NOBNDRY") = 0; @@ -158,7 +153,7 @@ public: XZHermiteSpline(int y_offset = 0, Mesh *mesh = nullptr); XZHermiteSpline(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZHermiteSpline(y_offset, mesh) { - region = regionFromMask(mask, localmesh); + setRegion(regionFromMask(mask, localmesh)); } void calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -216,7 +211,7 @@ public: XZLagrange4pt(int y_offset = 0, Mesh *mesh = nullptr); XZLagrange4pt(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZLagrange4pt(y_offset, mesh) { - region = regionFromMask(mask, localmesh); + setRegion(regionFromMask(mask, localmesh)); } void calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -249,7 +244,7 @@ public: XZBilinear(int y_offset = 0, Mesh *mesh = nullptr); XZBilinear(const BoutMask &mask, int y_offset = 0, Mesh *mesh = nullptr) : XZBilinear(y_offset, mesh) { - region = regionFromMask(mask, localmesh); + setRegion(regionFromMask(mask, localmesh)); } void calcWeights(const Field3D& delta_x, const Field3D& delta_z, diff --git a/include/mask.hxx b/include/mask.hxx index 6113e94d63..96d2c99ac3 100644 --- a/include/mask.hxx +++ b/include/mask.hxx @@ -73,7 +73,7 @@ public: inline const bool& operator[](const Ind3D& i) const { return mask[i]; } }; -inline std::unique_ptr> regionFromMask(const BoutMask& mask, +inline Region regionFromMask(const BoutMask& mask, const Mesh* mesh) { std::vector indices; for (auto i : mesh->getRegion("RGN_ALL")) { @@ -81,6 +81,6 @@ inline std::unique_ptr> regionFromMask(const BoutMask& mask, indices.push_back(i); } } - return std::make_unique>(indices); + return Region{indices}; } #endif //__MASK_H__ From ea5640f1b661ab2978deff9084d56f3184b513ad Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 16 Dec 2021 12:26:52 +0100 Subject: [PATCH 005/256] Switch hermite_spline_xz to index Might help with performance --- src/mesh/interpolation/hermite_spline_xz.cxx | 2 ++ src/mesh/interpolation/monotonic_hermite_spline_xz.cxx | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index a682c58839..1319f7532d 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -177,6 +177,8 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region } BOUT_FOR(i, getRegion(region)) { + const auto iyp = i.yp(y_offset); + const auto ic = i_corner[i]; const auto iczp = ic.zp(); const auto icxp = ic.xp(); diff --git a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx index fbffc0b1fd..e0cdf91ac8 100644 --- a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx +++ b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx @@ -60,10 +60,6 @@ Field3D XZMonotonicHermiteSpline::interpolate(const Field3D& f, const auto curregion{getRegion(region)}; BOUT_FOR(i, curregion) { - const int x = i.x(); - const int y = i.y(); - const int z = i.z(); - const auto iyp = i.yp(y_offset); const auto ic = i_corner[i]; From 089ddfa8413948f1fa8b1c759c64b05ce77e2468 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 16 Nov 2022 10:33:57 +0100 Subject: [PATCH 006/256] Improve hermitesplinesXZ Precalculate the matrix. This hard-codes the DDX and DDZ derivative to C2. Rather than taking derivatives, this first does the matrix-matrix multiplication and then does only a single matrix-vector operation, rather than 4. Retains a switch to go back to the old version. --- include/interpolation_xz.hxx | 2 + src/mesh/interpolation/hermite_spline_xz.cxx | 161 ++++++++++++++++--- 2 files changed, 142 insertions(+), 21 deletions(-) diff --git a/include/interpolation_xz.hxx b/include/interpolation_xz.hxx index df58d5b70e..e6eb8c3078 100644 --- a/include/interpolation_xz.hxx +++ b/include/interpolation_xz.hxx @@ -147,6 +147,8 @@ protected: Field3D h10_z; Field3D h11_z; + std::vector newWeights; + public: XZHermiteSpline(Mesh *mesh = nullptr) : XZHermiteSpline(0, mesh) {} diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 1319f7532d..08515c7056 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -49,6 +49,13 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) h01_z.allocate(); h10_z.allocate(); h11_z.allocate(); + + + newWeights.reserve(16); + for (int w=0; w<16;++w){ + newWeights.emplace_back(localmesh); + newWeights[w].allocate(); + } } void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -108,6 +115,115 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z h11_x[i] = (t_x * t_x * t_x) - (t_x * t_x); h11_z[i] = (t_z * t_z * t_z) - (t_z * t_z); + +#define USE_NEW_WEIGHTS 1 +#if USE_NEW_WEIGHTS + + for (int w =0; w<16;++w){ + newWeights[w][i]=0; + } + // The distribution of our weights: + // 0 4 8 12 + // 1 5 9 13 + // 2 6 10 14 + // 3 7 11 15 + // e.g. 1 == ic.xm(); 4 == ic.zm(); 5 == ic; 7 == ic.zp(2); + + // f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; + newWeights[5][i] += h00_x[i] * h00_z[i]; + newWeights[9][i] += h01_x[i] * h00_z[i]; + newWeights[9][i] += h10_x[i] * h00_z[i] / 2; + newWeights[1][i] -= h10_x[i] * h00_z[i] / 2; + newWeights[13][i] += h11_x[i] * h00_z[i] / 2; + newWeights[5][i] -= h11_x[i] * h00_z[i] / 2; + + // f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + + // fx[iczp] * h10_x[i] + fx[icxpzp] * h11_x[i]; + newWeights[6][i] += h00_x[i] * h01_z[i]; + newWeights[10][i] += h01_x[i] * h01_z[i]; + newWeights[10][i] += h10_x[i] * h01_z[i] / 2; + newWeights[2][i] -= h10_x[i] * h01_z[i] / 2; + newWeights[14][i] += h11_x[i] * h01_z[i] / 2; + newWeights[6][i] -= h11_x[i] * h01_z[i] / 2; + + // fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + + // fxz[ic] * h10_x[i]+ fxz[icxp] * h11_x[i]; + newWeights[6][i] += h00_x[i] * h10_z[i] / 2; + newWeights[4][i] -= h00_x[i] * h10_z[i] / 2; + newWeights[10][i] += h01_x[i] * h10_z[i] / 2; + newWeights[8][i] -= h01_x[i] * h10_z[i] / 2; + newWeights[10][i] += h10_x[i] * h10_z[i] / 4; + newWeights[8][i] -= h10_x[i] * h10_z[i] / 4; + newWeights[2][i] -= h10_x[i] * h10_z[i] / 4; + newWeights[0][i] += h10_x[i] * h10_z[i] / 4; + newWeights[14][i] += h11_x[i] * h10_z[i] / 4; + newWeights[12][i] -= h11_x[i] * h10_z[i] / 4; + newWeights[6][i] -= h11_x[i] * h10_z[i] / 4; + newWeights[4][i] += h11_x[i] * h10_z[i] / 4; + + // fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + + // fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; + newWeights[7][i] += h00_x[i] * h11_z[i] / 2; + newWeights[5][i] -= h00_x[i] * h11_z[i] / 2; + newWeights[11][i] += h01_x[i] * h11_z[i] / 2; + newWeights[9][i] -= h01_x[i] * h11_z[i] / 2; + newWeights[11][i] += h10_x[i] * h11_z[i] / 4; + newWeights[9][i] -= h10_x[i] * h11_z[i] / 4; + newWeights[3][i] -= h10_x[i] * h11_z[i] / 4; + newWeights[1][i] += h10_x[i] * h11_z[i] / 4; + newWeights[15][i] += h11_x[i] * h11_z[i] / 4; + newWeights[13][i] -= h11_x[i] * h11_z[i] / 4; + newWeights[7][i] -= h11_x[i] * h11_z[i] / 4; + newWeights[5][i] += h11_x[i] * h11_z[i] / 4; + + + // // f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; + // newWeights[5][i] += h00_x[i] * h00_z[i]; + // newWeights[9][i] += h01_x[i] * h00_z[i]; + // newWeights[9][i] += h10_x[i] * h00_z[i] / 2 / localmesh->dx[ic]; + // newWeights[1][i] -= h10_x[i] * h00_z[i] / 2 / localmesh->dx[ic]; + // newWeights[13][i] += h11_x[i] * h00_z[i] / 2 / localmesh->dx[ic.xp()]; + // newWeights[5][i] -= h11_x[i] * h00_z[i] / 2 / localmesh->dx[ic.xp()]; + + // // f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + + // // fx[iczp] * h10_x[i] + fx[icxpzp] * h11_x[i]; + // newWeights[6][i] += h00_x[i] * h01_z[i]; + // newWeights[10][i] += h01_x[i] * h01_z[i]; + // newWeights[10][i] += h10_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp()]; + // newWeights[2][i] -= h10_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp()]; + // newWeights[14][i] += h11_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp().xp()]; + // newWeights[6][i] -= h11_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp().xp()]; + + // // fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + + // // fxz[ic] * h10_x[i]+ fxz[icxp] * h11_x[i]; + // newWeights[6][i] += h00_x[i] * h10_z[i] / 2 / localmesh->dz[ic]; + // newWeights[4][i] -= h00_x[i] * h10_z[i] / 2 / localmesh->dz[ic]; + // newWeights[10][i] += h01_x[i] * h10_z[i] / 2 / localmesh->dz[ic.xp()]; + // newWeights[8][i] -= h01_x[i] * h10_z[i] / 2 / localmesh->dz[ic.xp()]; + // newWeights[10][i] += h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; + // newWeights[8][i] -= h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; + // newWeights[2][i] -= h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; + // newWeights[0][i] += h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; + // newWeights[14][i] += h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; + // newWeights[12][i] -= h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; + // newWeights[6][i] -= h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; + // newWeights[4][i] += h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; + + // // fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + + // // fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; + // newWeights[7][i] += h00_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp()]; + // newWeights[5][i] -= h00_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp()]; + // newWeights[11][i] += h01_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp().xp()]; + // newWeights[9][i] -= h01_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp().xp()]; + // newWeights[11][i] += h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; + // newWeights[9][i] -= h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; + // newWeights[3][i] -= h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; + // newWeights[1][i] += h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; + // newWeights[15][i] += h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; + // newWeights[13][i] -= h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; + // newWeights[7][i] -= h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; + // newWeights[5][i] += h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; +#endif } } @@ -152,29 +268,31 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; + +#if USE_NEW_WEIGHTS + BOUT_FOR(i, getRegion(region)) { + auto ic = i_corner[i]; + auto iyp = i.yp(y_offset); + + f_interp[iyp]=0; + for (int w = 0; w < 4; ++w){ + f_interp[iyp] += newWeights[w*4+0][i] * f[ic.zm().xp(w-1)]; + f_interp[iyp] += newWeights[w*4+1][i] * f[ic.xp(w-1)]; + f_interp[iyp] += newWeights[w*4+2][i] * f[ic.zp().xp(w-1)]; + f_interp[iyp] += newWeights[w*4+3][i] * f[ic.zp(2).xp(w-1)]; + } + } + return f_interp; +#else // Derivatives are used for tension and need to be on dimensionless // coordinates - Field3D fx = bout::derivatives::index::DDX(f, CELL_DEFAULT, "DEFAULT"); - localmesh->communicateXZ(fx); - // communicate in y, but do not calculate parallel slices - { - auto h = localmesh->sendY(fx); - localmesh->wait(h); - } - Field3D fz = bout::derivatives::index::DDZ(f, CELL_DEFAULT, "DEFAULT", "RGN_ALL"); - localmesh->communicateXZ(fz); - // communicate in y, but do not calculate parallel slices - { - auto h = localmesh->sendY(fz); - localmesh->wait(h); - } - Field3D fxz = bout::derivatives::index::DDX(fz, CELL_DEFAULT, "DEFAULT"); - localmesh->communicateXZ(fxz); - // communicate in y, but do not calculate parallel slices - { - auto h = localmesh->sendY(fxz); - localmesh->wait(h); - } + const auto region2 = fmt::format("RGN_YPAR_{:+d}", y_offset); + // f has been communcated, and thus we can assume that the x-boundaries are + // also valid in the y-boundary. Thus the differentiated field needs no + // extra comms. + Field3D fx = bout::derivatives::index::DDX(f, CELL_DEFAULT, "DEFAULT", region2); + Field3D fz = bout::derivatives::index::DDZ(f, CELL_DEFAULT, "DEFAULT", region2); + Field3D fxz = bout::derivatives::index::DDZ(fx, CELL_DEFAULT, "DEFAULT", region2); BOUT_FOR(i, getRegion(region)) { const auto iyp = i.yp(y_offset); @@ -208,6 +326,7 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region || i.x() > localmesh->xend); } return f_interp; +# endif } Field3D XZHermiteSpline::interpolate(const Field3D& f, const Field3D& delta_x, From de7d1deab6a419bf974fb8df5074fba4514175e3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 17 Nov 2022 10:36:29 +0100 Subject: [PATCH 007/256] Cleanup --- src/mesh/interpolation/hermite_spline_xz.cxx | 50 +------------------- 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 08515c7056..22da552125 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -158,7 +158,7 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z newWeights[0][i] += h10_x[i] * h10_z[i] / 4; newWeights[14][i] += h11_x[i] * h10_z[i] / 4; newWeights[12][i] -= h11_x[i] * h10_z[i] / 4; - newWeights[6][i] -= h11_x[i] * h10_z[i] / 4; + newWeights[6][i] -= h11_x[i] * h10_z[i] / 4; newWeights[4][i] += h11_x[i] * h10_z[i] / 4; // fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + @@ -175,54 +175,6 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z newWeights[13][i] -= h11_x[i] * h11_z[i] / 4; newWeights[7][i] -= h11_x[i] * h11_z[i] / 4; newWeights[5][i] += h11_x[i] * h11_z[i] / 4; - - - // // f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; - // newWeights[5][i] += h00_x[i] * h00_z[i]; - // newWeights[9][i] += h01_x[i] * h00_z[i]; - // newWeights[9][i] += h10_x[i] * h00_z[i] / 2 / localmesh->dx[ic]; - // newWeights[1][i] -= h10_x[i] * h00_z[i] / 2 / localmesh->dx[ic]; - // newWeights[13][i] += h11_x[i] * h00_z[i] / 2 / localmesh->dx[ic.xp()]; - // newWeights[5][i] -= h11_x[i] * h00_z[i] / 2 / localmesh->dx[ic.xp()]; - - // // f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + - // // fx[iczp] * h10_x[i] + fx[icxpzp] * h11_x[i]; - // newWeights[6][i] += h00_x[i] * h01_z[i]; - // newWeights[10][i] += h01_x[i] * h01_z[i]; - // newWeights[10][i] += h10_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp()]; - // newWeights[2][i] -= h10_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp()]; - // newWeights[14][i] += h11_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp().xp()]; - // newWeights[6][i] -= h11_x[i] * h01_z[i] / 2/ localmesh->dx[ic.zp().xp()]; - - // // fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + - // // fxz[ic] * h10_x[i]+ fxz[icxp] * h11_x[i]; - // newWeights[6][i] += h00_x[i] * h10_z[i] / 2 / localmesh->dz[ic]; - // newWeights[4][i] -= h00_x[i] * h10_z[i] / 2 / localmesh->dz[ic]; - // newWeights[10][i] += h01_x[i] * h10_z[i] / 2 / localmesh->dz[ic.xp()]; - // newWeights[8][i] -= h01_x[i] * h10_z[i] / 2 / localmesh->dz[ic.xp()]; - // newWeights[10][i] += h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; - // newWeights[8][i] -= h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; - // newWeights[2][i] -= h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; - // newWeights[0][i] += h10_x[i] * h10_z[i] / 4 / localmesh->dz[ic] / localmesh->dx[ic]; - // newWeights[14][i] += h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; - // newWeights[12][i] -= h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; - // newWeights[6][i] -= h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; - // newWeights[4][i] += h11_x[i] * h10_z[i] / 4 / localmesh->dz[ic.xp()] / localmesh->dx[ic.xp()]; - - // // fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + - // // fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; - // newWeights[7][i] += h00_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp()]; - // newWeights[5][i] -= h00_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp()]; - // newWeights[11][i] += h01_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp().xp()]; - // newWeights[9][i] -= h01_x[i] * h11_z[i] / 2 / localmesh->dz[ic.zp().xp()]; - // newWeights[11][i] += h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; - // newWeights[9][i] -= h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; - // newWeights[3][i] -= h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; - // newWeights[1][i] += h10_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp()] / localmesh->dx[ic.zp()]; - // newWeights[15][i] += h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; - // newWeights[13][i] -= h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; - // newWeights[7][i] -= h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; - // newWeights[5][i] += h11_x[i] * h11_z[i] / 4 / localmesh->dz[ic.zp().xp()] / localmesh->dx[ic.zp().xp()]; #endif } } From 8a1c2306b9496a09d768fe4fe990352f5fb34936 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 10:55:01 +0100 Subject: [PATCH 008/256] Enable splitting in X using PETSc --- include/interpolation_xz.hxx | 27 ++++ src/mesh/interpolation/hermite_spline_xz.cxx | 140 ++++++++++++++++++- 2 files changed, 161 insertions(+), 6 deletions(-) diff --git a/include/interpolation_xz.hxx b/include/interpolation_xz.hxx index e6eb8c3078..620d146edf 100644 --- a/include/interpolation_xz.hxx +++ b/include/interpolation_xz.hxx @@ -26,6 +26,15 @@ #include "mask.hxx" +#define USE_NEW_WEIGHTS 1 +#if BOUT_HAS_PETSC +#define HS_USE_PETSC 1 +#endif + +#ifdef HS_USE_PETSC +#include "bout/petsclib.hxx" +#endif + class Options; /// Interpolate a field onto a perturbed set of points @@ -149,6 +158,13 @@ protected: std::vector newWeights; +#if HS_USE_PETSC + PetscLib* petsclib; + bool isInit{false}; + Mat petscWeights; + Vec rhs, result; +#endif + public: XZHermiteSpline(Mesh *mesh = nullptr) : XZHermiteSpline(0, mesh) {} @@ -157,6 +173,17 @@ public: : XZHermiteSpline(y_offset, mesh) { setRegion(regionFromMask(mask, localmesh)); } + ~XZHermiteSpline() { +#if HS_USE_PETSC + if (isInit) { + MatDestroy(&petscWeights); + VecDestroy(&rhs); + VecDestroy(&result); + isInit = false; + delete petsclib; + } +#endif + } void calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region = "RGN_NOBNDRY") override; diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 22da552125..1b63c84231 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -23,10 +23,84 @@ #include "globals.hxx" #include "interpolation_xz.hxx" #include "bout/index_derivs_interface.hxx" -#include "bout/mesh.hxx" +#include "../impls/bout/boutmesh.hxx" #include +class IndConverter { +public: + IndConverter(Mesh* mesh) + : mesh(dynamic_cast(mesh)), nxpe(mesh->getNXPE()), nype(mesh->getNYPE()), + xstart(mesh->xstart), ystart(mesh->ystart), zstart(0), + lnx(mesh->LocalNx - 2 * xstart), lny(mesh->LocalNy - 2 * ystart), + lnz(mesh->LocalNz - 2 * zstart) {} + // ix and iy are global indices + // iy is local + int fromMeshToGlobal(int ix, int iy, int iz) { + const int xstart = mesh->xstart; + const int lnx = mesh->LocalNx - xstart * 2; + // x-proc-id + int pex = divToNeg(ix - xstart, lnx); + if (pex < 0) { + pex = 0; + } + if (pex >= nxpe) { + pex = nxpe - 1; + } + const int zstart = 0; + const int lnz = mesh->LocalNz - zstart * 2; + // z-proc-id + // pez only for wrapping around ; later needs similar treatment than pey + const int pez = divToNeg(iz - zstart, lnz); + // y proc-id - y is already local + const int ystart = mesh->ystart; + const int lny = mesh->LocalNy - ystart * 2; + const int pey_offset = divToNeg(iy - ystart, lny); + int pey = pey_offset + mesh->getYProcIndex(); + while (pey < 0) { + pey += nype; + } + while (pey >= nype) { + pey -= nype; + } + ASSERT2(pex >= 0); + ASSERT2(pex < nxpe); + ASSERT2(pey >= 0); + ASSERT2(pey < nype); + return fromLocalToGlobal(ix - pex * lnx, iy - pey_offset * lny, iz - pez * lnz, pex, + pey, 0); + } + int fromLocalToGlobal(const int ilocalx, const int ilocaly, const int ilocalz) { + return fromLocalToGlobal(ilocalx, ilocaly, ilocalz, mesh->getXProcIndex(), + mesh->getYProcIndex(), 0); + } + int fromLocalToGlobal(const int ilocalx, const int ilocaly, const int ilocalz, + const int pex, const int pey, const int pez) { + ASSERT3(ilocalx >= 0); + ASSERT3(ilocaly >= 0); + ASSERT3(ilocalz >= 0); + const int ilocal = ((ilocalx * mesh->LocalNy) + ilocaly) * mesh->LocalNz + ilocalz; + const int ret = ilocal + + mesh->LocalNx * mesh->LocalNy * mesh->LocalNz + * ((pey * nxpe + pex) * nzpe + pez); + ASSERT3(ret >= 0); + ASSERT3(ret < nxpe * nype * mesh->LocalNx * mesh->LocalNy * mesh->LocalNz); + return ret; + } + +private: + // number of procs + BoutMesh* mesh; + const int nxpe; + const int nype; + const int nzpe{1}; + const int xstart, ystart, zstart; + const int lnx, lny, lnz; + static int divToNeg(const int n, const int d) { + return (n < 0) ? ((n - d + 1) / d) : (n / d); + } +}; + XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) : XZInterpolation(y_offset, mesh), h00_x(localmesh), h01_x(localmesh), h10_x(localmesh), h11_x(localmesh), @@ -50,12 +124,27 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) h10_z.allocate(); h11_z.allocate(); - +#if USE_NEW_WEIGHTS newWeights.reserve(16); for (int w=0; w<16;++w){ newWeights.emplace_back(localmesh); newWeights[w].allocate(); } +#ifdef HS_USE_PETSC + petsclib = new PetscLib( + &Options::root()["mesh:paralleltransform:xzinterpolation:hermitespline"]); + // MatCreate(MPI_Comm comm,Mat *A) + // MatCreate(MPI_COMM_WORLD, &petscWeights); + // MatSetSizes(petscWeights, m, m, M, M); + // PetscErrorCode MatCreateAIJ(MPI_Comm comm, PetscInt m, PetscInt n, PetscInt M, + // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], PetscInt o_nz, const PetscInt + //o_nnz[], Mat *A) + // MatSetSizes(Mat A,PetscInt m,PetscInt n,PetscInt M,PetscInt N) + const int m = mesh->LocalNx * mesh->LocalNy * mesh->LocalNz; + const int M = m * mesh->getNXPE() * mesh->getNYPE(); + MatCreateAIJ(MPI_COMM_WORLD, m, m, M, M, 16, nullptr, 16, nullptr, &petscWeights); +#endif +#endif } void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -63,6 +152,11 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z const int ny = localmesh->LocalNy; const int nz = localmesh->LocalNz; + const int xend = (localmesh->xend - localmesh->xstart + 1) * localmesh->getNXPE() + + localmesh->xstart - 1; +#ifdef HS_USE_PETSC + IndConverter conv{localmesh}; +#endif BOUT_FOR(i, getRegion(region)) { const int x = i.x(); const int y = i.y(); @@ -79,8 +173,8 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z BoutReal t_z = delta_z(x, y, z) - static_cast(k_corner(x, y, z)); // NOTE: A (small) hack to avoid one-sided differences - if (i_corn >= localmesh->xend) { - i_corn = localmesh->xend - 1; + if (i_corn >= xend) { + i_corn = xend - 1; t_x = 1.0; } if (i_corn < localmesh->xstart) { @@ -116,7 +210,6 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z h11_x[i] = (t_x * t_x * t_x) - (t_x * t_x); h11_z[i] = (t_z * t_z * t_z) - (t_z * t_z); -#define USE_NEW_WEIGHTS 1 #if USE_NEW_WEIGHTS for (int w =0; w<16;++w){ @@ -175,8 +268,28 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z newWeights[13][i] -= h11_x[i] * h11_z[i] / 4; newWeights[7][i] -= h11_x[i] * h11_z[i] / 4; newWeights[5][i] += h11_x[i] * h11_z[i] / 4; +#ifdef HS_USE_PETSC + PetscInt idxn[1] = {/* ; idxn[0] = */ conv.fromLocalToGlobal(x, y + y_offset, z)}; + // ixstep = mesh->LocalNx * mesh->LocalNz; + for (int j = 0; j < 4; ++j) { + PetscInt idxm[4]; + PetscScalar vals[4]; + for (int k = 0; k < 4; ++k) { + idxm[k] = conv.fromMeshToGlobal(i_corn - 1 + j, y + y_offset, + k_corner(x, y, z) - 1 + k); + vals[k] = newWeights[j * 4 + k][i]; + } + MatSetValues(petscWeights, 1, idxn, 4, idxm, vals, INSERT_VALUES); + } +#endif #endif } +#ifdef HS_USE_PETSC + isInit = true; + MatAssemblyBegin(petscWeights, MAT_FINAL_ASSEMBLY); + MatAssemblyEnd(petscWeights, MAT_FINAL_ASSEMBLY); + MatCreateVecs(petscWeights, &rhs, &result); +#endif } void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, @@ -220,8 +333,22 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; - #if USE_NEW_WEIGHTS +#ifdef HS_USE_PETSC + BoutReal* ptr; + const BoutReal* cptr; + VecGetArray(rhs, &ptr); + BOUT_FOR(i, f.getRegion("RGN_NOY")) { ptr[int(i)] = f[i]; } + VecRestoreArray(rhs, &ptr); + MatMult(petscWeights, rhs, result); + VecGetArrayRead(result, &cptr); + const auto region2 = fmt::format("RGN_YPAR_{:+d}", y_offset); + BOUT_FOR(i, f.getRegion(region2)) { + f_interp[i] = cptr[int(i)]; + ASSERT2(std::isfinite(cptr[int(i)])); + } + VecRestoreArrayRead(result, &cptr); +#else BOUT_FOR(i, getRegion(region)) { auto ic = i_corner[i]; auto iyp = i.yp(y_offset); @@ -234,6 +361,7 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region f_interp[iyp] += newWeights[w*4+3][i] * f[ic.zp(2).xp(w-1)]; } } +#endif return f_interp; #else // Derivatives are used for tension and need to be on dimensionless From 8234ccf92a8725f05453b22e874ec989c4e52121 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 10:56:08 +0100 Subject: [PATCH 009/256] Test parallised interpolation if PETSc is found --- tests/MMS/spatial/fci/data/BOUT.inp | 2 +- tests/MMS/spatial/fci/runtest | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/MMS/spatial/fci/data/BOUT.inp b/tests/MMS/spatial/fci/data/BOUT.inp index 5f377bbb56..b845e22012 100644 --- a/tests/MMS/spatial/fci/data/BOUT.inp +++ b/tests/MMS/spatial/fci/data/BOUT.inp @@ -20,4 +20,4 @@ y_periodic = true z_periodic = true [mesh:paralleltransform:xzinterpolation] -type = lagrange4pt +type = hermitespline diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index 1613155ed2..afff928087 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -19,13 +19,13 @@ from sys import stdout import zoidberg as zb -nx = 3 # Not changed for these tests +nx = 4 # Not changed for these tests # Resolution in y and z nlist = [8, 16, 32, 64, 128] # Number of parallel slices (in each direction) -nslices = [1, 2] +nslices = [1] # , 2] directory = "data" @@ -59,7 +59,7 @@ for nslice in nslices: # Note that the Bz and Bzprime parameters here must be the same as in mms.py field = zb.field.Slab(Bz=0.05, Bzprime=0.1) # Create rectangular poloidal grids - poloidal_grid = zb.poloidal_grid.RectangularPoloidalGrid(nx, n, 0.1, 1.0) + poloidal_grid = zb.poloidal_grid.RectangularPoloidalGrid(nx, n, 0.1, 1.0, MXG=1) # Set the ylength and y locations ylength = 10.0 @@ -72,12 +72,12 @@ for nslice in nslices: # Create the grid grid = zb.grid.Grid(poloidal_grid, ycoords, ylength, yperiodic=yperiodic) # Make and write maps - maps = zb.make_maps(grid, field, nslice=nslice, quiet=True) + maps = zb.make_maps(grid, field, nslice=nslice, quiet=True, MXG=1) zb.write_maps( grid, field, maps, new_names=False, metric2d=conf.isMetric2D(), quiet=True ) - args = " MZ={} MYG={} mesh:paralleltransform:y_periodic={} mesh:ddy:first={}".format( + args = " MZ={} MYG={} mesh:paralleltransform:y_periodic={} mesh:ddy:first={} NXPE=2".format( n, nslice, yperiodic, method_orders[nslice]["name"] ) From 9525e2cb7ccdbfe4d888e9f0e0f019e29936d1de Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 12:43:13 +0100 Subject: [PATCH 010/256] Fall back to region if not shifted --- src/mesh/interpolation/hermite_spline_xz.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 1b63c84231..37dc48662f 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -342,7 +342,7 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region VecRestoreArray(rhs, &ptr); MatMult(petscWeights, rhs, result); VecGetArrayRead(result, &cptr); - const auto region2 = fmt::format("RGN_YPAR_{:+d}", y_offset); + const auto region2 = y_offset == 0 ? region : fmt::format("RGN_YPAR_{:+d}", y_offset); BOUT_FOR(i, f.getRegion(region2)) { f_interp[i] = cptr[int(i)]; ASSERT2(std::isfinite(cptr[int(i)])); From b62082c8cf680deb391fbeaab2184a56d178177b Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 14:05:56 +0100 Subject: [PATCH 011/256] Split in X only if we have PETSc --- tests/MMS/spatial/fci/runtest | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index afff928087..7a54b87ccf 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -77,8 +77,12 @@ for nslice in nslices: grid, field, maps, new_names=False, metric2d=conf.isMetric2D(), quiet=True ) - args = " MZ={} MYG={} mesh:paralleltransform:y_periodic={} mesh:ddy:first={} NXPE=2".format( - n, nslice, yperiodic, method_orders[nslice]["name"] + args = " MZ={} MYG={} mesh:paralleltransform:y_periodic={} mesh:ddy:first={} NXPE={}".format( + n, + nslice, + yperiodic, + method_orders[nslice]["name"], + 2 if conf.has["petsc"] else 1, ) # Command to run From e147bc5fa3151726a1b9281993c40ba19b6cfbdc Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 18:32:55 +0100 Subject: [PATCH 012/256] Add test-interpolate for splitting in X --- .../integrated/test-interpolate/data/BOUT.inp | 1 - tests/integrated/test-interpolate/runtest | 22 ++++++++++++++----- .../test-interpolate/test_interpolate.cxx | 3 ++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/integrated/test-interpolate/data/BOUT.inp b/tests/integrated/test-interpolate/data/BOUT.inp index 101c63f3c7..804e780bbe 100644 --- a/tests/integrated/test-interpolate/data/BOUT.inp +++ b/tests/integrated/test-interpolate/data/BOUT.inp @@ -4,7 +4,6 @@ # MZ = 4 # Z size -NXPE = 1 ZMAX = 1 MXG = 2 diff --git a/tests/integrated/test-interpolate/runtest b/tests/integrated/test-interpolate/runtest index 08975cfd33..3e8f2c5bc6 100755 --- a/tests/integrated/test-interpolate/runtest +++ b/tests/integrated/test-interpolate/runtest @@ -6,6 +6,7 @@ from boututils.run_wrapper import build_and_log, shell, launch_safe from boutdata import collect +import boutconfig from numpy import sqrt, max, abs, mean, array, log, polyfit from sys import stdout, exit @@ -16,7 +17,7 @@ show_plot = False nxlist = [16, 32, 64, 128] # Only testing 2D (x, z) slices, so only need one processor -nproc = 1 +nproc = 2 # Variables to compare varlist = ["a", "b", "c"] @@ -48,11 +49,9 @@ for method in methods: for nx in nxlist: dx = 1.0 / (nx) - args = ( - " mesh:nx={nx4} mesh:dx={dx} MZ={nx} xzinterpolation:type={method}".format( - nx4=nx + 4, dx=dx, nx=nx, method=method - ) - ) + args = f" mesh:nx={nx + 4} mesh:dx={dx} MZ={nx} xzinterpolation:type={method}" + NXPE = 2 if method == "hermitespline" and boutconfig.has["petsc"] else 1 + args += f" NXPE={NXPE}" cmd = "./test_interpolate" + args @@ -71,6 +70,17 @@ for method in methods: E = interp - solution + if False: + import matplotlib.pyplot as plt + + def myplot(f, lbl=None): + plt.plot(f[:, 0, 6], label=lbl) + + myplot(interp, "interp") + myplot(solution, "sol") + plt.legend() + plt.show() + l2 = float(sqrt(mean(E**2))) linf = float(max(abs(E))) diff --git a/tests/integrated/test-interpolate/test_interpolate.cxx b/tests/integrated/test-interpolate/test_interpolate.cxx index 958409bbc1..8f19cd2a28 100644 --- a/tests/integrated/test-interpolate/test_interpolate.cxx +++ b/tests/integrated/test-interpolate/test_interpolate.cxx @@ -72,7 +72,7 @@ int main(int argc, char **argv) { BoutReal dz = index.z() + dice(); // For the last point, put the displacement inwards // Otherwise we try to interpolate in the guard cells, which doesn't work so well - if (index.x() >= mesh->xend) { + if (index.x() >= mesh->xend && mesh->getNXPE() - 1 == mesh->getXProcIndex()) { dx = index.x() - dice(); } deltax[index] = dx; @@ -87,6 +87,7 @@ int main(int argc, char **argv) { c_solution[index] = c_gen->generate(pos); } + deltax += (mesh->LocalNx - mesh->xstart * 2) * mesh->getXProcIndex(); // Create the interpolation object from the input options auto interp = XZInterpolationFactory::getInstance().create(); From 62d5bbec211127ffa6aeb6c06a8abaeee237b85d Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 18:41:27 +0100 Subject: [PATCH 013/256] Cleanup --- src/mesh/interpolation/hermite_spline_xz.cxx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 37dc48662f..e41dfa4d03 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -269,7 +269,10 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z newWeights[7][i] -= h11_x[i] * h11_z[i] / 4; newWeights[5][i] += h11_x[i] * h11_z[i] / 4; #ifdef HS_USE_PETSC - PetscInt idxn[1] = {/* ; idxn[0] = */ conv.fromLocalToGlobal(x, y + y_offset, z)}; + PetscInt idxn[1] = {conv.fromLocalToGlobal(x, y + y_offset, z)}; + // output.write("debug: {:d} -> {:d}: {:d}:{:d} -> {:d}:{:d}\n", conv.fromLocalToGlobal(x, y + y_offset, z), + // conv.fromMeshToGlobal(i_corn, y + y_offset, k_corner(x, y, z)), + // x, z, i_corn, k_corner(x, y, z)); // ixstep = mesh->LocalNx * mesh->LocalNz; for (int j = 0; j < 4; ++j) { PetscInt idxm[4]; From 5c324157ca1d2f3eb241eac68e5f55feaef889ae Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 19:44:17 +0100 Subject: [PATCH 014/256] Only run in parallel if we split in X --- tests/integrated/test-interpolate/runtest | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/integrated/test-interpolate/runtest b/tests/integrated/test-interpolate/runtest index 3e8f2c5bc6..f5460aff2a 100755 --- a/tests/integrated/test-interpolate/runtest +++ b/tests/integrated/test-interpolate/runtest @@ -16,9 +16,6 @@ show_plot = False # List of NX values to use nxlist = [16, 32, 64, 128] -# Only testing 2D (x, z) slices, so only need one processor -nproc = 2 - # Variables to compare varlist = ["a", "b", "c"] markers = ["bo", "r^", "kx"] @@ -50,8 +47,8 @@ for method in methods: dx = 1.0 / (nx) args = f" mesh:nx={nx + 4} mesh:dx={dx} MZ={nx} xzinterpolation:type={method}" - NXPE = 2 if method == "hermitespline" and boutconfig.has["petsc"] else 1 - args += f" NXPE={NXPE}" + nproc = 2 if method == "hermitespline" and boutconfig.has["petsc"] else 1 + args += f" NXPE={nproc}" cmd = "./test_interpolate" + args From a4a28c6e3c4025500f7a8bbbc0b0e474720703e0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Feb 2023 13:10:21 +0100 Subject: [PATCH 015/256] Delete object release leaks the pointer, reset free's the object. --- tests/integrated/test-interpolate/test_interpolate.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrated/test-interpolate/test_interpolate.cxx b/tests/integrated/test-interpolate/test_interpolate.cxx index 8f19cd2a28..ed8e80f43f 100644 --- a/tests/integrated/test-interpolate/test_interpolate.cxx +++ b/tests/integrated/test-interpolate/test_interpolate.cxx @@ -113,6 +113,7 @@ int main(int argc, char **argv) { bout::writeDefaultOutputFile(dump); bout::checkForUnusedOptions(); + interp.reset(); BoutFinalise(); return 0; From 0e28dc1c5dbaa8c5e73bb09020ac2d01924aa850 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Feb 2023 14:05:38 +0100 Subject: [PATCH 016/256] Create PetscVecs only once --- src/mesh/interpolation/hermite_spline_xz.cxx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index e41dfa4d03..4e6fc8320c 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -288,10 +288,12 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z #endif } #ifdef HS_USE_PETSC - isInit = true; MatAssemblyBegin(petscWeights, MAT_FINAL_ASSEMBLY); MatAssemblyEnd(petscWeights, MAT_FINAL_ASSEMBLY); - MatCreateVecs(petscWeights, &rhs, &result); + if (!isInit) { + MatCreateVecs(petscWeights, &rhs, &result); + } + isInit = true; #endif } From 66bde042f6553e372465eb47be0ede532a815bde Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Feb 2023 14:06:09 +0100 Subject: [PATCH 017/256] Be more general about cleaning up before BoutFinialise --- tests/integrated/test-interpolate/test_interpolate.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrated/test-interpolate/test_interpolate.cxx b/tests/integrated/test-interpolate/test_interpolate.cxx index ed8e80f43f..517d9c2445 100644 --- a/tests/integrated/test-interpolate/test_interpolate.cxx +++ b/tests/integrated/test-interpolate/test_interpolate.cxx @@ -30,7 +30,7 @@ std::shared_ptr getGeneratorFromOptions(const std::string& varna int main(int argc, char **argv) { BoutInitialise(argc, argv); - + { // Random number generator std::default_random_engine generator; // Uniform distribution of BoutReals from 0 to 1 @@ -113,7 +113,7 @@ int main(int argc, char **argv) { bout::writeDefaultOutputFile(dump); bout::checkForUnusedOptions(); - interp.reset(); + } BoutFinalise(); return 0; From adba8774b274073e07eb13cd8165a7594dd99bd0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Feb 2023 14:56:13 +0100 Subject: [PATCH 018/256] Run different interpolations in the fci test --- tests/MMS/spatial/fci/runtest | 197 +++++++++++++++++++--------------- 1 file changed, 108 insertions(+), 89 deletions(-) diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index 7a54b87ccf..d68b6c6ca1 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -47,96 +47,115 @@ failures = [] build_and_log("FCI MMS test") for nslice in nslices: - error_2[nslice] = [] - error_inf[nslice] = [] - - # Which central difference scheme to use and its expected order - order = nslice * 2 - method_orders[nslice] = {"name": "C{}".format(order), "order": order} - - for n in nlist: - # Define the magnetic field using new poloidal gridding method - # Note that the Bz and Bzprime parameters here must be the same as in mms.py - field = zb.field.Slab(Bz=0.05, Bzprime=0.1) - # Create rectangular poloidal grids - poloidal_grid = zb.poloidal_grid.RectangularPoloidalGrid(nx, n, 0.1, 1.0, MXG=1) - # Set the ylength and y locations - ylength = 10.0 - - if yperiodic: - ycoords = linspace(0.0, ylength, n, endpoint=False) + for method in [ + "hermitespline", + "lagrange4pt", + "bilinear", + # "monotonichermitespline", + ]: + error_2[nslice] = [] + error_inf[nslice] = [] + + # Which central difference scheme to use and its expected order + order = nslice * 2 + method_orders[nslice] = {"name": "C{}".format(order), "order": order} + + for n in nlist: + # Define the magnetic field using new poloidal gridding method + # Note that the Bz and Bzprime parameters here must be the same as in mms.py + field = zb.field.Slab(Bz=0.05, Bzprime=0.1) + # Create rectangular poloidal grids + poloidal_grid = zb.poloidal_grid.RectangularPoloidalGrid( + nx, n, 0.1, 1.0, MXG=1 + ) + # Set the ylength and y locations + ylength = 10.0 + + if yperiodic: + ycoords = linspace(0.0, ylength, n, endpoint=False) + else: + # Doesn't include the end points + ycoords = (arange(n) + 0.5) * ylength / float(n) + + # Create the grid + grid = zb.grid.Grid(poloidal_grid, ycoords, ylength, yperiodic=yperiodic) + # Make and write maps + maps = zb.make_maps(grid, field, nslice=nslice, quiet=True, MXG=1) + zb.write_maps( + grid, + field, + maps, + new_names=False, + metric2d=conf.isMetric2D(), + quiet=True, + ) + + args = " MZ={} MYG={} mesh:paralleltransform:y_periodic={} mesh:ddy:first={} NXPE={}".format( + n, + nslice, + yperiodic, + method_orders[nslice]["name"], + 2 if conf.has["petsc"] and method == "hermitespline" else 1, + ) + args += f" mesh:paralleltransform:xzinterpolation:type={method}" + + # Command to run + cmd = "./fci_mms " + args + + print("Running command: " + cmd) + + # Launch using MPI + s, out = launch_safe(cmd, nproc=nproc, mthread=mthread, pipe=True) + + # Save output to log file + with open("run.log." + str(n), "w") as f: + f.write(out) + + if s: + print("Run failed!\nOutput was:\n") + print(out) + exit(s) + + # Collect data + l_2 = collect( + "l_2", + tind=[1, 1], + info=False, + path=directory, + xguards=False, + yguards=False, + ) + l_inf = collect( + "l_inf", + tind=[1, 1], + info=False, + path=directory, + xguards=False, + yguards=False, + ) + + error_2[nslice].append(l_2) + error_inf[nslice].append(l_inf) + + print("Errors : l-2 {:f} l-inf {:f}".format(l_2, l_inf)) + + dx = 1.0 / array(nlist) + + # Calculate convergence order + fit = polyfit(log(dx), log(error_2[nslice]), 1) + order = fit[0] + stdout.write("Convergence order = {:f} (fit)".format(order)) + + order = log(error_2[nslice][-2] / error_2[nslice][-1]) / log(dx[-2] / dx[-1]) + stdout.write(", {:f} (small spacing)".format(order)) + + # Should be close to the expected order + if order > method_orders[nslice]["order"] * 0.95: + print("............ PASS\n") else: - # Doesn't include the end points - ycoords = (arange(n) + 0.5) * ylength / float(n) - - # Create the grid - grid = zb.grid.Grid(poloidal_grid, ycoords, ylength, yperiodic=yperiodic) - # Make and write maps - maps = zb.make_maps(grid, field, nslice=nslice, quiet=True, MXG=1) - zb.write_maps( - grid, field, maps, new_names=False, metric2d=conf.isMetric2D(), quiet=True - ) - - args = " MZ={} MYG={} mesh:paralleltransform:y_periodic={} mesh:ddy:first={} NXPE={}".format( - n, - nslice, - yperiodic, - method_orders[nslice]["name"], - 2 if conf.has["petsc"] else 1, - ) - - # Command to run - cmd = "./fci_mms " + args - - print("Running command: " + cmd) - - # Launch using MPI - s, out = launch_safe(cmd, nproc=nproc, mthread=mthread, pipe=True) - - # Save output to log file - with open("run.log." + str(n), "w") as f: - f.write(out) - - if s: - print("Run failed!\nOutput was:\n") - print(out) - exit(s) - - # Collect data - l_2 = collect( - "l_2", tind=[1, 1], info=False, path=directory, xguards=False, yguards=False - ) - l_inf = collect( - "l_inf", - tind=[1, 1], - info=False, - path=directory, - xguards=False, - yguards=False, - ) - - error_2[nslice].append(l_2) - error_inf[nslice].append(l_inf) - - print("Errors : l-2 {:f} l-inf {:f}".format(l_2, l_inf)) - - dx = 1.0 / array(nlist) - - # Calculate convergence order - fit = polyfit(log(dx), log(error_2[nslice]), 1) - order = fit[0] - stdout.write("Convergence order = {:f} (fit)".format(order)) - - order = log(error_2[nslice][-2] / error_2[nslice][-1]) / log(dx[-2] / dx[-1]) - stdout.write(", {:f} (small spacing)".format(order)) - - # Should be close to the expected order - if order > method_orders[nslice]["order"] * 0.95: - print("............ PASS\n") - else: - print("............ FAIL\n") - success = False - failures.append(method_orders[nslice]["name"]) + print("............ FAIL\n") + success = False + failures.append(method_orders[nslice]["name"]) with open("fci_mms.pkl", "wb") as output: From cbaf894b2b77b09111f0b38d17653476b3263c35 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 7 Feb 2023 14:06:13 +0100 Subject: [PATCH 019/256] Fix parallel boundary region with x splitting --- src/mesh/parallel/fci.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 77f34fd282..baf0f3fc6b 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -158,6 +158,7 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, const int ncz = map_mesh.LocalNz; BoutMask to_remove(map_mesh); + const int xend = map_mesh.xstart + (map_mesh.xend - map_mesh.xstart + 1) * map_mesh.getNXPE() - 1; // Serial loop because call to BoundaryRegionPar::addPoint // (probably?) can't be done in parallel BOUT_FOR_SERIAL(i, xt_prime.getRegion("RGN_NOBNDRY")) { @@ -171,7 +172,7 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, } } - if ((xt_prime[i] >= map_mesh.xstart) and (xt_prime[i] <= map_mesh.xend)) { + if ((xt_prime[i] >= map_mesh.xstart) and (xt_prime[i] <= xend)) { // Not a boundary continue; } From 5673f0c2ea8ecef929b99fb473cc25bc393a76e7 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 7 Feb 2023 14:12:52 +0100 Subject: [PATCH 020/256] Add integrated test for FCI X splitting * helped finding the bug for the boundary * rather slow (around 1 minute) * needs internet connectivity --- cmake/BOUT++functions.cmake | 16 +++++- tests/integrated/CMakeLists.txt | 2 + tests/integrated/test-fci-mpi/CMakeLists.txt | 8 +++ tests/integrated/test-fci-mpi/data/BOUT.inp | 28 ++++++++++ tests/integrated/test-fci-mpi/fci_mpi.cxx | 37 +++++++++++++ tests/integrated/test-fci-mpi/runtest | 58 ++++++++++++++++++++ 6 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 tests/integrated/test-fci-mpi/CMakeLists.txt create mode 100644 tests/integrated/test-fci-mpi/data/BOUT.inp create mode 100644 tests/integrated/test-fci-mpi/fci_mpi.cxx create mode 100755 tests/integrated/test-fci-mpi/runtest diff --git a/cmake/BOUT++functions.cmake b/cmake/BOUT++functions.cmake index 40e45f99be..77279dfd4b 100644 --- a/cmake/BOUT++functions.cmake +++ b/cmake/BOUT++functions.cmake @@ -162,7 +162,7 @@ endfunction() # function(bout_add_integrated_or_mms_test BUILD_CHECK_TARGET TESTNAME) set(options USE_RUNTEST USE_DATA_BOUT_INP) - set(oneValueArgs EXECUTABLE_NAME PROCESSORS) + set(oneValueArgs EXECUTABLE_NAME PROCESSORS DOWNLOAD DOWNLOAD_NAME) set(multiValueArgs SOURCES EXTRA_FILES REQUIRES CONFLICTS TESTARGS EXTRA_DEPENDS) cmake_parse_arguments(BOUT_TEST_OPTIONS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -202,6 +202,20 @@ function(bout_add_integrated_or_mms_test BUILD_CHECK_TARGET TESTNAME) add_custom_target(${TESTNAME}) endif() + if (BOUT_TEST_OPTIONS_DOWNLOAD) + if (NOT BOUT_TEST_OPTIONS_DOWNLOAD_NAME) + message(FATAL_ERROR "We need DOWNLOAD_NAME if we should DOWNLOAD!") + endif() + set(output ) + add_custom_command(OUTPUT ${BOUT_TEST_OPTIONS_DOWNLOAD_NAME} + COMMAND wget ${BOUT_TEST_OPTIONS_DOWNLOAD} -O ${BOUT_TEST_OPTIONS_DOWNLOAD_NAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Downloading ${BOUT_TEST_OPTIONS_DOWNLOAD_NAME}" + ) + add_custom_target(download_test_data DEPENDS ${BOUT_TEST_OPTIONS_DOWNLOAD_NAME}) + add_dependencies(${TESTNAME} download_test_data) + endif() + if (BOUT_TEST_OPTIONS_EXTRA_DEPENDS) add_dependencies(${TESTNAME} ${BOUT_TEST_OPTIONS_EXTRA_DEPENDS}) endif() diff --git a/tests/integrated/CMakeLists.txt b/tests/integrated/CMakeLists.txt index 2fe72dfe2d..89cbf6ffe6 100644 --- a/tests/integrated/CMakeLists.txt +++ b/tests/integrated/CMakeLists.txt @@ -9,6 +9,8 @@ add_subdirectory(test-cyclic) add_subdirectory(test-delp2) add_subdirectory(test-drift-instability) add_subdirectory(test-drift-instability-staggered) +add_subdirectory(test-fci-boundary) +add_subdirectory(test-fci-mpi) add_subdirectory(test-fieldgroupComm) add_subdirectory(test-griddata) add_subdirectory(test-griddata-yboundary-guards) diff --git a/tests/integrated/test-fci-mpi/CMakeLists.txt b/tests/integrated/test-fci-mpi/CMakeLists.txt new file mode 100644 index 0000000000..6a1ec33ac6 --- /dev/null +++ b/tests/integrated/test-fci-mpi/CMakeLists.txt @@ -0,0 +1,8 @@ +bout_add_mms_test(test-fci-mpi + SOURCES fci_mpi.cxx + USE_RUNTEST + USE_DATA_BOUT_INP + PROCESSORS 6 + DOWNLOAD https://zenodo.org/record/7614499/files/W7X-conf4-36x8x128.fci.nc?download=1 + DOWNLOAD_NAME grid.fci.nc +) diff --git a/tests/integrated/test-fci-mpi/data/BOUT.inp b/tests/integrated/test-fci-mpi/data/BOUT.inp new file mode 100644 index 0000000000..47272dab61 --- /dev/null +++ b/tests/integrated/test-fci-mpi/data/BOUT.inp @@ -0,0 +1,28 @@ +grid = grid.fci.nc + +[mesh] +symmetricglobalx = true + +[mesh:ddy] +first = C2 +second = C2 + +[mesh:paralleltransform] +type = fci +y_periodic = true +z_periodic = true + +[mesh:paralleltransform:xzinterpolation] +type = hermitespline + +[input_0] +function = sin(z) + +[input_1] +function = cos(y) + +[input_2] +function = sin(x) + +[input_3] +function = sin(x) * sin(z) * cos(y) diff --git a/tests/integrated/test-fci-mpi/fci_mpi.cxx b/tests/integrated/test-fci-mpi/fci_mpi.cxx new file mode 100644 index 0000000000..b353493dda --- /dev/null +++ b/tests/integrated/test-fci-mpi/fci_mpi.cxx @@ -0,0 +1,37 @@ +#include "bout.hxx" +#include "derivs.hxx" +#include "field_factory.hxx" + +int main(int argc, char** argv) { + BoutInitialise(argc, argv); + { + using bout::globals::mesh; + Options *options = Options::getRoot(); + int i=0; + std::string default_str {"not_set"}; + Options dump; + while (true) { + std::string temp_str; + options->get(fmt::format("input_{:d}:function", i), temp_str, default_str); + if (temp_str == default_str) { + break; + } + Field3D input{FieldFactory::get()->create3D(fmt::format("input_{:d}:function", i), Options::getRoot(), mesh)}; + //options->get(fmt::format("input_{:d}:boundary_perp", i), temp_str, s"free_o3"); + mesh->communicate(input); + input.applyParallelBoundary("parallel_neumann_o2"); + for (int slice = -mesh->ystart; slice <= mesh->ystart; ++slice) { + if (slice) { + Field3D tmp{0.}; + BOUT_FOR(i, tmp.getRegion("RGN_NOBNDRY")) { + tmp[i] = input.ynext(slice)[i.yp(slice)]; + } + dump[fmt::format("output_{:d}_{:+d}", i, slice)] = tmp; + } + } + ++i; + } + bout::writeDefaultOutputFile(dump); + } + BoutFinalise(); +} diff --git a/tests/integrated/test-fci-mpi/runtest b/tests/integrated/test-fci-mpi/runtest new file mode 100755 index 0000000000..4ac0e43460 --- /dev/null +++ b/tests/integrated/test-fci-mpi/runtest @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Python script to run and analyse MMS test +# + +# Cores: 8 +# requires: metric_3d + +from boututils.run_wrapper import build_and_log, launch_safe, shell_safe +from boutdata.collect import collect +import boutconfig as conf +import itertools + +import numpy as np + +# Resolution in x and y +nlist = [1, 2, 4] + +maxcores = 8 + +nslices = [1] + +success = True + +build_and_log("FCI MMS test") + +for nslice in nslices: + for NXPE, NYPE in itertools.product(nlist, nlist): + + if NXPE * NYPE > maxcores: + continue + + args = f"NXPE={NXPE} NYPE={NYPE}" + # Command to run + cmd = f"./fci_mpi {args}" + + print(f"Running command: {cmd}") + + mthread = maxcores // (NXPE * NYPE) + # Launch using MPI + _, out = launch_safe(cmd, nproc=NXPE * NYPE, mthread=mthread, pipe=True) + + # Save output to log file + with open("run.log.{NXPE}.{NYPE}.{nslice}.log", "w") as f: + f.write(out) + + collect_kw = dict(info=False, xguards=False, yguards=False, path="data") + if NXPE == NYPE == 1: + # reference data! + ref = {} + for i in range(4): + for yp in range(1, nslice + 1): + for y in [-yp, yp]: + name = f"output_{i}_{y:+d}" + ref[name] = collect(name, **collect_kw) + else: + for name, val in ref.items(): + assert np.allclose(val, collect(name, **collect_kw)) From 9149bf406fe01a98966095455757f71c0305272c Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Wed, 8 Feb 2023 09:20:18 +0000 Subject: [PATCH 021/256] Apply black changes --- bin/bout-v5-xzinterpolation-upgrader.py | 3 --- bin/bout_3to4.py | 1 - src/field/gen_fieldops.py | 2 -- tests/MMS/GBS/circle.py | 1 - tests/MMS/GBS/mms-slab3d.py | 1 - tests/MMS/GBS/runtest-slab3d | 1 - tests/integrated/test-drift-instability/runtest | 1 - tests/integrated/test-fci-mpi/runtest | 1 - tests/integrated/test-multigrid_laplace/runtest | 1 - .../test-multigrid_laplace/runtest_multiple_grids | 1 - tests/integrated/test-multigrid_laplace/runtest_unsheared | 1 - tests/integrated/test-naulin-laplace/runtest | 1 - .../integrated/test-naulin-laplace/runtest_multiple_grids | 1 - tests/integrated/test-naulin-laplace/runtest_unsheared | 1 - tests/integrated/test-petsc_laplace_MAST-grid/runtest | 1 - tests/integrated/test-twistshift-staggered/runtest | 1 + tests/integrated/test-twistshift/runtest | 2 ++ tests/integrated/test-yupdown-weights/runtest | 1 - tests/integrated/test-yupdown/runtest | 1 - tests/integrated/test_suite | 1 + tools/pylib/post_bout/__init__.py | 1 - tools/pylib/post_bout/basic_info.py | 3 --- tools/pylib/post_bout/grate2.py | 2 -- tools/pylib/post_bout/pb_corral.py | 4 ---- tools/pylib/post_bout/pb_draw.py | 7 ------- tools/pylib/post_bout/pb_nonlinear.py | 1 - tools/pylib/post_bout/pb_present.py | 1 - tools/pylib/post_bout/read_cxx.py | 1 - tools/pylib/post_bout/read_inp.py | 3 --- tools/pylib/post_bout/rms.py | 1 - tools/tokamak_grids/elite/elite2nc | 1 + tools/tokamak_grids/gato/gato2nc | 1 + 32 files changed, 6 insertions(+), 44 deletions(-) diff --git a/bin/bout-v5-xzinterpolation-upgrader.py b/bin/bout-v5-xzinterpolation-upgrader.py index 1e19c6a034..37c79e0de8 100755 --- a/bin/bout-v5-xzinterpolation-upgrader.py +++ b/bin/bout-v5-xzinterpolation-upgrader.py @@ -64,7 +64,6 @@ def fix_header_includes(old_header, new_header, source): def fix_interpolations(old_interpolation, new_interpolation, source): - return re.sub( r""" \b{}\b @@ -118,7 +117,6 @@ def clang_fix_interpolation(old_interpolation, new_interpolation, node, source): def fix_factories(old_factory, new_factory, source): - return re.sub( r""" \b{}\b @@ -186,7 +184,6 @@ def apply_fixes(headers, interpolations, factories, source): def clang_apply_fixes(headers, interpolations, factories, filename, source): - # translation unit tu = clang_parse(filename, source) diff --git a/bin/bout_3to4.py b/bin/bout_3to4.py index 02481fcf6e..d41db36fbb 100755 --- a/bin/bout_3to4.py +++ b/bin/bout_3to4.py @@ -195,7 +195,6 @@ def throw_warnings(line_text, filename, line_num): if __name__ == "__main__": - epilog = """ Currently bout_3to4 can detect the following transformations are needed: - Triple square brackets instead of round brackets for subscripts diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 646580559f..68a3a1b059 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -189,7 +189,6 @@ def returnType(f1, f2): if __name__ == "__main__": - parser = argparse.ArgumentParser( description="Generate code for the Field arithmetic operators" ) @@ -274,7 +273,6 @@ def returnType(f1, f2): rhs.name = "rhs" for operator, operator_name in operators.items(): - template_args = { "operator": operator, "operator_name": operator_name, diff --git a/tests/MMS/GBS/circle.py b/tests/MMS/GBS/circle.py index b2553f47c3..b33cc77d8c 100644 --- a/tests/MMS/GBS/circle.py +++ b/tests/MMS/GBS/circle.py @@ -30,7 +30,6 @@ def generate( mxg=2, file="circle.nc", ): - # q = rBt / RBp Bp = r * Bt / (R * q) diff --git a/tests/MMS/GBS/mms-slab3d.py b/tests/MMS/GBS/mms-slab3d.py index f88a897c98..2f958cfa69 100644 --- a/tests/MMS/GBS/mms-slab3d.py +++ b/tests/MMS/GBS/mms-slab3d.py @@ -277,7 +277,6 @@ def C(f): # print "\n\nDelp2 phi = ", Delp2(phi, metric).subs(replace) if not estatic: - Spsi = Delp2(psi, metric) - 0.5 * Ne * mi_me * beta_e * psi - Ne * (Vi - VePsi) print("\n[psi]") print("\nsolution = " + exprToStr(psi.subs(replace))) diff --git a/tests/MMS/GBS/runtest-slab3d b/tests/MMS/GBS/runtest-slab3d index cdcde3e1b3..f193583e47 100755 --- a/tests/MMS/GBS/runtest-slab3d +++ b/tests/MMS/GBS/runtest-slab3d @@ -95,7 +95,6 @@ with open("mms-slab3d.pkl", "wb") as output: # plot errors for var, mark in zip(varlist, markers): - order = log(error_2[var][-1] / error_2[var][-2]) / log(dx[-1] / dx[-2]) print("%s Convergence order = %f" % (var, order)) if 1.9 < order < 2.1: diff --git a/tests/integrated/test-drift-instability/runtest b/tests/integrated/test-drift-instability/runtest index 16b35d69d9..e8ddddc11d 100755 --- a/tests/integrated/test-drift-instability/runtest +++ b/tests/integrated/test-drift-instability/runtest @@ -197,7 +197,6 @@ def run_zeff_case(zeff): if __name__ == "__main__": - parser = argparse.ArgumentParser("Run drift-instability test") parser.add_argument( "-Z", diff --git a/tests/integrated/test-fci-mpi/runtest b/tests/integrated/test-fci-mpi/runtest index 4ac0e43460..e12b330326 100755 --- a/tests/integrated/test-fci-mpi/runtest +++ b/tests/integrated/test-fci-mpi/runtest @@ -26,7 +26,6 @@ build_and_log("FCI MMS test") for nslice in nslices: for NXPE, NYPE in itertools.product(nlist, nlist): - if NXPE * NYPE > maxcores: continue diff --git a/tests/integrated/test-multigrid_laplace/runtest b/tests/integrated/test-multigrid_laplace/runtest index 76914d19e4..4a7455f80b 100755 --- a/tests/integrated/test-multigrid_laplace/runtest +++ b/tests/integrated/test-multigrid_laplace/runtest @@ -29,7 +29,6 @@ print("Running multigrid Laplacian inversion test") success = True for nproc in [1, 3]: - # Make sure we don't use too many cores: # Reduce number of OpenMP threads when using multiple MPI processes mthread = 2 diff --git a/tests/integrated/test-multigrid_laplace/runtest_multiple_grids b/tests/integrated/test-multigrid_laplace/runtest_multiple_grids index 26cdd96ff6..6817120b13 100755 --- a/tests/integrated/test-multigrid_laplace/runtest_multiple_grids +++ b/tests/integrated/test-multigrid_laplace/runtest_multiple_grids @@ -26,7 +26,6 @@ success = True for nproc in [1, 2, 4]: for inputfile in ["BOUT_jy4.inp", "BOUT_jy63.inp", "BOUT_jy127.inp"]: - # set nxpe on the command line as we only use solution from one point in y, so splitting in y-direction is redundant (and also doesn't help test the multigrid solver) cmd = "./test_multigrid_laplace -f " + inputfile + " NXPE=" + str(nproc) diff --git a/tests/integrated/test-multigrid_laplace/runtest_unsheared b/tests/integrated/test-multigrid_laplace/runtest_unsheared index fd0e11fbbf..cda68f2167 100755 --- a/tests/integrated/test-multigrid_laplace/runtest_unsheared +++ b/tests/integrated/test-multigrid_laplace/runtest_unsheared @@ -25,7 +25,6 @@ print("Running multigrid Laplacian inversion test") success = True for nproc in [1, 3]: - # Make sure we don't use too many cores: # Reduce number of OpenMP threads when using multiple MPI processes mthread = 2 diff --git a/tests/integrated/test-naulin-laplace/runtest b/tests/integrated/test-naulin-laplace/runtest index 82dd22776e..f972eab6cc 100755 --- a/tests/integrated/test-naulin-laplace/runtest +++ b/tests/integrated/test-naulin-laplace/runtest @@ -29,7 +29,6 @@ print("Running LaplaceNaulin inversion test") success = True for nproc in [1, 3]: - # Make sure we don't use too many cores: # Reduce number of OpenMP threads when using multiple MPI processes mthread = 2 diff --git a/tests/integrated/test-naulin-laplace/runtest_multiple_grids b/tests/integrated/test-naulin-laplace/runtest_multiple_grids index 2d583fd1b8..c0281c3a4e 100755 --- a/tests/integrated/test-naulin-laplace/runtest_multiple_grids +++ b/tests/integrated/test-naulin-laplace/runtest_multiple_grids @@ -26,7 +26,6 @@ success = True for nproc in [1, 2, 4]: for inputfile in ["BOUT_jy4.inp", "BOUT_jy63.inp", "BOUT_jy127.inp"]: - # set nxpe on the command line as we only use solution from one point in y, so splitting in y-direction is redundant (and also doesn't help test the solver) cmd = "./test_naulin_laplace -f " + inputfile + " NXPE=" + str(nproc) diff --git a/tests/integrated/test-naulin-laplace/runtest_unsheared b/tests/integrated/test-naulin-laplace/runtest_unsheared index ec956686ef..8f47f33026 100755 --- a/tests/integrated/test-naulin-laplace/runtest_unsheared +++ b/tests/integrated/test-naulin-laplace/runtest_unsheared @@ -25,7 +25,6 @@ print("Running LaplaceNaulin inversion test") success = True for nproc in [1, 3]: - # Make sure we don't use too many cores: # Reduce number of OpenMP threads when using multiple MPI processes mthread = 2 diff --git a/tests/integrated/test-petsc_laplace_MAST-grid/runtest b/tests/integrated/test-petsc_laplace_MAST-grid/runtest index c1c973e3b2..bf6656fbf2 100755 --- a/tests/integrated/test-petsc_laplace_MAST-grid/runtest +++ b/tests/integrated/test-petsc_laplace_MAST-grid/runtest @@ -43,7 +43,6 @@ for nproc in [1, 2, 4]: # if nproc > 2: # nxpe = 2 for jy in [2, 34, 65, 81, 113]: - cmd = ( "./test_petsc_laplace_MAST_grid grid=grids/grid_MAST_SOL_jyis{}.nc".format( jy diff --git a/tests/integrated/test-twistshift-staggered/runtest b/tests/integrated/test-twistshift-staggered/runtest index 2976a80ecd..5bb9963db7 100755 --- a/tests/integrated/test-twistshift-staggered/runtest +++ b/tests/integrated/test-twistshift-staggered/runtest @@ -24,6 +24,7 @@ check = collect("check", path=datapath, yguards=True, info=False) success = True + # Check test_aligned is *not* periodic in y def test1(ylower, yupper): global success diff --git a/tests/integrated/test-twistshift/runtest b/tests/integrated/test-twistshift/runtest index 26b5c2b135..c4858970c4 100755 --- a/tests/integrated/test-twistshift/runtest +++ b/tests/integrated/test-twistshift/runtest @@ -24,6 +24,7 @@ result = collect("result", path=datapath, yguards=True, info=False) success = True + # Check test_aligned is *not* periodic in y def test1(ylower, yupper): global success @@ -49,6 +50,7 @@ if numpy.any(numpy.abs(result - test) > tol): print("Fail - result has not been communicated correctly - is different from input") success = False + # Check result is periodic in y def test2(ylower, yupper): global success diff --git a/tests/integrated/test-yupdown-weights/runtest b/tests/integrated/test-yupdown-weights/runtest index e40b81368f..4b59d08cb0 100755 --- a/tests/integrated/test-yupdown-weights/runtest +++ b/tests/integrated/test-yupdown-weights/runtest @@ -10,7 +10,6 @@ build_and_log("parallel slices and weights test") failed = False for shifttype in ["shiftedinterp"]: - s, out = launch_safe( "./test_yupdown_weights mesh:paralleltransform:type=" + shifttype, nproc=1, diff --git a/tests/integrated/test-yupdown/runtest b/tests/integrated/test-yupdown/runtest index 1b24f86327..34fcd36496 100755 --- a/tests/integrated/test-yupdown/runtest +++ b/tests/integrated/test-yupdown/runtest @@ -12,7 +12,6 @@ build_and_log("parallel slices test") failed = False for shifttype in ["shifted", "shiftedinterp"]: - s, out = launch_safe( "./test_yupdown mesh:paralleltransform:type=" + shifttype, nproc=1, diff --git a/tests/integrated/test_suite b/tests/integrated/test_suite index e6012e3869..307a8d84b3 100755 --- a/tests/integrated/test_suite +++ b/tests/integrated/test_suite @@ -241,6 +241,7 @@ if args.get_list: print(test) sys.exit(0) + # A function to get more threads from the job server def get_threads(): global js_read diff --git a/tools/pylib/post_bout/__init__.py b/tools/pylib/post_bout/__init__.py index a06792ed41..069ae3e85b 100644 --- a/tools/pylib/post_bout/__init__.py +++ b/tools/pylib/post_bout/__init__.py @@ -15,7 +15,6 @@ import os try: - boutpath = os.environ["BOUT_TOP"] pylibpath = boutpath + "/tools/pylib" boutdatapath = pylibpath + "/boutdata" diff --git a/tools/pylib/post_bout/basic_info.py b/tools/pylib/post_bout/basic_info.py index 28660c39bc..563bfe6e98 100644 --- a/tools/pylib/post_bout/basic_info.py +++ b/tools/pylib/post_bout/basic_info.py @@ -10,7 +10,6 @@ def basic_info(data, meta, rescale=True, rotate=False, user_peak=0, nonlinear=None): - print("in basic_info") # from . import read_grid,parse_inp,read_inp,show @@ -227,7 +226,6 @@ def fft_info( jumps = np.where(abs(phase_r) > old_div(np.pi, 32)) # print jumps if len(jumps[0]) != 0: - all_pts = np.array(list(range(0, nt))) good_pts = (np.where(abs(phase_r) < old_div(np.pi, 3)))[0] # print good_pts,good_pts @@ -363,7 +361,6 @@ def fft_info( # return a 2d array fof boolean values, a very simple boolian filter def local_maxima(array2d, user_peak, index=False, count=4, floor=0, bug=False): - from operator import itemgetter, attrgetter if user_peak == 0: diff --git a/tools/pylib/post_bout/grate2.py b/tools/pylib/post_bout/grate2.py index 58f5a47e2b..5157863cc2 100644 --- a/tools/pylib/post_bout/grate2.py +++ b/tools/pylib/post_bout/grate2.py @@ -13,7 +13,6 @@ def avgrate(p, y=None, tind=None): - if tind is None: tind = 0 @@ -25,7 +24,6 @@ def avgrate(p, y=None, tind=None): growth = np.zeros((ni, nj)) with np.errstate(divide="ignore"): - for i in range(ni): for j in range(nj): growth[i, j] = np.gradient(np.log(rmsp_f[tind::, i, j]))[-1] diff --git a/tools/pylib/post_bout/pb_corral.py b/tools/pylib/post_bout/pb_corral.py index 412a677921..df9a9b00b6 100644 --- a/tools/pylib/post_bout/pb_corral.py +++ b/tools/pylib/post_bout/pb_corral.py @@ -40,7 +40,6 @@ def corral( cached=True, refresh=False, debug=False, IConly=1, logname="status.log", skew=False ): - print("in corral") log = read_log(logname=logname) # done = log['done'] @@ -53,7 +52,6 @@ def corral( print("current:", current) if refresh == True: - for i, path in enumerate(runs): print(i, path) a = post_bout.save(path=path, IConly=IConly) # re post-process a run @@ -61,7 +59,6 @@ def corral( elif ( cached == False ): # if all the ind. simulation pkl files are in place skip this part - a = post_bout.save(path=current) # save to current dir # here is really where you shoudl write to status.log # write_log('status.log', @@ -111,7 +108,6 @@ def islist(input): class LinRes(object): def __init__(self, all_modes): - self.mode_db = all_modes self.db = all_modes # self.ave_db = all_ave diff --git a/tools/pylib/post_bout/pb_draw.py b/tools/pylib/post_bout/pb_draw.py index 75bca03a15..272aab9c35 100644 --- a/tools/pylib/post_bout/pb_draw.py +++ b/tools/pylib/post_bout/pb_draw.py @@ -140,7 +140,6 @@ def plottheory( fig1.savefig(pp, format="pdf") plt.close(fig1) else: # if not plot its probably plotted iwth sim data, print chi somewhere - for i, m in enumerate(s.models): textstr = r"$\chi^2$" + "$=%.2f$" % (m.chi[comp].sum()) print(textstr) @@ -173,7 +172,6 @@ def plotomega( trans=False, infobox=True, ): - colors = [ "b.", "r.", @@ -1064,7 +1062,6 @@ def plotmodes( linestyle="-", summary=True, ): - Nplots = self.nrun colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] @@ -1265,7 +1262,6 @@ def plotmodes( def plotradeigen( self, pp, field="Ni", comp="amp", yscale="linear", xscale="linear" ): - Nplots = self.nrun colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] fig1 = plt.figure() @@ -1368,7 +1364,6 @@ def plotmodes2( xrange=1, debug=False, ): - Nplots = self.nrun Modes = subset(self.db, "field", [field]) # pick field colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] @@ -1464,7 +1459,6 @@ def plotMacroDep( def savemovie( self, field="Ni", yscale="log", xscale="log", moviename="spectrum.avi" ): - print("Making movie animation.mpg - this make take a while") files = [] @@ -1504,7 +1498,6 @@ def savemovie( os.system("rm *png") def printmeta(self, pp, filename="output2.pdf", debug=False): - import os from pyPdf import PdfFileWriter, PdfFileReader diff --git a/tools/pylib/post_bout/pb_nonlinear.py b/tools/pylib/post_bout/pb_nonlinear.py index c14072cff5..3fb726d4f8 100644 --- a/tools/pylib/post_bout/pb_nonlinear.py +++ b/tools/pylib/post_bout/pb_nonlinear.py @@ -46,7 +46,6 @@ def plotnlrhs( xscale="linear", xrange=1, ): - colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] Modes = subset(self.db, "field", [field]) # pick field diff --git a/tools/pylib/post_bout/pb_present.py b/tools/pylib/post_bout/pb_present.py index e1fc92ea6d..64fcaf1ec6 100644 --- a/tools/pylib/post_bout/pb_present.py +++ b/tools/pylib/post_bout/pb_present.py @@ -96,7 +96,6 @@ def show( for j in list( set(s.dz).union() ): # looping over runs, over unique 'dz' key values - ss = subset(s.db, "dz", [j]) # subset where dz = j plt.scatter(ss.MN[:, 1], ss.MN[:, 0], c=colors[i]) plt.annotate(str(j), (ss.MN[0, 1], ss.MN[0, 0])) diff --git a/tools/pylib/post_bout/read_cxx.py b/tools/pylib/post_bout/read_cxx.py index 907edc48a7..eda88aac5b 100644 --- a/tools/pylib/post_bout/read_cxx.py +++ b/tools/pylib/post_bout/read_cxx.py @@ -98,7 +98,6 @@ def get_evolved_cxx(cxxfile=None): def read_cxx(path=".", boutcxx="physics_code.cxx.ref", evolved=""): - # print path, boutcxx boutcxx = path + "/" + boutcxx # boutcxx = open(boutcxx,'r').readlines() diff --git a/tools/pylib/post_bout/read_inp.py b/tools/pylib/post_bout/read_inp.py index 40a60774af..87de7ddf3a 100644 --- a/tools/pylib/post_bout/read_inp.py +++ b/tools/pylib/post_bout/read_inp.py @@ -12,7 +12,6 @@ def read_inp(path="", boutinp="BOUT.inp"): - boutfile = path + "/" + boutinp boutinp = open(boutfile, "r").readlines() @@ -29,7 +28,6 @@ def read_inp(path="", boutinp="BOUT.inp"): def parse_inp(boutlist): - import re from ordereddict import OrderedDict @@ -67,7 +65,6 @@ def parse_inp(boutlist): def read_log(path=".", logname="status.log"): - print("in read_log") import re from ordereddict import OrderedDict diff --git a/tools/pylib/post_bout/rms.py b/tools/pylib/post_bout/rms.py index 9ec23d9f90..6a9bdb1929 100644 --- a/tools/pylib/post_bout/rms.py +++ b/tools/pylib/post_bout/rms.py @@ -14,7 +14,6 @@ def rms(f): - nt = f.shape[0] ns = f.shape[1] diff --git a/tools/tokamak_grids/elite/elite2nc b/tools/tokamak_grids/elite/elite2nc index eb17c13bd9..669c36aef9 100755 --- a/tools/tokamak_grids/elite/elite2nc +++ b/tools/tokamak_grids/elite/elite2nc @@ -57,6 +57,7 @@ if not desc: print("Description: " + desc) + # Define a generator to get the next token from the file def file_tokens(fp): toklist = [] diff --git a/tools/tokamak_grids/gato/gato2nc b/tools/tokamak_grids/gato/gato2nc index ba4cdc6e69..4ed2b2d632 100755 --- a/tools/tokamak_grids/gato/gato2nc +++ b/tools/tokamak_grids/gato/gato2nc @@ -68,6 +68,7 @@ print("Date: " + date) desc = f.readline() print("Description: " + desc) + # Define a generator to get the next token from the file def file_tokens(fp): """Generator to get numbers from a text file""" From a088700cd86a4a1244a9159b0f15dd4f62fa40da Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Wed, 8 Feb 2023 10:19:40 +0000 Subject: [PATCH 022/256] Apply clang-format changes --- include/interpolation_xz.hxx | 21 ++- include/mask.hxx | 3 +- src/mesh/interpolation/hermite_spline_xz.cxx | 84 +++++----- src/mesh/parallel/fci.cxx | 12 +- tests/integrated/test-fci-mpi/fci_mpi.cxx | 27 ++-- .../test-interpolate/test_interpolate.cxx | 146 +++++++++--------- 6 files changed, 147 insertions(+), 146 deletions(-) diff --git a/include/interpolation_xz.hxx b/include/interpolation_xz.hxx index 620d146edf..df4c7fc61a 100644 --- a/include/interpolation_xz.hxx +++ b/include/interpolation_xz.hxx @@ -63,25 +63,23 @@ public: setMask(mask); } XZInterpolation(const std::string& region_name, int y_offset = 0, Mesh* mesh = nullptr) - : y_offset(y_offset), localmesh(mesh), region_id(localmesh->getRegionID(region_name)) {} - XZInterpolation(const Region& region, int y_offset = 0, - Mesh* mesh = nullptr) - : y_offset(y_offset), localmesh(mesh){ + : y_offset(y_offset), localmesh(mesh), + region_id(localmesh->getRegionID(region_name)) {} + XZInterpolation(const Region& region, int y_offset = 0, Mesh* mesh = nullptr) + : y_offset(y_offset), localmesh(mesh) { setRegion(region); } virtual ~XZInterpolation() = default; - void setMask(const BoutMask& mask) { - setRegion(regionFromMask(mask, localmesh)); - } + void setMask(const BoutMask& mask) { setRegion(regionFromMask(mask, localmesh)); } void setRegion(const std::string& region_name) { this->region_id = localmesh->getRegionID(region_name); } void setRegion(const Region& region) { std::string name; - int i=0; + int i = 0; do { - name = fmt::format("unsec_reg_xz_interp_{:d}",i++); + name = fmt::format("unsec_reg_xz_interp_{:d}", i++); } while (localmesh->hasRegion3D(name)); localmesh->addRegion(name, region); this->region_id = localmesh->getRegionID(name); @@ -94,10 +92,11 @@ public: if (region_id == -1) { return localmesh->getRegion(region); } - if (region == "" or region == "RGN_ALL"){ + if (region == "" or region == "RGN_ALL") { return getRegion(); } - return localmesh->getRegion(localmesh->getCommonRegion(localmesh->getRegionID(region), region_id)); + return localmesh->getRegion( + localmesh->getCommonRegion(localmesh->getRegionID(region), region_id)); } virtual void calcWeights(const Field3D& delta_x, const Field3D& delta_z, const std::string& region = "RGN_NOBNDRY") = 0; diff --git a/include/mask.hxx b/include/mask.hxx index 96d2c99ac3..20211b5d02 100644 --- a/include/mask.hxx +++ b/include/mask.hxx @@ -73,8 +73,7 @@ public: inline const bool& operator[](const Ind3D& i) const { return mask[i]; } }; -inline Region regionFromMask(const BoutMask& mask, - const Mesh* mesh) { +inline Region regionFromMask(const BoutMask& mask, const Mesh* mesh) { std::vector indices; for (auto i : mesh->getRegion("RGN_ALL")) { if (not mask(i.x(), i.y(), i.z())) { diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 4e6fc8320c..a5b9c8bd05 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -20,10 +20,10 @@ * **************************************************************************/ +#include "../impls/bout/boutmesh.hxx" #include "globals.hxx" #include "interpolation_xz.hxx" #include "bout/index_derivs_interface.hxx" -#include "../impls/bout/boutmesh.hxx" #include @@ -126,7 +126,7 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) #if USE_NEW_WEIGHTS newWeights.reserve(16); - for (int w=0; w<16;++w){ + for (int w = 0; w < 16; ++w) { newWeights.emplace_back(localmesh); newWeights[w].allocate(); } @@ -137,8 +137,9 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) // MatCreate(MPI_COMM_WORLD, &petscWeights); // MatSetSizes(petscWeights, m, m, M, M); // PetscErrorCode MatCreateAIJ(MPI_Comm comm, PetscInt m, PetscInt n, PetscInt M, - // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], PetscInt o_nz, const PetscInt - //o_nnz[], Mat *A) + // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], PetscInt o_nz, + // const PetscInt + // o_nnz[], Mat *A) // MatSetSizes(Mat A,PetscInt m,PetscInt n,PetscInt M,PetscInt N) const int m = mesh->LocalNx * mesh->LocalNy * mesh->LocalNz; const int M = m * mesh->getNXPE() * mesh->getNYPE(); @@ -185,8 +186,8 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z // Check that t_x and t_z are in range if ((t_x < 0.0) || (t_x > 1.0)) { throw BoutException( - "t_x={:e} out of range at ({:d},{:d},{:d}) (delta_x={:e}, i_corn={:d})", t_x, - x, y, z, delta_x(x, y, z), i_corn); + "t_x={:e} out of range at ({:d},{:d},{:d}) (delta_x={:e}, i_corn={:d})", t_x, x, + y, z, delta_x(x, y, z), i_corn); } if ((t_z < 0.0) || (t_z > 1.0)) { @@ -212,8 +213,8 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z #if USE_NEW_WEIGHTS - for (int w =0; w<16;++w){ - newWeights[w][i]=0; + for (int w = 0; w < 16; ++w) { + newWeights[w][i] = 0; } // The distribution of our weights: // 0 4 8 12 @@ -223,54 +224,55 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z // e.g. 1 == ic.xm(); 4 == ic.zm(); 5 == ic; 7 == ic.zp(2); // f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; - newWeights[5][i] += h00_x[i] * h00_z[i]; - newWeights[9][i] += h01_x[i] * h00_z[i]; - newWeights[9][i] += h10_x[i] * h00_z[i] / 2; - newWeights[1][i] -= h10_x[i] * h00_z[i] / 2; + newWeights[5][i] += h00_x[i] * h00_z[i]; + newWeights[9][i] += h01_x[i] * h00_z[i]; + newWeights[9][i] += h10_x[i] * h00_z[i] / 2; + newWeights[1][i] -= h10_x[i] * h00_z[i] / 2; newWeights[13][i] += h11_x[i] * h00_z[i] / 2; - newWeights[5][i] -= h11_x[i] * h00_z[i] / 2; + newWeights[5][i] -= h11_x[i] * h00_z[i] / 2; // f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + // fx[iczp] * h10_x[i] + fx[icxpzp] * h11_x[i]; - newWeights[6][i] += h00_x[i] * h01_z[i]; + newWeights[6][i] += h00_x[i] * h01_z[i]; newWeights[10][i] += h01_x[i] * h01_z[i]; newWeights[10][i] += h10_x[i] * h01_z[i] / 2; - newWeights[2][i] -= h10_x[i] * h01_z[i] / 2; + newWeights[2][i] -= h10_x[i] * h01_z[i] / 2; newWeights[14][i] += h11_x[i] * h01_z[i] / 2; - newWeights[6][i] -= h11_x[i] * h01_z[i] / 2; + newWeights[6][i] -= h11_x[i] * h01_z[i] / 2; // fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + // fxz[ic] * h10_x[i]+ fxz[icxp] * h11_x[i]; - newWeights[6][i] += h00_x[i] * h10_z[i] / 2; - newWeights[4][i] -= h00_x[i] * h10_z[i] / 2; + newWeights[6][i] += h00_x[i] * h10_z[i] / 2; + newWeights[4][i] -= h00_x[i] * h10_z[i] / 2; newWeights[10][i] += h01_x[i] * h10_z[i] / 2; - newWeights[8][i] -= h01_x[i] * h10_z[i] / 2; + newWeights[8][i] -= h01_x[i] * h10_z[i] / 2; newWeights[10][i] += h10_x[i] * h10_z[i] / 4; - newWeights[8][i] -= h10_x[i] * h10_z[i] / 4; - newWeights[2][i] -= h10_x[i] * h10_z[i] / 4; - newWeights[0][i] += h10_x[i] * h10_z[i] / 4; + newWeights[8][i] -= h10_x[i] * h10_z[i] / 4; + newWeights[2][i] -= h10_x[i] * h10_z[i] / 4; + newWeights[0][i] += h10_x[i] * h10_z[i] / 4; newWeights[14][i] += h11_x[i] * h10_z[i] / 4; newWeights[12][i] -= h11_x[i] * h10_z[i] / 4; - newWeights[6][i] -= h11_x[i] * h10_z[i] / 4; - newWeights[4][i] += h11_x[i] * h10_z[i] / 4; + newWeights[6][i] -= h11_x[i] * h10_z[i] / 4; + newWeights[4][i] += h11_x[i] * h10_z[i] / 4; // fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + // fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; - newWeights[7][i] += h00_x[i] * h11_z[i] / 2; - newWeights[5][i] -= h00_x[i] * h11_z[i] / 2; + newWeights[7][i] += h00_x[i] * h11_z[i] / 2; + newWeights[5][i] -= h00_x[i] * h11_z[i] / 2; newWeights[11][i] += h01_x[i] * h11_z[i] / 2; - newWeights[9][i] -= h01_x[i] * h11_z[i] / 2; + newWeights[9][i] -= h01_x[i] * h11_z[i] / 2; newWeights[11][i] += h10_x[i] * h11_z[i] / 4; - newWeights[9][i] -= h10_x[i] * h11_z[i] / 4; - newWeights[3][i] -= h10_x[i] * h11_z[i] / 4; - newWeights[1][i] += h10_x[i] * h11_z[i] / 4; + newWeights[9][i] -= h10_x[i] * h11_z[i] / 4; + newWeights[3][i] -= h10_x[i] * h11_z[i] / 4; + newWeights[1][i] += h10_x[i] * h11_z[i] / 4; newWeights[15][i] += h11_x[i] * h11_z[i] / 4; newWeights[13][i] -= h11_x[i] * h11_z[i] / 4; - newWeights[7][i] -= h11_x[i] * h11_z[i] / 4; - newWeights[5][i] += h11_x[i] * h11_z[i] / 4; + newWeights[7][i] -= h11_x[i] * h11_z[i] / 4; + newWeights[5][i] += h11_x[i] * h11_z[i] / 4; #ifdef HS_USE_PETSC PetscInt idxn[1] = {conv.fromLocalToGlobal(x, y + y_offset, z)}; - // output.write("debug: {:d} -> {:d}: {:d}:{:d} -> {:d}:{:d}\n", conv.fromLocalToGlobal(x, y + y_offset, z), + // output.write("debug: {:d} -> {:d}: {:d}:{:d} -> {:d}:{:d}\n", + // conv.fromLocalToGlobal(x, y + y_offset, z), // conv.fromMeshToGlobal(i_corn, y + y_offset, k_corner(x, y, z)), // x, z, i_corn, k_corner(x, y, z)); // ixstep = mesh->LocalNx * mesh->LocalNz; @@ -355,15 +357,15 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region VecRestoreArrayRead(result, &cptr); #else BOUT_FOR(i, getRegion(region)) { - auto ic = i_corner[i]; + auto ic = i_corner[i]; auto iyp = i.yp(y_offset); - f_interp[iyp]=0; - for (int w = 0; w < 4; ++w){ - f_interp[iyp] += newWeights[w*4+0][i] * f[ic.zm().xp(w-1)]; - f_interp[iyp] += newWeights[w*4+1][i] * f[ic.xp(w-1)]; - f_interp[iyp] += newWeights[w*4+2][i] * f[ic.zp().xp(w-1)]; - f_interp[iyp] += newWeights[w*4+3][i] * f[ic.zp(2).xp(w-1)]; + f_interp[iyp] = 0; + for (int w = 0; w < 4; ++w) { + f_interp[iyp] += newWeights[w * 4 + 0][i] * f[ic.zm().xp(w - 1)]; + f_interp[iyp] += newWeights[w * 4 + 1][i] * f[ic.xp(w - 1)]; + f_interp[iyp] += newWeights[w * 4 + 2][i] * f[ic.zp().xp(w - 1)]; + f_interp[iyp] += newWeights[w * 4 + 3][i] * f[ic.zp(2).xp(w - 1)]; } } #endif @@ -411,7 +413,7 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region || i.x() > localmesh->xend); } return f_interp; -# endif +#endif } Field3D XZHermiteSpline::interpolate(const Field3D& f, const Field3D& delta_x, diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index baf0f3fc6b..4b964ae0fa 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -158,7 +158,8 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, const int ncz = map_mesh.LocalNz; BoutMask to_remove(map_mesh); - const int xend = map_mesh.xstart + (map_mesh.xend - map_mesh.xstart + 1) * map_mesh.getNXPE() - 1; + const int xend = + map_mesh.xstart + (map_mesh.xend - map_mesh.xstart + 1) * map_mesh.getNXPE() - 1; // Serial loop because call to BoundaryRegionPar::addPoint // (probably?) can't be done in parallel BOUT_FOR_SERIAL(i, xt_prime.getRegion("RGN_NOBNDRY")) { @@ -247,11 +248,10 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, const auto region = fmt::format("RGN_YPAR_{:+d}", offset); if (not map_mesh.hasRegion3D(region)) { // The valid region for this slice - map_mesh.addRegion3D(region, - Region(map_mesh.xstart, map_mesh.xend, - map_mesh.ystart+offset, map_mesh.yend+offset, - 0, map_mesh.LocalNz-1, - map_mesh.LocalNy, map_mesh.LocalNz)); + map_mesh.addRegion3D( + region, Region(map_mesh.xstart, map_mesh.xend, map_mesh.ystart + offset, + map_mesh.yend + offset, 0, map_mesh.LocalNz - 1, + map_mesh.LocalNy, map_mesh.LocalNz)); } } diff --git a/tests/integrated/test-fci-mpi/fci_mpi.cxx b/tests/integrated/test-fci-mpi/fci_mpi.cxx index b353493dda..6ae711351e 100644 --- a/tests/integrated/test-fci-mpi/fci_mpi.cxx +++ b/tests/integrated/test-fci-mpi/fci_mpi.cxx @@ -6,28 +6,29 @@ int main(int argc, char** argv) { BoutInitialise(argc, argv); { using bout::globals::mesh; - Options *options = Options::getRoot(); - int i=0; - std::string default_str {"not_set"}; + Options* options = Options::getRoot(); + int i = 0; + std::string default_str{"not_set"}; Options dump; while (true) { std::string temp_str; options->get(fmt::format("input_{:d}:function", i), temp_str, default_str); if (temp_str == default_str) { - break; + break; } - Field3D input{FieldFactory::get()->create3D(fmt::format("input_{:d}:function", i), Options::getRoot(), mesh)}; - //options->get(fmt::format("input_{:d}:boundary_perp", i), temp_str, s"free_o3"); + Field3D input{FieldFactory::get()->create3D(fmt::format("input_{:d}:function", i), + Options::getRoot(), mesh)}; + // options->get(fmt::format("input_{:d}:boundary_perp", i), temp_str, s"free_o3"); mesh->communicate(input); input.applyParallelBoundary("parallel_neumann_o2"); for (int slice = -mesh->ystart; slice <= mesh->ystart; ++slice) { - if (slice) { - Field3D tmp{0.}; - BOUT_FOR(i, tmp.getRegion("RGN_NOBNDRY")) { - tmp[i] = input.ynext(slice)[i.yp(slice)]; - } - dump[fmt::format("output_{:d}_{:+d}", i, slice)] = tmp; - } + if (slice) { + Field3D tmp{0.}; + BOUT_FOR(i, tmp.getRegion("RGN_NOBNDRY")) { + tmp[i] = input.ynext(slice)[i.yp(slice)]; + } + dump[fmt::format("output_{:d}_{:+d}", i, slice)] = tmp; + } } ++i; } diff --git a/tests/integrated/test-interpolate/test_interpolate.cxx b/tests/integrated/test-interpolate/test_interpolate.cxx index 517d9c2445..a14208e7b3 100644 --- a/tests/integrated/test-interpolate/test_interpolate.cxx +++ b/tests/integrated/test-interpolate/test_interpolate.cxx @@ -31,88 +31,88 @@ std::shared_ptr getGeneratorFromOptions(const std::string& varna int main(int argc, char **argv) { BoutInitialise(argc, argv); { - // Random number generator - std::default_random_engine generator; - // Uniform distribution of BoutReals from 0 to 1 - std::uniform_real_distribution distribution{0.0, 1.0}; - - using bout::globals::mesh; - - FieldFactory f(mesh); - - // Set up generators and solutions for three different analtyic functions - std::string a_func; - auto a_gen = getGeneratorFromOptions("a", a_func); - Field3D a = f.create3D(a_func); - Field3D a_solution = 0.0; - Field3D a_interp = 0.0; - - std::string b_func; - auto b_gen = getGeneratorFromOptions("b", b_func); - Field3D b = f.create3D(b_func); - Field3D b_solution = 0.0; - Field3D b_interp = 0.0; - - std::string c_func; - auto c_gen = getGeneratorFromOptions("c", c_func); - Field3D c = f.create3D(c_func); - Field3D c_solution = 0.0; - Field3D c_interp = 0.0; - - // x and z displacements - Field3D deltax = 0.0; - Field3D deltaz = 0.0; - - // Bind the random number generator and distribution into a single function - auto dice = std::bind(distribution, generator); - - for (const auto &index : deltax) { - // Get some random displacements - BoutReal dx = index.x() + dice(); - BoutReal dz = index.z() + dice(); - // For the last point, put the displacement inwards - // Otherwise we try to interpolate in the guard cells, which doesn't work so well - if (index.x() >= mesh->xend && mesh->getNXPE() - 1 == mesh->getXProcIndex()) { - dx = index.x() - dice(); + // Random number generator + std::default_random_engine generator; + // Uniform distribution of BoutReals from 0 to 1 + std::uniform_real_distribution distribution{0.0, 1.0}; + + using bout::globals::mesh; + + FieldFactory f(mesh); + + // Set up generators and solutions for three different analtyic functions + std::string a_func; + auto a_gen = getGeneratorFromOptions("a", a_func); + Field3D a = f.create3D(a_func); + Field3D a_solution = 0.0; + Field3D a_interp = 0.0; + + std::string b_func; + auto b_gen = getGeneratorFromOptions("b", b_func); + Field3D b = f.create3D(b_func); + Field3D b_solution = 0.0; + Field3D b_interp = 0.0; + + std::string c_func; + auto c_gen = getGeneratorFromOptions("c", c_func); + Field3D c = f.create3D(c_func); + Field3D c_solution = 0.0; + Field3D c_interp = 0.0; + + // x and z displacements + Field3D deltax = 0.0; + Field3D deltaz = 0.0; + + // Bind the random number generator and distribution into a single function + auto dice = std::bind(distribution, generator); + + for (const auto& index : deltax) { + // Get some random displacements + BoutReal dx = index.x() + dice(); + BoutReal dz = index.z() + dice(); + // For the last point, put the displacement inwards + // Otherwise we try to interpolate in the guard cells, which doesn't work so well + if (index.x() >= mesh->xend && mesh->getNXPE() - 1 == mesh->getXProcIndex()) { + dx = index.x() - dice(); + } + deltax[index] = dx; + deltaz[index] = dz; + // Get the global indices + bout::generator::Context pos{index, CELL_CENTRE, deltax.getMesh(), 0.0}; + pos.set("x", mesh->GlobalX(dx), "z", + TWOPI * static_cast(dz) / static_cast(mesh->LocalNz)); + // Generate the analytic solution at the displacements + a_solution[index] = a_gen->generate(pos); + b_solution[index] = b_gen->generate(pos); + c_solution[index] = c_gen->generate(pos); } - deltax[index] = dx; - deltaz[index] = dz; - // Get the global indices - bout::generator::Context pos{index, CELL_CENTRE, deltax.getMesh(), 0.0}; - pos.set("x", mesh->GlobalX(dx), - "z", TWOPI * static_cast(dz) / static_cast(mesh->LocalNz)); - // Generate the analytic solution at the displacements - a_solution[index] = a_gen->generate(pos); - b_solution[index] = b_gen->generate(pos); - c_solution[index] = c_gen->generate(pos); - } - deltax += (mesh->LocalNx - mesh->xstart * 2) * mesh->getXProcIndex(); - // Create the interpolation object from the input options - auto interp = XZInterpolationFactory::getInstance().create(); + deltax += (mesh->LocalNx - mesh->xstart * 2) * mesh->getXProcIndex(); + // Create the interpolation object from the input options + auto interp = XZInterpolationFactory::getInstance().create(); - // Interpolate the analytic functions at the displacements - a_interp = interp->interpolate(a, deltax, deltaz); - b_interp = interp->interpolate(b, deltax, deltaz); - c_interp = interp->interpolate(c, deltax, deltaz); + // Interpolate the analytic functions at the displacements + a_interp = interp->interpolate(a, deltax, deltaz); + b_interp = interp->interpolate(b, deltax, deltaz); + c_interp = interp->interpolate(c, deltax, deltaz); - Options dump; + Options dump; - dump["a"] = a; - dump["a_interp"] = a_interp; - dump["a_solution"] = a_solution; + dump["a"] = a; + dump["a_interp"] = a_interp; + dump["a_solution"] = a_solution; - dump["b"] = b; - dump["b_interp"] = b_interp; - dump["b_solution"] = b_solution; + dump["b"] = b; + dump["b_interp"] = b_interp; + dump["b_solution"] = b_solution; - dump["c"] = c; - dump["c_interp"] = c_interp; - dump["c_solution"] = c_solution; + dump["c"] = c; + dump["c_interp"] = c_interp; + dump["c_solution"] = c_solution; - bout::writeDefaultOutputFile(dump); + bout::writeDefaultOutputFile(dump); - bout::checkForUnusedOptions(); + bout::checkForUnusedOptions(); } BoutFinalise(); From 32ea2fdcf7a10efb86e9c8541d6dcb47aaa1f538 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Wed, 8 Feb 2023 13:10:31 +0000 Subject: [PATCH 023/256] Apply clang-format changes --- src/mesh/interpolation/hermite_spline_xz.cxx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index a5b9c8bd05..2d1649a7c1 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -137,9 +137,8 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) // MatCreate(MPI_COMM_WORLD, &petscWeights); // MatSetSizes(petscWeights, m, m, M, M); // PetscErrorCode MatCreateAIJ(MPI_Comm comm, PetscInt m, PetscInt n, PetscInt M, - // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], PetscInt o_nz, - // const PetscInt - // o_nnz[], Mat *A) + // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], PetscInt + // o_nz, const PetscInt o_nnz[], Mat *A) // MatSetSizes(Mat A,PetscInt m,PetscInt n,PetscInt M,PetscInt N) const int m = mesh->LocalNx * mesh->LocalNy * mesh->LocalNz; const int M = m * mesh->getNXPE() * mesh->getNYPE(); From faac69c321c957d542b7631e900d34e690238f02 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Wed, 8 Feb 2023 13:10:52 +0000 Subject: [PATCH 024/256] Apply clang-format changes --- src/mesh/interpolation/hermite_spline_xz.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 2d1649a7c1..93f1c326e8 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -137,8 +137,8 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) // MatCreate(MPI_COMM_WORLD, &petscWeights); // MatSetSizes(petscWeights, m, m, M, M); // PetscErrorCode MatCreateAIJ(MPI_Comm comm, PetscInt m, PetscInt n, PetscInt M, - // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], PetscInt - // o_nz, const PetscInt o_nnz[], Mat *A) + // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], + // PetscInt o_nz, const PetscInt o_nnz[], Mat *A) // MatSetSizes(Mat A,PetscInt m,PetscInt n,PetscInt M,PetscInt N) const int m = mesh->LocalNx * mesh->LocalNy * mesh->LocalNz; const int M = m * mesh->getNXPE() * mesh->getNYPE(); From 49cd98582a5048c779d93c223fa014b855abb5c7 Mon Sep 17 00:00:00 2001 From: David Bold Date: Sun, 4 Feb 2024 21:26:20 +0100 Subject: [PATCH 025/256] Merge remote-tracking branch 'origin/next' into fci-splitting --- .build_adios2_for_ci.sh | 62 + .clang-tidy | 2 +- .github/workflows/clang-tidy-review.yml | 2 +- .github/workflows/tests.yml | 8 +- CHANGELOG.md | 2 + CMakeLists.txt | 12 +- README.md | 77 +- bin/bout-config.in | 7 + bout++Config.cmake.in | 1 + cmake/SetupBOUTThirdParty.cmake | 39 + cmake_build_defines.hxx.in | 1 + examples/elm-pb-outerloop/data/BOUT.inp | 6 - .../elm-pb-outerloop/elm_pb_outerloop.cxx | 37 +- examples/elm-pb/data/BOUT.inp | 12 +- examples/elm-pb/elm_pb.cxx | 12 +- examples/elm-pb/plot_linear.py | 85 + examples/make-script/makefile | 4 +- .../performance/arithmetic/arithmetic.cxx | 14 +- examples/shear-alfven-wave/orig_test.idl.dat | Bin 2436 -> 0 bytes examples/uedge-benchmark/result_080917.idl | Bin 268684 -> 0 bytes examples/uedge-benchmark/ue_bmk.idl | Bin 324156 -> 0 bytes include/bout/adios_object.hxx | 83 + include/bout/boundary_op.hxx | 5 +- include/bout/bout.hxx | 10 +- include/bout/boutexception.hxx | 2 - include/bout/build_config.hxx | 1 + include/bout/field.hxx | 2 +- include/bout/field2d.hxx | 5 +- include/bout/field3d.hxx | 11 + include/bout/field_data.hxx | 2 - include/bout/fieldperp.hxx | 3 + include/bout/format.hxx | 16 - include/bout/generic_factory.hxx | 14 +- include/bout/globalindexer.hxx | 8 +- include/bout/index_derivs.hxx | 7 - include/bout/interpolation_xz.hxx | 5 +- include/bout/interpolation_z.hxx | 2 +- include/bout/invert_laplace.hxx | 4 +- include/bout/mask.hxx | 5 +- include/bout/mesh.hxx | 26 +- include/bout/monitor.hxx | 4 +- include/bout/msg_stack.hxx | 3 +- include/bout/options.hxx | 107 +- include/bout/options_io.hxx | 178 ++ include/bout/options_netcdf.hxx | 118 -- include/bout/optionsreader.hxx | 1 - include/bout/output.hxx | 3 +- include/bout/paralleltransform.hxx | 2 +- include/bout/physicsmodel.hxx | 11 +- include/bout/region.hxx | 9 +- include/bout/revision.hxx.in | 6 +- include/bout/solver.hxx | 9 +- include/bout/sundials_backports.hxx | 14 +- include/bout/sys/expressionparser.hxx | 13 +- include/bout/sys/generator_context.hxx | 16 +- include/bout/unused.hxx | 20 - include/bout/utils.hxx | 13 + include/bout/vector2d.hxx | 2 +- include/bout/vector3d.hxx | 5 +- include/bout/version.hxx.in | 14 +- manual/sphinx/index.rst | 1 + manual/sphinx/user_docs/adios2.rst | 45 + manual/sphinx/user_docs/advanced_install.rst | 6 +- manual/sphinx/user_docs/bout_options.rst | 109 +- manual/sphinx/user_docs/installing.rst | 4 + src/bout++.cxx | 23 +- src/field/field.cxx | 2 - src/field/field3d.cxx | 16 +- src/field/field_factory.cxx | 24 +- src/field/gen_fieldops.jinja | 20 +- src/field/generated_fieldops.cxx | 168 +- src/invert/fft_fftw.cxx | 24 +- .../laplace/impls/multigrid/multigrid_alg.cxx | 2 +- src/invert/laplace/invert_laplace.cxx | 11 +- src/invert/laplacexz/laplacexz.cxx | 5 - src/invert/parderiv/invert_parderiv.cxx | 4 - src/invert/pardiv/invert_pardiv.cxx | 4 - src/mesh/coordinates.cxx | 8 +- src/mesh/data/gridfromfile.cxx | 20 +- src/mesh/difops.cxx | 4 +- src/mesh/impls/bout/boutmesh.cxx | 47 +- src/mesh/index_derivs.cxx | 5 +- src/mesh/interpolation/hermite_spline_xz.cxx | 12 +- src/mesh/interpolation/hermite_spline_z.cxx | 7 - src/mesh/interpolation_xz.cxx | 8 - src/mesh/mesh.cxx | 114 +- src/mesh/parallel/fci.cxx | 23 +- src/mesh/parallel/fci.hxx | 2 +- src/physics/physicsmodel.cxx | 50 +- src/solver/impls/arkode/arkode.cxx | 2 +- src/solver/impls/cvode/cvode.cxx | 3 +- src/solver/impls/ida/ida.cxx | 2 +- src/solver/impls/imex-bdf2/imex-bdf2.cxx | 3 - src/solver/impls/petsc/petsc.cxx | 14 +- src/solver/impls/rkgeneric/rkscheme.cxx | 5 - src/solver/impls/slepc/slepc.cxx | 2 +- src/solver/solver.cxx | 7 +- src/sys/adios_object.cxx | 98 + src/sys/derivs.cxx | 18 +- src/sys/expressionparser.cxx | 44 + src/sys/hyprelib.cxx | 4 +- src/sys/options.cxx | 264 +-- src/sys/options/options_adios.cxx | 548 ++++++ src/sys/options/options_adios.hxx | 83 + src/sys/options/options_ini.cxx | 2 +- src/sys/options/options_io.cxx | 58 + src/sys/options/options_netcdf.cxx | 62 +- src/sys/options/options_netcdf.hxx | 84 + tests/MMS/spatial/fci/data/BOUT.inp | 1 + tests/MMS/spatial/fci/fci_mms.cxx | 2 + tests/MMS/spatial/fci/mms.py | 3 - tests/MMS/spatial/fci/runtest | 2 +- tests/MMS/time/time.cxx | 8 +- tests/integrated/CMakeLists.txt | 1 + tests/integrated/test-beuler/test_beuler.cxx | 3 - .../collect-staggered/data/BOUT.inp | 2 +- .../test-boutpp/collect/input/BOUT.inp | 2 +- .../test-boutpp/legacy-model/data/BOUT.inp | 2 +- .../test-boutpp/mms-ddz/data/BOUT.inp | 2 +- .../test-boutpp/slicing/basics.indexing.html | 1368 +++++++++++++ .../test-boutpp/slicing/basics.indexing.txt | 687 +++++++ .../test-boutpp/slicing/slicingexamples | 1 + tests/integrated/test-boutpp/slicing/test.py | 4 + .../test-griddata/test_griddata.cxx | 2 +- .../orig_test.idl.dat | Bin 2612 -> 0 bytes .../test-options-adios/CMakeLists.txt | 6 + .../test-options-adios/data/BOUT.inp | 6 + tests/integrated/test-options-adios/makefile | 6 + tests/integrated/test-options-adios/runtest | 74 + .../test-options-adios/test-options-adios.cxx | 111 ++ .../test-options-netcdf.cxx | 24 +- tests/integrated/test-solver/test_solver.cxx | 2 - .../test-twistshift.cxx | 10 +- .../test-twistshift/test-twistshift.cxx | 2 +- .../test_yupdown_weights.cxx | 2 +- tests/unit/CMakeLists.txt | 3 + tests/unit/field/test_vector2d.cxx | 4 - .../include/bout/test_generic_factory.cxx | 9 - tests/unit/include/bout/test_region.cxx | 18 + tests/unit/mesh/data/test_gridfromoptions.cxx | 13 +- tests/unit/sys/test_expressionparser.cxx | 39 +- tests/unit/sys/test_options.cxx | 79 +- tests/unit/sys/test_options_netcdf.cxx | 77 +- tests/unit/test_extras.cxx | 5 - tests/unit/test_extras.hxx | 24 +- tools/pylib/_boutpp_build/bout_options.pxd | 17 +- tools/pylib/_boutpp_build/boutcpp.pxd.jinja | 2 +- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 1 - tools/pylib/post_bout/ListDict.py | 61 - tools/pylib/post_bout/__init__.py | 95 - tools/pylib/post_bout/basic_info.py | 421 ---- tools/pylib/post_bout/grate2.py | 52 - tools/pylib/post_bout/pb_corral.py | 540 ------ tools/pylib/post_bout/pb_draw.py | 1692 ----------------- tools/pylib/post_bout/pb_nonlinear.py | 99 - tools/pylib/post_bout/pb_present.py | 213 --- tools/pylib/post_bout/read_cxx.py | 139 -- tools/pylib/post_bout/read_inp.py | 433 ----- tools/pylib/post_bout/rms.py | 55 - 159 files changed, 4839 insertions(+), 4798 deletions(-) create mode 100755 .build_adios2_for_ci.sh create mode 100644 examples/elm-pb/plot_linear.py delete mode 100644 examples/shear-alfven-wave/orig_test.idl.dat delete mode 100644 examples/uedge-benchmark/result_080917.idl delete mode 100644 examples/uedge-benchmark/ue_bmk.idl create mode 100755 include/bout/adios_object.hxx delete mode 100644 include/bout/format.hxx create mode 100644 include/bout/options_io.hxx delete mode 100644 include/bout/options_netcdf.hxx create mode 100644 manual/sphinx/user_docs/adios2.rst create mode 100644 src/sys/adios_object.cxx create mode 100644 src/sys/options/options_adios.cxx create mode 100644 src/sys/options/options_adios.hxx create mode 100644 src/sys/options/options_io.cxx create mode 100644 src/sys/options/options_netcdf.hxx create mode 100644 tests/integrated/test-boutpp/slicing/basics.indexing.html create mode 100644 tests/integrated/test-boutpp/slicing/basics.indexing.txt create mode 100644 tests/integrated/test-boutpp/slicing/slicingexamples create mode 100644 tests/integrated/test-boutpp/slicing/test.py delete mode 100644 tests/integrated/test-interchange-instability/orig_test.idl.dat create mode 100644 tests/integrated/test-options-adios/CMakeLists.txt create mode 100644 tests/integrated/test-options-adios/data/BOUT.inp create mode 100644 tests/integrated/test-options-adios/makefile create mode 100755 tests/integrated/test-options-adios/runtest create mode 100644 tests/integrated/test-options-adios/test-options-adios.cxx delete mode 100644 tools/pylib/post_bout/ListDict.py delete mode 100644 tools/pylib/post_bout/__init__.py delete mode 100644 tools/pylib/post_bout/basic_info.py delete mode 100644 tools/pylib/post_bout/grate2.py delete mode 100644 tools/pylib/post_bout/pb_corral.py delete mode 100644 tools/pylib/post_bout/pb_draw.py delete mode 100644 tools/pylib/post_bout/pb_nonlinear.py delete mode 100644 tools/pylib/post_bout/pb_present.py delete mode 100644 tools/pylib/post_bout/read_cxx.py delete mode 100644 tools/pylib/post_bout/read_inp.py delete mode 100644 tools/pylib/post_bout/rms.py diff --git a/.build_adios2_for_ci.sh b/.build_adios2_for_ci.sh new file mode 100755 index 0000000000..c6d4178884 --- /dev/null +++ b/.build_adios2_for_ci.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +set -e + +if test $BUILD_ADIOS2 ; then + if [[ ! -d $HOME/local/adios/include/adios2.h ]] || test $1 ; then + echo "****************************************" + echo "Building ADIOS2" + echo "****************************************" + + branch=${1:-release_29} + if [ ! -d adios2 ]; then + git clone -b $branch https://github.com/ornladios/ADIOS2.git adios2 --depth=1 + fi + + pushd adios2 + rm -rf build + mkdir -p build + pushd build + + cmake .. \ + -DCMAKE_INSTALL_PREFIX=$HOME/local \ + -DADIOS2_USE_MPI=ON \ + -DADIOS2_USE_Fortran=OFF \ + -DADIOS2_USE_Python=OFF \ + -DADIOS2_BUILD_EXAMPLES=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_TESTING=OFF \ + -DADIOS2_USE_SST=OFF \ + -DADIOS2_USE_MGARD=OFF \ + -DADIOS2_USE_HDF5=OFF \ + -DADIOS2_USE_BZip2=OFF \ + -DADIOS2_USE_Blosc2=OFF \ + -DADIOS2_USE_SZ=OFF \ + -DADIOS2_USE_ZFP=OFF \ + -DADIOS2_USE_DAOS=OFF \ + -DADIOS2_USE_UCX=OFF \ + -DADIOS2_USE_LIBPRESSIO=OFF \ + -DADIOS2_USE_Sodium=OFF \ + -DADIOS2_USE_ZeroMQ=OFF \ + -DADIOS2_USE_MHS=OFF \ + -DADIOS2_USE_DataMan=OFF + + make -j 4 && make install + popd + + echo "****************************************" + echo " Finished building ADIOS2" + echo "****************************************" + + else + + echo "****************************************" + echo " ADIOS2 already installed" + echo "****************************************" + fi +else + echo "****************************************" + echo " ADIOS2 not requested" + echo "****************************************" +fi diff --git a/.clang-tidy b/.clang-tidy index b52a287a8d..48a434bc14 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: 'clang-diagnostic-*,clang-analyzer-*,-*,performance-*,readability-*,bugprone-*,clang-analyzer-*,cppcoreguidelines-*,mpi-*,misc-*,-readability-magic-numbers,-cppcoreguidelines-avoid-magic-numbers,-misc-non-private-member-variables-in-classes,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-type-vararg,-clang-analyzer-optin.mpi*,-bugprone-exception-escape,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-readability-function-cognitive-complexity' +Checks: 'clang-diagnostic-*,clang-analyzer-*,-*,performance-*,readability-*,bugprone-*,clang-analyzer-*,cppcoreguidelines-*,mpi-*,misc-*,-readability-magic-numbers,-cppcoreguidelines-avoid-magic-numbers,-misc-non-private-member-variables-in-classes,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-type-vararg,-clang-analyzer-optin.mpi*,-bugprone-exception-escape,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-readability-function-cognitive-complexity,-misc-no-recursion' WarningsAsErrors: '' HeaderFilterRegex: '' AnalyzeTemporaryDtors: false diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index d546ce3af2..f50d5aeff7 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -32,7 +32,7 @@ jobs: # the unit tests until they're fixed or ignored upstream exclude: "tests/unit/*cxx" cmake_command: | - pip install cmake && \ + pip install --break-system-packages cmake && \ cmake --version && \ git config --global --add safe.directory "$GITHUB_WORKSPACE" && \ cmake . -B build -DBUILD_SHARED_LIBS=ON \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1083c5e059..c449beb4ca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,7 +39,7 @@ jobs: is_cron: - ${{ github.event_name == 'cron' }} config: - - name: "CMake, PETSc unreleased" + - name: "CMake, PETSc unreleased, ADIOS" os: ubuntu-20.04 cmake_options: "-DBUILD_SHARED_LIBS=ON -DBOUT_ENABLE_METRIC_3D=ON @@ -47,12 +47,15 @@ jobs: -DBOUT_USE_PETSC=ON -DBOUT_USE_SLEPC=ON -DBOUT_USE_SUNDIALS=ON + -DBOUT_USE_ADIOS2=ON -DBOUT_ENABLE_PYTHON=ON + -DADIOS2_ROOT=/home/runner/local/adios2 -DSUNDIALS_ROOT=/home/runner/local -DPETSC_DIR=/home/runner/local/petsc -DSLEPC_DIR=/home/runner/local/slepc" build_petsc: -petsc-main build_petsc_branch: main + build_adios2: true on_cron: true - name: "Default options, Ubuntu 20.04" @@ -201,6 +204,9 @@ jobs: - name: Build PETSc run: BUILD_PETSC=${{ matrix.config.build_petsc }} ./.build_petsc_for_ci.sh ${{ matrix.config.build_petsc_branch }} + - name: Build ADIOS2 + run: BUILD_ADIOS2=${{ matrix.config.build_adios2 }} ./.build_adios2_for_ci.sh + - name: Build BOUT++ run: UNIT_ONLY=${{ matrix.config.unit_only }} ./.ci_with_cmake.sh ${{ matrix.config.cmake_options }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 656b284b9d..d71dc470e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Breaking changes - The autotools `./configure` build system has been removed +- Parsing of booleans has changed [\#2828][https://github.com/boutproject/BOUT-dev/pull/2828] ([bendudson][https://github.com/bendudson]). + See the [manual page](https://bout-dev.readthedocs.io/en/stable/user_docs/bout_options.html#boolean-expressions) for details. ## [v5.1.0](https://github.com/boutproject/BOUT-dev/tree/v5.1.0) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7199bb376a..483672fb67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ function(bout_update_submodules) endfunction() set(BOUT_SOURCES + ./include/bout/adios_object.hxx ./include/bout/array.hxx ./include/bout/assert.hxx ./include/bout/boundary_factory.hxx @@ -114,7 +115,6 @@ set(BOUT_SOURCES ./include/bout/field_factory.hxx ./include/bout/fieldgroup.hxx ./include/bout/fieldperp.hxx - ./include/bout/format.hxx ./include/bout/fv_ops.hxx ./include/bout/generic_factory.hxx ./include/bout/globalfield.hxx @@ -146,7 +146,7 @@ set(BOUT_SOURCES ./include/bout/openmpwrap.hxx ./include/bout/operatorstencil.hxx ./include/bout/options.hxx - ./include/bout/options_netcdf.hxx + ./include/bout/options_io.hxx ./include/bout/optionsreader.hxx ./include/bout/output.hxx ./include/bout/output_bout_types.hxx @@ -325,6 +325,7 @@ set(BOUT_SOURCES ./src/solver/impls/split-rk/split-rk.cxx ./src/solver/impls/split-rk/split-rk.hxx ./src/solver/solver.cxx + ./src/sys/adios_object.cxx ./src/sys/bout_types.cxx ./src/sys/boutcomm.cxx ./src/sys/boutexception.cxx @@ -338,7 +339,11 @@ set(BOUT_SOURCES ./src/sys/options/optionparser.hxx ./src/sys/options/options_ini.cxx ./src/sys/options/options_ini.hxx + ./src/sys/options/options_io.cxx ./src/sys/options/options_netcdf.cxx + ./src/sys/options/options_netcdf.hxx + ./src/sys/options/options_adios.cxx + ./src/sys/options/options_adios.hxx ./src/sys/optionsreader.cxx ./src/sys/output.cxx ./src/sys/petsclib.cxx @@ -462,7 +467,7 @@ set_target_properties(bout++ PROPERTIES # Set some variables for the bout-config script set(CONFIG_LDFLAGS "${CONFIG_LDFLAGS} -L\$BOUT_LIB_PATH -lbout++") set(BOUT_INCLUDE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/include") -set(CONFIG_CFLAGS "${CONFIG_CFLAGS} -I\${BOUT_INCLUDE_PATH} -I${CMAKE_CURRENT_BINARY_DIR}/include ${CMAKE_CXX_FLAGS}") +set(CONFIG_CFLAGS "${CONFIG_CFLAGS} -I\${BOUT_INCLUDE_PATH} -I${CMAKE_CURRENT_BINARY_DIR}/include ${CMAKE_CXX_FLAGS} -std=c++17") target_compile_features(bout++ PUBLIC cxx_std_17) set_target_properties(bout++ PROPERTIES CXX_EXTENSIONS OFF) @@ -930,6 +935,7 @@ message(" SUNDIALS support : ${BOUT_HAS_SUNDIALS} HYPRE support : ${BOUT_HAS_HYPRE} NetCDF support : ${BOUT_HAS_NETCDF} + ADIOS support : ${BOUT_HAS_ADIOS} FFTW support : ${BOUT_HAS_FFTW} LAPACK support : ${BOUT_HAS_LAPACK} OpenMP support : ${BOUT_USE_OPENMP} diff --git a/README.md b/README.md index fd9e6931ab..5f774ad337 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,11 @@ For example, the following set of equations for magnetohydrodynamics (MHD): ![ddt_rho](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Crho%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%5Crho%20-%20%5Crho%5Cnabla%5Ccdot%5Cmathbf%7Bv%7D) + ![ddt_p](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20p%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%20p%20-%20%5Cgamma%20p%5Cnabla%5Ccdot%5Cmathbf%7Bv%7D) + ![ddt_v](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bv%7D%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%5Cmathbf%7Bv%7D%20+%20%5Cfrac%7B1%7D%7B%5Crho%7D%28-%5Cnabla%20p%20+%20%28%5Cnabla%5Ctimes%5Cmathbf%7BB%7D%29%5Ctimes%5Cmathbf%7BB%7D%29) + ![ddt_B](http://latex.codecogs.com/png.latex?%7B%7B%5Cfrac%7B%5Cpartial%20%5Cmathbf%7BB%7D%7D%7B%5Cpartial%20t%7D%7D%7D%20%3D%20%5Cnabla%5Ctimes%28%5Cmathbf%7Bv%7D%5Ctimes%5Cmathbf%7BB%7D%29) can be written simply as: @@ -43,7 +46,7 @@ The full code for this example can be found in the [orszag-tang example](examples/orszag-tang/mhd.cxx). Jointly developed by University of York (UK), LLNL, CCFE, DCU, DTU, -and other international partners. +and other international partners. See the Git logs for author details. Homepage found at [http://boutproject.github.io/](http://boutproject.github.io/) @@ -52,7 +55,6 @@ Homepage found at [http://boutproject.github.io/](http://boutproject.github.io/) * [Requirements](#requirements) * [Usage and installation](#usage-and-installation) * [Terms of use](#terms-of-use) -* [Overview of files](#overview-of-files) * [Contributing](#contributing) * [License](#license) @@ -66,18 +68,17 @@ BOUT++ needs the following: BOUT++ has the following optional dependencies: -* FFTW3 (strongly recommended!) -* OpenMP -* PETSc -* SLEPc -* ARKODE -* IDA -* CVODE +* [FFTW3](https://www.fftw.org/) (strongly recommended!) +* [SUNDIALS](https://computing.llnl.gov/projects/sundials): CVODE, IDA, ARKODE +* [PETSc](https://petsc.org) +* [ADIOS2](https://adios2.readthedocs.io/) +* [SLEPc](https://slepc.upv.es/) * LAPACK +* OpenMP * Score-p (for performance diagnostics) ## Usage and installation -Please see the [users manual](http://bout-dev.readthedocs.io) +Please see the [users manual](http://bout-dev.readthedocs.io). ## Terms of use @@ -105,58 +106,14 @@ You can convert the CITATION.cff file into a Bibtex file as follows: pip3 install --user cffconvert cffconvert -if CITATION.cff -f bibtex -of CITATION.bib -## Overview of files - -This directory contains - -* **bin** Files for setting the BOUT++ configuration -* **examples** Example models and test codes -* **externalpackages** External packages needed for installing BOUT++ -* **include** Header files used in BOUT++ -* **manual** Manuals and documentation (also [doxygen](http://www.stack.nl/~dimitri/doxygen/) documentation) -* **src** The main code directory -* **CITATION** Contains the paper citation for BOUT++ -* **LICENSE** LGPL license -* **LICENSE.GPL** GPL license -* **tools** Tools for helping with analysis, mesh generation, and data managment - - * **archiving** Routines for managing input/output files e.g. compressing data, converting formats, and managing runs - * **cyl_and_helimak_grids** IDL codes for generating cylindrical and helimak grids - * **eigensolver** Matlab routines for solving eigenmodes - * **idllib** Analysis codes in IDL. Add this to your IDL_PATH environment variable - * **line_tracing** IDL routines for line tracing of field lines - * **line_tracing_v2** Newer version of the IDL routines for line tracing of field lines - * **mathematicalib** Library for post processing using Mathematica - * **matlablib** Library for post processing using MATLAB - * **numlib** Numerical IDL routines - * **octave** Routines for post processing using octave - * **plasmalib** IDL routines for calculation of plasma parameters - * **pdb2idl** Library to read Portable Data Binary (PDB) files into IDL - * **pylib** Analysis codes in Python - - * **boutdata** Routines to simplify accessing BOUT++ output - * **boututils** Some useful routines for accessing and plotting data - * **post_bout** Routines for post processing in BOUT++ - - * **slab** IDL routine for grid generation of a slab - * **tokamak_grids** Code to generate input grids for tokamak equilibria - - * **gridgen** Grid generator in IDL. Hypnotoad GUI for converting G-EQDSK files into a flux-aligned orthogonal grid. - * **elite** Convert ELITE .eqin files into an intermediate binary file - * **gato** Convert DSKGATO files into intermediate binary format - * **all** Convert the intermediate binary file into BOUT++ input grid - * **coils** Routines for calculating the field due to external RMP coils and adding to existing equilibria - * **cyclone** Generate cyclone test cases (concentric circle "equilibrium" for local flux-surface calculations) - * **py_gridgen** Translation" into python of the corresponding IDL routines in the folder gridgen - * **shifted_circle** Produce shifted cirle equilibria input grids ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md). +See [CONTRIBUTING.md](CONTRIBUTING.md) and the [manual page](https://bout-dev.readthedocs.io/en/stable/developer_docs/contributing.html) ## License -Copyright 2010 B.D.Dudson, S.Farley, M.V.Umansky, X.Q.Xu +Copyright 2010-2024 BOUT++ contributors BOUT++ is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by @@ -171,15 +128,7 @@ GNU Lesser General Public License for more details. A copy of the LGPL license is in [LICENSE](LICENSE). Since this is based on (and refers to) the GPL, this is included in [LICENSE.GPL](LICENSE.GPL). -Some of the autoconf macros under [m4](m4) are licensed under -GPLv3. These are not necessary to either build or run BOUT++, but are -used in the creation of [configure](configure) from -[configure.ac](configure.ac), and are provided as a courtesy to -developers. You are free to substitute them with other autoconf macros -that provide equivalent functionality. - BOUT++ links by default with some GPL licensed libraries. Thus if you compile BOUT++ with any of them, BOUT++ will automatically be licensed as GPL. Thus if you want to use BOUT++ with GPL non-compatible code, make sure to compile without GPLed code. - diff --git a/bin/bout-config.in b/bin/bout-config.in index 697d3ddc71..a9045fff39 100755 --- a/bin/bout-config.in +++ b/bin/bout-config.in @@ -29,6 +29,7 @@ idlpath="@IDLCONFIGPATH@" pythonpath="@PYTHONCONFIGPATH@" has_netcdf="@BOUT_HAS_NETCDF@" +has_adios="@BOUT_HAS_ADIOS@" has_legacy_netcdf="@BOUT_HAS_LEGACY_NETCDF@" has_pnetcdf="@BOUT_HAS_PNETCDF@" has_pvode="@BOUT_HAS_PVODE@" @@ -71,6 +72,7 @@ Available values for OPTION include: --python Python path --has-netcdf NetCDF file support + --has-adios ADIOS file support --has-legacy-netcdf Legacy NetCDF file support --has-pnetcdf Parallel NetCDF file support --has-pvode PVODE solver support @@ -109,6 +111,7 @@ all() echo " --python -> $pythonpath" echo echo " --has-netcdf -> $has_netcdf" + echo " --has-adios -> $has_adios" echo " --has-legacy-netcdf -> $has_legacy_netcdf" echo " --has-pnetcdf -> $has_pnetcdf" echo " --has-pvode -> $has_pvode" @@ -197,6 +200,10 @@ while test $# -gt 0; do echo $has_netcdf ;; + --has-adios) + echo $has_adios + ;; + --has-legacy-netcdf) echo $has_legacy_netcdf ;; diff --git a/bout++Config.cmake.in b/bout++Config.cmake.in index e33e950e6f..3d824e455f 100644 --- a/bout++Config.cmake.in +++ b/bout++Config.cmake.in @@ -15,6 +15,7 @@ set(BOUT_USE_METRIC_3D @BOUT_USE_METRIC_3D@) set(BOUT_HAS_PVODE @BOUT_HAS_PVODE@) set(BOUT_HAS_NETCDF @BOUT_HAS_NETCDF@) +set(BOUT_HAS_ADIOS @BOUT_HAS_ADIOS@) set(BOUT_HAS_FFTW @BOUT_HAS_FFTW@) set(BOUT_HAS_LAPACK @BOUT_HAS_LAPACK@) set(BOUT_HAS_PETSC @BOUT_HAS_PETSC@) diff --git a/cmake/SetupBOUTThirdParty.cmake b/cmake/SetupBOUTThirdParty.cmake index 55f201bdad..e1d6f00cb4 100644 --- a/cmake/SetupBOUTThirdParty.cmake +++ b/cmake/SetupBOUTThirdParty.cmake @@ -156,6 +156,7 @@ option(BOUT_USE_NETCDF "Enable support for NetCDF output" ON) option(BOUT_DOWNLOAD_NETCDF_CXX4 "Download and build netCDF-cxx4" OFF) if (BOUT_USE_NETCDF) if (BOUT_DOWNLOAD_NETCDF_CXX4) + message(STATUS "Downloading and configuring NetCDF-cxx4") include(FetchContent) FetchContent_Declare( netcdf-cxx4 @@ -185,6 +186,44 @@ endif() message(STATUS "NetCDF support: ${BOUT_USE_NETCDF}") set(BOUT_HAS_NETCDF ${BOUT_USE_NETCDF}) +option(BOUT_USE_ADIOS "Enable support for ADIOS output" ON) +option(BOUT_DOWNLOAD_ADIOS "Download and build ADIOS2" OFF) +if (BOUT_USE_ADIOS) + if (BOUT_DOWNLOAD_ADIOS) + message(STATUS "Downloading and configuring ADIOS2") + include(FetchContent) + FetchContent_Declare( + adios2 + GIT_REPOSITORY https://github.com/ornladios/ADIOS2.git + GIT_TAG origin/master + GIT_SHALLOW 1 + ) + set(ADIOS2_USE_MPI ON CACHE BOOL "" FORCE) + set(ADIOS2_USE_Fortran OFF CACHE BOOL "" FORCE) + set(ADIOS2_USE_Python OFF CACHE BOOL "" FORCE) + set(ADIOS2_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + # Disable testing, or ADIOS will try to find or install GTEST + set(BUILD_TESTING OFF CACHE BOOL "" FORCE) + # Note: SST requires but doesn't check at configure time + set(ADIOS2_USE_SST OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(adios2) + target_link_libraries(bout++ PUBLIC adios2::cxx11_mpi) + message(STATUS "ADIOS2 done configuring") + else() + find_package(ADIOS2) + if (ADIOS2_FOUND) + ENABLE_LANGUAGE(C) + find_package(MPI REQUIRED COMPONENTS C) + target_link_libraries(bout++ PUBLIC adios2::cxx11_mpi MPI::MPI_C) + else() + set(BOUT_USE_ADIOS OFF) + endif() + endif() +endif() +message(STATUS "ADIOS support: ${BOUT_USE_ADIOS}") +set(BOUT_HAS_ADIOS ${BOUT_USE_ADIOS}) + + option(BOUT_USE_FFTW "Enable support for FFTW" ON) if (BOUT_USE_FFTW) find_package(FFTW REQUIRED) diff --git a/cmake_build_defines.hxx.in b/cmake_build_defines.hxx.in index a637dbc46a..ed6e8685f6 100644 --- a/cmake_build_defines.hxx.in +++ b/cmake_build_defines.hxx.in @@ -13,6 +13,7 @@ #cmakedefine01 BOUT_HAS_IDA #cmakedefine01 BOUT_HAS_LAPACK #cmakedefine01 BOUT_HAS_NETCDF +#cmakedefine01 BOUT_HAS_ADIOS #cmakedefine01 BOUT_HAS_PETSC #cmakedefine01 BOUT_HAS_PRETTY_FUNCTION #cmakedefine01 BOUT_HAS_PVODE diff --git a/examples/elm-pb-outerloop/data/BOUT.inp b/examples/elm-pb-outerloop/data/BOUT.inp index d06073f838..6c9f268057 100644 --- a/examples/elm-pb-outerloop/data/BOUT.inp +++ b/examples/elm-pb-outerloop/data/BOUT.inp @@ -13,9 +13,6 @@ MZ = 16 # Number of points in Z grid = "cbm18_dens8.grid_nx68ny64.nc" # Grid file -dump_format = "nc" # Dump file format. "nc" = NetCDF, "pdb" = PDB -restart_format = "nc" # Restart file format - [mesh] staggergrids = false # Use staggered grids @@ -44,9 +41,6 @@ first = C4 # Z derivatives can be done using FFT second = C4 upwind = W3 -[output] -shiftoutput = true # Put the output into field-aligned coordinates - ################################################## # FFTs diff --git a/examples/elm-pb-outerloop/elm_pb_outerloop.cxx b/examples/elm-pb-outerloop/elm_pb_outerloop.cxx index 691f4303f4..8e84901806 100644 --- a/examples/elm-pb-outerloop/elm_pb_outerloop.cxx +++ b/examples/elm-pb-outerloop/elm_pb_outerloop.cxx @@ -1576,7 +1576,17 @@ class ELMpb : public PhysicsModel { Field3D B0U = B0 * U; mesh->communicate(B0U); auto B0U_acc = FieldAccessor<>(B0U); -#endif +#else + Field3D B0phi = B0 * phi; + mesh->communicate(B0phi); + auto B0phi_acc = FieldAccessor<>(B0phi); + +#if EHALL + Field3D B0P = B0 * P; + mesh->communicate(B0 * P); + auto B0P_acc = FieldAccessor<>(B0P); +#endif // EHALL +#endif // EVOLVE_JPAR #if RELAX_J_VAC auto vac_mask_acc = FieldAccessor<>(vac_mask); @@ -1612,23 +1622,24 @@ class ELMpb : public PhysicsModel { #else // Evolve vector potential ddt(psi) - ddt(Psi_acc)[i] = - -GRAD_PARP(phi_acc) + eta_acc[i] * Jpar_acc[i] + ddt(Psi_acc)[i] = -GRAD_PARP(B0phi_acc) / B0_acc[i2d] + eta_acc[i] * Jpar_acc[i] - + EVAL_IF(EHALL, // electron parallel pressure - 0.25 * delta_i * (GRAD_PARP(P_acc) + bracket(P0_acc, Psi_acc, i))) + + EVAL_IF(EHALL, // electron parallel pressure + 0.25 * delta_i + * (GRAD_PARP(B0P_acc) / B0_acc[i2d] + + bracket(P0_acc, Psi_acc, i) * B0_acc[i2d])) - - EVAL_IF(DIAMAG_PHI0, // Equilibrium flow - bracket(phi0_acc, Psi_acc, i)) + - EVAL_IF(DIAMAG_PHI0, // Equilibrium flow + bracket(phi0_acc, Psi_acc, i) * B0_acc[i2d]) - + EVAL_IF(DIAMAG_GRAD_T, // grad_par(T_e) correction - 1.71 * dnorm * 0.5 * GRAD_PARP(P_acc) / B0_acc[i2d]) + + EVAL_IF(DIAMAG_GRAD_T, // grad_par(T_e) correction + 1.71 * dnorm * 0.5 * GRAD_PARP(P_acc) / B0_acc[i2d]) - - EVAL_IF(HYPERRESIST, // Hyper-resistivity - eta_acc[i] * hyperresist * Delp2(Jpar_acc, i)) + - EVAL_IF(HYPERRESIST, // Hyper-resistivity + eta_acc[i] * hyperresist * Delp2(Jpar_acc, i)) - - EVAL_IF(EHYPERVISCOS, // electron Hyper-viscosity - eta_acc[i] * ehyperviscos * Delp2(Jpar2_acc, i)); + - EVAL_IF(EHYPERVISCOS, // electron Hyper-viscosity + eta_acc[i] * ehyperviscos * Delp2(Jpar2_acc, i)); #endif //////////////////////////////////////////////////// diff --git a/examples/elm-pb/data/BOUT.inp b/examples/elm-pb/data/BOUT.inp index 69d8bb0976..55a4a30c06 100644 --- a/examples/elm-pb/data/BOUT.inp +++ b/examples/elm-pb/data/BOUT.inp @@ -12,21 +12,18 @@ zperiod = 15 # Fraction of a torus to simulate MZ = 16 # Number of points in Z grid = "cbm18_dens8.grid_nx68ny64.nc" # Grid file -restart_format = "nc" # Restart file format [mesh] staggergrids = false # Use staggered grids [mesh:paralleltransform] - type = shifted # Use shifted metric method ################################################## # derivative methods [mesh:ddx] - first = C4 # order of first x derivatives second = C4 # order of second x derivatives upwind = W3 # order of upwinding method W3 = Weno3 @@ -42,9 +39,6 @@ first = C4 # Z derivatives can be done using FFT second = C4 upwind = W3 -[output] -shiftoutput = true # Put the output into field-aligned coordinates - ################################################## # FFTs @@ -58,8 +52,8 @@ fft_measurement_flag = measure # If using FFTW, perform tests to determine fast [solver] # mudq, mldq, mukeep, mlkeep preconditioner options -atol = 1e-08 # absolute tolerance -rtol = 1e-05 # relative tolerance +atol = 1.0e-8 # absolute tolerance +rtol = 1.0e-5 # relative tolerance use_precon = false # Use preconditioner: User-supplied or BBD @@ -160,7 +154,7 @@ damp_t_const = 1e-2 # Damping time constant diffusion_par = -1.0 # Parallel pressure diffusion (< 0 = none) diffusion_p4 = -1e-05 # parallel hyper-viscous diffusion for pressure (< 0 = none) -diffusion_u4 = 1e-05 # parallel hyper-viscous diffusion for vorticity (< 0 = none) +diffusion_u4 = -1e-05 # parallel hyper-viscous diffusion for vorticity (< 0 = none) diffusion_a4 = -1e-05 # parallel hyper-viscous diffusion for vector potential (< 0 = none) ## heat source in pressure in watts diff --git a/examples/elm-pb/elm_pb.cxx b/examples/elm-pb/elm_pb.cxx index 6232c72d52..e81742747a 100644 --- a/examples/elm-pb/elm_pb.cxx +++ b/examples/elm-pb/elm_pb.cxx @@ -371,8 +371,9 @@ class ELMpb : public PhysicsModel { density = options["density"].doc("Number density [m^-3]").withDefault(1.0e19); - evolve_jpar = - options["evolve_jpar"].doc("If true, evolve J raher than Psi").withDefault(false); + evolve_jpar = options["evolve_jpar"] + .doc("If true, evolve J rather than Psi") + .withDefault(false); phi_constraint = options["phi_constraint"] .doc("Use solver constraint for phi?") .withDefault(false); @@ -1487,15 +1488,16 @@ class ELMpb : public PhysicsModel { } } else { // Vector potential - ddt(Psi) = -Grad_parP(phi, loc) + eta * Jpar; + ddt(Psi) = -Grad_parP(phi * B0, loc) / B0 + eta * Jpar; if (eHall) { // electron parallel pressure ddt(Psi) += 0.25 * delta_i - * (Grad_parP(P, loc) + bracket(interp_to(P0, loc), Psi, bm_mag)); + * (Grad_parP(B0 * P, loc) / B0 + + bracket(interp_to(P0, loc), Psi, bm_mag) * B0); } if (diamag_phi0) { // Equilibrium flow - ddt(Psi) -= bracket(interp_to(phi0, loc), Psi, bm_exb); + ddt(Psi) -= bracket(interp_to(phi0, loc), Psi, bm_exb) * B0; } if (withflow) { // net flow diff --git a/examples/elm-pb/plot_linear.py b/examples/elm-pb/plot_linear.py new file mode 100644 index 0000000000..42047ec5dc --- /dev/null +++ b/examples/elm-pb/plot_linear.py @@ -0,0 +1,85 @@ +# Plots an analysis of the linear growth rate +# +# Input argument is the directory containing data files. +# +# Example: +# $ python plot_linear.py data/ +# + +from boutdata import collect +import numpy as np +import matplotlib.pyplot as plt +import os +import sys + +if len(sys.argv) != 2: + raise ValueError(f"Usage: {sys.argv[0]} path") + +# Path to the data +path = sys.argv[1] + +# Read pressure at last time point, to find peak +p = collect("P", path=path, tind=-1).squeeze() +prms = np.sqrt(np.mean(p**2, axis=-1)) + +pyprof = np.amax(prms, axis=0) +yind = np.argmax(pyprof) +pxprof = prms[:, yind] +xind = np.argmax(pxprof) +print(f"Peak amplitude at x = {xind}, y = {yind}") + +# Read pressure time history at index of peak amplitude +p = collect("P", path=path, xind=xind, yind=yind).squeeze() + +# p = p[:,:-1] # Remove point in Z + +prms = np.sqrt(np.mean(p**2, axis=-1)) + +t = collect("t_array", path=path) +dt = t[1] - t[0] + +gamma = np.gradient(np.log(prms)) / dt +growth_rate = np.mean(gamma[len(gamma) // 2 :]) +growth_rate_std = np.std(gamma[len(gamma) // 2 :]) + +print(f"Mean growth rate: {growth_rate} +/- {growth_rate_std}") + +fig, axs = plt.subplots(2, 2) + +ax = axs[0, 0] +ax.plot(pyprof) +ax.set_xlabel("Y index") +ax.set_ylabel("RMS pressure") +ax.axvline(yind, linestyle="--", color="k") +ax.text(yind, 0.5 * pyprof[yind], f"y = {yind}") + +ax = axs[0, 1] +ax.plot(pxprof) +ax.set_xlabel("X index") +ax.set_ylabel("RMS pressure") +ax.axvline(xind, linestyle="--", color="k") +ax.text(xind, 0.5 * pxprof[xind], f"x = {xind}") + +ax = axs[1, 0] +ax.plot(t, prms) +ax.set_xlabel(r"Time [$\tau_A$]") +ax.set_ylabel("RMS pressure") +ax.set_yscale("log") +ax.plot(t, prms[-1] * np.exp(growth_rate * (t - t[-1])), "--k") + +ax = axs[1, 1] +ax.plot(t, gamma) +ax.set_xlabel(r"Time [$\tau_A$]") +ax.set_ylabel("Growth rate [$\omega_A$]") +ax.axhline(growth_rate, linestyle="--", color="k") +ax.text( + (t[-1] + t[0]) / 4, + growth_rate * 1.2, + rf"$\gamma = {growth_rate:.3f}\pm {growth_rate_std:.3f} \omega_A$", +) + +plt.savefig(os.path.join(path, "linear_growth.pdf")) +plt.savefig(os.path.join(path, "linear_growth.png")) + +plt.tight_layout() +plt.show() diff --git a/examples/make-script/makefile b/examples/make-script/makefile index ac5f4e96a4..b941125bce 100644 --- a/examples/make-script/makefile +++ b/examples/make-script/makefile @@ -22,11 +22,11 @@ LDFLAGS:=$(shell bout-config --libs) $(TARGET): makefile $(OBJ) @echo " Linking" $(TARGET) - @$(LD) -o $(TARGET) $(OBJ) $(LDFLAGS) + $(LD) -o $(TARGET) $(OBJ) $(LDFLAGS) %.o: %.cxx @echo " Compiling " $(@F:.o=.cxx) - @$(CXX) $(CFLAGS) -c $(@F:.o=.cxx) -o $@ + $(CXX) $(CFLAGS) -c $(@F:.o=.cxx) -o $@ .PHONY: clean clean: diff --git a/examples/performance/arithmetic/arithmetic.cxx b/examples/performance/arithmetic/arithmetic.cxx index 583c857e28..fc2357978a 100644 --- a/examples/performance/arithmetic/arithmetic.cxx +++ b/examples/performance/arithmetic/arithmetic.cxx @@ -18,6 +18,7 @@ using namespace std::chrono; SteadyClock start = steady_clock::now(); \ { __VA_ARGS__; } \ Duration diff = steady_clock::now() - start; \ + diff *= 1000 * 1000; \ elapsed.min = diff > elapsed.min ? elapsed.min : diff; \ elapsed.max = diff < elapsed.max ? elapsed.max : diff; \ elapsed.count++; \ @@ -38,6 +39,8 @@ class Arithmetic : public PhysicsModel { Field3D a = 1.0; Field3D b = 2.0; Field3D c = 3.0; + a.setRegion("RGN_ALL"); + b.setRegion("RGN_NOBNDRY"); Field3D result1, result2, result3, result4; @@ -48,7 +51,7 @@ class Arithmetic : public PhysicsModel { Durations elapsed1 = dur_init, elapsed2 = dur_init, elapsed3 = dur_init, elapsed4 = dur_init; - for (int ik = 0; ik < 1e2; ++ik) { + for (int ik = 0; ik < 1e3; ++ik) { TIMEIT(elapsed1, result1 = 2. * a + b * c;); // Using C loops @@ -77,12 +80,13 @@ class Arithmetic : public PhysicsModel { } output.enable(); - output << "TIMING\n======\n"; + output << "TIMING | minimum | mean | maximum\n" + << "----------- | ---------- | ---------- | ----------\n"; //#define PRINT(str,elapsed) output << str << elapsed.min.count()<< //elapsed.avg.count()<< elapsed.max.count() << endl; -#define PRINT(str, elapsed) \ - output.write("{:s} {:8.3g} {:8.3g} {:8.3g}\n", str, elapsed.min.count(), \ - elapsed.avg.count(), elapsed.max.count()) +#define PRINT(str, elapsed) \ + output.write("{:s} | {:7.3f} us | {:7.3f} us | {:7.3f} us\n", str, \ + elapsed.min.count(), elapsed.avg.count(), elapsed.max.count()) PRINT("Fields: ", elapsed1); PRINT("C loop: ", elapsed2); PRINT("Templates: ", elapsed3); diff --git a/examples/shear-alfven-wave/orig_test.idl.dat b/examples/shear-alfven-wave/orig_test.idl.dat deleted file mode 100644 index 4e2a7a18fa3a6752067ba7affaf9bfc8e96e70c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2436 zcmeHIO>@&Q5H++Vsb|au4n6J>N^9KCw57RqAV6l)mPy*)6?+qFu%#f$P4i#i-!S|N z{tGyCC8nE zqfW9<`$=*HXIpODzzqtRmO$)ALBpd#ygAAhF!aPb?BEHg;dpIbL}ur{Ie%W z|LUD)tQ|$+_FYF5(%Jb%?{&9BaHFvIrcXn0%`XRRoi?BRrKZ)WkT&Ox zrbbVVWGBp=q#A2e3MaKf4KdP{BbPJzYKWT{QN}djYc`gN)kG?0Bh`fT7;)0pCGkkh~JUSGFCv%*wpnLo67*#? zZYl&@5+G*yQzZ?EI1OK9Bv4EEbpOP6ABF$op&E&XoRcL&^Je8Wuue26F`a3(I~eu| zHEcDV77{pCkKN28*Et>%R1D3GbWpg_S*jY@Hy(w5pF=G$m6dgr9g2SsyT8Lz;UQ~wgg^r2u>LlcIl+PGWfGY!T{FPPX0ZU3f&#-i{x zogH+U#@M*)3%?!V(;Wm4gP>hss>Nk$UnP47=Y!rmto`@>DW<=V-uLstV#3tJ?=7GG z&$jyCHg*x`u|79xP3~2)fA3hQt?ye6)~4@mIl${6xo=*lFSv;GDwIBU~6Z`}p`XlfF diff --git a/examples/uedge-benchmark/result_080917.idl b/examples/uedge-benchmark/result_080917.idl deleted file mode 100644 index 240ee3d94e1706e13cacb12003f61e90b5a872b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 268684 zcmeFZd05Tw*FU=3?OnUw9+S+3%n6z2gv{gnwTu~}Qlye3^PJ4dlsOT}OeBTONk|ex zicm@OJf7wAeV%hY*XeVv-yi4v`E*@(d$)T}uY28V-LExmVX{af5{cv@kz^#l{_oHK z&cOf9!2ixb!>JDTrhs`qqB>{Qd5V;^W5xL zm@ad5UpCLhd8rcvcK2{|_i%RbwDVYD>g?+1=CRn$)7j0HuVFXOZJDR3r;~%}->dO) z3q0%=TbNFE@N)22>R{^Ny1?1h!NJ4Xb%CjyBOm9y^q;$!@++U@;lS5&P|ErDJZF1* z2UotUy@SU;*YkAnSiJNSn>`^T#~(^X-;8nE!Lcf8R+dU+&C+ z<~cC7`7X=sd9Ic_dpiAd%+mjc$6_*F;kL}h@pAmM)v`^grP7gYhc-Kf!DNcg0ybdwO~~FSVHOwwNXS|BEvGqg`T= zj{JYN^Kr*{yyfwY$4?&rb?*P? z*T2`|d;W9n-F&abq+QpTv~Jf(J8&&&&J~l|xReY>4JO_8M)-A4j+-BsV@^eo;FNgv z@A$u;MYB;$8a*wke@`Wi(RR|<{wB>;fixAaq_x~a+Nu2PuAND{av*8f^VnF;=XT@~ z#N#!OuRMP6_&?$M?`QtV$42s)&CflNujxQq?;E6bSWepB<)lgQBz4tlQcY<=hBm1r zEqjWJcp1`6M7V3S2YY%Zp<6f8Iyls^v7~yHO)C3Oq;d)+RX9l9^)RW!vq;lBhBT+U zlBT#JX)RWgR){Ciby zXe^r7!J(S+loUPlNnU76a;Li_cbiJ`?_0?*Fq0HM=A`rrCY3GUr`>TRsL)z};q(wW@I`K1k@|>@#@!%hYOR;vD4fXlRrI% zOsB3ql=qvFVg31JFsmoYJQXDCSxK@D(PYs5Aj#WDkfBv9DdsYsB{rmTVEOcLAoZu0 zq?t9EGV+U}IX1iZ#)xoJ zh%HR&$U`|QhUA@MNTxTF^rG64Ugvny8`Pfk;x3SWcnQf;VoBcWJQ?y_Doz|AWtf&! zr`V1-EFg7lHEEW`^LpDtT6r7N_UOp#Wfp1Y^dYTd6UNn&?cY1nj$~}@sz_55#x|%q zX?koX_3n41I{l86wui~^*atGOjv>8vhe>QoD6ia)Kkeq>i>o(c_iRPjqR}`ie}Xkr z9WmHPPpC|tRtJZosD=zyJR`m0P9&W$fh3E+lcdinlI$Bx(m8iXZ>p-ex2} zRzrq=3Q5_tm{hICk*abXsm}+Krt5yv9PlGeikh@)7hX4SNjqQ;j|jF89{gD}X+D1; z&7K3KX|a~n8|srP_$4V^X@E)loI^>^c6{!rg+@T1(%nn z;y_$7JX1HK@1gBNah!c!9MQu`R=bh(Iz1*y_ZFlpN+Iz^5s6Q{Al-rUNMd!7q}x+T zuU4OA8_$qItqU32HzviI&7_RTWZ6WJ+P0k3xf-_F^GI{PlAoDvc}WYl-2+LJ!S>-^ zd(wDcAdPkosn`7=)$Q-3yeyDnd%b(0Qc9K zBeYdI{FNHGW{pQr@9jc<;D@?6F3loYbVrh2en7g5OGzhp5$Uw~M53B#(y>n<@u1zL z>%NjCDa%Q3@kEkIUXy{}aWWKTlVXe>DV>gzYN{it8}=skC6-B(xukIzN}AoLNOSl# zX?$2-P?5UkE6ZUvsov(2GP@}$?&^_YuVgZ?xk7qLa?*{>BazGoC7-|H_strl%<6$x z2hSp^cUzpZ7=%6B;;^vF1(tEUg`6_0x;UaflkBcDNjEenU5P+CgA++)9*5f11*na= zL85JgNN4{Q5@$~$$+FR;XL5}6BmR)V*vll3enAR_Hz_+dBvpgGqBQ_tWzi`V zM76}PGeJmRqD1U3Gu;0C0cVC>$F7!lv0(ZzbX{mAWM8eju9Urv+2#%*J>Acw%epM8 z{fwH}EL6o0K$YYcswRq1?Kc9oM@N#5=_V3~1e0X^c#>BBA^j6?$e`ULlJA&7h7aD5 z;;A($4ofNyg15 z9ZOSGB={gd#0@`#^6_zVK4OhW;da{$oW4+jotIa^@%A)ymCqKkXaCFovEP~uvObgE zv3{f*d6{&kKSr&5D5|!3qO#i~R1|GRWq2N{rA2o^Olr0Gf4ULE~&K4Guru(O4gs0H}KX}w9}cAj+p^ha&n36@7JDhe%7VNrsLB{`@(k%H>o`$%LsfOMF5=r-#~ zlIUHeXFHYj(`?9K=opf(=tqWYhO+*9lH!{kDXklk(m9Zn(}PKAq$b7QdSv)Ek>rWj z$zaE3($5YfX~qxIb#WlkoB=2wvjTb6VfbNZjSmaE;dyIYgwM0WY2(}2In*1Dmi^G> zagLDP*|QD~m7^0G4tzzjcD!F$?I!U#C5gOPH#R1qvVA(rzqdvCxjv|vO{n~7jhY5* zSLPRx&X0AZyXrhi>v^&)ZAm6$J2tMGh6P=i zAFzIpc|`^aB7LvNBwbZOx-t(ENv5GJ#2mTT7a%?T06yqMAm;UQgtv-BP{tbUY(Er^ zCMoC=`B2C<_p6H|?kO2&z93oocG7FQhIBm-lTNF)s9D_xmDegzUbqeAgUnFAJ|7h) z#i&{)M$N|$BzludVyq_JGzXF{7)yHZ*O08uH!>KthvfakNnW;qWpjxPpEK`CK1hbw zE|B5a0FsA%B7+BPzjq6yFNq~-{YbVS4^g{tAj(YlGp9O%^zEzh{%#RsPPIk2tTTdA zzF}vlU2rr{N0)c|h3ttTb#SQL=aItKoeci2k%DxR3>!~6A1J5pX-r#eZlJxo^(wDX%S-10K&{ZIVDkGBnOG%!q zLxx7^23{greU^~k*!A)Zoa=ptrP4ykiMd!8$FE3IrlyeLmOs27&LO=? zgGl$$Hqvop9OriR_X6vuh;*iAlK4s?=~n+C zY0pB^8{eAr2b+>i_cF;2hLEgyEEzOwLk4n|)g4cgwdH+u_yW>Xnvld*%zE|$wSqUw zf@UCZni+mtRN!NJ2fXOf6cN8BAo#N{_9zRmsEqXHLVb?N4#n(F^A%yn=r}G2FM$MehysgrXBW z>*8q3+|40?Idl;j6t5xuWfw`3x1Gek^GRguifVZrD%u}GnVASBH4{*BsS2gdLs8ai zAj;nn^EY$8Ck9NA!TVi;=6pnePw5aeu=@pgVFGaXo$Xg zB|=GO)Wy;65UJezl48LOlCyuLf4+jG=58c@{D?$*{ZT!wJ1WKvKv{!PD2eQb63Y;j zEc%Yp3A}IJ?~00JT2wu$Ms0Jp4Ud^m7C$H5hU|-(9U!UpB}qS~ke+vE(#tqU`fZ=F zysJsC^cU%^_8{q1Kay-&MB-mPNffmLl`niyv}zfCcWaEV`>){5*aAdrd*A}|s{Mmo zVCm^W=)2EMC^DN zRf|x3ZzYN!%|(f47E0MyE_YF((lZLxH}senA7sAAdMl)o?!5XWAtGtpS0sHC$o69` z>78O<-)*Rx!+Tk9DT!7xA2_;-#1|KkZtzEvtba|? zJ`G9wOhJ0>d9R&Hq&K!F>v#l7XR}S2J($Glok^5%4^@WKP;|}&zniJ?wUIC4C&_Wo ze=p7_Si-+u8r*`C&}(LqP>?sEt~^@$k;==D_w@TDw`04aW*;-uk95b_k&f~Z?{{la zabZ77t0gEgi9%7OfTB|dC@O7_;vf4^I;j`Ro0*}~`~<4)nO|HyL!wek5*r4PuHh}x z&9f%S4UU6MTujmjCM2zOWFMg(zh@t`V?0UBJCXPT??YSTP}!$G3fFidb6ZDzF}jSG zUt1&cjXBQdj>MkX(OCF&AuPil3pqJOb#b&eCe`K#qzJTR?!BF4{aD_g7Lo4SVWcy# zH)`hfL&cXklo^JhxM~xMZtg-+moq4`ZH;2HwoutYnU6jjO!6>ej$w@>y~duT zdrwR{-u|e0l7vdL61Kb5C@v{Q(dJYXChDW`eLCx835w6fq1387$~)Mja%2{&x9nqo zIGz157nX^Z#I+qsH!+g!YYs{Evq*|xB%NzT(#9DiSu&U9-jZ~-GmmbhLFvr3$T?7p z@7scqq#lc>M#;EwtP4)Oa>LdXC1$tWhz`qQgde(9b#aXULaG-Aq}1t8^78j2dpL~r zT;`KR-JH2oIBIR$q0+}5Wea^!BGW;Uvn>jDjzgj26cnZhpeUDRve*}8*8Nc-tU=Y* z1k}WgWjorKbasc5c&nCl?R-dLVn&jq&q$J?$Na^TB!^2$_rn1a@4iE#;~}VgmWrbF z5AeHVCB7aE!JA9@c;Fg<%YX85gyOO3^KDGsF%4#SuM4RL|FVC$%SkQe_}T;!8IlJX zv}HdpQB4xht0Zn2iQ0_MsM5Hrj;Vq2LS zfRat^QRcdeZQC?d-Ht&`=5i8k(jlES^+-IONLRU%ba$C@d`E|LuN#nVmp~GCI7On( zZBaFJCyL|z@n>lbzBf9Cq>Jkj)BPvHA4MT}?RxCp8xC)~ju_J+2Tf{^2uYcb>#nP3 z%Sjy=MyjUiq_Fu-^3Lo>e3z4S?H1B)tRYdnDXKlJP@a&4l9X%|gk&vqzS-x~$1 zLQ!a34@KQnC=O3X>8|%E4;YNfdt*_Z!8k+blwz@c>PuqmuN`0ReuuaC6jpc zJJK<+M~!|fkh)^k`gJq9^y@+1dsaM#`Sjz2zhV z92*Pc`u@VJ$h!7Q+bM!$-|a}P-NiA1i)859kqo*WCcPJIPo^~`oq~a=b#_3d%MO%j zhNIYJG75L}MZw~yC@^?|g7MKPY&!%+UdAYC#db~86BVu6pz5#9wpmXiO)7~#nv#x- zE$Q5zM>^-%kWPEvV_OVFRi-t{+S(vL{w6YCOOa|a4~Z8Pcy_cKB6PeFQsIh2XDzU$ z-C4{ZF#!F1#n5c~TDyOQ&0qe;DMDJg6BkiujD8GO%RUU!o1?6^8AcHK^pwvwfd=ZDPA8siHDm_5T^3MnMu~z z=cJDf?&mSf#0EX@I6>KAz3}XGYF!-LIL{}wA&sOnsrJqzMN|WlAG9V}?|USTtR-D5 z-fL4I@-y{B`2|AB;eIF@!Fqlt3;DS%k)ITZf>|?A==_!Sy)#Nq$x*g59u*tiQ8jll zYIGHByZe(!r-noZLr^>79;%mDpkn1Ml$eh}-Vz|Q&vm4(*@>j#$%sAG2X{|f;fljb zoUA#IJzG++nsb~}w`HN5mmcKSvBKl)Wp!~}yGPo|4y5tmTxVm>sm*LihC>TTmVJ}- z+|QEkpV7QG??TNcLsa-3Lg|QL6h(?qP}m3g*$K!$wvO@Gq9E=%iW=IX`1){^t}sUV zD0fuUwnEj?X{cVueoBQ6sv{nwYCm)2x$Y>bNJD{*53;-bM*3xQe0pesH(odK$daOg=sXox|2bKcYr>*e0wlj<+93d*Cf;64#SD;07sk*;miB zB!i=GIc6}FBu5NMY`Pt_rzW9tLr0W}-=ny{KMH&GKz{yI#!B$}6qs@3e@#tmRwe*%TuSvT}Nkl%-KsQr+?Oo4)N z<|tgQMbYbvDCznMrAr&2Y|#sj8ErsWQUywj8n7*BgTg_tk=u0&GJkx+_o=4%w7C`u z-YXGfbpuhkF1WhCA5ITS$Nq{j*z#sN7M*^D(amDeZp2RLy4VVLliJt8VI)Z=ZB<*& z>v68dIe=8TYe=cyMDj0ef0qQ2G}D54#cmRnJV5n^B`6PWgA$LfC@kHKg5KW9@8pTR zM|sH0nveWV!6<#QV;!3Y4^VLQ!@{6g(M?Tt6TD?qh);S8S0| z5RLcxbMdlVgh%hs;C7fEE*>3)lZP*0?=4$wEPD>e_kOS$oD8#Bub{J1E<}DeuZyEz zITajsCwdshx>1JY3G9fHz= zwJ2_wg@XJO$oIaEyg)zX2{)1FRfN2ZAeM_G3Yt};VAnwuoOz6bK1+~qIs&=9RwH{> z2{P9G!FRtF__E_NlDyaA)x52EGQAxlU7F%*h!=wEdm$ja06Y8)v1(utX1#rkL6xa! z{yjmc49pX5_wlVOk5*^MXyho;mPT{_IE&Pk{YW)qIVolxB6&m3*#_H@)U++fG}zA_ z|D5*`50pg`iX(=h(0)4dBkm#Z;t}Mvdw@K3Hu8oxMBdTy$jiBmd~H7R-V4YxUV@yU z9mt-&6qy!R@k5k{uP-m-<3U#>*sn(H@P~LXDhc816u6lC4yOW#;K1I;@O`iiUbgEo zJ$x1I3L%6ALTo*^H{bWRY$*392W5qPq^@XI$WB%KSW6Fg` zNyhuRUK93(PPQSP{xzu4^yWCwE|k1Kgrc@DQBc1P@(z~qv!^4c+88-!-XX{NDsoy~ z!k^%DWIyx8@40r!99My#9!K#lQjC-)p7`MJhJ>~q5c_Q}9==P&9i=C(?t6-}u7hy& z=tuZ9T8mAo`B?m=IVNx0iJqUG&`@M56f8^^ZUn~F#nECca7r*=oGKd+~i1$ggojyE6Y;>9#?JZ`cb zQC$|`#)%w+E~$sWbFXo5uoZS#cE;KQS2&s;#F#=GbX_M$z3lNqc5abyHLWh+HL4#) zM&ljGsB{Ij6NazVk7)sdjeStERZ=k5Wmun;%A$C_?{(3n)WNc_*mm(KQAPW#I7><>LcfzhJ0VAC+qVtj2(B9f8WEMmT zSNX`~s%_UaQEnO`UEtt8TXU&!?<8uqJwf^_iaPurqTVLaV=@@&g6Ou){poPq#AdUV~o8y2eXd!OE^xm&Wv<* z^+^;^g{qKEDD&|^(Ps~c!}ig zMo8k?f;Z1sBhK{$o_f5*{gmCf9n}xlj7<^pTZF(NgK(&x7`vto#3q9taPJ?D>1{^B z>YXXt&5ea3H&FP#Y_)K)eqDZLB$-1-{hP9$2a@)|aOUurNS!l+R4b>D;&K?t4=p2^ z&0Ufvl#nhgNR-wX)s1yg_OcQ4jAZ1mJA<6{tkY>SWbQD;FS|ajupJczS4WxzVr^(&*_du z4iAA3P8isHHO$6mK)!d7klHU=2=$QEk%zWAos29K$jFrI4j*#OrFI5s^tt}vR4OUb zUz0qU>reJIB|Wn)qMFHbOe1*Jw;0=SI8z+2w&RA3nAZ87e`4J8Fg4dMvXj48_ji~jkxBm z$tY4i;@T+VCuC?+OR|rb*%x_9x&?1ZXW4GlH0+P^?MW!6NaSzeJwx6HSw9-%S4}W} z3_OW%TR!7!#8Z5}YmAScUU(Oqgf~~C@UqDY#OSZUL(dFEEx3-Gob$h&&vh`nLvb=H z5r@3h*nMXUwyd6mC=___{c9$1zX5L`Lc~ z(nficw%KCRbi7BZ)Rv?i)Q)q-k+6 z6IqYBF67F6q~AS@v;^j{Z`e;g@d_VWS|f2-5?)uX#*6-+@lTNuIP#PA z+YiA#d*S%}EF5TKgq=%SH%8cDY2Wpj{q-0|Z5oU2mJ&4O8WD-ZnJ9J!?F`GQoXy+~vCJl`6C$L%`do)C}Q zMl*5E$rz#2EfMr<1CAQ0;s0(iwwoWp2JIxc);eKE)=CWj6@;$R#%MIt3u0xCkmz3| zoY``*E{-p3t0X;1TfCRK`!cRa=J?5g+oU#lPRezsc`tlI25aY&e(PN%IoF5895b#R z*ccTqWhnms1o@Z5_!EPun=FxPbq$}>Yw(c{vydE;#pN3Ibhy5OBDL?X3s4J|BxUJ~AvkZjLF5A7E`c7Uqv#U~<|? zs7(tL-dvh1oHo2&7e@m7wS|SGP3PLkW0Of+k8AG+@8P;ITT&irNrumYI1lbX`jbsb zl2Aor>)xn!YKRJtA1J;nN4`*lKYiaJ({VI@#FXOO=pdxThvU=Wm3Y6V6W%ThrP`N*YV^`6_sU2Z;aXjN1&a{1`eVsts?ej=0YfPGfA4%QnASthM9yc?Q z4Bm-34z!gdwF^k>I0&`=TTrp|CW^O}bA8TlWN*_!hWiYp?|g~W=Uwqd@f05iI^&)7 zMZD3|N8Iwmc(%(14~MuTYJXeY;#l@o)5kbJUx}bei*Y1-Irdpz!;Y2>urZADXRm8u zw|p|j&*_DJeimrkI1E~oAfa^lZQ;dpg%D^stuBr`M@ajU>s4=`Agw3YbCxyWI%+l7 z>jseWVHPQ5%(1FZlKz!GB(=Cp;yuY+5A+rlTR)?CQ33L{Z^!Sf75K$_!uRQY@O5-6 zd>XF7d&{OssJP8JS3Nv`qr&5-8r+``M1*w3je}xb>XL-B-d;Gl>-d56Ct?aAI; zqxzM3NgioBaZOv>-=ut1MT(Z)N#624>3^EQd9W5Fj^cc|JPH-p!Ldad@@N`GA^t~m#0~PmvuUAt*wF(~oZq{ZmW``Vk0W%}51c;liesTt z9GGK(T~BXdOUgm48d-ydof~4>qRFt4twwis9$M_13&T*6kdxX+c&cz0j+_3AJOUdt z&*l2s&0N!M#W9n(z<=rr)h#=a@?8&7bm#hA))$%1CC*D4b8eO6&s{d5BK<6iSA9hu z$4h>jjmJ-`c%*F|hR<0Z_%MAo-ab&^wYno>E&cGMxi9X=0(ZKOM%eh{xNKH~b7A%f ztPaHyQ8oPUF2Ig%?XhV_8+a{V3x^$70i(9C);C5MuMjl#xB~<2HzDgwy6|Y?Na1MI znY!}u=Xl8uORm}G+FLVU(%hK9@gEOTH#kGeq?e?y;@Z@yGf37ln537ElCDBUq6uA4 zsgC0~=xgLPOF~xiH2jFJKx$kLJ{8p9y_pRXCdA{VgDGMVg-7)?xEs(I;VBz&E!_?m z&Ko1Rvk6YP-@~D$pWxSX6nt+?!usr{SQ1qMyLt(jkfy<)PCsF;BSRyPLC`y3FZ|LE z5TdUa2}hjD>f-P*;M#5W&&P3}fPN-v0;)(|n98-1XIUPcS06o<6WP6w# zbAe=_x$u4A8R6c*Gs5AHP3z)VkxAOQ;F|l5r1^c5G;28@lCpqQHHM^ovw##63rM~) zmt-&}>8}FP9X^RfYYw7vs3D5iWg$1l4p|)&kbbEvzK(B!WN8r+*=D_FejD2_1CL+D z;eP)zM63$HjcpoSnzse#^fd_dI*P+rzrp`zN9@?Bi_P_K!uyLWoEyHvw3N0O+1&!Y zO;@AMjn$}E9SCtfTOrltj1XBV69S|Gb#b`HkajHhmNY&`n)vgiv0p>#m}?x5vnSR{PR=zUb zh?rm_JTemF?rg53+8B>(t9IgoO;?;rTZYWY}B`a=Qme4j0^=? z&%>}J4SFcb(5ff}+LLV|Y7r!SY2YbD6b=&(*6y#1!=8H@`i~^7F88g3`;cbL4^rPa z#r(>jlut&HVrm)5_ZyIG)j+O6<~;I}W1N>{-*#$Ot_kRY+_8O;Ims5^7k|Q+GxzYm z;27&h0^+VE;hEAM52i*Va%%x@9&*Q(4Nf>e)Eq&{uW@XIF%I~g!R|x9u+=*g>)Rz_ ziA@yjl1eaHyd6X0Z=>6ol`vgj1NFwuLe!Wx` zJaV!xAmgA9zKx&7F{#N&5}V+4)iK0MJK%A>ew-gG!|i5maor#Z7gB~I_@F6Hc7BL} zphno6XN?{8w_vk+0#=0EV`1S+%uH;Iv9p?Ez#&tZ@9Kf3x(>c3dth4Hq3q-=|1Lo z+|S=!o7@@2_RW!#mxBz?B7D=~xcSKsct;)a+H^c(s@CG+w>yY>F%P#c$Ka|@BZQ8z z#p&|fIKJ~84vDhiH+ea>tsRAp8}`8~EDBEQ+n8QI8lzsYt?F|S9i45_c+pPCCv+8x zMwo+f`i3bBEihtqEPDT*i1t(5 z(eSW8WRGHn{O8_6LW4lz`kraRzOnzp6Gw9YlHM!s5gJJv$19w><*|4@sje*}ka#{2ubMx{v*7J` z(6<72-nT&5@~*gS9DsA73Y@Zhfg_HW}7Vm(ksQJj^C$qkjFfkiKXrWLJF0*blMw+au3u2$qVzg~=k~*ia5)0>)?x1$CAM$qg-yZX zSg~j#7R|baS*8s!{<9!0Y}DI6d8r8RrgStlc0CEb59b>>HZiHiEYFGITo!3K_BP!V4i)xU9S_ z>{a#o7kTI~jwsHdN0Vl%IrpbdC-r=eUtPb+`}uoPoSI99?HiH7v}n@r#r#?k0}Pk|OTDbPecgt!BQUlvz| z*w$gfr4b6j@6x~Q=fm07)^g11+9<|xf#uPjd+M6Ay*ft9w+d2RG9|;l^T@#6ko3pj zA<28LH8L{f`~lY|?m3J+j-O=my7{);1)sY4;O#Fx#GL}3IF7=-J|zfmBF9xl2+k`< zA*e|)j`o&ezwJcq^38xx#7L}5D1t}F?XX`m5>o;@V&rve^gHtb=H78=-YpdBq#n?5 z^A&z-Y=!4j8VVPmm&Y%)0g#QEorW^-JQ-oKsjSb?KG2AksN3E)P@uhCfslQ zjSRNfkiJuYk`!?r9{X}Nqsq7@c`@?7)I(;3GrqYD!6%s)-X5z)T#q|={Js_L`hLc( zajkHrjV(fq^l{4QH;%Nui+y9JVdokjY>8-xH3ji-_b|b{AFn{e0x@E%2l}2rfli^m zXtwhz)KjdXqiHYv*e(-dYNLgV2dxCZIsd{FM;MV-C!aK7+-o+IW1{&>NbT~J`*peR zHr983D^xQeu?FWVTI*PC*Dp&ycC9PoBP4PJz; zz~kOoh95hnPp8&+2yBTASf?k3GG-a}?{4{b5` zmw9kp^!Xi9=5RmS+c+|GnL-9vRHT0>fTV4>o^VbgYL0R~yxbOf`6b84tVm$&JpJ4ClGuU1}5u5F{Vb%F>a6M!K zyOm*pjTkn6jL~<&WOTYV1XEd=E6NY;Ur$s3^h+jqO5)r@+zH? z>F!>%n=j3y%XL8)C%X59rrz3d}iw)jY}`noBbvUg9KVbTAiQ*jyDZPYMwB&iEI3 zjN<&J9&`4{7u>(Nk^2>S4_U{2_Y-py{UA~l?IXhjfn<<6i0d2JN1wBi#D~mKQ@Ivp zgKi<;C>@#mHzVz<1j+I|By^5K?6fjGrGs=@<4D01x^fkz&^)&_?4`~ zcAquaY!QJ~&mDPP*<;=j73l3fj7*w`{s&B9!S>Cx`w18|xChiNI!1_NjGyo zC#n@{hm1nm$`a&TT|wrRGNk>8!pEx7NRVeC)+`SXr$^#WSOjhi9)$}vE;v&(5+_U} zacF%X>{X;;`@NRfyv-J?9~Z!VMRVBK55m-qcQE?mMGV|L1znjpw=z?qUha5ET}KLk zW=VwCtG$J5!Uw^hd2gNle2kR8pMm*qbO86tGj}&wLF(Py`c4W9S%iZ#a^dA*wNn@TRNCy?Wq0m81xv9^~IQe%>!er{V@0!_p2;u3^VU} zFfnz9{z4NW@0qEP&}X7>!}_+cuj1cu%;ui!IMO`a#C%sy8r3yYAB-SX^%GL{DkEi& zF=Y5Ff_q4tlWf=tl78Dtx-Exrp71=%t#hk~H;PNqu4& z$EmN9Y9xQZ!PsDqDRTXfEB7^dZR4Ic?oqRQPNK1QQT}EC3Vfb$+(#eZekURM(^w=t z=321mSoS@bv-dT_jd%$zUTTQogdR9KunGZLd*J_Cg`Hn5uvO6%8-}-qSAGvH>Tm*c zjQla#PlDk)uA_Ik1v9B|t9U@77n0tgSHY2^6oXqNa(G&{W$DzoiEm9kGKQ(s8_Im>56I7$6}04ECy~pi!SwFp+)gzXe_OT+B-Xi6m5|Zx!Xku z7~ilij#0dy%e+`Wcx`zycW=VI*H^d}w%u=1dH*5h{xzg<5s^GUntRRcN$;CENrwOJ z9pwJoJgyVH8-qW8^RpHK_|jk<5=&M+XY+1E3x)P2$qgHfd#WHFym`ej0<Plw;N_DTJV}ekz5bnWD>W2XKF-Ja zcC8Tf%p1q9TjJoS0oZHF+~o9E_%!ywdai?5wt{1DtKDJylj|@}#$(8l5LmvQhSn1< zqh8a?5RXv_X&+Y$_uB6l4xdrf#W5_G`4#Vjci0bE%zj7%=80E-k=jy3sy+4CCp=1u z;8ZfSW{$O`FX=lRCP^CC;Z_dgy3Zn%%=JW0{5t&1Rv_j5U?hdT$E(xF@bs<;?i(LN zctj|!-Uvjfq&rTB?8Nay9MgUL5dO`3VCSBL*eZ6$hB+Itye0cml75&y(gzdYuZQ)y z@#uc5AF#t8(>s8vh61gG!@GG_-zJ`b4{r($x zy+MU%hbH6x@5{Koy9=)Q%tPqA1e{sX0w)}IAiz%v|L@(gYxI6>z0w#P%2&g?T?H1s zFT!k=$D|<{u>N)n-DB6oOzeVs0ih5d7YW}6`3d*tG!+g<7}v!yXa#9X^+|Kdo&9r; zo5-0fhJe2tCz4bF$?Su$e!R)#K1uG&OJf|LxJP-(01^kCMNQ5(l$PWmZ%sdBoNb8I zgD>%c$}Q z;l0`zi+;Ld_O*0OjQ51K)m(Jf>xb5zJHqG@*Cl@JD5RD)6z;mO77q8>Sri7z?&P~RI%^FxeHbFNxU$ork2<;$mh}NDEQlc9Qk;&79fVuzT zNBv)Hy=hp^TloHcH&1tigxH1&)&Bh)= zGuV#%gEv^aA0gCPH!7`T_R)Q08-@@J*JPAWwy4uqJ-&GeARM7t}1D$NeZ9 zl#E+}fV*TrzFFu?X7 z7qRYKE0#WWz`PAQn7r>CM)ZhApV{l7*~^`NnZ?tqo91-$Y9@tDcJ8e^R69e#mvMXNoEyXjX3_OxH;od)kaVskrNrzf-A^s!c6j%#$d@jQ99KrpM;y`^k z_Q!XH+juFq4Qs;MZ8}&y%?Gnj0uuwRV5nb!o|~#5-D^uNmr|)%T1+>3WYW>MdL7p> zT$US1F zLjGs8wrSzpy?&_eu?!W1Oi-%14$m4nCv1#HX8&TO9-fVBGbiHWWPS^sJP}oL7hzT* zIMUFF11@p!ZrB4?vpd*wcNA>Gld-UCJpLIs8{@PJV30Tp-3O#YWp5*Wb9bU=)ly0t zkx0Rd|By%NbN?or<6TDsp`{xoH1DinhSLk7@$EnMc`g^~IX^fDc_-A$!kNEtjO)Cb zLh&fSg=$hE)0^MI#Aq}gn2Xx)_NY{^Kxvy4g>T)FTM~rKwyC%+{BW(7d;V$Oh`aBD zsK$R0HsLpp_>4!uBfzT(*xT-aO?OMM$|L{_Y_c$I3HLq*Wk8?%o?Q-bPi?XTeXjSR zC(dnjnfp11Q~q#Y^NCEpE^_X1hVQDcLeqST&?xOEG?p^^CM88kKiv>&ZQ_|2Zo*Ft{kBj(MpcX81Je4k^A&-d4$vgSX$E@Q3OJxe^k+K&786_FUjnsPoy(S`TLbf{Bs$M33}A=j=rf10l+G{1Bfnn6Nn4&yxP?i`^pRZpnL zuurE{mcOfyLe=*Q$5=^1u{2f4JN_qRqJr6rl8x`*%2Bs37gdRYC_DQZMgC>T+dLdu z8v~IR*bmoVRN>OBC5S8Sizu&;I6mPf4%H>WFXTHsjO4Myt{8Un+OfEY5oW!%#e}oz z7<$MIJx@D9{nHg{FE^%ltDPu|^VxGZLg}FWAKr5<)`*_g9yYXgj zAzm1)L0)?=WR*7}?e%xlkpclG zm|>C+6IB}w>Xrdr`$VW}n@~%)d@4=Mrn?mqii?^{2MQ89?uUvxGoOwNErU>@na=ms zV%F+^Pv5%0C$`&#onHv_rl(oD|@MUin zKBNxEneb3?l>4dr&E~2DC#qLPDQ|52PrMGdcldC57vA_=;f3K` z#F2$o@K-d1`+aL{8#Ntkv|J#@1YyRNxiC4Fgh5{iqWdFPNDV!yeP}sVsu)u4y9&A% z6HJj~+Q`?XCUy)M+fh6(A1|AcfP^RA3n3so8R zO}Qv@E_Ff3FR2tVi68OnpBVN>zQ-5M?f5WQ17(%hQDkj~$2VG#+3L;ws(rZXJP7A9 z)DW%fkK-;f2x|NeAJ=Da9k>RYUL<4XWeNVR-U2i3VUN5x4*eJU@O_&LWlLvjwvVDW z)7vO}a4KDqucq+ZQRFl65BpKe@%lzL>B?4!hh1ad?*p*<7^<$@E$v|Vw-k*kv>$v__^;+Pr>7OJ-3&v*YG<4t5QH%0dK}U@0-yOw*tfG04i5{kBBTNH zzeq9Vr4%DqEJ1%S?g=i4g~|hY`k9eSl@k)_NfqeU6?=;Jv7=)arR3RE+;JT*SFmoZ zfNMSf2~E{zp|N`d^V=Mm^~4&Plx!hYVm{;Ozl7>=Z+<&Y3MC3;9Tt1P+J~}!X*hnc z#;PGwf|{5xywmZ(OKmeexzm8GN%csLdyT7|vvKyw97K&y#IfHin2)9eFOw12xnu?G zo}|L^`CQD}Isp?BQ($<+9)0>~L3`m$D2}~O-`%D3hVQ0FYceUhvpk)7WJSTw@#LxW zhx74lI_t(Pg{J>Rem9s6up~^VXP#z;o3oJeIahmREL7{cA61blltZrz#n!Vz{%3}e z4Vr@AYx2QPR2*0{wTMXWgXlGJdWeK&*xgC^uLP4RkIPBVuq8GY;mM%3jAst z;clRUt(Gyc{<$4Axdqb=5-=u}>u&9-=<&r28V5BX`%0I-M7mPR<9K>7!jqCN2h-`v z+~d5PNgjhkJMPD0j&oVhpy|9_XuPl&8skfadiV_?{kd95|CI{0K-R-0EMrE`OriYG zAfb4QJ%z_v7uWA^v~OhZ@I@KacX7mr+r}unUyA1ohvHE%_s-8O$BnZgxNz$UPJdmC zuqB6aNG=omzdOQZLoGHQkz%>6KIXAEWJ>Qwj8rj1|M(v0+IkMEza8oK>HwP2!?#$D3N)yst_8aI12sMi= zp=$S!P+?}0()V_uXx=X5$1y+V#uEJ6TaBiO5vVJBhpHu;@p_aKo*i6>hsT%U&WI93O;)zeM1;ZtxoL5zfVJu^=1CcSTrUVqxPl^ z-hZ5o5_>u1Z?r&GcU#;#@dKB8O89JCNBChU9Ok~c@7P6f?P(8(;@Md7UmE70Nyn6@ zX&Cv`6Z(iClsedtK!kG<$2p#QZX%@WszQx@A*ze`n-YA+2L=npn=V4WmAMTs zSlc&c4_X(n_pVJAPM}d{~c|$~=I-gvJbJ1@4-_J@ZFG?X^7XG17%qb_Ub+{ob;~VPKt?xP+!!Jx|_Aw8f`od9{|e)R3RDZN$dNl$|8>CRG9 zO8n1`qF3tC(Jh7l8zb&%(dxL4^LawEh-=fb{C zDJL3dBUd1T`6Ndg{)TVDJM7EW!^RntvGjF6%sJ!$)8(@k2yKJb0uAuf= zd#L8S6}{}HMcGFS>E>c{IzOwFBFKmi+em2trgYl%?ho(BnI}T?fws{25Fs=acMJ7? zB|MgFLT44yvvK&yz znz0-y=j6>7aW(M?;y0+`ByGXrz+vz)S&cpE#;o}^fn~Zr%y-Sj_=S2HI!hV7XJkTq zl0B64dee_v$yAk}OwXET%bluyO;y;;E_yxgtG`STz4 z;}pLgw{8oKr;mjCdn1lG7D|A z4o$u0q3%T>s@&`Gdb9?fsg@(#bShHY$KtY|F=D%#A*|p44pyna%Rvu2WHey2pa2WX z{4n*57Dh)N#(=vK(Ea%i>XVEiyETZub}y!~24~8fpG^L9cbJDqFm+#@I+C)A$X@)_qi{n{|0e2Ce43z=2bQY_@xxeD3f z0wJ-TfuD!Z;rp`TsOw^e%G3`iU9%SjBc1Vpw2>;i9*Mh#Am-}@91q)t!0=Rfc4fZw zdP`n#xirWr6;{I{GVDCzvG>B>7w@@iXPEM z$65?2ATO9a&Pr&9>L1Q|B-DrCp{3yC7Ly`F}l@x(NIresvK%*U&+o_M-co@-WqxTX3SmmJz~x*!FicJFXt zNfz86+`{GqsaTO?f_V$r6F4vt!>8y%@4zwW$Y?>?JAm4BY^b_EfL^#*(u2?>x~Z2> z=YJ$qlyVy#9p97ucNUZT-FVvGJF(-v8##(|sJ=oYWf*g_V}J+dpdt>N%MfBEiLZ|QwC{FdG=I72-DU(aj zD)&(4l5uq1%$UyZETogQtaWt@BEKPywvGA2IgeD}z9z@HN&AHQ#alvpa=TD- z-NNsN#-}2Cyx%Q{k}l48az754 zY29&id?+sL$j7N9M;sfl9DYrE;W|qT_8a7}_;4)DyJll-^AHRk5|6)5WkPdS9^^HQ z=-ZV{dTTh2@~xBU?(j^yx+#X@lyvFD%Xm6ephZ6A+2s1mlAJ8e{;(h2xsT={G%oS^ zIMYu^5AZp+n3y$rrDufoN29M(%bvFNV~%m_EY7}t0Vyc3J= z%rH||Zi7sXBYnBhLa#ULQEsz5rSbcbcqyJ@PF2(K0$mCk%5TR>3v%tNM_V20|F8}l zp_#B-Xq+$P{E71pmrp`%K@XwYl{M6l426;d^U!`VAJE~2kiD^1NV0tKtJ4rPKXJp? zs7BP-ws1bn9Of%#$Xj*^_oiRLjWaelpPq@xA`b-Xc`-|6GWICRVr^0~aJLH6OdT=0 zzZvv5w4$3r1XN$u((lXW^ywk!Wq}htjOj_YbVBK(j3-6UaiWkSdkQ>YK;BmI5oXc12{545&NGH$IkDHSmS4g z1^Xw%tW_IEZzrSQ$PDPbEP`^T4gE|?rfQ{BDq2uTS@v@&d9V|mU#mw^3ifoA`yK&& z-o1t<(w@&zv^nJu=ObzjuOmQc#C8?x2Tg_4Hdv?)O%f_)ti#yH{zobAhvy0*_lDy` zoiriQ`HG(>E}#iBP(RcZ)$P|&&RT>Qc42tjoohY6&f+?sopbYxS#Nb4hu55dcbx;A zm2|P%JpvXROECF`0fzJc(d(=a?FZ3N%v?gv?@Fn{p0(fK&FJ0&&^1e4ihq|#C!ccP zMmdQ5w`h^aZy(wX3v&FZ)$zNEYUFjS;h6~yTw8xAq|2Dc{MR(DxmgM2+{cfF8XK8)|GeaxoTM(gN4%!oRTx|?SBa9jaz))k{@@G?C5n1yuKGhFLy zg0r2@A-v`Yg6dYoOT@t`WGYscHDPXy6cd`JW5}~y^fc-UjphlE>y%CPxvBK}T0P|^ z*wbwfD@s(irx^A~96y^)L3`85CqRqbvW&=ijw5Ya>iCCs{LlI5#0*7d$h&T3Ul%jt zSlg#^qEaaLW8e89Bj(*7V9nK3Av0Ej_VJJLgEJ!LEpBEkO8xJrHE>2G3JBv9teD!4*^TuWKVcaLcgjQ$2OFqQEQE};ggzh0 zr;^BAdYILdQrH`DA=Hsh)pPE2(~JV2C((W*A9B5ALEGP$k;AD!d{@!jpFI~TG$Jg8 z`W}5Doi%+({mVB$XA2YdnV&X^l8|K?;x7PuP6X0m~lLV%FM5jEzdfpv|M8o0|=_ zh!pxA-bOVZ%JhPN^8?jXN}eT8=V$0sWN|+CW)tbaR9EtN{97HqlS0R_zfgJ8Fc|7k8mJkGZV}j|kbM&O(xug)61(%|$}vIO~qL++_bI*Q-h@h05B0g;KE>_sZu9`L)bgcmIdyy*xmh zM>U#H5gIse{Zzjl6~%fey{Cs~u`zh)?}xM_5?raXLM-?B!eZPJ=xYr3heNPwh&`6{ zH^$7^*)Yy7g1&z`y1X9(7^0nI8ynP+evpQbu;&z${TIy`gamo~CLF2n7X*|@T}3bFHMAgrqo4zxDI zecU!UNU~x1B_1<4ZXE3~9R1CLp>wnjN)8p&yhe}S`Rh=AH)Fa};7FGPEa;4mE`?3y zcl?`#e3i?|ePc208jwhv`8%`M`NR7W$^Dzt?7un0`(exVw|<;^+#AaCMOf#W!1~a} z%|gCccOk2PleLv2@pC8lHMb5!edz_%94^B9Et>2n@YxR3%AJ@mv5!vOcM8H z-E(kYy##LYlVM+Gj78b(3!GSjQ5HGqr^^1u73ZKh-j%+~bIony9(r6>Ot){C(xoHW zbb8u!3O&ia*?p$8KZozCwJqd)#EcxD1knaxNyq&-xrlW!?}dgx`|MUO;#%l;p?ZCX zP%+`}DrAOGC_XRbew+|8E$ll#bp@?0Hu(OASvX^#;Nx2Zyh~nyl2eg*dT<3EcqC$0Pkb$vdKjT(?-zj_&oeDKDKiC|Yz}N7!$nd4lWHUR*0# zm?xwf%es}1HNr}ftRJ0 z$j>fERz`2!dhdzE#jg=#ZGg}zYY<>eaNRo+>yNQsdcP`4@+Ljze4rQ*ip1-^QTW6^i zI+*pkJ%z^JRG~hvw~)$?7ph@ApQ!V0p|l}fD4aSd&PkNe<9YY#NuFh<=+ z<|u7p9rY-Vb=6bwq^T13zmCGKZWC}RnCow`D-aT>3jYJv*mq|j)~#0sY`L#oX^asP z8}z=z+Vvi-kduj}x?~?J;rumQ&WhtID>}DoE=BCrq2PZ~$^UF9dAb_W-gQ>w$~?!cF^M_FZLFmecWdd; zGkdNd#*=$YJ?&ayL|eKWk^MRet>YeHhwmzY>kO_1Lc@xE5(WvZBjLF!b}7t|c`TF$ zc?pGo*~9toO(8RYnV=`L(c;f6QTdmsZS-dbzz&qj> zLb2VHDa^Ztf^O)Ok4Fo+!PBR*<61=Lt$}m zJUTWNcinp6`sKGgn=S^CA$~ZL)&w8fJ=n>0+tmvKFhALbb#AU0+%O&8KQ}>2~Bgx0<&5B+{m#MznroBH7LR!#eg1 zVxOI+&=|{iZu2Ff7U?fkhnn$xh|5Cpb&`;8!mt(5}v;}6PdB!e$b!FDaFkh~d z+(M~O0SYx@kaJf9>1PUXt;hxEcE3i%BOM%WpM(ALo3P`Z1+4YFF)zXi6Z{5Zu%rat zrR`8XpGH3;ld0lS8|5=6=8l#pUAjAtqPflq?JowjX~kOQn}U1=2A z$!m4ok8Nj#=Ay|wABlUL6;p+p?@^)Jnd^vqCkw@lL}si<3)y`3Nd%RnZQ@BZz0bv0 zbtlxsN$@_r6|YK)@XW#r51T&V&T~^-Q&^9)mwF?D>(_^;E5JJ{9@`@_Vf8%~bNzYF zLcll}bk&9K=m}6MZ>QD)ddyvr&=cz*x?MVsE}rH$(AScVEl=WH)|0%(SCfl`@7#6y zwDAD1W1$tTP3Lc`koTqV|1+P{f&I<1L)d#`FQlyHQQKV1x%mO1yy}Ti1-e=JK&4k0wcMk$_w{UZrfbCby~{) zz_}C|?@Gbvl*!*IpFFB8Xm`39ZQ=FV4|XIw+eBLX&;Rbl|9?MLz7m=vOoe(q&wV`Q z#I+LUil+Muvz*py>vh}@OV*F+ z-Db_$b)MDml=}tCg~};jhx|C9IIW2FNxy~c^cDF1-4Cq|JMevLB)-_MNA=^ec<|B%o1&U?|KYhc$iA{ zaud8&Q^iY}4CIfSiL8rxNOiQr<&YDIoo#}!Yv0nDRl;@lN}l<+@~lw0eMKm+MoC^{u#jm?#_xX@qIGHo z$B4G5<38nw+6t5hS)q7O8lIf3=brg&+$!=zVw*G0B!(lb{{#g3w8Eq7L2PQCizRnj zF!O;Kj9K5%fAA=Dj`M`V#WHGGlR%|Yt10_oHQmU`q_aA~6u#Sx4&~RAPrWO-C7YA8 zPC9Lh0%y7#;P}B@9QesR z;~*6{@O~_QHWxEKEy3swj_i+5g!WHuC}@S!*J&~IYJ4g^DAu9tUvwyb9>-SM>@gpu zOFnCi$j!`|cFYYThxT$>=TuI%p;}~XRo`(PW0{Td-dd=8e-P5%%yW)qFKu@#o^Qx( z>T^8b>efyn6FC#@5mWKQHwE9m5k5b@f)C8`coSxa7h6vw?_ee}zm_2-P79ZE(h=i6 z8==2?;()_-xT~gP<8Nl&)QrQ7&Yl<*vmJe}7eSl*hVna%seY&nYQtjU{|X9p`UKk@uv#q~kgU4q;Xz=jMyK zUr^~LR9Ewyr@Shm)R*TC2@-OvR|%QviTG8a&sr-B=9PCvZOdlnpYBFkObUwZC3w7J zBktv@;-)jt>o~rG=N`UCNKqXEPzg85BG{`3V-aiPrmdfek-ZC{N1O|4T0+jkf$DUm zRE!F`KQ5J$f)Xj#Fq@9Qt*3(?+?)N(d0;!Q!-UrX3$imYqBZv=w5G+i<2rgBV}`+7 z&Y=dfX4;TrM4pio&zjI?OQB%Eyeng#HP}T5zdV%CV$1UY?p;Ui0se*TF)uTmiRYiJ zkXv^j_e|Z8TrmL`Ws4D=ri+m2Verr9oObs-Y|yF4LK6o}4Os#su1WNXae>xm3-&z4 z&}VJdk=O@Q=Gp|h`dmt9y!7b!JaalIqf6dRL!Nm)@3-JrZyhxMc16RIcGULWi^^%I z@%q|aJl|-E+;#rQNGwJ&<>11muZSLMgJVgz;6Ld&_I=@=d2e%w1zqv?{d$auEJ5$) zENDKrhwK=0s_oK7MSi+;FFc8^_DQ6e_xTi>mPvu0Hsr0cT>#9d< z=GN1i+q^gbcYGz~*y`{Eq2Ad@s0A>)@&~hH7IkNh?ja%n@Ey?9xgwjEdj?I zm^Jst@b^CGJ#PgxC)Ppc{C4`(M~j|cu%wI^g>=P{d(5n_3N--*@`3d_7e#wtY0=g# zQM93s-|=C*7hQQzqJuiFL*7DYxHE_0XO2*F=qFU3vz~I;LgtB%7xEz=gzU+6Lb9_n z+Geanvp##F_UbbeH6Inrq$s`5zQ?V*@W`eFccZ1aF=;sapt|FfRtkpk?k8JvU%@HYkZ?RtV6SnYl&OfLtDXfWh{CLmCIT}S&o@2W|czT zhTn2qp2K6tu~k0LBfF4;uL_4z^Po50zfD1@c^;m9yN;Z)S= zO23d#iTg7t`h+PRyJJQH*~R2})SULH+%7wTVS&MTVw?yt3hhU9U@VWR1yS1X3BX}79b*{&x@@g3BT*qITb0PgclYY-g zq7Saqxjt=3>5@{qRIf+TU0dkb++Yf@T}GaN=hJQ%UWd09$5#CQY4bW(@L4x6@AzF+ zE#chUUr0~)6{3d>>SaFFSYRqsdR^;4DPR z6`oD7SQihc^~arE>bTY^2j_;=AmVc@j`%Kx&xjG&)n`6z&JBmfv_YOntQt;Q{ zB1kvbQoBVyRmCOIQ?4Q0S!_p_mS@wckVHCK7Eb}n7Ua=jNxOb((dNH+KYB#bnj$T- z8O!I~#H`~w-W0QDcbbp}Rxuyyw@~T7PADxk=C_>Zkq5GVVV@k@kCfs^UOXDZAEU13 z0jkf`;O(s(l=MEr8gK(-a~_*k<&LC%?l>Fv6%nf@;)u*k?7#38JBQ|D^@DTxcWW$6 zdB)0+G_I8#8VdhzY`pZU!PB7)$nH{)wE3#IT73#<71MF@sS*zFP{aPgpW!^Z z7S_z-oPWv!6XR4dq_Y{i{}>0g<)AhvQ|^1@QvNJUO4F^Ti~oS4;&kY!7Hdf?L&;-B zHtkYMBu7h*bv=0<>x^jiSKb$${EqwajM++hSA}$)tWZ72Ge6#55K3xWg+l-BLQbFO zB&!U>uOn~Ja{C>=xxB$=KBpgc97MUV6vZD$A)o6w51hB-_T`zlI?ogF4h}eJ`WA<( zgWw%~96L-FVpX#b<~?%8geqeUUfUbpt@5Dy!i;|UI#b0sd3q9JNw-~?L6k9Q^s!hOy5bXqsXiq?!Rr_}{Uv_|0%=Ogcc(CEVch($ar_s|ESoIygd zQccJ=X>z^CTS)S<@k>V=Eym~2@cA1)8xpEYUGV1902I$UfG3|@koAJ+Xm%QdE0-jQ zD~Lh(8SXbPJO%G=ZP-349jlZYF!$>+Oz77S2FaPweP94pivntm45s%R_fTGSD5X~K zp$mW8Q)CwR(*Cm~zZ?1F{!&6ahe>GDIX(~V^|VHX*Kv){NQ+j-{dl-eXs9wbW+tzL zS?9{9-wVY%%pH5^Eo8Z8APN48w%{x@pI^ax5*OC@xN<#I7H{Ub;YC~vp3GT|EaNt$ zvQFuW##F=^yCGbyCxS8t!OQO%whfsIE9QmH?K~6Xm*l`esUEsRE1>c)oPJ!3r*|=P zDbLiBQiUreWExTAGS1DLJ;~2Ll-$Ge$vIn#HZ5@_y9_I`Da)tT$N7wWvgo*uOxDOq z_&p!P{LR%|@9~CExWEV-PaXE$|&w{+>%M zIob5~l_5PoBu}?8ds4zI1Bz%&r6W;3qs`&gJ`vDBH6qN z>bQprSVLkMtrg^K&4zB z%9ID7$Z96?WI~WxQ;J&ynb(!Q1ZVht4=d=4gTY4d+%OkgHPU!);0ny17mKm?vM?~x z23_l=P`(jB&98ImtyUuCRxm$eR1%%fE2M}(b2>7|lzd;aW^6z;IZHW*dSFC$6ht;b z>9l&C9@+e#81aAB!n|E(wevsCThrLLvYqF6-4m)~YlZSWC!vU$LVlJ+$PPM)-$gtx z{I8E_;u>3h+ziw_bU}qpGG4FI#q*RGc)YYTGN;c*it9oo>QBI#1>+HB?2o`Ud3cnV zU~{_;mJd+ozIilEzW2j`Y9n<0*9Xdm3#h5ilFFx>QSMkDO5r^?&vnlT0}DFbR8GF3 ziRAXVns!_%CkJ;uvePpn8~b`%O%k%n`NMwjYzFoBtUXrB7OMSN!!VNP8uLH-KJ3?Q z?up-Pc~*5)UuM+uyqGRAs2On|?=!!k^sXVE>+r0Ct5Istp#uzXn8eJ}2gi>S~eQ(R9H`|Tqk*|bO)N|?F%X~VS z5krSh8(B^9D9=zj@MtaEZ^UBLtMynq!~*|>mSGG6p}*Y(T~w=~q zd^)$%icT)Kp~DtN*939y zQ6`kyCJKeR93hwIE@VO;@T{dW{5ThZM&>8fH7rE+&mnl{VTxDV1)gP^;nAwO$e5#p zWWS5JX#5PPH&i2Z$z2>6RR;HgQ`yT^fhC4vn3-4sV-I`uXN{cBS{EpKMo}Z{?q9z! zqMRY-l>8x)&Q>Z@c#97m`jtvPd5+{Zr=GSa8PUc&oC^lklZ~n)t?tI()g_CL`*HT0 zP=63A)Jjc-N+r)gfBRl2JbotRVk3l%-6OQ?rQnCreSG`A19eu`s9t#sZ_9&Gk{5?( z-H$P|YcuXf`rt*lVMJ%wM^X5ZTsm|n|js2Q=vO?u)u26cA!5nJl?Cz5lG7~s2zs7T2Z}h-7uHk%s zT#XO+R^hFw2TBI$qrg55IZB2|XCvypRqp_936ymgM?=9BuE! z>zK~_F_zCpo`kF$(#a;$sN*_L`LO3PMX24W7b0sgBe!j=8! z8v>VM(Xawc`&NTdYKG|RJ0IH4o{%rHq^~0r=+!C-Wp@px8$+ro-pPPYJhY)hN(SVk zY(}n^v}jv;I&D-6qP0>jTAdg~)^E+ohGUfu`*D(MR|&O3?F#S5xgep$tPBPFVj(xO zw~&-|L0kEHG-v&XhSj<}bLBaz{6^zVgC&adqVQBhitHFG+zB+twP!DI-dCRIX%^$? zv-$A5KNkBgd1C#x6f7KKf@z9*7&&GI^m3}uDZdT!(_^XLq>W1agXw`a^MQjsDXuw` zPOLViprq-v|9&~S?nv~s$8{WMPgKl!p%%khz7P^h zyQc_+c}s+xYMqcIR-tW2GMfLLi?3y4QLC$s%Fj}cNm5X(_Xzn*7vjMep21wa0@wQR zeD4<%5Y=xzjt-57pMDx#_Jw2JVnFm~_G#CA7;%4EPt^#W23bSyau514UYA~8wWF*A zJG$0pOmRCsDePx81zGvfe)no}oyq&roAo_`Mr51B=VQATS*N*@jn5y>$59iZej-k& z9ZMA|p7uh?nl<3VU4(3<56@&hhqg|JXev35uSH9w>(6!TLygHwXv7|c6Q@60Nrx@-9H#3+~S$FnFlRqW6AwxLv>>< zsQ5)L-T$Uf*H+n4><=pnJKRV?Y5}x=iZZ$KTkn*cNcI-#T$|=~Sn_vuK|(gJRvq`_ z&=8@1q(AosS(CUqgX^I@(?_eXkWIfYB#VvkGhqpu*4{?_D_PXEUPnc;9Li+vQ1swm zN^rFRr?wdGM(!B)MRpwKefs_s=wUPIaYTB2SPfk0z2B+;vw#zJN^@K#S zK4C#N&Osg5abSc{_v867d&Y6Uz>RY#S0UfvD`dl4gk<7$6*lzG!5a6h*Nsh5}F%8yZqzfq0|_7WU1sDaN|Z|o{=!kRh@{MXtOQ$|N) zScWWm-3W%}U(S%Jj;GpzHuOR#fHJpBDXCu~o%!lX$5V>w;IdHizG_MPW^fHopKB!r z`D8ns@2j3hWbJE2Hjb_x*Wt_Wp!-&#wrP@3nHRuw`pks<^9w?D7tfbh;CZ2+lFvsPR5m~BM~pFjg!kvad`Iv><>!8&hUY- zxvve2su)Zjy9I`?YtZvk3N+T*Fqg}kK6Qx}65oSt-0R*$?l z=#opJBW>mHYQv!*vX$_i+m=t(+xdLhnRQ%;ml1o^_`9-RBvd9cLsWh#>nfLWtjmnV zTmEPb+=TCabGVoG8z0v^!8_K8lp4H3VZmkO)+QlyJ#*m9O>z0ab;Qjd#q%RtaVReg z-ZjhNywV!0PYl7oPpdF#yfcP2pF_`H+o7JA1<83Is*$Os=TqiVMs+4#d7eu#uRSTW z*pmX!n3A_%F}bWv=UQSpZJ5uw;FEN+F0~@-)w~}oqdKm`ZMab1%>FcSOsEX`Pbhx= z!np_Q5yz_G_r`Lx8fWo*)O6Ifa!;`8CEhuy;nk4^C>))I+?l;_FHM4!P-P^(N8kgeDBt0{)tp0BOqtNK`IT^+zcc&S#HcPhGk) zxR7Gzv)6Go`)+2GlUJcD?ft+SpRPuid6JI% z;mkgqRVwV=tPv_o%tyR?hxMV%JZ|DygT)uQ2gP$ke|$z=-);ENl!Lb$52Iv9DV`|= z;E}QhGPb7R=8W06{n-(C`1&gDPU?zegI+$%(_XU)&^Jh7f308Louag*VCoN=5*?RW=U+yqyXM0Pyb}v zUFJ%g-MGK`BA;_7{-*TxXw^6Vdze$oq^uab}Fa*P6 z+(>MmwE)ZB*I|}-9md);VqjDty8UAZ71kC1NN}e2b86{{qm*ucSw?(IoYAK5AJ$>Z+?YwLgqmW!P)^SfitBdp zyxKY;vwIGHrBkQNX_e`vM>`1HrC%)3L>bM`)%-$SbBUD?G zSUbiwh6SI6-1|vFX4zimBdVjhbrBjSK11#3FjN*l!5ckYylAUIzSToy^L*nw0n8yX zPesD61Vo#-Bjn^J1TgR1{jZtWq@Ro>{HFhNydTDdFo%})1YOcvq14C{IE?S6f_k!%1ktKUD_V7m|5p0Lek|v? zL_-XOY7P5VPNWLOi7m`nF%&YBZ1KzJBkSLohf$e?PtUDUxy2b}mv7>QcQ&59cf^BR zI=CZO!8686asHDhPQ_kFh(;U&=CRN2h!z}r2V?OYRm@DNgYnHw=nv9Gm;Z;YHxG;X z|K7((y9!D6Eredytgl|y5R%I1+|N`h6CoraA=?O*gpnkKkt9h5sSrkzkPMP&QAv_y ztVQH^-k*Pd*Y(XGbLGMxb9Wxgd7N{f`)SM^T2L7n;C6 zgC_;eqzs`&_aFFGGXnMQ>G;H4>Gx|#fRX@V`WwfiQ!Ogv522)hb%>5~{jD|}+3D+X zy*?8uXLMMvlKW|Uzu~}76?RPwfOPi`_?S!JEUJO^yKxv{Y>D164QO}ZKD3(#(3gX9 zDk-+3`*N<8T#2E?pL%p`bQ0~KuAnf3bPDq1TB5s>e0g?d@gdL9|Nnh%?_{Ewd-)|H`UG9dOt30=9<&Bah z4m?BD0(q-M$hKr0t38m?>lhO4CPA*d1PA>4AuPiT($*PRHtH(obdkckO%sN*_f4-? zau`?TLEAl+KCkwn;`~s$m(oD#T>)oI*$WzK*zQ&NVna3KH==@C~lPjb)C{ECtOk zRg&bXA9?oXHFp0m`PKA~LMu^`pr35OwZz$icCkIb&3_4^>(=~++KcbLW1-$@g&NmL zynYde;zrg}N*TjGo2_sseGD?{n%ECfhBH|_XSbymk#%noHfArj^!bXV)5GCpS&AvF zD?dzXhF<-eU_5Rlv~C&FCnwegezK8r9!64nmj*hsn`;a|YANDhXWD7THJ{@Nw6=$W zmS5qV{T<`jCnZUYA4&e0(t<1M=JA-!x-s9|3;M?dLC;bxXy4}f(=k5;QOsC0-o1kF z-SVNfScG>!*^_9M0gA)Z@QgjyA8v@ioqvOnvDAh&eB5wm9CIrh9wBn6BX(Z-gw2EM zu*A&`f@>TmFVldW8)mtf1AufYcM@^zvCa-K~*OnmC%d;GHPOoohY{358xT zrL7aB6tGE7%SWqeQIHkQpR6K@mla8h_&t@NY>s0bYZ1L4D(LTFJ#aJjf=iq)Xm*bl zL|hMP-1rOM?r*`zf)jXmXc}I(youuO-23O*g4{o=k(D|W*WPr)#a+cnxbzdpR^G<` zDoyPCFCUx!WgVi`U4YFQm~>PFLw^ULr_upNpKPGHZxwwU1@LSNOB^rmZ_t;wY&ED7=@BS5|LLQ8wa~+UBy8yXC*2o%h1J^uW;-cna zBn)H?gPLyG?kmDzA4GQq8C}+}q4-A)Tw|~!nQ1PCxI~k*s~-7Bsc9Lnp@mcV-k7DPd6y+5S;qTg z)nCr5fp-MM`^5evtDa=8HP=}DT5eS4B3)9%|=0hqbT298`JJZlT#Taec zs8~zanraQ)=((N=Wo?#H%A~Ip-`AWDHD=M?qs|muEut;{Vp{#xhV!LyI{?6;!gd5dY_adrEQE|Q!uq+f@XDQr*=^@z!ZHa4 z>o%dgPB*mnOGXPDF}>H4P(hbax}DHKm$n(vsSPqZw8)b7c9Bsq*G)EG>$7vNsC%_h&>q141tCuOb9@edG!^4(-&)ie zvhQ?a0m|z4;YIaEJYkQ~`@1?Kv;SFKnS2H3O9aGgw7`)&f!Jqbfner_uJa7Tf>$XJ z^Bx+1)CYrRRH56PB(!?$0a2PSz5888&ju?gGha*>?-)|tlPrpUY)5;d`E9OUMw>o~ zX;rd_mbSOx+^UQuf|xx1Skb(1d|v(Uj{kq}!7dSkVFG(Z4f@QwP*H+*n`%MBo8#ef z7C$a@!xvQmKAbOMZ4dSr3{S_4fF?X{G{OB=OK~f%4C&X3aGo-8YOO1dj17fMnStP; znpnGN5axfnidndbajf4tFf1Ki6L=1FSRh3GMpHHKjr>W5bnB0rF6v4sZh##{kBgx_ z9gJv4q?$GjRnRJZ=CkjXb3N3LBq9lU+!E8gheouZue>=9lgF%Oz#75bSl>!!DrkM< zSwY)Y_|xtRek^E>FM%oe;CK^N6$+H~O2G^5g?PMe4DRjC#;xANS&y$blJ7epuJ#HJ zUswy-e=V@%>`VkKNydB)e>nI%U@ZFw4!G}yE)C`|l)a=s=3nXUN7iAWuXJmmgf7fT zqLW*BKJB!ccJEiwjy`3yk=N47Ma*N+=HGu`3!0ZGCyyXG&5Psns_kEJv}q$4Mzj$0 z4ZaGxfvk&IVU~_rHX_7v^C{|Fu}-6Ae#ez&@f1V+UzqKuhLhOc%l6qdzriSyL6)LY^$L zBBhfTDRw1L>(l8d?41l%q#sy&2g)8{ zNc%^Qb5|U3@Lyf2QT5^8pPQ*gMd#QagH6 zI*1;xO`)68EIR+tjAAXcC~8v8O_|1Frj757eRMcu%H#svw|Od&}0>ZhPB)Ad0iLOj&a1B%b_SO%f|ByF;I0s zf}GBmar0;vQctzUx#5O5Dc*)d9lBx9$x!x-IR&ntVcsxb%;cKff5BNW->#2N6<*L! z45h}jK~(uPpH!iCbR&^@KCM~*G}w*x3wTz-z=F2r@msJrbEy60w3urRp3kJ@5vL&c zOh1~}PeJpamNmoC`Zx2B{g^}jGDy%Jqa$dAFBDqjZN%@N+GsFOXD#p}cwc;s>+HrT zUGo#qtxEBTwaM=ubHdH;wMgw1h9uT;jm>F9^qxfQ>3$vCju~SW&;5H$S_fP9m9V^& zh5pJ~bQW{!l^JELPB3WhS&Ze-OVf`Jw(r3Hxb_ z@P6}sRMv)})W8x2D&WzuHMrX+4mXaAm^=0llKSf+cCrf5dNS-jG#J~u?Z(QDKjH4k zJ~0Lz@NfAL^!r_ljvEJ|<$_H5vB-`pGD0cuv>s(N`qMc+ljOt9DDo=LJ!(`_5ZAER zMajw6jB_goKC3=ikozY!xlbq~50!-GpJ{&Xw*H(g7`iSKT3$8g`LqB*%ejltLRN|2 z%PmmP^Jt&^WAMJ$091yKL5chZa~MVrfFY(dTKTvVEkM9JtlJX1#@Z~8%GTPSfo&JiicEO540Bu*`K| z;naUwlaF~NZ>j_>Gu8oT<<(#1_4xkL0_vRmthMq0ua%)F&aK0+d;2XWmz z8Yw-GBavr|=WUu#jsEfw}bU1M7xH z8`1-QM_pZ+z{SK%e?d6#=OK1QnP98g3M@Y-f=iMOrbUS`^5g{c`Kg4-We@1Q zFsHBC-c%Z{ru*gobmfpKC5FV&Q7QW=xf)Vvtr2bQ;7w~*a=##ebHui$G_Sjy+=5i( zHdIL--TD3y|K+?YWe?6_hC<7Y^H`U?K+ycsMrhHw6Mng{?zF&f^PX$)E;$tyjru5R zT#Wov*34(`i>wEx$Qa4}w0?v$aiehjngI@MJ%nAICqbGx2tGH}aL&oY)a$=6qOcqK zj91ardB@(j+2aa0miO!q#SQTi<{I+JZqG5P+q@0JbEd~nV#=RQ9ArfD+tA$>SS0~ ztJ(WI484Pe{-M>tuEpm zu`92k4~%0dW4LQdbK_Kehw%RBW7Ql-ej?AW4Cme;<2d+C(7e505S5A1SipV3TkJy} zxDeGpPoUyI*6JD@gr}_4k!yGcw=39tdTIhLj?h3tvIxg+pGV{_5yE($VoN{{mZ}pl z#~>fp9azU;h77&#u@yM;e6$(^=#zU46|F9$9209wYqyRPSbzG+#tf3ltSE%}m0Ols z(dsrvw8W>3=FjCbH;r+4OKGkLpIf|F<{SPc9;%6gVSDZkI`X>Oag=Lt>4GS$0*z5~ z@NL~$eEjD#s$!*NxW###(NkT5D4$875mX?&Hr;PKeJ zM+Zw=tKc*|6H{zPV_0xJdbQJsu~sm&=0%WtBKQAU+v={lgiKbojA~_BQZ5 z?Gta>EH$E4Q%uSG2H)j5623RoG`A(==)~vNLL>70s%{>STy4S7FqZkpHiFLT--2co z&q*YXK%;XXd>i&2bw$Rgwi}7^O}+7Qp(&pHD8vJcGGzX7#8vlJxG>ETr*9PD=+nE{ zAJdy_3_GzYAq7h;93i-sVe-Z>Sg~mf4M*I77K=2Ls@%z zwV*RUP0-|7bP@a4GSDq)l!%bua*sIC=zbNQyw9j8c2 zITT6p23mAzb2{z0??XH8deg?UQu0eOqQ#zKlEm^^Wv?Pv+cI(ulajkGv zgI~-oVXtbP*)@XZvQvU6U^{-k3c=TeBdDA79B<<~pzN+UUYxXIUq04)oO=qHy??N$ z`6MVLml6Nm2r=*3la{$bp@JtiylcYZ%>|$g4Oo4ghM}{1qsO0BFzR3jjf3V?`_qt~ zuL`BCm6mkrUM-!PRZP(Z)wE}1F723HMjJ9^w4z+fbAtRHHxkj@XIA9GJvP@V9GgN5 z@=R7VUspH32?k$HgqA(u3p$fxxR%&n5V@z~=c!P9UF3%k_0RCu@dC`Qfz3egGEVz|JqJ*QQW7skP)}$Y5Pq}3Vg}+>5eL%>EgR%lal7TsmUeW zf?R&d$nA_Nc^>>rJTh7d2DSZJAA@^!(Kpd5L&;GqQy$~)Ghf%&$eq(W^yE59A-r)Uy118E;;Qs3LWfk`YM@Qm(hle;7wg5+tt;W8koe|PTkNtstu&|vq zW-qS8gw$jV9yN$-CC+F)(G8-CGJ03wMfnkybn9^?UGP@Y$$v5^ifeM=8_a0i)iPRd zB&Ovd+^TSz$_UGkIP8h0oABWes!s?h1Op_6s`3#)4*7TS3%5 z9Y20=!Y0ioBeJkhG#tV4 z=dteie0bTrL3~Dv38Fd-+NpzXUV&(pFa#o}aH@VPrl-uKRyIgUangul19B;9`bOGS zQ%&2($Z6dWBl6)llb4o=+#i^d%XcL?`|(e6&P}>W$#b@-c{~(6PxQ<}(EHS0(9wt& zG?};k=hu7uxNDCuOB_*KqK~Ssl|1vf4bO|YpJwlaoJHK<>^2fkQo_%f&hwTDjO&Eq#H&6|(sA(nXb`5AI_fty!{BTX{^ z=Uy2hPP`6>y)HsF{1J8(GY%6|%+E8yEX!1kldCXrEb}o2_`)zKl$v%~Qq{|JdYo@V zH~E}8Zz`q}ofwCK4~0FFQP2d|9P{&|W#dIOKgELFlFP`&(2C|u5KJ(iXCEDck74Y0B?cTeL6?Xy7<_f5-$9Y| z=8ck6pOfguRuf8Ipdk5jLyGiMQJ8fOZOx3PH3v9Hu+`n#>vn!96B0_y~|HwN0&IPIaC49c^6?{$R4g@Km7B{3!Q)e zg29q8^lOVBRen>Ds=b7+r?L)QBItNl0_`u1p`96ewACh>{I7GbSt%#aW&Hb(mXmWo zKXUqGLC)=EG*>@j}SDjEybT{R%rN=fzRvNv+~JJ)^jD4 zybeY|_*LZ9Bp~~X0ymDb=W&@GlG485WdG+lG_DAHTd}up#wGYyWI~d>5%xovS8}cv z|IBwlXP^7fFCR{gTWhIOKY<>N3#W{_UzGIKi;njkN&6=^(9TioNUCDa)jQ4+b*xBo zpK%x&@tTs8(mX=fJ|t))bikiZ?CE;r5k61g znMdY>RVv(264f0AV^$+CauBi)W#ER@8(iM1LXz_eu9X-gnt6PC&cDHS$vXHC9S(`8 z40a(N7}L5G<`1?ruOtEbb6MwOXE?n!v!=Yog_JQcoRTIb(Xox(7d)TNeJBx0U5#jU zNB*7;e3uX6yJM%A=1j97C%ZD5Ba_nHR*d7`UvL~^pOW}gLGLVky2kqn8V6>hiG@BJ zcKPG8)p>(vwh$4E9qtGE` zwB>;xt(qZX9R?}Q+snC#mXw?dd8quGjONVZT(MkEl1uXD@i<^87#y1*=pDBZw4*Ep zjjapOlu?Zap$eaJ7T|qro~rI+hT^6`JUg_Sb>?+&=bZ}IccmdErwM0|#vsI zd{clE!gw59egeCvUP4gQQLL0+ggfUZw#=RRH>EZD$v&e4=lXhHvGlz(g{%%l^aA4r?L`v>jcIGpG^`HwsoD6{VI*p7bMe|C3dLR2$WMsn8bdL%Dt(c$ zWi>9P+aWRR7vw)PanNKU!s|>BbY>3xieAF)VHeB@D8;{Kd(f}tQgoQ61Kkgc=(}hg zmDYLD{cSQzk4m6~A8~YKvz+$+5K-_1Ic;=Q(29{F@~Tvj`@fuz=%`5;t|G)6krQO( zI)>vRv1pECJ8PC~5(|31tYhG`O3)b54ozBO)Cb9#dtA&lpQCuK_YFm=S;%(@M(&Yv zWbK)OjDBidVx7uF3-)-FuSC=}BZNogfLX}!vs?-{iwO28$;0S_&gi?{8tw0CL3dpc zee0J(rDmXeIXRS8!#L)$9z$IM?G0cbq8!$G++sw&YdJ@pC?U55K5KWI5+14vf`Xhn z@xC`^9Cm+MS6g`2z<&XAGT6Uo>UKe+haZ0D8=!us3hHC^c$Z^_id)N3w4LW~S~3os zoyfAZ!nM3wT+-IZnSzUuPw$7Q`R@^KwGBa{JgkWNj=52KuyM%7XhTc%ZDED>4uhbp zolRe7526we#?dyP(k5He>08Wc_f*iH?;~jkn5%T2ISeYd@u8Yx_>{svu5t` zh)}$&Ey2?z+;2XSiQCLyxW=<67ab?yOdlH@k9>{;mt3*yfH}6h*1^|#I9wOzVET|w z7}=N(vlb&^;vs`hKUeyEp_YnD(&=t;7F}+Pq4+5FqZND8?%(`|@>kOO?yRfCxzzmm zN}6lHI0nmz5*0+t6(lSZk;`#0NsJ}UajaraxT~9>H&!fYw~H29l(QbV^;LXN2*Jm9 zE%B~}2<0^=@bb76PdmlpfjxUFjk3hm>@m3T!xRbkyWse^g*f2mgOQ&I1AbXTJrU3ROac&!YIevn1GcSzY6inTXg zCA2Kbk38@5T@kD#A%kOKFDB|ECSe4h!-3pe`%~5&hwpyY2agu?hF%u5wXC>@vjo38 zay(dn;A8S1_QWVbdE6ts#1=fcXNdbh4Ut)zhO5gXapAZF68yU0*y|xUpv8I&@9iK> zWgOkv+i^@brnO_<)u&+e(S3q;Yx1FOUqhcvW9a2lC1pDoQ_3xWIu+ng(Y9K&yG;sh zJ1VEON943jLq(n=`L59B`_Dm1j-N&3_)*&N4`IKg1XP(iPE51upN zp4ydB_*Hcf-^WYvapfgcZ+1nw`(eCn*#l1kS;OG;Yh;G8|67M&xG+5w2_3)U*siUJ zOy0sim2DwixEDS_t>GNM2UA0)VT5mQ^xkcVcA7G1y{;j(+je?!xtQ);^rjT^bc(A{ z(!u)*3Xd5?+eY!*qfAaq-FPo^y~ib(?~Mw^5iKLf{Z@p#yw8VoZ>>n(9LGZ5=T@Ht zz4m#6Rz-l&;#3#>O8trNItqLo>4xetuTXAs2rsVL;E6eF1#=HNbNCiqx&9rB59M5g z3&yb#y%0I)Gs33mLuzsY%ig-exm^mZf8D@v=7#rfcM--BsnFWsNFR?_(F;u_-D#ac zmm*lN#50aH?Nk&#pq7FjnNz?&ezfF@k|gc?$VFd5SZ_*>lQ&Z)@#Xg>I-dTZfPtBN{&w50Sekfz@uoo*s@%Y6j+!x))t&(-P zvgjfd2W)V9;eH%_sX}Cnf3dUf2DSu$$1=Nhn6p(2)~ix5eCaRrI^qiBKCFpqqCs`V zaw>!~W%0hf_-7oQED})^_Xu}g<37|fJz5jMIeQl|%?q(2XPJu7R!Otpi^;Ji$3r3} z=MRj7&#q?Ym0PJ`F!VdmKfd6eGvnAahC zG|y#Iov95yKW#&|9~sg`Uo$%CltfXkzi5{O_XYI|$v>O#xXoNAY2x=^XBAPBlx83F zquE>=#SjTO=kmHr_{+L-=_?qRFB5b>Nd>KoSwf4Ye)u)$6ux;cK;5?Sc)QLLWs}NK z_|g=Q$GGC2YXEKy`;7E7InGzQAU-7wM+f?2zc2sUBF`c@LaCJTS!*7`Ds{ zrsty2c3KuRZ4&9jqH$F4i}^9b)9J!1P;9=84pj1+_H_eoO$a6b&SkXt4dYlVr8%eg zoSGq_*;Dzm!-(j&l$_6&(L8=5Hj4+!1OwBa%vllytyrEJbgD;V(|ddyIT>{`y5p^7 zFkby`g+f^(RJ9LqujL{rOIst|Gal!6cSF3}XT;?DBI3_^gnkXgrcd+GMUJQYB?ZZ>I~6NfZm_Q2QlNSRmuD)uYvZD&|#kUHyxS z=8Ui)$7%_Qzlj(N<9H}1=WrvM7y6g;YPP0e(8*QMeUU0??GI#Ki7GTcK7((fL8xoJ z4^{Q`?9Z-^!l~I%9nC;aY7La3Uy!b!jq{^`cx^MpY}$Z`#FYp=k%mp$74WVufH0v4 zCOgK#!k0CJS<}JD*BlyK$I$zR|LOURq)d?=&q{oy69wxivPnW=?UW?_!1{vs)wC#s z>sG5cpSmp}M}7~AR@Kcks1MiO|f7u^176}F|tpwfM69ug`4}}&z zN24+D25Xf}#fM`8sy6n;D~mupzq}PHtzzVK$iU5tw@8x&AUSjsPR%)tBaeF^qJ;@U zej8##P8AkkJq9L+V$!r^3|-R?JvfhV6J3lJ*KMd~pBd#Zv?1j)=3}IHBl)W=itNe# zW?OHPT5{hbloBj~2{ zyu@Oa(4tia8pUdSU6#wW-PNd?I2@%P-r)K2CwP>85qGmyxGB>?n!zUU$Tv=L?sQ~> zH})l|5pvc68wSQ=G4t~&A{SP-iZSHJD)d<5gf>In(84W{{ogv#)7!bEoa{sAhsTk8 zQ5o&OP)4EE%;i%`$gf603yl>t_c!MxQaR0DYeBPa`;mCIny8ZzIkywjJUdBq9FtcH z`i-`%=`%yna;f1S=XNxjx5d|S>G;5Pw<;~>$DFHU&*raqv|7-yyimnjJSj zwnpkAEu0I}N8B8)C9)3nK3(=JX_=1oUk6}e<90Z%S7GASMHr$r0o{Yc(Aq-*(NSBf z4yvXnJM!sfuSiNBluO5X_B^84ibDJI+nnp@E7BQkZ+l@CeBa{x2qu}@r0im_~6{=Qp9N)%<0B{Z#tLMjgD*QQ^e|M3c1IeM5Pg}=*@4aEpl=_Dk6wQB<5V+;X@hC zO5k`L;`iZA3!2y4ulc&NoG0kN?aG`!4MA%d^Yib!;-|t7UoLWOH|ig}nKKzB>TDFa zPe5Lj0@(+y;D%!;E@z!W(iiX0Z1(yVbZa-3m7b9Sm}p02uiJchGA_{%&&chfSS@46v~&W^>;g;n@6 z_ZiO;eMjZHcPLp`jb}x1$TOUQY|U6)zuE?u`%goXHS5i{d4WT_PGE1=VC=}dinWm) zupl*tS;&JhKD88s%mUE$fj$iH>apIHoZb}ak*b>!WqeAZq~UTpdO=G2CWUj&N6H%Q zjAI4Aq4=HTV$biVq5S=&7UW>YKlQB0u{Y!JWA0L;s5y?IdV+qwSkPU~p5_Ksg6PN$ z{G9k1Uo3b=O@nKqm!_j+{Cqq+!Zo-Y8{En0hwCfOBIRW)&VElptg@PG)EU?tFaSG( zmSU~Q0}KA)nc6&KjJK@9!1Jc)>JbLR1!DSr*qYvq{6&v;cu~d@37t)grlXUUBrEMk z!5-^q!@pwksZjji?2vOg@9QT|cw?A-{}fH>zollaf5@{&MaPnk4Aw$=L(I zM9>nk4t4k${Op#CFS_mU{wc>}3-hkDSlitFARg{0#ho?paJ}Dnr0mJS*)zQnyJ{Pv z*#l><`BnBizJh?|KQaHPF=p}1-MEY~+{4j8moLnB6&<5rZ*r(o7Dst%Yr0mkkC-!8*|tc$jDZ=cviECFdV&CFB?&BZo>WvOg&&hgi;e3wWQ8=Q?A>U-rj8 z>?NABLeTYmC1|q$plH(p{Al=%&n1uX{?tZP&b-CED-oXcGRDKHPPoH*Lm97HBW0>F z&bliQ+s^>e?0>VT_$s#7j>VdiNX$2mheNgqy#ex5m^A`4pfi0dKyIInHZ`=OhZ9HLFhexZmQc#qG%Ag4Kf zrR4Fftogd?$GUKttY_f-T+pm;C5U_v;>WWQ+{5w0`+#^iZcGAV3eUGp`_7&F5S_;oFM>tqVvZreY3~<_vEiaWDG*U-m~I*1gJLj=oqdXcnIrL=qqTxHJ!+caOw-+o^b6<%?qXe#k%L zjodrQ$U55-8IEgk>C$5)-W!Aydvg$NV2?f1Z)3Zw1N<|y;i<_zoUFqb>!8FxRbq5b ztAc)MHvN1WM-}H9=;4@kbk!-2&b-i~Baz-b|If8VZPxyOY(-0Na~&a`@9jwnn(f2i zUsFkTHEMD&Vq8nO_Wqss`3-UNc$l&;YU&n2cRKf+Rl$O2Hsjb^h0k7F*aPkmUZ-?G zalZiM`=%o|M1rg(G013biA&2RNZi^RC#F5c!OVWx{fGI-?Tq1X834)V1lUhw{julV zi{d$$&a=lsUkdu!FP|!m;wblQ4qbW1IJ~mx$iQ0K(-cSBFL2Fg9QOq$%4r^-V@@Uf zy*v1k{WS}+JI^sG=iGgonw(12ijy8Cd}6O-HXq&-YeL)SNoegE6)oZ3 zpodaPPJx^o-r}=rwVdqyO+S;Sle}4HEnM# zV&51GS~5XI9%6n&bv7cgFYoce60#dpM)o^6J{tV@oEv+@_%+AT(Lm6T3l($+c?z26 z>;+NZO77L(#-|S-P;(G?ZOXOwU~fFl%)tZZjom)OzLnF4;o@-voJl6gSB4;}R*KyN zJ+W;HVb!S#@M!eM%mVgx-DC&z5qap;SAv#44%9F>ly$po=z+cwrO$1k1YYxpes-hX zwMyD{J(L2%tjPPOlHAMHB&4fJ+*Lt#oX^`a|Igl;<5TWOPDdEWK3Q`dCJsDvS1;)H zX%aMJS=X+U2tRZa@ab+mYJ57NLfwQSM;$y3WB;_sWZYi50N1nvadEB$XO_4^K2VLQ z_}>VxipMrlE3E2W43Etc%$(IUvAMER_ zAALvA?NZBJI6Fbqat9jTB(q22XVh4!xo4n2QIBFg<%+-q*A(0yyaQM7^uWc=O*qqU zGmck}M%03K2tPRqLD#FWGIkL>G#X%A(*c&p^3dN)gpRDkrpLM@->ZG-Ro7a&_gjn7 zyqV9(cl#m!>>g*ybHQA@TIENJbGU!gl50G_Mf~nilAT0OGxu{mMEnMk%gAXZue*)n z=J9C7?~jN;LAPz3pc%$mAKwO`AybM^>v(^d7@|TJh?nm&@U*KA9`rOu=11V__F`Np zv_Qh^Bpi?9c=Y^;a7ht@HWp(gYs9(l%zn$kt6Da;3*LL%rDf~?m1wB!b|2r|)O626mHFe0G_yoRGy9v8-C2%@ zC&y#DlspzIn&Z$tCFlo_6mK))#`zF`_7@Fg$rIf z)ZxjRYpjnEfz1Cja3yCXE_BF20@qfKy&8!FuG!ditPi%{@WF~pZg4XfVMf11)*0M` zz6FVB|8)p-7Z0be>+`9&QjhNb7(|y3DCyK*H66SfO1s`!(biAVwAzJdH|u0Hm+Lwd zDJKVx$xL?(vc2I)b}kYU|L^|j$@`pC&EwI6{Z|4f{hyAE=G+DN6SNBrWA8!zN)PY6 zx1&6NKI_vmPjpf>?*DfgxBe`}mGH?>6xQMNtKT?wd@K%hxq@9Dsn{x=jTK{!FgGOw zHdpL0dQS@a2C$aF5nJfCccw3|8OK};y1TKEF83Wtrv`Y@!FiUn>mbiJCq&b#=Ugk{ zyK=5JzoEv<$-YrZGkYk=b{F62!+3w_$jQk-Mjp1!^Q%@(odx~X0fJ7YuAu1igXSy4*rIEu@*p@Z5ZY1cI7 zEAhFsYL1E)8d#8Ptco1p^4)h)Lbl~1vUOIHT}vyP_0EEX2HxjZe$C?1is!rZeOa%) zi0|_$m+{AD0qX0Wp_aYIyS9!f_piWKZ$n!Xw8e7q~}?YYG~(Mw2wW(h?P6P)f{ zg`?%W5b4nyVSN5d9~fYHaUAzJtuTFq4n{3-g4sO4B-)vKoVoOQNF2S)^`dNJf4Wp+ zN^u%)6eVa;SVA>PpQh7FcPV)du^^Xne#1^wlYMVqQ{GlI<69Zcye6Sp=Vc^3;dl&| zH^=c!B9Yj&5i6nYp@r&wn^5*{AYPdM!Q+uWxYxrQ z%0dOw-J)?mwgK_+{y6IO1^X)^5jJo?q^O6_4@0=@s=~B$lQA-u=RVGkLAy`U(B5rJ zpW5^6`~&u6_{Wbf>6_C@YY`pb95<|ul%%#fI&G0Y&rYXrVE-To6(fd{+lc6@$vg>R4?j;vMY5ctaFE| zsR%h$o=~!vZhEf{IKOB$;+Oc~X#ZO5k2k?ibuUP}c7o4NZ@BatgK0Ka7&*lneK=ok zcbqjcCTo$J=Y?K4SW#A*GhK|Wq}ZxLI^bkTJG1;qs++_77%Q63H3a8-Ml{=AM0So! zn!!D^8H*T)E#u(#mT-h|82@FT*RpTotVTiS{4PPWWdfS6vWASf2R=TTgz9mhc{X@I z3a<}>>i!hua4$gVbq{H8ym0=Xy@((9oxOIMQNsM#ohe7KB{K;=UAV`Ywg^)ztuf-w zcJywHM7vqW(5lZOwPpepK5?V0X)3xn-jHI$8t8x~d)fs`Xv;1?TCu5&=5r3{?8`Y} zlbGz<_|c55BAQ{z>#85Gt4WMwryqH={LB7$$M197o`Oz1`ztjqXWy0ms2^H|kLM1d z+GGIABqHWfn?rSQFml#+fpSnL($30|T<42ZYUbxB`(pp#1ngY@6~Cy57A%FtEypwzV!_Dm#`BwzO+UY^VREH zSK?#HOT4YoK-sVY6wdX5YQ8ISCSAhKFN8F|I3!;@fK!)VB4(8rB0d8Y%@{G*7`PfuA!Emzw)BnOYP{wnov6NT#F(-lPL7vI@%nn zBHw5W^2{)zIT=b4dx>f0E;-FG7L!d$8QGSW(JVth5*Bej-0+um^*UA1pU9q9QLG79 z-H4_oVfg;36F#zL{M*~pP^Qy}!a+)?EXE+mFdR3p#UgDq>vpZ(fm7=Y5i@!#BDn9i zQ~NqLuQ0~ar#YA-wSn~we+++Oi(ZX!FrFU)tsbkWF2IbQd)rZFlO-yA__ zPkTY5Bppo-dibt-jgMB9czgIXUcJ1;J+)zY^!q;UK99%E?H7^CeTC%luW)L74vzdX zN5rN+2+b|WW>FxPF0o?`nQT~lL}NJXq4r7`4deDQXg+6ciLYuZ_?%9e`(sFPiS@3` zMYKPQ^-5STaTC{cmR}K(q~4U~81Wj)Q_@TWCE3K6kxek)hle`dI>Ml_TF~ZEUBXpYn-WIb2Mc# zS4ja2l0RV&&b49+?Z`NcCA54d^BAJ|&3BF0&<>8lLypBNURQG&$J#P-P%@5@QgVMK zYmTGnte`*Ck#)#g3L5t~9^E|f{Xh}weih-ZgArZ@cf|7(qwwg61@5k}!Of0gNagv7 zbNBrbcdsRmu$DtaOP(j1dl;L{3b7fsVQgS%W z>&i?_?$7^%qwtZS|4*u*6L441xKV|s7CHDH5RSSRUGcWxFuZc*9_Ombc*H%vyJJPT zQSN|Lp%mx#@LD*x3$kCfjd>HLIobo_ZSMMQ0+5C;iuI4dKcR(>>ZlbVEVBXUqMj*g}@ z{e>S*FHw_idp~kmD2RoGDJ4Db1|5C#Wh^6nH^ z+^K`9Nezs6jcEj3r1!ygl%F9ZWd!#IU#sbOF!RCfnSb1>6ZZ_PXj!}k_skea0DtFY z3Yt;OI1)rO{j!vEML9W0xYzd0g4~r>&2c=A7WBK07j!&33L3GU@tgf|zcUB9Zmkij z(pWdYjQRN=&LXd91hV7U7ko+$E?+W2()+PE`PL1GPlaILKNSd(`eVa}i&)$=8pvyn z$s;mh!Je-@`HgPmk`E25m-N0hYkkb}C*{ywN)F4R$ib$J@J3DU>3=z|9xV{`JLCyEuI%Y^Xas(?cWk<76Ov8CXgoyM7iS9sWh& zxusZa$zrH&6EG>r5kqr_pyyx->jwLx#Z3v-^fsrb>&@xr@?tty#9ju`YTCEXje;Gd zwEi{o;MQA^M`syf55J*e`7XDR(DaUFG<_mt;{WT=mH&^xW>J6XB4ckL>_x5Wt&9f`Z+^f?r;%FwjXiQI*)ZYIUb<{ z5L|vAfzvawi07PHKnarucEixXO!O%Kj5fgyXt5=S-X&Yn(>7|l*_wN`e77B&nMM0X zn6qzJ0_d;eeFQmY(^ zps!aZ=uAxzG&W`6x5NnFQYHA%n1HJPkF__C%CT$z#w$f7WDemfrG#Wkn)KPt^RV}o zN~IE#DIp2XB~6kv&on7X5)yaF970G!$UJo?gz)bB`>yA^*86n-{`Za z81``-`xs`tWRFdy**%jgc0;9?U3eMHPH(SZHJt&hVyqq8FYCZEuGg|8TEU_#XR@u& z*Rd4=a*R4#FoTvlHcM?1o0Mk2h7nyRYS2mF=S*`R`8%^XS=A$DGh4UBSOvH{HXP(e9FrCXR`i9J}wRwN{jPZ zwfbj;V!WrsFLOSFGb@>WXm4gOrS;fj!CiK5qAt7sGml+3UcgRU*0Gu+&sh2MbXNGV zk7Z<-v!uyKS@gEKY%7=b6=TJj!|!%x5E9GO+YWQp>L2l3*8NE!b&! zNmdhT#mcMlSz+lcmcf1T-R~t?wAog+b&?ocej=1P6!G3&(`A@?TrHa<9?XWG;&p=; zZ0Kv37<%hr13ld&I7T6L?P?%4&1K71-I?2{(~l~vAxI2gS%Ma=6aSfXBXRjGL=P-dB6fs`?2Lql9_{5CR=pB zf~o7Zvx)WYY-nIJ6NyQsuio|XUO1ujbPmq}SU7{`2=Zx?3ZH$fph34><2oq!pL``* zN(D*OY6Yi<0_Usw97nVtH6LV0t*c}y3-hCHJ7xRh_{&!)^=*J~;B*OI`w`53Nj9-B zHV@f{$Qbsb_$$w4&}Dav$MgPtZR~=EKC3_7!D=QwV&zsXtk6)DrT;m}cJI_>Q7?j6 zpj#>P=60g}7cI6ZiudmNTar!OtigsZ;C-B}O6jZMR(fkx3_Z>L^77XyG`p4e?Ru$8 zBVU=(E#3yyhcr{ysXTVo#^a#VxUTxhWkV{Tns{|lvv;A?IxCd2ja|I2j9-5or+Lqh z?jyV(XP!{ZVL0pSkzijm?brt&J{NrFO7XQoD(NE2+T%OOC z7=DPQ-;iOug^et#<~rN*yO((%NM-i9g>2E>$86?-UN%vk*KvMLW+KBk(N`l!(dKGi z-}SDZmS^xfoFv`@pxB5;R%+0I-wkx->khhv#}plSY{c?IJ~cfhpvI3osEL6HHEWio z);kR-`^S&k`Y(J{cUvg+p-(t)BCj1Z`^EaW5AsERF#BM!l)YH~oIP4y$L=~+vFp>u zv&L3_cJAfRjyB9^2jAUb1s(A$J^Mb}t@MpWMLuI&3fh^sr3|w-_hI_aW7tgR_e}nc zJ{wY-O8*>cr7ue(X|sbatxwrR%Pj(FwgsohN?97Yxq${`^0SX59uwV_L>=t84^^E+ zO=B9UaSf-(#87H>OoX2m+EM15MBO%e^ry#(4&G~NkZ|DGTA>&Qu)Zq~*%xtl_F&v$ChR zr7N{~j9NF8It<}HR3PW8#j?~mPJ|l&qp~^iV|g~?q9T*`SjUEFHPPRfTIh@W^XSc#7<%fBJTHZq zL9_nibsz7X9h74Z$!rHG%)?rNr%1MR5E7tU7; z{iyK?j)Utj>xBZ!=Gsv=kEH%MsvY>8@L|G%;(W%03ZHouw~zJIb+GqscUkA_de;85 zg|(a?$eLoVv&LDI*{P}(QMu~guRMb~i-7d!ticMkppOaZydI19cIPh3 zZ+yzqLcLg`^l=usp5xegn|YaNFk5$NrZ?(6n|@G}O>mB3GN$76*N__8X~c7VRAp#g zULP&n*-0~x@j9GevNU22$KlG)qEZZ~^VMc*ca6stJUyt%Dp_j8?M7p#W@GS9D^!m*fT4(1$%M1)?=H@_}!0$8s56{mR)u77_^Qm(rKX>B(h{g0SYNF#u zjdDD=j8suGF%4?&eG~; z*6zHJwan^Z*WTN)^Rdb7lte76UVM<11-)nazK>YiR3(;hWIBr+`-*LPB*{Dr$1|IA zvP?I8Je#gS*!VV{dv!aW_Gz`#=Mg^idLYkP5^d!5k1;e;llK|qc2;;(7u{H5N4+SwUU^KS;!WM~pK+R{voCx}qfx3bjg5x0fEc~CdQf6==orb4MpLZL+GdZFmA z6Rb}ui1i?yy|?pWoi_HYU8{n%$V9Sh7uT@!URzn+!%S8^!-AFZ7;e7a7nb^cHcN;+ z$Rge|wkal`dD?3+o6S5IZebr&dpwkl&n{+zGyLgKE)UO@V(4|n7J9NqmzEyobDY2V z(FBJU8lJ`L3~Z{XcM*?Ke>b3ZqXg9aJg37mJ8I-mMUDCzsOfEPt2EhB`mT$*>HW(- zSR^WxI`>g1@tEi6e{p7gvQn&PS|xiw-+*;$sIm63SJ|C6YV2D63wECOPOGa&1dp};0bq;^X9)68ycP?14 zYutxDKjICm<2?+H+@Hisztyn3mj*1gU<*r_d7VY1&1M^)hcl1rMrQr^Hq)tC!_-`O zUj`mm89dE`{v5z_$as9ZDM*)|lo#;+$GtRTdjrp9kf-4bwWxoA2=$h+qfY)F)HXMi z`%QfBh~qe+>0w0{U!d4{cK6IZL;A|KnG$X@)^IjX%tLeMmV` zf6Feq?Cd}F1RRGq_nT&j@V(rQ8oq3xMh7@wC2+pV;J(S#eCjr%t3Qt1Q$nfQSfRwV za-rxWBlff99{c?K3w!q>ojt$5h&`-zVRu5Md9SD+?A!%AR;M+R9f`QjN((DlUXnda zwcN(yTY0XJ;W@Su4a{Tg0cLHP&U6$d*)-m(b6mL>lRovB{?KWmPiq?JwYy#P#LgaC zQdCbfB+O`h5Tjvja&$wlEM2yR=ktByG3prtYW7o>8ZStqhUc59QH&jr|8RQj;=ajW z9n?)Fzdw%bW}#Gd74I?eRw#P6l>Iz6fPKDR&fYbNvgdX2>|yE!cE{}=yC&n$&gEX@ zGv{BjBQDEXX`nC5TVBjk1rJ$#Wh9FjpTPXP#o5wp7R-8pCewZ_%cfzpg_ZpghmGhN}2n{O_q#KIt=(3qSzw$7TC33xG#@{m@>Ol<) zJgCtc18V9ipjLq$lydoTQ}pYPBlEIQsS9| zE~Atl_id%cMSRB6WZoC`kOmE#;za$9Hd8MroNJx-VLoE{1s zhj}xlseaUL5= zWHPB^d~S)G5A6_hqF1eD>2YO_V`U^wd#y_2w()+6-?XUT@=)qEGL$+V=kmezvYD(M zHCkUq4dtt-k%$I0mEnA)olj{z$HC>MpZ~F!_wdS#5lWP86^d4Au%BFZKYQe|cRb(o zx%v~<_G=xxeL|l1QsX%podg<@A_dMQL~y&-)AQk7ExLN@el={D43xTBgK) zGQvJv@f>}g|Mh$n?`_wyf!$6&!LDlaoQ$(t?Bp~tR<*{8l|=clT)(Mo-*g`qcZ&DP znDLfvP=3waM`SWf&tSGt_@1fG_{iiYe`Qkkiu7Bt5`E$-ORv;krnR}-Y0-yXn#S)r zZalA3%jdl%nq;YGyexGz;5cgdzIUAS)075E9`QY-iTmiCT<^+vQQqW>x{3eGe!hF1 zP%2$rD3QBYC|Yoz{UjFbvt|c-H`$3j`@V{`@jRv5fd|=DxiRc)jte_E;5e%?{=iDy zDa$oG&FjvGvAC3G7C!J9^J}+ZZcUsXld9Olv06-}JClulJBdk6zDmDwpY)0JCVGYU zR;qQCqeayoG|jk|#=gBwLj#=X`Y;jd$@$-Lpa!)L=k$o@F=(8o9(C- z=O?;8l)8xs`qLwU>#LOeLWzt-p=j1LKGUFzeV(Gg-btpgXZ$?8t=NapYJbG8e3NBo zd2h=TPo}ae!9-TFpqS+<oq(0oDzO+*3`kv|G7MPa2#y{ zO7b{gWpbaYzJprv`=={3sO!&v;j6e0La99qgc2$9grX^&uLSGZ=b_ix+rK$IF5G2p zyL#De9aVPao+CTE_&x8}uEh@j9L!3FUtu{vg)D{F-o%;BU}2}8GCxa2<_3-o<}9^oU(3luF?C)$R#G(S&8}=dfzl z{o^xx+ZM^5Rm@>+YfrG-@-ghnv21pBz8B9~n#&H~eZh*~J!3g{N3fKn6)aBCmxZN% zXMO`NGS|Mh%!1cMYb~9}RQMc|u~sfja<@MHYSc;}PpF`m_j%J}nIg1ET!W?_l%ugr zt7ym<-ix}3pE<{Hdbs9OtB;{vKY38Y~^6yR;KLg)J7W+$yh4}}u^``~QmFFc|jQ3|+Q|2(0VNGnT+*c;)p-#Vi63|E2`{-p` zGkVO0=fYh}r>R?bF5H|L8gihEu5-_)9_9^HsLXMksiG#&HK^fwj>Dvx8ai-&70L57 z?j=!b$z#ADHU5J`L_|bPL~KYW|Nft^LCk!qt%JGgKR=7}HHt6E|9LGcmM$VTG;2U{wFjnd zxrkY2fmmcP8Nyvs@Clj(*Ovs{(XRkxsdkv<~ z_Jz{wXHY173_94pXUG9mtSiYUz7Nf=j)$)R^w|I z|N9btou?2C;nz}XfxwM_e|iUsBHd6jn*r5$JIs3g9y;o6un0HAl1D?~J2(`+B{^`k zjON$)pa1i}I8r{!5SNt#BG?#+4-uPDUDJWsPwMb$bcbPkG3FSXLA7l>l*Wlc;g%i* zm!u(Z@P|Obj^D!!2qZWT(J%bE^?aG|#hlz%>(|D6*A zllU_+6qF}JQ7;)vvv)vcyA5U>{~PlHC9!DCM_Ac)!sXs5tbAF4btl)rr&bg8 zA5QS={a+lHMMTIyD|7A(vg$8uGP~H4NRHOP?c)(BR;@?$xe@UC+745v-_X=ufEn_i zFxCAXrYyPvh3p`Xrw4+i{5j893Bf2%BPlEXd^I>Ge!n80`8vkeZ+`FmGvY4%eP;+p z=0Tv!>0)aRLAn}_HqSmo^i%W6=q2az-k}7imy04Zs}^A!4X|{dI865k zW5F*esE?O~>PR~%Mc6?xa3>TfzuwWeoF{rY&xJxD_klla&hvx#YYEOPgQh|t!+#!Q z0l~Ct2(0cw5bFrR=dn=8*MZ`(Lr_}w3sc_=g<56GK7I4TJl& zcKGNTW9@}!Sfi~0&krMDspZ}eM}dd~2{xZi%w8QPllvGE$#24~QTI_{E`p@gKm=O7 zh0Du4m^eLvmU|RtDYasnW;v8YTQEh(7Ya{Ap)j1&CQB89#kU}s!{t;k9fEPi5Xhf| zK#kMLcpU_rl_6-{1_i_OP?S!`6s;mCeZ35o028Qv492X~MVOzl2zpY3V0?WxY~xJe zoKXs|t|RajG-K_=d05qA0{25ZU=}6&zc~KGSNoTjknneH-7s6JRrq?QlH)2F|2 z>W(aO{qhkbxgP7nUPAchJB-Xqv7o0Bvv$tGw3o*)b-_7IX^V!U>qRK6e*!^=0R-`4 z5V+sx&z{rDelP^9qqyF?1i|;0P>3pmqLUn^#0NpyXbDtG%%RpX0kh=3W8TJ0X#a5r zIiv(jqYyCm6mIH^;oT+=-isQm2S;JWiPKnOYYXGv&VD$GRtzQ4O7g_1Ri9|AvLdn% zUZTtKE3RyMfP?R5V($cBgf?Eo^7DSlb7efB zb!8_Og+7MqkPEP#`UHy|C&InQ3d^TTz~_bmzB61V2RqX4#V!j~L;?y+e?h_NEEE#?{SPpLVnY_D+!KZJVil;k24UKtUd&u# zh1n4|pc%aex}FIjf8=1HUJCmHX*hMR!BUUQ@D@J7ifiInp7a=;yZE<$<2bxb+39*Vazq3Avribijt7{Ybj(DRt`?GcpC=0jyzJEnPg zU-c?vMJp#rN7O>Jh28VAFaCY&5`~4Ppoi~KHHrJgSyy05j2>Y_L zShV1FKOAMR)ks2F3h}i{Ap{PDSNIUgUq*P7kd7F|h>VfIJcvyQ41l^ni=OGnvJM;&~|7a}0Nm zK-Fh5)I#24<`frbIJIHkloikl$>z4u29P6)Ff;3bb@yw~mD}NDZ4K9Xb#UK%5$wFa*Gui>0|8}#v0SPasDwjQ^`{tL$;`3+>RtpnNe-HF)YBAInrj*Kpr zAR@VCcw*Fx^JU*~Xml?!vkMUy?u%gG2KWXJfQ!#2SnrjG;p#)s2@1x17YAsZ{E3-N z1Zq95nC8drnUbYY-T4yJ#0b=6u4DTC6wExI0FBjFn44yf1qQXySz?GqclN?4K_BLx zyI?aZ9u85BaI|)Y^8|6YgzSRz*RNPSI~Mlw+hI1`3R=HP`{Vd!K~mmyk?rDqX!VXQ zWbVBUWSp-%5g${97iD_5lCFj73?<|pmqgM$J46Iq!GA#}JQsfjJ&_KxTkBzC1)%>Mca^JV0qwNDhf;p1R% zx*Eo6ZZI$S1?w+!VgGh8gnH&!oc$P1`=?d^kU?vg;N6AAxTz72<5zy;K%_BJ@|&?UehIc_>cdC863#W3 zVWT_-MxD2zmlA{puaq%2!xI`?jzV3??|axK%t$K7jPvs`GwlXu4Tyop&koGpau=Fw z{IRfUEc7C810g;z&K(8wQ&(YK-VA%KRbWbDaGYWW$5vh>*8LE4Y%Q#hS-|MUWh@YR z&=1GqNJo-(QHO-peITyE@kA$WHxXnVCDP>!@b|&TXh~Xt`crZ!8|jAhmH~+K8H7-l zhjo5`ur$&ge3%K$leK{cG3ew>gyyS}nCtt1-|r#J+SCE{zg|N9f){3OJOo0e$zUo^V83`Htfowb zVexus>Ie73Q8n{E$&l_O5$lc+591qTkwYC(8WT!}v&IhmjR9uAON7QRS7T1({LlS+T)?aZJ$LWG(f`KVKTrM<^l4ss-6hjUEg7UErsin5YTuZIP8*! z?X5sq+$jbr?!f#~&;B?rdy>rgyGYc)tz_A=4}|n+6P3_#BKz$Ne#wo;c7Z8+R2H%20xNThkhZC=0A+83);p3r~tBQqz$Dz5=3-eaCVXj^h z<~+E9xl_+!-pJvYAD@i{5i_yy#bW5@D`8R2FOZ8wtj31f8>{vN0onOPc z#2L0hBVbn?4ZCh9*eU&lb;|~rJ1Ya#x|pxt&>zR0Fp|Bff$Z#9MSOQ_67#liM14Xh z87n)Bh>w1Z*B=9Mb7n41c8)=*t|Kyb4ncxZB_d>pVzX)uR*nzAlE{2Ga6ihz?LCYP zR$<0+URQ1@%Ddye}4}e}eX_snE?(hCb&VgWCca z&X9p|QWi|#&V+^X99Yd-4(qS#uyr$qorWyzq;g^7VF8P!CqN!ghvu-N{y3^!NKW^0 z5^KGP_^n$(?9`T%dBUAU{&f$L^1p}f^1pHaR|3wqH{fuCUxj^;9xNIvVZ7!6V5<*({ch+ADmf2TLEA7A+H5GayRJi5JPLYS>##`2 z5SV8GL%T094rzpGwHeGG-G$}%AXtA_gKa|_?1ztn{mKKd8fz{)oUHf`3h*OUWQRRWW&h6CTzZGMb^$qg=F zDT4kuR)>(hrH@Ixvm)8Ny^07gDiLiNaiX~LAQ`$R9(}_;p<|aXu4K2Pc84vBqveq? zWf&6PZa`#YIkwUVSfe)zo}P|y+7JMH6(?9))xe~R+bmPB!eH+u=-=B6z1xY<^K*q> zx*zm4b+JfuE(~_IflLa9VRtf2#8$%0{yQvQ&w=&j0kC_f3nmf?$Ah=vG&l!~2k63~ z!Vu=?Cqvsytv`;xB1pcN9Z7&L*>ZY3ak7mkx`!o*QeP_>E;fmXSooo{awx8QYvbgH zQz$F(MAq*fB<*xW^y_=rQL76-!$d3}<%K1!Q@~6eVWSoTb6Z&$@6rW1H4g^o3$ZBb z0rcN$K)+1{i^xeBjM4(gZ;&g>Fe*ue$%`{E-~BhNj%32l;tZIs4V*LxmV`;dP30Gs zyw`-|4qD?tt{S^YG{VGyO`1yZIqHX{YGGLH5X*Jt6IkAV1k;uqFuG<6 zl2HI~8{NSEF$`pRwV`b=AkJY-^M_$W4~$zhVCKIamK6=Kjb8~So(Lx%GjogI0Z-#N zc-?D-=ZttP*&q+QZC|0kq`N;37j;sgc7-H%){<>ks)+0JPJ+-DGVM|>8J*7U#eHRX zpX-H|Lq2HOcL9e_2vLwQ5c_f*5tnod5tAwrIPyE^t*h`_`4BE<+Mq?cu(5FAyuJV? z7d2tz`VpkM9XL84up+=lO%TPeFmzi2r{k!ktP)X|!Y8sNG!m{Pu#W$`Dvgm(;r`PBxNo`( z@1ik04*Csi(>1V8P7}UgZ}T{V930JifueK$alDZvh1zRL($Xptvb=$KbPpiLS2T(G zXd5!_d^eGdX~gG0`|&VR0vDer;aG$YiXV$0Gh_~u2FqgSLL-EvU&hAC?>TR+hsU53 zaC&+VRK^`P&R1YjkqXln2Vndm7)HkmVKlD-M!vZ)c29%pq`fdd{TSBv+OU7ojm4Wv z;dXNeymxnFwRAW99G_vMC&Na$Zmb{ff)(-ga0*@pa<{o3j$<=@Nnw2g*)vL-gvReC zUiW;7>4lF(W0WD8P?JogJC)Ae8yYAiGrw$@2x+rP+tD{7M9D z+m6+j;^FmRAC?4gTifRe>>@6~s(dia&&Kn;p&llihQLImf%_0zFirP|dBby9y$j;@ z(>XW>`NOr*70XVE!}p#kHt@J^fL0U&ciqGmpJ4b0cVm_O6}T)kgkjzG{y6+h$$s6D zWKUx{3G?`ic$Z%w=2Zj8?B6M5ViF@V%5U)FstcYzNJCSjHtPPqjPfV*kT>}vQtXTo zrz(#KUpH*MFaztF{=)LSjc^z9#^Q$aaHvd&&5=r29(I9ws*vw1<}j1#g<0`FSm<)w zwqr8v_UA&FXpAL$2Er>K9joq+zy?Vd8RBD1jBcoSAndW$7H7sANR zwLgv{HDv$E0FvzQN5W@Yk`-IV5X<;JGWUTKnY=`o40*5}zxtBUc{c(#V>@v=N)45G zi2~tNq%N6<1nCw;sm#IlZ!fUnbR>LeGCV@=!)0g+*xhBYtKSXl3I@y6URVTK!oo!Y zmWF3wZMqJ2n`^;ZPr${u2A+e;;j=Ub>p!?5;7263AJs&td@@3p`eDbf0Bi}3!g#U3TKDyJ1wdVmczbt|%j5|t(g+vpPUEz3DYl_$op> z!Uk3sD`53L5;j_&VOPh%wspgK#~dsznSm8&E@Pcb6*fO@!uEap5PDA@;j`iqE*^kT zeeQ!=m0@j1D%@r#!IQ_3koqd)p*tmfxHj9(t z6Rr_4saJR#>VTFT<8W?;2dXbBp(tzuGPTo@lplzg0DFYR9Ke<(udp`!B9`0igZpkF zoF`MT5B{)!?Za)o`LJb#`$O%pPcj0_@P^Z;?{JTD!SY!nuqJCNHd(4;TgC%~zL<-M z;b#!h=YWV+#}Q`y6+v|&Sa)SH+$z7pSZ_ps9BcQJBBk#nW!)VTnW{uq*H;m{KljK& z?Np)^_LGb_(oF`0SKz}!aoo4RjmDGisCmy&60C{r5rIe+_9Aw09U?>m5j4gOe#0hU zWvv`M9#vz>G7&7+3Iwxlgu?*}2i+y00u2aVCcugNA#S&3VA;zP@cqPN7m-c~lAD9j z(CdgWxs0g6a}gCW4Uv0Y5I#W!+ql27enSh~e!IZflAr(lH$BQyNl}Ii**9`6iE=+r z)^0sc9Ih)89no22>KyJ*87mO+uP^cGE#-(C;@l7sM_{|Cz)bl}!51?MXRv3Q6$ga_InOq&VEH#`9Y(J=g^_@*{SM`Ij@qzw0_N z{gLdexy|G5m&y9MFA2+NBD$AX5tZjTWc2;(WZ)`QbjN$4eSQ(HXw~B6@F0{ss3R|@ z2Kx@wBVkH6qTY<;`^YzJcKnDnZDm;g<10KeHo&z~5-!{ia~?et&c5@pL|p>zZUyjq z&G+fDNUZmWMnHKIf+gP|+^-E$)@!iS-UBfPSF!UGx8WSPJ~UIr4if|T%~FLsKdUgF zGqN9!d-=>UzHKRw?m2Jat)%Nw4F>#T|&mLE+&H>uE7_Lp?I7tiEC>t zaB55}4yms|f#VURp#zBr*I}ny2g2s-AW;4d*6+80&)r8@mgb119ldZXiGl0Ob8sv0 z!qVH5uxzald>o%*ovR2o&m4&Dt}_sJ#1)bE=V7O_24Z>#U{~05#5`Pu=r09`sLH~Q zQ@7yfaSZNSKVdA#&szSAuZ}zBlHwOnN$UG&WM}bh;-C46IH_wB1E-HVn65M^tK!%&1^-??STl-KZ&5OcKAmWWA*LZSaH@L z%gjf^(@h&5-?ZU1+5p}MQ{YpmiM4n7ut{VTg07|^^ob%O)eNyyObxqKoe;ZXH)2!7 zuxo!gc7_xpQt}mo1#0k%dkJ^#O)wVA?T;h&4=J%uCTZc5NQ|c+*<{p1oIl4AB4{Nu zMLo#)Oa&r+K>|OvdE@z-S-5#^4*nWdimC_UIB=>3nQQN3&!NkRHQJ0wnf2Il5HB{p#kI@qX{i>(`D5IQU!5uR>{Ui%p_UrP~t zK?HH-V-XiS4zW%%u=95UBHaoR%QY z^UJwp$x%IG`0_YWKM_mh=lvuydd2vqqKFsw{>H7J?KnH)E~+bzp?Ebxw#a?#ozaW9 zrBNJ~0lOZR*;7@A|UYndoi$*EVkMclG_z#p$ z5+EmiDpIPtk)Y9v=&l%qHP6Aejq9*^?soW{J%DwiR$=X!F04Iv0_)pOVWUSX0!KAr zN97HKy^cWCKyU2)(u3Gw9mKEQj)bfNB)lGr1anT4LoJB;6N0D-FAB;KB$ zgL`X~aY6nlYCS?x_P`2x3vG~UupNmf2O?(c8qOof5PUotfh9+=Y2$eK5BZ7>OB3O5 zV~5*d8Z@k48sH!i8y;)Gcj$_u6<|4kv%p6x-Bqc377@c${h4q=n!v3( zuvmc2Uo^30`de(hy9PVzUL(x10+CYd5ZyTtySQ%@KfW4?qcgC3!$TyEypP?ll9BlG z65@YT#Eu$+XpvZiM*o6;vKE%QJ%q6X&k^}=dTewjW%4gbrs5zHS5rr}{pliJuCB!5 zawC}^)(T(9*exEo(q@;R_uRxh{A@jB!z6@8JVV&m9f71D znk)8-!D zqL@8Kj@obROB;aQ>thkS+a1vl*CA4S8p2bw5%zF9!g^OAyqW7|Qw>CGZ9+`Y55#s< zBSE_syWQqskK#rouPMY{Wf|<%nTO<}0PJBk*sXC9@nbcy^KKf#ryRoOIjQiR>jabe z|Ku6{zvqJ|-YJrUQr0AEdk;z25KKac_>&bMM2Ky+Kha)RNK|~1$(Y)5BDvrIz6Gwp zvvDH0`RE}UmP+F&G*G%gfIN{5q^(?rJqSR&&pE`L<$N>i6e6>@z54kpA}2Q?O7s+> zgI*$L`BcR2`-S)?1=u}$1ojL$hvbsi*n1@wDNDy8#YGEytKyMtzXC~$^AP__4>1*H z2!HN@fOVaGFC7Du(wKfYPRiaR2k9D;^=2nY=*u9X)k5OqE*??UE9K$D1#9wqo;>~Czaa$z$x-3$*)L~!aGo%)1A@x@$Qg7%Z zb>JMNB=sWM#udAVXCOBFEF!a8u+`8O-eXf>dgyz9dN}8jgU`%Jc3m>rEjg5geaaxK zl|K+Bs3VI8-XSwYGROqQ)vIBzzciAj9YS1zC-+5U5OmT6%O~}HJcx{B;*0+%KJB(w{M@m*DQWt3> zjr)`7<+;ePzlDtG5y;RBM*4DoUi2azDf_q%!*9eF`5}6xE!W+-STP|0W)&L!>5;dP zl>6QwIpI@D(x5;RG3gOmd-E-E{M}0M$Cs%8a3Yg9orbB{5HYyrPukJmayr2oVx-?8W@Pdf9U{B4m<)*9g02(c zc(m^pu5P z2$viLZzx3|Orvaq={SA_9U`UdF7L!QC5?Im40tA_dt)_KAwtQgS`-{@)16-Dq;S+KRxP1=a7o&Y9u!+mF(HzLLzU>BO7v`k|mS6h>7*z zWGQ5Zn- zERK+5`kq8RmL{8|iiz7f8)81HjA*KQ6J`0SWOQE;k!%=>AL+?>`S}&@=^e*qrHQC_ zrCTsiX9h9EaM8Ce|;NFS<>)8{dH{``NnBXe3jGVjhoR%;h>hSnih z_zQU{8}nxvg_v2|3cQkukOgd$V>TUdIvPyA-hc zoh&TP!}{Y0Xd;J3>?e7j8p&QYe-hnMO#+;65D%+-VtG-9XuY3CR37USxrzcZnD*jN zh$h};6r!!v98IofXjn53wW)Ea%!@~v>nNK z(KsTL)kj3Wwc*1XXSBCI#0|e*oLe;lCqjBqwe>O%Dvd&sRW1syOhw*WKjaP>i=0Rq z<=qN zfnYH5>U@wl{T%X)*C4MolII5=Ktb~j6!P4S1G2s-8c~U&cZ*RRn}p&=XHneLjbhy% z6iK>bzi1Qky0VeAw-)>IE+gSg03u?nu;ybIEF*>eaaL!et=F;w9Od zb&f21l|yU`GKuc{P%{1HK_b8Z8yTi*Oa`npMR%+Pp6V6gc5W3euGoZn=7gFV;W%{a zH%f2jqDZF|g|{!FV8s~Z-|RyE)4?cM^%jMO3$fok9tW1TqG~D8QemU0}Y73DnE008uEuvBmYkteXa(zjE98aXlp(hojU}7jq zIa)ws>U&7gekHP8CWhEO8cp;yEyzqw2{N&(l*q3BOvH2Nq37WfJm2Vombym#AGWSL ztmmkGx3?sOWM?H=WlQ%>BxFR&mQ`eLl8llh35kj%?Y&f5l8_`3l_*&uA(d3}JMZt$ zxA%`Ou1mSTPv>*ad7gWH9&CP#j2TOjq^gf-4_$bAlkkhe=6Srh$mt8N)$Hv}PKtsX*d_dt;07X)Tp zL!kOs1S*Stu-_UtN82N)TfTVq#P7aUeCB7t2t96!P@(e<>#Zj6mwbe(Y(TKPG;WTJ z!}W9n_%?im*98x_&su=vVivW#1t8c!1YK&hnzoS)TINqrKmGZZt}w zx#0~OC7tJpoO1Sc7*F-D=9HK3i$4cE@Tpv!y>*9>zt0QV!wQj7RD$^N{)id^gzX-W z;2Ki|t%}3VAo2V67>t`Wdk}OX8^IeB5Mt_!&>^!B`dtlS?qU|JGefw{R)mEK9RF@B zLWVTqW_UFMG$Pe|qwJ3G`qz)VnAt>E$&WmI z&WSejUBnB(iiw(7j*@m`zw}ym8R#nJTQjuW>5ngpWq2j`5e2*dBBx6NZifaV;op8l zSEeGO&s>CxJrw+70)mGGB3LRI!DhP<{9z74f(|2eUnRn(A4gd80ff6uL3n8r!e5B5 zb(;}pZGn&wR|F~9A;8=SekOO}6_N(`!GdGid<1q9$6&gDXZtz~J2L8WE(7$G>D4un zuC^QL;HX5~r9HV=Xcwnx4ddv^{n)>0vG9k?pyFC@v`Jc^W`sXptDi)njVkW_+<^3* zrbtQ}jo6cBh`c3*@XiGY-Fy`x7ab9@+6W=ONeCU_gV5*E2=nS9c!yMkH|Zfl^coSd z5{U5Xg9t^jKGoF-Ro#FfGjTTxuZ5p!CA`iGjIPNM$43yGo$kZ5w^jQ(7JX(^?hXcQ z8%M8A5_BzS<`LUeZdxfx3qNm8H=e*TTRo^fT%Q_ldQ>{?NeQhG)XsT^iY}*6G_MNx zMemn6>;#gRtwOw*(W571A>!mlgcW8Xv}P+pLtZ2Fr7yzfi)ZK8OoRuGMTB_*BC4Gb zxj<+i_o*Y2I}vfI3gJuUAatn6^vsaGO?*y7qik@+Sjq! zkx_L%42Zr$uk@}wJ^DQz2VdgmJ8d|$*%9f$Dr#f^0Iwx`{aZnVst!x>IyG`O>#gPi-Z`Dd(!;9|BO&Pa~O zv0=Kfjp+uHcU{`oajB8fvo7(5-cDXxZN<}#i99Nw&nm(jv4=P`)idJi$Ddm$=#HX^-~5UFd0 z$T52ndHf(ETY4kPApud1)`%9GndnV(5It!uqC(=t_lh;EZAIv*i3p+_uGf0Pci9eH zoOc0kE?GEw>pX0}O@PTpyY_WNzGU=nd)~17%S$oobhBvW(LLGRYGlo&1(r043FNpo zZw`6*n>~Azs{f`^X5Cse?y+t0EDV zvjtHmgAiqS22u7K5miwvK7TNx!|cVs*NXE^+@J2Qh4OC|nYb`?IznOR);6Yj+}P&|}2+ zbwF&-^@#p@5z)(i5pC#>=nd+Kj#+>hiAKaswL#2c1;pqnBRc6hqRNIM($NLsXDbl$ z*%CK5*$6CO6Fvq)|IuSMPA@tHN1qyO?A?r+row;qU-RJYl6poz9KajZyXig8hG)9m zdc`|>uC0xE5}a#5-GQ<70nx-;ETsmycS$+k?c=AsI0}E z(?4C`=M{fWazM;RBF1YVVxl7ub5;C(UXZBG1yK>X zf;%0Ha4j{2SVkc*dkg$W4#t(8%{afP5U!V6apdD(Y)}uujL`V@b<~bzblrOf4z#7W zUmnlwJi=os+T5WCu6T5nv!9>iq=|LZ?bycNtCmq+?=a=}_eHB!9ct{t@J8r_p9y?C zZ@&rdb|^)<;V~qa5s4A|5$_nYl|P?^7%pV+?R|~_buVDR}1TYI>NZE{d}G#ZNM0< z84O%gLGP~@c;<;aj}I8j9d#L8c`uK1%1SxeB#XoQ<_K;of}O{2r^11Bw3+0f)_4Xg z6GotTQ?l+}G z_ayx2b^}%0uHdD#0Ulq8N3QfjWXbv9_O4S%?xKmr=vpLn^FqAfnBsPcIlhAeVl##z zc5^LaC2A0}c_E^G1h#s#8zNO45Ux{zkX$t(cMHe$e_{q+ycd@)3jFKz6P*0_1&3BG zgZ0@hm@aUb|K1;Iy8evW@|}S~r*zrrFx^kS;PHlq+;yiPS0_B7+1p_>TrrFzCW8G= zoMzW)GE~~7O-Y3^{H(Oc`@VW8@tKMznliX=>xerG^N<#3ij=_%kQ9)IgpVPJzr7W4 zKe{7sSqoxc7a?|8HDa=*5dH6@;NeRUnd^m!(T)fkSAgKGRs_a5!T;-8_zHZ&%k?16 zp0&YAO?e!6(FJSPwPIRBdwqKvIxxm-5^r`{!^=;D=>D!ZPpsI>-9Z*y6Q;^}Z-Y5? zojylSo5KF5Rj4toCzVCtBKfNVzx*`tL2%k-{U@W)=`0?kNFv*bNEh0XRLxzuwQ~a! z&+bRUL@mLUWFXGr8e$6*5ZmV>Vl2%Nz1a;>vuh9`aN%&nDF_{zg`lWCxZ&f0YcEXT zqyGaJUEaX`nhujGN) zec}AdBQ&y|$WgOWIlz4nyNTyYWtlgnO1021+XtV_E%EB29-fvyz{78)xO+JR8DX!H z`q>7_J#~=OaS9ULhai4qBjWZnBKES_7q^EX`r%DPB^4ra?_q?uUO?C+Uxf63i<`b5 z5pZT0t`>&i%H)N@19TE+^5((mL^<~9>SC4cRIs9|eI2%zjQNnyo2N8+#m1Iry%p%B ztH`}uw7Aw|7Z<$FChTT%^xR}>yC<{zxDa+QIYQ~=k!bv?f$E}lD6e~hXI(AuNG=jN zXJwEnJgc|QMFJDo#KcXgFr<0-j2wxgAn17BW91W2pu1W z;9i~x6m?t|_qtzAEG}<0#`$;qai(7xPSp8h@8q#qdHWOStl7Sf!&4aBL6bMLpYTe& zGSB{up_8Wr_nD=0-O*SsEVU=MY~z@D3pnufH}=r&MAeapD08kqnhv?+%hX+XGiec? zkH3z5@p%r9yO7q?0m&`CjdqC1d5-8y^@v*i43V8%5gzP; zuukg`V%!r!qyC85S9k%6Cd1Fr5#Gi1IKSQ$Zpnf>^|8R7xCWsWY6h47Y+r{*6Jzze z^JZ%gUimwm9;Q#})U=WNM~vtC9WgY2mdj~&CDb=d6}_U+n`kw#W9NUAUAG^9+9u+lq7NCfAHjkkj5!2_yixnZ?E7v-mO`lcir%$dc(!UxtGEIHd}?59)AdVk^>Knjppc36dfTkZ@!l;-bXe(XATM0cnVu z;)00skqF=O0bx>&2ss#rpu#u=*6l|?P7wV2X$W1d1-#=f;r!lVaQiwAC*IUzuVD*T zmfL~JaqZXR)-T2$)nU+b}3ez?Ed8h3&(BE82Mq#UwDlCM7!Oe_#*Zh{y; z(c7wu{#VRP5qic5%dy}bwSYXu?V~uf&jsH`LA|^->eE;o@0dzI+FyCGYw8& zGqCSyFjkfLW7@*@eS6xYk&F!<%b=UK^bx+Oa|=K6!c5H(DtQ6H>5~z(O%{RwX5)tGBwX)(Q1A!dLKEeN zivoXfe}4(i>3?y+Q2}dKg<<;m|M6VCF=A}~X9ktm(zovgo(nDHN!Nos6f}{xgVM!J zc!V=oRC2tbG<8;4vRD3Ks=I_y;oN)tOL>7GQNegu?1GY(bUgWS5DyObM)o#Qhr94u z_$wf3&sD^iOCdI_9?|z25Y|!0H=er^$xd};j-iS|pg4nI25q(I^QIA>?VKE)yUG)$qdlaGl1qQKZ1cIZC5hT1} zH(#wmpot=`zbh2|mGF%3Tnx|BZg5?hfFnZBxItD+ctzaW)sZe;z_@Pu3^v_M->Bd8 zTwF<)`4f3~*)wix9Y!l(RnA&i$cdvLQFqpD_6eBBE_gm+2O;#vnb1d zhNrgg@bJ10?m`6_!rz$k>L8M~+amtDGGb?m*<*efB11eyhDCsw+Xb(SFKXV<=ARI1-gDXE{RKX4qM zddA~b+7A@zWg~C0DefltBV&yLQWYDJB=Gk5>td!dT7l@6`3IH=T(HYenVJvZ-gd1MR4*}1g@>dH5fXftq^w0p2PIO-1c=0 zab?`9FAPq6M!$)b^!#VTQ=LP2q(dO>3R=0$$%3hP;X>jakze0L5@IA!2oju`Qq{kH^A z?BN2$)TtnPXc5BmuOaAkl;GcX;o_n} zaQWFCyY?5r%*LmE9doubE?ANwy+6}0rytJ;8SvC|UplVH=hka`xnipV=NgWtVgFc; z)EU45tJT=;(HeHxQbw6+5@&z4+#y5(2*~= zfH)-=#8`_N*8D!A%1aQ{?1<>=Ziq>#QQ!)Lh)@RZdr(gznu`TzZucD7a(j} zxZqEV1P5Gz(<_y*w?%jf=eNH<(pPq6+{3R7S?Iy5^0#=t$%U@^J?Qx0IJY@J;!5i& zG@Gc#sr}SBYR~{`FYe0jiG!$W5=>bY3H+%ZhOd?1QBkr7#W%a)vBnMLN_rv7zyr4h zH+w6$I}+aHA}&!Iu~R!EW}o=oFK71wRD;>0mjEHYz0)5_89#BSXf)D0=Obm)P$b z9hvsiaA$6RWcT`nJ8tigxpV_=k9~lI($Qi*$$|gc894WB7#x+SVR6TZ_WR>pBI8H) zWylw4UM(2Q3tt*|+RTE-WS4X2gn?W$wG$VN9#0N*p#G4P9Bi_PJ*vW0DP0Lnk7+Pc4$%e<882L`i6Z?2qZ#7zvapS^KM>tIUbI6<-)H>ah zoqlwt!a*NO3<$!{3>kbFXNWQnXFR>Gk34}v=UDwfW@3M&P4`Ah?R+E$OhfXLDlwCM zMCzfwNK29x^HvSAPpRSFsj+yV)q;m_43W3N5)XGsAvbvd?))r4YN-WchnXNS)Cd=Q zEX1)%kA>g8y?!@6-iYx#4>5GkTwa?uoEKd_&`qwH$BGNMTWSl}4w+8#(F&YCdLhS7 zu;I|fV6U?gRR5Mi#cjcq>^K9zjw=bB+%uFSn|R#A8hIU?kn_D6>3g0d zeq||wwhV;V#(3d-|B59a{oAj{-N%eSug_5XOkQ)p%Zo3{=w>~i$EO%^_eV>v>$QT5 z#x&7*vKzf(Lq&arf~> zWTmf1#(ODb=y@VT=&mwLy>RER;Hz^TaKF(Oj|_LCz*rZB5h^I^K>kBD?HXhaiNS=)-%9El$kn0Lz!{b^7V=Pci=4TZWeJrN4>|y>=+^ zOg9srPCc!F|?Rm%9%^Mal(#J>Yn#v-~64_(4ImSzmJp|tBF5(5<)YcgsPw` zD6Kw%qW3O%y#ECr9xX!dw;#B7Z58g>4?^zhNIcLPiM%LFJdUqNVf}SH?-PX=uLDu? zyaJ_RQYclQi5F!jP;_T2@{fJQ-8Ugfz99I-&7rs~bSo!f6kyfkf7DT*#rR*J8KzuD zf77diliDtHyqY|*L!0|NE^?!U1(!@;NRzFqoajytPxNNLpK=ko*C0g~* z;Co6GK8T!y@*o`)KM6qLR0Y6?k}TCLa3TL|)QMhHS%Q@BB?#XrgUe5acS_2N{^cxjPfm(aq~4p=9MH#(-4C}4 zF2tU40b(x9UWdBXmr*TWgNjRHHvA=et;Q`VJm`o58z+I8mf-Q%WIWNcz|#r7cvh~C z7bT-m`mqS*DfM{M_7Rn!VCVmKjI*657a?~Q30-4PO90xR?O{9uN58xzcjG0Z!M*QG;v$^RqW53A7Wh8Yh|R;6u?fQxK2IXlyr zhElIMYT6zS^c+Ub*2nC0`XLpkHdCrgCYmI+;#+MfK0FgRkmqi^l3s+DB2VFk)i4zQ z(!>i-JCrC5L)owtl;8P;3ini0%^iaG@w4%9=U;r9bOs-Dj^O>9Z+JUP7O!%`@VwIx zJZxBn^s!=&3=*1|{vxO2R6VRrGTX0*{vRgn%wX7kp=p3LFV$Gn{e>2t6)kwEZxlCu zD&(^Li#g}*6;2(a$I%WKIH;J^nz4`S<>plK7Cy=SYtUS!j~_q$QQh(lRUaJ z@H+A;ip3fCXr!2Bv}Pk}a)0>jP==F#A*>8)+Sf7t8xuUiutqIjcQB#%xcfXiqBotl z+0b4#j+@_4<8oIsn#rxC(MBtd$ymc7y_T@og$L|1@(Pvfms6&^41e$3z^|_pQKPXN zAGKHG?N?V+OpL?pZ=dn{Ni5#@Z$jm0BUG6O?}SPOW;GmR8$-&!pmOG$hSCxj2}{nx~l=7L>V~6yoFV-_V-}MqEaTrrZHSQp4V?X z())N{o^|O?=VC|Nf0d`*+rwPp7s7eNt%$1uG?1+4P}>>o{n3zJoxRy%o(JU&izqRB zB^rx??>%PV^E8p2r868=mxJ+kvjyI6AgX4z;oaM5VwQJA_2w=3@~HyfMtkGCtSf$W zH4|SRsC^-DJNS8a~_|g!jU;{XR8V=z~_^)65clwsytWJ87sLvKc=X zm7q@26?N~dQ5Ss=KgaJttzi&8A8kfe?njh9O+dkx2gnlsujp8jWizM-PC3V5`9Z(^ zbG18&2|s)pe)K*A44&|Ek1%>nsN~7(m*|jK#jWLqT$Ss?1xNOCdS_dX^C+jT!fW<( zU;h7oijHl`RQRPsY57>Ru60GdIFG(}oQ|)(6h+R)b9{>3icbZzQJs4gUu>?R=13lD zpS9uVkWu(Gu?+ROzG!eXKtmsqpRjj1eoQaJSJN82pL-auI+fwc`uVuiYb0WNMZnj} z9!`&T!t#Mv`#MgXVxpQn!((PLVCP3(UiXY1o<4Mux8sp3Z@8^&AXmTA2}lYQA~fi{(d%d@B;a_x&C4#i0^kg8Jg? z%`x~k%Lw0>|HaSXO#Bj_>4wjp@Y`}cet*jnKPNQK=SQMWWNFp3^$}}10OiJWQE2`W zckT;*+W9wpUGKxG=oBm?bK2K&!G?(=>=^#$j?kEV=jG^k^!PfQE?Z>iIQ0s*zqrda zwG}ikI?NfTjX6=flzNd3)SfVvnq{Kz^*5rj`+mw^Q=r8CApH4Ji~8w8!?;=#wJJ04 zP4gqZnKTPsjWNFelo8L7(9=zeM&rR2G_4ms&FyG3k9vteXQc4kGY9q3{;0iXjZec; z@p{`gJe{I}?5zT)|Na%e>0RLTY5^?0{>O6_IFpHHg^cK+&w!fGyxjbZ=cb*cOOXj3 zV;6Eq5xG`!6Bjk#;LO}3oV4{HM@pJ-;8`7N4d2PmHJ(&?_JDG4tOcI68?CD~&=_Nc zx<`8WaUccX-In5e#vuH-yB2jD?NPt06pc~e(NtEC=0sn#{QiN~MHkR=;S>HG+<=Ch z2lz2&6{=5{>bt!QAG#{477qwrHgh5Q)s?Uf?0lYD^kXMYa z@|=%9PmLSIqy6o^ioAs$Jewq1#YO z>mNj$=(GMvtwTePKB&vxho7Y)vq{ApzdA*rKE4o*X;NtVu^BC$718>3BHB9dMcZ;Y z{BI? zH5^UYQDF3nrrwlUHIx#L?r51h6u-?pQU5yz^$LF=qT0|f#Tbp{L{mjGT2!~<@6hdN zt5v0hk_IIPy}&=gANV)4)8Dci;tfd$^Qp?p0JeP(|6}nUwTBgTJb#j_w=SAE~`KM z66eBs$T(4lUi&)UWiT<{k`cN0d84u?eFm!0Gr)kZ)@?ktqmFx`qq%WhDy^K}bB@0x zja)&4Svl0Hj$yw8tJy>S9XmaIOXVO9%H1%cRP0OqJE4u1!$$a{u@g;NwrHB}k3WWG zXntOW)+ZiltFNcT@fDO5c`1^$LntNPMoDL()n0fPt;fXvvYL-?KWE^b)=m_skH@_% zH^levD7ep3I8XM5<(7%<>-cq*iC?{*UmBOx+&)7uiJ1XMbdXK*ctWFtD z*XhOR2IMNtMf}Vf`2A^u^SD-6E{bedN2as`la%{1a=<|bT3OO3WtPYrE#c`Q&OB}= z$9D4o`m(oa$-%>+t*%EP}=o@f&O z`tQ5C;C+{0cwsE&$;JnW?&_7gNT+oYKvT zl-hp+|C+m^>3svfUwV&str95ySBZNU9*A7|@$j==38w^GED33UpJxs*X40%iMmmmU zV9{3k&Q;_2Ru`VGS;G^9_wc~huH0<=j4MRXwZJ%u#(Szcar-WgG!VHUkBr!Rx)L=~ zKT~bi2`c?vN4Y0UDIGI{k}kHCm|%u~a|Qpl=p!XGMJ`SG6-vHPr_`4vl>Re8^vM?} zyS9n4i`G*{_#323d?_(v2AUc#;rqH(cz3=lig(H)r!EI^sz2bH7Xv5Zzg^PJv;BIE z(Pfg*>_$eNl9P z3yaN$?dzD|e zbV^xnrL;yXWjZaP?4a(H8!qSSXw{PU5(?@g8XHf0Vf+Z<8k z=PP*Zr-+^zge!}yaa{No7gZ>>ufzHalkPud)P!8#%yFXMt`EGpNMKmox6|o)ZyxRx z$ZdLkX|1%1i=!scR4bEH&BKJg;Wu?<=5v7kQuY-7qb~pEv7_l~D%~ofyzy(wR1Tr^ zdQvLkxv1wDr4?)`o%5A4Y2qw+_?mJl-6(G=JaE&xP|l^8vNCT(c7iF|@-)$Ca}hP& zzn~)b5uUF0MOMjjMCq8~@~Rq`1?KH(m^l=b6G^ zbT+i);orNseSj<1s`jTvW&mf4KFr8bgJYd9a=1wewR0udODT^Uqn}bu=mnJHE>U5i z*t_BGl<8M3d@oUyUeHJE{SwO9dQo<1Bjv1iQC{aS<+2vCvy_!;!hv9E( zAsR+}!56{*ys=q~LYYa(JZ*!B<}2`CA&sL&yJ4==p6ARwc9*y0o-oQQltEK`1t&F{ z7ym7!`zT8~hb*8&xdnH0+skzw!@1;fE$3L+62ZqfE>*BGF8UlKG9!BnpJX?iaqQ&q zl`1hdRFrq1+}^R2c@_7ctW`??cA(4x3(87wrR--#$`wjc{-6To!|qd_5|q=~O&Rky zl#Go;>+Xg4b$=Z`n~EIGPhluH-H41K=MkRji%Vv?I4XY$3yuHB{_u?AtwHk`b+5O` z`O)Lm`Q`Ll^o{Q4LU>ZW2aja!=T4=bT;Fj#ElbyP?iM>TZz9KkUcwP~{&DbJH}>&Z z$nIAQsD9Oj9paW#u||dRct_a_BC9EDDP=AtQRedi%6e>}+}bsiADTw_z*@>*UrBid zOUmARKGANx7JT*bbo0E{j%iM_fvUELYHS38t~+Y`EJ7dFO`F zSZV?%jTQa7_Gk{hW6b_aW$dZ)lU=GuvEw~$DrY~Z!rKSpEb2kI4x*NOHk56SrQD$| zlvk^uyu%f-H$IDXF{IpBPs#-AQEE#F+WwBhZ~qghjqQy0noW3lUltGjgOTEW8NrdV zIA806!yW1|&%V9BBr|&@Z=FbF^!zOh9`TjehBpdq^aanx9i@xeD<0KvLqkZw&T&D zrQDO>n>KB4xco;Y7ldYW<}h%|_RrMc5l-EH8>qd1Bzql*WjDi_?DQ>=9b(F&@`LN2w%7+&UqZb0k6K8d$GN~B(qAMx9-|8I5Kba-*S3Dh)h)aQN&Q92jQ7-qBj@zD1v%6|1Ql zyOhd%PEb+wUh=IJ`{B9R_a7*~;H9W5fbs(;QSQ17WfMnIX1)`p)|%sA)@A(JcN{-o zJVy1yF?cf}3`J@;kTby@2{#*YV{tq@mcPKh7uuNP-##}g^JgA!RfRM9)f@(YSWAE9 zPV`o?H+wk~JQ`pKzRB{??wIQ4cab5L1#_IX#w z9#_rTWthl7xK&D(0niUyHi_$~1--d}r%vffT8&^(5$BSH%RQ=#?if-`k{u-jrXOmDZ( zhs)|Th{=6lFlL-NLsq<{|JBjFEak{^7e3QbQ4QzP^r%-^CeR`Hc!A#J=}wqFl2I z<#ZD$+ovz3Gfq+Rj}iVI&PTJf41U>02>od_s*ZaKj-d(pn$^g-_7Kr-A93~lR=5d& z?#|1KnDr>C{d)9GW%AV9j9K5EA(yM@|LHt0TMp;BhKqE4Sk2>aYIxAznp-Mwa?Kym z;%OA;Y+p?l-{pi>PmU@zqRt8r4mjMIy~a;rw}5k0|LDYyI(bytzK%+l`cu&=lL|ZR zDSzV~s#xsl|#WB1-@)0jb-leC72~X?KW3m(&+G!fE9}rla^dx#Fweqy+ zHBRJ=r2QH_ZYy;ZujetXT(fWf`8K8@aLbvdNf<`ZQ>t%F!4vZ z?t46a>LlJrP>vK?;XB+f{Pgw?IHh|5TO1Z*W>@+4b(pm=*~ft~-*knh;S#TB`|^tA z8J_PJPB+DabQ(9GhZlF@_LoImFL!}fwK<%3*_ShvvN^@58x2Nw= zRM2P}haZMZ@p*&>-fDM6iR=?RPESL&fj<(LDKUL$miYz7=50vJvXTU!!*U2YfQHK}D~1C>HPjc$A@uEYnel_t=S>E+YGWLJv4)RKe!P zL`*;ZKh9nI(@btCVC=5R4E-y`03Un$ELp<~6K3;_X%{+s9-(8;{oM6xFm3u}aYc`n zB1d&0XH6eKBZnG}-`mDfz4NGJZ9{FnRQ5hl!XD-Y?DAnDJI!pO>gp#{t|_8oVxHiw z7E;!&n9_Tni~PSB{H?r*rer(R{pp4p=jr$`qZeNHb;h&5X?S?QJ2E!BL~Q!vlXA&)ytJOX zMQ(tto*`EbGo<<3Dwlum4*mZuqPb!ryFL%1dZ!SowydDa z!uM4CB<3ccFv_kpq;&7~l*n^O>-B8>HuAyGh3E0*b`IWIpTR4=rzlb=!u`@(q`Ac) zM(qy*R$at-#{wKbSB~`;MRV8GUZo{#fng$(=!Q-_eIpMBdv;%d;EH!AQKKlj)v*%hjYTU`9 zdZ`~fURX+%WGO0jcNgoCP1(_Tl**eJ#Rr9^V<@WI*5U2x=O~@E6NP=Q z;NI`MNKGq1)T&PK7rGeFkrQ#uX*bsO7Uc4z_BkwBp>|9$%Vg~DxeN=J;f);?^j*}I z7p+3*?pnZ;-~D*hV*~eoap&eahFrbqIW5#aaL%cIoOb5|CnY_m{)#FNzp0XzG5rJAlDJ7~pF$@DShuL(?E>59nccf`LmWi-!BLH)kvs2#o%pUg^8 zkzIh7J657#S`Xy(yMpAVAtJy3BCc7zgh%ygk=eHr*6~4rOZz!0D|s?g>@*qI{{zF` z3!iOrCVhjmc`;=f-5Xr#vQC%BIxOM7pS83T_x2jQLtHYPR@T%gB_OC zOO)ggM}b`n%~P+9tJ&R4k6qSp61|ogI~?6XC5O?JziCRD57{C+L@< zMoqLWK14d|(fuF9VG|>1U=xuYF&6Hc#+Ub4T#Fd>Ri*598Jo@hOKoRCvu@jzL21f&sb^~c(Bjf7;0{?W!LW}>{Mq!)wu;!`f`x+$$FFt z5*Y7IW&EA?9KXA_;YZkUd@jF&s*AT!mfnOS!`XN!F%%iK>WHt;Mu^-s`1BEZk+U>$ zc-k?nOxua6ZYSHHSMQCOa#yTJ;3kHjozB41$LZ%^L9Z7X^w4+bsXMMb?xfB`D!JTt zS-eI#LzPQUe&Pbjk>U>dLZfa893QQ~Q6HakSa1Rd3SDF0)yeE>c7+;=W2t`Hk*Z0e zKhgH0e6BNP-0o9yLnvCE#-p*XCcYn;it3Zo@V0jiO7%iecr6t7m*t7~x_w35+GPkj zD~HREJHl;n8x9?ug%yt4;{6-$?dzz^WlCia#y!zt`27$DKJ8DxUz2&sU=ls9uA{3# zI!{R1(O%;nw@2G^eYqQ#g}voM?N;HZSWoEtazgHO(J$IjH(>$?l_#;^iXh?fc40Sr zH+CLW&W`;>ZlSd{ zzFmTtiha1Ltp#uAr8uotj00IESZ?M3!@1_|>bN60ktwZdjQf+w@P7gu)!5Ff#^9y% zYj{rnJ6***e&R?D5BIaqkPkGUJ)EX{my_msoLFH*edP`uen&%ObNjOY z(lYky=g#iN(d?X)&W`8msC+}rv)=_Cx51SXP3zGz*cSEkm*Lx78+??G!<(fW@nYg8 zJU(NC9O2bT9T$UWn>O4KHC>wB1+LQ0*x#)?mJLqFl*IOV`gb~BVQP;_jMr9X#5jN6 zT(*^0y=r;s=LnIrrpwd4&ho@xWjg3i=gv20Xw&N-S2VuoqFwhm+qag}_8jD-=0!9R zbCF&_5{GkB|*jwd2d#II*GiwXgWNuUW^Cl`N)lp_?H6Rj$UA0mIv`N3Uw8 z8h9~Yc+(=bjpt2&EncmjMsKqlJSXPv)93or>EL7@8MlFIIMzUTiCD zX%xJAj26DmwZOXs1-yFXjc132hB;2;MlVUjEz91BxM7a#dLm=4Q3EcrAF;bmytlJC z3zNMLwXZ|BA5$0PGk(7oBZAy{Q`{jUQjy-_ne=2F-DGyt>9@e+`pw}UwNP$aS1vT~ zQMAZk!nt*hG`{0N!|~P}H`j$DYkO0tMP!6nxUkQs0%}IOuxqp*)f!@`a_l%2<_q4y zyp|G&e&NsdX#A|p#h2ALQ6+RrW!}~(8l!-ROS>bpsTGN>GZ1brydUD7spnt)z{##- zv1@NHEU8z;yjQ-zFww3d-w4lu~hir2~vh3==C=LI+J z_Jwr*(a58^XSr|sciP1m)B0urt@JK%zIifDG{@2C*f&mae$FvnA91+)IS#hcV!sDh zskP9T-DE@AsZo+0hF4J`+?6tG^(isK5>2Oq@Z)MKs-r%mQg~iUmZspzeGlY5oq+VU zXA!^oGD1qz;dkQ-&fT96r+H(sJ!O}sJxJXqR%zR-83F)`FD}XJE`cpj|g?JhQDDRJdaqzxo{Hf_IcyT-5uEf zp96O9pNH*UHQ2Bz50vW(m5h^6SX_Z65=;L_!x7EP&RWe+ZY+>)9 zgrk24@%=Ux2GN(HT^ELpPRy+DY{$y$=~%u#6H8?+u*mo>=H%bQOrac1Hh01JA%z&# zr-R|sCPT!Fd8-Q#F`s_~y*qQ4UdZxq?j6>XYJZ^X!Jnz=OgU8vyVAu`rIZ%5j$--T z3+#+0*PJLiDjr8WX*Q`W94lCdjUp<$dQq(L4!LDfNMHUNiT|!b%&C5atv`SORSS3> zaDYp77#zZ`!fL@3m?`(buwylL4E)8W@))R%O2(?M?7=k)!7{bQkTO4yxffb6vy=A; zvy?GD{58aOII?%r0wSr!5SkrE-+W)vJ0V+o?s0~?MbhX$lODR}!?~shW2xfVD$4(` zpDvtHp_sGG1Nas^$yU;WP@B-wjcG>^gG$eUWa*jJ3%?Oh-4u z@|qEFF&kcw-ous5aXNn-tXUIx@bE+I9l410`%&2Z$qwpB;@#&=D0wc0tlm>dTjgV3 z+#k#;dygsiqcA~g0>tX9F+AxvL|S;4K8H1MTE+A>@E1LMsYDM|rRes#R=OI*evrMJ zsrz4DUTccv!Rd&7oQ&`uX9Nki!AC0%vx0vIw=pPufhD7hn zTWA%G#QJwtU;`agJ{v&cJnuhQ$GFg~7qfN#VEXj&m^An+-wTIfL_E)R^|nGtYc75L zR73s2E9mLoNyHQ|>X^HPTHnb~t^XD(d80+yUq4e)(i4j4UJe8D4Tb+fb$;o_sT-2Stmi{e%a#E>xhJl2R%eQAgPmeoD<^h zIv72bbAmcZ>5(AUxt@8!RZfv8QL#WC&pI<229TU!fw-`AL}aiA_VEGuuG#xXJ66nV!C|Pm@d7JrDDY)l(j~Ndy*VxUwM(w zW*s{7%!$nMxDL5dq!chu@cZE3&#Krol$c~8f5|9hiufbBe-PsO|3&1;KM2;E0N<*= z@Ywkh&NJ7;PEiU+V~ug(*b5kzt>V5n6`Rv4Slhb~Ys_Pz)Y}4Cp0`Pxq+;%8Y0Nw_ z3*vubF}76<|J+^8I|C~SHT2N8*)OSoS^z!KV;@L^B;CX~YN`K5mt+=E(GexewDF{b z#c>oCwvlsLo|5Bbbvn>CkG4kVaxS!xU>!X-QB|6X5}(`1-#-$W>c5bpatP-&auI3u z0Kqke@Kcb4#~r{qV=CxVB?$U?`No-+)}wHOOdx z#Jn42m?gIald~EzPA?53H_pNkr_+4@kfLweCt08GK~E&?=)O@L-KbHfmN|}8?Qn$( z3sNYfP?h4Xg($SwiM$6l(CM>*&`|xi!eyOoxTO>Q2%uHy?mcd_r+GxjrDJ- zIphXa-C^IV&?HL#J%-NLxKZeeR`NRRLk`74WV&P{Z8^J)6!Qew;P}Rqp>IV;z ze=Q%Gt>#F%orm+^n%P5y?+P%U|nHHybju)G#o8|NbRs08BEA0XR|R*VbJ)b9)Ys z+gai8*+VdS_J{L4lc9af8Jk#RttPV?tG5vpFZ5xV&{-^6une=8-^MiE)w`&Ql)YRGiT0Inq(ctKJBC6ld0sNwMJbSf-6?oku9O=jRbW1zA^z;=+&+BYQ8Gg}C|G@ zNnLitoE&Blr@h3)wwV~c#23S^dO-L>6a6^BegxL-JrN#F_cpPY^*;}449uVkv*DDl zp+gsDL{m&x90i@SB=?tXbkba$jIKw}CYhhK{9KG+9jpCNZLNgT#EU3s|BCE?m|3K~ z8wt7jh?)HWVP&Hbcys_hTkYYlXAh@9F`RmS4#$S~!OY_Z_Ugw%=d2L6zTFMY)TvMn z-H#P%_aIj)3+eDA%sr5a8Jm4D$><#SSgeCr=z{PME%sLoqPM%-=*fZ#y0=k{t~1N0 zaU1h*M=zzk?%R}_{gq-4T2hecByvBumrh)KPe!Yam|3ew%Qp-185%a0q1xA)wS6tP zC>o9IjZbmm{0$_E@Eydp2jP;P2<+|T-Xs_9UCMBt;syJmlVPQ>00+x8u`h5hbc=UF zOTq)|uWbiqPl8H%1?1ywA!Cw*c{7$WOSuV?SO+ymdMbt|jKJWnS82fN6}>HF&9u%- zy0>!?U5}kWjnj&$yzv+1xdc+G+Ea@D`HBK#^~in3W;!wSHtj3rtm?x}w0yeY`O%=; zf$9t1D1DoWi^>DY_Su6AFP|cDw?1N?8X??oG=g-7!B=A}Job%*GwZq?<{gK%;Xg2c zVGX0k5zu@22iv#jV*?ut(C`NI z11Y?FBKTgJK16k`6Urv);-cjwWdG}eH2F{@76>6$O%LH;E+DAY3BGk{%y$0_7ir$7 zQw(gJ6Jar7GmJk6VfVC$&~^^PrfIHN_kc71GT&qQ!6#V!BMtLCJ1|Qj6_dvj#?JeK z5yvAj_z&ydoy+On2{(FjV>{h*|4P^W8knaOOXb_%GN;UwQX)9d2@(|Wc!2Y=;>q@& zKJ9J%!}-ojNTEfL*VkZu2i5m?qHJXcF2?sEXV^BRow<)BQE9}c)FWbVB!X2`;HP2= zPvbXmDQt$Lfg5a7yK#6;8}`q+1B1;C*pbiLMedT+KF-GKtbd`n`5~4xx?zDz7iQg- zVBOzsj7?sI5j}0<)KGqi%0+W1_e>q7 zjE&WJ=7VY^WLL1~1N#V4WU>&{@xHK{tWnOboIJg2iZa0uVc07`D zStD-LipV8L5Hf8y{H0gH%jh{=Yc}Almmcp6hv0~x2@af=!JhPP=nQ#^t=>O*uil8Y zS(~6dVI!9902amuLvqv-OetpFNklyUDPqm>h-LI!L5AKpDAUt&dAdK^i1+3B)KJFU z@wbmCcNgCU@BgHzV;h;dmRZV@C83PN0s86v~A5#rMT|71gWb@N$>lQz4mu- zD7(d+pMyHc`P+{4WG5u=Ou_kS2NC(xi+wGo2$;;?FT;_{R^*H+9rja9iN>*;({Zph z6Z?Lgh8}A-w5~W}!#WvIYA06Qc>>uko@IS1!t7Pin96g#@veFp>C77Cs#^N9X9RsH z=%r^zmeGB`&D199K@Dv$sjM@Da%Vo^yZKX!8W~6a@gn4^ID)LxTgmYETw3qaLJAUs zbLxhR1-PVVgtG4gDC8b4cU2=Zi+qrrE{F4{ZXwEe5<(AdLqNb3c;924hGsLI9vZ`b z*=Jb2HHP_{Y8cPVfWBP;w!d)0#vS8;gbt{bPJ~=MAQiWSb8zQk8oHTH@dzWIvxaxv zHTv`ZE`8*@_h;7{=zhmGYLg#A4bPIOta}9Iju}SDmVOlZ`~msfE+*Fzwq&iVN`{p? zXubA(QW$zh@Ojj%!6n|emd~8WoSz2d+G`_2ZWfY%tit(Q8Hj5BgV5V92o$~wpB)$A z{_hYt?=OXe#sbzS?}f!kTbR%h?2dkm9l~3&$>=NAMbE;@(ElLMJAY}+XSPu)rq%oL zE<}rUavB(N@DKfUX{C>;>g?UNrw7wPZ3lP;C(OE-7j2X~Y=Dw=|59XY1o>~{z4_Bu zWIcwT$2nD6zfg-vg(!JrDrXvP zCV$yFa%rAIR=2vzaFZXc7tSGh*1}5skFTiV?krqN{tx8`ub@ck3vxebA*1#yQev+m z-a7-){=X2G^BIA!$HRC1YR|?By~1?>u(#Yg} zIbU_Sc&;A}mk-f6SS!%#g(2nGLPkK_z`v8#zXhZ@t3 zi4UmJ{vMStDxutI?OexMii{8?|6#AmB|MR=PAHJU56(?*P9^#3K*2iR<>1n-Y?K!` zqR6Koc?WN>o@_i)S6Lxpbr529u(rW<9D>^V`Ai=J&lXF#9&f{0?=d)O_7q1QEMQg| zkA1@~Ls#zrws}ZG(`q@^3W-5+TL%_5g%Ay4cry9@>R?@oIaz5Zkii2{&WL+L^7(>#hK8T* zxb)@@%5U4DsL~dB$u7tYc0#J(SR_QWB8ENa;XI27Rt>%pWlmGe_Ly-hCoQ4`MCyt0)W_ zQ%awo?x2_B%&0T9pKgR%QsezARBkE383CMwqHW1_tR}xN`Q&0>Lsm-NWKj2)G-Gs0 zK2^}S-8i%dHG}#4_^ybeSDDD`x{J&^?nvz<&i?U1thhHKbmt-XVln&|J%U#~->2-I z;j~={cH9eCrRBr?*%=rs41xZs2iP7chYhTKS1no!rKa6j^7|C#`)On5Dem2?hhnsr zHij+Ui$R;Jncr)|jDsuG`C%E|xRp#z|9qkHV9pFM_(;h{Tu1l~^85IWTx?{?YA(ni z_X279k?|@^-!^fIVPI2>^nG)tbInfU}uQLglfdz zKZnRgJjV*jM}T-Qyen_R-BAh71_TH0A8oD+p-S{Y(zSa@Jx{g*JU1VL`9P`BRs(FWuZapPH;9sGMiFx#3$W z*}aV-Gov{3?+m#dapPQdBQgk2Ak7mtBp*0eunuW&)NH(q3KL-zpE!+t??PmyFz2~t zEfT-YLEMVVhz!1uP+?|p2e5xwk$r5xI^f*ngVPm*Vf(Nfma~S#^x!HO#*D=&`rPpp)zjCaxAZE_j2;f= z45ymQ)O0hADwyY!d#jL=iwgKW=bVY*UF32+fS*SQ83a_3rsYJE_c9f%1HGuR*Fi=4 zPUe3sME=j|$X*nRG^1i9rG_Ey`zP+Pb|I9txPkU|@Lj9~k6%u3dA^^yAcZ(F@C8R# z#lY;`Gwe;zhfaVrwhqgI+Sh4VdE`6f{>jFo(e04%(8A=y(HK+Qj^VnjCEMUY-v;eu z-#?EbSD&YwA3;sOPg2FMD9Zi#fs(I@P$aVi{3kk+%ju(>sb-?YIh>9vr{{EkJz`yui65hmAaVa&Hn815er z;S*fP#&&wWa56nSeu&yqeaGt$^KFFIZ{cfO$|fjB?GO=aPnPPeP!PrGQm0>mk3s4^rbVKvK5{Q^t$% z9%LqlH<&><;UmwgBj~kBKRry>r1ov9)VyswRm3Gz-jXg#{>3c)Km0tFR+5V!_g4$w zkwMHD(mXPW>v$ko#{oUmTpB>-sC<;@1)w0A^-J$JAzghnk}nt`eu4s`!+s!K>L7yd zuzxt_J-klU!tF5U&m7Nz{nV+j-rb2q;mJJj?Sa01Gql>0py~1#tJ5SP|8gUwBHSR! zJuhj${W-7b}yZ9h^)^-IdzSj`z)<(&Dqll)iv zabHqIR?D@>ASr`1t;X?tr7Kv6RUT??hoW++2}<1Oqu@Tz8f06L9u$J)0Wri|r!ouV z48kLo5WL|II0F@4pC7=jPXkWh=E8yV8*Gf4V8K3elgyXcebfuv%QCTk_Ytfy`2z*d z>yVz(h1t^Pm{K(tV{6#E%6n+x54rSxd^o*s%b{-J3~G;OZen;jRXkw!sOeKmS@eRU z=5QUG49O*d`zutCLDq57wB^3U6M}V|{)C!0Lr|&3yorkrxH#rIa%{Sh-gy!!Th}rh z(ho8Fjv!*N1%j(B;P1=#LhHqFKYRtwc4;_0PZPGL8*w(^$vJm55!Gn*rH6jMzjFmBxlj0n1l!K%;bdq5JsS+4|6HF*cIW-f&!e_{}o4q zIHxKS zVkcAx>-CF{BUkWj_JG~9CVo~RVGkg1;1A|?w;7@5>JZ301>fB= z@HDFAdx#&-NnFM$eKj0&{Dy-^?_l51&(IN{fXxRPZt?vLlp?oc@rAFLyT$<1Ox|Ps z2p9Y#WsSil@ys+~9$D#N>aN^C9g3VOATC0co7Yla>jz44>ZT}58}j!UOfJ_}l9lT- zGU!~x{q9hb_qP#zuZkw4R(}vGzl=tyPcI5*&O&bCQDkmXLh74JBu3mr+_t%hnv{&t zx5Ws&Ck)??CGhNYhU@zdIE}mmyKP;tI$8*GeO2sxSqhyuao8fW31Hu>^5REWGB+A? z(>yS(v=!qwd*h!yIvC9C!5?;0nL)gTy7}JGu~mUuq%^3~kUjHvx+%r)4@I49C;tG> z@#^?RRsm*Y@Z>FN`ad9f{V;jwT~=!4CM5wMuk0ps{Q=y~>H zEAQIZeZPkl+b?42G*Qg6nStpBdzs_FxvTZ<5Lxh=nb2#fPxce_OdLTS%(ZLTr zd4vyT#Te7U7WW{K{U_Q!xd`~yyP|f6++>yUJm!YAK~oKfzxw#!B+M>4iA0- z6We;|Z##r-`$j-5Rue0_H?iJiI_BMejOp1Pm>{Nre?AR^$d=JGF!LGp>93)lWe=#s z{|vS89Xg_{es_V+-J7 z?gqziO*sC;43-s+FdcgryKmlwRt=!uAq|x`rdW2;3iCtCFhgS~CU{R^pOFqk!oJaf zmm2k@&ZQpH6zV95q897ZR2edz@&|cQN`VALg$|H^ni07?pGsD_e17~k;X3y5cU~=6 z$4CCXy7LTG7Y*4@pNXO!1IVB4gse~Vk=B~dz2Z*9`${0@v?(G^b|B=8Ap-nA!6$7! zJQ}j#^3V}y9`@qIy-PR}I2Wd!Lt)_a9NW!U+sj(^m50-?jPu9m_Zu-s`7>+deqyB4 zXo!>@rGZK>>brcLdK`~aN9}KFIX#jpBYP=d}L zg86sXL$HotDyV(4097}bH<9xaMeZMwzvn5kl_HToF^_x2{fK|TbFA)Ti0BDJ$m^R3 z`12LMQzGEWoEX>bN;q4ui<7LoI%@2K1BtdURLz0*2z6+DW&Xj7M96M-!2-3Pn8A#w z3BNvIWDdVq_sVJDbuab3n#nxUYU;S&O)ajYsWMKC@`v2vIxcY?>~T)Z;5z={=ix*K zzd}ee%%0El0)aZ3hNzrrMr511IpvOm1{!l*8FLIw4s?FcW1^zxp zl2zsto+pkX&7jxZUr7koF=h$s#*aevKjtX=C5IwrVdQu4^QbwD^xUJ|D}G19`CP;% zh4PvH7oiQV2z;0a-(NT3HSGo5mXCmwDqwejv;LR?dr-n3dlGo|zU~v&550m_9W{^} zABhF86ESn|8B8?yz{tPsc@|$wKh^$F|NIW>sa(Yj<;&C(KvbFF&KX}(l)^ges32kT z=Nt}~NBLxx(nbcK%t_Oif3Muz1?!kP7jZa3HOeLR%tjV%0gO^_h+%Cw#DQ_9)7$Y2$>PH0iTwP5a{n^wKY{<`pGckpoD24OH zqkN~5f1EVAboP-|tO}ole2;S1Cwb>O!8+#DqfTWRs?~p>e8Wc+ukYpDIOg{u2pM43 zn#O!2YE9x@VGW|}R1g-3wz&7V<(?M z8-|Hub?#2cU2VleS3}HvP=JY7=VH{RGKg-wML+pF?l)XUJr8uKiU5RzZej@c27vp5ghGnrDIJABy z_JtH+=g%S7;B1dI6J|hOvI`5Z9Kfsza+vr%45RqYD0=7w{Ve}W{kD^-=Y<$`{AlDl z=JD^<56b&Cky7G{Daw&^QvH3%rA>vbT=;%|XDMkOA5HS71pCA$#gC{n3PANa6O>0x zM{&9t3QD?=)A$$}w<3}Hyby_h9C=q5gy_|G5N>LOV1E_%tS*N4-88trRfY3=Z_Wj} z0^2c9aOlT97|DLcE_XL<`1%`bPJV^_@s(I4HXgG!@{D(m3q}R5f~fO0`pG%M{hn6T z^TCrke&tb1U=&pbbH3Eu7)lA9L{TWqDN`ivN4XUi7KR9qhna0aI{cIeXG~&gWg>Rz%0&NBF<>%shCH0Ev~4E394C1%**bSmY~&S$T<=WW5=q z{;g%M)DrqNmbtL$vDEYD4``EWnGDh& zk*3y0k~jG+SjUbrsEevY^(`APLjWbR`J7WWhPijlrq39O3+=;@^y?wwrC%Xt&t*ge zt01KD0|Gv3!B<=do-%Xcx}Xhbe*55rS|ASp;W~c*2i-A+*hC6g>y-?JU8}LEsQ|NB zFElA^FGfAdhv+3W`n7N-^%ptPBjKUc@$Dy{=eAU7D@A$#wo(dnl%rVBsTlE^A9Z3SZf_J8M*;A*p$(YwGSsiA=(*> zeo8@NNEap*U%{xKTOfK}kAAK6qW)?FuEUZ#KHQ-eD^IFCFof^5jg-O}&rvG^$$yI- zxrAQfd7>N{IPD@$Ii6E%8wu8NzzB7hcH`2BrR;lFK#6YwE;cd zq%6{kS0VY+HYCXNZtD03L>7-g=zGrhn_CV)_HlddGl$#GdN@hU$0^e&9C<$!`-^3v z=bD4f$L>MZu>;E|$3bfEOGxZ+z@+{fi1BZh=nEbCrJGLuP0iHvQu>jpWz*3D$8+4|R{#S)1_^72#GWc{&J% ziwlu=TnAYd9!UFh7%A&6v4_nIu{S0mYFrz_wtqvA*ID=#+=f@91Kdil!0D_N>?Yp9 z(co5?Vi)vy4!`+E5L8pp(LrQHUo zD0fBa=(8v^-io|@-sk*Uige9Rq=f1rp_@Gj3;Pgd<%6(7KJDMkwRPiU3@;39{`OP$ny!M>@UbK?Sid}Tf>p2++>ygHPh9p1h ztY96%iKw4=1D66EQ1Lq%r3d6tcv%?vbGdgv7l`zG!AM;uheQtz#NB^@=tYweZaoCS zxoYsg`x4%-MEHKoGty8+ILtJKmE}hqc(Mn(bthm;Zy)IVHZ1?L57HtVA#wFA#8u59 zrmKS?gP41IdqnD_BR2BI@V!dzHcd)LH2$4cy1x=kdtjtAXsgRmhmtj?^=jNW6C)=a(^0 z-1Q~G+cFU{vKj%(li{^)HD-dzEgjdoptsm6y&-dE%j+~Kkoqgw=^_MQfd$lM^4q;xRohA9j zEGOqLd30=A747E#rx9|M^gP23H1->HP@aRi|OTuLK+CPPj3gMt#G7K_gu=V9{ z!1yxr8%99-U>GDvNOLbX;iv~qU@jbDDqo~f}y98 zb9N&#J}BUVkr9%v>L7mEZNvnWBI0o_Lg$Gxo6sD-#9DN;Fr{EU{6bE6|2y&_DzP5C=FX(KsiD@**(T(qew4fThvpl0M4RPr9C zO#UK@swGjdCKox?n~=HO4HvSGA$h6;_a-|L^Q<0`D@_pUbQpno*6^)nZ%7j78*T2# znY|ZbYnBFcXEhkQox(PrC8666ik-HQPV9r^;sl5vSK;Sz5u%@M=+`te>Q|djJ?iJF zV^9S(8y0frtry#M*Hf~TJw;ABM1G`2&KdXVX#6hPO^T$kp_=4wUl**S@iOWihM{Jz zJ}R4xQD%^gqBp)MII#*jKO&K7KL!^*Wg^+U2Jv^EBX;E-M24}}??Eeqh98EXIO}?U z6u>o^Jr#d@U^@_sLxY)7_`w9*o*%@zX?jo+-45w0Gf1v{4skpF&P^vkv~Lal5{{+* zc?#4sTZ`H|1E^V{k1ED%Q0~4EN*38l5g(h#Pf~`Q{pZn9t4i8Ey_GbU_LAJ?g@Scl zy@`4san!6bK;=_rDY{NW@l?J`WiLQ3d%7~KI*_(%1d>aqAc5K2u|fTaeA9w3h3N>| zuLD1;7If}$bA>~?D2%wibGhZdkRWRRzv!BB_!A1VfMrdi0yj~ z(ca_q^La7#4_2q{UrN+oc#WDzu-@n86V{7!yy?kq)K^HN#-#{V;^Kxm$bFHEtaHba zK4uY8qV6D3ya{nWI}z1ai|`e95NuJ$y@@xxw>QGA{|B5_EW}BbY8+bQ=>&)SQH5uL)B;dYA8&2+%aI)nBd#5pCUb)c{$Ptkyv;H=cWA-9r6I-y+08$;Vm<=qT&5*AKd+;;B<2^ zPH8pc@Q+&TJF)`X+ulO$v;mYpNI^!k0g|UH`8zL!*oq|1JsC?sm)laGf+BTK380%z zH>qi|E|q<|Lphs*De-p%h5r>HU-j?gbmKG~Sw5fi=T0Sc7d{6q(*^7J@dx$%`&iR* znEk06QEoOHB@6g&`*<_*Lei1F@&(enT#;(Kf#*LOIPY*C(N8xaV)=7~nCl_H#)^9^ zC%AWBX0Og`oVq_6mbUY;?^YhPXMBcQu_BZwaTf8OZIE4`CZ`wmLZaPPj(3i(2P$S$!$hS4Tm5L$qwkQa!b=*7ON zZbaPEL+C%O$(oP}pBFosadH#RS%I))uk8`zaWIPOfOf@ksL$OFWn&@MBXb>LDiA+W z53#AsAbMmh4V3E9n`8}ocsiPHO0b^VW(<|>2%zkoR7yNJlEQ7P$)~l3oJ?+!c7-i5z8#(Fq4SGw_HBsD6FQQ6dWoKwGs66Inke4Q%!oL@#x ztH+b2ssZVlx{=!NGLl<(O0bSe4QQD45w+93QO)y>@=yPvgzt2P$gx~TM zKDL*9th&f)QZ`u%Ns*r1UQ+8^NOF?|_iD{E3em9mE^1|Wp?cA5R7?^>3GX%w+qrj7 zWM9jHGsv8^02i3;oNO@)@n4D&Ymtx0MmdCjyMn+!Z{T~)3!d9u;c{9G4yIg3xde=B zb+99*7wVS-p?uv2GR+$xnLU;J-G4YMggFiTZ$Gf~61^GfOI^1(@5(xi8tNseG_{zr z2ebFQF@Tq*g9Paw2Vl&tt9!8dkcZR=o|?YFkmE)Phn8 z&P4xPjQrb2kQ48OOv6t|6PwHVQaMOi!@Ws~HljvPMVQ(@2-@fkze%Uy8I=Z?8hJQ8 zQoynKb}*UVh8<6Wp#IAQ%I|X_bD#S}-ua4K48y4Yah!3mkOqY3(d&*+)D@FOHMgbtq>MY`7}lUmp{lKm*?OKp}`L&F9k)b18W z^}gw-&=*B1do+p`Pvbj?C35cMATyeKtZj>s{8LFP_3gV{BEV^?TqQVyR zBkc;kKD36q7MN1orHRxq{63Yun@?Fg(kMYfhr-m$$-5$!&b}W>7T;!)?*1lH+agV} zZBqs7P_{D4uiu!p6Fgw@)3NO%NlCqGP1H zbUdjo8b-2J*@AVf;pbts6SZM4QJuRH6_usafjb8mNq0XXCW>}_EeZW*IIj%^VU0M{MFF~OXg~{6>jm~bV zB#UE5NoQaosfjd^Y~~WdI@E!NGpsSm5JGiJ3M%efq4ZHPin=UOa7`7t`KHKn^G5oH z5~Td%d0Z50VW&(+w4Fb~^H^_kkv(}QI9Ki!KaZbbtdDDl^RRe_P@}dXKd4?yf=U#yceaRHzpQ?B_H30vz40tbE4L2ogoC)`UbwD&s=Z(iM zJ9%i%VO~z@8Z6!z2g$C{5ZA56sLVdjGvR!%kDP58DMp>FRlI)X7}bwcq~h;)DbsW( z#jjpKp|)=1^;?+EWckve=TAu|@H4GrE~%`WAitz}dlecYMxyq*1**SqMy2>Llr6fB z;${Vbo8ZlJ9XIRuI4k50 z>;6SBJ$@9sO0uD;H5w~!FXqg`JV^HP_p#FvqvGE|B_69*qPmFBRD9+dW%OBc<`vg5{2qD5%hH*lT6Acy4C(wo(%w3%>TdrUMeO!b?AmmP z7$Bu{s;EfYVs~IGb|ESPf}o&;fHX)q2uO)ycXvH@cfOzJyzlwl@gB~-|6IpdLv(}g zxz~5qXU)*e#GvI9(QArvT^y5cvNC-PkGwm@Qk{J)c74TyCAXRDsm`qJ`b=NGg0NH=fg_AP#aUjSH zL6;TqUUVPp^S)tT=WiJOHVcECG|0`o4qy;+yQ#bvd_n8O%++mq_l3 zg0n^+=9n4e(9 z@nz{E@#su0XTeo@uD%M#ui1TOw5%21(X~vv?Zf!EQ{pM5!N}WA3}-g?t!cnLE59

lvzwhN0uYOKdqu@}))}#+R3# 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⁡ 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,rO8=al9>@J}raOz+^ZCi|f~u?y#C3 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

0k}{4_z@gWQgB%t53bwCVlAR( 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 Pv96ksjBrsQT?<9?i|&6tXgn*3z$ky;>+u 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%0A2H%BgI67Q)!4L8$?5=%t2shDruPV~p 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 diff --git a/include/bout/adios_object.hxx b/include/bout/adios_object.hxx new file mode 100755 index 0000000000..9d2f545b46 --- /dev/null +++ b/include/bout/adios_object.hxx @@ -0,0 +1,83 @@ +/*!************************************************************************ + * Provides access to the ADIOS library, handling initialisation and + * finalisation. + * + * Usage + * ----- + * + * #include + * + **************************************************************************/ + +#ifndef ADIOS_OBJECT_HXX +#define ADIOS_OBJECT_HXX + +#include "bout/build_config.hxx" + +#if BOUT_HAS_ADIOS + +#include +#include +#include + +namespace bout { + +void ADIOSInit(MPI_Comm comm); +void ADIOSInit(const std::string configFile, MPI_Comm comm); +void ADIOSFinalize(); + +using ADIOSPtr = std::shared_ptr; +using EnginePtr = std::shared_ptr; +using IOPtr = std::shared_ptr; + +ADIOSPtr GetADIOSPtr(); +IOPtr GetIOPtr(const std::string IOName); + +class ADIOSStream { +public: + adios2::IO io; + adios2::Engine engine; + adios2::Variable vTime; + adios2::Variable vStep; + int adiosStep = 0; + bool isInStep = false; // true if BeginStep was called and EndStep was not yet called + + /** create or return the ADIOSStream based on the target file name */ + static ADIOSStream& ADIOSGetStream(const std::string& fname); + + ~ADIOSStream(); + + template + adios2::Variable GetValueVariable(const std::string& varname) { + auto v = io.InquireVariable(varname); + if (!v) { + v = io.DefineVariable(varname); + } + return v; + } + + template + adios2::Variable GetArrayVariable(const std::string& varname, adios2::Dims& shape) { + adios2::Variable v = io.InquireVariable(varname); + if (!v) { + adios2::Dims start(shape.size()); + v = io.DefineVariable(varname, shape, start, shape); + } else { + v.SetShape(shape); + } + return v; + } + +private: + ADIOSStream(const std::string fname) : fname(fname){}; + std::string fname; +}; + +/** Set user parameters for an IO group */ +void ADIOSSetParameters(const std::string& input, const char delimKeyValue, + const char delimItem, adios2::IO& io); + +} // namespace bout + +#endif //BOUT_HAS_ADIOS +#endif //ADIOS_OBJECT_HXX diff --git a/include/bout/boundary_op.hxx b/include/bout/boundary_op.hxx index 035caa2778..1a9aa1ad68 100644 --- a/include/bout/boundary_op.hxx +++ b/include/bout/boundary_op.hxx @@ -78,8 +78,9 @@ public: virtual void apply_ddt(Vector3D& f) { apply(ddt(f)); } BoundaryRegion* bndry; - bool - apply_to_ddt; // True if this boundary condition should be applied on the time derivatives, false if it should be applied to the field values + // True if this boundary condition should be applied on the time derivatives + // false if it should be applied to the field values + bool apply_to_ddt; }; class BoundaryModifier : public BoundaryOp { diff --git a/include/bout/bout.hxx b/include/bout/bout.hxx index 5e718dde33..d929a19c2f 100644 --- a/include/bout/bout.hxx +++ b/include/bout/bout.hxx @@ -2,8 +2,6 @@ * * @mainpage BOUT++ * - * @version 3.0 - * * @par Description * Framework for the solution of partial differential * equations, in particular fluid models in plasma physics. @@ -33,8 +31,8 @@ * **************************************************************************/ -#ifndef __BOUT_H__ -#define __BOUT_H__ +#ifndef BOUT_H +#define BOUT_H #include "bout/build_config.hxx" @@ -44,7 +42,7 @@ #include "bout/field3d.hxx" #include "bout/globals.hxx" #include "bout/mesh.hxx" -#include "bout/options_netcdf.hxx" +#include "bout/options_io.hxx" #include "bout/output.hxx" #include "bout/smoothing.hxx" // Smoothing functions #include "bout/solver.hxx" @@ -206,4 +204,4 @@ private: */ int BoutFinalise(bool write_settings = true); -#endif // __BOUT_H__ +#endif // BOUT_H diff --git a/include/bout/boutexception.hxx b/include/bout/boutexception.hxx index 5b8a421692..243b819961 100644 --- a/include/bout/boutexception.hxx +++ b/include/bout/boutexception.hxx @@ -10,8 +10,6 @@ class BoutException; #include #include -#include "bout/format.hxx" - #include "fmt/core.h" /// Throw BoutRhsFail with \p message if any one process has non-zero diff --git a/include/bout/build_config.hxx b/include/bout/build_config.hxx index a98c615c77..c97962f7cf 100644 --- a/include/bout/build_config.hxx +++ b/include/bout/build_config.hxx @@ -17,6 +17,7 @@ constexpr auto has_gettext = static_cast(BOUT_HAS_GETTEXT); constexpr auto has_lapack = static_cast(BOUT_HAS_LAPACK); constexpr auto has_legacy_netcdf = static_cast(BOUT_HAS_LEGACY_NETCDF); constexpr auto has_netcdf = static_cast(BOUT_HAS_NETCDF); +constexpr auto has_adios = static_cast(BOUT_HAS_ADIOS); constexpr auto has_petsc = static_cast(BOUT_HAS_PETSC); constexpr auto has_hypre = static_cast(BOUT_HAS_HYPRE); constexpr auto has_umpire = static_cast(BOUT_HAS_UMPIRE); diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 9a425942c1..a4f4f52803 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -370,7 +370,7 @@ inline bool isUniform(const T& f, bool allpe = false, /// @param[in] allpe Check over all processors /// @param[in] region The region to assume is uniform template > -inline BoutReal getUniform(const T& f, MAYBE_UNUSED(bool allpe) = false, +inline BoutReal getUniform(const T& f, [[maybe_unused]] bool allpe = false, const std::string& region = "RGN_ALL") { #if CHECK > 1 if (not isUniform(f, allpe, region)) { diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index a36f899692..5bac67beb2 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -33,7 +33,7 @@ class Field2D; class Mesh; #include "bout/field.hxx" #include "bout/field_data.hxx" -class Field3D; //#include "bout/field3d.hxx" +class Field3D; #include "bout/fieldperp.hxx" #include "bout/stencils.hxx" @@ -174,6 +174,9 @@ public: /// Return a Region reference to use to iterate over this field const Region& getRegion(REGION region) const; const Region& getRegion(const std::string& region_name) const; + const Region& getValidRegionWithDefault(const std::string& region_name) const { + return getRegion(region_name); + } Region::RegionIndices::const_iterator begin() const { return std::begin(getRegion("RGN_ALL")); diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 4e6510f8a6..9f5326253d 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -40,6 +40,7 @@ class Mesh; // #include "bout/mesh.hxx" #include "bout/utils.hxx" +#include #include /// Class for 3D X-Y-Z scalar fields @@ -313,6 +314,13 @@ public: /// const Region& getRegion(REGION region) const; const Region& getRegion(const std::string& region_name) const; + /// Use region provided by the default, and if none is set, use the provided one + const Region& getValidRegionWithDefault(const std::string& region_name) const; + void setRegion(const std::string& region_name); + void resetRegion() { regionID.reset(); }; + void setRegion(size_t id) { regionID = id; }; + void setRegion(std::optional id) { regionID = id; }; + std::optional getRegionID() const { return regionID; }; /// Return a Region reference to use to iterate over the x- and /// y-indices of this field @@ -503,6 +511,9 @@ private: /// Fields containing values along Y std::vector yup_fields{}, ydown_fields{}; + + /// RegionID over which the field is valid + std::optional regionID; }; // Non-member overloaded operators diff --git a/include/bout/field_data.hxx b/include/bout/field_data.hxx index 59bd751fc4..03b9d6759b 100644 --- a/include/bout/field_data.hxx +++ b/include/bout/field_data.hxx @@ -38,8 +38,6 @@ class FieldData; #include #include -// Including the next line leads to compiler errors -//#include "bout/boundary_op.hxx" class BoundaryOp; class BoundaryOpPar; class Coordinates; diff --git a/include/bout/fieldperp.hxx b/include/bout/fieldperp.hxx index 3b6e0567d8..3b8ed45db6 100644 --- a/include/bout/fieldperp.hxx +++ b/include/bout/fieldperp.hxx @@ -98,6 +98,9 @@ public: /// Return a Region reference to use to iterate over this field const Region& getRegion(REGION region) const; const Region& getRegion(const std::string& region_name) const; + const Region& getValidRegionWithDefault(const std::string& region_name) const { + return getRegion(region_name); + } Region::RegionIndices::const_iterator begin() const { return std::begin(getRegion("RGN_ALL")); diff --git a/include/bout/format.hxx b/include/bout/format.hxx deleted file mode 100644 index 846303a3fd..0000000000 --- a/include/bout/format.hxx +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef __BOUT_FORMAT_H__ -#define __BOUT_FORMAT_H__ - -/// Tell GCC that a function has a printf-style like argument -/// The first argument is the position of format string, and the -/// second is the position of the first variadic argument -/// Note that it seems to start counting from 1, and also counts a -/// *this pointer, as the first argument, so often 2 would be the -/// first argument. -#if defined(__GNUC__) -#define BOUT_FORMAT_ARGS(i, j) __attribute__((format(printf, i, j))) -#else -#define BOUT_FORMAT_ARGS(i, j) -#endif - -#endif //__BOUT_FORMAT_H__ diff --git a/include/bout/generic_factory.hxx b/include/bout/generic_factory.hxx index 3a2a63c94c..9493ef77f1 100644 --- a/include/bout/generic_factory.hxx +++ b/include/bout/generic_factory.hxx @@ -1,8 +1,8 @@ /// Base type for factories #pragma once -#ifndef __BOUT_GENERIC_FACTORY_H__ -#define __BOUT_GENERIC_FACTORY_H__ +#ifndef BOUT_GENERIC_FACTORY_H +#define BOUT_GENERIC_FACTORY_H #include "bout/boutexception.hxx" #include "bout/options.hxx" @@ -48,14 +48,6 @@ /// RegisterInFactory register("derived_type"); /// auto foo = MyFactory::getInstance().create("derived_type"); /// -/// In a .cxx file the static members should be declared: -/// -/// constexpr decltype(MyFactory::type_name) MyFactory::type_name; -/// constexpr decltype(MyFactory::section_name) MyFactory::section_name; -/// constexpr decltype(MyFactory::option_name) MyFactory::option_name; -/// constexpr decltype(MyFactory::default_type) MyFactory::default_type; -/// -/// /// @tparam BaseType The base class that this factory creates /// @tparam DerivedFactory The derived factory inheriting from this class /// @tparam TypeCreator The function signature for creating a new BaseType @@ -259,4 +251,4 @@ public: }; }; -#endif // __BOUT_GENERIC_FACTORY_H__ +#endif // BOUT_GENERIC_FACTORY_H diff --git a/include/bout/globalindexer.hxx b/include/bout/globalindexer.hxx index 3d0ef21cea..ab1c927832 100644 --- a/include/bout/globalindexer.hxx +++ b/include/bout/globalindexer.hxx @@ -70,14 +70,14 @@ public: bndryCandidate = mask(allCandidate, getRegionNobndry()); - regionInnerX = getIntersection(bndryCandidate, indices.getRegion("RGN_INNER_X")); - regionOuterX = getIntersection(bndryCandidate, indices.getRegion("RGN_OUTER_X")); + regionInnerX = intersection(bndryCandidate, indices.getRegion("RGN_INNER_X")); + regionOuterX = intersection(bndryCandidate, indices.getRegion("RGN_OUTER_X")); if (std::is_same::value) { regionLowerY = Region({}); regionUpperY = Region({}); } else { - regionLowerY = getIntersection(bndryCandidate, indices.getRegion("RGN_LOWER_Y")); - regionUpperY = getIntersection(bndryCandidate, indices.getRegion("RGN_UPPER_Y")); + regionLowerY = intersection(bndryCandidate, indices.getRegion("RGN_LOWER_Y")); + regionUpperY = intersection(bndryCandidate, indices.getRegion("RGN_UPPER_Y")); } regionBndry = regionLowerY + regionInnerX + regionOuterX + regionUpperY; regionAll = getRegionNobndry() + regionBndry; diff --git a/include/bout/index_derivs.hxx b/include/bout/index_derivs.hxx index 8cb5c88aff..456f98f8c2 100644 --- a/include/bout/index_derivs.hxx +++ b/include/bout/index_derivs.hxx @@ -124,13 +124,6 @@ public: BoutReal apply(const stencil& v, const stencil& f) const { return func(v, f); } }; -// Redundant definitions because C++ -// Not necessary in C++17 -template -constexpr FF DerivativeType::func; -template -constexpr metaData DerivativeType::meta; - ///////////////////////////////////////////////////////////////////////////////// /// Following code is for dealing with registering a method/methods for all /// template combinations, in conjunction with the template_combinations code. diff --git a/include/bout/interpolation_xz.hxx b/include/bout/interpolation_xz.hxx index 04b1ad0cdb..46f64256a8 100644 --- a/include/bout/interpolation_xz.hxx +++ b/include/bout/interpolation_xz.hxx @@ -75,6 +75,9 @@ public: void setRegion(const std::string& region_name) { this->region_id = localmesh->getRegionID(region_name); } + void setRegion(const std::unique_ptr> region){ + setRegion(*region); + } void setRegion(const Region& region) { std::string name; int i = 0; @@ -299,7 +302,7 @@ public: ReturnType create(Options* options = nullptr, Mesh* mesh = nullptr) const { return Factory::create(getType(options), mesh); } - ReturnType create(const std::string& type, MAYBE_UNUSED(Options* options)) const { + ReturnType create(const std::string& type, [[maybe_unused]] Options* options) const { return Factory::create(type, nullptr); } diff --git a/include/bout/interpolation_z.hxx b/include/bout/interpolation_z.hxx index 596b2634fb..b11d7ff5b6 100644 --- a/include/bout/interpolation_z.hxx +++ b/include/bout/interpolation_z.hxx @@ -82,7 +82,7 @@ public: Region region_in = {}) const { return Factory::create(getType(nullptr), y_offset, mesh, region_in); } - ReturnType create(const std::string& type, MAYBE_UNUSED(Options* options)) const { + ReturnType create(const std::string& type, [[maybe_unused]] Options* options) const { return Factory::create(type, 0, nullptr, Region{}); } diff --git a/include/bout/invert_laplace.hxx b/include/bout/invert_laplace.hxx index c4ae5efdf8..78417b9fce 100644 --- a/include/bout/invert_laplace.hxx +++ b/include/bout/invert_laplace.hxx @@ -275,8 +275,8 @@ public: /// performance information, with optional name for the time /// dimension void outputVars(Options& output_options) const { outputVars(output_options, "t"); } - virtual void outputVars(MAYBE_UNUSED(Options& output_options), - MAYBE_UNUSED(const std::string& time_dimension)) const {} + virtual void outputVars([[maybe_unused]] Options& output_options, + [[maybe_unused]] const std::string& time_dimension) const {} /// Register performance monitor with \p solver, prefix output with /// `Options` section name diff --git a/include/bout/mask.hxx b/include/bout/mask.hxx index 9c8c4b96dc..89197ddcf2 100644 --- a/include/bout/mask.hxx +++ b/include/bout/mask.hxx @@ -69,13 +69,14 @@ public: inline const bool& operator[](const Ind3D& i) const { return mask[i]; } }; -inline Region regionFromMask(const BoutMask& mask, const Mesh* mesh) { +inline std::unique_ptr> regionFromMask(const BoutMask& mask, + const Mesh* mesh) { std::vector indices; for (auto i : mesh->getRegion("RGN_ALL")) { if (not mask(i.x(), i.y(), i.z())) { indices.push_back(i); } } - return Region{indices}; + return std::make_unique>(indices); } #endif //__MASK_H__ diff --git a/include/bout/mesh.hxx b/include/bout/mesh.hxx index 45ea392482..8f73552ea5 100644 --- a/include/bout/mesh.hxx +++ b/include/bout/mesh.hxx @@ -74,6 +74,7 @@ class Mesh; #include #include #include +#include #include #include @@ -134,7 +135,7 @@ public: /// Add output variables to \p output_options /// These are used for post-processing - virtual void outputVars(MAYBE_UNUSED(Options& output_options)) {} + virtual void outputVars([[maybe_unused]] Options& output_options) {} // Get routines to request data from mesh file @@ -503,9 +504,20 @@ public: int GlobalNx, GlobalNy, GlobalNz; ///< Size of the global arrays. Note: can have holes /// Size of the global arrays excluding boundary points. int GlobalNxNoBoundaries, GlobalNyNoBoundaries, GlobalNzNoBoundaries; + + /// Note: These offsets only correct if Y guards are not included in the global array + /// and are corrected in gridfromfile.cxx int OffsetX, OffsetY, OffsetZ; ///< Offset of this mesh within the global array ///< so startx on this processor is OffsetX in global + /// Map between local and global indices + /// (MapGlobalX, MapGlobalY, MapGlobalZ) in the global index space maps to (MapLocalX, MapLocalY, MapLocalZ) locally. + /// Note that boundary cells are included in the global index space, but communication + /// guard cells are not. + int MapGlobalX, MapGlobalY, MapGlobalZ; ///< Start global indices + int MapLocalX, MapLocalY, MapLocalZ; ///< Start local indices + int MapCountX, MapCountY, MapCountZ; ///< Size of the mapped region + /// Returns the number of unique cells (i.e., ones not used for /// communication) on this processor for 3D fields. Boundaries /// are only included to a depth of 1. @@ -795,6 +807,14 @@ public: // Switch for communication of corner guard and boundary cells const bool include_corner_cells; + std::optional getCommonRegion(std::optional, std::optional); + size_t getRegionID(const std::string& region) const; + const Region& getRegion(size_t RegionID) const { return region3D[RegionID]; } + const Region& getRegion(std::optional RegionID) const { + ASSERT1(RegionID.has_value()); + return region3D[RegionID.value()]; + } + private: /// Allocates default Coordinates objects /// By default attempts to read staggered Coordinates from grid data source, @@ -807,7 +827,9 @@ private: bool force_interpolate_from_centre = false); //Internal region related information - std::map> regionMap3D; + std::map regionMap3D; + std::vector> region3D; + std::vector> region3Dintersect; std::map> regionMap2D; std::map> regionMapPerp; Array indexLookup3Dto2D; diff --git a/include/bout/monitor.hxx b/include/bout/monitor.hxx index eb86c01554..5bc4fc7e12 100644 --- a/include/bout/monitor.hxx +++ b/include/bout/monitor.hxx @@ -50,8 +50,8 @@ public: /// Callback function for when a clean shutdown is initiated virtual void cleanup(){}; - virtual void outputVars(MAYBE_UNUSED(Options& options), - MAYBE_UNUSED(const std::string& time_dimension)) {} + virtual void outputVars([[maybe_unused]] Options& options, + [[maybe_unused]] const std::string& time_dimension) {} protected: /// Get the currently set timestep for this monitor diff --git a/include/bout/msg_stack.hxx b/include/bout/msg_stack.hxx index cc19a7b9f6..993d8adb75 100644 --- a/include/bout/msg_stack.hxx +++ b/include/bout/msg_stack.hxx @@ -31,7 +31,6 @@ class MsgStack; #include "bout/build_config.hxx" -#include "bout/format.hxx" #include "bout/unused.hxx" #include "fmt/core.h" @@ -201,7 +200,7 @@ private: arguments and the optional arguments follow from there. */ #define TRACE(...) \ - MsgStackItem CONCATENATE(msgTrace_, __LINE__)(__FILE__, __LINE__, __VA_ARGS__) + const MsgStackItem CONCATENATE(msgTrace_, __LINE__)(__FILE__, __LINE__, __VA_ARGS__) #else #define TRACE(...) #endif diff --git a/include/bout/options.hxx b/include/bout/options.hxx index bf1704100f..c45149cbdd 100644 --- a/include/bout/options.hxx +++ b/include/bout/options.hxx @@ -53,7 +53,6 @@ class Options; #include #include -#include #include #include #include @@ -182,9 +181,20 @@ public: /// Example: { {"key1", 42}, {"key2", field} } Options(std::initializer_list> values); - /// Copy constructor Options(const Options& other); + /// Copy assignment + /// + /// This replaces the value, attributes and all children + /// + /// Note that if only the value is desired, then that can be copied using + /// the value member directly e.g. option2.value = option1.value; + /// + Options& operator=(const Options& other); + + Options(Options&& other) noexcept; + Options& operator=(Options&& other) noexcept; + ~Options() = default; /// Get a reference to the only root instance @@ -210,14 +220,11 @@ public: public: using Base = bout::utils::variant; - /// Constructor AttributeType() = default; - /// Copy constructor AttributeType(const AttributeType& other) = default; - /// Move constructor - AttributeType(AttributeType&& other) : Base(std::move(other)) {} - - /// Destructor + AttributeType(AttributeType&& other) = default; + AttributeType& operator=(const AttributeType& other) = default; + AttributeType& operator=(AttributeType&& other) = default; ~AttributeType() = default; /// Assignment operator, including move assignment @@ -229,9 +236,6 @@ public: return *this; } - /// Copy assignment operator - AttributeType& operator=(const AttributeType& other) = default; - /// Initialise with a value /// This enables AttributeTypes to be constructed using initializer lists template @@ -362,15 +366,6 @@ public: return inputvalue; } - /// Copy assignment - /// - /// This replaces the value, attributes and all children - /// - /// Note that if only the value is desired, then that can be copied using - /// the value member directly e.g. option2.value = option1.value; - /// - Options& operator=(const Options& other); - /// Assign a value to the option. /// This will throw an exception if already has a value /// @@ -382,9 +377,9 @@ public: /// Note: Specialised versions for types stored in ValueType template void assign(T val, std::string source = "") { - std::stringstream ss; - ss << val; - _set(ss.str(), std::move(source), false); + std::stringstream as_str; + as_str << val; + _set(as_str.str(), std::move(source), false); } /// Force to a value @@ -462,20 +457,20 @@ public: // If the variant is a string then we may be able to parse it if (bout::utils::holds_alternative(value)) { - std::stringstream ss(bout::utils::get(value)); - ss >> val; + std::stringstream as_str(bout::utils::get(value)); + as_str >> val; // Check if the parse failed - if (ss.fail()) { + if (as_str.fail()) { throw BoutException("Option {:s} could not be parsed ('{:s}')", full_name, bout::utils::variantToString(value)); } // Check if there are characters remaining std::string remainder; - std::getline(ss, remainder); - for (const char& ch : remainder) { - if (!std::isspace(static_cast(ch))) { + std::getline(as_str, remainder); + for (const unsigned char chr : remainder) { + if (!std::isspace(chr)) { // Meaningful character not parsed throw BoutException("Option {:s} could not be parsed", full_name); } @@ -495,7 +490,7 @@ public: // Specify the source of the setting output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; } - output_info << endl; + output_info << '\n'; return val; } @@ -514,7 +509,7 @@ public: value_used = true; // Mark the option as used output_info << _("\tOption ") << full_name << " = " << def << " (" << DEFAULT_SOURCE - << ")" << std::endl; + << ")\n"; return def; } T val = as(def); @@ -547,7 +542,7 @@ public: *this = def; output_info << _("\tOption ") << full_name << " = " << def.full_name << " (" - << DEFAULT_SOURCE << ")" << std::endl; + << DEFAULT_SOURCE << ")\n"; } else { // Check if this was previously set as a default option if (bout::utils::variantEqualTo(attributes.at("source"), DEFAULT_SOURCE)) { @@ -571,7 +566,7 @@ public: if (is_section) { // Option not found output_info << _("\tOption ") << full_name << " = " << def << " (" << DEFAULT_SOURCE - << ")" << std::endl; + << ")\n"; return def; } T val = as(def); @@ -657,8 +652,8 @@ public: // Setting options template - void forceSet(const std::string& key, T t, const std::string& source = "") { - (*this)[key].force(t, source); + void forceSet(const std::string& key, T val, const std::string& source = "") { + (*this)[key].force(val, source); } /*! @@ -670,11 +665,11 @@ public: if (!is_section) { return false; } - auto it = children.find(key); - if (it == children.end()) { + auto child = children.find(key); + if (child == children.end()) { return false; } - return it->second.isSet(); + return child->second.isSet(); } /// Get options, passing in a reference to a variable @@ -702,13 +697,12 @@ public: /// Print just the name of this object without parent sections std::string name() const { - auto pos = full_name.rfind(":"); + auto pos = full_name.rfind(':'); if (pos == std::string::npos) { // No parent section or sections return full_name; - } else { - return full_name.substr(pos + 1); } + return full_name.substr(pos + 1); } /// Return a new Options instance which contains all the values @@ -731,21 +725,6 @@ public: /// clean the cache of parsed options static void cleanCache(); - /*! - * Class used to store values, together with - * information about their origin and usage - */ - struct OptionValue { - std::string value; - std::string source; // Source of the setting - mutable bool used = false; // Set to true when used - - /// This constructor needed for map::emplace - /// Can be removed in C++17 with map::insert and brace initialisation - OptionValue(std::string value, std::string source, bool used) - : value(std::move(value)), source(std::move(source)), used(used) {} - }; - /// Read-only access to internal options and sections /// to allow iteration over the tree std::map subsections() const; @@ -784,8 +763,6 @@ private: /// The source label given to default values static const std::string DEFAULT_SOURCE; - static Options* root_instance; ///< Only instance of the root section - Options* parent_instance{nullptr}; std::string full_name; // full path name for logging only @@ -837,8 +814,8 @@ private: /// Tests if two values are similar. template - bool similar(T a, T b) const { - return a == b; + bool similar(T lhs, T rhs) const { + return lhs == rhs; } }; @@ -862,7 +839,7 @@ inline void Options::assign<>(std::string val, std::string source) { // Note: const char* version needed to avoid conversion to bool template <> inline void Options::assign<>(const char* val, std::string source) { - _set(std::string(val), source, false); + _set(std::string(val), std::move(source), false); } // Note: Field assignments don't check for previous assignment (always force) template <> @@ -880,8 +857,8 @@ void Options::assign<>(Tensor val, std::string source); /// Specialised similar comparison methods template <> -inline bool Options::similar(BoutReal a, BoutReal b) const { - return fabs(a - b) < 1e-10; +inline bool Options::similar(BoutReal lhs, BoutReal rhs) const { + return fabs(lhs - rhs) < 1e-10; } /// Specialised as routines @@ -972,6 +949,8 @@ private: template <> struct fmt::formatter : public bout::details::OptionsFormatterBase {}; +// NOLINTBEGIN(cppcoreguidelines-macro-usage) + /// Define for reading options which passes the variable name #define OPTION(options, var, def) pointer(options)->get(#var, var, def) @@ -1032,4 +1011,6 @@ struct fmt::formatter : public bout::details::OptionsFormatterBase {}; __LINE__) = Options::root()[name].overrideDefault(value); \ } +// NOLINTEND(cppcoreguidelines-macro-usage) + #endif // __OPTIONS_H__ diff --git a/include/bout/options_io.hxx b/include/bout/options_io.hxx new file mode 100644 index 0000000000..65c3cb7dd1 --- /dev/null +++ b/include/bout/options_io.hxx @@ -0,0 +1,178 @@ +/// Parent class for IO to binary files and streams +/// +/// +/// Usage: +/// +/// 1. Dump files, containing time history: +/// +/// auto dump = OptionsIOFactory::getInstance().createOutput(); +/// dump->write(data); +/// +/// where data is an Options tree. By default dump files are configured +/// with the root `output` section, or an Option tree can be passed to +/// `createOutput`. +/// +/// 2. Restart files: +/// +/// auto restart = OptionsIOFactory::getInstance().createOutput(); +/// restart->write(data); +/// +/// where data is an Options tree. By default restart files are configured +/// with the root `restart_files` section, or an Option tree can be passed to +/// `createRestart`. +/// +/// 3. Ad-hoc single files +/// Note: The caller should consider how multiple processors interact with the file. +/// +/// auto file = OptionsIOFactory::getInstance().createFile("some_file.nc"); +/// or +/// auto file = OptionsIO::create("some_file.nc"); +/// +/// + +#pragma once + +#ifndef OPTIONS_IO_H +#define OPTIONS_IO_H + +#include "bout/build_config.hxx" +#include "bout/generic_factory.hxx" +#include "bout/options.hxx" + +#include +#include + +namespace bout { + +class OptionsIO { +public: + /// No default constructor, as settings are required + OptionsIO() = delete; + + /// Constructor specifies the kind of file, and options to control + /// the name of file, mode of operation etc. + OptionsIO(Options&) {} + + virtual ~OptionsIO() = default; + + OptionsIO(const OptionsIO&) = delete; + OptionsIO(OptionsIO&&) noexcept = default; + OptionsIO& operator=(const OptionsIO&) = delete; + OptionsIO& operator=(OptionsIO&&) noexcept = default; + + /// Read options from file + virtual Options read() = 0; + + /// Write options to file + void write(const Options& options) { write(options, "t"); } + virtual void write(const Options& options, const std::string& time_dim) = 0; + + /// NetCDF: Check that all variables with the same time dimension have the + /// same size in that dimension. Throws BoutException if there are + /// any differences, otherwise is silent. + /// ADIOS: Indicate completion of an output step. + virtual void verifyTimesteps() const = 0; + + /// Create an OptionsIO for I/O to the given file. + /// This uses the default file type and default options. + static std::unique_ptr create(const std::string& file); + + /// Create an OptionsIO for I/O to the given file. + /// The file will be configured using the given `config` options: + /// - "type" : string The file type e.g. "netcdf" or "adios" + /// - "file" : string Name of the file + /// - "append" : bool Append to existing data (Default is false) + static std::unique_ptr create(Options& config); + + /// Create an OptionsIO for I/O to the given file. + /// The file will be configured using the given `config` options: + /// - "type" : string The file type e.g. "netcdf" or "adios" + /// - "file" : string Name of the file + /// - "append" : bool Append to existing data (Default is false) + /// + /// Example: + /// + /// auto file = OptionsIO::create({ + /// {"file", "some_file.nc"}, + /// {"type", "netcdf"}, + /// {"append", false} + /// }); + static std::unique_ptr + create(std::initializer_list> config_list) { + Options config(config_list); // Construct an Options to pass by reference + return create(config); + } +}; + +class OptionsIOFactory : public Factory { +public: + static constexpr auto type_name = "OptionsIO"; + static constexpr auto section_name = "io"; + static constexpr auto option_name = "type"; + static constexpr auto default_type = +#if BOUT_HAS_NETCDF + "netcdf"; +#elif BOUT_HAS_ADIOS + "adios"; +#else + "invalid"; +#endif + + /// Create a restart file, configured with options (if given), + /// or root "restart_files" section. + /// + /// Options: + /// - "type" The type of file e.g "netcdf" or "adios" + /// - "file" Name of the file. Default is /.[type-dependent] + /// - "path" Path to restart files. Default is root "datadir" option, + /// that defaults to "data" + /// - "prefix" Default is "BOUT.restart" + ReturnType createRestart(Options* optionsptr = nullptr) const; + + /// Create an output file for writing time history. + /// Configure with options (if given), or root "output" section. + /// + /// Options: + /// - "type" The type of file e.g "netcdf" or "adios" + /// - "file" Name of the file. Default is /.[type] + /// - "path" Path to output files. Default is root "datadir" option, + /// that defaults to "data" + /// - "prefix" Default is "BOUT.dmp" + /// - "append" Append to existing file? Default is root "append" option, + /// that defaults to false. + ReturnType createOutput(Options* optionsptr = nullptr) const; + + /// Create a single file (e.g. mesh file) of the default type + ReturnType createFile(const std::string& file) const; +}; + +/// Simpler name for Factory registration helper class +/// +/// Usage: +/// +/// #include +/// namespace { +/// RegisterOptionsIO registeroptionsiomine("myoptionsio"); +/// } +template +using RegisterOptionsIO = OptionsIOFactory::RegisterInFactory; + +/// Simpler name for indicating that an OptionsIO implementation +/// is unavailable. +/// +/// Usage: +/// +/// namespace { +/// RegisterUnavailableOptionsIO +/// unavailablemyoptionsio("myoptiosio", "BOUT++ was not configured with MyOptionsIO"); +/// } +using RegisterUnavailableOptionsIO = OptionsIOFactory::RegisterUnavailableInFactory; + +/// Convenient wrapper function around OptionsIOFactory::createOutput +/// Opens a dump file configured with the `output` root section, +/// and writes the given `data` to the file. +void writeDefaultOutputFile(Options& data); + +} // namespace bout + +#endif // OPTIONS_IO_H diff --git a/include/bout/options_netcdf.hxx b/include/bout/options_netcdf.hxx deleted file mode 100644 index 2fdb71c6d4..0000000000 --- a/include/bout/options_netcdf.hxx +++ /dev/null @@ -1,118 +0,0 @@ - -#pragma once - -#ifndef __OPTIONS_NETCDF_H__ -#define __OPTIONS_NETCDF_H__ - -#include "bout/build_config.hxx" - -#if !BOUT_HAS_NETCDF || BOUT_HAS_LEGACY_NETCDF - -#include - -#include "bout/boutexception.hxx" -#include "bout/options.hxx" - -namespace bout { - -class OptionsNetCDF { -public: - enum class FileMode { - replace, ///< Overwrite file when writing - append ///< Append to file when writing - }; - - OptionsNetCDF(const std::string& filename, FileMode mode = FileMode::replace) {} - OptionsNetCDF(const OptionsNetCDF&) = default; - OptionsNetCDF(OptionsNetCDF&&) = default; - OptionsNetCDF& operator=(const OptionsNetCDF&) = default; - OptionsNetCDF& operator=(OptionsNetCDF&&) = default; - - /// Read options from file - Options read() { throw BoutException("OptionsNetCDF not available\n"); } - - /// Write options to file - void write(const Options& options) { - throw BoutException("OptionsNetCDF not available\n"); - } -}; - -} // namespace bout - -#else - -#include -#include - -#include "bout/options.hxx" - -/// Forward declare netCDF file type so we don't need to depend -/// directly on netCDF -namespace netCDF { -class NcFile; -} - -namespace bout { - -class OptionsNetCDF { -public: - enum class FileMode { - replace, ///< Overwrite file when writing - append ///< Append to file when writing - }; - - // Constructors need to be defined in implementation due to forward - // declaration of NcFile - OptionsNetCDF(); - explicit OptionsNetCDF(std::string filename, FileMode mode = FileMode::replace); - ~OptionsNetCDF(); - OptionsNetCDF(const OptionsNetCDF&) = delete; - OptionsNetCDF(OptionsNetCDF&&) noexcept; - OptionsNetCDF& operator=(const OptionsNetCDF&) = delete; - OptionsNetCDF& operator=(OptionsNetCDF&&) noexcept; - - /// Read options from file - Options read(); - - /// Write options to file - void write(const Options& options) { write(options, "t"); } - void write(const Options& options, const std::string& time_dim); - - /// Check that all variables with the same time dimension have the - /// same size in that dimension. Throws BoutException if there are - /// any differences, otherwise is silent - void verifyTimesteps() const; - -private: - /// Name of the file on disk - std::string filename; - /// How to open the file for writing - FileMode file_mode{FileMode::replace}; - /// Pointer to netCDF file so we don't introduce direct dependence - std::unique_ptr data_file; -}; - -} // namespace bout - -#endif - -namespace bout { -/// Name of the directory for restart files -std::string getRestartDirectoryName(Options& options); -/// Name of the restart file on this rank -std::string getRestartFilename(Options& options); -/// Name of the restart file on \p rank -std::string getRestartFilename(Options& options, int rank); -/// Name of the main output file on this rank -std::string getOutputFilename(Options& options); -/// Name of the main output file on \p rank -std::string getOutputFilename(Options& options, int rank); -/// Write `Options::root()` to the main output file, overwriting any -/// existing files -void writeDefaultOutputFile(); -/// Write \p options to the main output file, overwriting any existing -/// files -void writeDefaultOutputFile(Options& options); -} // namespace bout - -#endif // __OPTIONS_NETCDF_H__ diff --git a/include/bout/optionsreader.hxx b/include/bout/optionsreader.hxx index 428c7a8c8f..32c302a3f7 100644 --- a/include/bout/optionsreader.hxx +++ b/include/bout/optionsreader.hxx @@ -34,7 +34,6 @@ class OptionsReader; #ifndef __OPTIONSREADER_H__ #define __OPTIONSREADER_H__ -#include "bout/format.hxx" #include "bout/options.hxx" #include "fmt/core.h" diff --git a/include/bout/output.hxx b/include/bout/output.hxx index ef09aa7ee5..a44e987197 100644 --- a/include/bout/output.hxx +++ b/include/bout/output.hxx @@ -37,7 +37,6 @@ class Output; #include "bout/assert.hxx" #include "bout/boutexception.hxx" -#include "bout/format.hxx" #include "bout/sys/gettext.hxx" // for gettext _() macro #include "bout/unused.hxx" @@ -144,7 +143,7 @@ public: void print([[maybe_unused]] const std::string& message) override{}; void enable() override{}; void disable() override{}; - void enable(MAYBE_UNUSED(bool enable)){}; + void enable([[maybe_unused]] bool enable){}; bool isEnabled() override { return false; } }; diff --git a/include/bout/paralleltransform.hxx b/include/bout/paralleltransform.hxx index bb00dcbd45..4a7e4989c8 100644 --- a/include/bout/paralleltransform.hxx +++ b/include/bout/paralleltransform.hxx @@ -83,7 +83,7 @@ public: } /// Output variables used by a ParallelTransform instance to \p output_options - virtual void outputVars(MAYBE_UNUSED(Options& output_options)) {} + virtual void outputVars([[maybe_unused]] Options& output_options) {} /// If \p twist_shift_enabled is true, does a `Field3D` with Y direction \p ytype /// require a twist-shift at branch cuts on closed field lines? diff --git a/include/bout/physicsmodel.hxx b/include/bout/physicsmodel.hxx index a9a7d7344d..e0f046eb1f 100644 --- a/include/bout/physicsmodel.hxx +++ b/include/bout/physicsmodel.hxx @@ -42,7 +42,7 @@ class PhysicsModel; #include "bout/macro_for_each.hxx" #include "bout/msg_stack.hxx" #include "bout/options.hxx" -#include "bout/options_netcdf.hxx" +#include "bout/options_io.hxx" #include "bout/sys/variant.hxx" #include "bout/unused.hxx" #include "bout/utils.hxx" @@ -88,9 +88,6 @@ public: void add(Vector2D* value, const std::string& name, bool save_repeat = false); void add(Vector3D* value, const std::string& name, bool save_repeat = false); - /// Write stored data to file immediately - bool write(); - private: /// Helper struct to save enough information so that we can save an /// object to file later @@ -148,7 +145,7 @@ public: bout::DataFileFacade restart{}; /*! - * Initialse the model, calling the init() and postInit() methods + * Initialise the model, calling the init() and postInit() methods * * Note: this is usually only called by the Solver */ @@ -383,13 +380,13 @@ private: /// State for outputs Options output_options; /// File to write the outputs to - bout::OptionsNetCDF output_file; + std::unique_ptr output_file; /// Should we write output files bool output_enabled{true}; /// Stores the state for restarting Options restart_options; /// File to write the restart-state to - bout::OptionsNetCDF restart_file; + std::unique_ptr restart_file; /// Should we write restart files bool restart_enabled{true}; /// Split operator model? diff --git a/include/bout/region.hxx b/include/bout/region.hxx index 541280420b..13c4a137fa 100644 --- a/include/bout/region.hxx +++ b/include/bout/region.hxx @@ -566,6 +566,10 @@ public: Region(ContiguousBlocks& blocks) : blocks(blocks) { indices = getRegionIndices(); }; + bool operator==(const Region& other) const { + return std::equal(this->begin(), this->end(), other.begin(), other.end()); + } + /// Destructor ~Region() = default; @@ -683,8 +687,7 @@ public: return *this; // To allow command chaining }; - /// Returns a new region including only indices contained in both - /// this region and the other. + /// Get a new region including only indices that are in both regions. Region getIntersection(const Region& otherRegion) { // Get other indices and sort as we're going to be searching through // this vector so if it's sorted we can be more efficient @@ -941,7 +944,7 @@ Region mask(const Region& region, const Region& mask) { /// Return the intersection of two regions template -Region getIntersection(const Region& region, const Region& otherRegion) { +Region intersection(const Region& region, const Region& otherRegion) { auto result = region; return result.getIntersection(otherRegion); } diff --git a/include/bout/revision.hxx.in b/include/bout/revision.hxx.in index f0e3abdb8b..393e8cc34f 100644 --- a/include/bout/revision.hxx.in +++ b/include/bout/revision.hxx.in @@ -7,18 +7,16 @@ #ifndef BOUT_REVISION_H #define BOUT_REVISION_H -// TODO: Make these all `inline` when we upgrade to C++17 - namespace bout { namespace version { /// The git commit hash #ifndef BOUT_REVISION -constexpr auto revision = "@BOUT_REVISION@"; +inline constexpr auto revision = "@BOUT_REVISION@"; #else // Stringify value passed at compile time #define BUILDFLAG1_(x) #x #define BUILDFLAG(x) BUILDFLAG1_(x) -constexpr auto revision = BUILDFLAG(BOUT_REVISION); +inline constexpr auto revision = BUILDFLAG(BOUT_REVISION); #undef BUILDFLAG1 #undef BUILDFLAG #endif diff --git a/include/bout/solver.hxx b/include/bout/solver.hxx index 8a3f07c27a..d110d7ff17 100644 --- a/include/bout/solver.hxx +++ b/include/bout/solver.hxx @@ -33,8 +33,8 @@ * **************************************************************************/ -#ifndef __SOLVER_H__ -#define __SOLVER_H__ +#ifndef SOLVER_H +#define SOLVER_H #include "bout/build_config.hxx" @@ -63,7 +63,6 @@ using Jacobian = int (*)(BoutReal t); /// Solution monitor, called each timestep using TimestepMonitorFunc = int (*)(Solver* solver, BoutReal simtime, BoutReal lastdt); -//#include "bout/globals.hxx" #include "bout/field2d.hxx" #include "bout/field3d.hxx" #include "bout/generic_factory.hxx" @@ -270,7 +269,7 @@ public: virtual void constraint(Vector3D& v, Vector3D& C_v, std::string name); /// Set a maximum internal timestep (only for explicit schemes) - virtual void setMaxTimestep(MAYBE_UNUSED(BoutReal dt)) {} + virtual void setMaxTimestep([[maybe_unused]] BoutReal dt) {} /// Return the current internal timestep virtual BoutReal getCurrentTimestep() { return 0.0; } @@ -597,4 +596,4 @@ private: BoutReal output_timestep; }; -#endif // __SOLVER_H__ +#endif // SOLVER_H diff --git a/include/bout/sundials_backports.hxx b/include/bout/sundials_backports.hxx index c5e0f3ab15..c4f4aa59ef 100644 --- a/include/bout/sundials_backports.hxx +++ b/include/bout/sundials_backports.hxx @@ -27,19 +27,19 @@ #if SUNDIALS_VERSION_MAJOR < 3 using SUNLinearSolver = int*; -inline void SUNLinSolFree(MAYBE_UNUSED(SUNLinearSolver solver)) {} +inline void SUNLinSolFree([[maybe_unused]] SUNLinearSolver solver) {} using sunindextype = long int; #endif #if SUNDIALS_VERSION_MAJOR < 4 using SUNNonlinearSolver = int*; -inline void SUNNonlinSolFree(MAYBE_UNUSED(SUNNonlinearSolver solver)) {} +inline void SUNNonlinSolFree([[maybe_unused]] SUNNonlinearSolver solver) {} #endif #if SUNDIALS_VERSION_MAJOR < 6 namespace sundials { struct Context { - Context(void* comm MAYBE_UNUSED()) {} + Context(void* comm [[maybe_unused]]) {} }; } // namespace sundials @@ -51,13 +51,13 @@ constexpr auto SUN_PREC_NONE = PREC_NONE; inline N_Vector N_VNew_Parallel(MPI_Comm comm, sunindextype local_length, sunindextype global_length, - MAYBE_UNUSED(SUNContext sunctx)) { + [[maybe_unused]] SUNContext sunctx) { return N_VNew_Parallel(comm, local_length, global_length); } #if SUNDIALS_VERSION_MAJOR >= 3 inline SUNLinearSolver SUNLinSol_SPGMR(N_Vector y, int pretype, int maxl, - MAYBE_UNUSED(SUNContext sunctx)) { + [[maybe_unused]] SUNContext sunctx) { #if SUNDIALS_VERSION_MAJOR == 3 return SUNSPGMR(y, pretype, maxl); #else @@ -66,12 +66,12 @@ inline SUNLinearSolver SUNLinSol_SPGMR(N_Vector y, int pretype, int maxl, } #if SUNDIALS_VERSION_MAJOR >= 4 inline SUNNonlinearSolver SUNNonlinSol_FixedPoint(N_Vector y, int m, - MAYBE_UNUSED(SUNContext sunctx)) { + [[maybe_unused]] SUNContext sunctx) { return SUNNonlinSol_FixedPoint(y, m); } inline SUNNonlinearSolver SUNNonlinSol_Newton(N_Vector y, - MAYBE_UNUSED(SUNContext sunctx)) { + [[maybe_unused]] SUNContext sunctx) { return SUNNonlinSol_Newton(y); } #endif // SUNDIALS_VERSION_MAJOR >= 4 diff --git a/include/bout/sys/expressionparser.hxx b/include/bout/sys/expressionparser.hxx index 0ee3a7f97b..660ad20ab3 100644 --- a/include/bout/sys/expressionparser.hxx +++ b/include/bout/sys/expressionparser.hxx @@ -3,9 +3,9 @@ * * Parses strings containing expressions, returning a tree of generators * - * Copyright 2010 B.D.Dudson, S.Farley, M.V.Umansky, X.Q.Xu + * Copyright 2010-2024 BOUT++ contributors * - * Contact: Ben Dudson, bd512@york.ac.uk + * Contact: Ben Dudson, dudson2@llnl.gov * * This file is part of BOUT++. * @@ -24,10 +24,9 @@ * **************************************************************************/ -#ifndef __EXPRESSION_PARSER_H__ -#define __EXPRESSION_PARSER_H__ +#ifndef EXPRESSION_PARSER_H +#define EXPRESSION_PARSER_H -#include "bout/format.hxx" #include "bout/unused.hxx" #include "fmt/core.h" @@ -158,7 +157,7 @@ protected: /// Characters which cannot be used in symbols without escaping; /// all other allowed. In addition, whitespace cannot be used. /// Adding a binary operator adds its symbol to this string - std::string reserved_chars = "+-*/^[](){},="; + std::string reserved_chars = "+-*/^[](){},=!"; private: std::map gen; ///< Generators, addressed by name @@ -260,4 +259,4 @@ private: std::string message; }; -#endif // __EXPRESSION_PARSER_H__ +#endif // EXPRESSION_PARSER_H diff --git a/include/bout/sys/generator_context.hxx b/include/bout/sys/generator_context.hxx index 3f912ec1da..528b96113d 100644 --- a/include/bout/sys/generator_context.hxx +++ b/include/bout/sys/generator_context.hxx @@ -28,10 +28,7 @@ public: Context(int ix, int iy, int iz, CELL_LOC loc, Mesh* msh, BoutReal t); /// If constructed without parameters, contains no values (null). - /// Requesting x,y,z or t should throw an exception - /// - /// NOTE: For backward compatibility, all locations are set to zero. - /// This should be changed in a future release. + /// Requesting x,y,z or t throws an exception Context() = default; /// The location on the boundary @@ -60,7 +57,13 @@ public: } /// Retrieve a value previously set - BoutReal get(const std::string& name) const { return parameters.at(name); } + BoutReal get(const std::string& name) const { + auto it = parameters.find(name); + if (it != parameters.end()) { + return it->second; + } + throw BoutException("Generator context doesn't contain '{:s}'", name); + } /// Get the mesh for this context (position) /// If the mesh is null this will throw a BoutException (if CHECK >= 1) @@ -73,8 +76,7 @@ private: Mesh* localmesh{nullptr}; ///< The mesh on which the position is defined /// Contains user-set values which can be set and retrieved - std::map parameters{ - {"x", 0.0}, {"y", 0.0}, {"z", 0.0}, {"t", 0.0}}; + std::map parameters{}; }; } // namespace generator diff --git a/include/bout/unused.hxx b/include/bout/unused.hxx index 6e7a46c7c0..74fd3c2f98 100644 --- a/include/bout/unused.hxx +++ b/include/bout/unused.hxx @@ -37,24 +37,4 @@ #define UNUSED(x) x #endif -/// Mark a function parameter as possibly unused in the function body -/// -/// Unlike `UNUSED`, this has to go around the type as well: -/// -/// MAYBE_UNUSED(int foo); -#ifdef __has_cpp_attribute -#if __has_cpp_attribute(maybe_unused) -#define MAYBE_UNUSED(x) [[maybe_unused]] x -#endif -#endif -#ifndef MAYBE_UNUSED -#if defined(__GNUC__) -#define MAYBE_UNUSED(x) [[gnu::unused]] x -#elif defined(_MSC_VER) -#define MAYBE_UNUSED(x) __pragma(warning(suppress : 4100)) x -#else -#define MAYBE_UNUSED(x) x -#endif -#endif - #endif //__UNUSED_H__ diff --git a/include/bout/utils.hxx b/include/bout/utils.hxx index 3e02b74c39..c650293c40 100644 --- a/include/bout/utils.hxx +++ b/include/bout/utils.hxx @@ -205,6 +205,8 @@ public: using size_type = int; Matrix() = default; + Matrix(Matrix&&) noexcept = default; + Matrix& operator=(Matrix&&) noexcept = default; Matrix(size_type n1, size_type n2) : n1(n1), n2(n2) { ASSERT2(n1 >= 0); ASSERT2(n2 >= 0); @@ -215,6 +217,7 @@ public: // Prevent copy on write for Matrix data.ensureUnique(); } + ~Matrix() = default; /// Reallocate the Matrix to shape \p new_size_1 by \p new_size_2 /// @@ -299,6 +302,8 @@ public: using size_type = int; Tensor() = default; + Tensor(Tensor&&) noexcept = default; + Tensor& operator=(Tensor&&) noexcept = default; Tensor(size_type n1, size_type n2, size_type n3) : n1(n1), n2(n2), n3(n3) { ASSERT2(n1 >= 0); ASSERT2(n2 >= 0); @@ -310,6 +315,7 @@ public: // Prevent copy on write for Tensor data.ensureUnique(); } + ~Tensor() = default; /// Reallocate the Tensor with shape \p new_size_1 by \p new_size_2 by \p new_size_3 /// @@ -355,6 +361,13 @@ public: ASSERT2(0 <= i.ind && i.ind < n1 * n2 * n3); return data[i.ind]; } + T& operator[](Ind3D i) { + // ny and nz are private :-( + // ASSERT2(i.nz == n3); + // ASSERT2(i.ny == n2); + ASSERT2(0 <= i.ind && i.ind < n1 * n2 * n3); + return data[i.ind]; + } Tensor& operator=(const T& val) { for (auto& i : data) { diff --git a/include/bout/vector2d.hxx b/include/bout/vector2d.hxx index 9ff4a69ba8..974c5f81db 100644 --- a/include/bout/vector2d.hxx +++ b/include/bout/vector2d.hxx @@ -39,7 +39,7 @@ class Vector2D; class Field2D; class Field3D; -class Vector3D; //#include "bout/vector3d.hxx" +class Vector3D; #include diff --git a/include/bout/vector3d.hxx b/include/bout/vector3d.hxx index f3adf41ae8..93ee798663 100644 --- a/include/bout/vector3d.hxx +++ b/include/bout/vector3d.hxx @@ -33,11 +33,10 @@ class Vector3D; #ifndef __VECTOR3D_H__ #define __VECTOR3D_H__ -class Field2D; //#include "bout/field2d.hxx" +class Field2D; +class Vector2D; #include "bout/field3d.hxx" -class Vector2D; //#include "bout/vector2d.hxx" - /*! * Represents a 3D vector, with x,y,z components * stored as separate Field3D objects diff --git a/include/bout/version.hxx.in b/include/bout/version.hxx.in index 06a32808af..3eaf40dbd7 100644 --- a/include/bout/version.hxx.in +++ b/include/bout/version.hxx.in @@ -5,22 +5,20 @@ #ifndef BOUT_VERSION_H #define BOUT_VERSION_H -// TODO: Make these all `inline` when we upgrade to C++17 - namespace bout { namespace version { /// The full version number -constexpr auto full = "@BOUT_VERSION@"; +inline constexpr auto full = "@BOUT_VERSION@"; /// The major version number -constexpr int major = @BOUT_VERSION_MAJOR@; +inline constexpr int major = @BOUT_VERSION_MAJOR@; /// The minor version number -constexpr int minor = @BOUT_VERSION_MINOR@; +inline constexpr int minor = @BOUT_VERSION_MINOR@; /// The patch version number -constexpr int patch = @BOUT_VERSION_PATCH@; +inline constexpr int patch = @BOUT_VERSION_PATCH@; /// The version pre-release identifier -constexpr auto prerelease = "@BOUT_VERSION_TAG@"; +inline constexpr auto prerelease = "@BOUT_VERSION_TAG@"; /// The full version number as a double -constexpr double as_double = @BOUT_VERSION_MAJOR@.@BOUT_VERSION_MINOR@@BOUT_VERSION_PATCH@; +inline constexpr double as_double = @BOUT_VERSION_MAJOR@.@BOUT_VERSION_MINOR@@BOUT_VERSION_PATCH@; } // namespace version } // namespace bout diff --git a/manual/sphinx/index.rst b/manual/sphinx/index.rst index 46728c7119..9f661ca187 100644 --- a/manual/sphinx/index.rst +++ b/manual/sphinx/index.rst @@ -42,6 +42,7 @@ The documentation is divided into the following sections: user_docs/boundary_options user_docs/testing user_docs/gpu_support + user_docs/adios2 .. toctree:: :maxdepth: 2 diff --git a/manual/sphinx/user_docs/adios2.rst b/manual/sphinx/user_docs/adios2.rst new file mode 100644 index 0000000000..8a6228cd3a --- /dev/null +++ b/manual/sphinx/user_docs/adios2.rst @@ -0,0 +1,45 @@ +.. _sec-adios2: + +ADIOS2 support +============== + +This section summarises the use of `ADIOS2 `_ in BOUT++. + +Installation +------------ + +The easiest way to configure BOUT++ with ADIOS2 is to tell CMake to download and build it +with this flag:: + + -DBOUT_DOWNLOAD_ADIOS=ON + +The ``master`` branch will be downloaded from `Github `_, +configured and built with BOUT++. + +Alternatively, if ADIOS is already installed then the following flags can be used:: + + -DBOUT_USE_ADIOS=ON -DADIOS2_ROOT=/path/to/adios2 + +Output files +------------ + +The output (dump) files are controlled with the root ``output`` options. +By default the output format is NetCDF, so to use ADIOS2 instead set +the output type in BOUT.inp:: + + [output] + type = adios + +or on the BOUT++ command line set ``output:type=adios``. The default +prefix is "BOUT.dmp" so the ADIOS file will be called "BOUT.dmp.bp". To change this, +set the ``output:prefix`` option. + +Restart files +------------- + +The restart files are contolled with the root ``restart_files`` options, +so to read and write restarts from an ADIOS dataset, put in BOUT.inp:: + + [restart_files] + type = adios + diff --git a/manual/sphinx/user_docs/advanced_install.rst b/manual/sphinx/user_docs/advanced_install.rst index 957173b820..e25be12b4b 100644 --- a/manual/sphinx/user_docs/advanced_install.rst +++ b/manual/sphinx/user_docs/advanced_install.rst @@ -170,8 +170,10 @@ for a production run use: File formats ------------ -BOUT++ can currently use the NetCDF-4_ file format, with experimental -support for the parallel flavour. NetCDF is a widely used format and +BOUT++ can currently use the NetCDF-4_ file format and the ADIOS2 library +for high-performance parallel output. + +NetCDF is a widely used format and has many tools for viewing and manipulating files. .. _NetCDF-4: https://www.unidata.ucar.edu/software/netcdf/ diff --git a/manual/sphinx/user_docs/bout_options.rst b/manual/sphinx/user_docs/bout_options.rst index 5422558ff9..85a8a17d59 100644 --- a/manual/sphinx/user_docs/bout_options.rst +++ b/manual/sphinx/user_docs/bout_options.rst @@ -48,9 +48,10 @@ name in square brackets. Option names can contain almost any character except ’=’ and ’:’, including unicode. If they start with a number or ``.``, contain -arithmetic symbols (``+-*/^``), brackets (``(){}[]``), equality -(``=``), whitespace or comma ``,``, then these will need to be escaped -in expressions. See below for how this is done. +arithmetic/boolean operator symbols (``+-*/^&|!<>``), brackets +(``(){}[]``), equality (``=``), whitespace or comma ``,``, then these +will need to be escaped in expressions. See below for how this is +done. Subsections can also be used, separated by colons ’:’, e.g. @@ -87,6 +88,13 @@ operators, with the usual precedence rules. In addition to ``π``, expressions can use predefined variables ``x``, ``y``, ``z`` and ``t`` to refer to the spatial and time coordinates (for definitions of the values these variables take see :ref:`sec-expressions`). + +.. note:: The variables ``x``, ``y``, ``z`` should only be defined + when reading a 3D field; ``t`` should only be defined when reading + a time-dependent value. Earlier BOUT++ versions (v5.1.0 and earler) + defined all of these to be 0 by default e.g. when reading scalar + inputs. + A number of functions are defined, listed in table :numref:`tab-initexprfunc`. One slightly unusual feature (borrowed from `Julia `_) is that if a number comes before a symbol or an opening bracket (``(``) @@ -109,11 +117,11 @@ The convention is the same as in `Python `_: If brackets are not balanced (closed) then the expression continues on the next line. All expressions are calculated in floating point and then converted to -an integer if needed when read inside BOUT++. The conversion is done by rounding -to the nearest integer, but throws an error if the floating point -value is not within :math:`1e-3` of an integer. This is to minimise -unexpected behaviour. If you want to round any result to an integer, -use the ``round`` function: +an integer (or boolean) if needed when read inside BOUT++. The +conversion is done by rounding to the nearest integer, but throws an +error if the floating point value is not within :math:`1e-3` of an +integer. This is to minimise unexpected behaviour. If you want to +round any result to an integer, use the ``round`` function: .. code-block:: cfg @@ -125,6 +133,43 @@ number, since the type is determined by how it is used. Have a look through the examples to see how the options are used. +Boolean expressions +~~~~~~~~~~~~~~~~~~~ + +Boolean values must be "true", "false", "True", "False", "1" or +"0". All lowercase ("true"/"false") is preferred, but the uppercase +versions are allowed to support Python string conversions. Booleans +can be combined into expressions using binary operators `&` (logical +AND), `|` (logical OR), and unary operator `!` (logical NOT). For +example "true & false" evaluates to `false`; "!false" evaluates to +`true`. Like real values and integers, boolean expressions can refer +to other variables: + +.. code-block:: cfg + + switch = true + other_switch = !switch + +Boolean expressions can be formed by comparing real values using +`>` and `<` comparison operators: + +.. code-block:: cfg + + value = 3.2 + is_true = value > 3 + is_false = value < 2 + +.. note:: + Previous BOUT++ versions (v5.1.0 and earlier) were case + insensitive when reading boolean values, so would read "True" or + "yEs" as `true`, and "False" or "No" as `false`. These earlier + versions did not allow boolean expressions. + +Internally, booleans are evaluated as real values, with `true` being 1 +and `false` being 0. Logical operators (`&`, `|`, `!`) check that +their left and right arguments are either close to 0 or close to 1 +(like integers, "close to" is within 1e-3). + Special symbols in Option names ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -523,6 +568,12 @@ options available are listed in table :numref:`tab-outputopts`. +-------------+----------------------------------------------------+--------------+ | enabled | Writing is enabled | true | +-------------+----------------------------------------------------+--------------+ + | type | File type e.g. "netcdf" or "adios" | "netcdf" | + +-------------+----------------------------------------------------+--------------+ + | prefix | File name prefix | "BOUT.dmp" | + +-------------+----------------------------------------------------+--------------+ + | path | Directory to write the file into | ``datadir`` | + +-------------+----------------------------------------------------+--------------+ | floats | Write floats rather than doubles | false | +-------------+----------------------------------------------------+--------------+ | flush | Flush the file to disk after each write | true | @@ -531,8 +582,6 @@ options available are listed in table :numref:`tab-outputopts`. +-------------+----------------------------------------------------+--------------+ | openclose | Re-open the file for each write, and close after | true | +-------------+----------------------------------------------------+--------------+ - | parallel | Use parallel I/O | false | - +-------------+----------------------------------------------------+--------------+ | @@ -541,20 +590,6 @@ want to exclude I/O from the timings. **floats** can be used to reduce the size of the output files: files are stored as double by default, but setting **floats = true** changes the output to single-precision floats. -To enable parallel I/O for either output or restart files, set - -.. code-block:: cfg - - parallel = true - -in the output or restart section. If you have compiled BOUT++ with a -parallel I/O library such as pnetcdf (see -:ref:`sec-advancedinstall`), then rather than outputting one file per -processor, all processors will output to the same file. For restart -files this is particularly useful, as it means that you can restart a -job with a different number of processors. Note that this feature is -still experimental, and incomplete: output dump files are not yet -supported by the collect routines. Implementation -------------- @@ -833,30 +868,30 @@ This is currently quite rudimentary and needs improving. .. _sec-options-netcdf: -Reading and writing to NetCDF ------------------------------ +Reading and writing to binary formats +------------------------------------- -The `bout::OptionsNetCDF` class provides an interface to read and -write options. Examples are in integrated test +The `bout::OptionsIO` class provides an interface to read and +write options to binary files. Examples are in integrated test ``tests/integrated/test-options-netcdf/`` To write the current `Options` tree (e.g. from ``BOUT.inp``) to a NetCDF file:: - bout::OptionsNetCDF("settings.nc").write(Options::root()); + bout::OptionsIO::create("settings.nc")->write(Options::root()); and to read it in again:: - Options data = bout::OptionsNetCDF("settings.nc").read(); + Options data = bout::OptionsIO::create("settings.nc")->read(); Fields can also be stored and written:: Options fields; fields["f2d"] = Field2D(1.0); fields["f3d"] = Field3D(2.0); - bout::OptionsNetCDF("fields.nc").write(fields); + bout::OptionsIO::create("fields.nc").write(fields); -This should allow the input settings and evolving variables to be +This allows the input settings and evolving variables to be combined into a single tree (see above on joining trees) and written to the output dump or restart files. @@ -865,7 +900,7 @@ an ``Array``, 2D as ``Matrix`` and 3D as ``Tensor``. These can be extracted directly from the ``Options`` tree, or converted to a Field:: - Options fields_in = bout::OptionsNetCDF("fields.nc").read(); + Options fields_in = bout::OptionsIO::create("fields.nc")->read(); Field2D f2d = fields_in["f2d"].as(); Field3D f3d = fields_in["f3d"].as(); @@ -907,7 +942,7 @@ automatically set the ``"time_dimension"`` attribute:: // Or use `assignRepeat` to do it automatically: data["field"].assignRepeat(Field3D(2.0)); - bout::OptionsNetCDF("time.nc").write(data); + bout::OptionsIO::create("time.nc")->write(data); // Update time-dependent values. This can be done without `force` if the time_dimension // attribute is set @@ -915,13 +950,13 @@ automatically set the ``"time_dimension"`` attribute:: data["field"] = Field3D(3.0); // Append data to file - bout::OptionsNetCDF("time.nc", bout::OptionsNetCDF::FileMode::append).write(data); + bout::OptionsIO({{"file", "time.nc"}, {"append", true}})->write(data); -.. note:: By default, `bout::OptionsNetCDF::write` will only write variables +.. note:: By default, `bout::OptionsIO::write` will only write variables with a ``"time_dimension"`` of ``"t"``. You can write variables with a different time dimension by passing it as the second argument: - ``OptionsNetCDF(filename).write(options, "t2")`` for example. + ``OptionsIO::create(filename)->write(options, "t2")`` for example. FFT diff --git a/manual/sphinx/user_docs/installing.rst b/manual/sphinx/user_docs/installing.rst index fc1b2ce2da..eb155909bf 100644 --- a/manual/sphinx/user_docs/installing.rst +++ b/manual/sphinx/user_docs/installing.rst @@ -373,6 +373,10 @@ For SUNDIALS, use ``-DBOUT_DOWNLOAD_SUNDIALS=ON``. If using ``ccmake`` this opti may not appear initially. This automatically sets ``BOUT_USE_SUNDIALS=ON``, and configures SUNDIALS to use MPI. +For ADIOS2, use ``-DBOUT_DOWNLOAD_ADIOS=ON``. This will download and +configure `ADIOS2 `_, enabling BOUT++ +to read and write this high-performance parallel file format. + Bundled Dependencies ~~~~~~~~~~~~~~~~~~~~ diff --git a/src/bout++.cxx b/src/bout++.cxx index 127cb6ff4a..481a928bec 100644 --- a/src/bout++.cxx +++ b/src/bout++.cxx @@ -4,9 +4,9 @@ * Adapted from the BOUT code by B.Dudson, University of York, Oct 2007 * ************************************************************************** - * Copyright 2010 B.D.Dudson, S.Farley, M.V.Umansky, X.Q.Xu + * Copyright 2010-2023 BOUT++ contributors * - * Contact Ben Dudson, bd512@york.ac.uk + * Contact Ben Dudson, dudson2@llnl.gov * * This file is part of BOUT++. * @@ -59,6 +59,10 @@ const char DEFAULT_DIR[] = "data"; #include "bout/bout.hxx" #undef BOUT_NO_USING_NAMESPACE_BOUTGLOBALS +#if BOUT_HAS_ADIOS +#include "bout/adios_object.hxx" +#endif + #include #include @@ -161,6 +165,10 @@ int BoutInitialise(int& argc, char**& argv) { savePIDtoFile(args.data_dir, MYPE); +#if BOUT_HAS_ADIOS + bout::ADIOSInit(BoutComm::get()); +#endif + // Print the different parts of the startup info printStartupHeader(MYPE, BoutComm::size()); printCompileTimeOptions(); @@ -182,8 +190,7 @@ int BoutInitialise(int& argc, char**& argv) { // but it's possible that only happens in BoutFinalise, which is // too late for that check. const auto datadir = Options::root()["datadir"].withDefault(DEFAULT_DIR); - MAYBE_UNUSED() - const auto optionfile = + [[maybe_unused]] const auto optionfile = Options::root()["optionfile"].withDefault(args.opt_file); const auto settingsfile = Options::root()["settingsfile"].withDefault(args.set_file); @@ -565,6 +572,7 @@ void printCompileTimeOptions() { constexpr auto netcdf_flavour = has_netcdf ? (has_legacy_netcdf ? " (Legacy)" : " (NetCDF4)") : ""; output_info.write(_("\tNetCDF support {}{}\n"), is_enabled(has_netcdf), netcdf_flavour); + output_info.write(_("\tADIOS support {}\n"), is_enabled(has_adios)); output_info.write(_("\tPETSc support {}\n"), is_enabled(has_petsc)); output_info.write(_("\tPretty function name support {}\n"), is_enabled(has_pretty_function)); @@ -693,6 +701,7 @@ void addBuildFlagsToOptions(Options& options) { options["has_gettext"].force(bout::build::has_gettext); options["has_lapack"].force(bout::build::has_lapack); options["has_netcdf"].force(bout::build::has_netcdf); + options["has_adios"].force(bout::build::has_adios); options["has_petsc"].force(bout::build::has_petsc); options["has_hypre"].force(bout::build::has_hypre); options["has_umpire"].force(bout::build::has_umpire); @@ -788,6 +797,10 @@ int BoutFinalise(bool write_settings) { // Call HYPER_Finalize if not already called bout::HypreLib::cleanup(); +#if BOUT_HAS_ADIOS + bout::ADIOSFinalize(); +#endif + // MPI communicator, including MPI_Finalize() BoutComm::cleanup(); @@ -823,7 +836,7 @@ BoutMonitor::BoutMonitor(BoutReal timestep, Options& options) .doc(_("Name of file whose existence triggers a stop")) .withDefault("BOUT.stop"))) {} -int BoutMonitor::call(Solver* solver, BoutReal t, MAYBE_UNUSED(int iter), int NOUT) { +int BoutMonitor::call(Solver* solver, BoutReal t, [[maybe_unused]] int iter, int NOUT) { TRACE("BoutMonitor::call({:e}, {:d}, {:d})", t, iter, NOUT); // Increment Solver's iteration counter, and set the global `iteration` diff --git a/src/field/field.cxx b/src/field/field.cxx index 54883c506c..e48a8f3ef7 100644 --- a/src/field/field.cxx +++ b/src/field/field.cxx @@ -23,8 +23,6 @@ * **************************************************************************/ -//#include - #include #include #include diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 0bd9e66fc4..b4bb0d394f 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -173,7 +173,7 @@ const Field3D& Field3D::ynext(int dir) const { if (dir > 0) { return yup(dir - 1); } else if (dir < 0) { - return ydown(std::abs(dir) - 1); + return ydown(-dir - 1); } else { return *this; } @@ -742,7 +742,7 @@ namespace { #if CHECK > 2 void checkDataIsFiniteOnRegion(const Field3D& f, const std::string& region) { // Do full checks - BOUT_FOR_SERIAL(i, f.getRegion(region)) { + BOUT_FOR_SERIAL(i, f.getValidRegionWithDefault(region)) { if (!finite(f[i])) { throw BoutException("Field3D: Operation on non-finite data at [{:d}][{:d}][{:d}]\n", i.x(), i.y(), i.z()); @@ -819,3 +819,15 @@ void swap(Field3D& first, Field3D& second) noexcept { swap(first.yup_fields, second.yup_fields); swap(first.ydown_fields, second.ydown_fields); } + +const Region& +Field3D::getValidRegionWithDefault(const std::string& region_name) const { + if (regionID.has_value()) { + return fieldmesh->getRegion(regionID.value()); + } + return fieldmesh->getRegion(region_name); +} + +void Field3D::setRegion(const std::string& region_name) { + regionID = fieldmesh->getRegionID(region_name); +} diff --git a/src/field/field_factory.cxx b/src/field/field_factory.cxx index fb03c3b174..f65f2e7f55 100644 --- a/src/field/field_factory.cxx +++ b/src/field/field_factory.cxx @@ -93,8 +93,20 @@ FieldFactory::FieldFactory(Mesh* localmesh, Options* opt) // Note: don't use 'options' here because 'options' is a 'const Options*' // pointer, so this would fail if the "input" section is not present. Options& nonconst_options{opt == nullptr ? Options::root() : *opt}; - transform_from_field_aligned = - nonconst_options["input"]["transform_from_field_aligned"].withDefault(true); + + // Convert from string, or FieldFactory is used to parse the string + auto str = + nonconst_options["input"]["transform_from_field_aligned"].withDefault( + "true"); + if ((str == "true") or (str == "True")) { + transform_from_field_aligned = true; + } else if ((str == "false") or (str == "False")) { + transform_from_field_aligned = false; + } else { + throw ParseException( + "Invalid boolean given as input:transform_from_field_aligned: '{:s}'", + nonconst_options["input"]["transform_from_field_aligned"].as()); + } // Convert using stoi rather than Options, or a FieldFactory is used to parse // the string, leading to infinite loop. @@ -114,6 +126,14 @@ FieldFactory::FieldFactory(Mesh* localmesh, Options* opt) addGenerator("pi", std::make_shared(PI)); addGenerator("π", std::make_shared(PI)); + // Boolean values + addGenerator("true", std::make_shared(1)); + addGenerator("false", std::make_shared(0)); + + // Python converts booleans to True/False + addGenerator("True", std::make_shared(1)); + addGenerator("False", std::make_shared(0)); + // Some standard functions addGenerator("sin", std::make_shared>(nullptr, "sin")); addGenerator("cos", std::make_shared>(nullptr, "cos")); diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index 249245b21c..ecd4e628cc 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -8,6 +8,17 @@ checkData({{lhs.name}}); checkData({{rhs.name}}); + {% if out == "Field3D" %} + {% if lhs == rhs == "Field3D" %} + {{out.name}}.setRegion({{lhs.name}}.getMesh()->getCommonRegion({{lhs.name}}.getRegionID(), + {{rhs.name}}.getRegionID())); + {% elif lhs == "Field3D" %} + {{out.name}}.setRegion({{lhs.name}}.getRegionID()); + {% elif rhs == "Field3D" %} + {{out.name}}.setRegion({{rhs.name}}.getRegionID()); + {% endif %} + {% endif %} + {% if (out == "Field3D") and ((lhs == "Field2D") or (rhs =="Field2D")) %} Mesh *localmesh = {{lhs.name if lhs.field_type != "BoutReal" else rhs.name}}.getMesh(); @@ -41,11 +52,11 @@ } {% elif (operator == "/") and (rhs == "BoutReal") %} const auto tmp = 1.0 / {{rhs.index}}; - {{region_loop}}({{index_var}}, {{out.name}}.getRegion({{region_name}})) { + {{region_loop}}({{index_var}}, {{out.name}}.getValidRegionWithDefault({{region_name}})) { {{out.index}} = {{lhs.index}} * tmp; } {% else %} - {{region_loop}}({{index_var}}, {{out.name}}.getRegion({{region_name}})) { + {{region_loop}}({{index_var}}, {{out.name}}.getValidRegionWithDefault({{region_name}})) { {{out.index}} = {{lhs.index}} {{operator}} {{rhs.index}}; } {% endif %} @@ -73,6 +84,11 @@ checkData(*this); checkData({{rhs.name}}); + {% if lhs == rhs == "Field3D" %} + regionID = fieldmesh->getCommonRegion(regionID, {{rhs.name}}.regionID); + {% endif %} + + {% if (lhs == "Field3D") and (rhs =="Field2D") %} {{region_loop}}({{index_var}}, {{rhs.name}}.getRegion({{region_name}})) { const auto {{mixed_base_ind}} = fieldmesh->ind2Dto3D({{index_var}}); diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index a3613eca3e..6b778acee3 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -14,7 +14,9 @@ Field3D operator*(const Field3D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] * rhs[index]; } @@ -36,6 +38,8 @@ Field3D& Field3D::operator*=(const Field3D& rhs) { checkData(*this); checkData(rhs); + regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } checkData(*this); @@ -54,7 +58,9 @@ Field3D operator/(const Field3D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] / rhs[index]; } @@ -76,6 +82,8 @@ Field3D& Field3D::operator/=(const Field3D& rhs) { checkData(*this); checkData(rhs); + regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } checkData(*this); @@ -94,7 +102,9 @@ Field3D operator+(const Field3D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] + rhs[index]; } @@ -116,6 +126,8 @@ Field3D& Field3D::operator+=(const Field3D& rhs) { checkData(*this); checkData(rhs); + regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } checkData(*this); @@ -134,7 +146,9 @@ Field3D operator-(const Field3D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] - rhs[index]; } @@ -156,6 +170,8 @@ Field3D& Field3D::operator-=(const Field3D& rhs) { checkData(*this); checkData(rhs); + regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } checkData(*this); @@ -174,6 +190,8 @@ Field3D operator*(const Field3D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(lhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { @@ -224,6 +242,8 @@ Field3D operator/(const Field3D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(lhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { @@ -276,6 +296,8 @@ Field3D operator+(const Field3D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(lhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { @@ -326,6 +348,8 @@ Field3D operator-(const Field3D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(lhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { @@ -455,7 +479,11 @@ Field3D operator*(const Field3D& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] * rhs; } + result.setRegion(lhs.getRegionID()); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * rhs; + } checkData(result); return result; @@ -491,8 +519,12 @@ Field3D operator/(const Field3D& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); + result.setRegion(lhs.getRegionID()); + const auto tmp = 1.0 / rhs; - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] * tmp; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * tmp; + } checkData(result); return result; @@ -529,7 +561,11 @@ Field3D operator+(const Field3D& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] + rhs; } + result.setRegion(lhs.getRegionID()); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] + rhs; + } checkData(result); return result; @@ -565,7 +601,11 @@ Field3D operator-(const Field3D& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] - rhs; } + result.setRegion(lhs.getRegionID()); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] - rhs; + } checkData(result); return result; @@ -602,6 +642,8 @@ Field3D operator*(const Field2D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(rhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, lhs.getRegion("RGN_ALL")) { @@ -623,6 +665,8 @@ Field3D operator/(const Field2D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(rhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, lhs.getRegion("RGN_ALL")) { @@ -644,6 +688,8 @@ Field3D operator+(const Field2D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(rhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, lhs.getRegion("RGN_ALL")) { @@ -665,6 +711,8 @@ Field3D operator-(const Field2D& lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); + result.setRegion(rhs.getRegionID()); + Mesh* localmesh = lhs.getMesh(); BOUT_FOR(index, lhs.getRegion("RGN_ALL")) { @@ -686,7 +734,7 @@ Field2D operator*(const Field2D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] * rhs[index]; } @@ -722,7 +770,7 @@ Field2D operator/(const Field2D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] / rhs[index]; } @@ -758,7 +806,7 @@ Field2D operator+(const Field2D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] + rhs[index]; } @@ -794,7 +842,7 @@ Field2D operator-(const Field2D& lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] - rhs[index]; } @@ -909,7 +957,9 @@ Field2D operator*(const Field2D& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] * rhs; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * rhs; + } checkData(result); return result; @@ -942,7 +992,9 @@ Field2D operator/(const Field2D& lhs, const BoutReal rhs) { checkData(rhs); const auto tmp = 1.0 / rhs; - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] * tmp; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * tmp; + } checkData(result); return result; @@ -975,7 +1027,9 @@ Field2D operator+(const Field2D& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] + rhs; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] + rhs; + } checkData(result); return result; @@ -1007,7 +1061,9 @@ Field2D operator-(const Field2D& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] - rhs; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] - rhs; + } checkData(result); return result; @@ -1408,7 +1464,7 @@ FieldPerp operator*(const FieldPerp& lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] * rhs[index]; } @@ -1444,7 +1500,7 @@ FieldPerp operator/(const FieldPerp& lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] / rhs[index]; } @@ -1480,7 +1536,7 @@ FieldPerp operator+(const FieldPerp& lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] + rhs[index]; } @@ -1516,7 +1572,7 @@ FieldPerp operator-(const FieldPerp& lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] - rhs[index]; } @@ -1551,7 +1607,9 @@ FieldPerp operator*(const FieldPerp& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] * rhs; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * rhs; + } checkData(result); return result; @@ -1584,7 +1642,9 @@ FieldPerp operator/(const FieldPerp& lhs, const BoutReal rhs) { checkData(rhs); const auto tmp = 1.0 / rhs; - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] * tmp; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * tmp; + } checkData(result); return result; @@ -1616,7 +1676,9 @@ FieldPerp operator+(const FieldPerp& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] + rhs; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] + rhs; + } checkData(result); return result; @@ -1648,7 +1710,9 @@ FieldPerp operator-(const FieldPerp& lhs, const BoutReal rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs[index] - rhs; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] - rhs; + } checkData(result); return result; @@ -1680,7 +1744,11 @@ Field3D operator*(const BoutReal lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs * rhs[index]; } + result.setRegion(rhs.getRegionID()); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs * rhs[index]; + } checkData(result); return result; @@ -1693,7 +1761,11 @@ Field3D operator/(const BoutReal lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs / rhs[index]; } + result.setRegion(rhs.getRegionID()); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs / rhs[index]; + } checkData(result); return result; @@ -1706,7 +1778,11 @@ Field3D operator+(const BoutReal lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs + rhs[index]; } + result.setRegion(rhs.getRegionID()); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs + rhs[index]; + } checkData(result); return result; @@ -1719,7 +1795,11 @@ Field3D operator-(const BoutReal lhs, const Field3D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs - rhs[index]; } + result.setRegion(rhs.getRegionID()); + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs - rhs[index]; + } checkData(result); return result; @@ -1732,7 +1812,9 @@ Field2D operator*(const BoutReal lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs * rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs * rhs[index]; + } checkData(result); return result; @@ -1745,7 +1827,9 @@ Field2D operator/(const BoutReal lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs / rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs / rhs[index]; + } checkData(result); return result; @@ -1758,7 +1842,9 @@ Field2D operator+(const BoutReal lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs + rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs + rhs[index]; + } checkData(result); return result; @@ -1771,7 +1857,9 @@ Field2D operator-(const BoutReal lhs, const Field2D& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs - rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs - rhs[index]; + } checkData(result); return result; @@ -1784,7 +1872,9 @@ FieldPerp operator*(const BoutReal lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs * rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs * rhs[index]; + } checkData(result); return result; @@ -1797,7 +1887,9 @@ FieldPerp operator/(const BoutReal lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs / rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs / rhs[index]; + } checkData(result); return result; @@ -1810,7 +1902,9 @@ FieldPerp operator+(const BoutReal lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs + rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs + rhs[index]; + } checkData(result); return result; @@ -1823,7 +1917,9 @@ FieldPerp operator-(const BoutReal lhs, const FieldPerp& rhs) { checkData(lhs); checkData(rhs); - BOUT_FOR(index, result.getRegion("RGN_ALL")) { result[index] = lhs - rhs[index]; } + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs - rhs[index]; + } checkData(result); return result; diff --git a/src/invert/fft_fftw.cxx b/src/invert/fft_fftw.cxx index f158fa3d7a..514396c828 100644 --- a/src/invert/fft_fftw.cxx +++ b/src/invert/fft_fftw.cxx @@ -106,8 +106,8 @@ void fft_init(bool fft_measure) { #if !BOUT_USE_OPENMP // Serial code -void rfft(MAYBE_UNUSED(const BoutReal* in), MAYBE_UNUSED(int length), - MAYBE_UNUSED(dcomplex* out)) { +void rfft([[maybe_unused]] const BoutReal* in, [[maybe_unused]] int length, + [[maybe_unused]] dcomplex* out) { #if !BOUT_HAS_FFTW throw BoutException("This instance of BOUT++ has been compiled without fftw support."); #else @@ -170,8 +170,8 @@ void rfft(MAYBE_UNUSED(const BoutReal* in), MAYBE_UNUSED(int length), #endif } -void irfft(MAYBE_UNUSED(const dcomplex* in), MAYBE_UNUSED(int length), - MAYBE_UNUSED(BoutReal* out)) { +void irfft([[maybe_unused]] const dcomplex* in, [[maybe_unused]] int length, + [[maybe_unused]] BoutReal* out) { #if !BOUT_HAS_FFTW throw BoutException("This instance of BOUT++ has been compiled without fftw support."); #else @@ -233,8 +233,8 @@ void irfft(MAYBE_UNUSED(const dcomplex* in), MAYBE_UNUSED(int length), } #else // Parallel thread-safe version of rfft and irfft -void rfft(MAYBE_UNUSED(const BoutReal* in), MAYBE_UNUSED(int length), - MAYBE_UNUSED(dcomplex* out)) { +void rfft([[maybe_unused]] const BoutReal* in, [[maybe_unused]] int length, + [[maybe_unused]] dcomplex* out) { #if !BOUT_HAS_FFTW throw BoutException("This instance of BOUT++ has been compiled without fftw support."); #else @@ -312,8 +312,8 @@ void rfft(MAYBE_UNUSED(const BoutReal* in), MAYBE_UNUSED(int length), #endif } -void irfft(MAYBE_UNUSED(const dcomplex* in), MAYBE_UNUSED(int length), - MAYBE_UNUSED(BoutReal* out)) { +void irfft([[maybe_unused]] const dcomplex* in, [[maybe_unused]] int length, + [[maybe_unused]] BoutReal* out) { #if !BOUT_HAS_FFTW throw BoutException("This instance of BOUT++ has been compiled without fftw support."); #else @@ -388,8 +388,8 @@ void irfft(MAYBE_UNUSED(const dcomplex* in), MAYBE_UNUSED(int length), #endif // Discrete sine transforms (B Shanahan) -void DST(MAYBE_UNUSED(const BoutReal* in), MAYBE_UNUSED(int length), - MAYBE_UNUSED(dcomplex* out)) { +void DST([[maybe_unused]] const BoutReal* in, [[maybe_unused]] int length, + [[maybe_unused]] dcomplex* out) { #if !BOUT_HAS_FFTW throw BoutException("This instance of BOUT++ has been compiled without fftw support."); #else @@ -446,8 +446,8 @@ void DST(MAYBE_UNUSED(const BoutReal* in), MAYBE_UNUSED(int length), #endif } -void DST_rev(MAYBE_UNUSED(dcomplex* in), MAYBE_UNUSED(int length), - MAYBE_UNUSED(BoutReal* out)) { +void DST_rev([[maybe_unused]] dcomplex* in, [[maybe_unused]] int length, + [[maybe_unused]] BoutReal* out) { #if !BOUT_HAS_FFTW throw BoutException("This instance of BOUT++ has been compiled without fftw support."); #else diff --git a/src/invert/laplace/impls/multigrid/multigrid_alg.cxx b/src/invert/laplace/impls/multigrid/multigrid_alg.cxx index 0043c95d18..88556e02ad 100644 --- a/src/invert/laplace/impls/multigrid/multigrid_alg.cxx +++ b/src/invert/laplace/impls/multigrid/multigrid_alg.cxx @@ -704,7 +704,7 @@ void MultigridAlg::communications(BoutReal* x, int level) { MPI_Status status[4]; int stag, rtag; - MAYBE_UNUSED(int ierr); + [[maybe_unused]] int ierr; if (zNP > 1) { MPI_Request requests[] = {MPI_REQUEST_NULL, MPI_REQUEST_NULL, MPI_REQUEST_NULL, diff --git a/src/invert/laplace/invert_laplace.cxx b/src/invert/laplace/invert_laplace.cxx index dc3d1a3c3f..505b04cc4f 100644 --- a/src/invert/laplace/invert_laplace.cxx +++ b/src/invert/laplace/invert_laplace.cxx @@ -782,9 +782,10 @@ void Laplacian::savePerformance(Solver& solver, const std::string& name) { solver.addMonitor(&monitor, Solver::BACK); } -int Laplacian::LaplacianMonitor::call(MAYBE_UNUSED(Solver* solver), - MAYBE_UNUSED(BoutReal time), MAYBE_UNUSED(int iter), - MAYBE_UNUSED(int nout)) { +int Laplacian::LaplacianMonitor::call([[maybe_unused]] Solver* solver, + [[maybe_unused]] BoutReal time, + [[maybe_unused]] int iter, + [[maybe_unused]] int nout) { // Nothing to do, values are always calculated return 0; } @@ -805,7 +806,3 @@ void laplace_tridag_coefs(int jx, int jy, int jz, dcomplex& a, dcomplex& b, dcom const Field2D* ccoef, const Field2D* d, CELL_LOC loc) { Laplacian::defaultInstance()->tridagCoefs(jx, jy, jz, a, b, c, ccoef, d, loc); } -constexpr decltype(LaplaceFactory::type_name) LaplaceFactory::type_name; -constexpr decltype(LaplaceFactory::section_name) LaplaceFactory::section_name; -constexpr decltype(LaplaceFactory::option_name) LaplaceFactory::option_name; -constexpr decltype(LaplaceFactory::default_type) LaplaceFactory::default_type; diff --git a/src/invert/laplacexz/laplacexz.cxx b/src/invert/laplacexz/laplacexz.cxx index a2b99280e6..d064f62104 100644 --- a/src/invert/laplacexz/laplacexz.cxx +++ b/src/invert/laplacexz/laplacexz.cxx @@ -5,8 +5,3 @@ // DO NOT REMOVE: ensures linker keeps all symbols in this TU void LaplaceXZFactory::ensureRegistered() {} - -constexpr decltype(LaplaceXZFactory::type_name) LaplaceXZFactory::type_name; -constexpr decltype(LaplaceXZFactory::section_name) LaplaceXZFactory::section_name; -constexpr decltype(LaplaceXZFactory::option_name) LaplaceXZFactory::option_name; -constexpr decltype(LaplaceXZFactory::default_type) LaplaceXZFactory::default_type; diff --git a/src/invert/parderiv/invert_parderiv.cxx b/src/invert/parderiv/invert_parderiv.cxx index bc8ad8669f..e14c0ee1d2 100644 --- a/src/invert/parderiv/invert_parderiv.cxx +++ b/src/invert/parderiv/invert_parderiv.cxx @@ -39,7 +39,3 @@ const Field2D InvertPar::solve(const Field2D& f) { // DO NOT REMOVE: ensures linker keeps all symbols in this TU void InvertParFactory::ensureRegistered() {} -constexpr decltype(InvertParFactory::type_name) InvertParFactory::type_name; -constexpr decltype(InvertParFactory::section_name) InvertParFactory::section_name; -constexpr decltype(InvertParFactory::option_name) InvertParFactory::option_name; -constexpr decltype(InvertParFactory::default_type) InvertParFactory::default_type; diff --git a/src/invert/pardiv/invert_pardiv.cxx b/src/invert/pardiv/invert_pardiv.cxx index d05b594aa1..30e421edd8 100644 --- a/src/invert/pardiv/invert_pardiv.cxx +++ b/src/invert/pardiv/invert_pardiv.cxx @@ -39,7 +39,3 @@ Field2D InvertParDiv::solve(const Field2D& f) { // DO NOT REMOVE: ensures linker keeps all symbols in this TU void InvertParDivFactory::ensureRegistered() {} -constexpr decltype(InvertParDivFactory::type_name) InvertParDivFactory::type_name; -constexpr decltype(InvertParDivFactory::section_name) InvertParDivFactory::section_name; -constexpr decltype(InvertParDivFactory::option_name) InvertParDivFactory::option_name; -constexpr decltype(InvertParDivFactory::default_type) InvertParDivFactory::default_type; diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 25d623aad8..5ec0bb79e1 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1528,7 +1528,7 @@ Field3D Coordinates::DDZ(const Field3D& f, CELL_LOC outloc, const std::string& m // Parallel gradient Coordinates::FieldMetric Coordinates::Grad_par(const Field2D& var, - MAYBE_UNUSED(CELL_LOC outloc), + [[maybe_unused]] CELL_LOC outloc, const std::string& UNUSED(method)) { TRACE("Coordinates::Grad_par( Field2D )"); ASSERT1(location == outloc @@ -1550,7 +1550,7 @@ Field3D Coordinates::Grad_par(const Field3D& var, CELL_LOC outloc, // vparallel times the parallel derivative along unperturbed B-field Coordinates::FieldMetric Coordinates::Vpar_Grad_par(const Field2D& v, const Field2D& f, - MAYBE_UNUSED(CELL_LOC outloc), + [[maybe_unused]] CELL_LOC outloc, const std::string& UNUSED(method)) { ASSERT1(location == outloc || (outloc == CELL_DEFAULT && location == f.getLocation())); @@ -1829,8 +1829,8 @@ Field3D Coordinates::Laplace(const Field3D& f, CELL_LOC outloc, // Full perpendicular Laplacian, in form of inverse of Laplacian operator in LaplaceXY // solver -Field2D Coordinates::Laplace_perpXY(MAYBE_UNUSED(const Field2D& A), - MAYBE_UNUSED(const Field2D& f)) { +Field2D Coordinates::Laplace_perpXY([[maybe_unused]] const Field2D& A, + [[maybe_unused]] const Field2D& f) { TRACE("Coordinates::Laplace_perpXY( Field2D )"); #if not(BOUT_USE_METRIC_3D) Field2D result; diff --git a/src/mesh/data/gridfromfile.cxx b/src/mesh/data/gridfromfile.cxx index 7e875bd109..41147ba76c 100644 --- a/src/mesh/data/gridfromfile.cxx +++ b/src/mesh/data/gridfromfile.cxx @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include #include @@ -14,7 +14,7 @@ #include GridFile::GridFile(std::string gridfilename) - : GridDataSource(true), data(bout::OptionsNetCDF(gridfilename).read()), + : GridDataSource(true), data(bout::OptionsIO::create(gridfilename)->read()), filename(std::move(gridfilename)) { TRACE("GridFile constructor"); @@ -134,10 +134,10 @@ bool GridFile::get(Mesh* m, Field3D& var, const std::string& name, BoutReal def, namespace { /// Visitor that returns the shape of its argument struct GetDimensions { - std::vector operator()(MAYBE_UNUSED(bool value)) { return {1}; } - std::vector operator()(MAYBE_UNUSED(int value)) { return {1}; } - std::vector operator()(MAYBE_UNUSED(BoutReal value)) { return {1}; } - std::vector operator()(MAYBE_UNUSED(const std::string& value)) { return {1}; } + std::vector operator()([[maybe_unused]] bool value) { return {1}; } + std::vector operator()([[maybe_unused]] int value) { return {1}; } + std::vector operator()([[maybe_unused]] BoutReal value) { return {1}; } + std::vector operator()([[maybe_unused]] const std::string& value) { return {1}; } std::vector operator()(const Array& array) { return {array.size()}; } std::vector operator()(const Matrix& array) { const auto shape = array.shape(); @@ -471,10 +471,10 @@ void GridFile::readField(Mesh* m, const std::string& name, int UNUSED(ys), int U } } -bool GridFile::get(MAYBE_UNUSED(Mesh* m), MAYBE_UNUSED(std::vector& var), - MAYBE_UNUSED(const std::string& name), MAYBE_UNUSED(int len), - MAYBE_UNUSED(int offset), - MAYBE_UNUSED(GridDataSource::Direction dir)) { +bool GridFile::get([[maybe_unused]] Mesh* m, [[maybe_unused]] std::vector& var, + [[maybe_unused]] const std::string& name, [[maybe_unused]] int len, + [[maybe_unused]] int offset, + [[maybe_unused]] GridDataSource::Direction dir) { TRACE("GridFile::get(vector)"); return false; diff --git a/src/mesh/difops.cxx b/src/mesh/difops.cxx index 9b7665ad30..f252abe0ea 100644 --- a/src/mesh/difops.cxx +++ b/src/mesh/difops.cxx @@ -645,7 +645,7 @@ Field3D bracket(const Field3D& f, const Field2D& g, BRACKET_METHOD method, } ASSERT1(outloc == g.getLocation()); - MAYBE_UNUSED(Mesh * mesh) = f.getMesh(); + [[maybe_unused]] Mesh* mesh = f.getMesh(); Field3D result{emptyFrom(f).setLocation(outloc)}; @@ -862,7 +862,7 @@ Field3D bracket(const Field2D& f, const Field3D& g, BRACKET_METHOD method, } Field3D bracket(const Field3D& f, const Field3D& g, BRACKET_METHOD method, - CELL_LOC outloc, MAYBE_UNUSED(Solver* solver)) { + CELL_LOC outloc, [[maybe_unused]] Solver* solver) { TRACE("Field3D, Field3D"); ASSERT1_FIELDS_COMPATIBLE(f, g); diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index a802d3f5b3..956aba0f79 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -333,7 +333,9 @@ void BoutMesh::setDerivedGridSizes() { } GlobalNx = nx; - GlobalNy = ny + 2 * MYG; + GlobalNy = + ny + + 2 * MYG; // Note: For double null this should be be 4 * MYG if boundary cells are stored GlobalNz = nz; // If we've got a second pair of diverator legs, we need an extra @@ -374,6 +376,7 @@ void BoutMesh::setDerivedGridSizes() { } // Set global offsets + // Note: These don't properly include guard/boundary cells OffsetX = PE_XIND * MXSUB; OffsetY = PE_YIND * MYSUB; OffsetZ = 0; @@ -392,6 +395,48 @@ void BoutMesh::setDerivedGridSizes() { zstart = MZG; zend = MZG + MZSUB - 1; + + // Mapping local to global indices + if (periodicX) { + // No boundary cells in X + MapGlobalX = PE_XIND * MXSUB; + MapLocalX = MXG; + MapCountX = MXSUB; + } else { + // X boundaries stored for firstX and lastX processors + if (firstX()) { + MapGlobalX = 0; + MapLocalX = 0; + MapCountX = MXG + MXSUB; + } else { + MapGlobalX = MXG + PE_XIND * MXSUB; + MapLocalX = MXG; // Guard cells not included + MapCountX = MXSUB; + } + if (lastX()) { + // Doesn't change the origin, but adds outer X boundary cells + MapCountX += MXG; + } + } + + if (PE_YIND == 0) { + // Include Y boundary cells + MapGlobalY = 0; + MapLocalY = 0; + MapCountY = MYG + MYSUB; + } else { + MapGlobalY = MYG + PE_YIND * MYSUB; + MapLocalY = MYG; + MapCountY = MYSUB; + } + if (PE_YIND == NYPE - 1) { + // Include Y upper boundary region. + MapCountY += MYG; + } + + MapGlobalZ = 0; + MapLocalZ = MZG; // Omit boundary cells + MapCountZ = MZSUB; } int BoutMesh::load() { diff --git a/src/mesh/index_derivs.cxx b/src/mesh/index_derivs.cxx index 769315ca68..d84f5ced37 100644 --- a/src/mesh/index_derivs.cxx +++ b/src/mesh/index_derivs.cxx @@ -59,7 +59,7 @@ STAGGER Mesh::getStagger(const CELL_LOC inloc, const CELL_LOC outloc, } } -STAGGER Mesh::getStagger(const CELL_LOC vloc, MAYBE_UNUSED(const CELL_LOC inloc), +STAGGER Mesh::getStagger(const CELL_LOC vloc, [[maybe_unused]] const CELL_LOC inloc, const CELL_LOC outloc, const CELL_LOC allowedStaggerLoc) const { TRACE("Mesh::getStagger -- four arguments"); ASSERT1(inloc == outloc); @@ -485,7 +485,6 @@ class FFTDerivativeType { } static constexpr metaData meta{"FFT", 0, DERIV::Standard}; }; -constexpr metaData FFTDerivativeType::meta; class FFT2ndDerivativeType { public: @@ -544,7 +543,6 @@ class FFT2ndDerivativeType { } static constexpr metaData meta{"FFT", 0, DERIV::StandardSecond}; }; -constexpr metaData FFT2ndDerivativeType::meta; produceCombinations, Set, Set>, @@ -574,7 +572,6 @@ class SplitFluxDerivativeType { } static constexpr metaData meta{"SPLIT", 2, DERIV::Flux}; }; -constexpr metaData SplitFluxDerivativeType::meta; produceCombinations< Set 1.0)) { @@ -198,7 +198,7 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z } i_corner[i] = SpecificInd( - (((i_corn * ny) + (y + y_offset)) * nz + k_corner(x, y, z)), ny, nz); + (((i_corn * ny) + (y + y_offset)) * nz + k_corner(x, y, z)), ny, nz); h00_x[i] = (2. * t_x * t_x * t_x) - (3. * t_x * t_x) + 1.; h00_z[i] = (2. * t_z * t_z * t_z) - (3. * t_z * t_z) + 1.; @@ -324,11 +324,11 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z */ std::vector XZHermiteSpline::getWeightsForYApproximation(int i, int j, int k, int yoffset) { - const int ncz = localmesh->LocalNz; + const int nz = localmesh->LocalNz; const int k_mod = k_corner(i, j, k); - const int k_mod_m1 = (k_mod > 0) ? (k_mod - 1) : (ncz - 1); - const int k_mod_p1 = (k_mod == ncz) ? 0 : k_mod + 1; - const int k_mod_p2 = (k_mod_p1 == ncz) ? 0 : k_mod_p1 + 1; + const int k_mod_m1 = (k_mod > 0) ? (k_mod - 1) : (nz - 1); + const int k_mod_p1 = (k_mod == nz) ? 0 : k_mod + 1; + const int k_mod_p2 = (k_mod_p1 == nz) ? 0 : k_mod_p1 + 1; return {{i, j + yoffset, k_mod_m1, -0.5 * h10_z(i, j, k)}, {i, j + yoffset, k_mod, h00_z(i, j, k) - 0.5 * h11_z(i, j, k)}, diff --git a/src/mesh/interpolation/hermite_spline_z.cxx b/src/mesh/interpolation/hermite_spline_z.cxx index 921094af73..c4c44cb7b4 100644 --- a/src/mesh/interpolation/hermite_spline_z.cxx +++ b/src/mesh/interpolation/hermite_spline_z.cxx @@ -192,10 +192,3 @@ void ZInterpolationFactory::ensureRegistered() {} namespace { RegisterZInterpolation registerzinterphermitespline{"hermitespline"}; } // namespace - -constexpr decltype(ZInterpolationFactory::type_name) ZInterpolationFactory::type_name; -constexpr decltype(ZInterpolationFactory::section_name) - ZInterpolationFactory::section_name; -constexpr decltype(ZInterpolationFactory::option_name) ZInterpolationFactory::option_name; -constexpr decltype(ZInterpolationFactory::default_type) - ZInterpolationFactory::default_type; diff --git a/src/mesh/interpolation_xz.cxx b/src/mesh/interpolation_xz.cxx index 11dbdc215d..f7f0b457f2 100644 --- a/src/mesh/interpolation_xz.cxx +++ b/src/mesh/interpolation_xz.cxx @@ -93,11 +93,3 @@ RegisterXZInterpolation registerinterpmonotonichermite RegisterXZInterpolation registerinterplagrange4pt{"lagrange4pt"}; RegisterXZInterpolation registerinterpbilinear{"bilinear"}; } // namespace - -constexpr decltype(XZInterpolationFactory::type_name) XZInterpolationFactory::type_name; -constexpr decltype(XZInterpolationFactory::section_name) - XZInterpolationFactory::section_name; -constexpr decltype(XZInterpolationFactory::option_name) - XZInterpolationFactory::option_name; -constexpr decltype(XZInterpolationFactory::default_type) - XZInterpolationFactory::default_type; diff --git a/src/mesh/mesh.cxx b/src/mesh/mesh.cxx index e754b4fe43..0f6315a987 100644 --- a/src/mesh/mesh.cxx +++ b/src/mesh/mesh.cxx @@ -555,6 +555,14 @@ Mesh::createDefaultCoordinates(const CELL_LOC location, } const Region<>& Mesh::getRegion3D(const std::string& region_name) const { + const auto found = regionMap3D.find(region_name); + if (found == end(regionMap3D)) { + throw BoutException(_("Couldn't find region {:s} in regionMap3D"), region_name); + } + return region3D[found->second]; +} + +size_t Mesh::getRegionID(const std::string& region_name) const { const auto found = regionMap3D.find(region_name); if (found == end(regionMap3D)) { throw BoutException(_("Couldn't find region {:s} in regionMap3D"), region_name); @@ -595,7 +603,21 @@ void Mesh::addRegion3D(const std::string& region_name, const Region<>& region) { throw BoutException(_("Trying to add an already existing region {:s} to regionMap3D"), region_name); } - regionMap3D[region_name] = region; + + std::optional id; + for (size_t i = 0; i < region3D.size(); ++i) { + if (region3D[i] == region) { + id = i; + break; + } + } + if (!id.has_value()) { + id = region3D.size(); + region3D.push_back(region); + } + + regionMap3D[region_name] = id.value(); + output_verbose.write(_("Registered region 3D {:s}"), region_name); output_verbose << "\n:\t" << region.getStats() << "\n"; } @@ -734,7 +756,89 @@ void Mesh::recalculateStaggeredCoordinates() { } } -constexpr decltype(MeshFactory::type_name) MeshFactory::type_name; -constexpr decltype(MeshFactory::section_name) MeshFactory::section_name; -constexpr decltype(MeshFactory::option_name) MeshFactory::option_name; -constexpr decltype(MeshFactory::default_type) MeshFactory::default_type; +std::optional Mesh::getCommonRegion(std::optional lhs, + std::optional rhs) { + if (!lhs.has_value()) { + return rhs; + } + if (!rhs.has_value()) { + return lhs; + } + if (lhs.value() == rhs.value()) { + return lhs; + } + const size_t low = std::min(lhs.value(), rhs.value()); + const size_t high = std::max(lhs.value(), rhs.value()); + + /* This function finds the ID of the region corresponding to the + intersection of two regions, and caches the result. The cache is a + vector, indexed by some function of the two input IDs. Because the + intersection of two regions doesn't depend on the order, and the + intersection of a region with itself is the identity operation, we can + order the IDs numerically and use a generalised triangle number: + $[n (n - 1) / 2] + m$ to construct the cache index. This diagram shows + the result for the first few numbers: + | 0 1 2 3 + ---------------- + 0 | + 1 | 0 + 2 | 1 2 + 3 | 3 4 5 + 4 | 6 7 8 9 + + These indices might be sparse, but presumably we don't expect to store + very many intersections so this shouldn't give much overhead. + + After calculating the cache index, we look it up in the cache (possibly + reallocating to ensure it's large enough). If the index is in the cache, + we can just return it as-is, otherwise we need to do a bit more work. + + First, we need to fully compute the intersection of the two regions. We + then check if this corresponds to an existing region. If so, we cache the + ID of that region and return it. Otherwise, we need to store this new + region in `region3D` -- the index in this vector is the ID we need to + cache and return here. + */ + const size_t pos = (high * (high - 1)) / 2 + low; + if (region3Dintersect.size() <= pos) { + BOUT_OMP(critical(mesh_intersection_realloc)) + // By default this function does not need the mutex, however, if we are + // going to allocate global memory, we need to use a mutex. + // Now that we have the mutex, we need to check again whether a + // different thread was faster and already allocated. + // BOUT_OMP(single) would work in most cases, but it would fail if the + // function is called in parallel with different arguments. While BOUT++ + // is not currently doing it, other openmp parallised projects might be + // calling BOUT++ in this way. +#if BOUT_USE_OPENMP + if (region3Dintersect.size() <= pos) +#endif + { + region3Dintersect.resize(pos + 1, std::nullopt); + } + } + if (region3Dintersect[pos].has_value()) { + return region3Dintersect[pos]; + } + { + BOUT_OMP(critical(mesh_intersection)) + // See comment above why we need to check again in case of OpenMP +#if BOUT_USE_OPENMP + if (!region3Dintersect[pos].has_value()) +#endif + { + auto common = intersection(region3D[low], region3D[high]); + for (size_t i = 0; i < region3D.size(); ++i) { + if (common == region3D[i]) { + region3Dintersect[pos] = i; + break; + } + } + if (!region3Dintersect[pos].has_value()) { + region3Dintersect[pos] = region3D.size(); + region3D.push_back(common); + } + } + } + return region3Dintersect[pos]; +} diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 6ac2e3533f..bc8f3a54db 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -212,20 +212,8 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, const BoutReal dR_dx = 0.5 * (R[i.xp()] - R[i.xm()]); const BoutReal dZ_dx = 0.5 * (Z[i.xp()] - Z[i.xm()]); - BoutReal dR_dz, dZ_dz; - // Handle the edge cases in Z - if (z == 0) { - dR_dz = R[i_zp] - R[i]; - dZ_dz = Z[i_zp] - Z[i]; - - } else if (z == map_mesh.LocalNz - 1) { - dR_dz = R[i] - R[i_zm]; - dZ_dz = Z[i] - Z[i_zm]; - - } else { - dR_dz = 0.5 * (R[i_zp] - R[i_zm]); - dZ_dz = 0.5 * (Z[i_zp] - Z[i_zm]); - } + const BoutReal dR_dz = 0.5 * (R[i_zp] - R[i_zm]); + const BoutReal dZ_dz = 0.5 * (Z[i_zp] - Z[i_zm]); const BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix @@ -238,7 +226,7 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, // Negative xt_prime means we've hit the inner boundary, otherwise // the outer boundary - auto* boundary = (xt_prime[i] < 0.0) ? inner_boundary : outer_boundary; + auto* boundary = (xt_prime[i] < map_mesh.xstart) ? inner_boundary : outer_boundary; boundary->add_point(x, y, z, x + dx, y + 0.5 * offset, z + dz, // Intersection point in local index space 0.5 * dy[i], // Distance to intersection @@ -247,6 +235,8 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& dy, Options& options, } region_no_boundary = region_no_boundary.mask(to_remove); + interp->setRegion(region_no_boundary); + const auto region = fmt::format("RGN_YPAR_{:+d}", offset); if (not map_mesh.hasRegion3D(region)) { // The valid region for this slice @@ -275,8 +265,6 @@ Field3D FCIMap::integrate(Field3D& f) const { result = BoutNaN; #endif - int nz = map_mesh.LocalNz; - BOUT_FOR(i, region_no_boundary) { const auto inext = i.yp(offset); BoutReal f_c = centre[inext]; @@ -337,6 +325,7 @@ void FCITransform::calcParallelSlices(Field3D& f) { // Interpolate f onto yup and ydown fields for (const auto& map : field_line_maps) { f.ynext(map.offset) = map.interpolate(f); + f.ynext(map.offset).setRegion(fmt::format("RGN_YPAR_{:+d}", map.offset)); } } diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index 6d7c3d14e2..a749c084cc 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -127,7 +127,7 @@ public: bool canToFromFieldAligned() const override { return false; } bool requiresTwistShift(bool UNUSED(twist_shift_enabled), - MAYBE_UNUSED(YDirectionType ytype)) override { + [[maybe_unused]] YDirectionType ytype) override { // No Field3Ds require twist-shift, because they cannot be field-aligned ASSERT1(ytype == YDirectionType::Standard); diff --git a/src/physics/physicsmodel.cxx b/src/physics/physicsmodel.cxx index cac4bda5cc..9f538895ed 100644 --- a/src/physics/physicsmodel.cxx +++ b/src/physics/physicsmodel.cxx @@ -58,35 +58,25 @@ void DataFileFacade::add(Vector3D* value, const std::string& name, bool save_rep add(value->y, name_prefix + "y"s, save_repeat); add(value->z, name_prefix + "z"s, save_repeat); } - -bool DataFileFacade::write() { - for (const auto& item : data) { - bout::utils::visit(bout::OptionsConversionVisitor{Options::root(), item.name}, - item.value); - if (item.repeat) { - Options::root()[item.name].attributes["time_dimension"] = "t"; - } - } - writeDefaultOutputFile(); - return true; -} } // namespace bout PhysicsModel::PhysicsModel() - : mesh(bout::globals::mesh), - output_file(bout::getOutputFilename(Options::root()), - Options::root()["append"] - .doc("Add output data to existing (dump) files?") - .withDefault(false) - ? bout::OptionsNetCDF::FileMode::append - : bout::OptionsNetCDF::FileMode::replace), - output_enabled(Options::root()["output"]["enabled"] - .doc("Write output files") - .withDefault(true)), - restart_file(bout::getRestartFilename(Options::root())), + : mesh(bout::globals::mesh), output_enabled(Options::root()["output"]["enabled"] + .doc("Write output files") + .withDefault(true)), restart_enabled(Options::root()["restart_files"]["enabled"] .doc("Write restart files") - .withDefault(true)) {} + .withDefault(true)) + +{ + if (output_enabled) { + output_file = bout::OptionsIOFactory::getInstance().createOutput(); + } + + if (restart_enabled) { + restart_file = bout::OptionsIOFactory::getInstance().createRestart(); + } +} void PhysicsModel::initialise(Solver* s) { if (initialised) { @@ -104,7 +94,7 @@ void PhysicsModel::initialise(Solver* s) { const bool restarting = Options::root()["restart"].withDefault(false); if (restarting) { - restart_options = restart_file.read(); + restart_options = restart_file->read(); } // Call user init code to specify evolving variables @@ -187,7 +177,7 @@ int PhysicsModel::postInit(bool restarting) { restart_options["BOUT_VERSION"].force(bout::version::as_double, "PhysicsModel"); // Write _everything_ to restart file - restart_file.write(restart_options); + restart_file->write(restart_options); } // Add monitor to the solver which calls restart.write() and @@ -219,7 +209,7 @@ void PhysicsModel::restartVars(Options& options) { void PhysicsModel::writeRestartFile() { if (restart_enabled) { - restart_file.write(restart_options); + restart_file->write(restart_options); } } @@ -227,20 +217,20 @@ void PhysicsModel::writeOutputFile() { writeOutputFile(output_options); } void PhysicsModel::writeOutputFile(const Options& options) { if (output_enabled) { - output_file.write(options, "t"); + output_file->write(options, "t"); } } void PhysicsModel::writeOutputFile(const Options& options, const std::string& time_dimension) { if (output_enabled) { - output_file.write(options, time_dimension); + output_file->write(options, time_dimension); } } void PhysicsModel::finishOutputTimestep() const { if (output_enabled) { - output_file.verifyTimesteps(); + output_file->verifyTimesteps(); } } diff --git a/src/solver/impls/arkode/arkode.cxx b/src/solver/impls/arkode/arkode.cxx index 13e0dd817e..9691f2f7da 100644 --- a/src/solver/impls/arkode/arkode.cxx +++ b/src/solver/impls/arkode/arkode.cxx @@ -161,7 +161,7 @@ constexpr auto& ARKStepSetUserData = ARKodeSetUserData; #if SUNDIALS_VERSION_MAJOR < 6 void* ARKStepCreate(ARKRhsFn fe, ARKRhsFn fi, BoutReal t0, N_Vector y0, - MAYBE_UNUSED(SUNContext context)) { + [[maybe_unused]] SUNContext context) { return ARKStepCreate(fe, fi, t0, y0); } #endif diff --git a/src/solver/impls/cvode/cvode.cxx b/src/solver/impls/cvode/cvode.cxx index 70eb3b8841..f35ae680d5 100644 --- a/src/solver/impls/cvode/cvode.cxx +++ b/src/solver/impls/cvode/cvode.cxx @@ -112,7 +112,8 @@ constexpr auto CV_NEWTON = 0; #endif #if SUNDIALS_VERSION_MAJOR >= 3 -void* CVodeCreate(int lmm, MAYBE_UNUSED(int iter), MAYBE_UNUSED(SUNContext context)) { +void* CVodeCreate(int lmm, [[maybe_unused]] int iter, + [[maybe_unused]] SUNContext context) { #if SUNDIALS_VERSION_MAJOR == 3 return CVodeCreate(lmm, iter); #elif SUNDIALS_VERSION_MAJOR == 4 || SUNDIALS_VERSION_MAJOR == 5 diff --git a/src/solver/impls/ida/ida.cxx b/src/solver/impls/ida/ida.cxx index b008ebf903..189a103bbe 100644 --- a/src/solver/impls/ida/ida.cxx +++ b/src/solver/impls/ida/ida.cxx @@ -85,7 +85,7 @@ constexpr auto& ida_pre_shim = ida_pre; #endif #if SUNDIALS_VERSION_MAJOR < 6 -void* IDACreate(MAYBE_UNUSED(SUNContext)) { return IDACreate(); } +void* IDACreate([[maybe_unused]] SUNContext) { return IDACreate(); } #endif IdaSolver::IdaSolver(Options* opts) diff --git a/src/solver/impls/imex-bdf2/imex-bdf2.cxx b/src/solver/impls/imex-bdf2/imex-bdf2.cxx index adafbb71c5..da62e9b8bb 100644 --- a/src/solver/impls/imex-bdf2/imex-bdf2.cxx +++ b/src/solver/impls/imex-bdf2/imex-bdf2.cxx @@ -18,9 +18,6 @@ #include "petscmat.h" #include "petscsnes.h" -// Redundent definition because < C++17 -constexpr int IMEXBDF2::MAX_SUPPORTED_ORDER; - IMEXBDF2::IMEXBDF2(Options* opt) : Solver(opt), maxOrder((*options)["maxOrder"] .doc("Maximum order of the scheme (1/2/3)") diff --git a/src/solver/impls/petsc/petsc.cxx b/src/solver/impls/petsc/petsc.cxx index 1b81ca36b6..7a2b2cf3de 100644 --- a/src/solver/impls/petsc/petsc.cxx +++ b/src/solver/impls/petsc/petsc.cxx @@ -29,7 +29,6 @@ #if BOUT_HAS_PETSC -//#include #include #include @@ -750,10 +749,10 @@ PetscErrorCode solver_rhsjacobian(TS UNUSED(ts), BoutReal UNUSED(t), Vec UNUSED( PetscFunctionReturn(0); } #else -PetscErrorCode solver_rhsjacobian(MAYBE_UNUSED(TS ts), MAYBE_UNUSED(BoutReal t), - MAYBE_UNUSED(Vec globalin), Mat* J, Mat* Jpre, - MAYBE_UNUSED(MatStructure* str), - MAYBE_UNUSED(void* f_data)) { +PetscErrorCode solver_rhsjacobian([[maybe_unused]] TS ts, [[maybe_unused]] BoutReal t, + [[maybe_unused]] Vec globalin, Mat* J, Mat* Jpre, + [[maybe_unused]] MatStructure* str, + [[maybe_unused]] void* f_data) { PetscErrorCode ierr; PetscFunctionBegin; @@ -798,8 +797,9 @@ PetscErrorCode solver_ijacobian(TS ts, BoutReal t, Vec globalin, Vec UNUSED(glob } #else PetscErrorCode solver_ijacobian(TS ts, BoutReal t, Vec globalin, - MAYBE_UNUSED(Vec globalindot), MAYBE_UNUSED(PetscReal a), - Mat* J, Mat* Jpre, MatStructure* str, void* f_data) { + [[maybe_unused]] Vec globalindot, + [[maybe_unused]] PetscReal a, Mat* J, Mat* Jpre, + MatStructure* str, void* f_data) { PetscErrorCode ierr; PetscFunctionBegin; diff --git a/src/solver/impls/rkgeneric/rkscheme.cxx b/src/solver/impls/rkgeneric/rkscheme.cxx index 740adec909..25de364533 100644 --- a/src/solver/impls/rkgeneric/rkscheme.cxx +++ b/src/solver/impls/rkgeneric/rkscheme.cxx @@ -308,8 +308,3 @@ void RKScheme::zeroSteps() { } } } - -constexpr decltype(RKSchemeFactory::type_name) RKSchemeFactory::type_name; -constexpr decltype(RKSchemeFactory::section_name) RKSchemeFactory::section_name; -constexpr decltype(RKSchemeFactory::option_name) RKSchemeFactory::option_name; -constexpr decltype(RKSchemeFactory::default_type) RKSchemeFactory::default_type; diff --git a/src/solver/impls/slepc/slepc.cxx b/src/solver/impls/slepc/slepc.cxx index f90afe717e..8cbb002d11 100644 --- a/src/solver/impls/slepc/slepc.cxx +++ b/src/solver/impls/slepc/slepc.cxx @@ -572,7 +572,7 @@ void SlepcSolver::monitor(PetscInt its, PetscInt nconv, PetscScalar eigr[], static int nConvPrev = 0; // Disable floating-point exceptions for the duration of this function - MAYBE_UNUSED(QuietFPE quiet_fpe{}); + [[maybe_unused]] QuietFPE quiet_fpe{}; // No output until after first iteration if (its < 1) { diff --git a/src/solver/solver.cxx b/src/solver/solver.cxx index 9dd31f011e..02e6ec6d04 100644 --- a/src/solver/solver.cxx +++ b/src/solver/solver.cxx @@ -1498,7 +1498,7 @@ void Solver::post_rhs(BoutReal UNUSED(t)) { } // Make sure 3D fields are at the correct cell location, etc. - for (MAYBE_UNUSED(const auto& f) : f3d) { + for ([[maybe_unused]] const auto& f : f3d) { ASSERT1_FIELDS_COMPATIBLE(*f.var, *f.F_var); } @@ -1571,8 +1571,3 @@ void Solver::calculate_mms_error(BoutReal t) { *(f.MMS_err) = *(f.var) - solution; } } - -constexpr decltype(SolverFactory::type_name) SolverFactory::type_name; -constexpr decltype(SolverFactory::section_name) SolverFactory::section_name; -constexpr decltype(SolverFactory::option_name) SolverFactory::option_name; -constexpr decltype(SolverFactory::default_type) SolverFactory::default_type; diff --git a/src/sys/adios_object.cxx b/src/sys/adios_object.cxx new file mode 100644 index 0000000000..c7d6dab9aa --- /dev/null +++ b/src/sys/adios_object.cxx @@ -0,0 +1,98 @@ +#include "bout/build_config.hxx" + +#if BOUT_HAS_ADIOS + +#include "bout/adios_object.hxx" +#include "bout/boutexception.hxx" + +#include +#include +#include + +namespace bout { + +static ADIOSPtr adios = nullptr; +static std::unordered_map adiosStreams; + +void ADIOSInit(MPI_Comm comm) { adios = std::make_shared(comm); } + +void ADIOSInit(const std::string configFile, MPI_Comm comm) { + adios = std::make_shared(configFile, comm); +} + +void ADIOSFinalize() { + if (adios == nullptr) { + throw BoutException( + "ADIOS needs to be initialized first before calling ADIOSFinalize()"); + } + adiosStreams.clear(); + adios.reset(); +} + +ADIOSPtr GetADIOSPtr() { + if (adios == nullptr) { + throw BoutException( + "ADIOS needs to be initialized first before calling GetADIOSPtr()"); + } + return adios; +} + +IOPtr GetIOPtr(const std::string IOName) { + auto adios = GetADIOSPtr(); + IOPtr io = nullptr; + try { + io = std::make_shared(adios->AtIO(IOName)); + } catch (std::invalid_argument& e) { + } + return io; +} + +ADIOSStream::~ADIOSStream() { + if (engine) { + if (isInStep) { + engine.EndStep(); + isInStep = false; + } + engine.Close(); + } +} + +ADIOSStream& ADIOSStream::ADIOSGetStream(const std::string& fname) { + auto it = adiosStreams.find(fname); + if (it == adiosStreams.end()) { + it = adiosStreams.emplace(fname, ADIOSStream(fname)).first; + } + return it->second; +} + +void ADIOSSetParameters(const std::string& input, const char delimKeyValue, + const char delimItem, adios2::IO& io) { + auto lf_Trim = [](std::string& input) { + input.erase(0, input.find_first_not_of(" \n\r\t")); // prefixing spaces + input.erase(input.find_last_not_of(" \n\r\t") + 1); // suffixing spaces + }; + + std::istringstream inputSS(input); + std::string parameter; + while (std::getline(inputSS, parameter, delimItem)) { + const size_t position = parameter.find(delimKeyValue); + if (position == parameter.npos) { + throw BoutException("ADIOSSetParameters(): wrong format for IO parameter " + + parameter + ", format must be key" + delimKeyValue + + "value for each entry"); + } + + std::string key = parameter.substr(0, position); + lf_Trim(key); + std::string value = parameter.substr(position + 1); + lf_Trim(value); + if (value.length() == 0) { + throw BoutException("ADIOS2SetParameters: empty value in IO parameter " + parameter + + ", format must be key" + delimKeyValue + "value"); + } + io.SetParameter(key, value); + } +} + +} // namespace bout +#endif //BOUT_HAS_ADIOS diff --git a/src/sys/derivs.cxx b/src/sys/derivs.cxx index 7f629cfbb5..ee9bcbcc2c 100644 --- a/src/sys/derivs.cxx +++ b/src/sys/derivs.cxx @@ -331,8 +331,8 @@ Field3D D2DXDY(const Field3D& f, CELL_LOC outloc, const std::string& method, } Coordinates::FieldMetric D2DXDZ(const Field2D& f, CELL_LOC outloc, - MAYBE_UNUSED(const std::string& method), - MAYBE_UNUSED(const std::string& region)) { + [[maybe_unused]] const std::string& method, + [[maybe_unused]] const std::string& region) { #if BOUT_USE_METRIC_3D Field3D tmp{f}; return D2DXDZ(tmp, outloc, method, region); @@ -356,8 +356,8 @@ Field3D D2DXDZ(const Field3D& f, CELL_LOC outloc, const std::string& method, } Coordinates::FieldMetric D2DYDZ(const Field2D& f, CELL_LOC outloc, - MAYBE_UNUSED(const std::string& method), - MAYBE_UNUSED(const std::string& region)) { + [[maybe_unused]] const std::string& method, + [[maybe_unused]] const std::string& region) { #if BOUT_USE_METRIC_3D Field3D tmp{f}; return D2DYDZ(tmp, outloc, method, region); @@ -369,8 +369,8 @@ Coordinates::FieldMetric D2DYDZ(const Field2D& f, CELL_LOC outloc, #endif } -Field3D D2DYDZ(const Field3D& f, CELL_LOC outloc, MAYBE_UNUSED(const std::string& method), - const std::string& region) { +Field3D D2DYDZ(const Field3D& f, CELL_LOC outloc, + [[maybe_unused]] const std::string& method, const std::string& region) { // If staggering in z, take y-derivative at f's location. const auto y_location = (outloc == CELL_ZLOW or f.getLocation() == CELL_ZLOW) ? CELL_DEFAULT : outloc; @@ -426,9 +426,9 @@ Coordinates::FieldMetric VDDZ(const Field2D& v, const Field2D& f, CELL_LOC outlo } // Note that this is zero because no compression is included -Coordinates::FieldMetric VDDZ(MAYBE_UNUSED(const Field3D& v), const Field2D& f, - CELL_LOC outloc, MAYBE_UNUSED(const std::string& method), - MAYBE_UNUSED(const std::string& region)) { +Coordinates::FieldMetric VDDZ([[maybe_unused]] const Field3D& v, const Field2D& f, + CELL_LOC outloc, [[maybe_unused]] const std::string& method, + [[maybe_unused]] const std::string& region) { #if BOUT_USE_METRIC_3D Field3D tmp{f}; return bout::derivatives::index::VDDZ(v, tmp, outloc, method, region) diff --git a/src/sys/expressionparser.cxx b/src/sys/expressionparser.cxx index 39f8d3bb71..8290a4cae0 100644 --- a/src/sys/expressionparser.cxx +++ b/src/sys/expressionparser.cxx @@ -184,10 +184,29 @@ FieldGeneratorPtr FieldBinary::clone(const list args) { return std::make_shared(args.front(), args.back(), op); } +/// Convert a real value to a Boolean +/// Throw exception if `rval` isn't close to 0 or 1 +bool toBool(BoutReal rval) { + int ival = ROUND(rval); + if ((fabs(rval - static_cast(ival)) > 1e-3) or (ival < 0) or (ival > 1)) { + throw BoutException(_("Boolean operator argument {:e} is not a bool"), rval); + } + return ival == 1; +} + BoutReal FieldBinary::generate(const Context& ctx) { BoutReal lval = lhs->generate(ctx); BoutReal rval = rhs->generate(ctx); + switch (op) { + case '|': // Logical OR + return (toBool(lval) or toBool(rval)) ? 1.0 : 0.0; + case '&': // Logical AND + return (toBool(lval) and toBool(rval)) ? 1.0 : 0.0; + case '>': // Comparison + return (lval > rval) ? 1.0 : 0.0; + case '<': + return (lval < rval) ? 1.0 : 0.0; case '+': return lval + rval; case '-': @@ -203,10 +222,30 @@ BoutReal FieldBinary::generate(const Context& ctx) { throw ParseException("Unknown binary operator '{:c}'", op); } +class LogicalNot : public FieldGenerator { +public: + /// Logically negate a boolean expression + LogicalNot(FieldGeneratorPtr expr) : expr(expr) {} + + /// Evaluate expression, check it's a bool, and return 1 or 0 + double generate(const Context& ctx) override { + return toBool(expr->generate(ctx)) ? 0.0 : 1.0; + } + + std::string str() const override { return "!"s + expr->str(); } + +private: + FieldGeneratorPtr expr; +}; + ///////////////////////////////////////////// ExpressionParser::ExpressionParser() { // Add standard binary operations + addBinaryOp('|', std::make_shared(nullptr, nullptr, '|'), 3); + addBinaryOp('&', std::make_shared(nullptr, nullptr, '&'), 5); + addBinaryOp('<', std::make_shared(nullptr, nullptr, '<'), 7); + addBinaryOp('>', std::make_shared(nullptr, nullptr, '>'), 7); addBinaryOp('+', std::make_shared(nullptr, nullptr, '+'), 10); addBinaryOp('-', std::make_shared(nullptr, nullptr, '-'), 10); addBinaryOp('*', std::make_shared(nullptr, nullptr, '*'), 20); @@ -482,6 +521,11 @@ FieldGeneratorPtr ExpressionParser::parsePrimary(LexInfo& lex) const { // Don't eat the minus, and return an implicit zero return std::make_shared(0.0); } + case '!': { + // Logical not + lex.nextToken(); // Eat '!' + return std::make_shared(parsePrimary(lex)); + } case '(': { return parseParenExpr(lex); } diff --git a/src/sys/hyprelib.cxx b/src/sys/hyprelib.cxx index e833162726..691e53230f 100644 --- a/src/sys/hyprelib.cxx +++ b/src/sys/hyprelib.cxx @@ -39,7 +39,7 @@ HypreLib::HypreLib() { } } -HypreLib::HypreLib(MAYBE_UNUSED() const HypreLib& other) noexcept { +HypreLib::HypreLib([[maybe_unused]] const HypreLib& other) noexcept { BOUT_OMP(critical(HypreLib)) { // No need to initialise Hypre, because it must already be initialised @@ -47,7 +47,7 @@ HypreLib::HypreLib(MAYBE_UNUSED() const HypreLib& other) noexcept { } } -HypreLib::HypreLib(MAYBE_UNUSED() HypreLib&& other) noexcept { +HypreLib::HypreLib([[maybe_unused]] HypreLib&& other) noexcept { BOUT_OMP(critical(HypreLib)) { // No need to initialise Hypre, because it must already be initialised diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 8b49b1f3f1..14d9e47d91 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -1,14 +1,34 @@ -#include -#include // Used for parsing expressions -#include -#include -#include - +#include "bout/options.hxx" + +#include "bout/array.hxx" +#include "bout/bout_types.hxx" +#include "bout/boutexception.hxx" +#include "bout/field2d.hxx" +#include "bout/field3d.hxx" +#include "bout/field_factory.hxx" // Used for parsing expressions +#include "bout/fieldperp.hxx" +#include "bout/output.hxx" +#include "bout/sys/expressionparser.hxx" +#include "bout/sys/gettext.hxx" +#include "bout/sys/type_name.hxx" +#include "bout/sys/variant.hxx" +#include "bout/traits.hxx" +#include "bout/unused.hxx" +#include "bout/utils.hxx" + +#include #include #include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /// The source label given to default values const std::string Options::DEFAULT_SOURCE{_("default")}; @@ -19,23 +39,12 @@ std::string Options::getDefaultSource() { return DEFAULT_SOURCE; } /// having been used constexpr auto conditionally_used_attribute = "conditionally used"; -Options* Options::root_instance{nullptr}; - Options& Options::root() { - if (root_instance == nullptr) { - // Create the singleton - root_instance = new Options(); - } - return *root_instance; + static Options root_instance; + return root_instance; } -void Options::cleanup() { - if (root_instance == nullptr) { - return; - } - delete root_instance; - root_instance = nullptr; -} +void Options::cleanup() { root() = Options{}; } Options::Options(const Options& other) : value(other.value), attributes(other.attributes), @@ -50,6 +59,19 @@ Options::Options(const Options& other) } } +Options::Options(Options&& other) noexcept + : value(std::move(other.value)), attributes(std::move(other.attributes)), + parent_instance(other.parent_instance), full_name(std::move(other.full_name)), + is_section(other.is_section), children(std::move(other.children)), + value_used(other.value_used) { + + // Ensure that this is the parent of all children, + // otherwise will point to the original Options instance + for (auto& child : children) { + child.second.parent_instance = this; + } +} + template <> Options::Options(const char* value) { assign(value); @@ -80,7 +102,7 @@ Options::Options(std::initializer_list> values) append_impl(children, section_name, append_impl); }; - for (auto& value : values) { + for (const auto& value : values) { (*this)[value.first] = value.second; // value.second was constructed from the "bare" `Options(T)` so // doesn't have `full_name` set. This clobbers @@ -107,15 +129,15 @@ Options& Options::operator[](const std::string& name) { } // If name is compound, e.g. "section:subsection", then split the name - auto subsection_split = name.find(":"); + auto subsection_split = name.find(':'); if (subsection_split != std::string::npos) { return (*this)[name.substr(0, subsection_split)][name.substr(subsection_split + 1)]; } // Find and return if already exists - auto it = children.find(name); - if (it != children.end()) { - return it->second; + auto child = children.find(name); + if (child != children.end()) { + return child->second; } // Doesn't exist yet, so add @@ -146,19 +168,19 @@ const Options& Options::operator[](const std::string& name) const { } // If name is compound, e.g. "section:subsection", then split the name - auto subsection_split = name.find(":"); + auto subsection_split = name.find(':'); if (subsection_split != std::string::npos) { return (*this)[name.substr(0, subsection_split)][name.substr(subsection_split + 1)]; } // Find and return if already exists - auto it = children.find(name); - if (it == children.end()) { + auto child = children.find(name); + if (child == children.end()) { // Doesn't exist throw BoutException(_("Option {:s}:{:s} does not exist"), full_name, name); } - return it->second; + return child->second; } std::multiset @@ -209,6 +231,10 @@ Options::fuzzyFind(const std::string& name, std::string::size_type distance) con } Options& Options::operator=(const Options& other) { + if (this == &other) { + return *this; + } + // Note: Here can't do copy-and-swap because pointers to parents are stored value = other.value; @@ -232,6 +258,28 @@ Options& Options::operator=(const Options& other) { return *this; } +Options& Options::operator=(Options&& other) noexcept { + if (this == &other) { + return *this; + } + + // Note: Here can't do copy-and-swap because pointers to parents are stored + + value = std::move(other.value); + attributes = std::move(other.attributes); + full_name = std::move(other.full_name); + is_section = other.is_section; + children = std::move(other.children); + value_used = other.value_used; + + // Ensure that this is the parent of all children, + // otherwise will point to the original Options instance + for (auto& child : children) { + child.second.parent_instance = this; + } + return *this; +} + bool Options::isSet() const { // Only values can be set/unset if (is_section) { @@ -247,18 +295,17 @@ bool Options::isSet() const { } bool Options::isSection(const std::string& name) const { - if (name == "") { + if (name.empty()) { // Test this object return is_section; } // Is there a child section? - auto it = children.find(name); - if (it == children.end()) { + const auto child = children.find(name); + if (child == children.end()) { return false; - } else { - return it->second.isSection(); } + return child->second.isSection(); } template <> @@ -296,27 +343,6 @@ void Options::assign<>(Tensor val, std::string source) { _set_no_check(std::move(val), std::move(source)); } -template <> -std::string Options::as(const std::string& UNUSED(similar_to)) const { - if (is_section) { - throw BoutException(_("Option {:s} has no value"), full_name); - } - - // Mark this option as used - value_used = true; - - std::string result = bout::utils::variantToString(value); - - output_info << _("\tOption ") << full_name << " = " << result; - if (attributes.count("source")) { - // Specify the source of the setting - output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; - } - output_info << endl; - - return result; -} - namespace { /// Use FieldFactory to evaluate expression double parseExpression(const Options::ValueType& value, const Options* options, @@ -336,22 +362,50 @@ double parseExpression(const Options::ValueType& value, const Options* options, full_name, bout::utils::variantToString(value), error.what()); } } + +/// Helper function to print `key = value` with optional source +template +void printNameValueSourceLine(const Options& option, const T& value) { + output_info.write(_("\tOption {} = {}"), option.str(), value); + if (option.hasAttribute("source")) { + // Specify the source of the setting + output_info.write(" ({})", + bout::utils::variantToString(option.attributes.at("source"))); + } + output_info.write("\n"); +} } // namespace +template <> +std::string Options::as(const std::string& UNUSED(similar_to)) const { + if (is_section) { + throw BoutException(_("Option {:s} has no value"), full_name); + } + + // Mark this option as used + value_used = true; + + std::string result = bout::utils::variantToString(value); + + printNameValueSourceLine(*this, result); + + return result; +} + template <> int Options::as(const int& UNUSED(similar_to)) const { if (is_section) { throw BoutException(_("Option {:s} has no value"), full_name); } - int result; + int result = 0; if (bout::utils::holds_alternative(value)) { result = bout::utils::get(value); } else { // Cases which get a BoutReal then check if close to an integer - BoutReal rval; + BoutReal rval = BoutNaN; if (bout::utils::holds_alternative(value)) { rval = bout::utils::get(value); @@ -376,12 +430,7 @@ int Options::as(const int& UNUSED(similar_to)) const { value_used = true; - output_info << _("\tOption ") << full_name << " = " << result; - if (attributes.count("source")) { - // Specify the source of the setting - output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; - } - output_info << endl; + printNameValueSourceLine(*this, result); return result; } @@ -392,7 +441,7 @@ BoutReal Options::as(const BoutReal& UNUSED(similar_to)) const { throw BoutException(_("Option {:s} has no value"), full_name); } - BoutReal result; + BoutReal result = BoutNaN; if (bout::utils::holds_alternative(value)) { result = static_cast(bout::utils::get(value)); @@ -411,12 +460,7 @@ BoutReal Options::as(const BoutReal& UNUSED(similar_to)) const { // Mark this option as used value_used = true; - output_info << _("\tOption ") << full_name << " = " << result; - if (attributes.count("source")) { - // Specify the source of the setting - output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; - } - output_info << endl; + printNameValueSourceLine(*this, result); return result; } @@ -427,25 +471,22 @@ bool Options::as(const bool& UNUSED(similar_to)) const { throw BoutException(_("Option {:s} has no value"), full_name); } - bool result; + bool result = false; if (bout::utils::holds_alternative(value)) { result = bout::utils::get(value); } else if (bout::utils::holds_alternative(value)) { - // case-insensitve check, so convert string to lower case - const auto strvalue = lowercase(bout::utils::get(value)); - - if ((strvalue == "y") or (strvalue == "yes") or (strvalue == "t") - or (strvalue == "true") or (strvalue == "1")) { - result = true; - } else if ((strvalue == "n") or (strvalue == "no") or (strvalue == "f") - or (strvalue == "false") or (strvalue == "0")) { - result = false; - } else { - throw BoutException(_("\tOption '{:s}': Boolean expected. Got '{:s}'\n"), full_name, - strvalue); + // Parse as floating point because that's the only type the parser understands + const BoutReal rval = parseExpression(value, this, "bool", full_name); + + // Check that the result is either close to 1 (true) or close to 0 (false) + const int ival = ROUND(rval); + if ((fabs(rval - static_cast(ival)) > 1e-3) or (ival < 0) or (ival > 1)) { + throw BoutException(_("Value for option {:s} = {:e} is not a bool"), full_name, + rval); } + result = ival == 1; } else { throw BoutException(_("Value for option {:s} cannot be converted to a bool"), full_name); @@ -453,13 +494,7 @@ bool Options::as(const bool& UNUSED(similar_to)) const { value_used = true; - output_info << _("\tOption ") << full_name << " = " << toString(result); - - if (attributes.count("source")) { - // Specify the source of the setting - output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; - } - output_info << endl; + printNameValueSourceLine(*this, toString(result)); return result; } @@ -493,7 +528,7 @@ Field3D Options::as(const Field3D& similar_to) const { if (bout::utils::holds_alternative(value) or bout::utils::holds_alternative(value)) { - BoutReal scalar_value = + const BoutReal scalar_value = bout::utils::variantStaticCastOrThrow(value); // Get metadata from similar_to, fill field with scalar_value @@ -549,7 +584,7 @@ Field2D Options::as(const Field2D& similar_to) const { if (bout::utils::holds_alternative(value) or bout::utils::holds_alternative(value)) { - BoutReal scalar_value = + const BoutReal scalar_value = bout::utils::variantStaticCastOrThrow(value); // Get metadata from similar_to, fill field with scalar_value @@ -601,7 +636,7 @@ FieldPerp Options::as(const FieldPerp& similar_to) const { if (bout::utils::holds_alternative(value) or bout::utils::holds_alternative(value)) { - BoutReal scalar_value = + const BoutReal scalar_value = bout::utils::variantStaticCastOrThrow(value); // Get metadata from similar_to, fill field with scalar_value @@ -686,7 +721,7 @@ struct ConvertContainer { Container operator()(const Container& value) { return value; } template - Container operator()(MAYBE_UNUSED(const Other& value)) { + Container operator()([[maybe_unused]] const Other& value) { throw BoutException(error_message); } @@ -713,12 +748,7 @@ Array Options::as>(const Array& similar_to) // Mark this option as used value_used = true; - output_info << _("\tOption ") << full_name << " = Array"; - if (hasAttribute("source")) { - // Specify the source of the setting - output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; - } - output_info << endl; + printNameValueSourceLine(*this, "Array"); return result; } @@ -740,12 +770,7 @@ Matrix Options::as>(const Matrix& similar_t // Mark this option as used value_used = true; - output_info << _("\tOption ") << full_name << " = Matrix"; - if (hasAttribute("source")) { - // Specify the source of the setting - output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; - } - output_info << endl; + printNameValueSourceLine(*this, "Matrix"); return result; } @@ -767,12 +792,7 @@ Tensor Options::as>(const Tensor& similar_t // Mark this option as used value_used = true; - output_info << _("\tOption ") << full_name << " = Tensor"; - if (hasAttribute("source")) { - // Specify the source of the setting - output_info << " (" << bout::utils::variantToString(attributes.at("source")) << ")"; - } - output_info << endl; + printNameValueSourceLine(*this, "Tensor"); return result; } @@ -858,7 +878,7 @@ Options Options::getUnused(const std::vector& exclude_sources) cons } void Options::printUnused() const { - Options unused = getUnused(); + const Options unused = getUnused(); // Two cases: single value, or a section. If it's a single value, // we can check it directly. If it's a section, we can see if it has @@ -882,9 +902,9 @@ void Options::cleanCache() { FieldFactory::get()->cleanCache(); } std::map Options::subsections() const { std::map sections; - for (const auto& it : children) { - if (it.second.is_section) { - sections[it.first] = &it.second; + for (const auto& child : children) { + if (child.second.is_section) { + sections[child.first] = &child.second; } } return sections; @@ -915,8 +935,8 @@ fmt::format_parse_context::iterator bout::details::OptionsFormatterBase::parse(fmt::format_parse_context& ctx) { const auto* closing_brace = std::find(ctx.begin(), ctx.end(), '}'); - std::for_each(ctx.begin(), closing_brace, [&](auto it) { - switch (it) { + std::for_each(ctx.begin(), closing_brace, [&](auto ctx_opt) { + switch (ctx_opt) { case 'd': docstrings = true; break; @@ -1014,7 +1034,7 @@ bout::details::OptionsFormatterBase::format(const Options& options, // Only print section headers if the section has a name and it has // non-section children - const auto children = options.getChildren(); + const auto& children = options.getChildren(); const bool has_child_values = std::any_of(children.begin(), children.end(), [](const auto& child) { return child.second.isValue(); }); @@ -1059,7 +1079,7 @@ void checkForUnusedOptions() { void checkForUnusedOptions(const Options& options, const std::string& data_dir, const std::string& option_file) { - Options unused = options.getUnused(); + const Options unused = options.getUnused(); if (not unused.getChildren().empty()) { // Construct a string with all the fuzzy matches for each unused option diff --git a/src/sys/options/options_adios.cxx b/src/sys/options/options_adios.cxx new file mode 100644 index 0000000000..e93a1fea94 --- /dev/null +++ b/src/sys/options/options_adios.cxx @@ -0,0 +1,548 @@ +#include "bout/build_config.hxx" + +#if BOUT_HAS_ADIOS + +#include "options_adios.hxx" +#include "bout/adios_object.hxx" + +#include "bout/bout.hxx" +#include "bout/boutexception.hxx" +#include "bout/globals.hxx" +#include "bout/mesh.hxx" +#include "bout/sys/timer.hxx" + +#include "adios2.h" +#include +#include +#include + +namespace bout { +/// Name of the attribute used to track individual variable's time indices +constexpr auto current_time_index_name = "current_time_index"; + +OptionsADIOS::OptionsADIOS(Options& options) : OptionsIO(options) { + if (options["file"].doc("File name. Defaults to /.pb").isSet()) { + filename = options["file"].as(); + } else { + // Both path and prefix must be set + filename = fmt::format("{}/{}.bp", options["path"].as(), + options["prefix"].as()); + } + + file_mode = (options["append"].doc("Append to existing file?").withDefault(false)) + ? FileMode::append + : FileMode::replace; + + singleWriteFile = options["singleWriteFile"].withDefault(false); +} + +template +Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& name, + const std::string& type) { + std::vector data; + adios2::Variable variable = io.InquireVariable(name); + + if (variable.ShapeID() == adios2::ShapeID::GlobalValue) { + T value; + reader.Get(variable, &value, adios2::Mode::Sync); + return Options(value); + } + + if (variable.ShapeID() == adios2::ShapeID::LocalArray) { + throw std::invalid_argument( + "ADIOS reader did not implement reading local arrays like " + type + " " + name + + " in file " + reader.Name()); + } + + if (type != "double" && type != "float") { + throw std::invalid_argument( + "ADIOS reader did not implement reading arrays that are not double/float type. " + "Found " + + type + " " + name + " in file " + reader.Name()); + } + + if (type == "double" && sizeof(BoutReal) != sizeof(double)) { + throw std::invalid_argument( + "ADIOS does not allow for implicit type conversions. BoutReal type is " + "float but found " + + type + " " + name + " in file " + reader.Name()); + } + + if (type == "float" && sizeof(BoutReal) != sizeof(float)) { + throw std::invalid_argument( + "ADIOS reader does not allow for implicit type conversions. BoutReal type is " + "double but found " + + type + " " + name + " in file " + reader.Name()); + } + + auto dims = variable.Shape(); + auto ndims = dims.size(); + adios2::Variable variableD = io.InquireVariable(name); + + switch (ndims) { + case 1: { + Array value(static_cast(dims[0])); + BoutReal* data = value.begin(); + reader.Get(variableD, data, adios2::Mode::Sync); + return Options(value); + } + case 2: { + Matrix value(static_cast(dims[0]), static_cast(dims[1])); + BoutReal* data = value.begin(); + reader.Get(variableD, data, adios2::Mode::Sync); + return Options(value); + } + case 3: { + Tensor value(static_cast(dims[0]), static_cast(dims[1]), + static_cast(dims[2])); + BoutReal* data = value.begin(); + reader.Get(variableD, data, adios2::Mode::Sync); + return Options(value); + } + } + throw BoutException("ADIOS reader failed to read '{}' of dimension {} in file '{}'", + name, ndims, reader.Name()); +} + +Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& name, + const std::string& type) { +#define declare_template_instantiation(T) \ + if (type == adios2::GetType()) { \ + return readVariable(reader, io, name, type); \ + } + ADIOS2_FOREACH_ATTRIBUTE_PRIMITIVE_STDTYPE_1ARG(declare_template_instantiation) + declare_template_instantiation(std::string) +#undef declare_template_instantiation + output_warn.write("ADIOS readVariable can't read type '{}' (variable '{}')", type, + name); + return Options{}; +} + +bool readAttribute(adios2::IO& io, const std::string& name, const std::string& type, + Options& result) { + // Attribute is the part of 'name' after the last '/' separator + std::string attrname; + auto pos = name.find_last_of('/'); + if (pos == std::string::npos) { + attrname = name; + } else { + attrname = name.substr(pos + 1); + } + +#define declare_template_instantiation(T) \ + if (type == adios2::GetType()) { \ + adios2::Attribute a = io.InquireAttribute(name); \ + result.attributes[attrname] = *a.Data().data(); \ + return true; \ + } + // Only some types of attributes are supported + //declare_template_instantiation(bool) + declare_template_instantiation(int) declare_template_instantiation(BoutReal) + declare_template_instantiation(std::string) +#undef declare_template_instantiation + output_warn.write("ADIOS readAttribute can't read type '{}' (variable '{}')", + type, name); + return false; +} + +Options OptionsADIOS::read() { + Timer timer("io"); + + // Open file + ADIOSPtr adiosp = GetADIOSPtr(); + adios2::IO io; + std::string ioname = "read_" + filename; + try { + io = adiosp->AtIO(ioname); + } catch (const std::invalid_argument& e) { + io = adiosp->DeclareIO(ioname); + } + + adios2::Engine reader = io.Open(filename, adios2::Mode::ReadRandomAccess); + if (!reader) { + throw BoutException("Could not open ADIOS file '{:s}' for reading", filename); + } + + Options result; + + // Iterate over all variables + for (const auto& varpair : io.AvailableVariables()) { + const auto& var_name = varpair.first; // Name of the variable + + auto it = varpair.second.find("Type"); + const std::string& var_type = it->second; + + Options* varptr = &result; + for (const auto& piece : strsplit(var_name, '/')) { + varptr = &(*varptr)[piece]; // Navigate to subsection if needed + } + Options& var = *varptr; + + // Note: Copying the value rather than simple assignment is used + // because the Options assignment operator overwrites full_name. + var = 0; // Setting is_section to false + var.value = readVariable(reader, io, var_name, var_type).value; + var.attributes["source"] = filename; + + // Get variable attributes + for (const auto& attpair : io.AvailableAttributes(var_name, "/", true)) { + const auto& att_name = attpair.first; // Attribute name + const auto& att = attpair.second; // attribute params + + auto it = att.find("Type"); + const std::string& att_type = it->second; + readAttribute(io, att_name, att_type, var); + } + } + + reader.Close(); + + return result; +} + +void OptionsADIOS::verifyTimesteps() const { + ADIOSStream& stream = ADIOSStream::ADIOSGetStream(filename); + stream.engine.EndStep(); + stream.isInStep = false; + return; +} + +/// Visit a variant type, and put the data into a NcVar +struct ADIOSPutVarVisitor { + ADIOSPutVarVisitor(const std::string& name, ADIOSStream& stream) + : varname(name), stream(stream) {} + template + void operator()(const T& value) { + adios2::Variable var = stream.GetValueVariable(varname); + stream.engine.Put(var, value); + } + +private: + const std::string& varname; + ADIOSStream& stream; +}; + +template <> +void ADIOSPutVarVisitor::operator()(const bool& value) { + // Scalars are only written from processor 0 + if (BoutComm::rank() != 0) { + return; + } + adios2::Variable var = stream.GetValueVariable(varname); + stream.engine.Put(var, static_cast(value)); +} + +template <> +void ADIOSPutVarVisitor::operator()(const int& value) { + // Scalars are only written from processor 0 + if (BoutComm::rank() != 0) { + return; + } + adios2::Variable var = stream.GetValueVariable(varname); + stream.engine.Put(var, value); +} + +template <> +void ADIOSPutVarVisitor::operator()(const BoutReal& value) { + // Scalars are only written from processor 0 + if (BoutComm::rank() != 0) { + return; + } + adios2::Variable var = stream.GetValueVariable(varname); + stream.engine.Put(var, value); +} + +template <> +void ADIOSPutVarVisitor::operator()(const std::string& value) { + // Scalars are only written from processor 0 + if (BoutComm::rank() != 0) { + return; + } + adios2::Variable var = stream.GetValueVariable(varname); + stream.engine.Put(var, value, adios2::Mode::Sync); +} + +template <> +void ADIOSPutVarVisitor::operator()(const Field2D& value) { + // Get the mesh that describes how local data relates to global arrays + auto mesh = value.getMesh(); + + // The global size of this array includes boundary cells but not communication guard cells. + // In general this array will be sparse because it may have gaps. + adios2::Dims shape = {static_cast(mesh->GlobalNx), + static_cast(mesh->GlobalNy)}; + + // Offset of this processor's data into the global array + adios2::Dims start = {static_cast(mesh->MapGlobalX), + static_cast(mesh->MapGlobalY)}; + + // The size of the mapped region + adios2::Dims count = {static_cast(mesh->MapCountX), + static_cast(mesh->MapCountY)}; + + // Where the actual data starts in data pointer (to exclude ghost cells) + adios2::Dims memStart = {static_cast(mesh->MapLocalX), + static_cast(mesh->MapLocalY)}; + + // The actual size of data pointer in memory (including ghost cells) + adios2::Dims memCount = {static_cast(value.getNx()), + static_cast(value.getNy())}; + + adios2::Variable var = stream.GetArrayVariable(varname, shape); + /* std::cout << "PutVar Field2D rank " << BoutComm::rank() << " var = " << varname + << " shape = " << shape[0] << "x" << shape[1] << " count = " << count[0] + << "x" << count[1] << " Nx*Ny = " << value.getNx() << "x" << value.getNy() + << " memStart = " << memStart[0] << "x" << memStart[1] + << " memCount = " << memCount[0] << "x" << memCount[1] << std::endl;*/ + var.SetSelection({start, count}); + var.SetMemorySelection({memStart, memCount}); + stream.engine.Put(var, &value(0, 0)); +} + +template <> +void ADIOSPutVarVisitor::operator()(const Field3D& value) { + // Get the mesh that describes how local data relates to global arrays + auto mesh = value.getMesh(); + + // The global size of this array includes boundary cells but not communication guard cells. + // In general this array will be sparse because it may have gaps. + adios2::Dims shape = {static_cast(mesh->GlobalNx), + static_cast(mesh->GlobalNy), + static_cast(mesh->GlobalNz)}; + + // Offset of this processor's data into the global array + adios2::Dims start = {static_cast(mesh->MapGlobalX), + static_cast(mesh->MapGlobalY), + static_cast(mesh->MapGlobalZ)}; + + // The size of the mapped region + adios2::Dims count = {static_cast(mesh->MapCountX), + static_cast(mesh->MapCountY), + static_cast(mesh->MapCountZ)}; + + // Where the actual data starts in data pointer (to exclude ghost cells) + adios2::Dims memStart = {static_cast(mesh->MapLocalX), + static_cast(mesh->MapLocalY), + static_cast(mesh->MapLocalZ)}; + + // The actual size of data pointer in memory (including ghost cells) + adios2::Dims memCount = {static_cast(value.getNx()), + static_cast(value.getNy()), + static_cast(value.getNz())}; + + adios2::Variable var = stream.GetArrayVariable(varname, shape); + /*std::cout << "PutVar Field3D rank " << BoutComm::rank() << " var = " << varname + << " shape = " << shape[0] << "x" << shape[1] << "x" << shape[2] + << " count = " << count[0] << "x" << count[1] << "x" << count[2] + << " Nx*Ny = " << value.getNx() << "x" << value.getNy() << "x" + << value.getNz() << " memStart = " << memStart[0] << "x" << memStart[1] << "x" + << memStart[2] << " memCount = " << memCount[0] << "x" << memCount[1] << "x" + << memCount[2] << std::endl;*/ + var.SetSelection({start, count}); + var.SetMemorySelection({memStart, memCount}); + stream.engine.Put(var, &value(0, 0, 0)); +} + +template <> +void ADIOSPutVarVisitor::operator()(const FieldPerp& value) { + // Get the mesh that describes how local data relates to global arrays + auto mesh = value.getMesh(); + + // The global size of this array includes boundary cells but not communication guard cells. + // In general this array will be sparse because it may have gaps. + adios2::Dims shape = {static_cast(mesh->GlobalNx), + static_cast(mesh->GlobalNz)}; + + // Offset of this processor's data into the global array + adios2::Dims start = {static_cast(mesh->MapGlobalX), + static_cast(mesh->MapGlobalZ)}; + + // The size of the mapped region + adios2::Dims count = {static_cast(mesh->MapCountX), + static_cast(mesh->MapCountZ)}; + + // Where the actual data starts in data pointer (to exclude ghost cells) + adios2::Dims memStart = {static_cast(mesh->MapLocalX), + static_cast(mesh->MapLocalZ)}; + + // The actual size of data pointer in memory (including ghost cells) + adios2::Dims memCount = {static_cast(value.getNx()), + static_cast(value.getNz())}; + + adios2::Variable var = stream.GetArrayVariable(varname, shape); + /* std::cout << "PutVar FieldPerp rank " << BoutComm::rank() << " var = " << varname + << " shape = " << shape[0] << "x" << shape[1] << " count = " << count[0] + << "x" << count[1] << " Nx*Ny = " << value.getNx() << "x" << value.getNy() + << " memStart = " << memStart[0] << "x" << memStart[1] + << " memCount = " << memCount[0] << "x" << memCount[1] << std::endl; */ + var.SetSelection({start, count}); + var.SetMemorySelection({memStart, memCount}); + stream.engine.Put(var, &value(0, 0)); +} + +template <> +void ADIOSPutVarVisitor::operator()>(const Array& value) { + // Pointer to data. Assumed to be contiguous array + adios2::Dims shape = {(size_t)BoutComm::size(), (size_t)value.size()}; + adios2::Dims start = {(size_t)BoutComm::rank(), 0}; + adios2::Dims count = {1, shape[1]}; + adios2::Variable var = stream.GetArrayVariable(varname, shape); + var.SetSelection({start, count}); + stream.engine.Put(var, value.begin()); +} + +template <> +void ADIOSPutVarVisitor::operator()>(const Matrix& value) { + // Pointer to data. Assumed to be contiguous array + auto s = value.shape(); + adios2::Dims shape = {(size_t)BoutComm::size(), (size_t)std::get<0>(s), + (size_t)std::get<1>(s)}; + adios2::Dims start = {(size_t)BoutComm::rank(), 0, 0}; + adios2::Dims count = {1, shape[1], shape[2]}; + adios2::Variable var = stream.GetArrayVariable(varname, shape); + var.SetSelection({start, count}); + stream.engine.Put(var, value.begin()); +} + +template <> +void ADIOSPutVarVisitor::operator()>(const Tensor& value) { + // Pointer to data. Assumed to be contiguous array + auto s = value.shape(); + adios2::Dims shape = {(size_t)BoutComm::size(), (size_t)std::get<0>(s), + (size_t)std::get<1>(s), (size_t)std::get<2>(s)}; + adios2::Dims start = {(size_t)BoutComm::rank(), 0, 0, 0}; + adios2::Dims count = {1, shape[1], shape[2], shape[3]}; + adios2::Variable var = stream.GetArrayVariable(varname, shape); + var.SetSelection({start, count}); + stream.engine.Put(var, value.begin()); +} + +/// Visit a variant type, and put the data into a NcVar +struct ADIOSPutAttVisitor { + ADIOSPutAttVisitor(const std::string& varname, const std::string& attrname, + ADIOSStream& stream) + : varname(varname), attrname(attrname), stream(stream) {} + template + void operator()(const T& value) { + stream.io.DefineAttribute(attrname, value, varname, "/", false); + } + +private: + const std::string& varname; + const std::string& attrname; + ADIOSStream& stream; +}; + +template <> +void ADIOSPutAttVisitor::operator()(const bool& value) { + stream.io.DefineAttribute(attrname, (int)value, varname, "/", false); +} + +void writeGroup(const Options& options, ADIOSStream& stream, const std::string& groupname, + const std::string& time_dimension) { + + for (const auto& childpair : options.getChildren()) { + const auto& name = childpair.first; + const auto& child = childpair.second; + + if (child.isSection()) { + TRACE("Writing group '{:s}'", name); + writeGroup(child, stream, name, time_dimension); + continue; + } + + if (child.isValue()) { + try { + auto time_it = child.attributes.find("time_dimension"); + if (time_it == child.attributes.end()) { + if (stream.adiosStep > 0) { + // we should only write the non-varying values in the first step + continue; + } + } else { + // Has a time dimension + + const auto& time_name = bout::utils::get(time_it->second); + + // Only write time-varying values that match current time + // dimension being written + if (time_name != time_dimension) { + continue; + } + } + + // Write the variable + // Note: ADIOS2 uses '/' to as a group separator; BOUT++ uses ':' + std::string varname = groupname.empty() ? name : groupname + "/" + name; + bout::utils::visit(ADIOSPutVarVisitor(varname, stream), child.value); + + // Write attributes + if (!BoutComm::rank()) { + for (const auto& attribute : child.attributes) { + const std::string& att_name = attribute.first; + const auto& att = attribute.second; + + bout::utils::visit(ADIOSPutAttVisitor(varname, att_name, stream), att); + } + } + + } catch (const std::exception& e) { + throw BoutException("Error while writing value '{:s}' : {:s}", name, e.what()); + } + } + } +} + +/// Write options to file +void OptionsADIOS::write(const Options& options, const std::string& time_dim) { + Timer timer("io"); + + // ADIOSStream is just a BOUT++ object, it does not create anything inside ADIOS + ADIOSStream& stream = ADIOSStream::ADIOSGetStream(filename); + + // Need to have an adios2::IO object first, which can only be created once. + if (!stream.io) { + ADIOSPtr adiosp = GetADIOSPtr(); + std::string ioname = "write_" + filename; + try { + stream.io = adiosp->AtIO(ioname); + } catch (const std::invalid_argument& e) { + stream.io = adiosp->DeclareIO(ioname); + stream.io.SetEngine("BP5"); + } + } + + /* Open file once and keep it open, close in stream desctructor + or close after writing if singleWriteFile == true + */ + if (!stream.engine) { + adios2::Mode amode = + (file_mode == FileMode::append ? adios2::Mode::Append : adios2::Mode::Write); + stream.engine = stream.io.Open(filename, amode); + if (!stream.engine) { + throw BoutException("Could not open ADIOS file '{:s}' for writing", filename); + } + } + + /* Multiple write() calls allowed in a single adios step to output multiple + Options objects in the same step. verifyTimesteps() will indicate the + completion of the step (and adios will publish the step). + */ + if (!stream.isInStep) { + stream.engine.BeginStep(); + stream.isInStep = true; + stream.adiosStep = stream.engine.CurrentStep(); + } + + writeGroup(options, stream, "", time_dim); + + /* In singleWriteFile mode, we complete the step and close the file */ + if (singleWriteFile) { + stream.engine.EndStep(); + stream.engine.Close(); + } +} + +} // namespace bout + +#endif // BOUT_HAS_ADIOS diff --git a/src/sys/options/options_adios.hxx b/src/sys/options/options_adios.hxx new file mode 100644 index 0000000000..eddb3976ff --- /dev/null +++ b/src/sys/options/options_adios.hxx @@ -0,0 +1,83 @@ + +#pragma once + +#ifndef OPTIONS_ADIOS_H +#define OPTIONS_ADIOS_H + +#include "bout/build_config.hxx" +#include "bout/options.hxx" +#include "bout/options_io.hxx" + +#if !BOUT_HAS_ADIOS + +namespace { +bout::RegisterUnavailableOptionsIO + registerunavailableoptionsadios("adios", "BOUT++ was not configured with ADIOS2"); +} + +#else + +#include +#include + +namespace bout { + +/// Forward declare ADIOS file type so we don't need to depend +/// directly on ADIOS +struct ADIOSStream; + +class OptionsADIOS : public OptionsIO { +public: + // Constructors need to be defined in implementation due to forward + // declaration of ADIOSStream + OptionsADIOS() = delete; + + /// Create an OptionsADIOS + /// + /// Options: + /// - "file" The name of the file + /// If not set then "path" and "prefix" must be set, + /// and file is set to {path}/{prefix}.bp + /// - "append" + /// - "singleWriteFile" + OptionsADIOS(Options& options); + + OptionsADIOS(const OptionsADIOS&) = delete; + OptionsADIOS(OptionsADIOS&&) noexcept = default; + ~OptionsADIOS() = default; + + OptionsADIOS& operator=(const OptionsADIOS&) = delete; + OptionsADIOS& operator=(OptionsADIOS&&) noexcept = default; + + /// Read options from file + Options read() override; + + /// Write options to file + void write(const Options& options, const std::string& time_dim) override; + + /// Check that all variables with the same time dimension have the + /// same size in that dimension. Throws BoutException if there are + /// any differences, otherwise is silent + void verifyTimesteps() const override; + +private: + enum class FileMode { + replace, ///< Overwrite file when writing + append ///< Append to file when writing + }; + + /// Name of the file on disk + std::string filename; + /// How to open the file for writing + FileMode file_mode{FileMode::replace}; + bool singleWriteFile = false; +}; + +namespace { +RegisterOptionsIO registeroptionsadios("adios"); +} + +} // namespace bout + +#endif // BOUT_HAS_ADIOS +#endif // OPTIONS_ADIOS_H diff --git a/src/sys/options/options_ini.cxx b/src/sys/options/options_ini.cxx index a1e9bcaeb1..d1889f993b 100644 --- a/src/sys/options/options_ini.cxx +++ b/src/sys/options/options_ini.cxx @@ -189,7 +189,7 @@ void OptionINI::parse(const string& buffer, string& key, string& value) { // Just set a flag to true // e.g. "restart" or "append" on command line key = buffer; - value = string("TRUE"); + value = string("true"); return; } diff --git a/src/sys/options/options_io.cxx b/src/sys/options/options_io.cxx new file mode 100644 index 0000000000..6717b6b07d --- /dev/null +++ b/src/sys/options/options_io.cxx @@ -0,0 +1,58 @@ +#include "bout/options_io.hxx" +#include "bout/bout.hxx" +#include "bout/globals.hxx" +#include "bout/mesh.hxx" + +#include "options_adios.hxx" +#include "options_netcdf.hxx" + +namespace bout { +std::unique_ptr OptionsIO::create(const std::string& file) { + return OptionsIOFactory::getInstance().createFile(file); +} + +std::unique_ptr OptionsIO::create(Options& config) { + auto& factory = OptionsIOFactory::getInstance(); + return factory.create(factory.getType(&config), config); +} + +OptionsIOFactory::ReturnType OptionsIOFactory::createRestart(Options* optionsptr) const { + Options& options = optionsptr ? *optionsptr : Options::root()["restart_files"]; + + // Set defaults + options["path"].overrideDefault( + Options::root()["datadir"].withDefault("data")); + options["prefix"].overrideDefault("BOUT.restart"); + options["append"].overrideDefault(false); + options["singleWriteFile"].overrideDefault(true); + return create(getType(&options), options); +} + +OptionsIOFactory::ReturnType OptionsIOFactory::createOutput(Options* optionsptr) const { + Options& options = optionsptr ? *optionsptr : Options::root()["output"]; + + // Set defaults + options["path"].overrideDefault( + Options::root()["datadir"].withDefault("data")); + options["prefix"].overrideDefault("BOUT.dmp"); + options["append"].overrideDefault(Options::root()["append"] + .doc("Add output data to existing (dump) files?") + .withDefault(false)); + return create(getType(&options), options); +} + +OptionsIOFactory::ReturnType OptionsIOFactory::createFile(const std::string& file) const { + Options options{{"file", file}}; + return create(getDefaultType(), options); +} + +void writeDefaultOutputFile(Options& data) { + // Add BOUT++ version and flags + bout::experimental::addBuildFlagsToOptions(data); + // Add mesh information + bout::globals::mesh->outputVars(data); + // Write to the default output file + OptionsIOFactory::getInstance().createOutput()->write(data); +} + +} // namespace bout diff --git a/src/sys/options/options_netcdf.cxx b/src/sys/options/options_netcdf.cxx index d7ceeaea60..65fbca14c0 100644 --- a/src/sys/options/options_netcdf.cxx +++ b/src/sys/options/options_netcdf.cxx @@ -2,7 +2,7 @@ #if BOUT_HAS_NETCDF && !BOUT_HAS_LEGACY_NETCDF -#include "bout/options_netcdf.hxx" +#include "options_netcdf.hxx" #include "bout/bout.hxx" #include "bout/globals.hxx" @@ -196,8 +196,7 @@ NcType NcTypeVisitor::operator()(const double& UNUSED(t)) { } template <> -MAYBE_UNUSED() -NcType NcTypeVisitor::operator()(const float& UNUSED(t)) { +[[maybe_unused]] NcType NcTypeVisitor::operator()(const float& UNUSED(t)) { return ncFloat; } @@ -403,8 +402,7 @@ void NcPutAttVisitor::operator()(const double& value) { var.putAtt(name, ncDouble, value); } template <> -MAYBE_UNUSED() -void NcPutAttVisitor::operator()(const float& value) { +[[maybe_unused]] void NcPutAttVisitor::operator()(const float& value) { var.putAtt(name, ncFloat, value); } template <> @@ -643,14 +641,19 @@ std::vector verifyTimesteps(const NcGroup& group) { namespace bout { -OptionsNetCDF::OptionsNetCDF() : data_file(nullptr) {} - -OptionsNetCDF::OptionsNetCDF(std::string filename, FileMode mode) - : filename(std::move(filename)), file_mode(mode), data_file(nullptr) {} +OptionsNetCDF::OptionsNetCDF(Options& options) : OptionsIO(options) { + if (options["file"].doc("File name. Defaults to /..nc").isSet()) { + filename = options["file"].as(); + } else { + // Both path and prefix must be set + filename = fmt::format("{}/{}.{}.nc", options["path"].as(), + options["prefix"].as(), BoutComm::rank()); + } -OptionsNetCDF::~OptionsNetCDF() = default; -OptionsNetCDF::OptionsNetCDF(OptionsNetCDF&&) noexcept = default; -OptionsNetCDF& OptionsNetCDF::operator=(OptionsNetCDF&&) noexcept = default; + file_mode = (options["append"].doc("Append to existing file?").withDefault(false)) + ? FileMode::append + : FileMode::replace; +} void OptionsNetCDF::verifyTimesteps() const { NcFile dataFile(filename, NcFile::read); @@ -699,41 +702,6 @@ void OptionsNetCDF::write(const Options& options, const std::string& time_dim) { data_file->sync(); } -std::string getRestartDirectoryName(Options& options) { - if (options["restartdir"].isSet()) { - // Solver-specific restart directory - return options["restartdir"].withDefault("data"); - } - // Use the root data directory - return options["datadir"].withDefault("data"); -} - -std::string getRestartFilename(Options& options) { - return getRestartFilename(options, BoutComm::rank()); -} - -std::string getRestartFilename(Options& options, int rank) { - return fmt::format("{}/BOUT.restart.{}.nc", bout::getRestartDirectoryName(options), - rank); -} - -std::string getOutputFilename(Options& options) { - return getOutputFilename(options, BoutComm::rank()); -} - -std::string getOutputFilename(Options& options, int rank) { - return fmt::format("{}/BOUT.dmp.{}.nc", - options["datadir"].withDefault("data"), rank); -} - -void writeDefaultOutputFile() { writeDefaultOutputFile(Options::root()); } - -void writeDefaultOutputFile(Options& options) { - bout::experimental::addBuildFlagsToOptions(options); - bout::globals::mesh->outputVars(options); - OptionsNetCDF(getOutputFilename(Options::root())).write(options); -} - } // namespace bout #endif // BOUT_HAS_NETCDF diff --git a/src/sys/options/options_netcdf.hxx b/src/sys/options/options_netcdf.hxx new file mode 100644 index 0000000000..8f195c9d92 --- /dev/null +++ b/src/sys/options/options_netcdf.hxx @@ -0,0 +1,84 @@ + +#pragma once + +#ifndef OPTIONS_NETCDF_H +#define OPTIONS_NETCDF_H + +#include "bout/build_config.hxx" + +#include "bout/options.hxx" +#include "bout/options_io.hxx" + +#if !BOUT_HAS_NETCDF || BOUT_HAS_LEGACY_NETCDF + +namespace { +RegisterUnavailableOptionsIO + registerunavailableoptionsnetcdf("netcdf", "BOUT++ was not configured with NetCDF"); +} + +#else + +#include +#include +#include + +namespace bout { + +class OptionsNetCDF : public OptionsIO { +public: + // Constructors need to be defined in implementation due to forward + // declaration of NcFile + OptionsNetCDF() = delete; + + /// Create an OptionsNetCDF + /// + /// Options: + /// - "file" The name of the file + /// If not set then "path" and "prefix" options must be set, + /// and file is set to {path}/{prefix}.{rank}.nc + /// - "append" File mode, default is false + OptionsNetCDF(Options& options); + + ~OptionsNetCDF() {} + + OptionsNetCDF(const OptionsNetCDF&) = delete; + OptionsNetCDF(OptionsNetCDF&&) noexcept = default; + OptionsNetCDF& operator=(const OptionsNetCDF&) = delete; + OptionsNetCDF& operator=(OptionsNetCDF&&) noexcept = default; + + /// Read options from file + Options read(); + + /// Write options to file + void write(const Options& options) { write(options, "t"); } + void write(const Options& options, const std::string& time_dim); + + /// Check that all variables with the same time dimension have the + /// same size in that dimension. Throws BoutException if there are + /// any differences, otherwise is silent + void verifyTimesteps() const; + +private: + enum class FileMode { + replace, ///< Overwrite file when writing + append ///< Append to file when writing + }; + + /// Pointer to netCDF file so we don't introduce direct dependence + std::unique_ptr data_file = nullptr; + + /// Name of the file on disk + std::string filename; + /// How to open the file for writing + FileMode file_mode{FileMode::replace}; +}; + +namespace { +RegisterOptionsIO registeroptionsnetcdf("netcdf"); +} + +} // namespace bout + +#endif + +#endif // OPTIONS_NETCDF_H diff --git a/tests/MMS/spatial/fci/data/BOUT.inp b/tests/MMS/spatial/fci/data/BOUT.inp index b845e22012..b4825c6207 100644 --- a/tests/MMS/spatial/fci/data/BOUT.inp +++ b/tests/MMS/spatial/fci/data/BOUT.inp @@ -5,6 +5,7 @@ input_field = sin(y - 2*z) + sin(y - z) solution = (6.28318530717959*(0.01*x + 0.045)*(-2*cos(y - 2*z) - cos(y - z)) + 0.628318530717959*cos(y - 2*z) + 0.628318530717959*cos(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0) MXG = 1 +MYG = 1 NXPE = 1 [mesh] diff --git a/tests/MMS/spatial/fci/fci_mms.cxx b/tests/MMS/spatial/fci/fci_mms.cxx index 5a2599368e..18405a7f88 100644 --- a/tests/MMS/spatial/fci/fci_mms.cxx +++ b/tests/MMS/spatial/fci/fci_mms.cxx @@ -17,6 +17,8 @@ int main(int argc, char** argv) { Field3D error{result - solution}; Options dump; + // Add mesh geometry variables + mesh->outputVars(dump); dump["l_2"] = sqrt(mean(SQ(error), true, "RGN_NOBNDRY")); dump["l_inf"] = max(abs(error), true, "RGN_NOBNDRY"); diff --git a/tests/MMS/spatial/fci/mms.py b/tests/MMS/spatial/fci/mms.py index 806441b330..1e71135c90 100755 --- a/tests/MMS/spatial/fci/mms.py +++ b/tests/MMS/spatial/fci/mms.py @@ -3,9 +3,6 @@ # Generate manufactured solution and sources for FCI test # -from __future__ import division -from __future__ import print_function - from boutdata.mms import * from sympy import sin, cos, sqrt diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index 0ec8b70e9f..542cefa07e 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -187,7 +187,7 @@ if False: dx, error_inf[nslice], "--", - label="{} $l_\inf$".format(method_orders[nslice]["name"]), + label="{} $l_\\inf$".format(method_orders[nslice]["name"]), ) ax.legend(loc="upper left") ax.grid() diff --git a/tests/MMS/time/time.cxx b/tests/MMS/time/time.cxx index 346662a8e3..17e1f547ab 100644 --- a/tests/MMS/time/time.cxx +++ b/tests/MMS/time/time.cxx @@ -10,7 +10,7 @@ class TimeTest : public PhysicsModel { public: - int init(MAYBE_UNUSED(bool restart)) { + int init([[maybe_unused]] bool restart) override { solver->add(f, "f"); // Solve a single 3D field setSplitOperator(); @@ -18,16 +18,16 @@ class TimeTest : public PhysicsModel { return 0; } - int rhs(MAYBE_UNUSED(BoutReal time)) { + int rhs([[maybe_unused]] BoutReal time) override { ddt(f) = f; return 0; } - int convective(MAYBE_UNUSED(BoutReal time)) { + int convective([[maybe_unused]] BoutReal time) { ddt(f) = 0.5 * f; return 0; } - int diffusive(MAYBE_UNUSED(BoutReal time)) { + int diffusive([[maybe_unused]] BoutReal time) { ddt(f) = 0.5 * f; return 0; } diff --git a/tests/integrated/CMakeLists.txt b/tests/integrated/CMakeLists.txt index 1fe0e13b2d..8c4f096f83 100644 --- a/tests/integrated/CMakeLists.txt +++ b/tests/integrated/CMakeLists.txt @@ -31,6 +31,7 @@ add_subdirectory(test-laplacexz) add_subdirectory(test-multigrid_laplace) add_subdirectory(test-naulin-laplace) add_subdirectory(test-options-netcdf) +add_subdirectory(test-options-adios) add_subdirectory(test-petsc_laplace) add_subdirectory(test-petsc_laplace_MAST-grid) add_subdirectory(test-restart-io) diff --git a/tests/integrated/test-beuler/test_beuler.cxx b/tests/integrated/test-beuler/test_beuler.cxx index cfdae89eb2..5a080767dd 100644 --- a/tests/integrated/test-beuler/test_beuler.cxx +++ b/tests/integrated/test-beuler/test_beuler.cxx @@ -41,7 +41,6 @@ class TestSolver : public PhysicsModel { }; int main(int argc, char** argv) { - // Absolute tolerance for difference between the actual value and the // expected value constexpr BoutReal tolerance = 1.e-5; @@ -87,8 +86,6 @@ int main(int argc, char** argv) { solver->solve(); - BoutFinalise(false); - if (model.check_solution(tolerance)) { output_test << " PASSED\n"; return 0; diff --git a/tests/integrated/test-boutpp/collect-staggered/data/BOUT.inp b/tests/integrated/test-boutpp/collect-staggered/data/BOUT.inp index 6dc68a7e8b..cb1d1ec7f6 100644 --- a/tests/integrated/test-boutpp/collect-staggered/data/BOUT.inp +++ b/tests/integrated/test-boutpp/collect-staggered/data/BOUT.inp @@ -2,7 +2,7 @@ nout = 10 timestep = 0.1 [mesh] -staggergrids = True +staggergrids = true n = 1 nx = n+2*MXG ny = 16 diff --git a/tests/integrated/test-boutpp/collect/input/BOUT.inp b/tests/integrated/test-boutpp/collect/input/BOUT.inp index a390bb1891..cad2f17c52 100644 --- a/tests/integrated/test-boutpp/collect/input/BOUT.inp +++ b/tests/integrated/test-boutpp/collect/input/BOUT.inp @@ -5,7 +5,7 @@ MXG = 2 MYG = 2 [mesh] -staggergrids = True +staggergrids = true n = 1 nx = n+2*MXG ny = n diff --git a/tests/integrated/test-boutpp/legacy-model/data/BOUT.inp b/tests/integrated/test-boutpp/legacy-model/data/BOUT.inp index c6dd2fd761..b6b44502f6 100644 --- a/tests/integrated/test-boutpp/legacy-model/data/BOUT.inp +++ b/tests/integrated/test-boutpp/legacy-model/data/BOUT.inp @@ -2,7 +2,7 @@ nout = 10 timestep = 0.1 [mesh] -staggergrids = True +staggergrids = true n = 1 nx = n+2*MXG ny = n diff --git a/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp b/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp index a390bb1891..cad2f17c52 100644 --- a/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp +++ b/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp @@ -5,7 +5,7 @@ MXG = 2 MYG = 2 [mesh] -staggergrids = True +staggergrids = true n = 1 nx = n+2*MXG ny = n diff --git a/tests/integrated/test-boutpp/slicing/basics.indexing.html b/tests/integrated/test-boutpp/slicing/basics.indexing.html new file mode 100644 index 0000000000..180f39c6ed --- /dev/null +++ b/tests/integrated/test-boutpp/slicing/basics.indexing.html @@ -0,0 +1,1368 @@ + + + + + + + + + Indexing on ndarrays — NumPy v1.23 Manual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + +
+
+ + + + + + + + + + + + + + + +
+ +
+ +
+

Indexing on ndarrays#

+
+

See also

+

Indexing routines

+
+

ndarrays can be indexed using the standard Python +x[obj] syntax, where x is the array and obj the selection. +There are different kinds of indexing available depending on obj: +basic indexing, advanced indexing and field access.

+

Most of the following examples show the use of indexing when +referencing data in an array. The examples work just as well +when assigning to an array. See Assigning values to indexed arrays for +specific examples and explanations on how assignments work.

+

Note that in Python, x[(exp1, exp2, ..., expN)] is equivalent to +x[exp1, exp2, ..., expN]; the latter is just syntactic sugar +for the former.

+
+

Basic indexing#

+
+

Single element indexing#

+

Single element indexing works +exactly like that for other standard Python sequences. It is 0-based, +and accepts negative indices for indexing from the end of the array.

+
>>> x = np.arange(10)
+>>> x[2]
+2
+>>> x[-2]
+8
+
+
+

It is not necessary to +separate each dimension’s index into its own set of square brackets.

+
>>> x.shape = (2, 5)  # now x is 2-dimensional
+>>> x[1, 3]
+8
+>>> x[1, -1]
+9
+
+
+

Note that if one indexes a multidimensional array with fewer indices +than dimensions, one gets a subdimensional array. For example:

+
>>> x[0]
+array([0, 1, 2, 3, 4])
+
+
+

That is, each index specified selects the array corresponding to the +rest of the dimensions selected. In the above example, choosing 0 +means that the remaining dimension of length 5 is being left unspecified, +and that what is returned is an array of that dimensionality and size. +It must be noted that the returned array is a view, i.e., it is not a +copy of the original, but points to the same values in memory as does the +original array. +In this case, the 1-D array at the first position (0) is returned. +So using a single index on the returned array, results in a single +element being returned. That is:

+
>>> x[0][2]
+2
+
+
+

So note that x[0, 2] == x[0][2] though the second case is more +inefficient as a new temporary array is created after the first index +that is subsequently indexed by 2.

+
+

Note

+

NumPy uses C-order indexing. That means that the last +index usually represents the most rapidly changing memory location, +unlike Fortran or IDL, where the first index represents the most +rapidly changing location in memory. This difference represents a +great potential for confusion.

+
+
+
+

Slicing and striding#

+

Basic slicing extends Python’s basic concept of slicing to N +dimensions. Basic slicing occurs when obj is a slice object +(constructed by start:stop:step notation inside of brackets), an +integer, or a tuple of slice objects and integers. Ellipsis +and newaxis objects can be interspersed with these as +well.

+

The simplest case of indexing with N integers returns an array +scalar representing the corresponding item. As in +Python, all indices are zero-based: for the i-th index \(n_i\), +the valid range is \(0 \le n_i < d_i\) where \(d_i\) is the +i-th element of the shape of the array. Negative indices are +interpreted as counting from the end of the array (i.e., if +\(n_i < 0\), it means \(n_i + d_i\)).

+

All arrays generated by basic slicing are always views +of the original array.

+
+

Note

+

NumPy slicing creates a view instead of a copy as in the case of +built-in Python sequences such as string, tuple and list. +Care must be taken when extracting +a small portion from a large array which becomes useless after the +extraction, because the small portion extracted contains a reference +to the large original array whose memory will not be released until +all arrays derived from it are garbage-collected. In such cases an +explicit copy() is recommended.

+
+

The standard rules of sequence slicing apply to basic slicing on a +per-dimension basis (including using a step index). Some useful +concepts to remember include:

+
    +
  • The basic slice syntax is i:j:k where i is the starting index, +j is the stopping index, and k is the step (\(k\neq0\)). +This selects the m elements (in the corresponding dimension) with +index values i, i + k, …, i + (m - 1) k where +\(m = q + (r\neq0)\) and q and r are the quotient and remainder +obtained by dividing j - i by k: j - i = q k + r, so that +i + (m - 1) k < j. +For example:

    +
    >>> x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    +>>> x[1:7:2]
    +array([1, 3, 5])
    +
    +
    +
  • +
  • Negative i and j are interpreted as n + i and n + j where +n is the number of elements in the corresponding dimension. +Negative k makes stepping go towards smaller indices. +From the above example:

    +
    >>> x[-2:10]
    +array([8, 9])
    +>>> x[-3:3:-1]
    +array([7, 6, 5, 4])
    +
    +
    +
  • +
  • Assume n is the number of elements in the dimension being +sliced. Then, if i is not given it defaults to 0 for k > 0 and +n - 1 for k < 0 . If j is not given it defaults to n for k > 0 +and -n-1 for k < 0 . If k is not given it defaults to 1. Note that +:: is the same as : and means select all indices along this +axis. +From the above example:

    +
    >>> x[5:]
    +array([5, 6, 7, 8, 9])
    +
    +
    +
  • +
  • If the number of objects in the selection tuple is less than +N, then : is assumed for any subsequent dimensions. +For example:

    +
    >>> x = np.array([[[1],[2],[3]], [[4],[5],[6]]])
    +>>> x.shape
    +(2, 3, 1)
    +>>> x[1:2]
    +array([[[4],
    +        [5],
    +        [6]]])
    +
    +
    +
  • +
  • An integer, i, returns the same values as i:i+1 +except the dimensionality of the returned object is reduced by +1. In particular, a selection tuple with the p-th +element an integer (and all other entries :) returns the +corresponding sub-array with dimension N - 1. If N = 1 +then the returned object is an array scalar. These objects are +explained in Scalars.

  • +
  • If the selection tuple has all entries : except the +p-th entry which is a slice object i:j:k, +then the returned array has dimension N formed by +concatenating the sub-arrays returned by integer indexing of +elements i, i+k, …, i + (m - 1) k < j,

  • +
  • Basic slicing with more than one non-: entry in the slicing +tuple, acts like repeated application of slicing using a single +non-: entry, where the non-: entries are successively taken +(with all other non-: entries replaced by :). Thus, +x[ind1, ..., ind2,:] acts like x[ind1][..., ind2, :] under basic +slicing.

    +
    +

    Warning

    +

    The above is not true for advanced indexing.

    +
    +
  • +
  • You may use slicing to set values in the array, but (unlike lists) you +can never grow the array. The size of the value to be set in +x[obj] = value must be (broadcastable) to the same shape as +x[obj].

  • +
  • A slicing tuple can always be constructed as obj +and used in the x[obj] notation. Slice objects can be used in +the construction in place of the [start:stop:step] +notation. For example, x[1:10:5, ::-1] can also be implemented +as obj = (slice(1, 10, 5), slice(None, None, -1)); x[obj] . This +can be useful for constructing generic code that works on arrays +of arbitrary dimensions. See Dealing with variable numbers of indices within programs +for more information.

  • +
+
+
+

Dimensional indexing tools#

+

There are some tools to facilitate the easy matching of array shapes with +expressions and in assignments.

+

Ellipsis expands to the number of : objects needed for the +selection tuple to index all dimensions. In most cases, this means that the +length of the expanded selection tuple is x.ndim. There may only be a +single ellipsis present. +From the above example:

+
>>> x[..., 0]
+array([[1, 2, 3],
+      [4, 5, 6]])
+
+
+

This is equivalent to:

+
>>> x[:, :, 0]
+array([[1, 2, 3],
+      [4, 5, 6]])
+
+
+

Each newaxis object in the selection tuple serves to expand +the dimensions of the resulting selection by one unit-length +dimension. The added dimension is the position of the newaxis +object in the selection tuple. newaxis is an alias for +None, and None can be used in place of this with the same result. +From the above example:

+
>>> x[:, np.newaxis, :, :].shape
+(2, 1, 3, 1)
+>>> x[:, None, :, :].shape
+(2, 1, 3, 1)
+
+
+

This can be handy to combine two +arrays in a way that otherwise would require explicit reshaping +operations. For example:

+
>>> x = np.arange(5)
+>>> x[:, np.newaxis] + x[np.newaxis, :]
+array([[0, 1, 2, 3, 4],
+      [1, 2, 3, 4, 5],
+      [2, 3, 4, 5, 6],
+      [3, 4, 5, 6, 7],
+      [4, 5, 6, 7, 8]])
+
+
+
+
+
+

Advanced indexing#

+

Advanced indexing is triggered when the selection object, obj, is a +non-tuple sequence object, an ndarray (of data type integer or bool), +or a tuple with at least one sequence object or ndarray (of data type +integer or bool). There are two types of advanced indexing: integer +and Boolean.

+

Advanced indexing always returns a copy of the data (contrast with +basic slicing that returns a view).

+
+

Warning

+

The definition of advanced indexing means that x[(1, 2, 3),] is +fundamentally different than x[(1, 2, 3)]. The latter is +equivalent to x[1, 2, 3] which will trigger basic selection while +the former will trigger advanced indexing. Be sure to understand +why this occurs.

+

Also recognize that x[[1, 2, 3]] will trigger advanced indexing, +whereas due to the deprecated Numeric compatibility mentioned above, +x[[1, 2, slice(None)]] will trigger basic slicing.

+
+
+

Integer array indexing#

+

Integer array indexing allows selection of arbitrary items in the array +based on their N-dimensional index. Each integer array represents a number +of indices into that dimension.

+

Negative values are permitted in the index arrays and work as they do with +single indices or slices:

+
>>> x = np.arange(10, 1, -1)
+>>> x
+array([10,  9,  8,  7,  6,  5,  4,  3,  2])
+>>> x[np.array([3, 3, 1, 8])]
+array([7, 7, 9, 2])
+>>> x[np.array([3, 3, -3, 8])]
+array([7, 7, 4, 2])
+
+
+

If the index values are out of bounds then an IndexError is thrown:

+
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
+>>> x[np.array([1, -1])]
+array([[3, 4],
+      [5, 6]])
+>>> x[np.array([3, 4])]
+Traceback (most recent call last):
+  ...
+IndexError: index 3 is out of bounds for axis 0 with size 3
+
+
+

When the index consists of as many integer arrays as dimensions of the array +being indexed, the indexing is straightforward, but different from slicing.

+

Advanced indices always are broadcast and +iterated as one:

+
result[i_1, ..., i_M] == x[ind_1[i_1, ..., i_M], ind_2[i_1, ..., i_M],
+                           ..., ind_N[i_1, ..., i_M]]
+
+
+

Note that the resulting shape is identical to the (broadcast) indexing array +shapes ind_1, ..., ind_N. If the indices cannot be broadcast to the +same shape, an exception IndexError: shape mismatch: indexing arrays could +not be broadcast together with shapes... is raised.

+

Indexing with multidimensional index arrays tend +to be more unusual uses, but they are permitted, and they are useful for some +problems. We’ll start with the simplest multidimensional case:

+
>>> y = np.arange(35).reshape(5, 7)
+>>> y
+array([[ 0,  1,  2,  3,  4,  5,  6],
+       [ 7,  8,  9, 10, 11, 12, 13],
+       [14, 15, 16, 17, 18, 19, 20],
+       [21, 22, 23, 24, 25, 26, 27],
+       [28, 29, 30, 31, 32, 33, 34]])
+>>> y[np.array([0, 2, 4]), np.array([0, 1, 2])]
+array([ 0, 15, 30])
+
+
+

In this case, if the index arrays have a matching shape, and there is an +index array for each dimension of the array being indexed, the resultant +array has the same shape as the index arrays, and the values correspond +to the index set for each position in the index arrays. In this example, +the first index value is 0 for both index arrays, and thus the first value +of the resultant array is y[0, 0]. The next value is y[2, 1], and +the last is y[4, 2].

+

If the index arrays do not have the same shape, there is an attempt to +broadcast them to the same shape. If they cannot be broadcast to the same +shape, an exception is raised:

+
>>> y[np.array([0, 2, 4]), np.array([0, 1])]
+Traceback (most recent call last):
+  ...
+IndexError: shape mismatch: indexing arrays could not be broadcast
+together with shapes (3,) (2,)
+
+
+

The broadcasting mechanism permits index arrays to be combined with +scalars for other indices. The effect is that the scalar value is used +for all the corresponding values of the index arrays:

+
>>> y[np.array([0, 2, 4]), 1]
+array([ 1, 15, 29])
+
+
+

Jumping to the next level of complexity, it is possible to only partially +index an array with index arrays. It takes a bit of thought to understand +what happens in such cases. For example if we just use one index array +with y:

+
>>> y[np.array([0, 2, 4])]
+array([[ 0,  1,  2,  3,  4,  5,  6],
+      [14, 15, 16, 17, 18, 19, 20],
+      [28, 29, 30, 31, 32, 33, 34]])
+
+
+

It results in the construction of a new array where each value of the +index array selects one row from the array being indexed and the resultant +array has the resulting shape (number of index elements, size of row).

+

In general, the shape of the resultant array will be the concatenation of +the shape of the index array (or the shape that all the index arrays were +broadcast to) with the shape of any unused dimensions (those not indexed) +in the array being indexed.

+

Example

+

From each row, a specific element should be selected. The row index is just +[0, 1, 2] and the column index specifies the element to choose for the +corresponding row, here [0, 1, 0]. Using both together the task +can be solved using advanced indexing:

+
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
+>>> x[[0, 1, 2], [0, 1, 0]]
+array([1, 4, 5])
+
+
+

To achieve a behaviour similar to the basic slicing above, broadcasting can be +used. The function ix_ can help with this broadcasting. This is best +understood with an example.

+

Example

+

From a 4x3 array the corner elements should be selected using advanced +indexing. Thus all elements for which the column is one of [0, 2] and +the row is one of [0, 3] need to be selected. To use advanced indexing +one needs to select all elements explicitly. Using the method explained +previously one could write:

+
>>> x = np.array([[ 0,  1,  2],
+...               [ 3,  4,  5],
+...               [ 6,  7,  8],
+...               [ 9, 10, 11]])
+>>> rows = np.array([[0, 0],
+...                  [3, 3]], dtype=np.intp)
+>>> columns = np.array([[0, 2],
+...                     [0, 2]], dtype=np.intp)
+>>> x[rows, columns]
+array([[ 0,  2],
+       [ 9, 11]])
+
+
+

However, since the indexing arrays above just repeat themselves, +broadcasting can be used (compare operations such as +rows[:, np.newaxis] + columns) to simplify this:

+
>>> rows = np.array([0, 3], dtype=np.intp)
+>>> columns = np.array([0, 2], dtype=np.intp)
+>>> rows[:, np.newaxis]
+array([[0],
+       [3]])
+>>> x[rows[:, np.newaxis], columns]
+array([[ 0,  2],
+       [ 9, 11]])
+
+
+

This broadcasting can also be achieved using the function ix_:

+
>>> x[np.ix_(rows, columns)]
+array([[ 0,  2],
+       [ 9, 11]])
+
+
+

Note that without the np.ix_ call, only the diagonal elements would +be selected:

+
>>> x[rows, columns]
+array([ 0, 11])
+
+
+

This difference is the most important thing to remember about +indexing with multiple advanced indices.

+

Example

+

A real-life example of where advanced indexing may be useful is for a color +lookup table where we want to map the values of an image into RGB triples for +display. The lookup table could have a shape (nlookup, 3). Indexing +such an array with an image with shape (ny, nx) with dtype=np.uint8 +(or any integer type so long as values are with the bounds of the +lookup table) will result in an array of shape (ny, nx, 3) where a +triple of RGB values is associated with each pixel location.

+
+
+

Boolean array indexing#

+

This advanced indexing occurs when obj is an array object of Boolean +type, such as may be returned from comparison operators. A single +boolean index array is practically identical to x[obj.nonzero()] where, +as described above, obj.nonzero() returns a +tuple (of length obj.ndim) of integer index +arrays showing the True elements of obj. However, it is +faster when obj.shape == x.shape.

+

If obj.ndim == x.ndim, x[obj] returns a 1-dimensional array +filled with the elements of x corresponding to the True +values of obj. The search order will be row-major, +C-style. If obj has True values at entries that are outside +of the bounds of x, then an index error will be raised. If obj is +smaller than x it is identical to filling it with False.

+

A common use case for this is filtering for desired element values. +For example, one may wish to select all entries from an array which +are not NaN:

+
>>> x = np.array([[1., 2.], [np.nan, 3.], [np.nan, np.nan]])
+>>> x[~np.isnan(x)]
+array([1., 2., 3.])
+
+
+

Or wish to add a constant to all negative elements:

+
>>> x = np.array([1., -1., -2., 3])
+>>> x[x < 0] += 20
+>>> x
+array([ 1., 19., 18., 3.])
+
+
+

In general if an index includes a Boolean array, the result will be +identical to inserting obj.nonzero() into the same position +and using the integer array indexing mechanism described above. +x[ind_1, boolean_array, ind_2] is equivalent to +x[(ind_1,) + boolean_array.nonzero() + (ind_2,)].

+

If there is only one Boolean array and no integer indexing array present, +this is straightforward. Care must only be taken to make sure that the +boolean index has exactly as many dimensions as it is supposed to work +with.

+

In general, when the boolean array has fewer dimensions than the array being +indexed, this is equivalent to x[b, ...], which means x is indexed by b +followed by as many : as are needed to fill out the rank of x. Thus the +shape of the result is one dimension containing the number of True elements of +the boolean array, followed by the remaining dimensions of the array being +indexed:

+
>>> x = np.arange(35).reshape(5, 7)
+>>> b = x > 20
+>>> b[:, 5]
+array([False, False, False,  True,  True])
+>>> x[b[:, 5]]
+array([[21, 22, 23, 24, 25, 26, 27],
+      [28, 29, 30, 31, 32, 33, 34]])
+
+
+

Here the 4th and 5th rows are selected from the indexed array and +combined to make a 2-D array.

+

Example

+

From an array, select all rows which sum up to less or equal two:

+
>>> x = np.array([[0, 1], [1, 1], [2, 2]])
+>>> rowsum = x.sum(-1)
+>>> x[rowsum <= 2, :]
+array([[0, 1],
+       [1, 1]])
+
+
+

Combining multiple Boolean indexing arrays or a Boolean with an integer +indexing array can best be understood with the +obj.nonzero() analogy. The function ix_ +also supports boolean arrays and will work without any surprises.

+

Example

+

Use boolean indexing to select all rows adding up to an even +number. At the same time columns 0 and 2 should be selected with an +advanced integer index. Using the ix_ function this can be done +with:

+
>>> x = np.array([[ 0,  1,  2],
+...               [ 3,  4,  5],
+...               [ 6,  7,  8],
+...               [ 9, 10, 11]])
+>>> rows = (x.sum(-1) % 2) == 0
+>>> rows
+array([False,  True, False,  True])
+>>> columns = [0, 2]
+>>> x[np.ix_(rows, columns)]
+array([[ 3,  5],
+       [ 9, 11]])
+
+
+

Without the np.ix_ call, only the diagonal elements would be +selected.

+

Or without np.ix_ (compare the integer array examples):

+
>>> rows = rows.nonzero()[0]
+>>> x[rows[:, np.newaxis], columns]
+array([[ 3,  5],
+       [ 9, 11]])
+
+
+

Example

+

Use a 2-D boolean array of shape (2, 3) +with four True elements to select rows from a 3-D array of shape +(2, 3, 5) results in a 2-D result of shape (4, 5):

+
>>> x = np.arange(30).reshape(2, 3, 5)
+>>> x
+array([[[ 0,  1,  2,  3,  4],
+        [ 5,  6,  7,  8,  9],
+        [10, 11, 12, 13, 14]],
+      [[15, 16, 17, 18, 19],
+        [20, 21, 22, 23, 24],
+        [25, 26, 27, 28, 29]]])
+>>> b = np.array([[True, True, False], [False, True, True]])
+>>> x[b]
+array([[ 0,  1,  2,  3,  4],
+      [ 5,  6,  7,  8,  9],
+      [20, 21, 22, 23, 24],
+      [25, 26, 27, 28, 29]])
+
+
+
+
+

Combining advanced and basic indexing#

+

When there is at least one slice (:), ellipsis (...) or newaxis +in the index (or the array has more dimensions than there are advanced indices), +then the behaviour can be more complicated. It is like concatenating the +indexing result for each advanced index element.

+

In the simplest case, there is only a single advanced index combined with +a slice. For example:

+
>>> y = np.arange(35).reshape(5,7)
+>>> y[np.array([0, 2, 4]), 1:3]
+array([[ 1,  2],
+       [15, 16],
+       [29, 30]])
+
+
+

In effect, the slice and index array operation are independent. The slice +operation extracts columns with index 1 and 2, (i.e. the 2nd and 3rd columns), +followed by the index array operation which extracts rows with index 0, 2 and 4 +(i.e the first, third and fifth rows). This is equivalent to:

+
>>> y[:, 1:3][np.array([0, 2, 4]), :]
+array([[ 1,  2],
+       [15, 16],
+       [29, 30]])
+
+
+

A single advanced index can, for example, replace a slice and the result array +will be the same. However, it is a copy and may have a different memory layout. +A slice is preferable when it is possible. +For example:

+
>>> x = np.array([[ 0,  1,  2],
+...               [ 3,  4,  5],
+...               [ 6,  7,  8],
+...               [ 9, 10, 11]])
+>>> x[1:2, 1:3]
+array([[4, 5]])
+>>> x[1:2, [1, 2]]
+array([[4, 5]])
+
+
+

The easiest way to understand a combination of multiple advanced indices may +be to think in terms of the resulting shape. There are two parts to the indexing +operation, the subspace defined by the basic indexing (excluding integers) and +the subspace from the advanced indexing part. Two cases of index combination +need to be distinguished:

+
    +
  • The advanced indices are separated by a slice, Ellipsis or +newaxis. For example x[arr1, :, arr2].

  • +
  • The advanced indices are all next to each other. +For example x[..., arr1, arr2, :] but not x[arr1, :, 1] +since 1 is an advanced index in this regard.

  • +
+

In the first case, the dimensions resulting from the advanced indexing +operation come first in the result array, and the subspace dimensions after +that. +In the second case, the dimensions from the advanced indexing operations +are inserted into the result array at the same spot as they were in the +initial array (the latter logic is what makes simple advanced indexing +behave just like slicing).

+

Example

+

Suppose x.shape is (10, 20, 30) and ind is a (2, 3, 4)-shaped +indexing intp array, then result = x[..., ind, :] has +shape (10, 2, 3, 4, 30) because the (20,)-shaped subspace has been +replaced with a (2, 3, 4)-shaped broadcasted indexing subspace. If +we let i, j, k loop over the (2, 3, 4)-shaped subspace then +result[..., i, j, k, :] = x[..., ind[i, j, k], :]. This example +produces the same result as x.take(ind, axis=-2).

+

Example

+

Let x.shape be (10, 20, 30, 40, 50) and suppose ind_1 +and ind_2 can be broadcast to the shape (2, 3, 4). Then +x[:, ind_1, ind_2] has shape (10, 2, 3, 4, 40, 50) because the +(20, 30)-shaped subspace from X has been replaced with the +(2, 3, 4) subspace from the indices. However, +x[:, ind_1, :, ind_2] has shape (2, 3, 4, 10, 30, 50) because there +is no unambiguous place to drop in the indexing subspace, thus +it is tacked-on to the beginning. It is always possible to use +.transpose() to move the subspace +anywhere desired. Note that this example cannot be replicated +using take.

+

Example

+

Slicing can be combined with broadcasted boolean indices:

+
>>> x = np.arange(35).reshape(5, 7)
+>>> b = x > 20
+>>> b
+array([[False, False, False, False, False, False, False],
+      [False, False, False, False, False, False, False],
+      [False, False, False, False, False, False, False],
+      [ True,  True,  True,  True,  True,  True,  True],
+      [ True,  True,  True,  True,  True,  True,  True]])
+>>> x[b[:, 5], 1:3]
+array([[22, 23],
+      [29, 30]])
+
+
+
+
+
+

Field access#

+
+

See also

+

Structured arrays

+
+

If the ndarray object is a structured array the fields +of the array can be accessed by indexing the array with strings, +dictionary-like.

+

Indexing x['field-name'] returns a new view to the array, +which is of the same shape as x (except when the field is a +sub-array) but of data type x.dtype['field-name'] and contains +only the part of the data in the specified field. Also, +record array scalars can be “indexed” this way.

+

Indexing into a structured array can also be done with a list of field names, +e.g. x[['field-name1', 'field-name2']]. As of NumPy 1.16, this returns a +view containing only those fields. In older versions of NumPy, it returned a +copy. See the user guide section on Structured arrays for more +information on multifield indexing.

+

If the accessed field is a sub-array, the dimensions of the sub-array +are appended to the shape of the result. +For example:

+
>>> x = np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))])
+>>> x['a'].shape
+(2, 2)
+>>> x['a'].dtype
+dtype('int32')
+>>> x['b'].shape
+(2, 2, 3, 3)
+>>> x['b'].dtype
+dtype('float64')
+
+
+
+
+

Flat Iterator indexing#

+

x.flat returns an iterator that will iterate +over the entire array (in C-contiguous style with the last index +varying the fastest). This iterator object can also be indexed using +basic slicing or advanced indexing as long as the selection object is +not a tuple. This should be clear from the fact that x.flat is a 1-dimensional view. It can be used for integer +indexing with 1-dimensional C-style-flat indices. The shape of any +returned array is therefore the shape of the integer indexing object.

+
+
+

Assigning values to indexed arrays#

+

As mentioned, one can select a subset of an array to assign to using +a single index, slices, and index and mask arrays. The value being +assigned to the indexed array must be shape consistent (the same shape +or broadcastable to the shape the index produces). For example, it is +permitted to assign a constant to a slice:

+
>>> x = np.arange(10)
+>>> x[2:7] = 1
+
+
+

or an array of the right size:

+
>>> x[2:7] = np.arange(5)
+
+
+

Note that assignments may result in changes if assigning +higher types to lower types (like floats to ints) or even +exceptions (assigning complex to floats or ints):

+
>>> x[1] = 1.2
+>>> x[1]
+1
+>>> x[1] = 1.2j
+Traceback (most recent call last):
+  ...
+TypeError: can't convert complex to int
+
+
+

Unlike some of the references (such as array and mask indices) +assignments are always made to the original data in the array +(indeed, nothing else would make sense!). Note though, that some +actions may not work as one may naively expect. This particular +example is often surprising to people:

+
>>> x = np.arange(0, 50, 10)
+>>> x
+array([ 0, 10, 20, 30, 40])
+>>> x[np.array([1, 1, 3, 1])] += 1
+>>> x
+array([ 0, 11, 20, 31, 40])
+
+
+

Where people expect that the 1st location will be incremented by 3. +In fact, it will only be incremented by 1. The reason is that +a new array is extracted from the original (as a temporary) containing +the values at 1, 1, 3, 1, then the value 1 is added to the temporary, +and then the temporary is assigned back to the original array. Thus +the value of the array at x[1] + 1 is assigned to x[1] three times, +rather than being incremented 3 times.

+
+
+

Dealing with variable numbers of indices within programs#

+

The indexing syntax is very powerful but limiting when dealing with +a variable number of indices. For example, if you want to write +a function that can handle arguments with various numbers of +dimensions without having to write special case code for each +number of possible dimensions, how can that be done? If one +supplies to the index a tuple, the tuple will be interpreted +as a list of indices. For example:

+
>>> z = np.arange(81).reshape(3, 3, 3, 3)
+>>> indices = (1, 1, 1, 1)
+>>> z[indices]
+40
+
+
+

So one can use code to construct tuples of any number of indices +and then use these within an index.

+

Slices can be specified within programs by using the slice() function +in Python. For example:

+
>>> indices = (1, 1, 1, slice(0, 2))  # same as [1, 1, 1, 0:2]
+>>> z[indices]
+array([39, 40])
+
+
+

Likewise, ellipsis can be specified by code by using the Ellipsis +object:

+
>>> indices = (1, Ellipsis, 1)  # same as [1, ..., 1]
+>>> z[indices]
+array([[28, 31, 34],
+       [37, 40, 43],
+       [46, 49, 52]])
+
+
+

For this reason, it is possible to use the output from the +np.nonzero() function directly as an index since +it always returns a tuple of index arrays.

+

Because of the special treatment of tuples, they are not automatically +converted to an array as a list would be. As an example:

+
>>> z[[1, 1, 1, 1]]  # produces a large array
+array([[[[27, 28, 29],
+         [30, 31, 32], ...
+>>> z[(1, 1, 1, 1)]  # returns a single value
+40
+
+
+
+
+

Detailed notes#

+

These are some detailed notes, which are not of importance for day to day +indexing (in no particular order):

+
    +
  • The native NumPy indexing type is intp and may differ from the +default integer array type. intp is the smallest data type +sufficient to safely index any array; for advanced indexing it may be +faster than other types.

  • +
  • For advanced assignments, there is in general no guarantee for the +iteration order. This means that if an element is set more than once, +it is not possible to predict the final result.

  • +
  • An empty (tuple) index is a full scalar index into a zero-dimensional array. +x[()] returns a scalar if x is zero-dimensional and a view +otherwise. On the other hand, x[...] always returns a view.

  • +
  • If a zero-dimensional array is present in the index and it is a full +integer index the result will be a scalar and not a zero-dimensional array. +(Advanced indexing is not triggered.)

  • +
  • When an ellipsis (...) is present but has no size (i.e. replaces zero +:) the result will still always be an array. A view if no advanced index +is present, otherwise a copy.

  • +
  • The nonzero equivalence for Boolean arrays does not hold for zero +dimensional boolean arrays.

  • +
  • When the result of an advanced indexing operation has no elements but an +individual index is out of bounds, whether or not an IndexError is +raised is undefined (e.g. x[[], [123]] with 123 being out of bounds).

  • +
  • When a casting error occurs during assignment (for example updating a +numerical array using a sequence of strings), the array being assigned +to may end up in an unpredictable partially updated state. +However, if any other error (such as an out of bounds index) occurs, the +array will remain unchanged.

  • +
  • The memory layout of an advanced indexing result is optimized for each +indexing operation and no particular memory order can be assumed.

  • +
  • When using a subclass (especially one which manipulates its shape), the +default ndarray.__setitem__ behaviour will call __getitem__ for +basic indexing but not for advanced indexing. For such a subclass it may +be preferable to call ndarray.__setitem__ with a base class ndarray +view on the data. This must be done if the subclasses __getitem__ does +not return views.

  • +
+
+
+ + +
+ + + + + +
+ + +
+
+ + + +
+
+ + + + + +
+
+ + \ No newline at end of file diff --git a/tests/integrated/test-boutpp/slicing/basics.indexing.txt b/tests/integrated/test-boutpp/slicing/basics.indexing.txt new file mode 100644 index 0000000000..eba782d5e2 --- /dev/null +++ b/tests/integrated/test-boutpp/slicing/basics.indexing.txt @@ -0,0 +1,687 @@ + +logo + + User Guide + API reference + Development + Release notes + Learn + + GitHub + Twitter + + What is NumPy? + Installation + NumPy quickstart + NumPy: the absolute basics for beginners + NumPy fundamentals + Array creation + Indexing on ndarrays + I/O with NumPy + Data types + Broadcasting + Byte-swapping + Structured arrays + Writing custom array containers + Subclassing ndarray + Universal functions ( ufunc ) basics + Copies and views + Interoperability with NumPy + Miscellaneous + NumPy for MATLAB users + Building from source + Using NumPy C-API + NumPy Tutorials + NumPy How Tos + For downstream package authors + + F2PY user guide and reference manual + Glossary + Under-the-hood Documentation for developers + Reporting bugs + Release notes + NumPy license + +On this page + + Basic indexing + Single element indexing + Slicing and striding + Dimensional indexing tools + Advanced indexing + Field access + Flat Iterator indexing + Assigning values to indexed arrays + Dealing with variable numbers of indices within programs + Detailed notes + +Indexing on ndarrays + +See also + +Indexing routines + +ndarrays can be indexed using the standard Python x[obj] syntax, where x is the array and obj the selection. There are different kinds of indexing available depending on obj: basic indexing, advanced indexing and field access. + +Most of the following examples show the use of indexing when referencing data in an array. The examples work just as well when assigning to an array. See Assigning values to indexed arrays for specific examples and explanations on how assignments work. + +Note that in Python, x[(exp1, exp2, ..., expN)] is equivalent to x[exp1, exp2, ..., expN]; the latter is just syntactic sugar for the former. +Basic indexing +Single element indexing + +Single element indexing works exactly like that for other standard Python sequences. It is 0-based, and accepts negative indices for indexing from the end of the array. + +x = np.arange(10) + +x[2] +2 + +x[-2] +8 + +It is not necessary to separate each dimension’s index into its own set of square brackets. + +x.shape = (2, 5) # now x is 2-dimensional + +x[1, 3] +8 + +x[1, -1] +9 + +Note that if one indexes a multidimensional array with fewer indices than dimensions, one gets a subdimensional array. For example: + +x[0] +array([0, 1, 2, 3, 4]) + +That is, each index specified selects the array corresponding to the rest of the dimensions selected. In the above example, choosing 0 means that the remaining dimension of length 5 is being left unspecified, and that what is returned is an array of that dimensionality and size. It must be noted that the returned array is a view, i.e., it is not a copy of the original, but points to the same values in memory as does the original array. In this case, the 1-D array at the first position (0) is returned. So using a single index on the returned array, results in a single element being returned. That is: + +x[0][2] +2 + +So note that x[0, 2] == x[0][2] though the second case is more inefficient as a new temporary array is created after the first index that is subsequently indexed by 2. + +Note + +NumPy uses C-order indexing. That means that the last index usually represents the most rapidly changing memory location, unlike Fortran or IDL, where the first index represents the most rapidly changing location in memory. This difference represents a great potential for confusion. +Slicing and striding + +Basic slicing extends Python’s basic concept of slicing to N dimensions. Basic slicing occurs when obj is a slice object (constructed by start:stop:step notation inside of brackets), an integer, or a tuple of slice objects and integers. Ellipsis and newaxis objects can be interspersed with these as well. + +The simplest case of indexing with N integers returns an array scalar representing the corresponding item. As in Python, all indices are zero-based: for the i-th index +, the valid range is where is the i-th element of the shape of the array. Negative indices are interpreted as counting from the end of the array (i.e., if , it means + +). + +All arrays generated by basic slicing are always views of the original array. + +Note + +NumPy slicing creates a view instead of a copy as in the case of built-in Python sequences such as string, tuple and list. Care must be taken when extracting a small portion from a large array which becomes useless after the extraction, because the small portion extracted contains a reference to the large original array whose memory will not be released until all arrays derived from it are garbage-collected. In such cases an explicit copy() is recommended. + +The standard rules of sequence slicing apply to basic slicing on a per-dimension basis (including using a step index). Some useful concepts to remember include: + + The basic slice syntax is i:j:k where i is the starting index, j is the stopping index, and k is the step ( + +). This selects the m elements (in the corresponding dimension) with index values i, i + k, …, i + (m - 1) k where + +and q and r are the quotient and remainder obtained by dividing j - i by k: j - i = q k + r, so that i + (m - 1) k < j. For example: + +x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + +x[1:7:2] +array([1, 3, 5]) + +Negative i and j are interpreted as n + i and n + j where n is the number of elements in the corresponding dimension. Negative k makes stepping go towards smaller indices. From the above example: + +x[-2:10] +array([8, 9]) + +x[-3:3:-1] +array([7, 6, 5, 4]) + +Assume n is the number of elements in the dimension being sliced. Then, if i is not given it defaults to 0 for k > 0 and n - 1 for k < 0 . If j is not given it defaults to n for k > 0 and -n-1 for k < 0 . If k is not given it defaults to 1. Note that :: is the same as : and means select all indices along this axis. From the above example: + +x[5:] +array([5, 6, 7, 8, 9]) + +If the number of objects in the selection tuple is less than N, then : is assumed for any subsequent dimensions. For example: + +x = np.array([[[1],[2],[3]], [[4],[5],[6]]]) + +x.shape +(2, 3, 1) + + x[1:2] + array([[[4], + [5], + [6]]]) + + An integer, i, returns the same values as i:i+1 except the dimensionality of the returned object is reduced by 1. In particular, a selection tuple with the p-th element an integer (and all other entries :) returns the corresponding sub-array with dimension N - 1. If N = 1 then the returned object is an array scalar. These objects are explained in Scalars. + + If the selection tuple has all entries : except the p-th entry which is a slice object i:j:k, then the returned array has dimension N formed by concatenating the sub-arrays returned by integer indexing of elements i, i+k, …, i + (m - 1) k < j, + + Basic slicing with more than one non-: entry in the slicing tuple, acts like repeated application of slicing using a single non-: entry, where the non-: entries are successively taken (with all other non-: entries replaced by :). Thus, x[ind1, ..., ind2,:] acts like x[ind1][..., ind2, :] under basic slicing. + + Warning + + The above is not true for advanced indexing. + + You may use slicing to set values in the array, but (unlike lists) you can never grow the array. The size of the value to be set in x[obj] = value must be (broadcastable) to the same shape as x[obj]. + + A slicing tuple can always be constructed as obj and used in the x[obj] notation. Slice objects can be used in the construction in place of the [start:stop:step] notation. For example, x[1:10:5, ::-1] can also be implemented as obj = (slice(1, 10, 5), slice(None, None, -1)); x[obj] . This can be useful for constructing generic code that works on arrays of arbitrary dimensions. See Dealing with variable numbers of indices within programs for more information. + +Dimensional indexing tools + +There are some tools to facilitate the easy matching of array shapes with expressions and in assignments. + +Ellipsis expands to the number of : objects needed for the selection tuple to index all dimensions. In most cases, this means that the length of the expanded selection tuple is x.ndim. There may only be a single ellipsis present. From the above example: + +x[..., 0] +array([[1, 2, 3], + [4, 5, 6]]) + +This is equivalent to: + +x[:, :, 0] +array([[1, 2, 3], + [4, 5, 6]]) + +Each newaxis object in the selection tuple serves to expand the dimensions of the resulting selection by one unit-length dimension. The added dimension is the position of the newaxis object in the selection tuple. newaxis is an alias for None, and None can be used in place of this with the same result. From the above example: + +x[:, np.newaxis, :, :].shape +(2, 1, 3, 1) + +x[:, None, :, :].shape +(2, 1, 3, 1) + +This can be handy to combine two arrays in a way that otherwise would require explicit reshaping operations. For example: + +x = np.arange(5) + +x[:, np.newaxis] + x[np.newaxis, :] +array([[0, 1, 2, 3, 4], + [1, 2, 3, 4, 5], + [2, 3, 4, 5, 6], + [3, 4, 5, 6, 7], + [4, 5, 6, 7, 8]]) + +Advanced indexing + +Advanced indexing is triggered when the selection object, obj, is a non-tuple sequence object, an ndarray (of data type integer or bool), or a tuple with at least one sequence object or ndarray (of data type integer or bool). There are two types of advanced indexing: integer and Boolean. + +Advanced indexing always returns a copy of the data (contrast with basic slicing that returns a view). + +Warning + +The definition of advanced indexing means that x[(1, 2, 3),] is fundamentally different than x[(1, 2, 3)]. The latter is equivalent to x[1, 2, 3] which will trigger basic selection while the former will trigger advanced indexing. Be sure to understand why this occurs. + +Also recognize that x[[1, 2, 3]] will trigger advanced indexing, whereas due to the deprecated Numeric compatibility mentioned above, x[[1, 2, slice(None)]] will trigger basic slicing. +Integer array indexing + +Integer array indexing allows selection of arbitrary items in the array based on their N-dimensional index. Each integer array represents a number of indices into that dimension. + +Negative values are permitted in the index arrays and work as they do with single indices or slices: + +x = np.arange(10, 1, -1) + +x +array([10, 9, 8, 7, 6, 5, 4, 3, 2]) + +x[np.array([3, 3, 1, 8])] +array([7, 7, 9, 2]) + +x[np.array([3, 3, -3, 8])] +array([7, 7, 4, 2]) + +If the index values are out of bounds then an IndexError is thrown: + +x = np.array([[1, 2], [3, 4], [5, 6]]) + +x[np.array([1, -1])] +array([[3, 4], + [5, 6]]) + +x[np.array([3, 4])] +Traceback (most recent call last): + ... +IndexError: index 3 is out of bounds for axis 0 with size 3 + +When the index consists of as many integer arrays as dimensions of the array being indexed, the indexing is straightforward, but different from slicing. + +Advanced indices always are broadcast and iterated as one: + +result[i_1, ..., i_M] == x[ind_1[i_1, ..., i_M], ind_2[i_1, ..., i_M], + ..., ind_N[i_1, ..., i_M]] + +Note that the resulting shape is identical to the (broadcast) indexing array shapes ind_1, ..., ind_N. If the indices cannot be broadcast to the same shape, an exception IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes... is raised. + +Indexing with multidimensional index arrays tend to be more unusual uses, but they are permitted, and they are useful for some problems. We’ll start with the simplest multidimensional case: + +y = np.arange(35).reshape(5, 7) + +y +array([[ 0, 1, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13], + [14, 15, 16, 17, 18, 19, 20], + [21, 22, 23, 24, 25, 26, 27], + [28, 29, 30, 31, 32, 33, 34]]) + +y[np.array([0, 2, 4]), np.array([0, 1, 2])] +array([ 0, 15, 30]) + +In this case, if the index arrays have a matching shape, and there is an index array for each dimension of the array being indexed, the resultant array has the same shape as the index arrays, and the values correspond to the index set for each position in the index arrays. In this example, the first index value is 0 for both index arrays, and thus the first value of the resultant array is y[0, 0]. The next value is y[2, 1], and the last is y[4, 2]. + +If the index arrays do not have the same shape, there is an attempt to broadcast them to the same shape. If they cannot be broadcast to the same shape, an exception is raised: + +y[np.array([0, 2, 4]), np.array([0, 1])] +Traceback (most recent call last): + ... +IndexError: shape mismatch: indexing arrays could not be broadcast +together with shapes (3,) (2,) + +The broadcasting mechanism permits index arrays to be combined with scalars for other indices. The effect is that the scalar value is used for all the corresponding values of the index arrays: + +y[np.array([0, 2, 4]), 1] +array([ 1, 15, 29]) + +Jumping to the next level of complexity, it is possible to only partially index an array with index arrays. It takes a bit of thought to understand what happens in such cases. For example if we just use one index array with y: + +y[np.array([0, 2, 4])] +array([[ 0, 1, 2, 3, 4, 5, 6], + [14, 15, 16, 17, 18, 19, 20], + [28, 29, 30, 31, 32, 33, 34]]) + +It results in the construction of a new array where each value of the index array selects one row from the array being indexed and the resultant array has the resulting shape (number of index elements, size of row). + +In general, the shape of the resultant array will be the concatenation of the shape of the index array (or the shape that all the index arrays were broadcast to) with the shape of any unused dimensions (those not indexed) in the array being indexed. + +Example + +From each row, a specific element should be selected. The row index is just [0, 1, 2] and the column index specifies the element to choose for the corresponding row, here [0, 1, 0]. Using both together the task can be solved using advanced indexing: + +x = np.array([[1, 2], [3, 4], [5, 6]]) + +x[[0, 1, 2], [0, 1, 0]] +array([1, 4, 5]) + +To achieve a behaviour similar to the basic slicing above, broadcasting can be used. The function ix_ can help with this broadcasting. This is best understood with an example. + +Example + +From a 4x3 array the corner elements should be selected using advanced indexing. Thus all elements for which the column is one of [0, 2] and the row is one of [0, 3] need to be selected. To use advanced indexing one needs to select all elements explicitly. Using the method explained previously one could write: + +x = np.array([[ 0, 1, 2], + + [ 3, 4, 5], + + [ 6, 7, 8], + + [ 9, 10, 11]]) + +rows = np.array([[0, 0], + + [3, 3]], dtype=np.intp) + +columns = np.array([[0, 2], + + [0, 2]], dtype=np.intp) + +x[rows, columns] +array([[ 0, 2], + [ 9, 11]]) + +However, since the indexing arrays above just repeat themselves, broadcasting can be used (compare operations such as rows[:, np.newaxis] + columns) to simplify this: + +rows = np.array([0, 3], dtype=np.intp) + +columns = np.array([0, 2], dtype=np.intp) + +rows[:, np.newaxis] +array([[0], + [3]]) + +x[rows[:, np.newaxis], columns] +array([[ 0, 2], + [ 9, 11]]) + +This broadcasting can also be achieved using the function ix_: + +x[np.ix_(rows, columns)] +array([[ 0, 2], + [ 9, 11]]) + +Note that without the np.ix_ call, only the diagonal elements would be selected: + +x[rows, columns] +array([ 0, 11]) + +This difference is the most important thing to remember about indexing with multiple advanced indices. + +Example + +A real-life example of where advanced indexing may be useful is for a color lookup table where we want to map the values of an image into RGB triples for display. The lookup table could have a shape (nlookup, 3). Indexing such an array with an image with shape (ny, nx) with dtype=np.uint8 (or any integer type so long as values are with the bounds of the lookup table) will result in an array of shape (ny, nx, 3) where a triple of RGB values is associated with each pixel location. +Boolean array indexing + +This advanced indexing occurs when obj is an array object of Boolean type, such as may be returned from comparison operators. A single boolean index array is practically identical to x[obj.nonzero()] where, as described above, obj.nonzero() returns a tuple (of length obj.ndim) of integer index arrays showing the True elements of obj. However, it is faster when obj.shape == x.shape. + +If obj.ndim == x.ndim, x[obj] returns a 1-dimensional array filled with the elements of x corresponding to the True values of obj. The search order will be row-major, C-style. If obj has True values at entries that are outside of the bounds of x, then an index error will be raised. If obj is smaller than x it is identical to filling it with False. + +A common use case for this is filtering for desired element values. For example, one may wish to select all entries from an array which are not NaN: + +x = np.array([[1., 2.], [np.nan, 3.], [np.nan, np.nan]]) + +x[~np.isnan(x)] +array([1., 2., 3.]) + +Or wish to add a constant to all negative elements: + +x = np.array([1., -1., -2., 3]) + +x[x < 0] += 20 + +x +array([ 1., 19., 18., 3.]) + +In general if an index includes a Boolean array, the result will be identical to inserting obj.nonzero() into the same position and using the integer array indexing mechanism described above. x[ind_1, boolean_array, ind_2] is equivalent to x[(ind_1,) + boolean_array.nonzero() + (ind_2,)]. + +If there is only one Boolean array and no integer indexing array present, this is straightforward. Care must only be taken to make sure that the boolean index has exactly as many dimensions as it is supposed to work with. + +In general, when the boolean array has fewer dimensions than the array being indexed, this is equivalent to x[b, ...], which means x is indexed by b followed by as many : as are needed to fill out the rank of x. Thus the shape of the result is one dimension containing the number of True elements of the boolean array, followed by the remaining dimensions of the array being indexed: + +x = np.arange(35).reshape(5, 7) + +b = x > 20 + +b[:, 5] +array([False, False, False, True, True]) + +x[b[:, 5]] +array([[21, 22, 23, 24, 25, 26, 27], + [28, 29, 30, 31, 32, 33, 34]]) + +Here the 4th and 5th rows are selected from the indexed array and combined to make a 2-D array. + +Example + +From an array, select all rows which sum up to less or equal two: + +x = np.array([[0, 1], [1, 1], [2, 2]]) + +rowsum = x.sum(-1) + +x[rowsum <= 2, :] +array([[0, 1], + [1, 1]]) + +Combining multiple Boolean indexing arrays or a Boolean with an integer indexing array can best be understood with the obj.nonzero() analogy. The function ix_ also supports boolean arrays and will work without any surprises. + +Example + +Use boolean indexing to select all rows adding up to an even number. At the same time columns 0 and 2 should be selected with an advanced integer index. Using the ix_ function this can be done with: + +x = np.array([[ 0, 1, 2], + + [ 3, 4, 5], + + [ 6, 7, 8], + + [ 9, 10, 11]]) + +rows = (x.sum(-1) % 2) == 0 + +rows +array([False, True, False, True]) + +columns = [0, 2] + +x[np.ix_(rows, columns)] +array([[ 3, 5], + [ 9, 11]]) + +Without the np.ix_ call, only the diagonal elements would be selected. + +Or without np.ix_ (compare the integer array examples): + +rows = rows.nonzero()[0] + +x[rows[:, np.newaxis], columns] +array([[ 3, 5], + [ 9, 11]]) + +Example + +Use a 2-D boolean array of shape (2, 3) with four True elements to select rows from a 3-D array of shape (2, 3, 5) results in a 2-D result of shape (4, 5): + +x = np.arange(30).reshape(2, 3, 5) + +x +array([[[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]], + [[15, 16, 17, 18, 19], + [20, 21, 22, 23, 24], + [25, 26, 27, 28, 29]]]) + +b = np.array([[True, True, False], [False, True, True]]) + +x[b] +array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [20, 21, 22, 23, 24], + [25, 26, 27, 28, 29]]) + +Combining advanced and basic indexing + +When there is at least one slice (:), ellipsis (...) or newaxis in the index (or the array has more dimensions than there are advanced indices), then the behaviour can be more complicated. It is like concatenating the indexing result for each advanced index element. + +In the simplest case, there is only a single advanced index combined with a slice. For example: + +y = np.arange(35).reshape(5,7) + +y[np.array([0, 2, 4]), 1:3] +array([[ 1, 2], + [15, 16], + [29, 30]]) + +In effect, the slice and index array operation are independent. The slice operation extracts columns with index 1 and 2, (i.e. the 2nd and 3rd columns), followed by the index array operation which extracts rows with index 0, 2 and 4 (i.e the first, third and fifth rows). This is equivalent to: + +y[:, 1:3][np.array([0, 2, 4]), :] +array([[ 1, 2], + [15, 16], + [29, 30]]) + +A single advanced index can, for example, replace a slice and the result array will be the same. However, it is a copy and may have a different memory layout. A slice is preferable when it is possible. For example: + +x = np.array([[ 0, 1, 2], + + [ 3, 4, 5], + + [ 6, 7, 8], + + [ 9, 10, 11]]) + +x[1:2, 1:3] +array([[4, 5]]) + +x[1:2, [1, 2]] +array([[4, 5]]) + +The easiest way to understand a combination of multiple advanced indices may be to think in terms of the resulting shape. There are two parts to the indexing operation, the subspace defined by the basic indexing (excluding integers) and the subspace from the advanced indexing part. Two cases of index combination need to be distinguished: + + The advanced indices are separated by a slice, Ellipsis or newaxis. For example x[arr1, :, arr2]. + + The advanced indices are all next to each other. For example x[..., arr1, arr2, :] but not x[arr1, :, 1] since 1 is an advanced index in this regard. + +In the first case, the dimensions resulting from the advanced indexing operation come first in the result array, and the subspace dimensions after that. In the second case, the dimensions from the advanced indexing operations are inserted into the result array at the same spot as they were in the initial array (the latter logic is what makes simple advanced indexing behave just like slicing). + +Example + +Suppose x.shape is (10, 20, 30) and ind is a (2, 3, 4)-shaped indexing intp array, then result = x[..., ind, :] has shape (10, 2, 3, 4, 30) because the (20,)-shaped subspace has been replaced with a (2, 3, 4)-shaped broadcasted indexing subspace. If we let i, j, k loop over the (2, 3, 4)-shaped subspace then result[..., i, j, k, :] = x[..., ind[i, j, k], :]. This example produces the same result as x.take(ind, axis=-2). + +Example + +Let x.shape be (10, 20, 30, 40, 50) and suppose ind_1 and ind_2 can be broadcast to the shape (2, 3, 4). Then x[:, ind_1, ind_2] has shape (10, 2, 3, 4, 40, 50) because the (20, 30)-shaped subspace from X has been replaced with the (2, 3, 4) subspace from the indices. However, x[:, ind_1, :, ind_2] has shape (2, 3, 4, 10, 30, 50) because there is no unambiguous place to drop in the indexing subspace, thus it is tacked-on to the beginning. It is always possible to use .transpose() to move the subspace anywhere desired. Note that this example cannot be replicated using take. + +Example + +Slicing can be combined with broadcasted boolean indices: + +x = np.arange(35).reshape(5, 7) + +b = x > 20 + +b +array([[False, False, False, False, False, False, False], + [False, False, False, False, False, False, False], + [False, False, False, False, False, False, False], + [ True, True, True, True, True, True, True], + [ True, True, True, True, True, True, True]]) + +x[b[:, 5], 1:3] +array([[22, 23], + [29, 30]]) + +Field access + +See also + +Structured arrays + +If the ndarray object is a structured array the fields of the array can be accessed by indexing the array with strings, dictionary-like. + +Indexing x['field-name'] returns a new view to the array, which is of the same shape as x (except when the field is a sub-array) but of data type x.dtype['field-name'] and contains only the part of the data in the specified field. Also, record array scalars can be “indexed” this way. + +Indexing into a structured array can also be done with a list of field names, e.g. x[['field-name1', 'field-name2']]. As of NumPy 1.16, this returns a view containing only those fields. In older versions of NumPy, it returned a copy. See the user guide section on Structured arrays for more information on multifield indexing. + +If the accessed field is a sub-array, the dimensions of the sub-array are appended to the shape of the result. For example: + +x = np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))]) + +x['a'].shape +(2, 2) + +x['a'].dtype +dtype('int32') + +x['b'].shape +(2, 2, 3, 3) + +x['b'].dtype +dtype('float64') + +Flat Iterator indexing + +x.flat returns an iterator that will iterate over the entire array (in C-contiguous style with the last index varying the fastest). This iterator object can also be indexed using basic slicing or advanced indexing as long as the selection object is not a tuple. This should be clear from the fact that x.flat is a 1-dimensional view. It can be used for integer indexing with 1-dimensional C-style-flat indices. The shape of any returned array is therefore the shape of the integer indexing object. +Assigning values to indexed arrays + +As mentioned, one can select a subset of an array to assign to using a single index, slices, and index and mask arrays. The value being assigned to the indexed array must be shape consistent (the same shape or broadcastable to the shape the index produces). For example, it is permitted to assign a constant to a slice: + +x = np.arange(10) + +x[2:7] = 1 + +or an array of the right size: + +x[2:7] = np.arange(5) + +Note that assignments may result in changes if assigning higher types to lower types (like floats to ints) or even exceptions (assigning complex to floats or ints): + +x[1] = 1.2 + +x[1] +1 + +x[1] = 1.2j +Traceback (most recent call last): + ... +TypeError: can't convert complex to int + +Unlike some of the references (such as array and mask indices) assignments are always made to the original data in the array (indeed, nothing else would make sense!). Note though, that some actions may not work as one may naively expect. This particular example is often surprising to people: + +x = np.arange(0, 50, 10) + +x +array([ 0, 10, 20, 30, 40]) + +x[np.array([1, 1, 3, 1])] += 1 + +x +array([ 0, 11, 20, 31, 40]) + +Where people expect that the 1st location will be incremented by 3. In fact, it will only be incremented by 1. The reason is that a new array is extracted from the original (as a temporary) containing the values at 1, 1, 3, 1, then the value 1 is added to the temporary, and then the temporary is assigned back to the original array. Thus the value of the array at x[1] + 1 is assigned to x[1] three times, rather than being incremented 3 times. +Dealing with variable numbers of indices within programs + +The indexing syntax is very powerful but limiting when dealing with a variable number of indices. For example, if you want to write a function that can handle arguments with various numbers of dimensions without having to write special case code for each number of possible dimensions, how can that be done? If one supplies to the index a tuple, the tuple will be interpreted as a list of indices. For example: + +z = np.arange(81).reshape(3, 3, 3, 3) + +indices = (1, 1, 1, 1) + +z[indices] +40 + +So one can use code to construct tuples of any number of indices and then use these within an index. + +Slices can be specified within programs by using the slice() function in Python. For example: + +indices = (1, 1, 1, slice(0, 2)) # same as [1, 1, 1, 0:2] + +z[indices] +array([39, 40]) + +Likewise, ellipsis can be specified by code by using the Ellipsis object: + +indices = (1, Ellipsis, 1) # same as [1, ..., 1] + +z[indices] +array([[28, 31, 34], + [37, 40, 43], + [46, 49, 52]]) + +For this reason, it is possible to use the output from the np.nonzero() function directly as an index since it always returns a tuple of index arrays. + +Because of the special treatment of tuples, they are not automatically converted to an array as a list would be. As an example: + +z[[1, 1, 1, 1]] # produces a large array +array([[[[27, 28, 29], + [30, 31, 32], ... + +z[(1, 1, 1, 1)] # returns a single value +40 + +Detailed notes + +These are some detailed notes, which are not of importance for day to day indexing (in no particular order): + + The native NumPy indexing type is intp and may differ from the default integer array type. intp is the smallest data type sufficient to safely index any array; for advanced indexing it may be faster than other types. + + For advanced assignments, there is in general no guarantee for the iteration order. This means that if an element is set more than once, it is not possible to predict the final result. + + An empty (tuple) index is a full scalar index into a zero-dimensional array. x[()] returns a scalar if x is zero-dimensional and a view otherwise. On the other hand, x[...] always returns a view. + + If a zero-dimensional array is present in the index and it is a full integer index the result will be a scalar and not a zero-dimensional array. (Advanced indexing is not triggered.) + + When an ellipsis (...) is present but has no size (i.e. replaces zero :) the result will still always be an array. A view if no advanced index is present, otherwise a copy. + + The nonzero equivalence for Boolean arrays does not hold for zero dimensional boolean arrays. + + When the result of an advanced indexing operation has no elements but an individual index is out of bounds, whether or not an IndexError is raised is undefined (e.g. x[[], [123]] with 123 being out of bounds). + + When a casting error occurs during assignment (for example updating a numerical array using a sequence of strings), the array being assigned to may end up in an unpredictable partially updated state. However, if any other error (such as an out of bounds index) occurs, the array will remain unchanged. + + The memory layout of an advanced indexing result is optimized for each indexing operation and no particular memory order can be assumed. + + When using a subclass (especially one which manipulates its shape), the default ndarray.__setitem__ behaviour will call __getitem__ for basic indexing but not for advanced indexing. For such a subclass it may be preferable to call ndarray.__setitem__ with a base class ndarray view on the data. This must be done if the subclasses __getitem__ does not return views. + +previous + +Array creation + +next + +I/O with NumPy + +© Copyright 2008-2022, NumPy Developers. + +Created using Sphinx 4.5.0. diff --git a/tests/integrated/test-boutpp/slicing/slicingexamples b/tests/integrated/test-boutpp/slicing/slicingexamples new file mode 100644 index 0000000000..7edb2fa5bc --- /dev/null +++ b/tests/integrated/test-boutpp/slicing/slicingexamples @@ -0,0 +1 @@ +, diff --git a/tests/integrated/test-boutpp/slicing/test.py b/tests/integrated/test-boutpp/slicing/test.py new file mode 100644 index 0000000000..2f36b362cb --- /dev/null +++ b/tests/integrated/test-boutpp/slicing/test.py @@ -0,0 +1,4 @@ +import boutcore as bc + +bc.init("-d test") +bc.print("We can print to the log from python 🎉") diff --git a/tests/integrated/test-griddata/test_griddata.cxx b/tests/integrated/test-griddata/test_griddata.cxx index 9f12d48d9d..0277f9e001 100644 --- a/tests/integrated/test-griddata/test_griddata.cxx +++ b/tests/integrated/test-griddata/test_griddata.cxx @@ -13,7 +13,7 @@ int main(int argc, char** argv) { dump["Bpxy"] = Bpxy; bout::experimental::addBuildFlagsToOptions(dump); bout::globals::mesh->outputVars(dump); - bout::OptionsNetCDF("data.nc").write(dump); + bout::OptionsIO::create("data.nc")->write(dump); BoutFinalise(); return 0; diff --git a/tests/integrated/test-interchange-instability/orig_test.idl.dat b/tests/integrated/test-interchange-instability/orig_test.idl.dat deleted file mode 100644 index 93652ca8563239dd35cd365709fb936bea40e9b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2612 zcmeHITWl0n7~a~2T}QStsnQCyIWee}lw>w{55fk1eH2miCX)rcmV@Y+B5 z=l|!N|Mq=nF7cSl?Q*#om&@G?c2!mxSY=?9f%2EIjJ8TTs;@=0EZ@+?*VUoAK%g0@ z@i>~KTRXCpB~O}QUB)W)OW+$aLY;zm!4{9OP4sq+s_0H(v$)A0plhX&K1ZFr6gT^r z%?~O*21TO%;m#0`!1XcVu09l(hHy;`Ted+kN+~L~5Y`fgoJEdqItf*=G62>jhDj7` zOC*aFJ!KG0vK2#z8dAb=Y-DFJqSYWSO(e~aVt5D>3nQ$j6&+)u=xJo6AWpGzT@YAE zA{c669|PBfBFk8Zt}-UMdNwASl~2kS59u-*uwb`ONn#y}2Z3L#Vcmu~s%ZPrKwt1V zKMJa9ZW;%IEhGUjf?snK0zhm7d`U(U)RJgAU7^$an1A+zYDmh1vlR_OGx~5DX+6Fq9vK zX;UCSDOtEW7qL>=#|$d~DS-i9QgviNF|62`T+E+v;Gj^}a0q3<#7X)P_|Xe0hH5Yb z!KOj%`@qWvAl8in63cKWsec1Hah^ziyZ7A(zX0yYM1W1Je$IX^Tqp0Pr(@QPiN4K?B zFWzZ=<8D#wH;dD4n@(P88y&mWHvhvb?G?+F?asQ%_Ss}2`1sC`g36wo!I{BqM~T_c zF*rKcaq7#7PS2-}o&Aq&=$w52#jc`PTf4Td|E25rm9nloV|PRC6O*CCjyv@0x{2=Q zqk-fn5dgo=iKq5epv5ZTQOTB=OgH*`Qaw*0Jc_GP#5byOC9@B2*j-o79{x&4yZ@Z0BN t^yU|$ey>N|AMX&4FSLv2wpED>xKzA86lr<*UA3jMv=b1oT_K9vKLFhc!k7R6 diff --git a/tests/integrated/test-options-adios/CMakeLists.txt b/tests/integrated/test-options-adios/CMakeLists.txt new file mode 100644 index 0000000000..110773d6fd --- /dev/null +++ b/tests/integrated/test-options-adios/CMakeLists.txt @@ -0,0 +1,6 @@ +bout_add_integrated_test(test-options-adios + SOURCES test-options-adios.cxx + USE_RUNTEST + USE_DATA_BOUT_INP + REQUIRES BOUT_HAS_ADIOS + ) diff --git a/tests/integrated/test-options-adios/data/BOUT.inp b/tests/integrated/test-options-adios/data/BOUT.inp new file mode 100644 index 0000000000..fa0f6d3681 --- /dev/null +++ b/tests/integrated/test-options-adios/data/BOUT.inp @@ -0,0 +1,6 @@ + + +[mesh] +nx = 5 +ny = 2 +nz = 2 diff --git a/tests/integrated/test-options-adios/makefile b/tests/integrated/test-options-adios/makefile new file mode 100644 index 0000000000..7dbbce8736 --- /dev/null +++ b/tests/integrated/test-options-adios/makefile @@ -0,0 +1,6 @@ + +BOUT_TOP = ../../.. + +SOURCEC = test-options-adios.cxx + +include $(BOUT_TOP)/make.config diff --git a/tests/integrated/test-options-adios/runtest b/tests/integrated/test-options-adios/runtest new file mode 100755 index 0000000000..1621c686a3 --- /dev/null +++ b/tests/integrated/test-options-adios/runtest @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +# Note: This test requires NCDF4, whereas on Travis NCDF is used +# requires: netcdf +# requires: adios +# requires: not legacy_netcdf + +from boututils.datafile import DataFile +from boututils.run_wrapper import build_and_log, shell, launch +from boutdata.data import BoutOptionsFile + +import math +import numpy as np + +build_and_log("options-netcdf test") +shell("rm -f test-out.ini") +shell("rm -f test-out.nc") + +# Create a NetCDF input file +with DataFile("test.nc", create=True, format="NETCDF4") as f: + f.write("int", 42) + f.write("real", 3.1415) + f.write("string", "hello") + +# run BOUT++ +launch("./test-options-adios", nproc=1, mthread=1) + +# Check the output INI file +result = BoutOptionsFile("test-out.ini") + +print(result) + +assert result["int"] == 42 +assert math.isclose(result["real"], 3.1415) +assert result["string"] == "hello" + +print("Checking saved ADIOS test-out file -- Not implemented") + +# Check the output NetCDF file +# with DataFile("test-out.nc") as f: +# assert f["int"] == 42 +# assert math.isclose(f["real"], 3.1415) +# assert result["string"] == "hello" + +print("Checking saved settings.ini") + +# Check the settings.ini file, coming from BOUT.inp +# which is converted to NetCDF, read in, then written again +settings = BoutOptionsFile("settings.ini") + +assert settings["mesh"]["nx"] == 5 +assert settings["mesh"]["ny"] == 2 + +print("Checking saved fields.bp -- Not implemented") + +# with DataFile("fields.nc") as f: +# assert f["f2d"].shape == (5, 6) # Field2D +# assert f["f3d"].shape == (5, 6, 2) # Field3D +# assert f["fperp"].shape == (5, 2) # FieldPerp +# assert np.allclose(f["f2d"], 1.0) +# assert np.allclose(f["f3d"], 2.0) +# assert np.allclose(f["fperp"], 3.0) + +print("Checking saved fields2.bp -- Not implemented") + +# with DataFile("fields2.nc") as f: +# assert f["f2d"].shape == (5, 6) # Field2D +# assert f["f3d"].shape == (5, 6, 2) # Field3D +# assert f["fperp"].shape == (5, 2) # FieldPerp +# assert np.allclose(f["f2d"], 1.0) +# assert np.allclose(f["f3d"], 2.0) +# assert np.allclose(f["fperp"], 3.0) + +print(" => Passed") diff --git a/tests/integrated/test-options-adios/test-options-adios.cxx b/tests/integrated/test-options-adios/test-options-adios.cxx new file mode 100644 index 0000000000..60604e1aa3 --- /dev/null +++ b/tests/integrated/test-options-adios/test-options-adios.cxx @@ -0,0 +1,111 @@ + +#include "bout/bout.hxx" + +#include "bout/options_io.hxx" +#include "bout/optionsreader.hxx" + +using bout::OptionsIO; + +int main(int argc, char** argv) { + BoutInitialise(argc, argv); + + // Read values from a NetCDF file + auto file = bout::OptionsIO::create("test.nc"); + + auto values = file->read(); + + values.printUnused(); + + // Write to an INI text file + OptionsReader* reader = OptionsReader::getInstance(); + reader->write(&values, "test-out.ini"); + + // Write to ADIOS file, by setting file type "adios" + OptionsIO::create({{"file", "test-out.bp"}, + {"type", "adios"}, + {"append", false}, + {"singleWriteFile", true}}) + ->write(values); + + /////////////////////////// + + // Write the BOUT.inp settings to ADIOS file + OptionsIO::create({{"file", "settings.bp"}, + {"type", "adios"}, + {"append", false}, + {"singleWriteFile", true}}) + ->write(Options::root()); + + // Read back in + auto settings = OptionsIO::create({{"file", "settings.bp"}, {"type", "adios"}})->read(); + + // Write to INI file + reader->write(&settings, "settings.ini"); + + /////////////////////////// + // Write fields + + Options fields; + fields["f2d"] = Field2D(1.0); + fields["f3d"] = Field3D(2.0); + fields["fperp"] = FieldPerp(3.0); + auto f = OptionsIO::create({{"file", "fields.bp"}, {"type", "adios"}}); + /* + write() for adios only buffers data but does not guarantee writing to disk + unless singleWriteFile is set to true + */ + f->write(fields); + // indicate completion of step, required to get data on disk + f->verifyTimesteps(); + + /////////////////////////// + // Read fields + + Options fields_in = + OptionsIO::create({{"file", "fields.bp"}, {"type", "adios"}})->read(); + + auto f2d = fields_in["f2d"].as(bout::globals::mesh); + auto f3d = fields_in["f3d"].as(bout::globals::mesh); + auto fperp = fields_in["fperp"].as(bout::globals::mesh); + + Options fields2; + fields2["f2d"] = f2d; + fields2["f3d"] = f3d; + fields2["fperp"] = fperp; + + // Write out again + auto f2 = OptionsIO::create({{"file", "fields2.bp"}, + {"type", "adios"}, + {"append", false}, + {"singleWriteFile", true}}); + f2->write(fields2); + + /////////////////////////// + // Time dependent values + + Options data; + data["scalar"] = 1.0; + data["scalar"].attributes["time_dimension"] = "t"; + + data["field"] = Field3D(2.0); + data["field"].attributes["time_dimension"] = "t"; + + OptionsIO::create({{"file", "time.bp"}, + {"type", "adios"}, + {"append", false}, + {"singleWriteFile", true}}) + ->write(data); + + // Update time-dependent values + data["scalar"] = 2.0; + data["field"] = Field3D(3.0); + + // Append data to file + OptionsIO::create({{"file", "time.bp"}, + {"type", "adios"}, + {"append", true}, + {"singleWriteFile", true}}) + ->write(data); + + BoutFinalise(); +}; diff --git a/tests/integrated/test-options-netcdf/test-options-netcdf.cxx b/tests/integrated/test-options-netcdf/test-options-netcdf.cxx index 01c5749972..f5daf8919c 100644 --- a/tests/integrated/test-options-netcdf/test-options-netcdf.cxx +++ b/tests/integrated/test-options-netcdf/test-options-netcdf.cxx @@ -1,18 +1,18 @@ #include "bout/bout.hxx" -#include "bout/options_netcdf.hxx" +#include "bout/options_io.hxx" #include "bout/optionsreader.hxx" -using bout::OptionsNetCDF; +using bout::OptionsIO; int main(int argc, char** argv) { BoutInitialise(argc, argv); // Read values from a NetCDF file - OptionsNetCDF file("test.nc"); + auto file = OptionsIO::create("test.nc"); - auto values = file.read(); + auto values = file->read(); values.printUnused(); @@ -21,15 +21,15 @@ int main(int argc, char** argv) { reader->write(&values, "test-out.ini"); // Write to a NetCDF file - OptionsNetCDF("test-out.nc").write(values); + OptionsIO::create("test-out.nc")->write(values); /////////////////////////// // Write the BOUT.inp settings to NetCDF file - OptionsNetCDF("settings.nc").write(Options::root()); + OptionsIO::create("settings.nc")->write(Options::root()); // Read back in - auto settings = OptionsNetCDF("settings.nc").read(); + auto settings = OptionsIO::create("settings.nc")->read(); // Write to INI file reader->write(&settings, "settings.ini"); @@ -41,12 +41,12 @@ int main(int argc, char** argv) { fields["f2d"] = Field2D(1.0); fields["f3d"] = Field3D(2.0); fields["fperp"] = FieldPerp(3.0); - OptionsNetCDF("fields.nc").write(fields); + OptionsIO::create("fields.nc")->write(fields); /////////////////////////// // Read fields - Options fields_in = OptionsNetCDF("fields.nc").read(); + Options fields_in = OptionsIO::create("fields.nc")->read(); auto f2d = fields_in["f2d"].as(bout::globals::mesh); auto f3d = fields_in["f3d"].as(bout::globals::mesh); @@ -58,7 +58,7 @@ int main(int argc, char** argv) { fields2["fperp"] = fperp; // Write out again - OptionsNetCDF("fields2.nc").write(fields2); + OptionsIO::create("fields2.nc")->write(fields2); /////////////////////////// // Time dependent values @@ -70,14 +70,14 @@ int main(int argc, char** argv) { data["field"] = Field3D(2.0); data["field"].attributes["time_dimension"] = "t"; - OptionsNetCDF("time.nc").write(data); + OptionsIO::create("time.nc")->write(data); // Update time-dependent values data["scalar"] = 2.0; data["field"] = Field3D(3.0); // Append data to file - OptionsNetCDF("time.nc", OptionsNetCDF::FileMode::append).write(data); + OptionsIO::create({{"file", "time.nc"}, {"append", true}})->write(data); BoutFinalise(); }; diff --git a/tests/integrated/test-solver/test_solver.cxx b/tests/integrated/test-solver/test_solver.cxx index ee64c6097f..2e4345c8cc 100644 --- a/tests/integrated/test-solver/test_solver.cxx +++ b/tests/integrated/test-solver/test_solver.cxx @@ -146,8 +146,6 @@ int main(int argc, char** argv) { } } - BoutFinalise(false); - if (!errors.empty()) { output_test << "\n => Some failed tests\n\n"; for (auto& error : errors) { diff --git a/tests/integrated/test-twistshift-staggered/test-twistshift.cxx b/tests/integrated/test-twistshift-staggered/test-twistshift.cxx index 87b9e0a094..33b45b662d 100644 --- a/tests/integrated/test-twistshift-staggered/test-twistshift.cxx +++ b/tests/integrated/test-twistshift-staggered/test-twistshift.cxx @@ -4,11 +4,11 @@ int main(int argc, char** argv) { BoutInitialise(argc, argv); - Field3D test = FieldFactory::get()->create3D("test", nullptr, nullptr, CELL_YLOW); + using bout::globals::mesh; - Field3D test_aligned = toFieldAligned(test); + Field3D test = FieldFactory::get()->create3D("test", nullptr, mesh, CELL_YLOW); - using bout::globals::mesh; + Field3D test_aligned = toFieldAligned(test); // zero guard cells to check that communication is doing something for (int x = 0; x < mesh->LocalNx; x++) { @@ -25,12 +25,12 @@ int main(int argc, char** argv) { mesh->communicate(test_aligned); Options::root()["check"] = - FieldFactory::get()->create3D("check", nullptr, nullptr, CELL_YLOW); + FieldFactory::get()->create3D("check", nullptr, mesh, CELL_YLOW); Options::root()["test"] = test; Options::root()["test_aligned"] = test_aligned; - bout::writeDefaultOutputFile(); + bout::writeDefaultOutputFile(Options::root()); BoutFinalise(); } diff --git a/tests/integrated/test-twistshift/test-twistshift.cxx b/tests/integrated/test-twistshift/test-twistshift.cxx index ccde8b82b6..2c0fc79563 100644 --- a/tests/integrated/test-twistshift/test-twistshift.cxx +++ b/tests/integrated/test-twistshift/test-twistshift.cxx @@ -28,7 +28,7 @@ int main(int argc, char** argv) { Options::root()["test"] = test; Options::root()["test_aligned"] = test_aligned; - bout::writeDefaultOutputFile(); + bout::writeDefaultOutputFile(Options::root()); BoutFinalise(); } diff --git a/tests/integrated/test-yupdown-weights/test_yupdown_weights.cxx b/tests/integrated/test-yupdown-weights/test_yupdown_weights.cxx index 22d1fb9e07..e8a2982bfd 100644 --- a/tests/integrated/test-yupdown-weights/test_yupdown_weights.cxx +++ b/tests/integrated/test-yupdown-weights/test_yupdown_weights.cxx @@ -70,7 +70,7 @@ int main(int argc, char** argv) { Options::root()["ddy"] = ddy; Options::root()["ddy2"] = ddy2; - bout::writeDefaultOutputFile(); + bout::writeDefaultOutputFile(Options::root()); BoutFinalise(); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index c0fd4a1e1f..c4ffa4fa75 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -19,6 +19,9 @@ if(NOT TARGET gtest) message(FATAL_ERROR "googletest not found! Have you disabled the git submodules (GIT_SUBMODULE)?") endif() +# Some unit tests require GMOCK, so make sure we build it +set(BUILD_GMOCK ON) + mark_as_advanced( BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS gmock_build_tests gtest_build_samples gtest_build_tests diff --git a/tests/unit/field/test_vector2d.cxx b/tests/unit/field/test_vector2d.cxx index 263c8d6d11..838876c02b 100644 --- a/tests/unit/field/test_vector2d.cxx +++ b/tests/unit/field/test_vector2d.cxx @@ -85,10 +85,6 @@ class Vector2DTest : public ::testing::Test { Mesh* mesh_staggered = nullptr; }; -constexpr int Vector2DTest::nx; -constexpr int Vector2DTest::ny; -constexpr int Vector2DTest::nz; - TEST_F(Vector2DTest, ApplyBoundaryString) { Vector2D v; v = 0.0; diff --git a/tests/unit/include/bout/test_generic_factory.cxx b/tests/unit/include/bout/test_generic_factory.cxx index 9368356151..f8a1e20bfa 100644 --- a/tests/unit/include/bout/test_generic_factory.cxx +++ b/tests/unit/include/bout/test_generic_factory.cxx @@ -37,10 +37,6 @@ class BaseFactory : public Factory { static constexpr auto option_name = "type"; static constexpr auto default_type = "base"; }; -constexpr decltype(BaseFactory::type_name) BaseFactory::type_name; -constexpr decltype(BaseFactory::section_name) BaseFactory::section_name; -constexpr decltype(BaseFactory::option_name) BaseFactory::option_name; -constexpr decltype(BaseFactory::default_type) BaseFactory::default_type; BaseFactory::RegisterInFactory registerme("base"); BaseFactory::RegisterInFactory registerme1("derived1"); @@ -76,11 +72,6 @@ class ComplicatedFactory static constexpr auto default_type = "basecomplicated"; }; -constexpr decltype(ComplicatedFactory::type_name) ComplicatedFactory::type_name; -constexpr decltype(ComplicatedFactory::section_name) ComplicatedFactory::section_name; -constexpr decltype(ComplicatedFactory::option_name) ComplicatedFactory::option_name; -constexpr decltype(ComplicatedFactory::default_type) ComplicatedFactory::default_type; - namespace { ComplicatedFactory::RegisterInFactory registerme3("basecomplicated"); ComplicatedFactory::RegisterInFactory diff --git a/tests/unit/include/bout/test_region.cxx b/tests/unit/include/bout/test_region.cxx index f13fe8bd78..fa46fed769 100644 --- a/tests/unit/include/bout/test_region.cxx +++ b/tests/unit/include/bout/test_region.cxx @@ -274,6 +274,24 @@ TEST_F(RegionTest, regionLoopAllSection) { EXPECT_EQ(count, nmesh); } +TEST_F(RegionTest, regionIntersection) { + auto& region1 = mesh->getRegion3D("RGN_ALL"); + + auto& region2 = mesh->getRegion3D("RGN_NOBNDRY"); + + const int nmesh = RegionTest::nx * RegionTest::ny * RegionTest::nz; + + EXPECT_EQ(region1.size(), nmesh); + EXPECT_GT(region1.size(), region2.size()); + + const auto& region3 = intersection(region1, region2); + + EXPECT_EQ(region2.size(), region3.size()); + // Ensure this did not change + EXPECT_EQ(region1.size(), nmesh); + EXPECT_EQ(mesh->getRegion3D("RGN_ALL").size(), nmesh); +} + TEST_F(RegionTest, regionLoopNoBndrySection) { const auto& region = mesh->getRegion3D("RGN_NOBNDRY"); diff --git a/tests/unit/mesh/data/test_gridfromoptions.cxx b/tests/unit/mesh/data/test_gridfromoptions.cxx index d2a038cb93..84f08ff47b 100644 --- a/tests/unit/mesh/data/test_gridfromoptions.cxx +++ b/tests/unit/mesh/data/test_gridfromoptions.cxx @@ -33,6 +33,8 @@ class GridFromOptionsTest : public ::testing::Test { output_progress.disable(); output_warn.disable(); options["f"] = expected_string; + options["n"] = 12; + options["r"] = 3.14; // modify mesh section in global options options["dx"] = "1."; @@ -116,10 +118,11 @@ TEST_F(GridFromOptionsTest, GetStringNone) { TEST_F(GridFromOptionsTest, GetInt) { int result{-1}; - int expected{3}; - EXPECT_TRUE(griddata->get(&mesh_from_options, result, "f")); - EXPECT_EQ(result, expected); + // The expression must not depend on x,y,z or t + EXPECT_THROW(griddata->get(&mesh_from_options, result, "f"), BoutException); + griddata->get(&mesh_from_options, result, "n"); + EXPECT_EQ(result, 12); } TEST_F(GridFromOptionsTest, GetIntNone) { @@ -132,9 +135,9 @@ TEST_F(GridFromOptionsTest, GetIntNone) { TEST_F(GridFromOptionsTest, GetBoutReal) { BoutReal result{-1.}; - BoutReal expected{3.}; + BoutReal expected{3.14}; - EXPECT_TRUE(griddata->get(&mesh_from_options, result, "f")); + EXPECT_TRUE(griddata->get(&mesh_from_options, result, "r")); EXPECT_EQ(result, expected); } diff --git a/tests/unit/sys/test_expressionparser.cxx b/tests/unit/sys/test_expressionparser.cxx index 00cb23c042..c4f0ebfcf3 100644 --- a/tests/unit/sys/test_expressionparser.cxx +++ b/tests/unit/sys/test_expressionparser.cxx @@ -379,9 +379,9 @@ TEST_F(ExpressionParserTest, BadBinaryOp) { TEST_F(ExpressionParserTest, AddBinaryOp) { // Add a synonym for multiply with a lower precedence than addition - parser.addBinaryOp('&', std::make_shared(nullptr, nullptr, '*'), 5); + parser.addBinaryOp('$', std::make_shared(nullptr, nullptr, '*'), 5); - auto fieldgen = parser.parseString("2 & x + 3"); + auto fieldgen = parser.parseString("2 $ x + 3"); EXPECT_EQ(fieldgen->str(), "(2*(x+3))"); for (auto x : x_array) { @@ -679,3 +679,38 @@ TEST_F(ExpressionParserTest, FuzzyFind) { EXPECT_EQ(first_CAPS_match->name, "multiply"); EXPECT_EQ(first_CAPS_match->distance, 1); } + +TEST_F(ExpressionParserTest, LogicalOR) { + EXPECT_DOUBLE_EQ(parser.parseString("1 | 0")->generate({}), 1.0); + EXPECT_DOUBLE_EQ(parser.parseString("0 | 1")->generate({}), 1.0); + EXPECT_DOUBLE_EQ(parser.parseString("1 | 1")->generate({}), 1.0); + EXPECT_DOUBLE_EQ(parser.parseString("0 | 0")->generate({}), 0.0); +} + +TEST_F(ExpressionParserTest, LogicalAND) { + EXPECT_DOUBLE_EQ(parser.parseString("1 & 0")->generate({}), 0.0); + EXPECT_DOUBLE_EQ(parser.parseString("0 & 1")->generate({}), 0.0); + EXPECT_DOUBLE_EQ(parser.parseString("1 & 1")->generate({}), 1.0); + EXPECT_DOUBLE_EQ(parser.parseString("0 & 0")->generate({}), 0.0); +} + +TEST_F(ExpressionParserTest, LogicalNOT) { + EXPECT_DOUBLE_EQ(parser.parseString("!0")->generate({}), 1.0); + EXPECT_DOUBLE_EQ(parser.parseString("!1")->generate({}), 0.0); +} + +TEST_F(ExpressionParserTest, LogicalNOTprecedence) { + // Should bind more strongly than all binary operators + EXPECT_DOUBLE_EQ(parser.parseString("!1 & 0")->generate({}), 0.0); + EXPECT_DOUBLE_EQ(parser.parseString("1 & !0")->generate({}), 1.0); +} + +TEST_F(ExpressionParserTest, CompareGT) { + EXPECT_DOUBLE_EQ(parser.parseString("1 > 0")->generate({}), 1.0); + EXPECT_DOUBLE_EQ(parser.parseString("3 > 5")->generate({}), 0.0); +} + +TEST_F(ExpressionParserTest, CompareLT) { + EXPECT_DOUBLE_EQ(parser.parseString("1 < 0")->generate({}), 0.0); + EXPECT_DOUBLE_EQ(parser.parseString("3 < 5")->generate({}), 1.0); +} diff --git a/tests/unit/sys/test_options.cxx b/tests/unit/sys/test_options.cxx index a357923053..1f0ae92ed2 100644 --- a/tests/unit/sys/test_options.cxx +++ b/tests/unit/sys/test_options.cxx @@ -232,10 +232,9 @@ TEST_F(OptionsTest, GetBoolFromString) { EXPECT_EQ(value, true); + // "yes" is not an acceptable bool bool value2; - options.get("bool_key2", value2, false, false); - - EXPECT_EQ(value2, true); + EXPECT_THROW(options.get("bool_key2", value2, false, false), BoutException); } TEST_F(OptionsTest, DefaultValueBool) { @@ -327,7 +326,7 @@ TEST_F(OptionsTest, ValueUsed) { Options options; options["key1"] = 1; EXPECT_FALSE(options["key1"].valueUsed()); - MAYBE_UNUSED(const int value) = options["key1"]; + [[maybe_unused]] const int value = options["key1"]; EXPECT_TRUE(options["key1"].valueUsed()); } @@ -1246,17 +1245,17 @@ TEST_F(OptionsTest, GetUnused) { // This shouldn't count as unused option["section2"]["value5"].attributes["source"] = "Output"; - MAYBE_UNUSED(auto value1) = option["section1"]["value1"].as(); - MAYBE_UNUSED(auto value3) = option["section2"]["subsection1"]["value3"].as(); + [[maybe_unused]] auto value1 = option["section1"]["value1"].as(); + [[maybe_unused]] auto value3 = option["section2"]["subsection1"]["value3"].as(); Options expected_unused{{"section1", {{"value2", "hello"}}}, {"section2", {{"subsection1", {{"value4", 3.2}}}}}}; EXPECT_EQ(option.getUnused(), expected_unused); - MAYBE_UNUSED(auto value2) = option["section1"]["value2"].as(); - MAYBE_UNUSED(auto value4) = option["section2"]["subsection1"]["value4"].as(); - MAYBE_UNUSED(auto value5) = option["section2"]["value5"].as(); + [[maybe_unused]] auto value2 = option["section1"]["value2"].as(); + [[maybe_unused]] auto value4 = option["section2"]["subsection1"]["value4"].as(); + [[maybe_unused]] auto value5 = option["section2"]["value5"].as(); Options expected_empty{}; @@ -1334,8 +1333,8 @@ TEST_F(OptionsTest, CheckForUnusedOptions) { // This shouldn't count as unused option["section2"]["value5"].attributes["source"] = "Output"; - MAYBE_UNUSED(auto value1) = option["section1"]["value1"].as(); - MAYBE_UNUSED(auto value3) = option["section2"]["subsection1"]["value3"].as(); + [[maybe_unused]] auto value1 = option["section1"]["value1"].as(); + [[maybe_unused]] auto value3 = option["section2"]["subsection1"]["value3"].as(); EXPECT_THROW(bout::checkForUnusedOptions(option, "data", "BOUT.inp"), BoutException); } @@ -1361,8 +1360,7 @@ TEST_P(BoolTrueTestParametrized, BoolTrueFromString) { } INSTANTIATE_TEST_CASE_P(BoolTrueTests, BoolTrueTestParametrized, - ::testing::Values("y", "Y", "yes", "Yes", "yeS", "t", "true", "T", - "True", "tRuE", "1")); + ::testing::Values("true", "True", "1")); class BoolFalseTestParametrized : public OptionsTest, public ::testing::WithParamInterface {}; @@ -1376,8 +1374,7 @@ TEST_P(BoolFalseTestParametrized, BoolFalseFromString) { } INSTANTIATE_TEST_CASE_P(BoolFalseTests, BoolFalseTestParametrized, - ::testing::Values("n", "N", "no", "No", "nO", "f", "false", "F", - "False", "fAlSe", "0")); + ::testing::Values("false", "False", "0")); class BoolInvalidTestParametrized : public OptionsTest, public ::testing::WithParamInterface {}; @@ -1391,6 +1388,52 @@ TEST_P(BoolInvalidTestParametrized, BoolInvalidFromString) { } INSTANTIATE_TEST_CASE_P(BoolInvalidTests, BoolInvalidTestParametrized, - ::testing::Values("a", "B", "yellow", "Yogi", "test", "truelong", - "Tim", "2", "not", "No bool", "nOno", - "falsebuttoolong", "-1")); + ::testing::Values("yes", "no", "y", "n", "a", "B", "yellow", + "Yogi", "test", "truelong", "Tim", "2", "not", + "No bool", "nOno", "falsebuttoolong", "-1", + "1.1")); + +TEST_F(OptionsTest, BoolLogicalOR) { + ASSERT_TRUE(Options("true | false").as()); + ASSERT_TRUE(Options("false | true").as()); + ASSERT_TRUE(Options("true | true").as()); + ASSERT_FALSE(Options("false | false").as()); + ASSERT_TRUE(Options("true | false | true").as()); +} + +TEST_F(OptionsTest, BoolLogicalAND) { + ASSERT_FALSE(Options("true & false").as()); + ASSERT_FALSE(Options("false & true").as()); + ASSERT_TRUE(Options("true & true").as()); + ASSERT_FALSE(Options("false & false").as()); + ASSERT_FALSE(Options("true & false & true").as()); + + EXPECT_THROW(Options("true & 1.3").as(), BoutException); + EXPECT_THROW(Options("2 & false").as(), BoutException); +} + +TEST_F(OptionsTest, BoolLogicalNOT) { + ASSERT_FALSE(Options("!true").as()); + ASSERT_TRUE(Options("!false").as()); + ASSERT_FALSE(Options("!true & false").as()); + ASSERT_TRUE(Options("!(true & false)").as()); + ASSERT_TRUE(Options("true & !false").as()); + + EXPECT_THROW(Options("!2").as(), BoutException); + EXPECT_THROW(Options("!1.2").as(), BoutException); +} + +TEST_F(OptionsTest, BoolComparisonGT) { + ASSERT_TRUE(Options("2 > 1").as()); + ASSERT_FALSE(Options("2 > 3").as()); +} + +TEST_F(OptionsTest, BoolComparisonLT) { + ASSERT_FALSE(Options("2 < 1").as()); + ASSERT_TRUE(Options("2 < 3").as()); +} + +TEST_F(OptionsTest, BoolCompound) { + ASSERT_TRUE(Options("true & !false").as()); + ASSERT_TRUE(Options("2 > 1 & 2 < 3").as()); +} diff --git a/tests/unit/sys/test_options_netcdf.cxx b/tests/unit/sys/test_options_netcdf.cxx index b086043822..5869cf4932 100644 --- a/tests/unit/sys/test_options_netcdf.cxx +++ b/tests/unit/sys/test_options_netcdf.cxx @@ -9,9 +9,9 @@ #include "test_extras.hxx" #include "bout/field3d.hxx" #include "bout/mesh.hxx" -#include "bout/options_netcdf.hxx" +#include "bout/options_io.hxx" -using bout::OptionsNetCDF; +using bout::OptionsIO; #include @@ -39,11 +39,11 @@ TEST_F(OptionsNetCDFTest, ReadWriteInt) { options["test"] = 42; // Write the file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read again - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["test"], 42); } @@ -54,11 +54,11 @@ TEST_F(OptionsNetCDFTest, ReadWriteString) { options["test"] = std::string{"hello"}; // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["test"], std::string("hello")); } @@ -69,11 +69,11 @@ TEST_F(OptionsNetCDFTest, ReadWriteField2D) { options["test"] = Field2D(1.0); // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); Field2D value = data["test"].as(bout::globals::mesh); @@ -87,11 +87,11 @@ TEST_F(OptionsNetCDFTest, ReadWriteField3D) { options["test"] = Field3D(2.4); // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); Field3D value = data["test"].as(bout::globals::mesh); @@ -106,11 +106,11 @@ TEST_F(OptionsNetCDFTest, Groups) { options["test"]["key"] = 42; // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["test"]["key"], 42); } @@ -121,11 +121,11 @@ TEST_F(OptionsNetCDFTest, AttributeInt) { options["test"].attributes["thing"] = 4; // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["test"].attributes["thing"].as(), 4); } @@ -136,11 +136,11 @@ TEST_F(OptionsNetCDFTest, AttributeBoutReal) { options["test"].attributes["thing"] = 3.14; // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_DOUBLE_EQ(data["test"].attributes["thing"].as(), 3.14); } @@ -151,11 +151,11 @@ TEST_F(OptionsNetCDFTest, AttributeString) { options["test"].attributes["thing"] = "hello"; // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["test"].attributes["thing"].as(), "hello"); } @@ -165,11 +165,11 @@ TEST_F(OptionsNetCDFTest, Field2DWriteCellCentre) { options["f2d"] = Field2D(2.0); // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["f2d"].attributes["cell_location"].as(), toString(CELL_CENTRE)); @@ -181,11 +181,11 @@ TEST_F(OptionsNetCDFTest, Field2DWriteCellYLow) { options["f2d"] = Field2D(2.0, mesh_staggered).setLocation(CELL_YLOW); // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["f2d"].attributes["cell_location"].as(), toString(CELL_YLOW)); @@ -197,11 +197,11 @@ TEST_F(OptionsNetCDFTest, Field3DWriteCellCentre) { options["f3d"] = Field3D(2.0); // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["f3d"].attributes["cell_location"].as(), toString(CELL_CENTRE)); @@ -213,11 +213,11 @@ TEST_F(OptionsNetCDFTest, Field3DWriteCellYLow) { options["f3d"] = Field3D(2.0, mesh_staggered).setLocation(CELL_YLOW); // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["f3d"].attributes["cell_location"].as(), toString(CELL_YLOW)); @@ -235,11 +235,11 @@ TEST_F(OptionsNetCDFTest, FieldPerpWriteCellCentre) { fperp.getMesh()->getXcomm(); // Write file - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } // Read file - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_EQ(data["fperp"].attributes["cell_location"].as(), toString(CELL_CENTRE)); @@ -252,10 +252,10 @@ TEST_F(OptionsNetCDFTest, VerifyTimesteps) { options["thing1"] = 1.0; options["thing1"].attributes["time_dimension"] = "t"; - OptionsNetCDF(filename).write(options); + OptionsIO::create(filename)->write(options); } - EXPECT_NO_THROW(OptionsNetCDF(filename).verifyTimesteps()); + EXPECT_NO_THROW(OptionsIO::create(filename)->verifyTimesteps()); { Options options; @@ -265,10 +265,11 @@ TEST_F(OptionsNetCDFTest, VerifyTimesteps) { options["thing2"] = 3.0; options["thing2"].attributes["time_dimension"] = "t"; - OptionsNetCDF(filename, OptionsNetCDF::FileMode::append).write(options); + OptionsIO::create({{"type", "netcdf"}, {"file", filename}, {"append", true}}) + ->write(options); } - EXPECT_THROW(OptionsNetCDF(filename).verifyTimesteps(), BoutException); + EXPECT_THROW(OptionsIO::create(filename)->verifyTimesteps(), BoutException); } TEST_F(OptionsNetCDFTest, WriteTimeDimension) { @@ -278,10 +279,10 @@ TEST_F(OptionsNetCDFTest, WriteTimeDimension) { options["thing2"].assignRepeat(2.0, "t2"); // non-default // Only write non-default time dim - OptionsNetCDF(filename).write(options, "t2"); + OptionsIO::create(filename)->write(options, "t2"); } - Options data = OptionsNetCDF(filename).read(); + Options data = OptionsIO::create(filename)->read(); EXPECT_FALSE(data.isSet("thing1")); EXPECT_TRUE(data.isSet("thing2")); @@ -297,12 +298,12 @@ TEST_F(OptionsNetCDFTest, WriteMultipleTimeDimensions) { options["thing4_t2"].assignRepeat(2.0, "t2"); // non-default // Write the non-default time dim twice - OptionsNetCDF(filename).write(options, "t2"); - OptionsNetCDF(filename).write(options, "t2"); - OptionsNetCDF(filename).write(options, "t"); + OptionsIO::create(filename)->write(options, "t2"); + OptionsIO::create(filename)->write(options, "t2"); + OptionsIO::create(filename)->write(options, "t"); } - EXPECT_NO_THROW(OptionsNetCDF(filename).verifyTimesteps()); + EXPECT_NO_THROW(OptionsIO::create(filename)->verifyTimesteps()); } #endif // BOUT_HAS_NETCDF diff --git a/tests/unit/test_extras.cxx b/tests/unit/test_extras.cxx index a4c51ac3c4..491dd189fc 100644 --- a/tests/unit/test_extras.cxx +++ b/tests/unit/test_extras.cxx @@ -3,11 +3,6 @@ #include -// Need to provide a redundant declaration because C++ -constexpr int FakeMeshFixture::nx; -constexpr int FakeMeshFixture::ny; -constexpr int FakeMeshFixture::nz; - ::testing::AssertionResult IsSubString(const std::string& str, const std::string& substring) { if (str.find(substring) != std::string::npos) { diff --git a/tests/unit/test_extras.hxx b/tests/unit/test_extras.hxx index f0868ddf49..845ea4f255 100644 --- a/tests/unit/test_extras.hxx +++ b/tests/unit/test_extras.hxx @@ -51,13 +51,13 @@ inline std::ostream& operator<<(std::ostream& out, const SpecificInd& index) } /// Helpers to get the type of a Field as a string -auto inline getFieldType(MAYBE_UNUSED(const Field2D& field)) -> std::string { +auto inline getFieldType([[maybe_unused]] const Field2D& field) -> std::string { return "Field2D"; } -auto inline getFieldType(MAYBE_UNUSED(const Field3D& field)) -> std::string { +auto inline getFieldType([[maybe_unused]] const Field3D& field) -> std::string { return "Field3D"; } -auto inline getFieldType(MAYBE_UNUSED(const FieldPerp& field)) -> std::string { +auto inline getFieldType([[maybe_unused]] const FieldPerp& field) -> std::string { return "FieldPerp"; } @@ -339,7 +339,7 @@ public: bool hasVar(const std::string& UNUSED(name)) override { return false; } - bool get(MAYBE_UNUSED(Mesh* m), std::string& sval, const std::string& name, + bool get([[maybe_unused]] Mesh* m, std::string& sval, const std::string& name, const std::string& def = "") override { if (values[name].isSet()) { sval = values[name].as(); @@ -348,7 +348,7 @@ public: sval = def; return false; } - bool get(MAYBE_UNUSED(Mesh* m), int& ival, const std::string& name, + bool get([[maybe_unused]] Mesh* m, int& ival, const std::string& name, int def = 0) override { if (values[name].isSet()) { ival = values[name].as(); @@ -357,7 +357,7 @@ public: ival = def; return false; } - bool get(MAYBE_UNUSED(Mesh* m), BoutReal& rval, const std::string& name, + bool get([[maybe_unused]] Mesh* m, BoutReal& rval, const std::string& name, BoutReal def = 0.0) override { if (values[name].isSet()) { rval = values[name].as(); @@ -394,15 +394,15 @@ public: return false; } - bool get(MAYBE_UNUSED(Mesh* m), MAYBE_UNUSED(std::vector& var), - MAYBE_UNUSED(const std::string& name), MAYBE_UNUSED(int len), - MAYBE_UNUSED(int def) = 0, Direction = GridDataSource::X) override { + bool get([[maybe_unused]] Mesh* m, [[maybe_unused]] std::vector& var, + [[maybe_unused]] const std::string& name, [[maybe_unused]] int len, + [[maybe_unused]] int def = 0, Direction = GridDataSource::X) override { throw BoutException("Not Implemented"); return false; } - bool get(MAYBE_UNUSED(Mesh* m), MAYBE_UNUSED(std::vector& var), - MAYBE_UNUSED(const std::string& name), MAYBE_UNUSED(int len), - MAYBE_UNUSED(int def) = 0, + bool get([[maybe_unused]] Mesh* m, [[maybe_unused]] std::vector& var, + [[maybe_unused]] const std::string& name, [[maybe_unused]] int len, + [[maybe_unused]] int def = 0, Direction UNUSED(dir) = GridDataSource::X) override { throw BoutException("Not Implemented"); return false; diff --git a/tools/pylib/_boutpp_build/bout_options.pxd b/tools/pylib/_boutpp_build/bout_options.pxd index ba5e64c8e3..be17608cea 100644 --- a/tools/pylib/_boutpp_build/bout_options.pxd +++ b/tools/pylib/_boutpp_build/bout_options.pxd @@ -8,20 +8,11 @@ cdef extern from "boutexception_helper.hxx": cdef void raise_bout_py_error() -cdef extern from "bout/options_netcdf.hxx" namespace "bout": - cdef void writeDefaultOutputFile(); +cdef extern from "bout/options_io.hxx" namespace "bout": cdef void writeDefaultOutputFile(Options& options); - cppclass OptionsNetCDF: - enum FileMode: - replace - append - OptionsNetCDF() except +raise_bout_py_error - OptionsNetCDF(string filename) except +raise_bout_py_error - OptionsNetCDF(string filename, FileMode mode) except +raise_bout_py_error - OptionsNetCDF(const OptionsNetCDF&); - OptionsNetCDF(OptionsNetCDF&&); - OptionsNetCDF& operator=(const OptionsNetCDF&); - OptionsNetCDF& operator=(OptionsNetCDF&&); + cppclass OptionsIO: + @staticmethod + OptionsIO * create(string filename) Options read(); void write(const Options& options); void write(const Options& options, string time_dim); diff --git a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja index c94fd14a17..12e210a5b5 100644 --- a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja +++ b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja @@ -5,7 +5,7 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string cimport resolve_enum as benum -from bout_options cimport Options, OptionsReader, OptionsNetCDF, writeDefaultOutputFile +from bout_options cimport Options, OptionsReader, OptionsIO, writeDefaultOutputFile cdef extern from "boutexception_helper.hxx": cdef void raise_bout_py_error() diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 657e2f28c1..3aeb1428eb 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -1723,7 +1723,6 @@ cdef class Options: del self.cobj self.cobj = NULL - def writeDefaultOutputFile(options: Options): c.writeDefaultOutputFile(deref(options.cobj)) diff --git a/tools/pylib/post_bout/ListDict.py b/tools/pylib/post_bout/ListDict.py deleted file mode 100644 index f500825651..0000000000 --- a/tools/pylib/post_bout/ListDict.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import print_function -import sys -import os - -try: - boutpath = os.environ["BOUT_TOP"] - pylibpath = boutpath + "/pylib" - pbpath = pylibpath + "/post_bout" - boutdatapath = pylibpath + "/boutdata" - boututilpath = pylibpath + "/boututils" - - allpath = [boutpath, pylibpath, pbpath, boutdatapath, boututilpath] - [sys.path.append(elem) for elem in allpath] -except: - print("meh") - -import numpy as np - - -def ListDictKey(input, key): - # given a key and a list of dictionaries this method returns an ordered - # list of requested key values - output = [] - for x in input: - try: - # print x[key] - output.append(x[key]) - except: - print("Key not found") - return 1 - - return output - - -def ListDictFilt(input, key, valuelist): - # given a key,value pair and a list of dictionaries this - # method returns an ordered list of dictionaries where (dict(key)==value) = True - # http://stackoverflow.com/questions/5762643/how-to-filter-list-of-dictionaries-with-matching-values-for-a-given-key - try: - x = copyf(input, key, valuelist) - return x - except: - return [] - - -def copyf(dictlist, key, valuelist): - return [dictio for dictio in dictlist if dictio[key] in valuelist] - - -# def subset(obj): -# #def __init__(self,alldb,key,valuelist,model=False): -# selection = ListDictFilt(obj.mode_db,obj.key,valuelist) -# if len(selection) !=0: -# LinRes.__init__(obj,selection) -# self.skey = key -# if model==True: -# self.model() -# else: -# LinRes.__init__(self,alldb) -# if model==True: -# self.model() diff --git a/tools/pylib/post_bout/__init__.py b/tools/pylib/post_bout/__init__.py deleted file mode 100644 index 069ae3e85b..0000000000 --- a/tools/pylib/post_bout/__init__.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import - -################################################## -# BOUT++ data package -# -# Routines for examining simulation results for BOUT++ -# -################################################## - -print("Loading BOUT++ post processing routines") - -# Load routines from separate files -import sys -import os - -try: - boutpath = os.environ["BOUT_TOP"] - pylibpath = boutpath + "/tools/pylib" - boutdatapath = pylibpath + "/boutdata" - boututilpath = pylibpath + "/boututils" - allpath = [boutpath, pylibpath, boutdatapath, boututilpath] - [sys.path.append(elem) for elem in allpath] - print(sys.path) - - # sys.path.append('/home/cryosphere/BOUT/tools/pylib') - # sys.path.append('/home/cryosphere/BOUT/tools/pylib/boutdata') - # sys.path.append('/home/cryosphere/BOUT/tools/pylib/boututils') - - print("in post_bout/__init__.py") - - import matplotlib - - matplotlib.use("pdf") # savemovie must be called as a diff. sesssion - - import gobject - import numpy as np -except ImportError: - print("can't find the modules I need, you fail") - sys.exit() # no point in going on - - -# import some bout specific modules -try: - import boutdata - import boututils -except: - print("can't find bout related modules, you fail") - -# import some home-brewed modules - - -# create some aliases - - -try: - from read_grid import read_grid -except: - print("Sorry, no read_grid") - -try: - from .read_inp import parse_inp, read_inp, read_log, metadata -except: - print("Sorry no parse_inp") - -try: - from .read_cxx import read_cxx, get_evolved_cxx, no_comment_cxx -except: - print("Sorry no read_cxx") - -try: - from post_bout import save, read -except: - print("Sorry, no show") - -try: - from .basic_info import basic_info, fft_info -except: - print("Sorry, no basic_info") - -try: - from .pb_corral import corral, LinRes, subset -except: - print("No corral") - -try: - from . import ListDict -except: - print("No ListDict") - -try: - # from rotate_mp import rotate - from rotate2 import rotate -except: - print("No rotate") diff --git a/tools/pylib/post_bout/basic_info.py b/tools/pylib/post_bout/basic_info.py deleted file mode 100644 index 563bfe6e98..0000000000 --- a/tools/pylib/post_bout/basic_info.py +++ /dev/null @@ -1,421 +0,0 @@ -from __future__ import print_function -from __future__ import division -from builtins import zip -from builtins import range -from past.utils import old_div - -# basic_info return some statistical averages and harmonic info -import numpy as np -import math - - -def basic_info(data, meta, rescale=True, rotate=False, user_peak=0, nonlinear=None): - print("in basic_info") - # from . import read_grid,parse_inp,read_inp,show - - dims = data.shape - ndims = len(dims) - - mxg = meta["MXG"]["v"] - - if ndims == 4: - nt, nx, ny, nz = data.shape - print(nt, nx, ny) - else: - print("something with dimesions") - - dc = ( - data.mean(1).mean(1).mean(1) - ) # there MUST be a way to indicate all axis at once - amp = abs(data).max(1).max(1).max(1) - dt = meta["dt"]["v"] - - if rescale: - amp_o = amp - dc - fourDamp = np.repeat(amp_o, nx * ny * nz) - fourDamp = fourDamp.reshape(nt, nx, ny, nz) - dc_n = old_div(dc, amp_o) - data_n = old_div(data, fourDamp) - - print(data.shape) - dfdt = np.gradient(data)[0] - dfdt = abs(dfdt).max(1).max(1).max(1) - - ave = {"amp": amp, "dc": dc, "amp_o": amp_o, "dfdt": dfdt} - - else: - print("no rescaling") - ave = {"amp": amp, "dc": dc} - - if nonlinear is not None: # add nonlinear part if user provides - nl = abs(nonlinear[:, mxg : -1.0 * mxg, :, :]).max(1).max(1).max(1) - nl_norm = (old_div(nl, dfdt)) * dt - - ave["nl"] = nl - ave["nl_norm"] = nl_norm - - if rotate: - print("rotate stuff") - # will need to provide some grid geometry to do this one - else: - print("or not") - - # let's identify the dominant modes, for now at every [t,x] slice - # if the data set is too large we can average over x - peaks_db = fft_info( - data, user_peak, meta=meta - ) # Nt X Nx X (# of loc. max) list of dict - - # print peaks[0]['gamma'] - return peaks_db, ave - - -def fft_info( - data, - user_peak, - dimension=[3, 4], - rescale=False, - wavelet=False, - show=False, - meta=0, - edgefix=False, -): - import numpy as np - import math - - print("in fft_inf0") - - dims = data.shape - ndims = len(dims) - - if ndims == 4: - nt, nx, ny, nz = data.shape - print(data.shape) - else: - print("something with dimesions") - - # data2 = data - # if edgefix: - # data2 = np.zeros((nt,nx,ny+1,nz+1)) - - # for t in range(nt): - # for x in range(nx): - # temp = np.append(data[t,x,:,:],[data[t,x,0,:]],0) - # data2[t,x,:,:] = np.append(temp, - # np.transpose([temp[:,0]]),1) - - # dt, k labels for the revelant dimensions - - dt = meta["dt"]["v"] - dz = meta["dz"] - # IC = meta['IC'] - ky_max = old_div(ny, 2) - kz_max = old_div(nz, 2) - amp = abs(data).max(2).max(2) # nt x nx - - print("dt: ", dt) - - # print data[0,2,:,:] - - IC = amp[0, :].max() # intial condition, set - print(IC) - - fft_data = np.fft.fft2(data)[ - :, :, 0:ky_max, 0:kz_max - ] # by default the last 2 dimensions - - power = fft_data.conj() * fft_data - # print power[0].max(), (IC*(ky_max)*(kz_max))**2 - - cross_pow = old_div((fft_data * (np.roll(fft_data, 1, axis=0)).conj()), (ny * nz)) - - if rescale: - fft_data_n = np.fft.fft2(data_n)[:, :, 0:ky_max, 0:kz_max] - pow_n = np.sqrt((fft_data_n.conj() * fft_data_n).real) - - peaks = [[[] for i in range(nx)] for j in range(nt)] # a list of dictionaries - peaks_db = [] - - peak_hist = [[0 for i in range(kz_max)] for j in range(ky_max)] # a 2d bin array - - # for now using a lame 2x loop method - - if user_peak != 0: - for mem in user_peak: - print(mem) - peak_hist[int(mem[0])][int(mem[1])] = abs( - power.mean(0).mean(0)[int(mem[0]), int(mem[1])] - ) - - # floor = ((IC*(kz_max*ky_max))**2)/10000 - - else: - for t in range(nt): - for x in range(nx): - peaks[t][x] = local_maxima( - power[t, x, :, :], 0, floor=(IC * (kz_max * ky_max)) ** 2 - ) - for p in peaks[t][ - x - ]: # looping over each returned peakset at some fixed t,x pair - peak_hist[p["y_i"]][ - p["z_i"] - ] += 1 # average across t and x, at least exclude pad - floor = 0 - - # this array is usefull for determining what the dominant modes are - # but we want to retain the option of observing how the amplitude - # of any give harmonic varies in space - - peak_hist = np.array(peak_hist) - - # let's find the top N overall powerfull harmnonics - net_peak = local_maxima(peak_hist, user_peak, bug=False) - - print("net_peak: ", net_peak, user_peak != 0) - # dom_mode = [{'amp':[],'amp_n':[],'phase':[],'freq':[],'gamma':[]} for x in net_peak] - dom_mode_db = [] - - Bp = meta["Bpxy"]["v"][:, old_div(ny, 2)] - B = meta["Bxy"]["v"][:, old_div(ny, 2)] - Bt = meta["Btxy"]["v"][:, old_div(ny, 2)] - - rho_s = meta["rho_s"]["v"] - - L_z = old_div(meta["L_z"], rho_s) - # L_z = - L_y = meta["lpar"] # already normalized earlier in read_inp.py - L_norm = old_div(meta["lbNorm"], rho_s) - - hthe0_n = 1e2 * meta["hthe0"]["v"] / rho_s # no x dep - hthe0_n_x = old_div(L_y, (2 * np.pi)) # no x dep - - print("L_z,Ly: ", L_z, L_y) - - # if user provides the harmo nic info overide the found peaks - - # thi is where all the good stuff is picked up - - # look at each mode annd pull out some usefull linear measures - for i, p in enumerate(net_peak): - # print i,p['y_i'],p['z_i'],fft_data.shape,fft_data[:,:,p['y_i'],p['z_i']].shape - - amp = ( - old_div(np.sqrt(power[:, :, p["y_i"], p["z_i"]]), (kz_max * ky_max)) - ).real - - # print (np.angle(fft_data[:,:,p['y_i'],p['z_i']],deg=False)).real - - phase = -np.array( - np.gradient( - np.squeeze(np.angle(fft_data[:, :, p["y_i"], p["z_i"]], deg=False)) - )[0].real - ) # nt x nx - - gamma_instant = np.array(np.gradient(np.log(np.squeeze(amp)))[0]) - - # loop over radaii - phasenew = [] - gammanew = [] - from scipy.interpolate import interp2d, interp1d - from scipy import interp - - gamma_t = np.transpose(gamma_instant) - for i, phase_r in enumerate(np.transpose(phase)): - gamma_r = gamma_t[i] - jumps = np.where(abs(phase_r) > old_div(np.pi, 32)) - # print jumps - if len(jumps[0]) != 0: - all_pts = np.array(list(range(0, nt))) - good_pts = (np.where(abs(phase_r) < old_div(np.pi, 3)))[0] - # print good_pts,good_pts - # f = interp1d(good_pts,phase_r[good_pts],fill_value=.001) - # print max(all_pts), max(good_pts) - # phasenew.append(f(all_pts)) - try: - phase_r = interp(all_pts, good_pts, phase_r[good_pts]) - gamma_r = interp(all_pts, good_pts, gamma_r[good_pts]) - except: - "no phase smoothing" - phasenew.append(phase_r) - gammanew.append(gamma_r) - - phase = old_div(np.transpose(phasenew), dt) - - gamma_i = old_div(np.transpose(gammanew), dt) - - amp_n = ( - old_div(np.sqrt(power[:, :, p["y_i"], p["z_i"]]), (kz_max * ky_max * amp)) - ).real - # amp_n = dom_mode[i]['amp_n'] #nt x nx - - # let just look over the nx range - # lnamp = np.log(amp[nt/2:,2:-2]) - try: - lnamp = np.log(amp[old_div(nt, 2) :, :]) - except: - print("some log(0) stuff in basic_info") - - t = dt * np.array(list(range(nt))) # dt matters obviouslyww - r = np.polyfit(t[old_div(nt, 2) :], lnamp, 1, full=True) - - gamma_est = r[0][0] # nx - f0 = np.exp(r[0][1]) # nx - res = r[1] - pad = [0, 0] - # gamma_est = np.concatenate([pad,gamma_est,pad]) - # f0 = np.concatenate([pad,f0,pad]) - # res = np.concatenate([pad,res,pad]) - - # sig = res/np.sqrt((x['nt']-2)) - sig = np.sqrt(old_div(res, (nt - 2))) - # sig0 = sig*np.sqrt(1/(x['nt'])+ ) # who cares - sig1 = sig * np.sqrt(old_div(1.0, (nt * t.var()))) - nt = np.array(nt) - print("shapes ", nt.shape, nt, lnamp.shape, res.shape, gamma_est) - # print r - res = 1 - old_div(res, (nt * lnamp.var(0))) # nx - res[0:2] = 0 - res[-2:] = 0 - - gamma = [gamma_est, sig1, f0, res] - - # gamma_est2 = np.gradient(amp)[0]/(amp[:,:]*dt) - # gamma_w = np.gradient(gamma_est2)[0] - - # gamma_i = np.abs(gamma_w).argmin(0) #index of the minimum for any given run - # for j in range(nx): - # gamma_w[0:max([gamma_i[j],nt/3]),j] = np.average(gamma_w)*100000.0 - - freq = np.array( - weighted_avg_and_std(phase[-10:, :], weights=np.ones(phase[-10:, :].shape)) - ) - - # gamma = weighted_avg_and_std( - # gamma_est2[-5:,:],weights=np.ones(gamma_est2[-5:,:].shape)) - - k = [ - [p["y_i"], p["z_i"]], - [2 * math.pi * float(p["y_i"]) / L_y, 2 * math.pi * p["z_i"] / L_z], - ] - # L_y is normalized - - # simple k def, works in drift-instability fine - # k = [[p['y_i'],p['z_i']], - # [(B/Bp)**-1*2*math.pi*float(p['y_i'])/(L_y),(B/Bp)*2*math.pi*p['z_i']/L_z]] - - # k_r = [[p['y_i'],p['z_i']], - # [(Bp/B)*2*math.pi*float(p['y_i'])/(L_y), - # (B/Bp)*2*math.pi*p['z_i']/L_z]] - - k_r = [ - [p["y_i"], p["z_i"]], - [ - 2 * math.pi * float(p["y_i"]) / (L_y), - (old_div(B, Bp)) * 2 * math.pi * p["z_i"] / L_z - + (old_div(Bt, B)) * 2 * math.pi * float(p["y_i"]) / (L_y), - ], - ] - - # revised - # k_r = [[p['y_i'],p['z_i']], - # [2*math.pi*float(p['y_i'])/(L_y), - # (Bp/B)*2*math.pi*p['z_i']/L_z - # - (Bt/Bp)*2*math.pi*float(p['y_i'])/(L_y)]] - # revised - - # what I think is the most general one, works in drift-instability again - # seems to work for Bz only helimak, now trying Bp = Bt - # k = [[p['y_i'],p['z_i']], - # [((Bp/B)*float(p['y_i'])/(hthe0_n)) + - # 2*np.pi*p['z_i']*np.sqrt(1-(Bp/B)**2)/L_z, - # 2*math.pi*p['z_i']/L_norm - - # float(p['y_i'])*np.sqrt(1-(Bp/B)**2)/(hthe0_n)]] - # k = [[p['y_i'],p['z_i']], - # [((Bp/B)*float(p['y_i'])/(hthe0_n)), - # 2*math.pi*p['z_i']/L_norm]] - # BOTH SEEM TO PRODOCE SAME RESULTS - - # k = [[p['y_i'],p['z_i']], - # [(float(p['y_i'])/(hthe0_n_x)), - # 2*math.pi*float(p['z_i'])/L_norm]] - - dom_mode_db.append( - { - "modeid": i, - "k": k[1], - "gamma": gamma, - "freq": freq, - "amp": amp, - "amp_n": amp_n, - "phase": phase, - "mn": k[0], - "nt": nt, - "k_r": k_r[1], - "gamma_i": gamma_i, - } - ) - - return dom_mode_db - - -# return a 2d array fof boolean values, a very simple boolian filter -def local_maxima(array2d, user_peak, index=False, count=4, floor=0, bug=False): - from operator import itemgetter, attrgetter - - if user_peak == 0: - where = ( - (array2d >= np.roll(array2d, 1, 0)) - & (array2d >= np.roll(array2d, -1, 0)) - & (array2d >= np.roll(array2d, 0, 1)) - & (array2d >= np.roll(array2d, 0, -1)) - & (array2d >= old_div(array2d.max(), 5.0)) - & (array2d > floor * np.ones(array2d.shape)) - & (array2d >= array2d.mean()) - ) - else: # some simpler filter if user indicated some modes - where = array2d > floor - - # ignore the lesser local maxima, throw out anything with a ZERO - if bug == True: - print(array2d, array2d[where.nonzero()], where.nonzero()[0]) - - peaks = list(zip(where.nonzero()[0], where.nonzero()[1], array2d[where.nonzero()])) - - peaks = sorted(peaks, key=itemgetter(2), reverse=True) - - if len(peaks) > count and user_peak == 0: - peaks = peaks[0:count] - - keys = ["y_i", "z_i", "amp"] - - peaks = [dict(list(zip(keys, peaks[x]))) for x in range(len(peaks))] - - return peaks - # return np.array(peak_dic) - - -def weighted_avg_and_std(values, weights): - """ - Returns the weighted average and standard deviation. - - values, weights -- Numpy ndarrays with the same shape. - """ - - if len(values.shape) == 2: - average = np.average(values, 0) # , weights=weights) - variance = old_div( - ( - np.inner( - weights.transpose(), ((values - average) ** 2).transpose() - ).diagonal() - ), - weights.sum(0), - ) - else: - average = np.average(values, weights=weights) - variance = old_div( - np.dot(weights, (values - average) ** 2), weights.sum() - ) # Fast and numerically precise - - return [average, variance] diff --git a/tools/pylib/post_bout/grate2.py b/tools/pylib/post_bout/grate2.py deleted file mode 100644 index 5157863cc2..0000000000 --- a/tools/pylib/post_bout/grate2.py +++ /dev/null @@ -1,52 +0,0 @@ -from __future__ import print_function -from builtins import range - -### -# compute average growth rate bout variable f and plane y -# prints value in plane y and total average -# optional tind excludes initial 'tind' time steps -# Note it masks the values != Inf -### -import numpy as np -from boutdata.collect import collect -from boututils.moment_xyzt import moment_xyzt - - -def avgrate(p, y=None, tind=None): - if tind is None: - tind = 0 - - rmsp_f = moment_xyzt(p, "RMS").rms - - ni = np.shape(rmsp_f)[1] - nj = np.shape(rmsp_f)[2] - - growth = np.zeros((ni, nj)) - - with np.errstate(divide="ignore"): - for i in range(ni): - for j in range(nj): - growth[i, j] = np.gradient(np.log(rmsp_f[tind::, i, j]))[-1] - - d = np.ma.masked_array(growth, np.isnan(growth)) - - # masked arrays - # http://stackoverflow.com/questions/5480694/numpy-calculate-averages-with-nans-removed - - print("Total average growth rate= ", np.mean(np.ma.masked_array(d, np.isinf(d)))) - if y is not None: - print( - "Growth rate in plane", - y, - "= ", - np.mean(np.ma.masked_array(growth[:, y], np.isnan(growth[:, y]))), - ) - - -# test -if __name__ == "__main__": - path = "/Users/brey/BOUT/bout/examples/elm-pb/data" - - data = collect("P", path=path) - - avgrate(data, 32) diff --git a/tools/pylib/post_bout/pb_corral.py b/tools/pylib/post_bout/pb_corral.py deleted file mode 100644 index df9a9b00b6..0000000000 --- a/tools/pylib/post_bout/pb_corral.py +++ /dev/null @@ -1,540 +0,0 @@ -# note - these commands are only run by default in interactive mode -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division -from builtins import filter -from builtins import range -from past.utils import old_div -from builtins import object -import os -import sys - -try: - boutpath = os.environ["BOUT_TOP"] - pylibpath = boutpath + "tools/pylib" - pbpath = pylibpath + "/post_bout" - boutdatapath = pylibpath + "/boutdata" - boututilpath = pylibpath + "/boututils" - - allpath = [boutpath, pylibpath, pbpath, boutdatapath, boututilpath] - [sys.path.append(elem) for elem in allpath] - -except: - print("unable to append needed .py files") - -sys.path.append("/usr/local/pylib") - -import post_bout as post_bout -from .ListDict import ListDictKey, ListDictFilt -from .read_inp import parse_inp, read_inp, read_log -from .basic_info import weighted_avg_and_std -from .read_cxx import read_cxx, findlowpass - - -import os -import numpy as np -import pickle -import subprocess - - -def corral( - cached=True, refresh=False, debug=False, IConly=1, logname="status.log", skew=False -): - print("in corral") - log = read_log(logname=logname) - # done = log['done'] - runs = log["runs"] # a list of all directories, we need this, - # only need 'runs' if the simulation is done - - current = log["current"] # always return the last data_dir - - print(log) - print("current:", current) - - if refresh == True: - for i, path in enumerate(runs): - print(i, path) - a = post_bout.save(path=path, IConly=IConly) # re post-process a run - - elif ( - cached == False - ): # if all the ind. simulation pkl files are in place skip this part - a = post_bout.save(path=current) # save to current dir - # here is really where you shoudl write to status.log - # write_log('status.log', - cached = True - - # if done: - - all_ave = [] - all_modes = [] - print("last_one: ") - for i, val in enumerate(runs): - print(val) - mode_db, ave_db = post_bout.read(path=val) - # alldata.append(array) - all_modes.append(mode_db) - all_ave.append(ave_db) - - # build the end database - - # remove the read in pickle - - # return alldb - def islist(input): - return isinstance(input, list) - - all_modes = list(filter(islist, all_modes)) - # all_ave = filter(islist,all_ave) - - # alldb = sum(alldb,[]) - # alldata = np.array(alldata) - all_modes = sum(all_modes, []) - - nt = [] - for mode in all_modes: - nt.append(len(mode["amp"])) - nt = [max(nt)] - - nt = nt[0] - t = list(range(nt)) - i = 0 - - if debug: - return all_modes, all_ave - else: - return LinRes(all_modes) - - -class LinRes(object): - def __init__(self, all_modes): - self.mode_db = all_modes - self.db = all_modes - # self.ave_db = all_ave - - alldb = self.db - # self.modekeys = data[0]['fields']['Ni']['modes'][0].keys() - # print len(alldb) - - self.meta = np.array(ListDictKey(self.db, "meta"))[0] - - self.keys = list((self.mode_db)[0].keys()) - # self.avekeys = data[0]['fields']['Ni']['ave'].keys() - - # self.nrun = len(alldb) #number of runs - - self.path = np.array(ListDictKey(self.db, "path")) - self.cxx = [] - self.maxN = [] - - self.ave = np.array(ListDictKey(alldb, "ave")) - - # [self.cxx.append(read_cxx(path=elem,boutcxx='2fluid.cxx.ref')) for elem in self.path] - # [self.maxN.append(findlowpass(elem)) for elem in self.cxx] - [ - self.cxx.append(read_cxx(path=elem, boutcxx="physics_code.cxx.ref")) - for elem in self.path - ] - - self.maxZ = np.array(ListDictKey(alldb, "maxZ")) - self.maxN = self.maxZ - # self.maxN = findlowpass(self.cxx) #low pass filt from .cxx - - self.nx = np.array(ListDictKey(alldb, "nx"))[0] - self.ny = np.array(ListDictKey(alldb, "ny"))[0] - self.nz = np.array(ListDictKey(alldb, "nz"))[0] - - # self.nt = int(data[0]['meta']['NOUT']['v']+1) - self.Rxy = np.array(ListDictKey(alldb, "Rxy")) - self.Rxynorm = np.array(ListDictKey(alldb, "Rxynorm")) - self.nt = np.array(ListDictKey(alldb, "nt")) - - self.dt = np.array(ListDictKey(alldb, "dt")) - self.nfields = np.array(ListDictKey(alldb, "nfields")) - - self.field = np.array(ListDictKey(alldb, "field")) - - self.k = np.array(ListDictKey(alldb, "k")) - self.k_r = np.array(ListDictKey(alldb, "k_r")) - - self.mn = np.array(ListDictKey(alldb, "mn")) - - # return ListDictKey(alldb,'phase') - - # self.phase = np.array(ListDictKey(alldb,'phase')) - self.phase = ListDictKey(alldb, "phase") - - # self.amp= np.array(ListDictKey(alldb,'amp')) - # self.amp_n=np.array(ListDictKey(alldb,'amp_n')) - # self.dc= [] - # self.freq = np.array(ListDictKey(alldb,'k')) - # self.gamma = np.array(ListDictKey(alldb,'gamma')) - - self.amp = ListDictKey(alldb, "amp") - self.amp_n = ListDictKey(alldb, "amp_n") - self.dc = [] - # self.freq = np.array(ListDictKey(alldb,'k')) - self.gamma = np.array(ListDictKey(alldb, "gamma")) - self.gamma_i = np.array(ListDictKey(alldb, "gamma_i")) - - self.freq = np.array(ListDictKey(alldb, "freq")) - - self.IC = np.array(ListDictKey(alldb, "IC")) - self.dz = np.array(ListDictKey(alldb, "dz")) - self.meta["dz"] = np.array(list(set(self.dz).union())) - - self.nmodes = self.dz.size - - self.MN = np.array(ListDictKey(alldb, "MN")) - # self.MN = np.float32(self.mn) - # self.MN[:,1] = self.mn[:,1]/self.dz - self.nrun = len(set(self.path).union()) - self.L = np.array(ListDictKey(alldb, "L")) - # self.C_s = - self.modeid = np.array(ListDictKey(alldb, "modeid")) - - self.trans = np.array(ListDictKey(alldb, "transform")) - - if np.any(self.trans): - self.phase_r = ListDictKey(alldb, "phase_r") - self.gamma_r = np.array(ListDictKey(alldb, "gamma_r")) - self.amp_r = ListDictKey(alldb, "amp_r") - self.freq_r = np.array(ListDictKey(alldb, "freq_r")) - - # try: - # self.model(haswak=False) # - # except: - # self.M = 0 - - # try: - try: # analytic model based on simple matrix - self.models = [] - # self.models.append(_model(self)) #create a list to contain models - self.models.append( - _model(self, haswak=True, name="haswak") - ) # another model - # self.models.append(_model(self,haswak=True,name='haswak_0',m=0)) - # for Ln in range(10): - # Lval = 10**((.2*Ln -1)/10) - # #Lval = 10**(Ln-1) - # #Lval = - # print Lval - # self.models.append(_model(self,varL=True,name='varL'+str(Lval),Lval=Lval,haswak=True)) - - except: - self.M = 0 - - try: # analytic models based on user defined complex omega - self.ref = [] - self.ref.append(_ref(self)) - # self.ref.append(_ref(self,haswak=False,name='drift')) - # demand a complex omega to compare - # self.ref.append(_ref(self,haswas=True,name='haswak')) - except: - self.ref = 0 - - # self.models.append(_model(self,haswak2=True,name='haswak2')) - - # except: - # print 'FAIL' - - def _amp(self, tind, xind): - # first select modes that actually have valid (tind,xind) - # indecies - # s = subset(self.db,'modeid',modelist) - return np.array([self.amp[i][tind, xind] for i in range(self.nmodes)]) - - # def model(self,field='Ni',plot=False,haswak=False): - - # #enrich the object - # allk = self.k_r[:,1,self.nx/2] #one location for now - # allkpar = self.k_r[:,0,self.nx/2] #one location for now - - # self.M = [] - # self.eigsys = [] - # self.gammaA = [] - # self.omegaA = [] - # self.eigvec = [] - # self.gammamax = [] - # self.omegamax = [] - - # #allk = np.arange(0.1,100.0,.1) - # #allk= np.sort(list(set(allk).union())) - - # for i,k in enumerate(allk): - # #print i - # #M =np.matrix(np.random.rand(3,3),dtype=complex) - # M = np.zeros([3,3],dtype=complex) - # M[0,0] = 0 - # M[0,1] = k/(self.L[i,self.nx/2,self.ny/2]) - # M[1,0] = (2*np.pi/self.meta['lpar'][self.nx/2])**2 * self.meta['sig_par'][0]*complex(0,k**-2) - # M[1,1]= -(2*np.pi/self.meta['lpar'][self.nx/2])**2 * self.meta['sig_par'][0]*complex(0,k**-2) - - # if haswak: - # M[0,0] = M[0,0] + M[1,1]*complex(0,k**2) - # M[0,1] = M[0,1] + M[1,0]*complex(0,k**2) - - # #if rho_conv: - - # #M[1,0] = (allkpar[i])**2 * self.meta['sig_par'][0]*complex(0,k**-2) - # #M[1,1]= -(allkpar[i])**2 * self.meta['sig_par'][0]*complex(0,k**-2) - - # eigsys= np.linalg.eig(M) - # gamma = (eigsys)[0].imag - # omega =(eigsys)[0].real - # eigvec = eigsys[1] - - # self.M.append(M) - # self.eigsys.append(eigsys) - # self.gammaA.append(gamma) - # self.gammamax.append(max(gamma)) - # where = ((gamma == gamma.max()) & (omega != 0)) - # self.omegamax.append(omega[where[0]]) - # self.eigvec.append(eigvec) - # self.omegaA.append(omega) - - class __model__(object): - def __init__(self): - self.M = 0 - - -class subset(LinRes): - def __init__(self, alldb, key, valuelist, model=False): - selection = ListDictFilt(alldb, key, valuelist) - if len(selection) != 0: - LinRes.__init__(self, selection) - self.skey = key - if model == True: - self.model() - else: - LinRes.__init__(self, alldb) - if model == True: - self.model() - - -# class subset(originClass): -# def __init__(self,alldb,key,valuelist,model=False): -# selection = ListDictFilt(alldb,key,valuelist) -# if len(selection) !=0: -# originClass.__init__(self,selection,input_obj.ave_db) -# self.skey = key -# if model==True: -# self.model() -# else: -# origin.__init__(self,alldb) -# if model==True: -# self.model() - -# not sure if this is the best way . . . - - -# class subset(object): -# def __init__(self,input_obj,key,valuelist,model=False): -# selection = ListDictFilt(input_obj.mode_db,key,valuelist) -# if len(selection) !=0: -# import copy -# self = copy.copy(input_obj) -# self.__init__(selection,input_obj.ave_db) -# self.skey = key -# if model==True: -# self.model() -# else: -# self = input_obj -# if model==True: -# self.model() - - -class _ref(object): # NOT a derived obj, just takes one as a var - def __init__(self, input_obj, name="haswak", haswak=True): - allk = input_obj.k_r[:, 1, old_div(input_obj.nx, 2)] # one location for now - allkpar = input_obj.k_r[:, 0, old_div(input_obj.nx, 2)] # one location for now - self.name = name - - self.gamma = [] - self.omega = [] - self.soln = {} - self.soln["gamma"] = [] - self.soln["freq"] = [] - - for i, k in enumerate(allk): - omega_star = old_div( - -(k), - (input_obj.L[i, old_div(input_obj.nx, 2), old_div(input_obj.ny, 2)]), - ) - - nu = ( - 2 * np.pi / input_obj.meta["lpar"][old_div(input_obj.nx, 2)] - ) ** 2 * input_obj.meta["sig_par"][0] - - if haswak: - omega = old_div(-omega_star, (1 + (k) ** 2)) - gamma = old_div(((k**2) * omega_star**2), (nu * (1 + k**2) ** 3)) - else: - # omega = -np.sqrt(nu*omega_star)/(np.sqrt(2)*k) + nu**(3/2)/(8*np.sqrt(2*omega_star)*k**3) - # gamma = np.sqrt(nu*omega_star)/(np.sqrt(2)*k) - nu/(2* k**2) + nu**(3/2)/(8*np.sqrt(2*omega_star)*k**3) - omega = -omega_star + old_div((2 * k**4 * omega_star**3), nu**2) - gamma = old_div((k * omega_star) ** 2, nu) - ( - 5 * (k**6 * omega_star * 4 / nu**3) - ) - self.gamma.append(gamma) - self.omega.append(omega) - - self.soln["freq"] = np.transpose(np.array(self.omega)) - self.soln["gamma"] = np.transpose(np.array(self.gamma)) - - -class _model(object): # NOT a derived class,but one that takes a class as input - def __init__( - self, - input_obj, - name="drift", - haswak=False, - rho_conv=False, - haswak2=False, - varL=False, - Lval=1.0, - m=1, - ): - allk = input_obj.k_r[:, 1, old_div(input_obj.nx, 2)] # one location for now - allkpar = input_obj.k_r[:, 0, old_div(input_obj.nx, 2)] # one location for now - - # numerical value to compare against - - numgam = input_obj.gamma[:, 0, old_div(input_obj.nx, 2)] - numfreq = input_obj.freq[:, 0, old_div(input_obj.nx, 2)] - self.name = name - - self.M = [] - self.eigsys = [] - self.gammaA = [] - self.omegaA = [] - self.eigvec = [] - self.gammamax = [] - self.omegamax = [] - self.k = [] - self.m = m - - self.soln = {} - self.soln["freq"] = [] - self.soln["gamma"] = [] - self.soln["gammamax"] = [] - self.soln["freqmax"] = [] - - self.chi = {} - self.chi["freq"] = [] - self.chi["gamma"] = [] - - for i, k in enumerate(allk): - # print i - # M =np.matrix(np.random.rand(3,3),dtype=complex) - M = np.zeros([4, 4], dtype=complex) - M[0, 0] = 0 - # k = k/np.sqrt(10) - # L = (input_obj.L)*np.sqrt(10) - - if k == 0: - k = 1e-5 - - # print k {n,phi,v,ajpar} - M[0, 1] = old_div( - k, (input_obj.L[i, old_div(input_obj.nx, 2), old_div(input_obj.ny, 2)]) - ) - M[1, 0] = ( - (2 * m * np.pi / input_obj.meta["lpar"][old_div(input_obj.nx, 2)]) ** 2 - * input_obj.meta["sig_par"][0] - * complex(0, (k) ** -2) - ) - M[1, 1] = ( - -( - (2 * m * np.pi / input_obj.meta["lpar"][old_div(input_obj.nx, 2)]) - ** 2 - ) - * input_obj.meta["sig_par"][0] - * complex(0, (k) ** -2) - ) - - # parallel dynamics - # M[2,2] = k/(input_obj.L[i,input_obj.nx/2,input_obj.ny/2]) - # M[2,0] = -(2*m*np.pi/input_obj.meta['lpar'][input_obj.nx/2]) - # M[0,2] = -(2*m*np.pi/input_obj.meta['lpar'][input_obj.nx/2]) - - # M[1,0] = (2*m*np.pi/input_obj.meta['lpar'][input_obj.nx/2])**2 * input_obj.meta['sig_par'][0] - # M[1,1]= -(2*m*np.pi/input_obj.meta['lpar'][input_obj.nx/2])**2 * input_obj.meta['sig_par'][0] - - # ajpar dynamics - effectively parallel electron dynamics instead of - - if haswak: - M[0, 0] = ( - -( - ( - 2 - * m - * np.pi - / input_obj.meta["lpar"][old_div(input_obj.nx, 2)] - ) - ** 2 - ) - * input_obj.meta["sig_par"][0] - * complex(0, 1) - ) - M[0, 1] = ( - 2 * m * np.pi / input_obj.meta["lpar"][old_div(input_obj.nx, 2)] - ) ** 2 * input_obj.meta["sig_par"][0] * complex(0, 1) + M[0, 1] - - if varL: - M[0, 1] = Lval * M[0, 1] - - if rho_conv: # not used - M[1, 0] = ( - (allkpar[i]) ** 2 - * input_obj.meta["sig_par"][0] - * complex(0, (k) ** -2) - ) - M[1, 1] = ( - -((allkpar[i]) ** 2) - * input_obj.meta["sig_par"][0] - * complex(0, (k) ** -2) - ) - - eigsys = np.linalg.eig(M) - gamma = (eigsys)[0].imag - omega = (eigsys)[0].real - eigvec = eigsys[1] - self.k.append(k) - - self.M.append(M) - self.eigsys.append(eigsys) - - self.gammaA.append(gamma) - self.soln["gamma"].append(gamma) - - self.gammamax.append(max(gamma)) - self.soln["gammamax"].append(max(gamma)) - - where = (gamma == gamma.max()) & (omega != 0) - # if len(where) > 1: - # where = where[0] - self.omegamax.append(omega[where]) - self.soln["freqmax"].append(omega[where]) - - # print k,gamma,where,M,omega - chigam = old_div(((numgam - max(gamma)) ** 2), max(gamma)) - chifreq = old_div(((numfreq - omega[where]) ** 2), omega[where]) - - self.eigvec.append(eigvec) - self.omegaA.append(omega) - self.soln["freq"].append(omega) - - self.chi["freq"].append(chifreq[i]) - - self.chi["gamma"].append(chigam[i]) - - self.dim = M.shape[0] - self.soln["freq"] = np.transpose(np.array(self.soln["freq"])) - self.soln["gamma"] = np.transpose(np.array(self.soln["gamma"])) - self.chi["freq"] = np.transpose(np.array(self.chi["freq"])) - self.chi["gamma"] = np.transpose(np.array(self.chi["gamma"])) - - # self.soln = {} - # self.soln['freq'] = self.omegaA - # self.soln['gamma'] = self.gammaA diff --git a/tools/pylib/post_bout/pb_draw.py b/tools/pylib/post_bout/pb_draw.py deleted file mode 100644 index 272aab9c35..0000000000 --- a/tools/pylib/post_bout/pb_draw.py +++ /dev/null @@ -1,1692 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division -from builtins import str -from builtins import range -from past.utils import old_div - -# some standard analytic stuff to plot, if appending just overplot gam or omeg -from .pb_corral import LinRes -from .ListDict import ListDictKey, ListDictFilt -import numpy as np - -import matplotlib.pyplot as plt -from matplotlib import cm -import matplotlib.artist as artist -import matplotlib.ticker as ticker -import matplotlib.pyplot as plt -import matplotlib.patches as patches -from matplotlib.figure import Figure -from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas -from matplotlib.backends.backend_pdf import PdfPages - -from reportlab.platypus import * -from reportlab.lib.styles import getSampleStyleSheet -from reportlab.rl_config import defaultPageSize -from reportlab.lib.units import inch -from reportlab.graphics.charts.linecharts import HorizontalLineChart -from reportlab.graphics.shapes import Drawing -from reportlab.graphics.charts.lineplots import LinePlot -from reportlab.graphics.widgets.markers import makeMarker -from reportlab.lib import colors - -from replab_x_vs_y import RL_Plot -from matplotlib.ticker import ScalarFormatter, FormatStrFormatter, MultipleLocator - - -class LinResDraw(LinRes): - def __init__(self, alldb): - LinRes.__init__(self, alldb) - - def plottheory( - self, pp, m=1, canvas=None, comp="gamma", field="Ni", allroots=False - ): - if len(self.models) == 0: - try: - self.models = [] - self.models.append(_model(self)) # create a list to contain models - self.models.append( - _model(self, haswak=True, name="haswak") - ) # another model - except: - return 0 - - s = subset(self.db, "field", [field]) - - modelist = [] - - [modelist.append([m, n + 1]) for n in range(min(s.maxN) - 1)] - - s = subset(s.db, "mn", modelist) - - allk = s.k[:, 1, old_div(s.nx, 2)] - ki = np.argsort(allk) - - ownpage = False - if canvas is None: - ownpage = True - - if ownpage: # if not an overplot - fig1 = plt.figure() - canvas = fig1.add_subplot(1, 1, 1) - - label = "gamma analytic" - - # if comp=='gamma': - # y = np.array(s.gammamax)[ki] - # else: - # y = np.array(s.omegamax)[ki] - - for m in s.models: - print(m.name) - - for i, m in enumerate(s.models): - print(m.name, comp, m.soln[comp].shape) - - if allroots: - for elem in m.soln[comp]: - # y = [] - # elem has 2 or more elements - y = (np.array(elem)[ki]).flatten() # n values - # y = y.astype('float') - print(y.shape) - canvas.plot( - (allk[ki]).flatten(), y, ",", label=label, c=cm.jet(0.2 * i) - ) - try: - ymax = (np.array(m.soln[comp + "max"])[ki]).flatten() - # ymax = (np.array(m.gammamax)[ki]).flatten() - ymax = ymax.astype("float") - print(comp, " ymax:", ymax) - canvas.plot( - (allk[ki]).flatten(), ymax, "-", label=label, c=cm.jet(0.2 * i) - ) - # if comp=='gamma': - # y = (np.array(m.gammamax)[ki]).flatten() - - # else: - # y = (np.array(m.omegamax)[ki]).flatten() - - # print m.name, ':' ,y.astype('float') - - except: - print("fail to add theory curve") - - canvas.annotate(m.name, (allk[ki[0]], 1.1 * ymax[0]), fontsize=8) - canvas.annotate(m.name, (1.1 * allk[ki[-1]], 1.1 * ymax[-1]), fontsize=8) - - try: - for i, m in enumerate(s.ref): - if not allroots: - y = (np.array(m.soln[comp])[ki]).flatten() - y = y.astype("float") - canvas.plot( - (allk[ki]).flatten(), - y, - "--", - label=label, - c=cm.jet(0.2 * i), - ) - except: - print("no reference curve") - - if ownpage: # set scales if this is its own plot - # canvas.set_yscale('symlog',linthreshy=1e-13) - # canvas.set_xscale('log') - - canvas.axis("tight") - canvas.set_xscale("log") - canvas.set_yscale("symlog") - fig1.savefig(pp, format="pdf") - plt.close(fig1) - else: # if not plot its probably plotted iwth sim data, print chi somewhere - for i, m in enumerate(s.models): - textstr = r"$\chi^2$" + "$=%.2f$" % (m.chi[comp].sum()) - print(textstr) - # textstr = '$\L=%.2f$'%(m.chi[comp].sum()) - props = dict(boxstyle="square", facecolor="white", alpha=0.3) - textbox = canvas.text( - 0.1, - 0.1, - textstr, - transform=canvas.transAxes, - fontsize=10, - verticalalignment="top", - bbox=props, - ) - - def plotomega( - self, - pp, - canvas=None, - field="Ni", - yscale="linear", - clip=0, - xaxis="t", - xscale="linear", - xrange=1, - comp="gamma", - pltlegend="both", - overplot=False, - gridON=True, - trans=False, - infobox=True, - ): - colors = [ - "b.", - "r.", - "k.", - "c.", - "g.", - "y.", - "m.", - "b.", - "r.", - "k.", - "c.", - "g.", - "y.", - "m.", - ] - colordash = [ - "b", - "r", - "k", - "c", - "g", - "y", - "m", - "b", - "r", - "k", - "c", - "g", - "y", - "m", - ] - - if canvas is None: - ownpage = True - else: - ownpage = False - - if ownpage: - fig1 = plt.figure() - fig1.subplots_adjust(bottom=0.12) - fig1.subplots_adjust(top=0.80) - fig1.subplots_adjust(right=0.83) - fig1.subplots_adjust(left=0.17) - canvas = fig1.add_subplot(1, 1, 1) - clonex = canvas.twinx() - # if trans: - # cloney = canvas.twiny() - - dzhandles = [] - parhandles = [] - parlabels = [] - dzlabels = [] - - m_shift = 1 - for q in np.array(list(range(1))) + m_shift: - s = subset(self.db, "field", [field]) # pick field - maxZ = min(s.maxN) - modelist = [] - [modelist.append([q, p + 1]) for p in range(maxZ - 1)] - print(modelist) - print(q, "in plotgamma") - - s = subset(s.db, "mn", modelist) - - xrange = old_div(s.nx, 2) - 2 - - xrange = [old_div(s.nx, 2), old_div(s.nx, 2) + xrange] - - y = np.array(ListDictKey(s.db, comp)) - - # y = s.gamma #nmodes x 2 x nx ndarray - k = s.k ##nmodes x 2 x nx ndarray k_zeta - - kfactor = np.mean( - old_div(s.k_r[:, 1, old_div(s.nx, 2)], s.k[:, 1, old_div(s.nx, 2)]) - ) # good enough for now - - print( - k[:, 1, old_div(s.nx, 2)].shape, - y[:, 0, old_div(s.nx, 2)].shape, - len(colors), - ownpage, - ) # ,k[:,1,s.nx/2],y[:,0,s.nx/2] - - parhandles.append( - canvas.errorbar( - np.squeeze(k[:, 1, old_div(s.nx, 2)]), - np.squeeze(y[:, 0, old_div(s.nx, 2)]), - yerr=np.squeeze(y[:, 1, old_div(s.nx, 2)]), - fmt=colors[q], - ) - ) - - parlabels.append("m " + str(q)) - - # loop over dz sets and connect with dotted line . . . - jj = 0 - - ymin_data = np.max(np.array(ListDictKey(s.db, comp))) - ymax_data = 0 # for bookeeping - - for p in list(set(s.path).union()): - print(p, "in plotomega") - - sub_s = subset(s.db, "path", [p]) - j = sub_s.dz[0] - # print sub_s.amp.shape - s_i = np.argsort(sub_s.mn[:, 1]) # sort by 'local' m, global m is ok also - # print s_i, sub_s.mn, sub_s.nx, jj - y = np.array(ListDictKey(sub_s.db, comp)) - y_alt = 2.0 * np.array(ListDictKey(sub_s.db, comp)) - - k = sub_s.k ## - if q == m_shift: # fix the parallel mode - dzhandles.append( - canvas.plot( - k[s_i, 1, old_div(sub_s.nx, 2)], - y[s_i, 0, old_div(sub_s.nx, 2)], - color=colordash[jj], - alpha=0.5, - ) - ) - # clonex.plot(k[s_i,1,sub_s.nx/2], - # y_alt[s_i,0,sub_s.nx/2],color=colordash[jj],alpha=.5) - # if np.any(sub_s.trans) and trans: - # comp_r = comp+'_r' - # k_r = sub_s.k_r - # y2 = np.array(ListDictKey(sub_s.db,comp_r)) - # cloney.plot(k[s_i,1,sub_s.nx/2], - # y2[s_i,0,sub_s.nx/2],'k.',ms = 3) - - ymin_data = np.min([np.min(y[s_i, 0, old_div(sub_s.nx, 2)]), ymin_data]) - ymax_data = np.max([np.max(y[s_i, 0, old_div(sub_s.nx, 2)]), ymax_data]) - - print("dzhandle color", jj) - # dzlabels.append("DZ: "+ str(2*j)+r'$\pi$') - dzlabels.append(j) - - if yscale == "log": - factor = 10 - else: - factor = 2 - print("annotating") - canvas.annotate( - str(j), - ( - k[s_i[0], 1, old_div(sub_s.nx, 2)], - y[s_i[0], 0, old_div(sub_s.nx, 2)], - ), - fontsize=8, - ) - p = canvas.axvspan( - k[s_i[0], 1, old_div(sub_s.nx, 2)], - k[s_i[-1], 1, old_div(sub_s.nx, 2)], - facecolor=colordash[jj], - alpha=0.01, - ) - print("done annotating") - else: - canvas.plot( - k[s_i, 1, old_div(sub_s.nx, 2)], - y[s_i, 0, old_div(sub_s.nx, 2)], - color=colordash[jj], - alpha=0.3, - ) - - jj = jj + 1 - - dzhandles = np.array(dzhandles).flatten() - dzlabels = np.array(dzlabels).flatten() - - dzlabels = list(set(dzlabels).union()) - - dz_i = np.argsort(dzlabels) - - dzhandles = dzhandles[dz_i] - dzlabels_cp = np.array(dzlabels)[dz_i] - - print(type(dzlabels), np.size(dzlabels)) - for i in range(np.size(dzlabels)): - dzlabels[i] = "DZ: " + str(dzlabels_cp[i]) # +r"$\pi$" - - parlabels = np.array(parlabels).flatten() - - # if pltlegend =='both': # - - print("legends") - - # l1 = legend(parhandles,parlabels,loc = 3,prop={'size':6}) - # l2 = legend(dzhandles,dzlabels,loc = 1,prop={'size':6}) - # plt.gca().add_artist(l1) - - # else: - # legend(dzhandles,dzlabels,loc=3,prop={'size':6}) - if overplot == True: - try: - self.plottheory(pp, canvas=canvas, comp=comp, field=field) - # self.plottheory(pp,comp=comp) - except: - print("no theory plot") - if infobox: - textstr = "$\L_{\parallel}=%.2f$\n$\L_{\partial_r n}=%.2f$\n$B=%.2f$" % ( - s.meta["lpar"][old_div(s.nx, 2)], - s.meta["L"][old_div(s.nx, 2), old_div(s.ny, 2)], - s.meta["Bpxy"]["v"][old_div(s.nx, 2), old_div(s.ny, 2)], - ) - props = dict(boxstyle="square", facecolor="white", alpha=0.3) - textbox = canvas.text( - 0.82, - 0.95, - textstr, - transform=canvas.transAxes, - fontsize=10, - verticalalignment="top", - bbox=props, - ) - # leg = canvas.legend(handles,labels,ncol=2,loc='best',prop={'size':4},fancybox=True) - # textbox.get_frame().set_alpha(0.3) - # matplotlib.patches.Rectangle - # p = patches.Rectangle((0, 0), 1, 1, fc="r") - # p = str('L_par') - # leg = canvas.legend([p], ["Red Rectangle"],loc='best',prop={'size':4}) - # leg.get_frame().set_alpha(0.3) - - # cloney.set_xlim(xmin,xmax) - try: - canvas.set_yscale(yscale) - canvas.set_xscale(xscale) - - if yscale == "symlog": - canvas.set_yscale(yscale, linthreshy=1e-13) - if xscale == "symlog": - canvas.set_xscale(xscale, linthreshy=1e-13) - - if gridON: - canvas.grid() - except: - try: - canvas.set_yscale("symlog") - except: - print("scaling failed completely") - - # print '[xmin, xmax, ymin, ymax]: ',[xmin, xmax, ymin, ymax] - - clonex.set_yscale(yscale) # must be called before limits are set - - try: - if yscale == "linear": - formatter = ticker.ScalarFormatter() - formatter.set_powerlimits((-2, 2)) # force scientific notation - canvas.yaxis.set_major_formatter(formatter) - clonex.yaxis.set_major_formatter(formatter) - # canvas.useOffset=False - except: - print("fail 1") - [xmin, xmax, ymin, ymax] = canvas.axis() - - if yscale == "symlog": - clonex.set_yscale(yscale, linthreshy=1e-9) - if xscale == "symlog": - clonex.set_xscale(xscale, linthreshy=1e-9) - # if np.any(s.trans) and trans: - [xmin1, xmax1, ymin1, ymax1] = canvas.axis() - if trans: - try: - cloney = canvas.twiny() - # cloney.set_yscale(yscale) - cloney.set_xscale(xscale) - [xmin1, xmax1, ymin2, ymax2] = canvas.axis() - - if xscale == "symlog": - cloney.set_xscale(xscale, linthreshy=1e-9) - if yscale == "symlog": - cloney.set_yscale(yscale, linthreshy=1e-9) - if yscale == "linear": - cloney.yaxis.set_major_formatter(formatter) - except: - print("fail trans") - # cloney.useOffset=False - - # if xscale =='symlog' and trans: - # cloney.set_yscale(yscale,linthreshy=1e-9) - # cloney.set_xscale(xscale,linthreshy=1e-9) - - Ln_drive_scale = s.meta["w_Ln"][0] ** -1 - # Ln_drive_scale = 2.1e3 - clonex.set_ylim(Ln_drive_scale * ymin, Ln_drive_scale * ymax) - - try: - if trans: - # k_factor = #scales from k_zeta to k_perp - cloney.set_xlim(kfactor * xmin, kfactor * xmax) - # if np.any(sub_s.trans) and trans: - # comp_r = comp+'_r' - # y2 = np.array(ListDictKey(sub_s.db,comp_r)) - # #canvas.plot(k[s_i,1,sub_s.nx/2], - # # y2[s_i,0,sub_s.nx/2],'k.',ms = 3) - - # cloney.plot(k_r[s_i,1,sub_s.nx/2], - # y2[s_i,0,sub_s.nx/2],'k.',ms = 3) - # print 'np.sum(np.abs(y-y2)): ',np.sum(np.abs(y-y2)),comp_r - # kfactor =1.0 - # cloney.set_xlim(xmin,xmax) - cloney.set_ylim( - ymin, ymax - ) # because cloney shares the yaxis with canvas it may overide them, this fixes that - cloney.set_xlabel(r"$k_{\perp} \rho_{ci}$", fontsize=18) - except: - print("moar fail") - # clonex.set_xscale(xscale) - - # except: - # #canvas.set_xscale('symlog', linthreshx=0.1) - # print 'extra axis FAIL' - - # if yscale == 'linear': - # canvas.yaxis.set_major_locator(ticker.LinearLocator(numticks=8)) - - # minorLocator = MultipleLocator(.005) - # canvas.yaxis.set_minor_locator(minorLocator) - # spawn another y label - - # clone = canvas.twinx() - # s2 = np.sin(2*np.pi*t) - # ax2.plot(x, s2, 'r.') - - # ion_acoust_str = r"$\frac{c_s}{L_{\partial_r n}}}$" - - if comp == "gamma": - canvas.set_ylabel( - r"$\frac{\gamma}{\omega_{ci}}$", fontsize=18, rotation="horizontal" - ) - clonex.set_ylabel( - r"$\frac{\gamma}{\frac{c_s}{L_n}}$", - color="k", - fontsize=18, - rotation="horizontal", - ) - if comp == "freq": - canvas.set_ylabel( - r"$\frac{\omega}{\omega_{ci}}$", fontsize=18, rotation="horizontal" - ) - clonex.set_ylabel( - r"$\frac{\omega}{\frac{c_s}{L_n}}$", - color="k", - fontsize=18, - rotation="horizontal", - ) - - if comp == "amp": - canvas.set_ylabel(r"$A_k$", fontsize=18, rotation="horizontal") - clonex.set_ylabel( - r"$\frac{A_k}{A_{max}}$", color="k", fontsize=18, rotation="horizontal" - ) - - canvas.set_xlabel(r"$k_{\zeta} \rho_{ci}$", fontsize=18) - - title = comp + " computed from " + field - # canvas.set_title(title,fontsize=14) - fig1.suptitle(title, fontsize=14) - - if ownpage: - try: - fig1.savefig(pp, format="pdf") - except: - print("pyplt doesnt like you") - plt.close(fig1) - - def plotfreq( - self, pp, field="Ni", clip=0, xaxis="t", xscale="linear", yscale="linear" - ): - # colors = ['b','g','r','c','m','y','k','b','g','r','c','m','y','k'] - colors = [ - "b.", - "g.", - "r.", - "c.", - "m.", - "y.", - "k.", - "b.", - "g.", - "r.", - "c.", - "m.", - "y", - "k", - ] - plt.figure() - - # s = subset(self.db,'field',[field]) #pick field - - for q in range(4): - s = subset(self.db, "field", [field]) # pick field across all dz sets - modelist = [] - [modelist.append([q + 1, p + 1]) for p in range(5)] - print(q, "in plotgamma") - s = subset(s.db, "mn", modelist) - - gamma = s.freq # nmodes x 2 x nx ndarray - k = s.k ##nmodes x 2 x nx ndarray - - plt.errorbar( - k[:, 1, old_div(s.nx, 2)], - gamma[:, 0, old_div(s.nx, 2)], - yerr=gamma[:, 1, old_div(s.nx, 2)], - fmt=colors[q], - ) - plt.plot( - k[:, 1, old_div(s.nx, 2)], - gamma[:, 0, old_div(s.nx, 2)], - "k:", - alpha=0.3, - ) - - # loop over dz sets and connect with dotted line . . . - for j in list(set(s.dz).union()): - # print j,len(s.mn) - sub_s = subset(s.db, "dz", [j]) - gamma = sub_s.gamma - k = sub_s.k ## - plt.plot( - k[:, 1, old_div(sub_s.nx, 2)], - gamma[:, 0, old_div(sub_s.nx, 2)], - "k:", - alpha=0.1, - ) - - try: - plt.yscale(yscale) - except: - print("yscale fail") - - try: - plt.xscale(yscale) - except: - plt.xscale("symlog") - plt.xlabel(r"$k \rho_{ci}$", fontsize=14) - plt.ylabel(r"$\frac{\omega}{\omega_{ci}}$", fontsize=14) - # plt.title(r'$\frac{\omega}\{\omega_{ci}}$ '+ 'computed from'+field+ 'field',fontsize=10) - - plt.savefig(pp, format="pdf") - plt.close() - - def plotgamma( - self, - pp, - field="Ni", - yscale="symlog", - clip=0, - xaxis="t", - xscale="linear", - xrange=1, - comp="gamma", - overplot=False, - trans=True, - ): - self.plotomega( - pp, - field=field, - yscale=yscale, - clip=clip, - xaxis=xaxis, - xscale=xscale, - xrange=xrange, - comp=comp, - overplot=overplot, - trans=trans, - ) - - def plotfreq2( - self, - pp, - field="Ni", - yscale="symlog", - clip=0, - xaxis="t", - xscale="linear", - xrange=1, - comp="freq", - overplot=False, - trans=True, - ): - self.plotomega( - pp, - field=field, - yscale=yscale, - clip=clip, - xaxis=xaxis, - xscale=xscale, - xrange=xrange, - comp=comp, - overplot=overplot, - trans=trans, - ) - - def plotvsK( - self, - pp, - rootfig=None, - field="Ni", - yscale="log", - clip=0, - xaxis="t", - xscale="linear", - xrange=1, - comp="amp", - pltlegend="both", - overplot=False, - gridON=True, - trans=False, - infobox=True, - m=1, - t=[0], - file=None, - save=True, - ): - colors = [ - "b.", - "r.", - "k.", - "c.", - "g.", - "y.", - "m.", - "b.", - "r.", - "k.", - "c.", - "g.", - "y.", - "m.", - ] - colordash = [ - "b", - "r", - "k", - "c", - "g", - "y", - "m", - "b", - "r", - "k", - "c", - "g", - "y", - "m", - ] - - if rootfig is None: - ownpage = True - else: - ownpage = False - - if ownpage: - fig1 = plt.figure() - fig1.subplots_adjust(bottom=0.12) - fig1.subplots_adjust(top=0.80) - fig1.subplots_adjust(right=0.83) - fig1.subplots_adjust(left=0.17) - canvas = fig1.add_subplot(1, 1, 1) - clonex = canvas.twinx() - # if trans: - # cloney = canvas.twiny() - else: - canvas = rootfig.add_subplot(1, 1, 1) - - dzhandles = [] - parhandles = [] - parlabels = [] - dzlabels = [] - - # pick the modes - m_shift = m - for q in np.array(list(range(1))) + m_shift: - s = subset(self.db, "field", [field]) # pick field - maxZ = min(s.maxN) - modelist = [] - [modelist.append([q, p + 1]) for p in range(maxZ - 1)] - # print q,'in plotgamma' - s = subset(s.db, "mn", modelist) - - # set x-range - xrange = old_div(s.nx, 2) - 2 - xrange = [old_div(s.nx, 2), old_div(s.nx, 2) + xrange] - - # pull up the data - y = np.array(ListDictKey(s.db, comp)) - print("y.shape", y.shape) - - # in case multiple timesteps are indicated - all_y = [] - all_yerr = [] - if comp == "amp": - for elem in t: - all_y.append(np.squeeze(y[:, elem, :])) - all_yerr.append(np.squeeze(0 * y[:, elem, :])) - ynorm = np.max(all_y) - # all_y = np.array(np.squeeze(all_y)) - # all_yerr = np.array(np.squeeze(all_yerr)) - - else: - all_y.append(np.squeeze(y[:, 0, :])) - all_yerr.append(np.squeeze(y[:, 1, :])) - ynorm = s.meta["w_Ln"][0] - - k = s.k ##nmodes x 2 x nx ndarray k_zeta - - kfactor = np.mean( - old_div(s.k_r[:, 1, old_div(s.nx, 2)], s.k[:, 1, old_div(s.nx, 2)]) - ) # good enough for now - - for elem in range(np.size(t)): - # print 'printing line' , elem - errorline = parhandles.append( - canvas.errorbar( - k[:, 1, old_div(s.nx, 2)], - all_y[elem][:, old_div(s.nx, 2)], - yerr=all_yerr[elem][:, old_div(s.nx, 2)], - fmt=colors[q], - ) - ) - - parlabels.append("m " + str(q)) - - # loop over dz sets and connect with dotted line . . . - jj = 0 # will reference dz color - - ymin_data = np.max(np.array(ListDictKey(s.db, comp))) - ymax_data = 0 # for bookeeping - - for p in list(set(s.path).union()): - sub_s = subset(s.db, "path", [p]) - j = sub_s.dz[0] - # print sub_s.amp.shape - s_i = np.argsort(sub_s.mn[:, 1]) # sort by 'local' m, global m is ok also - # print s_i, sub_s.mn, sub_s.nx, jj - y = np.array(ListDictKey(sub_s.db, comp)) - y_alt = 2.0 * np.array(ListDictKey(sub_s.db, comp)) - all_y = [] - all_yerr = [] - if comp == "amp": - for elem in t: - all_y.append(np.squeeze(y[:, elem, :])) - all_yerr.append(np.squeeze(0 * y[:, elem, :])) - else: - all_y.append(np.squeeze(y[:, 0, :])) - all_yerr.append(np.squeeze(y[:, 1, :])) - - k = sub_s.k ## - - for elem in range(np.size(t)): - if q == m_shift: # fix the parallel mode - dzhandles.append( - canvas.plot( - k[s_i, 1, old_div(sub_s.nx, 2)], - all_y[elem][s_i, old_div(sub_s.nx, 2)], - color=colordash[jj], - alpha=0.5, - ) - ) - - ymin_data = np.min( - [np.min(y[s_i, old_div(sub_s.nx, 2)]), ymin_data] - ) - ymax_data = np.max( - [np.max(y[s_i, old_div(sub_s.nx, 2)]), ymax_data] - ) - - dzlabels.append(j) - - if yscale == "log": - factor = 10 - else: - factor = 2 - # print 'annotating' - canvas.annotate( - str(j), - ( - k[s_i[0], 1, old_div(sub_s.nx, 2)], - y[elem][s_i[0], old_div(sub_s.nx, 2)], - ), - fontsize=8, - ) - # p = canvas.axvspan(k[s_i[0],1,sub_s.nx/2], k[s_i[-1],1,sub_s.nx/2], - # facecolor=colordash[jj], alpha=0.01) - print("done annotating") - else: - canvas.plot( - k[s_i, 1, old_div(sub_s.nx, 2)], - y[elem][s_i, old_div(sub_s.nx, 2)], - color=colordash[jj], - alpha=0.3, - ) - - jj = jj + 1 - - dzhandles = np.array(dzhandles).flatten() - dzlabels = np.array(dzlabels).flatten() - - dzlabels = list(set(dzlabels).union()) - - dz_i = np.argsort(dzlabels) - - dzhandles = dzhandles[dz_i] - dzlabels_cp = np.array(dzlabels)[dz_i] - - # print type(dzlabels), np.size(dzlabels) - for i in range(np.size(dzlabels)): - dzlabels[i] = "DZ: " + str(dzlabels_cp[i]) # +r"$\pi$" - - parlabels = np.array(parlabels).flatten() - - if overplot == True: - try: - self.plottheory(pp, canvas=canvas, comp=comp, field=field) - # self.plottheory(pp,comp=comp) - except: - print("no theory plot") - if infobox: - textstr = "$\L_{\parallel}=%.2f$\n$\L_{\partial_r n}=%.2f$\n$B=%.2f$" % ( - s.meta["lpar"][old_div(s.nx, 2)], - s.meta["L"][old_div(s.nx, 2), old_div(s.ny, 2)], - s.meta["Bpxy"]["v"][old_div(s.nx, 2), old_div(s.ny, 2)], - ) - props = dict(boxstyle="square", facecolor="white", alpha=0.3) - textbox = canvas.text( - 0.82, - 0.95, - textstr, - transform=canvas.transAxes, - fontsize=10, - verticalalignment="top", - bbox=props, - ) - # leg = canvas.legend(handles,labels,ncol=2,loc='best',prop={'size':4},fancybox=True) - - # cloney.set_xlim(xmin,xmax) - try: - canvas.set_yscale(yscale) - canvas.set_xscale(xscale) - - if yscale == "symlog": - canvas.set_yscale(yscale, linthreshy=1e-13) - if xscale == "symlog": - canvas.set_xscale(xscale, linthreshy=1e-13) - - if gridON: - canvas.grid() - except: - try: - canvas.set_yscale("symlog") - except: - print("scaling failed completely") - - ################################################################## - - if ownpage and rootfig is None: - clonex.set_yscale(yscale) # must be called before limits are set - - try: - if yscale == "linear": - formatter = ticker.ScalarFormatter() - formatter.set_powerlimits((-2, 2)) # force scientific notation - canvas.yaxis.set_major_formatter(formatter) - clonex.yaxis.set_major_formatter(formatter) - # canvas.useOffset=False - except: - print("fail 1") - [xmin, xmax, ymin, ymax] = canvas.axis() - - if yscale == "symlog": - clonex.set_yscale(yscale, linthreshy=1e-9) - if xscale == "symlog": - clonex.set_xscale(xscale, linthreshy=1e-9) - # if np.any(s.trans) and trans: - [xmin1, xmax1, ymin1, ymax1] = canvas.axis() - if trans: - try: - cloney = canvas.twiny() - # cloney.set_yscale(yscale) - cloney.set_xscale(xscale) - [xmin1, xmax1, ymin2, ymax2] = canvas.axis() - - if xscale == "symlog": - cloney.set_xscale(xscale, linthreshy=1e-9) - if yscale == "symlog": - cloney.set_yscale(yscale, linthreshy=1e-9) - if yscale == "linear": - cloney.yaxis.set_major_formatter(formatter) - except: - print("fail trans") - - Ln_drive_scale = s.meta["w_Ln"][0] ** -1 - # Ln_drive_scale = 2.1e3 - # clonex.set_ylim(Ln_drive_scale*ymin, Ln_drive_scale*ymax) - clonex.set_ylim(ynorm**-1 * ymin, ynorm**-1 * ymax) - - try: - if trans: - # k_factor = #scales from k_zeta to k_perp - cloney.set_xlim(kfactor * xmin, kfactor * xmax) - - cloney.set_ylim( - ymin, ymax - ) # because cloney shares the yaxis with canvas it may overide them, this fixes that - cloney.set_xlabel(r"$k_{\perp} \rho_{ci}$", fontsize=18) - except: - print("moar fail") - # clonex.set_xscale(xscale) - - # ion_acoust_str = r"$\frac{c_s}{L_{\partial_r n}}}$" - - if comp == "gamma": - canvas.set_ylabel( - r"$\frac{\gamma}{\omega_{ci}}$", fontsize=18, rotation="horizontal" - ) - clonex.set_ylabel( - r"$\frac{\gamma}{\frac{c_s}{L_n}}$", - color="k", - fontsize=18, - rotation="horizontal", - ) - if comp == "freq": - canvas.set_ylabel( - r"$\frac{\omega}{\omega_{ci}}$", fontsize=18, rotation="horizontal" - ) - clonex.set_ylabel( - r"$\frac{\omega}{\frac{c_s}{L_n}}$", - color="k", - fontsize=18, - rotation="horizontal", - ) - - if comp == "amp": - canvas.set_ylabel(r"$A_k$", fontsize=18, rotation="horizontal") - clonex.set_ylabel( - r"$\frac{A_k}{A_{max}}$", - color="k", - fontsize=18, - rotation="horizontal", - ) - - canvas.set_xlabel(r"$k_{\zeta} \rho_{ci}$", fontsize=18) - - title = comp + " computed from " + field - # canvas.set_title(title,fontsize=14) - fig1.suptitle(title, fontsize=14) - - if not ownpage: - print("probably for a movie") - fig1 = rootfig - # canvasjunk = fig1.add_subplot(1,1,1) - # canvasjunk = canvas - - if save: - if file is None: - try: - fig1.savefig(pp, format="pdf") - except: - print("pyplt doesnt like you") - else: - try: - fig1.savefig(file, dpi=200) - except: - print("no movie for you ;(") - - if ownpage: - # fig1.close() - plt.close(fig1) - - def plotmodes( - self, - pp, - field="Ni", - comp="amp", - math="1", - ylim=1, - yscale="symlog", - clip=False, - xaxis="t", - xscale="linear", - xrange=1, - debug=False, - yaxis=r"$\frac{Ni}{Ni_0}$", - linestyle="-", - summary=True, - ): - Nplots = self.nrun - - colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] - styles = ["^", "s"] - - fig1 = plt.figure() - - fig2 = plt.figure() - - Modes = subset(self.db, "field", [field]) # pick field - - adj = fig2.subplots_adjust(hspace=0.4, wspace=0.4) - fig2.suptitle("Dominant mode " + comp + " for " + field) - props = dict(alpha=0.8, edgecolors="none") - - allcurves = fig1.add_subplot(1, 1, 1) - fig1.suptitle("Dominant mode behavior for " + field) - - modenames = [] - k = 0 - - for j in list(set(Modes.path).union()): # - s = subset(Modes.db, "path", [j]) # pick run - dz = s.dz[0] - xr = list( - range( - old_div(s.nx, 2) - old_div(xrange, 2), - old_div(s.nx, 2) + old_div(xrange, 2) + 1, - ) - ) - data = np.array( - ListDictKey(s.db, comp) - ) # pick component should be ok for a fixed dz key - - data = data # + 1e-32 #hacky way to deal with buggy scaling - ax = fig2.add_subplot(round(old_div(Nplots, 3.0) + 1.0), 3, k + 1) - - ax.grid(True, linestyle="-", color=".75") - handles = [] - # modenames.append(str(j)) - - # find the "biggest" mode for this dz - d = data[:, s.nt[0] - 1, :] # nmode X nx array - # d = s.gamma[:,2,:] - where = d == np.nanmax(d) - z = where.nonzero() # mode index and n index - imax = z[0][0] - # xi_max = z[1][0] - xi_max = old_div(s.nx, 2) - - if debug and yscale == "log": - gamma = np.array(ListDictKey(s.db, "gamma")) # nmodes x 2 x nx - - for i in range(s.nmodes): - if math == "gamma": - out = old_div(np.gradient(data[i, :, xr])[1], data[i, :, xr]) - else: - out = data[i, 2:, xi_max] # skip the first 2 points - - if xaxis == "t": - # print 'out.size', out.size, out.shape - x = np.array(list(range(out.size))) - # plt.plot(x,out.flatten(),c=colors[k]) - label = str(s.mn[i]) - # handles.append(ax.plot(x,out.flatten(), - # c=cm.jet(1.*k),label = label)) - ax.plot( - x, out.flatten(), c=cm.jet(0.2 * i), label=label, linestyle="-" - ) - - else: - x = np.array(ListDictKey(s.db, xaxis))[i, :, xr] - # x #an N? by nx array - print(x[:, 1], out[:, 0]) - plt.scatter(x[:, 1], out[:, 0]) # ,c=colors[k]) - ax.scatter( - x[:, 1], out[:, 0] - ) # ,c=colors[k])#,alpha = (1 +i)/s.nmodes) - - # detect error (bar data - print("error bars:", x, out) - - # ax.legend(handles,labels,loc='best',prop={'size':6}) - - formatter = ticker.ScalarFormatter() - formatter.set_powerlimits((0, 0)) - ax.xaxis.set_major_formatter(formatter) - # ax.axis('tight') - if yscale == "linear": - ax.yaxis.set_major_formatter(formatter) - if yscale == "symlog": - ax.set_yscale("symlog", linthreshy=1e-13) - else: - try: - ax.set_yscale(yscale) - except: - print("may get weird axis") - ax.set_yscale("symlog") - # if comp=='phase' or yscale=='linear': - # ax.set_xscale('symlog',linthreshx=1.0) - - ax.set_xscale(xscale) - - ax.axis("tight") - artist.setp(ax.axes.get_xticklabels(), fontsize=6) - artist.setp(ax.axes.get_yticklabels(), fontsize=8) - # artist.setp(ax.axes.get_yscale(), fontsize=8) - ax.set_title(str(dz), fontsize=10) - ax.set_xlabel(xaxis) - handles, labels = ax.get_legend_handles_labels() - leg = ax.legend( - handles, labels, ncol=2, loc="best", prop={"size": 4}, fancybox=True - ) - leg.get_frame().set_alpha(0.3) - # x = s.Rxy[imax,:,s.ny/2] - - t0 = 2 - if clip == True: - t0 = round(old_div(s.nt[0], 3)) - y = np.squeeze(data[imax, t0:, xi_max]) - x = np.array(list(range(y.size))) - - print(imax, xi_max) - - label = ( - str([round(elem, 3) for elem in s.MN[imax]]) - + str(s.mn[imax]) - + " at x= " - + str(xi_max) - + " ," - + str( - round( - old_div(s.gamma[imax, 2, xi_max], s.gamma[imax, 0, xi_max]), 3 - ) - ) - + "% " - + str(round(s.gamma[imax, 0, xi_max], 4)) - ) - - short_label = str(dz) - print(short_label, x.shape, y.shape) - allcurves.plot(x, y, ".", c=cm.jet(1.0 * k / len(x)), label=label) - # print len(x), k*len(x)/(Nplots+2),s.nrun - allcurves.annotate( - short_label, - (x[k * len(x) / (Nplots + 1)], y[k * len(x) / (Nplots + 1)]), - fontsize=8, - ) - - # modenames.append(str([round(elem,3) for elem in s.MN[imax]]) - # +str(s.mn[imax])+' at x= '+str(xi_max)+' ,'+str(s.gamma[imax,2,xi_max])) - - if debug and yscale == "log": - gam = gamma[imax, 0, xi_max] - f0 = gamma[imax, 1, xi_max] - allcurves.plot(x, f0 * np.exp(gam * s.dt[imax] * x), "k:") - - k += 1 - - # if ylim: - # allcurves.set_ylim(data[,xi_max].min(),5*data[:,xi_max].max()) - - fig2.savefig(pp, format="pdf") - - handles, labels = allcurves.get_legend_handles_labels() - allcurves.legend(handles, labels, loc="best", prop={"size": 6}) - # allcurves.legend(modenames,loc='best',prop={'size':6}) - allcurves.set_title( - field + " " + comp + ", all runs, " + yscale + " yscale", fontsize=10 - ) - allcurves.set_ylabel(yaxis) - allcurves.set_xlabel(xaxis) - - if yscale == "linear": - allcurves.yaxis.set_major_formatter(formatter) - else: - try: - allcurves.set_yscale(yscale) - except: - print("may get weird axis scaling") - if yscale == "log": - allcurves.axis("tight") - # allcurves.set_ylim(data.min(),data.max()) - # allcurves.set_yscale(yscale,nonposy='mask') - - # plt.xscale(xscale) - - # plt.legend(modenames,loc='best') - if summary: - fig1.savefig(pp, format="pdf") - plt.close(fig1) - - plt.close(fig2) - - # except: - # print "Sorry you fail" - - def plotradeigen( - self, pp, field="Ni", comp="amp", yscale="linear", xscale="linear" - ): - Nplots = self.nrun - colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] - fig1 = plt.figure() - - fig2 = plt.figure() - adj = fig2.subplots_adjust(hspace=0.4, wspace=0.4) - - # canvas = FigureCanvas(fig) - - Modes = subset(self.db, "field", [field]) - - k = 0 - fig2.suptitle("Dominant mode behavior for " + field) - props = dict(alpha=0.8, edgecolors="none") - - allcurves = fig1.add_subplot(1, 1, 1) - fig1.suptitle("Dominant mode behavior for " + field) - - modeleg = [] - - for p in list(set(Modes.path).union()): - print(p) - s = subset(Modes.db, "path", [p]) # pick run - # data = np.array(ListDictKey(s.db,comp)) #pick component - j = s.dz[0] - ax = fig2.add_subplot(round(old_div(Nplots, 3.0) + 1.0), 3, k + 1) - ax.grid(True, linestyle="-", color=".75") - data = np.array(ListDictKey(s.db, comp)) # pick component - handles = [] - - # find the "biggest" mode for this dz - d = data[:, s.nt[0] - 1, :] # nmode X nx array - where = d == d.max() - z = where.nonzero() # mode index and n index - imax = z[0][0] - modeleg.append( - str([round(elem, 3) for elem in s.MN[imax]]) + str(s.mn[imax]) - ) - - # str(s.k[:,1,:][z]) - - for i in range(s.nmodes): - # out = mode[mode.ny/2,:] - # print i,s.Rxynorm.shape,s.ny - x = np.squeeze(s.Rxynorm[i, :, old_div(s.ny, 2)]) - y = data[i, s.nt[0] - 1, :] - - handles.append(ax.plot(x, y, c=cm.jet(1.0 * k / len(x)))) - - formatter = ticker.ScalarFormatter() - formatter.set_powerlimits((0, 0)) - ax.xaxis.set_major_formatter(formatter) - - if yscale == "linear": - ax.yaxis.set_major_formatter(formatter) - else: - ax.set_yscale(yscale) - - artist.setp(ax.axes.get_xticklabels(), fontsize=6) - artist.setp(ax.axes.get_yticklabels(), fontsize=8) - # artist.setp(ax.axes.get_yscale(), fontsize=8) - ax.set_title(str(j), fontsize=10) - - x = np.squeeze(s.Rxynorm[imax, :, old_div(s.ny, 2)]) - y = data[imax, s.nt[0] - 1, :] - # allcurves.plot(x,y,c= colors[k]) - allcurves.plot(x, y, c=cm.jet(0.1 * k / len(x))) - print(k) - k = k + 1 - - fig2.savefig(pp, format="pdf") - if yscale == "linear": - allcurves.yaxis.set_major_formatter(formatter) - else: - allcurves.set_yscale(yscale) - try: - allcurves.set_xscale(xscale) - except: - allcurves.set_xscale("symlog") - - # allcurves.xaxis.set_major_formatter(ticker.NullFormatter()) - allcurves.legend(modeleg, loc="best", prop={"size": 6}) - allcurves.set_xlabel(r"$\frac{x}{\rho_{ci}}$") - allcurves.set_ylabel(r"$\frac{Ni}{Ni_0}$") - fig1.savefig(pp, format="pdf") - plt.close(fig1) - plt.close(fig2) - - def plotmodes2( - self, - pp, - field="Ni", - comp="amp", - math="1", - ylim=1, - yscale="symlog", - clip=0, - xaxis="t", - xscale="linear", - xrange=1, - debug=False, - ): - Nplots = self.nrun - Modes = subset(self.db, "field", [field]) # pick field - colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] - - fig = Figure() - plt.figure() - - canvas = FigureCanvas(fig) - k = 0 - nrow = round(old_div(Nplots, 3.0) + 1.0) - ncol = 3 - # nrow = round(Nplots/3.0 + 1.0) - # ncol = round(Nplots/3.0 + 1.0) - f, axarr = plt.subplots(int(nrow), int(ncol)) - - for p in list(set(Modes.path).union()): # - s = subset(Modes.db, "path", [p]) # pick run - j = s.dz[0] - xr = list( - range( - old_div(s.nx, 2) - old_div(xrange, 2), - old_div(s.nx, 2) + old_div(xrange, 2) + 1, - ) - ) - data = np.array(ListDictKey(s.db, comp)) # pick component - # ax =fig.add_subplot(round(Nplots/3.0 + 1.0),3,k+1) - - for i in range(s.nmodes): - out = data[i, :, xr] - - print(j, i) - if xaxis == "t": - x = list(range(out.size)) - # plt.scatter(x,out.flatten(),c=colors[k]) - plt.scatter(x, out.flatten(), c=cm.jet(1.0 * k / len(x))) - # axarr[j%(ncol),j/ncol].scatter(x,out.flatten(),c=colors[k])#,alpha = (1 +i)/s.nmodes) - axarr[old_div(j, ncol), j % (ncol)].scatter( - x, out.flatten(), c=cm.jet(1.0 * k / len(x)) - ) # - - else: - x = np.array(ListDictKey(s.db, xaxis))[i, :, xr] - # x #an N? by nx array - print(x[:, 1], out[:, 0]) - plt.scatter(x[:, 1], out[:, 0]) # ,c=colors[k]) - axarr[j % (col), old_div(j, col)].scatter( - x[:, 1], out[:, 0] - ) # ,c=colors[k])#,alpha = (1 +i)/s.nmodes) - - # detect error (bar data - print("error bars:", x, out) - - axarr[old_div(j, ncol), j % (ncol)].set_yscale(yscale) - axarr[old_div(j, ncol), j % (ncol)].set_xscale(xscale) - axarr[old_div(j, ncol), j % (ncol)].set_title(str(j), fontsize=10) - axarr[old_div(j, ncol), j % (ncol)].set_xlabel(xaxis) - - plt.setp([a.get_xticklabels() for a in axarr[0, :]], visible=False) - plt.setp([a.get_yticklabels() for a in axarr[:, ncol - 1]], visible=False) - - if ylim: - axarr[j % (ncol), old_div(j, ncol)].set_ylim(data.min(), 5 * data.max()) - k += 1 - - plt.title(field + " " + comp + ", all runs, " + yscale + " yscale", fontsize=10) - plt.xlabel(xaxis) - if ylim: - plt.ylim(data.min(), 10 * data.max()) - - plt.yscale(yscale, nonposy="mask") - plt.xscale(xscale) - - fig.savefig(pp, format="pdf") - plt.savefig(pp, format="pdf") - - plt.close() - - return 0 - - def plotMacroDep( - self, - pp, - field="Ni", - yscale="symlog", - clip=0, - xaxis="t", - xscale="linear", - xrange=1, - ): - colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] - plt.figure() - - def savemovie( - self, field="Ni", yscale="log", xscale="log", moviename="spectrum.avi" - ): - print("Making movie animation.mpg - this make take a while") - files = [] - - for t in range(self.nt[0] - 3): - print(t) - filename = str("%03d" % (t + 1) + ".png") - self.plotvsK( - "dont need pp", - yscale="log", - t=[1, t + 2], - xscale="log", - overplot=False, - comp="amp", - trans=True, - file=filename, - ) - files.append(filename) - - command = ( - "mencoder", - "mf://*.png", - "-mf", - "type=png:w=800:h=600:fps=10", - "-ovc", - "lavc", - "-lavcopts", - "vcodec=mpeg4", - "-oac", - "copy", - "-o", - moviename, - ) - - import subprocess, os - - subprocess.check_call(command) - os.system("rm *png") - - def printmeta(self, pp, filename="output2.pdf", debug=False): - import os - from pyPdf import PdfFileWriter, PdfFileReader - - PAGE_HEIGHT = defaultPageSize[1] - styles = getSampleStyleSheet() - Title = "BOUT++ Results" - Author = "Dmitry Meyerson" - URL = "" - email = "dmitry.meyerson@gmail.com" - Abstract = """This document highlights some results from BOUT++ simulation""" - Elements = [] - HeaderStyle = styles["Heading1"] - ParaStyle = styles["Normal"] - PreStyle = styles["Code"] - - def header( - txt, style=HeaderStyle, klass=Paragraph, sep=0.3 - ): # return styled text with a space - s = Spacer(0.2 * inch, sep * inch) - para = klass(txt, style) - sect = [s, para] - result = KeepTogether(sect) - return result - - def p(txt): # wrapper for header - return header(txt, style=ParaStyle, sep=0.1) - - def pre(txt): # return styled text with a space - s = Spacer(0.1 * inch, 0.1 * inch) - p = Preformatted(txt, PreStyle) - precomps = [s, p] - result = KeepTogether(precomps) - return result - - def graphout(name, datain, xaxis=None): - if xaxis is None: - xaxis = list(range(datain.size)) - if xlabel is None: - xlabel = "" - if ylabel is None: - ylabel = "" - - drawing = Drawing(400, 200) - # data = [ - # ((1,1), (2,2), (2.5,1), (3,3), (4,5)), - # ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) - # ] - dataview = [tuple([(xaxis[i], datain[i]) for i in range(datain.size)])] - lp = LinePlot() - lp.x = 50 - lp.y = 50 - lp.height = 125 - lp.width = 300 - lp.data = dataview - lp.xValueAxis.xLabelFormat = "{mmm} {yy}" - lp.lineLabels.fontSize = 6 - lp.lineLabels.boxStrokeWidth = 0.5 - lp.lineLabels.visible = 1 - lp.lineLabels.boxAnchor = "c" - # lp.joinedLines = 1 - # lp.lines[0].symbol = makeMarker('FilledCircle') - # lp.lines[1].symbol = makeMarker('Circle') - # lp.lineLabelFormat = '%2.0f' - # lp.strokeColor = colors.black - # lp.xValueAxis.valueMin = min(xaxis) - # lp.xValueAxis.valueMax = max(xaxis) - # lp.xValueAxis.valueSteps = xaxis - # lp.xValueAxis.labelTextFormat = '%2.1f' - # lp.yValueAxis.valueMin = min(datain) - # lp.yValueAxis.valueMax = max(datain) - # lp.yValueAxis.valueSteps = [1, 2, 3, 5, 6] - drawing.add(lp) - return drawing - - def go(): - doc = SimpleDocTemplate("meta.pdf") - doc.build(Elements) - - mytitle = header(Title) - myname = header(Author, sep=0.1, style=ParaStyle) - mysite = header(URL, sep=0.1, style=ParaStyle) - mymail = header(email, sep=0.1, style=ParaStyle) - abstract_title = header("ABSTRACT") - myabstract = p(Abstract) - head_info = [mytitle, myname, mysite, mymail, abstract_title, myabstract] - Elements.extend(head_info) - - meta_title = header("metadata", sep=0) - metasection = [] - metasection.append(meta_title) - - for i, elem in enumerate(self.meta): - # if type(self.meta[elem]) != type(np.array([])): - - print(elem, type(self.meta[elem])) - - if type(self.meta[elem]) == type({}): - print("{}") - data = np.array(self.meta[elem]["v"]) - unit_label = str(self.meta[elem]["u"]) - else: - data = np.array(self.meta[elem]) - unit_label = "" - - xaxis = np.squeeze(self.meta["Rxy"]["v"][:, old_div(self.ny, 2)]) - - if data.shape == (self.nx, self.ny): - datastr = np.squeeze(data[:, old_div(self.ny, 2)]) - # metasection.append(graphout('stuff',datastr,xaxis=xaxis)) - # metasection.append(RL_Plot(datastr,xaxis)) - - metasection.append(RL_Plot(datastr, xaxis, linelabel=str(elem))) - # metasection.append(RL_Plot(datastr,xaxis,xlabel='xlabel')) - elif data.shape == self.nx: - datastr = data - # metasection.append(graphout('stuff',datastr,xaxis=xaxis)) - # metasection.append(RL_Plot(datastr,xaxis,linelabel=str(elem))) - elif data.shape == (1,): - data = data[0] - metasection.append( - header( - str(elem) + ": " + str(data) + " " + unit_label, - sep=0.1, - style=ParaStyle, - ) - ) - else: - print(elem, data, data.shape) - metasection.append( - header( - str(elem) + ": " + str(data) + " " + unit_label, - sep=0.1, - style=ParaStyle, - ) - ) - - src = KeepTogether(metasection) - Elements.append(src) - - cxxtitle = header("Equations in CXX") - cxxsection = [] - # print self.cxx - cxxsection.append(header(self.cxx[0], sep=0.1, style=ParaStyle)) - cxxsrc = KeepTogether(cxxsection) - - Elements.append(cxxsrc) - # for i,elem in enumerate(self.cxx): - # if type(self.meta[elem])== type({}): - # print elem #np.array(self.meta[elem]['v']).shape() - # if np.array(self.meta[elem]['v']).shape == (self.nx,self.ny): - # datastr = str(self.meta[elem]['v'][:,self.ny/2]) - # metasection.append(graphout('stuff', - # self.meta[elem]['v'][:,self.ny/2])) - # else: - # datastr = str(self.meta[elem]['v']) - # metasection.append(header(str(elem)+': '+datastr - # + ' '+ str(self.meta[elem]['u']), - # sep=0.1, style=ParaStyle)) - - if debug: - return Elements - go() - - output = PdfFileWriter() - metapdf = PdfFileReader(file("meta.pdf", "rb")) - mainpdf = PdfFileReader(file("output.pdf", "rb")) - - for i in range(0, metapdf.getNumPages()): - output.addPage(metapdf.getPage(i)) - - for i in range(0, mainpdf.getNumPages()): - output.addPage(mainpdf.getPage(i)) - - outputFile = filename - outputStream = file(outputFile, "wb") - output.write(outputStream) - outputStream.close() - print("Consolidation complete.") - - -class subset(LinResDraw): - def __init__(self, alldb, key, valuelist, model=False): - selection = ListDictFilt(alldb, key, valuelist) - if len(selection) != 0: - LinRes.__init__(self, selection) - self.skey = key - if model == True: - self.model() - else: - LinRes.__init__(self, alldb) - if model == True: - self.model() diff --git a/tools/pylib/post_bout/pb_nonlinear.py b/tools/pylib/post_bout/pb_nonlinear.py deleted file mode 100644 index 3fb726d4f8..0000000000 --- a/tools/pylib/post_bout/pb_nonlinear.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from builtins import range -from past.utils import old_div - -# some function to plot nonlinear stuff -from .pb_corral import LinRes -from .ListDict import ListDictKey, ListDictFilt -import numpy as np - -import matplotlib.pyplot as plt -from matplotlib import cm -import matplotlib.artist as artist -import matplotlib.ticker as ticker -import matplotlib.pyplot as plt -import matplotlib.patches as patches -from matplotlib.figure import Figure -from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas -from matplotlib.backends.backend_pdf import PdfPages - -from reportlab.platypus import * -from reportlab.lib.styles import getSampleStyleSheet -from reportlab.rl_config import defaultPageSize -from reportlab.lib.units import inch -from reportlab.graphics.charts.linecharts import HorizontalLineChart -from reportlab.graphics.shapes import Drawing -from reportlab.graphics.charts.lineplots import LinePlot -from reportlab.graphics.widgets.markers import makeMarker -from reportlab.lib import colors - -from replab_x_vs_y import RL_Plot -from matplotlib.ticker import ScalarFormatter, FormatStrFormatter, MultipleLocator - - -class NLinResDraw(LinRes): - def __init__(self, alldb): - LinRes.__init__(self, alldb) - - def plotnlrhs( - self, - pp, - field="Ni", - yscale="linear", - clip=0, - xaxis="t", - xscale="linear", - xrange=1, - ): - colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] - - Modes = subset(self.db, "field", [field]) # pick field - comp = "ave" - - fig1 = plt.figure() - adj = fig1.subplots_adjust(hspace=0.4, wspace=0.4) - fig1.suptitle("Nonlinear contribution for " + field) - props = dict(alpha=0.8, edgecolors="none") - Nplots = self.nrun - - k = 0 - for j in list(set(Modes.path).union()): - s = subset(Modes.db, "path", [j]) # pick a run folder - many modes - dz = s.dz[0] - data = s.ave[0]["nl"] - x = np.array(list(range(data.size))) - - ax = fig1.add_subplot(round(old_div(Nplots, 2.0) + 1.0), 2, k + 1) - ax.set_ylabel(r"$\frac{ddt_N}{ddt}$", fontsize=12, rotation="horizontal") - k += 1 - ax.grid(True, linestyle="-", color=".75") - try: - ax.set_yscale(yscale, linthreshy=1e-13) - except: - ax.set_yscale("linear") - i = 1 - ax.plot(x, data.flatten(), c=cm.jet(0.2 * i), linestyle="-") - - # data = np.array(ListDictKey(s.db,comp)) #pick component should be ok for a fixed dz key - - # we are not interested in looping over all modes - - fig1.savefig(pp, format="pdf") - plt.close(fig1) - - # return 0 - - -class subset(NLinResDraw): - def __init__(self, alldb, key, valuelist, model=False): - selection = ListDictFilt(alldb, key, valuelist) - if len(selection) != 0: - LinRes.__init__(self, selection) - self.skey = key - if model == True: - self.model() - else: - LinRes.__init__(self, alldb) - if model == True: - self.model() diff --git a/tools/pylib/post_bout/pb_present.py b/tools/pylib/post_bout/pb_present.py deleted file mode 100644 index 64fcaf1ec6..0000000000 --- a/tools/pylib/post_bout/pb_present.py +++ /dev/null @@ -1,213 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from builtins import str -from builtins import range -from .pb_draw import LinResDraw, subset -from .pb_corral import LinRes -from .pb_nonlinear import NLinResDraw -from pb_transport import Transport - -import numpy as np -import matplotlib.pyplot as plt - -import matplotlib.pyplot as plt -from matplotlib.figure import Figure -from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas -from matplotlib.backends.backend_pdf import PdfPages -import matplotlib.artist as artist -import matplotlib.ticker as ticker - -# from matplotlib.ticker import FuncFormatter -# from matplotlib.ticker import ScalarFormatter - -from reportlab.platypus import * -from reportlab.lib.styles import getSampleStyleSheet -from reportlab.rl_config import defaultPageSize -from reportlab.lib.units import inch -from reportlab.graphics.charts.linecharts import HorizontalLineChart -from reportlab.graphics.shapes import Drawing -from reportlab.graphics.charts.lineplots import LinePlot -from reportlab.graphics.widgets.markers import makeMarker -from reportlab.lib import colors - -from replab_x_vs_y import RL_Plot - -# for movie making -from multiprocessing import Queue, Pool -import multiprocessing -import subprocess - -# uses LinResDraw to make a pdf - - -class LinResPresent(LinResDraw, NLinResDraw, Transport): - def __init__(self, alldb): - LinResDraw.__init__(self, alldb) - NLinResDraw.__init__(self, alldb) - Transport.__init__(self, alldb) - - def show( - self, - filter=True, - quick=False, - pdfname="output2.pdf", - debug=False, - spectrum_movie=False, - ): - colors = ["b", "g", "r", "c", "m", "y", "k", "b", "g", "r", "c", "m", "y", "k"] - pp = PdfPages("output.pdf") - - # start by removing modes above the maxN threshold - modelist = [] - [ - modelist.append(list(self.modeid[p])) - for p in range(self.nmodes) - if self.mn[p][1] <= self.maxN[p] - ] - - s = subset(self.db, "modeid", modelist) - - try: - # fig = Figure(figsize=(6,6)) - # fig = plt.figure() - dz0 = list(set(s.dz).union())[0] - ss = subset(s.db, "dz", [dz0]) - - # show initial condition and the first step after - s.plotvsK( - pp, - yscale="log", - xscale="log", - t=[0, 1, -1], - overplot=False, - comp="amp", - trans=True, - ) - - if spectrum_movie: - ss.savemovie() - - except: - print("no scatter") - # 2D true NM spectrum with color code and boxes around spectral res regions log scale - - plt.figure() - i = 0 - for j in list( - set(s.dz).union() - ): # looping over runs, over unique 'dz' key values - ss = subset(s.db, "dz", [j]) # subset where dz = j - plt.scatter(ss.MN[:, 1], ss.MN[:, 0], c=colors[i]) - plt.annotate(str(j), (ss.MN[0, 1], ss.MN[0, 0])) - i += 1 - - plt.title(" Ni spectrum at t=0, all x") - plt.ylabel("M -parallel") - plt.xlabel("N - axisymmteric") - plt.xscale("log") - plt.grid(True, linestyle="-", color=".75") - - try: - plt.savefig(pp, format="pdf") - except: - print("FAILED TO save 1st part") - - plt.close() - - # for elem in self.meta['evolved']['v']: - # s.plotnl(pp - - if self.meta["nonlinear"]["v"] == "true": - self.plotnlrhs(pp) - - if self.meta["transport"] == "true": - self.plotnlrms(pp) - - for elem in self.meta["evolved"]: - s.plotmodes( - pp, - yscale="symlog", - comp="phase", - linestyle=".", - field=elem, - summary=False, - ) - s.plotmodes(pp, yscale="symlog", field=elem, summary=False) - print(elem) - try: - s.plotmodes( - pp, yscale="symlog", field=elem, comp="gamma_i", summary=False - ) - except: - print("gamma_i plot for " + elem + " failed") - - # s.plotmodes(pp,yscale='symlog',summary=False) - - modelist = [] - # maxZ = - # [modelist.append([1,p+1]) for p in range(maxZ-1)] - [ - modelist.append(list(self.modeid[p])) - for p in range(self.nmodes) - if self.mn[p][1] <= self.maxN[p] - ] - ss = subset(s.db, "mn", modelist) - - if debug: # just a few problematic slides - fig1 = plt.figure() - pp_bug = PdfPages("debug.pdf") - # ss.plotmodes(pp_bug,yscale='symlog',comp='phase',summary=False) - s.plotfreq2(pp_bug, xscale="log", yscale="symlog", overplot=True) - ss.plotgamma(pp_bug, xscale="log", yscale="symlog", overplot=True) - ss.plottheory(pp_bug) - ss.plottheory(pp_bug, comp="freq") - fig1.savefig(pp_bug, format="pdf") - pp_bug.close() - pp.close() - return 0 - - dir(ss) - ss.plotmodes(pp, yscale="log", debug=True, summary=False) - ss.plotmodes(pp, yscale="symlog", comp="phase", summary=False) - ss.plotmodes(pp, yscale="symlog", comp="phase", field="rho", summary=False) - print(dir(ss)) - - # ss.plotmodes(pp,yscale='log',comp='phase',clip=True) - - # ss.plotfreq2(pp,xscale='log',yscale='linear',overplot=False) - for elem in self.meta["evolved"]: - ss.plotfreq2( - pp, xscale="log", yscale="symlog", field=elem, overplot=True, trans=True - ) - - # ss.plotfreq2(pp,xscale='log',yscale='symlog',field='rho',overplot=True) - - if quick == True: - pp.close() - s.printmeta(pp) - - # plt.savefig(pp, format='pdf') - return 0 - - all_fields = list(set(s.field).union()) - - s.plotgamma(pp, xscale="log", yscale="linear", overplot=True, trans=True) - - s.plotgamma(pp, yscale="symlog", xscale="log", overplot=True) - s.plotgamma(pp, yscale="symlog", xscale="log", field="rho", overplot=True) - - try: - s.plotfreq2(pp, xscale="log", yscale="linear", overplot=True) - # s.plotfreq2(pp,xscale='log',yscale='symlog',overplot=False) - s.plotfreq2(pp, xscale="log", yscale="symlog", field="rho", overplot=True) - - # s.plotfreq2(pp,xscale='log',yscale='linear') - except: - print("something terrible") - - s.plotradeigen(pp, yscale="linear") - # s.plotradeigen(pp,field ='Vi',yscale='linear') - s.plotradeigen(pp, field="rho", yscale="log") - - pp.close() - s.printmeta(pp, filename=pdfname) # append a metadata header diff --git a/tools/pylib/post_bout/read_cxx.py b/tools/pylib/post_bout/read_cxx.py deleted file mode 100644 index eda88aac5b..0000000000 --- a/tools/pylib/post_bout/read_cxx.py +++ /dev/null @@ -1,139 +0,0 @@ -from builtins import range -from read_grid import read_grid -from ordereddict import OrderedDict -import numpy as np -import string -import re - - -def findlowpass(cxxstring): - ##p1="lowPass\(.*\)" - p1 = "lowPass\(.*\,(.*)\)" - maxN = np.array(re.findall(p1, cxxstring)) - # print substrings - - # p2=("[0-9]") - - # maxN = np.array([re.findall(p2,elem) for elem in substrings]).flatten() - - if maxN.size == 0: - return 100 - else: - output = int(min(maxN)) - if output == 0: - return 20 - else: - return output - - -def no_comment_cxx(path=".", boutcxx="physics_code.cxx.ref"): - # print 'no_comment' - boutcxx = path + "/" + boutcxx - # boutcxx = open(boutcxx,'r').readlines() - f = open(boutcxx, "r") - boutcxx = f.read() - f.close() - - start = string.find(boutcxx, "/*") - end = string.find(boutcxx, "*/") + 2 - - s = boutcxx[0:start] - for i in range(string.count(boutcxx, "/*")): - start = string.find(boutcxx, "/*", end) - s = s + boutcxx[end + 1 : start - 1] - - end = string.find(boutcxx, "*/", end) + 2 - - s = s + boutcxx[end + 1 :] - - # pattern = "\n \s* \(//)* .* \n" #pattern for a section start [All],[Ni], etc - pattern = "\n+.*;" # everythin - pattern = re.compile(pattern) - result = re.findall(pattern, s) - - # print result - - nocomment = [] - for elem in result: - # print elem - elem = elem.lstrip() - stop = elem.find("//") - # print start,stop - - if stop > 0: - nocomment.append(elem[0:stop]) - elif stop == -1: - nocomment.append(elem) - - # result = pattern.match(val) - # start = string.find(z,'\n //') - # end =string.find(boutcxx,'*/')+2 - # print nocomment - - return nocomment - - -def get_evolved_cxx(cxxfile=None): - if cxxfile is None: - cxxfile = no_comment_cxx() - - # s = cxxfile - # section_0 = string.find(s,'int physics_run(BoutReal t)') - # section_1 = string.find(s,'return',section_0) - # s = s[section_0:section_1] - temp = [] - - for x in cxxfile: - i = x.find("bout_solve(") - # print i,x - if i != -1: - comma_i = x[i::].find('"') - comma_j = x[i::].rfind('"') - # print x[i+comma_i:i+comma_j+1] - temp.append(x[i + comma_i + 1 : i + comma_j]) - - evolved = [] - [evolved.append(x) for x in set(temp)] - return np.array(evolved) - - -def read_cxx(path=".", boutcxx="physics_code.cxx.ref", evolved=""): - # print path, boutcxx - boutcxx = path + "/" + boutcxx - # boutcxx = open(boutcxx,'r').readlines() - f = open(boutcxx, "r") - boutcxx = f.read() - f.close() - - # start by stripping out all comments - # look at the 1st character of all list elements - # for now use a gross loop, vectorize later - - start = string.find(boutcxx, "/*") - end = string.find(boutcxx, "*/") + 2 - - s = boutcxx[0:start] - for i in range(string.count(boutcxx, "/*")): - start = string.find(boutcxx, "/*", end) - s = s + boutcxx[end + 1 : start - 1] - - end = string.find(boutcxx, "*/", end) + 2 - - s = s + boutcxx[end + 1 :] - - section_0 = string.find(s, "int physics_run(BoutReal t)") - section_1 = string.find(s, "return", section_0) - s = s[section_0:section_1] - - tmp = open("./read_cxx.tmp", "w") - tmp.write(s) - tmp.close() - tmp = open("./read_cxx.tmp", "r") - - cxxlist = "" - - for line in tmp: - if line[0] != "//" and line.isspace() == False: - cxxlist = cxxlist + line.split("//")[0] - - return cxxlist diff --git a/tools/pylib/post_bout/read_inp.py b/tools/pylib/post_bout/read_inp.py deleted file mode 100644 index 87de7ddf3a..0000000000 --- a/tools/pylib/post_bout/read_inp.py +++ /dev/null @@ -1,433 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division -from builtins import str -from past.utils import old_div -from builtins import object -from read_grid import read_grid -from ordereddict import OrderedDict -import numpy as np -from boututils.file_import import file_import -from .read_cxx import * - - -def read_inp(path="", boutinp="BOUT.inp"): - boutfile = path + "/" + boutinp - boutinp = open(boutfile, "r").readlines() - - # start by stripping out all comments - # look at the 1st character of all list elements - # for now use a gross loop, vectorize later - boutlist = [] - - for i, val in enumerate(boutinp): - if val[0] != "#" and val.isspace() == False: - boutlist.append(val.split("#")[0]) - - return boutlist - - -def parse_inp(boutlist): - import re - from ordereddict import OrderedDict - - if not boutlist: - return 0 - - # boutdic={} unordered standard dict - boutdic = OrderedDict() - - # regex is messy see http://docs.python.org/howto/regex.html#regex-howto - pattern = "\[\S*\]" # pattern for a section start [All],[Ni], etc - - pattern = re.compile(pattern) - - boutdic["[main]"] = {} - current = "[main]" - - for i, val in enumerate(boutlist): - # print i,val - result = pattern.match(val) - # while the current value is not a new section name add everything to the current section - - if result is None: - # print val - key, value = val.split("=") - value = value.replace('"', "") - # print current, key,value - - boutdic[current][key.strip()] = value.strip() - else: - boutdic[result.group()] = {} - current = result.group() - - return boutdic - - -def read_log(path=".", logname="status.log"): - print("in read_log") - import re - from ordereddict import OrderedDict - - # logfile = path+'/'+logname - logfile = logname - print(logfile) - logcase = open(logfile, "r").readlines() - - # start by stripping out all comments - # look at the 1st character of all list elements - # for now use a gross loop, vectorize later - loglist = [] - - for i, val in enumerate(logcase): - if val[0] != "#" and val.isspace() == False: - loglist.append(val.split("#")[0]) - - if not loglist: - return 0 - - logdict = OrderedDict() - logdict["runs"] = [] - # print len(loglist) - print(loglist) - # print loglist[len(loglist)-1] == 'last one\n' - - # last = loglist.pop().rstrip() - - # logdict['done'] = last == 'done' - - logdict["current"] = loglist.pop().rstrip() - for i, val in enumerate(loglist): - print(val) - logdict["runs"].append(val.rstrip()) - - logdict["runs"].append(logdict["current"]) - - # print logdict - return logdict - - -def metadata(inpfile="BOUT.inp", path=".", v=False): - filepath = path + "/" + inpfile - print(filepath) - inp = read_inp(path=path, boutinp=inpfile) - inp = parse_inp(inp) # inp file - print(path) - outinfo = file_import(path + "/BOUT.dmp.0.nc") # output data - - try: - print(path) - cxxinfo = no_comment_cxx(path=path, boutcxx="physics_code.cxx.ref") - # evolved = get_evolved_cxx(cxxinfo) - fieldkeys = get_evolved_cxx(cxxinfo) - fieldkeys = ["[" + elem + "]" for elem in fieldkeys] - except: - print("cant find the cxx file") - - # gridoptions = {'grid':grid,'mesh':mesh} - if "[mesh]" in list(inp.keys()): - # IC = outinfo - IC = read_grid(path + "/BOUT.dmp.0.nc") # output data again - elif "grid" in inp["[main]"]: - gridname = inp["[main]"]["grid"] - try: - IC = read_grid(gridname) # have to be an ansoulte file path for now - print("IC: ", type(IC)) - # print IC.variables - # print gridname - except: - # print gridname - print("Fail to load the grid file") - # print IC - - # print gridname - # print len(IC) - # print IC - - evolved = [] - collected = [] - ICscale = [] - - # fieldkeys = ['[Ni]','[Te]','[Ti]','[Vi]','[rho]', - # '[Ajpar]','[Apar]','[vEBx]','[vEBy]','[vEBz]', - # '[jpar]','[phi]'] - - # construct fieldkeys from cxx info - # fieldkeys = ['['+x+']' for x in evolved] - # fieldkeys = evolved - - # just look ahead and see what 3D fields have been output - available = np.array([str(x) for x in outinfo]) - a = np.array([(len(outinfo[x].shape) == 4) for x in available]) - available = available[a] - - defaultIC = float(inp["[All]"].get("scale", 0.0)) - - # print inp.keys() - - # figure out which fields are evolved - print(fieldkeys) - - for section in list(inp.keys()): # loop over section keys - print("section: ", section) - if section in fieldkeys: # pick the relevant sections - print(section) - # print inp[section].get('evolve','True') - # rint (inp[section].get('evolve','True')).lower().strip() - if ( - inp[section].get("evolve", "True").lower().strip() == "true" - ): # and section[1:-1] in available : - print("ok reading") - evolved.append(section.strip("[]")) - ICscale.append(float(inp[section].get("scale", defaultIC))) - - if inp[section].get("collect", "False").lower().strip() == "true": - collected.append(section.strip("[]")) - - try: - if inp["[physics]"].get("transport", "False").lower().strip() == "true": - vEBstr = ["vEBx", "vEBy", "vEBz", "vEBrms"] - [collected.append(item) for item in vEBstr] - except: - print("no [physics] key") - - meta = OrderedDict() - - class ValUnit(object): - def __init__(self, value=0, units=""): - self.u = units - self.v = value - - def todict(self): - return {"u": self.u, "v": self.v} - - # def decode_valunit(d): - - def ToFloat(metaString): - try: - return float(metaString) - except ValueError: - return metaString - - # meta['evolved'] = ValUnit(evolved,'') - meta["evolved"] = evolved - meta["collected"] = collected - meta["IC"] = np.array(ICscale) - d = {} - - print("evolved: ", evolved) - - # read meta data from .inp file, this is whre most metadata get written - for section in list(inp.keys()): - if ("evolve" not in inp[section]) and ( - "first" not in inp[section] - ): # hacky way to exclude some less relevant metadata - for elem in list(inp[section].keys()): - meta[elem] = ValUnit(ToFloat(inp[section][elem])) - d[elem] = np.array(ToFloat(inp[section][elem])) - - # read in some values from the grid(IC) and scale them as needed - norms = { - "Ni0": ValUnit(1.0e14, "cm^-3"), - "bmag": ValUnit(1.0e4, "gauss"), - "Ni_x": ValUnit(1.0e14, "cm^-3"), - "Te_x": ValUnit(1.0, "eV"), - "Ti_x": ValUnit(1.0, "eV"), - "Rxy": ValUnit(1, "m"), - "Bxy": ValUnit(1.0e4, "gauss"), - "Bpxy": ValUnit(1.0e4, "gauss"), - "Btxy": ValUnit(1.0e4, "gauss"), - "Zxy": ValUnit(1, "m"), - "dlthe": ValUnit(1, "m"), - "dx": ValUnit(1, "m"), - "hthe0": ValUnit(1, "m"), - } - - availkeys = np.array([str(x) for x in outinfo]) - tmp1 = np.array([x for x in availkeys]) - # b = np.array([x if x not in available for x in a]) - tmp2 = np.array([x for x in tmp1 if x not in available]) - static_fields = np.array([x for x in tmp2 if x in list(norms.keys())]) - # static_fields = tmp2 - - # print availkeys - # print meta.keys() - # print IC.variables.keys() - # print tmp1 - # print tmp2 - - for elem in static_fields: - print("elem: ", elem) - meta[elem] = ValUnit(IC.variables[elem][:] * norms[elem].v, norms[elem].u) - d[elem] = np.array(IC.variables[elem][:] * norms[elem].v) - - for elem in IC.variables: - if elem not in meta: - if elem in list(norms.keys()): - meta[elem] = ValUnit( - IC.variables[elem][:] * norms[elem].v, norms[elem].u - ) - d[elem] = np.array(IC.variables[elem][:] * norms[elem].v) - else: - meta[elem] = IC.variables[elem][:] - d[elem] = IC.variables[elem][:] - - # print d.keys() - - # if case some values are missing - default = { - "bmag": 1, - "Ni_x": 1, - "NOUT": 100, - "TIMESTEP": 1, - "MZ": 32, - "AA": 1, - "Zeff": ValUnit(1, ""), - "ZZ": 1, - "zlowpass": 0.0, - "transport": False, - } - diff = set(default.keys()).difference(set(d.keys())) - - for elem in diff: - # print 'diff: ',elem - meta[elem] = default[elem] - d[elem] = np.array(default[elem]) - - # print meta.keys() - # print d.keys() - - # print meta['zlowpass'] - - if meta["zlowpass"] != 0: - print(meta["MZ"].v, meta["zlowpass"].v) - meta["maxZ"] = int(np.floor(meta["MZ"].v * meta["zlowpass"].v)) - else: - meta["maxZ"] = 5 - - # meta['nx'] = nx - # meta['ny']= ny - meta["dt"] = meta["TIMESTEP"] - - # nx,ny = d['Rxy'].shape - - # print meta['AA'].v - - meta["rho_s"] = ValUnit( - 1.02e2 * np.sqrt(d["AA"] * d["Te_x"]) / (d["ZZ"] * d["bmag"]), "cm" - ) # ion gyrorad at T_e, in cm - meta["rho_i"] = ValUnit( - 1.02e2 * np.sqrt(d["AA"] * d["Ti_x"]) / (d["ZZ"] * d["bmag"]), "cm" - ) - meta["rho_e"] = ValUnit(2.38 * np.sqrt(d["Te_x"]) / (d["bmag"]), "cm") - - meta["fmei"] = ValUnit(1.0 / 1836.2 / d["AA"]) - - meta["lambda_ei"] = 24.0 - np.log(old_div(np.sqrt(d["Ni_x"]), d["Te_x"])) - meta["lambda_ii"] = 23.0 - np.log( - d["ZZ"] ** 3 * np.sqrt(2.0 * d["Ni_x"]) / (d["Ti_x"] ** 1.5) - ) # - - meta["wci"] = 1.0 * 9.58e3 * d["ZZ"] * d["bmag"] / d["AA"] # ion gyrofrteq - meta["wpi"] = ( - 1.32e3 * d["ZZ"] * np.sqrt(old_div(d["Ni_x"], d["AA"])) - ) # ion plasma freq - - meta["wce"] = 1.78e7 * d["bmag"] # electron gyrofreq - meta["wpe"] = 5.64e4 * np.sqrt(d["Ni_x"]) # electron plasma freq - - meta["v_the"] = 4.19e7 * np.sqrt(d["Te_x"]) # cm/s - meta["v_thi"] = 9.79e5 * np.sqrt(old_div(d["Ti_x"], d["AA"])) # cm/s - meta["c_s"] = 9.79e5 * np.sqrt(5.0 / 3.0 * d["ZZ"] * d["Te_x"] / d["AA"]) # - meta["v_A"] = 2.18e11 * np.sqrt(old_div(1.0, (d["AA"] * d["Ni_x"]))) - - meta["nueix"] = 2.91e-6 * d["Ni_x"] * meta["lambda_ei"] / d["Te_x"] ** 1.5 # - meta["nuiix"] = ( - 4.78e-8 - * d["ZZ"] ** 4.0 - * d["Ni_x"] - * meta["lambda_ii"] - / d["Ti_x"] ** 1.5 - / np.sqrt(d["AA"]) - ) # - meta["nu_hat"] = meta["Zeff"].v * meta["nueix"] / meta["wci"] - - meta["L_d"] = 7.43e2 * np.sqrt(old_div(d["Te_x"], d["Ni_x"])) - meta["L_i_inrt"] = ( - 2.28e7 * np.sqrt(old_div(d["AA"], d["Ni_x"])) / d["ZZ"] - ) # ion inertial length in cm - meta["L_e_inrt"] = 5.31e5 * np.sqrt(d["Ni_x"]) # elec inertial length in cm - - meta["Ve_x"] = 4.19e7 * d["Te_x"] - - meta["R0"] = old_div((d["Rxy"].max() + d["Rxy"].min()), 2.0) - - print(d["Rxy"].mean(1)) - print(d["ZMAX"]) - print(d["ZMIN"]) - meta["L_z"] = ( - 1e2 * 2 * np.pi * d["Rxy"].mean(1) * (d["ZMAX"] - d["ZMIN"]) - ) # in cm toroidal range - meta["dz"] = d["ZMAX"] - d["ZMIN"] - - # meta['lbNorm']=meta['L_z']*(d['Bpxy']/d['Bxy']).mean(1) #-binormal coord range [cm] - meta["lbNorm"] = meta["L_z"] * (old_div(d["Bxy"], d["Bpxy"])).mean(1) - - # meta['zPerp']=np.array(meta['lbNorm']).mean*np.array(range(d['MZ']))/(d['MZ']-1) - # let's calculate some profile properties - dx = np.gradient(d["Rxy"])[0] - meta["L"] = ( - 1.0 - * 1e2 - * dx - * (meta["Ni0"].v) - / np.gradient(meta["Ni0"].v)[0] - / meta["rho_s"].v - ) - - meta["w_Ln"] = old_div( - meta["c_s"], (np.min(abs(meta["L"])) * meta["wci"] * meta["rho_s"].v) - ) # normed to wci - - AA = meta["AA"].v - ZZ = d["ZZ"] - Te_x = d["Te_x"] - Ti_x = d["Ti_x"] - fmei = meta["fmei"].v - - meta["lpar"] = ( - 1e2 * ((old_div(d["Bxy"], d["Bpxy"])) * d["dlthe"]).sum(1) / meta["rho_s"].v - ) # -[normed], average over flux surfaces, parallel length - - # yes dlthe is always the vertical displacement - # dlthe = (hthe0*2 pi)/nz - # meta['lpar']=1e2*(d['Bxy']/d['Bpxy']).mean(1)*d['dlthe'].mean(1) #function of x - meta["sig_par"] = old_div(1.0, (fmei * 0.51 * meta["nu_hat"])) - # meta['heat_nue'] = ((2*np.pi/meta['lpar'])**2)/(fmei*meta['nu_hat']) - # kz_e = kz_i*(rho_e/rho_i) - # kz_s = kz_i*(rho_s/rho_i) - # kz_i = (TWOPI/L_z)*(indgen((*current_str).fft.nz+1))*rho_i - - # knorm = (TWOPI/lbNorm)*(indgen((*current_str).fft.nz+1))*rho_s - - # for now just translate - for elem in meta: - if type(meta[elem]).__name__ == "ValUnit": - meta[elem] = {"u": meta[elem].u, "v": meta[elem].v} - - print("meta: ", type(meta)) - return meta - - # meta['DZ'] =inp['[main]']['ZMAX']#-b['[main]']['ZMIN'] - # AA = inp['[2fluid]']['AA'] - # Ni0 = IC.variables['Ni0'][:]*1.e14 - # bmag = IC.variables['bmag'][:]*1.e4 #to cgs - # Ni_x = IC.variables['Ni_x'][:]*1.e14 # cm^-3 - # Te_x - - # rho_s = 1.02e2*sqrt(AA.v*Te_x.v)/ZZ.v/bmag.v - # rho_i - # rho_e - - -# for i,val in enumerate(boutlist): diff --git a/tools/pylib/post_bout/rms.py b/tools/pylib/post_bout/rms.py deleted file mode 100644 index 6a9bdb1929..0000000000 --- a/tools/pylib/post_bout/rms.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import division -from builtins import range -from past.utils import old_div - -### -# rms(f) : compute growth rate vs. time based on rms of bout variable f for all grid points -# plot_rms (x,y): plots the graph growth_rate vs. time for grid point x,y -### - - -import numpy as np -from pylab import plot, show, xlabel, ylabel, tight_layout -from boutdata.collect import collect - - -def rms(f): - nt = f.shape[0] - - ns = f.shape[1] - ne = f.shape[2] - nz = f.shape[3] - - ar = np.zeros([nz]) - - rms = np.zeros([nt, ns, ne]) - - for i in range(nt): - for j in range(ns): - for k in range(ne): - ar = f[i, j, k, :] - valav = np.sum(ar) - tot = np.sum(old_div(np.power(ar - valav, 2), nz)) - rms[i, j, k] = np.sqrt(tot) - return rms - - -def plot_rms(x, y): - s = plot(np.gradient(np.log(rmsp[:, x, y]))) - ylabel("$\gamma / \omega_A$", fontsize=25) - xlabel("Time$(\\tau_A)$", fontsize=25) - tight_layout() - return s - - -# test -if __name__ == "__main__": - path = "../../../examples/elm-pb/data" - - data = collect("P", path=path) - - rmsp = rms(data) - - plot_rms(34, 32) - tight_layout() - show() From 4566405dfb88c8ca6564a769ea37aa07ba3c8eb1 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Sun, 4 Feb 2024 20:27:11 +0000 Subject: [PATCH 026/256] Apply black changes --- tests/MMS/diffusion2/runtest | 2 +- tools/tokamak_grids/all/grid2bout.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/MMS/diffusion2/runtest b/tests/MMS/diffusion2/runtest index d915018d8d..6039c6faa6 100755 --- a/tests/MMS/diffusion2/runtest +++ b/tests/MMS/diffusion2/runtest @@ -27,7 +27,7 @@ build_and_log("MMS diffusion test") inputs = [ ("X", ["mesh:nx"]), ("Y", ["mesh:ny"]), - ("Z", ["MZ"]) # , + ("Z", ["MZ"]), # , # ("XYZ", ["mesh:nx", "mesh:ny", "MZ"]) ] diff --git a/tools/tokamak_grids/all/grid2bout.py b/tools/tokamak_grids/all/grid2bout.py index da62a37aeb..6520e8f116 100644 --- a/tools/tokamak_grids/all/grid2bout.py +++ b/tools/tokamak_grids/all/grid2bout.py @@ -3,6 +3,7 @@ """ + from __future__ import print_function from numpy import max From 4935742c6075583067c5909fe82d292d233cc4f4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Sun, 4 Feb 2024 21:39:39 +0100 Subject: [PATCH 027/256] CI: install wget --- .ci_fedora.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci_fedora.sh b/.ci_fedora.sh index 452afb4b7e..18b86467fb 100755 --- a/.ci_fedora.sh +++ b/.ci_fedora.sh @@ -41,7 +41,7 @@ then # Ignore weak depencies echo "install_weak_deps=False" >> /etc/dnf/dnf.conf time dnf -y install dnf5 - time dnf5 -y install dnf5-plugins cmake python3-zoidberg python3-natsort + time dnf5 -y install dnf5-plugins cmake python3-zoidberg python3-natsort wget # Allow to override packages - see #2073 time dnf5 copr enable -y davidsch/fixes4bout || : time dnf5 -y upgrade From 147a872cc4fc2056268d57d43d947cbe6605a8c0 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Sun, 4 Feb 2024 20:53:06 +0000 Subject: [PATCH 028/256] Apply clang-format changes --- include/bout/interpolation_xz.hxx | 4 +--- src/mesh/interpolation/hermite_spline_xz.cxx | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/include/bout/interpolation_xz.hxx b/include/bout/interpolation_xz.hxx index 46f64256a8..3b45498595 100644 --- a/include/bout/interpolation_xz.hxx +++ b/include/bout/interpolation_xz.hxx @@ -75,9 +75,7 @@ public: void setRegion(const std::string& region_name) { this->region_id = localmesh->getRegionID(region_name); } - void setRegion(const std::unique_ptr> region){ - setRegion(*region); - } + void setRegion(const std::unique_ptr> region) { setRegion(*region); } void setRegion(const Region& region) { std::string name; int i = 0; diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 9e41bac191..782a701c2e 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -22,8 +22,8 @@ #include "../impls/bout/boutmesh.hxx" #include "bout/globals.hxx" -#include "bout/interpolation_xz.hxx" #include "bout/index_derivs_interface.hxx" +#include "bout/interpolation_xz.hxx" #include @@ -101,10 +101,10 @@ class IndConverter { } }; -XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh *mesh) - : XZInterpolation(y_offset, mesh), - h00_x(localmesh), h01_x(localmesh), h10_x(localmesh), h11_x(localmesh), - h00_z(localmesh), h01_z(localmesh), h10_z(localmesh), h11_z(localmesh) { +XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* mesh) + : XZInterpolation(y_offset, mesh), h00_x(localmesh), h01_x(localmesh), + h10_x(localmesh), h11_x(localmesh), h00_z(localmesh), h01_z(localmesh), + h10_z(localmesh), h11_z(localmesh) { // Index arrays contain guard cells in order to get subscripts right i_corner.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); @@ -198,7 +198,7 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z } i_corner[i] = SpecificInd( - (((i_corn * ny) + (y + y_offset)) * nz + k_corner(x, y, z)), ny, nz); + (((i_corn * ny) + (y + y_offset)) * nz + k_corner(x, y, z)), ny, nz); h00_x[i] = (2. * t_x * t_x * t_x) - (3. * t_x * t_x) + 1.; h00_z[i] = (2. * t_z * t_z * t_z) - (3. * t_z * t_z) + 1.; From cee68f2aa24f1cac7605cc5326de70a3491dc81c Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 7 Feb 2024 14:20:26 +0100 Subject: [PATCH 029/256] Add asserts to serial methods to avoid using in parallel --- include/bout/interpolation_xz.hxx | 21 ++++++++++++++++---- src/mesh/interpolation/hermite_spline_xz.cxx | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/include/bout/interpolation_xz.hxx b/include/bout/interpolation_xz.hxx index 3b45498595..3dee48fedb 100644 --- a/include/bout/interpolation_xz.hxx +++ b/include/bout/interpolation_xz.hxx @@ -212,11 +212,24 @@ public: /// problems most obviously occur. class XZMonotonicHermiteSpline : public XZHermiteSpline { public: - XZMonotonicHermiteSpline(Mesh* mesh = nullptr) : XZHermiteSpline(0, mesh) {} + XZMonotonicHermiteSpline(Mesh* mesh = nullptr) + : XZHermiteSpline(0, mesh) { + if (localmesh->getNXPE() > 1){ + throw BoutException("Do not support MPI splitting in X"); + } + } XZMonotonicHermiteSpline(int y_offset = 0, Mesh* mesh = nullptr) - : XZHermiteSpline(y_offset, mesh) {} - XZMonotonicHermiteSpline(const BoutMask& mask, int y_offset = 0, Mesh* mesh = nullptr) - : XZHermiteSpline(mask, y_offset, mesh) {} + : XZHermiteSpline(y_offset, mesh) { + if (localmesh->getNXPE() > 1){ + throw BoutException("Do not support MPI splitting in X"); + } + } + XZMonotonicHermiteSpline(const BoutMask &mask, int y_offset = 0, Mesh* mesh = nullptr) + : XZHermiteSpline(mask, y_offset, mesh) { + if (localmesh->getNXPE() > 1){ + throw BoutException("Do not support MPI splitting in X"); + } + } using XZHermiteSpline::interpolate; /// Interpolate using precalculated weights. diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 782a701c2e..b5093a2da9 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -145,6 +145,11 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* mesh) MatCreateAIJ(MPI_COMM_WORLD, m, m, M, M, 16, nullptr, 16, nullptr, &petscWeights); #endif #endif +#ifndef HS_USE_PETSC + if (localmesh->getNXPE() > 1){ + throw BoutException("Require PETSc for MPI splitting in X"); + } +#endif } void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, From 15aaa055ad424e63194123c1f81aa6a1a5f38ca6 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Wed, 7 Feb 2024 13:25:23 +0000 Subject: [PATCH 030/256] Apply clang-format changes --- include/bout/interpolation_xz.hxx | 11 +++++------ src/mesh/interpolation/hermite_spline_xz.cxx | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/include/bout/interpolation_xz.hxx b/include/bout/interpolation_xz.hxx index 3dee48fedb..4f171e3420 100644 --- a/include/bout/interpolation_xz.hxx +++ b/include/bout/interpolation_xz.hxx @@ -212,21 +212,20 @@ public: /// problems most obviously occur. class XZMonotonicHermiteSpline : public XZHermiteSpline { public: - XZMonotonicHermiteSpline(Mesh* mesh = nullptr) - : XZHermiteSpline(0, mesh) { - if (localmesh->getNXPE() > 1){ + XZMonotonicHermiteSpline(Mesh* mesh = nullptr) : XZHermiteSpline(0, mesh) { + if (localmesh->getNXPE() > 1) { throw BoutException("Do not support MPI splitting in X"); } } XZMonotonicHermiteSpline(int y_offset = 0, Mesh* mesh = nullptr) : XZHermiteSpline(y_offset, mesh) { - if (localmesh->getNXPE() > 1){ + if (localmesh->getNXPE() > 1) { throw BoutException("Do not support MPI splitting in X"); } } - XZMonotonicHermiteSpline(const BoutMask &mask, int y_offset = 0, Mesh* mesh = nullptr) + XZMonotonicHermiteSpline(const BoutMask& mask, int y_offset = 0, Mesh* mesh = nullptr) : XZHermiteSpline(mask, y_offset, mesh) { - if (localmesh->getNXPE() > 1){ + if (localmesh->getNXPE() > 1) { throw BoutException("Do not support MPI splitting in X"); } } diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index b5093a2da9..f167a7576d 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -146,7 +146,7 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* mesh) #endif #endif #ifndef HS_USE_PETSC - if (localmesh->getNXPE() > 1){ + if (localmesh->getNXPE() > 1) { throw BoutException("Require PETSc for MPI splitting in X"); } #endif From 30ea7e397d9e6c78625ba33102fb49a3493ffde9 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 13 Feb 2024 14:30:50 +0100 Subject: [PATCH 031/256] Remove accidentially added files --- .../test-boutpp/slicing/basics.indexing.html | 1368 ----------------- .../test-boutpp/slicing/basics.indexing.txt | 687 --------- .../test-boutpp/slicing/slicingexamples | 1 - tests/integrated/test-boutpp/slicing/test.py | 4 - 4 files changed, 2060 deletions(-) delete mode 100644 tests/integrated/test-boutpp/slicing/basics.indexing.html delete mode 100644 tests/integrated/test-boutpp/slicing/basics.indexing.txt delete mode 100644 tests/integrated/test-boutpp/slicing/slicingexamples delete mode 100644 tests/integrated/test-boutpp/slicing/test.py diff --git a/tests/integrated/test-boutpp/slicing/basics.indexing.html b/tests/integrated/test-boutpp/slicing/basics.indexing.html deleted file mode 100644 index 180f39c6ed..0000000000 --- a/tests/integrated/test-boutpp/slicing/basics.indexing.html +++ /dev/null @@ -1,1368 +0,0 @@ - - - - - - - - - Indexing on ndarrays — NumPy v1.23 Manual - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - -
- -
- -
-

Indexing on ndarrays#

-
-

See also

-

Indexing routines

-
-

ndarrays can be indexed using the standard Python -x[obj] syntax, where x is the array and obj the selection. -There are different kinds of indexing available depending on obj: -basic indexing, advanced indexing and field access.

-

Most of the following examples show the use of indexing when -referencing data in an array. The examples work just as well -when assigning to an array. See Assigning values to indexed arrays for -specific examples and explanations on how assignments work.

-

Note that in Python, x[(exp1, exp2, ..., expN)] is equivalent to -x[exp1, exp2, ..., expN]; the latter is just syntactic sugar -for the former.

-
-

Basic indexing#

-
-

Single element indexing#

-

Single element indexing works -exactly like that for other standard Python sequences. It is 0-based, -and accepts negative indices for indexing from the end of the array.

-
>>> x = np.arange(10)
->>> x[2]
-2
->>> x[-2]
-8
-
-
-

It is not necessary to -separate each dimension’s index into its own set of square brackets.

-
>>> x.shape = (2, 5)  # now x is 2-dimensional
->>> x[1, 3]
-8
->>> x[1, -1]
-9
-
-
-

Note that if one indexes a multidimensional array with fewer indices -than dimensions, one gets a subdimensional array. For example:

-
>>> x[0]
-array([0, 1, 2, 3, 4])
-
-
-

That is, each index specified selects the array corresponding to the -rest of the dimensions selected. In the above example, choosing 0 -means that the remaining dimension of length 5 is being left unspecified, -and that what is returned is an array of that dimensionality and size. -It must be noted that the returned array is a view, i.e., it is not a -copy of the original, but points to the same values in memory as does the -original array. -In this case, the 1-D array at the first position (0) is returned. -So using a single index on the returned array, results in a single -element being returned. That is:

-
>>> x[0][2]
-2
-
-
-

So note that x[0, 2] == x[0][2] though the second case is more -inefficient as a new temporary array is created after the first index -that is subsequently indexed by 2.

-
-

Note

-

NumPy uses C-order indexing. That means that the last -index usually represents the most rapidly changing memory location, -unlike Fortran or IDL, where the first index represents the most -rapidly changing location in memory. This difference represents a -great potential for confusion.

-
-
-
-

Slicing and striding#

-

Basic slicing extends Python’s basic concept of slicing to N -dimensions. Basic slicing occurs when obj is a slice object -(constructed by start:stop:step notation inside of brackets), an -integer, or a tuple of slice objects and integers. Ellipsis -and newaxis objects can be interspersed with these as -well.

-

The simplest case of indexing with N integers returns an array -scalar representing the corresponding item. As in -Python, all indices are zero-based: for the i-th index \(n_i\), -the valid range is \(0 \le n_i < d_i\) where \(d_i\) is the -i-th element of the shape of the array. Negative indices are -interpreted as counting from the end of the array (i.e., if -\(n_i < 0\), it means \(n_i + d_i\)).

-

All arrays generated by basic slicing are always views -of the original array.

-
-

Note

-

NumPy slicing creates a view instead of a copy as in the case of -built-in Python sequences such as string, tuple and list. -Care must be taken when extracting -a small portion from a large array which becomes useless after the -extraction, because the small portion extracted contains a reference -to the large original array whose memory will not be released until -all arrays derived from it are garbage-collected. In such cases an -explicit copy() is recommended.

-
-

The standard rules of sequence slicing apply to basic slicing on a -per-dimension basis (including using a step index). Some useful -concepts to remember include:

-
    -
  • The basic slice syntax is i:j:k where i is the starting index, -j is the stopping index, and k is the step (\(k\neq0\)). -This selects the m elements (in the corresponding dimension) with -index values i, i + k, …, i + (m - 1) k where -\(m = q + (r\neq0)\) and q and r are the quotient and remainder -obtained by dividing j - i by k: j - i = q k + r, so that -i + (m - 1) k < j. -For example:

    -
    >>> x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    ->>> x[1:7:2]
    -array([1, 3, 5])
    -
    -
    -
  • -
  • Negative i and j are interpreted as n + i and n + j where -n is the number of elements in the corresponding dimension. -Negative k makes stepping go towards smaller indices. -From the above example:

    -
    >>> x[-2:10]
    -array([8, 9])
    ->>> x[-3:3:-1]
    -array([7, 6, 5, 4])
    -
    -
    -
  • -
  • Assume n is the number of elements in the dimension being -sliced. Then, if i is not given it defaults to 0 for k > 0 and -n - 1 for k < 0 . If j is not given it defaults to n for k > 0 -and -n-1 for k < 0 . If k is not given it defaults to 1. Note that -:: is the same as : and means select all indices along this -axis. -From the above example:

    -
    >>> x[5:]
    -array([5, 6, 7, 8, 9])
    -
    -
    -
  • -
  • If the number of objects in the selection tuple is less than -N, then : is assumed for any subsequent dimensions. -For example:

    -
    >>> x = np.array([[[1],[2],[3]], [[4],[5],[6]]])
    ->>> x.shape
    -(2, 3, 1)
    ->>> x[1:2]
    -array([[[4],
    -        [5],
    -        [6]]])
    -
    -
    -
  • -
  • An integer, i, returns the same values as i:i+1 -except the dimensionality of the returned object is reduced by -1. In particular, a selection tuple with the p-th -element an integer (and all other entries :) returns the -corresponding sub-array with dimension N - 1. If N = 1 -then the returned object is an array scalar. These objects are -explained in Scalars.

  • -
  • If the selection tuple has all entries : except the -p-th entry which is a slice object i:j:k, -then the returned array has dimension N formed by -concatenating the sub-arrays returned by integer indexing of -elements i, i+k, …, i + (m - 1) k < j,

  • -
  • Basic slicing with more than one non-: entry in the slicing -tuple, acts like repeated application of slicing using a single -non-: entry, where the non-: entries are successively taken -(with all other non-: entries replaced by :). Thus, -x[ind1, ..., ind2,:] acts like x[ind1][..., ind2, :] under basic -slicing.

    -
    -

    Warning

    -

    The above is not true for advanced indexing.

    -
    -
  • -
  • You may use slicing to set values in the array, but (unlike lists) you -can never grow the array. The size of the value to be set in -x[obj] = value must be (broadcastable) to the same shape as -x[obj].

  • -
  • A slicing tuple can always be constructed as obj -and used in the x[obj] notation. Slice objects can be used in -the construction in place of the [start:stop:step] -notation. For example, x[1:10:5, ::-1] can also be implemented -as obj = (slice(1, 10, 5), slice(None, None, -1)); x[obj] . This -can be useful for constructing generic code that works on arrays -of arbitrary dimensions. See Dealing with variable numbers of indices within programs -for more information.

  • -
-
-
-

Dimensional indexing tools#

-

There are some tools to facilitate the easy matching of array shapes with -expressions and in assignments.

-

Ellipsis expands to the number of : objects needed for the -selection tuple to index all dimensions. In most cases, this means that the -length of the expanded selection tuple is x.ndim. There may only be a -single ellipsis present. -From the above example:

-
>>> x[..., 0]
-array([[1, 2, 3],
-      [4, 5, 6]])
-
-
-

This is equivalent to:

-
>>> x[:, :, 0]
-array([[1, 2, 3],
-      [4, 5, 6]])
-
-
-

Each newaxis object in the selection tuple serves to expand -the dimensions of the resulting selection by one unit-length -dimension. The added dimension is the position of the newaxis -object in the selection tuple. newaxis is an alias for -None, and None can be used in place of this with the same result. -From the above example:

-
>>> x[:, np.newaxis, :, :].shape
-(2, 1, 3, 1)
->>> x[:, None, :, :].shape
-(2, 1, 3, 1)
-
-
-

This can be handy to combine two -arrays in a way that otherwise would require explicit reshaping -operations. For example:

-
>>> x = np.arange(5)
->>> x[:, np.newaxis] + x[np.newaxis, :]
-array([[0, 1, 2, 3, 4],
-      [1, 2, 3, 4, 5],
-      [2, 3, 4, 5, 6],
-      [3, 4, 5, 6, 7],
-      [4, 5, 6, 7, 8]])
-
-
-
-
-
-

Advanced indexing#

-

Advanced indexing is triggered when the selection object, obj, is a -non-tuple sequence object, an ndarray (of data type integer or bool), -or a tuple with at least one sequence object or ndarray (of data type -integer or bool). There are two types of advanced indexing: integer -and Boolean.

-

Advanced indexing always returns a copy of the data (contrast with -basic slicing that returns a view).

-
-

Warning

-

The definition of advanced indexing means that x[(1, 2, 3),] is -fundamentally different than x[(1, 2, 3)]. The latter is -equivalent to x[1, 2, 3] which will trigger basic selection while -the former will trigger advanced indexing. Be sure to understand -why this occurs.

-

Also recognize that x[[1, 2, 3]] will trigger advanced indexing, -whereas due to the deprecated Numeric compatibility mentioned above, -x[[1, 2, slice(None)]] will trigger basic slicing.

-
-
-

Integer array indexing#

-

Integer array indexing allows selection of arbitrary items in the array -based on their N-dimensional index. Each integer array represents a number -of indices into that dimension.

-

Negative values are permitted in the index arrays and work as they do with -single indices or slices:

-
>>> x = np.arange(10, 1, -1)
->>> x
-array([10,  9,  8,  7,  6,  5,  4,  3,  2])
->>> x[np.array([3, 3, 1, 8])]
-array([7, 7, 9, 2])
->>> x[np.array([3, 3, -3, 8])]
-array([7, 7, 4, 2])
-
-
-

If the index values are out of bounds then an IndexError is thrown:

-
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
->>> x[np.array([1, -1])]
-array([[3, 4],
-      [5, 6]])
->>> x[np.array([3, 4])]
-Traceback (most recent call last):
-  ...
-IndexError: index 3 is out of bounds for axis 0 with size 3
-
-
-

When the index consists of as many integer arrays as dimensions of the array -being indexed, the indexing is straightforward, but different from slicing.

-

Advanced indices always are broadcast and -iterated as one:

-
result[i_1, ..., i_M] == x[ind_1[i_1, ..., i_M], ind_2[i_1, ..., i_M],
-                           ..., ind_N[i_1, ..., i_M]]
-
-
-

Note that the resulting shape is identical to the (broadcast) indexing array -shapes ind_1, ..., ind_N. If the indices cannot be broadcast to the -same shape, an exception IndexError: shape mismatch: indexing arrays could -not be broadcast together with shapes... is raised.

-

Indexing with multidimensional index arrays tend -to be more unusual uses, but they are permitted, and they are useful for some -problems. We’ll start with the simplest multidimensional case:

-
>>> y = np.arange(35).reshape(5, 7)
->>> y
-array([[ 0,  1,  2,  3,  4,  5,  6],
-       [ 7,  8,  9, 10, 11, 12, 13],
-       [14, 15, 16, 17, 18, 19, 20],
-       [21, 22, 23, 24, 25, 26, 27],
-       [28, 29, 30, 31, 32, 33, 34]])
->>> y[np.array([0, 2, 4]), np.array([0, 1, 2])]
-array([ 0, 15, 30])
-
-
-

In this case, if the index arrays have a matching shape, and there is an -index array for each dimension of the array being indexed, the resultant -array has the same shape as the index arrays, and the values correspond -to the index set for each position in the index arrays. In this example, -the first index value is 0 for both index arrays, and thus the first value -of the resultant array is y[0, 0]. The next value is y[2, 1], and -the last is y[4, 2].

-

If the index arrays do not have the same shape, there is an attempt to -broadcast them to the same shape. If they cannot be broadcast to the same -shape, an exception is raised:

-
>>> y[np.array([0, 2, 4]), np.array([0, 1])]
-Traceback (most recent call last):
-  ...
-IndexError: shape mismatch: indexing arrays could not be broadcast
-together with shapes (3,) (2,)
-
-
-

The broadcasting mechanism permits index arrays to be combined with -scalars for other indices. The effect is that the scalar value is used -for all the corresponding values of the index arrays:

-
>>> y[np.array([0, 2, 4]), 1]
-array([ 1, 15, 29])
-
-
-

Jumping to the next level of complexity, it is possible to only partially -index an array with index arrays. It takes a bit of thought to understand -what happens in such cases. For example if we just use one index array -with y:

-
>>> y[np.array([0, 2, 4])]
-array([[ 0,  1,  2,  3,  4,  5,  6],
-      [14, 15, 16, 17, 18, 19, 20],
-      [28, 29, 30, 31, 32, 33, 34]])
-
-
-

It results in the construction of a new array where each value of the -index array selects one row from the array being indexed and the resultant -array has the resulting shape (number of index elements, size of row).

-

In general, the shape of the resultant array will be the concatenation of -the shape of the index array (or the shape that all the index arrays were -broadcast to) with the shape of any unused dimensions (those not indexed) -in the array being indexed.

-

Example

-

From each row, a specific element should be selected. The row index is just -[0, 1, 2] and the column index specifies the element to choose for the -corresponding row, here [0, 1, 0]. Using both together the task -can be solved using advanced indexing:

-
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
->>> x[[0, 1, 2], [0, 1, 0]]
-array([1, 4, 5])
-
-
-

To achieve a behaviour similar to the basic slicing above, broadcasting can be -used. The function ix_ can help with this broadcasting. This is best -understood with an example.

-

Example

-

From a 4x3 array the corner elements should be selected using advanced -indexing. Thus all elements for which the column is one of [0, 2] and -the row is one of [0, 3] need to be selected. To use advanced indexing -one needs to select all elements explicitly. Using the method explained -previously one could write:

-
>>> x = np.array([[ 0,  1,  2],
-...               [ 3,  4,  5],
-...               [ 6,  7,  8],
-...               [ 9, 10, 11]])
->>> rows = np.array([[0, 0],
-...                  [3, 3]], dtype=np.intp)
->>> columns = np.array([[0, 2],
-...                     [0, 2]], dtype=np.intp)
->>> x[rows, columns]
-array([[ 0,  2],
-       [ 9, 11]])
-
-
-

However, since the indexing arrays above just repeat themselves, -broadcasting can be used (compare operations such as -rows[:, np.newaxis] + columns) to simplify this:

-
>>> rows = np.array([0, 3], dtype=np.intp)
->>> columns = np.array([0, 2], dtype=np.intp)
->>> rows[:, np.newaxis]
-array([[0],
-       [3]])
->>> x[rows[:, np.newaxis], columns]
-array([[ 0,  2],
-       [ 9, 11]])
-
-
-

This broadcasting can also be achieved using the function ix_:

-
>>> x[np.ix_(rows, columns)]
-array([[ 0,  2],
-       [ 9, 11]])
-
-
-

Note that without the np.ix_ call, only the diagonal elements would -be selected:

-
>>> x[rows, columns]
-array([ 0, 11])
-
-
-

This difference is the most important thing to remember about -indexing with multiple advanced indices.

-

Example

-

A real-life example of where advanced indexing may be useful is for a color -lookup table where we want to map the values of an image into RGB triples for -display. The lookup table could have a shape (nlookup, 3). Indexing -such an array with an image with shape (ny, nx) with dtype=np.uint8 -(or any integer type so long as values are with the bounds of the -lookup table) will result in an array of shape (ny, nx, 3) where a -triple of RGB values is associated with each pixel location.

-
-
-

Boolean array indexing#

-

This advanced indexing occurs when obj is an array object of Boolean -type, such as may be returned from comparison operators. A single -boolean index array is practically identical to x[obj.nonzero()] where, -as described above, obj.nonzero() returns a -tuple (of length obj.ndim) of integer index -arrays showing the True elements of obj. However, it is -faster when obj.shape == x.shape.

-

If obj.ndim == x.ndim, x[obj] returns a 1-dimensional array -filled with the elements of x corresponding to the True -values of obj. The search order will be row-major, -C-style. If obj has True values at entries that are outside -of the bounds of x, then an index error will be raised. If obj is -smaller than x it is identical to filling it with False.

-

A common use case for this is filtering for desired element values. -For example, one may wish to select all entries from an array which -are not NaN:

-
>>> x = np.array([[1., 2.], [np.nan, 3.], [np.nan, np.nan]])
->>> x[~np.isnan(x)]
-array([1., 2., 3.])
-
-
-

Or wish to add a constant to all negative elements:

-
>>> x = np.array([1., -1., -2., 3])
->>> x[x < 0] += 20
->>> x
-array([ 1., 19., 18., 3.])
-
-
-

In general if an index includes a Boolean array, the result will be -identical to inserting obj.nonzero() into the same position -and using the integer array indexing mechanism described above. -x[ind_1, boolean_array, ind_2] is equivalent to -x[(ind_1,) + boolean_array.nonzero() + (ind_2,)].

-

If there is only one Boolean array and no integer indexing array present, -this is straightforward. Care must only be taken to make sure that the -boolean index has exactly as many dimensions as it is supposed to work -with.

-

In general, when the boolean array has fewer dimensions than the array being -indexed, this is equivalent to x[b, ...], which means x is indexed by b -followed by as many : as are needed to fill out the rank of x. Thus the -shape of the result is one dimension containing the number of True elements of -the boolean array, followed by the remaining dimensions of the array being -indexed:

-
>>> x = np.arange(35).reshape(5, 7)
->>> b = x > 20
->>> b[:, 5]
-array([False, False, False,  True,  True])
->>> x[b[:, 5]]
-array([[21, 22, 23, 24, 25, 26, 27],
-      [28, 29, 30, 31, 32, 33, 34]])
-
-
-

Here the 4th and 5th rows are selected from the indexed array and -combined to make a 2-D array.

-

Example

-

From an array, select all rows which sum up to less or equal two:

-
>>> x = np.array([[0, 1], [1, 1], [2, 2]])
->>> rowsum = x.sum(-1)
->>> x[rowsum <= 2, :]
-array([[0, 1],
-       [1, 1]])
-
-
-

Combining multiple Boolean indexing arrays or a Boolean with an integer -indexing array can best be understood with the -obj.nonzero() analogy. The function ix_ -also supports boolean arrays and will work without any surprises.

-

Example

-

Use boolean indexing to select all rows adding up to an even -number. At the same time columns 0 and 2 should be selected with an -advanced integer index. Using the ix_ function this can be done -with:

-
>>> x = np.array([[ 0,  1,  2],
-...               [ 3,  4,  5],
-...               [ 6,  7,  8],
-...               [ 9, 10, 11]])
->>> rows = (x.sum(-1) % 2) == 0
->>> rows
-array([False,  True, False,  True])
->>> columns = [0, 2]
->>> x[np.ix_(rows, columns)]
-array([[ 3,  5],
-       [ 9, 11]])
-
-
-

Without the np.ix_ call, only the diagonal elements would be -selected.

-

Or without np.ix_ (compare the integer array examples):

-
>>> rows = rows.nonzero()[0]
->>> x[rows[:, np.newaxis], columns]
-array([[ 3,  5],
-       [ 9, 11]])
-
-
-

Example

-

Use a 2-D boolean array of shape (2, 3) -with four True elements to select rows from a 3-D array of shape -(2, 3, 5) results in a 2-D result of shape (4, 5):

-
>>> x = np.arange(30).reshape(2, 3, 5)
->>> x
-array([[[ 0,  1,  2,  3,  4],
-        [ 5,  6,  7,  8,  9],
-        [10, 11, 12, 13, 14]],
-      [[15, 16, 17, 18, 19],
-        [20, 21, 22, 23, 24],
-        [25, 26, 27, 28, 29]]])
->>> b = np.array([[True, True, False], [False, True, True]])
->>> x[b]
-array([[ 0,  1,  2,  3,  4],
-      [ 5,  6,  7,  8,  9],
-      [20, 21, 22, 23, 24],
-      [25, 26, 27, 28, 29]])
-
-
-
-
-

Combining advanced and basic indexing#

-

When there is at least one slice (:), ellipsis (...) or newaxis -in the index (or the array has more dimensions than there are advanced indices), -then the behaviour can be more complicated. It is like concatenating the -indexing result for each advanced index element.

-

In the simplest case, there is only a single advanced index combined with -a slice. For example:

-
>>> y = np.arange(35).reshape(5,7)
->>> y[np.array([0, 2, 4]), 1:3]
-array([[ 1,  2],
-       [15, 16],
-       [29, 30]])
-
-
-

In effect, the slice and index array operation are independent. The slice -operation extracts columns with index 1 and 2, (i.e. the 2nd and 3rd columns), -followed by the index array operation which extracts rows with index 0, 2 and 4 -(i.e the first, third and fifth rows). This is equivalent to:

-
>>> y[:, 1:3][np.array([0, 2, 4]), :]
-array([[ 1,  2],
-       [15, 16],
-       [29, 30]])
-
-
-

A single advanced index can, for example, replace a slice and the result array -will be the same. However, it is a copy and may have a different memory layout. -A slice is preferable when it is possible. -For example:

-
>>> x = np.array([[ 0,  1,  2],
-...               [ 3,  4,  5],
-...               [ 6,  7,  8],
-...               [ 9, 10, 11]])
->>> x[1:2, 1:3]
-array([[4, 5]])
->>> x[1:2, [1, 2]]
-array([[4, 5]])
-
-
-

The easiest way to understand a combination of multiple advanced indices may -be to think in terms of the resulting shape. There are two parts to the indexing -operation, the subspace defined by the basic indexing (excluding integers) and -the subspace from the advanced indexing part. Two cases of index combination -need to be distinguished:

-
    -
  • The advanced indices are separated by a slice, Ellipsis or -newaxis. For example x[arr1, :, arr2].

  • -
  • The advanced indices are all next to each other. -For example x[..., arr1, arr2, :] but not x[arr1, :, 1] -since 1 is an advanced index in this regard.

  • -
-

In the first case, the dimensions resulting from the advanced indexing -operation come first in the result array, and the subspace dimensions after -that. -In the second case, the dimensions from the advanced indexing operations -are inserted into the result array at the same spot as they were in the -initial array (the latter logic is what makes simple advanced indexing -behave just like slicing).

-

Example

-

Suppose x.shape is (10, 20, 30) and ind is a (2, 3, 4)-shaped -indexing intp array, then result = x[..., ind, :] has -shape (10, 2, 3, 4, 30) because the (20,)-shaped subspace has been -replaced with a (2, 3, 4)-shaped broadcasted indexing subspace. If -we let i, j, k loop over the (2, 3, 4)-shaped subspace then -result[..., i, j, k, :] = x[..., ind[i, j, k], :]. This example -produces the same result as x.take(ind, axis=-2).

-

Example

-

Let x.shape be (10, 20, 30, 40, 50) and suppose ind_1 -and ind_2 can be broadcast to the shape (2, 3, 4). Then -x[:, ind_1, ind_2] has shape (10, 2, 3, 4, 40, 50) because the -(20, 30)-shaped subspace from X has been replaced with the -(2, 3, 4) subspace from the indices. However, -x[:, ind_1, :, ind_2] has shape (2, 3, 4, 10, 30, 50) because there -is no unambiguous place to drop in the indexing subspace, thus -it is tacked-on to the beginning. It is always possible to use -.transpose() to move the subspace -anywhere desired. Note that this example cannot be replicated -using take.

-

Example

-

Slicing can be combined with broadcasted boolean indices:

-
>>> x = np.arange(35).reshape(5, 7)
->>> b = x > 20
->>> b
-array([[False, False, False, False, False, False, False],
-      [False, False, False, False, False, False, False],
-      [False, False, False, False, False, False, False],
-      [ True,  True,  True,  True,  True,  True,  True],
-      [ True,  True,  True,  True,  True,  True,  True]])
->>> x[b[:, 5], 1:3]
-array([[22, 23],
-      [29, 30]])
-
-
-
-
-
-

Field access#

-
-

See also

-

Structured arrays

-
-

If the ndarray object is a structured array the fields -of the array can be accessed by indexing the array with strings, -dictionary-like.

-

Indexing x['field-name'] returns a new view to the array, -which is of the same shape as x (except when the field is a -sub-array) but of data type x.dtype['field-name'] and contains -only the part of the data in the specified field. Also, -record array scalars can be “indexed” this way.

-

Indexing into a structured array can also be done with a list of field names, -e.g. x[['field-name1', 'field-name2']]. As of NumPy 1.16, this returns a -view containing only those fields. In older versions of NumPy, it returned a -copy. See the user guide section on Structured arrays for more -information on multifield indexing.

-

If the accessed field is a sub-array, the dimensions of the sub-array -are appended to the shape of the result. -For example:

-
>>> x = np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))])
->>> x['a'].shape
-(2, 2)
->>> x['a'].dtype
-dtype('int32')
->>> x['b'].shape
-(2, 2, 3, 3)
->>> x['b'].dtype
-dtype('float64')
-
-
-
-
-

Flat Iterator indexing#

-

x.flat returns an iterator that will iterate -over the entire array (in C-contiguous style with the last index -varying the fastest). This iterator object can also be indexed using -basic slicing or advanced indexing as long as the selection object is -not a tuple. This should be clear from the fact that x.flat is a 1-dimensional view. It can be used for integer -indexing with 1-dimensional C-style-flat indices. The shape of any -returned array is therefore the shape of the integer indexing object.

-
-
-

Assigning values to indexed arrays#

-

As mentioned, one can select a subset of an array to assign to using -a single index, slices, and index and mask arrays. The value being -assigned to the indexed array must be shape consistent (the same shape -or broadcastable to the shape the index produces). For example, it is -permitted to assign a constant to a slice:

-
>>> x = np.arange(10)
->>> x[2:7] = 1
-
-
-

or an array of the right size:

-
>>> x[2:7] = np.arange(5)
-
-
-

Note that assignments may result in changes if assigning -higher types to lower types (like floats to ints) or even -exceptions (assigning complex to floats or ints):

-
>>> x[1] = 1.2
->>> x[1]
-1
->>> x[1] = 1.2j
-Traceback (most recent call last):
-  ...
-TypeError: can't convert complex to int
-
-
-

Unlike some of the references (such as array and mask indices) -assignments are always made to the original data in the array -(indeed, nothing else would make sense!). Note though, that some -actions may not work as one may naively expect. This particular -example is often surprising to people:

-
>>> x = np.arange(0, 50, 10)
->>> x
-array([ 0, 10, 20, 30, 40])
->>> x[np.array([1, 1, 3, 1])] += 1
->>> x
-array([ 0, 11, 20, 31, 40])
-
-
-

Where people expect that the 1st location will be incremented by 3. -In fact, it will only be incremented by 1. The reason is that -a new array is extracted from the original (as a temporary) containing -the values at 1, 1, 3, 1, then the value 1 is added to the temporary, -and then the temporary is assigned back to the original array. Thus -the value of the array at x[1] + 1 is assigned to x[1] three times, -rather than being incremented 3 times.

-
-
-

Dealing with variable numbers of indices within programs#

-

The indexing syntax is very powerful but limiting when dealing with -a variable number of indices. For example, if you want to write -a function that can handle arguments with various numbers of -dimensions without having to write special case code for each -number of possible dimensions, how can that be done? If one -supplies to the index a tuple, the tuple will be interpreted -as a list of indices. For example:

-
>>> z = np.arange(81).reshape(3, 3, 3, 3)
->>> indices = (1, 1, 1, 1)
->>> z[indices]
-40
-
-
-

So one can use code to construct tuples of any number of indices -and then use these within an index.

-

Slices can be specified within programs by using the slice() function -in Python. For example:

-
>>> indices = (1, 1, 1, slice(0, 2))  # same as [1, 1, 1, 0:2]
->>> z[indices]
-array([39, 40])
-
-
-

Likewise, ellipsis can be specified by code by using the Ellipsis -object:

-
>>> indices = (1, Ellipsis, 1)  # same as [1, ..., 1]
->>> z[indices]
-array([[28, 31, 34],
-       [37, 40, 43],
-       [46, 49, 52]])
-
-
-

For this reason, it is possible to use the output from the -np.nonzero() function directly as an index since -it always returns a tuple of index arrays.

-

Because of the special treatment of tuples, they are not automatically -converted to an array as a list would be. As an example:

-
>>> z[[1, 1, 1, 1]]  # produces a large array
-array([[[[27, 28, 29],
-         [30, 31, 32], ...
->>> z[(1, 1, 1, 1)]  # returns a single value
-40
-
-
-
-
-

Detailed notes#

-

These are some detailed notes, which are not of importance for day to day -indexing (in no particular order):

-
    -
  • The native NumPy indexing type is intp and may differ from the -default integer array type. intp is the smallest data type -sufficient to safely index any array; for advanced indexing it may be -faster than other types.

  • -
  • For advanced assignments, there is in general no guarantee for the -iteration order. This means that if an element is set more than once, -it is not possible to predict the final result.

  • -
  • An empty (tuple) index is a full scalar index into a zero-dimensional array. -x[()] returns a scalar if x is zero-dimensional and a view -otherwise. On the other hand, x[...] always returns a view.

  • -
  • If a zero-dimensional array is present in the index and it is a full -integer index the result will be a scalar and not a zero-dimensional array. -(Advanced indexing is not triggered.)

  • -
  • When an ellipsis (...) is present but has no size (i.e. replaces zero -:) the result will still always be an array. A view if no advanced index -is present, otherwise a copy.

  • -
  • The nonzero equivalence for Boolean arrays does not hold for zero -dimensional boolean arrays.

  • -
  • When the result of an advanced indexing operation has no elements but an -individual index is out of bounds, whether or not an IndexError is -raised is undefined (e.g. x[[], [123]] with 123 being out of bounds).

  • -
  • When a casting error occurs during assignment (for example updating a -numerical array using a sequence of strings), the array being assigned -to may end up in an unpredictable partially updated state. -However, if any other error (such as an out of bounds index) occurs, the -array will remain unchanged.

  • -
  • The memory layout of an advanced indexing result is optimized for each -indexing operation and no particular memory order can be assumed.

  • -
  • When using a subclass (especially one which manipulates its shape), the -default ndarray.__setitem__ behaviour will call __getitem__ for -basic indexing but not for advanced indexing. For such a subclass it may -be preferable to call ndarray.__setitem__ with a base class ndarray -view on the data. This must be done if the subclasses __getitem__ does -not return views.

  • -
-
-
- - -
- - - - - -
- - -
-
- - - -
-
- - - - - -
-
- - \ No newline at end of file diff --git a/tests/integrated/test-boutpp/slicing/basics.indexing.txt b/tests/integrated/test-boutpp/slicing/basics.indexing.txt deleted file mode 100644 index eba782d5e2..0000000000 --- a/tests/integrated/test-boutpp/slicing/basics.indexing.txt +++ /dev/null @@ -1,687 +0,0 @@ - -logo - - User Guide - API reference - Development - Release notes - Learn - - GitHub - Twitter - - What is NumPy? - Installation - NumPy quickstart - NumPy: the absolute basics for beginners - NumPy fundamentals - Array creation - Indexing on ndarrays - I/O with NumPy - Data types - Broadcasting - Byte-swapping - Structured arrays - Writing custom array containers - Subclassing ndarray - Universal functions ( ufunc ) basics - Copies and views - Interoperability with NumPy - Miscellaneous - NumPy for MATLAB users - Building from source - Using NumPy C-API - NumPy Tutorials - NumPy How Tos - For downstream package authors - - F2PY user guide and reference manual - Glossary - Under-the-hood Documentation for developers - Reporting bugs - Release notes - NumPy license - -On this page - - Basic indexing - Single element indexing - Slicing and striding - Dimensional indexing tools - Advanced indexing - Field access - Flat Iterator indexing - Assigning values to indexed arrays - Dealing with variable numbers of indices within programs - Detailed notes - -Indexing on ndarrays - -See also - -Indexing routines - -ndarrays can be indexed using the standard Python x[obj] syntax, where x is the array and obj the selection. There are different kinds of indexing available depending on obj: basic indexing, advanced indexing and field access. - -Most of the following examples show the use of indexing when referencing data in an array. The examples work just as well when assigning to an array. See Assigning values to indexed arrays for specific examples and explanations on how assignments work. - -Note that in Python, x[(exp1, exp2, ..., expN)] is equivalent to x[exp1, exp2, ..., expN]; the latter is just syntactic sugar for the former. -Basic indexing -Single element indexing - -Single element indexing works exactly like that for other standard Python sequences. It is 0-based, and accepts negative indices for indexing from the end of the array. - -x = np.arange(10) - -x[2] -2 - -x[-2] -8 - -It is not necessary to separate each dimension’s index into its own set of square brackets. - -x.shape = (2, 5) # now x is 2-dimensional - -x[1, 3] -8 - -x[1, -1] -9 - -Note that if one indexes a multidimensional array with fewer indices than dimensions, one gets a subdimensional array. For example: - -x[0] -array([0, 1, 2, 3, 4]) - -That is, each index specified selects the array corresponding to the rest of the dimensions selected. In the above example, choosing 0 means that the remaining dimension of length 5 is being left unspecified, and that what is returned is an array of that dimensionality and size. It must be noted that the returned array is a view, i.e., it is not a copy of the original, but points to the same values in memory as does the original array. In this case, the 1-D array at the first position (0) is returned. So using a single index on the returned array, results in a single element being returned. That is: - -x[0][2] -2 - -So note that x[0, 2] == x[0][2] though the second case is more inefficient as a new temporary array is created after the first index that is subsequently indexed by 2. - -Note - -NumPy uses C-order indexing. That means that the last index usually represents the most rapidly changing memory location, unlike Fortran or IDL, where the first index represents the most rapidly changing location in memory. This difference represents a great potential for confusion. -Slicing and striding - -Basic slicing extends Python’s basic concept of slicing to N dimensions. Basic slicing occurs when obj is a slice object (constructed by start:stop:step notation inside of brackets), an integer, or a tuple of slice objects and integers. Ellipsis and newaxis objects can be interspersed with these as well. - -The simplest case of indexing with N integers returns an array scalar representing the corresponding item. As in Python, all indices are zero-based: for the i-th index -, the valid range is where is the i-th element of the shape of the array. Negative indices are interpreted as counting from the end of the array (i.e., if , it means - -). - -All arrays generated by basic slicing are always views of the original array. - -Note - -NumPy slicing creates a view instead of a copy as in the case of built-in Python sequences such as string, tuple and list. Care must be taken when extracting a small portion from a large array which becomes useless after the extraction, because the small portion extracted contains a reference to the large original array whose memory will not be released until all arrays derived from it are garbage-collected. In such cases an explicit copy() is recommended. - -The standard rules of sequence slicing apply to basic slicing on a per-dimension basis (including using a step index). Some useful concepts to remember include: - - The basic slice syntax is i:j:k where i is the starting index, j is the stopping index, and k is the step ( - -). This selects the m elements (in the corresponding dimension) with index values i, i + k, …, i + (m - 1) k where - -and q and r are the quotient and remainder obtained by dividing j - i by k: j - i = q k + r, so that i + (m - 1) k < j. For example: - -x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - -x[1:7:2] -array([1, 3, 5]) - -Negative i and j are interpreted as n + i and n + j where n is the number of elements in the corresponding dimension. Negative k makes stepping go towards smaller indices. From the above example: - -x[-2:10] -array([8, 9]) - -x[-3:3:-1] -array([7, 6, 5, 4]) - -Assume n is the number of elements in the dimension being sliced. Then, if i is not given it defaults to 0 for k > 0 and n - 1 for k < 0 . If j is not given it defaults to n for k > 0 and -n-1 for k < 0 . If k is not given it defaults to 1. Note that :: is the same as : and means select all indices along this axis. From the above example: - -x[5:] -array([5, 6, 7, 8, 9]) - -If the number of objects in the selection tuple is less than N, then : is assumed for any subsequent dimensions. For example: - -x = np.array([[[1],[2],[3]], [[4],[5],[6]]]) - -x.shape -(2, 3, 1) - - x[1:2] - array([[[4], - [5], - [6]]]) - - An integer, i, returns the same values as i:i+1 except the dimensionality of the returned object is reduced by 1. In particular, a selection tuple with the p-th element an integer (and all other entries :) returns the corresponding sub-array with dimension N - 1. If N = 1 then the returned object is an array scalar. These objects are explained in Scalars. - - If the selection tuple has all entries : except the p-th entry which is a slice object i:j:k, then the returned array has dimension N formed by concatenating the sub-arrays returned by integer indexing of elements i, i+k, …, i + (m - 1) k < j, - - Basic slicing with more than one non-: entry in the slicing tuple, acts like repeated application of slicing using a single non-: entry, where the non-: entries are successively taken (with all other non-: entries replaced by :). Thus, x[ind1, ..., ind2,:] acts like x[ind1][..., ind2, :] under basic slicing. - - Warning - - The above is not true for advanced indexing. - - You may use slicing to set values in the array, but (unlike lists) you can never grow the array. The size of the value to be set in x[obj] = value must be (broadcastable) to the same shape as x[obj]. - - A slicing tuple can always be constructed as obj and used in the x[obj] notation. Slice objects can be used in the construction in place of the [start:stop:step] notation. For example, x[1:10:5, ::-1] can also be implemented as obj = (slice(1, 10, 5), slice(None, None, -1)); x[obj] . This can be useful for constructing generic code that works on arrays of arbitrary dimensions. See Dealing with variable numbers of indices within programs for more information. - -Dimensional indexing tools - -There are some tools to facilitate the easy matching of array shapes with expressions and in assignments. - -Ellipsis expands to the number of : objects needed for the selection tuple to index all dimensions. In most cases, this means that the length of the expanded selection tuple is x.ndim. There may only be a single ellipsis present. From the above example: - -x[..., 0] -array([[1, 2, 3], - [4, 5, 6]]) - -This is equivalent to: - -x[:, :, 0] -array([[1, 2, 3], - [4, 5, 6]]) - -Each newaxis object in the selection tuple serves to expand the dimensions of the resulting selection by one unit-length dimension. The added dimension is the position of the newaxis object in the selection tuple. newaxis is an alias for None, and None can be used in place of this with the same result. From the above example: - -x[:, np.newaxis, :, :].shape -(2, 1, 3, 1) - -x[:, None, :, :].shape -(2, 1, 3, 1) - -This can be handy to combine two arrays in a way that otherwise would require explicit reshaping operations. For example: - -x = np.arange(5) - -x[:, np.newaxis] + x[np.newaxis, :] -array([[0, 1, 2, 3, 4], - [1, 2, 3, 4, 5], - [2, 3, 4, 5, 6], - [3, 4, 5, 6, 7], - [4, 5, 6, 7, 8]]) - -Advanced indexing - -Advanced indexing is triggered when the selection object, obj, is a non-tuple sequence object, an ndarray (of data type integer or bool), or a tuple with at least one sequence object or ndarray (of data type integer or bool). There are two types of advanced indexing: integer and Boolean. - -Advanced indexing always returns a copy of the data (contrast with basic slicing that returns a view). - -Warning - -The definition of advanced indexing means that x[(1, 2, 3),] is fundamentally different than x[(1, 2, 3)]. The latter is equivalent to x[1, 2, 3] which will trigger basic selection while the former will trigger advanced indexing. Be sure to understand why this occurs. - -Also recognize that x[[1, 2, 3]] will trigger advanced indexing, whereas due to the deprecated Numeric compatibility mentioned above, x[[1, 2, slice(None)]] will trigger basic slicing. -Integer array indexing - -Integer array indexing allows selection of arbitrary items in the array based on their N-dimensional index. Each integer array represents a number of indices into that dimension. - -Negative values are permitted in the index arrays and work as they do with single indices or slices: - -x = np.arange(10, 1, -1) - -x -array([10, 9, 8, 7, 6, 5, 4, 3, 2]) - -x[np.array([3, 3, 1, 8])] -array([7, 7, 9, 2]) - -x[np.array([3, 3, -3, 8])] -array([7, 7, 4, 2]) - -If the index values are out of bounds then an IndexError is thrown: - -x = np.array([[1, 2], [3, 4], [5, 6]]) - -x[np.array([1, -1])] -array([[3, 4], - [5, 6]]) - -x[np.array([3, 4])] -Traceback (most recent call last): - ... -IndexError: index 3 is out of bounds for axis 0 with size 3 - -When the index consists of as many integer arrays as dimensions of the array being indexed, the indexing is straightforward, but different from slicing. - -Advanced indices always are broadcast and iterated as one: - -result[i_1, ..., i_M] == x[ind_1[i_1, ..., i_M], ind_2[i_1, ..., i_M], - ..., ind_N[i_1, ..., i_M]] - -Note that the resulting shape is identical to the (broadcast) indexing array shapes ind_1, ..., ind_N. If the indices cannot be broadcast to the same shape, an exception IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes... is raised. - -Indexing with multidimensional index arrays tend to be more unusual uses, but they are permitted, and they are useful for some problems. We’ll start with the simplest multidimensional case: - -y = np.arange(35).reshape(5, 7) - -y -array([[ 0, 1, 2, 3, 4, 5, 6], - [ 7, 8, 9, 10, 11, 12, 13], - [14, 15, 16, 17, 18, 19, 20], - [21, 22, 23, 24, 25, 26, 27], - [28, 29, 30, 31, 32, 33, 34]]) - -y[np.array([0, 2, 4]), np.array([0, 1, 2])] -array([ 0, 15, 30]) - -In this case, if the index arrays have a matching shape, and there is an index array for each dimension of the array being indexed, the resultant array has the same shape as the index arrays, and the values correspond to the index set for each position in the index arrays. In this example, the first index value is 0 for both index arrays, and thus the first value of the resultant array is y[0, 0]. The next value is y[2, 1], and the last is y[4, 2]. - -If the index arrays do not have the same shape, there is an attempt to broadcast them to the same shape. If they cannot be broadcast to the same shape, an exception is raised: - -y[np.array([0, 2, 4]), np.array([0, 1])] -Traceback (most recent call last): - ... -IndexError: shape mismatch: indexing arrays could not be broadcast -together with shapes (3,) (2,) - -The broadcasting mechanism permits index arrays to be combined with scalars for other indices. The effect is that the scalar value is used for all the corresponding values of the index arrays: - -y[np.array([0, 2, 4]), 1] -array([ 1, 15, 29]) - -Jumping to the next level of complexity, it is possible to only partially index an array with index arrays. It takes a bit of thought to understand what happens in such cases. For example if we just use one index array with y: - -y[np.array([0, 2, 4])] -array([[ 0, 1, 2, 3, 4, 5, 6], - [14, 15, 16, 17, 18, 19, 20], - [28, 29, 30, 31, 32, 33, 34]]) - -It results in the construction of a new array where each value of the index array selects one row from the array being indexed and the resultant array has the resulting shape (number of index elements, size of row). - -In general, the shape of the resultant array will be the concatenation of the shape of the index array (or the shape that all the index arrays were broadcast to) with the shape of any unused dimensions (those not indexed) in the array being indexed. - -Example - -From each row, a specific element should be selected. The row index is just [0, 1, 2] and the column index specifies the element to choose for the corresponding row, here [0, 1, 0]. Using both together the task can be solved using advanced indexing: - -x = np.array([[1, 2], [3, 4], [5, 6]]) - -x[[0, 1, 2], [0, 1, 0]] -array([1, 4, 5]) - -To achieve a behaviour similar to the basic slicing above, broadcasting can be used. The function ix_ can help with this broadcasting. This is best understood with an example. - -Example - -From a 4x3 array the corner elements should be selected using advanced indexing. Thus all elements for which the column is one of [0, 2] and the row is one of [0, 3] need to be selected. To use advanced indexing one needs to select all elements explicitly. Using the method explained previously one could write: - -x = np.array([[ 0, 1, 2], - - [ 3, 4, 5], - - [ 6, 7, 8], - - [ 9, 10, 11]]) - -rows = np.array([[0, 0], - - [3, 3]], dtype=np.intp) - -columns = np.array([[0, 2], - - [0, 2]], dtype=np.intp) - -x[rows, columns] -array([[ 0, 2], - [ 9, 11]]) - -However, since the indexing arrays above just repeat themselves, broadcasting can be used (compare operations such as rows[:, np.newaxis] + columns) to simplify this: - -rows = np.array([0, 3], dtype=np.intp) - -columns = np.array([0, 2], dtype=np.intp) - -rows[:, np.newaxis] -array([[0], - [3]]) - -x[rows[:, np.newaxis], columns] -array([[ 0, 2], - [ 9, 11]]) - -This broadcasting can also be achieved using the function ix_: - -x[np.ix_(rows, columns)] -array([[ 0, 2], - [ 9, 11]]) - -Note that without the np.ix_ call, only the diagonal elements would be selected: - -x[rows, columns] -array([ 0, 11]) - -This difference is the most important thing to remember about indexing with multiple advanced indices. - -Example - -A real-life example of where advanced indexing may be useful is for a color lookup table where we want to map the values of an image into RGB triples for display. The lookup table could have a shape (nlookup, 3). Indexing such an array with an image with shape (ny, nx) with dtype=np.uint8 (or any integer type so long as values are with the bounds of the lookup table) will result in an array of shape (ny, nx, 3) where a triple of RGB values is associated with each pixel location. -Boolean array indexing - -This advanced indexing occurs when obj is an array object of Boolean type, such as may be returned from comparison operators. A single boolean index array is practically identical to x[obj.nonzero()] where, as described above, obj.nonzero() returns a tuple (of length obj.ndim) of integer index arrays showing the True elements of obj. However, it is faster when obj.shape == x.shape. - -If obj.ndim == x.ndim, x[obj] returns a 1-dimensional array filled with the elements of x corresponding to the True values of obj. The search order will be row-major, C-style. If obj has True values at entries that are outside of the bounds of x, then an index error will be raised. If obj is smaller than x it is identical to filling it with False. - -A common use case for this is filtering for desired element values. For example, one may wish to select all entries from an array which are not NaN: - -x = np.array([[1., 2.], [np.nan, 3.], [np.nan, np.nan]]) - -x[~np.isnan(x)] -array([1., 2., 3.]) - -Or wish to add a constant to all negative elements: - -x = np.array([1., -1., -2., 3]) - -x[x < 0] += 20 - -x -array([ 1., 19., 18., 3.]) - -In general if an index includes a Boolean array, the result will be identical to inserting obj.nonzero() into the same position and using the integer array indexing mechanism described above. x[ind_1, boolean_array, ind_2] is equivalent to x[(ind_1,) + boolean_array.nonzero() + (ind_2,)]. - -If there is only one Boolean array and no integer indexing array present, this is straightforward. Care must only be taken to make sure that the boolean index has exactly as many dimensions as it is supposed to work with. - -In general, when the boolean array has fewer dimensions than the array being indexed, this is equivalent to x[b, ...], which means x is indexed by b followed by as many : as are needed to fill out the rank of x. Thus the shape of the result is one dimension containing the number of True elements of the boolean array, followed by the remaining dimensions of the array being indexed: - -x = np.arange(35).reshape(5, 7) - -b = x > 20 - -b[:, 5] -array([False, False, False, True, True]) - -x[b[:, 5]] -array([[21, 22, 23, 24, 25, 26, 27], - [28, 29, 30, 31, 32, 33, 34]]) - -Here the 4th and 5th rows are selected from the indexed array and combined to make a 2-D array. - -Example - -From an array, select all rows which sum up to less or equal two: - -x = np.array([[0, 1], [1, 1], [2, 2]]) - -rowsum = x.sum(-1) - -x[rowsum <= 2, :] -array([[0, 1], - [1, 1]]) - -Combining multiple Boolean indexing arrays or a Boolean with an integer indexing array can best be understood with the obj.nonzero() analogy. The function ix_ also supports boolean arrays and will work without any surprises. - -Example - -Use boolean indexing to select all rows adding up to an even number. At the same time columns 0 and 2 should be selected with an advanced integer index. Using the ix_ function this can be done with: - -x = np.array([[ 0, 1, 2], - - [ 3, 4, 5], - - [ 6, 7, 8], - - [ 9, 10, 11]]) - -rows = (x.sum(-1) % 2) == 0 - -rows -array([False, True, False, True]) - -columns = [0, 2] - -x[np.ix_(rows, columns)] -array([[ 3, 5], - [ 9, 11]]) - -Without the np.ix_ call, only the diagonal elements would be selected. - -Or without np.ix_ (compare the integer array examples): - -rows = rows.nonzero()[0] - -x[rows[:, np.newaxis], columns] -array([[ 3, 5], - [ 9, 11]]) - -Example - -Use a 2-D boolean array of shape (2, 3) with four True elements to select rows from a 3-D array of shape (2, 3, 5) results in a 2-D result of shape (4, 5): - -x = np.arange(30).reshape(2, 3, 5) - -x -array([[[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]], - [[15, 16, 17, 18, 19], - [20, 21, 22, 23, 24], - [25, 26, 27, 28, 29]]]) - -b = np.array([[True, True, False], [False, True, True]]) - -x[b] -array([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [20, 21, 22, 23, 24], - [25, 26, 27, 28, 29]]) - -Combining advanced and basic indexing - -When there is at least one slice (:), ellipsis (...) or newaxis in the index (or the array has more dimensions than there are advanced indices), then the behaviour can be more complicated. It is like concatenating the indexing result for each advanced index element. - -In the simplest case, there is only a single advanced index combined with a slice. For example: - -y = np.arange(35).reshape(5,7) - -y[np.array([0, 2, 4]), 1:3] -array([[ 1, 2], - [15, 16], - [29, 30]]) - -In effect, the slice and index array operation are independent. The slice operation extracts columns with index 1 and 2, (i.e. the 2nd and 3rd columns), followed by the index array operation which extracts rows with index 0, 2 and 4 (i.e the first, third and fifth rows). This is equivalent to: - -y[:, 1:3][np.array([0, 2, 4]), :] -array([[ 1, 2], - [15, 16], - [29, 30]]) - -A single advanced index can, for example, replace a slice and the result array will be the same. However, it is a copy and may have a different memory layout. A slice is preferable when it is possible. For example: - -x = np.array([[ 0, 1, 2], - - [ 3, 4, 5], - - [ 6, 7, 8], - - [ 9, 10, 11]]) - -x[1:2, 1:3] -array([[4, 5]]) - -x[1:2, [1, 2]] -array([[4, 5]]) - -The easiest way to understand a combination of multiple advanced indices may be to think in terms of the resulting shape. There are two parts to the indexing operation, the subspace defined by the basic indexing (excluding integers) and the subspace from the advanced indexing part. Two cases of index combination need to be distinguished: - - The advanced indices are separated by a slice, Ellipsis or newaxis. For example x[arr1, :, arr2]. - - The advanced indices are all next to each other. For example x[..., arr1, arr2, :] but not x[arr1, :, 1] since 1 is an advanced index in this regard. - -In the first case, the dimensions resulting from the advanced indexing operation come first in the result array, and the subspace dimensions after that. In the second case, the dimensions from the advanced indexing operations are inserted into the result array at the same spot as they were in the initial array (the latter logic is what makes simple advanced indexing behave just like slicing). - -Example - -Suppose x.shape is (10, 20, 30) and ind is a (2, 3, 4)-shaped indexing intp array, then result = x[..., ind, :] has shape (10, 2, 3, 4, 30) because the (20,)-shaped subspace has been replaced with a (2, 3, 4)-shaped broadcasted indexing subspace. If we let i, j, k loop over the (2, 3, 4)-shaped subspace then result[..., i, j, k, :] = x[..., ind[i, j, k], :]. This example produces the same result as x.take(ind, axis=-2). - -Example - -Let x.shape be (10, 20, 30, 40, 50) and suppose ind_1 and ind_2 can be broadcast to the shape (2, 3, 4). Then x[:, ind_1, ind_2] has shape (10, 2, 3, 4, 40, 50) because the (20, 30)-shaped subspace from X has been replaced with the (2, 3, 4) subspace from the indices. However, x[:, ind_1, :, ind_2] has shape (2, 3, 4, 10, 30, 50) because there is no unambiguous place to drop in the indexing subspace, thus it is tacked-on to the beginning. It is always possible to use .transpose() to move the subspace anywhere desired. Note that this example cannot be replicated using take. - -Example - -Slicing can be combined with broadcasted boolean indices: - -x = np.arange(35).reshape(5, 7) - -b = x > 20 - -b -array([[False, False, False, False, False, False, False], - [False, False, False, False, False, False, False], - [False, False, False, False, False, False, False], - [ True, True, True, True, True, True, True], - [ True, True, True, True, True, True, True]]) - -x[b[:, 5], 1:3] -array([[22, 23], - [29, 30]]) - -Field access - -See also - -Structured arrays - -If the ndarray object is a structured array the fields of the array can be accessed by indexing the array with strings, dictionary-like. - -Indexing x['field-name'] returns a new view to the array, which is of the same shape as x (except when the field is a sub-array) but of data type x.dtype['field-name'] and contains only the part of the data in the specified field. Also, record array scalars can be “indexed” this way. - -Indexing into a structured array can also be done with a list of field names, e.g. x[['field-name1', 'field-name2']]. As of NumPy 1.16, this returns a view containing only those fields. In older versions of NumPy, it returned a copy. See the user guide section on Structured arrays for more information on multifield indexing. - -If the accessed field is a sub-array, the dimensions of the sub-array are appended to the shape of the result. For example: - -x = np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))]) - -x['a'].shape -(2, 2) - -x['a'].dtype -dtype('int32') - -x['b'].shape -(2, 2, 3, 3) - -x['b'].dtype -dtype('float64') - -Flat Iterator indexing - -x.flat returns an iterator that will iterate over the entire array (in C-contiguous style with the last index varying the fastest). This iterator object can also be indexed using basic slicing or advanced indexing as long as the selection object is not a tuple. This should be clear from the fact that x.flat is a 1-dimensional view. It can be used for integer indexing with 1-dimensional C-style-flat indices. The shape of any returned array is therefore the shape of the integer indexing object. -Assigning values to indexed arrays - -As mentioned, one can select a subset of an array to assign to using a single index, slices, and index and mask arrays. The value being assigned to the indexed array must be shape consistent (the same shape or broadcastable to the shape the index produces). For example, it is permitted to assign a constant to a slice: - -x = np.arange(10) - -x[2:7] = 1 - -or an array of the right size: - -x[2:7] = np.arange(5) - -Note that assignments may result in changes if assigning higher types to lower types (like floats to ints) or even exceptions (assigning complex to floats or ints): - -x[1] = 1.2 - -x[1] -1 - -x[1] = 1.2j -Traceback (most recent call last): - ... -TypeError: can't convert complex to int - -Unlike some of the references (such as array and mask indices) assignments are always made to the original data in the array (indeed, nothing else would make sense!). Note though, that some actions may not work as one may naively expect. This particular example is often surprising to people: - -x = np.arange(0, 50, 10) - -x -array([ 0, 10, 20, 30, 40]) - -x[np.array([1, 1, 3, 1])] += 1 - -x -array([ 0, 11, 20, 31, 40]) - -Where people expect that the 1st location will be incremented by 3. In fact, it will only be incremented by 1. The reason is that a new array is extracted from the original (as a temporary) containing the values at 1, 1, 3, 1, then the value 1 is added to the temporary, and then the temporary is assigned back to the original array. Thus the value of the array at x[1] + 1 is assigned to x[1] three times, rather than being incremented 3 times. -Dealing with variable numbers of indices within programs - -The indexing syntax is very powerful but limiting when dealing with a variable number of indices. For example, if you want to write a function that can handle arguments with various numbers of dimensions without having to write special case code for each number of possible dimensions, how can that be done? If one supplies to the index a tuple, the tuple will be interpreted as a list of indices. For example: - -z = np.arange(81).reshape(3, 3, 3, 3) - -indices = (1, 1, 1, 1) - -z[indices] -40 - -So one can use code to construct tuples of any number of indices and then use these within an index. - -Slices can be specified within programs by using the slice() function in Python. For example: - -indices = (1, 1, 1, slice(0, 2)) # same as [1, 1, 1, 0:2] - -z[indices] -array([39, 40]) - -Likewise, ellipsis can be specified by code by using the Ellipsis object: - -indices = (1, Ellipsis, 1) # same as [1, ..., 1] - -z[indices] -array([[28, 31, 34], - [37, 40, 43], - [46, 49, 52]]) - -For this reason, it is possible to use the output from the np.nonzero() function directly as an index since it always returns a tuple of index arrays. - -Because of the special treatment of tuples, they are not automatically converted to an array as a list would be. As an example: - -z[[1, 1, 1, 1]] # produces a large array -array([[[[27, 28, 29], - [30, 31, 32], ... - -z[(1, 1, 1, 1)] # returns a single value -40 - -Detailed notes - -These are some detailed notes, which are not of importance for day to day indexing (in no particular order): - - The native NumPy indexing type is intp and may differ from the default integer array type. intp is the smallest data type sufficient to safely index any array; for advanced indexing it may be faster than other types. - - For advanced assignments, there is in general no guarantee for the iteration order. This means that if an element is set more than once, it is not possible to predict the final result. - - An empty (tuple) index is a full scalar index into a zero-dimensional array. x[()] returns a scalar if x is zero-dimensional and a view otherwise. On the other hand, x[...] always returns a view. - - If a zero-dimensional array is present in the index and it is a full integer index the result will be a scalar and not a zero-dimensional array. (Advanced indexing is not triggered.) - - When an ellipsis (...) is present but has no size (i.e. replaces zero :) the result will still always be an array. A view if no advanced index is present, otherwise a copy. - - The nonzero equivalence for Boolean arrays does not hold for zero dimensional boolean arrays. - - When the result of an advanced indexing operation has no elements but an individual index is out of bounds, whether or not an IndexError is raised is undefined (e.g. x[[], [123]] with 123 being out of bounds). - - When a casting error occurs during assignment (for example updating a numerical array using a sequence of strings), the array being assigned to may end up in an unpredictable partially updated state. However, if any other error (such as an out of bounds index) occurs, the array will remain unchanged. - - The memory layout of an advanced indexing result is optimized for each indexing operation and no particular memory order can be assumed. - - When using a subclass (especially one which manipulates its shape), the default ndarray.__setitem__ behaviour will call __getitem__ for basic indexing but not for advanced indexing. For such a subclass it may be preferable to call ndarray.__setitem__ with a base class ndarray view on the data. This must be done if the subclasses __getitem__ does not return views. - -previous - -Array creation - -next - -I/O with NumPy - -© Copyright 2008-2022, NumPy Developers. - -Created using Sphinx 4.5.0. diff --git a/tests/integrated/test-boutpp/slicing/slicingexamples b/tests/integrated/test-boutpp/slicing/slicingexamples deleted file mode 100644 index 7edb2fa5bc..0000000000 --- a/tests/integrated/test-boutpp/slicing/slicingexamples +++ /dev/null @@ -1 +0,0 @@ -, diff --git a/tests/integrated/test-boutpp/slicing/test.py b/tests/integrated/test-boutpp/slicing/test.py deleted file mode 100644 index 2f36b362cb..0000000000 --- a/tests/integrated/test-boutpp/slicing/test.py +++ /dev/null @@ -1,4 +0,0 @@ -import boutcore as bc - -bc.init("-d test") -bc.print("We can print to the log from python 🎉") From 10d320f700ed1fc8da6d356e52aaacfa23a8c665 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 15 Feb 2024 22:04:49 +0100 Subject: [PATCH 032/256] Use parallel_neumann as BC --- tests/integrated/test-fci-mpi/fci_mpi.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-fci-mpi/fci_mpi.cxx b/tests/integrated/test-fci-mpi/fci_mpi.cxx index 6ae711351e..e19572c52c 100644 --- a/tests/integrated/test-fci-mpi/fci_mpi.cxx +++ b/tests/integrated/test-fci-mpi/fci_mpi.cxx @@ -20,7 +20,7 @@ int main(int argc, char** argv) { Options::getRoot(), mesh)}; // options->get(fmt::format("input_{:d}:boundary_perp", i), temp_str, s"free_o3"); mesh->communicate(input); - input.applyParallelBoundary("parallel_neumann_o2"); + input.applyParallelBoundary("parallel_neumann"); for (int slice = -mesh->ystart; slice <= mesh->ystart; ++slice) { if (slice) { Field3D tmp{0.}; From 4b05708bb5def102ff83920a38e4396542c36901 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 15 Feb 2024 22:05:04 +0100 Subject: [PATCH 033/256] fix usage of f-string --- tests/integrated/test-fci-mpi/runtest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-fci-mpi/runtest b/tests/integrated/test-fci-mpi/runtest index e12b330326..6676f8f7a5 100755 --- a/tests/integrated/test-fci-mpi/runtest +++ b/tests/integrated/test-fci-mpi/runtest @@ -40,7 +40,7 @@ for nslice in nslices: _, out = launch_safe(cmd, nproc=NXPE * NYPE, mthread=mthread, pipe=True) # Save output to log file - with open("run.log.{NXPE}.{NYPE}.{nslice}.log", "w") as f: + with open(f"run.log.{NXPE}.{NYPE}.{nslice}.log", "w") as f: f.write(out) collect_kw = dict(info=False, xguards=False, yguards=False, path="data") From 60224e4292254f2a7954550fc03002fde165d303 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Feb 2024 14:05:26 +0100 Subject: [PATCH 034/256] Use localmesh mesh may not be initialised or be a different mesh --- src/mesh/interpolation/hermite_spline_xz.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index f167a7576d..c0040d096e 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -140,8 +140,8 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* mesh) // PetscInt N, PetscInt d_nz, const PetscInt d_nnz[], // PetscInt o_nz, const PetscInt o_nnz[], Mat *A) // MatSetSizes(Mat A,PetscInt m,PetscInt n,PetscInt M,PetscInt N) - const int m = mesh->LocalNx * mesh->LocalNy * mesh->LocalNz; - const int M = m * mesh->getNXPE() * mesh->getNYPE(); + const int m = localmesh->LocalNx * localmesh->LocalNy * localmesh->LocalNz; + const int M = m * localmesh->getNXPE() * localmesh->getNYPE(); MatCreateAIJ(MPI_COMM_WORLD, m, m, M, M, 16, nullptr, 16, nullptr, &petscWeights); #endif #endif From 608bb5d15575236589d4956dd4c241a493067f15 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Feb 2024 14:52:48 +0100 Subject: [PATCH 035/256] add PETSc requirement for MPI test --- tests/integrated/test-fci-mpi/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrated/test-fci-mpi/CMakeLists.txt b/tests/integrated/test-fci-mpi/CMakeLists.txt index 6a1ec33ac6..0dd38487a3 100644 --- a/tests/integrated/test-fci-mpi/CMakeLists.txt +++ b/tests/integrated/test-fci-mpi/CMakeLists.txt @@ -5,4 +5,5 @@ bout_add_mms_test(test-fci-mpi PROCESSORS 6 DOWNLOAD https://zenodo.org/record/7614499/files/W7X-conf4-36x8x128.fci.nc?download=1 DOWNLOAD_NAME grid.fci.nc + REQUIRES BOUT_HAS_PETSC ) From 88741c5b26e1b318e1d4d167a595814b4eac041f Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Feb 2024 14:53:06 +0100 Subject: [PATCH 036/256] Update header location --- tests/integrated/test-fci-mpi/fci_mpi.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integrated/test-fci-mpi/fci_mpi.cxx b/tests/integrated/test-fci-mpi/fci_mpi.cxx index e19572c52c..d102a7c8e3 100644 --- a/tests/integrated/test-fci-mpi/fci_mpi.cxx +++ b/tests/integrated/test-fci-mpi/fci_mpi.cxx @@ -1,6 +1,6 @@ -#include "bout.hxx" -#include "derivs.hxx" -#include "field_factory.hxx" +#include "bout/bout.hxx" +#include "bout/derivs.hxx" +#include "bout/field_factory.hxx" int main(int argc, char** argv) { BoutInitialise(argc, argv); From 09f609b96a42e0e3df3a701472236d2c516c4b68 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Feb 2024 17:29:18 +0100 Subject: [PATCH 037/256] More const correctness --- tests/integrated/test-fci-mpi/fci_mpi.cxx | 4 ++-- .../integrated/test-interpolate/test_interpolate.cxx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/integrated/test-fci-mpi/fci_mpi.cxx b/tests/integrated/test-fci-mpi/fci_mpi.cxx index d102a7c8e3..f4c26adc96 100644 --- a/tests/integrated/test-fci-mpi/fci_mpi.cxx +++ b/tests/integrated/test-fci-mpi/fci_mpi.cxx @@ -8,7 +8,7 @@ int main(int argc, char** argv) { using bout::globals::mesh; Options* options = Options::getRoot(); int i = 0; - std::string default_str{"not_set"}; + const std::string default_str{"not_set"}; Options dump; while (true) { std::string temp_str; @@ -22,7 +22,7 @@ int main(int argc, char** argv) { mesh->communicate(input); input.applyParallelBoundary("parallel_neumann"); for (int slice = -mesh->ystart; slice <= mesh->ystart; ++slice) { - if (slice) { + if (slice != 0) { Field3D tmp{0.}; BOUT_FOR(i, tmp.getRegion("RGN_NOBNDRY")) { tmp[i] = input.ynext(slice)[i.yp(slice)]; diff --git a/tests/integrated/test-interpolate/test_interpolate.cxx b/tests/integrated/test-interpolate/test_interpolate.cxx index 7b0ced5a21..33963dbb9e 100644 --- a/tests/integrated/test-interpolate/test_interpolate.cxx +++ b/tests/integrated/test-interpolate/test_interpolate.cxx @@ -32,30 +32,30 @@ int main(int argc, char** argv) { BoutInitialise(argc, argv); { // Random number generator - std::default_random_engine generator; + const std::default_random_engine generator; // Uniform distribution of BoutReals from 0 to 1 - std::uniform_real_distribution distribution{0.0, 1.0}; + const std::uniform_real_distribution distribution{0.0, 1.0}; using bout::globals::mesh; - FieldFactory f(mesh); + const FieldFactory fieldfact(mesh); // Set up generators and solutions for three different analtyic functions std::string a_func; auto a_gen = getGeneratorFromOptions("a", a_func); - Field3D a = f.create3D(a_func); + const Field3D a = fieldfact.create3D(a_func); Field3D a_solution = 0.0; Field3D a_interp = 0.0; std::string b_func; auto b_gen = getGeneratorFromOptions("b", b_func); - Field3D b = f.create3D(b_func); + const Field3D b = fieldfact.create3D(b_func); Field3D b_solution = 0.0; Field3D b_interp = 0.0; std::string c_func; auto c_gen = getGeneratorFromOptions("c", c_func); - Field3D c = f.create3D(c_func); + const Field3D c = fieldfact.create3D(c_func); Field3D c_solution = 0.0; Field3D c_interp = 0.0; From 1fb921e54c29c711954fa8fa20f572e7cff7e42b Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 2 Feb 2023 12:42:27 +0100 Subject: [PATCH 038/256] Add some asserts for non parallelised XZ interpolation --- src/mesh/interpolation/bilinear_xz.cxx | 4 ++++ src/mesh/interpolation/hermite_spline_xz.cxx | 4 ++-- src/mesh/interpolation/lagrange_4pt_xz.cxx | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/mesh/interpolation/bilinear_xz.cxx b/src/mesh/interpolation/bilinear_xz.cxx index 8445764a8f..4facdac34c 100644 --- a/src/mesh/interpolation/bilinear_xz.cxx +++ b/src/mesh/interpolation/bilinear_xz.cxx @@ -31,6 +31,10 @@ XZBilinear::XZBilinear(int y_offset, Mesh* mesh) : XZInterpolation(y_offset, mesh), w0(localmesh), w1(localmesh), w2(localmesh), w3(localmesh) { + if (localmesh->getNXPE() > 1) { + throw BoutException("Do not support MPI splitting in X"); + } + // Index arrays contain guard cells in order to get subscripts right i_corner.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); k_corner.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index c0040d096e..165d387d66 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -101,8 +101,8 @@ class IndConverter { } }; -XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* mesh) - : XZInterpolation(y_offset, mesh), h00_x(localmesh), h01_x(localmesh), +XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* meshin) + : XZInterpolation(y_offset, meshin), h00_x(localmesh), h01_x(localmesh), h10_x(localmesh), h11_x(localmesh), h00_z(localmesh), h01_z(localmesh), h10_z(localmesh), h11_z(localmesh) { diff --git a/src/mesh/interpolation/lagrange_4pt_xz.cxx b/src/mesh/interpolation/lagrange_4pt_xz.cxx index 92c14ecfd5..8fa201ba72 100644 --- a/src/mesh/interpolation/lagrange_4pt_xz.cxx +++ b/src/mesh/interpolation/lagrange_4pt_xz.cxx @@ -29,6 +29,10 @@ XZLagrange4pt::XZLagrange4pt(int y_offset, Mesh* mesh) : XZInterpolation(y_offset, mesh), t_x(localmesh), t_z(localmesh) { + if (localmesh->getNXPE() > 1) { + throw BoutException("Do not support MPI splitting in X"); + } + // Index arrays contain guard cells in order to get subscripts right i_corner.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); k_corner.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); From 4a79fb49ee0d9a78fa91bf96e25b13396600e9e6 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 23 Feb 2023 13:52:03 +0100 Subject: [PATCH 039/256] enable openmp for sundials if it is enabled for BOUT++ --- cmake/SetupBOUTThirdParty.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/SetupBOUTThirdParty.cmake b/cmake/SetupBOUTThirdParty.cmake index 53adbec92d..f783487556 100644 --- a/cmake/SetupBOUTThirdParty.cmake +++ b/cmake/SetupBOUTThirdParty.cmake @@ -288,7 +288,7 @@ if (BOUT_USE_SUNDIALS) set(EXAMPLES_ENABLE_C OFF CACHE BOOL "" FORCE) set(EXAMPLES_INSTALL OFF CACHE BOOL "" FORCE) set(ENABLE_MPI ${BOUT_USE_MPI} CACHE BOOL "" FORCE) - set(ENABLE_OPENMP OFF CACHE BOOL "" FORCE) + set(ENABLE_OPENMP ${BOUT_USE_OPENMP} CACHE BOOL "" FORCE) if (BUILD_SHARED_LIBS) set(BUILD_STATIC_LIBS OFF CACHE BOOL "" FORCE) else() From b5bd5f0258856860ba6a5c9f4cfe8fdf59eb9d52 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Nov 2023 14:02:42 +0100 Subject: [PATCH 040/256] Add required interfaces --- include/bout/coordinates.hxx | 4 ++++ include/bout/field3d.hxx | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/include/bout/coordinates.hxx b/include/bout/coordinates.hxx index 49feffa0a7..c0a13aafab 100644 --- a/include/bout/coordinates.hxx +++ b/include/bout/coordinates.hxx @@ -133,6 +133,10 @@ public: transform = std::move(pt); } + bool hasParallelTransform() const{ + return transform != nullptr; + } + /// Return the parallel transform ParallelTransform& getParallelTransform() { ASSERT1(transform != nullptr); diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index ba8c8e879e..964e3f096c 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -261,6 +261,13 @@ public: #endif } + /// get number of parallel slices + size_t numberParallelSlices() const { + // Do checks + hasParallelSlices(); + return yup_fields.size(); + } + /// Check if this field has yup and ydown fields /// Return reference to yup field Field3D& yup(std::vector::size_type index = 0) { From 681971830276a899c433597c40a12c084cb7438d Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 19 Mar 2024 15:41:00 +0100 Subject: [PATCH 041/256] Add maskFromRegion --- include/bout/mask.hxx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/bout/mask.hxx b/include/bout/mask.hxx index 4250d21105..624f3d7513 100644 --- a/include/bout/mask.hxx +++ b/include/bout/mask.hxx @@ -67,6 +67,7 @@ public: inline bool& operator()(int jx, int jy, int jz) { return mask(jx, jy, jz); } inline const bool& operator()(int jx, int jy, int jz) const { return mask(jx, jy, jz); } inline const bool& operator[](const Ind3D& i) const { return mask[i]; } + inline bool& operator[](const Ind3D& i) { return mask[i]; } }; inline std::unique_ptr> regionFromMask(const BoutMask& mask, @@ -79,4 +80,13 @@ inline std::unique_ptr> regionFromMask(const BoutMask& mask, } return std::make_unique>(indices); } + +inline BoutMask maskFromRegion(const Region& region, const Mesh* mesh) { + BoutMask mask{mesh, false}; + //(int nx, int ny, int nz, bool value=false) : + + BOUT_FOR(i, region) { mask[i] = true; } + return mask; +} + #endif //BOUT_MASK_H From fee27c95cbf1ac53eb2e7612293e90716548f761 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Nov 2023 11:51:03 +0100 Subject: [PATCH 042/256] Dump field before and after rhs evaluation for debugging --- src/solver/impls/pvode/pvode.cxx | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index b2cfd233a9..8a51a9cb19 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -293,6 +293,49 @@ BoutReal PvodeSolver::run(BoutReal tout) { // Check return flag if (flag != SUCCESS) { output_error.write("ERROR CVODE step failed, flag = {:d}\n", flag); + CVodeMemRec* cv_mem = (CVodeMem)cvode_mem; + if (f2d.empty() and v2d.empty() and v3d.empty()) { + Options debug{}; + using namespace std::string_literals; + Mesh* mesh{}; + for (const auto& prefix : {"pre_"s, "residuum_"s}) { + std::vector ffs{}; + std::vector evolve_bndrys{}; + for (const auto& f : f3d) { + Field3D ff{0.}; + ff.allocate(); + ff.setLocation(f.location); + mesh = ff.getMesh(); + debug[fmt::format("{:s}{:s}", prefix, f.name)] = ff; + ffs.push_back(ff); + evolve_bndrys.push_back(f.evolve_bndry); + } + pvode_load_data_f3d(evolve_bndrys, ffs, + prefix == "pre_"s ? udata : N_VDATA(cv_mem->cv_acor)); + } + + for (auto& f : f3d) { + f.F_var->enableTracking(fmt::format("ddt_{:s}", f.name), debug); + setName(f.var, f.name); + } + run_rhs(simtime); + + for (auto& f : f3d) { + debug[f.name] = *f.var; + } + + if (mesh) { + mesh->outputVars(debug); + debug["BOUT_VERSION"].force(bout::version::as_double); + } + + std::string outname = fmt::format( + "{}/BOUT.debug.{}.nc", + Options::root()["datadir"].withDefault("data"), BoutComm::rank()); + + bout::OptionsNetCDF(outname).write(debug); + MPI_Barrier(BoutComm::get()); + } return (-1.0); } From 96da2e9f88e313f4dd388b2c83d701046aedf96c Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Nov 2023 11:49:32 +0100 Subject: [PATCH 043/256] Add setName function --- include/bout/field.hxx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index c0693ec0fb..0867560c3b 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -683,4 +683,12 @@ inline T floor(const T& var, BoutReal f, const std::string& rgn = "RGN_ALL") { #undef FIELD_FUNC +template , class... Types> +inline T setName(T&& f, const std::string& name, Types... args) { +#if BOUT_USE_TRACK + f.name = fmt::format(name, args...); +#endif + return f; +} + #endif /* FIELD_H */ From e56981ceeb0409d7d48d81e1225e7332c0346519 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Jun 2023 09:33:00 +0200 Subject: [PATCH 044/256] Set div_par and grad_par names --- src/mesh/coordinates.cxx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 01f0fe46ca..3948c75b94 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1542,7 +1542,11 @@ Field3D Coordinates::Grad_par(const Field3D& var, CELL_LOC outloc, TRACE("Coordinates::Grad_par( Field3D )"); ASSERT1(location == outloc || outloc == CELL_DEFAULT); - return ::DDY(var, outloc, method) * invSg(); + if (invSg == nullptr) { + invSg = std::make_unique(); + (*invSg) = 1.0 / sqrt(g_22); + } + return setName(::DDY(var, outloc, method) * invSg(), "Grad_par({:s})", var.name); } ///////////////////////////////////////////////////////// @@ -1601,7 +1605,7 @@ Field3D Coordinates::Div_par(const Field3D& f, CELL_LOC outloc, f_B.yup(i) = f.yup(i) / Bxy_floc.yup(i); f_B.ydown(i) = f.ydown(i) / Bxy_floc.ydown(i); } - return Bxy * Grad_par(f_B, outloc, method); + return setName(Bxy * Grad_par(f_B, outloc, method), "Div_par({:s})", f.name); } ///////////////////////////////////////////////////////// From 6b2c132e3db61fa4f453e43a6988e8534f6c0a43 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Jun 2023 09:32:25 +0200 Subject: [PATCH 045/256] Dump debug file if PVODE fails Use the new track feature (better name required) to dump the different components of the ddt() as well as the residuum for the evolved fields. --- src/solver/impls/pvode/pvode.cxx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index 8a51a9cb19..7283b7d0eb 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -42,12 +42,39 @@ #include // contains the enum for types of preconditioning #include // band preconditioner function prototypes +#include + using namespace pvode; void solver_f(integer N, BoutReal t, N_Vector u, N_Vector udot, void* f_data); void solver_gloc(integer N, BoutReal t, BoutReal* u, BoutReal* udot, void* f_data); void solver_cfn(integer N, BoutReal t, N_Vector u, void* f_data); +namespace { +// local only +void pvode_load_data_f3d(const std::vector& evolve_bndrys, + std::vector& ffs, BoutReal* udata) { + int p = 0; + Mesh* mesh = ffs[0].getMesh(); + const int nz = mesh->LocalNz; + for (const auto& bndry : {true, false}) { + for (const auto& i2d : mesh->getRegion2D(bndry ? "RGN_BNDRY" : "RGN_NOBNDRY")) { + for (int jz = 0; jz < nz; jz++) { + // Loop over 3D variables + std::vector::const_iterator evolve_bndry = evolve_bndrys.begin(); + for (std::vector::iterator ff = ffs.begin(); ff != ffs.end(); ++ff) { + if (bndry && !*evolve_bndry) + continue; + (*ff)[mesh->ind2Dto3D(i2d, jz)] = udata[p]; + p++; + } + ++evolve_bndry; + } + } + } +} +} // namespace + const BoutReal ZERO = 0.0; long int iopt[OPT_SIZE]; From df2d66189f4d2e87cc024521ad287936b9c0ba85 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Jun 2023 09:27:19 +0200 Subject: [PATCH 046/256] Add tracking to Field3D This keeps track of all the changes done to the field and stores them to a OptionsObject. --- include/bout/field3d.hxx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index ba8c8e879e..8992575be6 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -514,6 +514,13 @@ private: /// RegionID over which the field is valid std::optional regionID; + + int tracking_state{0}; + Options* tracking{nullptr}; + std::string selfname{""}; + template + Options* track(const T& change, std::string op); + Options* track(const BoutReal& change, std::string op); }; // Non-member overloaded operators From 263f9fedaad854832bfdbf6a5ac9322eb492c71e Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 19 Jun 2023 09:27:19 +0200 Subject: [PATCH 047/256] Add tracking to Field3D This keeps track of all the changes done to the field and stores them to a OptionsObject. --- include/bout/field3d.hxx | 4 + src/field/field3d.cxx | 44 ++++ src/field/gen_fieldops.jinja | 14 ++ src/field/generated_fieldops.cxx | 348 +++++++++++++++++++++++++++++++ 4 files changed, 410 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 8992575be6..bf7a9cc180 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -295,6 +295,10 @@ public: /// cuts on closed field lines? bool requiresTwistShift(bool twist_shift_enabled); + /// Enable a special tracking mode for debugging + /// Save all changes that, are done to the field, to tracking + Field3D& enableTracking(const std::string& name, Options& tracking); + ///////////////////////////////////////////////////////// // Data access diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 011353f34a..f0f088b656 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -243,6 +243,7 @@ Field3D& Field3D::operator=(const Field3D& rhs) { } TRACE("Field3D: Assignment from Field3D"); + track(rhs, "operator="); // Copy base slice Field::operator=(rhs); @@ -263,6 +264,7 @@ Field3D& Field3D::operator=(const Field3D& rhs) { Field3D& Field3D::operator=(Field3D&& rhs) { TRACE("Field3D: Assignment from Field3D"); + track(rhs, "operator="); // Move parallel slices or delete existing ones. yup_fields = std::move(rhs.yup_fields); @@ -283,6 +285,7 @@ Field3D& Field3D::operator=(Field3D&& rhs) { Field3D& Field3D::operator=(const Field2D& rhs) { TRACE("Field3D = Field2D"); + track(rhs, "operator="); /// Check that the data is allocated ASSERT1(rhs.isAllocated()); @@ -327,6 +330,7 @@ void Field3D::operator=(const FieldPerp& rhs) { Field3D& Field3D::operator=(const BoutReal val) { TRACE("Field3D = BoutReal"); + track(val, "operator="); // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. @@ -831,3 +835,43 @@ Field3D::getValidRegionWithDefault(const std::string& region_name) const { void Field3D::setRegion(const std::string& region_name) { regionID = fieldmesh->getRegionID(region_name); } + +Field3D& Field3D::enableTracking(const std::string& name, Options& _tracking) { + tracking = &_tracking; + tracking_state = 1; + selfname = name; + return *this; +} + +template +Options* Field3D::track(const T& change, std::string op) { + if (tracking and tracking_state) { + const std::string outname{fmt::format("track_{:s}_{:d}", selfname, tracking_state++)}; + tracking->set(outname, change, "tracking"); + (*tracking)[outname].setAttributes({ + {"operation", op}, +#if BOUT_USE_TRACK + {"rhs.name", change.name}, +#endif + }); + return &(*tracking)[outname]; + } + return nullptr; +} + +template Options* Field3D::track(const Field3D&, std::string); +template Options* Field3D::track(const Field2D&, std::string); +template Options* Field3D::track(const FieldPerp&, std::string); + +Options* Field3D::track(const BoutReal& change, std::string op) { + if (tracking and tracking_state) { + const std::string outname{fmt::format("track_{:s}_{:d}", selfname, tracking_state++)}; + tracking->set(outname, change, "tracking"); + (*tracking)[outname].setAttributes({ + {"operation", op}, + {"rhs.name", "BR"}, + }); + return &(*tracking)[outname]; + } + return nullptr; +} diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index ecd4e628cc..58b1ae28ba 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -61,6 +61,10 @@ } {% endif %} +#if BOUT_USE_TRACK + {{out.name}}.name = fmt::format("{:s} {{operator}} {:s}", {{'"BR"' if lhs == "BoutReal" else lhs.name + ".name"}} + , {{'"BR"' if rhs == "BoutReal" else rhs.name + ".name"}}); +#endif checkData({{out.name}}); return {{out.name}}; } @@ -129,9 +133,19 @@ } {% endif %} + {% if lhs == "Field3D" %} + track(rhs, "operator{{operator}}="); + {% endif %} +#if BOUT_USE_TRACK + name = fmt::format("{:s} {{operator}}= {:s}", this->name, {{'"BR"' if rhs == "BoutReal" else rhs.name + ".name"}}); +#endif + checkData(*this); } else { + {% if lhs == "Field3D" %} + track(rhs, "operator{{operator}}="); + {% endif %} (*this) = (*this) {{operator}} {{rhs.name}}; } return *this; diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 6b778acee3..3495d87dbc 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -20,6 +20,9 @@ Field3D operator*(const Field3D& lhs, const Field3D& rhs) { result[index] = lhs[index] * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -42,9 +45,15 @@ Field3D& Field3D::operator*=(const Field3D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } + track(rhs, "operator*="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator*="); (*this) = (*this) * rhs; } return *this; @@ -64,6 +73,9 @@ Field3D operator/(const Field3D& lhs, const Field3D& rhs) { result[index] = lhs[index] / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -86,9 +98,15 @@ Field3D& Field3D::operator/=(const Field3D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } + track(rhs, "operator/="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator/="); (*this) = (*this) / rhs; } return *this; @@ -108,6 +126,9 @@ Field3D operator+(const Field3D& lhs, const Field3D& rhs) { result[index] = lhs[index] + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -130,9 +151,15 @@ Field3D& Field3D::operator+=(const Field3D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } + track(rhs, "operator+="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator+="); (*this) = (*this) + rhs; } return *this; @@ -152,6 +179,9 @@ Field3D operator-(const Field3D& lhs, const Field3D& rhs) { result[index] = lhs[index] - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -174,9 +204,15 @@ Field3D& Field3D::operator-=(const Field3D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } + track(rhs, "operator-="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator-="); (*this) = (*this) - rhs; } return *this; @@ -201,6 +237,9 @@ Field3D operator*(const Field3D& lhs, const Field2D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -226,9 +265,15 @@ Field3D& Field3D::operator*=(const Field2D& rhs) { } } + track(rhs, "operator*="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator*="); (*this) = (*this) * rhs; } return *this; @@ -254,6 +299,9 @@ Field3D operator/(const Field3D& lhs, const Field2D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -280,9 +328,15 @@ Field3D& Field3D::operator/=(const Field2D& rhs) { } } + track(rhs, "operator/="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator/="); (*this) = (*this) / rhs; } return *this; @@ -307,6 +361,9 @@ Field3D operator+(const Field3D& lhs, const Field2D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -332,9 +389,15 @@ Field3D& Field3D::operator+=(const Field2D& rhs) { } } + track(rhs, "operator+="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator+="); (*this) = (*this) + rhs; } return *this; @@ -359,6 +422,9 @@ Field3D operator-(const Field3D& lhs, const Field2D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -384,9 +450,15 @@ Field3D& Field3D::operator-=(const Field2D& rhs) { } } + track(rhs, "operator-="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { + track(rhs, "operator-="); (*this) = (*this) - rhs; } return *this; @@ -408,6 +480,9 @@ FieldPerp operator*(const Field3D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -428,6 +503,9 @@ FieldPerp operator/(const Field3D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -448,6 +526,9 @@ FieldPerp operator+(const Field3D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -468,6 +549,9 @@ FieldPerp operator-(const Field3D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -485,6 +569,9 @@ Field3D operator*(const Field3D& lhs, const BoutReal rhs) { result[index] = lhs[index] * rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -504,9 +591,15 @@ Field3D& Field3D::operator*=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs; } + track(rhs, "operator*="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { + track(rhs, "operator*="); (*this) = (*this) * rhs; } return *this; @@ -526,6 +619,9 @@ Field3D operator/(const Field3D& lhs, const BoutReal rhs) { result[index] = lhs[index] * tmp; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -546,9 +642,15 @@ Field3D& Field3D::operator/=(const BoutReal rhs) { const auto tmp = 1.0 / rhs; BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= tmp; } + track(rhs, "operator/="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { + track(rhs, "operator/="); (*this) = (*this) / rhs; } return *this; @@ -567,6 +669,9 @@ Field3D operator+(const Field3D& lhs, const BoutReal rhs) { result[index] = lhs[index] + rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -586,9 +691,15 @@ Field3D& Field3D::operator+=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs; } + track(rhs, "operator+="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, "BR"); +#endif + checkData(*this); } else { + track(rhs, "operator+="); (*this) = (*this) + rhs; } return *this; @@ -607,6 +718,9 @@ Field3D operator-(const Field3D& lhs, const BoutReal rhs) { result[index] = lhs[index] - rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -626,9 +740,15 @@ Field3D& Field3D::operator-=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs; } + track(rhs, "operator-="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { + track(rhs, "operator-="); (*this) = (*this) - rhs; } return *this; @@ -653,6 +773,9 @@ Field3D operator*(const Field2D& lhs, const Field3D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -676,6 +799,9 @@ Field3D operator/(const Field2D& lhs, const Field3D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -699,6 +825,9 @@ Field3D operator+(const Field2D& lhs, const Field3D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -722,6 +851,9 @@ Field3D operator-(const Field2D& lhs, const Field3D& rhs) { } } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -738,6 +870,9 @@ Field2D operator*(const Field2D& lhs, const Field2D& rhs) { result[index] = lhs[index] * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -754,6 +889,10 @@ Field2D& Field2D::operator*=(const Field2D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -774,6 +913,9 @@ Field2D operator/(const Field2D& lhs, const Field2D& rhs) { result[index] = lhs[index] / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -790,6 +932,10 @@ Field2D& Field2D::operator/=(const Field2D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -810,6 +956,9 @@ Field2D operator+(const Field2D& lhs, const Field2D& rhs) { result[index] = lhs[index] + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -826,6 +975,10 @@ Field2D& Field2D::operator+=(const Field2D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -846,6 +999,9 @@ Field2D operator-(const Field2D& lhs, const Field2D& rhs) { result[index] = lhs[index] - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -862,6 +1018,10 @@ Field2D& Field2D::operator-=(const Field2D& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -886,6 +1046,9 @@ FieldPerp operator*(const Field2D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -906,6 +1069,9 @@ FieldPerp operator/(const Field2D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -926,6 +1092,9 @@ FieldPerp operator+(const Field2D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -946,6 +1115,9 @@ FieldPerp operator-(const Field2D& lhs, const FieldPerp& rhs) { result[index] = lhs[base_ind] - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -961,6 +1133,9 @@ Field2D operator*(const Field2D& lhs, const BoutReal rhs) { result[index] = lhs[index] * rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -976,6 +1151,10 @@ Field2D& Field2D::operator*=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -996,6 +1175,9 @@ Field2D operator/(const Field2D& lhs, const BoutReal rhs) { result[index] = lhs[index] * tmp; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -1012,6 +1194,10 @@ Field2D& Field2D::operator/=(const BoutReal rhs) { const auto tmp = 1.0 / rhs; BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= tmp; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -1031,6 +1217,9 @@ Field2D operator+(const Field2D& lhs, const BoutReal rhs) { result[index] = lhs[index] + rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -1046,6 +1235,10 @@ Field2D& Field2D::operator+=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -1065,6 +1258,9 @@ Field2D operator-(const Field2D& lhs, const BoutReal rhs) { result[index] = lhs[index] - rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -1080,6 +1276,10 @@ Field2D& Field2D::operator-=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -1104,6 +1304,9 @@ FieldPerp operator*(const FieldPerp& lhs, const Field3D& rhs) { result[index] = lhs[index] * rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1126,6 +1329,10 @@ FieldPerp& FieldPerp::operator*=(const Field3D& rhs) { (*this)[index] *= rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1150,6 +1357,9 @@ FieldPerp operator/(const FieldPerp& lhs, const Field3D& rhs) { result[index] = lhs[index] / rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1172,6 +1382,10 @@ FieldPerp& FieldPerp::operator/=(const Field3D& rhs) { (*this)[index] /= rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1196,6 +1410,9 @@ FieldPerp operator+(const FieldPerp& lhs, const Field3D& rhs) { result[index] = lhs[index] + rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1218,6 +1435,10 @@ FieldPerp& FieldPerp::operator+=(const Field3D& rhs) { (*this)[index] += rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1242,6 +1463,9 @@ FieldPerp operator-(const FieldPerp& lhs, const Field3D& rhs) { result[index] = lhs[index] - rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1264,6 +1488,10 @@ FieldPerp& FieldPerp::operator-=(const Field3D& rhs) { (*this)[index] -= rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1288,6 +1516,9 @@ FieldPerp operator*(const FieldPerp& lhs, const Field2D& rhs) { result[index] = lhs[index] * rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1310,6 +1541,10 @@ FieldPerp& FieldPerp::operator*=(const Field2D& rhs) { (*this)[index] *= rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1334,6 +1569,9 @@ FieldPerp operator/(const FieldPerp& lhs, const Field2D& rhs) { result[index] = lhs[index] / rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1356,6 +1594,10 @@ FieldPerp& FieldPerp::operator/=(const Field2D& rhs) { (*this)[index] /= rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1380,6 +1622,9 @@ FieldPerp operator+(const FieldPerp& lhs, const Field2D& rhs) { result[index] = lhs[index] + rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1402,6 +1647,10 @@ FieldPerp& FieldPerp::operator+=(const Field2D& rhs) { (*this)[index] += rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1426,6 +1675,9 @@ FieldPerp operator-(const FieldPerp& lhs, const Field2D& rhs) { result[index] = lhs[index] - rhs[base_ind]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1448,6 +1700,10 @@ FieldPerp& FieldPerp::operator-=(const Field2D& rhs) { (*this)[index] -= rhs[base_ind]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1468,6 +1724,9 @@ FieldPerp operator*(const FieldPerp& lhs, const FieldPerp& rhs) { result[index] = lhs[index] * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1484,6 +1743,10 @@ FieldPerp& FieldPerp::operator*=(const FieldPerp& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1504,6 +1767,9 @@ FieldPerp operator/(const FieldPerp& lhs, const FieldPerp& rhs) { result[index] = lhs[index] / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1520,6 +1786,10 @@ FieldPerp& FieldPerp::operator/=(const FieldPerp& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1540,6 +1810,9 @@ FieldPerp operator+(const FieldPerp& lhs, const FieldPerp& rhs) { result[index] = lhs[index] + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1556,6 +1829,10 @@ FieldPerp& FieldPerp::operator+=(const FieldPerp& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1576,6 +1853,9 @@ FieldPerp operator-(const FieldPerp& lhs, const FieldPerp& rhs) { result[index] = lhs[index] - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, rhs.name); +#endif checkData(result); return result; } @@ -1592,6 +1872,10 @@ FieldPerp& FieldPerp::operator-=(const FieldPerp& rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + checkData(*this); } else { @@ -1611,6 +1895,9 @@ FieldPerp operator*(const FieldPerp& lhs, const BoutReal rhs) { result[index] = lhs[index] * rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -1626,6 +1913,10 @@ FieldPerp& FieldPerp::operator*=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -1646,6 +1937,9 @@ FieldPerp operator/(const FieldPerp& lhs, const BoutReal rhs) { result[index] = lhs[index] * tmp; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -1661,6 +1955,10 @@ FieldPerp& FieldPerp::operator/=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -1680,6 +1978,9 @@ FieldPerp operator+(const FieldPerp& lhs, const BoutReal rhs) { result[index] = lhs[index] + rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -1695,6 +1996,10 @@ FieldPerp& FieldPerp::operator+=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -1714,6 +2019,9 @@ FieldPerp operator-(const FieldPerp& lhs, const BoutReal rhs) { result[index] = lhs[index] - rhs; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, "BR"); +#endif checkData(result); return result; } @@ -1729,6 +2037,10 @@ FieldPerp& FieldPerp::operator-=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs; } +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, "BR"); +#endif + checkData(*this); } else { @@ -1750,6 +2062,9 @@ Field3D operator*(const BoutReal lhs, const Field3D& rhs) { result[index] = lhs * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1767,6 +2082,9 @@ Field3D operator/(const BoutReal lhs, const Field3D& rhs) { result[index] = lhs / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1784,6 +2102,9 @@ Field3D operator+(const BoutReal lhs, const Field3D& rhs) { result[index] = lhs + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1801,6 +2122,9 @@ Field3D operator-(const BoutReal lhs, const Field3D& rhs) { result[index] = lhs - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1816,6 +2140,9 @@ Field2D operator*(const BoutReal lhs, const Field2D& rhs) { result[index] = lhs * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1831,6 +2158,9 @@ Field2D operator/(const BoutReal lhs, const Field2D& rhs) { result[index] = lhs / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1846,6 +2176,9 @@ Field2D operator+(const BoutReal lhs, const Field2D& rhs) { result[index] = lhs + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1861,6 +2194,9 @@ Field2D operator-(const BoutReal lhs, const Field2D& rhs) { result[index] = lhs - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1876,6 +2212,9 @@ FieldPerp operator*(const BoutReal lhs, const FieldPerp& rhs) { result[index] = lhs * rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1891,6 +2230,9 @@ FieldPerp operator/(const BoutReal lhs, const FieldPerp& rhs) { result[index] = lhs / rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1906,6 +2248,9 @@ FieldPerp operator+(const BoutReal lhs, const FieldPerp& rhs) { result[index] = lhs + rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", "BR", rhs.name); +#endif checkData(result); return result; } @@ -1921,6 +2266,9 @@ FieldPerp operator-(const BoutReal lhs, const FieldPerp& rhs) { result[index] = lhs - rhs[index]; } +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", "BR", rhs.name); +#endif checkData(result); return result; } From bac4ca92a31b60c40e87dd47b2ef764f571a58be Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 25 Apr 2023 09:16:38 +0200 Subject: [PATCH 048/256] cvode: Add option to use Adams Moulton solver instead of BDF --- src/solver/impls/pvode/pvode.cxx | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index 7283b7d0eb..ae5cd783a8 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -214,7 +214,34 @@ int PvodeSolver::init() { } iopt[MXSTEP] = pvode_mxstep; - cvode_mem = CVodeMalloc(neq, solver_f, simtime, u, BDF, NEWTON, SS, &reltol, &abstol, + { + /* ropt[H0] : initial step size. Optional input. */ + + /* ropt[HMAX] : maximum absolute value of step size allowed. * + * Optional input. (Default is infinity). */ + const BoutReal hmax( + (*options)["max_timestep"].doc("Maximum internal timestep").withDefault(-1.)); + if (hmax > 0) { + ropt[HMAX] = hmax; + } + /* ropt[HMIN] : minimum absolute value of step size allowed. * + * Optional input. (Default is 0.0). */ + const BoutReal hmin( + (*options)["min_timestep"].doc("Minimum internal timestep").withDefault(-1.)); + if (hmin > 0) { + ropt[HMIN] = hmin; + } + /* iopt[MAXORD] : maximum lmm order to be used by the solver. * + * Optional input. (Default = 12 for ADAMS, 5 for * + * BDF). */ + const int maxOrder((*options)["max_order"].doc("Maximum order").withDefault(-1)); + if (maxOrder > 0) { + iopt[MAXORD] = maxOrder; + } + } + const bool use_adam((*options)["adams_moulton"].doc("Use Adams Moulton solver instead of BDF").withDefault(false)); + + cvode_mem = CVodeMalloc(neq, solver_f, simtime, u, use_adam ? ADAMS : BDF, NEWTON, SS, &reltol, &abstol, this, nullptr, optIn, iopt, ropt, machEnv); if (cvode_mem == nullptr) { From 708bdcb2ff0a5c346edb43f7e609949b7c7afbd9 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 29 Mar 2023 12:59:22 +0200 Subject: [PATCH 049/256] Expose more pvode option to user --- src/solver/impls/pvode/pvode.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index ae5cd783a8..ac6e981b50 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -212,6 +212,9 @@ int PvodeSolver::init() { for (i = 0; i < OPT_SIZE; i++) { ropt[i] = ZERO; } + /* iopt[MXSTEP] : maximum number of internal steps to be taken by * + * the solver in its attempt to reach tout. * + * Optional input. (Default = 500). */ iopt[MXSTEP] = pvode_mxstep; { From fa357c118dd2db162ce8288046c871f835b9b37f Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 19 Mar 2024 15:43:50 +0100 Subject: [PATCH 050/256] Add isFci to check if a field is a FCI field. --- include/bout/field.hxx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index c0693ec0fb..c0ce04dbed 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -178,6 +178,11 @@ inline bool areFieldsCompatible(const Field& field1, const Field& field2) { #define ASSERT1_FIELDS_COMPATIBLE(field1, field2) ; #endif +template +inline bool isFci(const F& f) { + return not f.getCoordinates()->getParallelTransform().canToFromFieldAligned(); +} + /// Return an empty shell field of some type derived from Field, with metadata /// copied and a data array that is allocated but not initialised. template From ca59edb366be164e3583dc6041ea26d28c569ed8 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Nov 2023 14:04:13 +0100 Subject: [PATCH 051/256] Improve isFci Ensure this does not crash if coordinates or transform is not set. In this case no FCI transformation is set, and this returns false. --- include/bout/field.hxx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index c0ce04dbed..4433af6d19 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -180,7 +180,14 @@ inline bool areFieldsCompatible(const Field& field1, const Field& field2) { template inline bool isFci(const F& f) { - return not f.getCoordinates()->getParallelTransform().canToFromFieldAligned(); + const auto coords = f.getCoordinates(); + if (coords == nullptr){ + return false; + } + if (not coords->hasParallelTransform()) { + return false; + } + return not coords->getParallelTransform().canToFromFieldAligned(); } /// Return an empty shell field of some type derived from Field, with metadata From d88b454aa2b2924a26a180440f956911c05a0abc Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 19 Mar 2024 16:04:48 +0100 Subject: [PATCH 052/256] Fix bad cherry-pick --- src/mesh/coordinates.cxx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 3948c75b94..32774d6229 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1542,10 +1542,6 @@ Field3D Coordinates::Grad_par(const Field3D& var, CELL_LOC outloc, TRACE("Coordinates::Grad_par( Field3D )"); ASSERT1(location == outloc || outloc == CELL_DEFAULT); - if (invSg == nullptr) { - invSg = std::make_unique(); - (*invSg) = 1.0 / sqrt(g_22); - } return setName(::DDY(var, outloc, method) * invSg(), "Grad_par({:s})", var.name); } From 8b1fbba650fbaa17c572c7036c4a3d949892cece Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Tue, 19 Mar 2024 15:27:28 +0000 Subject: [PATCH 053/256] Apply clang-format changes --- include/bout/field.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 4433af6d19..61e4af4d4b 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -181,7 +181,7 @@ inline bool areFieldsCompatible(const Field& field1, const Field& field2) { template inline bool isFci(const F& f) { const auto coords = f.getCoordinates(); - if (coords == nullptr){ + if (coords == nullptr) { return false; } if (not coords->hasParallelTransform()) { From 023bc41730de39040a50ae245363945d2447d63b Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 19 Mar 2024 16:35:43 +0100 Subject: [PATCH 054/256] Update to new API --- include/bout/field.hxx | 7 +++++++ src/solver/impls/pvode/pvode.cxx | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 0867560c3b..04035f5b76 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -683,6 +683,13 @@ inline T floor(const T& var, BoutReal f, const std::string& rgn = "RGN_ALL") { #undef FIELD_FUNC +template , class... Types> +inline void setName(T& f, const std::string& name, Types... args) { +#if BOUT_USE_TRACK + f.name = fmt::format(name, args...); +#endif +} + template , class... Types> inline T setName(T&& f, const std::string& name, Types... args) { #if BOUT_USE_TRACK diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index ac6e981b50..762fba32d1 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -373,7 +373,7 @@ BoutReal PvodeSolver::run(BoutReal tout) { for (auto& f : f3d) { f.F_var->enableTracking(fmt::format("ddt_{:s}", f.name), debug); - setName(f.var, f.name); + setName(*f.var, f.name); } run_rhs(simtime); @@ -390,7 +390,7 @@ BoutReal PvodeSolver::run(BoutReal tout) { "{}/BOUT.debug.{}.nc", Options::root()["datadir"].withDefault("data"), BoutComm::rank()); - bout::OptionsNetCDF(outname).write(debug); + bout::OptionsIO::create(outname)->write(debug); MPI_Barrier(BoutComm::get()); } return (-1.0); From affc995c4ba482c79677c890cc44ccb47d45b648 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 19 Mar 2024 16:35:53 +0100 Subject: [PATCH 055/256] Fix documentation --- manual/sphinx/user_docs/bout_options.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/sphinx/user_docs/bout_options.rst b/manual/sphinx/user_docs/bout_options.rst index 85a8a17d59..330a0dad7e 100644 --- a/manual/sphinx/user_docs/bout_options.rst +++ b/manual/sphinx/user_docs/bout_options.rst @@ -889,7 +889,7 @@ Fields can also be stored and written:: Options fields; fields["f2d"] = Field2D(1.0); fields["f3d"] = Field3D(2.0); - bout::OptionsIO::create("fields.nc").write(fields); + bout::OptionsIO::create("fields.nc")->write(fields); This allows the input settings and evolving variables to be combined into a single tree (see above on joining trees) and written From 71f5b6adb6a8ad7b8941ba783773897906d870d2 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Tue, 19 Mar 2024 15:50:33 +0000 Subject: [PATCH 056/256] Apply clang-format changes --- src/solver/impls/pvode/pvode.cxx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index 762fba32d1..a12f330964 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -242,10 +242,12 @@ int PvodeSolver::init() { iopt[MAXORD] = maxOrder; } } - const bool use_adam((*options)["adams_moulton"].doc("Use Adams Moulton solver instead of BDF").withDefault(false)); + const bool use_adam((*options)["adams_moulton"] + .doc("Use Adams Moulton solver instead of BDF") + .withDefault(false)); - cvode_mem = CVodeMalloc(neq, solver_f, simtime, u, use_adam ? ADAMS : BDF, NEWTON, SS, &reltol, &abstol, - this, nullptr, optIn, iopt, ropt, machEnv); + cvode_mem = CVodeMalloc(neq, solver_f, simtime, u, use_adam ? ADAMS : BDF, NEWTON, SS, + &reltol, &abstol, this, nullptr, optIn, iopt, ropt, machEnv); if (cvode_mem == nullptr) { throw BoutException("\tError: CVodeMalloc failed.\n"); @@ -373,12 +375,12 @@ BoutReal PvodeSolver::run(BoutReal tout) { for (auto& f : f3d) { f.F_var->enableTracking(fmt::format("ddt_{:s}", f.name), debug); - setName(*f.var, f.name); + setName(*f.var, f.name); } run_rhs(simtime); for (auto& f : f3d) { - debug[f.name] = *f.var; + debug[f.name] = *f.var; } if (mesh) { From 31fd46153fad6977524394179d0b83ac51f26b9e Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 09:59:13 +0100 Subject: [PATCH 057/256] Apply recomendations from code-review --- include/bout/field3d.hxx | 6 +++--- src/field/field3d.cxx | 10 +++++----- src/solver/impls/pvode/pvode.cxx | 5 +++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index bf7a9cc180..cfde9e5328 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -521,10 +521,10 @@ private: int tracking_state{0}; Options* tracking{nullptr}; - std::string selfname{""}; + std::string selfname; template - Options* track(const T& change, std::string op); - Options* track(const BoutReal& change, std::string op); + Options* track(const T& change, std::string operation); + Options* track(const BoutReal& change, std::string operation); }; // Non-member overloaded operators diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index f0f088b656..2196d6eea4 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -844,12 +844,12 @@ Field3D& Field3D::enableTracking(const std::string& name, Options& _tracking) { } template -Options* Field3D::track(const T& change, std::string op) { - if (tracking and tracking_state) { +Options* Field3D::track(const T& change, std::string operation) { + if (tracking != nullptr and tracking_state != 0) { const std::string outname{fmt::format("track_{:s}_{:d}", selfname, tracking_state++)}; tracking->set(outname, change, "tracking"); (*tracking)[outname].setAttributes({ - {"operation", op}, + {"operation", operation}, #if BOUT_USE_TRACK {"rhs.name", change.name}, #endif @@ -863,12 +863,12 @@ template Options* Field3D::track(const Field3D&, std::string); template Options* Field3D::track(const Field2D&, std::string); template Options* Field3D::track(const FieldPerp&, std::string); -Options* Field3D::track(const BoutReal& change, std::string op) { +Options* Field3D::track(const BoutReal& change, std::string operation) { if (tracking and tracking_state) { const std::string outname{fmt::format("track_{:s}_{:d}", selfname, tracking_state++)}; tracking->set(outname, change, "tracking"); (*tracking)[outname].setAttributes({ - {"operation", op}, + {"operation", operation}, {"rhs.name", "BR"}, }); return &(*tracking)[outname]; diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index a12f330964..f3a96b03af 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -53,7 +53,7 @@ void solver_cfn(integer N, BoutReal t, N_Vector u, void* f_data); namespace { // local only void pvode_load_data_f3d(const std::vector& evolve_bndrys, - std::vector& ffs, BoutReal* udata) { + std::vector& ffs, const BoutReal* udata) { int p = 0; Mesh* mesh = ffs[0].getMesh(); const int nz = mesh->LocalNz; @@ -63,8 +63,9 @@ void pvode_load_data_f3d(const std::vector& evolve_bndrys, // Loop over 3D variables std::vector::const_iterator evolve_bndry = evolve_bndrys.begin(); for (std::vector::iterator ff = ffs.begin(); ff != ffs.end(); ++ff) { - if (bndry && !*evolve_bndry) + if (bndry && !*evolve_bndry) { continue; + } (*ff)[mesh->ind2Dto3D(i2d, jz)] = udata[p]; p++; } From 17e46cfc1c0a835fea474ac68f64a9addbf4379f Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 10:02:28 +0100 Subject: [PATCH 058/256] Use more meaningful names --- src/solver/impls/pvode/pvode.cxx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index f3a96b03af..c389a3c0d1 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -359,18 +359,18 @@ BoutReal PvodeSolver::run(BoutReal tout) { using namespace std::string_literals; Mesh* mesh{}; for (const auto& prefix : {"pre_"s, "residuum_"s}) { - std::vector ffs{}; + std::vector list_of_fields{}; std::vector evolve_bndrys{}; for (const auto& f : f3d) { - Field3D ff{0.}; - ff.allocate(); - ff.setLocation(f.location); - mesh = ff.getMesh(); - debug[fmt::format("{:s}{:s}", prefix, f.name)] = ff; - ffs.push_back(ff); + mesh = f.var->getMesh(); + Field3D to_load{0., mesh}; + to_load.allocate(); + to_load.setLocation(f.location); + debug[fmt::format("{:s}{:s}", prefix, f.name)] = to_load; + list_of_fields.push_back(to_load); evolve_bndrys.push_back(f.evolve_bndry); } - pvode_load_data_f3d(evolve_bndrys, ffs, + pvode_load_data_f3d(evolve_bndrys, list_of_fields, prefix == "pre_"s ? udata : N_VDATA(cv_mem->cv_acor)); } From 9c0ae16ed905588b50f3e4fe634dcedf47de22b5 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Wed, 20 Mar 2024 09:03:10 +0000 Subject: [PATCH 059/256] Apply clang-format changes --- src/solver/impls/pvode/pvode.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index c389a3c0d1..fe231e1086 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -65,7 +65,7 @@ void pvode_load_data_f3d(const std::vector& evolve_bndrys, for (std::vector::iterator ff = ffs.begin(); ff != ffs.end(); ++ff) { if (bndry && !*evolve_bndry) { continue; - } + } (*ff)[mesh->ind2Dto3D(i2d, jz)] = udata[p]; p++; } From 4a17b4982df9788fc26407db70f14d0ce16098e3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 10:04:52 +0100 Subject: [PATCH 060/256] Apply suggestions from code review --- src/solver/impls/pvode/pvode.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index fe231e1086..db28f64d86 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -384,12 +384,12 @@ BoutReal PvodeSolver::run(BoutReal tout) { debug[f.name] = *f.var; } - if (mesh) { + if (mesh != nullptr) { mesh->outputVars(debug); debug["BOUT_VERSION"].force(bout::version::as_double); } - std::string outname = fmt::format( + const std::string outname = fmt::format( "{}/BOUT.debug.{}.nc", Options::root()["datadir"].withDefault("data"), BoutComm::rank()); From 23d309f2532376be77e653535e9e67f96915d20b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 10:28:57 +0100 Subject: [PATCH 061/256] Make isFci a member function and add missing func --- include/bout/coordinates.hxx | 5 +++-- include/bout/field.hxx | 14 ++------------ src/field/field.cxx | 11 +++++++++++ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/include/bout/coordinates.hxx b/include/bout/coordinates.hxx index 49feffa0a7..5ea2d276fc 100644 --- a/include/bout/coordinates.hxx +++ b/include/bout/coordinates.hxx @@ -133,9 +133,10 @@ public: transform = std::move(pt); } + bool hasParallelTransform() const { return transform != nullptr; } /// Return the parallel transform - ParallelTransform& getParallelTransform() { - ASSERT1(transform != nullptr); + ParallelTransform& getParallelTransform() const { + ASSERT1(hasParallelTransform()); return *transform; } diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 61e4af4d4b..d0828e8f6c 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -86,6 +86,8 @@ public: std::string name; + bool isFci() const; + #if CHECK > 0 // Routines to test guard/boundary cells set @@ -178,18 +180,6 @@ inline bool areFieldsCompatible(const Field& field1, const Field& field2) { #define ASSERT1_FIELDS_COMPATIBLE(field1, field2) ; #endif -template -inline bool isFci(const F& f) { - const auto coords = f.getCoordinates(); - if (coords == nullptr) { - return false; - } - if (not coords->hasParallelTransform()) { - return false; - } - return not coords->getParallelTransform().canToFromFieldAligned(); -} - /// Return an empty shell field of some type derived from Field, with metadata /// copied and a data array that is allocated but not initialised. template diff --git a/src/field/field.cxx b/src/field/field.cxx index e48a8f3ef7..c9373454bf 100644 --- a/src/field/field.cxx +++ b/src/field/field.cxx @@ -39,3 +39,14 @@ int Field::getNx() const { return getMesh()->LocalNx; } int Field::getNy() const { return getMesh()->LocalNy; } int Field::getNz() const { return getMesh()->LocalNz; } + +bool Field::isFci() const { + const auto coords = this->getCoordinates(); + if (coords == nullptr) { + return false; + } + if (not coords->hasParallelTransform()) { + return false; + } + return not coords->getParallelTransform().canToFromFieldAligned(); +} From 4bbd9ba699185b6491f1e408825daa2a00884ef3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 3 Nov 2023 14:05:15 +0100 Subject: [PATCH 062/256] Add option to automatically compute parallel fields --- CMakeLists.txt | 3 + cmake_build_defines.hxx.in | 1 + src/field/gen_fieldops.jinja | 21 +++- src/field/generated_fieldops.cxx | 180 ++++++++++++++++++++++++++++--- 4 files changed, 192 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1c82ea4e3..ac4be59575 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -591,6 +591,9 @@ else() endif() set(BOUT_USE_METRIC_3D ${BOUT_ENABLE_METRIC_3D}) +option(BOUT_ENABLE_FCI_AUTOMAGIC "Enable (slow?) automatic features for FCI" ON) +set(BOUT_USE_FCI_AUTOMAGIC ${BOUT_ENABLE_FCI_AUTOMAGIC}) + include(CheckCXXSourceCompiles) check_cxx_source_compiles("int main() { const char* name = __PRETTY_FUNCTION__; }" HAS_PRETTY_FUNCTION) diff --git a/cmake_build_defines.hxx.in b/cmake_build_defines.hxx.in index ed6e8685f6..d3a4ea0334 100644 --- a/cmake_build_defines.hxx.in +++ b/cmake_build_defines.hxx.in @@ -35,6 +35,7 @@ #cmakedefine BOUT_METRIC_TYPE @BOUT_METRIC_TYPE@ #cmakedefine01 BOUT_USE_METRIC_3D #cmakedefine01 BOUT_USE_MSGSTACK +#cmakedefine01 BOUT_USE_FCI_AUTOMAGIC // CMake build does not support legacy interface #define BOUT_HAS_LEGACY_NETCDF 0 diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index ecd4e628cc..6360cba783 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -12,6 +12,15 @@ {% if lhs == rhs == "Field3D" %} {{out.name}}.setRegion({{lhs.name}}.getMesh()->getCommonRegion({{lhs.name}}.getRegionID(), {{rhs.name}}.getRegionID())); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci({{lhs.name}}) and {{lhs.name}}.hasParallelSlices() and {{rhs.name}}.hasParallelSlices()) { + {{out.name}}.splitParallelSlices(); + for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { + {{out.name}}.yup(i) = {{lhs.name}}.yup(i) {{operator}} {{rhs.name}}.yup(i); + {{out.name}}.ydown(i) = {{lhs.name}}.ydown(i) {{operator}} {{rhs.name}}.ydown(i); + } + } +#endif {% elif lhs == "Field3D" %} {{out.name}}.setRegion({{lhs.name}}.getRegionID()); {% elif rhs == "Field3D" %} @@ -78,7 +87,17 @@ {% if (lhs == "Field3D") %} // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices() {% if rhs == "Field3D" %} and {{rhs.name}}.hasParallelSlices() {% endif %}) { + for (size_t i{0} ; i < yup_fields.size() ; ++i) { + yup(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.yup(i){% endif %}; + ydown(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.ydown(i){% endif %}; + } + } else +#endif + { + clearParallelSlices(); + } {% endif %} checkData(*this); diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 6b778acee3..72379b313c 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -15,6 +15,15 @@ Field3D operator*(const Field3D& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) * rhs.yup(i); + result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] * rhs[index]; @@ -33,7 +42,17 @@ Field3D& Field3D::operator*=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs.yup(i); + ydown(i) *= rhs.ydown(i); + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -59,6 +78,15 @@ Field3D operator/(const Field3D& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) / rhs.yup(i); + result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] / rhs[index]; @@ -77,7 +105,17 @@ Field3D& Field3D::operator/=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs.yup(i); + ydown(i) /= rhs.ydown(i); + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -103,6 +141,15 @@ Field3D operator+(const Field3D& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) + rhs.yup(i); + result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] + rhs[index]; @@ -121,7 +168,17 @@ Field3D& Field3D::operator+=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs.yup(i); + ydown(i) += rhs.ydown(i); + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -147,6 +204,15 @@ Field3D operator-(const Field3D& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) - rhs.yup(i); + result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] - rhs[index]; @@ -165,7 +231,17 @@ Field3D& Field3D::operator-=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs.yup(i); + ydown(i) -= rhs.ydown(i); + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -214,7 +290,17 @@ Field3D& Field3D::operator*=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs; + ydown(i) *= rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -267,7 +353,17 @@ Field3D& Field3D::operator/=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs; + ydown(i) /= rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -320,7 +416,17 @@ Field3D& Field3D::operator+=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs; + ydown(i) += rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -372,7 +478,17 @@ Field3D& Field3D::operator-=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs; + ydown(i) -= rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -497,7 +613,17 @@ Field3D& Field3D::operator*=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs; + ydown(i) *= rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -538,7 +664,17 @@ Field3D& Field3D::operator/=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs; + ydown(i) /= rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -579,7 +715,17 @@ Field3D& Field3D::operator+=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs; + ydown(i) += rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); @@ -619,7 +765,17 @@ Field3D& Field3D::operator-=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. - clearParallelSlices(); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this) and this->hasParallelSlices()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs; + ydown(i) -= rhs; + } + } else +#endif + { + clearParallelSlices(); + } checkData(*this); checkData(rhs); From 4fac0185e8015cfa60ebf6b5e36ec3d3606be24a Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 6 Nov 2023 15:20:55 +0100 Subject: [PATCH 063/256] Explicitly set parallel boundary order --- examples/fci-wave-logn/boundary/BOUT.inp | 4 ++-- examples/fci-wave-logn/div-integrate/BOUT.inp | 2 +- examples/fci-wave-logn/expanded/BOUT.inp | 2 +- examples/fci-wave/div-integrate/BOUT.inp | 2 +- examples/fci-wave/div/BOUT.inp | 2 +- examples/fci-wave/logn/BOUT.inp | 2 +- manual/sphinx/user_docs/boundary_options.rst | 11 ++++++----- src/mesh/boundary_factory.cxx | 2 +- 8 files changed, 14 insertions(+), 13 deletions(-) diff --git a/examples/fci-wave-logn/boundary/BOUT.inp b/examples/fci-wave-logn/boundary/BOUT.inp index 11e57ec47d..a33fd07136 100644 --- a/examples/fci-wave-logn/boundary/BOUT.inp +++ b/examples/fci-wave-logn/boundary/BOUT.inp @@ -40,5 +40,5 @@ bndry_par_ydown = parallel_neumann [v] -bndry_par_yup = parallel_dirichlet(+1.0) -bndry_par_ydown = parallel_dirichlet(-1.0) +bndry_par_yup = parallel_dirichlet_o2(+1.0) +bndry_par_ydown = parallel_dirichlet_o2(-1.0) diff --git a/examples/fci-wave-logn/div-integrate/BOUT.inp b/examples/fci-wave-logn/div-integrate/BOUT.inp index a37bf3e2a5..22d2c00aa2 100644 --- a/examples/fci-wave-logn/div-integrate/BOUT.inp +++ b/examples/fci-wave-logn/div-integrate/BOUT.inp @@ -40,4 +40,4 @@ bndry_par_ydown = parallel_neumann [v] -bndry_par_all = parallel_dirichlet +bndry_par_all = parallel_dirichlet_o2 diff --git a/examples/fci-wave-logn/expanded/BOUT.inp b/examples/fci-wave-logn/expanded/BOUT.inp index 3a2935c6e8..347299ca12 100644 --- a/examples/fci-wave-logn/expanded/BOUT.inp +++ b/examples/fci-wave-logn/expanded/BOUT.inp @@ -40,4 +40,4 @@ bndry_par_ydown = parallel_neumann [v] -bndry_par_all = parallel_dirichlet +bndry_par_all = parallel_dirichlet_o2 diff --git a/examples/fci-wave/div-integrate/BOUT.inp b/examples/fci-wave/div-integrate/BOUT.inp index eb41d5f228..68bc1093c1 100644 --- a/examples/fci-wave/div-integrate/BOUT.inp +++ b/examples/fci-wave/div-integrate/BOUT.inp @@ -41,4 +41,4 @@ bndry_par_ydown = parallel_neumann [v] -bndry_par_all = parallel_dirichlet +bndry_par_all = parallel_dirichlet_o2 diff --git a/examples/fci-wave/div/BOUT.inp b/examples/fci-wave/div/BOUT.inp index 70b60757eb..b954dd94a9 100644 --- a/examples/fci-wave/div/BOUT.inp +++ b/examples/fci-wave/div/BOUT.inp @@ -41,4 +41,4 @@ bndry_par_ydown = parallel_neumann [v] -bndry_par_all = parallel_dirichlet +bndry_par_all = parallel_dirichlet_o2 diff --git a/examples/fci-wave/logn/BOUT.inp b/examples/fci-wave/logn/BOUT.inp index f97d8cc891..c2cfd46465 100644 --- a/examples/fci-wave/logn/BOUT.inp +++ b/examples/fci-wave/logn/BOUT.inp @@ -41,4 +41,4 @@ bndry_par_ydown = parallel_neumann [nv] -bndry_par_all = parallel_dirichlet +bndry_par_all = parallel_dirichlet_o2 diff --git a/manual/sphinx/user_docs/boundary_options.rst b/manual/sphinx/user_docs/boundary_options.rst index 57c6658891..826f873dc1 100644 --- a/manual/sphinx/user_docs/boundary_options.rst +++ b/manual/sphinx/user_docs/boundary_options.rst @@ -147,8 +147,9 @@ shifted``, see :ref:`sec-shifted-metric`), the recommended method is to apply boundary conditions directly to the ``yup`` and ``ydown`` parallel slices. This can be done by setting ``bndry_par_yup`` and ``bndry_par_ydown``, or ``bndry_par_all`` to set both at once. The -possible values are ``parallel_dirichlet``, ``parallel_dirichlet_O3`` -and ``parallel_neumann``. The stencils used are the same as for the +possible values are ``parallel_dirichlet_o1``, ``parallel_dirichlet_o2``, +``parallel_dirichlet_o3``, ``parallel_neumann_o1``, ``parallel_neumann_o2`` +and ``parallel_neumann_o3``. The stencils used are the same as for the standard boundary conditions without the ``parallel_`` prefix, but are applied directly to parallel slices. The boundary condition can only be applied after the parallel slices are calculated, which is usually @@ -168,7 +169,7 @@ For example, for an evolving variable ``f``, put a section in the [f] bndry_xin = dirichlet bndry_xout = dirichlet - bndry_par_all = parallel_neumann + bndry_par_all = parallel_neumann_o2 bndry_ydown = none bndry_yup = none @@ -278,7 +279,7 @@ cells of the base variable. For example, for an evolving variable [f] bndry_xin = dirichlet bndry_xout = dirichlet - bndry_par_all = parallel_dirichlet + bndry_par_all = parallel_dirichlet_o2 bndry_ydown = none bndry_yup = none @@ -289,7 +290,7 @@ communication, while the perpendicular ones before: f.applyBoundary(); mesh->communicate(f); - f.applyParallelBoundary("parallel_neumann"); + f.applyParallelBoundary("parallel_neumann_o2"); Note that during grid generation care has to be taken to ensure that there are no "short" connection lengths. Otherwise it can happen that for a point on a diff --git a/src/mesh/boundary_factory.cxx b/src/mesh/boundary_factory.cxx index 5f5978f132..35c8d845b9 100644 --- a/src/mesh/boundary_factory.cxx +++ b/src/mesh/boundary_factory.cxx @@ -314,7 +314,7 @@ BoundaryOpBase* BoundaryFactory::createFromOptions(const string& varname, /// Then (all, all) if (region->isParallel) { // Different default for parallel boundary regions - varOpts->get(prefix + "par_all", set, "parallel_dirichlet"); + varOpts->get(prefix + "par_all", set, "parallel_dirichlet_o2"); } else { varOpts->get(prefix + "all", set, "dirichlet"); } From 29a195f490a4323d2d93e0e5e2388ba2b2799d39 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 6 Nov 2023 16:30:06 +0100 Subject: [PATCH 064/256] Make Field2d and Field3D more similar Useful for templates --- include/bout/field2d.hxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index 10b801ef8d..ee43a005d3 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -135,8 +135,9 @@ public: return *this; } - /// Check if this field has yup and ydown fields + /// Dummy functions to increase portability bool hasParallelSlices() const { return true; } + void calcParallelSlices() const {} Field2D& yup(std::vector::size_type UNUSED(index) = 0) { return *this; } const Field2D& yup(std::vector::size_type UNUSED(index) = 0) const { From 4ee86309b5c56b33020b83a5b11064c0b66d463b Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 6 Nov 2023 16:30:35 +0100 Subject: [PATCH 065/256] Do more things automagically --- src/field/field3d.cxx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 011353f34a..bccd676ace 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -89,6 +89,15 @@ Field3D::Field3D(const BoutReal val, Mesh* localmesh) : Field3D(localmesh) { TRACE("Field3D: Copy constructor from value"); *this = val; +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this)) { + splitParallelSlices(); + for (size_t i=0; i data_in, Mesh* localmesh, CELL_LOC datalocation, @@ -341,6 +350,11 @@ Field3D& Field3D::operator=(const BoutReal val) { Field3D& Field3D::calcParallelSlices() { getCoordinates()->getParallelTransform().calcParallelSlices(*this); +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(*this)) { + this->applyParallelBoundary("parallel_neumann_o2"); + } +#endif return *this; } From 2e216be56995286e75df52f8b0f81e96b5ec9dd0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 6 Nov 2023 16:30:59 +0100 Subject: [PATCH 066/256] Allow DDY without parallel slices --- include/bout/index_derivs_interface.hxx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 8f7e41a68e..9e9288b564 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -200,9 +200,17 @@ template T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { AUTO_TRACE(); - if (f.hasParallelSlices()) { + if (isFci(f)) { ASSERT1(f.getDirectionY() == YDirectionType::Standard); - return standardDerivative(f, outloc, + T f_tmp = f; + if (!f.hasParallelSlices()){ +#if BOUT_USE_FCI_AUTOMAGIC + f_tmp.calcParallelSlices(); +#else + raise BoutException("parallel slices needed for parallel derivatives. Make sure to communicate and apply parallel boundary conditions before calling derivative"); +#endif + } + return standardDerivative(f_tmp, outloc, method, region); } else { const bool is_unaligned = (f.getDirectionY() == YDirectionType::Standard); From faa1046809ebf8682ed65b2e243345a7323471d6 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 18 Mar 2024 17:28:36 +0100 Subject: [PATCH 067/256] Add more fci-auto-magic --- include/bout/field.hxx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index c0693ec0fb..9b95e00437 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -677,7 +677,25 @@ inline T floor(const T& var, BoutReal f, const std::string& rgn = "RGN_ALL") { result[d] = f; } } - +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci(var)) { + for (size_t i=0; i < result.numberParallelSlices(); ++i) { + BOUT_FOR(d, result.yup(i).getRegion(rgn)) { + if (result.yup(i)[d] < f) { + result.yup(i)[d] = f; + } + } + BOUT_FOR(d, result.ydown(i).getRegion(rgn)) { + if (result.ydown(i)[d] < f) { + result.ydown(i)[d] = f; + } + } + } + } else +#endif + { + result.clearParallelSlices(); + } return result; } From 414247b1c40e8ad7c7b5983f3c7d4ec56aa6444d Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 18 Mar 2024 17:29:04 +0100 Subject: [PATCH 068/256] Add copy function --- include/bout/field3d.hxx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 964e3f096c..c29ecbfeca 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -663,4 +663,14 @@ bool operator==(const Field3D& a, const Field3D& b); /// Output a string describing a Field3D to a stream std::ostream& operator<<(std::ostream& out, const Field3D& value); +inline Field3D copy(const Field3D& f) { + Field3D result{f}; + result.allocate(); + for (size_t i = 0; i < result.numberParallelSlices(); ++i) { + result.yup(i).allocate(); + result.ydown(i).allocate(); + } + return result; +} + #endif /* BOUT_FIELD3D_H */ From 6ba17ed64e9207e00e1d292bb3cbca58fc817fdd Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 18 Mar 2024 17:29:18 +0100 Subject: [PATCH 069/256] Inherit applyParallelBoundary functions --- include/bout/field3d.hxx | 1 + 1 file changed, 1 insertion(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index c29ecbfeca..7b8d0861ef 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -494,6 +494,7 @@ public: /// Note: does not just copy values in boundary region. void setBoundaryTo(const Field3D& f3d); + using FieldData::applyParallelBoundary; void applyParallelBoundary() override; void applyParallelBoundary(BoutReal t) override; void applyParallelBoundary(const std::string& condition) override; From b814c9bdb2a9df2f925f61f6fb8d2e892d056f3f Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 14:21:28 +0100 Subject: [PATCH 070/256] update calls to isFci --- include/bout/index_derivs_interface.hxx | 2 +- src/field/field3d.cxx | 4 ++-- src/field/gen_fieldops.jinja | 4 ++-- src/field/generated_fieldops.cxx | 32 ++++++++++++------------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 9e9288b564..86dd4c9287 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -200,7 +200,7 @@ template T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { AUTO_TRACE(); - if (isFci(f)) { + if (f.isFci()) { ASSERT1(f.getDirectionY() == YDirectionType::Standard); T f_tmp = f; if (!f.hasParallelSlices()){ diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index bccd676ace..3430be008f 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -90,7 +90,7 @@ Field3D::Field3D(const BoutReal val, Mesh* localmesh) : Field3D(localmesh) { *this = val; #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this)) { + if (this->isFci()) { splitParallelSlices(); for (size_t i=0; igetParallelTransform().calcParallelSlices(*this); #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this)) { + if (this->isFci()) { this->applyParallelBoundary("parallel_neumann_o2"); } #endif diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index 6360cba783..dede7d120f 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -13,7 +13,7 @@ {{out.name}}.setRegion({{lhs.name}}.getMesh()->getCommonRegion({{lhs.name}}.getRegionID(), {{rhs.name}}.getRegionID())); #if BOUT_USE_FCI_AUTOMAGIC - if (isFci({{lhs.name}}) and {{lhs.name}}.hasParallelSlices() and {{rhs.name}}.hasParallelSlices()) { + if ({{lhs.name}}.isFci() and {{lhs.name}}.hasParallelSlices() and {{rhs.name}}.hasParallelSlices()) { {{out.name}}.splitParallelSlices(); for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { {{out.name}}.yup(i) = {{lhs.name}}.yup(i) {{operator}} {{rhs.name}}.yup(i); @@ -88,7 +88,7 @@ // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices() {% if rhs == "Field3D" %} and {{rhs.name}}.hasParallelSlices() {% endif %}) { + if (this->isFci() and this->hasParallelSlices() {% if rhs == "Field3D" %} and {{rhs.name}}.hasParallelSlices() {% endif %}) { for (size_t i{0} ; i < yup_fields.size() ; ++i) { yup(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.yup(i){% endif %}; ydown(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.ydown(i){% endif %}; diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 72379b313c..74b319e314 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -16,7 +16,7 @@ Field3D operator*(const Field3D& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + if (lhs.isFci() and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); @@ -43,7 +43,7 @@ Field3D& Field3D::operator*=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) *= rhs.yup(i); ydown(i) *= rhs.ydown(i); @@ -79,7 +79,7 @@ Field3D operator/(const Field3D& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + if (lhs.isFci() and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); @@ -106,7 +106,7 @@ Field3D& Field3D::operator/=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) /= rhs.yup(i); ydown(i) /= rhs.ydown(i); @@ -142,7 +142,7 @@ Field3D operator+(const Field3D& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + if (lhs.isFci() and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); @@ -169,7 +169,7 @@ Field3D& Field3D::operator+=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) += rhs.yup(i); ydown(i) += rhs.ydown(i); @@ -205,7 +205,7 @@ Field3D operator-(const Field3D& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(lhs) and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { + if (lhs.isFci() and lhs.hasParallelSlices() and rhs.hasParallelSlices()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); @@ -232,7 +232,7 @@ Field3D& Field3D::operator-=(const Field3D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices() and rhs.hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) -= rhs.yup(i); ydown(i) -= rhs.ydown(i); @@ -291,7 +291,7 @@ Field3D& Field3D::operator*=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) *= rhs; ydown(i) *= rhs; @@ -354,7 +354,7 @@ Field3D& Field3D::operator/=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) /= rhs; ydown(i) /= rhs; @@ -417,7 +417,7 @@ Field3D& Field3D::operator+=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) += rhs; ydown(i) += rhs; @@ -479,7 +479,7 @@ Field3D& Field3D::operator-=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) -= rhs; ydown(i) -= rhs; @@ -614,7 +614,7 @@ Field3D& Field3D::operator*=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) *= rhs; ydown(i) *= rhs; @@ -665,7 +665,7 @@ Field3D& Field3D::operator/=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) /= rhs; ydown(i) /= rhs; @@ -716,7 +716,7 @@ Field3D& Field3D::operator+=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) += rhs; ydown(i) += rhs; @@ -766,7 +766,7 @@ Field3D& Field3D::operator-=(const BoutReal rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(*this) and this->hasParallelSlices()) { + if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { yup(i) -= rhs; ydown(i) -= rhs; From 4a6fdba0b8fd02dd67b38f16fd8d716a18a1fb85 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 14:56:28 +0100 Subject: [PATCH 071/256] Fix remaining usage of free isFci function --- include/bout/field.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index b4524b5347..1ed5ab2a5f 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -680,7 +680,7 @@ inline T floor(const T& var, BoutReal f, const std::string& rgn = "RGN_ALL") { } } #if BOUT_USE_FCI_AUTOMAGIC - if (isFci(var)) { + if (var.isFci()) { for (size_t i=0; i < result.numberParallelSlices(); ++i) { BOUT_FOR(d, result.yup(i).getRegion(rgn)) { if (result.yup(i)[d] < f) { From 2f7c3c0664c954c016b949ea8c199f6f35ac289f Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 16:02:22 +0100 Subject: [PATCH 072/256] Workaround for gcc 9.4 gcc 9.4 is unable to correctly parse the construction for the function argument, if name.change is used directly. First making a copy seems to work around that issue. --- src/field/field3d.cxx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 2196d6eea4..e0d4dda01d 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -848,10 +848,14 @@ Options* Field3D::track(const T& change, std::string operation) { if (tracking != nullptr and tracking_state != 0) { const std::string outname{fmt::format("track_{:s}_{:d}", selfname, tracking_state++)}; tracking->set(outname, change, "tracking"); + // Workaround for bug in gcc9.4 +#if BOUT_USE_TRACK + const std::string changename = change.name; +#endif (*tracking)[outname].setAttributes({ {"operation", operation}, #if BOUT_USE_TRACK - {"rhs.name", change.name}, + {"rhs.name", changename}, #endif }); return &(*tracking)[outname]; From 413e54f0ba4cc360420c1e15566b43fa7e015535 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 20 Mar 2024 16:48:28 +0100 Subject: [PATCH 073/256] Fixup porting to shared_ptr --- src/mesh/parallel/fci.hxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index c78080d9e9..751a177c0e 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -101,8 +101,8 @@ public: backward_boundary_xout, zperiodic); } ASSERT0(mesh.ystart == 1); - BoundaryRegionPar* bndries[]{forward_boundary_xin, forward_boundary_xout, - backward_boundary_xin, backward_boundary_xout}; + std::shared_ptr bndries[]{forward_boundary_xin, forward_boundary_xout, + backward_boundary_xin, backward_boundary_xout}; for (auto bndry : bndries) { for (auto bndry2 : bndries) { if (bndry->dir == bndry2->dir) { From d5d7c6a5e783f5da6b4f9fc11489b7616f13c6e3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Mar 2024 11:21:37 +0100 Subject: [PATCH 074/256] Update include to moved location --- src/mesh/impls/bout/boutmesh.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index 684c8afc40..59f936d265 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -49,8 +49,8 @@ #include #include -#include "boundary_region.hxx" -#include "parallel_boundary_region.hxx" +#include "bout/boundary_region.hxx" +#include "bout/parallel_boundary_region.hxx" #include #include From 652be61a9c5fedb228ce75468abe5bdbad62ba97 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Mar 2024 11:44:19 +0100 Subject: [PATCH 075/256] Move iteration mostly to iterator This allows to use range-based-for loop on the iterator --- include/bout/parallel_boundary_op.hxx | 41 ++-- include/bout/parallel_boundary_region.hxx | 249 ++++++++++++++++------ src/mesh/parallel_boundary_op.cxx | 7 +- 3 files changed, 208 insertions(+), 89 deletions(-) diff --git a/include/bout/parallel_boundary_op.hxx b/include/bout/parallel_boundary_op.hxx index d8620e892b..9e551ebc17 100644 --- a/include/bout/parallel_boundary_op.hxx +++ b/include/bout/parallel_boundary_op.hxx @@ -49,7 +49,7 @@ protected: enum class ValueType { GEN, FIELD, REAL }; const ValueType value_type{ValueType::REAL}; - BoutReal getValue(const BoundaryRegionPar& bndry, BoutReal t); + BoutReal getValue(const BoundaryRegionParIter& bndry, BoutReal t); }; template @@ -95,12 +95,13 @@ public: auto dy = f.getCoordinates()->dy; - for (bndry->first(); !bndry->isDone(); bndry->next()) { - BoutReal value = getValue(*bndry, t); + for (auto pnt : *bndry) { + //for (bndry->first(); !bndry->isDone(); bndry->next()) { + BoutReal value = getValue(pnt, t); if (isNeumann) { - value *= dy[bndry->ind()]; + value *= dy[pnt.ind()]; } - static_cast(this)->apply_stencil(f, bndry, value); + static_cast(this)->apply_stencil(f, pnt, value); } } }; @@ -111,24 +112,27 @@ public: class BoundaryOpPar_dirichlet_o1 : public BoundaryOpParTemp { public: using BoundaryOpParTemp::BoundaryOpParTemp; - static void apply_stencil(Field3D& f, const BoundaryRegionPar* bndry, BoutReal value) { - bndry->dirichlet_o1(f, value); + static void apply_stencil(Field3D& f, const BoundaryRegionParIter& pnt, + BoutReal value) { + pnt.dirichlet_o1(f, value); } }; class BoundaryOpPar_dirichlet_o2 : public BoundaryOpParTemp { public: using BoundaryOpParTemp::BoundaryOpParTemp; - static void apply_stencil(Field3D& f, const BoundaryRegionPar* bndry, BoutReal value) { - bndry->dirichlet_o2(f, value); + static void apply_stencil(Field3D& f, const BoundaryRegionParIter& pnt, + BoutReal value) { + pnt.dirichlet_o2(f, value); } }; class BoundaryOpPar_dirichlet_o3 : public BoundaryOpParTemp { public: using BoundaryOpParTemp::BoundaryOpParTemp; - static void apply_stencil(Field3D& f, const BoundaryRegionPar* bndry, BoutReal value) { - bndry->dirichlet_o3(f, value); + static void apply_stencil(Field3D& f, const BoundaryRegionParIter& pnt, + BoutReal value) { + pnt.dirichlet_o3(f, value); } }; @@ -136,8 +140,9 @@ class BoundaryOpPar_neumann_o1 : public BoundaryOpParTemp { public: using BoundaryOpParTemp::BoundaryOpParTemp; - static void apply_stencil(Field3D& f, const BoundaryRegionPar* bndry, BoutReal value) { - bndry->neumann_o1(f, value); + static void apply_stencil(Field3D& f, const BoundaryRegionParIter& pnt, + BoutReal value) { + pnt.neumann_o1(f, value); } }; @@ -145,8 +150,9 @@ class BoundaryOpPar_neumann_o2 : public BoundaryOpParTemp { public: using BoundaryOpParTemp::BoundaryOpParTemp; - static void apply_stencil(Field3D& f, const BoundaryRegionPar* bndry, BoutReal value) { - bndry->neumann_o2(f, value); + static void apply_stencil(Field3D& f, const BoundaryRegionParIter& pnt, + BoutReal value) { + pnt.neumann_o2(f, value); } }; @@ -154,8 +160,9 @@ class BoundaryOpPar_neumann_o3 : public BoundaryOpParTemp { public: using BoundaryOpParTemp::BoundaryOpParTemp; - static void apply_stencil(Field3D& f, const BoundaryRegionPar* bndry, BoutReal value) { - bndry->neumann_o3(f, value); + static void apply_stencil(Field3D& f, const BoundaryRegionParIter& pnt, + BoutReal value) { + pnt.neumann_o3(f, value); } }; diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 308b5ac5d7..7831d1af82 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -3,6 +3,7 @@ #include "bout/boundary_region.hxx" #include "bout/bout_types.hxx" +#include #include #include @@ -52,61 +53,41 @@ inline BoutReal neumann_o3(BoutReal spacing0, BoutReal value0, BoutReal spacing1 } } // namespace parallel_stencil -class BoundaryRegionPar : public BoundaryRegionBase { +namespace bout { +namespace parallel_boundary_region { - struct RealPoint { - BoutReal s_x; - BoutReal s_y; - BoutReal s_z; - }; - - struct Indices { - // Indices of the boundary point - Ind3D index; - // Intersection with boundary in index space - RealPoint intersection; - // Distance to intersection - BoutReal length; - // Angle between field line and boundary - // BoutReal angle; - // How many points we can go in the opposite direction - signed char valid; - }; - - using IndicesVec = std::vector; - using IndicesIter = IndicesVec::iterator; +struct RealPoint { + BoutReal s_x; + BoutReal s_y; + BoutReal s_z; +}; - /// Vector of points in the boundary - IndicesVec bndry_points; - /// Current position in the boundary points - IndicesIter bndry_position; +struct Indices { + // Indices of the boundary point + Ind3D index; + // Intersection with boundary in index space + RealPoint intersection; + // Distance to intersection + BoutReal length; + // Angle between field line and boundary + // BoutReal angle; + // How many points we can go in the opposite direction + signed char valid; +}; -public: - BoundaryRegionPar(const std::string& name, int dir, Mesh* passmesh) - : BoundaryRegionBase(name, passmesh), dir(dir) { - ASSERT0(std::abs(dir) == 1); - BoundaryRegionBase::isParallel = true; - } - BoundaryRegionPar(const std::string& name, BndryLoc loc, int dir, Mesh* passmesh) - : BoundaryRegionBase(name, loc, passmesh), dir(dir) { - BoundaryRegionBase::isParallel = true; - ASSERT0(std::abs(dir) == 1); - } +using IndicesVec = std::vector; +using IndicesIter = IndicesVec::iterator; +using IndicesIterConst = IndicesVec::const_iterator; - /// Add a point to the boundary - void add_point(Ind3D ind, BoutReal x, BoutReal y, BoutReal z, BoutReal length, - char valid) { - bndry_points.push_back({ind, {x, y, z}, length, valid}); - } - void add_point(int ix, int iy, int iz, BoutReal x, BoutReal y, BoutReal z, - BoutReal length, char valid) { - bndry_points.push_back({xyz2ind(ix, iy, iz, localmesh), {x, y, z}, length, valid}); - } +//} - // final, so they can be inlined - void first() final { bndry_position = begin(bndry_points); } - void next() final { ++bndry_position; } - bool isDone() final { return (bndry_position == end(bndry_points)); } +template +class BoundaryRegionParIterBase { +public: + BoundaryRegionParIterBase(IndicesVec& bndry_points, IndicesIter bndry_position, int dir, + Mesh* localmesh) + : bndry_points(bndry_points), bndry_position(bndry_position), dir(dir), + localmesh(localmesh){}; // getter Ind3D ind() const { return bndry_position->index; } @@ -116,23 +97,73 @@ public: BoutReal length() const { return bndry_position->length; } char valid() const { return bndry_position->valid; } - // setter - void setValid(char val) { bndry_position->valid = val; } + // extrapolate a given point to the boundary + BoutReal extrapolate_sheath_o1(const Field3D& f) const { return f[ind()]; } + BoutReal extrapolate_sheath_o2(const Field3D& f) const { + ASSERT3(valid() >= 0); + if (valid() < 1) { + return extrapolate_sheath_o1(f); + } + return f[ind()] * (1 + length()) - f.ynext(-dir)[ind().yp(-dir)] * length(); + } + inline BoutReal + extrapolate_sheath_o1(const std::function& f) const { + return f(0, ind()); + } + inline BoutReal + extrapolate_sheath_o2(const std::function& f) const { + ASSERT3(valid() >= 0); + if (valid() < 1) { + return extrapolate_sheath_o1(f); + } + return f(0, ind()) * (1 + length()) - f(-dir, ind().yp(-dir)) * length(); + } - bool contains(const BoundaryRegionPar& bndry) const { - return std::binary_search( - begin(bndry_points), end(bndry_points), *bndry.bndry_position, - [](const Indices& i1, const Indices& i2) { return i1.index < i2.index; }); + inline BoutReal interpolate_sheath(const Field3D& f) const { + return f[ind()] * (1 - length()) + ynext(f) * length(); } - // extrapolate a given point to the boundary - BoutReal extrapolate_o1(const Field3D& f) const { return f[ind()]; } - BoutReal extrapolate_o2(const Field3D& f) const { + inline BoutReal extrapolate_next_o1(const Field3D& f) const { return f[ind()]; } + inline BoutReal extrapolate_next_o2(const Field3D& f) const { ASSERT3(valid() >= 0); if (valid() < 1) { - return extrapolate_o1(f); + return extrapolate_next_o1(f); } - return f[ind()] * (1 + length()) - f.ynext(-dir)[ind().yp(-dir)] * length(); + return f[ind()] * 2 - f.ynext(-dir)[ind().yp(-dir)]; + } + + inline BoutReal + extrapolate_next_o1(const std::function& f) const { + return f(0, ind()); + } + inline BoutReal + extrapolate_next_o2(const std::function& f) const { + ASSERT3(valid() >= 0); + if (valid() < 1) { + return extrapolate_sheath_o1(f); + } + return f(0, ind()) * 2 - f(-dir, ind().yp(-dir)); + } + + // extrapolate the gradient into the boundary + inline BoutReal extrapolate_grad_o1(const Field3D& f) const { return 0; } + inline BoutReal extrapolate_grad_o2(const Field3D& f) const { + ASSERT3(valid() >= 0); + if (valid() < 1) { + return extrapolate_grad_o1(f); + } + return f[ind()] - f.ynext(-dir)[ind().yp(-dir)]; + } + + BoundaryRegionParIterBase& operator*() { return *this; } + + BoundaryRegionParIterBase& operator++() { + ++bndry_position; + return *this; + } + + bool operator!=(const BoundaryRegionParIterBase& rhs) { + return bndry_position != rhs.bndry_position; } // dirichlet boundary code @@ -185,16 +216,100 @@ public: parallel_stencil::neumann_o3(1 - length(), value, 1, f[ind()], 2, yprev(f)); } - const int dir; + // BoutReal get(const Field3D& f, int off) + const BoutReal& ynext(const Field3D& f) const { return f.ynext(dir)[ind().yp(dir)]; } + BoutReal& ynext(Field3D& f) const { return f.ynext(dir)[ind().yp(dir)]; } + + const BoutReal& yprev(const Field3D& f) const { + ASSERT3(valid() > 0); + return f.ynext(-dir)[ind().yp(-dir)]; + } + BoutReal& yprev(Field3D& f) const { + ASSERT3(valid() > 0); + return f.ynext(-dir)[ind().yp(-dir)]; + } private: + const IndicesVec& bndry_points; + IndicesIter bndry_position; + constexpr static BoutReal small_value = 1e-2; - // BoutReal get(const Field3D& f, int off) - const BoutReal& ynext(const Field3D& f) const { return f.ynext(dir)[ind().yp(dir)]; } - BoutReal& ynext(Field3D& f) const { return f.ynext(dir)[ind().yp(dir)]; } - const BoutReal& yprev(const Field3D& f) const { return f.ynext(-dir)[ind().yp(-dir)]; } - BoutReal& yprev(Field3D& f) const { return f.ynext(-dir)[ind().yp(-dir)]; } +public: + const int dir; + Mesh* localmesh; +}; +} // namespace parallel_boundary_region +} // namespace bout +using BoundaryRegionParIter = bout::parallel_boundary_region::BoundaryRegionParIterBase< + bout::parallel_boundary_region::IndicesVec, + bout::parallel_boundary_region::IndicesIter>; +using BoundaryRegionParIterConst = + bout::parallel_boundary_region::BoundaryRegionParIterBase< + const bout::parallel_boundary_region::IndicesVec, + bout::parallel_boundary_region::IndicesIterConst>; + +class BoundaryRegionPar : public BoundaryRegionBase { +public: + BoundaryRegionPar(const std::string& name, int dir, Mesh* passmesh) + : BoundaryRegionBase(name, passmesh), dir(dir) { + ASSERT0(std::abs(dir) == 1); + BoundaryRegionBase::isParallel = true; + } + BoundaryRegionPar(const std::string& name, BndryLoc loc, int dir, Mesh* passmesh) + : BoundaryRegionBase(name, loc, passmesh), dir(dir) { + BoundaryRegionBase::isParallel = true; + ASSERT0(std::abs(dir) == 1); + } + + /// Add a point to the boundary + void add_point(Ind3D ind, BoutReal x, BoutReal y, BoutReal z, BoutReal length, + char valid) { + bndry_points.push_back({ind, {x, y, z}, length, valid}); + } + void add_point(int ix, int iy, int iz, BoutReal x, BoutReal y, BoutReal z, + BoutReal length, char valid) { + bndry_points.push_back({xyz2ind(ix, iy, iz, localmesh), {x, y, z}, length, valid}); + } + + // final, so they can be inlined + void first() final { bndry_position = std::begin(bndry_points); } + void next() final { ++bndry_position; } + bool isDone() final { return (bndry_position == std::end(bndry_points)); } + + bool contains(const BoundaryRegionPar& bndry) const { + return std::binary_search(std::begin(bndry_points), std::end(bndry_points), + *bndry.bndry_position, + [](const bout::parallel_boundary_region::Indices& i1, + const bout::parallel_boundary_region::Indices& i2) { + return i1.index < i2.index; + }); + } + + // setter + void setValid(char val) { bndry_position->valid = val; } + + // BoundaryRegionParIterConst begin() const { + // return BoundaryRegionParIterConst(bndry_points, bndry_points.begin(), dir); + // } + // BoundaryRegionParIterConst end() const { + // return BoundaryRegionParIterConst(bndry_points, bndry_points.begin(), dir); + // } + BoundaryRegionParIter begin() { + return BoundaryRegionParIter(bndry_points, bndry_points.begin(), dir, localmesh); + } + BoundaryRegionParIter end() { + return BoundaryRegionParIter(bndry_points, bndry_points.end(), dir, localmesh); + } + + const int dir; + +private: + /// Vector of points in the boundary + bout::parallel_boundary_region::IndicesVec bndry_points; + /// Current position in the boundary points + bout::parallel_boundary_region::IndicesIter bndry_position; + static Ind3D xyz2ind(int x, int y, int z, Mesh* mesh) { const int ny = mesh->LocalNy; const int nz = mesh->LocalNz; diff --git a/src/mesh/parallel_boundary_op.cxx b/src/mesh/parallel_boundary_op.cxx index ebd9852791..df164ce43f 100644 --- a/src/mesh/parallel_boundary_op.cxx +++ b/src/mesh/parallel_boundary_op.cxx @@ -5,17 +5,14 @@ #include "bout/mesh.hxx" #include "bout/output.hxx" -BoutReal BoundaryOpPar::getValue(const BoundaryRegionPar& bndry, BoutReal t) { - BoutReal value; - +BoutReal BoundaryOpPar::getValue(const BoundaryRegionParIter& bndry, BoutReal t) { switch (value_type) { case ValueType::GEN: return gen_values->generate(bout::generator::Context( bndry.s_x(), bndry.s_y(), bndry.s_z(), CELL_CENTRE, bndry.localmesh, t)); case ValueType::FIELD: // FIXME: Interpolate to s_x, s_y, s_z... - value = (*field_values)[bndry.ind()]; - return value; + return (*field_values)[bndry.ind()]; case ValueType::REAL: return real_value; default: From 7d48dbd339311e068de5afab6e112356b18cd156 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 27 Mar 2024 13:29:58 +0100 Subject: [PATCH 076/256] Move stencils to separte header Makes it easier to reuse for other code --- include/bout/parallel_boundary_region.hxx | 39 +---------------------- include/bout/sys/parallel_stencils.hxx | 39 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 38 deletions(-) create mode 100644 include/bout/sys/parallel_stencils.hxx diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 7831d1af82..07150a55b3 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -6,6 +6,7 @@ #include #include +#include "bout/sys/parallel_stencils.hxx" #include #include @@ -15,44 +16,6 @@ * */ -namespace parallel_stencil { -// generated by src/mesh/parallel_boundary_stencil.cxx.py -inline BoutReal pow(BoutReal val, int exp) { - // constexpr int expval = exp; - // static_assert(expval == 2 or expval == 3, "This pow is only for exponent 2 or 3"); - if (exp == 2) { - return val * val; - } - ASSERT3(exp == 3); - return val * val * val; -} -inline BoutReal dirichlet_o1(BoutReal UNUSED(spacing0), BoutReal value0) { - return value0; -} -inline BoutReal dirichlet_o2(BoutReal spacing0, BoutReal value0, BoutReal spacing1, - BoutReal value1) { - return (spacing0 * value1 - spacing1 * value0) / (spacing0 - spacing1); -} -inline BoutReal neumann_o2(BoutReal UNUSED(spacing0), BoutReal value0, BoutReal spacing1, - BoutReal value1) { - return -spacing1 * value0 + value1; -} -inline BoutReal dirichlet_o3(BoutReal spacing0, BoutReal value0, BoutReal spacing1, - BoutReal value1, BoutReal spacing2, BoutReal value2) { - return (pow(spacing0, 2) * spacing1 * value2 - pow(spacing0, 2) * spacing2 * value1 - - spacing0 * pow(spacing1, 2) * value2 + spacing0 * pow(spacing2, 2) * value1 - + pow(spacing1, 2) * spacing2 * value0 - spacing1 * pow(spacing2, 2) * value0) - / ((spacing0 - spacing1) * (spacing0 - spacing2) * (spacing1 - spacing2)); -} -inline BoutReal neumann_o3(BoutReal spacing0, BoutReal value0, BoutReal spacing1, - BoutReal value1, BoutReal spacing2, BoutReal value2) { - return (2 * spacing0 * spacing1 * value2 - 2 * spacing0 * spacing2 * value1 - + pow(spacing1, 2) * spacing2 * value0 - pow(spacing1, 2) * value2 - - spacing1 * pow(spacing2, 2) * value0 + pow(spacing2, 2) * value1) - / ((spacing1 - spacing2) * (2 * spacing0 - spacing1 - spacing2)); -} -} // namespace parallel_stencil - namespace bout { namespace parallel_boundary_region { diff --git a/include/bout/sys/parallel_stencils.hxx b/include/bout/sys/parallel_stencils.hxx new file mode 100644 index 0000000000..34a51c5285 --- /dev/null +++ b/include/bout/sys/parallel_stencils.hxx @@ -0,0 +1,39 @@ +#pragma once + +namespace parallel_stencil { +// generated by src/mesh/parallel_boundary_stencil.cxx.py +inline BoutReal pow(BoutReal val, int exp) { + // constexpr int expval = exp; + // static_assert(expval == 2 or expval == 3, "This pow is only for exponent 2 or 3"); + if (exp == 2) { + return val * val; + } + ASSERT3(exp == 3); + return val * val * val; +} +inline BoutReal dirichlet_o1(BoutReal UNUSED(spacing0), BoutReal value0) { + return value0; +} +inline BoutReal dirichlet_o2(BoutReal spacing0, BoutReal value0, BoutReal spacing1, + BoutReal value1) { + return (spacing0 * value1 - spacing1 * value0) / (spacing0 - spacing1); +} +inline BoutReal neumann_o2(BoutReal UNUSED(spacing0), BoutReal value0, BoutReal spacing1, + BoutReal value1) { + return -spacing1 * value0 + value1; +} +inline BoutReal dirichlet_o3(BoutReal spacing0, BoutReal value0, BoutReal spacing1, + BoutReal value1, BoutReal spacing2, BoutReal value2) { + return (pow(spacing0, 2) * spacing1 * value2 - pow(spacing0, 2) * spacing2 * value1 + - spacing0 * pow(spacing1, 2) * value2 + spacing0 * pow(spacing2, 2) * value1 + + pow(spacing1, 2) * spacing2 * value0 - spacing1 * pow(spacing2, 2) * value0) + / ((spacing0 - spacing1) * (spacing0 - spacing2) * (spacing1 - spacing2)); +} +inline BoutReal neumann_o3(BoutReal spacing0, BoutReal value0, BoutReal spacing1, + BoutReal value1, BoutReal spacing2, BoutReal value2) { + return (2 * spacing0 * spacing1 * value2 - 2 * spacing0 * spacing2 * value1 + + pow(spacing1, 2) * spacing2 * value0 - pow(spacing1, 2) * value2 + - spacing1 * pow(spacing2, 2) * value0 + pow(spacing2, 2) * value1) + / ((spacing1 - spacing2) * (2 * spacing0 - spacing1 - spacing2)); +} +} // namespace parallel_stencil From 59cd39dd915ba4e8cb028c31a1b8ae4dfe3f427b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 27 Mar 2024 13:30:54 +0100 Subject: [PATCH 077/256] Add more dummy functions to field2d Allows to write code for Field3D, that also works for Field2D --- include/bout/field2d.hxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index ee43a005d3..cd036c04ff 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -138,6 +138,8 @@ public: /// Dummy functions to increase portability bool hasParallelSlices() const { return true; } void calcParallelSlices() const {} + void clearParallelSlices() {} + int numberParallelSlices() { return 0; } Field2D& yup(std::vector::size_type UNUSED(index) = 0) { return *this; } const Field2D& yup(std::vector::size_type UNUSED(index) = 0) const { @@ -281,7 +283,7 @@ public: friend void swap(Field2D& first, Field2D& second) noexcept; - int size() const override { return nx * ny; }; + int size() const override { return nx * ny; } private: /// Internal data array. Handles allocation/freeing of memory From 127fc9a756379c1033331bacb525ec73ec356e3a Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 27 Mar 2024 13:40:07 +0100 Subject: [PATCH 078/256] Add basic boundary region iterator Mimicks the parallel case, to write FCI independent code --- include/bout/boundary_region.hxx | 93 ++++++++++++++++++++++++++++++++ src/mesh/boundary_region.cxx | 3 ++ 2 files changed, 96 insertions(+) diff --git a/include/bout/boundary_region.hxx b/include/bout/boundary_region.hxx index 58de12045e..6e7a939d3c 100644 --- a/include/bout/boundary_region.hxx +++ b/include/bout/boundary_region.hxx @@ -4,6 +4,9 @@ class BoundaryRegion; #ifndef BOUT_BNDRY_REGION_H #define BOUT_BNDRY_REGION_H +#include "bout/mesh.hxx" +#include "bout/region.hxx" +#include "bout/sys/parallel_stencils.hxx" #include #include @@ -62,6 +65,7 @@ public: isDone() = 0; ///< Returns true if outside domain. Can use this with nested nextX, nextY }; +class BoundaryRegionIter; /// Describes a region of the boundary, and a means of iterating over it class BoundaryRegion : public BoundaryRegionBase { public: @@ -80,6 +84,95 @@ public: virtual void next1d() = 0; ///< Loop over the innermost elements virtual void nextX() = 0; ///< Just loop over X virtual void nextY() = 0; ///< Just loop over Y + + BoundaryRegionIter begin(); + BoundaryRegionIter end(); +}; + +class BoundaryRegionIter { +public: + BoundaryRegionIter(BoundaryRegion* rgn, bool is_end) + : rgn(rgn), is_end(is_end), dir(rgn->bx + rgn->by) { + //static_assert(std::is_base_of, "BoundaryRegionIter only works on BoundaryRegion"); + + // Ensure only one is non-zero + ASSERT3(rgn->bx * rgn->by == 0); + if (!is_end) { + rgn->first(); + } + } + bool operator!=(const BoundaryRegionIter& rhs) { + if (is_end) { + if (rhs.is_end || rhs.rgn->isDone()) { + return false; + } else { + return true; + } + } + if (rhs.is_end) { + return !rgn->isDone(); + } + return ind() != rhs.ind(); + } + + Ind3D ind() const { return xyz2ind(rgn->x - rgn->bx, rgn->y - rgn->by, z); } + BoundaryRegionIter& operator++() { + ASSERT3(z < nz()); + z++; + if (z == nz()) { + z = 0; + rgn->next(); + } + return *this; + } + BoundaryRegionIter& operator*() { return *this; } + + void dirichlet_o2(Field3D& f, BoutReal value) const { + ynext(f) = parallel_stencil::dirichlet_o2(1, f[ind()], 0.5, value); + } + + BoutReal extrapolate_grad_o2(const Field3D& f) const { return f[ind()] - yprev(f); } + + BoutReal extrapolate_sheath_o2(const Field3D& f) const { + return (f[ind()] * 3 - yprev(f)) * 0.5; + } + + BoutReal extrapolate_next_o2(const Field3D& f) const { return 2 * f[ind()] - yprev(f); } + + BoutReal + extrapolate_next_o2(const std::function& f) const { + return 2 * f(0, ind()) - f(0, ind().yp(-rgn->by).xp(-rgn->bx)); + } + + BoutReal interpolate_sheath(const Field3D& f) const { + return (f[ind()] + ynext(f)) * 0.5; + } + + BoutReal& ynext(Field3D& f) const { return f[ind().yp(rgn->by).xp(rgn->bx)]; } + const BoutReal& ynext(const Field3D& f) const { + return f[ind().yp(rgn->by).xp(rgn->bx)]; + } + BoutReal& yprev(Field3D& f) const { return f[ind().yp(-rgn->by).xp(-rgn->bx)]; } + const BoutReal& yprev(const Field3D& f) const { + return f[ind().yp(-rgn->by).xp(-rgn->bx)]; + } + +private: + BoundaryRegion* rgn; + const bool is_end; + int z{0}; + +public: + const int dir; + +private: + int nx() const { return rgn->localmesh->LocalNx; } + int ny() const { return rgn->localmesh->LocalNy; } + int nz() const { return rgn->localmesh->LocalNz; } + + Ind3D xyz2ind(int x, int y, int z) const { + return Ind3D{(x * ny() + y) * nz() + z, ny(), nz()}; + } }; class BoundaryRegionXIn : public BoundaryRegion { diff --git a/src/mesh/boundary_region.cxx b/src/mesh/boundary_region.cxx index 700ef8a91f..ef4aa13a66 100644 --- a/src/mesh/boundary_region.cxx +++ b/src/mesh/boundary_region.cxx @@ -202,3 +202,6 @@ void BoundaryRegionYUp::nextY() { bool BoundaryRegionYUp::isDone() { return (x > xe) || (y >= localmesh->LocalNy); // Return true if gone out of the boundary } + +BoundaryRegionIter BoundaryRegion::begin() { return BoundaryRegionIter(this, false); } +BoundaryRegionIter BoundaryRegion::end() { return BoundaryRegionIter(this, true); } From 9b68bf2820a80c316b8391e9780533831d900cad Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 9 Apr 2024 14:40:17 +0200 Subject: [PATCH 079/256] Provide a new boundary iterator based on RangeIterator The boundary region does not match what is done for the parallel case, thus porting it to a iterator does not work. --- include/bout/boundary_iterator.hxx | 117 +++++++++++++++++++++++++++++ include/bout/boundary_region.hxx | 90 ---------------------- 2 files changed, 117 insertions(+), 90 deletions(-) create mode 100644 include/bout/boundary_iterator.hxx diff --git a/include/bout/boundary_iterator.hxx b/include/bout/boundary_iterator.hxx new file mode 100644 index 0000000000..93f02c004d --- /dev/null +++ b/include/bout/boundary_iterator.hxx @@ -0,0 +1,117 @@ +#pragma once + +#include "bout/mesh.hxx" +#include "bout/sys/parallel_stencils.hxx" +#include "bout/sys/range.hxx" + +class BoundaryRegionIter { +public: + BoundaryRegionIter(int x, int y, int bx, int by, Mesh* mesh) + : dir(bx + by), x(x), y(y), bx(bx), by(by), localmesh(mesh) { + ASSERT3(bx * by == 0); + } + bool operator!=(const BoundaryRegionIter& rhs) { return ind() != rhs.ind(); } + + Ind3D ind() const { return xyz2ind(x, y, z); } + BoundaryRegionIter& operator++() { + ASSERT3(z < nz()); + z++; + if (z == nz()) { + z = 0; + _next(); + } + return *this; + } + virtual void _next() = 0; + BoundaryRegionIter& operator*() { return *this; } + + void dirichlet_o2(Field3D& f, BoutReal value) const { + ynext(f) = parallel_stencil::dirichlet_o2(1, f[ind()], 0.5, value); + } + + BoutReal extrapolate_grad_o2(const Field3D& f) const { return f[ind()] - yprev(f); } + + BoutReal extrapolate_sheath_o2(const Field3D& f) const { + return (f[ind()] * 3 - yprev(f)) * 0.5; + } + + BoutReal extrapolate_next_o2(const Field3D& f) const { return 2 * f[ind()] - yprev(f); } + + BoutReal + extrapolate_next_o2(const std::function& f) const { + return 2 * f(0, ind()) - f(0, ind().yp(-by).xp(-bx)); + } + + BoutReal interpolate_sheath(const Field3D& f) const { + return (f[ind()] + ynext(f)) * 0.5; + } + + BoutReal& ynext(Field3D& f) const { return f[ind().yp(by).xp(bx)]; } + const BoutReal& ynext(const Field3D& f) const { return f[ind().yp(by).xp(bx)]; } + BoutReal& yprev(Field3D& f) const { return f[ind().yp(-by).xp(-bx)]; } + const BoutReal& yprev(const Field3D& f) const { return f[ind().yp(-by).xp(-bx)]; } + + const int dir; + +protected: + int z{0}; + int x; + int y; + const int bx; + const int by; + +private: + Mesh* localmesh; + int nx() const { return localmesh->LocalNx; } + int ny() const { return localmesh->LocalNy; } + int nz() const { return localmesh->LocalNz; } + + Ind3D xyz2ind(int x, int y, int z) const { + return Ind3D{(x * ny() + y) * nz() + z, ny(), nz()}; + } +}; + +class BoundaryRegionIterY : public BoundaryRegionIter { +public: + BoundaryRegionIterY(RangeIterator r, int y, int dir, bool is_end, Mesh* mesh) + : BoundaryRegionIter(r.ind, y, 0, dir, mesh), r(r), is_end(is_end) {} + + bool operator!=(const BoundaryRegionIterY& rhs) { + ASSERT2(y == rhs.y); + if (is_end) { + if (rhs.is_end) { + return false; + } + return !rhs.r.isDone(); + } + if (rhs.is_end) { + return !r.isDone(); + } + return x != rhs.x; + } + + virtual void _next() override { + ++r; + x = r.ind; + } + +private: + RangeIterator r; + bool is_end; +}; + +class NewBoundaryRegionY { +public: + NewBoundaryRegionY(Mesh* mesh, bool lower, RangeIterator r) + : mesh(mesh), lower(lower), r(std::move(r)) {} + BoundaryRegionIterY begin(bool begin = true) { + return BoundaryRegionIterY(r, lower ? mesh->ystart : mesh->yend, lower ? -1 : +1, + !begin, mesh); + } + BoundaryRegionIterY end() { return begin(false); } + +private: + Mesh* mesh; + bool lower; + RangeIterator r; +}; diff --git a/include/bout/boundary_region.hxx b/include/bout/boundary_region.hxx index 6e7a939d3c..22956d1d4a 100644 --- a/include/bout/boundary_region.hxx +++ b/include/bout/boundary_region.hxx @@ -65,7 +65,6 @@ public: isDone() = 0; ///< Returns true if outside domain. Can use this with nested nextX, nextY }; -class BoundaryRegionIter; /// Describes a region of the boundary, and a means of iterating over it class BoundaryRegion : public BoundaryRegionBase { public: @@ -84,95 +83,6 @@ public: virtual void next1d() = 0; ///< Loop over the innermost elements virtual void nextX() = 0; ///< Just loop over X virtual void nextY() = 0; ///< Just loop over Y - - BoundaryRegionIter begin(); - BoundaryRegionIter end(); -}; - -class BoundaryRegionIter { -public: - BoundaryRegionIter(BoundaryRegion* rgn, bool is_end) - : rgn(rgn), is_end(is_end), dir(rgn->bx + rgn->by) { - //static_assert(std::is_base_of, "BoundaryRegionIter only works on BoundaryRegion"); - - // Ensure only one is non-zero - ASSERT3(rgn->bx * rgn->by == 0); - if (!is_end) { - rgn->first(); - } - } - bool operator!=(const BoundaryRegionIter& rhs) { - if (is_end) { - if (rhs.is_end || rhs.rgn->isDone()) { - return false; - } else { - return true; - } - } - if (rhs.is_end) { - return !rgn->isDone(); - } - return ind() != rhs.ind(); - } - - Ind3D ind() const { return xyz2ind(rgn->x - rgn->bx, rgn->y - rgn->by, z); } - BoundaryRegionIter& operator++() { - ASSERT3(z < nz()); - z++; - if (z == nz()) { - z = 0; - rgn->next(); - } - return *this; - } - BoundaryRegionIter& operator*() { return *this; } - - void dirichlet_o2(Field3D& f, BoutReal value) const { - ynext(f) = parallel_stencil::dirichlet_o2(1, f[ind()], 0.5, value); - } - - BoutReal extrapolate_grad_o2(const Field3D& f) const { return f[ind()] - yprev(f); } - - BoutReal extrapolate_sheath_o2(const Field3D& f) const { - return (f[ind()] * 3 - yprev(f)) * 0.5; - } - - BoutReal extrapolate_next_o2(const Field3D& f) const { return 2 * f[ind()] - yprev(f); } - - BoutReal - extrapolate_next_o2(const std::function& f) const { - return 2 * f(0, ind()) - f(0, ind().yp(-rgn->by).xp(-rgn->bx)); - } - - BoutReal interpolate_sheath(const Field3D& f) const { - return (f[ind()] + ynext(f)) * 0.5; - } - - BoutReal& ynext(Field3D& f) const { return f[ind().yp(rgn->by).xp(rgn->bx)]; } - const BoutReal& ynext(const Field3D& f) const { - return f[ind().yp(rgn->by).xp(rgn->bx)]; - } - BoutReal& yprev(Field3D& f) const { return f[ind().yp(-rgn->by).xp(-rgn->bx)]; } - const BoutReal& yprev(const Field3D& f) const { - return f[ind().yp(-rgn->by).xp(-rgn->bx)]; - } - -private: - BoundaryRegion* rgn; - const bool is_end; - int z{0}; - -public: - const int dir; - -private: - int nx() const { return rgn->localmesh->LocalNx; } - int ny() const { return rgn->localmesh->LocalNy; } - int nz() const { return rgn->localmesh->LocalNz; } - - Ind3D xyz2ind(int x, int y, int z) const { - return Ind3D{(x * ny() + y) * nz() + z, ny(), nz()}; - } }; class BoundaryRegionXIn : public BoundaryRegion { From d611758ae6b02a93e51eeeaebe025b628a23c9b3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 9 Apr 2024 15:15:30 +0200 Subject: [PATCH 080/256] Add option to debug on failure --- src/solver/impls/pvode/pvode.cxx | 80 ++++++++++++++++++-------------- src/solver/impls/pvode/pvode.hxx | 9 +++- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index db28f64d86..9dce5d357f 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -247,6 +247,11 @@ int PvodeSolver::init() { .doc("Use Adams Moulton solver instead of BDF") .withDefault(false)); + debug_on_failure = + (*options)["debug_on_failure"] + .doc("Run an aditional rhs if the solver fails with extra tracking") + .withDefault(false); + cvode_mem = CVodeMalloc(neq, solver_f, simtime, u, use_adam ? ADAMS : BDF, NEWTON, SS, &reltol, &abstol, this, nullptr, optIn, iopt, ropt, machEnv); @@ -354,47 +359,52 @@ BoutReal PvodeSolver::run(BoutReal tout) { if (flag != SUCCESS) { output_error.write("ERROR CVODE step failed, flag = {:d}\n", flag); CVodeMemRec* cv_mem = (CVodeMem)cvode_mem; - if (f2d.empty() and v2d.empty() and v3d.empty()) { - Options debug{}; - using namespace std::string_literals; - Mesh* mesh{}; - for (const auto& prefix : {"pre_"s, "residuum_"s}) { - std::vector list_of_fields{}; - std::vector evolve_bndrys{}; - for (const auto& f : f3d) { - mesh = f.var->getMesh(); - Field3D to_load{0., mesh}; - to_load.allocate(); - to_load.setLocation(f.location); - debug[fmt::format("{:s}{:s}", prefix, f.name)] = to_load; - list_of_fields.push_back(to_load); - evolve_bndrys.push_back(f.evolve_bndry); + if (debug_on_failure) { + if (f2d.empty() and v2d.empty() and v3d.empty()) { + Options debug{}; + using namespace std::string_literals; + Mesh* mesh{}; + for (const auto& prefix : {"pre_"s, "residuum_"s}) { + std::vector list_of_fields{}; + std::vector evolve_bndrys{}; + for (const auto& f : f3d) { + mesh = f.var->getMesh(); + Field3D to_load{0., mesh}; + to_load.allocate(); + to_load.setLocation(f.location); + debug[fmt::format("{:s}{:s}", prefix, f.name)] = to_load; + list_of_fields.push_back(to_load); + evolve_bndrys.push_back(f.evolve_bndry); + } + pvode_load_data_f3d(evolve_bndrys, list_of_fields, + prefix == "pre_"s ? udata : N_VDATA(cv_mem->cv_acor)); } - pvode_load_data_f3d(evolve_bndrys, list_of_fields, - prefix == "pre_"s ? udata : N_VDATA(cv_mem->cv_acor)); - } - for (auto& f : f3d) { - f.F_var->enableTracking(fmt::format("ddt_{:s}", f.name), debug); - setName(*f.var, f.name); - } - run_rhs(simtime); + for (auto& f : f3d) { + f.F_var->enableTracking(fmt::format("ddt_{:s}", f.name), debug); + setName(*f.var, f.name); + } + run_rhs(simtime); - for (auto& f : f3d) { - debug[f.name] = *f.var; - } + for (auto& f : f3d) { + debug[f.name] = *f.var; + } - if (mesh != nullptr) { - mesh->outputVars(debug); - debug["BOUT_VERSION"].force(bout::version::as_double); - } + if (mesh != nullptr) { + mesh->outputVars(debug); + debug["BOUT_VERSION"].force(bout::version::as_double); + } - const std::string outname = fmt::format( - "{}/BOUT.debug.{}.nc", - Options::root()["datadir"].withDefault("data"), BoutComm::rank()); + const std::string outname = + fmt::format("{}/BOUT.debug.{}.nc", + Options::root()["datadir"].withDefault("data"), + BoutComm::rank()); - bout::OptionsIO::create(outname)->write(debug); - MPI_Barrier(BoutComm::get()); + bout::OptionsIO::create(outname)->write(debug); + MPI_Barrier(BoutComm::get()); + } else { + output_warn.write("debug_on_failure is currently only supported for Field3Ds"); + } } return (-1.0); } diff --git a/src/solver/impls/pvode/pvode.hxx b/src/solver/impls/pvode/pvode.hxx index d29135d02e..3b385af99d 100644 --- a/src/solver/impls/pvode/pvode.hxx +++ b/src/solver/impls/pvode/pvode.hxx @@ -75,10 +75,15 @@ private: pvode::machEnvType machEnv{nullptr}; void* cvode_mem{nullptr}; - BoutReal abstol, reltol; // addresses passed in init must be preserved + BoutReal abstol, reltol; + // addresses passed in init must be preserved pvode::PVBBDData pdata{nullptr}; - bool pvode_initialised = false; + /// is pvode already initialised? + bool pvode_initialised{false}; + + /// Add debugging data if solver fails + bool debug_on_failure{false}; }; #endif // BOUT_PVODE_SOLVER_H From db96b7ecc3df8613a80203d59a421bf4186f25d1 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 9 Apr 2024 15:56:25 +0200 Subject: [PATCH 081/256] Add option to euler solver to dump debug info --- src/solver/impls/euler/euler.cxx | 36 +++++++++++++++++++++++++++++++- src/solver/impls/euler/euler.hxx | 2 ++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index 3976f4402c..45ba5ccdbf 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -20,7 +20,10 @@ EulerSolver::EulerSolver(Options* options) .withDefault(2.)), timestep((*options)["timestep"] .doc("Internal timestep (defaults to output timestep)") - .withDefault(getOutputTimestep())) {} + .withDefault(getOutputTimestep())), + dump_at_time((*options)["dump_at_time"] + .doc("Dump debug info about the simulation") + .withDefault(-1)) {} void EulerSolver::setMaxTimestep(BoutReal dt) { if (dt >= cfl_factor * timestep) { @@ -141,7 +144,38 @@ void EulerSolver::take_step(BoutReal curtime, BoutReal dt, Array& star Array& result) { load_vars(std::begin(start)); + const bool dump_now = dump_at_time > 0 && std::abs(dump_at_time - curtime) < dt; + std::unique_ptr debug_ptr; + if (dump_now) { + debug_ptr = std::make_unique(); + Options& debug = *debug_ptr; + for (auto& f : f3d) { + f.F_var->enableTracking(fmt::format("ddt_{:s}", f.name), debug); + setName(*f.var, f.name); + } + } + run_rhs(curtime); + if (dump_now) { + Options& debug = *debug_ptr; + Mesh* mesh; + for (auto& f : f3d) { + debug[f.name] = *f.var; + mesh = f.var->getMesh(); + } + + if (mesh != nullptr) { + mesh->outputVars(debug); + debug["BOUT_VERSION"].force(bout::version::as_double); + } + + const std::string outname = fmt::format( + "{}/BOUT.debug.{}.nc", + Options::root()["datadir"].withDefault("data"), BoutComm::rank()); + + bout::OptionsIO::create(outname)->write(debug); + MPI_Barrier(BoutComm::get()); + } save_derivs(std::begin(result)); BOUT_OMP_PERF(parallel for) diff --git a/src/solver/impls/euler/euler.hxx b/src/solver/impls/euler/euler.hxx index 0ee81a3d33..4b6dc60a62 100644 --- a/src/solver/impls/euler/euler.hxx +++ b/src/solver/impls/euler/euler.hxx @@ -64,6 +64,8 @@ private: /// Take a single step to calculate f1 void take_step(BoutReal curtime, BoutReal dt, Array& start, Array& result); + + BoutReal dump_at_time{-1.}; }; #endif // BOUT_KARNIADAKIS_SOLVER_H From 6f3bbf5ca9672c729aeb70d5fd02e2b49f857141 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 2 Jul 2024 14:37:46 +0200 Subject: [PATCH 082/256] Fall back to non fv div_par for fci --- include/bout/fv_ops.hxx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index 94007a57a2..97558ddcfb 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -11,6 +11,7 @@ #include "bout/utils.hxx" #include +#include namespace FV { /*! @@ -192,6 +193,12 @@ template const Field3D Div_par(const Field3D& f_in, const Field3D& v_in, const Field3D& wave_speed_in, bool fixflux = true) { +#if BOUT_USE_FCI_AUTOMAGIC + if (f_in.isFci()) { + return ::Div_par(f_in, v_in); + } +#endif + ASSERT1_FIELDS_COMPATIBLE(f_in, v_in); ASSERT1_FIELDS_COMPATIBLE(f_in, wave_speed_in); From a73080d8d55914973a03a189f85154af7578bbfa Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 2 Jul 2024 14:38:47 +0200 Subject: [PATCH 083/256] Add isFci also to mesh --- include/bout/mesh.hxx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/include/bout/mesh.hxx b/include/bout/mesh.hxx index c80716fc12..a3a36ad933 100644 --- a/include/bout/mesh.hxx +++ b/include/bout/mesh.hxx @@ -828,6 +828,17 @@ public: ASSERT1(RegionID.has_value()); return region3D[RegionID.value()]; } + bool isFci() const { + const auto coords = this->getCoordinatesConst(); + if (coords == nullptr) { + return false; + } + if (not coords->hasParallelTransform()) { + return false; + } + return not coords->getParallelTransform().canToFromFieldAligned(); + } + private: /// Allocates default Coordinates objects From d8cf334cec39a07e045ad63b0a27050011f259a5 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 2 Jul 2024 14:41:42 +0200 Subject: [PATCH 084/256] Only check hasBndry*Y if they would be included hasBndryUpperY / hasBndryLowerY does not work for FCI and thus the request does not make sense / can be configured to throw. Thus it should not be checked if it is not needed. --- src/invert/laplace/invert_laplace.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invert/laplace/invert_laplace.cxx b/src/invert/laplace/invert_laplace.cxx index 505b04cc4f..963a8763d2 100644 --- a/src/invert/laplace/invert_laplace.cxx +++ b/src/invert/laplace/invert_laplace.cxx @@ -226,10 +226,10 @@ Field3D Laplacian::solve(const Field3D& b, const Field3D& x0) { // Setting the start and end range of the y-slices int ys = localmesh->ystart, ye = localmesh->yend; - if (localmesh->hasBndryLowerY() && include_yguards) { + if (include_yguards && localmesh->hasBndryLowerY()) { ys = 0; // Mesh contains a lower boundary } - if (localmesh->hasBndryUpperY() && include_yguards) { + if (include_yguards && localmesh->hasBndryUpperY()) { ye = localmesh->LocalNy - 1; // Contains upper boundary } From 98da34a442f3411406f17ab576261f4fe3383102 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 2 Jul 2024 15:38:01 +0200 Subject: [PATCH 085/256] Add option to disable tracking --- include/bout/field3d.hxx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index b8ea64a738..99359a4d4f 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -306,6 +306,13 @@ public: /// Save all changes that, are done to the field, to tracking Field3D& enableTracking(const std::string& name, Options& tracking); + /// Disable tracking + Field3D& disableTracking() { + tracking = nullptr; + tracking_state = 0; + return *this; + } + ///////////////////////////////////////////////////////// // Data access From e25884e16d5475ad855771f3c9db2f3370f6f425 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 2 Jul 2024 15:39:11 +0200 Subject: [PATCH 086/256] DEBUG: add debug statements for regionID --- include/bout/field3d.hxx | 6 +++--- src/field/field3d.cxx | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 99359a4d4f..2a37d9e0c8 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -335,9 +335,9 @@ public: /// Use region provided by the default, and if none is set, use the provided one const Region& getValidRegionWithDefault(const std::string& region_name) const; void setRegion(const std::string& region_name); - void resetRegion() { regionID.reset(); }; - void setRegion(size_t id) { regionID = id; }; - void setRegion(std::optional id) { regionID = id; }; + void resetRegion(); + void setRegion(size_t id); + void setRegion(std::optional id); std::optional getRegionID() const { return regionID; }; /// Return a Region reference to use to iterate over the x- and diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index cfbc4ba2fa..7c39c5aff1 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -47,6 +47,26 @@ #include #include + +#include "bout/output.hxx" +#include + +namespace fmt { +template +struct formatter> : fmt::formatter { + + template + auto format(const std::optional& opt, FormatContext& ctx) { + if (opt) { + fmt::formatter::format(opt.value(), ctx); + return ctx.out(); + } + return fmt::format_to(ctx.out(), "NO VALUE"); + } +}; +} // namespace fmt + + /// Constructor Field3D::Field3D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in) : Field(localmesh, location_in, directions_in) { @@ -850,7 +870,22 @@ Field3D::getValidRegionWithDefault(const std::string& region_name) const { void Field3D::setRegion(const std::string& region_name) { regionID = fieldmesh->getRegionID(region_name); -} + output.write("{:p}: set {} {}\n", static_cast(this), regionID, region_name); +} + +void Field3D::resetRegion() { + regionID.reset(); + output.write("{:p}: reset\n", static_cast(this)); +}; +void Field3D::setRegion(size_t id) { + regionID = id; + //output.write("{:p}: set {:d}\n", static_cast(this), regionID); + output.write("{:p}: set {}\n", static_cast(this), regionID); +}; +void Field3D::setRegion(std::optional id) { + regionID = id; + output.write("{:p}: set {}\n", static_cast(this), regionID); +}; Field3D& Field3D::enableTracking(const std::string& name, Options& _tracking) { tracking = &_tracking; From 6b6ee52b74853750b012d643411070bcd4c6c63c Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 5 Jul 2024 11:38:16 +0200 Subject: [PATCH 087/256] Fix: preserve regionID --- src/field/field3d.cxx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 7c39c5aff1..af21f7d784 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -84,7 +84,8 @@ Field3D::Field3D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes direction /// Doesn't copy any data, just create a new reference to the same data (copy on change /// later) Field3D::Field3D(const Field3D& f) - : Field(f), data(f.data), yup_fields(f.yup_fields), ydown_fields(f.ydown_fields) { + : Field(f), data(f.data), yup_fields(f.yup_fields), ydown_fields(f.ydown_fields), + regionID(f.regionID) { TRACE("Field3D(Field3D&)"); @@ -282,6 +283,7 @@ Field3D& Field3D::operator=(const Field3D& rhs) { // Copy parallel slices or delete existing ones. yup_fields = rhs.yup_fields; ydown_fields = rhs.ydown_fields; + regionID = rhs.regionID; // Copy the data and data sizes nx = rhs.nx; @@ -305,6 +307,7 @@ Field3D& Field3D::operator=(Field3D&& rhs) { nx = rhs.nx; ny = rhs.ny; nz = rhs.nz; + regionID = rhs.regionID; data = std::move(rhs.data); @@ -324,6 +327,7 @@ Field3D& Field3D::operator=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); + resetRegion(); setLocation(rhs.getLocation()); @@ -351,6 +355,7 @@ void Field3D::operator=(const FieldPerp& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); + resetRegion(); /// Make sure there's a unique array to copy data into allocate(); @@ -366,6 +371,7 @@ Field3D& Field3D::operator=(const BoutReal val) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); + resetRegion(); allocate(); From 03c98daf5a0b41cd4f68d005711c34be4612811b Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 5 Jul 2024 11:41:02 +0200 Subject: [PATCH 088/256] Fix OOB read/write in boutmesh Previously fortran indexing was used. This resulted in reads and writes one past the last index. Valgrind has complained about this ever since I started, but I only noticed now that it was a genuine bug and not something MPI related. --- src/mesh/impls/bout/boutmesh.cxx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index 59f936d265..da00fbf11e 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -2303,8 +2303,8 @@ int BoutMesh::pack_data(const std::vector& var_list, int xge, int xl ASSERT2(var3d_ref.isAllocated()); for (int jx = xge; jx != xlt; jx++) { for (int jy = yge; jy < ylt; jy++) { - for (int jz = 0; jz < LocalNz; jz++, len++) { - buffer[len] = var3d_ref(jx, jy, jz); + for (int jz = 0; jz < LocalNz; jz++) { + buffer[len++] = var3d_ref(jx, jy, jz); } } } @@ -2313,8 +2313,8 @@ int BoutMesh::pack_data(const std::vector& var_list, int xge, int xl auto& var2d_ref = *dynamic_cast(var); ASSERT2(var2d_ref.isAllocated()); for (int jx = xge; jx != xlt; jx++) { - for (int jy = yge; jy < ylt; jy++, len++) { - buffer[len] = var2d_ref(jx, jy); + for (int jy = yge; jy < ylt; jy++) { + buffer[len++] = var2d_ref(jx, jy); } } } @@ -2335,8 +2335,8 @@ int BoutMesh::unpack_data(const std::vector& var_list, int xge, int auto& var3d_ref = *dynamic_cast(var); for (int jx = xge; jx != xlt; jx++) { for (int jy = yge; jy < ylt; jy++) { - for (int jz = 0; jz < LocalNz; jz++, len++) { - var3d_ref(jx, jy, jz) = buffer[len]; + for (int jz = 0; jz < LocalNz; jz++) { + var3d_ref(jx, jy, jz) = buffer[len++]; } } } @@ -2344,8 +2344,8 @@ int BoutMesh::unpack_data(const std::vector& var_list, int xge, int // 2D variable auto& var2d_ref = *dynamic_cast(var); for (int jx = xge; jx != xlt; jx++) { - for (int jy = yge; jy < ylt; jy++, len++) { - var2d_ref(jx, jy) = buffer[len]; + for (int jy = yge; jy < ylt; jy++) { + var2d_ref(jx, jy) = buffer[len++]; } } } From 8bf48400778856d94a7f127c15248a97a2c2f053 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 5 Jul 2024 11:41:52 +0200 Subject: [PATCH 089/256] Ensure pointer is checked before dereferencing dynamic_cast can return a nullptr, thus we should check. Otherwise gcc raises a warning. --- src/mesh/impls/bout/boutmesh.cxx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index da00fbf11e..83e12f88b8 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -2299,7 +2299,9 @@ int BoutMesh::pack_data(const std::vector& var_list, int xge, int xl for (const auto& var : var_list) { if (var->is3D()) { // 3D variable - auto& var3d_ref = *dynamic_cast(var); + auto var3d_ref_ptr = dynamic_cast(var); + ASSERT0(var3d_ref_ptr != nullptr); + auto& var3d_ref = *var3d_ref_ptr; ASSERT2(var3d_ref.isAllocated()); for (int jx = xge; jx != xlt; jx++) { for (int jy = yge; jy < ylt; jy++) { @@ -2310,7 +2312,9 @@ int BoutMesh::pack_data(const std::vector& var_list, int xge, int xl } } else { // 2D variable - auto& var2d_ref = *dynamic_cast(var); + auto var2d_ref_ptr = dynamic_cast(var); + ASSERT0(var2d_ref_ptr != nullptr); + auto& var2d_ref = *var2d_ref_ptr; ASSERT2(var2d_ref.isAllocated()); for (int jx = xge; jx != xlt; jx++) { for (int jy = yge; jy < ylt; jy++) { From e39774a575714f40b5c98c88c65c3211f118d993 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 5 Jul 2024 11:42:18 +0200 Subject: [PATCH 090/256] ensure we dont mix non-fci BCs with fci --- src/mesh/impls/bout/boutmesh.cxx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index 83e12f88b8..dcd5c8bc85 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -2821,6 +2821,9 @@ void BoutMesh::addBoundaryRegions() { } RangeIterator BoutMesh::iterateBndryLowerInnerY() const { + if (this->isFci()) { + throw BoutException("FCI should never use this iterator"); + } int xs = 0; int xe = LocalNx - 1; @@ -2856,6 +2859,9 @@ RangeIterator BoutMesh::iterateBndryLowerInnerY() const { } RangeIterator BoutMesh::iterateBndryLowerOuterY() const { + if (this->isFci()) { + throw BoutException("FCI should never use this iterator"); + } int xs = 0; int xe = LocalNx - 1; @@ -2890,6 +2896,10 @@ RangeIterator BoutMesh::iterateBndryLowerOuterY() const { } RangeIterator BoutMesh::iterateBndryLowerY() const { + if (this->isFci()) { + throw BoutException("FCI should never use this iterator"); + } + int xs = 0; int xe = LocalNx - 1; if ((DDATA_INDEST >= 0) && (DDATA_XSPLIT > xstart)) { @@ -2919,6 +2929,10 @@ RangeIterator BoutMesh::iterateBndryLowerY() const { } RangeIterator BoutMesh::iterateBndryUpperInnerY() const { + if (this->isFci()) { + throw BoutException("FCI should never use this iterator"); + } + int xs = 0; int xe = LocalNx - 1; @@ -2953,6 +2967,10 @@ RangeIterator BoutMesh::iterateBndryUpperInnerY() const { } RangeIterator BoutMesh::iterateBndryUpperOuterY() const { + if (this->isFci()) { + throw BoutException("FCI should never use this iterator"); + } + int xs = 0; int xe = LocalNx - 1; @@ -2987,6 +3005,10 @@ RangeIterator BoutMesh::iterateBndryUpperOuterY() const { } RangeIterator BoutMesh::iterateBndryUpperY() const { + if (this->isFci()) { + throw BoutException("FCI should never use this iterator"); + } + int xs = 0; int xe = LocalNx - 1; if ((UDATA_INDEST >= 0) && (UDATA_XSPLIT > xstart)) { From 22f493cc0a2a96c79f6e942cc0be63150c8dcb88 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 10:29:19 +0200 Subject: [PATCH 091/256] Fix exception message --- include/bout/field3d.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 2a37d9e0c8..3b98294160 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -250,7 +250,7 @@ public: #if CHECK > 2 if (yup_fields.size() != ydown_fields.size()) { throw BoutException( - "Field3D::splitParallelSlices: forward/backward parallel slices not in sync.\n" + "Field3D::hasParallelSlices: forward/backward parallel slices not in sync.\n" " This is an internal library error"); } #endif From 6c2c82c6cb10180706540f1b129d7f149b38c858 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 10:30:32 +0200 Subject: [PATCH 092/256] Expose tracking Useful for physics module to record additional data if the solver is failing --- include/bout/field3d.hxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 3b98294160..a246fd15ab 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -518,6 +518,8 @@ public: int size() const override { return nx * ny * nz; }; + Options* getTracking() { return tracking; }; + private: /// Array sizes (from fieldmesh). These are valid only if fieldmesh is not null int nx{-1}, ny{-1}, nz{-1}; From c6c259bd9ce2c06813a2e817b3aa40dc82f1061d Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 10:32:17 +0200 Subject: [PATCH 093/256] Add simple interface to store parallel fields Just dumping the parallel slices does in general not work, as then collect discards that, especially if NYPE==ny --- include/bout/options.hxx | 3 +++ src/sys/options.cxx | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/include/bout/options.hxx b/include/bout/options.hxx index 839c847289..e1f5ae68fa 100644 --- a/include/bout/options.hxx +++ b/include/bout/options.hxx @@ -946,6 +946,9 @@ Tensor Options::as>(const Tensor& similar_t /// Convert \p value to string std::string toString(const Options& value); +/// Save the parallel fields +void saveParallel(Options& opt, const std::string name, const Field3D& tosave); + /// Output a stringified \p value to a stream /// /// This is templated to avoid implict casting: anything is diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 49a81cfa88..71339b6089 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -306,6 +306,22 @@ void Options::assign<>(Tensor val, std::string source) { _set_no_check(std::move(val), std::move(source)); } +void saveParallel(Options& opt, const std::string name, const Field3D& tosave){ + ASSERT2(tosave.hasParallelSlices()); + opt[name] = tosave; + for (size_t i0=1 ; i0 <= tosave.numberParallelSlices(); ++i0) { + for (int i: {i0, -i0} ) { + Field3D tmp; + tmp.allocate(); + const auto& fpar = tosave.ynext(i); + for (auto j: fpar.getValidRegionWithDefault("RGN_NO_BOUNDARY")){ + tmp[j.yp(-i)] = fpar[j]; + } + opt[fmt::format("{}_y{:+d}", name, i)] = tmp; + } + } +} + namespace { /// Use FieldFactory to evaluate expression double parseExpression(const Options::ValueType& value, const Options* options, From de99accd5e9785cd370106b759a939208721f06d Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 10:47:16 +0200 Subject: [PATCH 094/256] Add const version for getCoordinates --- include/bout/mesh.hxx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/bout/mesh.hxx b/include/bout/mesh.hxx index a3a36ad933..ccc979987b 100644 --- a/include/bout/mesh.hxx +++ b/include/bout/mesh.hxx @@ -636,6 +636,19 @@ public: return inserted.first->second; } + std::shared_ptr + getCoordinatesConst(const CELL_LOC location = CELL_CENTRE) const { + ASSERT1(location != CELL_DEFAULT); + ASSERT1(location != CELL_VSHIFT); + + auto found = coords_map.find(location); + if (found != coords_map.end()) { + // True branch most common, returns immediately + return found->second; + } + throw BoutException("Coordinates not yet set. Use non-const version!"); + } + /// Returns the non-CELL_CENTRE location /// allowed as a staggered location CELL_LOC getAllowedStaggerLoc(DIRECTION direction) const { From c35d7736d320dc97d4ed9473a45a4d6300159a11 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:45:23 +0200 Subject: [PATCH 095/256] rename to include order --- include/bout/parallel_boundary_region.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 07150a55b3..f8fe3d8ee1 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -82,7 +82,7 @@ public: return f(0, ind()) * (1 + length()) - f(-dir, ind().yp(-dir)) * length(); } - inline BoutReal interpolate_sheath(const Field3D& f) const { + inline BoutReal interpolate_sheath_o1(const Field3D& f) const { return f[ind()] * (1 - length()) + ynext(f) * length(); } From 22bbd27acb64ea71b073a7d0c096105296918b13 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:46:06 +0200 Subject: [PATCH 096/256] Set using value rather then using self Using self is cheaper, but then the parallel slices have parallel fields them self, which causes issues --- src/field/field3d.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index af21f7d784..329ff6898a 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -116,8 +116,8 @@ Field3D::Field3D(const BoutReal val, Mesh* localmesh) : Field3D(localmesh) { if (this->isFci()) { splitParallelSlices(); for (size_t i=0; i Date: Fri, 9 Aug 2024 13:46:32 +0200 Subject: [PATCH 097/256] Also set parallel fields for operator= --- src/field/field3d.cxx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 329ff6898a..7e30bc8bb8 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -370,7 +370,16 @@ Field3D& Field3D::operator=(const BoutReal val) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. +#if BOUT_USE_FCI_AUTOMAGIC + if (isFci() && hasParallelSlices()) { + for (size_t i=0; i Date: Fri, 9 Aug 2024 13:46:44 +0200 Subject: [PATCH 098/256] Set parallel region by default --- src/field/field3d.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 7e30bc8bb8..386d24f376 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -176,7 +176,9 @@ void Field3D::splitParallelSlices() { // Note the fields constructed here will be fully overwritten by the // ParallelTransform, so we don't need a full constructor yup_fields.emplace_back(fieldmesh); + yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", i + 1)); ydown_fields.emplace_back(fieldmesh); + yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", -i - 1)); } } From 61a2521a0363e3a0f3505856c5b18a5f5cd90d35 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:49:21 +0200 Subject: [PATCH 099/256] Set name in operator= --- src/field/field3d.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 386d24f376..0720778e84 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -387,6 +387,7 @@ Field3D& Field3D::operator=(const BoutReal val) { allocate(); BOUT_FOR(i, getRegion("RGN_ALL")) { (*this)[i] = val; } + this->name = "BR"; return *this; } From 27db217cf5566dd9afed7266fc15f13ba1a45bdc Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:49:59 +0200 Subject: [PATCH 100/256] preserve parallel fields more often --- src/field/gen_fieldops.jinja | 24 ++++++ src/field/generated_fieldops.cxx | 136 +++++++++++++++++++------------ 2 files changed, 108 insertions(+), 52 deletions(-) diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index d268790255..88e877c197 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -23,8 +23,30 @@ #endif {% elif lhs == "Field3D" %} {{out.name}}.setRegion({{lhs.name}}.getRegionID()); + {% if rhs == "BoutReal" %} +#if BOUT_USE_FCI_AUTOMAGIC + if ({{lhs.name}}.isFci() and {{lhs.name}}.hasParallelSlices()) { + {{out.name}}.splitParallelSlices(); + for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { + {{out.name}}.yup(i) = {{lhs.name}}.yup(i) {{operator}} {{rhs.name}}; + {{out.name}}.ydown(i) = {{lhs.name}}.ydown(i) {{operator}} {{rhs.name}}; + } + } +#endif + {% endif %} {% elif rhs == "Field3D" %} {{out.name}}.setRegion({{rhs.name}}.getRegionID()); + {% if lhs == "BoutReal" %} +#if BOUT_USE_FCI_AUTOMAGIC + if ({{rhs.name}}.isFci() and {{rhs.name}}.hasParallelSlices()) { + {{out.name}}.splitParallelSlices(); + for (size_t i{0} ; i < {{rhs.name}}.numberParallelSlices() ; ++i) { + {{out.name}}.yup(i) = {{lhs.name}} {{operator}} {{rhs.name}}.yup(i); + {{out.name}}.ydown(i) = {{lhs.name}} {{operator}} {{rhs.name}}.ydown(i); + } + } +#endif + {% endif %} {% endif %} {% endif %} @@ -91,6 +113,7 @@ {% if (lhs == "Field3D") %} // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. + {% if (rhs == "Field3D" or rhs == "BoutReal") %} #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices() {% if rhs == "Field3D" %} and {{rhs.name}}.hasParallelSlices() {% endif %}) { for (size_t i{0} ; i < yup_fields.size() ; ++i) { @@ -99,6 +122,7 @@ } } else #endif + {% endif %} { clearParallelSlices(); } diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 18e857ba92..75d2ede82d 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -44,7 +44,7 @@ Field3D& Field3D::operator*=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -116,7 +116,7 @@ Field3D& Field3D::operator/=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -188,7 +188,7 @@ Field3D& Field3D::operator+=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -260,7 +260,7 @@ Field3D& Field3D::operator-=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices() and rhs.hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -329,17 +329,7 @@ Field3D& Field3D::operator*=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. -#if BOUT_USE_FCI_AUTOMAGIC - if (this->isFci() and this->hasParallelSlices()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) *= rhs; - ydown(i) *= rhs; - } - } else -#endif - { - clearParallelSlices(); - } + { clearParallelSlices(); } checkData(*this); checkData(rhs); @@ -401,17 +391,7 @@ Field3D& Field3D::operator/=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. -#if BOUT_USE_FCI_AUTOMAGIC - if (this->isFci() and this->hasParallelSlices()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) /= rhs; - ydown(i) /= rhs; - } - } else -#endif - { - clearParallelSlices(); - } + { clearParallelSlices(); } checkData(*this); checkData(rhs); @@ -473,17 +453,7 @@ Field3D& Field3D::operator+=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. -#if BOUT_USE_FCI_AUTOMAGIC - if (this->isFci() and this->hasParallelSlices()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) += rhs; - ydown(i) += rhs; - } - } else -#endif - { - clearParallelSlices(); - } + { clearParallelSlices(); } checkData(*this); checkData(rhs); @@ -544,17 +514,7 @@ Field3D& Field3D::operator-=(const Field2D& rhs) { // Delete existing parallel slices. We don't copy parallel slices, so any // that currently exist will be incorrect. -#if BOUT_USE_FCI_AUTOMAGIC - if (this->isFci() and this->hasParallelSlices()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) -= rhs; - ydown(i) -= rhs; - } - } else -#endif - { - clearParallelSlices(); - } + { clearParallelSlices(); } checkData(*this); checkData(rhs); @@ -680,6 +640,15 @@ Field3D operator*(const Field3D& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (lhs.isFci() and lhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) * rhs; + result.ydown(i) = lhs.ydown(i) * rhs; + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] * rhs; @@ -699,7 +668,7 @@ Field3D& Field3D::operator*=(const BoutReal rhs) { if (data.unique()) { // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -739,6 +708,15 @@ Field3D operator/(const Field3D& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (lhs.isFci() and lhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) / rhs; + result.ydown(i) = lhs.ydown(i) / rhs; + } + } +#endif const auto tmp = 1.0 / rhs; BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -759,7 +737,7 @@ Field3D& Field3D::operator/=(const BoutReal rhs) { if (data.unique()) { // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -800,6 +778,15 @@ Field3D operator+(const Field3D& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (lhs.isFci() and lhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) + rhs; + result.ydown(i) = lhs.ydown(i) + rhs; + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] + rhs; @@ -819,7 +806,7 @@ Field3D& Field3D::operator+=(const BoutReal rhs) { if (data.unique()) { // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -859,6 +846,15 @@ Field3D operator-(const Field3D& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (lhs.isFci() and lhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) - rhs; + result.ydown(i) = lhs.ydown(i) - rhs; + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] - rhs; @@ -878,7 +874,7 @@ Field3D& Field3D::operator-=(const BoutReal rhs) { if (data.unique()) { // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. +// that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci() and this->hasParallelSlices()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -2213,6 +2209,15 @@ Field3D operator*(const BoutReal lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (rhs.isFci() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs * rhs.yup(i); + result.ydown(i) = lhs * rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs * rhs[index]; @@ -2233,6 +2238,15 @@ Field3D operator/(const BoutReal lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (rhs.isFci() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs / rhs.yup(i); + result.ydown(i) = lhs / rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs / rhs[index]; @@ -2253,6 +2267,15 @@ Field3D operator+(const BoutReal lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (rhs.isFci() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs + rhs.yup(i); + result.ydown(i) = lhs + rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs + rhs[index]; @@ -2273,6 +2296,15 @@ Field3D operator-(const BoutReal lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); +#if BOUT_USE_FCI_AUTOMAGIC + if (rhs.isFci() and rhs.hasParallelSlices()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs - rhs.yup(i); + result.ydown(i) = lhs - rhs.ydown(i); + } + } +#endif BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs - rhs[index]; From 6b9e86e5ad5beec7c778f6a4b840e9988651039c Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:50:34 +0200 Subject: [PATCH 101/256] Fix: remove broken code BoundaryRegionIter has been delted --- src/mesh/boundary_region.cxx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/mesh/boundary_region.cxx b/src/mesh/boundary_region.cxx index ef4aa13a66..700ef8a91f 100644 --- a/src/mesh/boundary_region.cxx +++ b/src/mesh/boundary_region.cxx @@ -202,6 +202,3 @@ void BoundaryRegionYUp::nextY() { bool BoundaryRegionYUp::isDone() { return (x > xe) || (y >= localmesh->LocalNy); // Return true if gone out of the boundary } - -BoundaryRegionIter BoundaryRegion::begin() { return BoundaryRegionIter(this, false); } -BoundaryRegionIter BoundaryRegion::end() { return BoundaryRegionIter(this, true); } From d3b0d65bad721793790c8dcd37f18fe4ae5f8cef Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:50:41 +0200 Subject: [PATCH 102/256] Add comment --- src/mesh/boundary_standard.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/boundary_standard.cxx b/src/mesh/boundary_standard.cxx index 80c2053f39..c8b3269198 100644 --- a/src/mesh/boundary_standard.cxx +++ b/src/mesh/boundary_standard.cxx @@ -2164,7 +2164,7 @@ void BoundaryNeumann_NonOrthogonal::apply(Field3D& f) { } else { throw BoutException("Unrecognized location"); } - } else { + } else { // loc == CELL_CENTRE for (; !bndry->isDone(); bndry->next1d()) { #if BOUT_USE_METRIC_3D for (int zk = 0; zk < mesh->LocalNz; zk++) { From 63a1f74655a164671e0210d486362a9be3e811f3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:51:22 +0200 Subject: [PATCH 103/256] Communicate automatically in Div_par with automagic --- src/mesh/difops.cxx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/mesh/difops.cxx b/src/mesh/difops.cxx index 2e25dfeedb..5270895e52 100644 --- a/src/mesh/difops.cxx +++ b/src/mesh/difops.cxx @@ -234,7 +234,21 @@ Field3D Div_par(const Field3D& f, const std::string& method, CELL_LOC outloc) { return f.getCoordinates(outloc)->Div_par(f, outloc, method); } -Field3D Div_par(const Field3D& f, const Field3D& v) { +Field3D Div_par(const Field3D& f_in, const Field3D& v_in) { +#if BOUT_USE_FCI_AUTOMAGIC + auto f{f_in}; + auto v{v_in}; + if (!f.hasParallelSlices()) { + f.calcParallelSlices(); + } + if (!v.hasParallelSlices()) { + v.calcParallelSlices(); + } +#else + const auto& f{f_in}; + const auto& v{v_in}; +#endif + ASSERT1_FIELDS_COMPATIBLE(f, v); ASSERT1(f.hasParallelSlices()); ASSERT1(v.hasParallelSlices()); From 8e773f7f7e5a5b516550d47c8313f11b13c435fa Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 11:48:56 +0200 Subject: [PATCH 104/256] Add option to disallow calculating parallel fields Calculating parallel fields for metrics terms does not make sense. Using such parallel fields is very, very likely a bug. --- include/bout/field3d.hxx | 11 +++++++++++ src/field/field3d.cxx | 3 +++ src/mesh/coordinates.cxx | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index a246fd15ab..4eccedd7e3 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -272,23 +272,27 @@ public: /// Return reference to yup field Field3D& yup(std::vector::size_type index = 0) { ASSERT2(index < yup_fields.size()); + ASSERT2(allow_parallel_slices); return yup_fields[index]; } /// Return const reference to yup field const Field3D& yup(std::vector::size_type index = 0) const { ASSERT2(index < yup_fields.size()); + ASSERT2(allow_parallel_slices); return yup_fields[index]; } /// Return reference to ydown field Field3D& ydown(std::vector::size_type index = 0) { ASSERT2(index < ydown_fields.size()); + ASSERT2(allow_parallel_slices); return ydown_fields[index]; } /// Return const reference to ydown field const Field3D& ydown(std::vector::size_type index = 0) const { ASSERT2(index < ydown_fields.size()); + ASSERT2(allow_parallel_slices); return ydown_fields[index]; } @@ -491,6 +495,11 @@ public: friend class Vector2D; Field3D& calcParallelSlices(); + void allowParallelSlices([[maybe_unused]] bool allow){ +#if CHECK > 0 + allow_parallel_slices = allow; +#endif + } void applyBoundary(bool init = false) override; void applyBoundary(BoutReal t); @@ -542,6 +551,8 @@ private: template Options* track(const T& change, std::string operation); Options* track(const BoutReal& change, std::string operation); + bool allow_parallel_slices{true}; + }; // Non-member overloaded operators diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 0720778e84..345e1c227d 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -167,6 +167,7 @@ BOUT_HOST_DEVICE Field3D* Field3D::timeDeriv() { void Field3D::splitParallelSlices() { TRACE("Field3D::splitParallelSlices"); + ASSERT2(allow_parallel_slices); if (hasParallelSlices()) { return; @@ -195,6 +196,7 @@ void Field3D::clearParallelSlices() { const Field3D& Field3D::ynext(int dir) const { #if CHECK > 0 + ASSERT2(allow_parallel_slices); // Asked for more than yguards if (std::abs(dir) > fieldmesh->ystart) { throw BoutException( @@ -393,6 +395,7 @@ Field3D& Field3D::operator=(const BoutReal val) { } Field3D& Field3D::calcParallelSlices() { + ASSERT2(allow_parallel_slices); getCoordinates()->getParallelTransform().calcParallelSlices(*this); #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci()) { diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index e4b75d0032..cbf50afcfe 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -942,6 +942,13 @@ const Field2D& Coordinates::zlength() const { int Coordinates::geometry(bool recalculate_staggered, bool force_interpolate_from_centre) { TRACE("Coordinates::geometry"); + { + std::vector fields{dx, dy, dz, g11, g22, g33, g12, g13, g23, g_11, g_22, g_33, g_12, g_13, + g_23, J}; + for (auto& f: fields) { + f.allowParallelSlices(false); + } + } communicate(dx, dy, dz, g11, g22, g33, g12, g13, g23, g_11, g_22, g_33, g_12, g_13, g_23, J, Bxy); From 84853532ff7078379c798d205b995a917492ebbf Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 11:50:33 +0200 Subject: [PATCH 105/256] Disable metric components that require y-derivatives for fci --- src/mesh/coordinates.cxx | 234 ++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 112 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index cbf50afcfe..b12ca24ee8 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -970,119 +970,129 @@ int Coordinates::geometry(bool recalculate_staggered, checkContravariant(); checkCovariant(); - // Calculate Christoffel symbol terms (18 independent values) - // Note: This calculation is completely general: metric - // tensor can be 2D or 3D. For 2D, all DDZ terms are zero - - G1_11 = 0.5 * g11 * DDX(g_11) + g12 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g13 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G1_22 = g11 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g12 * DDY(g_22) - + g13 * (DDY(g_23) - 0.5 * DDZ(g_22)); - G1_33 = g11 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g12 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g13 * DDZ(g_33); - G1_12 = 0.5 * g11 * DDY(g_11) + 0.5 * g12 * DDX(g_22) - + 0.5 * g13 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G1_13 = 0.5 * g11 * DDZ(g_11) + 0.5 * g12 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - + 0.5 * g13 * DDX(g_33); - G1_23 = 0.5 * g11 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) - + 0.5 * g12 * (DDZ(g_22) + DDY(g_23) - DDY(g_23)) - // + 0.5 *g13*(DDZ(g_32) + DDY(g_33) - DDZ(g_23)); - // which equals - + 0.5 * g13 * DDY(g_33); - - G2_11 = 0.5 * g12 * DDX(g_11) + g22 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g23 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G2_22 = g12 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g22 * DDY(g_22) - + g23 * (DDY(g23) - 0.5 * DDZ(g_22)); - G2_33 = g12 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g22 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g23 * DDZ(g_33); - G2_12 = 0.5 * g12 * DDY(g_11) + 0.5 * g22 * DDX(g_22) - + 0.5 * g23 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G2_13 = - // 0.5 *g21*(DDZ(g_11) + DDX(g_13) - DDX(g_13)) - // which equals - 0.5 * g12 * (DDZ(g_11) + DDX(g_13) - DDX(g_13)) - // + 0.5 *g22*(DDZ(g_21) + DDX(g_23) - DDY(g_13)) - // which equals - + 0.5 * g22 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - // + 0.5 *g23*(DDZ(g_31) + DDX(g_33) - DDZ(g_13)); - // which equals - + 0.5 * g23 * DDX(g_33); - G2_23 = 0.5 * g12 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g22 * DDZ(g_22) - + 0.5 * g23 * DDY(g_33); - - G3_11 = 0.5 * g13 * DDX(g_11) + g23 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g33 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G3_22 = g13 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g23 * DDY(g_22) - + g33 * (DDY(g_23) - 0.5 * DDZ(g_22)); - G3_33 = g13 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g23 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g33 * DDZ(g_33); - G3_12 = - // 0.5 *g31*(DDY(g_11) + DDX(g_12) - DDX(g_12)) - // which equals to - 0.5 * g13 * DDY(g_11) - // + 0.5 *g32*(DDY(g_21) + DDX(g_22) - DDY(g_12)) - // which equals to - + 0.5 * g23 * DDX(g_22) - //+ 0.5 *g33*(DDY(g_31) + DDX(g_32) - DDZ(g_12)); - // which equals to - + 0.5 * g33 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G3_13 = 0.5 * g13 * DDZ(g_11) + 0.5 * g23 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - + 0.5 * g33 * DDX(g_33); - G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) - + 0.5 * g33 * DDY(g_33); - - auto tmp = J * g12; - communicate(tmp); - G1 = (DDX(J * g11) + DDY(tmp) + DDZ(J * g13)) / J; - tmp = J * g22; - communicate(tmp); - G2 = (DDX(J * g12) + DDY(tmp) + DDZ(J * g23)) / J; - tmp = J * g23; - communicate(tmp); - G3 = (DDX(J * g13) + DDY(tmp) + DDZ(J * g33)) / J; - - // Communicate christoffel symbol terms - output_progress.write("\tCommunicating connection terms\n"); - - communicate(G1_11, G1_22, G1_33, G1_12, G1_13, G1_23, G2_11, G2_22, G2_33, G2_12, G2_13, - G2_23, G3_11, G3_22, G3_33, G3_12, G3_13, G3_23, G1, G2, G3); - - // Set boundary guard cells of Christoffel symbol terms - // Ideally, when location is staggered, we would set the upper/outer boundary point - // correctly rather than by extrapolating here: e.g. if location==CELL_YLOW and we are - // at the upper y-boundary the x- and z-derivatives at yend+1 at the boundary can be - // calculated because the guard cells are available, while the y-derivative could be - // calculated from the CELL_CENTRE metric components (which have guard cells available - // past the boundary location). This would avoid the problem that the y-boundary on the - // CELL_YLOW grid is at a 'guard cell' location (yend+1). - // However, the above would require lots of special handling, so just extrapolate for - // now. - G1_11 = interpolateAndExtrapolate(G1_11, location, true, true, true, transform.get()); - G1_22 = interpolateAndExtrapolate(G1_22, location, true, true, true, transform.get()); - G1_33 = interpolateAndExtrapolate(G1_33, location, true, true, true, transform.get()); - G1_12 = interpolateAndExtrapolate(G1_12, location, true, true, true, transform.get()); - G1_13 = interpolateAndExtrapolate(G1_13, location, true, true, true, transform.get()); - G1_23 = interpolateAndExtrapolate(G1_23, location, true, true, true, transform.get()); - - G2_11 = interpolateAndExtrapolate(G2_11, location, true, true, true, transform.get()); - G2_22 = interpolateAndExtrapolate(G2_22, location, true, true, true, transform.get()); - G2_33 = interpolateAndExtrapolate(G2_33, location, true, true, true, transform.get()); - G2_12 = interpolateAndExtrapolate(G2_12, location, true, true, true, transform.get()); - G2_13 = interpolateAndExtrapolate(G2_13, location, true, true, true, transform.get()); - G2_23 = interpolateAndExtrapolate(G2_23, location, true, true, true, transform.get()); - - G3_11 = interpolateAndExtrapolate(G3_11, location, true, true, true, transform.get()); - G3_22 = interpolateAndExtrapolate(G3_22, location, true, true, true, transform.get()); - G3_33 = interpolateAndExtrapolate(G3_33, location, true, true, true, transform.get()); - G3_12 = interpolateAndExtrapolate(G3_12, location, true, true, true, transform.get()); - G3_13 = interpolateAndExtrapolate(G3_13, location, true, true, true, transform.get()); - G3_23 = interpolateAndExtrapolate(G3_23, location, true, true, true, transform.get()); - - G1 = interpolateAndExtrapolate(G1, location, true, true, true, transform.get()); - G2 = interpolateAndExtrapolate(G2, location, true, true, true, transform.get()); - G3 = interpolateAndExtrapolate(G3, location, true, true, true, transform.get()); + if (g_11.isFci()) { + // for FCI the y derivatives of metric components is meaningless. + G1_11 = G1_22 = G1_33 = G1_12 = G1_13 = G1_23 = + G2_11 = G2_22 = G2_33 = G2_12 = G2_13 = G2_23 = + + G3_11 = G3_22 = G3_33 = G3_12 = G3_13 = G3_23 = + + G1 = G2 = G3 = BoutNaN; + } else { + // Calculate Christoffel symbol terms (18 independent values) + // Note: This calculation is completely general: metric + // tensor can be 2D or 3D. For 2D, all DDZ terms are zero + + G1_11 = 0.5 * g11 * DDX(g_11) + g12 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g13 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G1_22 = g11 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g12 * DDY(g_22) + + g13 * (DDY(g_23) - 0.5 * DDZ(g_22)); + G1_33 = g11 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g12 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g13 * DDZ(g_33); + G1_12 = 0.5 * g11 * DDY(g_11) + 0.5 * g12 * DDX(g_22) + + 0.5 * g13 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G1_13 = 0.5 * g11 * DDZ(g_11) + 0.5 * g12 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + + 0.5 * g13 * DDX(g_33); + G1_23 = 0.5 * g11 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + + 0.5 * g12 * (DDZ(g_22) + DDY(g_23) - DDY(g_23)) + // + 0.5 *g13*(DDZ(g_32) + DDY(g_33) - DDZ(g_23)); + // which equals + + 0.5 * g13 * DDY(g_33); + + G2_11 = 0.5 * g12 * DDX(g_11) + g22 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g23 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G2_22 = g12 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g22 * DDY(g_22) + + g23 * (DDY(g23) - 0.5 * DDZ(g_22)); + G2_33 = g12 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g22 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g23 * DDZ(g_33); + G2_12 = 0.5 * g12 * DDY(g_11) + 0.5 * g22 * DDX(g_22) + + 0.5 * g23 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G2_13 = + // 0.5 *g21*(DDZ(g_11) + DDX(g_13) - DDX(g_13)) + // which equals + 0.5 * g12 * (DDZ(g_11) + DDX(g_13) - DDX(g_13)) + // + 0.5 *g22*(DDZ(g_21) + DDX(g_23) - DDY(g_13)) + // which equals + + 0.5 * g22 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + // + 0.5 *g23*(DDZ(g_31) + DDX(g_33) - DDZ(g_13)); + // which equals + + 0.5 * g23 * DDX(g_33); + G2_23 = 0.5 * g12 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g22 * DDZ(g_22) + + 0.5 * g23 * DDY(g_33); + + G3_11 = 0.5 * g13 * DDX(g_11) + g23 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g33 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G3_22 = g13 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g23 * DDY(g_22) + + g33 * (DDY(g_23) - 0.5 * DDZ(g_22)); + G3_33 = g13 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g23 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g33 * DDZ(g_33); + G3_12 = + // 0.5 *g31*(DDY(g_11) + DDX(g_12) - DDX(g_12)) + // which equals to + 0.5 * g13 * DDY(g_11) + // + 0.5 *g32*(DDY(g_21) + DDX(g_22) - DDY(g_12)) + // which equals to + + 0.5 * g23 * DDX(g_22) + //+ 0.5 *g33*(DDY(g_31) + DDX(g_32) - DDZ(g_12)); + // which equals to + + 0.5 * g33 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G3_13 = 0.5 * g13 * DDZ(g_11) + 0.5 * g23 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + + 0.5 * g33 * DDX(g_33); + G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) + + 0.5 * g33 * DDY(g_33); + + auto tmp = J * g12; + communicate(tmp); + G1 = (DDX(J * g11) + DDY(tmp) + DDZ(J * g13)) / J; + tmp = J * g22; + communicate(tmp); + G2 = (DDX(J * g12) + DDY(tmp) + DDZ(J * g23)) / J; + tmp = J * g23; + communicate(tmp); + G3 = (DDX(J * g13) + DDY(tmp) + DDZ(J * g33)) / J; + + // Communicate christoffel symbol terms + output_progress.write("\tCommunicating connection terms\n"); + + communicate(G1_11, G1_22, G1_33, G1_12, G1_13, G1_23, G2_11, G2_22, G2_33, G2_12, + G2_13, G2_23, G3_11, G3_22, G3_33, G3_12, G3_13, G3_23, G1, G2, G3); + + // Set boundary guard cells of Christoffel symbol terms + // Ideally, when location is staggered, we would set the upper/outer boundary point + // correctly rather than by extrapolating here: e.g. if location==CELL_YLOW and we are + // at the upper y-boundary the x- and z-derivatives at yend+1 at the boundary can be + // calculated because the guard cells are available, while the y-derivative could be + // calculated from the CELL_CENTRE metric components (which have guard cells available + // past the boundary location). This would avoid the problem that the y-boundary on the + // CELL_YLOW grid is at a 'guard cell' location (yend+1). + // However, the above would require lots of special handling, so just extrapolate for + // now. + G1_11 = interpolateAndExtrapolate(G1_11, location, true, true, true, transform.get()); + G1_22 = interpolateAndExtrapolate(G1_22, location, true, true, true, transform.get()); + G1_33 = interpolateAndExtrapolate(G1_33, location, true, true, true, transform.get()); + G1_12 = interpolateAndExtrapolate(G1_12, location, true, true, true, transform.get()); + G1_13 = interpolateAndExtrapolate(G1_13, location, true, true, true, transform.get()); + G1_23 = interpolateAndExtrapolate(G1_23, location, true, true, true, transform.get()); + + G2_11 = interpolateAndExtrapolate(G2_11, location, true, true, true, transform.get()); + G2_22 = interpolateAndExtrapolate(G2_22, location, true, true, true, transform.get()); + G2_33 = interpolateAndExtrapolate(G2_33, location, true, true, true, transform.get()); + G2_12 = interpolateAndExtrapolate(G2_12, location, true, true, true, transform.get()); + G2_13 = interpolateAndExtrapolate(G2_13, location, true, true, true, transform.get()); + G2_23 = interpolateAndExtrapolate(G2_23, location, true, true, true, transform.get()); + + G3_11 = interpolateAndExtrapolate(G3_11, location, true, true, true, transform.get()); + G3_22 = interpolateAndExtrapolate(G3_22, location, true, true, true, transform.get()); + G3_33 = interpolateAndExtrapolate(G3_33, location, true, true, true, transform.get()); + G3_12 = interpolateAndExtrapolate(G3_12, location, true, true, true, transform.get()); + G3_13 = interpolateAndExtrapolate(G3_13, location, true, true, true, transform.get()); + G3_23 = interpolateAndExtrapolate(G3_23, location, true, true, true, transform.get()); + + G1 = interpolateAndExtrapolate(G1, location, true, true, true, transform.get()); + G2 = interpolateAndExtrapolate(G2, location, true, true, true, transform.get()); + G3 = interpolateAndExtrapolate(G3, location, true, true, true, transform.get()); + } ////////////////////////////////////////////////////// /// Non-uniform meshes. Need to use DDX, DDY From 484a4731693f01f2d43c7a5fb8ddc660197cc267 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:51:57 +0200 Subject: [PATCH 106/256] Communicate Bxy if needed --- src/mesh/coordinates.cxx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index b12ca24ee8..af04cb0781 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1611,6 +1611,12 @@ Field3D Coordinates::Div_par(const Field3D& f, CELL_LOC outloc, return Bxy * Grad_par(f / Bxy_floc, outloc, method); } +#if BOUT_USE_FCI_AUTOMAGIC + if (!Bxy_floc.hasParallelSlices()) { + localmesh->communicate(Bxy_floc); + Bxy_floc.applyParallelBoundary("parallel_neumann_o2"); + } +#endif // Need to modify yup and ydown fields Field3D f_B = f / Bxy_floc; f_B.splitParallelSlices(); From a45f681351028a77f4e9185e0f1274f5c3253e3b Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 11:53:26 +0200 Subject: [PATCH 107/256] Clarify which div_par has been used --- src/mesh/coordinates.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index af04cb0781..f728189d82 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1624,7 +1624,7 @@ Field3D Coordinates::Div_par(const Field3D& f, CELL_LOC outloc, f_B.yup(i) = f.yup(i) / Bxy_floc.yup(i); f_B.ydown(i) = f.ydown(i) / Bxy_floc.ydown(i); } - return setName(Bxy * Grad_par(f_B, outloc, method), "Div_par({:s})", f.name); + return setName(Bxy * Grad_par(f_B, outloc, method), "C:Div_par({:s})", f.name); } ///////////////////////////////////////////////////////// From 1a265153ff1fa895b7f1447a1a0e0c06a32cef72 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 11:56:16 +0200 Subject: [PATCH 108/256] Avoid using FV in y direction with FCI --- src/mesh/fv_ops.cxx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/mesh/fv_ops.cxx b/src/mesh/fv_ops.cxx index 0a5d5f9624..ddf2715a71 100644 --- a/src/mesh/fv_ops.cxx +++ b/src/mesh/fv_ops.cxx @@ -63,17 +63,8 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { } } - const bool fci = f.hasParallelSlices() && a.hasParallelSlices(); - - if (bout::build::use_metric_3d and fci) { - // 3D Metric, need yup/ydown fields. - // Requires previous communication of metrics - // -- should insert communication here? - if (!coord->g23.hasParallelSlices() || !coord->g_23.hasParallelSlices() - || !coord->dy.hasParallelSlices() || !coord->dz.hasParallelSlices() - || !coord->Bxy.hasParallelSlices() || !coord->J.hasParallelSlices()) { - throw BoutException("metrics have no yup/down: Maybe communicate in init?"); - } + if (a.isFci()) + throw BoutException("FCI does not work with FV methods in y direction"); } // Y and Z fluxes require Y derivatives @@ -183,6 +174,10 @@ const Field3D Div_par_K_Grad_par(const Field3D& Kin, const Field3D& fin, bool bndry_flux) { TRACE("FV::Div_par_K_Grad_par"); + if (Kin.isFci()) { + return ::Div_par_K_Grad_par(Kin, fin); + } + ASSERT2(Kin.getLocation() == fin.getLocation()); Mesh* mesh = Kin.getMesh(); From 167ba2e3f5e008801675598fd8ce5d45178a0546 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:01:43 +0200 Subject: [PATCH 109/256] fixup fv_ops --- src/mesh/fv_ops.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/fv_ops.cxx b/src/mesh/fv_ops.cxx index ddf2715a71..02e059b571 100644 --- a/src/mesh/fv_ops.cxx +++ b/src/mesh/fv_ops.cxx @@ -77,7 +77,7 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { const auto a_slice = makeslices(fci, a); // Only in 3D case with FCI do the metrics have parallel slices - const bool metric_fci = fci and bout::build::use_metric_3d; + const bool metric_fci = a.isFci() and bout::build::use_metric_3d; const auto g23 = makeslices(metric_fci, coord->g23); const auto g_23 = makeslices(metric_fci, coord->g_23); const auto J = makeslices(metric_fci, coord->J); From df4299838f61375c62bf25f6db0401fc41f9a09d Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 13:16:38 +0200 Subject: [PATCH 110/256] fixup again --- src/mesh/fv_ops.cxx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/mesh/fv_ops.cxx b/src/mesh/fv_ops.cxx index 02e059b571..1646e07e1d 100644 --- a/src/mesh/fv_ops.cxx +++ b/src/mesh/fv_ops.cxx @@ -63,7 +63,7 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { } } - if (a.isFci()) + if (a.isFci()) { throw BoutException("FCI does not work with FV methods in y direction"); } @@ -73,11 +73,11 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { // Values on this y slice (centre). // This is needed because toFieldAligned may modify the field - const auto f_slice = makeslices(fci, f); - const auto a_slice = makeslices(fci, a); + const auto f_slice = makeslices(false, f); + const auto a_slice = makeslices(false, a); // Only in 3D case with FCI do the metrics have parallel slices - const bool metric_fci = a.isFci() and bout::build::use_metric_3d; + const bool metric_fci = false; const auto g23 = makeslices(metric_fci, coord->g23); const auto g_23 = makeslices(metric_fci, coord->g_23); const auto J = makeslices(metric_fci, coord->J); @@ -87,9 +87,7 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { // Result of the Y and Z fluxes Field3D yzresult(0.0, mesh); - if (!fci) { - yzresult.setDirectionY(YDirectionType::Aligned); - } + yzresult.setDirectionY(YDirectionType::Aligned); // Y flux @@ -160,12 +158,7 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { } } - // Check if we need to transform back - if (fci) { - result += yzresult; - } else { - result += fromFieldAligned(yzresult); - } + result += fromFieldAligned(yzresult); return result; } From b53d27d817ea1ce509e0d30cfc7c2838d7cbfc18 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 11:56:57 +0200 Subject: [PATCH 111/256] Always set region for hermite_spline_xz --- src/mesh/interpolation/hermite_spline_xz.cxx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 165d387d66..69df6d8906 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -346,6 +346,8 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; + const auto region2 = fmt::format("RGN_YPAR_{:+d}", y_offset); + #if USE_NEW_WEIGHTS #ifdef HS_USE_PETSC BoutReal* ptr; @@ -355,7 +357,6 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region VecRestoreArray(rhs, &ptr); MatMult(petscWeights, rhs, result); VecGetArrayRead(result, &cptr); - const auto region2 = y_offset == 0 ? region : fmt::format("RGN_YPAR_{:+d}", y_offset); BOUT_FOR(i, f.getRegion(region2)) { f_interp[i] = cptr[int(i)]; ASSERT2(std::isfinite(cptr[int(i)])); @@ -375,11 +376,10 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region } } #endif - return f_interp; #else // Derivatives are used for tension and need to be on dimensionless // coordinates - const auto region2 = fmt::format("RGN_YPAR_{:+d}", y_offset); + // f has been communcated, and thus we can assume that the x-boundaries are // also valid in the y-boundary. Thus the differentiated field needs no // extra comms. @@ -418,8 +418,10 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region ASSERT2(std::isfinite(f_interp[iyp]) || i.x() < localmesh->xstart || i.x() > localmesh->xend); } - return f_interp; #endif + f_interp.setRegion(region2); + ASSERT2(f_interp.getRegionID()); + return f_interp; } Field3D XZHermiteSpline::interpolate(const Field3D& f, const Field3D& delta_x, From 43669c88020dd11eedd9b10b1ed300d091f42621 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 11:59:36 +0200 Subject: [PATCH 112/256] Remove debugging code --- src/field/field3d.cxx | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 345e1c227d..cdcc2af261 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -47,26 +47,6 @@ #include #include - -#include "bout/output.hxx" -#include - -namespace fmt { -template -struct formatter> : fmt::formatter { - - template - auto format(const std::optional& opt, FormatContext& ctx) { - if (opt) { - fmt::formatter::format(opt.value(), ctx); - return ctx.out(); - } - return fmt::format_to(ctx.out(), "NO VALUE"); - } -}; -} // namespace fmt - - /// Constructor Field3D::Field3D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in) : Field(localmesh, location_in, directions_in) { @@ -372,8 +352,6 @@ Field3D& Field3D::operator=(const BoutReal val) { TRACE("Field3D = BoutReal"); track(val, "operator="); - // Delete existing parallel slices. We don't copy parallel slices, so any - // that currently exist will be incorrect. #if BOUT_USE_FCI_AUTOMAGIC if (isFci() && hasParallelSlices()) { for (size_t i=0; igetRegionID(region_name); - output.write("{:p}: set {} {}\n", static_cast(this), regionID, region_name); } void Field3D::resetRegion() { regionID.reset(); - output.write("{:p}: reset\n", static_cast(this)); }; void Field3D::setRegion(size_t id) { regionID = id; - //output.write("{:p}: set {:d}\n", static_cast(this), regionID); - output.write("{:p}: set {}\n", static_cast(this), regionID); }; void Field3D::setRegion(std::optional id) { regionID = id; - output.write("{:p}: set {}\n", static_cast(this), regionID); }; Field3D& Field3D::enableTracking(const std::string& name, Options& _tracking) { From b600cdaa9b57768a7e0c94f265902830450cbdf4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 12:00:21 +0200 Subject: [PATCH 113/256] Allow dumping at 0 --- src/solver/impls/euler/euler.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index 45ba5ccdbf..f43f1e4c29 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -144,7 +144,8 @@ void EulerSolver::take_step(BoutReal curtime, BoutReal dt, Array& star Array& result) { load_vars(std::begin(start)); - const bool dump_now = dump_at_time > 0 && std::abs(dump_at_time - curtime) < dt; + const bool dump_now = + (dump_at_time >= 0 && std::abs(dump_at_time - curtime) < dt) || dump_at_time < -3; std::unique_ptr debug_ptr; if (dump_now) { debug_ptr = std::make_unique(); From 1ce3f2af05e9b7b91bce2dc3b1a8c7b002cc84b4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 12:00:41 +0200 Subject: [PATCH 114/256] Dump variables before rhs() --- src/solver/impls/euler/euler.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index f43f1e4c29..4d5ed08cc2 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -153,6 +153,8 @@ void EulerSolver::take_step(BoutReal curtime, BoutReal dt, Array& star for (auto& f : f3d) { f.F_var->enableTracking(fmt::format("ddt_{:s}", f.name), debug); setName(*f.var, f.name); + debug[fmt::format("pre_{:s}", f.name)] = *f.var; + f.var->allocate(); } } From 6bc5010e62f0d5e34f9203d8c888655200b7fbd0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 12:01:05 +0200 Subject: [PATCH 115/256] Ensure mesh is either valid of nullptr --- src/solver/impls/euler/euler.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index 4d5ed08cc2..788aae70ed 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -161,7 +161,7 @@ void EulerSolver::take_step(BoutReal curtime, BoutReal dt, Array& star run_rhs(curtime); if (dump_now) { Options& debug = *debug_ptr; - Mesh* mesh; + Mesh* mesh{nullptr}; for (auto& f : f3d) { debug[f.name] = *f.var; mesh = f.var->getMesh(); From 3831c37e6e02e81b62480702efff82fdb4493116 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 12:01:16 +0200 Subject: [PATCH 116/256] Also dump parallel fields --- src/solver/impls/euler/euler.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index 788aae70ed..31100c5e20 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -163,7 +163,7 @@ void EulerSolver::take_step(BoutReal curtime, BoutReal dt, Array& star Options& debug = *debug_ptr; Mesh* mesh{nullptr}; for (auto& f : f3d) { - debug[f.name] = *f.var; + saveParallel(debug, f.name, *f.var); mesh = f.var->getMesh(); } From 35a2c4f3c7258d4d92472769e49fe13b046b367d Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 12:01:56 +0200 Subject: [PATCH 117/256] Allow dumping several times --- src/solver/impls/euler/euler.cxx | 9 ++++++--- src/solver/impls/euler/euler.hxx | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index 31100c5e20..dd091ae808 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -172,9 +172,12 @@ void EulerSolver::take_step(BoutReal curtime, BoutReal dt, Array& star debug["BOUT_VERSION"].force(bout::version::as_double); } - const std::string outname = fmt::format( - "{}/BOUT.debug.{}.nc", - Options::root()["datadir"].withDefault("data"), BoutComm::rank()); + const std::string outnumber = + dump_at_time < -3 ? fmt::format(".{}", debug_counter++) : ""; + const std::string outname = + fmt::format("{}/BOUT.debug{}.{}.nc", + Options::root()["datadir"].withDefault("data"), + outnumber, BoutComm::rank()); bout::OptionsIO::create(outname)->write(debug); MPI_Barrier(BoutComm::get()); diff --git a/src/solver/impls/euler/euler.hxx b/src/solver/impls/euler/euler.hxx index 4b6dc60a62..fc9b7f53bb 100644 --- a/src/solver/impls/euler/euler.hxx +++ b/src/solver/impls/euler/euler.hxx @@ -66,6 +66,7 @@ private: Array& result); BoutReal dump_at_time{-1.}; + int debug_counter{0}; }; #endif // BOUT_KARNIADAKIS_SOLVER_H From 3e5bf8cf55830e57713de6f2cb967afedfc128c9 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 12:02:26 +0200 Subject: [PATCH 118/256] Stop debugging after dump has been written --- src/solver/impls/euler/euler.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index dd091ae808..5477b5760b 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -181,6 +181,9 @@ void EulerSolver::take_step(BoutReal curtime, BoutReal dt, Array& star bout::OptionsIO::create(outname)->write(debug); MPI_Barrier(BoutComm::get()); + for (auto& f : f3d) { + f.F_var->disableTracking(); + } } save_derivs(std::begin(result)); From a78350fa323b57dd78aecbfeb997623758133802 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 9 Aug 2024 12:02:43 +0200 Subject: [PATCH 119/256] Dump also parallel fields by default --- src/solver/impls/pvode/pvode.cxx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index 9dce5d357f..66344f7cde 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -358,8 +358,8 @@ BoutReal PvodeSolver::run(BoutReal tout) { // Check return flag if (flag != SUCCESS) { output_error.write("ERROR CVODE step failed, flag = {:d}\n", flag); - CVodeMemRec* cv_mem = (CVodeMem)cvode_mem; if (debug_on_failure) { + CVodeMemRec* cv_mem = (CVodeMem)cvode_mem; if (f2d.empty() and v2d.empty() and v3d.empty()) { Options debug{}; using namespace std::string_literals; @@ -388,6 +388,9 @@ BoutReal PvodeSolver::run(BoutReal tout) { for (auto& f : f3d) { debug[f.name] = *f.var; + if (f.var->hasParallelSlices()) { + saveParallel(debug, f.name, *f.var); + } } if (mesh != nullptr) { From f9438800f45a3948244687d9ed3b9f9ec9427d80 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Sep 2024 09:03:35 +0200 Subject: [PATCH 120/256] Set name for Field functions --- include/bout/field.hxx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index dd32c42a63..0898b716c9 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -528,15 +528,16 @@ T pow(BoutReal lhs, const T& rhs, const std::string& rgn = "RGN_ALL") { #ifdef FIELD_FUNC #error This macro has already been defined #else -#define FIELD_FUNC(name, func) \ +#define FIELD_FUNC(_name, func) \ template > \ - inline T name(const T& f, const std::string& rgn = "RGN_ALL") { \ + inline T _name(const T& f, const std::string& rgn = "RGN_ALL") { \ AUTO_TRACE(); \ /* Check if the input is allocated */ \ checkData(f); \ /* Define and allocate the output result */ \ T result{emptyFrom(f)}; \ BOUT_FOR(d, result.getRegion(rgn)) { result[d] = func(f[d]); } \ + result.name = std::string(#_name "(") + f.name + std::string(")"); \ checkData(result); \ return result; \ } From a128be584c9a72a27367efa03a4ae63a2d70f30e Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Sep 2024 09:05:04 +0200 Subject: [PATCH 121/256] Fix code path without FCI automagic --- include/bout/index_derivs_interface.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 86dd4c9287..2c2c21d6cf 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -207,7 +207,7 @@ T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D #if BOUT_USE_FCI_AUTOMAGIC f_tmp.calcParallelSlices(); #else - raise BoutException("parallel slices needed for parallel derivatives. Make sure to communicate and apply parallel boundary conditions before calling derivative"); + throw BoutException("parallel slices needed for parallel derivatives. Make sure to communicate and apply parallel boundary conditions before calling derivative"); #endif } return standardDerivative(f_tmp, outloc, From 653d8360eddddd3526407e399350487db5117fcf Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Sep 2024 09:05:41 +0200 Subject: [PATCH 122/256] Only set region of parallel fields for FCI --- src/field/field3d.cxx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index cdcc2af261..f45cfdcb61 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -157,9 +157,11 @@ void Field3D::splitParallelSlices() { // Note the fields constructed here will be fully overwritten by the // ParallelTransform, so we don't need a full constructor yup_fields.emplace_back(fieldmesh); - yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", i + 1)); ydown_fields.emplace_back(fieldmesh); - yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", -i - 1)); + if (isFci()) { + yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", i + 1)); + yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", -i - 1)); + } } } From 34c3f8f6bfd6cf1c50e967ea2d65fc6505f48242 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Sep 2024 09:08:21 +0200 Subject: [PATCH 123/256] Ensure field to be saved is allocated Storing parallel slices needs them to be to exist. If some field is stored that is not allocated, that will throw an error if the field is stored, but at that point it is going to be difficult to figure out where it came from. --- src/sys/options.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 71339b6089..c1f031be24 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -307,7 +307,7 @@ void Options::assign<>(Tensor val, std::string source) { } void saveParallel(Options& opt, const std::string name, const Field3D& tosave){ - ASSERT2(tosave.hasParallelSlices()); + ASSERT0(tosave.isAllocated()); opt[name] = tosave; for (size_t i0=1 ; i0 <= tosave.numberParallelSlices(); ++i0) { for (int i: {i0, -i0} ) { From b37ef0ebd160f0b90308583a6145d61cd875f8d3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Sep 2024 10:17:51 +0200 Subject: [PATCH 124/256] Output model vars to debug file --- include/bout/physicsmodel.hxx | 2 ++ include/bout/solver.hxx | 2 ++ src/solver/impls/pvode/pvode.cxx | 1 + src/solver/solver.cxx | 4 ++++ 4 files changed, 9 insertions(+) diff --git a/include/bout/physicsmodel.hxx b/include/bout/physicsmodel.hxx index 9fa25d8b0f..fa113670ba 100644 --- a/include/bout/physicsmodel.hxx +++ b/include/bout/physicsmodel.hxx @@ -270,8 +270,10 @@ protected: virtual int rhs(BoutReal UNUSED(t)) { return 1; } virtual int rhs(BoutReal t, bool UNUSED(linear)) { return rhs(t); } +public: /// Output additional variables other than the evolving variables virtual void outputVars(Options& options); +protected: /// Add additional variables other than the evolving variables to the restart files virtual void restartVars(Options& options); diff --git a/include/bout/solver.hxx b/include/bout/solver.hxx index 896ce62965..ea34feb2d3 100644 --- a/include/bout/solver.hxx +++ b/include/bout/solver.hxx @@ -321,6 +321,8 @@ public: /// @param[in] save_repeat If true, add variables with time dimension virtual void outputVars(Options& output_options, bool save_repeat = true); + void modelOutputVars(Options& output_options); + /// Copy evolving variables out of \p options virtual void readEvolvingVariablesFromOptions(Options& options); diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index 66344f7cde..5c41dbf93b 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -385,6 +385,7 @@ BoutReal PvodeSolver::run(BoutReal tout) { setName(*f.var, f.name); } run_rhs(simtime); + modelOutputVars(debug); for (auto& f : f3d) { debug[f.name] = *f.var; diff --git a/src/solver/solver.cxx b/src/solver/solver.cxx index 1b7ec1fd74..7c0b6247dc 100644 --- a/src/solver/solver.cxx +++ b/src/solver/solver.cxx @@ -673,6 +673,10 @@ int Solver::init() { return 0; } +void Solver::modelOutputVars(Options& output_options) { + model->outputVars(output_options); +} + void Solver::outputVars(Options& output_options, bool save_repeat) { Timer time("io"); output_options["tt"].force(simtime, "Solver"); From 761bfc41b60ce0edd72a972baf6ddda8e0bdfb43 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Sep 2024 10:18:27 +0200 Subject: [PATCH 125/256] Update PETSc download url --- bin/bout-build-deps.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/bout-build-deps.sh b/bin/bout-build-deps.sh index 19e3b2a0d3..6e53ecc0e8 100755 --- a/bin/bout-build-deps.sh +++ b/bin/bout-build-deps.sh @@ -10,7 +10,7 @@ NCVER=${NCVER:-4.7.4} NCCXXVER=${NCCXXVER:-4.3.1} FFTWVER=${FFTWVER:-3.3.9} SUNVER=${SUNVER:-5.7.0} -PETSCVER=${PETSCVER:-3.15.0} +PETSCVER=${PETSCVER:-3.21.4} HDF5FLAGS=${HDF5FLAGS:-} @@ -147,7 +147,7 @@ petsc() { test -z $PETSC_DIR || error "\$PETSC_DIR is set ($PETSC_DIR) - please unset" test -z $PETSC_ARCH || error "\$PETSC_ARCH is set ($PETSC_ARCH) - please unset" cd $BUILD - wget -c https://ftp.mcs.anl.gov/pub/petsc/release-snapshots/petsc-$PETSCVER.tar.gz || : + wget -c https://web.cels.anl.gov/projects/petsc/download/release-snapshots/petsc-$PETSCVER.tar.gz || : tar -xf petsc-$PETSCVER.tar.gz cd petsc-$PETSCVER unset PETSC_DIR From 805b4c1269c9d47bed6945d10966119d8e8c315c Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 27 Sep 2024 11:05:33 +0200 Subject: [PATCH 126/256] Add dummy functions for FieldPerp --- include/bout/fieldperp.hxx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/include/bout/fieldperp.hxx b/include/bout/fieldperp.hxx index 6995308dbe..ad069f0d01 100644 --- a/include/bout/fieldperp.hxx +++ b/include/bout/fieldperp.hxx @@ -157,6 +157,25 @@ public: return *this; } + /// Dummy functions to increase portability + bool hasParallelSlices() const { return true; } + void calcParallelSlices() const {} + void clearParallelSlices() {} + int numberParallelSlices() { return 0; } + + FieldPerp& yup(std::vector::size_type UNUSED(index) = 0) { return *this; } + const FieldPerp& yup(std::vector::size_type UNUSED(index) = 0) const { + return *this; + } + + FieldPerp& ydown(std::vector::size_type UNUSED(index) = 0) { return *this; } + const FieldPerp& ydown(std::vector::size_type UNUSED(index) = 0) const { + return *this; + } + + FieldPerp& ynext(int UNUSED(dir)) { return *this; } + const FieldPerp& ynext(int UNUSED(dir)) const { return *this; } + /*! * Ensure that data array is allocated and unique */ From d2200ccc7dc32127ecb47fcf0f6866a301865ce2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 27 Sep 2024 11:05:58 +0200 Subject: [PATCH 127/256] Allow XZHermiteSpline also without y-offset --- src/mesh/interpolation/hermite_spline_xz.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 69df6d8906..650e4022e7 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -346,7 +346,7 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; - const auto region2 = fmt::format("RGN_YPAR_{:+d}", y_offset); + const auto region2 = y_offset != 0 ? fmt::format("RGN_YPAR_{:+d}", y_offset) : region; #if USE_NEW_WEIGHTS #ifdef HS_USE_PETSC From 433df79d2a96335f975db125122fc9315803579e Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 27 Sep 2024 11:06:33 +0200 Subject: [PATCH 128/256] Convert test to new iterator scheeme --- tests/integrated/test-fci-boundary/get_par_bndry.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integrated/test-fci-boundary/get_par_bndry.cxx b/tests/integrated/test-fci-boundary/get_par_bndry.cxx index ac0f5de2a6..4079b55574 100644 --- a/tests/integrated/test-fci-boundary/get_par_bndry.cxx +++ b/tests/integrated/test-fci-boundary/get_par_bndry.cxx @@ -14,11 +14,11 @@ int main(int argc, char** argv) { for (int i = 0; i < fields.size(); i++) { fields[i] = Field3D{0.0}; mesh->communicate(fields[i]); - for (const auto& bndry_par : + for (auto& bndry_par : mesh->getBoundariesPar(static_cast(i))) { output.write("{:s} region\n", toString(static_cast(i))); - for (bndry_par->first(); !bndry_par->isDone(); bndry_par->next()) { - fields[i][bndry_par->ind()] += 1; + for (const auto& pnt: *bndry_par) { + fields[i][pnt.ind()] += 1; output.write("{:s} increment\n", toString(static_cast(i))); } } From 8112e9881a844dd34a2894f2eda0e3807b2d0387 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 27 Sep 2024 15:54:33 +0200 Subject: [PATCH 129/256] Fix segfault in unit test The coordinate is not set, thus transforming to field aligned fails. --- tests/unit/include/test_derivs.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/include/test_derivs.cxx b/tests/unit/include/test_derivs.cxx index a6b8e43ef0..783af4f446 100644 --- a/tests/unit/include/test_derivs.cxx +++ b/tests/unit/include/test_derivs.cxx @@ -332,6 +332,7 @@ TEST_P(FirstDerivativesInterfaceTest, Sanity) { result = bout::derivatives::index::DDX(input); break; case DIRECTION::Y: + input.setDirectionY(YDirectionType::Aligned); result = bout::derivatives::index::DDY(input); break; case DIRECTION::Z: From 5c80fbd1a3331e0bce81b59fce89aaa949c4f5ca Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 27 Sep 2024 15:55:15 +0200 Subject: [PATCH 130/256] Ensure we do not segfault if coords is not set --- include/bout/field.hxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 0898b716c9..c2340f3d34 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -292,6 +292,7 @@ inline void checkPositive(const T& f, const std::string& name = "field", template inline T toFieldAligned(const T& f, const std::string& region = "RGN_ALL") { static_assert(bout::utils::is_Field_v, "toFieldAligned only works on Fields"); + ASSERT3(f.getCoordinates() != nullptr); return f.getCoordinates()->getParallelTransform().toFieldAligned(f, region); } @@ -299,6 +300,7 @@ inline T toFieldAligned(const T& f, const std::string& region = "RGN_ALL") { template inline T fromFieldAligned(const T& f, const std::string& region = "RGN_ALL") { static_assert(bout::utils::is_Field_v, "fromFieldAligned only works on Fields"); + ASSERT3(f.getCoordinates() != nullptr); return f.getCoordinates()->getParallelTransform().fromFieldAligned(f, region); } From 7b9b7e4bf43836a996018ebb1cdae5f26291b311 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 30 Sep 2024 09:14:15 +0200 Subject: [PATCH 131/256] fix boundary condition --- tests/integrated/test-fci-mpi/fci_mpi.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-fci-mpi/fci_mpi.cxx b/tests/integrated/test-fci-mpi/fci_mpi.cxx index f4c26adc96..94520dd4a6 100644 --- a/tests/integrated/test-fci-mpi/fci_mpi.cxx +++ b/tests/integrated/test-fci-mpi/fci_mpi.cxx @@ -20,7 +20,7 @@ int main(int argc, char** argv) { Options::getRoot(), mesh)}; // options->get(fmt::format("input_{:d}:boundary_perp", i), temp_str, s"free_o3"); mesh->communicate(input); - input.applyParallelBoundary("parallel_neumann"); + input.applyParallelBoundary("parallel_neumann_o2"); for (int slice = -mesh->ystart; slice <= mesh->ystart; ++slice) { if (slice != 0) { Field3D tmp{0.}; From d91607f66cdd7a6a476e641e0c86fbce14521f40 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 1 Oct 2024 15:27:21 +0200 Subject: [PATCH 132/256] Add Field2D version for 2D metrics --- include/bout/parallel_boundary_region.hxx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index f8fe3d8ee1..837aeba392 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -192,6 +192,20 @@ public: return f.ynext(-dir)[ind().yp(-dir)]; } +#if BOUT_USE_METRIC_3D == 0 + const BoutReal& ynext(const Field2D& f) const { return f.ynext(dir)[ind().yp(dir)]; } + BoutReal& ynext(Field2D& f) const { return f.ynext(dir)[ind().yp(dir)]; } + + const BoutReal& yprev(const Field2D& f) const { + ASSERT3(valid() > 0); + return f.ynext(-dir)[ind().yp(-dir)]; + } + BoutReal& yprev(Field2D& f) const { + ASSERT3(valid() > 0); + return f.ynext(-dir)[ind().yp(-dir)]; + } +#endif + private: const IndicesVec& bndry_points; IndicesIter bndry_position; From e95636ebc54eadef8a8fbb4ece82a9ee9b973b7c Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 1 Oct 2024 15:52:40 +0200 Subject: [PATCH 133/256] Add setYPrevIfValid --- include/bout/parallel_boundary_region.hxx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 837aeba392..622843c858 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -191,6 +191,11 @@ public: ASSERT3(valid() > 0); return f.ynext(-dir)[ind().yp(-dir)]; } + void setYPrevIfValid(Field3D& f, BoutReal val) const { + if (valid() > 0) { + yprev(f) = val; + } + } #if BOUT_USE_METRIC_3D == 0 const BoutReal& ynext(const Field2D& f) const { return f.ynext(dir)[ind().yp(dir)]; } From 63531f0b81368bb95b63a59e18a84cf61ed8be1c Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 1 Oct 2024 16:15:55 +0200 Subject: [PATCH 134/256] Fix default region name --- src/sys/options.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 080fb180ba..ce238bd09b 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -345,7 +345,7 @@ void saveParallel(Options& opt, const std::string name, const Field3D& tosave){ Field3D tmp; tmp.allocate(); const auto& fpar = tosave.ynext(i); - for (auto j: fpar.getValidRegionWithDefault("RGN_NO_BOUNDARY")){ + for (auto j: fpar.getValidRegionWithDefault("RGN_NOBNDRY")){ tmp[j.yp(-i)] = fpar[j]; } opt[fmt::format("{}_y{:+d}", name, i)] = tmp; From 2b1d9fb86dc2860bb831e26f579dafb7696c6004 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 11 Oct 2024 09:52:03 +0200 Subject: [PATCH 135/256] Rename to allowCalcParallelSlices It is not that the parallel slices may not be set - but rather that they must not be calculated by interpolation. --- include/bout/field3d.hxx | 12 ++++++------ src/field/field3d.cxx | 6 +++--- src/mesh/parallel/fci.cxx | 2 ++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 4eccedd7e3..70ae53178e 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -272,27 +272,27 @@ public: /// Return reference to yup field Field3D& yup(std::vector::size_type index = 0) { ASSERT2(index < yup_fields.size()); - ASSERT2(allow_parallel_slices); + ASSERT2(allowCalcParallelSlices); return yup_fields[index]; } /// Return const reference to yup field const Field3D& yup(std::vector::size_type index = 0) const { ASSERT2(index < yup_fields.size()); - ASSERT2(allow_parallel_slices); + ASSERT2(allowCalcParallelSlices); return yup_fields[index]; } /// Return reference to ydown field Field3D& ydown(std::vector::size_type index = 0) { ASSERT2(index < ydown_fields.size()); - ASSERT2(allow_parallel_slices); + ASSERT2(allowCalcParallelSlices); return ydown_fields[index]; } /// Return const reference to ydown field const Field3D& ydown(std::vector::size_type index = 0) const { ASSERT2(index < ydown_fields.size()); - ASSERT2(allow_parallel_slices); + ASSERT2(allowCalcParallelSlices); return ydown_fields[index]; } @@ -497,7 +497,7 @@ public: Field3D& calcParallelSlices(); void allowParallelSlices([[maybe_unused]] bool allow){ #if CHECK > 0 - allow_parallel_slices = allow; + allowCalcParallelSlices = allow; #endif } @@ -551,7 +551,7 @@ private: template Options* track(const T& change, std::string operation); Options* track(const BoutReal& change, std::string operation); - bool allow_parallel_slices{true}; + bool allowCalcParallelSlices{true}; }; diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index f45cfdcb61..c1704c9d36 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -147,7 +147,7 @@ BOUT_HOST_DEVICE Field3D* Field3D::timeDeriv() { void Field3D::splitParallelSlices() { TRACE("Field3D::splitParallelSlices"); - ASSERT2(allow_parallel_slices); + ASSERT2(allowCalcParallelSlices); if (hasParallelSlices()) { return; @@ -178,7 +178,7 @@ void Field3D::clearParallelSlices() { const Field3D& Field3D::ynext(int dir) const { #if CHECK > 0 - ASSERT2(allow_parallel_slices); + ASSERT2(allowCalcParallelSlices); // Asked for more than yguards if (std::abs(dir) > fieldmesh->ystart) { throw BoutException( @@ -377,7 +377,7 @@ Field3D& Field3D::operator=(const BoutReal val) { } Field3D& Field3D::calcParallelSlices() { - ASSERT2(allow_parallel_slices); + ASSERT2(allowCalcParallelSlices); getCoordinates()->getParallelTransform().calcParallelSlices(*this); #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci()) { diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 3363d331e1..2989bc2702 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -317,6 +317,8 @@ void FCITransform::checkInputGrid() { void FCITransform::calcParallelSlices(Field3D& f) { TRACE("FCITransform::calcParallelSlices"); + ASSERT1(f.allowCalcParallelSlices); + ASSERT1(f.getDirectionY() == YDirectionType::Standard); // Only have forward_map/backward_map for CELL_CENTRE, so can only deal with // CELL_CENTRE inputs From 749bddbbd4280920098651518f58bc4bf9b3bbd5 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 10 Oct 2024 12:45:35 +0200 Subject: [PATCH 136/256] Add code to load parallel metric slices --- include/bout/paralleltransform.hxx | 4 ++ src/mesh/coordinates.cxx | 5 ++ src/mesh/parallel/fci.cxx | 79 +++++++++++++++++++++++------- src/mesh/parallel/fci.hxx | 1 + 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/include/bout/paralleltransform.hxx b/include/bout/paralleltransform.hxx index 0aafa04303..c8050eec23 100644 --- a/include/bout/paralleltransform.hxx +++ b/include/bout/paralleltransform.hxx @@ -89,6 +89,10 @@ public: /// require a twist-shift at branch cuts on closed field lines? virtual bool requiresTwistShift(bool twist_shift_enabled, YDirectionType ytype) = 0; + /// Can be implemented to load parallel metrics + /// Needed by FCI + virtual void loadParallelMetrics(MAYBE_UNUSED(Coordinates* coords)) {} + protected: /// This method should be called in the constructor to check that if the grid /// has a 'parallel_transform' variable, it has the correct value diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index f728189d82..96dec02e52 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -601,6 +601,9 @@ Coordinates::Coordinates(Mesh* mesh, Options* options) // IntShiftTorsion will not be used, but set to zero to avoid uninitialized field IntShiftTorsion = 0.; } + + // Allow transform to fix things up + transform->loadParallelMetrics(this); } Coordinates::Coordinates(Mesh* mesh, Options* options, const CELL_LOC loc, @@ -889,6 +892,8 @@ Coordinates::Coordinates(Mesh* mesh, Options* options, const CELL_LOC loc, true, true, false, transform.get()); } } + // Allow transform to fix things up + transform->loadParallelMetrics(this); } void Coordinates::outputVars(Options& output_options) { diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 2989bc2702..a71d19cfa8 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -47,6 +47,52 @@ #include +namespace { +// Get a unique name for a field based on the sign/magnitude of the offset +std::string parallel_slice_field_name(std::string field, int offset) { + const std::string direction = (offset > 0) ? "forward" : "backward"; + // We only have a suffix for parallel slices beyond the first + // This is for backwards compatibility + const std::string slice_suffix = + (std::abs(offset) > 1) ? "_" + std::to_string(std::abs(offset)) : ""; + return direction + "_" + field + slice_suffix; +}; + +void load_parallel_metric_component(std::string name, Field3D& component, int offset) { + Mesh* mesh = component.getMesh(); + Field3D tmp{mesh}; + const auto pname = parallel_slice_field_name(name, offset); + if (mesh->get(tmp, pname, 0.0, false) != 0) { + throw BoutException("Could not read {:s} from grid file!\n" + " Fix it up with `zoidberg-update-parallel-metrics `", pname); + } + if (!component.hasParallelSlices()){ + component.splitParallelSlices(); + component.allowCalcParallelSlices = false; + } + auto& pcom = component.ynext(offset); + pcom.allocate(); + BOUT_FOR(i, component.getRegion("RGN_NOBNDRY")) { + pcom[i.yp(offset)] = tmp[i]; + } +} + +void load_parallel_metric_components(Coordinates* coords, int offset){ +#define LOAD_PAR(var) load_parallel_metric_component(#var, coords->var, offset) + LOAD_PAR(g11); + LOAD_PAR(g22); + LOAD_PAR(g33); + LOAD_PAR(g13); + LOAD_PAR(g_11); + LOAD_PAR(g_22); + LOAD_PAR(g_33); + LOAD_PAR(g_13); + LOAD_PAR(J); +#undef LOAD_PAR +} + +} // namespace + FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& options, int offset_, const std::shared_ptr& inner_boundary, const std::shared_ptr& outer_boundary, bool zperiodic) @@ -82,38 +128,30 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& map_mesh.get(R, "R", 0.0, false); map_mesh.get(Z, "Z", 0.0, false); - // Get a unique name for a field based on the sign/magnitude of the offset - const auto parallel_slice_field_name = [&](std::string field) -> std::string { - const std::string direction = (offset > 0) ? "forward" : "backward"; - // We only have a suffix for parallel slices beyond the first - // This is for backwards compatibility - const std::string slice_suffix = - (std::abs(offset) > 1) ? "_" + std::to_string(std::abs(offset)) : ""; - return direction + "_" + field + slice_suffix; - }; // If we can't read in any of these fields, things will silently not // work, so best throw - if (map_mesh.get(xt_prime, parallel_slice_field_name("xt_prime"), 0.0, false) != 0) { + if (map_mesh.get(xt_prime, parallel_slice_field_name("xt_prime", offset), 0.0, false) != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("xt_prime")); + parallel_slice_field_name("xt_prime", offset)); } - if (map_mesh.get(zt_prime, parallel_slice_field_name("zt_prime"), 0.0, false) != 0) { + if (map_mesh.get(zt_prime, parallel_slice_field_name("zt_prime", offset), 0.0, false) != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("zt_prime")); + parallel_slice_field_name("zt_prime", offset)); } - if (map_mesh.get(R_prime, parallel_slice_field_name("R"), 0.0, false) != 0) { + if (map_mesh.get(R_prime, parallel_slice_field_name("R", offset), 0.0, false) != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("R")); + parallel_slice_field_name("R", offset)); } - if (map_mesh.get(Z_prime, parallel_slice_field_name("Z"), 0.0, false) != 0) { + if (map_mesh.get(Z_prime, parallel_slice_field_name("Z", offset), 0.0, false) != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("Z")); + parallel_slice_field_name("Z", offset)); } + // Cell corners Field3D xt_prime_corner{emptyFrom(xt_prime)}; @@ -350,3 +388,10 @@ void FCITransform::integrateParallelSlices(Field3D& f) { f.ynext(map.offset) = map.integrate(f); } } + +void FCITransform::loadParallelMetrics(Coordinates* coords) { + for (int i=1; i<= mesh.ystart; ++i) { + load_parallel_metric_components(coords, -i); + load_parallel_metric_components(coords, i); + } +} diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index 3ec3321a6a..7085a71535 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -150,6 +150,7 @@ public: return false; } + void loadParallelMetrics(Coordinates* coords) override; protected: void checkInputGrid() override; From a00625f3613eafc16690391e0c54e3683c2074df Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 16 Oct 2024 10:27:20 +0200 Subject: [PATCH 137/256] add setRegion / getRegionID to all fields --- include/bout/field.hxx | 7 +++++++ include/bout/field3d.hxx | 14 +++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index c2340f3d34..e37b504744 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -34,6 +34,7 @@ class Field; #include #include #include +#include #include #include "bout/field_data.hxx" @@ -134,6 +135,12 @@ public: swap(first.directions, second.directions); } + virtual void setRegion(size_t UNUSED(regionID)) {} + virtual void setRegion(std::optional UNUSED(regionID)) {} + virtual void setRegion(const std::string& UNUSED(region_name)) {} + virtual void resetRegion() {} + virtual std::optional getRegionID() const { return {}; } + private: /// Labels for the type of coordinate system this field is defined over DirectionTypes directions{YDirectionType::Standard, ZDirectionType::Standard}; diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 70ae53178e..d400fc101d 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -338,11 +338,11 @@ public: const Region& getRegion(const std::string& region_name) const; /// Use region provided by the default, and if none is set, use the provided one const Region& getValidRegionWithDefault(const std::string& region_name) const; - void setRegion(const std::string& region_name); - void resetRegion(); - void setRegion(size_t id); - void setRegion(std::optional id); - std::optional getRegionID() const { return regionID; }; + void setRegion(const std::string& region_name) override; + void resetRegion() override; + void setRegion(size_t id) override; + void setRegion(std::optional id) override; + std::optional getRegionID() const override { return regionID; }; /// Return a Region reference to use to iterate over the x- and /// y-indices of this field @@ -529,6 +529,8 @@ public: Options* getTracking() { return tracking; }; + bool allowCalcParallelSlices{true}; + private: /// Array sizes (from fieldmesh). These are valid only if fieldmesh is not null int nx{-1}, ny{-1}, nz{-1}; @@ -551,8 +553,6 @@ private: template Options* track(const T& change, std::string operation); Options* track(const BoutReal& change, std::string operation); - bool allowCalcParallelSlices{true}; - }; // Non-member overloaded operators From 1b4128fd463ea2face831065de9740088fde648b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 16 Oct 2024 10:46:13 +0200 Subject: [PATCH 138/256] Prefer UNUSED over MAYBE_UNUSED MAYBE_UNUSED seems to no be defined --- include/bout/paralleltransform.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/paralleltransform.hxx b/include/bout/paralleltransform.hxx index c8050eec23..63c75228fc 100644 --- a/include/bout/paralleltransform.hxx +++ b/include/bout/paralleltransform.hxx @@ -91,7 +91,7 @@ public: /// Can be implemented to load parallel metrics /// Needed by FCI - virtual void loadParallelMetrics(MAYBE_UNUSED(Coordinates* coords)) {} + virtual void loadParallelMetrics(Coordinates* UNUSED(coords)) {} protected: /// This method should be called in the constructor to check that if the grid From 4c50c6eac96e57c8e1676633eb4c4b25de93b292 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 16 Oct 2024 10:46:47 +0200 Subject: [PATCH 139/256] Preserve regionID in emptyFrom --- include/bout/field.hxx | 3 ++- include/bout/field2d.hxx | 3 ++- include/bout/field3d.hxx | 3 ++- include/bout/fieldperp.hxx | 3 ++- src/field/field2d.cxx | 3 ++- src/field/field3d.cxx | 5 +++-- src/field/fieldperp.cxx | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index e37b504744..51c30d78ad 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -192,7 +192,8 @@ inline bool areFieldsCompatible(const Field& field1, const Field& field2) { template inline T emptyFrom(const T& f) { static_assert(bout::utils::is_Field_v, "emptyFrom only works on Fields"); - return T(f.getMesh(), f.getLocation(), {f.getDirectionY(), f.getDirectionZ()}) + return T(f.getMesh(), f.getLocation(), {f.getDirectionY(), f.getDirectionZ()}, + f.getRegionID()) .allocate(); } diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index cd036c04ff..97f04a3b83 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -68,7 +68,8 @@ public: */ Field2D(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, DirectionTypes directions_in = {YDirectionType::Standard, - ZDirectionType::Average}); + ZDirectionType::Average}, + std::optional region = {}); /*! * Copy constructor. After this both fields diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index d400fc101d..d03f489f62 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -170,7 +170,8 @@ public: */ Field3D(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, DirectionTypes directions_in = {YDirectionType::Standard, - ZDirectionType::Standard}); + ZDirectionType::Standard}, + std::optional regionID = {}); /*! * Copy constructor diff --git a/include/bout/fieldperp.hxx b/include/bout/fieldperp.hxx index ad069f0d01..b50eef1991 100644 --- a/include/bout/fieldperp.hxx +++ b/include/bout/fieldperp.hxx @@ -58,7 +58,8 @@ public: FieldPerp(Mesh* fieldmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, int yindex_in = -1, DirectionTypes directions_in = {YDirectionType::Standard, - ZDirectionType::Standard}); + ZDirectionType::Standard}, + std::optional regionID = {}); /*! * Copy constructor. After this the data diff --git a/src/field/field2d.cxx b/src/field/field2d.cxx index 6a6740669b..00a2777125 100644 --- a/src/field/field2d.cxx +++ b/src/field/field2d.cxx @@ -48,7 +48,8 @@ #include -Field2D::Field2D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in) +Field2D::Field2D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in, + std::optional UNUSED(regionID)) : Field(localmesh, location_in, directions_in) { if (fieldmesh) { diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index c1704c9d36..334b1e9ebd 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -48,8 +48,9 @@ #include /// Constructor -Field3D::Field3D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in) - : Field(localmesh, location_in, directions_in) { +Field3D::Field3D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in, + std::optional regionID) + : Field(localmesh, location_in, directions_in), regionID{regionID} { #if BOUT_USE_TRACK name = ""; #endif diff --git a/src/field/fieldperp.cxx b/src/field/fieldperp.cxx index 22e8aa994e..4012647454 100644 --- a/src/field/fieldperp.cxx +++ b/src/field/fieldperp.cxx @@ -35,7 +35,7 @@ #include FieldPerp::FieldPerp(Mesh* localmesh, CELL_LOC location_in, int yindex_in, - DirectionTypes directions) + DirectionTypes directions, std::optional UNUSED(regionID)) : Field(localmesh, location_in, directions), yindex(yindex_in) { if (fieldmesh) { nx = fieldmesh->LocalNx; From f88a35f52154b999125eec7bc6e32277d910f410 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 11 Oct 2024 17:04:17 +0200 Subject: [PATCH 140/256] set region in loaded parallel fields --- src/mesh/parallel/fci.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index a71d19cfa8..82300f73b9 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -72,6 +72,7 @@ void load_parallel_metric_component(std::string name, Field3D& component, int of } auto& pcom = component.ynext(offset); pcom.allocate(); + pcom.setRegion(fmt::format("RGN_YPAR_{:+d}", offset)); BOUT_FOR(i, component.getRegion("RGN_NOBNDRY")) { pcom[i.yp(offset)] = tmp[i]; } From 638438483a53d170377646c3b5ffc2aaf01961f7 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 15 Oct 2024 16:21:41 +0200 Subject: [PATCH 141/256] Only load parallel J if J is loadable --- src/mesh/parallel/fci.cxx | 78 +++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 82300f73b9..2532785540 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -58,13 +58,35 @@ std::string parallel_slice_field_name(std::string field, int offset) { return direction + "_" + field + slice_suffix; }; -void load_parallel_metric_component(std::string name, Field3D& component, int offset) { +bool load_parallel_metric_component(std::string name, Field3D& component, int offset, + bool doZero) { Mesh* mesh = component.getMesh(); Field3D tmp{mesh}; - const auto pname = parallel_slice_field_name(name, offset); - if (mesh->get(tmp, pname, 0.0, false) != 0) { - throw BoutException("Could not read {:s} from grid file!\n" - " Fix it up with `zoidberg-update-parallel-metrics `", pname); + bool doload = mesh->sourceHasVar(name); + bool isValid{false}; + if (doload) { + const auto pname = parallel_slice_field_name(name, offset); + isValid = mesh->get(tmp, pname, 0.0, false) == 0; + if (not isValid) { + throw BoutException("Could not read {:s} from grid file!\n" + " Fix it up with `zoidberg-update-parallel-metrics `", + pname); + } + } else { + auto lmin = min(component, true); + auto lmax = max(component, true); + if (lmin != lmax) { + if (doZero) { + lmin = lmax = 0.0; + } else { + throw BoutException("{:s} not in grid file but not constant!\n" + " Cannot determine value for parallel slices", + name); + } + } else { + isValid = true; + } + tmp = lmin; } if (!component.hasParallelSlices()){ component.splitParallelSlices(); @@ -76,19 +98,45 @@ void load_parallel_metric_component(std::string name, Field3D& component, int of BOUT_FOR(i, component.getRegion("RGN_NOBNDRY")) { pcom[i.yp(offset)] = tmp[i]; } + return isValid; } void load_parallel_metric_components(Coordinates* coords, int offset){ -#define LOAD_PAR(var) load_parallel_metric_component(#var, coords->var, offset) - LOAD_PAR(g11); - LOAD_PAR(g22); - LOAD_PAR(g33); - LOAD_PAR(g13); - LOAD_PAR(g_11); - LOAD_PAR(g_22); - LOAD_PAR(g_33); - LOAD_PAR(g_13); - LOAD_PAR(J); +#define LOAD_PAR(var, doZero) \ + load_parallel_metric_component(#var, coords->var, offset, doZero) + LOAD_PAR(g11, false); + LOAD_PAR(g22, false); + LOAD_PAR(g33, false); + LOAD_PAR(g12, false); + LOAD_PAR(g13, false); + LOAD_PAR(g23, false); + + LOAD_PAR(g_11, false); + LOAD_PAR(g_22, false); + LOAD_PAR(g_33, false); + LOAD_PAR(g_12, false); + LOAD_PAR(g_13, false); + LOAD_PAR(g_23, false); + + if (not LOAD_PAR(J, true)) { + auto g = + coords->g11.ynext(offset) * coords->g22.ynext(offset) * coords->g33.ynext(offset) + + 2.0 * coords->g12.ynext(offset) * coords->g13.ynext(offset) + * coords->g23.ynext(offset) + - coords->g11.ynext(offset) * coords->g23.ynext(offset) + * coords->g23.ynext(offset) + - coords->g22.ynext(offset) * coords->g13.ynext(offset) + * coords->g13.ynext(offset) + - coords->g33.ynext(offset) * coords->g12.ynext(offset) + * coords->g12.ynext(offset); + + const auto rgn = fmt::format("RGN_YPAR_{:+d}", offset); + // Check that g is positive + bout::checkPositive(g, "The determinant of g^ij", rgn); + auto J = 1. / sqrt(g); + auto& pcom = coords->J.ynext(offset); + BOUT_FOR(i, J.getRegion(rgn)) { pcom[i] = J[i]; } + } #undef LOAD_PAR } From 1ab7fb0e925a43aa2e2ffc77a074a76f926a5bc2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 11 Oct 2024 15:31:47 +0200 Subject: [PATCH 142/256] Fix Div_par --- src/mesh/coordinates.cxx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 96dec02e52..41c2a0bc21 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1616,20 +1616,15 @@ Field3D Coordinates::Div_par(const Field3D& f, CELL_LOC outloc, return Bxy * Grad_par(f / Bxy_floc, outloc, method); } -#if BOUT_USE_FCI_AUTOMAGIC - if (!Bxy_floc.hasParallelSlices()) { - localmesh->communicate(Bxy_floc); - Bxy_floc.applyParallelBoundary("parallel_neumann_o2"); - } -#endif + auto coords = f.getCoordinates(); // Need to modify yup and ydown fields - Field3D f_B = f / Bxy_floc; + Field3D f_B = f / coords->J * sqrt(coords->g_22); f_B.splitParallelSlices(); for (int i = 0; i < f.getMesh()->ystart; ++i) { - f_B.yup(i) = f.yup(i) / Bxy_floc.yup(i); - f_B.ydown(i) = f.ydown(i) / Bxy_floc.ydown(i); + f_B.yup(i) = f.yup(i) / coords->J.yup(i) * sqrt(coords->g_22.yup(i)); + f_B.ydown(i) = f.ydown(i) / coords->J.ydown(i) * sqrt(coords->g_22.ydown(i)); } - return setName(Bxy * Grad_par(f_B, outloc, method), "C:Div_par({:s})", f.name); + return setName(coords->J / sqrt(coords->g_22) * Grad_par(f_B, outloc, method), "Div_par({:s})", f.name); } ///////////////////////////////////////////////////////// From 5f7a7992cb8a9968b93c5e2f1c1e5dc6e92aa55e Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 18 Oct 2024 15:39:45 +0200 Subject: [PATCH 143/256] Only check for allowCalcParallelSlices if we are about to calculate Previously this flag was used to prevent the usage of parallel slices, now it only prevents calculation. --- include/bout/field3d.hxx | 4 ---- src/field/field3d.cxx | 2 -- 2 files changed, 6 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index d03f489f62..ddbc628050 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -273,27 +273,23 @@ public: /// Return reference to yup field Field3D& yup(std::vector::size_type index = 0) { ASSERT2(index < yup_fields.size()); - ASSERT2(allowCalcParallelSlices); return yup_fields[index]; } /// Return const reference to yup field const Field3D& yup(std::vector::size_type index = 0) const { ASSERT2(index < yup_fields.size()); - ASSERT2(allowCalcParallelSlices); return yup_fields[index]; } /// Return reference to ydown field Field3D& ydown(std::vector::size_type index = 0) { ASSERT2(index < ydown_fields.size()); - ASSERT2(allowCalcParallelSlices); return ydown_fields[index]; } /// Return const reference to ydown field const Field3D& ydown(std::vector::size_type index = 0) const { ASSERT2(index < ydown_fields.size()); - ASSERT2(allowCalcParallelSlices); return ydown_fields[index]; } diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 334b1e9ebd..cc6e3509fc 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -148,7 +148,6 @@ BOUT_HOST_DEVICE Field3D* Field3D::timeDeriv() { void Field3D::splitParallelSlices() { TRACE("Field3D::splitParallelSlices"); - ASSERT2(allowCalcParallelSlices); if (hasParallelSlices()) { return; @@ -179,7 +178,6 @@ void Field3D::clearParallelSlices() { const Field3D& Field3D::ynext(int dir) const { #if CHECK > 0 - ASSERT2(allowCalcParallelSlices); // Asked for more than yguards if (std::abs(dir) > fieldmesh->ystart) { throw BoutException( From f7919e0ee1bf7b14cd34a348b5bad4dd22302530 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 6 Nov 2024 14:52:41 +0100 Subject: [PATCH 144/256] Fix bad merge --- include/bout/mask.hxx | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/bout/mask.hxx b/include/bout/mask.hxx index 386bcbf127..624f3d7513 100644 --- a/include/bout/mask.hxx +++ b/include/bout/mask.hxx @@ -66,8 +66,6 @@ public: inline bool& operator()(int jx, int jy, int jz) { return mask(jx, jy, jz); } inline const bool& operator()(int jx, int jy, int jz) const { return mask(jx, jy, jz); } - - inline bool& operator[](const Ind3D& i) { return mask[i]; } inline const bool& operator[](const Ind3D& i) const { return mask[i]; } inline bool& operator[](const Ind3D& i) { return mask[i]; } }; From c9124f605c2ede89b6e010ae11da7d5cef6fa1dc Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 6 Nov 2024 14:54:00 +0100 Subject: [PATCH 145/256] Fix error message --- src/mesh/parallel/fci.cxx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 2532785540..29e6e14739 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -69,7 +69,7 @@ bool load_parallel_metric_component(std::string name, Field3D& component, int of isValid = mesh->get(tmp, pname, 0.0, false) == 0; if (not isValid) { throw BoutException("Could not read {:s} from grid file!\n" - " Fix it up with `zoidberg-update-parallel-metrics `", + "Regenerate the grid with a recent zoidberg!", pname); } } else { @@ -80,7 +80,8 @@ bool load_parallel_metric_component(std::string name, Field3D& component, int of lmin = lmax = 0.0; } else { throw BoutException("{:s} not in grid file but not constant!\n" - " Cannot determine value for parallel slices", + " Cannot determine value for parallel slices.\n" + " Regenerate the grid with a recent zoidberg!", name); } } else { From adf3e51663815f35b6f5c8d7de6280f22a30d97a Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 6 Nov 2024 15:24:49 +0100 Subject: [PATCH 146/256] Set parallel slices only for 3D metrics --- src/mesh/parallel/fci.cxx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 29e6e14739..eca119b9c5 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -58,6 +58,7 @@ std::string parallel_slice_field_name(std::string field, int offset) { return direction + "_" + field + slice_suffix; }; +#if BOUT_USE_METRIC3D bool load_parallel_metric_component(std::string name, Field3D& component, int offset, bool doZero) { Mesh* mesh = component.getMesh(); @@ -101,8 +102,10 @@ bool load_parallel_metric_component(std::string name, Field3D& component, int of } return isValid; } +#endif -void load_parallel_metric_components(Coordinates* coords, int offset){ +void load_parallel_metric_components([[maybe_unused]] Coordinates* coords, [[maybe_unused]] int offset){ +#if BOUT_USE_METRIC3D #define LOAD_PAR(var, doZero) \ load_parallel_metric_component(#var, coords->var, offset, doZero) LOAD_PAR(g11, false); @@ -139,6 +142,7 @@ void load_parallel_metric_components(Coordinates* coords, int offset){ BOUT_FOR(i, J.getRegion(rgn)) { pcom[i] = J[i]; } } #undef LOAD_PAR +#endif } } // namespace From 0619ffefa326a20452faf938ca1db8b7329d0035 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 8 Nov 2024 15:24:20 +0100 Subject: [PATCH 147/256] Fix #if guard --- src/mesh/parallel/fci.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index eca119b9c5..758b26a377 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -58,7 +58,7 @@ std::string parallel_slice_field_name(std::string field, int offset) { return direction + "_" + field + slice_suffix; }; -#if BOUT_USE_METRIC3D +#if BOUT_USE_METRIC_3D bool load_parallel_metric_component(std::string name, Field3D& component, int offset, bool doZero) { Mesh* mesh = component.getMesh(); @@ -105,7 +105,7 @@ bool load_parallel_metric_component(std::string name, Field3D& component, int of #endif void load_parallel_metric_components([[maybe_unused]] Coordinates* coords, [[maybe_unused]] int offset){ -#if BOUT_USE_METRIC3D +#if BOUT_USE_METRIC_3D #define LOAD_PAR(var, doZero) \ load_parallel_metric_component(#var, coords->var, offset, doZero) LOAD_PAR(g11, false); From ac212ef834bed90ae000600d696ed7622a9a5e63 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 8 Nov 2024 15:50:07 +0100 Subject: [PATCH 148/256] Fix bad merge --- include/bout/field.hxx | 1 + src/solver/impls/euler/euler.cxx | 1 + src/solver/impls/pvode/pvode.cxx | 1 + 3 files changed, 3 insertions(+) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 863163ce60..d56322070e 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -31,6 +31,7 @@ class Field; #include #include +#include #include #include "bout/bout_types.hxx" diff --git a/src/solver/impls/euler/euler.cxx b/src/solver/impls/euler/euler.cxx index 5477b5760b..709ac5ba9b 100644 --- a/src/solver/impls/euler/euler.cxx +++ b/src/solver/impls/euler/euler.cxx @@ -6,6 +6,7 @@ #include #include #include +#include #include diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index 7524d21238..65d44d6e49 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -35,6 +35,7 @@ #include #include #include +#include #include "bout/unused.hxx" From 70726f3dfff66b5908ea1e4ccd8ca129919afc2c Mon Sep 17 00:00:00 2001 From: Ben Dudson Date: Thu, 21 Nov 2024 15:14:22 -0800 Subject: [PATCH 149/256] Fix Field3D::setBoundaryTo for FCI methods Without this fix, boundary conditions set on yup/down fields are not applied when a boundary is copied from one field to another. --- src/field/field3d.cxx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index cc6e3509fc..eec87b4f33 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -491,7 +491,25 @@ void Field3D::setBoundaryTo(const Field3D& f3d) { allocate(); // Make sure data allocated - /// Loop over boundary regions + if (isFci()) { + // Set yup/ydown using midpoint values from f3d + ASSERT1(f3d.hasParallelSlices()); + ASSERT1(hasParallelSlices()); + + for (auto& region : fieldmesh->getBoundariesPar()) { + for (const auto& pnt : *region) { + // Interpolate midpoint value in f3d + const BoutReal val = pnt.interpolate_sheath_o1(f3d); + // Set the same boundary value in this field + pnt.dirichlet_o2(*this, val); + } + } + return; + } + + // Non-FCI. + // Transform to field-aligned coordinates? + // Loop over boundary regions for (const auto& reg : fieldmesh->getBoundaries()) { /// Loop within each region for (reg->first(); !reg->isDone(); reg->next()) { From c64d43934ceac9ce05752fd34323146703bde7ea Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 25 Nov 2024 16:44:29 +0100 Subject: [PATCH 150/256] Copy BCs in x-direction also for FCI --- src/field/field3d.cxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index eec87b4f33..8772f4aed3 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -504,13 +504,15 @@ void Field3D::setBoundaryTo(const Field3D& f3d) { pnt.dirichlet_o2(*this, val); } } - return; } // Non-FCI. // Transform to field-aligned coordinates? // Loop over boundary regions for (const auto& reg : fieldmesh->getBoundaries()) { + if (isFci() && reg->by != 0) { + continue; + } /// Loop within each region for (reg->first(); !reg->isDone(); reg->next()) { for (int z = 0; z < nz; z++) { From 2d64a0d7a9f4a46b235e7007f07b85ee71d385aa Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 25 Nov 2024 16:48:06 +0100 Subject: [PATCH 151/256] Use consistently first order interpolation --- src/field/field3d.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 8772f4aed3..2f97e8d02b 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -501,7 +501,7 @@ void Field3D::setBoundaryTo(const Field3D& f3d) { // Interpolate midpoint value in f3d const BoutReal val = pnt.interpolate_sheath_o1(f3d); // Set the same boundary value in this field - pnt.dirichlet_o2(*this, val); + pnt.dirichlet_o1(*this, val); } } } From d59517ef5f0e957c39132f4951c9b449d89594ce Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 25 Nov 2024 16:50:34 +0100 Subject: [PATCH 152/256] Disable broken test-laplace-petsc3d by default --- tests/integrated/test-laplace-petsc3d/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-laplace-petsc3d/CMakeLists.txt b/tests/integrated/test-laplace-petsc3d/CMakeLists.txt index 93bf4f7efa..d0d5bd5958 100644 --- a/tests/integrated/test-laplace-petsc3d/CMakeLists.txt +++ b/tests/integrated/test-laplace-petsc3d/CMakeLists.txt @@ -6,5 +6,5 @@ bout_add_integrated_test(test-laplace-petsc3d data_slab_core/BOUT.inp data_slab_sol/BOUT.inp USE_RUNTEST - REQUIRES BOUT_HAS_PETSC + REQUIRES BOUT_HAS_PETSC BOUT_ENABLE_ALL_TESTS ) From a65b1d8c483cb6d80b71a027bc0971239ae85dea Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 25 Nov 2024 17:49:54 +0100 Subject: [PATCH 153/256] Fix unit test for FCI --- tests/unit/include/bout/test_single_index_ops.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/include/bout/test_single_index_ops.cxx b/tests/unit/include/bout/test_single_index_ops.cxx index 4359d1d282..8ce9f77a19 100644 --- a/tests/unit/include/bout/test_single_index_ops.cxx +++ b/tests/unit/include/bout/test_single_index_ops.cxx @@ -276,6 +276,9 @@ TEST_F(SingleIndexOpsTest, Div_par) { // Need parallel derivatives of input input.calcParallelSlices(); + // and of coordinates + input.getMesh()->getCoordinates()->J.calcParallelSlices(); + input.getMesh()->getCoordinates()->g_22.calcParallelSlices(); // Differentiate whole field Field3D difops = Div_par(input); From b71e978385ac02b61bbc5960d1777c3f483b5384 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 09:35:20 +0100 Subject: [PATCH 154/256] Update to new grid with parallel metrics --- tests/integrated/test-fci-mpi/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-fci-mpi/CMakeLists.txt b/tests/integrated/test-fci-mpi/CMakeLists.txt index 0dd38487a3..783b30bfd4 100644 --- a/tests/integrated/test-fci-mpi/CMakeLists.txt +++ b/tests/integrated/test-fci-mpi/CMakeLists.txt @@ -3,7 +3,7 @@ bout_add_mms_test(test-fci-mpi USE_RUNTEST USE_DATA_BOUT_INP PROCESSORS 6 - DOWNLOAD https://zenodo.org/record/7614499/files/W7X-conf4-36x8x128.fci.nc?download=1 + DOWNLOAD https://zenodo.org/records/14221309/files/W7X-conf0-36x8x128.fci.nc?download=1 DOWNLOAD_NAME grid.fci.nc REQUIRES BOUT_HAS_PETSC ) From 93b6c485add37f435abbf5330108f6970de2c0b4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 25 Nov 2024 12:43:42 +0100 Subject: [PATCH 155/256] Avoid define conflict with sundials --- externalpackages/PVODE/include/pvode/band.h | 46 ++++++++++----------- externalpackages/PVODE/precon/band.h | 46 ++++++++++----------- externalpackages/PVODE/precon/pvbbdpre.cpp | 4 +- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/externalpackages/PVODE/include/pvode/band.h b/externalpackages/PVODE/include/pvode/band.h index 1fd04a2057..d8eb2d92e9 100644 --- a/externalpackages/PVODE/include/pvode/band.h +++ b/externalpackages/PVODE/include/pvode/band.h @@ -107,13 +107,13 @@ namespace pvode { * references and without knowing too much about the underlying * * element storage. The only storage assumption needed is that * * elements are stored columnwise and that a pointer into the jth * - * column of elements can be obtained via the BAND_COL macro. The * - * BAND_COL_ELEM macro selects an element from a column which has * - * already been isolated via BAND_COL. BAND_COL_ELEM allows the * + * column of elements can be obtained via the PVODE_BAND_COL macro. The * + * PVODE_BAND_COL_ELEM macro selects an element from a column which has * + * already been isolated via PVODE_BAND_COL. PVODE_BAND_COL_ELEM allows the * * user to avoid the translation from the matrix location (i,j) * - * to the index in the array returned by BAND_COL at which the * - * (i,j)th element is stored. See the documentation for BAND_COL * - * and BAND_COL_ELEM for usage details. Users should use these * + * to the index in the array returned by PVODE_BAND_COL at which the * + * (i,j)th element is stored. See the documentation for PVODE_BAND_COL * + * and PVODE_BAND_COL_ELEM for usage details. Users should use these * * macros whenever possible. * * * ******************************************************************/ @@ -131,49 +131,49 @@ typedef struct bandmat_type { /****************************************************************** * * - * Macro : BAND_ELEM * - * Usage : BAND_ELEM(A,i,j) = a_ij; OR * - * a_ij = BAND_ELEM(A,i,j); * + * Macro : PVODE_BAND_ELEM * + * Usage : PVODE_BAND_ELEM(A,i,j) = a_ij; OR * + * a_ij = PVODE_BAND_ELEM(A,i,j); * *----------------------------------------------------------------* - * BAND_ELEM(A,i,j) references the (i,j)th element of the * + * PVODE_BAND_ELEM(A,i,j) references the (i,j)th element of the * * N by N band matrix A, where 0 <= i,j <= N-1. The location * * (i,j) should further satisfy j-(A->mu) <= i <= j+(A->ml). * * * ******************************************************************/ -#define BAND_ELEM(A,i,j) ((A->data)[j][i-j+(A->smu)]) +#define PVODE_BAND_ELEM(A,i,j) ((A->data)[j][i-j+(A->smu)]) /****************************************************************** * * - * Macro : BAND_COL * - * Usage : col_j = BAND_COL(A,j); * + * Macro : PVODE_BAND_COL * + * Usage : col_j = PVODE_BAND_COL(A,j); * *----------------------------------------------------------------* - * BAND_COL(A,j) references the diagonal element of the jth * + * PVODE_BAND_COL(A,j) references the diagonal element of the jth * * column of the N by N band matrix A, 0 <= j <= N-1. The type of * - * the expression BAND_COL(A,j) is real *. The pointer returned * - * by the call BAND_COL(A,j) can be treated as an array which is * + * the expression PVODE_BAND_COL(A,j) is real *. The pointer returned * + * by the call PVODE_BAND_COL(A,j) can be treated as an array which is * * indexed from -(A->mu) to (A->ml). * * * ******************************************************************/ -#define BAND_COL(A,j) (((A->data)[j])+(A->smu)) +#define PVODE_BAND_COL(A,j) (((A->data)[j])+(A->smu)) /****************************************************************** * * - * Macro : BAND_COL_ELEM * - * Usage : col_j = BAND_COL(A,j); * - * BAND_COL_ELEM(col_j,i,j) = a_ij; OR * - * a_ij = BAND_COL_ELEM(col_j,i,j); * + * Macro : PVODE_BAND_COL_ELEM * + * Usage : col_j = PVODE_BAND_COL(A,j); * + * PVODE_BAND_COL_ELEM(col_j,i,j) = a_ij; OR * + * a_ij = PVODE_BAND_COL_ELEM(col_j,i,j); * *----------------------------------------------------------------* * This macro references the (i,j)th entry of the band matrix A * - * when used in conjunction with BAND_COL as shown above. The * + * when used in conjunction with PVODE_BAND_COL as shown above. The * * index (i,j) should satisfy j-(A->mu) <= i <= j+(A->ml). * * * ******************************************************************/ -#define BAND_COL_ELEM(col_j,i,j) (col_j[i-j]) +#define PVODE_BAND_COL_ELEM(col_j,i,j) (col_j[i-j]) /* Functions that use the BandMat representation for a band matrix */ diff --git a/externalpackages/PVODE/precon/band.h b/externalpackages/PVODE/precon/band.h index 1fd04a2057..d8eb2d92e9 100644 --- a/externalpackages/PVODE/precon/band.h +++ b/externalpackages/PVODE/precon/band.h @@ -107,13 +107,13 @@ namespace pvode { * references and without knowing too much about the underlying * * element storage. The only storage assumption needed is that * * elements are stored columnwise and that a pointer into the jth * - * column of elements can be obtained via the BAND_COL macro. The * - * BAND_COL_ELEM macro selects an element from a column which has * - * already been isolated via BAND_COL. BAND_COL_ELEM allows the * + * column of elements can be obtained via the PVODE_BAND_COL macro. The * + * PVODE_BAND_COL_ELEM macro selects an element from a column which has * + * already been isolated via PVODE_BAND_COL. PVODE_BAND_COL_ELEM allows the * * user to avoid the translation from the matrix location (i,j) * - * to the index in the array returned by BAND_COL at which the * - * (i,j)th element is stored. See the documentation for BAND_COL * - * and BAND_COL_ELEM for usage details. Users should use these * + * to the index in the array returned by PVODE_BAND_COL at which the * + * (i,j)th element is stored. See the documentation for PVODE_BAND_COL * + * and PVODE_BAND_COL_ELEM for usage details. Users should use these * * macros whenever possible. * * * ******************************************************************/ @@ -131,49 +131,49 @@ typedef struct bandmat_type { /****************************************************************** * * - * Macro : BAND_ELEM * - * Usage : BAND_ELEM(A,i,j) = a_ij; OR * - * a_ij = BAND_ELEM(A,i,j); * + * Macro : PVODE_BAND_ELEM * + * Usage : PVODE_BAND_ELEM(A,i,j) = a_ij; OR * + * a_ij = PVODE_BAND_ELEM(A,i,j); * *----------------------------------------------------------------* - * BAND_ELEM(A,i,j) references the (i,j)th element of the * + * PVODE_BAND_ELEM(A,i,j) references the (i,j)th element of the * * N by N band matrix A, where 0 <= i,j <= N-1. The location * * (i,j) should further satisfy j-(A->mu) <= i <= j+(A->ml). * * * ******************************************************************/ -#define BAND_ELEM(A,i,j) ((A->data)[j][i-j+(A->smu)]) +#define PVODE_BAND_ELEM(A,i,j) ((A->data)[j][i-j+(A->smu)]) /****************************************************************** * * - * Macro : BAND_COL * - * Usage : col_j = BAND_COL(A,j); * + * Macro : PVODE_BAND_COL * + * Usage : col_j = PVODE_BAND_COL(A,j); * *----------------------------------------------------------------* - * BAND_COL(A,j) references the diagonal element of the jth * + * PVODE_BAND_COL(A,j) references the diagonal element of the jth * * column of the N by N band matrix A, 0 <= j <= N-1. The type of * - * the expression BAND_COL(A,j) is real *. The pointer returned * - * by the call BAND_COL(A,j) can be treated as an array which is * + * the expression PVODE_BAND_COL(A,j) is real *. The pointer returned * + * by the call PVODE_BAND_COL(A,j) can be treated as an array which is * * indexed from -(A->mu) to (A->ml). * * * ******************************************************************/ -#define BAND_COL(A,j) (((A->data)[j])+(A->smu)) +#define PVODE_BAND_COL(A,j) (((A->data)[j])+(A->smu)) /****************************************************************** * * - * Macro : BAND_COL_ELEM * - * Usage : col_j = BAND_COL(A,j); * - * BAND_COL_ELEM(col_j,i,j) = a_ij; OR * - * a_ij = BAND_COL_ELEM(col_j,i,j); * + * Macro : PVODE_BAND_COL_ELEM * + * Usage : col_j = PVODE_BAND_COL(A,j); * + * PVODE_BAND_COL_ELEM(col_j,i,j) = a_ij; OR * + * a_ij = PVODE_BAND_COL_ELEM(col_j,i,j); * *----------------------------------------------------------------* * This macro references the (i,j)th entry of the band matrix A * - * when used in conjunction with BAND_COL as shown above. The * + * when used in conjunction with PVODE_BAND_COL as shown above. The * * index (i,j) should satisfy j-(A->mu) <= i <= j+(A->ml). * * * ******************************************************************/ -#define BAND_COL_ELEM(col_j,i,j) (col_j[i-j]) +#define PVODE_BAND_COL_ELEM(col_j,i,j) (col_j[i-j]) /* Functions that use the BandMat representation for a band matrix */ diff --git a/externalpackages/PVODE/precon/pvbbdpre.cpp b/externalpackages/PVODE/precon/pvbbdpre.cpp index 3a1181dcf1..b5e35b8e35 100644 --- a/externalpackages/PVODE/precon/pvbbdpre.cpp +++ b/externalpackages/PVODE/precon/pvbbdpre.cpp @@ -364,13 +364,13 @@ static void PVBBDDQJac(integer Nlocal, integer mudq, integer mldq, /* Restore ytemp, then form and load difference quotients */ for (j=group-1; j < Nlocal; j+=width) { ytemp_data[j] = y_data[j]; - col_j = BAND_COL(J,j); + col_j = PVODE_BAND_COL(J,j); inc = MAX(rely*ABS(y_data[j]), minInc/ewt_data[j]); inc_inv = ONE/inc; i1 = MAX(0, j-mukeep); i2 = MIN(j+mlkeep, Nlocal-1); for (i=i1; i <= i2; i++) - BAND_COL_ELEM(col_j,i,j) = + PVODE_BAND_COL_ELEM(col_j,i,j) = inc_inv * (gtemp_data[i] - gy_data[i]); } } From 095c980f74d3d916ca6fbbfab6ed178a95c09a10 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 11:37:22 +0100 Subject: [PATCH 156/256] Allow setter to be chained --- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 9aedbb291a..1972a4e530 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -268,6 +268,7 @@ cdef class {{ field.field_type }}: dims_in = self._checkDims(dims, data.shape) cdef np.ndarray[double, mode="c", ndim={{ field.ndims }}] data_ = np.ascontiguousarray(data) c_set_all(self.cobj,&data_[{{ zeros }}]) + return self def get(self): """ From 6c674fed1173dbf96423f1ae27e3bb499961360d Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 11:39:13 +0100 Subject: [PATCH 157/256] Expose more arguments of Laplacian --- tools/pylib/_boutpp_build/boutcpp.pxd.jinja | 4 +--- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja index 8f838b864c..4324fe0c03 100644 --- a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja +++ b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja @@ -90,9 +90,7 @@ cdef extern from "bout/fieldgroup.hxx": cdef extern from "bout/invert_laplace.hxx": cppclass Laplacian: @staticmethod - unique_ptr[Laplacian] create() - @staticmethod - unique_ptr[Laplacian] create(Options *) + unique_ptr[Laplacian] create(Options*, benum.CELL_LOC, Mesh*, Solver*) Field3D solve(Field3D,Field3D) void setCoefA(Field3D) void setCoefC(Field3D) diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 1972a4e530..00ff75eb09 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -868,7 +868,7 @@ cdef class Laplacian: """ cdef unique_ptr[c.Laplacian] cobj cdef c.bool isSelfOwned - def __init__(self,section=None): + def __init__(self, section=None, loc="CELL_CENTRE", mesh=None): """ Initialiase a Laplacian solver @@ -878,11 +878,22 @@ cdef class Laplacian: The section from the Option tree to take the options from """ checkInit() + cdef c.Options* copt = NULL if section: - self.cobj = c.Laplacian.create((section).cobj) - else: - self.cobj = c.Laplacian.create(NULL) + if isinstance(section, str): + section = Options.root(section) + copt = (section).cobj + cdef benum.CELL_LOC cloc = benum.resolve_cell_loc(loc) + cdef c.Mesh* cmesh = NULL + if mesh: + cmesh = (mesh).cobj + # Solver is not exposed yet + # cdef c.Solver* csolver = NULL + # if solver: + # csolver = (solver).cobj + self.cobj = c.Laplacian.create(copt, cloc, cmesh, NULL) self.isSelfOwned = True + def solve(self,Field3D x, Field3D guess): """ Calculate the Laplacian inversion From 6f7eff8322d2a62e85bb6e8d513484b5ad28da8b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 11:40:38 +0100 Subject: [PATCH 158/256] Expose `Mesh::get` for Field3D --- tools/pylib/_boutpp_build/boutcpp.pxd.jinja | 1 + tools/pylib/_boutpp_build/boutpp.pyx.jinja | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja index 4324fe0c03..659ad8ff6d 100644 --- a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja +++ b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja @@ -63,6 +63,7 @@ cdef extern from "bout/mesh.hxx": int LocalNx int LocalNy Coordinates * getCoordinates() + int get(Field3D, const string) cdef extern from "bout/coordinates.hxx": cppclass Coordinates: diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 00ff75eb09..a5a1609454 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -742,6 +742,17 @@ cdef class Mesh: msh.isSelfOwned = False return msh + def get(self, name): + """ + Read a variable from the input source + + Currently only supports reading a Field3D + """ + checkInit() + cdef Field3D f3d = Field3D.fromMesh(self) + self.cobj.get(f3d.cobj[0], name.encode()) + return f3d + def __dealloc__(self): self._boutpp_dealloc() From 6934acbf2e62b9d8c08165cd2ebadad9f02cf42a Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 11:42:18 +0100 Subject: [PATCH 159/256] Avoid using kwargs, to avoid hiding typos --- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index a5a1609454..57bf6dcece 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -924,19 +924,20 @@ cdef class Laplacian: """ return f3dFromObj(deref(self.cobj).solve(x.cobj[0],guess.cobj[0])) - def setCoefs(self, **kwargs): +{% set coeffs="A C C1 C2 D Ex Ez".split() %} + def setCoefs(self, *{% for coeff in coeffs %}, {{coeff}}=None{% endfor %}): """ Set the coefficients for the Laplacian solver. The coefficients A, C, C1, C2, D, Ex and Ez can be passed as keyword arguments """ {% set coeffs="A C C1 C2 D Ex Ez".split() %} {% for coeff in coeffs %} - if "{{ coeff }}" in kwargs: - self.setCoef{{ coeff}}(kwargs["{{ coeff }}"]) + if {{ coeff }} is not None: + self.setCoef{{ coeff}}({{ coeff }}) {% endfor %} {% for coeff in coeffs %} - def setCoef{{ coeff }}(self,Field3D {{ coeff }}): + def setCoef{{ coeff }}(self, Field3D {{ coeff }}): """ Set the "{{ coeff }}" coefficient of the Laplacian solver From 50e01aec4df6e9e3f501b2d6ef4efb3d8b0edd81 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 11:42:41 +0100 Subject: [PATCH 160/256] Fix deallocation of Laplacian --- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 57bf6dcece..7b07cd8296 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -949,6 +949,13 @@ cdef class Laplacian: deref(self.cobj).setCoef{{ coeff }}({{ coeff }}.cobj[0]) {% endfor %} + def __dealloc__(self): + self._boutpp_dealloc() + + def _boutpp_dealloc(self): + if self.cobj and self.isSelfOwned: + self.cobj.release() + cdef class FieldFactory: cdef c.FieldFactory * cobj def __init__(self): From 9f3fb54860e585082dbbab52126000c642007d20 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 11:43:25 +0100 Subject: [PATCH 161/256] Set mesh for Fields in Laplacian --- src/invert/laplace/impls/petsc/petsc_laplace.cxx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.cxx b/src/invert/laplace/impls/petsc/petsc_laplace.cxx index f06f4c7de6..40efdb4655 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.cxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.cxx @@ -63,8 +63,9 @@ static PetscErrorCode laplacePCapply(PC pc, Vec x, Vec y) { LaplacePetsc::LaplacePetsc(Options* opt, const CELL_LOC loc, Mesh* mesh_in, Solver* UNUSED(solver)) - : Laplacian(opt, loc, mesh_in), A(0.0), C1(1.0), C2(1.0), D(1.0), Ex(0.0), Ez(0.0), - issetD(false), issetC(false), issetE(false), + : Laplacian(opt, loc, mesh_in), A(0.0, mesh_in), C1(1.0, mesh_in), C2(1.0, mesh_in), + D(1.0, mesh_in), Ex(0.0, mesh_in), Ez(0.0, mesh_in), issetD(false), issetC(false), + issetE(false), sol(mesh_in), lib(opt == nullptr ? &(Options::root()["laplace"]) : opt) { A.setLocation(location); C1.setLocation(location); From 1f93c7356be3aa5170e3b76263e13de8e2db633b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 11:43:51 +0100 Subject: [PATCH 162/256] Fix some unused variable warning for 3D metrics --- src/mesh/boundary_standard.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/boundary_standard.cxx b/src/mesh/boundary_standard.cxx index c8b3269198..367f6b7d54 100644 --- a/src/mesh/boundary_standard.cxx +++ b/src/mesh/boundary_standard.cxx @@ -1593,7 +1593,7 @@ BoundaryOp* BoundaryNeumann_NonOrthogonal::clone(BoundaryRegion* region, return new BoundaryNeumann_NonOrthogonal(region); } -void BoundaryNeumann_NonOrthogonal::apply(Field2D& f) { +void BoundaryNeumann_NonOrthogonal::apply(Field2D& [[maybe_unused]] f) { #if not(BOUT_USE_METRIC_3D) Mesh* mesh = bndry->localmesh; ASSERT1(mesh == f.getMesh()); @@ -1728,7 +1728,7 @@ void BoundaryNeumann_NonOrthogonal::apply(Field3D& f) { void BoundaryNeumann::apply(Field2D & f) { BoundaryNeumann::apply(f, 0.); } - void BoundaryNeumann::apply(Field2D & f, BoutReal t) { + void BoundaryNeumann::apply(Field2D& [[maybe_unused]] f, BoutReal t) { // Set (at 2nd order / 3rd order) the value at the mid-point between // the guard cell and the grid cell to be val // N.B. First guard cells (closest to the grid) is 2nd order, while From ec5fe922a7c3f8c130f5bb8738609d71d2c8968a Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 30 Oct 2024 13:58:31 +0000 Subject: [PATCH 163/256] Move `invert3x3` out of general purpose `utils.hxx` header Only used in `Coordinates`, so make private implementation detail --- CMakeLists.txt | 1 + include/bout/utils.hxx | 53 ------------------ src/mesh/coordinates.cxx | 1 + src/mesh/invert3x3.hxx | 81 +++++++++++++++++++++++++++ tests/unit/CMakeLists.txt | 1 + tests/unit/mesh/test_invert3x3.cxx | 89 ++++++++++++++++++++++++++++++ tests/unit/sys/test_utils.cxx | 85 ---------------------------- 7 files changed, 173 insertions(+), 138 deletions(-) create mode 100644 src/mesh/invert3x3.hxx create mode 100644 tests/unit/mesh/test_invert3x3.cxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 257308d578..7df044c867 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -270,6 +270,7 @@ set(BOUT_SOURCES ./src/mesh/interpolation/interpolation_z.cxx ./src/mesh/interpolation/lagrange_4pt_xz.cxx ./src/mesh/interpolation/monotonic_hermite_spline_xz.cxx + ./src/mesh/invert3x3.hxx ./src/mesh/mesh.cxx ./src/mesh/parallel/fci.cxx ./src/mesh/parallel/fci.hxx diff --git a/include/bout/utils.hxx b/include/bout/utils.hxx index f4a41c1a20..c25a8f0ec8 100644 --- a/include/bout/utils.hxx +++ b/include/bout/utils.hxx @@ -410,59 +410,6 @@ bool operator==(const Tensor& lhs, const Tensor& rhs) { return std::equal(lhs.begin(), lhs.end(), rhs.begin()); } -/************************************************************************** - * Matrix routines - **************************************************************************/ -/// Explicit inversion of a 3x3 matrix \p a -/// -/// The input \p small determines how small the determinant must be for -/// us to throw due to the matrix being singular (ill conditioned); -/// If small is less than zero then instead of throwing we return 1. -/// This is ugly but can be used to support some use cases. -template -int invert3x3(Matrix& a, BoutReal small = 1.0e-15) { - TRACE("invert3x3"); - - // Calculate the first co-factors - T A = a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1); - T B = a(1, 2) * a(2, 0) - a(1, 0) * a(2, 2); - T C = a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0); - - // Calculate the determinant - T det = a(0, 0) * A + a(0, 1) * B + a(0, 2) * C; - - if (std::abs(det) < std::abs(small)) { - if (small >= 0) { - throw BoutException("Determinant of matrix < {:e} --> Poorly conditioned", small); - } else { - return 1; - } - } - - // Calculate the rest of the co-factors - T D = a(0, 2) * a(2, 1) - a(0, 1) * a(2, 2); - T E = a(0, 0) * a(2, 2) - a(0, 2) * a(2, 0); - T F = a(0, 1) * a(2, 0) - a(0, 0) * a(2, 1); - T G = a(0, 1) * a(1, 2) - a(0, 2) * a(1, 1); - T H = a(0, 2) * a(1, 0) - a(0, 0) * a(1, 2); - T I = a(0, 0) * a(1, 1) - a(0, 1) * a(1, 0); - - // Now construct the output, overwrites input - T detinv = 1.0 / det; - - a(0, 0) = A * detinv; - a(0, 1) = D * detinv; - a(0, 2) = G * detinv; - a(1, 0) = B * detinv; - a(1, 1) = E * detinv; - a(1, 2) = H * detinv; - a(2, 0) = C * detinv; - a(2, 1) = F * detinv; - a(2, 2) = I * detinv; - - return 0; -} - /*! * Get Random number between 0 and 1 */ diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 41c2a0bc21..eff5672ce6 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -20,6 +20,7 @@ #include "parallel/fci.hxx" #include "parallel/shiftedmetricinterp.hxx" +#include "invert3x3.hxx" // use anonymous namespace so this utility function is not available outside this file namespace { diff --git a/src/mesh/invert3x3.hxx b/src/mesh/invert3x3.hxx new file mode 100644 index 0000000000..dce208338d --- /dev/null +++ b/src/mesh/invert3x3.hxx @@ -0,0 +1,81 @@ +/*!************************************************************************* + * \file invert3x3.hxx + * + * A mix of short utilities for memory management, strings, and some + * simple but common calculations + * + ************************************************************************** + * Copyright 2010-2024 B.D.Dudson, BOUT++ Team + * + * Contact: Ben Dudson, dudson2@llnl.gov + * + * This file is part of BOUT++. + * + * BOUT++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * BOUT++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with BOUT++. If not, see . + * + **************************************************************************/ + +#pragma once + +#include + +/// Explicit inversion of a 3x3 matrix \p a +/// +/// The input \p small determines how small the determinant must be for +/// us to throw due to the matrix being singular (ill conditioned); +/// If small is less than zero then instead of throwing we return 1. +/// This is ugly but can be used to support some use cases. +template +int invert3x3(Matrix& a, BoutReal small = 1.0e-15) { + TRACE("invert3x3"); + + // Calculate the first co-factors + T A = a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1); + T B = a(1, 2) * a(2, 0) - a(1, 0) * a(2, 2); + T C = a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0); + + // Calculate the determinant + T det = a(0, 0) * A + a(0, 1) * B + a(0, 2) * C; + + if (std::abs(det) < std::abs(small)) { + if (small >= 0) { + throw BoutException("Determinant of matrix < {:e} --> Poorly conditioned", small); + } else { + return 1; + } + } + + // Calculate the rest of the co-factors + T D = a(0, 2) * a(2, 1) - a(0, 1) * a(2, 2); + T E = a(0, 0) * a(2, 2) - a(0, 2) * a(2, 0); + T F = a(0, 1) * a(2, 0) - a(0, 0) * a(2, 1); + T G = a(0, 1) * a(1, 2) - a(0, 2) * a(1, 1); + T H = a(0, 2) * a(1, 0) - a(0, 0) * a(1, 2); + T I = a(0, 0) * a(1, 1) - a(0, 1) * a(1, 0); + + // Now construct the output, overwrites input + T detinv = 1.0 / det; + + a(0, 0) = A * detinv; + a(0, 1) = D * detinv; + a(0, 2) = G * detinv; + a(1, 0) = B * detinv; + a(1, 1) = E * detinv; + a(1, 2) = H * detinv; + a(2, 0) = C * detinv; + a(2, 1) = F * detinv; + a(2, 2) = I * detinv; + + return 0; +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 44f1fe5b22..47253c508f 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -69,6 +69,7 @@ set(serial_tests_source ./mesh/test_coordinates.cxx ./mesh/test_coordinates_accessor.cxx ./mesh/test_interpolation.cxx + ./mesh/test_invert3x3.cxx ./mesh/test_mesh.cxx ./mesh/test_paralleltransform.cxx ./solver/test_fakesolver.cxx diff --git a/tests/unit/mesh/test_invert3x3.cxx b/tests/unit/mesh/test_invert3x3.cxx new file mode 100644 index 0000000000..02beeec644 --- /dev/null +++ b/tests/unit/mesh/test_invert3x3.cxx @@ -0,0 +1,89 @@ +#include "../../src/mesh/invert3x3.hxx" + +#include "gtest/gtest.h" + +TEST(Invert3x3Test, Identity) { + Matrix input(3, 3); + input = 0; + for (int i = 0; i < 3; i++) { + input(i, i) = 1.0; + } + auto expected = input; + invert3x3(input); + + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 3; i++) { + EXPECT_EQ(input(i, j), expected(i, j)); + } + } +} + +TEST(Invert3x3Test, InvertTwice) { + std::vector rawDataMat = {0.05567105, 0.92458227, 0.19954631, + 0.28581972, 0.54009039, 0.13234403, + 0.8841194, 0.161224, 0.74853209}; + std::vector rawDataInv = {-2.48021781, 4.27410022, -0.09449605, + 0.6278449, 0.87275842, -0.32168092, + 2.79424897, -5.23628123, 1.51684677}; + + Matrix input(3, 3); + Matrix expected(3, 3); + + int counter = 0; + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 3; i++) { + input(i, j) = rawDataMat[counter]; + expected(i, j) = rawDataInv[counter]; + counter++; + } + } + + // Invert twice to check if we get back to where we started + invert3x3(input); + + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 3; i++) { + // Note we only check to single tolerance here + EXPECT_FLOAT_EQ(input(i, j), expected(i, j)); + } + } +} + +TEST(Invert3x3Test, Singular) { + Matrix input(3, 3); + input = 0; + EXPECT_THROW(invert3x3(input), BoutException); +} + +TEST(Invert3x3Test, BadCondition) { + Matrix input(3, 3); + + // Default small + input = 0.; + input(0, 0) = 1.0e-16; + input(1, 1) = 1.0; + input(2, 2) = 1.0; + EXPECT_THROW(invert3x3(input), BoutException); + + // Default small -- not quite bad enough condition + input = 0.; + input(0, 0) = 1.0e-12; + input(1, 1) = 1.0; + input(2, 2) = 1.0; + EXPECT_NO_THROW(invert3x3(input)); + + // Non-default small + input = 0.; + input(0, 0) = 1.0e-12; + input(1, 1) = 1.0; + input(2, 2) = 1.0; + EXPECT_THROW(invert3x3(input, 1.0e-10), BoutException); + + // Non-default small + input = 0.; + input(0, 0) = 1.0e-12; + input(1, 1) = 1.0; + input(2, 2) = 1.0; + EXPECT_NO_THROW(invert3x3(input, -1.0e-10)); +} + diff --git a/tests/unit/sys/test_utils.cxx b/tests/unit/sys/test_utils.cxx index 747257bafc..6d84813c48 100644 --- a/tests/unit/sys/test_utils.cxx +++ b/tests/unit/sys/test_utils.cxx @@ -386,91 +386,6 @@ TEST(TensorTest, ConstGetData) { std::all_of(std::begin(tensor), std::end(tensor), [](int a) { return a == 3; })); } -TEST(Invert3x3Test, Identity) { - Matrix input(3, 3); - input = 0; - for (int i = 0; i < 3; i++) { - input(i, i) = 1.0; - } - auto expected = input; - invert3x3(input); - - for (int j = 0; j < 3; j++) { - for (int i = 0; i < 3; i++) { - EXPECT_EQ(input(i, j), expected(i, j)); - } - } -} - -TEST(Invert3x3Test, InvertTwice) { - std::vector rawDataMat = {0.05567105, 0.92458227, 0.19954631, - 0.28581972, 0.54009039, 0.13234403, - 0.8841194, 0.161224, 0.74853209}; - std::vector rawDataInv = {-2.48021781, 4.27410022, -0.09449605, - 0.6278449, 0.87275842, -0.32168092, - 2.79424897, -5.23628123, 1.51684677}; - - Matrix input(3, 3); - Matrix expected(3, 3); - - int counter = 0; - for (int j = 0; j < 3; j++) { - for (int i = 0; i < 3; i++) { - input(i, j) = rawDataMat[counter]; - expected(i, j) = rawDataInv[counter]; - counter++; - } - } - - // Invert twice to check if we get back to where we started - invert3x3(input); - - for (int j = 0; j < 3; j++) { - for (int i = 0; i < 3; i++) { - // Note we only check to single tolerance here - EXPECT_FLOAT_EQ(input(i, j), expected(i, j)); - } - } -} - -TEST(Invert3x3Test, Singular) { - Matrix input(3, 3); - input = 0; - EXPECT_THROW(invert3x3(input), BoutException); -} - -TEST(Invert3x3Test, BadCondition) { - Matrix input(3, 3); - - // Default small - input = 0.; - input(0, 0) = 1.0e-16; - input(1, 1) = 1.0; - input(2, 2) = 1.0; - EXPECT_THROW(invert3x3(input), BoutException); - - // Default small -- not quite bad enough condition - input = 0.; - input(0, 0) = 1.0e-12; - input(1, 1) = 1.0; - input(2, 2) = 1.0; - EXPECT_NO_THROW(invert3x3(input)); - - // Non-default small - input = 0.; - input(0, 0) = 1.0e-12; - input(1, 1) = 1.0; - input(2, 2) = 1.0; - EXPECT_THROW(invert3x3(input, 1.0e-10), BoutException); - - // Non-default small - input = 0.; - input(0, 0) = 1.0e-12; - input(1, 1) = 1.0; - input(2, 2) = 1.0; - EXPECT_NO_THROW(invert3x3(input, -1.0e-10)); -} - TEST(NumberUtilitiesTest, SquareInt) { EXPECT_EQ(4, SQ(2)); EXPECT_EQ(4, SQ(-2)); From bd2f36d1e1ca4ee8beacf47b721394c42c322918 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 30 Oct 2024 14:20:14 +0000 Subject: [PATCH 164/256] Return `bool` instead of `int` from `invert3x3` --- src/mesh/coordinates.cxx | 4 ++-- src/mesh/invert3x3.hxx | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index eff5672ce6..1907c311a7 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1264,7 +1264,7 @@ int Coordinates::calcCovariant(const std::string& region) { a(1, 2) = a(2, 1) = g23[i]; a(0, 2) = a(2, 0) = g13[i]; - if (invert3x3(a)) { + if (!invert3x3(a)) { output_error.write("\tERROR: metric tensor is singular at ({:d}, {:d})\n", i.x(), i.y()); return 1; @@ -1320,7 +1320,7 @@ int Coordinates::calcContravariant(const std::string& region) { a(1, 2) = a(2, 1) = g_23[i]; a(0, 2) = a(2, 0) = g_13[i]; - if (invert3x3(a)) { + if (!invert3x3(a)) { output_error.write("\tERROR: metric tensor is singular at ({:d}, {:d})\n", i.x(), i.y()); return 1; diff --git a/src/mesh/invert3x3.hxx b/src/mesh/invert3x3.hxx index dce208338d..84278e2e43 100644 --- a/src/mesh/invert3x3.hxx +++ b/src/mesh/invert3x3.hxx @@ -34,10 +34,10 @@ /// /// The input \p small determines how small the determinant must be for /// us to throw due to the matrix being singular (ill conditioned); -/// If small is less than zero then instead of throwing we return 1. +/// If small is less than zero then instead of throwing we return false. /// This is ugly but can be used to support some use cases. template -int invert3x3(Matrix& a, BoutReal small = 1.0e-15) { +bool invert3x3(Matrix& a, T small = 1.0e-15) { TRACE("invert3x3"); // Calculate the first co-factors @@ -51,9 +51,8 @@ int invert3x3(Matrix& a, BoutReal small = 1.0e-15) { if (std::abs(det) < std::abs(small)) { if (small >= 0) { throw BoutException("Determinant of matrix < {:e} --> Poorly conditioned", small); - } else { - return 1; } + return false; } // Calculate the rest of the co-factors @@ -77,5 +76,5 @@ int invert3x3(Matrix& a, BoutReal small = 1.0e-15) { a(2, 1) = F * detinv; a(2, 2) = I * detinv; - return 0; + return true; } From 3ceef07fd1edef4e6d7f282637433e5dfd8b81a7 Mon Sep 17 00:00:00 2001 From: ZedThree Date: Wed, 30 Oct 2024 14:21:46 +0000 Subject: [PATCH 165/256] Apply clang-format changes --- src/mesh/coordinates.cxx | 2 +- tests/unit/mesh/test_invert3x3.cxx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 1907c311a7..93d748a61c 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -18,9 +18,9 @@ #include +#include "invert3x3.hxx" #include "parallel/fci.hxx" #include "parallel/shiftedmetricinterp.hxx" -#include "invert3x3.hxx" // use anonymous namespace so this utility function is not available outside this file namespace { diff --git a/tests/unit/mesh/test_invert3x3.cxx b/tests/unit/mesh/test_invert3x3.cxx index 02beeec644..77b08354cc 100644 --- a/tests/unit/mesh/test_invert3x3.cxx +++ b/tests/unit/mesh/test_invert3x3.cxx @@ -86,4 +86,3 @@ TEST(Invert3x3Test, BadCondition) { input(2, 2) = 1.0; EXPECT_NO_THROW(invert3x3(input, -1.0e-10)); } - From c42dc24910ef7a4131af48f1e94398c0de1aaab5 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 7 Nov 2024 16:15:28 +0000 Subject: [PATCH 166/256] Return `std::optional` from `invert3x3` Allows throwing more specific error in coordinates --- src/mesh/coordinates.cxx | 14 +++++----- src/mesh/invert3x3.hxx | 43 ++++++++++++++---------------- tests/unit/mesh/test_invert3x3.cxx | 28 +++++-------------- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 93d748a61c..2a94d55d5e 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1264,9 +1264,10 @@ int Coordinates::calcCovariant(const std::string& region) { a(1, 2) = a(2, 1) = g23[i]; a(0, 2) = a(2, 0) = g13[i]; - if (!invert3x3(a)) { - output_error.write("\tERROR: metric tensor is singular at ({:d}, {:d})\n", i.x(), - i.y()); + if (const auto det = bout::invert3x3(a); det.has_value()) { + output_error.write( + "\tERROR: metric tensor is singular at ({:d}, {:d}), determinant: {:d}\n", + i.x(), i.y(), det.value()); return 1; } @@ -1320,9 +1321,10 @@ int Coordinates::calcContravariant(const std::string& region) { a(1, 2) = a(2, 1) = g_23[i]; a(0, 2) = a(2, 0) = g_13[i]; - if (!invert3x3(a)) { - output_error.write("\tERROR: metric tensor is singular at ({:d}, {:d})\n", i.x(), - i.y()); + if (const auto det = bout::invert3x3(a); det.has_value()) { + output_error.write( + "\tERROR: metric tensor is singular at ({:d}, {:d}), determinant: {:d}\n", + i.x(), i.y(), det.value()); return 1; } diff --git a/src/mesh/invert3x3.hxx b/src/mesh/invert3x3.hxx index 84278e2e43..9e635d8150 100644 --- a/src/mesh/invert3x3.hxx +++ b/src/mesh/invert3x3.hxx @@ -29,42 +29,38 @@ #pragma once #include +#include /// Explicit inversion of a 3x3 matrix \p a /// -/// The input \p small determines how small the determinant must be for -/// us to throw due to the matrix being singular (ill conditioned); -/// If small is less than zero then instead of throwing we return false. -/// This is ugly but can be used to support some use cases. -template -bool invert3x3(Matrix& a, T small = 1.0e-15) { +/// If the matrix is singular (ill conditioned), the determinant is +/// return. Otherwise, an empty `std::optional` is return +namespace bout { +inline std::optional invert3x3(Matrix& a) { TRACE("invert3x3"); // Calculate the first co-factors - T A = a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1); - T B = a(1, 2) * a(2, 0) - a(1, 0) * a(2, 2); - T C = a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0); + BoutReal A = a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1); + BoutReal B = a(1, 2) * a(2, 0) - a(1, 0) * a(2, 2); + BoutReal C = a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0); // Calculate the determinant - T det = a(0, 0) * A + a(0, 1) * B + a(0, 2) * C; - + const BoutReal det = a(0, 0) * A + a(0, 1) * B + a(0, 2) * C; + constexpr BoutReal small = 1.0e-15; if (std::abs(det) < std::abs(small)) { - if (small >= 0) { - throw BoutException("Determinant of matrix < {:e} --> Poorly conditioned", small); - } - return false; + return std::optional{det}; } // Calculate the rest of the co-factors - T D = a(0, 2) * a(2, 1) - a(0, 1) * a(2, 2); - T E = a(0, 0) * a(2, 2) - a(0, 2) * a(2, 0); - T F = a(0, 1) * a(2, 0) - a(0, 0) * a(2, 1); - T G = a(0, 1) * a(1, 2) - a(0, 2) * a(1, 1); - T H = a(0, 2) * a(1, 0) - a(0, 0) * a(1, 2); - T I = a(0, 0) * a(1, 1) - a(0, 1) * a(1, 0); + BoutReal D = a(0, 2) * a(2, 1) - a(0, 1) * a(2, 2); + BoutReal E = a(0, 0) * a(2, 2) - a(0, 2) * a(2, 0); + BoutReal F = a(0, 1) * a(2, 0) - a(0, 0) * a(2, 1); + BoutReal G = a(0, 1) * a(1, 2) - a(0, 2) * a(1, 1); + BoutReal H = a(0, 2) * a(1, 0) - a(0, 0) * a(1, 2); + BoutReal I = a(0, 0) * a(1, 1) - a(0, 1) * a(1, 0); // Now construct the output, overwrites input - T detinv = 1.0 / det; + BoutReal detinv = 1.0 / det; a(0, 0) = A * detinv; a(0, 1) = D * detinv; @@ -76,5 +72,6 @@ bool invert3x3(Matrix& a, T small = 1.0e-15) { a(2, 1) = F * detinv; a(2, 2) = I * detinv; - return true; + return std::nullopt; } +} // namespace bout diff --git a/tests/unit/mesh/test_invert3x3.cxx b/tests/unit/mesh/test_invert3x3.cxx index 77b08354cc..3bc4ae69d8 100644 --- a/tests/unit/mesh/test_invert3x3.cxx +++ b/tests/unit/mesh/test_invert3x3.cxx @@ -9,7 +9,7 @@ TEST(Invert3x3Test, Identity) { input(i, i) = 1.0; } auto expected = input; - invert3x3(input); + bout::invert3x3(input); for (int j = 0; j < 3; j++) { for (int i = 0; i < 3; i++) { @@ -39,7 +39,7 @@ TEST(Invert3x3Test, InvertTwice) { } // Invert twice to check if we get back to where we started - invert3x3(input); + bout::invert3x3(input); for (int j = 0; j < 3; j++) { for (int i = 0; i < 3; i++) { @@ -52,37 +52,23 @@ TEST(Invert3x3Test, InvertTwice) { TEST(Invert3x3Test, Singular) { Matrix input(3, 3); input = 0; - EXPECT_THROW(invert3x3(input), BoutException); + auto result = bout::invert3x3(input); + EXPECT_TRUE(result.has_value()); } TEST(Invert3x3Test, BadCondition) { Matrix input(3, 3); - // Default small input = 0.; input(0, 0) = 1.0e-16; input(1, 1) = 1.0; input(2, 2) = 1.0; - EXPECT_THROW(invert3x3(input), BoutException); + EXPECT_TRUE(bout::invert3x3(input).has_value()); - // Default small -- not quite bad enough condition + // not quite bad enough condition input = 0.; input(0, 0) = 1.0e-12; input(1, 1) = 1.0; input(2, 2) = 1.0; - EXPECT_NO_THROW(invert3x3(input)); - - // Non-default small - input = 0.; - input(0, 0) = 1.0e-12; - input(1, 1) = 1.0; - input(2, 2) = 1.0; - EXPECT_THROW(invert3x3(input, 1.0e-10), BoutException); - - // Non-default small - input = 0.; - input(0, 0) = 1.0e-12; - input(1, 1) = 1.0; - input(2, 2) = 1.0; - EXPECT_NO_THROW(invert3x3(input, -1.0e-10)); + EXPECT_FALSE(bout::invert3x3(input).has_value()); } From 36a06f32e8bbc544acaefd09b174952fa3d0ca2b Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 11:10:28 +0100 Subject: [PATCH 167/256] simplify return statement --- src/mesh/invert3x3.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/invert3x3.hxx b/src/mesh/invert3x3.hxx index 9e635d8150..9c6b614168 100644 --- a/src/mesh/invert3x3.hxx +++ b/src/mesh/invert3x3.hxx @@ -48,7 +48,7 @@ inline std::optional invert3x3(Matrix& a) { const BoutReal det = a(0, 0) * A + a(0, 1) * B + a(0, 2) * C; constexpr BoutReal small = 1.0e-15; if (std::abs(det) < std::abs(small)) { - return std::optional{det}; + return det; } // Calculate the rest of the co-factors From b4dd92faa294e586c4624060380649a9ab461350 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 11:11:26 +0100 Subject: [PATCH 168/256] Use formatter for SpecificInd This works for 2D and 3D fields (and is also shorter code) --- src/mesh/coordinates.cxx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 2a94d55d5e..aba401818d 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1266,8 +1266,8 @@ int Coordinates::calcCovariant(const std::string& region) { if (const auto det = bout::invert3x3(a); det.has_value()) { output_error.write( - "\tERROR: metric tensor is singular at ({:d}, {:d}), determinant: {:d}\n", - i.x(), i.y(), det.value()); + "\tERROR: metric tensor is singular at {}, determinant: {:d}\n", + i, det.value()); return 1; } @@ -1323,8 +1323,8 @@ int Coordinates::calcContravariant(const std::string& region) { if (const auto det = bout::invert3x3(a); det.has_value()) { output_error.write( - "\tERROR: metric tensor is singular at ({:d}, {:d}), determinant: {:d}\n", - i.x(), i.y(), det.value()); + "\tERROR: metric tensor is singular at {}, determinant: {:d}\n", + i, det.value()); return 1; } From a7e783a497e4b41356bc6c2026c46a469a667e91 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Dec 2024 12:10:03 +0100 Subject: [PATCH 169/256] Apply clang-format changes --- externalpackages/PVODE/include/pvode/band.h | 12 +++--------- externalpackages/PVODE/precon/band.h | 12 +++--------- src/mesh/coordinates.cxx | 10 ++++------ 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/externalpackages/PVODE/include/pvode/band.h b/externalpackages/PVODE/include/pvode/band.h index d8eb2d92e9..49a98b63d6 100644 --- a/externalpackages/PVODE/include/pvode/band.h +++ b/externalpackages/PVODE/include/pvode/band.h @@ -57,7 +57,6 @@ namespace pvode { - /****************************************************************** * * * Type: BandMat * @@ -118,7 +117,6 @@ namespace pvode { * * ******************************************************************/ - typedef struct bandmat_type { integer size; integer mu, ml, smu; @@ -128,7 +126,6 @@ typedef struct bandmat_type { /* BandMat accessor macros */ - /****************************************************************** * * * Macro : PVODE_BAND_ELEM * @@ -141,8 +138,7 @@ typedef struct bandmat_type { * * ******************************************************************/ -#define PVODE_BAND_ELEM(A,i,j) ((A->data)[j][i-j+(A->smu)]) - +#define PVODE_BAND_ELEM(A, i, j) ((A->data)[j][i - j + (A->smu)]) /****************************************************************** * * @@ -157,8 +153,7 @@ typedef struct bandmat_type { * * ******************************************************************/ -#define PVODE_BAND_COL(A,j) (((A->data)[j])+(A->smu)) - +#define PVODE_BAND_COL(A, j) (((A->data)[j]) + (A->smu)) /****************************************************************** * * @@ -173,8 +168,7 @@ typedef struct bandmat_type { * * ******************************************************************/ -#define PVODE_BAND_COL_ELEM(col_j,i,j) (col_j[i-j]) - +#define PVODE_BAND_COL_ELEM(col_j, i, j) (col_j[i - j]) /* Functions that use the BandMat representation for a band matrix */ diff --git a/externalpackages/PVODE/precon/band.h b/externalpackages/PVODE/precon/band.h index d8eb2d92e9..49a98b63d6 100644 --- a/externalpackages/PVODE/precon/band.h +++ b/externalpackages/PVODE/precon/band.h @@ -57,7 +57,6 @@ namespace pvode { - /****************************************************************** * * * Type: BandMat * @@ -118,7 +117,6 @@ namespace pvode { * * ******************************************************************/ - typedef struct bandmat_type { integer size; integer mu, ml, smu; @@ -128,7 +126,6 @@ typedef struct bandmat_type { /* BandMat accessor macros */ - /****************************************************************** * * * Macro : PVODE_BAND_ELEM * @@ -141,8 +138,7 @@ typedef struct bandmat_type { * * ******************************************************************/ -#define PVODE_BAND_ELEM(A,i,j) ((A->data)[j][i-j+(A->smu)]) - +#define PVODE_BAND_ELEM(A, i, j) ((A->data)[j][i - j + (A->smu)]) /****************************************************************** * * @@ -157,8 +153,7 @@ typedef struct bandmat_type { * * ******************************************************************/ -#define PVODE_BAND_COL(A,j) (((A->data)[j])+(A->smu)) - +#define PVODE_BAND_COL(A, j) (((A->data)[j]) + (A->smu)) /****************************************************************** * * @@ -173,8 +168,7 @@ typedef struct bandmat_type { * * ******************************************************************/ -#define PVODE_BAND_COL_ELEM(col_j,i,j) (col_j[i-j]) - +#define PVODE_BAND_COL_ELEM(col_j, i, j) (col_j[i - j]) /* Functions that use the BandMat representation for a band matrix */ diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index aba401818d..102d8ba7b5 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1265,9 +1265,8 @@ int Coordinates::calcCovariant(const std::string& region) { a(0, 2) = a(2, 0) = g13[i]; if (const auto det = bout::invert3x3(a); det.has_value()) { - output_error.write( - "\tERROR: metric tensor is singular at {}, determinant: {:d}\n", - i, det.value()); + output_error.write("\tERROR: metric tensor is singular at {}, determinant: {:d}\n", + i, det.value()); return 1; } @@ -1322,9 +1321,8 @@ int Coordinates::calcContravariant(const std::string& region) { a(0, 2) = a(2, 0) = g_13[i]; if (const auto det = bout::invert3x3(a); det.has_value()) { - output_error.write( - "\tERROR: metric tensor is singular at {}, determinant: {:d}\n", - i, det.value()); + output_error.write("\tERROR: metric tensor is singular at {}, determinant: {:d}\n", + i, det.value()); return 1; } From 33a72a5ea9af1fbc2085861385b4f6e9c2a2e887 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 13:23:10 +0100 Subject: [PATCH 170/256] Add missing header to format SpecificInd --- src/mesh/coordinates.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 102d8ba7b5..4db84601af 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -15,6 +15,7 @@ #include #include #include +#include #include From 4f2da4dedbd159c0e4c549325d0b3c998c9f24c0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 14:10:43 +0100 Subject: [PATCH 171/256] Prefere const --- src/mesh/invert3x3.hxx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/mesh/invert3x3.hxx b/src/mesh/invert3x3.hxx index 9c6b614168..c011f55bf7 100644 --- a/src/mesh/invert3x3.hxx +++ b/src/mesh/invert3x3.hxx @@ -40,9 +40,9 @@ inline std::optional invert3x3(Matrix& a) { TRACE("invert3x3"); // Calculate the first co-factors - BoutReal A = a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1); - BoutReal B = a(1, 2) * a(2, 0) - a(1, 0) * a(2, 2); - BoutReal C = a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0); + const BoutReal A = a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1); + const BoutReal B = a(1, 2) * a(2, 0) - a(1, 0) * a(2, 2); + const BoutReal C = a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0); // Calculate the determinant const BoutReal det = a(0, 0) * A + a(0, 1) * B + a(0, 2) * C; @@ -52,15 +52,15 @@ inline std::optional invert3x3(Matrix& a) { } // Calculate the rest of the co-factors - BoutReal D = a(0, 2) * a(2, 1) - a(0, 1) * a(2, 2); - BoutReal E = a(0, 0) * a(2, 2) - a(0, 2) * a(2, 0); - BoutReal F = a(0, 1) * a(2, 0) - a(0, 0) * a(2, 1); - BoutReal G = a(0, 1) * a(1, 2) - a(0, 2) * a(1, 1); - BoutReal H = a(0, 2) * a(1, 0) - a(0, 0) * a(1, 2); - BoutReal I = a(0, 0) * a(1, 1) - a(0, 1) * a(1, 0); + const BoutReal D = a(0, 2) * a(2, 1) - a(0, 1) * a(2, 2); + const BoutReal E = a(0, 0) * a(2, 2) - a(0, 2) * a(2, 0); + const BoutReal F = a(0, 1) * a(2, 0) - a(0, 0) * a(2, 1); + const BoutReal G = a(0, 1) * a(1, 2) - a(0, 2) * a(1, 1); + const BoutReal H = a(0, 2) * a(1, 0) - a(0, 0) * a(1, 2); + const BoutReal I = a(0, 0) * a(1, 1) - a(0, 1) * a(1, 0); // Now construct the output, overwrites input - BoutReal detinv = 1.0 / det; + const BoutReal detinv = 1.0 / det; a(0, 0) = A * detinv; a(0, 1) = D * detinv; From a1f4b46aa771546ba49bf1592582c67e3057d998 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 10:00:17 +0100 Subject: [PATCH 172/256] Use PEP 625 compatible archive name --- tools/pylib/_boutpp_build/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pylib/_boutpp_build/backend.py b/tools/pylib/_boutpp_build/backend.py index e89f37bb42..254fa7c2fd 100644 --- a/tools/pylib/_boutpp_build/backend.py +++ b/tools/pylib/_boutpp_build/backend.py @@ -198,7 +198,7 @@ def build_sdist(sdist_directory, config_settings=None): if k == "nightly": useLocalVersion = False pkgname = "boutpp-nightly" - prefix = f"{pkgname}-{getversion()}" + prefix = f"{pkgname.replace('-', '_')}-{getversion()}" fname = f"{prefix}.tar" run(f"git archive HEAD --prefix {prefix}/ -o {sdist_directory}/{fname}") _, tmp = tempfile.mkstemp(suffix=".tar") From b4bd5b89a078dcbcad2bbfa7ced681fb03bdc7c1 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 09:37:59 +0100 Subject: [PATCH 173/256] CI: Increase check level for debug run --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f1fc19aeac..e493ca88ea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,7 +79,7 @@ jobs: - name: "Debug, shared" os: ubuntu-latest - cmake_options: "-DCHECK=3 + cmake_options: "-DCHECK=4 -DCMAKE_BUILD_TYPE=Debug -DBOUT_ENABLE_SIGNAL=ON -DBOUT_ENABLE_TRACK=ON From 58638563986cd73acc0793b1c07d81e26ae80b53 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 26 Nov 2024 09:46:29 +0100 Subject: [PATCH 174/256] Fix unit test for CHECK=4 --- tests/unit/include/bout/test_stencil.cxx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/include/bout/test_stencil.cxx b/tests/unit/include/bout/test_stencil.cxx index 033a865154..4a919614b4 100644 --- a/tests/unit/include/bout/test_stencil.cxx +++ b/tests/unit/include/bout/test_stencil.cxx @@ -12,10 +12,12 @@ class IndexOffsetStructTests : public ::testing::Test { public: IndexOffsetStructTests() { zero = T(0, std::is_same_v ? 1 : 5, std::is_same_v ? 1 : 7); + finite = T(239, std::is_same_v ? 1 : 5, std::is_same_v ? 1 : 12); } IndexOffset noOffset; T zero; + T finite; }; template @@ -144,15 +146,15 @@ TYPED_TEST(IndexOffsetStructTests, AddToIndex) { TYPED_TEST(IndexOffsetStructTests, SubtractFromIndex) { IndexOffset offset1 = {1, 0, 0}, offset2 = {0, 2, 0}, offset3 = {0, 0, 11}, offset4 = {2, 3, -2}; - EXPECT_EQ(this->zero - offset1, this->zero.xm()); + EXPECT_EQ(this->finite - offset1, this->finite.xm()); if constexpr (!std::is_same_v) { - EXPECT_EQ(this->zero - offset2, this->zero.ym(2)); + EXPECT_EQ(this->finite - offset2, this->finite.ym(2)); } if constexpr (!std::is_same_v) { - EXPECT_EQ(this->zero - offset3, this->zero.zm(11)); + EXPECT_EQ(this->finite - offset3, this->finite.zm(11)); } if constexpr (std::is_same_v) { - EXPECT_EQ(this->zero - offset4, this->zero.zp(2).xm(2).ym(3)); + EXPECT_EQ(this->finite - offset4, this->finite.zp(2).xm(2).ym(3)); } } From 57f0553d261215b75c9e2ed0f792dbb38ab83b98 Mon Sep 17 00:00:00 2001 From: dschwoerer Date: Tue, 26 Nov 2024 09:27:33 +0000 Subject: [PATCH 175/256] Apply clang-format changes --- tests/unit/include/bout/test_stencil.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/include/bout/test_stencil.cxx b/tests/unit/include/bout/test_stencil.cxx index 4a919614b4..2d76a9a7f1 100644 --- a/tests/unit/include/bout/test_stencil.cxx +++ b/tests/unit/include/bout/test_stencil.cxx @@ -12,7 +12,8 @@ class IndexOffsetStructTests : public ::testing::Test { public: IndexOffsetStructTests() { zero = T(0, std::is_same_v ? 1 : 5, std::is_same_v ? 1 : 7); - finite = T(239, std::is_same_v ? 1 : 5, std::is_same_v ? 1 : 12); + finite = + T(239, std::is_same_v ? 1 : 5, std::is_same_v ? 1 : 12); } IndexOffset noOffset; From cc9d5cc33f71c0badce09514649c8bcb10ebbf5e Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 29 Nov 2024 14:18:40 +0100 Subject: [PATCH 176/256] Avoid using the wrong grid by accident I sometimes have `[mesh:file]` set in the input file, and specify `grid` on the command line, only to be confused why the wrong grid was picked. --- src/mesh/mesh.cxx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/mesh/mesh.cxx b/src/mesh/mesh.cxx index 6d7a5de512..6eb435a663 100644 --- a/src/mesh/mesh.cxx +++ b/src/mesh/mesh.cxx @@ -29,8 +29,16 @@ MeshFactory::ReturnType MeshFactory::create(const std::string& type, Options* op if (options->isSet("file") or Options::root().isSet("grid")) { // Specified mesh file - const auto grid_name = - (*options)["file"].withDefault(Options::root()["grid"].withDefault("")); + const auto grid_name1 = Options::root()["grid"].withDefault(""); + const auto grid_name = (*options)["file"].withDefault(grid_name1); + if (options->isSet("file") and Options::root().isSet("grid")) { + if (grid_name1 != grid_name) { + throw BoutException( + "Mismatch in grid names - specified `{:s}` in grid and `{:s} in " + "mesh:file!\nPlease specify only one name or ensure they are the same!", + grid_name1, grid_name); + } + } output << "\nGetting grid data from file " << grid_name << "\n"; // Create a grid file, using specified format if given From aef421523f760c15125688359fa29dc6587c87c9 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 16 Dec 2024 15:14:06 +0100 Subject: [PATCH 177/256] Add some checks to petsc_laplace --- .../laplace/impls/petsc/petsc_laplace.cxx | 62 ++++++++++--------- .../laplace/impls/petsc/petsc_laplace.hxx | 2 +- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.cxx b/src/invert/laplace/impls/petsc/petsc_laplace.cxx index 40efdb4655..9a09b7edad 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.cxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.cxx @@ -347,7 +347,7 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { checkFlags(); #endif - int y = b.getIndex(); // Get the Y index + const int y = b.getIndex(); // Get the Y index sol.setIndex(y); // Initialize the solution field. sol = 0.; @@ -455,6 +455,7 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { val = x0[x][z]; VecSetValues(xs, 1, &i, &val, INSERT_VALUES); + ASSERT3(i == getIndex(x, z)); i++; // Increment row in Petsc matrix } } @@ -472,11 +473,11 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { // Set the matrix coefficients Coeffs(x, y, z, A1, A2, A3, A4, A5); - BoutReal dx = coords->dx(x, y, z); - BoutReal dx2 = SQ(dx); - BoutReal dz = coords->dz(x, y, z); - BoutReal dz2 = SQ(dz); - BoutReal dxdz = dx * dz; + const BoutReal dx = coords->dx(x, y, z); + const BoutReal dx2 = SQ(dx); + const BoutReal dz = coords->dz(x, y, z); + const BoutReal dz2 = SQ(dz); + const BoutReal dxdz = dx * dz; ASSERT3(finite(A1)); ASSERT3(finite(A2)); @@ -632,6 +633,7 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { // Set Components of Trial Solution Vector val = x0[x][z]; VecSetValues(xs, 1, &i, &val, INSERT_VALUES); + ASSERT3(i == getIndex(x, z)); i++; } } @@ -715,7 +717,7 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { // INSERT_VALUES replaces existing entries with new values val = x0[x][z]; VecSetValues(xs, 1, &i, &val, INSERT_VALUES); - + ASSERT3(i == getIndex(x, z)); i++; // Increment row in Petsc matrix } } @@ -871,24 +873,7 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { return sol; } -/*! - * Sets the elements of the matrix A, which is used to solve the problem Ax=b. - * - * \param[in] - * i - * The row of the PETSc matrix - * \param[in] x Local x index of the mesh - * \param[in] z Local z index of the mesh - * \param[in] xshift The shift in rows from the index x - * \param[in] zshift The shift in columns from the index z - * \param[in] ele Value of the element - * \param[in] MatA The matrix A used in the inversion - * - * \param[out] MatA The matrix A used in the inversion - */ -void LaplacePetsc::Element(int i, int x, int z, int xshift, int zshift, PetscScalar ele, - Mat& MatA) { - +int LaplacePetsc::getIndex(const int x, const int z) { // Need to convert LOCAL x to GLOBAL x in order to correctly calculate // PETSC Matrix Index. int xoffset = Istart / meshz; @@ -897,22 +882,43 @@ void LaplacePetsc::Element(int i, int x, int z, int xshift, int zshift, PetscSca } // Calculate the row to be set - int row_new = x + xshift; // should never be out of range. + int row_new = x; // should never be out of range. if (!localmesh->firstX()) { row_new += (xoffset - localmesh->xstart); } // Calculate the column to be set - int col_new = z + zshift; + int col_new = z; if (col_new < 0) { col_new += meshz; } else if (col_new > meshz - 1) { col_new -= meshz; } + ASSERT3(0 <= col_new and col_new < meshz); // Convert to global indices - int index = (row_new * meshz) + col_new; + return (row_new * meshz) + col_new; +} + +/*! + * Sets the elements of the matrix A, which is used to solve the problem Ax=b. + * + * \param[in] + * i + * The row of the PETSc matrix + * \param[in] x Local x index of the mesh + * \param[in] z Local z index of the mesh + * \param[in] xshift The shift in rows from the index x + * \param[in] zshift The shift in columns from the index z + * \param[in] ele Value of the element + * \param[in] MatA The matrix A used in the inversion + * + * \param[out] MatA The matrix A used in the inversion + */ +void LaplacePetsc::Element(const int i, const int x, const int z, const int xshift, + const int zshift, const PetscScalar ele, Mat& MatA) { + const int index = getIndex(x + xshift, z + zshift); #if CHECK > 2 if (!finite(ele)) { throw BoutException("Non-finite element at x={:d}, z={:d}, row={:d}, col={:d}\n", x, diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.hxx b/src/invert/laplace/impls/petsc/petsc_laplace.hxx index 1d56abd00b..3a616b4b09 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.hxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.hxx @@ -202,7 +202,7 @@ private: void Element(int i, int x, int z, int xshift, int zshift, PetscScalar ele, Mat& MatA); void Coeffs(int x, int y, int z, BoutReal& A1, BoutReal& A2, BoutReal& A3, BoutReal& A4, BoutReal& A5); - + int getIndex(int x, int z); /* Ex and Ez * Additional 1st derivative terms to allow for solution field to be * components of a vector From bbc8e080828eefd4bff503b931091e7db6258f72 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 16 Dec 2024 15:17:30 +0100 Subject: [PATCH 178/256] Add forward method to Laplacian inversion Mostly for debugging and testing purposes. Allows to implement a forward operator for the inversion. Here only the forward operator for the PETSc based inversion is implemented. --- include/bout/invert_laplace.hxx | 5 ++++ .../laplace/impls/petsc/petsc_laplace.cxx | 24 ++++++++++----- .../laplace/impls/petsc/petsc_laplace.hxx | 8 ++++- src/invert/laplace/invert_laplace.cxx | 30 +++++++++++++++++++ tools/pylib/_boutpp_build/boutcpp.pxd.jinja | 3 +- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 20 ++++++++++++- 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/include/bout/invert_laplace.hxx b/include/bout/invert_laplace.hxx index 187056d115..ee0c4493a7 100644 --- a/include/bout/invert_laplace.hxx +++ b/include/bout/invert_laplace.hxx @@ -255,6 +255,11 @@ public: virtual Field3D solve(const Field3D& b, const Field3D& x0); virtual Field2D solve(const Field2D& b, const Field2D& x0); + /// Some implementations can also implement the forward operator for testing + /// and debugging + virtual FieldPerp forward(const FieldPerp& f); + virtual Field3D forward(const Field3D& f); + /// Coefficients in tridiagonal inversion void tridagCoefs(int jx, int jy, int jz, dcomplex& a, dcomplex& b, dcomplex& c, const Field2D* ccoef = nullptr, const Field2D* d = nullptr, diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.cxx b/src/invert/laplace/impls/petsc/petsc_laplace.cxx index 9a09b7edad..d0d68bee52 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.cxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.cxx @@ -336,7 +336,8 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b) { return solve(b, b); } * * \returns sol The solution x of the problem Ax=b. */ -FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { +FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0, + const bool forward) { TRACE("LaplacePetsc::solve"); ASSERT1(localmesh == b.getMesh() && localmesh == x0.getMesh()); @@ -355,12 +356,12 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { MatGetOwnershipRange(MatA, &Istart, &Iend); int i = Istart; // The row in the PETSc matrix - { - Timer timer("petscsetup"); - // if ((fourth_order) && !(lastflag&INVERT_4TH_ORDER)) throw BoutException("Should not change INVERT_4TH_ORDER flag in LaplacePetsc: 2nd order and 4th order require different pre-allocation to optimize PETSc solver"); + auto timer = std::make_unique("petscsetup"); + + // if ((fourth_order) && !(lastflag&INVERT_4TH_ORDER)) throw BoutException("Should not change INVERT_4TH_ORDER flag in LaplacePetsc: 2nd order and 4th order require different pre-allocation to optimize PETSc solver"); - /* Set Matrix Elements + /* Set Matrix Elements * * Loop over locally owned rows of matrix A * i labels NODE POINT from @@ -742,6 +743,7 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { VecAssemblyBegin(xs); VecAssemblyEnd(xs); + if (not forward) { // Configure Linear Solver #if PETSC_VERSION_GE(3, 5, 0) KSPSetOperators(ksp, MatA, MatA); @@ -808,7 +810,8 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { lib.setOptionsFromInputFile(ksp); } - } + timer.reset(); + // Call the actual solver { @@ -826,8 +829,15 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0) { "petsc_laplace: inversion failed to converge. KSPConvergedReason: {} ({})", KSPConvergedReasons[reason], static_cast(reason)); } + } else { + timer.reset(); + PetscErrorCode err = MatMult(MatA, bs, xs); + if (err != PETSC_SUCCESS) { + throw BoutException("MatMult failed with {:d}", static_cast(err)); + } + } - // Add data to FieldPerp Object + // Add data to FieldPerp Object i = Istart; // Set the inner boundary values if (localmesh->firstX()) { diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.hxx b/src/invert/laplace/impls/petsc/petsc_laplace.hxx index 3a616b4b09..5a73030c8c 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.hxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.hxx @@ -194,7 +194,13 @@ public: using Laplacian::solve; FieldPerp solve(const FieldPerp& b) override; - FieldPerp solve(const FieldPerp& b, const FieldPerp& x0) override; + FieldPerp solve(const FieldPerp& b, const FieldPerp& x0) override { + return solve(b, x0, false); + } + FieldPerp solve(const FieldPerp& b, const FieldPerp& x0, bool forward); + + using Laplacian::forward; + FieldPerp forward(const FieldPerp& b) override { return solve(b, b, true); } int precon(Vec x, Vec y); ///< Preconditioner function diff --git a/src/invert/laplace/invert_laplace.cxx b/src/invert/laplace/invert_laplace.cxx index bd839256c3..897e7e45a9 100644 --- a/src/invert/laplace/invert_laplace.cxx +++ b/src/invert/laplace/invert_laplace.cxx @@ -256,6 +256,36 @@ Field2D Laplacian::solve(const Field2D& b, const Field2D& x0) { return DC(f); } +Field3D Laplacian::forward(const Field3D& b) { + TRACE("Laplacian::solve(Field3D, Field3D)"); + + ASSERT1(b.getLocation() == location); + ASSERT1(localmesh == b.getMesh()); + + // Setting the start and end range of the y-slices + int ys = localmesh->ystart, ye = localmesh->yend; + if (include_yguards && localmesh->hasBndryLowerY()) { + ys = 0; // Mesh contains a lower boundary + } + if (include_yguards && localmesh->hasBndryUpperY()) { + ye = localmesh->LocalNy - 1; // Contains upper boundary + } + + Field3D x{emptyFrom(b)}; + + for (int jy = ys; jy <= ye; jy++) { + // 1. Slice b and x (i.e. take a X-Z plane out of the field) + // 2. Send them to the solver of the implementation (determined during creation) + x = forward(sliceXZ(b, jy)); + } + + return x; +} + +FieldPerp Laplacian::forward([[maybe_unused]] const FieldPerp& b) { + throw BoutException("Not implemented for this inversion"); +} + /********************************************************************************** * MATRIX ELEMENTS **********************************************************************************/ diff --git a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja index 659ad8ff6d..71ca09cb46 100644 --- a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja +++ b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja @@ -92,7 +92,8 @@ cdef extern from "bout/invert_laplace.hxx": cppclass Laplacian: @staticmethod unique_ptr[Laplacian] create(Options*, benum.CELL_LOC, Mesh*, Solver*) - Field3D solve(Field3D,Field3D) + Field3D solve(Field3D, Field3D) + Field3D forward(Field3D) void setCoefA(Field3D) void setCoefC(Field3D) void setCoefC1(Field3D) diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 7b07cd8296..39aa327cb9 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -905,7 +905,7 @@ cdef class Laplacian: self.cobj = c.Laplacian.create(copt, cloc, cmesh, NULL) self.isSelfOwned = True - def solve(self,Field3D x, Field3D guess): + def solve(self, Field3D x, Field3D guess): """ Calculate the Laplacian inversion @@ -924,6 +924,24 @@ cdef class Laplacian: """ return f3dFromObj(deref(self.cobj).solve(x.cobj[0],guess.cobj[0])) + def forward(self, Field3D x): + """ + Calculate the Laplacian + + Parameters + ---------- + x : Field3D + Field to take the derivative + + + Returns + ------- + Field3D + the inversion of x, where guess is a guess to start with + """ + return f3dFromObj(deref(self.cobj).forward(x.cobj[0])) + + {% set coeffs="A C C1 C2 D Ex Ez".split() %} def setCoefs(self, *{% for coeff in coeffs %}, {{coeff}}=None{% endfor %}): """ From 552c2fd9c8c908d61e5a248e32dbdf1cf9941605 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 09:54:15 +0100 Subject: [PATCH 179/256] Add function to check wehther point is in the boundary --- include/bout/parallel_boundary_region.hxx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index d6bfc7556e..144d6c55ab 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -268,6 +268,15 @@ public: }); } + bool contains(const int ix, const int iy, const int iz) const { + const auto i2 = xyz2ind(ix, iy, iz, localmesh); + for (auto i1 : bndry_points) { + if (i1.index == i2) { + return true; + } + } + return false; + } // setter void setValid(char val) { bndry_position->valid = val; } From 00233cf1e658c2570a23f8e89f39a685d8af1b17 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 09:53:15 +0100 Subject: [PATCH 180/256] Add offset to parallel boundary region This allows to extend the boundary code to place the boundary further away from the boundary. --- include/bout/parallel_boundary_region.hxx | 18 ++++++++++++++---- src/mesh/parallel/fci.cxx | 20 ++++++++++++++------ src/mesh/parallel/shiftedmetricinterp.cxx | 8 ++++---- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 144d6c55ab..d6234117bf 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -36,6 +36,8 @@ struct Indices { // BoutReal angle; // How many points we can go in the opposite direction signed char valid; + signed char offset; + unsigned char abs_offset; }; using IndicesVec = std::vector; @@ -59,6 +61,9 @@ public: BoutReal s_z() const { return bndry_position->intersection.s_z; } BoutReal length() const { return bndry_position->length; } signed char valid() const { return bndry_position->valid; } + signed char offset() const { return bndry_position->offset; } + unsigned char abs_offset() const { return bndry_position->abs_offset; } + // extrapolate a given point to the boundary BoutReal extrapolate_sheath_o1(const Field3D& f) const { return f[ind()]; } @@ -246,12 +251,17 @@ public: /// Add a point to the boundary void add_point(Ind3D ind, BoutReal x, BoutReal y, BoutReal z, BoutReal length, - char valid) { - bndry_points.push_back({ind, {x, y, z}, length, valid}); + char valid, signed char offset) { + bndry_points.push_back({ind, + {x, y, z}, + length, + valid, + offset, + static_cast(std::abs(offset))}); } void add_point(int ix, int iy, int iz, BoutReal x, BoutReal y, BoutReal z, - BoutReal length, char valid) { - bndry_points.push_back({xyz2ind(ix, iy, iz, localmesh), {x, y, z}, length, valid}); + BoutReal length, char valid, signed char offset) { + add_point(xyz2ind(ix, iy, iz, localmesh), x, y, z, length, valid, offset); } // final, so they can be inlined diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 758b26a377..7629cbe9c4 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -144,7 +144,12 @@ void load_parallel_metric_components([[maybe_unused]] Coordinates* coords, [[may #undef LOAD_PAR #endif } - + +template +int sgn(T val) { + return (T(0) < val) - (val < T(0)); +} + } // namespace FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& options, @@ -254,6 +259,8 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& BoutMask to_remove(map_mesh); const int xend = map_mesh.xstart + (map_mesh.xend - map_mesh.xstart + 1) * map_mesh.getNXPE() - 1; + // Default to the maximum number of points + const int defValid{map_mesh.ystart - 1 + std::abs(offset)}; // Serial loop because call to BoundaryRegionPar::addPoint // (probably?) can't be done in parallel BOUT_FOR_SERIAL(i, xt_prime.getRegion("RGN_NOBNDRY")) { @@ -322,11 +329,12 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& // need at least 2 points in the domain. ASSERT2(map_mesh.xend - map_mesh.xstart >= 2); auto boundary = (xt_prime[i] < map_mesh.xstart) ? inner_boundary : outer_boundary; - boundary->add_point(x, y, z, x + dx, y + 0.5 * offset, - z + dz, // Intersection point in local index space - 0.5, // Distance to intersection - 1 // Default to that there is a point in the other direction - ); + if (!boundary->contains(x, y, z)) { + boundary->add_point(x, y, z, x + dx, y + offset - sgn(offset) * 0.5, + z + dz, // Intersection point in local index space + std::abs(offset) - 0.5, // Distance to intersection + defValid, offset); + } } region_no_boundary = region_no_boundary.mask(to_remove); diff --git a/src/mesh/parallel/shiftedmetricinterp.cxx b/src/mesh/parallel/shiftedmetricinterp.cxx index ce27843267..dfb397c626 100644 --- a/src/mesh/parallel/shiftedmetricinterp.cxx +++ b/src/mesh/parallel/shiftedmetricinterp.cxx @@ -135,7 +135,7 @@ ShiftedMetricInterp::ShiftedMetricInterp(Mesh& mesh, CELL_LOC location_in, 0.25 * (1 // dy/2 + dy(it.ind, mesh.yend + 1) / dy(it.ind, mesh.yend)), // length - yvalid); + yvalid, 1); } } auto backward_boundary_xin = std::make_shared( @@ -151,7 +151,7 @@ ShiftedMetricInterp::ShiftedMetricInterp(Mesh& mesh, CELL_LOC location_in, 0.25 * (1 // dy/2 + dy(it.ind, mesh.ystart - 1) / dy(it.ind, mesh.ystart)), - yvalid); + yvalid, -1); } } // Create regions for parallel boundary conditions @@ -168,7 +168,7 @@ ShiftedMetricInterp::ShiftedMetricInterp(Mesh& mesh, CELL_LOC location_in, 0.25 * (1 // dy/2 + dy(it.ind, mesh.yend + 1) / dy(it.ind, mesh.yend)), - yvalid); + yvalid, 1); } } auto backward_boundary_xout = std::make_shared( @@ -184,7 +184,7 @@ ShiftedMetricInterp::ShiftedMetricInterp(Mesh& mesh, CELL_LOC location_in, 0.25 * (dy(it.ind, mesh.ystart - 1) / dy(it.ind, mesh.ystart) // dy/2 + 1), - yvalid); + yvalid, -1); } } From 136fa8a0b8f8b6c784ac2d12c4e119b5153aff86 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 09:55:59 +0100 Subject: [PATCH 181/256] Add setValid to BoundaryRegionParIterBase --- include/bout/parallel_boundary_region.hxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index d6234117bf..8d8cabe295 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -64,6 +64,8 @@ public: signed char offset() const { return bndry_position->offset; } unsigned char abs_offset() const { return bndry_position->abs_offset; } + // setter + void setValid(signed char valid) { bndry_position->valid = valid; } // extrapolate a given point to the boundary BoutReal extrapolate_sheath_o1(const Field3D& f) const { return f[ind()]; } From 0e089cd40c18a6ab365f85fdaa72a07501f9564f Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 09:57:32 +0100 Subject: [PATCH 182/256] Reimplement ynext for the boundary region This is more general and takes the offset() into account, and thus works for cases where the boundary is between the first and second guard cell --- include/bout/parallel_boundary_region.hxx | 49 ++++++++++++++++++----- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 8d8cabe295..5aa5403104 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -186,18 +186,49 @@ public: parallel_stencil::neumann_o3(1 - length(), value, 1, f[ind()], 2, yprev(f)); } - // BoutReal get(const Field3D& f, int off) - const BoutReal& ynext(const Field3D& f) const { return f.ynext(dir)[ind().yp(dir)]; } - BoutReal& ynext(Field3D& f) const { return f.ynext(dir)[ind().yp(dir)]; } + template + BoutReal& getAt(Field3D& f, int off) const { + if constexpr (check) { + ASSERT3(valid() > -off - 2); + } + auto _off = offset() + off * dir; + return f.ynext(_off)[ind().yp(_off)]; + } + template + const BoutReal& getAt(const Field3D& f, int off) const { + if constexpr (check) { + ASSERT3(valid() > -off - 2); + } + auto _off = offset() + off * dir; + return f.ynext(_off)[ind().yp(_off)]; + } - const BoutReal& yprev(const Field3D& f) const { - ASSERT3(valid() > 0); - return f.ynext(-dir)[ind().yp(-dir)]; + const BoutReal& ynext(const Field3D& f) const { return getAt(f, 0); } + BoutReal& ynext(Field3D& f) const { return getAt(f, 0); } + const BoutReal& ythis(const Field3D& f) const { return getAt(f, -1); } + BoutReal& ythis(Field3D& f) const { return getAt(f, -1); } + const BoutReal& yprev(const Field3D& f) const { return getAt(f, -2); } + BoutReal& yprev(Field3D& f) const { return getAt(f, -2); } + + template + BoutReal getAt(const std::function& f, + int off) const { + if constexpr (check) { + ASSERT3(valid() > -off - 2); + } + auto _off = offset() + off * dir; + return f(_off, ind().yp(_off)); } - BoutReal& yprev(Field3D& f) const { - ASSERT3(valid() > 0); - return f.ynext(-dir)[ind().yp(-dir)]; + BoutReal ynext(const std::function& f) const { + return getAt(f, 0); } + BoutReal ythis(const std::function& f) const { + return getAt(f, -1); + } + BoutReal yprev(const std::function& f) const { + return getAt(f, -2); + } + void setYPrevIfValid(Field3D& f, BoutReal val) const { if (valid() > 0) { yprev(f) = val; From 7e1067a7188ef4163f915a670ff9ea2988154195 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 09:58:41 +0100 Subject: [PATCH 183/256] Fix extrapolaton / interpolation --- include/bout/parallel_boundary_region.hxx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 5aa5403104..41b06c693c 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -68,17 +68,17 @@ public: void setValid(signed char valid) { bndry_position->valid = valid; } // extrapolate a given point to the boundary - BoutReal extrapolate_sheath_o1(const Field3D& f) const { return f[ind()]; } + BoutReal extrapolate_sheath_o1(const Field3D& f) const { return ythis(f); } BoutReal extrapolate_sheath_o2(const Field3D& f) const { ASSERT3(valid() >= 0); if (valid() < 1) { return extrapolate_sheath_o1(f); } - return f[ind()] * (1 + length()) - f.ynext(-dir)[ind().yp(-dir)] * length(); + return ythis(f) * (1 + length()) - yprev(f) * length(); } inline BoutReal extrapolate_sheath_o1(const std::function& f) const { - return f(0, ind()); + return ythis(f); } inline BoutReal extrapolate_sheath_o2(const std::function& f) const { @@ -86,25 +86,25 @@ public: if (valid() < 1) { return extrapolate_sheath_o1(f); } - return f(0, ind()) * (1 + length()) - f(-dir, ind().yp(-dir)) * length(); + return ythis(f) * (1 + length()) - yprev(f) * length(); } inline BoutReal interpolate_sheath_o1(const Field3D& f) const { - return f[ind()] * (1 - length()) + ynext(f) * length(); + return ythis(f) * (1 - length()) + ynext(f) * length(); } - inline BoutReal extrapolate_next_o1(const Field3D& f) const { return f[ind()]; } + inline BoutReal extrapolate_next_o1(const Field3D& f) const { return ythis(f); } inline BoutReal extrapolate_next_o2(const Field3D& f) const { ASSERT3(valid() >= 0); if (valid() < 1) { return extrapolate_next_o1(f); } - return f[ind()] * 2 - f.ynext(-dir)[ind().yp(-dir)]; + return ythis(f) * 2 - yprev(f); } inline BoutReal extrapolate_next_o1(const std::function& f) const { - return f(0, ind()); + return ythis(f); } inline BoutReal extrapolate_next_o2(const std::function& f) const { @@ -112,7 +112,7 @@ public: if (valid() < 1) { return extrapolate_sheath_o1(f); } - return f(0, ind()) * 2 - f(-dir, ind().yp(-dir)); + return ythis(f) * 2 - yprev(f); } // extrapolate the gradient into the boundary @@ -122,7 +122,7 @@ public: if (valid() < 1) { return extrapolate_grad_o1(f); } - return f[ind()] - f.ynext(-dir)[ind().yp(-dir)]; + return ythis(f) - ynext(f); } BoundaryRegionParIterBase& operator*() { return *this; } @@ -320,6 +320,7 @@ public: } return false; } + // setter void setValid(char val) { bndry_position->valid = val; } From d416b5d939df6d2416153879769b591dc3f530de Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 11:12:38 +0100 Subject: [PATCH 184/256] Fix parallel boundary to interpolate into the boundary Previously only the first boundary point was set --- include/bout/parallel_boundary_region.hxx | 32 ++++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 41b06c693c..c637b9e908 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -136,17 +136,20 @@ public: return bndry_position != rhs.bndry_position; } +#define ITER() for (int i = 0; i < localmesh->ystart - abs_offset(); ++i) // dirichlet boundary code void dirichlet_o1(Field3D& f, BoutReal value) const { - f.ynext(dir)[ind().yp(dir)] = value; + ITER() { getAt(f, i) = value; } } void dirichlet_o2(Field3D& f, BoutReal value) const { if (length() < small_value) { return dirichlet_o1(f, value); } - ynext(f) = parallel_stencil::dirichlet_o2(1, f[ind()], 1 - length(), value); - // ynext(f) = f[ind()] * (1 + 1/length()) + value / length(); + ITER() { + getAt(f, i) = + parallel_stencil::dirichlet_o2(i + 1, ythis(f), i + 1 - length(), value); + } } void dirichlet_o3(Field3D& f, BoutReal value) const { @@ -155,17 +158,24 @@ public: return dirichlet_o2(f, value); } if (length() < small_value) { - ynext(f) = parallel_stencil::dirichlet_o2(2, yprev(f), 1 - length(), value); + ITER() { + getAt(f, i) = + parallel_stencil::dirichlet_o2(i + 2, yprev(f), i + 1 - length(), value); + } } else { - ynext(f) = - parallel_stencil::dirichlet_o3(2, yprev(f), 1, f[ind()], 1 - length(), value); + ITER() { + getAt(f, i) = parallel_stencil::dirichlet_o3(i + 2, yprev(f), i + 1, ythis(f), + i + 1 - length(), value); + } } } // NB: value needs to be scaled by dy // neumann_o1 is actually o2 if we would use an appropriate one-sided stencil. // But in general we do not, and thus for normal C2 stencils, this is 1st order. - void neumann_o1(Field3D& f, BoutReal value) const { ynext(f) = f[ind()] + value; } + void neumann_o1(Field3D& f, BoutReal value) const { + ITER() { getAt(f, i) = ythis(f) + value; } + } // NB: value needs to be scaled by dy void neumann_o2(Field3D& f, BoutReal value) const { @@ -173,7 +183,7 @@ public: if (valid() < 1) { return neumann_o1(f, value); } - ynext(f) = yprev(f) + 2 * value; + ITER() { getAt(f, i) = yprev(f) + 2 * value; } } // NB: value needs to be scaled by dy @@ -182,8 +192,10 @@ public: if (valid() < 1) { return neumann_o1(f, value); } - ynext(f) = - parallel_stencil::neumann_o3(1 - length(), value, 1, f[ind()], 2, yprev(f)); + ITER() { + getAt(f, i) = parallel_stencil::neumann_o3(i + 1 - length(), value, i + 1, ythis(f), + 2, yprev(f)); + } } template From 6cca6a41c055be1ab988db3ebd65d8662b683047 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 11:13:14 +0100 Subject: [PATCH 185/256] Ensure data is sorted --- include/bout/parallel_boundary_region.hxx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index c637b9e908..7e4e340c8a 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -297,6 +297,9 @@ public: /// Add a point to the boundary void add_point(Ind3D ind, BoutReal x, BoutReal y, BoutReal z, BoutReal length, char valid, signed char offset) { + if (!bndry_points.empty() && bndry_points.back().index > ind) { + is_sorted = false; + } bndry_points.push_back({ind, {x, y, z}, length, @@ -315,6 +318,7 @@ public: bool isDone() final { return (bndry_position == std::end(bndry_points)); } bool contains(const BoundaryRegionPar& bndry) const { + ASSERT2(is_sorted); return std::binary_search(std::begin(bndry_points), std::end(bndry_points), *bndry.bndry_position, [](const bout::parallel_boundary_region::Indices& i1, @@ -362,6 +366,7 @@ private: const int nz = mesh->LocalNz; return Ind3D{(x * ny + y) * nz + z, ny, nz}; } + bool is_sorted{true}; }; #endif // BOUT_PAR_BNDRY_H From 62b62bf44189357959ba5dd338a5bee75edffb6b Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 11:13:26 +0100 Subject: [PATCH 186/256] fix typo --- src/mesh/parallel/fci.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 7629cbe9c4..07a7a6490b 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -159,7 +159,7 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& region_no_boundary(map_mesh.getRegion("RGN_NOBNDRY")), corner_boundary_mask(map_mesh) { - TRACE("Creating FCIMAP for direction {:d}", offset); + TRACE("Creating FCIMap for direction {:d}", offset); if (offset == 0) { throw BoutException( From 628a6ce705cedddda561bb26b89bbb8f66534701 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 13 Jan 2025 11:13:59 +0100 Subject: [PATCH 187/256] Calculate valid for the general case --- src/mesh/parallel/fci.hxx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index 7085a71535..f993812a43 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -100,7 +100,6 @@ public: field_line_maps.emplace_back(mesh, dy, options, -offset, backward_boundary_xin, backward_boundary_xout, zperiodic); } - ASSERT0(mesh.ystart == 1); std::shared_ptr bndries[]{ forward_boundary_xin, forward_boundary_xout, backward_boundary_xin, backward_boundary_xout}; @@ -109,9 +108,13 @@ public: if (bndry->dir == bndry2->dir) { continue; } - for (bndry->first(); !bndry->isDone(); bndry->next()) { - if (bndry2->contains(*bndry)) { - bndry->setValid(0); + for (auto pnt : *bndry) { + for (auto pnt2 : *bndry2) { +#warning this could likely be done faster + if (pnt.ind() == pnt2.ind()) { + pnt.setValid( + static_cast(std::abs((pnt2.offset() - pnt.offset())) - 2)); + } } } } From e5c9fc125e7cbc34fb1358342bf08fdea6e97338 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 15 Jan 2025 13:20:10 +0100 Subject: [PATCH 188/256] Add communication routine for FCI operation For FCI we need to be able to access "random" data from the adjacent slices. If they are split in x-direction, this requires some tricky communication pattern. It can be used like this: ``` // Create object GlobalField3DAccess fci_comm(thismesh); // let it know what data points will be required: // where IndG3D is an index in the global field, which would be the // normal Ind3D if there would be only one proc. fci_comm.get(IndG3D(i, ny, nz)); // If all index have been added, the communication pattern will be // established. This has to be called by all processors in parallel fci_comm.setup() // Once the data for a given field is needed, it needs to be // communicated: GlobalField3DAccessInstance global_data = fci_comm.communicate(f3d); // and can be accessed like this BoutReal data = global_data[IndG3D(i, ny, nz)]; // ny and nz in the IndG3D are always optional. ``` --- CMakeLists.txt | 2 + include/bout/region.hxx | 3 +- src/mesh/parallel/fci_comm.cxx | 34 +++++ src/mesh/parallel/fci_comm.hxx | 241 +++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 src/mesh/parallel/fci_comm.cxx create mode 100644 src/mesh/parallel/fci_comm.hxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 7df044c867..781fc65672 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -274,6 +274,8 @@ set(BOUT_SOURCES ./src/mesh/mesh.cxx ./src/mesh/parallel/fci.cxx ./src/mesh/parallel/fci.hxx + ./src/mesh/parallel/fci_comm.cxx + ./src/mesh/parallel/fci_comm.hxx ./src/mesh/parallel/identity.cxx ./src/mesh/parallel/shiftedmetric.cxx ./src/mesh/parallel/shiftedmetricinterp.cxx diff --git a/include/bout/region.hxx b/include/bout/region.hxx index bb1cf82bf1..f441b3edd7 100644 --- a/include/bout/region.hxx +++ b/include/bout/region.hxx @@ -139,7 +139,7 @@ class BoutMask; BOUT_FOR_OMP(index, (region), for schedule(BOUT_OPENMP_SCHEDULE) nowait) // NOLINTEND(cppcoreguidelines-macro-usage,bugprone-macro-parentheses) -enum class IND_TYPE { IND_3D = 0, IND_2D = 1, IND_PERP = 2 }; +enum class IND_TYPE { IND_3D = 0, IND_2D = 1, IND_PERP = 2, IND_GLOBAL_3D }; /// Indices base class for Fields -- Regions are dereferenced into these /// @@ -386,6 +386,7 @@ inline SpecificInd operator-(SpecificInd lhs, const SpecificInd& rhs) { using Ind3D = SpecificInd; using Ind2D = SpecificInd; using IndPerp = SpecificInd; +using IndG3D = SpecificInd; /// Get string representation of Ind3D inline std::string toString(const Ind3D& i) { diff --git a/src/mesh/parallel/fci_comm.cxx b/src/mesh/parallel/fci_comm.cxx new file mode 100644 index 0000000000..c0d51d1eb9 --- /dev/null +++ b/src/mesh/parallel/fci_comm.cxx @@ -0,0 +1,34 @@ +/************************************************************************** + * Communication for Flux-coordinate Independent interpolation + * + ************************************************************************** + * Copyright 2025 BOUT++ contributors + * + * Contact: Ben Dudson, dudson2@llnl.gov + * + * This file is part of BOUT++. + * + * BOUT++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * BOUT++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with BOUT++. If not, see . + * + **************************************************************************/ + +#include "fci_comm.hxx" + +#include + +const BoutReal& GlobalField3DAccessInstance::operator[](IndG3D ind) const { + auto it = gfa.mapping.find(ind.ind); + ASSERT2(it != gfa.mapping.end()); + return data[it->second]; +} diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx new file mode 100644 index 0000000000..aa3b5ecfb5 --- /dev/null +++ b/src/mesh/parallel/fci_comm.hxx @@ -0,0 +1,241 @@ +/************************************************************************** + * Communication for Flux-coordinate Independent interpolation + * + ************************************************************************** + * Copyright 2025 BOUT++ contributors + * + * Contact: Ben Dudson, dudson2@llnl.gov + * + * This file is part of BOUT++. + * + * BOUT++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * BOUT++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with BOUT++. If not, see . + * + **************************************************************************/ + +#pragma once + +#include "bout/assert.hxx" +#include "bout/bout_types.hxx" +#include "bout/boutcomm.hxx" +#include "bout/field3d.hxx" +#include "bout/mesh.hxx" +#include "bout/region.hxx" +#include +#include +#include +#include +#include +#include +#include +class GlobalField3DAccess; + +namespace fci_comm { +struct ProcLocal { + int proc; + int ind; +}; +struct globalToLocal1D { + const int mg; + const int npe; + const int localwith; + const int local; + const int global; + const int globalwith; + globalToLocal1D(int mg, int npe, int localwith) + : mg(mg), npe(npe), localwith(localwith), local(localwith - 2 * mg), + global(local * npe), globalwith(global + 2 * mg) {}; + ProcLocal convert(int id) const { + int idwo = id - mg; + int proc = idwo / local; + if (proc >= npe) { + proc = npe - 1; + } + ASSERT2(proc >= 0); + int loc = id - local * proc; + ASSERT2(0 <= loc); + ASSERT2(loc < (local + 2 * mg)); + return {proc, loc}; + } +}; +template +struct XYZ2Ind { + const int nx; + const int ny; + const int nz; + ind convert(const int x, const int y, const int z) const { + return {z + (y + x * ny) * nz, ny, nz}; + } + ind operator()(const int x, const int y, const int z) const { return convert(x, y, z); } + XYZ2Ind(const int nx, const int ny, const int nz) : nx(nx), ny(ny), nz(nz) {} +}; +} // namespace fci_comm + +class GlobalField3DAccessInstance { +public: + const BoutReal& operator[](IndG3D ind) const; + + GlobalField3DAccessInstance(const GlobalField3DAccess* gfa, + const std::vector&& data) + : gfa(*gfa), data(std::move(data)) {}; + +private: + const GlobalField3DAccess& gfa; + const std::vector data; +}; + +class GlobalField3DAccess { +public: + friend class GlobalField3DAccessInstance; + GlobalField3DAccess(Mesh* mesh) + : mesh(mesh), g2lx(mesh->xstart, mesh->getNXPE(), mesh->LocalNx), + g2ly(mesh->ystart, mesh->getNYPE(), mesh->LocalNy), + g2lz(mesh->zstart, 1, mesh->LocalNz), + xyzl(g2lx.localwith, g2ly.localwith, g2lz.localwith), + xyzg(g2lx.globalwith, g2ly.globalwith, g2lz.globalwith), comm(BoutComm::get()) {}; + void get(IndG3D ind) { ids.emplace(ind.ind); } + void operator[](IndG3D ind) { return get(ind); } + void setup() { + ASSERT2(is_setup == false); + toGet.resize(g2lx.npe * g2ly.npe * g2lz.npe); + for (const auto id : ids) { + IndG3D gind{id, g2ly.globalwith, g2lz.globalwith}; + const auto pix = g2lx.convert(gind.x()); + const auto piy = g2ly.convert(gind.y()); + const auto piz = g2lz.convert(gind.z()); + ASSERT3(piz.proc == 0); + toGet[piy.proc * g2lx.npe + pix.proc].push_back( + xyzl.convert(pix.ind, piy.ind, piz.ind).ind); + } + for (auto v : toGet) { + std::sort(v.begin(), v.end()); + } + commCommLists(); + { + int offset = 0; + for (auto get : toGet) { + offsets.push_back(offset); + offset += get.size(); + } + offsets.push_back(offset); + } + std::map mapping; + for (const auto id : ids) { + IndG3D gind{id, g2ly.globalwith, g2lz.globalwith}; + const auto pix = g2lx.convert(gind.x()); + const auto piy = g2ly.convert(gind.y()); + const auto piz = g2lz.convert(gind.z()); + ASSERT3(piz.proc == 0); + const auto proc = piy.proc * g2lx.npe + pix.proc; + const auto& vec = toGet[proc]; + auto it = + std::find(vec.begin(), vec.end(), xyzl.convert(pix.ind, piy.ind, piz.ind).ind); + ASSERT3(it != vec.end()); + mapping[id] = it - vec.begin() + offsets[proc]; + } + is_setup = true; + } + GlobalField3DAccessInstance communicate(const Field3D& f) { + return {this, communicate_data(f)}; + } + std::unique_ptr communicate_asPtr(const Field3D& f) { + return std::make_unique(this, communicate_data(f)); + } + +private: + void commCommLists() { + toSend.resize(toGet.size()); + std::vector toGetSizes(toGet.size()); + std::vector toSendSizes(toSend.size()); + //const int thisproc = mesh->getYProcIndex() * g2lx.npe + mesh->getXProcIndex(); + std::vector reqs(toSend.size()); + for (size_t proc = 0; proc < toGet.size(); ++proc) { + auto ret = MPI_Irecv(static_cast(&toSendSizes[proc]), 1, MPI_INT, proc, + 666 + proc, comm, &reqs[proc]); + ASSERT0(ret == MPI_SUCCESS); + } + for (size_t proc = 0; proc < toGet.size(); ++proc) { + toGetSizes[proc] = toGet[proc].size(); + sendBufferSize += toGetSizes[proc]; + auto ret = MPI_Send(static_cast(&toGetSizes[proc]), 1, MPI_INT, proc, + 666 + proc, comm); + ASSERT0(ret == MPI_SUCCESS); + } + for ([[maybe_unused]] auto dummy : reqs) { + int ind{0}; + auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); + ASSERT0(ret == MPI_SUCCESS); + ASSERT3(ind != MPI_UNDEFINED); + toSend[ind].resize(toSendSizes[ind]); + ret = MPI_Irecv(static_cast(&toSend[ind]), toSend[ind].size(), MPI_INT, ind, + 666 * 666 + ind, comm, &reqs[ind]); + ASSERT0(ret == MPI_SUCCESS); + } + for (size_t proc = 0; proc < toGet.size(); ++proc) { + const auto ret = MPI_Send(static_cast(&toGet[proc]), toGet[proc].size(), + MPI_INT, proc, 666 * 666 + proc, comm); + ASSERT0(ret == MPI_SUCCESS); + } + for ([[maybe_unused]] auto dummy : reqs) { + int ind{0}; + const auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); + ASSERT0(ret == MPI_SUCCESS); + ASSERT3(ind != MPI_UNDEFINED); + } + } + Mesh* mesh; + std::set ids; + std::map mapping; + bool is_setup{false}; + const fci_comm::globalToLocal1D g2lx; + const fci_comm::globalToLocal1D g2ly; + const fci_comm::globalToLocal1D g2lz; + +public: + const fci_comm::XYZ2Ind xyzl; + const fci_comm::XYZ2Ind xyzg; + +private: + std::vector> toGet; + std::vector> toSend; + std::vector offsets; + int sendBufferSize{0}; + MPI_Comm comm; + std::vector communicate_data(const Field3D& f) { + ASSERT2(f.getMesh() == mesh); + std::vector data(offsets.back()); + std::vector sendBuffer(sendBufferSize); + std::vector reqs(toSend.size()); + for (size_t proc = 0; proc < toGet.size(); ++proc) { + auto ret = MPI_Irecv(static_cast(&data[proc]), toGet[proc].size(), + MPI_DOUBLE, proc, 666 + proc, comm, &reqs[proc]); + ASSERT0(ret == MPI_SUCCESS); + } + int cnt = 0; + for (size_t proc = 0; proc < toGet.size(); ++proc) { + void* start = static_cast(&sendBuffer[cnt]); + for (auto i : toSend[proc]) { + sendBuffer[cnt++] = f[Ind3D(i)]; + } + auto ret = MPI_Send(start, toSend[proc].size(), MPI_DOUBLE, proc, 666 + proc, comm); + ASSERT0(ret == MPI_SUCCESS); + } + for ([[maybe_unused]] auto dummy : reqs) { + int ind{0}; + auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); + ASSERT0(ret == MPI_SUCCESS); + ASSERT3(ind != MPI_UNDEFINED); + } + return data; + } +}; From 40dac599408ce1b2f2699e48960e83ed4f97d7bc Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 15 Jan 2025 13:26:37 +0100 Subject: [PATCH 189/256] Unify XZMonotonicHermiteSpline and XZMonotonicHermiteSpline If they are two instances of the same template, this allows to have an if in the inner loop that can be optimised out. --- CMakeLists.txt | 1 - include/bout/interpolation_xz.hxx | 46 ++----- src/mesh/interpolation/hermite_spline_xz.cxx | 74 +++++++++-- .../monotonic_hermite_spline_xz.cxx | 117 ------------------ src/mesh/interpolation_xz.cxx | 1 + 5 files changed, 76 insertions(+), 163 deletions(-) delete mode 100644 src/mesh/interpolation/monotonic_hermite_spline_xz.cxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 781fc65672..9e57495885 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -269,7 +269,6 @@ set(BOUT_SOURCES ./src/mesh/interpolation/hermite_spline_z.cxx ./src/mesh/interpolation/interpolation_z.cxx ./src/mesh/interpolation/lagrange_4pt_xz.cxx - ./src/mesh/interpolation/monotonic_hermite_spline_xz.cxx ./src/mesh/invert3x3.hxx ./src/mesh/mesh.cxx ./src/mesh/parallel/fci.cxx diff --git a/include/bout/interpolation_xz.hxx b/include/bout/interpolation_xz.hxx index def8a60a3e..261b4c1515 100644 --- a/include/bout/interpolation_xz.hxx +++ b/include/bout/interpolation_xz.hxx @@ -36,6 +36,7 @@ #endif class Options; +class GlobalField3DAccess; /// Interpolate a field onto a perturbed set of points const Field3D interpolate(const Field3D& f, const Field3D& delta_x, @@ -133,7 +134,8 @@ public: } }; -class XZHermiteSpline : public XZInterpolation { +template +class XZHermiteSplineBase : public XZInterpolation { protected: /// This is protected rather than private so that it can be /// extended and used by HermiteSplineMonotonic @@ -141,6 +143,9 @@ protected: Tensor> i_corner; // index of bottom-left grid point Tensor k_corner; // z-index of bottom-left grid point + std::unique_ptr gf3daccess; + Tensor> g3dinds; + // Basis functions for cubic Hermite spline interpolation // see http://en.wikipedia.org/wiki/Cubic_Hermite_spline // The h00 and h01 basis functions are applied to the function itself @@ -166,13 +171,13 @@ protected: #endif public: - XZHermiteSpline(Mesh* mesh = nullptr) : XZHermiteSpline(0, mesh) {} - XZHermiteSpline(int y_offset = 0, Mesh* mesh = nullptr); - XZHermiteSpline(const BoutMask& mask, int y_offset = 0, Mesh* mesh = nullptr) - : XZHermiteSpline(y_offset, mesh) { + XZHermiteSplineBase(Mesh* mesh = nullptr) : XZHermiteSplineBase(0, mesh) {} + XZHermiteSplineBase(int y_offset = 0, Mesh* mesh = nullptr); + XZHermiteSplineBase(const BoutMask& mask, int y_offset = 0, Mesh* mesh = nullptr) + : XZHermiteSplineBase(y_offset, mesh) { setRegion(regionFromMask(mask, localmesh)); } - ~XZHermiteSpline() { + ~XZHermiteSplineBase() { #if HS_USE_PETSC if (isInit) { MatDestroy(&petscWeights); @@ -210,33 +215,8 @@ public: /// but also degrades accuracy near maxima and minima. /// Perhaps should only impose near boundaries, since that is where /// problems most obviously occur. -class XZMonotonicHermiteSpline : public XZHermiteSpline { -public: - XZMonotonicHermiteSpline(Mesh* mesh = nullptr) : XZHermiteSpline(0, mesh) { - if (localmesh->getNXPE() > 1) { - throw BoutException("Do not support MPI splitting in X"); - } - } - XZMonotonicHermiteSpline(int y_offset = 0, Mesh* mesh = nullptr) - : XZHermiteSpline(y_offset, mesh) { - if (localmesh->getNXPE() > 1) { - throw BoutException("Do not support MPI splitting in X"); - } - } - XZMonotonicHermiteSpline(const BoutMask& mask, int y_offset = 0, Mesh* mesh = nullptr) - : XZHermiteSpline(mask, y_offset, mesh) { - if (localmesh->getNXPE() > 1) { - throw BoutException("Do not support MPI splitting in X"); - } - } - - using XZHermiteSpline::interpolate; - /// Interpolate using precalculated weights. - /// This function is called by the other interpolate functions - /// in the base class XZHermiteSpline. - Field3D interpolate(const Field3D& f, - const std::string& region = "RGN_NOBNDRY") const override; -}; +using XZMonotonicHermiteSpline = XZHermiteSplineBase; +using XZHermiteSpline = XZHermiteSplineBase; class XZLagrange4pt : public XZInterpolation { Tensor i_corner; // x-index of bottom-left grid point diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 650e4022e7..85786d5381 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -21,6 +21,7 @@ **************************************************************************/ #include "../impls/bout/boutmesh.hxx" +#include "../parallel/fci_comm.hxx" #include "bout/globals.hxx" #include "bout/index_derivs_interface.hxx" #include "bout/interpolation_xz.hxx" @@ -101,7 +102,8 @@ class IndConverter { } }; -XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* meshin) +template +XZHermiteSplineBase::XZHermiteSplineBase(int y_offset, Mesh* meshin) : XZInterpolation(y_offset, meshin), h00_x(localmesh), h01_x(localmesh), h10_x(localmesh), h11_x(localmesh), h00_z(localmesh), h01_z(localmesh), h10_z(localmesh), h11_z(localmesh) { @@ -145,6 +147,10 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* meshin) MatCreateAIJ(MPI_COMM_WORLD, m, m, M, M, 16, nullptr, 16, nullptr, &petscWeights); #endif #endif + if constexpr (monotonic) { + gf3daccess = std::make_unique(localmesh); + g3dinds.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); + } #ifndef HS_USE_PETSC if (localmesh->getNXPE() > 1) { throw BoutException("Require PETSc for MPI splitting in X"); @@ -152,8 +158,10 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* meshin) #endif } -void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, - const std::string& region) { +template +void XZHermiteSplineBase::calcWeights(const Field3D& delta_x, + const Field3D& delta_z, + const std::string& region) { const int ny = localmesh->LocalNy; const int nz = localmesh->LocalNz; @@ -294,6 +302,14 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z } #endif #endif + if constexpr (monotonic) { + const auto gind = gf3daccess->xyzg(i_corn, y + y_offset, k_corner(x, y, z)); + gf3daccess->get(gind); + gf3daccess->get(gind.xp(1)); + gf3daccess->get(gind.zp(1)); + gf3daccess->get(gind.xp(1).zp(1)); + g3dinds[i] = {gind.ind, gind.xp(1).ind, gind.zp(1).ind, gind.xp(1).zp(1).ind}; + } } #ifdef HS_USE_PETSC MatAssemblyBegin(petscWeights, MAT_FINAL_ASSEMBLY); @@ -305,8 +321,11 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z #endif } -void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z, - const BoutMask& mask, const std::string& region) { +template +void XZHermiteSplineBase::calcWeights(const Field3D& delta_x, + const Field3D& delta_z, + const BoutMask& mask, + const std::string& region) { setMask(mask); calcWeights(delta_x, delta_z, region); } @@ -327,8 +346,10 @@ void XZHermiteSpline::calcWeights(const Field3D& delta_x, const Field3D& delta_z * (i, j+1, k+1) h01_z + h10_z / 2 * (i, j+1, k+2) h11_z / 2 */ +template std::vector -XZHermiteSpline::getWeightsForYApproximation(int i, int j, int k, int yoffset) { +XZHermiteSplineBase::getWeightsForYApproximation(int i, int j, int k, + int yoffset) { const int nz = localmesh->LocalNz; const int k_mod = k_corner(i, j, k); const int k_mod_m1 = (k_mod > 0) ? (k_mod - 1) : (nz - 1); @@ -341,7 +362,9 @@ XZHermiteSpline::getWeightsForYApproximation(int i, int j, int k, int yoffset) { {i, j + yoffset, k_mod_p2, 0.5 * h11_z(i, j, k)}}; } -Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region) const { +template +Field3D XZHermiteSplineBase::interpolate(const Field3D& f, + const std::string& region) const { ASSERT1(f.getMesh() == localmesh); Field3D f_interp{emptyFrom(f)}; @@ -387,6 +410,11 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region Field3D fz = bout::derivatives::index::DDZ(f, CELL_DEFAULT, "DEFAULT", region2); Field3D fxz = bout::derivatives::index::DDZ(fx, CELL_DEFAULT, "DEFAULT", region2); + std::unique_ptr g3d; + if constexpr (monotonic) { + gf = gf3daccess.communicate_asPtr(f); + } + BOUT_FOR(i, getRegion(region)) { const auto iyp = i.yp(y_offset); @@ -415,6 +443,19 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region f_interp[iyp] = +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; + if constexpr (monotonic) { + const auto corners = {gf[g3dinds[i][0]], gf[g3dinds[i][1]], gf[g3dinds[i][2]], + gf[g3dinds[i][3]]}; + const auto minmax = std::minmax(corners); + if (f_interp[iyp] < minmax.first) { + f_interp[iyp] = minmax.first; + } else { + if (f_interp[iyp] > minmax.second) { + f_interp[iyp] = minmax.second; + } + } + } + ASSERT2(std::isfinite(f_interp[iyp]) || i.x() < localmesh->xstart || i.x() > localmesh->xend); } @@ -424,15 +465,24 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region return f_interp; } -Field3D XZHermiteSpline::interpolate(const Field3D& f, const Field3D& delta_x, - const Field3D& delta_z, const std::string& region) { +template +Field3D XZHermiteSplineBase::interpolate(const Field3D& f, + const Field3D& delta_x, + const Field3D& delta_z, + const std::string& region) { calcWeights(delta_x, delta_z, region); return interpolate(f, region); } -Field3D XZHermiteSpline::interpolate(const Field3D& f, const Field3D& delta_x, - const Field3D& delta_z, const BoutMask& mask, - const std::string& region) { +template +Field3D +XZHermiteSplineBase::interpolate(const Field3D& f, const Field3D& delta_x, + const Field3D& delta_z, const BoutMask& mask, + const std::string& region) { calcWeights(delta_x, delta_z, mask, region); return interpolate(f, region); } + +// ensure they are instantiated +template class XZHermiteSplineBase; +template class XZHermiteSplineBase; diff --git a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx b/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx deleted file mode 100644 index 4b84bcd265..0000000000 --- a/src/mesh/interpolation/monotonic_hermite_spline_xz.cxx +++ /dev/null @@ -1,117 +0,0 @@ -/************************************************************************** - * Copyright 2018 B.D.Dudson, P. Hill - * - * Contact: Ben Dudson, bd512@york.ac.uk - * - * This file is part of BOUT++. - * - * BOUT++ is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * BOUT++ is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with BOUT++. If not, see . - * - **************************************************************************/ - -#include "bout/globals.hxx" -#include "bout/index_derivs_interface.hxx" -#include "bout/interpolation_xz.hxx" -#include "bout/mesh.hxx" - -#include - -Field3D XZMonotonicHermiteSpline::interpolate(const Field3D& f, - const std::string& region) const { - ASSERT1(f.getMesh() == localmesh); - Field3D f_interp(f.getMesh()); - f_interp.allocate(); - - // Derivatives are used for tension and need to be on dimensionless - // coordinates - Field3D fx = bout::derivatives::index::DDX(f, CELL_DEFAULT, "DEFAULT"); - localmesh->communicateXZ(fx); - // communicate in y, but do not calculate parallel slices - { - auto h = localmesh->sendY(fx); - localmesh->wait(h); - } - Field3D fz = bout::derivatives::index::DDZ(f, CELL_DEFAULT, "DEFAULT", "RGN_ALL"); - localmesh->communicateXZ(fz); - // communicate in y, but do not calculate parallel slices - { - auto h = localmesh->sendY(fz); - localmesh->wait(h); - } - Field3D fxz = bout::derivatives::index::DDX(fz, CELL_DEFAULT, "DEFAULT"); - localmesh->communicateXZ(fxz); - // communicate in y, but do not calculate parallel slices - { - auto h = localmesh->sendY(fxz); - localmesh->wait(h); - } - - const auto curregion{getRegion(region)}; - BOUT_FOR(i, curregion) { - const auto iyp = i.yp(y_offset); - - const auto ic = i_corner[i]; - const auto iczp = ic.zp(); - const auto icxp = ic.xp(); - const auto icxpzp = iczp.xp(); - - // Interpolate f in X at Z - const BoutReal f_z = - f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; - - // Interpolate f in X at Z+1 - const BoutReal f_zp1 = f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + fx[iczp] * h10_x[i] - + fx[icxpzp] * h11_x[i]; - - // Interpolate fz in X at Z - const BoutReal fz_z = fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + fxz[ic] * h10_x[i] - + fxz[icxp] * h11_x[i]; - - // Interpolate fz in X at Z+1 - const BoutReal fz_zp1 = fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] - + fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; - - // Interpolate in Z - BoutReal result = - +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; - - ASSERT2(std::isfinite(result) || i.x() < localmesh->xstart - || i.x() > localmesh->xend); - - // Monotonicity - // Force the interpolated result to be in the range of the - // neighbouring cell values. This prevents unphysical overshoots, - // but also degrades accuracy near maxima and minima. - // Perhaps should only impose near boundaries, since that is where - // problems most obviously occur. - const BoutReal localmax = BOUTMAX(f[ic], f[icxp], f[iczp], f[icxpzp]); - - const BoutReal localmin = BOUTMIN(f[ic], f[icxp], f[iczp], f[icxpzp]); - - ASSERT2(std::isfinite(localmax) || i.x() < localmesh->xstart - || i.x() > localmesh->xend); - ASSERT2(std::isfinite(localmin) || i.x() < localmesh->xstart - || i.x() > localmesh->xend); - - if (result > localmax) { - result = localmax; - } - if (result < localmin) { - result = localmin; - } - - f_interp[iyp] = result; - } - return f_interp; -} diff --git a/src/mesh/interpolation_xz.cxx b/src/mesh/interpolation_xz.cxx index f7f0b457f2..0bc25111ab 100644 --- a/src/mesh/interpolation_xz.cxx +++ b/src/mesh/interpolation_xz.cxx @@ -23,6 +23,7 @@ * **************************************************************************/ +#include "parallel/fci_comm.hxx" #include #include #include From 9a98a8be4defbf918189c8f8986c62d3898514bd Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 15 Jan 2025 13:26:59 +0100 Subject: [PATCH 190/256] Use x-splitting for monotonichermitespline test --- tests/MMS/spatial/fci/runtest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index b51c311a50..f817db0b73 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -53,7 +53,7 @@ for nslice in nslices: "hermitespline", "lagrange4pt", "bilinear", - # "monotonichermitespline", + "monotonichermitespline", ]: error_2[nslice] = [] error_inf[nslice] = [] @@ -97,7 +97,7 @@ for nslice in nslices: nslice, yperiodic, method_orders[nslice]["name"], - 2 if conf.has["petsc"] and method == "hermitespline" else 1, + 2 if conf.has["petsc"] and "hermitespline" in method else 1, ) args += f" mesh:paralleltransform:xzinterpolation:type={method}" From cb068ea5ac27a54e95e9be7ad8a27b3829a58ca3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 15 Jan 2025 13:45:20 +0100 Subject: [PATCH 191/256] Fix position of maybe_unused for old gcc --- src/mesh/boundary_standard.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/boundary_standard.cxx b/src/mesh/boundary_standard.cxx index 367f6b7d54..8c6c0d19fc 100644 --- a/src/mesh/boundary_standard.cxx +++ b/src/mesh/boundary_standard.cxx @@ -1593,7 +1593,7 @@ BoundaryOp* BoundaryNeumann_NonOrthogonal::clone(BoundaryRegion* region, return new BoundaryNeumann_NonOrthogonal(region); } -void BoundaryNeumann_NonOrthogonal::apply(Field2D& [[maybe_unused]] f) { +void BoundaryNeumann_NonOrthogonal::apply([[maybe_unused]] Field2D& f) { #if not(BOUT_USE_METRIC_3D) Mesh* mesh = bndry->localmesh; ASSERT1(mesh == f.getMesh()); @@ -1728,7 +1728,7 @@ void BoundaryNeumann_NonOrthogonal::apply(Field3D& f) { void BoundaryNeumann::apply(Field2D & f) { BoundaryNeumann::apply(f, 0.); } - void BoundaryNeumann::apply(Field2D& [[maybe_unused]] f, BoutReal t) { + void BoundaryNeumann::apply([[maybe_unused]] Field2D& f, BoutReal t) { // Set (at 2nd order / 3rd order) the value at the mid-point between // the guard cell and the grid cell to be val // N.B. First guard cells (closest to the grid) is 2nd order, while From d7d7c91338816f4ef6fd5ffce38e811b48a09887 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 15 Jan 2025 13:45:48 +0100 Subject: [PATCH 192/256] Ensure PETSC_SUCCESS is defined --- src/invert/laplace/impls/petsc/petsc_laplace.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.cxx b/src/invert/laplace/impls/petsc/petsc_laplace.cxx index d0d68bee52..c2b62dc570 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.cxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.cxx @@ -29,6 +29,7 @@ #if BOUT_HAS_PETSC #include "petsc_laplace.hxx" +#include "petscsys.h" #include #include From a05201bebe94884dfae9b916b63b0446cd40889b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 15 Jan 2025 14:06:44 +0100 Subject: [PATCH 193/256] Revert "Ensure PETSC_SUCCESS is defined" This reverts commit d7d7c91338816f4ef6fd5ffce38e811b48a09887. --- src/invert/laplace/impls/petsc/petsc_laplace.cxx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.cxx b/src/invert/laplace/impls/petsc/petsc_laplace.cxx index c2b62dc570..d0d68bee52 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.cxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.cxx @@ -29,7 +29,6 @@ #if BOUT_HAS_PETSC #include "petsc_laplace.hxx" -#include "petscsys.h" #include #include From 7d73c5c771e1fac9c50b4babcc73ea565147fec8 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 15 Jan 2025 14:07:06 +0100 Subject: [PATCH 194/256] Define PETSC_SUCCESS for old petsc versions --- include/bout/petsclib.hxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/bout/petsclib.hxx b/include/bout/petsclib.hxx index 2008671286..aa6f874f11 100644 --- a/include/bout/petsclib.hxx +++ b/include/bout/petsclib.hxx @@ -156,6 +156,10 @@ private: #endif // PETSC_VERSION_GE +#if ! PETSC_VERSION_GE(3, 19, 0) +#define PETSC_SUCCESS ((PetscErrorCode)0) +#endif + #else // BOUT_HAS_PETSC #include "bout/unused.hxx" From ebf2d07d071b4a1a2b99c78f14cb0e7022593c96 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 16 Jan 2025 09:07:51 +0100 Subject: [PATCH 195/256] Set region for lagrange4pt --- src/mesh/interpolation/lagrange_4pt_xz.cxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mesh/interpolation/lagrange_4pt_xz.cxx b/src/mesh/interpolation/lagrange_4pt_xz.cxx index 8fa201ba72..16368299a0 100644 --- a/src/mesh/interpolation/lagrange_4pt_xz.cxx +++ b/src/mesh/interpolation/lagrange_4pt_xz.cxx @@ -132,7 +132,11 @@ Field3D XZLagrange4pt::interpolate(const Field3D& f, const std::string& region) // Then in X f_interp(x, y_next, z) = lagrange_4pt(xvals, t_x(x, y, z)); + ASSERT2(std::isfinite(f_interp(x, y_next, z))); + } + const auto region2 = y_offset != 0 ? fmt::format("RGN_YPAR_{:+d}", y_offset) : region; + f_interp.setRegion(region2); return f_interp; } From ae1fcfe067c4a499ec957107c2c470b53088c93a Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 20 Jan 2025 16:28:15 +0100 Subject: [PATCH 196/256] Fix split parallel slices --- src/field/field3d.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 2f97e8d02b..33980e22df 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -160,7 +160,7 @@ void Field3D::splitParallelSlices() { ydown_fields.emplace_back(fieldmesh); if (isFci()) { yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", i + 1)); - yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", -i - 1)); + ydown_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", -i - 1)); } } } From 8b7d97d01cabd5b198ff1b784d4833652c86c30b Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 20 Jan 2025 16:28:31 +0100 Subject: [PATCH 197/256] Add option to split + allocate --- include/bout/field3d.hxx | 2 ++ src/field/field3d.cxx | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index eaae59a7f9..bc87c6ba3f 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -237,6 +237,8 @@ public: */ void splitParallelSlices(); + void splitParallelSlicesAndAllocate(); + /*! * Clear the parallel slices, yup and ydown */ diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 33980e22df..1d323ade29 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -164,6 +164,14 @@ void Field3D::splitParallelSlices() { } } } +void Field3D::splitParallelSlicesAndAllocate() { + splitParallelSlices(); + allocate(); + for (int i = 0; i < fieldmesh->ystart; ++i) { + yup_fields[i].allocate(); + ydown_fields[i].allocate(); + } +} void Field3D::clearParallelSlices() { TRACE("Field3D::clearParallelSlices"); From ad56d8e77ea5c8924a9e3bd0f18fdbdbede76f7e Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 20 Jan 2025 16:29:05 +0100 Subject: [PATCH 198/256] fix parallel neumann BC --- include/bout/parallel_boundary_region.hxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 7e4e340c8a..2cbb0977e7 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -174,7 +174,7 @@ public: // neumann_o1 is actually o2 if we would use an appropriate one-sided stencil. // But in general we do not, and thus for normal C2 stencils, this is 1st order. void neumann_o1(Field3D& f, BoutReal value) const { - ITER() { getAt(f, i) = ythis(f) + value; } + ITER() { getAt(f, i) = ythis(f) + value * (i + 1); } } // NB: value needs to be scaled by dy @@ -183,14 +183,14 @@ public: if (valid() < 1) { return neumann_o1(f, value); } - ITER() { getAt(f, i) = yprev(f) + 2 * value; } + ITER() { getAt(f, i) = yprev(f) + (2 + i) * value; } } // NB: value needs to be scaled by dy void neumann_o3(Field3D& f, BoutReal value) const { ASSERT3(valid() >= 0); if (valid() < 1) { - return neumann_o1(f, value); + return neumann_o2(f, value); } ITER() { getAt(f, i) = parallel_stencil::neumann_o3(i + 1 - length(), value, i + 1, ythis(f), From 2a4834138371eca80ba6f4442c19da18388e138d Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 20 Jan 2025 16:30:25 +0100 Subject: [PATCH 199/256] set name for parallel component We have the info, so might as well store it --- src/mesh/parallel/fci.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 07a7a6490b..cf6b8cb986 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -97,6 +97,7 @@ bool load_parallel_metric_component(std::string name, Field3D& component, int of auto& pcom = component.ynext(offset); pcom.allocate(); pcom.setRegion(fmt::format("RGN_YPAR_{:+d}", offset)); + pcom.name = name; BOUT_FOR(i, component.getRegion("RGN_NOBNDRY")) { pcom[i.yp(offset)] = tmp[i]; } From f73813be1fb6cda77d42a98e4c7540be2f281fb4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 20 Jan 2025 16:30:54 +0100 Subject: [PATCH 200/256] Add limitFree Taken from hermes-3, adopted for higher order --- include/bout/parallel_boundary_region.hxx | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 2cbb0977e7..5c0df00be6 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -44,7 +44,21 @@ using IndicesVec = std::vector; using IndicesIter = IndicesVec::iterator; using IndicesIterConst = IndicesVec::const_iterator; -//} +inline BoutReal limitFreeScale(BoutReal fm, BoutReal fc) { + if (fm < fc) { + return 1; // Neumann rather than increasing into boundary + } + if (fm < 1e-10) { + return 1; // Low / no density condition + } + BoutReal fp = fc / fm; +#if CHECKLEVEL >= 2 + if (!std::isfinite(fp)) { + throw BoutException("SheathBoundaryParallel limitFree: {}, {} -> {}", fm, fc, fp); + } +#endif + return fp; +} template class BoundaryRegionParIterBase { @@ -198,6 +212,17 @@ public: } } + // extrapolate into the boundary using only monotonic decreasing values. + // f needs to be positive + void limitFree(Field3D& f) const { + const auto fac = valid() > 0 ? limitFreeScale(yprev(f), ythis(f)) : 1; + auto val = ythis(f); + ITER() { + val *= fac; + getAt(f, i) = val; + } + } + template BoutReal& getAt(Field3D& f, int off) const { if constexpr (check) { From ff7525db692dee68d90f90ac5a93f1eeba630336 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 20 Jan 2025 16:31:18 +0100 Subject: [PATCH 201/256] add setAll to parallel BC --- include/bout/parallel_boundary_region.hxx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index 5c0df00be6..f808296edb 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -223,6 +223,12 @@ public: } } + void setAll(Field3D& f, const BoutReal val) const { + for (int i = -localmesh->ystart; i <= localmesh->ystart; ++i) { + f.ynext(i)[ind().yp(i)] = val; + } + } + template BoutReal& getAt(Field3D& f, int off) const { if constexpr (check) { From 1c44f98bd6925f9a3393e3cb73fbc194f27826bc Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 10:43:58 +0100 Subject: [PATCH 202/256] Add monotonic check also to other code branches --- src/mesh/interpolation/hermite_spline_xz.cxx | 27 +++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 85786d5381..70c311c08f 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -35,7 +35,7 @@ class IndConverter { xstart(mesh->xstart), ystart(mesh->ystart), zstart(0), lnx(mesh->LocalNx - 2 * xstart), lny(mesh->LocalNy - 2 * ystart), lnz(mesh->LocalNz - 2 * zstart) {} - // ix and iy are global indices + // ix and iz are global indices // iy is local int fromMeshToGlobal(int ix, int iy, int iz) { const int xstart = mesh->xstart; @@ -382,6 +382,19 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, VecGetArrayRead(result, &cptr); BOUT_FOR(i, f.getRegion(region2)) { f_interp[i] = cptr[int(i)]; + if constexpr (monotonic) { + const auto corners = {gf[g3dinds[i][0]], gf[g3dinds[i][1]], gf[g3dinds[i][2]], + gf[g3dinds[i][3]]}; + const auto minmax = std::minmax(corners); + if (f_interp[iyp] < minmax.first) { + f_interp[iyp] = minmax.first; + } else { + if (f_interp[iyp] > minmax.second) { + f_interp[iyp] = minmax.second; + } + } + } + ASSERT2(std::isfinite(cptr[int(i)])); } VecRestoreArrayRead(result, &cptr); @@ -397,6 +410,18 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, f_interp[iyp] += newWeights[w * 4 + 2][i] * f[ic.zp().xp(w - 1)]; f_interp[iyp] += newWeights[w * 4 + 3][i] * f[ic.zp(2).xp(w - 1)]; } + if constexpr (monotonic) { + const auto corners = {gf[g3dinds[i][0]], gf[g3dinds[i][1]], gf[g3dinds[i][2]], + gf[g3dinds[i][3]]}; + const auto minmax = std::minmax(corners); + if (f_interp[iyp] < minmax.first) { + f_interp[iyp] = minmax.first; + } else { + if (f_interp[iyp] > minmax.second) { + f_interp[iyp] = minmax.second; + } + } + } } #endif #else From 42c095874b30dad76aec6dab9777a1980b62583f Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 13:31:14 +0100 Subject: [PATCH 203/256] use lower_bound instead of find lower_bound takes into account the data is sorted --- src/mesh/parallel/fci_comm.hxx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index aa3b5ecfb5..a847700548 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -138,10 +138,11 @@ public: ASSERT3(piz.proc == 0); const auto proc = piy.proc * g2lx.npe + pix.proc; const auto& vec = toGet[proc]; - auto it = - std::find(vec.begin(), vec.end(), xyzl.convert(pix.ind, piy.ind, piz.ind).ind); + const auto tofind = xyzl.convert(pix.ind, piy.ind, piz.ind).ind; + auto it = std::lower_bound(vec.begin(), vec.end(), tofind); ASSERT3(it != vec.end()); - mapping[id] = it - vec.begin() + offsets[proc]; + ASSERT3(*it == tofind); + mapping[id] = std::distance(vec.begin(), it) + offsets[proc]; } is_setup = true; } From 21d19850f8529b8cf61caf87e99f3f155a294b08 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 13:31:27 +0100 Subject: [PATCH 204/256] Do not shadow mapping --- src/mesh/parallel/fci_comm.hxx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index a847700548..5250fb99d3 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -129,7 +129,6 @@ public: } offsets.push_back(offset); } - std::map mapping; for (const auto id : ids) { IndG3D gind{id, g2ly.globalwith, g2lz.globalwith}; const auto pix = g2lx.convert(gind.x()); From 4f39c1dd336035a44bc8edc928448ba4f45d57a6 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 13:32:01 +0100 Subject: [PATCH 205/256] Ensure setup has been called --- src/mesh/parallel/fci_comm.hxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 5250fb99d3..6c83d58f63 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -212,6 +212,7 @@ private: int sendBufferSize{0}; MPI_Comm comm; std::vector communicate_data(const Field3D& f) { + ASSERT2(is_setup); ASSERT2(f.getMesh() == mesh); std::vector data(offsets.back()); std::vector sendBuffer(sendBufferSize); From cabdc4cdaf713aed543f170c5afda9aac9463fc1 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 13:32:12 +0100 Subject: [PATCH 206/256] Use pointer to data --- src/mesh/parallel/fci_comm.hxx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 6c83d58f63..d44eea76ef 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -176,13 +176,14 @@ private: auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); ASSERT0(ret == MPI_SUCCESS); ASSERT3(ind != MPI_UNDEFINED); + ASSERT2(static_cast(ind) < toSend.size()); toSend[ind].resize(toSendSizes[ind]); - ret = MPI_Irecv(static_cast(&toSend[ind]), toSend[ind].size(), MPI_INT, ind, - 666 * 666 + ind, comm, &reqs[ind]); + ret = MPI_Irecv(static_cast(&toSend[ind][0]), toSend[ind].size(), MPI_INT, + ind, 666 * 666 + ind, comm, &reqs[ind]); ASSERT0(ret == MPI_SUCCESS); } for (size_t proc = 0; proc < toGet.size(); ++proc) { - const auto ret = MPI_Send(static_cast(&toGet[proc]), toGet[proc].size(), + const auto ret = MPI_Send(static_cast(&toGet[proc][0]), toGet[proc].size(), MPI_INT, proc, 666 * 666 + proc, comm); ASSERT0(ret == MPI_SUCCESS); } From aa7938dff460874b49be12663368279f23870802 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 13:32:36 +0100 Subject: [PATCH 207/256] Call setup before communicator is used --- src/mesh/interpolation/hermite_spline_xz.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 70c311c08f..6932e5d7e4 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -311,6 +311,9 @@ void XZHermiteSplineBase::calcWeights(const Field3D& delta_x, g3dinds[i] = {gind.ind, gind.xp(1).ind, gind.zp(1).ind, gind.xp(1).zp(1).ind}; } } + if constexpr (monotonic) { + gf3daccess->setup(); + } #ifdef HS_USE_PETSC MatAssemblyBegin(petscWeights, MAT_FINAL_ASSEMBLY); MatAssemblyEnd(petscWeights, MAT_FINAL_ASSEMBLY); From d1dee59a984a2b0e452713579f0444255bf885d0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 13:34:36 +0100 Subject: [PATCH 208/256] Deduplicate code Ensures we all ways check for monotonicity --- src/mesh/interpolation/hermite_spline_xz.cxx | 61 +++++++------------- 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 6932e5d7e4..b8b30e68d8 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -374,8 +374,12 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, const auto region2 = y_offset != 0 ? fmt::format("RGN_YPAR_{:+d}", y_offset) : region; -#if USE_NEW_WEIGHTS -#ifdef HS_USE_PETSC + std::unique_ptr gf; + if constexpr (monotonic) { + gf = gf3daccess->communicate_asPtr(f); + } + +#if USE_NEW_WEIGHTS and defined(HS_USE_PETSC) BoutReal* ptr; const BoutReal* cptr; VecGetArray(rhs, &ptr); @@ -386,22 +390,9 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, BOUT_FOR(i, f.getRegion(region2)) { f_interp[i] = cptr[int(i)]; if constexpr (monotonic) { - const auto corners = {gf[g3dinds[i][0]], gf[g3dinds[i][1]], gf[g3dinds[i][2]], - gf[g3dinds[i][3]]}; - const auto minmax = std::minmax(corners); - if (f_interp[iyp] < minmax.first) { - f_interp[iyp] = minmax.first; - } else { - if (f_interp[iyp] > minmax.second) { - f_interp[iyp] = minmax.second; - } - } - } - - ASSERT2(std::isfinite(cptr[int(i)])); - } - VecRestoreArrayRead(result, &cptr); -#else + const auto iyp = i; + const auto i = iyp.ym(y_offset); +#elif USE_NEW_WEIGHTS // No Petsc BOUT_FOR(i, getRegion(region)) { auto ic = i_corner[i]; auto iyp = i.yp(y_offset); @@ -414,20 +405,7 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, f_interp[iyp] += newWeights[w * 4 + 3][i] * f[ic.zp(2).xp(w - 1)]; } if constexpr (monotonic) { - const auto corners = {gf[g3dinds[i][0]], gf[g3dinds[i][1]], gf[g3dinds[i][2]], - gf[g3dinds[i][3]]}; - const auto minmax = std::minmax(corners); - if (f_interp[iyp] < minmax.first) { - f_interp[iyp] = minmax.first; - } else { - if (f_interp[iyp] > minmax.second) { - f_interp[iyp] = minmax.second; - } - } - } - } -#endif -#else +#else // Legacy interpolation // Derivatives are used for tension and need to be on dimensionless // coordinates @@ -438,11 +416,6 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, Field3D fz = bout::derivatives::index::DDZ(f, CELL_DEFAULT, "DEFAULT", region2); Field3D fxz = bout::derivatives::index::DDZ(fx, CELL_DEFAULT, "DEFAULT", region2); - std::unique_ptr g3d; - if constexpr (monotonic) { - gf = gf3daccess.communicate_asPtr(f); - } - BOUT_FOR(i, getRegion(region)) { const auto iyp = i.yp(y_offset); @@ -472,8 +445,9 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; if constexpr (monotonic) { - const auto corners = {gf[g3dinds[i][0]], gf[g3dinds[i][1]], gf[g3dinds[i][2]], - gf[g3dinds[i][3]]}; +#endif + const auto corners = {(*gf)[IndG3D(g3dinds[i][0])], (*gf)[IndG3D(g3dinds[i][1])], + (*gf)[IndG3D(g3dinds[i][2])], (*gf)[IndG3D(g3dinds[i][3])]}; const auto minmax = std::minmax(corners); if (f_interp[iyp] < minmax.first) { f_interp[iyp] = minmax.first; @@ -483,7 +457,14 @@ Field3D XZHermiteSplineBase::interpolate(const Field3D& f, } } } - +#if USE_NEW_WEIGHTS and defined(HS_USE_PETSC) + ASSERT2(std::isfinite(cptr[int(i)])); + } + VecRestoreArrayRead(result, &cptr); +#elif USE_NEW_WEIGHTS + ASSERT2(std::isfinite(f_interp[iyp])); + } +#else ASSERT2(std::isfinite(f_interp[iyp]) || i.x() < localmesh->xstart || i.x() > localmesh->xend); } From 58183272043740f43b6079d781ee9e27370f7a35 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 14:30:05 +0100 Subject: [PATCH 209/256] Fix tags for comm Tags were different for sender and receiver --- src/mesh/parallel/fci_comm.hxx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index d44eea76ef..c0227f4773 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -161,14 +161,14 @@ private: std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { auto ret = MPI_Irecv(static_cast(&toSendSizes[proc]), 1, MPI_INT, proc, - 666 + proc, comm, &reqs[proc]); + 666, comm, &reqs[proc]); ASSERT0(ret == MPI_SUCCESS); } for (size_t proc = 0; proc < toGet.size(); ++proc) { toGetSizes[proc] = toGet[proc].size(); sendBufferSize += toGetSizes[proc]; auto ret = MPI_Send(static_cast(&toGetSizes[proc]), 1, MPI_INT, proc, - 666 + proc, comm); + 666, comm); ASSERT0(ret == MPI_SUCCESS); } for ([[maybe_unused]] auto dummy : reqs) { @@ -179,12 +179,12 @@ private: ASSERT2(static_cast(ind) < toSend.size()); toSend[ind].resize(toSendSizes[ind]); ret = MPI_Irecv(static_cast(&toSend[ind][0]), toSend[ind].size(), MPI_INT, - ind, 666 * 666 + ind, comm, &reqs[ind]); + ind, 666 * 666, comm, &reqs[ind]); ASSERT0(ret == MPI_SUCCESS); } for (size_t proc = 0; proc < toGet.size(); ++proc) { const auto ret = MPI_Send(static_cast(&toGet[proc][0]), toGet[proc].size(), - MPI_INT, proc, 666 * 666 + proc, comm); + MPI_INT, proc, 666 * 666, comm); ASSERT0(ret == MPI_SUCCESS); } for ([[maybe_unused]] auto dummy : reqs) { @@ -220,7 +220,7 @@ private: std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { auto ret = MPI_Irecv(static_cast(&data[proc]), toGet[proc].size(), - MPI_DOUBLE, proc, 666 + proc, comm, &reqs[proc]); + MPI_DOUBLE, proc, 666, comm, &reqs[proc]); ASSERT0(ret == MPI_SUCCESS); } int cnt = 0; @@ -229,7 +229,7 @@ private: for (auto i : toSend[proc]) { sendBuffer[cnt++] = f[Ind3D(i)]; } - auto ret = MPI_Send(start, toSend[proc].size(), MPI_DOUBLE, proc, 666 + proc, comm); + auto ret = MPI_Send(start, toSend[proc].size(), MPI_DOUBLE, proc, 666, comm); ASSERT0(ret == MPI_SUCCESS); } for ([[maybe_unused]] auto dummy : reqs) { From aa2d8b6ea7f1c22a0cb0f96a32fb67b61a4d446c Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 14:39:43 +0100 Subject: [PATCH 210/256] Use pointer instead of std::vector --- src/mesh/parallel/fci_comm.hxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index c0227f4773..257639bb32 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -216,7 +216,8 @@ private: ASSERT2(is_setup); ASSERT2(f.getMesh() == mesh); std::vector data(offsets.back()); - std::vector sendBuffer(sendBufferSize); + //std::vector sendBuffer(sendBufferSize); + BoutReal* sendBuffer = new BoutReal[sendBufferSize]; std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { auto ret = MPI_Irecv(static_cast(&data[proc]), toGet[proc].size(), @@ -238,6 +239,7 @@ private: ASSERT0(ret == MPI_SUCCESS); ASSERT3(ind != MPI_UNDEFINED); } + delete[] sendBuffer; return data; } }; From 007fed0936d0abe3884c16d1b4273074a2b11efc Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Jan 2025 14:43:12 +0100 Subject: [PATCH 211/256] Do not reuse requests if the array is still in use Otherwise mpi might wait for the wrong request. --- src/mesh/parallel/fci_comm.hxx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 257639bb32..ec34d622a6 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -171,6 +171,7 @@ private: 666, comm); ASSERT0(ret == MPI_SUCCESS); } + std::vector reqs2(toSend.size()); for ([[maybe_unused]] auto dummy : reqs) { int ind{0}; auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); @@ -179,7 +180,7 @@ private: ASSERT2(static_cast(ind) < toSend.size()); toSend[ind].resize(toSendSizes[ind]); ret = MPI_Irecv(static_cast(&toSend[ind][0]), toSend[ind].size(), MPI_INT, - ind, 666 * 666, comm, &reqs[ind]); + ind, 666 * 666, comm, &reqs2[ind]); ASSERT0(ret == MPI_SUCCESS); } for (size_t proc = 0; proc < toGet.size(); ++proc) { @@ -189,7 +190,7 @@ private: } for ([[maybe_unused]] auto dummy : reqs) { int ind{0}; - const auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); + const auto ret = MPI_Waitany(reqs.size(), &reqs2[0], &ind, MPI_STATUS_IGNORE); ASSERT0(ret == MPI_SUCCESS); ASSERT3(ind != MPI_UNDEFINED); } From c40110b31ceb2cd81c11dbb389853f70ee7c491c Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 10:27:11 +0100 Subject: [PATCH 212/256] rename offset to getOffsets to avoid confusion whether the offsets are for sending or receiving --- src/mesh/parallel/fci_comm.hxx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index ec34d622a6..f079385dc1 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -124,10 +124,10 @@ public: { int offset = 0; for (auto get : toGet) { - offsets.push_back(offset); + getOffsets.push_back(offset); offset += get.size(); } - offsets.push_back(offset); + getOffsets.push_back(offset); } for (const auto id : ids) { IndG3D gind{id, g2ly.globalwith, g2lz.globalwith}; @@ -141,7 +141,7 @@ public: auto it = std::lower_bound(vec.begin(), vec.end(), tofind); ASSERT3(it != vec.end()); ASSERT3(*it == tofind); - mapping[id] = std::distance(vec.begin(), it) + offsets[proc]; + mapping[id] = std::distance(vec.begin(), it) + getOffsets[proc]; } is_setup = true; } @@ -155,9 +155,8 @@ public: private: void commCommLists() { toSend.resize(toGet.size()); - std::vector toGetSizes(toGet.size()); - std::vector toSendSizes(toSend.size()); - //const int thisproc = mesh->getYProcIndex() * g2lx.npe + mesh->getXProcIndex(); + std::vector toGetSizes(toGet.size(), -1); + std::vector toSendSizes(toSend.size(), -1); std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { auto ret = MPI_Irecv(static_cast(&toSendSizes[proc]), 1, MPI_INT, proc, @@ -210,15 +209,15 @@ public: private: std::vector> toGet; std::vector> toSend; - std::vector offsets; + std::vector getOffsets; int sendBufferSize{0}; MPI_Comm comm; std::vector communicate_data(const Field3D& f) { ASSERT2(is_setup); ASSERT2(f.getMesh() == mesh); - std::vector data(offsets.back()); //std::vector sendBuffer(sendBufferSize); BoutReal* sendBuffer = new BoutReal[sendBufferSize]; + std::vector data(getOffsets.back()); std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { auto ret = MPI_Irecv(static_cast(&data[proc]), toGet[proc].size(), From 2a10ccd6395adaf2d3e482167ad17cc946138f41 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 10:28:04 +0100 Subject: [PATCH 213/256] Fix: mixup of sending / receiving size --- src/mesh/parallel/fci_comm.hxx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index f079385dc1..0926f5c415 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -165,7 +165,6 @@ private: } for (size_t proc = 0; proc < toGet.size(); ++proc) { toGetSizes[proc] = toGet[proc].size(); - sendBufferSize += toGetSizes[proc]; auto ret = MPI_Send(static_cast(&toGetSizes[proc]), 1, MPI_INT, proc, 666, comm); ASSERT0(ret == MPI_SUCCESS); @@ -177,6 +176,8 @@ private: ASSERT0(ret == MPI_SUCCESS); ASSERT3(ind != MPI_UNDEFINED); ASSERT2(static_cast(ind) < toSend.size()); + ASSERT3(toSendSizes[ind] >= 0); + sendBufferSize += toSendSizes[ind]; toSend[ind].resize(toSendSizes[ind]); ret = MPI_Irecv(static_cast(&toSend[ind][0]), toSend[ind].size(), MPI_INT, ind, 666 * 666, comm, &reqs2[ind]); @@ -215,9 +216,8 @@ private: std::vector communicate_data(const Field3D& f) { ASSERT2(is_setup); ASSERT2(f.getMesh() == mesh); - //std::vector sendBuffer(sendBufferSize); - BoutReal* sendBuffer = new BoutReal[sendBufferSize]; std::vector data(getOffsets.back()); + std::vector sendBuffer(sendBufferSize); std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { auto ret = MPI_Irecv(static_cast(&data[proc]), toGet[proc].size(), @@ -239,7 +239,6 @@ private: ASSERT0(ret == MPI_SUCCESS); ASSERT3(ind != MPI_UNDEFINED); } - delete[] sendBuffer; return data; } }; From 296cc15747eb8c7c9737f24af9e481b3cd45ca03 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 10:28:43 +0100 Subject: [PATCH 214/256] Fix receive data offset --- src/mesh/parallel/fci_comm.hxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 0926f5c415..8db9d8ef14 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -220,8 +220,8 @@ private: std::vector sendBuffer(sendBufferSize); std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { - auto ret = MPI_Irecv(static_cast(&data[proc]), toGet[proc].size(), - MPI_DOUBLE, proc, 666, comm, &reqs[proc]); + auto ret = MPI_Irecv(static_cast(&data[getOffsets[proc]]), + toGet[proc].size(), MPI_DOUBLE, proc, 666, comm, &reqs[proc]); ASSERT0(ret == MPI_SUCCESS); } int cnt = 0; From 31d7702dd65fffff38ab957bc0d687b23c480fb9 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 10:30:47 +0100 Subject: [PATCH 215/256] Add check to ensure the proc layout is as expected --- src/mesh/parallel/fci_comm.hxx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 8db9d8ef14..30f7df76ee 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -157,6 +157,13 @@ private: toSend.resize(toGet.size()); std::vector toGetSizes(toGet.size(), -1); std::vector toSendSizes(toSend.size(), -1); +#if CHECK > 3 + { + int thisproc; + MPI_Comm_rank(comm, thisproc); + assert(thisproc == mesh->getYProcIndex() * g2lx.npe + mesh->getXProcIndex()); + } +#endif std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { auto ret = MPI_Irecv(static_cast(&toSendSizes[proc]), 1, MPI_INT, proc, From 71dd37c6138aa97267227b63bec2d76531d68f1f Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 10:31:54 +0100 Subject: [PATCH 216/256] clang-format --- src/mesh/parallel/fci_comm.hxx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 30f7df76ee..bd4b955f12 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -166,14 +166,14 @@ private: #endif std::vector reqs(toSend.size()); for (size_t proc = 0; proc < toGet.size(); ++proc) { - auto ret = MPI_Irecv(static_cast(&toSendSizes[proc]), 1, MPI_INT, proc, - 666, comm, &reqs[proc]); + auto ret = MPI_Irecv(static_cast(&toSendSizes[proc]), 1, MPI_INT, proc, 666, + comm, &reqs[proc]); ASSERT0(ret == MPI_SUCCESS); } for (size_t proc = 0; proc < toGet.size(); ++proc) { toGetSizes[proc] = toGet[proc].size(); - auto ret = MPI_Send(static_cast(&toGetSizes[proc]), 1, MPI_INT, proc, - 666, comm); + auto ret = + MPI_Send(static_cast(&toGetSizes[proc]), 1, MPI_INT, proc, 666, comm); ASSERT0(ret == MPI_SUCCESS); } std::vector reqs2(toSend.size()); From 90a7f4ffcaf7ddbd3f107bab0381f86d8886b50e Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 10:40:35 +0100 Subject: [PATCH 217/256] Use BOUT++ assert --- src/mesh/parallel/fci_comm.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index bd4b955f12..39348d2e10 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -161,7 +161,7 @@ private: { int thisproc; MPI_Comm_rank(comm, thisproc); - assert(thisproc == mesh->getYProcIndex() * g2lx.npe + mesh->getXProcIndex()); + ASSERT0(thisproc == mesh->getYProcIndex() * g2lx.npe + mesh->getXProcIndex()); } #endif std::vector reqs(toSend.size()); From db77ef3ce78280dbe1f1f3ff98452f6b1038f9de Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 10:41:15 +0100 Subject: [PATCH 218/256] Fix check --- src/mesh/parallel/fci_comm.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 39348d2e10..ec4aeaba49 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -160,7 +160,7 @@ private: #if CHECK > 3 { int thisproc; - MPI_Comm_rank(comm, thisproc); + MPI_Comm_rank(comm, &thisproc); ASSERT0(thisproc == mesh->getYProcIndex() * g2lx.npe + mesh->getXProcIndex()); } #endif From 147a874cd6ce6d8896a4f31b75cbfa6f75c1475b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 13:22:19 +0100 Subject: [PATCH 219/256] Expect lower convergence for monotonic correction --- tests/MMS/spatial/fci/runtest | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index 6248c75afb..a2785a24ae 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -159,7 +159,9 @@ for nslice in nslices: stdout.write(", {:f} (small spacing)".format(order)) # Should be close to the expected order - if order > method_orders[nslice]["order"] * 0.95: + if order > method_orders[nslice]["order"] * ( + 0.6 if "monot" in method else 0.95 + ): print("............ PASS\n") else: print("............ FAIL\n") From 81d929d7fc37697ad791e4334a7f16f372491a70 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 17:17:04 +0100 Subject: [PATCH 220/256] Add delay on error --- include/bout/physicsmodel.hxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/bout/physicsmodel.hxx b/include/bout/physicsmodel.hxx index 9fa25d8b0f..876f51940d 100644 --- a/include/bout/physicsmodel.hxx +++ b/include/bout/physicsmodel.hxx @@ -47,6 +47,8 @@ class PhysicsModel; #include "bout/unused.hxx" #include "bout/utils.hxx" +#include +#include #include #include @@ -435,6 +437,7 @@ private: } catch (const BoutException& e) { \ output << "Error encountered: " << e.what(); \ output << e.getBacktrace() << endl; \ + std::this_thread::sleep_for(std::chrono::milliseconds(100)); \ MPI_Abort(BoutComm::get(), 1); \ } \ BoutFinalise(); \ From 276007c0c5c892a5a877410ac131f4e679945832 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 31 Jan 2025 10:07:22 +0100 Subject: [PATCH 221/256] Move yboundary iterator from hermes to BOUT++ This allows to write code for FCI and non-FCI using templates. --- include/bout/boundary_iterator.hxx | 51 ++++++++++++++++++++++- include/bout/yboundary_regions.hxx | 66 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 include/bout/yboundary_regions.hxx diff --git a/include/bout/boundary_iterator.hxx b/include/bout/boundary_iterator.hxx index 93f02c004d..601f6a8a28 100644 --- a/include/bout/boundary_iterator.hxx +++ b/include/bout/boundary_iterator.hxx @@ -1,6 +1,7 @@ #pragma once #include "bout/mesh.hxx" +#include "bout/parallel_boundary_region.hxx" #include "bout/sys/parallel_stencils.hxx" #include "bout/sys/range.hxx" @@ -42,14 +43,62 @@ public: return 2 * f(0, ind()) - f(0, ind().yp(-by).xp(-bx)); } - BoutReal interpolate_sheath(const Field3D& f) const { + BoutReal interpolate_sheath_o1(const Field3D& f) const { return (f[ind()] + ynext(f)) * 0.5; } + BoutReal + extrapolate_sheath_o2(const std::function& f) const { + return 0.5 * (3 * f(0, ind()) - f(0, ind().yp(-by).xp(-bx))); + } + + void limitFree(Field3D& f) const { + const BoutReal fac = + bout::parallel_boundary_region::limitFreeScale(yprev(f), ythis(f)); + BoutReal val = ythis(f); + for (int i = 1; i <= localmesh->ystart; ++i) { + val *= fac; + f[ind().yp(by * i).xp(bx * i)] = val; + } + } + + void neumann_o1(Field3D& f, BoutReal grad) const { + BoutReal val = ythis(f); + for (int i = 1; i <= localmesh->ystart; ++i) { + val += grad; + f[ind().yp(by * i).xp(bx * i)] = val; + } + } + + void neumann_o2(Field3D& f, BoutReal grad) const { + BoutReal val = yprev(f) + grad; + for (int i = 1; i <= localmesh->ystart; ++i) { + val += grad; + f[ind().yp(by * i).xp(bx * i)] = val; + } + } BoutReal& ynext(Field3D& f) const { return f[ind().yp(by).xp(bx)]; } const BoutReal& ynext(const Field3D& f) const { return f[ind().yp(by).xp(bx)]; } BoutReal& yprev(Field3D& f) const { return f[ind().yp(-by).xp(-bx)]; } const BoutReal& yprev(const Field3D& f) const { return f[ind().yp(-by).xp(-bx)]; } + BoutReal& ythis(Field3D& f) const { return f[ind()]; } + const BoutReal& ythis(const Field3D& f) const { return f[ind()]; } + + void setYPrevIfValid(Field3D& f, BoutReal val) const { yprev(f) = val; } + void setAll(Field3D& f, const BoutReal val) const { + for (int i = -localmesh->ystart; i <= localmesh->ystart; ++i) { + f[ind().yp(by * i).xp(bx * i)] = val; + } + } + + int abs_offset() const { return 1; } + +#if BOUT_USE_METRIC_3D == 0 + BoutReal& ynext(Field2D& f) const { return f[ind().yp(by).xp(bx)]; } + const BoutReal& ynext(const Field2D& f) const { return f[ind().yp(by).xp(bx)]; } + BoutReal& yprev(Field2D& f) const { return f[ind().yp(-by).xp(-bx)]; } + const BoutReal& yprev(const Field2D& f) const { return f[ind().yp(-by).xp(-bx)]; } +#endif const int dir; diff --git a/include/bout/yboundary_regions.hxx b/include/bout/yboundary_regions.hxx new file mode 100644 index 0000000000..e0e93e17f9 --- /dev/null +++ b/include/bout/yboundary_regions.hxx @@ -0,0 +1,66 @@ +#pragma once + +#include "./boundary_iterator.hxx" +#include "bout/parallel_boundary_region.hxx" + +class YBoundary { +public: + template + void iter_regions(const T& f) { + ASSERT1(is_init); + for (auto& region : boundary_regions) { + f(*region); + } + for (auto& region : boundary_regions_par) { + f(*region); + } + } + + template + void iter(const F& f) { + return iter_regions(f); + } + + void init(Options& options, Mesh* mesh = nullptr) { + if (mesh == nullptr) { + mesh = bout::globals::mesh; + } + + bool lower_y = options["lower_y"].doc("Boundary on lower y?").withDefault(true); + bool upper_y = options["upper_y"].doc("Boundary on upper y?").withDefault(true); + bool outer_x = options["outer_x"].doc("Boundary on outer x?").withDefault(true); + bool inner_x = + options["inner_x"].doc("Boundary on inner x?").withDefault(false); + + if (mesh->isFci()) { + if (outer_x) { + for (auto& bndry : mesh->getBoundariesPar(BoundaryParType::xout)) { + boundary_regions_par.push_back(bndry); + } + } + if (inner_x) { + for (auto& bndry : mesh->getBoundariesPar(BoundaryParType::xin)) { + boundary_regions_par.push_back(bndry); + } + } + } else { + if (lower_y) { + boundary_regions.push_back( + std::make_shared(mesh, true, mesh->iterateBndryLowerY())); + } + if (upper_y) { + boundary_regions.push_back(std::make_shared( + mesh, false, mesh->iterateBndryUpperY())); + } + } + is_init = true; + } + +private: + std::vector> boundary_regions_par; + std::vector> boundary_regions; + + bool is_init{false}; +}; + +extern YBoundary yboundary; From 731db609b7b5abd1770badeae7b456b255ec2312 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 17:17:04 +0100 Subject: [PATCH 222/256] Add delay on error --- include/bout/physicsmodel.hxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/bout/physicsmodel.hxx b/include/bout/physicsmodel.hxx index fa113670ba..7588b86f79 100644 --- a/include/bout/physicsmodel.hxx +++ b/include/bout/physicsmodel.hxx @@ -47,6 +47,8 @@ class PhysicsModel; #include "bout/unused.hxx" #include "bout/utils.hxx" +#include +#include #include #include @@ -437,6 +439,7 @@ private: } catch (const BoutException& e) { \ output << "Error encountered: " << e.what(); \ output << e.getBacktrace() << endl; \ + std::this_thread::sleep_for(std::chrono::milliseconds(100)); \ MPI_Abort(BoutComm::get(), 1); \ } \ BoutFinalise(); \ From dc94e06465fedf311495ac4a512a0c0467bed46b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 22 Jan 2025 16:53:58 +0100 Subject: [PATCH 223/256] Add legacy monotonic hermite spline implementation again --- include/bout/interpolation_xz.hxx | 14 +++ src/mesh/interpolation/hermite_spline_xz.cxx | 89 ++++++++++++++++++++ src/mesh/interpolation_xz.cxx | 2 + tests/MMS/spatial/fci/runtest | 7 +- 4 files changed, 111 insertions(+), 1 deletion(-) diff --git a/include/bout/interpolation_xz.hxx b/include/bout/interpolation_xz.hxx index 261b4c1515..fd4a4fcd50 100644 --- a/include/bout/interpolation_xz.hxx +++ b/include/bout/interpolation_xz.hxx @@ -134,6 +134,7 @@ public: } }; + template class XZHermiteSplineBase : public XZInterpolation { protected: @@ -281,6 +282,19 @@ public: const std::string& region = "RGN_NOBNDRY") override; }; + +class XZMonotonicHermiteSplineLegacy: public XZHermiteSplineBase { +public: + using XZHermiteSplineBase::interpolate; + virtual Field3D interpolate(const Field3D& f, + const std::string& region = "RGN_NOBNDRY") const override; + template + XZMonotonicHermiteSplineLegacy(Ts... args) : + XZHermiteSplineBase(args...) + {} +}; + + class XZInterpolationFactory : public Factory { public: diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index b8b30e68d8..c0952bae5e 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -495,3 +495,92 @@ XZHermiteSplineBase::interpolate(const Field3D& f, const Field3D& del // ensure they are instantiated template class XZHermiteSplineBase; template class XZHermiteSplineBase; + +Field3D XZMonotonicHermiteSplineLegacy::interpolate(const Field3D& f, + const std::string& region) const { + ASSERT1(f.getMesh() == localmesh); + Field3D f_interp(f.getMesh()); + f_interp.allocate(); + + // Derivatives are used for tension and need to be on dimensionless + // coordinates + Field3D fx = bout::derivatives::index::DDX(f, CELL_DEFAULT, "DEFAULT"); + localmesh->communicateXZ(fx); + // communicate in y, but do not calculate parallel slices + { + auto h = localmesh->sendY(fx); + localmesh->wait(h); + } + Field3D fz = bout::derivatives::index::DDZ(f, CELL_DEFAULT, "DEFAULT", "RGN_ALL"); + localmesh->communicateXZ(fz); + // communicate in y, but do not calculate parallel slices + { + auto h = localmesh->sendY(fz); + localmesh->wait(h); + } + Field3D fxz = bout::derivatives::index::DDX(fz, CELL_DEFAULT, "DEFAULT"); + localmesh->communicateXZ(fxz); + // communicate in y, but do not calculate parallel slices + { + auto h = localmesh->sendY(fxz); + localmesh->wait(h); + } + + const auto curregion{getRegion(region)}; + BOUT_FOR(i, curregion) { + const auto iyp = i.yp(y_offset); + + const auto ic = i_corner[i]; + const auto iczp = ic.zp(); + const auto icxp = ic.xp(); + const auto icxpzp = iczp.xp(); + + // Interpolate f in X at Z + const BoutReal f_z = + f[ic] * h00_x[i] + f[icxp] * h01_x[i] + fx[ic] * h10_x[i] + fx[icxp] * h11_x[i]; + + // Interpolate f in X at Z+1 + const BoutReal f_zp1 = f[iczp] * h00_x[i] + f[icxpzp] * h01_x[i] + fx[iczp] * h10_x[i] + + fx[icxpzp] * h11_x[i]; + + // Interpolate fz in X at Z + const BoutReal fz_z = fz[ic] * h00_x[i] + fz[icxp] * h01_x[i] + fxz[ic] * h10_x[i] + + fxz[icxp] * h11_x[i]; + + // Interpolate fz in X at Z+1 + const BoutReal fz_zp1 = fz[iczp] * h00_x[i] + fz[icxpzp] * h01_x[i] + + fxz[iczp] * h10_x[i] + fxz[icxpzp] * h11_x[i]; + + // Interpolate in Z + BoutReal result = + +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; + + ASSERT2(std::isfinite(result) || i.x() < localmesh->xstart + || i.x() > localmesh->xend); + + // Monotonicity + // Force the interpolated result to be in the range of the + // neighbouring cell values. This prevents unphysical overshoots, + // but also degrades accuracy near maxima and minima. + // Perhaps should only impose near boundaries, since that is where + // problems most obviously occur. + const BoutReal localmax = BOUTMAX(f[ic], f[icxp], f[iczp], f[icxpzp]); + + const BoutReal localmin = BOUTMIN(f[ic], f[icxp], f[iczp], f[icxpzp]); + + ASSERT2(std::isfinite(localmax) || i.x() < localmesh->xstart + || i.x() > localmesh->xend); + ASSERT2(std::isfinite(localmin) || i.x() < localmesh->xstart + || i.x() > localmesh->xend); + + if (result > localmax) { + result = localmax; + } + if (result < localmin) { + result = localmin; + } + + f_interp[iyp] = result; + } + return f_interp; +} diff --git a/src/mesh/interpolation_xz.cxx b/src/mesh/interpolation_xz.cxx index 0bc25111ab..bf22ba995d 100644 --- a/src/mesh/interpolation_xz.cxx +++ b/src/mesh/interpolation_xz.cxx @@ -91,6 +91,8 @@ namespace { RegisterXZInterpolation registerinterphermitespline{"hermitespline"}; RegisterXZInterpolation registerinterpmonotonichermitespline{ "monotonichermitespline"}; +RegisterXZInterpolation registerinterpmonotonichermitesplinelegacy{ + "monotonichermitesplinelegacy"}; RegisterXZInterpolation registerinterplagrange4pt{"lagrange4pt"}; RegisterXZInterpolation registerinterpbilinear{"bilinear"}; } // namespace diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index a2785a24ae..c8bb3f914b 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -51,6 +51,7 @@ for nslice in nslices: "lagrange4pt", "bilinear", "monotonichermitespline", + "monotonichermitesplinelegacy", ]: error_2[nslice] = [] error_inf[nslice] = [] @@ -104,7 +105,11 @@ for nslice in nslices: nslice, yperiodic, method_orders[nslice]["name"], - 2 if conf.has["petsc"] and "hermitespline" in method else 1, + ( + 1 + if "legacy" in method + else 2 if conf.has["petsc"] and "hermitespline" in method else 1 + ), ) args += f" mesh:paralleltransform:xzinterpolation:type={method}" From 18231442a0fc5a6bc7d6dca739f5ceab23618d17 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 28 Jan 2025 09:49:41 +0100 Subject: [PATCH 224/256] Take periodicity into account --- src/mesh/parallel/fci_comm.hxx | 36 ++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index ec4aeaba49..a93b55244a 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -52,19 +52,35 @@ struct globalToLocal1D { const int local; const int global; const int globalwith; - globalToLocal1D(int mg, int npe, int localwith) + const bool periodic; + globalToLocal1D(int mg, int npe, int localwith, bool periodic) : mg(mg), npe(npe), localwith(localwith), local(localwith - 2 * mg), - global(local * npe), globalwith(global + 2 * mg) {}; + global(local * npe), globalwith(global + 2 * mg), periodic(periodic) {}; ProcLocal convert(int id) const { + if (periodic) { + while (id < mg) { + id += global; + } + while (id >= global + mg) { + id -= global; + } + } int idwo = id - mg; int proc = idwo / local; - if (proc >= npe) { - proc = npe - 1; + if (not periodic) { + if (proc >= npe) { + proc = npe - 1; + } } - ASSERT2(proc >= 0); int loc = id - local * proc; - ASSERT2(0 <= loc); - ASSERT2(loc < (local + 2 * mg)); +#if CHECK > 1 + if ((loc < 0 or loc > localwith or proc < 0 or proc > npe) + or (periodic and (loc < mg or loc >= local + mg))) { + printf("globalToLocal1D failure: %d %d, %d %d, %d %d %s\n", id, idwo, globalwith, + npe, proc, loc, periodic ? "periodic" : "non-periodic"); + ASSERT0(false); + } +#endif return {proc, loc}; } }; @@ -98,9 +114,9 @@ class GlobalField3DAccess { public: friend class GlobalField3DAccessInstance; GlobalField3DAccess(Mesh* mesh) - : mesh(mesh), g2lx(mesh->xstart, mesh->getNXPE(), mesh->LocalNx), - g2ly(mesh->ystart, mesh->getNYPE(), mesh->LocalNy), - g2lz(mesh->zstart, 1, mesh->LocalNz), + : mesh(mesh), g2lx(mesh->xstart, mesh->getNXPE(), mesh->LocalNx, false), + g2ly(mesh->ystart, mesh->getNYPE(), mesh->LocalNy, true), + g2lz(mesh->zstart, 1, mesh->LocalNz, true), xyzl(g2lx.localwith, g2ly.localwith, g2lz.localwith), xyzg(g2lx.globalwith, g2ly.globalwith, g2lz.globalwith), comm(BoutComm::get()) {}; void get(IndG3D ind) { ids.emplace(ind.ind); } From 1e96b3e8bfa7b86a1939d1150e4c350ccb689859 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 28 Jan 2025 09:49:56 +0100 Subject: [PATCH 225/256] Sort a reference, not a copy --- src/mesh/parallel/fci_comm.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index a93b55244a..9f296848b1 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -133,7 +133,7 @@ public: toGet[piy.proc * g2lx.npe + pix.proc].push_back( xyzl.convert(pix.ind, piy.ind, piz.ind).ind); } - for (auto v : toGet) { + for (auto& v : toGet) { std::sort(v.begin(), v.end()); } commCommLists(); From ad8f403f9c427e2497b8e08c23fe3c26f33793a2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 28 Jan 2025 09:52:06 +0100 Subject: [PATCH 226/256] Only communicate non-empty vectors --- src/mesh/parallel/fci_comm.hxx | 44 ++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 9f296848b1..1ca67f0ecc 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -193,6 +193,7 @@ private: ASSERT0(ret == MPI_SUCCESS); } std::vector reqs2(toSend.size()); + int cnt = 0; for ([[maybe_unused]] auto dummy : reqs) { int ind{0}; auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); @@ -200,20 +201,26 @@ private: ASSERT3(ind != MPI_UNDEFINED); ASSERT2(static_cast(ind) < toSend.size()); ASSERT3(toSendSizes[ind] >= 0); + if (toSendSizes[ind] == 0) { + continue; + } sendBufferSize += toSendSizes[ind]; - toSend[ind].resize(toSendSizes[ind]); - ret = MPI_Irecv(static_cast(&toSend[ind][0]), toSend[ind].size(), MPI_INT, - ind, 666 * 666, comm, &reqs2[ind]); + toSend[ind].resize(toSendSizes[ind], -1); + + ret = MPI_Irecv(static_cast(toSend[ind].data()), toSend[ind].size(), MPI_INT, + ind, 666 * 666, comm, reqs2.data() + cnt++); ASSERT0(ret == MPI_SUCCESS); } for (size_t proc = 0; proc < toGet.size(); ++proc) { - const auto ret = MPI_Send(static_cast(&toGet[proc][0]), toGet[proc].size(), - MPI_INT, proc, 666 * 666, comm); - ASSERT0(ret == MPI_SUCCESS); + if (toGet.size() != 0) { + const auto ret = MPI_Send(static_cast(toGet[proc].data()), + toGet[proc].size(), MPI_INT, proc, 666 * 666, comm); + ASSERT0(ret == MPI_SUCCESS); + } } - for ([[maybe_unused]] auto dummy : reqs) { + for (int c = 0; c < cnt; c++) { int ind{0}; - const auto ret = MPI_Waitany(reqs.size(), &reqs2[0], &ind, MPI_STATUS_IGNORE); + const auto ret = MPI_Waitany(cnt, reqs2.data(), &ind, MPI_STATUS_IGNORE); ASSERT0(ret == MPI_SUCCESS); ASSERT3(ind != MPI_UNDEFINED); } @@ -242,25 +249,36 @@ private: std::vector data(getOffsets.back()); std::vector sendBuffer(sendBufferSize); std::vector reqs(toSend.size()); + int cnt1 = 0; for (size_t proc = 0; proc < toGet.size(); ++proc) { - auto ret = MPI_Irecv(static_cast(&data[getOffsets[proc]]), - toGet[proc].size(), MPI_DOUBLE, proc, 666, comm, &reqs[proc]); + if (toGet[proc].size() == 0) { + continue; + } + auto ret = + MPI_Irecv(static_cast(data.data() + getOffsets[proc]), + toGet[proc].size(), MPI_DOUBLE, proc, 666, comm, reqs.data() + cnt1); ASSERT0(ret == MPI_SUCCESS); + cnt1++; } int cnt = 0; for (size_t proc = 0; proc < toGet.size(); ++proc) { - void* start = static_cast(&sendBuffer[cnt]); + if (toSend[proc].size() == 0) { + continue; + } + const void* start = static_cast(sendBuffer.data() + cnt); for (auto i : toSend[proc]) { sendBuffer[cnt++] = f[Ind3D(i)]; } auto ret = MPI_Send(start, toSend[proc].size(), MPI_DOUBLE, proc, 666, comm); ASSERT0(ret == MPI_SUCCESS); } - for ([[maybe_unused]] auto dummy : reqs) { + for (int j = 0; j < cnt1; ++j) { int ind{0}; - auto ret = MPI_Waitany(reqs.size(), &reqs[0], &ind, MPI_STATUS_IGNORE); + auto ret = MPI_Waitany(cnt1, reqs.data(), &ind, MPI_STATUS_IGNORE); ASSERT0(ret == MPI_SUCCESS); ASSERT3(ind != MPI_UNDEFINED); + ASSERT3(ind >= 0); + ASSERT3(ind < cnt1); } return data; } From 5adf893cb4f205147984151fe10c89fc6bb3419a Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 31 Jan 2025 11:45:08 +0100 Subject: [PATCH 227/256] Make check stricter --- src/mesh/parallel/fci_comm.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 1ca67f0ecc..df08a02dda 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -74,7 +74,7 @@ struct globalToLocal1D { } int loc = id - local * proc; #if CHECK > 1 - if ((loc < 0 or loc > localwith or proc < 0 or proc > npe) + if ((loc < 0 or loc > localwith or proc < 0 or proc >= npe) or (periodic and (loc < mg or loc >= local + mg))) { printf("globalToLocal1D failure: %d %d, %d %d, %d %d %s\n", id, idwo, globalwith, npe, proc, loc, periodic ? "periodic" : "non-periodic"); From cd383e24b13b8925bee0c68b5775d38d4d47068a Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 31 Jan 2025 11:49:11 +0100 Subject: [PATCH 228/256] Minor improvements to mms test --- tests/MMS/spatial/fci/data/BOUT.inp | 1 + tests/MMS/spatial/fci/mms.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/MMS/spatial/fci/data/BOUT.inp b/tests/MMS/spatial/fci/data/BOUT.inp index b4825c6207..f3e5046c54 100644 --- a/tests/MMS/spatial/fci/data/BOUT.inp +++ b/tests/MMS/spatial/fci/data/BOUT.inp @@ -1,5 +1,6 @@ grid = fci.grid.nc +# generated by ../mms.py input_field = sin(y - 2*z) + sin(y - z) solution = (6.28318530717959*(0.01*x + 0.045)*(-2*cos(y - 2*z) - cos(y - z)) + 0.628318530717959*cos(y - 2*z) + 0.628318530717959*cos(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0) diff --git a/tests/MMS/spatial/fci/mms.py b/tests/MMS/spatial/fci/mms.py index 1e71135c90..994b3f9761 100755 --- a/tests/MMS/spatial/fci/mms.py +++ b/tests/MMS/spatial/fci/mms.py @@ -30,5 +30,5 @@ def FCI_ddy(f): ############################################ # Equations solved -print("input = " + exprToStr(f)) +print("input_field = " + exprToStr(f)) print("solution = " + exprToStr(FCI_ddy(f))) From 2d72bab00647ce0e3dc0290916282d9d3f51257b Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 31 Jan 2025 11:49:47 +0100 Subject: [PATCH 229/256] Fix: include global offset in monotonic spline --- src/mesh/interpolation/hermite_spline_xz.cxx | 5 ++++- tests/integrated/test_suite | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index c0952bae5e..1990bddb21 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -170,6 +170,8 @@ void XZHermiteSplineBase::calcWeights(const Field3D& delta_x, #ifdef HS_USE_PETSC IndConverter conv{localmesh}; #endif + [[maybe_unused]] const int y_global_offset = + localmesh->getYProcIndex() * (localmesh->yend - localmesh->ystart + 1); BOUT_FOR(i, getRegion(region)) { const int x = i.x(); const int y = i.y(); @@ -303,7 +305,8 @@ void XZHermiteSplineBase::calcWeights(const Field3D& delta_x, #endif #endif if constexpr (monotonic) { - const auto gind = gf3daccess->xyzg(i_corn, y + y_offset, k_corner(x, y, z)); + const auto gind = + gf3daccess->xyzg(i_corn, y + y_offset + y_global_offset, k_corner(x, y, z)); gf3daccess->get(gind); gf3daccess->get(gind.xp(1)); gf3daccess->get(gind.zp(1)); diff --git a/tests/integrated/test_suite b/tests/integrated/test_suite index 307a8d84b3..77ad7882c4 100755 --- a/tests/integrated/test_suite +++ b/tests/integrated/test_suite @@ -188,7 +188,7 @@ class Test(threading.Thread): self.output += "\n(It is likely that a timeout occured)" else: # ❌ Failed - print("\u274C", end="") # No newline + print("\u274c", end="") # No newline print(" %7.3f s" % (time.time() - self.local.start_time), flush=True) def _cost(self): From b3841fb21eb589426fc594b071ef20caaab92797 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 31 Jan 2025 14:51:11 +0100 Subject: [PATCH 230/256] make fci_comm openmp thread safe Using a local set for each thread ensures we do not need a mutex for adding data, at the cost of having to merge the different sets later. --- src/mesh/parallel/fci_comm.hxx | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index df08a02dda..40c1fe9f72 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -118,11 +118,30 @@ public: g2ly(mesh->ystart, mesh->getNYPE(), mesh->LocalNy, true), g2lz(mesh->zstart, 1, mesh->LocalNz, true), xyzl(g2lx.localwith, g2ly.localwith, g2lz.localwith), - xyzg(g2lx.globalwith, g2ly.globalwith, g2lz.globalwith), comm(BoutComm::get()) {}; - void get(IndG3D ind) { ids.emplace(ind.ind); } + xyzg(g2lx.globalwith, g2ly.globalwith, g2lz.globalwith), comm(BoutComm::get()) { +#ifdef _OPENMP + o_ids.resize(omp_get_max_threads()); +#endif + }; + void get(IndG3D ind) { + ASSERT2(is_setup == false); +#ifdef _OPENMP + ASSERT2(o_ids.size() > static_cast(omp_get_thread_num())); + o_ids[omp_get_thread_num()].emplace(ind.ind); +#else + ids.emplace(ind.ind); +#endif + } + void operator[](IndG3D ind) { return get(ind); } void setup() { ASSERT2(is_setup == false); +#ifdef _OPENMP + for (auto& o_id : o_ids) { + ids.merge(o_id); + } + o_ids.clear(); +#endif toGet.resize(g2lx.npe * g2ly.npe * g2lz.npe); for (const auto id : ids) { IndG3D gind{id, g2ly.globalwith, g2lz.globalwith}; @@ -226,6 +245,9 @@ private: } } Mesh* mesh; +#ifdef _OPENMP + std::vector> o_ids; +#endif std::set ids; std::map mapping; bool is_setup{false}; From 63e8cb9e793b4047ca709066e1afa651dd86c5a4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 7 Feb 2025 13:50:10 +0100 Subject: [PATCH 231/256] Fix communication for fci We want to skip sending if there is no data for this process ... --- src/mesh/parallel/fci_comm.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 40c1fe9f72..27dd111765 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -231,7 +231,7 @@ private: ASSERT0(ret == MPI_SUCCESS); } for (size_t proc = 0; proc < toGet.size(); ++proc) { - if (toGet.size() != 0) { + if (toGet[proc].size() != 0) { const auto ret = MPI_Send(static_cast(toGet[proc].data()), toGet[proc].size(), MPI_INT, proc, 666 * 666, comm); ASSERT0(ret == MPI_SUCCESS); From 371c928fa0b57238044b6ef53bab1e5314e4a4af Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 7 Feb 2025 15:58:07 +0100 Subject: [PATCH 232/256] Add check to reduce risk of bugs The result needs to be well understood, as the indices are a mix of local and global indices, as we need to access non-local data. --- src/mesh/interpolation/hermite_spline_xz.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 1990bddb21..f850704b22 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -356,6 +356,9 @@ template std::vector XZHermiteSplineBase::getWeightsForYApproximation(int i, int j, int k, int yoffset) { + if (localmesh->getNXPE() > 1){ + throw BoutException("It is likely that the function calling this is not handling the result correctly."); + } const int nz = localmesh->LocalNz; const int k_mod = k_corner(i, j, k); const int k_mod_m1 = (k_mod > 0) ? (k_mod - 1) : (nz - 1); From f2939c66a4e0935ea6cbf303c4dcc45ba2a03235 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 7 Feb 2025 16:45:29 +0100 Subject: [PATCH 233/256] Handle C++ exception for more functions --- tools/pylib/_boutpp_build/boutcpp.pxd.jinja | 62 ++++++++++----------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja index 71ca09cb46..9a6435a018 100644 --- a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja +++ b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja @@ -16,12 +16,12 @@ cdef extern from "bout/{{ field.header }}.hxx": cppclass {{ field.field_type }}: {{ field.field_type }}(Mesh * mesh); {{ field.field_type }}(const {{ field.field_type }} &) - double & operator()(int, int, int) + double & operator()(int, int, int) except +raise_bout_py_error int getNx() int getNy() int getNz() bool isAllocated() - void setLocation(benum.CELL_LOC) + void setLocation(benum.CELL_LOC) except +raise_bout_py_error benum.CELL_LOC getLocation() Mesh* getMesh() {% for boundaryMethod in field.boundaries %} @@ -30,12 +30,12 @@ cdef extern from "bout/{{ field.header }}.hxx": void {{ boundaryMethod }}(double t) {% endfor %} {% for fun in "sqrt", "exp", "log", "sin", "cos", "abs" %} - {{ field.field_type }} {{ fun }}({{ field.field_type }}) + {{ field.field_type }} {{ fun }}({{ field.field_type }}) except +raise_bout_py_error {% endfor %} double max({{ field.field_type }}) double min({{ field.field_type }}) - {{ field.field_type }} pow({{ field.field_type }},double) - {{ field.field_type }} & ddt({{ field.field_type }}) + {{ field.field_type }} pow({{ field.field_type }}, double) except +raise_bout_py_error + {{ field.field_type }} & ddt({{ field.field_type }}) except +raise_bout_py_error {% endfor %} {% for vec in vecs %} cdef extern from "bout/{{ vec.header }}.hxx": @@ -51,9 +51,9 @@ cdef extern from "bout/mesh.hxx": cppclass Mesh: Mesh() @staticmethod - Mesh * create(Options * option) - void load() - void communicate(FieldGroup&) + Mesh * create(Options * option) except +raise_bout_py_error + void load() except +raise_bout_py_error + void communicate(FieldGroup&) except +raise_bout_py_error int getNXPE() int getNYPE() int getXProcIndex() @@ -62,8 +62,8 @@ cdef extern from "bout/mesh.hxx": int ystart int LocalNx int LocalNy - Coordinates * getCoordinates() - int get(Field3D, const string) + Coordinates * getCoordinates() except +raise_bout_py_error + int get(Field3D, const string) except +raise_bout_py_error cdef extern from "bout/coordinates.hxx": cppclass Coordinates: @@ -79,10 +79,10 @@ cdef extern from "bout/coordinates.hxx": {{ metric_field }} G1, G2, G3 {{ metric_field }} ShiftTorsion {{ metric_field }} IntShiftTorsion - int geometry() - int calcCovariant() - int calcContravariant() - int jacobian() + int geometry() except +raise_bout_py_error + int calcCovariant() except +raise_bout_py_error + int calcContravariant() except +raise_bout_py_error + int jacobian() except +raise_bout_py_error cdef extern from "bout/fieldgroup.hxx": cppclass FieldGroup: @@ -91,8 +91,8 @@ cdef extern from "bout/fieldgroup.hxx": cdef extern from "bout/invert_laplace.hxx": cppclass Laplacian: @staticmethod - unique_ptr[Laplacian] create(Options*, benum.CELL_LOC, Mesh*, Solver*) - Field3D solve(Field3D, Field3D) + unique_ptr[Laplacian] create(Options*, benum.CELL_LOC, Mesh*, Solver*) except +raise_bout_py_error + Field3D solve(Field3D, Field3D) except +raise_bout_py_error Field3D forward(Field3D) void setCoefA(Field3D) void setCoefC(Field3D) @@ -104,12 +104,12 @@ cdef extern from "bout/invert_laplace.hxx": void setCoefEz(Field3D) cdef extern from "bout/difops.hxx": - Field3D Div_par(Field3D, benum.CELL_LOC, string) - Field3D Grad_par(Field3D, benum.CELL_LOC, string) - Field3D Laplace(Field3D) - Field3D Vpar_Grad_par(Field3D, Field3D, benum.CELL_LOC, string) - Field3D bracket(Field3D,Field3D, benum.BRACKET_METHOD, benum.CELL_LOC) - Field3D Delp2(Field3D) + Field3D Div_par(Field3D, benum.CELL_LOC, string) except +raise_bout_py_error + Field3D Grad_par(Field3D, benum.CELL_LOC, string) except +raise_bout_py_error + Field3D Laplace(Field3D) except +raise_bout_py_error + Field3D Vpar_Grad_par(Field3D, Field3D, benum.CELL_LOC, string) except +raise_bout_py_error + Field3D bracket(Field3D,Field3D, benum.BRACKET_METHOD, benum.CELL_LOC) except +raise_bout_py_error + Field3D Delp2(Field3D) except +raise_bout_py_error cdef extern from "bout/derivs.hxx": {% for d in "XYZ" %} @@ -131,16 +131,16 @@ cdef extern from "bout/interpolation.hxx": cdef extern from "bout/field_factory.hxx": cppclass FieldFactory: - FieldFactory(Mesh*,Options*) - Field3D create3D(string bla, Options * o, Mesh * m,benum.CELL_LOC loc, double t) + FieldFactory(Mesh*,Options*) except +raise_bout_py_error + Field3D create3D(string bla, Options * o, Mesh * m,benum.CELL_LOC loc, double t) except +raise_bout_py_error cdef extern from "bout/solver.hxx": cppclass Solver: @staticmethod - Solver * create() - void setModel(PhysicsModel *) - void add(Field3D, char * name) - void solve() + Solver * create() except +raise_bout_py_error + void setModel(PhysicsModel *) except +raise_bout_py_error + void add(Field3D, char * name) except +raise_bout_py_error + void solve() except +raise_bout_py_error cdef extern from "bout/physicsmodel.hxx": cppclass PhysicsModel: @@ -166,8 +166,8 @@ cdef extern from "bout/output.hxx": ConditionalOutput output_info cdef extern from "bout/vecops.hxx": - Vector3D Grad(const Field3D& f, benum.CELL_LOC, string) - Vector3D Grad_perp(const Field3D& f, benum.CELL_LOC, string) + Vector3D Grad(const Field3D& f, benum.CELL_LOC, string) except +raise_bout_py_error + Vector3D Grad_perp(const Field3D& f, benum.CELL_LOC, string) except +raise_bout_py_error cdef extern from "bout/vector3d.hxx": - Vector3D cross(Vector3D, Vector3D) + Vector3D cross(Vector3D, Vector3D) except +raise_bout_py_error From ad09499e640f1c3bcfcac98021fe7ed0786fea49 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 10 Feb 2025 10:20:13 +0100 Subject: [PATCH 234/256] Expose LaplaceXZ --- tools/pylib/_boutpp_build/boutcpp.pxd.jinja | 8 +++ tools/pylib/_boutpp_build/boutpp.pyx.jinja | 58 +++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja index 9a6435a018..aa39e9843b 100644 --- a/tools/pylib/_boutpp_build/boutcpp.pxd.jinja +++ b/tools/pylib/_boutpp_build/boutcpp.pxd.jinja @@ -103,6 +103,14 @@ cdef extern from "bout/invert_laplace.hxx": void setCoefEy(Field3D) void setCoefEz(Field3D) +cdef extern from "bout/invert/laplacexz.hxx": + cppclass LaplaceXZ: + LaplaceXZ(Mesh*, Options*, benum.CELL_LOC) + void setCoefs(const Field3D& A, const Field3D& B) except +raise_bout_py_error + Field3D solve(const Field3D& b, const Field3D& x0) except +raise_bout_py_error + @staticmethod + unique_ptr[LaplaceXZ] create(Mesh* m, Options* opt, benum.CELL_LOC loc) + cdef extern from "bout/difops.hxx": Field3D Div_par(Field3D, benum.CELL_LOC, string) except +raise_bout_py_error Field3D Grad_par(Field3D, benum.CELL_LOC, string) except +raise_bout_py_error diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 0712dbc499..c64d7aba80 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -943,6 +943,64 @@ Equation solved is: d\\nabla^2_\\perp x + (1/c1)\\nabla_perp c2\\cdot\\nabla_\\p {% endfor %} +{{ class("LaplaceXZ", comment=""" +LaplaceXZ inversion solver + +Compute the Laplacian inversion of objects. + +Equation solved is: \\nabla\\cdot\\left( A \\nabla_\\perp f \\right) + Bf = b +""", uniquePtr=True) }} + + def __init__(self, section=None, loc="CELL_CENTRE", mesh=None): + """ + Initialiase a Laplacian solver + + Parameters + ---------- + section : Options, optional + The section from the Option tree to take the options from + """ + checkInit() + cdef c.Options* copt = NULL + if section: + if isinstance(section, str): + section = Options.root(section) + copt = (section).cobj + cdef benum.CELL_LOC cloc = benum.resolve_cell_loc(loc) + cdef c.Mesh* cmesh = NULL + if mesh: + cmesh = (mesh).cobj + self.cobj = c.LaplaceXZ.create(cmesh, copt, cloc) + self.isSelfOwned = True + + def solve(self, Field3D x, Field3D guess): + """ + Calculate the Laplacian inversion + + Parameters + ---------- + x : Field3D + Field to be inverted + guess : Field3D + initial guess for the inversion + + + Returns + ------- + Field3D + the inversion of x, where guess is a guess to start with + """ + return f3dFromObj(deref(self.cobj).solve(x.cobj[0],guess.cobj[0])) + + def setCoefs(self, *, Field3D A, Field3D B): + """ + Set the coefficients for the Laplacian solver. + The coefficients A and B have both to be passed. + A and B have to be Field3D. + """ + deref(self.cobj).setCoefs(A.cobj[0], B.cobj[0]) + + {{ class("FieldFactory", defaultSO=False) }} cdef void callback(void * parameter, void * method) with gil: From def8a06e3d8bcb095b75a1fd91abb3f39200a5a5 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 10 Feb 2025 10:20:37 +0100 Subject: [PATCH 235/256] Formatting fixes --- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index c64d7aba80..819ffcd489 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -781,13 +781,13 @@ cdef options norm : float The length with which to rescale """ - if self.isNormalised>0: - t=norm - norm=norm/self.isNormalised - self.isNormalised=t - c_mesh_normalise(self.cobj,norm) + if self.isNormalised > 0: + t = norm + norm = norm/self.isNormalised + self.isNormalised = t + c_mesh_normalise(self.cobj, norm) - def communicate(self,*args): + def communicate(self, *args): """ Communicate (MPI) the boundaries of the Field3Ds with neighbours @@ -1542,8 +1542,8 @@ def create3D(string, Mesh msh=None,outloc="CELL_DEFAULT",time=0): cdef benum.CELL_LOC outloc_=benum.resolve_cell_loc(outloc) if msh is None: msh=Mesh.getGlobal() - cdef FieldFactory fact=msh.getFactory() - cdef c.string str_=string.encode() + cdef FieldFactory fact = msh.getFactory() + cdef c.string str_ = string.encode() return f3dFromObj( (fact).cobj.create3D(str_,0,0 ,outloc_,time)) From f1534f2db7b93a15a6cc49b91427808a0804afe4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 10 Feb 2025 10:20:57 +0100 Subject: [PATCH 236/256] Expose Mesh::get for Field3D --- tools/pylib/_boutpp_build/boutpp.pyx.jinja | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tools/pylib/_boutpp_build/boutpp.pyx.jinja b/tools/pylib/_boutpp_build/boutpp.pyx.jinja index 819ffcd489..ff224cd70f 100644 --- a/tools/pylib/_boutpp_build/boutpp.pyx.jinja +++ b/tools/pylib/_boutpp_build/boutpp.pyx.jinja @@ -812,6 +812,20 @@ cdef options self._coords = coordsFromPtr(self.cobj.getCoordinates()) return self._coords + def get(self, name, default=None): + """ + Load a variable from the grid source + + If no default is given, and the variable is not found, an error is raised. + """ + cdef c.string cstr = name.encode() + cdef Field3D defaultfield = default if isinstance(default, Field3D) else Field3D.fromMesh(self) + if deref(self.cobj).get(deref(defaultfield.cobj), cstr): + if default is None: + raise ValueError(f"No default value for {name} given and not set for this mesh") + return default + return defaultfield + cdef Mesh meshFromPtr(c.Mesh * obj): mesh = Mesh() From 0bcc047d64e87ca9446fe1127b761a58215201f5 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 10 Feb 2025 13:50:12 +0100 Subject: [PATCH 237/256] remove non-petsc inversions This allows to also run the tests with 3D metrics. It also tightens the tollerances, as this is a regression test. Also removing the preconditioner is needed. --- .../test-petsc_laplace/CMakeLists.txt | 1 - .../test-petsc_laplace/data/BOUT.inp | 44 ++----------------- tests/integrated/test-petsc_laplace/runtest | 16 +++---- .../test-petsc_laplace/test_petsc_laplace.cxx | 32 ++++---------- 4 files changed, 17 insertions(+), 76 deletions(-) diff --git a/tests/integrated/test-petsc_laplace/CMakeLists.txt b/tests/integrated/test-petsc_laplace/CMakeLists.txt index 9492b9f34f..15286ecfda 100644 --- a/tests/integrated/test-petsc_laplace/CMakeLists.txt +++ b/tests/integrated/test-petsc_laplace/CMakeLists.txt @@ -1,7 +1,6 @@ bout_add_integrated_test(test-petsc-laplace SOURCES test_petsc_laplace.cxx REQUIRES BOUT_HAS_PETSC - CONFLICTS BOUT_USE_METRIC_3D # default preconditioner uses 'cyclic' Laplace solver which is not available with 3d metrics USE_RUNTEST USE_DATA_BOUT_INP PROCESSORS 4 diff --git a/tests/integrated/test-petsc_laplace/data/BOUT.inp b/tests/integrated/test-petsc_laplace/data/BOUT.inp index e7c285b54c..3fb3f25b63 100644 --- a/tests/integrated/test-petsc_laplace/data/BOUT.inp +++ b/tests/integrated/test-petsc_laplace/data/BOUT.inp @@ -26,20 +26,9 @@ nonuniform = true rtol = 1e-08 atol = 1e-06 include_yguards = false -maxits = 1000 +maxits = 100000 -gmres_max_steps = 300 - -pctype = shell # Supply a second solver as a preconditioner -rightprec = true # Right precondition - -[petsc2nd:precon] # Options for the preconditioning solver -# Leave default type (tri or spt) -all_terms = true -nonuniform = true -filter = 0.0 # Must not filter -inner_boundary_flags = 32 # Identity in boundary -outer_boundary_flags = 32 # Identity in boundary +gmres_max_steps = 3000 ############################################# @@ -50,32 +39,7 @@ nonuniform = true rtol = 1e-08 atol = 1e-06 include_yguards = false -maxits = 1000 +maxits = 100000 fourth_order = true -gmres_max_steps = 30 - -pctype = shell -rightprec = true - -[petsc4th:precon] -all_terms = true -nonuniform = true -filter = 0.0 -inner_boundary_flags = 32 # Identity in boundary -outer_boundary_flags = 32 # Identity in boundary - -############################################# - -[SPT] -#type=spt -all_terms = true -nonuniform = true -#flags=15 -include_yguards = false - -#maxits=10000 - -[laplace] -all_terms = true -nonuniform = true +gmres_max_steps = 300 diff --git a/tests/integrated/test-petsc_laplace/runtest b/tests/integrated/test-petsc_laplace/runtest index ac248c4ce7..87c3991d00 100755 --- a/tests/integrated/test-petsc_laplace/runtest +++ b/tests/integrated/test-petsc_laplace/runtest @@ -9,20 +9,14 @@ # cores: 4 # Variables to compare -from __future__ import print_function -from builtins import str - vars = [ ("max_error1", 2.0e-4), - ("max_error2", 2.0e-4), + ("max_error2", 2.0e-8), ("max_error3", 2.0e-4), - ("max_error4", 1.0e-5), - ("max_error5", 2.0e-4), - ("max_error6", 2.0e-5), - ("max_error7", 2.0e-4), - ("max_error8", 2.0e-5), + ("max_error4", 2.0e-4), + ("max_error5", 4.0e-6), + ("max_error6", 2.0e-4), ] -# tol = 1e-4 # Absolute (?) tolerance from boututils.run_wrapper import build_and_log, shell, launch_safe from boutdata.collect import collect @@ -59,7 +53,7 @@ for nproc in [1, 2, 4]: print("Convergence error") success = False elif error > tol: - print("Fail, maximum error is = " + str(error)) + print(f"Fail, maximum error is = {error}") success = False else: print("Pass") diff --git a/tests/integrated/test-petsc_laplace/test_petsc_laplace.cxx b/tests/integrated/test-petsc_laplace/test_petsc_laplace.cxx index 1e3cdde310..8ca8383244 100644 --- a/tests/integrated/test-petsc_laplace/test_petsc_laplace.cxx +++ b/tests/integrated/test-petsc_laplace/test_petsc_laplace.cxx @@ -66,14 +66,10 @@ void check_laplace(int test_num, std::string_view test_name, Laplacian& invert, Field3D abs_error; BoutReal max_error = -1; - try { - sol = invert.solve(sliceXZ(bcoef, ystart)); - error = (field - sol) / field; - abs_error = field - sol; - max_error = max_error_at_ystart(abs(abs_error)); - } catch (BoutException& err) { - output.write("BoutException occured in invert->solve(b1): {}\n", err.what()); - } + sol = invert.solve(sliceXZ(bcoef, ystart)); + error = (field - sol) / field; + abs_error = field - sol; + max_error = max_error_at_ystart(abs(abs_error)); output.write("\nTest {}: {}\n", test_num, test_name); output.write("Magnitude of maximum absolute error is {}\n", max_error); @@ -147,7 +143,7 @@ int main(int argc, char** argv) { INVERT_AC_GRAD, a_1, c_1, d_1, b_1, f_1, mesh->ystart, dump); //////////////////////////////////////////////////////////////////////////////////////// - // Test 3+4: Gaussian x-profiles, z-independent coefficients and compare with SPT method + // Test 3: Gaussian x-profiles, z-independent coefficients const Field2D a_3 = DC(a_1); const Field2D c_3 = DC(c_1); @@ -158,15 +154,8 @@ int main(int argc, char** argv) { INVERT_AC_GRAD, INVERT_AC_GRAD, a_3, c_3, d_3, b_3, f_1, mesh->ystart, dump); - Options* SPT_options = Options::getRoot()->getSection("SPT"); - auto invert_SPT = Laplacian::create(SPT_options); - - check_laplace(++test_num, "with coefficients constant in z, default solver", - *invert_SPT, INVERT_AC_GRAD, INVERT_AC_GRAD | INVERT_DC_GRAD, a_3, c_3, - d_3, b_3, f_1, mesh->ystart, dump); - ////////////////////////////////////////////// - // Test 5: Cosine x-profiles, 2nd order Krylov + // Test 4: Cosine x-profiles, 2nd order Krylov Field3D f_5 = generate_f5(*mesh); Field3D a_5 = generate_a5(*mesh); Field3D c_5 = generate_c5(*mesh); @@ -181,14 +170,14 @@ int main(int argc, char** argv) { dump); ////////////////////////////////////////////// - // Test 6: Cosine x-profiles, 4th order Krylov + // Test 5: Cosine x-profiles, 4th order Krylov check_laplace(++test_num, "different profiles, PETSc 4th order", *invert_4th, INVERT_AC_GRAD, INVERT_AC_GRAD, a_5, c_5, d_5, b_5, f_5, mesh->ystart, dump); ////////////////////////////////////////////////////////////////////////////////////// - // Test 7+8: Cosine x-profiles, z-independent coefficients and compare with SPT method + // Test 6: Cosine x-profiles, z-independent coefficients const Field2D a_7 = DC(a_5); const Field2D c_7 = DC(c_5); @@ -200,11 +189,6 @@ int main(int argc, char** argv) { *invert, INVERT_AC_GRAD, INVERT_AC_GRAD, a_7, c_7, d_7, b_7, f_5, mesh->ystart, dump); - check_laplace(++test_num, - "different profiles, with coefficients constant in z, default solver", - *invert_SPT, INVERT_AC_GRAD, INVERT_AC_GRAD | INVERT_DC_GRAD, a_7, c_7, - d_7, b_7, f_5, mesh->ystart, dump); - // Write and close the output file bout::writeDefaultOutputFile(dump); From f6106a255f36c6f11e47ab8a53993e140d5e8660 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 24 Feb 2025 13:38:40 +0100 Subject: [PATCH 238/256] Allow to overwrite MYG with options. --- src/mesh/impls/bout/boutmesh.cxx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index 909754d507..5c61e23554 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -493,8 +493,18 @@ int BoutMesh::load() { } ASSERT0(MXG >= 0); - if (Mesh::get(MYG, "MYG") != 0) { - MYG = options["MYG"].doc("Number of guard cells on each side in Y").withDefault(2); + bool meshHasMyg = Mesh::get(MYG, "MYG") == 0; + int meshMyg; + if (!meshHasMyg) { + MYG = 2; + } else { + meshMyg = MYG; + } + MYG = options["MYG"].doc("Number of guard cells on each side in Y").withDefault(MYG); + if (meshHasMyg && MYG != meshMyg) { + output_warn.write(_("Options changed the number of y-guard cells. Grid has {} but " + "option specified {}! Continuing with {}"), + meshMyg, MYG, MYG); } ASSERT0(MYG >= 0); From 8665a6752d57f6107cf1fe701d03764b319221d3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 27 Feb 2025 09:44:36 +0100 Subject: [PATCH 239/256] Revert "Disable metric components that require y-derivatives for fci" This reverts commit 84853532ff7078379c798d205b995a917492ebbf. The parallel metric components are loaded, thus we can take meaningful derivatives in y-direction. --- src/mesh/coordinates.cxx | 234 +++++++++++++++++++-------------------- 1 file changed, 112 insertions(+), 122 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 4db84601af..53ec4e9d16 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -977,129 +977,119 @@ int Coordinates::geometry(bool recalculate_staggered, checkContravariant(); checkCovariant(); - if (g_11.isFci()) { - // for FCI the y derivatives of metric components is meaningless. - G1_11 = G1_22 = G1_33 = G1_12 = G1_13 = G1_23 = + // Calculate Christoffel symbol terms (18 independent values) + // Note: This calculation is completely general: metric + // tensor can be 2D or 3D. For 2D, all DDZ terms are zero + + G1_11 = 0.5 * g11 * DDX(g_11) + g12 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g13 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G1_22 = g11 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g12 * DDY(g_22) + + g13 * (DDY(g_23) - 0.5 * DDZ(g_22)); + G1_33 = g11 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g12 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g13 * DDZ(g_33); + G1_12 = 0.5 * g11 * DDY(g_11) + 0.5 * g12 * DDX(g_22) + + 0.5 * g13 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G1_13 = 0.5 * g11 * DDZ(g_11) + 0.5 * g12 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + + 0.5 * g13 * DDX(g_33); + G1_23 = 0.5 * g11 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + + 0.5 * g12 * (DDZ(g_22) + DDY(g_23) - DDY(g_23)) + // + 0.5 *g13*(DDZ(g_32) + DDY(g_33) - DDZ(g_23)); + // which equals + + 0.5 * g13 * DDY(g_33); + + G2_11 = 0.5 * g12 * DDX(g_11) + g22 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g23 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G2_22 = g12 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g22 * DDY(g_22) + + g23 * (DDY(g23) - 0.5 * DDZ(g_22)); + G2_33 = g12 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g22 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g23 * DDZ(g_33); + G2_12 = 0.5 * g12 * DDY(g_11) + 0.5 * g22 * DDX(g_22) + + 0.5 * g23 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G2_13 = + // 0.5 *g21*(DDZ(g_11) + DDX(g_13) - DDX(g_13)) + // which equals + 0.5 * g12 * (DDZ(g_11) + DDX(g_13) - DDX(g_13)) + // + 0.5 *g22*(DDZ(g_21) + DDX(g_23) - DDY(g_13)) + // which equals + + 0.5 * g22 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + // + 0.5 *g23*(DDZ(g_31) + DDX(g_33) - DDZ(g_13)); + // which equals + + 0.5 * g23 * DDX(g_33); + G2_23 = 0.5 * g12 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g22 * DDZ(g_22) + + 0.5 * g23 * DDY(g_33); + + G3_11 = 0.5 * g13 * DDX(g_11) + g23 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g33 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G3_22 = g13 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g23 * DDY(g_22) + + g33 * (DDY(g_23) - 0.5 * DDZ(g_22)); + G3_33 = g13 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g23 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g33 * DDZ(g_33); + G3_12 = + // 0.5 *g31*(DDY(g_11) + DDX(g_12) - DDX(g_12)) + // which equals to + 0.5 * g13 * DDY(g_11) + // + 0.5 *g32*(DDY(g_21) + DDX(g_22) - DDY(g_12)) + // which equals to + + 0.5 * g23 * DDX(g_22) + //+ 0.5 *g33*(DDY(g_31) + DDX(g_32) - DDZ(g_12)); + // which equals to + + 0.5 * g33 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G3_13 = 0.5 * g13 * DDZ(g_11) + 0.5 * g23 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + + 0.5 * g33 * DDX(g_33); + G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) + + 0.5 * g33 * DDY(g_33); + + auto tmp = J * g12; + communicate(tmp); + G1 = (DDX(J * g11) + DDY(tmp) + DDZ(J * g13)) / J; + tmp = J * g22; + communicate(tmp); + G2 = (DDX(J * g12) + DDY(tmp) + DDZ(J * g23)) / J; + tmp = J * g23; + communicate(tmp); + G3 = (DDX(J * g13) + DDY(tmp) + DDZ(J * g33)) / J; + + // Communicate christoffel symbol terms + output_progress.write("\tCommunicating connection terms\n"); + + communicate(G1_11, G1_22, G1_33, G1_12, G1_13, G1_23, G2_11, G2_22, G2_33, G2_12, G2_13, + G2_23, G3_11, G3_22, G3_33, G3_12, G3_13, G3_23, G1, G2, G3); + + // Set boundary guard cells of Christoffel symbol terms + // Ideally, when location is staggered, we would set the upper/outer boundary point + // correctly rather than by extrapolating here: e.g. if location==CELL_YLOW and we are + // at the upper y-boundary the x- and z-derivatives at yend+1 at the boundary can be + // calculated because the guard cells are available, while the y-derivative could be + // calculated from the CELL_CENTRE metric components (which have guard cells available + // past the boundary location). This would avoid the problem that the y-boundary on the + // CELL_YLOW grid is at a 'guard cell' location (yend+1). + // However, the above would require lots of special handling, so just extrapolate for + // now. + G1_11 = interpolateAndExtrapolate(G1_11, location, true, true, true, transform.get()); + G1_22 = interpolateAndExtrapolate(G1_22, location, true, true, true, transform.get()); + G1_33 = interpolateAndExtrapolate(G1_33, location, true, true, true, transform.get()); + G1_12 = interpolateAndExtrapolate(G1_12, location, true, true, true, transform.get()); + G1_13 = interpolateAndExtrapolate(G1_13, location, true, true, true, transform.get()); + G1_23 = interpolateAndExtrapolate(G1_23, location, true, true, true, transform.get()); + + G2_11 = interpolateAndExtrapolate(G2_11, location, true, true, true, transform.get()); + G2_22 = interpolateAndExtrapolate(G2_22, location, true, true, true, transform.get()); + G2_33 = interpolateAndExtrapolate(G2_33, location, true, true, true, transform.get()); + G2_12 = interpolateAndExtrapolate(G2_12, location, true, true, true, transform.get()); + G2_13 = interpolateAndExtrapolate(G2_13, location, true, true, true, transform.get()); + G2_23 = interpolateAndExtrapolate(G2_23, location, true, true, true, transform.get()); + + G3_11 = interpolateAndExtrapolate(G3_11, location, true, true, true, transform.get()); + G3_22 = interpolateAndExtrapolate(G3_22, location, true, true, true, transform.get()); + G3_33 = interpolateAndExtrapolate(G3_33, location, true, true, true, transform.get()); + G3_12 = interpolateAndExtrapolate(G3_12, location, true, true, true, transform.get()); + G3_13 = interpolateAndExtrapolate(G3_13, location, true, true, true, transform.get()); + G3_23 = interpolateAndExtrapolate(G3_23, location, true, true, true, transform.get()); + + G1 = interpolateAndExtrapolate(G1, location, true, true, true, transform.get()); + G2 = interpolateAndExtrapolate(G2, location, true, true, true, transform.get()); + G3 = interpolateAndExtrapolate(G3, location, true, true, true, transform.get()); - G2_11 = G2_22 = G2_33 = G2_12 = G2_13 = G2_23 = - - G3_11 = G3_22 = G3_33 = G3_12 = G3_13 = G3_23 = - - G1 = G2 = G3 = BoutNaN; - } else { - // Calculate Christoffel symbol terms (18 independent values) - // Note: This calculation is completely general: metric - // tensor can be 2D or 3D. For 2D, all DDZ terms are zero - - G1_11 = 0.5 * g11 * DDX(g_11) + g12 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g13 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G1_22 = g11 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g12 * DDY(g_22) - + g13 * (DDY(g_23) - 0.5 * DDZ(g_22)); - G1_33 = g11 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g12 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g13 * DDZ(g_33); - G1_12 = 0.5 * g11 * DDY(g_11) + 0.5 * g12 * DDX(g_22) - + 0.5 * g13 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G1_13 = 0.5 * g11 * DDZ(g_11) + 0.5 * g12 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - + 0.5 * g13 * DDX(g_33); - G1_23 = 0.5 * g11 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) - + 0.5 * g12 * (DDZ(g_22) + DDY(g_23) - DDY(g_23)) - // + 0.5 *g13*(DDZ(g_32) + DDY(g_33) - DDZ(g_23)); - // which equals - + 0.5 * g13 * DDY(g_33); - - G2_11 = 0.5 * g12 * DDX(g_11) + g22 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g23 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G2_22 = g12 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g22 * DDY(g_22) - + g23 * (DDY(g23) - 0.5 * DDZ(g_22)); - G2_33 = g12 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g22 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g23 * DDZ(g_33); - G2_12 = 0.5 * g12 * DDY(g_11) + 0.5 * g22 * DDX(g_22) - + 0.5 * g23 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G2_13 = - // 0.5 *g21*(DDZ(g_11) + DDX(g_13) - DDX(g_13)) - // which equals - 0.5 * g12 * (DDZ(g_11) + DDX(g_13) - DDX(g_13)) - // + 0.5 *g22*(DDZ(g_21) + DDX(g_23) - DDY(g_13)) - // which equals - + 0.5 * g22 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - // + 0.5 *g23*(DDZ(g_31) + DDX(g_33) - DDZ(g_13)); - // which equals - + 0.5 * g23 * DDX(g_33); - G2_23 = 0.5 * g12 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g22 * DDZ(g_22) - + 0.5 * g23 * DDY(g_33); - - G3_11 = 0.5 * g13 * DDX(g_11) + g23 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g33 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G3_22 = g13 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g23 * DDY(g_22) - + g33 * (DDY(g_23) - 0.5 * DDZ(g_22)); - G3_33 = g13 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g23 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g33 * DDZ(g_33); - G3_12 = - // 0.5 *g31*(DDY(g_11) + DDX(g_12) - DDX(g_12)) - // which equals to - 0.5 * g13 * DDY(g_11) - // + 0.5 *g32*(DDY(g_21) + DDX(g_22) - DDY(g_12)) - // which equals to - + 0.5 * g23 * DDX(g_22) - //+ 0.5 *g33*(DDY(g_31) + DDX(g_32) - DDZ(g_12)); - // which equals to - + 0.5 * g33 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G3_13 = 0.5 * g13 * DDZ(g_11) + 0.5 * g23 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - + 0.5 * g33 * DDX(g_33); - G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) - + 0.5 * g33 * DDY(g_33); - - auto tmp = J * g12; - communicate(tmp); - G1 = (DDX(J * g11) + DDY(tmp) + DDZ(J * g13)) / J; - tmp = J * g22; - communicate(tmp); - G2 = (DDX(J * g12) + DDY(tmp) + DDZ(J * g23)) / J; - tmp = J * g23; - communicate(tmp); - G3 = (DDX(J * g13) + DDY(tmp) + DDZ(J * g33)) / J; - - // Communicate christoffel symbol terms - output_progress.write("\tCommunicating connection terms\n"); - - communicate(G1_11, G1_22, G1_33, G1_12, G1_13, G1_23, G2_11, G2_22, G2_33, G2_12, - G2_13, G2_23, G3_11, G3_22, G3_33, G3_12, G3_13, G3_23, G1, G2, G3); - - // Set boundary guard cells of Christoffel symbol terms - // Ideally, when location is staggered, we would set the upper/outer boundary point - // correctly rather than by extrapolating here: e.g. if location==CELL_YLOW and we are - // at the upper y-boundary the x- and z-derivatives at yend+1 at the boundary can be - // calculated because the guard cells are available, while the y-derivative could be - // calculated from the CELL_CENTRE metric components (which have guard cells available - // past the boundary location). This would avoid the problem that the y-boundary on the - // CELL_YLOW grid is at a 'guard cell' location (yend+1). - // However, the above would require lots of special handling, so just extrapolate for - // now. - G1_11 = interpolateAndExtrapolate(G1_11, location, true, true, true, transform.get()); - G1_22 = interpolateAndExtrapolate(G1_22, location, true, true, true, transform.get()); - G1_33 = interpolateAndExtrapolate(G1_33, location, true, true, true, transform.get()); - G1_12 = interpolateAndExtrapolate(G1_12, location, true, true, true, transform.get()); - G1_13 = interpolateAndExtrapolate(G1_13, location, true, true, true, transform.get()); - G1_23 = interpolateAndExtrapolate(G1_23, location, true, true, true, transform.get()); - - G2_11 = interpolateAndExtrapolate(G2_11, location, true, true, true, transform.get()); - G2_22 = interpolateAndExtrapolate(G2_22, location, true, true, true, transform.get()); - G2_33 = interpolateAndExtrapolate(G2_33, location, true, true, true, transform.get()); - G2_12 = interpolateAndExtrapolate(G2_12, location, true, true, true, transform.get()); - G2_13 = interpolateAndExtrapolate(G2_13, location, true, true, true, transform.get()); - G2_23 = interpolateAndExtrapolate(G2_23, location, true, true, true, transform.get()); - - G3_11 = interpolateAndExtrapolate(G3_11, location, true, true, true, transform.get()); - G3_22 = interpolateAndExtrapolate(G3_22, location, true, true, true, transform.get()); - G3_33 = interpolateAndExtrapolate(G3_33, location, true, true, true, transform.get()); - G3_12 = interpolateAndExtrapolate(G3_12, location, true, true, true, transform.get()); - G3_13 = interpolateAndExtrapolate(G3_13, location, true, true, true, transform.get()); - G3_23 = interpolateAndExtrapolate(G3_23, location, true, true, true, transform.get()); - - G1 = interpolateAndExtrapolate(G1, location, true, true, true, transform.get()); - G2 = interpolateAndExtrapolate(G2, location, true, true, true, transform.get()); - G3 = interpolateAndExtrapolate(G3, location, true, true, true, transform.get()); - } ////////////////////////////////////////////////////// /// Non-uniform meshes. Need to use DDX, DDY From ff711821172a0ac30b1a5559974f84c7028c6359 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 3 Mar 2025 10:01:55 +0100 Subject: [PATCH 240/256] Add iter_pnts function Directly iterate over the points --- include/bout/yboundary_regions.hxx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/include/bout/yboundary_regions.hxx b/include/bout/yboundary_regions.hxx index e0e93e17f9..c58a7a59b7 100644 --- a/include/bout/yboundary_regions.hxx +++ b/include/bout/yboundary_regions.hxx @@ -5,8 +5,8 @@ class YBoundary { public: - template - void iter_regions(const T& f) { + template + void iter_regions(const F& f) { ASSERT1(is_init); for (auto& region : boundary_regions) { f(*region); @@ -15,6 +15,14 @@ public: f(*region); } } + template + void iter_pnts(const F& f) { + iter_regions([&](auto& region) { + for (auto& pnt : region) { + f(pnt); + } + } + } template void iter(const F& f) { From d3bc5cc6e50e674019bb73e4319d6a81a6bba072 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 3 Mar 2025 10:02:46 +0100 Subject: [PATCH 241/256] Add example on how to replace RangeIterator with YBoundary --- include/bout/example_yboundary_regions.cxx | 65 ++++++++++++++++++++++ include/bout/yboundary_regions.hxx | 13 +++++ 2 files changed, 78 insertions(+) create mode 100644 include/bout/example_yboundary_regions.cxx diff --git a/include/bout/example_yboundary_regions.cxx b/include/bout/example_yboundary_regions.cxx new file mode 100644 index 0000000000..0e8d9b07dc --- /dev/null +++ b/include/bout/example_yboundary_regions.cxx @@ -0,0 +1,65 @@ +#include + +class yboundary_example_legacy { +public: + yboundary_example_legacy(Options* opt, const Field3D& N, const Field3D& V) + : N(N), V(V) { + Options& options = *opt; + lower_y = options["lower_y"].doc("Boundary on lower y?").withDefault(lower_y); + upper_y = options["upper_y"].doc("Boundary on upper y?").withDefault(upper_y); + } + + void rhs() { + BoutReal totalFlux = 0; + if (lower_y) { + for (RangeIterator r = mesh->iterateBndryLowerY(); !r.isDone(); r++) { + for (int jz = 0; jz < mesh->LocalNz; jz++) { + // Calculate flux through surface [normalised m^-2 s^-1], + // should be positive since V < 0.0 + BoutReal flux = + -0.5 * (N(r.ind, mesh->ystart, jz) + N(r.ind, mesh->ystart - 1, jz)) * 0.5 + * (V(r.ind, mesh->ystart, jz) + V(r.ind, mesh->ystart - 1, jz)); + totalFlux += flux; + } + } + } + if (upper_y) { + for (RangeIterator r = mesh->iterateBndryUpperY(); !r.isDone(); r++) { + for (int jz = 0; jz < mesh->LocalNz; jz++) { + // Calculate flux through surface [normalised m^-2 s^-1], + // should be positive since V < 0.0 + BoutReal flux = -0.5 * (N(r.ind, mesh->yend, jz) + N(r.ind, mesh->yend + 1, jz)) + * 0.5 + * (V(r.ind, mesh->yend, jz) + V(r.ind, mesh->yend + 1, jz)); + totalFlux += flux; + } + } + } + } + +private: + bool lower_y{true}; + bool upper_y{true}; + const Field3D& N; + const Field3D& V; +} + +class yboundary_example { +public: + yboundary_example(Options* opt, const Field3D& N, const Field3D& V) : N(N), V(V) { + // Set what kind of yboundaries you want to include + yboundary.init(opt); + } + + void rhs() { + BoutReal totalFlux = 0; + yboundary.iter_pnts([&](auto& pnt) { + BoutReal flux = pnt.interpolate_sheath_o1(N) * pnt.interpolate_sheath_o1(V); + }); + } + +private: + YBoundary ybounday; + const Field3D& N; + const Field3D& V; +}; diff --git a/include/bout/yboundary_regions.hxx b/include/bout/yboundary_regions.hxx index c58a7a59b7..67fdebc823 100644 --- a/include/bout/yboundary_regions.hxx +++ b/include/bout/yboundary_regions.hxx @@ -2,6 +2,19 @@ #include "./boundary_iterator.hxx" #include "bout/parallel_boundary_region.hxx" +/*! + * This class allows to simplify iterating over y-boundaries. + * + * It makes it easier to write code for FieldAligned boundaries, but if a bit + * care is taken the code also works with FluxCoordinateIndependent code. + * + * An example how to replace old code is given here: + * + * \example example_yboundary_regions.hxx + * This is an example how to use the YBoundary class to replace RangeIterator + * boundaries. + */ + class YBoundary { public: From 83375317790c7a4d7a3299ea1a2bafb0080eb2ba Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 3 Mar 2025 11:25:50 +0100 Subject: [PATCH 242/256] Move documentation to sphinx --- include/bout/example_yboundary_regions.cxx | 65 ---------------- include/bout/yboundary_regions.hxx | 21 +++--- manual/sphinx/user_docs/boundary_options.rst | 79 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 77 deletions(-) delete mode 100644 include/bout/example_yboundary_regions.cxx diff --git a/include/bout/example_yboundary_regions.cxx b/include/bout/example_yboundary_regions.cxx deleted file mode 100644 index 0e8d9b07dc..0000000000 --- a/include/bout/example_yboundary_regions.cxx +++ /dev/null @@ -1,65 +0,0 @@ -#include - -class yboundary_example_legacy { -public: - yboundary_example_legacy(Options* opt, const Field3D& N, const Field3D& V) - : N(N), V(V) { - Options& options = *opt; - lower_y = options["lower_y"].doc("Boundary on lower y?").withDefault(lower_y); - upper_y = options["upper_y"].doc("Boundary on upper y?").withDefault(upper_y); - } - - void rhs() { - BoutReal totalFlux = 0; - if (lower_y) { - for (RangeIterator r = mesh->iterateBndryLowerY(); !r.isDone(); r++) { - for (int jz = 0; jz < mesh->LocalNz; jz++) { - // Calculate flux through surface [normalised m^-2 s^-1], - // should be positive since V < 0.0 - BoutReal flux = - -0.5 * (N(r.ind, mesh->ystart, jz) + N(r.ind, mesh->ystart - 1, jz)) * 0.5 - * (V(r.ind, mesh->ystart, jz) + V(r.ind, mesh->ystart - 1, jz)); - totalFlux += flux; - } - } - } - if (upper_y) { - for (RangeIterator r = mesh->iterateBndryUpperY(); !r.isDone(); r++) { - for (int jz = 0; jz < mesh->LocalNz; jz++) { - // Calculate flux through surface [normalised m^-2 s^-1], - // should be positive since V < 0.0 - BoutReal flux = -0.5 * (N(r.ind, mesh->yend, jz) + N(r.ind, mesh->yend + 1, jz)) - * 0.5 - * (V(r.ind, mesh->yend, jz) + V(r.ind, mesh->yend + 1, jz)); - totalFlux += flux; - } - } - } - } - -private: - bool lower_y{true}; - bool upper_y{true}; - const Field3D& N; - const Field3D& V; -} - -class yboundary_example { -public: - yboundary_example(Options* opt, const Field3D& N, const Field3D& V) : N(N), V(V) { - // Set what kind of yboundaries you want to include - yboundary.init(opt); - } - - void rhs() { - BoutReal totalFlux = 0; - yboundary.iter_pnts([&](auto& pnt) { - BoutReal flux = pnt.interpolate_sheath_o1(N) * pnt.interpolate_sheath_o1(V); - }); - } - -private: - YBoundary ybounday; - const Field3D& N; - const Field3D& V; -}; diff --git a/include/bout/yboundary_regions.hxx b/include/bout/yboundary_regions.hxx index 67fdebc823..f9ee0ff21c 100644 --- a/include/bout/yboundary_regions.hxx +++ b/include/bout/yboundary_regions.hxx @@ -2,18 +2,15 @@ #include "./boundary_iterator.hxx" #include "bout/parallel_boundary_region.hxx" -/*! - * This class allows to simplify iterating over y-boundaries. - * - * It makes it easier to write code for FieldAligned boundaries, but if a bit - * care is taken the code also works with FluxCoordinateIndependent code. - * - * An example how to replace old code is given here: - * - * \example example_yboundary_regions.hxx - * This is an example how to use the YBoundary class to replace RangeIterator - * boundaries. - */ + +/// This class allows to simplify iterating over y-boundaries. +/// +/// It makes it easier to write code for FieldAligned boundaries, but if a bit +/// care is taken the code also works with FluxCoordinateIndependent code. +/// +/// An example how to replace old code is given here: +/// ../../manual/sphinx/user_docs/boundary_options.rst + class YBoundary { diff --git a/manual/sphinx/user_docs/boundary_options.rst b/manual/sphinx/user_docs/boundary_options.rst index 826f873dc1..d3cea5edb6 100644 --- a/manual/sphinx/user_docs/boundary_options.rst +++ b/manual/sphinx/user_docs/boundary_options.rst @@ -435,6 +435,85 @@ the upper Y boundary of a 2D variable ``var``:: The `BoundaryRegion` class is defined in ``include/boundary_region.hxx`` +Y-Boundaries +------------ + +The sheath boundaries are often implemented in the physics model. +Previously of they where implemented using a `RangeIterator`:: + + class yboundary_example_legacy { + public: + yboundary_example_legacy(Options* opt, const Field3D& N, const Field3D& V) + : N(N), V(V) { + Options& options = *opt; + lower_y = options["lower_y"].doc("Boundary on lower y?").withDefault(lower_y); + upper_y = options["upper_y"].doc("Boundary on upper y?").withDefault(upper_y); + } + + void rhs() { + BoutReal totalFlux = 0; + if (lower_y) { + for (RangeIterator r = mesh->iterateBndryLowerY(); !r.isDone(); r++) { + for (int jz = 0; jz < mesh->LocalNz; jz++) { + // Calculate flux through surface [normalised m^-2 s^-1], + // should be positive since V < 0.0 + BoutReal flux = + -0.5 * (N(r.ind, mesh->ystart, jz) + N(r.ind, mesh->ystart - 1, jz)) * 0.5 + * (V(r.ind, mesh->ystart, jz) + V(r.ind, mesh->ystart - 1, jz)); + totalFlux += flux; + } + } + } + if (upper_y) { + for (RangeIterator r = mesh->iterateBndryUpperY(); !r.isDone(); r++) { + for (int jz = 0; jz < mesh->LocalNz; jz++) { + // Calculate flux through surface [normalised m^-2 s^-1], + // should be positive since V < 0.0 + BoutReal flux = -0.5 * (N(r.ind, mesh->yend, jz) + N(r.ind, mesh->yend + 1, jz)) + * 0.5 + * (V(r.ind, mesh->yend, jz) + V(r.ind, mesh->yend + 1, jz)); + totalFlux += flux; + } + } + } + } + + private: + bool lower_y{true}; + bool upper_y{true}; + const Field3D& N; + const Field3D& V; + } + + +This can be replaced using the `YBoundary` class, which not only simplifies the +code, but also allows to have the same code working with non-field-aligned +geometries, as flux coordinate independent (FCI) method:: + + #include + + class yboundary_example { + public: + yboundary_example(Options* opt, const Field3D& N, const Field3D& V) : N(N), V(V) { + // Set what kind of yboundaries you want to include + yboundary.init(opt); + } + + void rhs() { + BoutReal totalFlux = 0; + yboundary.iter_pnts([&](auto& pnt) { + BoutReal flux = pnt.interpolate_sheath_o1(N) * pnt.interpolate_sheath_o1(V); + }); + } + + private: + YBoundary ybounday; + const Field3D& N; + const Field3D& V; + }; + + + Boundary regions ---------------- From fcc3af65263ee8e227868fb9d10b9466aab8333a Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 3 Mar 2025 13:40:58 +0100 Subject: [PATCH 243/256] Only read `MYG` if it set or mesh:MYG is not set This avoids errors in the MMS tests --- src/mesh/impls/bout/boutmesh.cxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index 5c61e23554..d81bd10698 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -500,7 +500,9 @@ int BoutMesh::load() { } else { meshMyg = MYG; } - MYG = options["MYG"].doc("Number of guard cells on each side in Y").withDefault(MYG); + if (options.isSet("MYG") or (!meshHasMyg)) { + MYG = options["MYG"].doc("Number of guard cells on each side in Y").withDefault(MYG); + } if (meshHasMyg && MYG != meshMyg) { output_warn.write(_("Options changed the number of y-guard cells. Grid has {} but " "option specified {}! Continuing with {}"), From f925c94f3d6cc70975ca4a602fcac2d88928bf07 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 3 Mar 2025 14:02:54 +0100 Subject: [PATCH 244/256] Do not set MYG/MXG if it is not needed --- tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp b/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp index d5ca4c4d71..519faa0403 100644 --- a/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp +++ b/tests/integrated/test-boutpp/mms-ddz/data/BOUT.inp @@ -1,7 +1,3 @@ - -MXG = 2 -MYG = 2 - [mesh] staggergrids = true n = 1 From 51e7b58991b10ff033813980b547f9af03d84675 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 3 Mar 2025 14:03:41 +0100 Subject: [PATCH 245/256] Do not set MYG/MXG if it is not needed --- tests/integrated/test-boutpp/collect/input/BOUT.inp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integrated/test-boutpp/collect/input/BOUT.inp b/tests/integrated/test-boutpp/collect/input/BOUT.inp index d5ca4c4d71..519faa0403 100644 --- a/tests/integrated/test-boutpp/collect/input/BOUT.inp +++ b/tests/integrated/test-boutpp/collect/input/BOUT.inp @@ -1,7 +1,3 @@ - -MXG = 2 -MYG = 2 - [mesh] staggergrids = true n = 1 From 1c8fb4737a52805a128a002ac382ab0fa825380c Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 4 Mar 2025 15:51:40 +0100 Subject: [PATCH 246/256] Fix iter_pnts --- include/bout/yboundary_regions.hxx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/include/bout/yboundary_regions.hxx b/include/bout/yboundary_regions.hxx index f9ee0ff21c..1d434e2420 100644 --- a/include/bout/yboundary_regions.hxx +++ b/include/bout/yboundary_regions.hxx @@ -11,8 +11,6 @@ /// An example how to replace old code is given here: /// ../../manual/sphinx/user_docs/boundary_options.rst - - class YBoundary { public: template @@ -29,9 +27,9 @@ public: void iter_pnts(const F& f) { iter_regions([&](auto& region) { for (auto& pnt : region) { - f(pnt); + f(pnt); } - } + }); } template @@ -80,5 +78,3 @@ private: bool is_init{false}; }; - -extern YBoundary yboundary; From bfaf9867b9d9c94155f25b606e65638dec575432 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 4 Mar 2025 15:52:00 +0100 Subject: [PATCH 247/256] Add more documentation on YBoundary --- manual/sphinx/user_docs/boundary_options.rst | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/manual/sphinx/user_docs/boundary_options.rst b/manual/sphinx/user_docs/boundary_options.rst index d3cea5edb6..e2d8f28b3f 100644 --- a/manual/sphinx/user_docs/boundary_options.rst +++ b/manual/sphinx/user_docs/boundary_options.rst @@ -514,6 +514,51 @@ geometries, as flux coordinate independent (FCI) method:: +There are several member functions of ``pnt``. ``pnt`` is of type +`BoundaryRegionParIterBase` and `BoundaryRegionIter`, and both should provide +the same interface. If they don't that is a bug, as the above code is a +template, that gets instantiated for both types, and thus requires both +classes to provide the same interface, one for FCI-like boundaries and one for +field aligned boundaries. + +Here is a short summary of some members of ``pnt``, where ``f`` is a : + +.. list-table:: Members for boundary operation + :widths: 15 70 + :header-rows: 1 + + * - Function + - Description + * - ``pnt.ythis(f)`` + - Returns the value at the last point in the domain + * - ``pnt.ynext(f)`` + - Returns the value at the first point in the domain + * - ``pnt.yprev(f)`` + - Returns the value at the second to last point in the domain, if it is + valid. NB: this point may not be valid. + * - ``pnt.interpolate_sheath_o1(f)`` + - Returns the value at the boundary, assuming the bounday value has been set + * - ``pnt.extrapolate_sheath_o1(f)`` + - Returns the value at the boundary, extrapolating from the bulk, first order + * - ``pnt.extrapolate_sheath_o2(f)`` + - Returns the value at the boundary, extrapolating from the bulk, second order + * - ``pnt.extrapolate_next_o{1,2}(f)`` + - Extrapolate into the boundary from the bulk, first or second order + * - ``pnt.extrapolate_grad_o{1,2}(f)`` + - Extrapolate the gradient into the boundary, first or second order + * - ``pnt.dirichlet_o{1,2,3}(f, v)`` + - Apply dirichlet boundary conditions with value ``v`` and given order + * - ``pnt.neumann_o{1,2,3}(f, v)`` + - Applies a gradient of ``v / dy`` boundary condition. + * - ``pnt.limitFree(f)`` + - Extrapolate into the boundary using only monotonic decreasing values. + ``f`` needs to be positive. + * - ``pnt.dir`` + - The direction of the boundary. + + + + Boundary regions ---------------- From f4acdb0bcbb3599d8e99d3cf4bee29b94bc1512e Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 7 Mar 2025 10:39:28 +0100 Subject: [PATCH 248/256] Ensure the field has parallel slices --- include/bout/parallel_boundary_region.hxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index f808296edb..ad2bcd0331 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -231,6 +231,7 @@ public: template BoutReal& getAt(Field3D& f, int off) const { + ASSERT4(f.hasParallelSlices()); if constexpr (check) { ASSERT3(valid() > -off - 2); } @@ -239,6 +240,7 @@ public: } template const BoutReal& getAt(const Field3D& f, int off) const { + ASSERT4(f.hasParallelSlices()); if constexpr (check) { ASSERT3(valid() > -off - 2); } From 23f599230b36044a969e9658b68bf4f7f42d3a32 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 7 Mar 2025 11:30:36 +0100 Subject: [PATCH 249/256] Lower check level --- include/bout/parallel_boundary_region.hxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/bout/parallel_boundary_region.hxx b/include/bout/parallel_boundary_region.hxx index ad2bcd0331..849bb0ffe3 100644 --- a/include/bout/parallel_boundary_region.hxx +++ b/include/bout/parallel_boundary_region.hxx @@ -231,7 +231,7 @@ public: template BoutReal& getAt(Field3D& f, int off) const { - ASSERT4(f.hasParallelSlices()); + ASSERT3(f.hasParallelSlices()); if constexpr (check) { ASSERT3(valid() > -off - 2); } @@ -240,7 +240,7 @@ public: } template const BoutReal& getAt(const Field3D& f, int off) const { - ASSERT4(f.hasParallelSlices()); + ASSERT3(f.hasParallelSlices()); if constexpr (check) { ASSERT3(valid() > -off - 2); } From d05e7336a839c10b1687a0517ddb90cbf2786a1c Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 11 Mar 2025 15:02:12 +0100 Subject: [PATCH 250/256] Loosen tolereances again This partially reverts 0bcc047d64e87ca9446fe1127b761a58215201f5 It seems the achieved accuracy depends on some factors that are not well controlled. --- tests/integrated/test-petsc_laplace/runtest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrated/test-petsc_laplace/runtest b/tests/integrated/test-petsc_laplace/runtest index 87c3991d00..befb87c04e 100755 --- a/tests/integrated/test-petsc_laplace/runtest +++ b/tests/integrated/test-petsc_laplace/runtest @@ -11,10 +11,10 @@ # Variables to compare vars = [ ("max_error1", 2.0e-4), - ("max_error2", 2.0e-8), + ("max_error2", 2.0e-4), ("max_error3", 2.0e-4), ("max_error4", 2.0e-4), - ("max_error5", 4.0e-6), + ("max_error5", 2.0e-4), ("max_error6", 2.0e-4), ] From 35206d44c900eaae284e507110d79d5b6a531aea Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 11 Mar 2025 16:14:49 +0100 Subject: [PATCH 251/256] Remove broken code Likely this is not needed. --- src/mesh/coordinates.cxx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 53ec4e9d16..d650c9e9ec 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1512,14 +1512,6 @@ Coordinates::FieldMetric Coordinates::DDY(const Field2D& f, CELL_LOC loc, Field3D Coordinates::DDY(const Field3D& f, CELL_LOC outloc, const std::string& method, const std::string& region) const { -#if BOUT_USE_METRIC_3D - if (!f.hasParallelSlices() and !transform->canToFromFieldAligned()) { - Field3D f_parallel = f; - transform->calcParallelSlices(f_parallel); - f_parallel.applyParallelBoundary("parallel_neumann_o2"); - return bout::derivatives::index::DDY(f_parallel, outloc, method, region); - } -#endif return bout::derivatives::index::DDY(f, outloc, method, region) / dy; }; From df490b9278b601ce1ed47a0bef6f100e5a39f26e Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 12 Mar 2025 09:16:05 +0100 Subject: [PATCH 252/256] CI: Avoid issues with special characters --- .github/workflows/clang-format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index a6508a2dcd..d99b370810 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -22,7 +22,7 @@ jobs: - name: Run clang-format id: format - run: git clang-format origin/${{ github.base_ref }} || : + run: 'git clang-format origin/${{ github.base_ref }} || :' - name: Commit to the PR branch uses: stefanzweifel/git-auto-commit-action@v5 From eef32f937436cffccabdbf733936a1be6f0d7e08 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 12 Mar 2025 09:50:40 +0100 Subject: [PATCH 253/256] CI: run git-clang-format until there are no more changes That might format more code at once, but should avoid a CI loop. --- .github/workflows/clang-format.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index d99b370810..5e25154300 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -22,7 +22,11 @@ jobs: - name: Run clang-format id: format - run: 'git clang-format origin/${{ github.base_ref }} || :' + run: + while ! git clang-format origin/${{ github.base_ref }} + do + true + done - name: Commit to the PR branch uses: stefanzweifel/git-auto-commit-action@v5 From 9fd76bfe5c526cfa3d9984f6e761c235403d1723 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 12 Mar 2025 11:06:31 +0100 Subject: [PATCH 254/256] CI: use one line --- .github/workflows/clang-format.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 5e25154300..3b8cf6ee50 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -23,10 +23,7 @@ jobs: - name: Run clang-format id: format run: - while ! git clang-format origin/${{ github.base_ref }} - do - true - done + while ! git clang-format origin/${{ github.base_ref }} ; do true ; done - name: Commit to the PR branch uses: stefanzweifel/git-auto-commit-action@v5 From ee9dc990700cac267bd4555ac499fe68d57dd8bc Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 12 Mar 2025 11:16:07 +0100 Subject: [PATCH 255/256] CI: stage before we run git-clang-format again --- .github/workflows/clang-format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 3b8cf6ee50..49dfb31e25 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -23,7 +23,7 @@ jobs: - name: Run clang-format id: format run: - while ! git clang-format origin/${{ github.base_ref }} ; do true ; done + while ! git clang-format origin/${{ github.base_ref }} ; do git add . ; done - name: Commit to the PR branch uses: stefanzweifel/git-auto-commit-action@v5 From d10cd711ee1cfb1019aae0206776ffb4fb91b4b9 Mon Sep 17 00:00:00 2001 From: dschwoerer <5637662+dschwoerer@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:16:59 +0000 Subject: [PATCH 256/256] Apply clang-format changes --- include/bout/field.hxx | 38 ++-- include/bout/field3d.hxx | 2 +- include/bout/fv_ops.hxx | 2 +- include/bout/index_derivs_interface.hxx | 6 +- include/bout/interpolation_xz.hxx | 11 +- include/bout/mesh.hxx | 1 - include/bout/petsclib.hxx | 2 +- include/bout/physicsmodel.hxx | 1 + src/field/field3d.cxx | 20 +- .../laplace/impls/petsc/petsc_laplace.cxx | 189 +++++++++--------- src/mesh/boundary_standard.cxx | 2 +- src/mesh/coordinates.cxx | 9 +- src/mesh/fv_ops.cxx | 2 +- src/mesh/impls/bout/boutmesh.cxx | 4 +- src/mesh/interpolation/hermite_spline_xz.cxx | 7 +- src/mesh/interpolation/lagrange_4pt_xz.cxx | 1 - src/mesh/interpolation_xz.cxx | 4 +- src/mesh/parallel/fci.cxx | 31 ++- src/mesh/parallel/fci.hxx | 1 + src/mesh/parallel/fci_comm.hxx | 4 +- src/solver/impls/pvode/pvode.cxx | 6 +- src/sys/options.cxx | 10 +- .../test-fci-boundary/get_par_bndry.cxx | 5 +- 23 files changed, 174 insertions(+), 184 deletions(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index d56322070e..27835ecbd7 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -531,18 +531,18 @@ T pow(BoutReal lhs, const T& rhs, const std::string& rgn = "RGN_ALL") { #ifdef FIELD_FUNC #error This macro has already been defined #else -#define FIELD_FUNC(_name, func) \ - template > \ - inline T _name(const T& f, const std::string& rgn = "RGN_ALL") { \ - AUTO_TRACE(); \ - /* Check if the input is allocated */ \ - checkData(f); \ - /* Define and allocate the output result */ \ - T result{emptyFrom(f)}; \ - BOUT_FOR(d, result.getRegion(rgn)) { result[d] = func(f[d]); } \ - result.name = std::string(#_name "(") + f.name + std::string(")"); \ - checkData(result); \ - return result; \ +#define FIELD_FUNC(_name, func) \ + template > \ + inline T _name(const T& f, const std::string& rgn = "RGN_ALL") { \ + AUTO_TRACE(); \ + /* Check if the input is allocated */ \ + checkData(f); \ + /* Define and allocate the output result */ \ + T result{emptyFrom(f)}; \ + BOUT_FOR(d, result.getRegion(rgn)) { result[d] = func(f[d]); } \ + result.name = std::string(#_name "(") + f.name + std::string(")"); \ + checkData(result); \ + return result; \ } #endif @@ -685,16 +685,16 @@ inline T floor(const T& var, BoutReal f, const std::string& rgn = "RGN_ALL") { } #if BOUT_USE_FCI_AUTOMAGIC if (var.isFci()) { - for (size_t i=0; i < result.numberParallelSlices(); ++i) { + for (size_t i = 0; i < result.numberParallelSlices(); ++i) { BOUT_FOR(d, result.yup(i).getRegion(rgn)) { - if (result.yup(i)[d] < f) { - result.yup(i)[d] = f; - } + if (result.yup(i)[d] < f) { + result.yup(i)[d] = f; + } } BOUT_FOR(d, result.ydown(i).getRegion(rgn)) { - if (result.ydown(i)[d] < f) { - result.ydown(i)[d] = f; - } + if (result.ydown(i)[d] < f) { + result.ydown(i)[d] = f; + } } } } else diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 2d4c2e243d..763c334cc1 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -488,7 +488,7 @@ public: friend class Vector2D; Field3D& calcParallelSlices(); - void allowParallelSlices([[maybe_unused]] bool allow){ + void allowParallelSlices([[maybe_unused]] bool allow) { #if CHECK > 0 allowCalcParallelSlices = allow; #endif diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index 97558ddcfb..8a9baaf3e7 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -10,8 +10,8 @@ #include "bout/vector2d.hxx" #include "bout/utils.hxx" -#include #include +#include namespace FV { /*! diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 2c2c21d6cf..bc9a687b34 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -203,11 +203,13 @@ T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D if (f.isFci()) { ASSERT1(f.getDirectionY() == YDirectionType::Standard); T f_tmp = f; - if (!f.hasParallelSlices()){ + if (!f.hasParallelSlices()) { #if BOUT_USE_FCI_AUTOMAGIC f_tmp.calcParallelSlices(); #else - throw BoutException("parallel slices needed for parallel derivatives. Make sure to communicate and apply parallel boundary conditions before calling derivative"); + throw BoutException( + "parallel slices needed for parallel derivatives. Make sure to communicate and " + "apply parallel boundary conditions before calling derivative"); #endif } return standardDerivative(f_tmp, outloc, diff --git a/include/bout/interpolation_xz.hxx b/include/bout/interpolation_xz.hxx index fd4a4fcd50..9a7e788e67 100644 --- a/include/bout/interpolation_xz.hxx +++ b/include/bout/interpolation_xz.hxx @@ -134,7 +134,6 @@ public: } }; - template class XZHermiteSplineBase : public XZInterpolation { protected: @@ -282,19 +281,15 @@ public: const std::string& region = "RGN_NOBNDRY") override; }; - -class XZMonotonicHermiteSplineLegacy: public XZHermiteSplineBase { +class XZMonotonicHermiteSplineLegacy : public XZHermiteSplineBase { public: using XZHermiteSplineBase::interpolate; virtual Field3D interpolate(const Field3D& f, const std::string& region = "RGN_NOBNDRY") const override; - template - XZMonotonicHermiteSplineLegacy(Ts... args) : - XZHermiteSplineBase(args...) - {} + template + XZMonotonicHermiteSplineLegacy(Ts... args) : XZHermiteSplineBase(args...) {} }; - class XZInterpolationFactory : public Factory { public: diff --git a/include/bout/mesh.hxx b/include/bout/mesh.hxx index 563d792bf8..af1caad89b 100644 --- a/include/bout/mesh.hxx +++ b/include/bout/mesh.hxx @@ -842,7 +842,6 @@ public: return not coords->getParallelTransform().canToFromFieldAligned(); } - private: /// Allocates default Coordinates objects /// By default attempts to read staggered Coordinates from grid data source, diff --git a/include/bout/petsclib.hxx b/include/bout/petsclib.hxx index aa6f874f11..83e57184aa 100644 --- a/include/bout/petsclib.hxx +++ b/include/bout/petsclib.hxx @@ -156,7 +156,7 @@ private: #endif // PETSC_VERSION_GE -#if ! PETSC_VERSION_GE(3, 19, 0) +#if !PETSC_VERSION_GE(3, 19, 0) #define PETSC_SUCCESS ((PetscErrorCode)0) #endif diff --git a/include/bout/physicsmodel.hxx b/include/bout/physicsmodel.hxx index 7588b86f79..ff53bc6845 100644 --- a/include/bout/physicsmodel.hxx +++ b/include/bout/physicsmodel.hxx @@ -275,6 +275,7 @@ protected: public: /// Output additional variables other than the evolving variables virtual void outputVars(Options& options); + protected: /// Add additional variables other than the evolving variables to the restart files virtual void restartVars(Options& options); diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 5980274e4e..85077a73ff 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -96,7 +96,7 @@ Field3D::Field3D(const BoutReal val, Mesh* localmesh) : Field3D(localmesh) { #if BOUT_USE_FCI_AUTOMAGIC if (this->isFci()) { splitParallelSlices(); - for (size_t i=0; igetRegionID(region_name); } -void Field3D::resetRegion() { - regionID.reset(); -}; -void Field3D::setRegion(size_t id) { - regionID = id; -}; -void Field3D::setRegion(std::optional id) { - regionID = id; -}; +void Field3D::resetRegion() { regionID.reset(); }; +void Field3D::setRegion(size_t id) { regionID = id; }; +void Field3D::setRegion(std::optional id) { regionID = id; }; Field3D& Field3D::enableTracking(const std::string& name, Options& _tracking) { tracking = &_tracking; @@ -929,9 +923,9 @@ Options* Field3D::track(const T& change, std::string operation) { const std::string changename = change.name; #endif (*tracking)[outname].setAttributes({ - {"operation", operation}, + {"operation", operation}, #if BOUT_USE_TRACK - {"rhs.name", changename}, + {"rhs.name", changename}, #endif }); return &(*tracking)[outname]; diff --git a/src/invert/laplace/impls/petsc/petsc_laplace.cxx b/src/invert/laplace/impls/petsc/petsc_laplace.cxx index 19978939f6..bcfe6264d7 100644 --- a/src/invert/laplace/impls/petsc/petsc_laplace.cxx +++ b/src/invert/laplace/impls/petsc/petsc_laplace.cxx @@ -375,92 +375,92 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0, * In other word the indexing is done in a row-major order, but starting at * bottom left rather than top left */ - // X=0 to localmesh->xstart-1 defines the boundary region of the domain. - // Set the values for the inner boundary region - if (localmesh->firstX()) { - for (int x = 0; x < localmesh->xstart; x++) { - for (int z = 0; z < localmesh->LocalNz; z++) { - PetscScalar val; // Value of element to be set in the matrix - // If Neumann Boundary Conditions are set. - if (isInnerBoundaryFlagSet(INVERT_AC_GRAD)) { - // Set values corresponding to nodes adjacent in x - if (fourth_order) { - // Fourth Order Accuracy on Boundary - Element(i, x, z, 0, 0, - -25.0 / (12.0 * coords->dx(x, y, z)) / sqrt(coords->g_11(x, y, z)), - MatA); - Element(i, x, z, 1, 0, - 4.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); - Element(i, x, z, 2, 0, - -3.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); - Element(i, x, z, 3, 0, - 4.0 / (3.0 * coords->dx(x, y, z)) / sqrt(coords->g_11(x, y, z)), - MatA); - Element(i, x, z, 4, 0, - -1.0 / (4.0 * coords->dx(x, y, z)) / sqrt(coords->g_11(x, y, z)), - MatA); - } else { - // Second Order Accuracy on Boundary - // Element(i,x,z, 0, 0, -3.0 / (2.0*coords->dx(x,y)), MatA ); - // Element(i,x,z, 1, 0, 2.0 / coords->dx(x,y), MatA ); - // Element(i,x,z, 2, 0, -1.0 / (2.0*coords->dx(x,y)), MatA ); - // Element(i,x,z, 3, 0, 0.0, MatA ); // Reset these elements to 0 - // in case 4th order flag was used previously: not allowed now - // Element(i,x,z, 4, 0, 0.0, MatA ); - // Second Order Accuracy on Boundary, set half-way between grid points - Element(i, x, z, 0, 0, - -1.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); - Element(i, x, z, 1, 0, - 1.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); - Element(i, x, z, 2, 0, 0.0, MatA); - // Element(i,x,z, 3, 0, 0.0, MatA ); // Reset - // these elements to 0 in case 4th order flag was - // used previously: not allowed now - // Element(i,x,z, 4, 0, 0.0, MatA ); - } + // X=0 to localmesh->xstart-1 defines the boundary region of the domain. + // Set the values for the inner boundary region + if (localmesh->firstX()) { + for (int x = 0; x < localmesh->xstart; x++) { + for (int z = 0; z < localmesh->LocalNz; z++) { + PetscScalar val; // Value of element to be set in the matrix + // If Neumann Boundary Conditions are set. + if (isInnerBoundaryFlagSet(INVERT_AC_GRAD)) { + // Set values corresponding to nodes adjacent in x + if (fourth_order) { + // Fourth Order Accuracy on Boundary + Element(i, x, z, 0, 0, + -25.0 / (12.0 * coords->dx(x, y, z)) / sqrt(coords->g_11(x, y, z)), + MatA); + Element(i, x, z, 1, 0, + 4.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); + Element(i, x, z, 2, 0, + -3.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); + Element(i, x, z, 3, 0, + 4.0 / (3.0 * coords->dx(x, y, z)) / sqrt(coords->g_11(x, y, z)), + MatA); + Element(i, x, z, 4, 0, + -1.0 / (4.0 * coords->dx(x, y, z)) / sqrt(coords->g_11(x, y, z)), + MatA); } else { - if (fourth_order) { - // Set Diagonal Values to 1 - Element(i, x, z, 0, 0, 1., MatA); - - // Set off diagonal elements to zero - Element(i, x, z, 1, 0, 0.0, MatA); - Element(i, x, z, 2, 0, 0.0, MatA); - Element(i, x, z, 3, 0, 0.0, MatA); - Element(i, x, z, 4, 0, 0.0, MatA); - } else { - Element(i, x, z, 0, 0, 0.5, MatA); - Element(i, x, z, 1, 0, 0.5, MatA); - Element(i, x, z, 2, 0, 0., MatA); - } + // Second Order Accuracy on Boundary + // Element(i,x,z, 0, 0, -3.0 / (2.0*coords->dx(x,y)), MatA ); + // Element(i,x,z, 1, 0, 2.0 / coords->dx(x,y), MatA ); + // Element(i,x,z, 2, 0, -1.0 / (2.0*coords->dx(x,y)), MatA ); + // Element(i,x,z, 3, 0, 0.0, MatA ); // Reset these elements to 0 + // in case 4th order flag was used previously: not allowed now + // Element(i,x,z, 4, 0, 0.0, MatA ); + // Second Order Accuracy on Boundary, set half-way between grid points + Element(i, x, z, 0, 0, + -1.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); + Element(i, x, z, 1, 0, + 1.0 / coords->dx(x, y, z) / sqrt(coords->g_11(x, y, z)), MatA); + Element(i, x, z, 2, 0, 0.0, MatA); + // Element(i,x,z, 3, 0, 0.0, MatA ); // Reset + // these elements to 0 in case 4th order flag was + // used previously: not allowed now + // Element(i,x,z, 4, 0, 0.0, MatA ); } - - val = 0; // Initialize val - - // Set Components of RHS - // If the inner boundary value should be set by b or x0 - if (isInnerBoundaryFlagSet(INVERT_RHS)) { - val = b[x][z]; - } else if (isInnerBoundaryFlagSet(INVERT_SET)) { - val = x0[x][z]; + } else { + if (fourth_order) { + // Set Diagonal Values to 1 + Element(i, x, z, 0, 0, 1., MatA); + + // Set off diagonal elements to zero + Element(i, x, z, 1, 0, 0.0, MatA); + Element(i, x, z, 2, 0, 0.0, MatA); + Element(i, x, z, 3, 0, 0.0, MatA); + Element(i, x, z, 4, 0, 0.0, MatA); + } else { + Element(i, x, z, 0, 0, 0.5, MatA); + Element(i, x, z, 1, 0, 0.5, MatA); + Element(i, x, z, 2, 0, 0., MatA); } + } - // Set components of the RHS (the PETSc vector bs) - // 1 element is being set in row i to val - // INSERT_VALUES replaces existing entries with new values - VecSetValues(bs, 1, &i, &val, INSERT_VALUES); + val = 0; // Initialize val - // Set components of the and trial solution (the PETSc vector xs) - // 1 element is being set in row i to val - // INSERT_VALUES replaces existing entries with new values + // Set Components of RHS + // If the inner boundary value should be set by b or x0 + if (isInnerBoundaryFlagSet(INVERT_RHS)) { + val = b[x][z]; + } else if (isInnerBoundaryFlagSet(INVERT_SET)) { val = x0[x][z]; - VecSetValues(xs, 1, &i, &val, INSERT_VALUES); - - ASSERT3(i == getIndex(x, z)); - i++; // Increment row in Petsc matrix } + + // Set components of the RHS (the PETSc vector bs) + // 1 element is being set in row i to val + // INSERT_VALUES replaces existing entries with new values + VecSetValues(bs, 1, &i, &val, INSERT_VALUES); + + // Set components of the and trial solution (the PETSc vector xs) + // 1 element is being set in row i to val + // INSERT_VALUES replaces existing entries with new values + val = x0[x][z]; + VecSetValues(xs, 1, &i, &val, INSERT_VALUES); + + ASSERT3(i == getIndex(x, z)); + i++; // Increment row in Petsc matrix } } + } // Set the values for the main domain for (int x = localmesh->xstart; x <= localmesh->xend; x++) { @@ -744,7 +744,7 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0, VecAssemblyEnd(xs); if (not forward) { - // Configure Linear Solver + // Configure Linear Solver #if PETSC_VERSION_GE(3, 5, 0) KSPSetOperators(ksp, MatA, MatA); #else @@ -811,13 +811,12 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0, lib.setOptionsFromInputFile(ksp); } timer.reset(); - - // Call the actual solver - { - Timer timer("petscsolve"); - KSPSolve(ksp, bs, xs); // Call the solver to solve the system - } + // Call the actual solver + { + Timer timer("petscsolve"); + KSPSolve(ksp, bs, xs); // Call the solver to solve the system + } KSPConvergedReason reason; KSPGetConvergedReason(ksp, &reason); @@ -833,23 +832,23 @@ FieldPerp LaplacePetsc::solve(const FieldPerp& b, const FieldPerp& x0, timer.reset(); PetscErrorCode err = MatMult(MatA, bs, xs); if (err != PETSC_SUCCESS) { - throw BoutException("MatMult failed with {:d}", static_cast(err)); + throw BoutException("MatMult failed with {:d}", static_cast(err)); } } // Add data to FieldPerp Object - i = Istart; - // Set the inner boundary values - if (localmesh->firstX()) { - for (int x = 0; x < localmesh->xstart; x++) { - for (int z = 0; z < localmesh->LocalNz; z++) { - PetscScalar val = 0; - VecGetValues(xs, 1, &i, &val); - sol[x][z] = val; - i++; // Increment row in Petsc matrix + i = Istart; + // Set the inner boundary values + if (localmesh->firstX()) { + for (int x = 0; x < localmesh->xstart; x++) { + for (int z = 0; z < localmesh->LocalNz; z++) { + PetscScalar val = 0; + VecGetValues(xs, 1, &i, &val); + sol[x][z] = val; + i++; // Increment row in Petsc matrix + } } } - } // Set the main domain values for (int x = localmesh->xstart; x <= localmesh->xend; x++) { diff --git a/src/mesh/boundary_standard.cxx b/src/mesh/boundary_standard.cxx index dd7d353a48..fc313689be 100644 --- a/src/mesh/boundary_standard.cxx +++ b/src/mesh/boundary_standard.cxx @@ -1728,7 +1728,7 @@ void BoundaryNeumann_NonOrthogonal::apply(Field3D& f) { void BoundaryNeumann::apply(Field2D & f) { BoundaryNeumann::apply(f, 0.); } - void BoundaryNeumann::apply([[maybe_unused]] Field2D& f, BoutReal t) { + void BoundaryNeumann::apply([[maybe_unused]] Field2D & f, BoutReal t) { // Set (at 2nd order / 3rd order) the value at the mid-point between // the guard cell and the grid cell to be val // N.B. First guard cells (closest to the grid) is 2nd order, while diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index d650c9e9ec..d013634644 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -950,9 +950,9 @@ int Coordinates::geometry(bool recalculate_staggered, bool force_interpolate_from_centre) { TRACE("Coordinates::geometry"); { - std::vector fields{dx, dy, dz, g11, g22, g33, g12, g13, g23, g_11, g_22, g_33, g_12, g_13, - g_23, J}; - for (auto& f: fields) { + std::vector fields{dx, dy, dz, g11, g22, g33, g12, g13, + g23, g_11, g_22, g_33, g_12, g_13, g_23, J}; + for (auto& f : fields) { f.allowParallelSlices(false); } } @@ -1608,7 +1608,8 @@ Field3D Coordinates::Div_par(const Field3D& f, CELL_LOC outloc, f_B.yup(i) = f.yup(i) / coords->J.yup(i) * sqrt(coords->g_22.yup(i)); f_B.ydown(i) = f.ydown(i) / coords->J.ydown(i) * sqrt(coords->g_22.ydown(i)); } - return setName(coords->J / sqrt(coords->g_22) * Grad_par(f_B, outloc, method), "Div_par({:s})", f.name); + return setName(coords->J / sqrt(coords->g_22) * Grad_par(f_B, outloc, method), + "Div_par({:s})", f.name); } ///////////////////////////////////////////////////////// diff --git a/src/mesh/fv_ops.cxx b/src/mesh/fv_ops.cxx index bccae9e362..acfa6aa3ed 100644 --- a/src/mesh/fv_ops.cxx +++ b/src/mesh/fv_ops.cxx @@ -170,7 +170,7 @@ const Field3D Div_par_K_Grad_par(const Field3D& Kin, const Field3D& fin, if (Kin.isFci()) { return ::Div_par_K_Grad_par(Kin, fin); } - + ASSERT2(Kin.getLocation() == fin.getLocation()); Mesh* mesh = Kin.getMesh(); diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index c7c6523244..d9d029e7ca 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -496,8 +496,8 @@ int BoutMesh::load() { } if (meshHasMyg && MYG != meshMyg) { output_warn.write(_("Options changed the number of y-guard cells. Grid has {} but " - "option specified {}! Continuing with {}"), - meshMyg, MYG, MYG); + "option specified {}! Continuing with {}"), + meshMyg, MYG, MYG); } ASSERT0(MYG >= 0); diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index f850704b22..03a062df42 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -356,8 +356,9 @@ template std::vector XZHermiteSplineBase::getWeightsForYApproximation(int i, int j, int k, int yoffset) { - if (localmesh->getNXPE() > 1){ - throw BoutException("It is likely that the function calling this is not handling the result correctly."); + if (localmesh->getNXPE() > 1) { + throw BoutException("It is likely that the function calling this is not handling the " + "result correctly."); } const int nz = localmesh->LocalNz; const int k_mod = k_corner(i, j, k); @@ -503,7 +504,7 @@ template class XZHermiteSplineBase; template class XZHermiteSplineBase; Field3D XZMonotonicHermiteSplineLegacy::interpolate(const Field3D& f, - const std::string& region) const { + const std::string& region) const { ASSERT1(f.getMesh() == localmesh); Field3D f_interp(f.getMesh()); f_interp.allocate(); diff --git a/src/mesh/interpolation/lagrange_4pt_xz.cxx b/src/mesh/interpolation/lagrange_4pt_xz.cxx index 16368299a0..1a1e484c07 100644 --- a/src/mesh/interpolation/lagrange_4pt_xz.cxx +++ b/src/mesh/interpolation/lagrange_4pt_xz.cxx @@ -133,7 +133,6 @@ Field3D XZLagrange4pt::interpolate(const Field3D& f, const std::string& region) // Then in X f_interp(x, y_next, z) = lagrange_4pt(xvals, t_x(x, y, z)); ASSERT2(std::isfinite(f_interp(x, y_next, z))); - } const auto region2 = y_offset != 0 ? fmt::format("RGN_YPAR_{:+d}", y_offset) : region; f_interp.setRegion(region2); diff --git a/src/mesh/interpolation_xz.cxx b/src/mesh/interpolation_xz.cxx index bf22ba995d..5ee20c1a06 100644 --- a/src/mesh/interpolation_xz.cxx +++ b/src/mesh/interpolation_xz.cxx @@ -91,8 +91,8 @@ namespace { RegisterXZInterpolation registerinterphermitespline{"hermitespline"}; RegisterXZInterpolation registerinterpmonotonichermitespline{ "monotonichermitespline"}; -RegisterXZInterpolation registerinterpmonotonichermitesplinelegacy{ - "monotonichermitesplinelegacy"}; +RegisterXZInterpolation + registerinterpmonotonichermitesplinelegacy{"monotonichermitesplinelegacy"}; RegisterXZInterpolation registerinterplagrange4pt{"lagrange4pt"}; RegisterXZInterpolation registerinterpbilinear{"bilinear"}; } // namespace diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index d8b0e95296..95afe4f3e7 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -54,7 +54,7 @@ std::string parallel_slice_field_name(std::string field, int offset) { // We only have a suffix for parallel slices beyond the first // This is for backwards compatibility const std::string slice_suffix = - (std::abs(offset) > 1) ? "_" + std::to_string(std::abs(offset)) : ""; + (std::abs(offset) > 1) ? "_" + std::to_string(std::abs(offset)) : ""; return direction + "_" + field + slice_suffix; }; @@ -90,7 +90,7 @@ bool load_parallel_metric_component(std::string name, Field3D& component, int of } tmp = lmin; } - if (!component.hasParallelSlices()){ + if (!component.hasParallelSlices()) { component.splitParallelSlices(); component.allowCalcParallelSlices = false; } @@ -98,14 +98,13 @@ bool load_parallel_metric_component(std::string name, Field3D& component, int of pcom.allocate(); pcom.setRegion(fmt::format("RGN_YPAR_{:+d}", offset)); pcom.name = name; - BOUT_FOR(i, component.getRegion("RGN_NOBNDRY")) { - pcom[i.yp(offset)] = tmp[i]; - } + BOUT_FOR(i, component.getRegion("RGN_NOBNDRY")) { pcom[i.yp(offset)] = tmp[i]; } return isValid; } #endif -void load_parallel_metric_components([[maybe_unused]] Coordinates* coords, [[maybe_unused]] int offset){ +void load_parallel_metric_components([[maybe_unused]] Coordinates* coords, + [[maybe_unused]] int offset) { #if BOUT_USE_METRIC_3D #define LOAD_PAR(var, doZero) \ load_parallel_metric_component(#var, coords->var, offset, doZero) @@ -188,15 +187,16 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& map_mesh.get(R, "R", 0.0, false); map_mesh.get(Z, "Z", 0.0, false); - // If we can't read in any of these fields, things will silently not // work, so best throw - if (map_mesh.get(xt_prime, parallel_slice_field_name("xt_prime", offset), 0.0, false) != 0) { + if (map_mesh.get(xt_prime, parallel_slice_field_name("xt_prime", offset), 0.0, false) + != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", parallel_slice_field_name("xt_prime", offset)); } - if (map_mesh.get(zt_prime, parallel_slice_field_name("zt_prime", offset), 0.0, false) != 0) { + if (map_mesh.get(zt_prime, parallel_slice_field_name("zt_prime", offset), 0.0, false) + != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", parallel_slice_field_name("zt_prime", offset)); @@ -211,7 +211,6 @@ FCIMap::FCIMap(Mesh& mesh, const Coordinates::FieldMetric& UNUSED(dy), Options& " Either add it to the grid file, or reduce MYG", parallel_slice_field_name("Z", offset)); } - // Cell corners Field3D xt_prime_corner{emptyFrom(xt_prime)}; @@ -453,13 +452,13 @@ void FCITransform::integrateParallelSlices(Field3D& f) { } void FCITransform::loadParallelMetrics(Coordinates* coords) { - for (int i=1; i<= mesh.ystart; ++i) { + for (int i = 1; i <= mesh.ystart; ++i) { load_parallel_metric_components(coords, -i); load_parallel_metric_components(coords, i); } -void FCITransform::outputVars(Options& output_options) { - // Real-space coordinates of grid points - output_options["R"].force(R, "FCI"); - output_options["Z"].force(Z, "FCI"); -} + void FCITransform::outputVars(Options & output_options) { + // Real-space coordinates of grid points + output_options["R"].force(R, "FCI"); + output_options["Z"].force(Z, "FCI"); + } diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index f7acd276a4..513a1d86c3 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -162,6 +162,7 @@ public: } void loadParallelMetrics(Coordinates* coords) override; + protected: void checkInputGrid() override; diff --git a/src/mesh/parallel/fci_comm.hxx b/src/mesh/parallel/fci_comm.hxx index 27dd111765..3514e4ba17 100644 --- a/src/mesh/parallel/fci_comm.hxx +++ b/src/mesh/parallel/fci_comm.hxx @@ -55,7 +55,7 @@ struct globalToLocal1D { const bool periodic; globalToLocal1D(int mg, int npe, int localwith, bool periodic) : mg(mg), npe(npe), localwith(localwith), local(localwith - 2 * mg), - global(local * npe), globalwith(global + 2 * mg), periodic(periodic) {}; + global(local * npe), globalwith(global + 2 * mg), periodic(periodic){}; ProcLocal convert(int id) const { if (periodic) { while (id < mg) { @@ -103,7 +103,7 @@ public: GlobalField3DAccessInstance(const GlobalField3DAccess* gfa, const std::vector&& data) - : gfa(*gfa), data(std::move(data)) {}; + : gfa(*gfa), data(std::move(data)){}; private: const GlobalField3DAccess& gfa; diff --git a/src/solver/impls/pvode/pvode.cxx b/src/solver/impls/pvode/pvode.cxx index 65d44d6e49..a4af3117ad 100644 --- a/src/solver/impls/pvode/pvode.cxx +++ b/src/solver/impls/pvode/pvode.cxx @@ -390,9 +390,9 @@ BoutReal PvodeSolver::run(BoutReal tout) { for (auto& f : f3d) { debug[f.name] = *f.var; - if (f.var->hasParallelSlices()) { - saveParallel(debug, f.name, *f.var); - } + if (f.var->hasParallelSlices()) { + saveParallel(debug, f.name, *f.var); + } } if (mesh != nullptr) { diff --git a/src/sys/options.cxx b/src/sys/options.cxx index e13f7931ee..ee2326df29 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -343,16 +343,16 @@ Options& Options::assign<>(Tensor val, std::string source) { return *this; } -void saveParallel(Options& opt, const std::string name, const Field3D& tosave){ +void saveParallel(Options& opt, const std::string name, const Field3D& tosave) { ASSERT0(tosave.isAllocated()); opt[name] = tosave; - for (size_t i0=1 ; i0 <= tosave.numberParallelSlices(); ++i0) { - for (int i: {i0, -i0} ) { + for (size_t i0 = 1; i0 <= tosave.numberParallelSlices(); ++i0) { + for (int i : {i0, -i0}) { Field3D tmp; tmp.allocate(); const auto& fpar = tosave.ynext(i); - for (auto j: fpar.getValidRegionWithDefault("RGN_NOBNDRY")){ - tmp[j.yp(-i)] = fpar[j]; + for (auto j : fpar.getValidRegionWithDefault("RGN_NOBNDRY")) { + tmp[j.yp(-i)] = fpar[j]; } opt[fmt::format("{}_y{:+d}", name, i)] = tmp; } diff --git a/tests/integrated/test-fci-boundary/get_par_bndry.cxx b/tests/integrated/test-fci-boundary/get_par_bndry.cxx index 4079b55574..6c5c38eaf6 100644 --- a/tests/integrated/test-fci-boundary/get_par_bndry.cxx +++ b/tests/integrated/test-fci-boundary/get_par_bndry.cxx @@ -14,10 +14,9 @@ int main(int argc, char** argv) { for (int i = 0; i < fields.size(); i++) { fields[i] = Field3D{0.0}; mesh->communicate(fields[i]); - for (auto& bndry_par : - mesh->getBoundariesPar(static_cast(i))) { + for (auto& bndry_par : mesh->getBoundariesPar(static_cast(i))) { output.write("{:s} region\n", toString(static_cast(i))); - for (const auto& pnt: *bndry_par) { + for (const auto& pnt : *bndry_par) { fields[i][pnt.ind()] += 1; output.write("{:s} increment\n", toString(static_cast(i))); }