From 358347602cfd43673f4cc64f598440cd5417baeb Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 16 Dec 2021 12:26:52 +0100 Subject: [PATCH 01/40] 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 02/40] 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 03/40] 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 04/40] 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 05/40] 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 06/40] 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 07/40] 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 08/40] 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 09/40] 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 10/40] 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 11/40] 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 12/40] 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 13/40] 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 14/40] 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 15/40] 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 16/40] 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 17/40] 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 18/40] 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 19/40] 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 20/40] 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 21/40] 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 22/40] 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 23/40] 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 24/40] 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 25/40] 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 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

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 26/40] 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 27/40] 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 28/40] 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 29/40] 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 30/40] 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 31/40] 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 32/40] 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 33/40] 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 34/40] 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 35/40] 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 36/40] 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 37/40] 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 e5157adbbd31921c3597b004b557f22f5cc99f78 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 29 Apr 2024 10:45:27 +0200 Subject: [PATCH 38/40] Fix bad merge --- include/bout/utils.hxx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/include/bout/utils.hxx b/include/bout/utils.hxx index 12f438c7d3..e965c5d433 100644 --- a/include/bout/utils.hxx +++ b/include/bout/utils.hxx @@ -369,14 +369,6 @@ public: 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) { i = val; From 25667e4f8bd6eca17219a3ed3225b6d86c62bd8a Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 30 May 2024 15:18:11 +0200 Subject: [PATCH 39/40] Update docs --- manual/sphinx/user_docs/boundary_options.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/manual/sphinx/user_docs/boundary_options.rst b/manual/sphinx/user_docs/boundary_options.rst index 57c6658891..a3cdf0078b 100644 --- a/manual/sphinx/user_docs/boundary_options.rst +++ b/manual/sphinx/user_docs/boundary_options.rst @@ -147,8 +147,10 @@ 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`` +and ``parallel_neumann_o1``, ``parallel_neumann_o2``, +``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 +170,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 +280,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 +291,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 From 1c31c299ce4f2c8de6bb6a3922bc556b0421e6cc Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 30 May 2024 15:18:19 +0200 Subject: [PATCH 40/40] Update test --- 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.};