From 5c6e3a76ac9f0bd3ce34fef53fa83624ed90ba85 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 6 Nov 2025 15:09:29 +0000 Subject: [PATCH 01/98] Port `FV::Div_par_mod` from Hermes-3 --- include/bout/fv_ops.hxx | 251 +++++++++++++++++++++++++++- tests/MMS/spatial/fci/data/BOUT.inp | 1 + tests/MMS/spatial/fci/fci_mms.cxx | 5 + tests/MMS/spatial/fci/mms.py | 2 + tests/MMS/spatial/fci/runtest | 9 +- 5 files changed, 265 insertions(+), 3 deletions(-) diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index 0ec1fbe3ad..adba5f21d7 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -5,12 +5,16 @@ #ifndef BOUT_FV_OPS_H #define BOUT_FV_OPS_H -#include "bout/build_defines.hxx" +#include + +#include "bout/bout_types.hxx" #include "bout/field3d.hxx" #include "bout/globals.hxx" +#include "bout/mesh.hxx" +#include "bout/output_bout_types.hxx" // NOLINT(unused-includes) +#include "bout/region.hxx" #include "bout/utils.hxx" #include "bout/vector2d.hxx" -#include namespace FV { /*! @@ -524,5 +528,248 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { * X-Z Finite Volume diffusion operator */ Field3D Div_Perp_Lap(const Field3D& a, const Field3D& f, CELL_LOC outloc = CELL_DEFAULT); + +/// Finite volume parallel divergence +/// +/// NOTE: Modified version, applies limiter to velocity and field +/// Performs better (smaller overshoots) than Div_par +/// +/// Preserves the sum of f*J*dx*dy*dz over the domain +/// +/// @param[in] f_in The field being advected. +/// This will be reconstructed at cell faces +/// using the given CellEdges method +/// @param[in] v_in The advection velocity. +/// This will be interpolated to cell boundaries +/// using linear interpolation +/// @param[in] wave_speed_in Local maximum speed of all waves in the system at each +// point in space +/// @param[in] fixflux Fix the flux at the boundary to be the value at the +/// midpoint (for boundary conditions) +/// +/// @param[out] flow_ylow Flow at the lower Y cell boundary +/// Already includes area factor * flux +template +Field3D Div_par_mod(const Field3D& f_in, const Field3D& v_in, + const Field3D& wave_speed_in, Field3D& flow_ylow, + bool fixflux = true) { + + Coordinates* coord = f_in.getCoordinates(); + + if (f_in.isFci()) { + // Use mid-point (cell boundary) averages + if (flow_ylow.isAllocated()) { + flow_ylow = emptyFrom(flow_ylow); + } + + ASSERT1(f_in.hasParallelSlices()); + ASSERT1(v_in.hasParallelSlices()); + + const auto& f_up = f_in.yup(); + const auto& f_down = f_in.ydown(); + + const auto& v_up = v_in.yup(); + const auto& v_down = v_in.ydown(); + + Field3D result{emptyFrom(f_in)}; + BOUT_FOR(i, f_in.getRegion("RGN_NOBNDRY")) { + const auto iyp = i.yp(); + const auto iym = i.ym(); + + result[i] = (0.25 * (f_in[i] + f_up[iyp]) * (v_in[i] + v_up[iyp]) + * (coord->J[i] + coord->J.yup()[iyp]) + / (sqrt(coord->g_22[i]) + sqrt(coord->g_22.yup()[iyp])) + - 0.25 * (f_in[i] + f_down[iym]) * (v_in[i] + v_down[iym]) + * (coord->J[i] + coord->J.ydown()[iym]) + / (sqrt(coord->g_22[i]) + sqrt(coord->g_22.ydown()[iym]))) + / (coord->dy[i] * coord->J[i]); + } + return result; + } + ASSERT1_FIELDS_COMPATIBLE(f_in, v_in); + ASSERT1_FIELDS_COMPATIBLE(f_in, wave_speed_in); + + const Mesh* mesh = f_in.getMesh(); + + CellEdges cellboundary; + + ASSERT2(f_in.getDirectionY() == v_in.getDirectionY()); + ASSERT2(f_in.getDirectionY() == wave_speed_in.getDirectionY()); + const bool are_unaligned = + ((f_in.getDirectionY() == YDirectionType::Standard) + and (v_in.getDirectionY() == YDirectionType::Standard) + and (wave_speed_in.getDirectionY() == YDirectionType::Standard)); + + const Field3D f = are_unaligned ? toFieldAligned(f_in, "RGN_NOX") : f_in; + const Field3D v = are_unaligned ? toFieldAligned(v_in, "RGN_NOX") : v_in; + const Field3D wave_speed = + are_unaligned ? toFieldAligned(wave_speed_in, "RGN_NOX") : wave_speed_in; + + Field3D result{zeroFrom(f)}; + flow_ylow = zeroFrom(f); + + for (int i = mesh->xstart; i <= mesh->xend; i++) { + const bool is_periodic_y = mesh->periodicY(i); + const bool is_first_y = mesh->firstY(i); + const bool is_last_y = mesh->lastY(i); + + // Only need one guard cell, so no need to communicate fluxes Instead + // calculate in guard cells to get fluxes consistent between processors, but + // don't include the boundary cell. Note that this implies special handling + // of boundaries later + const int ys = (!is_first_y || is_periodic_y) ? mesh->ystart - 1 : mesh->ystart; + const int ye = (!is_last_y || is_periodic_y) ? mesh->yend + 1 : mesh->yend; + + for (int j = ys; j <= ye; j++) { + // Pre-calculate factors which multiply fluxes +#if not(BOUT_USE_METRIC_3D) + // For right cell boundaries + const BoutReal common_factor_r = + (coord->J(i, j) + coord->J(i, j + 1)) + / (sqrt(coord->g_22(i, j)) + sqrt(coord->g_22(i, j + 1))); + + const BoutReal flux_factor_rc = + common_factor_r / (coord->dy(i, j) * coord->J(i, j)); + const BoutReal flux_factor_rp = + common_factor_r / (coord->dy(i, j + 1) * coord->J(i, j + 1)); + + const BoutReal area_rp = + common_factor_r * coord->dx(i, j + 1) * coord->dz(i, j + 1); + + // For left cell boundaries + const BoutReal common_factor_l = + (coord->J(i, j) + coord->J(i, j - 1)) + / (sqrt(coord->g_22(i, j)) + sqrt(coord->g_22(i, j - 1))); + + const BoutReal flux_factor_lc = + common_factor_l / (coord->dy(i, j) * coord->J(i, j)); + const BoutReal flux_factor_lm = + common_factor_l / (coord->dy(i, j - 1) * coord->J(i, j - 1)); + + const BoutReal area_lc = common_factor_l * coord->dx(i, j) * coord->dz(i, j); +#endif + for (int k = 0; k < mesh->LocalNz; k++) { +#if BOUT_USE_METRIC_3D + // For right cell boundaries + const BoutReal common_factor_r = + (coord->J(i, j, k) + coord->J(i, j + 1, k)) + / (sqrt(coord->g_22(i, j, k)) + sqrt(coord->g_22(i, j + 1, k))); + + const BoutReal flux_factor_rc = + common_factor_r / (coord->dy(i, j, k) * coord->J(i, j, k)); + const BoutReal flux_factor_rp = + common_factor_r / (coord->dy(i, j + 1, k) * coord->J(i, j + 1, k)); + + const BoutReal area_rp = + common_factor_r * coord->dx(i, j + 1, k) * coord->dz(i, j + 1, k); + + // For left cell boundaries + const BoutReal common_factor_l = + (coord->J(i, j, k) + coord->J(i, j - 1, k)) + / (sqrt(coord->g_22(i, j, k)) + sqrt(coord->g_22(i, j - 1, k))); + + const BoutReal flux_factor_lc = + common_factor_l / (coord->dy(i, j, k) * coord->J(i, j, k)); + const BoutReal flux_factor_lm = + common_factor_l / (coord->dy(i, j - 1, k) * coord->J(i, j - 1, k)); + + const BoutReal area_lc = + common_factor_l * coord->dx(i, j, k) * coord->dz(i, j, k); +#endif + + //////////////////////////////////////////// + // Reconstruct f at the cell faces + // This calculates s.R and s.L for the Right and Left + // face values on this cell + + // Reconstruct f at the cell faces + // TODO(peter): We can remove this #ifdef guard after switching to C++20 +#if __cpp_designated_initializers >= 201707L + Stencil1D s{.c = f(i, j, k), .m = f(i, j - 1, k), .p = f(i, j + 1, k)}; +#else + Stencil1D s{f(i, j, k), f(i, j - 1, k), f(i, j + 1, k), BoutNaN, + BoutNaN, BoutNaN, BoutNaN}; +#endif + cellboundary(s); // Calculate s.R and s.L + + //////////////////////////////////////////// + // Reconstruct v at the cell faces + // TODO(peter): We can remove this #ifdef guard after switching to C++20 +#if __cpp_designated_initializers >= 201707L + Stencil1D sv{.c = v(i, j, k), .m = v(i, j - 1, k), .p = v(i, j + 1, k)}; +#else + Stencil1D sv{v(i, j, k), v(i, j - 1, k), v(i, j + 1, k), BoutNaN, + BoutNaN, BoutNaN, BoutNaN}; +#endif + cellboundary(sv); // Calculate sv.R and sv.L + + //////////////////////////////////////////// + // Right boundary + + BoutReal flux = BoutNaN; + + if (is_last_y && (j == mesh->yend) && !is_periodic_y) { + // Last point in domain + + // Calculate velocity at right boundary (y+1/2) + const BoutReal vpar = 0.5 * (v(i, j, k) + v(i, j + 1, k)); + + const BoutReal bndryval = 0.5 * (s.c + s.p); + if (fixflux) { + // Use mid-point to be consistent with boundary conditions + flux = bndryval * vpar; + } else { + // Add flux due to difference in boundary values + flux = (s.R * vpar) + (wave_speed(i, j, k) * (s.R - bndryval)); + } + + } else { + // Maximum wave speed in the two cells + const BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j + 1, k), + fabs(v(i, j, k)), fabs(v(i, j + 1, k))); + + flux = s.R * 0.5 * (sv.R + amax); + } + + result(i, j, k) += flux * flux_factor_rc; + result(i, j + 1, k) -= flux * flux_factor_rp; + + flow_ylow(i, j + 1, k) += flux * area_rp; + + //////////////////////////////////////////// + // Calculate at left boundary + + if (is_first_y && (j == mesh->ystart) && !is_periodic_y) { + // First point in domain + const BoutReal bndryval = 0.5 * (s.c + s.m); + const BoutReal vpar = 0.5 * (v(i, j, k) + v(i, j - 1, k)); + if (fixflux) { + // Use mid-point to be consistent with boundary conditions + flux = bndryval * vpar; + } else { + // Add flux due to difference in boundary values + flux = (s.L * vpar) - (wave_speed(i, j, k) * (s.L - bndryval)); + } + } else { + + // Maximum wave speed in the two cells + const BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j - 1, k), + fabs(v(i, j, k)), fabs(v(i, j - 1, k))); + + flux = s.L * 0.5 * (sv.L - amax); + } + + result(i, j, k) -= flux * flux_factor_lc; + result(i, j - 1, k) += flux * flux_factor_lm; + + flow_ylow(i, j, k) += flux * area_lc; + } + } + } + if (are_unaligned) { + flow_ylow = fromFieldAligned(flow_ylow, "RGN_NOBNDRY"); + } + return are_unaligned ? fromFieldAligned(result, "RGN_NOBNDRY") : result; +} } // namespace FV #endif // BOUT_FV_OPS_H diff --git a/tests/MMS/spatial/fci/data/BOUT.inp b/tests/MMS/spatial/fci/data/BOUT.inp index 93e2101473..9171178d5d 100644 --- a/tests/MMS/spatial/fci/data/BOUT.inp +++ b/tests/MMS/spatial/fci/data/BOUT.inp @@ -5,6 +5,7 @@ div_par_solution = (0.01*x + 0.045)*(-12.5663706143592*cos(y - 2*z) - 6.28318530 div_par_K_grad_par_solution = (0.01*x + 0.045)*(6.28318530717959*sin(y - z) - 0.628318530717959*sin(y - z)/(0.01*x + 0.045))*(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))/((0.01*x + 0.045)^2 + 1.0) + (6.28318530717959*(0.01*x + 0.045)*(6.28318530717959*(0.01*x + 0.045)*(-4*sin(y - 2*z) - sin(y - z)) + 1.25663706143592*sin(y - 2*z) + 0.628318530717959*sin(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0) + 0.628318530717959*(6.28318530717959*(0.01*x + 0.045)*(2*sin(y - 2*z) + sin(y - z)) - 0.628318530717959*sin(y - 2*z) - 0.628318530717959*sin(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0))*cos(y - z)/sqrt((0.01*x + 0.045)^2 + 1.0) K = cos(y - z) laplace_par_solution = (0.01*x + 0.045)*(6.28318530717959*(6.28318530717959*(0.01*x + 0.045)*(-4*sin(y - 2*z) - sin(y - z)) + 1.25663706143592*sin(y - 2*z) + 0.628318530717959*sin(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0) + 0.628318530717959*(6.28318530717959*(0.01*x + 0.045)*(2*sin(y - 2*z) + sin(y - z)) - 0.628318530717959*sin(y - 2*z) - 0.628318530717959*sin(y - z))/((0.01*x + 0.045)*sqrt((0.01*x + 0.045)^2 + 1.0)))/sqrt((0.01*x + 0.045)^2 + 1.0) +FV_div_par_mod_solution = (0.01*x + 0.045)*(6.28318530717959*(0.01*x + 0.045)*((sin(y - 2*z) + sin(y - z))*sin(y - z)/(0.01*x + 0.045) + (-2*cos(y - 2*z) - cos(y - z))*cos(y - z)/(0.01*x + 0.045)) - 0.628318530717959*(sin(y - 2*z) + sin(y - z))*sin(y - z)/(0.01*x + 0.045) + 0.628318530717959*(cos(y - 2*z) + cos(y - z))*cos(y - z)/(0.01*x + 0.045))/sqrt((0.01*x + 0.045)^2 + 1.0) [mesh] symmetricglobalx = true diff --git a/tests/MMS/spatial/fci/fci_mms.cxx b/tests/MMS/spatial/fci/fci_mms.cxx index 7967452f3d..ca8ee8cd9f 100644 --- a/tests/MMS/spatial/fci/fci_mms.cxx +++ b/tests/MMS/spatial/fci/fci_mms.cxx @@ -4,6 +4,7 @@ #include "bout/field.hxx" #include "bout/field3d.hxx" #include "bout/field_factory.hxx" +#include "bout/fv_ops.hxx" #include "bout/globals.hxx" #include "bout/options.hxx" #include "bout/options_io.hxx" @@ -64,6 +65,10 @@ int main(int argc, char** argv) { fci_op_test("div_par_K_grad_par", dump, input, Div_par_K_Grad_par(K, input)); fci_op_test("laplace_par", dump, input, Laplace_par(input)); + // Finite volume methods + Field3D flow_ylow; + fci_op_test("FV_div_par_mod", dump, input, FV::Div_par_mod(input, K, K, flow_ylow)); + bout::writeDefaultOutputFile(dump); BoutFinalise(); diff --git a/tests/MMS/spatial/fci/mms.py b/tests/MMS/spatial/fci/mms.py index b28e337ac0..2fb2bd6aa6 100755 --- a/tests/MMS/spatial/fci/mms.py +++ b/tests/MMS/spatial/fci/mms.py @@ -16,6 +16,7 @@ f = sin(y - z) + sin(y - 2 * z) K = cos(z - y) + Lx = 0.1 Ly = 10.0 Lz = 1.0 @@ -61,6 +62,7 @@ def FCI_Laplace_par(f: Expr) -> Expr: ("div_par_solution", FCI_div_par(f)), ("div_par_K_grad_par_solution", FCI_div_par_K_grad_par(f, K)), ("laplace_par_solution", FCI_Laplace_par(f)), + ("FV_div_par_mod_solution", FCI_div_par(f * K)), ): expr_str = exprToStr(expr) print(f"{name} = {expr_str}") diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index 34340e53f4..7e960cb024 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -23,7 +23,14 @@ from scipy.interpolate import RectBivariateSpline as RBS DIRECTORY = "data" NPROC = 2 MTHREAD = 2 -OPERATORS = ("grad_par", "grad2_par2", "div_par", "div_par_K_grad_par", "laplace_par") +OPERATORS = ( + "grad_par", + "grad2_par2", + "div_par", + "div_par_K_grad_par", + "laplace_par", + "FV_div_par_mod", +) # Note that we need at least _2_ interior points for hermite spline # interpolation due to an awkwardness with the boundaries NX = 4 From 3059d128d44aacca266587a460a5b010afd3ba00 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 6 Nov 2025 15:43:38 +0000 Subject: [PATCH 02/98] Port `FV::Div_par_fvv` from Hermes-3 --- include/bout/fv_ops.hxx | 208 ++++++++++++++++++++++++++++ tests/MMS/spatial/fci/data/BOUT.inp | 1 + tests/MMS/spatial/fci/fci_mms.cxx | 1 + tests/MMS/spatial/fci/mms.py | 1 + tests/MMS/spatial/fci/runtest | 1 + 5 files changed, 212 insertions(+) diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index adba5f21d7..bd0fa812d8 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -771,5 +771,213 @@ Field3D Div_par_mod(const Field3D& f_in, const Field3D& v_in, } return are_unaligned ? fromFieldAligned(result, "RGN_NOBNDRY") : result; } + +/// This operator calculates Div_par(f v v) +/// It is used primarily (only?) in the parallel momentum equation. +/// +/// This operator is used rather than Div(f fv) so that the values of +/// f and v are consistent with other advection equations: The product +/// fv is not interpolated to cell boundaries. +template +Field3D Div_par_fvv(const Field3D& f_in, const Field3D& v_in, + const Field3D& wave_speed_in, bool fixflux = true) { + ASSERT1_FIELDS_COMPATIBLE(f_in, v_in); + const Mesh* mesh = f_in.getMesh(); + const Coordinates* coord = f_in.getCoordinates(); + CellEdges cellboundary; + + if (f_in.isFci()) { + // FCI version, using yup/down fields + ASSERT1(f_in.hasParallelSlices()); + ASSERT1(v_in.hasParallelSlices()); + + const auto& B = coord->Bxy; + const auto& B_up = coord->Bxy.yup(); + const auto& B_down = coord->Bxy.ydown(); + + const auto& f_up = f_in.yup(); + const auto& f_down = f_in.ydown(); + + const auto& v_up = v_in.yup(); + const auto& v_down = v_in.ydown(); + + const auto& g_22 = coord->g_22; + const auto& dy = coord->dy; + + Field3D result{emptyFrom(f_in)}; + BOUT_FOR(i, f_in.getRegion("RGN_NOBNDRY")) { + const auto iyp = i.yp(); + const auto iym = i.ym(); + + // Maximum local wave speed + const BoutReal amax = + BOUTMAX(wave_speed_in[i], fabs(v_in[i]), fabs(v_up[iyp]), fabs(v_down[iym])); + + const BoutReal term = (f_up[iyp] * v_up[iyp] * v_up[iyp] / B_up[iyp]) + - (f_down[iym] * v_down[iym] * v_down[iym] / B_down[iym]); + + // Penalty terms. This implementation is very dissipative. + BoutReal penalty = + (amax * (f_in[i] * v_in[i] - f_up[iyp] * v_up[iyp]) / (B[i] + B_up[iyp])) + + (amax * (f_in[i] * v_in[i] - f_down[iym] * v_down[iym]) + / (B[i] + B_down[iym])); + + if (fabs(penalty) > fabs(term) and penalty * v_in[i] > 0) { + if (term * penalty > 0) { + penalty = term; + } else { + penalty = -term; + } + } + + result[i] = B[i] * (term + penalty) / (2 * dy[i] * sqrt(g_22[i])); + +#if CHECK > 0 + if (!std::isfinite(result[i])) { + throw BoutException("Non-finite value in Div_par_fvv at {}\n" + "fup {} vup {} fdown {} vdown {} amax {}\n", + "B {} Bup {} Bdown {} dy {} sqrt(g_22} {}", i, f_up[i], + v_up[i], f_down[i], v_down[i], amax, B[i], B_up[i], B_down[i], + dy[i], sqrt(g_22[i])); + } +#endif + } + return result; + } + + ASSERT1(areFieldsCompatible(f_in, wave_speed_in)); + + /// Ensure that f, v and wave_speed are field aligned + Field3D f = toFieldAligned(f_in, "RGN_NOX"); + Field3D v = toFieldAligned(v_in, "RGN_NOX"); + Field3D wave_speed = toFieldAligned(wave_speed_in, "RGN_NOX"); + + Field3D result{zeroFrom(f)}; + + for (int i = mesh->xstart; i <= mesh->xend; i++) { + const bool is_periodic_y = mesh->periodicY(i); + const bool is_first_y = mesh->firstY(i); + const bool is_last_y = mesh->lastY(i); + + // Only need one guard cell, so no need to communicate fluxes Instead + // calculate in guard cells to get fluxes consistent between processors, but + // don't include the boundary cell. Note that this implies special handling + // of boundaries later + const int ys = (!is_first_y || is_periodic_y) ? mesh->ystart - 1 : mesh->ystart; + const int ye = (!is_last_y || is_periodic_y) ? mesh->yend + 1 : mesh->yend; + + for (int j = ys; j <= ye; j++) { + // Pre-calculate factors which multiply fluxes + + for (int k = 0; k < mesh->LocalNz; k++) { + // For right cell boundaries + const BoutReal common_factor_r = + (coord->J(i, j, k) + coord->J(i, j + 1, k)) + / (sqrt(coord->g_22(i, j, k)) + sqrt(coord->g_22(i, j + 1, k))); + + const BoutReal flux_factor_rc = + common_factor_r / (coord->dy(i, j, k) * coord->J(i, j, k)); + const BoutReal flux_factor_rp = + common_factor_r / (coord->dy(i, j + 1, k) * coord->J(i, j + 1, k)); + + // For left cell boundaries + const BoutReal common_factor_l = + (coord->J(i, j, k) + coord->J(i, j - 1, k)) + / (sqrt(coord->g_22(i, j, k)) + sqrt(coord->g_22(i, j - 1, k))); + + const BoutReal flux_factor_lc = + common_factor_l / (coord->dy(i, j, k) * coord->J(i, j, k)); + const BoutReal flux_factor_lm = + common_factor_l / (coord->dy(i, j - 1, k) * coord->J(i, j - 1, k)); + + //////////////////////////////////////////// + // Reconstruct f at the cell faces + // This calculates s.R and s.L for the Right and Left + // face values on this cell + + // Reconstruct f at the cell faces +#if __cpp_designated_initializers >= 201707L + Stencil1D s{.c = f(i, j, k), .m = f(i, j - 1, k), .p = f(i, j + 1, k)}; +#else + Stencil1D s{f(i, j, k), f(i, j - 1, k), f(i, j + 1, k), BoutNaN, + BoutNaN, BoutNaN, BoutNaN}; +#endif + cellboundary(s); // Calculate s.R and s.L + + //////////////////////////////////////////// + // Reconstruct v at the cell faces + // TODO(peter): We can remove this #ifdef guard after switching to C++20 +#if __cpp_designated_initializers >= 201707L + Stencil1D sv{.c = v(i, j, k), .m = v(i, j - 1, k), .p = v(i, j + 1, k)}; +#else + Stencil1D sv{v(i, j, k), v(i, j - 1, k), v(i, j + 1, k), BoutNaN, + BoutNaN, BoutNaN, BoutNaN}; +#endif + cellboundary(sv); + + //////////////////////////////////////////// + // Right boundary + + // Calculate velocity at right boundary (y+1/2) + const BoutReal v_mid_r = 0.5 * (sv.c + sv.p); + // And mid-point density at right boundary + const BoutReal n_mid_r = 0.5 * (s.c + s.p); + BoutReal flux = NAN; + + if (mesh->lastY(i) && (j == mesh->yend) && !mesh->periodicY(i)) { + // Last point in domain + + if (fixflux) { + // Use mid-point to be consistent with boundary conditions + flux = n_mid_r * v_mid_r * v_mid_r; + } else { + // Add flux due to difference in boundary values + flux = (s.R * sv.R * sv.R) // Use right cell edge values + + (BOUTMAX(wave_speed(i, j, k), fabs(sv.c), fabs(sv.p)) * n_mid_r + * (sv.R - v_mid_r)); // Damp differences in velocity, not flux + } + } else { + // Maximum wave speed in the two cells + const BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j + 1, k), + fabs(sv.c), fabs(sv.p)); + + flux = s.R * 0.5 * (sv.R + amax) * sv.R; + } + + result(i, j, k) += flux * flux_factor_rc; + result(i, j + 1, k) -= flux * flux_factor_rp; + + //////////////////////////////////////////// + // Calculate at left boundary + + const BoutReal v_mid_l = 0.5 * (sv.c + sv.m); + const BoutReal n_mid_l = 0.5 * (s.c + s.m); + + if (mesh->firstY(i) && (j == mesh->ystart) && !mesh->periodicY(i)) { + // First point in domain + if (fixflux) { + // Use mid-point to be consistent with boundary conditions + flux = n_mid_l * v_mid_l * v_mid_l; + } else { + // Add flux due to difference in boundary values + flux = (s.L * sv.L * sv.L) + - (BOUTMAX(wave_speed(i, j, k), fabs(sv.c), fabs(sv.m)) * n_mid_l + * (sv.L - v_mid_l)); + } + } else { + // Maximum wave speed in the two cells + const BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j - 1, k), + fabs(sv.c), fabs(sv.m)); + + flux = s.L * 0.5 * (sv.L - amax) * sv.L; + } + + result(i, j, k) -= flux * flux_factor_lc; + result(i, j - 1, k) += flux * flux_factor_lm; + } + } + } + return fromFieldAligned(result, "RGN_NOBNDRY"); +} } // namespace FV #endif // BOUT_FV_OPS_H diff --git a/tests/MMS/spatial/fci/data/BOUT.inp b/tests/MMS/spatial/fci/data/BOUT.inp index 9171178d5d..99edee6ecc 100644 --- a/tests/MMS/spatial/fci/data/BOUT.inp +++ b/tests/MMS/spatial/fci/data/BOUT.inp @@ -6,6 +6,7 @@ div_par_K_grad_par_solution = (0.01*x + 0.045)*(6.28318530717959*sin(y - z) - 0. K = cos(y - z) laplace_par_solution = (0.01*x + 0.045)*(6.28318530717959*(6.28318530717959*(0.01*x + 0.045)*(-4*sin(y - 2*z) - sin(y - z)) + 1.25663706143592*sin(y - 2*z) + 0.628318530717959*sin(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0) + 0.628318530717959*(6.28318530717959*(0.01*x + 0.045)*(2*sin(y - 2*z) + sin(y - z)) - 0.628318530717959*sin(y - 2*z) - 0.628318530717959*sin(y - z))/((0.01*x + 0.045)*sqrt((0.01*x + 0.045)^2 + 1.0)))/sqrt((0.01*x + 0.045)^2 + 1.0) FV_div_par_mod_solution = (0.01*x + 0.045)*(6.28318530717959*(0.01*x + 0.045)*((sin(y - 2*z) + sin(y - z))*sin(y - z)/(0.01*x + 0.045) + (-2*cos(y - 2*z) - cos(y - z))*cos(y - z)/(0.01*x + 0.045)) - 0.628318530717959*(sin(y - 2*z) + sin(y - z))*sin(y - z)/(0.01*x + 0.045) + 0.628318530717959*(cos(y - 2*z) + cos(y - z))*cos(y - z)/(0.01*x + 0.045))/sqrt((0.01*x + 0.045)^2 + 1.0) +FV_div_par_fvv_solution = (0.01*x + 0.045)*(6.28318530717959*(0.01*x + 0.045)*(2*(sin(y - 2*z) + sin(y - z))*sin(y - z)*cos(y - z)/(0.01*x + 0.045) + (-2*cos(y - 2*z) - cos(y - z))*cos(y - z)^2/(0.01*x + 0.045)) - 1.25663706143592*(sin(y - 2*z) + sin(y - z))*sin(y - z)*cos(y - z)/(0.01*x + 0.045) + 0.628318530717959*(cos(y - 2*z) + cos(y - z))*cos(y - z)^2/(0.01*x + 0.045))/sqrt((0.01*x + 0.045)^2 + 1.0) [mesh] symmetricglobalx = true diff --git a/tests/MMS/spatial/fci/fci_mms.cxx b/tests/MMS/spatial/fci/fci_mms.cxx index ca8ee8cd9f..13744c4965 100644 --- a/tests/MMS/spatial/fci/fci_mms.cxx +++ b/tests/MMS/spatial/fci/fci_mms.cxx @@ -68,6 +68,7 @@ int main(int argc, char** argv) { // Finite volume methods Field3D flow_ylow; fci_op_test("FV_div_par_mod", dump, input, FV::Div_par_mod(input, K, K, flow_ylow)); + fci_op_test("FV_div_par_fvv", dump, input, FV::Div_par_fvv(input, K, K)); bout::writeDefaultOutputFile(dump); diff --git a/tests/MMS/spatial/fci/mms.py b/tests/MMS/spatial/fci/mms.py index 2fb2bd6aa6..62089c7e21 100755 --- a/tests/MMS/spatial/fci/mms.py +++ b/tests/MMS/spatial/fci/mms.py @@ -63,6 +63,7 @@ def FCI_Laplace_par(f: Expr) -> Expr: ("div_par_K_grad_par_solution", FCI_div_par_K_grad_par(f, K)), ("laplace_par_solution", FCI_Laplace_par(f)), ("FV_div_par_mod_solution", FCI_div_par(f * K)), + ("FV_div_par_fvv_solution", FCI_div_par(f * K * K)), ): expr_str = exprToStr(expr) print(f"{name} = {expr_str}") diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index 7e960cb024..c0f0a45132 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -30,6 +30,7 @@ OPERATORS = ( "div_par_K_grad_par", "laplace_par", "FV_div_par_mod", + "FV_div_par_fvv", ) # Note that we need at least _2_ interior points for hermite spline # interpolation due to an awkwardness with the boundaries From 60f194643a0819e0c7a9ac0dff4013968dac80e2 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 6 Nov 2025 16:12:41 +0000 Subject: [PATCH 03/98] Port `Div_par_K_Grad_par_mod` from Hermes-3 --- include/bout/difops.hxx | 4 ++ src/mesh/difops.cxx | 102 ++++++++++++++++++++++++++++ tests/MMS/spatial/fci/data/BOUT.inp | 1 + tests/MMS/spatial/fci/fci_mms.cxx | 6 +- tests/MMS/spatial/fci/mms.py | 1 + tests/MMS/spatial/fci/runtest | 1 + 6 files changed, 114 insertions(+), 1 deletion(-) diff --git a/include/bout/difops.hxx b/include/bout/difops.hxx index 18220b63ad..2cd99f8d33 100644 --- a/include/bout/difops.hxx +++ b/include/bout/difops.hxx @@ -195,6 +195,10 @@ Field3D Div_par_K_Grad_par(const Field3D& kY, const Field2D& f, Field3D Div_par_K_Grad_par(const Field3D& kY, const Field3D& f, CELL_LOC outloc = CELL_DEFAULT); +/// Version with energy flow diagnostic +Field3D Div_par_K_Grad_par_mod(const Field3D& k, const Field3D& f, Field3D& flow_ylow, + bool bndry_flux = true); + /*! * Perpendicular Laplacian operator * diff --git a/src/mesh/difops.cxx b/src/mesh/difops.cxx index 09433b0685..09d2441951 100644 --- a/src/mesh/difops.cxx +++ b/src/mesh/difops.cxx @@ -366,6 +366,108 @@ Field3D Div_par_K_Grad_par(const Field3D& kY, const Field3D& f, CELL_LOC outloc) + Div_par(kY, outloc) * Grad_par(f, outloc); } +Field3D Div_par_K_Grad_par_mod(const Field3D& Kin, const Field3D& fin, + Field3D& flow_ylow, bool bndry_flux) { + TRACE("FV::Div_par_K_Grad_par_mod"); + + ASSERT2(Kin.getLocation() == fin.getLocation()); + + Mesh* mesh = Kin.getMesh(); + Coordinates* coord = fin.getCoordinates(); + + if (Kin.hasParallelSlices() && fin.hasParallelSlices()) { + // Using parallel slices. + // Note: Y slices may use different coordinate systems + // -> Only B, dy and g_22 can be used in yup/ydown + // Others (e.g J) may not be averaged between y planes. + + const auto& K_up = Kin.yup(); + const auto& K_down = Kin.ydown(); + + const auto& f_up = fin.yup(); + const auto& f_down = fin.ydown(); + + Field3D result{zeroFrom(fin)}; + flow_ylow = zeroFrom(fin); + + BOUT_FOR(i, result.getRegion("RGN_NOBNDRY")) { + const auto iyp = i.yp(); + const auto iym = i.ym(); + + // Upper cell edge + const BoutReal c_up = 0.5 * (Kin[i] + K_up[iyp]); // K at the upper boundary + const BoutReal J_up = 0.5 * (coord->J[i] + coord->J.yup()[iyp]); // Jacobian at boundary + const BoutReal g_22_up = 0.5 * (coord->g_22[i] + coord->g_22.yup()[iyp]); + const BoutReal gradient_up = 2. * (f_up[iyp] - fin[i]) / (coord->dy[i] + coord->dy.yup()[iyp]); + + const BoutReal flux_up = c_up * J_up * gradient_up / g_22_up; + + // Lower cell edge + const BoutReal c_down = 0.5 * (Kin[i] + K_down[iym]); // K at the lower boundary + const BoutReal J_down = 0.5 * (coord->J[i] + coord->J.ydown()[iym]); // Jacobian at boundary + const BoutReal g_22_down = 0.5 * (coord->g_22[i] + coord->g_22.ydown()[iym]); + const BoutReal gradient_down = 2. * (fin[i] - f_down[iym]) / (coord->dy[i] + coord->dy.ydown()[iym]); + + const BoutReal flux_down = c_down * J_down * gradient_down / g_22_down; + + result[i] = (flux_up - flux_down) / (coord->dy[i] * coord->J[i]); + } + + return result; + } + + // Calculate in field-aligned coordinates + const auto& K = toFieldAligned(Kin, "RGN_NOX"); + const auto& f = toFieldAligned(fin, "RGN_NOX"); + + Field3D result{zeroFrom(f)}; + flow_ylow = zeroFrom(f); + + BOUT_FOR(i, result.getRegion("RGN_NOBNDRY")) { + // Calculate flux at upper surface + + const auto iyp = i.yp(); + const auto iym = i.ym(); + + if (bndry_flux || mesh->periodicY(i.x()) || !mesh->lastY(i.x()) + || (i.y() != mesh->yend)) { + + BoutReal c = 0.5 * (K[i] + K[iyp]); // K at the upper boundary + BoutReal J = 0.5 * (coord->J[i] + coord->J[iyp]); // Jacobian at boundary + BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iyp]); + + BoutReal gradient = 2. * (f[iyp] - f[i]) / (coord->dy[i] + coord->dy[iyp]); + + BoutReal flux = c * J * gradient / g_22; + + result[i] += flux / (coord->dy[i] * coord->J[i]); + } + + // Calculate flux at lower surface + if (bndry_flux || mesh->periodicY(i.x()) || !mesh->firstY(i.x()) + || (i.y() != mesh->ystart)) { + BoutReal c = 0.5 * (K[i] + K[iym]); // K at the lower boundary + BoutReal J = 0.5 * (coord->J[i] + coord->J[iym]); // Jacobian at boundary + + BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iym]); + + BoutReal gradient = 2. * (f[i] - f[iym]) / (coord->dy[i] + coord->dy[iym]); + + BoutReal flux = c * J * gradient / g_22; + + result[i] -= flux / (coord->dy[i] * coord->J[i]); + flow_ylow[i] = -flux * coord->dx[i] * coord->dz[i]; + } + } + + // Shifted to field aligned coordinates, so need to shift back + result = fromFieldAligned(result, "RGN_NOBNDRY"); + flow_ylow = fromFieldAligned(flow_ylow); + + return result; +} + + /******************************************************************************* * Delp2 * perpendicular Laplacian operator diff --git a/tests/MMS/spatial/fci/data/BOUT.inp b/tests/MMS/spatial/fci/data/BOUT.inp index 99edee6ecc..76ac3035c9 100644 --- a/tests/MMS/spatial/fci/data/BOUT.inp +++ b/tests/MMS/spatial/fci/data/BOUT.inp @@ -7,6 +7,7 @@ K = cos(y - z) laplace_par_solution = (0.01*x + 0.045)*(6.28318530717959*(6.28318530717959*(0.01*x + 0.045)*(-4*sin(y - 2*z) - sin(y - z)) + 1.25663706143592*sin(y - 2*z) + 0.628318530717959*sin(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0) + 0.628318530717959*(6.28318530717959*(0.01*x + 0.045)*(2*sin(y - 2*z) + sin(y - z)) - 0.628318530717959*sin(y - 2*z) - 0.628318530717959*sin(y - z))/((0.01*x + 0.045)*sqrt((0.01*x + 0.045)^2 + 1.0)))/sqrt((0.01*x + 0.045)^2 + 1.0) FV_div_par_mod_solution = (0.01*x + 0.045)*(6.28318530717959*(0.01*x + 0.045)*((sin(y - 2*z) + sin(y - z))*sin(y - z)/(0.01*x + 0.045) + (-2*cos(y - 2*z) - cos(y - z))*cos(y - z)/(0.01*x + 0.045)) - 0.628318530717959*(sin(y - 2*z) + sin(y - z))*sin(y - z)/(0.01*x + 0.045) + 0.628318530717959*(cos(y - 2*z) + cos(y - z))*cos(y - z)/(0.01*x + 0.045))/sqrt((0.01*x + 0.045)^2 + 1.0) FV_div_par_fvv_solution = (0.01*x + 0.045)*(6.28318530717959*(0.01*x + 0.045)*(2*(sin(y - 2*z) + sin(y - z))*sin(y - z)*cos(y - z)/(0.01*x + 0.045) + (-2*cos(y - 2*z) - cos(y - z))*cos(y - z)^2/(0.01*x + 0.045)) - 1.25663706143592*(sin(y - 2*z) + sin(y - z))*sin(y - z)*cos(y - z)/(0.01*x + 0.045) + 0.628318530717959*(cos(y - 2*z) + cos(y - z))*cos(y - z)^2/(0.01*x + 0.045))/sqrt((0.01*x + 0.045)^2 + 1.0) +div_par_K_grad_par_mod_solution = (0.01*x + 0.045)*(6.28318530717959*sin(y - z) - 0.628318530717959*sin(y - z)/(0.01*x + 0.045))*(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))/((0.01*x + 0.045)^2 + 1.0) + (6.28318530717959*(0.01*x + 0.045)*(6.28318530717959*(0.01*x + 0.045)*(-4*sin(y - 2*z) - sin(y - z)) + 1.25663706143592*sin(y - 2*z) + 0.628318530717959*sin(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0) + 0.628318530717959*(6.28318530717959*(0.01*x + 0.045)*(2*sin(y - 2*z) + sin(y - z)) - 0.628318530717959*sin(y - 2*z) - 0.628318530717959*sin(y - z))/sqrt((0.01*x + 0.045)^2 + 1.0))*cos(y - z)/sqrt((0.01*x + 0.045)^2 + 1.0) [mesh] symmetricglobalx = true diff --git a/tests/MMS/spatial/fci/fci_mms.cxx b/tests/MMS/spatial/fci/fci_mms.cxx index 13744c4965..17408aeeab 100644 --- a/tests/MMS/spatial/fci/fci_mms.cxx +++ b/tests/MMS/spatial/fci/fci_mms.cxx @@ -59,14 +59,18 @@ int main(int argc, char** argv) { // Add mesh geometry variables mesh->outputVars(dump); + // Dummy variable for *_mod overloads + Field3D flow_ylow; + fci_op_test("grad_par", dump, input, Grad_par(input)); fci_op_test("grad2_par2", dump, input, Grad2_par2(input)); fci_op_test("div_par", dump, input, Div_par(input)); fci_op_test("div_par_K_grad_par", dump, input, Div_par_K_Grad_par(K, input)); + fci_op_test("div_par_K_grad_par_mod", dump, input, + Div_par_K_Grad_par_mod(K, input, flow_ylow)); fci_op_test("laplace_par", dump, input, Laplace_par(input)); // Finite volume methods - Field3D flow_ylow; fci_op_test("FV_div_par_mod", dump, input, FV::Div_par_mod(input, K, K, flow_ylow)); fci_op_test("FV_div_par_fvv", dump, input, FV::Div_par_fvv(input, K, K)); diff --git a/tests/MMS/spatial/fci/mms.py b/tests/MMS/spatial/fci/mms.py index 62089c7e21..801a8d3f26 100755 --- a/tests/MMS/spatial/fci/mms.py +++ b/tests/MMS/spatial/fci/mms.py @@ -61,6 +61,7 @@ def FCI_Laplace_par(f: Expr) -> Expr: ("grad2_par2_solution", FCI_grad2_par2(f)), ("div_par_solution", FCI_div_par(f)), ("div_par_K_grad_par_solution", FCI_div_par_K_grad_par(f, K)), + ("div_par_K_grad_par_mod_solution", FCI_div_par_K_grad_par(f, K)), ("laplace_par_solution", FCI_Laplace_par(f)), ("FV_div_par_mod_solution", FCI_div_par(f * K)), ("FV_div_par_fvv_solution", FCI_div_par(f * K * K)), diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index c0f0a45132..73babc9691 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -28,6 +28,7 @@ OPERATORS = ( "grad2_par2", "div_par", "div_par_K_grad_par", + "div_par_K_grad_par_mod", "laplace_par", "FV_div_par_mod", "FV_div_par_fvv", From f53b8f3c5ec7d6af148200e8a2af4ab1ec80aac0 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 6 Nov 2025 16:59:34 +0000 Subject: [PATCH 04/98] Add MMS tests for finite volume operators --- tests/MMS/CMakeLists.txt | 1 + .../MMS/spatial/finite-volume/CMakeLists.txt | 6 + tests/MMS/spatial/finite-volume/data/BOUT.inp | 23 ++ tests/MMS/spatial/finite-volume/fv_mms.cxx | 61 +++++ tests/MMS/spatial/finite-volume/makefile | 6 + tests/MMS/spatial/finite-volume/mms.py | 62 ++++++ tests/MMS/spatial/finite-volume/runtest | 209 ++++++++++++++++++ 7 files changed, 368 insertions(+) create mode 100644 tests/MMS/spatial/finite-volume/CMakeLists.txt create mode 100644 tests/MMS/spatial/finite-volume/data/BOUT.inp create mode 100644 tests/MMS/spatial/finite-volume/fv_mms.cxx create mode 100644 tests/MMS/spatial/finite-volume/makefile create mode 100755 tests/MMS/spatial/finite-volume/mms.py create mode 100755 tests/MMS/spatial/finite-volume/runtest diff --git a/tests/MMS/CMakeLists.txt b/tests/MMS/CMakeLists.txt index a6667bcfa5..510385d54c 100644 --- a/tests/MMS/CMakeLists.txt +++ b/tests/MMS/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(spatial/d2dx2) add_subdirectory(spatial/d2dz2) add_subdirectory(spatial/diffusion) add_subdirectory(spatial/fci) +add_subdirectory(spatial/finite-volume) add_subdirectory(time) add_subdirectory(time-petsc) add_subdirectory(wave-1d) diff --git a/tests/MMS/spatial/finite-volume/CMakeLists.txt b/tests/MMS/spatial/finite-volume/CMakeLists.txt new file mode 100644 index 0000000000..6d9c839a05 --- /dev/null +++ b/tests/MMS/spatial/finite-volume/CMakeLists.txt @@ -0,0 +1,6 @@ +bout_add_mms_test(MMS-spatial-finite-volume + SOURCES fv_mms.cxx + USE_RUNTEST + USE_DATA_BOUT_INP + PROCESSORS 2 +) diff --git a/tests/MMS/spatial/finite-volume/data/BOUT.inp b/tests/MMS/spatial/finite-volume/data/BOUT.inp new file mode 100644 index 0000000000..afab4a34d5 --- /dev/null +++ b/tests/MMS/spatial/finite-volume/data/BOUT.inp @@ -0,0 +1,23 @@ +input_field = 0.1*sin(2.0*y) + 1 +K = 0.1*cos(3.0*y) + 1 +FV_Div_par_mod_solution = -0.188495559215388*(0.1*sin(2.0*y) + 1)*sin(3.0*y) + 0.125663706143592*(0.1*cos(3.0*y) + 1)*cos(2.0*y) +FV_Div_par_fvv_solution = -0.376991118430775*(0.1*sin(2.0*y) + 1)*(0.1*cos(3.0*y) + 1)*sin(3.0*y) + 0.125663706143592*(0.1*cos(3.0*y) + 1)^2*cos(2.0*y) +FV_Div_par_solution = -0.188495559215388*(0.1*sin(2.0*y) + 1)*sin(3.0*y) + 0.125663706143592*(0.1*cos(3.0*y) + 1)*cos(2.0*y) +FV_Div_par_K_Grad_par_solution = -0.15791367041743*(0.1*cos(3.0*y) + 1)*sin(2.0*y) - 0.0236870505626145*sin(3.0*y)*cos(2.0*y) +FV_Div_par_K_Grad_par_mod_solution = -0.15791367041743*(0.1*cos(3.0*y) + 1)*sin(2.0*y) - 0.0236870505626145*sin(3.0*y)*cos(2.0*y) + +[mesh] +MXG = 0 + +nx = 1 +ny = 128 +nz = 1 + +Ly = 10 + +dy = Ly / ny +J = 1 # Identity metric + +[mesh:ddy] +first = C2 +second = C2 diff --git a/tests/MMS/spatial/finite-volume/fv_mms.cxx b/tests/MMS/spatial/finite-volume/fv_mms.cxx new file mode 100644 index 0000000000..6b45ef3259 --- /dev/null +++ b/tests/MMS/spatial/finite-volume/fv_mms.cxx @@ -0,0 +1,61 @@ +#include "bout/bout.hxx" +#include "bout/field.hxx" +#include "bout/field3d.hxx" +#include "bout/field_factory.hxx" +#include "bout/fv_ops.hxx" +#include "bout/globals.hxx" +#include "bout/options.hxx" +#include "bout/options_io.hxx" +#include "bout/utils.hxx" + +#include + +#include +#include + +namespace { +auto fv_op_test(const std::string& name, Options& dump, const Field3D& input, + const Field3D& result) { + auto* mesh = input.getMesh(); + const Field3D solution{FieldFactory::get()->create3D(fmt::format("{}_solution", name), + Options::getRoot(), mesh)}; + const Field3D error{result - solution}; + + dump[fmt::format("{}_l_2", name)] = sqrt(mean(SQ(error), true, "RGN_NOBNDRY")); + dump[fmt::format("{}_l_inf", name)] = max(abs(error), true, "RGN_NOBNDRY"); + + dump[fmt::format("{}_result", name)] = result; + dump[fmt::format("{}_error", name)] = error; + dump[fmt::format("{}_input", name)] = input; + dump[fmt::format("{}_solution", name)] = solution; +} +} // namespace + +int main(int argc, char** argv) { + BoutInitialise(argc, argv); + + using bout::globals::mesh; + + Field3D input{FieldFactory::get()->create3D("input_field", Options::getRoot(), mesh)}; + Field3D K{FieldFactory::get()->create3D("K", Options::getRoot(), mesh)}; + + // Communicate to calculate parallel transform. + mesh->communicate(input, K); + + Options dump; + // Add mesh geometry variables + mesh->outputVars(dump); + + // Dummy variable for *_mod overloads + Field3D flow_ylow; + + fv_op_test("FV_Div_par", dump, input, FV::Div_par(input, K, K)); + fv_op_test("FV_Div_par_mod", dump, input, FV::Div_par_mod(input, K, K, flow_ylow)); + fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, K, K)); + fv_op_test("FV_Div_par_K_Grad_par", dump, input, FV::Div_par_K_Grad_par(K, input)); + fv_op_test("FV_Div_par_K_Grad_par_mod", dump, input, FV::Div_par_K_Grad_par(K, input)); + + bout::writeDefaultOutputFile(dump); + + BoutFinalise(); +} diff --git a/tests/MMS/spatial/finite-volume/makefile b/tests/MMS/spatial/finite-volume/makefile new file mode 100644 index 0000000000..88ba6c77e7 --- /dev/null +++ b/tests/MMS/spatial/finite-volume/makefile @@ -0,0 +1,6 @@ + +BOUT_TOP = ../../../.. + +SOURCEC = fci_mms.cxx + +include $(BOUT_TOP)/make.config diff --git a/tests/MMS/spatial/finite-volume/mms.py b/tests/MMS/spatial/finite-volume/mms.py new file mode 100755 index 0000000000..c8b473138d --- /dev/null +++ b/tests/MMS/spatial/finite-volume/mms.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# Generate manufactured solution and sources for FCI test +# + +from math import pi +import warnings + +from boututils.boutwarnings import AlwaysWarning +from boutdata.data import BoutOptionsFile +from boutdata.mms import exprToStr, y, Grad_par, Div_par, Metric +from sympy import sin, cos, Expr + +warnings.simplefilter("ignore", AlwaysWarning) + +# Length of the y domain +Ly = 10.0 + +# Identity +metric = Metric() + +# Define solution in terms of input x,y,z +f = 1 + 0.1 * sin(2 * y) +K = 1 + 0.1 * cos(3 * y) + +# Turn solution into real x and z coordinates +replace = [(y, metric.y * 2 * pi / Ly)] + +f = f.subs(replace) +K = K.subs(replace) + +# Substitute back to get input y coordinates +replace = [ (metric.y, y*Ly/(2*pi) ) ] + + +def Grad2_par2(f: Expr) -> Expr: + return Grad_par(Grad_par(f)) + + +def Div_par_K_Grad_par(f: Expr, K: Expr) -> Expr: + return (K * Grad2_par2(f)) + (Div_par(K) * Grad_par(f)) + + +############################################ +# Equations solved + +options = BoutOptionsFile("data/BOUT.inp") + +for name, expr in ( + ("input_field", f), + ("K", K), + ("FV_Div_par_solution", Div_par(f * K)), + ("FV_Div_par_K_Grad_par_solution", Div_par_K_Grad_par(f, K)), + ("FV_Div_par_K_Grad_par_mod_solution", Div_par_K_Grad_par(f, K)), + ("FV_Div_par_mod_solution", Div_par(f * K)), + ("FV_Div_par_fvv_solution", Div_par(f * K * K)), +): + expr_str = exprToStr(expr.subs(replace)) + print(f"{name} = {expr_str}") + options[name] = expr_str + +options.write("data/BOUT.inp", overwrite=True) diff --git a/tests/MMS/spatial/finite-volume/runtest b/tests/MMS/spatial/finite-volume/runtest new file mode 100755 index 0000000000..a836f5e735 --- /dev/null +++ b/tests/MMS/spatial/finite-volume/runtest @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# +# Python script to run and analyse MMS test +# + +import argparse +import json +import pathlib +import sys +from time import time + +from boutdata.collect import collect +from boututils.run_wrapper import build_and_log, launch_safe +from numpy import array, log, polyfit + +# Global parameters +DIRECTORY = "data" +NPROC = 2 +MTHREAD = 2 +OPERATORS = ( + "FV_Div_par", + "FV_Div_par_K_Grad_par", + "FV_Div_par_K_Grad_par_mod", + "FV_Div_par_mod", + "FV_Div_par_fvv", +) +# Resolution in y and z +NLIST = [8, 16, 32, 64] +dx = 1.0 / array(NLIST) + + +def quiet_collect(name: str) -> float: + # Index to return a plain (numpy) float rather than `BoutArray` + return collect( + name, + tind=[1, 1], + info=False, + path=DIRECTORY, + xguards=False, + yguards=False, + )[()] + + +def assert_convergence(error, dx, name, expected) -> bool: + fit = polyfit(log(dx), log(error), 1) + order = fit[0] + print(f"{name} convergence order = {order:f} (fit)", end="") + + order = log(error[-2] / error[-1]) / log(dx[-2] / dx[-1]) + print(f", {order:f} (small spacing)", end="") + + # Should be close to the expected order + success = order > expected * 0.95 + print(f"\t............ {'PASS' if success else 'FAIL'}") + + return success + + +def run_fv_operators(nz: int, name: str) -> dict[str, float]: + # Command to run + args = f"MZ={nz} mesh:ny={nz} {name}" + cmd = f"./fv_mms {args}" + print(f"Running command: {cmd}", end="") + + # Launch using MPI + start = time() + status, out = launch_safe(cmd, nproc=NPROC, mthread=MTHREAD, pipe=True) + print(f" ... done in {time() - start:.3}s") + + # Save output to log file + pathlib.Path(f"run.log.{nz}").write_text(out) + + if status: + print(f"Run failed!\nOutput was:\n{out}") + sys.exit(status) + + return { + operator: { + "l_2": quiet_collect(f"{operator}_l_2"), + "l_inf": quiet_collect(f"{operator}_l_inf"), + } + for operator in OPERATORS + } + + +def transpose( + errors: list[dict[str, dict[str, float]]], +) -> dict[str, dict[str, list[float]]]: + """Turn a list of {operator: error} into a dict of {operator: [errors]}""" + + kinds = ("l_2", "l_inf") + result = {operator: {kind: [] for kind in kinds} for operator in OPERATORS} + for error in errors: + for k, v in error.items(): + for kind in kinds: + result[k][kind].append(v[kind]) + return result + + +def check_fv_operators(name: str, case: dict) -> bool: + failures = [] + + order = case["order"] + args = case["args"] + + all_errors = [] + + for n in NLIST: + errors = run_fv_operators(n, args) + all_errors.append(errors) + + for operator in OPERATORS: + l_2 = errors[operator]["l_2"] + l_inf = errors[operator]["l_inf"] + + print(f"{operator} errors: l-2 {l_2:f} l-inf {l_inf:f}") + + final_errors = transpose(all_errors) + for operator in OPERATORS: + test_name = f"{operator} {name}" + success = assert_convergence( + final_errors[operator]["l_2"], dx, test_name, order + ) + if not success: + failures.append(test_name) + + return final_errors, failures + + +def make_plots(cases: dict[str, dict]): + try: + import matplotlib.pyplot as plt + except ImportError: + print("No matplotlib") + return + + num_operators = len(OPERATORS) + fig, axes = plt.subplots(1, num_operators, figsize=(num_operators * 4, 4)) + + for ax, operator in zip(axes, OPERATORS): + for name, case in cases.items(): + ax.loglog(dx, case[operator]["l_2"], "-", label=f"{name} $l_2$") + ax.loglog(dx, case[operator]["l_inf"], "--", label=f"{name} $l_\\inf$") + ax.legend(loc="upper left") + ax.grid() + ax.set_title(f"Error scaling for {operator}") + ax.set_xlabel(r"Mesh spacing $\delta x$") + ax.set_ylabel("Error norm") + + fig.tight_layout() + fig.savefig("fv_mms.pdf") + print("Plot saved to fv_mms.pdf") + + if args.show_plots: + plt.show() + plt.close() + + +if __name__ == "__main__": + build_and_log("Finite volume MMS test") + + parser = argparse.ArgumentParser("Error scaling test for finite volume operators") + parser.add_argument( + "--make-plots", action="store_true", help="Create plots of error scaling" + ) + parser.add_argument( + "--show-plots", + action="store_true", + help="Stop and show plots, implies --make-plots", + ) + parser.add_argument( + "--dump-errors", + type=str, + help="Output file to dump errors as JSON", + default="fv_operator_errors.json", + ) + + args = parser.parse_args() + + success = True + failures = [] + + cases = { + "default": { + "order": 2, + "args": "", + }, + } + + for name, case in cases.items(): + error2, failures_ = check_fv_operators(name, case) + case.update(error2) + failures.extend(failures_) + success &= len(failures) == 0 + + if args.dump_errors: + pathlib.Path(args.dump_errors).write_text(json.dumps(cases)) + + if args.make_plots or args.show_plots: + make_plots(cases) + + if success: + print("\nAll tests passed") + else: + print("\nSome tests failed:") + for failure in failures: + print(f"\t{failure}") + + sys.exit(0 if success else 1) From 207802fdf4e9c2d328bef8d86f3929b5f0f9ce06 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Fri, 7 Nov 2025 14:20:14 +0000 Subject: [PATCH 05/98] Add tests for all FV slope limiters --- tests/MMS/spatial/finite-volume/data/BOUT.inp | 12 +-- tests/MMS/spatial/finite-volume/fv_mms.cxx | 51 +++++++++---- tests/MMS/spatial/finite-volume/mms.py | 18 +++-- tests/MMS/spatial/finite-volume/runtest | 73 +++++++++---------- 4 files changed, 87 insertions(+), 67 deletions(-) diff --git a/tests/MMS/spatial/finite-volume/data/BOUT.inp b/tests/MMS/spatial/finite-volume/data/BOUT.inp index afab4a34d5..029011e437 100644 --- a/tests/MMS/spatial/finite-volume/data/BOUT.inp +++ b/tests/MMS/spatial/finite-volume/data/BOUT.inp @@ -1,10 +1,10 @@ input_field = 0.1*sin(2.0*y) + 1 -K = 0.1*cos(3.0*y) + 1 -FV_Div_par_mod_solution = -0.188495559215388*(0.1*sin(2.0*y) + 1)*sin(3.0*y) + 0.125663706143592*(0.1*cos(3.0*y) + 1)*cos(2.0*y) -FV_Div_par_fvv_solution = -0.376991118430775*(0.1*sin(2.0*y) + 1)*(0.1*cos(3.0*y) + 1)*sin(3.0*y) + 0.125663706143592*(0.1*cos(3.0*y) + 1)^2*cos(2.0*y) -FV_Div_par_solution = -0.188495559215388*(0.1*sin(2.0*y) + 1)*sin(3.0*y) + 0.125663706143592*(0.1*cos(3.0*y) + 1)*cos(2.0*y) -FV_Div_par_K_Grad_par_solution = -0.15791367041743*(0.1*cos(3.0*y) + 1)*sin(2.0*y) - 0.0236870505626145*sin(3.0*y)*cos(2.0*y) -FV_Div_par_K_Grad_par_mod_solution = -0.15791367041743*(0.1*cos(3.0*y) + 1)*sin(2.0*y) - 0.0236870505626145*sin(3.0*y)*cos(2.0*y) +FV_Div_par_mod_solution = -0.188495559215388*sin(3.0*y) +FV_Div_par_fvv_solution = -0.376991118430775*(0.1*cos(3.0*y) + 1)*sin(3.0*y)/(0.1*sin(2.0*y) + 1) - 0.125663706143592*(0.1*cos(3.0*y) + 1)^2*cos(2.0*y)/(0.1*sin(2.0*y) + 1)^2 +FV_Div_par_solution = -0.188495559215388*sin(3.0*y) +FV_Div_par_K_Grad_par_solution = 0.125663706143592*(-0.188495559215388*sin(3.0*y)/(0.1*sin(2.0*y) + 1) - 0.125663706143592*(0.1*cos(3.0*y) + 1)*cos(2.0*y)/(0.1*sin(2.0*y) + 1)^2)*cos(2.0*y) - 0.15791367041743*(0.1*cos(3.0*y) + 1)*sin(2.0*y)/(0.1*sin(2.0*y) + 1) +FV_Div_par_K_Grad_par_mod_solution = 0.125663706143592*(-0.188495559215388*sin(3.0*y)/(0.1*sin(2.0*y) + 1) - 0.125663706143592*(0.1*cos(3.0*y) + 1)*cos(2.0*y)/(0.1*sin(2.0*y) + 1)^2)*cos(2.0*y) - 0.15791367041743*(0.1*cos(3.0*y) + 1)*sin(2.0*y)/(0.1*sin(2.0*y) + 1) +v = (0.1*cos(3.0*y) + 1)/(0.1*sin(2.0*y) + 1) [mesh] MXG = 0 diff --git a/tests/MMS/spatial/finite-volume/fv_mms.cxx b/tests/MMS/spatial/finite-volume/fv_mms.cxx index 6b45ef3259..09f9986e72 100644 --- a/tests/MMS/spatial/finite-volume/fv_mms.cxx +++ b/tests/MMS/spatial/finite-volume/fv_mms.cxx @@ -15,19 +15,20 @@ namespace { auto fv_op_test(const std::string& name, Options& dump, const Field3D& input, - const Field3D& result) { + const Field3D& result, std::string suffix = "") { auto* mesh = input.getMesh(); const Field3D solution{FieldFactory::get()->create3D(fmt::format("{}_solution", name), Options::getRoot(), mesh)}; const Field3D error{result - solution}; - dump[fmt::format("{}_l_2", name)] = sqrt(mean(SQ(error), true, "RGN_NOBNDRY")); - dump[fmt::format("{}_l_inf", name)] = max(abs(error), true, "RGN_NOBNDRY"); + dump[fmt::format("{}{}_l_2", name, suffix)] = + sqrt(mean(SQ(error), true, "RGN_NOBNDRY")); + dump[fmt::format("{}{}_l_inf", name, suffix)] = max(abs(error), true, "RGN_NOBNDRY"); - dump[fmt::format("{}_result", name)] = result; - dump[fmt::format("{}_error", name)] = error; - dump[fmt::format("{}_input", name)] = input; - dump[fmt::format("{}_solution", name)] = solution; + dump[fmt::format("{}{}_result", name, suffix)] = result; + dump[fmt::format("{}{}_error", name, suffix)] = error; + dump[fmt::format("{}{}_input", name, suffix)] = input; + dump[fmt::format("{}{}_solution", name, suffix)] = solution; } } // namespace @@ -37,23 +38,45 @@ int main(int argc, char** argv) { using bout::globals::mesh; Field3D input{FieldFactory::get()->create3D("input_field", Options::getRoot(), mesh)}; - Field3D K{FieldFactory::get()->create3D("K", Options::getRoot(), mesh)}; + Field3D v{FieldFactory::get()->create3D("v", Options::getRoot(), mesh)}; // Communicate to calculate parallel transform. - mesh->communicate(input, K); + mesh->communicate(input, v); Options dump; // Add mesh geometry variables mesh->outputVars(dump); + dump["v"] = v; // Dummy variable for *_mod overloads Field3D flow_ylow; - fv_op_test("FV_Div_par", dump, input, FV::Div_par(input, K, K)); - fv_op_test("FV_Div_par_mod", dump, input, FV::Div_par_mod(input, K, K, flow_ylow)); - fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, K, K)); - fv_op_test("FV_Div_par_K_Grad_par", dump, input, FV::Div_par_K_Grad_par(K, input)); - fv_op_test("FV_Div_par_K_Grad_par_mod", dump, input, FV::Div_par_K_Grad_par(K, input)); + fv_op_test("FV_Div_par", dump, input, FV::Div_par(input, v, v), "_MC"); + fv_op_test("FV_Div_par_mod", dump, input, + FV::Div_par_mod(input, v, v, flow_ylow), "_MC"); + fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, v, v), "_MC"); + + fv_op_test("FV_Div_par", dump, input, FV::Div_par(input, v, v), "_Upwind"); + fv_op_test("FV_Div_par_mod", dump, input, + FV::Div_par_mod(input, v, v, flow_ylow), "_Upwind"); + fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, v, v), + "_Upwind"); + + fv_op_test("FV_Div_par", dump, input, FV::Div_par(input, v, v), "_Fromm"); + fv_op_test("FV_Div_par_mod", dump, input, + FV::Div_par_mod(input, v, v, flow_ylow), "_Fromm"); + fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, v, v), + "_Fromm"); + + fv_op_test("FV_Div_par", dump, input, FV::Div_par(input, v, v), "_MinMod"); + fv_op_test("FV_Div_par_mod", dump, input, + FV::Div_par_mod(input, v, v, flow_ylow), "_MinMod"); + fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, v, v), + "_MinMod"); + + fv_op_test("FV_Div_par_K_Grad_par", dump, input, FV::Div_par_K_Grad_par(v, input)); + fv_op_test("FV_Div_par_K_Grad_par_mod", dump, input, + Div_par_K_Grad_par_mod(v, input, flow_ylow)); bout::writeDefaultOutputFile(dump); diff --git a/tests/MMS/spatial/finite-volume/mms.py b/tests/MMS/spatial/finite-volume/mms.py index c8b473138d..dfcfce9a09 100755 --- a/tests/MMS/spatial/finite-volume/mms.py +++ b/tests/MMS/spatial/finite-volume/mms.py @@ -21,13 +21,15 @@ # Define solution in terms of input x,y,z f = 1 + 0.1 * sin(2 * y) -K = 1 + 0.1 * cos(3 * y) +fv = 1 + 0.1 * cos(3 * y) + # Turn solution into real x and z coordinates replace = [(y, metric.y * 2 * pi / Ly)] f = f.subs(replace) -K = K.subs(replace) +fv = fv.subs(replace) +v = fv / f # Substitute back to get input y coordinates replace = [ (metric.y, y*Ly/(2*pi) ) ] @@ -48,12 +50,12 @@ def Div_par_K_Grad_par(f: Expr, K: Expr) -> Expr: for name, expr in ( ("input_field", f), - ("K", K), - ("FV_Div_par_solution", Div_par(f * K)), - ("FV_Div_par_K_Grad_par_solution", Div_par_K_Grad_par(f, K)), - ("FV_Div_par_K_Grad_par_mod_solution", Div_par_K_Grad_par(f, K)), - ("FV_Div_par_mod_solution", Div_par(f * K)), - ("FV_Div_par_fvv_solution", Div_par(f * K * K)), + ("v", v), + ("FV_Div_par_solution", Div_par(f * v)), + ("FV_Div_par_K_Grad_par_solution", Div_par_K_Grad_par(f, v)), + ("FV_Div_par_K_Grad_par_mod_solution", Div_par_K_Grad_par(f, v)), + ("FV_Div_par_mod_solution", Div_par(f * v)), + ("FV_Div_par_fvv_solution", Div_par(f * v * v)), ): expr_str = exprToStr(expr.subs(replace)) print(f"{name} = {expr_str}") diff --git a/tests/MMS/spatial/finite-volume/runtest b/tests/MMS/spatial/finite-volume/runtest index a836f5e735..5a02be1e50 100755 --- a/tests/MMS/spatial/finite-volume/runtest +++ b/tests/MMS/spatial/finite-volume/runtest @@ -17,13 +17,27 @@ from numpy import array, log, polyfit DIRECTORY = "data" NPROC = 2 MTHREAD = 2 -OPERATORS = ( - "FV_Div_par", - "FV_Div_par_K_Grad_par", - "FV_Div_par_K_Grad_par_mod", - "FV_Div_par_mod", - "FV_Div_par_fvv", -) +OPERATORS = { + # Slope-limiters necessarily reduce the accuracy in places + "FV_Div_par_MC": 1.5, + "FV_Div_par_mod_MC": 1.5, + "FV_Div_par_fvv_MC": 1.5, + + "FV_Div_par_Upwind": 1, + "FV_Div_par_mod_Upwind": 1, + "FV_Div_par_fvv_Upwind": 1, + + "FV_Div_par_Fromm": 1.5, + "FV_Div_par_mod_Fromm": 1.5, + "FV_Div_par_fvv_Fromm": 1.5, + + "FV_Div_par_MinMod": 1.5, + "FV_Div_par_mod_MinMod": 1.5, + "FV_Div_par_fvv_MinMod": 1.5, + + "FV_Div_par_K_Grad_par": 2, + "FV_Div_par_K_Grad_par_mod": 2, +} # Resolution in y and z NLIST = [8, 16, 32, 64] dx = 1.0 / array(NLIST) @@ -56,10 +70,9 @@ def assert_convergence(error, dx, name, expected) -> bool: return success -def run_fv_operators(nz: int, name: str) -> dict[str, float]: +def run_fv_operators(nz: int) -> dict[str, float]: # Command to run - args = f"MZ={nz} mesh:ny={nz} {name}" - cmd = f"./fv_mms {args}" + cmd = f"./fv_mms MZ={nz} mesh:ny={nz}" print(f"Running command: {cmd}", end="") # Launch using MPI @@ -97,16 +110,13 @@ def transpose( return result -def check_fv_operators(name: str, case: dict) -> bool: +def test_fv_operators() -> bool: failures = [] - order = case["order"] - args = case["args"] - all_errors = [] for n in NLIST: - errors = run_fv_operators(n, args) + errors = run_fv_operators(n) all_errors.append(errors) for operator in OPERATORS: @@ -116,13 +126,12 @@ def check_fv_operators(name: str, case: dict) -> bool: print(f"{operator} errors: l-2 {l_2:f} l-inf {l_inf:f}") final_errors = transpose(all_errors) - for operator in OPERATORS: - test_name = f"{operator} {name}" + for operator, order in OPERATORS.items(): success = assert_convergence( - final_errors[operator]["l_2"], dx, test_name, order + final_errors[operator]["l_2"], dx, operator, order ) if not success: - failures.append(test_name) + failures.append(operator) return final_errors, failures @@ -138,9 +147,8 @@ def make_plots(cases: dict[str, dict]): fig, axes = plt.subplots(1, num_operators, figsize=(num_operators * 4, 4)) for ax, operator in zip(axes, OPERATORS): - for name, case in cases.items(): - ax.loglog(dx, case[operator]["l_2"], "-", label=f"{name} $l_2$") - ax.loglog(dx, case[operator]["l_inf"], "--", label=f"{name} $l_\\inf$") + ax.loglog(dx, cases[operator]["l_2"], "-", label="$l_2$") + ax.loglog(dx, cases[operator]["l_inf"], "--", label="$l_\\inf$") ax.legend(loc="upper left") ax.grid() ax.set_title(f"Error scaling for {operator}") @@ -177,27 +185,14 @@ if __name__ == "__main__": args = parser.parse_args() - success = True - failures = [] - - cases = { - "default": { - "order": 2, - "args": "", - }, - } - - for name, case in cases.items(): - error2, failures_ = check_fv_operators(name, case) - case.update(error2) - failures.extend(failures_) - success &= len(failures) == 0 + error2, failures = test_fv_operators() + success = len(failures) == 0 if args.dump_errors: - pathlib.Path(args.dump_errors).write_text(json.dumps(cases)) + pathlib.Path(args.dump_errors).write_text(json.dumps(error2)) if args.make_plots or args.show_plots: - make_plots(cases) + make_plots(error2) if success: print("\nAll tests passed") From a230642c788a491ce0b8264b3c2ce24d84e326b0 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Fri, 7 Nov 2025 14:25:53 +0000 Subject: [PATCH 06/98] Port `Superbee` finite volume limiter from Hermes-3 Includes bug fix: ```diff - BoutReal gL = n.c - n.L; - BoutReal gR = n.R - n.c; + BoutReal gL = n.c - n.m; + BoutReal gR = n.p - n.c; ``` --- include/bout/fv_ops.hxx | 45 ++++++++++++++++++++++ tests/MMS/spatial/finite-volume/fv_mms.cxx | 7 ++++ tests/MMS/spatial/finite-volume/runtest | 4 ++ 3 files changed, 56 insertions(+) diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index bd0fa812d8..37d7b3b6fe 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -170,6 +170,51 @@ private: } }; +/// Superbee limiter +/// +/// This corresponds to the limiter function +/// φ(r) = max(0, min(2r, 1), min(r,2) +/// +/// The value at cell right (i.e. i + 1/2) is: +/// +/// n.R = n.c - φ(r) (n.c - (n.p + n.c)/2) +/// = n.c + φ(r) (n.p - n.c)/2 +/// +/// Four regimes: +/// a) r < 1/2 -> φ(r) = 2r +/// n.R = n.c + gL +/// b) 1/2 < r < 1 -> φ(r) = 1 +/// n.R = n.c + gR/2 +/// c) 1 < r < 2 -> φ(r) = r +/// n.R = n.c + gL/2 +/// d) 2 < r -> φ(r) = 2 +/// n.R = n.c + gR +/// +/// where the left and right gradients are: +/// gL = n.c - n.m +/// gR = n.p - n.c +/// +struct Superbee { + void operator()(Stencil1D& n) { + BoutReal gL = n.c - n.m; + BoutReal gR = n.p - n.c; + + // r = gL / gR + // Limiter is φ(r) + if (gL * gR < 0) { + // Different signs => Zero gradient + n.L = n.R = n.c; + } else { + BoutReal sign = SIGN(gL); + gL = fabs(gL); + gR = fabs(gR); + BoutReal half_slope = sign * BOUTMAX(BOUTMIN(gL, 0.5 * gR), BOUTMIN(gR, 0.5 * gL)); + n.L = n.c - half_slope; + n.R = n.c + half_slope; + } + } +}; + /*! * Communicate fluxes between processors * Takes values in guard cells, and adds them to cells diff --git a/tests/MMS/spatial/finite-volume/fv_mms.cxx b/tests/MMS/spatial/finite-volume/fv_mms.cxx index 09f9986e72..edf4bbc16a 100644 --- a/tests/MMS/spatial/finite-volume/fv_mms.cxx +++ b/tests/MMS/spatial/finite-volume/fv_mms.cxx @@ -74,6 +74,13 @@ int main(int argc, char** argv) { fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, v, v), "_MinMod"); + fv_op_test("FV_Div_par", dump, input, FV::Div_par(input, v, v), + "_Superbee"); + fv_op_test("FV_Div_par_mod", dump, input, + FV::Div_par_mod(input, v, v, flow_ylow), "_Superbee"); + fv_op_test("FV_Div_par_fvv", dump, input, FV::Div_par_fvv(input, v, v), + "_Superbee"); + fv_op_test("FV_Div_par_K_Grad_par", dump, input, FV::Div_par_K_Grad_par(v, input)); fv_op_test("FV_Div_par_K_Grad_par_mod", dump, input, Div_par_K_Grad_par_mod(v, input, flow_ylow)); diff --git a/tests/MMS/spatial/finite-volume/runtest b/tests/MMS/spatial/finite-volume/runtest index 5a02be1e50..b38a6359ac 100755 --- a/tests/MMS/spatial/finite-volume/runtest +++ b/tests/MMS/spatial/finite-volume/runtest @@ -35,6 +35,10 @@ OPERATORS = { "FV_Div_par_mod_MinMod": 1.5, "FV_Div_par_fvv_MinMod": 1.5, + "FV_Div_par_Superbee": 1.5, + "FV_Div_par_mod_Superbee": 1.5, + "FV_Div_par_fvv_Superbee": 1.5, + "FV_Div_par_K_Grad_par": 2, "FV_Div_par_K_Grad_par_mod": 2, } From 30dc67002eff2e2f216a937d8f10e3496e8281c8 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Fri, 7 Nov 2025 14:55:39 +0000 Subject: [PATCH 07/98] Fix clang-tidy warnings for fv_ops --- include/bout/fv_ops.hxx | 172 +++++++++---------- src/mesh/difops.cxx | 85 +++++----- src/mesh/fv_ops.cxx | 185 +++++++++++---------- tests/MMS/spatial/finite-volume/fv_mms.cxx | 1 + 4 files changed, 223 insertions(+), 220 deletions(-) diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index 37d7b3b6fe..cd9a3536c1 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -5,33 +5,38 @@ #ifndef BOUT_FV_OPS_H #define BOUT_FV_OPS_H -#include - +#include "bout/assert.hxx" #include "bout/bout_types.hxx" +#include "bout/boutexception.hxx" +#include "bout/build_defines.hxx" +#include "bout/coordinates.hxx" +#include "bout/field.hxx" +#include "bout/field2d.hxx" #include "bout/field3d.hxx" #include "bout/globals.hxx" #include "bout/mesh.hxx" -#include "bout/output_bout_types.hxx" // NOLINT(unused-includes) +#include "bout/output_bout_types.hxx" // NOLINT(unused-includes, misc-include-cleaner) #include "bout/region.hxx" #include "bout/utils.hxx" #include "bout/vector2d.hxx" +#include + namespace FV { /*! * Div ( a Grad_perp(f) ) -- ∇⊥ ( a ⋅ ∇⊥ f) -- Vorticity */ -Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& x); +Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f); [[deprecated("Please use Div_a_Grad_perp instead")]] inline Field3D -Div_a_Laplace_perp(const Field3D& a, const Field3D& x) { - return Div_a_Grad_perp(a, x); +Div_a_Laplace_perp(const Field3D& a, const Field3D& f) { + return Div_a_Grad_perp(a, f); } /*! * Divergence of a parallel diffusion Div( k * Grad_par(f) ) */ -const Field3D Div_par_K_Grad_par(const Field3D& k, const Field3D& f, - bool bndry_flux = true); +Field3D Div_par_K_Grad_par(const Field3D& k, const Field3D& f, bool bndry_flux = true); /*! * 4th-order derivative in Y, using derivatives @@ -53,7 +58,7 @@ const Field3D Div_par_K_Grad_par(const Field3D& k, const Field3D& f, * * No fluxes through domain boundaries */ -const Field3D D4DY4(const Field3D& d, const Field3D& f); +Field3D D4DY4(const Field3D& d, const Field3D& f); /*! * 4th-order dissipation term @@ -71,18 +76,24 @@ const Field3D D4DY4(const Field3D& d, const Field3D& f); * f_2 | f_1 | f_0 | * f_b */ -const Field3D D4DY4_Index(const Field3D& f, bool bndry_flux = true); +Field3D D4DY4_Index(const Field3D& f, bool bndry_flux = true); /*! * Stencil used for Finite Volume calculations * which includes cell face values L and R */ struct Stencil1D { - // Cell centre values - BoutReal c, m, p, mm, pp; - - // Left and right cell face values - BoutReal L, R; + /// Cell centre values + BoutReal c; + BoutReal m; + BoutReal p; + BoutReal mm = BoutNaN; + BoutReal pp = BoutNaN; + + /// Left cell face value + BoutReal L = BoutNaN; + /// Right cell face value + BoutReal R = BoutNaN; }; /*! @@ -97,8 +108,8 @@ struct Upwind { */ struct Fromm { void operator()(Stencil1D& n) { - n.L = n.c - 0.25 * (n.p - n.m); - n.R = n.c + 0.25 * (n.p - n.m); + n.L = n.c - (0.25 * (n.p - n.m)); + n.R = n.c + (0.25 * (n.p - n.m)); } }; @@ -114,9 +125,9 @@ struct MinMod { void operator()(Stencil1D& n) { // Choose the gradient within the cell // as the minimum (smoothest) solution - BoutReal slope = _minmod(n.p - n.c, n.c - n.m); - n.L = n.c - 0.5 * slope; - n.R = n.c + 0.5 * slope; + const BoutReal slope = _minmod(n.p - n.c, n.c - n.m); + n.L = n.c - (0.5 * slope); + n.R = n.c + (0.5 * slope); } private: @@ -127,7 +138,7 @@ private: * returns zero, otherwise chooses the value * with the minimum magnitude. */ - BoutReal _minmod(BoutReal a, BoutReal b) { + static BoutReal _minmod(BoutReal a, BoutReal b) { if (a * b <= 0.0) { return 0.0; } @@ -149,17 +160,17 @@ private: */ struct MC { void operator()(Stencil1D& n) { - BoutReal slope = minmod(2. * (n.p - n.c), // 2*right difference - 0.5 * (n.p - n.m), // Central difference - 2. * (n.c - n.m)); // 2*left difference - n.L = n.c - 0.5 * slope; - n.R = n.c + 0.5 * slope; + const BoutReal slope = minmod(2. * (n.p - n.c), // 2*right difference + 0.5 * (n.p - n.m), // Central difference + 2. * (n.c - n.m)); // 2*left difference + n.L = n.c - (0.5 * slope); + n.R = n.c + (0.5 * slope); } private: // Return zero if any signs are different // otherwise return the value with the minimum magnitude - BoutReal minmod(BoutReal a, BoutReal b, BoutReal c) { + static BoutReal minmod(BoutReal a, BoutReal b, BoutReal c) { // if any of the signs are different, return zero gradient if ((a * b <= 0.0) || (a * c <= 0.0)) { return 0.0; @@ -196,8 +207,8 @@ private: /// struct Superbee { void operator()(Stencil1D& n) { - BoutReal gL = n.c - n.m; - BoutReal gR = n.p - n.c; + const BoutReal gL = n.c - n.m; + const BoutReal gR = n.p - n.c; // r = gL / gR // Limiter is φ(r) @@ -205,10 +216,11 @@ struct Superbee { // Different signs => Zero gradient n.L = n.R = n.c; } else { - BoutReal sign = SIGN(gL); - gL = fabs(gL); - gR = fabs(gR); - BoutReal half_slope = sign * BOUTMAX(BOUTMIN(gL, 0.5 * gR), BOUTMIN(gR, 0.5 * gL)); + const BoutReal sign = SIGN(gL); + const BoutReal abs_gL = fabs(gL); + const BoutReal abs_gR = fabs(gR); + const BoutReal half_slope = + sign * BOUTMAX(BOUTMIN(abs_gL, 0.5 * abs_gR), BOUTMIN(abs_gR, 0.5 * abs_gL)); n.L = n.c - half_slope; n.R = n.c + half_slope; } @@ -238,13 +250,13 @@ void communicateFluxes(Field3D& f); /// /// NB: Uses to/from FieldAligned coordinates template -const Field3D Div_par(const Field3D& f_in, const Field3D& v_in, - const Field3D& wave_speed_in, bool fixflux = true) { +Field3D Div_par(const Field3D& f_in, const Field3D& v_in, const Field3D& wave_speed_in, + bool fixflux = true) { ASSERT1_FIELDS_COMPATIBLE(f_in, v_in); ASSERT1_FIELDS_COMPATIBLE(f_in, wave_speed_in); - Mesh* mesh = f_in.getMesh(); + Mesh const* mesh = f_in.getMesh(); CellEdges cellboundary; @@ -264,29 +276,17 @@ const Field3D Div_par(const Field3D& f_in, const Field3D& v_in, Field3D result{zeroFrom(f)}; - // Only need one guard cell, so no need to communicate fluxes - // Instead calculate in guard cells to preserve fluxes - int ys = mesh->ystart - 1; - int ye = mesh->yend + 1; - for (int i = mesh->xstart; i <= mesh->xend; i++) { + const bool is_periodic_y = mesh->periodicY(i); + const bool is_first_y = mesh->firstY(i); + const bool is_last_y = mesh->lastY(i); - if (!mesh->firstY(i) || mesh->periodicY(i)) { - // Calculate in guard cell to get fluxes consistent between processors - ys = mesh->ystart - 1; - } else { - // Don't include the boundary cell. Note that this implies special - // handling of boundaries later - ys = mesh->ystart; - } - - if (!mesh->lastY(i) || mesh->periodicY(i)) { - // Calculate in guard cells - ye = mesh->yend + 1; - } else { - // Not in boundary cells - ye = mesh->yend; - } + // Only need one guard cell, so no need to communicate fluxes Instead + // calculate in guard cells to get fluxes consistent between processors, but + // don't include the boundary cell. Note that this implies special handling + // of boundaries later + const int ys = (!is_first_y || is_periodic_y) ? mesh->ystart - 1 : mesh->ystart; + const int ye = (!is_last_y || is_periodic_y) ? mesh->yend + 1 : mesh->yend; for (int j = ys; j <= ye; j++) { // Pre-calculate factors which multiply fluxes @@ -295,16 +295,16 @@ const Field3D Div_par(const Field3D& f_in, const Field3D& v_in, BoutReal common_factor = (coord->J(i, j) + coord->J(i, j + 1)) / (sqrt(coord->g_22(i, j)) + sqrt(coord->g_22(i, j + 1))); - BoutReal flux_factor_rc = common_factor / (coord->dy(i, j) * coord->J(i, j)); - BoutReal flux_factor_rp = + const BoutReal flux_factor_rc = common_factor / (coord->dy(i, j) * coord->J(i, j)); + const BoutReal flux_factor_rp = common_factor / (coord->dy(i, j + 1) * coord->J(i, j + 1)); // For left cell boundaries common_factor = (coord->J(i, j) + coord->J(i, j - 1)) / (sqrt(coord->g_22(i, j)) + sqrt(coord->g_22(i, j - 1))); - BoutReal flux_factor_lc = common_factor / (coord->dy(i, j) * coord->J(i, j)); - BoutReal flux_factor_lm = + const BoutReal flux_factor_lc = common_factor / (coord->dy(i, j) * coord->J(i, j)); + const BoutReal flux_factor_lm = common_factor / (coord->dy(i, j - 1) * coord->J(i, j - 1)); #endif for (int k = mesh->zstart; k <= mesh->zend; k++) { @@ -347,23 +347,23 @@ const Field3D Div_par(const Field3D& f_in, const Field3D& v_in, // Calculate velocity at right boundary (y+1/2) BoutReal vpar = 0.5 * (v(i, j, k) + v(i, j + 1, k)); - BoutReal flux; + BoutReal flux = NAN; - if (mesh->lastY(i) && (j == mesh->yend) && !mesh->periodicY(i)) { + if (is_last_y && (j == mesh->yend) && !is_periodic_y) { // Last point in domain - BoutReal bndryval = 0.5 * (s.c + s.p); + const BoutReal bndryval = 0.5 * (s.c + s.p); if (fixflux) { // Use mid-point to be consistent with boundary conditions flux = bndryval * vpar; } else { // Add flux due to difference in boundary values - flux = s.R * vpar + wave_speed(i, j, k) * (s.R - bndryval); + flux = (s.R * vpar) + (wave_speed(i, j, k) * (s.R - bndryval)); } } else { // Maximum wave speed in the two cells - BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j + 1, k)); + const BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j + 1, k)); if (vpar > amax) { // Supersonic flow out of this cell @@ -385,20 +385,20 @@ const Field3D Div_par(const Field3D& f_in, const Field3D& v_in, vpar = 0.5 * (v(i, j, k) + v(i, j - 1, k)); - if (mesh->firstY(i) && (j == mesh->ystart) && !mesh->periodicY(i)) { + if (is_first_y && (j == mesh->ystart) && !is_periodic_y) { // First point in domain - BoutReal bndryval = 0.5 * (s.c + s.m); + const BoutReal bndryval = 0.5 * (s.c + s.m); if (fixflux) { // Use mid-point to be consistent with boundary conditions flux = bndryval * vpar; } else { // Add flux due to difference in boundary values - flux = s.L * vpar - wave_speed(i, j, k) * (s.L - bndryval); + flux = (s.L * vpar) - (wave_speed(i, j, k) * (s.L - bndryval)); } } else { // Maximum wave speed in the two cells - BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j - 1, k)); + const BoutReal amax = BOUTMAX(wave_speed(i, j, k), wave_speed(i, j - 1, k)); if (vpar < -amax) { // Supersonic out of this cell @@ -432,11 +432,11 @@ const Field3D Div_par(const Field3D& f_in, const Field3D& v_in, * */ template -const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { +Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { ASSERT1(n_in.getLocation() == v.getLocation()); ASSERT1_FIELDS_COMPATIBLE(n_in, v.x); - Mesh* mesh = n_in.getMesh(); + const Mesh* mesh = n_in.getMesh(); CellEdges cellboundary; @@ -455,10 +455,10 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { BOUT_FOR(i, result.getRegion("RGN_NOBNDRY")) { // Calculate velocities - BoutReal vU = 0.25 * (vz[i.zp()] + vz[i]) * (coord->J[i.zp()] + coord->J[i]); - BoutReal vD = 0.25 * (vz[i.zm()] + vz[i]) * (coord->J[i.zm()] + coord->J[i]); - BoutReal vL = 0.25 * (vx[i.xm()] + vx[i]) * (coord->J[i.xm()] + coord->J[i]); - BoutReal vR = 0.25 * (vx[i.xp()] + vx[i]) * (coord->J[i.xp()] + coord->J[i]); + const BoutReal vU = 0.25 * (vz[i.zp()] + vz[i]) * (coord->J[i.zp()] + coord->J[i]); + const BoutReal vD = 0.25 * (vz[i.zm()] + vz[i]) * (coord->J[i.zm()] + coord->J[i]); + const BoutReal vL = 0.25 * (vx[i.xm()] + vx[i]) * (coord->J[i.xm()] + coord->J[i]); + const BoutReal vR = 0.25 * (vx[i.xp()] + vx[i]) * (coord->J[i.xp()] + coord->J[i]); // X direction Stencil1D s; @@ -473,7 +473,7 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { if ((i.x() == mesh->xend) && (mesh->lastX())) { // At right boundary in X if (bndry_flux) { - BoutReal flux; + BoutReal flux = NAN; if (vR > 0.0) { // Flux to boundary flux = vR * s.R; @@ -488,7 +488,7 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { // Not at a boundary if (vR > 0.0) { // Flux out into next cell - BoutReal flux = vR * s.R; + const BoutReal flux = vR * s.R; result[i] += flux / (coord->dx[i] * coord->J[i]); result[i.xp()] -= flux / (coord->dx[i.xp()] * coord->J[i.xp()]); } @@ -500,7 +500,7 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { // At left boundary in X if (bndry_flux) { - BoutReal flux; + BoutReal flux = NAN; if (vL < 0.0) { // Flux to boundary flux = vL * s.L; @@ -514,7 +514,7 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { } else { // Not at a boundary if (vL < 0.0) { - BoutReal flux = vL * s.L; + const BoutReal flux = vL * s.L; result[i] -= flux / (coord->dx[i] * coord->J[i]); result[i.xm()] += flux / (coord->dx[i.xm()] * coord->J[i.xm()]); } @@ -531,12 +531,12 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { cellboundary(s); if (vU > 0.0) { - BoutReal flux = vU * s.R; + const BoutReal flux = vU * s.R; result[i] += flux / (coord->J[i] * coord->dz[i]); result[i.zp()] -= flux / (coord->J[i.zp()] * coord->dz[i.zp()]); } if (vD < 0.0) { - BoutReal flux = vD * s.L; + const BoutReal flux = vD * s.L; result[i] -= flux / (coord->J[i] * coord->dz[i]); result[i.zm()] += flux / (coord->J[i.zm()] * coord->dz[i.zm()]); } @@ -556,13 +556,13 @@ const Field3D Div_f_v(const Field3D& n_in, const Vector3D& v, bool bndry_flux) { BOUT_FOR(i, result.getRegion("RGN_NOBNDRY")) { // Y velocities on y boundaries - BoutReal vU = 0.25 * (vy[i] + vy[i.yp()]) * (coord->J[i] + coord->J[i.yp()]); - BoutReal vD = 0.25 * (vy[i] + vy[i.ym()]) * (coord->J[i] + coord->J[i.ym()]); + const BoutReal vU = 0.25 * (vy[i] + vy[i.yp()]) * (coord->J[i] + coord->J[i.yp()]); + const BoutReal vD = 0.25 * (vy[i] + vy[i.ym()]) * (coord->J[i] + coord->J[i.ym()]); // n (advected quantity) on y boundaries // Note: Use unshifted n_in variable - BoutReal nU = 0.5 * (n[i] + n[i.yp()]); - BoutReal nD = 0.5 * (n[i] + n[i.ym()]); + const BoutReal nU = 0.5 * (n[i] + n[i.yp()]); + const BoutReal nD = 0.5 * (n[i] + n[i.ym()]); yresult[i] = (nU * vU - nD * vD) / (coord->J[i] * coord->dy[i]); } diff --git a/src/mesh/difops.cxx b/src/mesh/difops.cxx index 09d2441951..56773f3c4c 100644 --- a/src/mesh/difops.cxx +++ b/src/mesh/difops.cxx @@ -25,19 +25,20 @@ #include "bout/build_defines.hxx" -#include -#include -#include -#include -#include -#include -#include -#include - -#include // Delp2 uses same coefficients as inversion code - -#include -#include +#include "bout/assert.hxx" +#include "bout/derivs.hxx" +#include "bout/difops.hxx" +#include "bout/fft.hxx" +#include "bout/field2d.hxx" +#include "bout/globals.hxx" +#include "bout/interpolation.hxx" +#include "bout/invert_laplace.hxx" // Delp2 uses same coefficients as inversion code +#include "bout/msg_stack.hxx" +#include "bout/region.hxx" +#include "bout/solver.hxx" +#include "bout/unused.hxx" +#include "bout/utils.hxx" +#include "bout/vecops.hxx" #include @@ -366,14 +367,14 @@ Field3D Div_par_K_Grad_par(const Field3D& kY, const Field3D& f, CELL_LOC outloc) + Div_par(kY, outloc) * Grad_par(f, outloc); } -Field3D Div_par_K_Grad_par_mod(const Field3D& Kin, const Field3D& fin, - Field3D& flow_ylow, bool bndry_flux) { +Field3D Div_par_K_Grad_par_mod(const Field3D& Kin, const Field3D& fin, Field3D& flow_ylow, + bool bndry_flux) { TRACE("FV::Div_par_K_Grad_par_mod"); ASSERT2(Kin.getLocation() == fin.getLocation()); - Mesh* mesh = Kin.getMesh(); - Coordinates* coord = fin.getCoordinates(); + const Mesh* mesh = Kin.getMesh(); + const Coordinates* coord = fin.getCoordinates(); if (Kin.hasParallelSlices() && fin.hasParallelSlices()) { // Using parallel slices. @@ -395,18 +396,22 @@ Field3D Div_par_K_Grad_par_mod(const Field3D& Kin, const Field3D& fin, const auto iym = i.ym(); // Upper cell edge - const BoutReal c_up = 0.5 * (Kin[i] + K_up[iyp]); // K at the upper boundary - const BoutReal J_up = 0.5 * (coord->J[i] + coord->J.yup()[iyp]); // Jacobian at boundary + const BoutReal c_up = 0.5 * (Kin[i] + K_up[iyp]); // K at the upper boundary + const BoutReal J_up = + 0.5 * (coord->J[i] + coord->J.yup()[iyp]); // Jacobian at boundary const BoutReal g_22_up = 0.5 * (coord->g_22[i] + coord->g_22.yup()[iyp]); - const BoutReal gradient_up = 2. * (f_up[iyp] - fin[i]) / (coord->dy[i] + coord->dy.yup()[iyp]); + const BoutReal gradient_up = + 2. * (f_up[iyp] - fin[i]) / (coord->dy[i] + coord->dy.yup()[iyp]); const BoutReal flux_up = c_up * J_up * gradient_up / g_22_up; // Lower cell edge - const BoutReal c_down = 0.5 * (Kin[i] + K_down[iym]); // K at the lower boundary - const BoutReal J_down = 0.5 * (coord->J[i] + coord->J.ydown()[iym]); // Jacobian at boundary + const BoutReal c_down = 0.5 * (Kin[i] + K_down[iym]); // K at the lower boundary + const BoutReal J_down = + 0.5 * (coord->J[i] + coord->J.ydown()[iym]); // Jacobian at boundary const BoutReal g_22_down = 0.5 * (coord->g_22[i] + coord->g_22.ydown()[iym]); - const BoutReal gradient_down = 2. * (fin[i] - f_down[iym]) / (coord->dy[i] + coord->dy.ydown()[iym]); + const BoutReal gradient_down = + 2. * (fin[i] - f_down[iym]) / (coord->dy[i] + coord->dy.ydown()[iym]); const BoutReal flux_down = c_down * J_down * gradient_down / g_22_down; @@ -425,35 +430,32 @@ Field3D Div_par_K_Grad_par_mod(const Field3D& Kin, const Field3D& fin, BOUT_FOR(i, result.getRegion("RGN_NOBNDRY")) { // Calculate flux at upper surface - + const auto ix = i.x(); + const auto iy = i.y(); const auto iyp = i.yp(); const auto iym = i.ym(); - if (bndry_flux || mesh->periodicY(i.x()) || !mesh->lastY(i.x()) - || (i.y() != mesh->yend)) { - - BoutReal c = 0.5 * (K[i] + K[iyp]); // K at the upper boundary - BoutReal J = 0.5 * (coord->J[i] + coord->J[iyp]); // Jacobian at boundary - BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iyp]); + const bool is_periodic_y = mesh->periodicY(ix); - BoutReal gradient = 2. * (f[iyp] - f[i]) / (coord->dy[i] + coord->dy[iyp]); + if (bndry_flux || is_periodic_y || !mesh->lastY(ix) || (iy != mesh->yend)) { + const BoutReal c = 0.5 * (K[i] + K[iyp]); // K at the upper boundary + const BoutReal J = 0.5 * (coord->J[i] + coord->J[iyp]); // Jacobian at boundary + const BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iyp]); + const BoutReal gradient = 2. * (f[iyp] - f[i]) / (coord->dy[i] + coord->dy[iyp]); - BoutReal flux = c * J * gradient / g_22; + const BoutReal flux = c * J * gradient / g_22; result[i] += flux / (coord->dy[i] * coord->J[i]); } // Calculate flux at lower surface - if (bndry_flux || mesh->periodicY(i.x()) || !mesh->firstY(i.x()) - || (i.y() != mesh->ystart)) { - BoutReal c = 0.5 * (K[i] + K[iym]); // K at the lower boundary - BoutReal J = 0.5 * (coord->J[i] + coord->J[iym]); // Jacobian at boundary + if (bndry_flux || is_periodic_y || !mesh->firstY(ix) || (iy != mesh->ystart)) { + const BoutReal c = 0.5 * (K[i] + K[iym]); // K at the lower boundary + const BoutReal J = 0.5 * (coord->J[i] + coord->J[iym]); // Jacobian at boundary + const BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iym]); + const BoutReal gradient = 2. * (f[i] - f[iym]) / (coord->dy[i] + coord->dy[iym]); - BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iym]); - - BoutReal gradient = 2. * (f[i] - f[iym]) / (coord->dy[i] + coord->dy[iym]); - - BoutReal flux = c * J * gradient / g_22; + const BoutReal flux = c * J * gradient / g_22; result[i] -= flux / (coord->dy[i] * coord->J[i]); flow_ylow[i] = -flux * coord->dx[i] * coord->dz[i]; @@ -467,7 +469,6 @@ Field3D Div_par_K_Grad_par_mod(const Field3D& Kin, const Field3D& fin, return result; } - /******************************************************************************* * Delp2 * perpendicular Laplacian operator diff --git a/src/mesh/fv_ops.cxx b/src/mesh/fv_ops.cxx index fab8beb794..6b8d8a6f21 100644 --- a/src/mesh/fv_ops.cxx +++ b/src/mesh/fv_ops.cxx @@ -1,7 +1,16 @@ -#include -#include -#include -#include +#include "bout/fv_ops.hxx" + +#include "bout/assert.hxx" +#include "bout/bout_types.hxx" +#include "bout/boutexception.hxx" +#include "bout/build_config.hxx" +#include "bout/coordinates.hxx" +#include "bout/field2d.hxx" +#include "bout/field3d.hxx" +#include "bout/globals.hxx" +#include "bout/msg_stack.hxx" +#include "bout/region.hxx" +#include "bout/utils.hxx" namespace { template @@ -33,28 +42,19 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { // Flux in x - int xs = mesh->xstart - 1; - int xe = mesh->xend; - - /* - if(mesh->firstX()) - xs += 1; - */ - /* - if(mesh->lastX()) - xe -= 1; - */ + const int xs = mesh->xstart - 1; + const int xe = mesh->xend; for (int i = xs; i <= xe; i++) { for (int j = mesh->ystart; j <= mesh->yend; j++) { for (int k = mesh->zstart; k <= mesh->zend; k++) { // Calculate flux from i to i+1 - BoutReal fout = 0.5 * (a(i, j, k) + a(i + 1, j, k)) - * (coord->J(i, j, k) * coord->g11(i, j, k) - + coord->J(i + 1, j, k) * coord->g11(i + 1, j, k)) - * (f(i + 1, j, k) - f(i, j, k)) - / (coord->dx(i, j, k) + coord->dx(i + 1, j, k)); + const BoutReal fout = 0.5 * (a(i, j, k) + a(i + 1, j, k)) + * (coord->J(i, j, k) * coord->g11(i, j, k) + + coord->J(i + 1, j, k) * coord->g11(i + 1, j, k)) + * (f(i + 1, j, k) - f(i, j, k)) + / (coord->dx(i, j, k) + coord->dx(i + 1, j, k)); result(i, j, k) += fout / (coord->dx(i, j, k) * coord->J(i, j, k)); result(i + 1, j, k) -= fout / (coord->dx(i + 1, j, k) * coord->J(i + 1, j, k)); @@ -178,14 +178,13 @@ Field3D Div_a_Grad_perp(const Field3D& a, const Field3D& f) { return result; } -const Field3D Div_par_K_Grad_par(const Field3D& Kin, const Field3D& fin, - bool bndry_flux) { +Field3D Div_par_K_Grad_par(const Field3D& Kin, const Field3D& fin, bool bndry_flux) { ASSERT2(Kin.getLocation() == fin.getLocation()); - Mesh* mesh = Kin.getMesh(); + const Mesh* mesh = Kin.getMesh(); - bool use_parallel_slices = (Kin.hasParallelSlices() && fin.hasParallelSlices()); + const bool use_parallel_slices = (Kin.hasParallelSlices() && fin.hasParallelSlices()); const auto& K = use_parallel_slices ? Kin : toFieldAligned(Kin, "RGN_NOX"); const auto& f = use_parallel_slices ? fin : toFieldAligned(fin, "RGN_NOX"); @@ -209,13 +208,13 @@ const Field3D Div_par_K_Grad_par(const Field3D& Kin, const Field3D& fin, if (bndry_flux || mesh->periodicY(i.x()) || !mesh->lastY(i.x()) || (i.y() != mesh->yend)) { - BoutReal c = 0.5 * (K[i] + Kup[iyp]); // K at the upper boundary - BoutReal J = 0.5 * (coord->J[i] + coord->J[iyp]); // Jacobian at boundary - BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iyp]); + const BoutReal c = 0.5 * (K[i] + Kup[iyp]); // K at the upper boundary + const BoutReal J = 0.5 * (coord->J[i] + coord->J[iyp]); // Jacobian at boundary + const BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iyp]); - BoutReal gradient = 2. * (fup[iyp] - f[i]) / (coord->dy[i] + coord->dy[iyp]); + const BoutReal gradient = 2. * (fup[iyp] - f[i]) / (coord->dy[i] + coord->dy[iyp]); - BoutReal flux = c * J * gradient / g_22; + const BoutReal flux = c * J * gradient / g_22; result[i] += flux / (coord->dy[i] * coord->J[i]); } @@ -223,14 +222,15 @@ const Field3D Div_par_K_Grad_par(const Field3D& Kin, const Field3D& fin, // Calculate flux at lower surface if (bndry_flux || mesh->periodicY(i.x()) || !mesh->firstY(i.x()) || (i.y() != mesh->ystart)) { - BoutReal c = 0.5 * (K[i] + Kdown[iym]); // K at the lower boundary - BoutReal J = 0.5 * (coord->J[i] + coord->J[iym]); // Jacobian at boundary + const BoutReal c = 0.5 * (K[i] + Kdown[iym]); // K at the lower boundary + const BoutReal J = 0.5 * (coord->J[i] + coord->J[iym]); // Jacobian at boundary - BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iym]); + const BoutReal g_22 = 0.5 * (coord->g_22[i] + coord->g_22[iym]); - BoutReal gradient = 2. * (f[i] - fdown[iym]) / (coord->dy[i] + coord->dy[iym]); + const BoutReal gradient = + 2. * (f[i] - fdown[iym]) / (coord->dy[i] + coord->dy[iym]); - BoutReal flux = c * J * gradient / g_22; + const BoutReal flux = c * J * gradient / g_22; result[i] -= flux / (coord->dy[i] * coord->J[i]); } @@ -244,10 +244,10 @@ const Field3D Div_par_K_Grad_par(const Field3D& Kin, const Field3D& fin, return result; } -const Field3D D4DY4(const Field3D& d_in, const Field3D& f_in) { +Field3D D4DY4(const Field3D& d_in, const Field3D& f_in) { ASSERT1_FIELDS_COMPATIBLE(d_in, f_in); - Mesh* mesh = d_in.getMesh(); + const Mesh* mesh = d_in.getMesh(); Coordinates* coord = f_in.getCoordinates(); @@ -263,9 +263,9 @@ const Field3D D4DY4(const Field3D& d_in, const Field3D& f_in) { for (int i = mesh->xstart; i <= mesh->xend; i++) { // Check for boundaries - bool yperiodic = mesh->periodicY(i); - bool has_upper_boundary = !yperiodic && mesh->lastY(i); - bool has_lower_boundary = !yperiodic && mesh->firstY(i); + const bool yperiodic = mesh->periodicY(i); + const bool has_upper_boundary = !yperiodic && mesh->lastY(i); + const bool has_lower_boundary = !yperiodic && mesh->firstY(i); // Always calculate fluxes at upper Y cell boundary const int ystart = @@ -281,15 +281,15 @@ const Field3D D4DY4(const Field3D& d_in, const Field3D& f_in) { for (int j = ystart; j <= yend; j++) { for (int k = mesh->zstart; k <= mesh->zend; k++) { - BoutReal dy3 = SQ(coord->dy(i, j, k)) * coord->dy(i, j, k); + const BoutReal dy3 = SQ(coord->dy(i, j, k)) * coord->dy(i, j, k); // 3rd derivative at upper boundary - BoutReal d3fdy3 = + const BoutReal d3fdy3 = (f(i, j + 2, k) - 3. * f(i, j + 1, k) + 3. * f(i, j, k) - f(i, j - 1, k)) / dy3; - BoutReal flux = 0.5 * (d(i, j, k) + d(i, j + 1, k)) - * (coord->J(i, j, k) + coord->J(i, j + 1, k)) * d3fdy3; + const BoutReal flux = 0.5 * (d(i, j, k) + d(i, j + 1, k)) + * (coord->J(i, j, k) + coord->J(i, j + 1, k)) * d3fdy3; result(i, j, k) += flux / (coord->J(i, j, k) * coord->dy(i, j, k)); result(i, j + 1, k) -= flux / (coord->J(i, j + 1, k) * coord->dy(i, j + 1, k)); @@ -301,8 +301,8 @@ const Field3D D4DY4(const Field3D& d_in, const Field3D& f_in) { return are_unaligned ? fromFieldAligned(result, "RGN_NOBNDRY") : result; } -const Field3D D4DY4_Index(const Field3D& f_in, bool bndry_flux) { - Mesh* mesh = f_in.getMesh(); +Field3D D4DY4_Index(const Field3D& f_in, bool bndry_flux) { + const Mesh* mesh = f_in.getMesh(); // Convert to field aligned coordinates const bool is_unaligned = (f_in.getDirectionY() == YDirectionType::Standard); @@ -313,10 +313,10 @@ const Field3D D4DY4_Index(const Field3D& f_in, bool bndry_flux) { Coordinates* coord = f_in.getCoordinates(); for (int i = mesh->xstart; i <= mesh->xend; i++) { - bool yperiodic = mesh->periodicY(i); + const bool yperiodic = mesh->periodicY(i); - bool has_upper_boundary = !yperiodic && mesh->lastY(i); - bool has_lower_boundary = !yperiodic && mesh->firstY(i); + const bool has_upper_boundary = !yperiodic && mesh->lastY(i); + const bool has_lower_boundary = !yperiodic && mesh->firstY(i); for (int j = mesh->ystart; j <= mesh->yend; j++) { @@ -341,8 +341,8 @@ const Field3D D4DY4_Index(const Field3D& f_in, bool bndry_flux) { // Not on domain boundary // 3rd derivative at right cell boundary - const BoutReal d3fdx3 = - (f(i, j + 2, k) - 3. * f(i, j + 1, k) + 3. * f(i, j, k) - f(i, j - 1, k)); + const BoutReal d3fdx3 = (f(i, j + 2, k) - (3. * f(i, j + 1, k)) + + (3. * f(i, j, k)) - f(i, j - 1, k)); result(i, j, k) += d3fdx3 * factor_rc; result(i, j + 1, k) -= d3fdx3 * factor_rp; @@ -363,10 +363,10 @@ const Field3D D4DY4_Index(const Field3D& f_in, bool bndry_flux) { common_factor / (coord->J(i, j + 1, k) * coord->dy(i, j + 1, k)); const BoutReal d3fdx3 = - -((16. / 5) * 0.5 * (f(i, j + 1, k) + f(i, j, k)) // Boundary value f_b - - 6. * f(i, j, k) // f_0 - + 4. * f(i, j - 1, k) // f_1 - - (6. / 5) * f(i, j - 2, k) // f_2 + -(((16. / 5) * 0.5 * (f(i, j + 1, k) + f(i, j, k))) // Boundary value f_b + - (6. * f(i, j, k)) // f_0 + + (4. * f(i, j - 1, k)) // f_1 + - ((6. / 5) * f(i, j - 2, k)) // f_2 ); result(i, j, k) += d3fdx3 * factor_rc; @@ -392,8 +392,8 @@ const Field3D D4DY4_Index(const Field3D& f_in, bool bndry_flux) { common_factor / (coord->J(i, j - 1, k) * coord->dy(i, j - 1, k)); // Not on a domain boundary - const BoutReal d3fdx3 = - (f(i, j + 1, k) - 3. * f(i, j, k) + 3. * f(i, j - 1, k) - f(i, j - 2, k)); + const BoutReal d3fdx3 = (f(i, j + 1, k) - (3. * f(i, j, k)) + + (3. * f(i, j - 1, k)) - f(i, j - 2, k)); result(i, j, k) -= d3fdx3 * factor_lc; result(i, j - 1, k) += d3fdx3 * factor_lm; @@ -410,10 +410,10 @@ const Field3D D4DY4_Index(const Field3D& f_in, bool bndry_flux) { const BoutReal factor_lm = common_factor / (coord->J(i, j - 1, k) * coord->dy(i, j - 1, k)); const BoutReal d3fdx3 = - -(-(16. / 5) * 0.5 * (f(i, j - 1, k) + f(i, j, k)) // Boundary value f_b - + 6. * f(i, j, k) // f_0 - - 4. * f(i, j + 1, k) // f_1 - + (6. / 5) * f(i, j + 2, k) // f_2 + -((-(16. / 5) * 0.5 * (f(i, j - 1, k) + f(i, j, k))) // Boundary value f_b + + (6. * f(i, j, k)) // f_0 + - (4. * f(i, j + 1, k)) // f_1 + + ((6. / 5) * f(i, j + 2, k)) // f_2 ); result(i, j, k) -= d3fdx3 * factor_lc; @@ -436,8 +436,9 @@ void communicateFluxes(Field3D& f) { throw BoutException("communicateFluxes: Sorry!"); } - int size = mesh->LocalNy * mesh->LocalNz; - comm_handle xin, xout; + const int size = mesh->LocalNy * mesh->LocalNz; + comm_handle xin = nullptr; + comm_handle xout = nullptr; // Cache results to silence spurious compiler warning about xin, // xout possibly being uninitialised when used const bool not_first = mesh->periodicX || !mesh->firstX(); @@ -496,45 +497,45 @@ Field3D Div_Perp_Lap(const Field3D& a, const Field3D& f, CELL_LOC outloc) { // o --- gD --- o // Coordinates* coords = a.getCoordinates(outloc); - Mesh* mesh = f.getMesh(); + const Mesh* mesh = f.getMesh(); for (int i = mesh->xstart; i <= mesh->xend; i++) { for (int j = mesh->ystart; j <= mesh->yend; j++) { for (int k = 0; k < mesh->LocalNz; k++) { // wrap k-index around as Z is (currently) periodic. - int kp = (k + 1) % (mesh->LocalNz); - int km = (k - 1 + mesh->LocalNz) % (mesh->LocalNz); + const int kp = (k + 1) % (mesh->LocalNz); + const int km = (k - 1 + mesh->LocalNz) % (mesh->LocalNz); // Calculate gradients on cell faces -- assumes constant grid spacing - BoutReal gR = - (coords->g11(i, j, k) + coords->g11(i + 1, j, k)) - * (f(i + 1, j, k) - f(i, j, k)) - / (coords->dx(i + 1, j, k) + coords->dx(i, j, k)) - + 0.5 * (coords->g13(i, j, k) + coords->g13(i + 1, j, k)) - * (f(i + 1, j, kp) - f(i + 1, j, km) + f(i, j, kp) - f(i, j, km)) - / (4. * coords->dz(i, j, k)); - - BoutReal gL = - (coords->g11(i - 1, j, k) + coords->g11(i, j, k)) - * (f(i, j, k) - f(i - 1, j, k)) - / (coords->dx(i - 1, j, k) + coords->dx(i, j, k)) - + 0.5 * (coords->g13(i - 1, j, k) + coords->g13(i, j, k)) - * (f(i - 1, j, kp) - f(i - 1, j, km) + f(i, j, kp) - f(i, j, km)) - / (4 * coords->dz(i, j, k)); - - BoutReal gD = - coords->g13(i, j, k) - * (f(i + 1, j, km) - f(i - 1, j, km) + f(i + 1, j, k) - f(i - 1, j, k)) - / (4. * coords->dx(i, j, k)) - + coords->g33(i, j, k) * (f(i, j, k) - f(i, j, km)) / coords->dz(i, j, k); - - BoutReal gU = - coords->g13(i, j, k) - * (f(i + 1, j, kp) - f(i - 1, j, kp) + f(i + 1, j, k) - f(i - 1, j, k)) - / (4. * coords->dx(i, j, k)) - + coords->g33(i, j, k) * (f(i, j, kp) - f(i, j, k)) / coords->dz(i, j, k); + const BoutReal gR = + ((coords->g11(i, j, k) + coords->g11(i + 1, j, k)) + * (f(i + 1, j, k) - f(i, j, k)) + / (coords->dx(i + 1, j, k) + coords->dx(i, j, k))) + + (0.5 * (coords->g13(i, j, k) + coords->g13(i + 1, j, k)) + * (f(i + 1, j, kp) - f(i + 1, j, km) + f(i, j, kp) - f(i, j, km)) + / (4. * coords->dz(i, j, k))); + + const BoutReal gL = + ((coords->g11(i - 1, j, k) + coords->g11(i, j, k)) + * (f(i, j, k) - f(i - 1, j, k)) + / (coords->dx(i - 1, j, k) + coords->dx(i, j, k))) + + (0.5 * (coords->g13(i - 1, j, k) + coords->g13(i, j, k)) + * (f(i - 1, j, kp) - f(i - 1, j, km) + f(i, j, kp) - f(i, j, km)) + / (4 * coords->dz(i, j, k))); + + const BoutReal gD = + (coords->g13(i, j, k) + * (f(i + 1, j, km) - f(i - 1, j, km) + f(i + 1, j, k) - f(i - 1, j, k)) + / (4. * coords->dx(i, j, k))) + + (coords->g33(i, j, k) * (f(i, j, k) - f(i, j, km)) / coords->dz(i, j, k)); + + const BoutReal gU = + (coords->g13(i, j, k) + * (f(i + 1, j, kp) - f(i - 1, j, kp) + f(i + 1, j, k) - f(i - 1, j, k)) + / (4. * coords->dx(i, j, k))) + + (coords->g33(i, j, k) * (f(i, j, kp) - f(i, j, k)) / coords->dz(i, j, k)); // Flow right BoutReal flux = gR * 0.25 * (coords->J(i + 1, j, k) + coords->J(i, j, k)) diff --git a/tests/MMS/spatial/finite-volume/fv_mms.cxx b/tests/MMS/spatial/finite-volume/fv_mms.cxx index edf4bbc16a..19f3f14610 100644 --- a/tests/MMS/spatial/finite-volume/fv_mms.cxx +++ b/tests/MMS/spatial/finite-volume/fv_mms.cxx @@ -1,4 +1,5 @@ #include "bout/bout.hxx" +#include "bout/difops.hxx" #include "bout/field.hxx" #include "bout/field3d.hxx" #include "bout/field_factory.hxx" From fcfe963eece7eb1a4c7afe3b0996acf1767b204e Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 12 Nov 2025 09:53:30 +0100 Subject: [PATCH 08/98] Move check up --- include/bout/fv_ops.hxx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index cd9a3536c1..07a2c2976b 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -600,12 +600,10 @@ Field3D Div_par_mod(const Field3D& f_in, const Field3D& v_in, bool fixflux = true) { Coordinates* coord = f_in.getCoordinates(); + ASSERT1_FIELDS_COMPATIBLE(f_in, v_in); if (f_in.isFci()) { // Use mid-point (cell boundary) averages - if (flow_ylow.isAllocated()) { - flow_ylow = emptyFrom(flow_ylow); - } ASSERT1(f_in.hasParallelSlices()); ASSERT1(v_in.hasParallelSlices()); @@ -631,7 +629,6 @@ Field3D Div_par_mod(const Field3D& f_in, const Field3D& v_in, } return result; } - ASSERT1_FIELDS_COMPATIBLE(f_in, v_in); ASSERT1_FIELDS_COMPATIBLE(f_in, wave_speed_in); const Mesh* mesh = f_in.getMesh(); From b209a5ad52f302f54983ed2b96037faab4b4a2c1 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 20 Jan 2026 09:27:22 +0100 Subject: [PATCH 09/98] Ensure FCI path is always used for FCI --- src/mesh/difops.cxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mesh/difops.cxx b/src/mesh/difops.cxx index 56773f3c4c..b0b5cc60d5 100644 --- a/src/mesh/difops.cxx +++ b/src/mesh/difops.cxx @@ -376,7 +376,9 @@ Field3D Div_par_K_Grad_par_mod(const Field3D& Kin, const Field3D& fin, Field3D& const Mesh* mesh = Kin.getMesh(); const Coordinates* coord = fin.getCoordinates(); - if (Kin.hasParallelSlices() && fin.hasParallelSlices()) { + if (Kin.isFci()) { + ASSERT1(Kin.hasParallelSlices()); + ASSERT1(fin.hasParallelSlices()); // Using parallel slices. // Note: Y slices may use different coordinate systems // -> Only B, dy and g_22 can be used in yup/ydown From e0480fc00c367e3c4df08e46326f1b79dd033898 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Feb 2026 10:47:41 +0100 Subject: [PATCH 10/98] FV_div_par_fvv seems to be reduced order due to slope limiting character --- tests/MMS/spatial/fci/runtest | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/MMS/spatial/fci/runtest b/tests/MMS/spatial/fci/runtest index 73babc9691..a08f36fbb8 100755 --- a/tests/MMS/spatial/fci/runtest +++ b/tests/MMS/spatial/fci/runtest @@ -33,6 +33,11 @@ OPERATORS = ( "FV_div_par_mod", "FV_div_par_fvv", ) + +# div_par_fvv is also tested in ../finite-volume, where 1.5th order is achieved +operator_order = { + "FV_div_par_fvv": 1, +} # Note that we need at least _2_ interior points for hermite spline # interpolation due to an awkwardness with the boundaries NX = 4 @@ -172,7 +177,10 @@ def check_fci_operators(name: str, case: dict) -> bool: for operator in OPERATORS: test_name = f"{operator} {name}" success = assert_convergence( - final_errors[operator]["l_2"], dx, test_name, order + final_errors[operator]["l_2"], + dx, + test_name, + operator_order.get(operator, order), ) if not success: failures.append(test_name) From 7ffc7facd5ebef7259c0560220d390f7a7d17af2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Feb 2026 11:34:25 +0100 Subject: [PATCH 11/98] Add doc to option Co-authored-by: Peter Hill --- src/mesh/interpolation/hermite_spline_xz.cxx | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index 27a4f1d614..1c140b0de2 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -107,6 +107,18 @@ XZHermiteSpline::XZHermiteSpline(int y_offset, Mesh* meshin) h10_x(localmesh), h11_x(localmesh), h00_z(localmesh), h01_z(localmesh), h10_z(localmesh), h11_z(localmesh) { + if constexpr (monotonic) { + if (options == nullptr) { + options = &Options::root()["mesh:paralleltransform:xzinterpolation"]; + } + abs_fac_monotonic = (*options)["atol"] + .doc("Absolute tolerance for clipping overshoot") + .withDefault(abs_fac_monotonic); + rel_fac_monotonic = (*options)["rtol"] + .doc("Relative tolerance for clipping overshoot") + .withDefault(rel_fac_monotonic); + } + // Index arrays contain guard cells in order to get subscripts right i_corner.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); k_corner.reallocate(localmesh->LocalNx, localmesh->LocalNy, localmesh->LocalNz); @@ -422,6 +434,25 @@ Field3D XZHermiteSpline::interpolate(const Field3D& f, const std::string& region f_interp[iyp] = +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; + if constexpr (monotonic) { +#endif + const auto corners = {(*gf)[IndG3D(g3dinds[i][0])], (*gf)[IndG3D(g3dinds[i][1])], + (*gf)[IndG3D(g3dinds[i][2])], (*gf)[IndG3D(g3dinds[i][3])]}; + const auto minmax = std::minmax(corners); + + const auto diff = + ((minmax.second - minmax.first) * rel_fac_monotonic) + abs_fac_monotonic; + f_interp[iyp] = std::max(f_interp[iyp], minmax.first - diff); + f_interp[iyp] = std::min(f_interp[iyp], minmax.second + diff); + } +#if USE_NEW_WEIGHTS and defined(HS_USE_PETSC) + ASSERT2(std::isfinite(cptr[int(i)])); + } + VecRestoreArrayRead(result, &cptr); +#elif USE_NEW_WEIGHTS + ASSERT2(std::isfinite(f_interp[iyp])); + } +#else ASSERT2(std::isfinite(f_interp[iyp]) || i.x() < localmesh->xstart || i.x() > localmesh->xend); } From f5ca1d06ae7779dacc2e831f15c7356d37307e0b Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Jun 2025 10:53:25 +0200 Subject: [PATCH 12/98] Introduce Field3DParallel Field3DParallel enforces that parallel derivatives can be taken. This means for FCI, parallel fields are present. It also ensures that if an operation is taken on such a field, the parallel fields are retained. This replaces part of fci-automagic, that always retained parallel fields on operations. --- include/bout/field3d.hxx | 53 ++- src/field/field3d.cxx | 10 + src/field/gen_fieldops.jinja | 58 ++- src/field/gen_fieldops.py | 26 +- src/field/generated_fieldops.cxx | 704 +++++++++++++++++++++++++++++-- 5 files changed, 806 insertions(+), 45 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 54d9fb85c9..be4e2d6ef0 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -517,7 +517,7 @@ public: bool areCalcParallelSlicesAllowed() const { return _allowCalcParallelSlices; }; void disallowCalcParallelSlices() { _allowCalcParallelSlices = false; }; -private: +protected: /// Array sizes (from fieldmesh). These are valid only if fieldmesh is not null int nx{-1}, ny{-1}, nz{-1}; @@ -692,4 +692,55 @@ bool operator==(const Field3D& a, const Field3D& b); /// Output a string describing a Field3D to a stream std::ostream& operator<<(std::ostream& out, const Field3D& value); +inline Field3D copy(const Field3D& f) { + Field3D result{f}; + result.allocate(); + for (size_t i = 0; i < result.numberParallelSlices(); ++i) { + result.yup(i).allocate(); + result.ydown(i).allocate(); + } + return result; +} + +class Field3DParallel : public Field3D { +public: + template + Field3DParallel(Types... args) : Field3D(&args...) { + ensureFieldAligned(); + } + Field3DParallel(Field3D&& f3d) : Field3D(std::move(f3d)) { ensureFieldAligned(); } + Field3DParallel(const Field3D& f3d) : Field3D(f3d) { ensureFieldAligned(); } + // Explicitly needed, as DirectionTypes is sometimes constructed from a + // brace enclosed list + Field3DParallel(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, + DirectionTypes directions_in = {YDirectionType::Standard, + ZDirectionType::Standard}, + std::optional regionID = {}) + : Field3D(localmesh, location_in, directions_in, regionID) { + ensureFieldAligned(); + } + Field3DParallel(Array data, Mesh* localmesh, CELL_LOC location = CELL_CENTRE, + DirectionTypes directions_in = {YDirectionType::Standard, + ZDirectionType::Standard}) + : Field3D(std::move(data), localmesh, location, directions_in) { + ensureFieldAligned(); + } + + Field3DParallel& operator*=(const Field3D&); + Field3DParallel& operator/=(const Field3D&); + Field3DParallel& operator+=(const Field3D&); + Field3DParallel& operator-=(const Field3D&); + Field3DParallel& operator*=(const Field3DParallel&); + Field3DParallel& operator/=(const Field3DParallel&); + Field3DParallel& operator+=(const Field3DParallel&); + Field3DParallel& operator-=(const Field3DParallel&); + Field3DParallel& operator*=(BoutReal); + Field3DParallel& operator/=(BoutReal); + Field3DParallel& operator+=(BoutReal); + Field3DParallel& operator-=(BoutReal); + +private: + void ensureFieldAligned(); +}; + #endif /* BOUT_FIELD3D_H */ diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index da7bd494c9..c5d157788d 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -910,3 +910,13 @@ void Field3D::_track(const BoutReal& change, std::string operation) { {"trace", trace}, }); } + +void Field3DParallel::ensureFieldAligned() { + if (isFci()) { + ASSERT2(hasParallelSlices()); + } else { + if (getDirectionY() != YDirectionType::Aligned) { + *this = toFieldAligned(*this); + } + } +} diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index 5476242d50..46c9fcad68 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -8,14 +8,41 @@ checkData({{lhs.name}}); checkData({{rhs.name}}); - {% if out == "Field3D" %} - {% if lhs == rhs == "Field3D" %} + {% if out.region_type == "3D" %} + {% if lhs.region_type == rhs.region_type == "3D" %} {{out.name}}.setRegion({{lhs.name}}.getMesh()->getCommonRegion({{lhs.name}}.getRegionID(), {{rhs.name}}.getRegionID())); - {% elif lhs == "Field3D" %} + {% if out == "Field3DParallel" %} + if ({{lhs.name}}.isFci()) { + {{out.name}}.splitParallelSlices(); + for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { + {{out.name}}.yup(i) = {{lhs.name}}.yup(i) {{operator}} {{rhs.name}}.yup(i); + {{out.name}}.ydown(i) = {{lhs.name}}.ydown(i) {{operator}} {{rhs.name}}.ydown(i); + } + } + {% endif %} + {% elif lhs.region_type == "3D" %} {{out.name}}.setRegion({{lhs.name}}.getRegionID()); - {% elif rhs == "Field3D" %} + {% if rhs == "BoutReal" and out == "Field3DParallel" %} + if ({{lhs.name}}.isFci()) { + {{out.name}}.splitParallelSlices(); + for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { + {{out.name}}.yup(i) = {{lhs.name}}.yup(i) {{operator}} {{rhs.name}}; + {{out.name}}.ydown(i) = {{lhs.name}}.ydown(i) {{operator}} {{rhs.name}}; + } + } + {% endif %} + {% elif rhs.region_type == "3D" %} {{out.name}}.setRegion({{rhs.name}}.getRegionID()); + {% if lhs == "BoutReal" and rhs == "Field3DParallel" %} + if ({{rhs.name}}.isFci()) { + {{out.name}}.splitParallelSlices(); + for (size_t i{0} ; i < {{rhs.name}}.numberParallelSlices() ; ++i) { + {{out.name}}.yup(i) = {{lhs.name}} {{operator}} {{rhs.name}}.yup(i); + {{out.name}}.ydown(i) = {{lhs.name}} {{operator}} {{rhs.name}}.ydown(i); + } + } + {% endif %} {% endif %} {% endif %} @@ -75,17 +102,26 @@ ASSERT1_FIELDS_COMPATIBLE(*this, rhs); {% endif %} - {% if (lhs == "Field3D") %} - // Delete existing parallel slices. We don't copy parallel slices, so any + {% if lhs == "Field3D" %} + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - + {% endif %} + {% if lhs == "Field3DParallel" and (rhs.region_type == "3D" or rhs == "BoutReal") %} + if (this->isFci()) { + for (size_t i{0} ; i < yup_fields.size() ; ++i) { + yup(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.yup(i){% endif %}; + ydown(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.ydown(i){% endif %}; + } + } else { + clearParallelSlices(); + } {% endif %} checkData(*this); checkData({{rhs.name}}); - {% if lhs == rhs == "Field3D" %} - regionID = fieldmesh->getCommonRegion(regionID, {{rhs.name}}.regionID); + {% if lhs.region_type == rhs.region_type == "3D" %} + regionID = fieldmesh->getCommonRegion(regionID, {{rhs.name}}.getRegionID()); {% endif %} @@ -129,14 +165,14 @@ } {% endif %} - {% if lhs == "Field3D" %} + {% if lhs.region_type == "3D" %} track(rhs, "operator{{operator}}="); {% endif %} checkData(*this); } else { - {% if lhs == "Field3D" %} + {% if lhs.region_type == "3D" %} track(rhs, "operator{{operator}}="); {% endif %} (*this) = (*this) {{operator}} {{rhs.name}}; diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 3e07d6fec4..1560a82dc5 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -103,7 +103,7 @@ def __init__( self.mixed_base_ind_var = mixed_base_ind_var # Note region_type isn't actually used currently but # may be useful in future. - if self.field_type == "Field3D": + if "Field3D" in self.field_type: self.region_type = "3D" elif self.field_type == "Field2D": self.region_type = "2D" @@ -183,6 +183,8 @@ def returnType(f1, f2): return copy(f1) elif f1 == "FieldPerp" or f2 == "FieldPerp": return copy(fieldPerp) + elif f1 == "Field3DParallel" or f2 == "Field3DParallel": + return copy(field3DPar) else: return copy(field3D) @@ -226,6 +228,13 @@ def returnType(f1, f2): jz_var=jz_var, mixed_base_ind_var=mixed_base_ind_var, ) + field3DPar = Field( + "Field3DParallel", + ["x", "y", "z"], + index_var=index_var, + jz_var=jz_var, + mixed_base_ind_var=mixed_base_ind_var, + ) field2D = Field( "Field2D", ["x", "y"], @@ -248,7 +257,8 @@ def returnType(f1, f2): mixed_base_ind_var=mixed_base_ind_var, ) - fields = [field3D, field2D, fieldPerp, boutreal] + fields = (field3D, field2D, fieldPerp, boutreal) + fields2 = (field3D, field3DPar, boutreal) with smart_open(args.filename, "w") as f: f.write(header) @@ -258,10 +268,16 @@ def returnType(f1, f2): template = env.get_template("gen_fieldops.jinja") - for lhs, rhs in itertools.product(fields, fields): - # We don't have to define BoutReal BoutReal operations - if lhs == rhs == "BoutReal": + # We don't have to define BoutReal BoutReal operations + done = [(boutreal, boutreal)] + for lhs, rhs in itertools.chain( + itertools.product(fields, fields), + itertools.product((field3D, field3DPar, boutreal), (field3D, field3DPar)), + ): + if (lhs, rhs) in done: continue + done.append((lhs, rhs)) + rhs = copy(rhs) lhs = copy(lhs) diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 24bfde425c..b3b81f07b8 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -31,14 +31,13 @@ Field3D& Field3D::operator*=(const Field3D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); - regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } @@ -78,14 +77,13 @@ Field3D& Field3D::operator/=(const Field3D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); - regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } @@ -125,14 +123,13 @@ Field3D& Field3D::operator+=(const Field3D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); - regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } @@ -172,14 +169,13 @@ Field3D& Field3D::operator-=(const Field3D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); - regionID = fieldmesh->getCommonRegion(regionID, rhs.regionID); + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } @@ -224,10 +220,9 @@ Field3D& Field3D::operator*=(const Field2D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -280,10 +275,9 @@ Field3D& Field3D::operator/=(const Field2D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -336,10 +330,9 @@ Field3D& Field3D::operator+=(const Field2D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -391,10 +384,9 @@ Field3D& Field3D::operator-=(const Field2D& rhs) { if (data.unique()) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -519,10 +511,9 @@ Field3D& Field3D::operator*=(const BoutReal rhs) { // otherwise just call the non-inplace version if (data.unique()) { - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -563,10 +554,9 @@ Field3D& Field3D::operator/=(const BoutReal rhs) { // otherwise just call the non-inplace version if (data.unique()) { - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -607,10 +597,9 @@ Field3D& Field3D::operator+=(const BoutReal rhs) { // otherwise just call the non-inplace version if (data.unique()) { - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -650,10 +639,9 @@ Field3D& Field3D::operator-=(const BoutReal rhs) { // otherwise just call the non-inplace version if (data.unique()) { - // Delete existing parallel slices. We don't copy parallel slices, so any + // Delete existing parallel slices. We don't update parallel slices, so any // that currently exist will be incorrect. clearParallelSlices(); - checkData(*this); checkData(rhs); @@ -1960,3 +1948,663 @@ FieldPerp operator-(const BoutReal lhs, const FieldPerp& rhs) { checkData(result); return result; } + +// Provide the C++ wrapper for multiplication of Field3D and Field3DParallel +Field3DParallel operator*(const Field3D& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) * rhs.yup(i); + result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ wrapper for division of Field3D and Field3DParallel +Field3DParallel operator/(const Field3D& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) / rhs.yup(i); + result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] / rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ wrapper for addition of Field3D and Field3DParallel +Field3DParallel operator+(const Field3D& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) + rhs.yup(i); + result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] + rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ wrapper for subtraction of Field3D and Field3DParallel +Field3DParallel operator-(const Field3D& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) - rhs.yup(i); + result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] - rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ wrapper for multiplication of Field3DParallel and Field3D +Field3DParallel operator*(const Field3DParallel& lhs, const Field3D& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) * rhs.yup(i); + result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by multiplication with Field3D +Field3DParallel& Field3DParallel::operator*=(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs.yup(i); + ydown(i) *= rhs.ydown(i); + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } + + track(rhs, "operator*="); + + checkData(*this); + + } else { + track(rhs, "operator*="); + (*this) = (*this) * rhs; + } + return *this; +} + +// Provide the C++ wrapper for division of Field3DParallel and Field3D +Field3DParallel operator/(const Field3DParallel& lhs, const Field3D& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) / rhs.yup(i); + result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] / rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by division with Field3D +Field3DParallel& Field3DParallel::operator/=(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs.yup(i); + ydown(i) /= rhs.ydown(i); + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } + + track(rhs, "operator/="); + + checkData(*this); + + } else { + track(rhs, "operator/="); + (*this) = (*this) / rhs; + } + return *this; +} + +// Provide the C++ wrapper for addition of Field3DParallel and Field3D +Field3DParallel operator+(const Field3DParallel& lhs, const Field3D& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) + rhs.yup(i); + result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] + rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by addition with Field3D +Field3DParallel& Field3DParallel::operator+=(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs.yup(i); + ydown(i) += rhs.ydown(i); + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } + + track(rhs, "operator+="); + + checkData(*this); + + } else { + track(rhs, "operator+="); + (*this) = (*this) + rhs; + } + return *this; +} + +// Provide the C++ wrapper for subtraction of Field3DParallel and Field3D +Field3DParallel operator-(const Field3DParallel& lhs, const Field3D& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) - rhs.yup(i); + result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] - rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by subtraction with Field3D +Field3DParallel& Field3DParallel::operator-=(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs.yup(i); + ydown(i) -= rhs.ydown(i); + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } + + track(rhs, "operator-="); + + checkData(*this); + + } else { + track(rhs, "operator-="); + (*this) = (*this) - rhs; + } + return *this; +} + +// Provide the C++ wrapper for multiplication of Field3DParallel and Field3DParallel +Field3DParallel operator*(const Field3DParallel& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) * rhs.yup(i); + result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by multiplication with Field3DParallel +Field3DParallel& Field3DParallel::operator*=(const Field3DParallel& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs; + ydown(i) *= rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } + + track(rhs, "operator*="); + + checkData(*this); + + } else { + track(rhs, "operator*="); + (*this) = (*this) * rhs; + } + return *this; +} + +// Provide the C++ wrapper for division of Field3DParallel and Field3DParallel +Field3DParallel operator/(const Field3DParallel& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) / rhs.yup(i); + result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] / rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by division with Field3DParallel +Field3DParallel& Field3DParallel::operator/=(const Field3DParallel& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs; + ydown(i) /= rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } + + track(rhs, "operator/="); + + checkData(*this); + + } else { + track(rhs, "operator/="); + (*this) = (*this) / rhs; + } + return *this; +} + +// Provide the C++ wrapper for addition of Field3DParallel and Field3DParallel +Field3DParallel operator+(const Field3DParallel& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) + rhs.yup(i); + result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] + rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by addition with Field3DParallel +Field3DParallel& Field3DParallel::operator+=(const Field3DParallel& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs; + ydown(i) += rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } + + track(rhs, "operator+="); + + checkData(*this); + + } else { + track(rhs, "operator+="); + (*this) = (*this) + rhs; + } + return *this; +} + +// Provide the C++ wrapper for subtraction of Field3DParallel and Field3DParallel +Field3DParallel operator-(const Field3DParallel& lhs, const Field3DParallel& rhs) { + ASSERT1_FIELDS_COMPATIBLE(lhs, rhs); + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) - rhs.yup(i); + result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] - rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by subtraction with Field3DParallel +Field3DParallel& Field3DParallel::operator-=(const Field3DParallel& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs; + ydown(i) -= rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } + + track(rhs, "operator-="); + + checkData(*this); + + } else { + track(rhs, "operator-="); + (*this) = (*this) - rhs; + } + return *this; +} + +// Provide the C++ wrapper for multiplication of BoutReal and Field3DParallel +Field3DParallel operator*(const BoutReal lhs, const Field3DParallel& rhs) { + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(rhs.getRegionID()); + if (rhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs * rhs.yup(i); + result.ydown(i) = lhs * rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs * rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ wrapper for division of BoutReal and Field3DParallel +Field3DParallel operator/(const BoutReal lhs, const Field3DParallel& rhs) { + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(rhs.getRegionID()); + if (rhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs / rhs.yup(i); + result.ydown(i) = lhs / rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs / rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ wrapper for addition of BoutReal and Field3DParallel +Field3DParallel operator+(const BoutReal lhs, const Field3DParallel& rhs) { + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(rhs.getRegionID()); + if (rhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs + rhs.yup(i); + result.ydown(i) = lhs + rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs + rhs[index]; + } + + checkData(result); + return result; +} + +// Provide the C++ wrapper for subtraction of BoutReal and Field3DParallel +Field3DParallel operator-(const BoutReal lhs, const Field3DParallel& rhs) { + + Field3DParallel result{emptyFrom(rhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(rhs.getRegionID()); + if (rhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs - rhs.yup(i); + result.ydown(i) = lhs - rhs.ydown(i); + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs - rhs[index]; + } + + checkData(result); + return result; +} From 12df335c7f708d6999c998e9234e1448abcd7b0d Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Jun 2025 10:54:01 +0200 Subject: [PATCH 13/98] No need for orderedDicts dicts have been preserving order for several releases now. --- src/field/gen_fieldops.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 1560a82dc5..29a8c47a54 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -53,15 +53,13 @@ def smart_open(filename, mode="r"): # The arthimetic operators -# OrderedDict to (try to) ensure consistency between python 2 & 3 -operators = OrderedDict( - [ - ("*", "multiplication"), - ("/", "division"), - ("+", "addition"), - ("-", "subtraction"), - ] -) +operators = { + "*": "multiplication", + "/": "division", + "+": "addition", + "-": "subtraction", +} + header = """// This file is autogenerated - see gen_fieldops.py #include From e71eebee3831c6ce3048b6f587dad4838aabbc56 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 26 Jun 2025 10:54:22 +0200 Subject: [PATCH 14/98] Remove wrong comment, FieldPerp is present --- include/bout/field.hxx | 3 ++- include/bout/field3d.hxx | 25 ++++++++++++++++++++++--- src/field/field3d.cxx | 35 +++++++++++++++++++++++++++++++++++ src/field/gen_fieldops.py | 1 - 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 3f7f591528..ce7b0b651c 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -189,7 +189,8 @@ inline bool areFieldsCompatible(const Field& field1, const Field& field2) { template inline T emptyFrom(const T& f) { static_assert(bout::utils::is_Field_v, "emptyFrom only works on Fields"); - return T(f.getMesh(), f.getLocation(), {f.getDirectionY(), f.getDirectionZ()}) + return T(f.getMesh(), f.getLocation(), + DirectionTypes{f.getDirectionY(), f.getDirectionZ()}, f.getRegionID()) .allocate(); } diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index be4e2d6ef0..00167ea153 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -44,6 +44,7 @@ class Field3D; class Mesh; class Options; +class Field3DParallel; /// Class for 3D X-Y-Z scalar fields /*! @@ -517,6 +518,8 @@ public: bool areCalcParallelSlicesAllowed() const { return _allowCalcParallelSlices; }; void disallowCalcParallelSlices() { _allowCalcParallelSlices = false; }; + inline Field3DParallel asF3dwy(); + protected: /// Array sizes (from fieldmesh). These are valid only if fieldmesh is not null int nx{-1}, ny{-1}, nz{-1}; @@ -705,11 +708,12 @@ inline Field3D copy(const Field3D& f) { class Field3DParallel : public Field3D { public: template - Field3DParallel(Types... args) : Field3D(&args...) { + Field3DParallel(Types... args) : Field3D(args...) { ensureFieldAligned(); } - Field3DParallel(Field3D&& f3d) : Field3D(std::move(f3d)) { ensureFieldAligned(); } - Field3DParallel(const Field3D& f3d) : Field3D(f3d) { ensureFieldAligned(); } + // Field3DParallel(const Field2D& f) : Field3D(f) { + // ensureFieldAligned(); + // } // Explicitly needed, as DirectionTypes is sometimes constructed from a // brace enclosed list Field3DParallel(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, @@ -725,6 +729,9 @@ public: : Field3D(std::move(data), localmesh, location, directions_in) { ensureFieldAligned(); } + Field3DParallel(BoutReal, Mesh*); + Field3D& asF3d() { return *this; } + const Field3D& asF3d() const { return *this; } Field3DParallel& operator*=(const Field3D&); Field3DParallel& operator/=(const Field3D&); @@ -738,9 +745,21 @@ public: Field3DParallel& operator/=(BoutReal); Field3DParallel& operator+=(BoutReal); Field3DParallel& operator-=(BoutReal); + Field3DParallel& operator=(const Field3D& rhs) { + Field3D::operator=(rhs); + ensureFieldAligned(); + return *this; + } + Field3DParallel& operator=(Field3D&& rhs) { + Field3D::operator=(std::move(rhs)); + ensureFieldAligned(); + return *this; + } + Field3DParallel& operator=(BoutReal); private: void ensureFieldAligned(); }; +Field3DParallel Field3D::asF3dwy() { return Field3DParallel(*this); } #endif /* BOUT_FIELD3D_H */ diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index c5d157788d..04444de3f0 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -92,6 +92,19 @@ Field3D::Field3D(const BoutReal val, Mesh* localmesh) : Field3D(localmesh) { *this = val; } +Field3DParallel::Field3DParallel(const BoutReal val, Mesh* localmesh) + : Field3D(localmesh) { + + *this = val; + if (this->isFci()) { + splitParallelSlices(); + for (size_t i = 0; i < numberParallelSlices(); ++i) { + yup(i) = val; + ydown(i) = val; + } + } +} + Field3D::Field3D(Array data_in, Mesh* localmesh, CELL_LOC datalocation, DirectionTypes directions_in) : Field(localmesh, datalocation, directions_in), data(std::move(data_in)) { @@ -337,6 +350,27 @@ Field3D& Field3D::operator=(const BoutReal val) { allocate(); + BOUT_FOR(i, getRegion("RGN_ALL")) { (*this)[i] = val; } + this->name = "BR"; + + return *this; +} + +Field3DParallel& Field3DParallel::operator=(const BoutReal val) { + TRACE("Field3DParallel = BoutReal"); + track(val, "operator="); + + if (isFci()) { + ASSERT2(hasParallelSlices()); + for (size_t i = 0; i < numberParallelSlices(); ++i) { + yup(i) = val; + ydown(i) = val; + } + } + resetRegion(); + + allocate(); + BOUT_FOR(i, getRegion("RGN_ALL")) { (*this)[i] = val; } return *this; @@ -890,6 +924,7 @@ void Field3D::_track(const T& change, std::string operation) { template void Field3D::_track>(const Field3D&, std::string); +template void Field3D::_track(const Field3DParallel&, std::string); template void Field3D::_track(const Field2D&, std::string); template void Field3D::_track<>(const FieldPerp&, std::string); diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 29a8c47a54..7dbebc79a3 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -218,7 +218,6 @@ def returnType(f1, f2): region_loop = "BOUT_FOR" # Declare what fields we currently support: - # Field perp is currently missing field3D = Field( "Field3D", ["x", "y", "z"], From a9366cbd203470b5ebecf10fa776cf5e3190c78e Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 10 Jul 2025 13:59:06 +0200 Subject: [PATCH 15/98] Start using Field3DParallel for derivatives --- include/bout/coordinates.hxx | 14 ++++++------- include/bout/derivs.hxx | 6 +++--- include/bout/index_derivs_interface.hxx | 17 +++++++++++++++- src/mesh/coordinates.cxx | 26 +++++++++---------------- src/sys/derivs.cxx | 6 +++--- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/include/bout/coordinates.hxx b/include/bout/coordinates.hxx index e7ead42ee5..8a4bfb5ef6 100644 --- a/include/bout/coordinates.hxx +++ b/include/bout/coordinates.hxx @@ -160,7 +160,7 @@ public: const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY"); - Field3D DDY(const Field3D& f, CELL_LOC outloc = CELL_DEFAULT, + Field3D DDY(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") const; @@ -172,7 +172,7 @@ public: FieldMetric Grad_par(const Field2D& var, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); - Field3D Grad_par(const Field3D& var, CELL_LOC outloc = CELL_DEFAULT, + Field3D Grad_par(const Field3DParallel& var, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); /// Advection along magnetic field V*b.Grad(f) @@ -180,7 +180,7 @@ public: CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); - Field3D Vpar_Grad_par(const Field3D& v, const Field3D& f, + Field3D Vpar_Grad_par(const Field3D& v, const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); @@ -188,14 +188,14 @@ public: FieldMetric Div_par(const Field2D& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); - Field3D Div_par(const Field3D& f, CELL_LOC outloc = CELL_DEFAULT, + Field3D Div_par(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); // Second derivative along magnetic field FieldMetric Grad2_par2(const Field2D& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); - Field3D Grad2_par2(const Field3D& f, CELL_LOC outloc = CELL_DEFAULT, + Field3D Grad2_par2(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT"); // Perpendicular Laplacian operator, using only X-Z derivatives // NOTE: This might be better bundled with the Laplacian inversion code @@ -207,13 +207,13 @@ public: // Full parallel Laplacian operator on scalar field // Laplace_par(f) = Div( b (b dot Grad(f)) ) FieldMetric Laplace_par(const Field2D& f, CELL_LOC outloc = CELL_DEFAULT); - Field3D Laplace_par(const Field3D& f, CELL_LOC outloc = CELL_DEFAULT); + Field3D Laplace_par(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT); // Full Laplacian operator on scalar field FieldMetric Laplace(const Field2D& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& dfdy_boundary_conditions = "free_o3", const std::string& dfdy_dy_region = ""); - Field3D Laplace(const Field3D& f, CELL_LOC outloc = CELL_DEFAULT, + Field3D Laplace(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& dfdy_boundary_conditions = "free_o3", const std::string& dfdy_dy_region = ""); diff --git a/include/bout/derivs.hxx b/include/bout/derivs.hxx index 1c360bb9cd..a8d9279378 100644 --- a/include/bout/derivs.hxx +++ b/include/bout/derivs.hxx @@ -82,7 +82,7 @@ Coordinates::FieldMetric DDX(const Field2D& f, CELL_LOC outloc = CELL_DEFAULT, /// If not given, defaults to DIFF_DEFAULT /// @param[in] region What region is expected to be calculated /// If not given, defaults to RGN_NOBNDRY -Field3D DDY(const Field3D& f, CELL_LOC outloc = CELL_DEFAULT, +Field3D DDY(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY"); @@ -410,7 +410,7 @@ Coordinates::FieldMetric VDDX(const Field2D& v, const Field2D& f, /// If not given, defaults to DIFF_DEFAULT /// @param[in] region What region is expected to be calculated /// If not given, defaults to RGN_NOBNDRY -Field3D VDDY(const Field3D& v, const Field3D& f, CELL_LOC outloc = CELL_DEFAULT, +Field3D VDDY(const Field3D& v, const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY"); @@ -533,7 +533,7 @@ Coordinates::FieldMetric FDDX(const Field2D& v, const Field2D& f, /// If not given, defaults to DIFF_DEFAULT /// @param[in] region What region is expected to be calculated /// If not given, defaults to RGN_NOBNDRY -Field3D FDDY(const Field3D& v, const Field3D& f, CELL_LOC outloc = CELL_DEFAULT, +Field3D FDDY(const Field3D& v, const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY"); diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 242492c3f8..02926a0fe1 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -198,8 +198,13 @@ template T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { - if (f.hasParallelSlices()) { + if (f.isFci()) { ASSERT1(f.getDirectionY() == YDirectionType::Standard); + if (!f.hasParallelSlices()) { + throw BoutException( + "parallel slices needed for parallel derivatives. Make sure to communicate and " + "apply parallel boundary conditions before calling derivative"); + } return standardDerivative(f, outloc, method, region); } else { @@ -356,6 +361,11 @@ T VDDY(const T& vel, const T& f, CELL_LOC outloc = CELL_DEFAULT, return are_unaligned ? fromFieldAligned(result, region) : result; } } +inline Field3D VDDY(const Field3D& v, const Field3DParallel& f, + CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", + const std::string& region = "RGN_NOBNDRY") { + return VDDY(v, f.asF3d(), outloc, method, region); +} template T FDDY(const T& vel, const T& f, CELL_LOC outloc = CELL_DEFAULT, @@ -380,6 +390,11 @@ T FDDY(const T& vel, const T& f, CELL_LOC outloc = CELL_DEFAULT, return are_unaligned ? fromFieldAligned(result, region) : result; } } +inline Field3D FDDY(const Field3D& v, const Field3DParallel& f, + CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", + const std::string& region = "RGN_NOBNDRY") { + return FDDY(v, f.asF3d(), outloc, method, region); +} ////////////// Z DERIVATIVE ///////////////// diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index d2634d3f88..aa9b62264b 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1485,16 +1485,8 @@ Coordinates::FieldMetric Coordinates::DDY(const Field2D& f, CELL_LOC loc, return bout::derivatives::index::DDY(f, loc, method, region) / dy; } -Field3D Coordinates::DDY(const Field3D& f, CELL_LOC outloc, const std::string& method, - const std::string& region) const { -#if BOUT_USE_METRIC_3D - if (!f.hasParallelSlices() and !transform->canToFromFieldAligned()) { - Field3D f_parallel = f; - transform->calcParallelSlices(f_parallel); - f_parallel.applyParallelBoundary("parallel_neumann_o2"); - return bout::derivatives::index::DDY(f_parallel, outloc, method, region); - } -#endif +Field3D Coordinates::DDY(const Field3DParallel& f, CELL_LOC outloc, + const std::string& method, const std::string& region) const { return bout::derivatives::index::DDY(f, outloc, method, region) / dy; }; @@ -1526,7 +1518,7 @@ Coordinates::FieldMetric Coordinates::Grad_par(const Field2D& var, return DDY(var) * invSg(); } -Field3D Coordinates::Grad_par(const Field3D& var, CELL_LOC outloc, +Field3D Coordinates::Grad_par(const Field3DParallel& var, CELL_LOC outloc, const std::string& method) { ASSERT1(location == outloc || outloc == CELL_DEFAULT); @@ -1546,8 +1538,8 @@ Coordinates::FieldMetric Coordinates::Vpar_Grad_par(const Field2D& v, const Fiel return VDDY(v, f) * invSg(); } -Field3D Coordinates::Vpar_Grad_par(const Field3D& v, const Field3D& f, CELL_LOC outloc, - const std::string& method) { +Field3D Coordinates::Vpar_Grad_par(const Field3D& v, const Field3DParallel& f, + CELL_LOC outloc, const std::string& method) { ASSERT1(location == outloc || outloc == CELL_DEFAULT); return VDDY(v, f, outloc, method) * invSg(); @@ -1568,7 +1560,7 @@ Coordinates::FieldMetric Coordinates::Div_par(const Field2D& f, CELL_LOC outloc, return Bxy * Grad_par(f / Bxy_floc, outloc, method); } -Field3D Coordinates::Div_par(const Field3D& f, CELL_LOC outloc, +Field3D Coordinates::Div_par(const Field3DParallel& f, CELL_LOC outloc, const std::string& method) { ASSERT1(location == outloc || outloc == CELL_DEFAULT); @@ -1608,7 +1600,7 @@ Coordinates::FieldMetric Coordinates::Grad2_par2(const Field2D& f, CELL_LOC outl return result; } -Field3D Coordinates::Grad2_par2(const Field3D& f, CELL_LOC outloc, +Field3D Coordinates::Grad2_par2(const Field3DParallel& f, CELL_LOC outloc, const std::string& method) { if (outloc == CELL_DEFAULT) { @@ -1774,7 +1766,7 @@ Coordinates::FieldMetric Coordinates::Laplace_par(const Field2D& f, CELL_LOC out return D2DY2(f, outloc) / g_22 + DDY(J / g_22, outloc) * DDY(f, outloc) / J; } -Field3D Coordinates::Laplace_par(const Field3D& f, CELL_LOC outloc) { +Field3D Coordinates::Laplace_par(const Field3DParallel& f, CELL_LOC outloc) { ASSERT1(location == outloc || outloc == CELL_DEFAULT); return D2DY2(f, outloc) / g_22 + DDY(J / g_22, outloc) * ::DDY(f, outloc) / J; } @@ -1796,7 +1788,7 @@ Coordinates::FieldMetric Coordinates::Laplace(const Field2D& f, CELL_LOC outloc, return result; } -Field3D Coordinates::Laplace(const Field3D& f, CELL_LOC outloc, +Field3D Coordinates::Laplace(const Field3DParallel& f, CELL_LOC outloc, const std::string& dfdy_boundary_conditions, const std::string& dfdy_dy_region) { diff --git a/src/sys/derivs.cxx b/src/sys/derivs.cxx index aab75b8f19..2b606b9e5d 100644 --- a/src/sys/derivs.cxx +++ b/src/sys/derivs.cxx @@ -66,7 +66,7 @@ Coordinates::FieldMetric DDX(const Field2D& f, CELL_LOC outloc, const std::strin ////////////// Y DERIVATIVE ///////////////// -Field3D DDY(const Field3D& f, CELL_LOC outloc, const std::string& method, +Field3D DDY(const Field3DParallel& f, CELL_LOC outloc, const std::string& method, const std::string& region) { return bout::derivatives::index::DDY(f, outloc, method, region) / f.getCoordinates(outloc)->dy; @@ -406,7 +406,7 @@ Coordinates::FieldMetric VDDY(const Field2D& v, const Field2D& f, CELL_LOC outlo } // general case -Field3D VDDY(const Field3D& v, const Field3D& f, CELL_LOC outloc, +Field3D VDDY(const Field3D& v, const Field3DParallel& f, CELL_LOC outloc, const std::string& method, const std::string& region) { return bout::derivatives::index::VDDY(v, f, outloc, method, region) / f.getCoordinates(outloc)->dy; @@ -467,7 +467,7 @@ Coordinates::FieldMetric FDDY(const Field2D& v, const Field2D& f, CELL_LOC outlo / f.getCoordinates(outloc)->dy; } -Field3D FDDY(const Field3D& v, const Field3D& f, CELL_LOC outloc, +Field3D FDDY(const Field3D& v, const Field3DParallel& f, CELL_LOC outloc, const std::string& method, const std::string& region) { return bout::derivatives::index::FDDY(v, f, outloc, method, region) / f.getCoordinates(outloc)->dy; From b3983c1eadb57ae85d580dfd834195377d9f4a57 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 16 Jul 2025 14:06:10 +0200 Subject: [PATCH 16/98] Do not use separate derivative store for Field3DParallel --- include/bout/deriv_store.hxx | 9 +++++++++ include/bout/index_derivs.hxx | 2 +- include/bout/index_derivs_interface.hxx | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/include/bout/deriv_store.hxx b/include/bout/deriv_store.hxx index b2b7928c2e..83bc34d7c8 100644 --- a/include/bout/deriv_store.hxx +++ b/include/bout/deriv_store.hxx @@ -522,4 +522,13 @@ private: } }; +template +auto& getStore() { + if constexpr (std::is_same::value) { + return DerivativeStore::getInstance(); + } else { + return DerivativeStore::getInstance(); + } +} + #endif diff --git a/include/bout/index_derivs.hxx b/include/bout/index_derivs.hxx index ccce9a7f5e..a8d1bbb1d3 100644 --- a/include/bout/index_derivs.hxx +++ b/include/bout/index_derivs.hxx @@ -148,7 +148,7 @@ struct registerMethod { // removed and we can use nGuard directly in the template statement. const int nGuards = method.meta.nGuards; - auto& derivativeRegister = DerivativeStore::getInstance(); + auto& derivativeRegister = getStore(); switch (method.meta.derivType) { case (DERIV::Standard): diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 02926a0fe1..043175a82b 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -148,8 +148,8 @@ T standardDerivative(const T& f, CELL_LOC outloc, const std::string& method, } // Lookup the method - auto derivativeMethod = DerivativeStore::getInstance().getStandardDerivative( - method, direction, stagger, derivType); + auto derivativeMethod = + getStore().getStandardDerivative(method, direction, stagger, derivType); // Create the result field T result{emptyFrom(f).setLocation(outloc)}; From 32f4c6511226a43f9f5b9ca98fb919fce8037dde Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 16 Jul 2025 14:16:24 +0200 Subject: [PATCH 17/98] Do not change non-FCI fields --- src/field/field3d.cxx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 04444de3f0..c6461d5040 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -949,9 +949,9 @@ void Field3D::_track(const BoutReal& change, std::string operation) { void Field3DParallel::ensureFieldAligned() { if (isFci()) { ASSERT2(hasParallelSlices()); - } else { - if (getDirectionY() != YDirectionType::Aligned) { - *this = toFieldAligned(*this); - } - } + } // else { + // if (getDirectionY() != YDirectionType::Aligned) { + // *this = toFieldAligned(*this); + // } + // } } From 09f7466a431de522691deefcf7de3dd29d00db2c Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 17 Jul 2025 11:02:46 +0200 Subject: [PATCH 18/98] Add function to header file Otherwise, the Field3DParallel is casted to Field3D and the wrong overloads are used --- include/bout/field3d.hxx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 00167ea153..62afd44d07 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -583,6 +583,26 @@ Field3D operator-(BoutReal lhs, const Field3D& rhs); Field3D operator*(BoutReal lhs, const Field3D& rhs); Field3D operator/(BoutReal lhs, const Field3D& rhs); +Field3DParallel operator+(const Field3D& lhs, const Field3DParallel& rhs); +Field3DParallel operator-(const Field3D& lhs, const Field3DParallel& rhs); +Field3DParallel operator*(const Field3D& lhs, const Field3DParallel& rhs); +Field3DParallel operator/(const Field3D& lhs, const Field3DParallel& rhs); + +Field3DParallel operator+(const Field3DParallel& lhs, const Field3D& rhs); +Field3DParallel operator-(const Field3DParallel& lhs, const Field3D& rhs); +Field3DParallel operator*(const Field3DParallel& lhs, const Field3D& rhs); +Field3DParallel operator/(const Field3DParallel& lhs, const Field3D& rhs); + +Field3DParallel operator+(const Field3DParallel& lhs, const Field3DParallel& rhs); +Field3DParallel operator-(const Field3DParallel& lhs, const Field3DParallel& rhs); +Field3DParallel operator*(const Field3DParallel& lhs, const Field3DParallel& rhs); +Field3DParallel operator/(const Field3DParallel& lhs, const Field3DParallel& rhs); + +Field3DParallel operator+(const BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator-(const BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator*(const BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator/(const BoutReal lhs, const Field3DParallel& rhs); + /*! * Unary minus. Returns the negative of given field, * iterates over whole domain including guard/boundary cells. From e1a15ddf4244f579040f6bb326802a9eded01f19 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 17 Jul 2025 11:03:10 +0200 Subject: [PATCH 19/98] Ensure emptyFrom works --- include/bout/field3d.hxx | 1 + 1 file changed, 1 insertion(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 62afd44d07..c4a41f3443 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -741,6 +741,7 @@ public: ZDirectionType::Standard}, std::optional regionID = {}) : Field3D(localmesh, location_in, directions_in, regionID) { + splitParallelSlices(); ensureFieldAligned(); } Field3DParallel(Array data, Mesh* localmesh, CELL_LOC location = CELL_CENTRE, From f98d5dec0ba86421345811b16667305a10fdd067 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 17 Jul 2025 23:51:26 +0200 Subject: [PATCH 20/98] Use f3dwy to preserve parallel fields The communication was a no-op, as that did never calculate the parallel fields. --- src/mesh/coordinates.cxx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index aa9b62264b..13126cf2b4 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1015,15 +1015,9 @@ int Coordinates::geometry(bool recalculate_staggered, G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) + 0.5 * g33 * DDY(g_33); - auto tmp = J * g12; - localmesh->communicate_no_slices(tmp); - G1 = (DDX(J * g11) + DDY(tmp) + DDZ(J * g13)) / J; - tmp = J * g22; - localmesh->communicate_no_slices(tmp); - G2 = (DDX(J * g12) + DDY(tmp) + DDZ(J * g23)) / J; - tmp = J * g23; - localmesh->communicate_no_slices(tmp); - G3 = (DDX(J * g13) + DDY(tmp) + DDZ(J * g33)) / J; + G1 = (DDX(J * g11) + DDY(J.asF3dwy() * g12) + DDZ(J * g13)) / J; + G2 = (DDX(J * g12) + DDY(J.asF3dwy() * g22) + DDZ(J * g23)) / J; + G3 = (DDX(J * g13) + DDY(J.asF3dwy() * g23) + DDZ(J * g33)) / J; // Communicate christoffel symbol terms output_progress.write("\tCommunicating connection terms\n"); From fc98ac49b09a6f5f57f416f07729ccea11bd1a18 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 17 Jul 2025 23:53:10 +0200 Subject: [PATCH 21/98] Preserve parallel fields for d1_dy calculation --- src/mesh/coordinates.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 13126cf2b4..5924579c5b 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1096,7 +1096,7 @@ int Coordinates::geometry(bool recalculate_staggered, if (localmesh->get(d2y, "d2y" + suffix, 0.0, false, location)) { output_warn.write( "\tWARNING: differencing quantity 'd2y' not found. Calculating from dy\n"); - d1_dy = DDY(1. / dy); // d/di(1/dy) + d1_dy = DDY(1. / dy.asF3dwy()); // d/di(1/dy) localmesh->communicate_no_slices(d1_dy); d1_dy = From 205d5024c44d0806ab7f86a9ef1d24776507f6c2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 17 Jul 2025 23:54:03 +0200 Subject: [PATCH 22/98] Add overloads for DD?(Field3DParallel) They should return Field3D, not Field3DParallel --- include/bout/index_derivs_interface.hxx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 043175a82b..62322a64e1 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -175,6 +175,11 @@ T DDX(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D return standardDerivative(f, outloc, method, region); } +inline Field3D DDX(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, + const std::string& method = "DEFAULT", + const std::string& region = "RGN_NOBNDRY") { + return DDX(f.asF3d(), outloc, method, region); +} template T D2DX2(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", @@ -215,6 +220,11 @@ T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D return is_unaligned ? fromFieldAligned(result, region) : result; } } +inline Field3D DDY(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, + const std::string& method = "DEFAULT", + const std::string& region = "RGN_NOBNDRY") { + return DDY(f.asF3d(), outloc, method, region); +} template T D2DY2(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", @@ -257,6 +267,11 @@ T DDZ(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D return standardDerivative(f, outloc, method, region); } +inline Field3D DDZ(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, + const std::string& method = "DEFAULT", + const std::string& region = "RGN_NOBNDRY") { + return DDZ(f.asF3d(), outloc, method, region); +} template T D2DZ2(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", From 6a7c4076d8e69f42730c91ab21bcf6f3038655b2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 18 Jul 2025 07:24:03 +0200 Subject: [PATCH 23/98] add asF3dwy() stub to Field2D --- include/bout/field2d.hxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index 906af38a1d..d0d3225572 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -273,6 +273,8 @@ public: int size() const override { return nx * ny; } + Field2D& asF3dwy() { return *this; } + private: /// Internal data array. Handles allocation/freeing of memory Array data; From 268ecfc21a5d127e65687bbe2d633faaf399e26d Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 18 Jul 2025 10:56:38 +0200 Subject: [PATCH 24/98] Add some documentation --- include/bout/field3d.hxx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index c4a41f3443..76295fa6fe 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -725,15 +725,16 @@ inline Field3D copy(const Field3D& f) { return result; } +/// Field3DParallel is intended to behave like Field3D, but preserve parallel +/// Fields. +/// Operations on Field3D, like multiplication, exp and floor only work on the +/// "main" field, Field3DParallel will retain the parallel slices. class Field3DParallel : public Field3D { public: template Field3DParallel(Types... args) : Field3D(args...) { ensureFieldAligned(); } - // Field3DParallel(const Field2D& f) : Field3D(f) { - // ensureFieldAligned(); - // } // Explicitly needed, as DirectionTypes is sometimes constructed from a // brace enclosed list Field3DParallel(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, From f3b36a45f6e324e03ef07cc507ccfb9207c8b986 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 18 Jul 2025 10:58:02 +0200 Subject: [PATCH 25/98] Be more explicit in the naming --- include/bout/field2d.hxx | 2 +- include/bout/field3d.hxx | 8 ++++---- include/bout/index_derivs_interface.hxx | 12 +++++++----- src/mesh/coordinates.cxx | 8 ++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index d0d3225572..212efe5cad 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -273,7 +273,7 @@ public: int size() const override { return nx * ny; } - Field2D& asF3dwy() { return *this; } + Field2D& asField3DParallel() { return *this; } private: /// Internal data array. Handles allocation/freeing of memory diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 76295fa6fe..44faa0a107 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -518,7 +518,7 @@ public: bool areCalcParallelSlicesAllowed() const { return _allowCalcParallelSlices; }; void disallowCalcParallelSlices() { _allowCalcParallelSlices = false; }; - inline Field3DParallel asF3dwy(); + inline Field3DParallel asField3DParallel(); protected: /// Array sizes (from fieldmesh). These are valid only if fieldmesh is not null @@ -752,8 +752,8 @@ public: ensureFieldAligned(); } Field3DParallel(BoutReal, Mesh*); - Field3D& asF3d() { return *this; } - const Field3D& asF3d() const { return *this; } + Field3D& asField3D() { return *this; } + const Field3D& asField3D() const { return *this; } Field3DParallel& operator*=(const Field3D&); Field3DParallel& operator/=(const Field3D&); @@ -783,5 +783,5 @@ private: void ensureFieldAligned(); }; -Field3DParallel Field3D::asF3dwy() { return Field3DParallel(*this); } +Field3DParallel Field3D::asField3DParallel() { return Field3DParallel(*this); } #endif /* BOUT_FIELD3D_H */ diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 62322a64e1..03cc480323 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -175,10 +175,11 @@ T DDX(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D return standardDerivative(f, outloc, method, region); } + inline Field3D DDX(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { - return DDX(f.asF3d(), outloc, method, region); + return DDX(f.asField3D(), outloc, method, region); } template @@ -223,7 +224,7 @@ T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D inline Field3D DDY(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { - return DDY(f.asF3d(), outloc, method, region); + return DDY(f.asField3D(), outloc, method, region); } template @@ -267,10 +268,11 @@ T DDZ(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D return standardDerivative(f, outloc, method, region); } + inline Field3D DDZ(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { - return DDZ(f.asF3d(), outloc, method, region); + return DDZ(f.asField3D(), outloc, method, region); } template @@ -379,7 +381,7 @@ T VDDY(const T& vel, const T& f, CELL_LOC outloc = CELL_DEFAULT, inline Field3D VDDY(const Field3D& v, const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { - return VDDY(v, f.asF3d(), outloc, method, region); + return VDDY(v, f.asField3D(), outloc, method, region); } template @@ -408,7 +410,7 @@ T FDDY(const T& vel, const T& f, CELL_LOC outloc = CELL_DEFAULT, inline Field3D FDDY(const Field3D& v, const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { - return FDDY(v, f.asF3d(), outloc, method, region); + return FDDY(v, f.asField3D(), outloc, method, region); } ////////////// Z DERIVATIVE ///////////////// diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 5924579c5b..b1b850b577 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1015,9 +1015,9 @@ int Coordinates::geometry(bool recalculate_staggered, G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) + 0.5 * g33 * DDY(g_33); - G1 = (DDX(J * g11) + DDY(J.asF3dwy() * g12) + DDZ(J * g13)) / J; - G2 = (DDX(J * g12) + DDY(J.asF3dwy() * g22) + DDZ(J * g23)) / J; - G3 = (DDX(J * g13) + DDY(J.asF3dwy() * g23) + DDZ(J * g33)) / J; + G1 = (DDX(J * g11) + DDY(J.asField3DParallel() * g12) + DDZ(J * g13)) / J; + G2 = (DDX(J * g12) + DDY(J.asField3DParallel() * g22) + DDZ(J * g23)) / J; + G3 = (DDX(J * g13) + DDY(J.asField3DParallel() * g23) + DDZ(J * g33)) / J; // Communicate christoffel symbol terms output_progress.write("\tCommunicating connection terms\n"); @@ -1096,7 +1096,7 @@ int Coordinates::geometry(bool recalculate_staggered, if (localmesh->get(d2y, "d2y" + suffix, 0.0, false, location)) { output_warn.write( "\tWARNING: differencing quantity 'd2y' not found. Calculating from dy\n"); - d1_dy = DDY(1. / dy.asF3dwy()); // d/di(1/dy) + d1_dy = DDY(1. / dy.asField3DParallel()); // d/di(1/dy) localmesh->communicate_no_slices(d1_dy); d1_dy = From b47343fdc3a8ad91e37bc5764c03d3cb02c8765b Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 21 Jul 2025 11:01:45 +0200 Subject: [PATCH 26/98] do not use const for BoutReal in signature --- include/bout/field3d.hxx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 44faa0a107..5936a30310 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -598,10 +598,10 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3DParallel& rhs Field3DParallel operator*(const Field3DParallel& lhs, const Field3DParallel& rhs); Field3DParallel operator/(const Field3DParallel& lhs, const Field3DParallel& rhs); -Field3DParallel operator+(const BoutReal lhs, const Field3DParallel& rhs); -Field3DParallel operator-(const BoutReal lhs, const Field3DParallel& rhs); -Field3DParallel operator*(const BoutReal lhs, const Field3DParallel& rhs); -Field3DParallel operator/(const BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator+(BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator-(BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator*(BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator/(BoutReal lhs, const Field3DParallel& rhs); /*! * Unary minus. Returns the negative of given field, From d07bde5e87b9de66a7a9432aec1e1fb4be04fc8d Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 21 Jul 2025 11:12:18 +0200 Subject: [PATCH 27/98] Add Field3DParallel + Field2D fieldops This is needed for 2D metrics, and in this case we probably want to clear the parallel fields. --- include/bout/field3d.hxx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 5936a30310..ee439f1cda 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -603,6 +603,32 @@ Field3DParallel operator-(BoutReal lhs, const Field3DParallel& rhs); Field3DParallel operator*(BoutReal lhs, const Field3DParallel& rhs); Field3DParallel operator/(BoutReal lhs, const Field3DParallel& rhs); +Field3D operator+(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} +Field3D operator-(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} +Field3D operator*(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} +Field3D operator/(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} + +Field3D operator+(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() + rhs; +} +Field3D operator-(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() - rhs; +} +Field3D operator*(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() * rhs; +} +Field3D operator/(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() / rhs; +} + /*! * Unary minus. Returns the negative of given field, * iterates over whole domain including guard/boundary cells. From 6374539e62714d14dc46862f0ef8e5ef769e5350 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 21 Jul 2025 11:14:10 +0200 Subject: [PATCH 28/98] Move function definition after class definition --- include/bout/field3d.hxx | 53 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index ee439f1cda..13b68272ee 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -603,32 +603,6 @@ Field3DParallel operator-(BoutReal lhs, const Field3DParallel& rhs); Field3DParallel operator*(BoutReal lhs, const Field3DParallel& rhs); Field3DParallel operator/(BoutReal lhs, const Field3DParallel& rhs); -Field3D operator+(const Field2D& lhs, const Field3DParallel& rhs) { - return lhs + rhs.asField3D(); -} -Field3D operator-(const Field2D& lhs, const Field3DParallel& rhs) { - return lhs + rhs.asField3D(); -} -Field3D operator*(const Field2D& lhs, const Field3DParallel& rhs) { - return lhs + rhs.asField3D(); -} -Field3D operator/(const Field2D& lhs, const Field3DParallel& rhs) { - return lhs + rhs.asField3D(); -} - -Field3D operator+(const Field3DParallel& lhs, const Field2D& rhs) { - return lhs.asField3D() + rhs; -} -Field3D operator-(const Field3DParallel& lhs, const Field2D& rhs) { - return lhs.asField3D() - rhs; -} -Field3D operator*(const Field3DParallel& lhs, const Field2D& rhs) { - return lhs.asField3D() * rhs; -} -Field3D operator/(const Field3DParallel& lhs, const Field2D& rhs) { - return lhs.asField3D() / rhs; -} - /*! * Unary minus. Returns the negative of given field, * iterates over whole domain including guard/boundary cells. @@ -810,4 +784,31 @@ private: }; Field3DParallel Field3D::asField3DParallel() { return Field3DParallel(*this); } + +inline Field3D operator+(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} +inline Field3D operator-(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} +inline Field3D operator*(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} +inline Field3D operator/(const Field2D& lhs, const Field3DParallel& rhs) { + return lhs + rhs.asField3D(); +} + +inline Field3D operator+(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() + rhs; +} +inline Field3D operator-(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() - rhs; +} +inline Field3D operator*(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() * rhs; +} +inline Field3D operator/(const Field3DParallel& lhs, const Field2D& rhs) { + return lhs.asField3D() / rhs; +} + #endif /* BOUT_FIELD3D_H */ From f49c94cd634dce276965a571451c1ddd2c6e39d1 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 21 Jul 2025 11:17:16 +0200 Subject: [PATCH 29/98] Prefer std::move --- include/bout/field3d.hxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 13b68272ee..cc34094fef 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -40,6 +40,7 @@ class Field3D; #include #include #include +#include #include class Mesh; @@ -732,7 +733,7 @@ inline Field3D copy(const Field3D& f) { class Field3DParallel : public Field3D { public: template - Field3DParallel(Types... args) : Field3D(args...) { + Field3DParallel(Types... args) : Field3D(std::move(args)...) { ensureFieldAligned(); } // Explicitly needed, as DirectionTypes is sometimes constructed from a From 140bf78c9d5b66e8a6bc70e5265a3427ef1cfefa Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 21 Jul 2025 11:30:05 +0200 Subject: [PATCH 30/98] Add binary operators Field2D - FieldPerp the functions where implemented, but not defined in the header --- include/bout/field2d.hxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index 212efe5cad..a28c8bfa8f 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -287,6 +287,10 @@ private: }; // Non-member overloaded operators +FieldPerp operator+(const Field2D& lhs, const FieldPerp& rhs); +FieldPerp operator-(const Field2D& lhs, const FieldPerp& rhs); +FieldPerp operator*(const Field2D& lhs, const FieldPerp& rhs); +FieldPerp operator/(const Field2D& lhs, const FieldPerp& rhs); Field2D operator+(const Field2D& lhs, const Field2D& rhs); Field2D operator-(const Field2D& lhs, const Field2D& rhs); From 84d99b3e15fb2101100bcb68b548e253c33b7acd Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 21 Jul 2025 13:03:42 +0200 Subject: [PATCH 31/98] Declare Field3DParallel --- include/bout/field.hxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index ce7b0b651c..6c22a4a258 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -659,6 +659,8 @@ T copy(const T& f) { return result; } +class Field3DParallel; + /// Apply a floor value \p f to a field \p var. Any value lower than /// the floor is set to the floor. /// From 12191976c605800526554c3b34ec4e1ac4f3346d Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 21 Aug 2025 09:53:50 +0200 Subject: [PATCH 32/98] Add default argument like for Field3D Otherwise `Field3DParallel(0.0)` fails, as it constructs a `Field3D` without parallel slices, and then fails as that cannot be converted to a `Field3DParallel` --- include/bout/field3d.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index cc34094fef..d9928b45b5 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -752,7 +752,7 @@ public: : Field3D(std::move(data), localmesh, location, directions_in) { ensureFieldAligned(); } - Field3DParallel(BoutReal, Mesh*); + Field3DParallel(BoutReal, Mesh* mesh = nullptr); Field3D& asField3D() { return *this; } const Field3D& asField3D() const { return *this; } From 7bb8367b6f53aef2c21e1cf83cf93e5ecbca6094 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 21 Aug 2025 09:54:08 +0200 Subject: [PATCH 33/98] Add missing functions --- include/bout/field3d.hxx | 5 + src/field/gen_fieldops.py | 2 +- src/field/generated_fieldops.cxx | 241 +++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index d9928b45b5..8a585914ce 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -604,6 +604,11 @@ Field3DParallel operator-(BoutReal lhs, const Field3DParallel& rhs); Field3DParallel operator*(BoutReal lhs, const Field3DParallel& rhs); Field3DParallel operator/(BoutReal lhs, const Field3DParallel& rhs); +Field3DParallel operator+(const Field3DParallel& lhs, BoutReal rhs); +Field3DParallel operator-(const Field3DParallel& lhs, BoutReal rhs); +Field3DParallel operator*(const Field3DParallel& lhs, BoutReal rhs); +Field3DParallel operator/(const Field3DParallel& lhs, BoutReal rhs); + /*! * Unary minus. Returns the negative of given field, * iterates over whole domain including guard/boundary cells. diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 7dbebc79a3..473cf3df1f 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -269,7 +269,7 @@ def returnType(f1, f2): done = [(boutreal, boutreal)] for lhs, rhs in itertools.chain( itertools.product(fields, fields), - itertools.product((field3D, field3DPar, boutreal), (field3D, field3DPar)), + itertools.product(fields2, fields2), ): if (lhs, rhs) in done: continue diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index b3b81f07b8..9dad114ec8 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -2513,6 +2513,247 @@ Field3DParallel& Field3DParallel::operator-=(const Field3DParallel& rhs) { return *this; } +// Provide the C++ wrapper for multiplication of Field3DParallel and BoutReal +Field3DParallel operator*(const Field3DParallel& lhs, const BoutReal rhs) { + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getRegionID()); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) * rhs; + result.ydown(i) = lhs.ydown(i) * rhs; + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * rhs; + } + +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} * {:s}", lhs.name, "BR"); +#endif + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by multiplication with BoutReal +Field3DParallel& Field3DParallel::operator*=(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs; + ydown(i) *= rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs; } + + track(rhs, "operator*="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, "BR"); +#endif + + checkData(*this); + + } else { + track(rhs, "operator*="); + (*this) = (*this) * rhs; + } + return *this; +} + +// Provide the C++ wrapper for division of Field3DParallel and BoutReal +Field3DParallel operator/(const Field3DParallel& lhs, const BoutReal rhs) { + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getRegionID()); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) / rhs; + result.ydown(i) = lhs.ydown(i) / rhs; + } + } + + const auto tmp = 1.0 / rhs; + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] * tmp; + } + +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} / {:s}", lhs.name, "BR"); +#endif + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by division with BoutReal +Field3DParallel& Field3DParallel::operator/=(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs; + ydown(i) /= rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs; } + + track(rhs, "operator/="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, "BR"); +#endif + + checkData(*this); + + } else { + track(rhs, "operator/="); + (*this) = (*this) / rhs; + } + return *this; +} + +// Provide the C++ wrapper for addition of Field3DParallel and BoutReal +Field3DParallel operator+(const Field3DParallel& lhs, const BoutReal rhs) { + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getRegionID()); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) + rhs; + result.ydown(i) = lhs.ydown(i) + rhs; + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] + rhs; + } + +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} + {:s}", lhs.name, "BR"); +#endif + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by addition with BoutReal +Field3DParallel& Field3DParallel::operator+=(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs; + ydown(i) += rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs; } + + track(rhs, "operator+="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, "BR"); +#endif + + checkData(*this); + + } else { + track(rhs, "operator+="); + (*this) = (*this) + rhs; + } + return *this; +} + +// Provide the C++ wrapper for subtraction of Field3DParallel and BoutReal +Field3DParallel operator-(const Field3DParallel& lhs, const BoutReal rhs) { + + Field3DParallel result{emptyFrom(lhs)}; + checkData(lhs); + checkData(rhs); + + result.setRegion(lhs.getRegionID()); + if (lhs.isFci()) { + result.splitParallelSlices(); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + result.yup(i) = lhs.yup(i) - rhs; + result.ydown(i) = lhs.ydown(i) - rhs; + } + } + + BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { + result[index] = lhs[index] - rhs; + } + +#if BOUT_USE_TRACK + result.name = fmt::format("{:s} - {:s}", lhs.name, "BR"); +#endif + checkData(result); + return result; +} + +// Provide the C++ operator to update Field3DParallel by subtraction with BoutReal +Field3DParallel& Field3DParallel::operator-=(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + if (data.unique()) { + + if (this->isFci()) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs; + ydown(i) -= rhs; + } + } else { + clearParallelSlices(); + } + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs; } + + track(rhs, "operator-="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, "BR"); +#endif + + checkData(*this); + + } else { + track(rhs, "operator-="); + (*this) = (*this) - rhs; + } + return *this; +} + // Provide the C++ wrapper for multiplication of BoutReal and Field3DParallel Field3DParallel operator*(const BoutReal lhs, const Field3DParallel& rhs) { From acd9b9312c8dc834a07851156912dd560f04ab07 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 21 Aug 2025 09:54:42 +0200 Subject: [PATCH 34/98] Ensure assignment to a Field3DParallel does not fail --- src/field/field3d.cxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index c6461d5040..36e0b6901d 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -361,7 +361,9 @@ Field3DParallel& Field3DParallel::operator=(const BoutReal val) { track(val, "operator="); if (isFci()) { - ASSERT2(hasParallelSlices()); + if (!hasParallelSlices()) { + splitParallelSlices(); + } for (size_t i = 0; i < numberParallelSlices(); ++i) { yup(i) = val; ydown(i) = val; From 0e6fc75af496eb14fe2a2b0cbcd0c94807c2ecfe Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 25 Aug 2025 11:41:39 +0200 Subject: [PATCH 35/98] add filledFrom for Field3DParallel --- include/bout/field3d.hxx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 8a585914ce..60e1a1b83f 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -37,6 +37,7 @@ class Field3D; #include "bout/region.hxx" #include "bout/traits.hxx" +#include #include #include #include @@ -817,4 +818,26 @@ inline Field3D operator/(const Field3DParallel& lhs, const Field2D& rhs) { return lhs.asField3D() / rhs; } +inline Field3DParallel +filledFrom(const Field3DParallel& f, + std::function func) { + auto result{emptyFrom(f)}; + if (f.hasParallelSlices()) { + BOUT_FOR(i, result.getRegion("RGN_NOY")) { result[i] = func(0, i); } + + for (size_t i = 0; i < result.numberParallelSlices(); ++i) { + BOUT_FOR(d, result.yup(i).getValidRegionWithDefault("RGN_INVALID")) { + result.yup(i)[d] = func(i + 1, d); + } + BOUT_FOR(d, result.ydown(i).getValidRegionWithDefault("RGN_INVALID")) { + result.ydown(i)[d] = func(-i - 1, d); + } + } + } else { + BOUT_FOR(i, result.getRegion("RGN_ALL")) { result[i] = func(0, i); } + } + + return result; +} + #endif /* BOUT_FIELD3D_H */ From 9b119c89c99fee2ef8cc20c8f557dae8ba206e94 Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 25 Aug 2025 13:04:21 +0200 Subject: [PATCH 36/98] Allocate before usage --- include/bout/field3d.hxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 60e1a1b83f..8d278099b6 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -826,9 +826,11 @@ filledFrom(const Field3DParallel& f, BOUT_FOR(i, result.getRegion("RGN_NOY")) { result[i] = func(0, i); } for (size_t i = 0; i < result.numberParallelSlices(); ++i) { + result.yup(i).allocate(); BOUT_FOR(d, result.yup(i).getValidRegionWithDefault("RGN_INVALID")) { result.yup(i)[d] = func(i + 1, d); } + result.ydown(i).allocate(); BOUT_FOR(d, result.ydown(i).getValidRegionWithDefault("RGN_INVALID")) { result.ydown(i)[d] = func(-i - 1, d); } From 69dfaadccf130ed1f412fab4ee70451f092cb2e2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 30 Sep 2025 15:12:50 +0200 Subject: [PATCH 37/98] Remove commented out code --- src/field/field3d.cxx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 36e0b6901d..abc8610183 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -951,9 +951,11 @@ void Field3D::_track(const BoutReal& change, std::string operation) { void Field3DParallel::ensureFieldAligned() { if (isFci()) { ASSERT2(hasParallelSlices()); - } // else { - // if (getDirectionY() != YDirectionType::Aligned) { - // *this = toFieldAligned(*this); - // } - // } + if (fieldmesh != nullptr) { + for (int i = 0; i < fieldmesh->ystart; ++i) { + ASSERT2(yup_fields[i].getRegionID().has_value()); + ASSERT2(ydown_fields[i].getRegionID().has_value()); + } + } + } } From 7b9525c3b204c458a0d985b2d7ddf36bfc997d35 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 21 Oct 2025 16:15:42 +0200 Subject: [PATCH 38/98] add asField3DParallel() const --- include/bout/field3d.hxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 8d278099b6..6e2fa31a65 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -521,6 +521,7 @@ public: void disallowCalcParallelSlices() { _allowCalcParallelSlices = false; }; inline Field3DParallel asField3DParallel(); + inline const Field3DParallel asField3DParallel() const; protected: /// Array sizes (from fieldmesh). These are valid only if fieldmesh is not null @@ -791,6 +792,9 @@ private: }; Field3DParallel Field3D::asField3DParallel() { return Field3DParallel(*this); } +const Field3DParallel Field3D::asField3DParallel() const { + return Field3DParallel(*this); +} inline Field3D operator+(const Field2D& lhs, const Field3DParallel& rhs) { return lhs + rhs.asField3D(); From fb6c0165dee4474f47ee9b911c17ce442322e61c Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 23 Oct 2025 14:31:27 +0200 Subject: [PATCH 39/98] Add Field3DParallel::allocate --- include/bout/field3d.hxx | 20 +++++++++++--------- src/field/field3d.cxx | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 6e2fa31a65..821add3b58 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -739,27 +739,28 @@ inline Field3D copy(const Field3D& f) { /// "main" field, Field3DParallel will retain the parallel slices. class Field3DParallel : public Field3D { public: - template + explicit template Field3DParallel(Types... args) : Field3D(std::move(args)...) { ensureFieldAligned(); } // Explicitly needed, as DirectionTypes is sometimes constructed from a // brace enclosed list - Field3DParallel(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, - DirectionTypes directions_in = {YDirectionType::Standard, - ZDirectionType::Standard}, - std::optional regionID = {}) + explicit Field3DParallel(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, + DirectionTypes directions_in = {YDirectionType::Standard, + ZDirectionType::Standard}, + std::optional regionID = {}) : Field3D(localmesh, location_in, directions_in, regionID) { splitParallelSlices(); ensureFieldAligned(); } - Field3DParallel(Array data, Mesh* localmesh, CELL_LOC location = CELL_CENTRE, - DirectionTypes directions_in = {YDirectionType::Standard, - ZDirectionType::Standard}) + explicit Field3DParallel(Array data, Mesh* localmesh, + CELL_LOC location = CELL_CENTRE, + DirectionTypes directions_in = {YDirectionType::Standard, + ZDirectionType::Standard}) : Field3D(std::move(data), localmesh, location, directions_in) { ensureFieldAligned(); } - Field3DParallel(BoutReal, Mesh* mesh = nullptr); + explicit Field3DParallel(BoutReal, Mesh* mesh = nullptr); Field3D& asField3D() { return *this; } const Field3D& asField3D() const { return *this; } @@ -786,6 +787,7 @@ public: return *this; } Field3DParallel& operator=(BoutReal); + Field3DParallel& allocate(); private: void ensureFieldAligned(); diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index abc8610183..c0d514f972 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -959,3 +959,17 @@ void Field3DParallel::ensureFieldAligned() { } } } + +Field3DParallel& Field3DParallel::allocate() { + Field3D::allocate(); + if (isFci()) { + ASSERT2(hasParallelSlices()); + if (fieldmesh != nullptr) { + for (int i = 0; i < fieldmesh->ystart; ++i) { + yup_fields[i].allocate(); + ydown_fields[i].allocate(); + } + } + } + return *this; +} From 033d872d6282f87784064075b4423a68e0f1a535 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 29 Oct 2025 12:00:36 +0100 Subject: [PATCH 40/98] Add implict constructors only for Field2D and Field3D --- include/bout/field3d.hxx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 821add3b58..368bb2297b 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -739,10 +739,12 @@ inline Field3D copy(const Field3D& f) { /// "main" field, Field3DParallel will retain the parallel slices. class Field3DParallel : public Field3D { public: - explicit template - Field3DParallel(Types... args) : Field3D(std::move(args)...) { + template + explicit Field3DParallel(Types... args) : Field3D(std::move(args)...) { ensureFieldAligned(); } + Field3DParallel(const Field3D& f) : Field3D(std::move(f)) { ensureFieldAligned(); } + Field3DParallel(const Field2D& f) : Field3D(std::move(f)) { ensureFieldAligned(); } // Explicitly needed, as DirectionTypes is sometimes constructed from a // brace enclosed list explicit Field3DParallel(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, From 2590f7c9256a60ef95f0af49d78676496c71f949 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 5 Nov 2025 10:56:06 +0100 Subject: [PATCH 41/98] add yup and ydown property --- src/field/gen_fieldops.jinja | 12 ++++++------ src/field/gen_fieldops.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index 46c9fcad68..19d40ab367 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -16,8 +16,8 @@ if ({{lhs.name}}.isFci()) { {{out.name}}.splitParallelSlices(); for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { - {{out.name}}.yup(i) = {{lhs.name}}.yup(i) {{operator}} {{rhs.name}}.yup(i); - {{out.name}}.ydown(i) = {{lhs.name}}.ydown(i) {{operator}} {{rhs.name}}.ydown(i); + {{out.name}}.yup(i) = {{lhs.yup}} {{operator}} {{rhs.yup}}; + {{out.name}}.ydown(i) = {{lhs.ydown}} {{operator}} {{rhs.ydown}}; } } {% endif %} @@ -27,8 +27,8 @@ if ({{lhs.name}}.isFci()) { {{out.name}}.splitParallelSlices(); for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { - {{out.name}}.yup(i) = {{lhs.name}}.yup(i) {{operator}} {{rhs.name}}; - {{out.name}}.ydown(i) = {{lhs.name}}.ydown(i) {{operator}} {{rhs.name}}; + {{out.name}}.yup(i) = {{lhs.yup}} {{operator}} {{rhs.yup}}; + {{out.name}}.ydown(i) = {{lhs.ydown}} {{operator}} {{rhs.ydown}}; } } {% endif %} @@ -38,8 +38,8 @@ if ({{rhs.name}}.isFci()) { {{out.name}}.splitParallelSlices(); for (size_t i{0} ; i < {{rhs.name}}.numberParallelSlices() ; ++i) { - {{out.name}}.yup(i) = {{lhs.name}} {{operator}} {{rhs.name}}.yup(i); - {{out.name}}.ydown(i) = {{lhs.name}} {{operator}} {{rhs.name}}.ydown(i); + {{out.name}}.yup(i) = {{lhs.yup}} {{operator}} {{rhs.yup}}; + {{out.name}}.ydown(i) = {{lhs.ydown}} {{operator}} {{rhs.ydown}}; } } {% endif %} diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 473cf3df1f..df3f223707 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -155,6 +155,24 @@ def base_index(self): else: return "{self.name}[{self.mixed_base_ind_var}]".format(self=self) + @property + def yup(self): + """Returns {{name}}.yup(i) if it is a field with parallel slices. + If it is BoutReal just {{name}}""" + if self.field_type == "BoutReal": + return "{self.name}".format(self=self) + else: + return "{self.name}.yup(i)".format(self=self) + + @property + def ydown(self): + """Returns {{name}}.ydown(i) if it is a field with parallel slices. + If it is BoutReal just {{name}}""" + if self.field_type == "BoutReal": + return "{self.name}".format(self=self) + else: + return "{self.name}.ydown(i)".format(self=self) + def __eq__(self, other): try: return self.field_type == other.field_type From 957f058e569c80db988623b39d256f7f4b04259c Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 5 Nov 2025 10:56:30 +0100 Subject: [PATCH 42/98] simplify template --- src/field/gen_fieldops.jinja | 30 +++++++----------------- src/field/generated_fieldops.cxx | 40 ++++++++++++++++---------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index 19d40ab367..be57c00373 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -12,37 +12,23 @@ {% if lhs.region_type == rhs.region_type == "3D" %} {{out.name}}.setRegion({{lhs.name}}.getMesh()->getCommonRegion({{lhs.name}}.getRegionID(), {{rhs.name}}.getRegionID())); - {% if out == "Field3DParallel" %} - if ({{lhs.name}}.isFci()) { - {{out.name}}.splitParallelSlices(); - for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { - {{out.name}}.yup(i) = {{lhs.yup}} {{operator}} {{rhs.yup}}; - {{out.name}}.ydown(i) = {{lhs.ydown}} {{operator}} {{rhs.ydown}}; - } - } - {% endif %} {% elif lhs.region_type == "3D" %} {{out.name}}.setRegion({{lhs.name}}.getRegionID()); - {% if rhs == "BoutReal" and out == "Field3DParallel" %} - if ({{lhs.name}}.isFci()) { - {{out.name}}.splitParallelSlices(); - for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { - {{out.name}}.yup(i) = {{lhs.yup}} {{operator}} {{rhs.yup}}; - {{out.name}}.ydown(i) = {{lhs.ydown}} {{operator}} {{rhs.ydown}}; - } - } - {% endif %} {% elif rhs.region_type == "3D" %} {{out.name}}.setRegion({{rhs.name}}.getRegionID()); - {% if lhs == "BoutReal" and rhs == "Field3DParallel" %} - if ({{rhs.name}}.isFci()) { + {% endif %} + {% if out == "Field3DParallel" %} + if ({{out.name}}.isFci()) { {{out.name}}.splitParallelSlices(); - for (size_t i{0} ; i < {{rhs.name}}.numberParallelSlices() ; ++i) { + {% if lhs.region_type == "3D" %} + for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { + {% else %} + for (size_t i{0} ; i < {{rhs.name}}.numberParallelSlices() ; ++i) { + {% endif %} {{out.name}}.yup(i) = {{lhs.yup}} {{operator}} {{rhs.yup}}; {{out.name}}.ydown(i) = {{lhs.ydown}} {{operator}} {{rhs.ydown}}; } } - {% endif %} {% endif %} {% endif %} diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 9dad114ec8..9bbf6b06d2 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -1958,7 +1958,7 @@ Field3DParallel operator*(const Field3D& lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); @@ -1983,7 +1983,7 @@ Field3DParallel operator/(const Field3D& lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); @@ -2008,7 +2008,7 @@ Field3DParallel operator+(const Field3D& lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); @@ -2033,7 +2033,7 @@ Field3DParallel operator-(const Field3D& lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); @@ -2058,7 +2058,7 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); @@ -2116,7 +2116,7 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); @@ -2174,7 +2174,7 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); @@ -2232,7 +2232,7 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3D& rhs) { checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); @@ -2290,7 +2290,7 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3DParallel& rhs checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); @@ -2348,7 +2348,7 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3DParallel& rhs checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); @@ -2406,7 +2406,7 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3DParallel& rhs checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); @@ -2464,7 +2464,7 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3DParallel& rhs checkData(rhs); result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); @@ -2521,7 +2521,7 @@ Field3DParallel operator*(const Field3DParallel& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs; @@ -2581,7 +2581,7 @@ Field3DParallel operator/(const Field3DParallel& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs; @@ -2642,7 +2642,7 @@ Field3DParallel operator+(const Field3DParallel& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs; @@ -2702,7 +2702,7 @@ Field3DParallel operator-(const Field3DParallel& lhs, const BoutReal rhs) { checkData(rhs); result.setRegion(lhs.getRegionID()); - if (lhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs; @@ -2762,7 +2762,7 @@ Field3DParallel operator*(const BoutReal lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); - if (rhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs * rhs.yup(i); @@ -2786,7 +2786,7 @@ Field3DParallel operator/(const BoutReal lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); - if (rhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs / rhs.yup(i); @@ -2810,7 +2810,7 @@ Field3DParallel operator+(const BoutReal lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); - if (rhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs + rhs.yup(i); @@ -2834,7 +2834,7 @@ Field3DParallel operator-(const BoutReal lhs, const Field3DParallel& rhs) { checkData(rhs); result.setRegion(rhs.getRegionID()); - if (rhs.isFci()) { + if (result.isFci()) { result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs - rhs.yup(i); From ea71ae205cfcd600cb19ee19992668e6e059dc78 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 5 Nov 2025 11:25:39 +0100 Subject: [PATCH 43/98] Add some asserts for Field3DParallel --- src/field/gen_fieldops.jinja | 2 ++ src/field/gen_fieldops.py | 12 ++++++---- src/field/generated_fieldops.cxx | 40 ++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index be57c00373..adc4655e87 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -19,6 +19,8 @@ {% endif %} {% if out == "Field3DParallel" %} if ({{out.name}}.isFci()) { + {{ lhs.assertParallelSlices }} + {{ rhs.assertParallelSlices }} {{out.name}}.splitParallelSlices(); {% if lhs.region_type == "3D" %} for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index df3f223707..e44b0645fe 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -161,8 +161,7 @@ def yup(self): If it is BoutReal just {{name}}""" if self.field_type == "BoutReal": return "{self.name}".format(self=self) - else: - return "{self.name}.yup(i)".format(self=self) + return "{self.name}.yup(i)".format(self=self) @property def ydown(self): @@ -170,8 +169,13 @@ def ydown(self): If it is BoutReal just {{name}}""" if self.field_type == "BoutReal": return "{self.name}".format(self=self) - else: - return "{self.name}.ydown(i)".format(self=self) + return "{self.name}.ydown(i)".format(self=self) + + @property + def assertParallelSlices(self): + if self.field_type == "BoutReal": + return "" + return f"ASSERT2({self.name}.hasParallelSlices());" def __eq__(self, other): try: diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 9bbf6b06d2..7e6b93662d 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -1960,6 +1960,8 @@ Field3DParallel operator*(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); @@ -1985,6 +1987,8 @@ Field3DParallel operator/(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); @@ -2010,6 +2014,8 @@ Field3DParallel operator+(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); @@ -2035,6 +2041,8 @@ Field3DParallel operator-(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); @@ -2060,6 +2068,8 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); @@ -2118,6 +2128,8 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); @@ -2176,6 +2188,8 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); @@ -2234,6 +2248,8 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); @@ -2292,6 +2308,8 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); @@ -2350,6 +2368,8 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); @@ -2408,6 +2428,8 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); @@ -2466,6 +2488,8 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); @@ -2523,6 +2547,8 @@ Field3DParallel operator*(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs; result.ydown(i) = lhs.ydown(i) * rhs; @@ -2583,6 +2609,8 @@ Field3DParallel operator/(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs; result.ydown(i) = lhs.ydown(i) / rhs; @@ -2644,6 +2672,8 @@ Field3DParallel operator+(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs; result.ydown(i) = lhs.ydown(i) + rhs; @@ -2704,6 +2734,8 @@ Field3DParallel operator-(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs; result.ydown(i) = lhs.ydown(i) - rhs; @@ -2764,6 +2796,8 @@ Field3DParallel operator*(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs * rhs.yup(i); result.ydown(i) = lhs * rhs.ydown(i); @@ -2788,6 +2822,8 @@ Field3DParallel operator/(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs / rhs.yup(i); result.ydown(i) = lhs / rhs.ydown(i); @@ -2812,6 +2848,8 @@ Field3DParallel operator+(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs + rhs.yup(i); result.ydown(i) = lhs + rhs.ydown(i); @@ -2836,6 +2874,8 @@ Field3DParallel operator-(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { result.splitParallelSlices(); + + ASSERT2(rhs.hasParallelSlices()); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs - rhs.yup(i); result.ydown(i) = lhs - rhs.ydown(i); From 46d666350f0a0b5fb23ed93f52068944258d7fb3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 5 Nov 2025 11:51:04 +0100 Subject: [PATCH 44/98] Ensure parallel slices are present --- src/mesh/coordinates.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index b1b850b577..5d3667a609 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1762,7 +1762,8 @@ Coordinates::FieldMetric Coordinates::Laplace_par(const Field2D& f, CELL_LOC out Field3D Coordinates::Laplace_par(const Field3DParallel& f, CELL_LOC outloc) { ASSERT1(location == outloc || outloc == CELL_DEFAULT); - return D2DY2(f, outloc) / g_22 + DDY(J / g_22, outloc) * ::DDY(f, outloc) / J; + return D2DY2(f, outloc) / g_22 + + DDY(J.asField3DParallel() / g_22, outloc) * ::DDY(f, outloc) / J; } // Full Laplacian operator on scalar field From b1068d1cce0d3d34f866f753baa2f876325fe52b Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 5 Nov 2025 11:51:24 +0100 Subject: [PATCH 45/98] Simplify div_par --- src/mesh/coordinates.cxx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 5d3667a609..64c67ade1d 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1570,13 +1570,8 @@ Field3D Coordinates::Div_par(const Field3DParallel& f, CELL_LOC outloc, } // Need to modify yup and ydown fields - Field3D f_B = f / Bxy_floc; - f_B.splitParallelSlices(); - for (int i = 0; i < f.getMesh()->ystart; ++i) { - f_B.yup(i) = f.yup(i) / Bxy_floc.yup(i); - f_B.ydown(i) = f.ydown(i) / Bxy_floc.ydown(i); - } - return Bxy * Grad_par(f_B, outloc, method); + Field3D Jg = coords->J / sqrt(coords->g_22.asField3DParallel()); + return Jg * Grad_par(f / Jg, outloc, method); } ///////////////////////////////////////////////////////// From f15f2045659464dec429cb5a2bb67c0850632073 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 5 Nov 2025 11:51:47 +0100 Subject: [PATCH 46/98] Do not communicate the magnetic field --- tests/MMS/spatial/fci/fci_mms.cxx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/MMS/spatial/fci/fci_mms.cxx b/tests/MMS/spatial/fci/fci_mms.cxx index 7967452f3d..90bf107f3c 100644 --- a/tests/MMS/spatial/fci/fci_mms.cxx +++ b/tests/MMS/spatial/fci/fci_mms.cxx @@ -46,12 +46,6 @@ int main(int argc, char** argv) { Field3D K{FieldFactory::get()->create3D("K", Options::getRoot(), mesh)}; // Communicate to calculate parallel transform. - if constexpr (bout::build::use_metric_3d) { - // Div_par operators require B parallel slices: - // Coordinates::geometry doesn't ensure this (yet) - auto& Bxy = mesh->getCoordinates()->Bxy; - mesh->communicate(Bxy); - } mesh->communicate(input, K); Options dump; From d8b22402e44e38c2336c20aca1e6dd81e381298e Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 5 Nov 2025 11:52:52 +0100 Subject: [PATCH 47/98] Ensure parallel slices are set in FIELD_FUNC --- include/bout/field.hxx | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 6c22a4a258..1e54a4f96b 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -522,20 +522,27 @@ T pow(BoutReal lhs, const T& rhs, const std::string& rgn = "RGN_ALL") { * result for non-finite numbers * */ +class Field3DParallel; #ifdef FIELD_FUNC #error This macro has already been defined #else -#define FIELD_FUNC(name, func) \ - template > \ - inline T name(const T& f, const std::string& rgn = "RGN_ALL") { \ - \ - /* Check if the input is allocated */ \ - checkData(f); \ - /* Define and allocate the output result */ \ - T result{emptyFrom(f)}; \ - BOUT_FOR(d, result.getRegion(rgn)) { result[d] = func(f[d]); } \ - checkData(result); \ - return result; \ +#define FIELD_FUNC(_name, func) \ + template > \ + inline T _name(const T& f, const std::string& rgn = "RGN_ALL") { \ + /* Check if the input is allocated */ \ + checkData(f); \ + /* Define and allocate the output result */ \ + T result{emptyFrom(f)}; \ + BOUT_FOR(d, result.getRegion(rgn)) { result[d] = func(f[d]); } \ + if constexpr (std::is_base_of_v) { \ + for (int i = 0; i < f.numberParallelSlices(); ++i) { \ + result.yup(i) = func(f.yup(i)); \ + result.ydown(i) = func(f.ydown(i)); \ + } \ + } \ + result.name = std::string(#_name "(") + f.name + std::string(")"); \ + checkData(result); \ + return result; \ } #endif From 9a8ebcb838c5ef1d45f881526245bdba9d7e479c Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 20 Jan 2026 09:27:00 +0100 Subject: [PATCH 48/98] ensure parallel fields are allocated --- src/field/field3d.cxx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index c0d514f972..0a1dae3929 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -957,6 +957,12 @@ void Field3DParallel::ensureFieldAligned() { ASSERT2(ydown_fields[i].getRegionID().has_value()); } } + if (isAllocated()) { + for (int i = 0; i < fieldmesh->ystart; ++i) { + ASSERT2(yup_fields[i].isAllocated()); + ASSERT2(ydown_fields[i].isAllocated()); + } + } } } From b6e1b56b805532814e985808ee482656ff5af390 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 21 Jan 2026 13:53:49 +0100 Subject: [PATCH 49/98] Ensure `f3d.asField3DParallel() *= x` updates f3d Add unit test as well as fix --- include/bout/field3d.hxx | 6 +++++- src/field/gen_fieldops.jinja | 4 ++++ src/field/generated_fieldops.cxx | 24 ++++++++++++------------ tests/unit/field/test_field3d.cxx | 20 ++++++++++++++++++++ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 368bb2297b..ac9f94bcec 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -744,6 +744,9 @@ public: ensureFieldAligned(); } Field3DParallel(const Field3D& f) : Field3D(std::move(f)) { ensureFieldAligned(); } + Field3DParallel(const Field3D& f, bool isRef) : Field3D(std::move(f)), isRef(isRef) { + ensureFieldAligned(); + } Field3DParallel(const Field2D& f) : Field3D(std::move(f)) { ensureFieldAligned(); } // Explicitly needed, as DirectionTypes is sometimes constructed from a // brace enclosed list @@ -793,9 +796,10 @@ public: private: void ensureFieldAligned(); + bool isRef{false}; }; -Field3DParallel Field3D::asField3DParallel() { return Field3DParallel(*this); } +Field3DParallel Field3D::asField3DParallel() { return Field3DParallel(*this, true); } const Field3DParallel Field3D::asField3DParallel() const { return Field3DParallel(*this); } diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index adc4655e87..8cf097e8df 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -85,7 +85,11 @@ {{lhs}} &{{lhs}}::operator{{operator}}=(const {{rhs.passByReference}}) { // only if data is unique we update the field // otherwise just call the non-inplace version +{% if lhs == "Field3DParallel" %} + if (data.unique() or isRef) { +{% else %} if (data.unique()) { +{% endif %} {% if lhs != "BoutReal" and rhs != "BoutReal" %} ASSERT1_FIELDS_COMPATIBLE(*this, rhs); {% endif %} diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 7e6b93662d..8b9a3b50e5 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -2088,7 +2088,7 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3D& rhs) { Field3DParallel& Field3DParallel::operator*=(const Field3D& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2148,7 +2148,7 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3D& rhs) { Field3DParallel& Field3DParallel::operator/=(const Field3D& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2208,7 +2208,7 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3D& rhs) { Field3DParallel& Field3DParallel::operator+=(const Field3D& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2268,7 +2268,7 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3D& rhs) { Field3DParallel& Field3DParallel::operator-=(const Field3D& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2328,7 +2328,7 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3DParallel& rhs Field3DParallel& Field3DParallel::operator*=(const Field3DParallel& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2388,7 +2388,7 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3DParallel& rhs Field3DParallel& Field3DParallel::operator/=(const Field3DParallel& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2448,7 +2448,7 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3DParallel& rhs Field3DParallel& Field3DParallel::operator+=(const Field3DParallel& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2508,7 +2508,7 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3DParallel& rhs Field3DParallel& Field3DParallel::operator-=(const Field3DParallel& rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { @@ -2570,7 +2570,7 @@ Field3DParallel operator*(const Field3DParallel& lhs, const BoutReal rhs) { Field3DParallel& Field3DParallel::operator*=(const BoutReal rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { if (this->isFci()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -2633,7 +2633,7 @@ Field3DParallel operator/(const Field3DParallel& lhs, const BoutReal rhs) { Field3DParallel& Field3DParallel::operator/=(const BoutReal rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { if (this->isFci()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -2695,7 +2695,7 @@ Field3DParallel operator+(const Field3DParallel& lhs, const BoutReal rhs) { Field3DParallel& Field3DParallel::operator+=(const BoutReal rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { if (this->isFci()) { for (size_t i{0}; i < yup_fields.size(); ++i) { @@ -2757,7 +2757,7 @@ Field3DParallel operator-(const Field3DParallel& lhs, const BoutReal rhs) { Field3DParallel& Field3DParallel::operator-=(const BoutReal rhs) { // only if data is unique we update the field // otherwise just call the non-inplace version - if (data.unique()) { + if (data.unique() or isRef) { if (this->isFci()) { for (size_t i{0}; i < yup_fields.size(); ++i) { diff --git a/tests/unit/field/test_field3d.cxx b/tests/unit/field/test_field3d.cxx index d80affc3f7..7672ec7dae 100644 --- a/tests/unit/field/test_field3d.cxx +++ b/tests/unit/field/test_field3d.cxx @@ -2450,5 +2450,25 @@ TEST_F(Field3DTest, ZeroFrom) { EXPECT_TRUE(field2.isAllocated()); EXPECT_TRUE(IsFieldEqual(field2, 0.)); } + +TEST_F(Field3DTest, Field3DParallel) { + Field3DParallel field(1.0); + field = 1.0; + + Field3D field2 = field; + + auto& field3 = field.asField3D(); + + field *= 2; + + EXPECT_TRUE(IsFieldEqual(field, 2.0)); + EXPECT_TRUE(IsFieldEqual(field2, 1.0)); + EXPECT_TRUE(IsFieldEqual(field3, 2.0)); + + field3.asField3DParallel() *= 3; + + EXPECT_TRUE(IsFieldEqual(field3, 6.0)); +} + // Restore compiler warnings #pragma GCC diagnostic pop From 82d046a6261243f9778d9fc72fcbb8e7deb78a13 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 22 Jan 2026 13:44:34 +0100 Subject: [PATCH 50/98] ensure also parallel fields are updated in-place --- include/bout/field3d.hxx | 22 +- src/field/gen_fieldops.jinja | 79 +++++- src/field/generated_fieldops.cxx | 470 ++++++++++++++++++++++++++++--- 3 files changed, 531 insertions(+), 40 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index ac9f94bcec..f57a098430 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -475,6 +475,19 @@ public: Field3D& operator/=(BoutReal rhs); ///@} + Field3D& update_multiplication_inplace(const Field3D& rhs); + Field3D& update_division_inplace(const Field3D& rhs); + Field3D& update_addition_inplace(const Field3D& rhs); + Field3D& update_subtraction_inplace(const Field3D& rhs); + Field3D& update_multiplication_inplace(const Field2D& rhs); + Field3D& update_division_inplace(const Field2D& rhs); + Field3D& update_addition_inplace(const Field2D& rhs); + Field3D& update_subtraction_inplace(const Field2D& rhs); + Field3D& update_multiplication_inplace(BoutReal rhs); + Field3D& update_division_inplace(BoutReal rhs); + Field3D& update_addition_inplace(BoutReal rhs); + Field3D& update_subtraction_inplace(BoutReal rhs); + // FieldData virtual functions bool is3D() const override { return true; } @@ -799,7 +812,14 @@ private: bool isRef{false}; }; -Field3DParallel Field3D::asField3DParallel() { return Field3DParallel(*this, true); } +Field3DParallel Field3D::asField3DParallel() { + allocate(); + for (size_t i = 0; i < numberParallelSlices(); ++i) { + yup(i).allocate(); + ydown(i).allocate(); + } + return Field3DParallel(*this, true); +} const Field3DParallel Field3D::asField3DParallel() const { return Field3DParallel(*this); } diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index 8cf097e8df..ba2e4d65c8 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -80,6 +80,72 @@ return {{out.name}}; } +{% if out.field_type == lhs.field_type and lhs == "Field3D" %} +// Provide the C++ operator to update {{lhs}} by {{operator_name}} with {{rhs}} +{{lhs}} &{{lhs}}::update_{{operator_name}}_inplace(const {{rhs.passByReference}}) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + {% if lhs != "BoutReal" and rhs != "BoutReal" %} + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + {% endif %} + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData({{rhs.name}}); + + {% if lhs.region_type == rhs.region_type == "3D" %} + regionID = fieldmesh->getCommonRegion(regionID, {{rhs.name}}.getRegionID()); + {% 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}}); + {% if (operator == "/") and (rhs == "Field2D") %} + const auto tmp = 1.0 / {{rhs.mixed_index}}; + for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ + (*this)[{{mixed_base_ind}} + {{jz_var}}] *= tmp; + {% else %} + for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ + (*this)[{{mixed_base_ind}} + {{jz_var}}] {{operator}}= {{rhs.index}}; + {% endif %} + } + } + {% elif rhs == "FieldPerp" and (lhs == "Field3D" or lhs == "Field2D")%} + Mesh *localmesh = this->getMesh(); + + {{region_loop}}({{index_var}}, {{rhs.name}}.getRegion({{region_name}})) { + int yind = {{rhs.name}}.getIndex(); + const auto {{mixed_base_ind}} = localmesh->indPerpto3D({{index_var}}, yind); + (*this)[{{base_ind_var}}] {{operator}}= {{rhs.index}}; + } + {% elif (operator == "/") and (lhs == "Field3D" or lhs == "Field2D") and (rhs =="BoutReal") %} + const auto tmp = 1.0 / {{rhs.index}}; + {{region_loop}}({{index_var}}, this->getRegion({{region_name}})) { + (*this)[{{index_var}}] *= tmp; + } + {% else %} + {{region_loop}}({{index_var}}, this->getRegion({{region_name}})) { + (*this)[{{index_var}}] {{operator}}= {{rhs.index}}; + } + {% endif %} + + {% if lhs.region_type == "3D" %} + track(rhs, "operator{{operator}}="); + {% endif %} +#if BOUT_USE_TRACK + name = fmt::format("{:s} {{operator}}= {:s}", this->name, {{'"BR"' if rhs == "BoutReal" else rhs.name + ".name"}}); +#endif + + checkData(*this); + + return *this; +} +{% endif %} + + {% if out.field_type == lhs.field_type %} // Provide the C++ operator to update {{lhs}} by {{operator_name}} with {{rhs}} {{lhs}} &{{lhs}}::operator{{operator}}=(const {{rhs.passByReference}}) { @@ -101,9 +167,16 @@ {% endif %} {% if lhs == "Field3DParallel" and (rhs.region_type == "3D" or rhs == "BoutReal") %} if (this->isFci()) { - for (size_t i{0} ; i < yup_fields.size() ; ++i) { - yup(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.yup(i){% endif %}; - ydown(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.ydown(i){% endif %}; + if (isRef) { + for (size_t i{0} ; i < yup_fields.size() ; ++i) { + yup(i).update_{{operator_name}}_inplace({{rhs.name}}{% if rhs == "Field3D" %}.yup(i){% endif %}); + ydown(i).update_{{operator_name}}_inplace({{rhs.name}}{% if rhs == "Field3D" %}.ydown(i){% endif %}); + } + } else { + for (size_t i{0} ; i < yup_fields.size() ; ++i) { + yup(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.yup(i){% endif %}; + ydown(i) {{operator}}= {{rhs.name}}{% if rhs == "Field3D" %}.ydown(i){% endif %}; + } } } else { clearParallelSlices(); diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 8b9a3b50e5..98cb1a0286 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -24,6 +24,32 @@ Field3D operator*(const Field3D& lhs, const Field3D& rhs) { return result; } +// Provide the C++ operator to update Field3D by multiplication with Field3D +Field3D& Field3D::update_multiplication_inplace(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs[index]; } + + track(rhs, "operator*="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by multiplication with Field3D Field3D& Field3D::operator*=(const Field3D& rhs) { // only if data is unique we update the field @@ -70,6 +96,32 @@ Field3D operator/(const Field3D& lhs, const Field3D& rhs) { return result; } +// Provide the C++ operator to update Field3D by division with Field3D +Field3D& Field3D::update_division_inplace(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs[index]; } + + track(rhs, "operator/="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by division with Field3D Field3D& Field3D::operator/=(const Field3D& rhs) { // only if data is unique we update the field @@ -116,6 +168,32 @@ Field3D operator+(const Field3D& lhs, const Field3D& rhs) { return result; } +// Provide the C++ operator to update Field3D by addition with Field3D +Field3D& Field3D::update_addition_inplace(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs[index]; } + + track(rhs, "operator+="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by addition with Field3D Field3D& Field3D::operator+=(const Field3D& rhs) { // only if data is unique we update the field @@ -162,6 +240,32 @@ Field3D operator-(const Field3D& lhs, const Field3D& rhs) { return result; } +// Provide the C++ operator to update Field3D by subtraction with Field3D +Field3D& Field3D::update_subtraction_inplace(const Field3D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + regionID = fieldmesh->getCommonRegion(regionID, rhs.getRegionID()); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs[index]; } + + track(rhs, "operator-="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by subtraction with Field3D Field3D& Field3D::operator-=(const Field3D& rhs) { // only if data is unique we update the field @@ -213,6 +317,35 @@ Field3D operator*(const Field3D& lhs, const Field2D& rhs) { return result; } +// Provide the C++ operator to update Field3D by multiplication with Field2D +Field3D& Field3D::update_multiplication_inplace(const Field2D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { + const auto base_ind = fieldmesh->ind2Dto3D(index); + for (int jz = 0; jz < fieldmesh->LocalNz; ++jz) { + (*this)[base_ind + jz] *= rhs[index]; + } + } + + track(rhs, "operator*="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by multiplication with Field2D Field3D& Field3D::operator*=(const Field2D& rhs) { // only if data is unique we update the field @@ -268,6 +401,36 @@ Field3D operator/(const Field3D& lhs, const Field2D& rhs) { return result; } +// Provide the C++ operator to update Field3D by division with Field2D +Field3D& Field3D::update_division_inplace(const Field2D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { + const auto base_ind = fieldmesh->ind2Dto3D(index); + const auto tmp = 1.0 / rhs[index]; + for (int jz = 0; jz < fieldmesh->LocalNz; ++jz) { + (*this)[base_ind + jz] *= tmp; + } + } + + track(rhs, "operator/="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by division with Field2D Field3D& Field3D::operator/=(const Field2D& rhs) { // only if data is unique we update the field @@ -323,6 +486,35 @@ Field3D operator+(const Field3D& lhs, const Field2D& rhs) { return result; } +// Provide the C++ operator to update Field3D by addition with Field2D +Field3D& Field3D::update_addition_inplace(const Field2D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { + const auto base_ind = fieldmesh->ind2Dto3D(index); + for (int jz = 0; jz < fieldmesh->LocalNz; ++jz) { + (*this)[base_ind + jz] += rhs[index]; + } + } + + track(rhs, "operator+="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by addition with Field2D Field3D& Field3D::operator+=(const Field2D& rhs) { // only if data is unique we update the field @@ -377,6 +569,35 @@ Field3D operator-(const Field3D& lhs, const Field2D& rhs) { return result; } +// Provide the C++ operator to update Field3D by subtraction with Field2D +Field3D& Field3D::update_subtraction_inplace(const Field2D& rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + ASSERT1_FIELDS_COMPATIBLE(*this, rhs); + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, rhs.getRegion("RGN_ALL")) { + const auto base_ind = fieldmesh->ind2Dto3D(index); + for (int jz = 0; jz < fieldmesh->LocalNz; ++jz) { + (*this)[base_ind + jz] -= rhs[index]; + } + } + + track(rhs, "operator-="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, rhs.name); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by subtraction with Field2D Field3D& Field3D::operator-=(const Field2D& rhs) { // only if data is unique we update the field @@ -505,6 +726,29 @@ Field3D operator*(const Field3D& lhs, const BoutReal rhs) { return result; } +// Provide the C++ operator to update Field3D by multiplication with BoutReal +Field3D& Field3D::update_multiplication_inplace(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs; } + + track(rhs, "operator*="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} *= {:s}", this->name, "BR"); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by multiplication with BoutReal Field3D& Field3D::operator*=(const BoutReal rhs) { // only if data is unique we update the field @@ -548,6 +792,30 @@ Field3D operator/(const Field3D& lhs, const BoutReal rhs) { return result; } +// Provide the C++ operator to update Field3D by division with BoutReal +Field3D& Field3D::update_division_inplace(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + const auto tmp = 1.0 / rhs; + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= tmp; } + + track(rhs, "operator/="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} /= {:s}", this->name, "BR"); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by division with BoutReal Field3D& Field3D::operator/=(const BoutReal rhs) { // only if data is unique we update the field @@ -591,6 +859,29 @@ Field3D operator+(const Field3D& lhs, const BoutReal rhs) { return result; } +// Provide the C++ operator to update Field3D by addition with BoutReal +Field3D& Field3D::update_addition_inplace(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs; } + + track(rhs, "operator+="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} += {:s}", this->name, "BR"); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by addition with BoutReal Field3D& Field3D::operator+=(const BoutReal rhs) { // only if data is unique we update the field @@ -633,6 +924,29 @@ Field3D operator-(const Field3D& lhs, const BoutReal rhs) { return result; } +// Provide the C++ operator to update Field3D by subtraction with BoutReal +Field3D& Field3D::update_subtraction_inplace(const BoutReal rhs) { + // only if data is unique we update the field + // otherwise just call the non-inplace version + + // Delete existing parallel slices. We don't update parallel slices, so any + // that currently exist will be incorrect. + clearParallelSlices(); + checkData(*this); + checkData(rhs); + + BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs; } + + track(rhs, "operator-="); +#if BOUT_USE_TRACK + name = fmt::format("{:s} -= {:s}", this->name, "BR"); +#endif + + checkData(*this); + + return *this; +} + // Provide the C++ operator to update Field3D by subtraction with BoutReal Field3D& Field3D::operator-=(const BoutReal rhs) { // only if data is unique we update the field @@ -2092,9 +2406,16 @@ Field3DParallel& Field3DParallel::operator*=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) *= rhs.yup(i); - ydown(i) *= rhs.ydown(i); + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_multiplication_inplace(rhs.yup(i)); + ydown(i).update_multiplication_inplace(rhs.ydown(i)); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs.yup(i); + ydown(i) *= rhs.ydown(i); + } } } else { clearParallelSlices(); @@ -2152,9 +2473,16 @@ Field3DParallel& Field3DParallel::operator/=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) /= rhs.yup(i); - ydown(i) /= rhs.ydown(i); + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_division_inplace(rhs.yup(i)); + ydown(i).update_division_inplace(rhs.ydown(i)); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs.yup(i); + ydown(i) /= rhs.ydown(i); + } } } else { clearParallelSlices(); @@ -2212,9 +2540,16 @@ Field3DParallel& Field3DParallel::operator+=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) += rhs.yup(i); - ydown(i) += rhs.ydown(i); + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_addition_inplace(rhs.yup(i)); + ydown(i).update_addition_inplace(rhs.ydown(i)); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs.yup(i); + ydown(i) += rhs.ydown(i); + } } } else { clearParallelSlices(); @@ -2272,9 +2607,16 @@ Field3DParallel& Field3DParallel::operator-=(const Field3D& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) -= rhs.yup(i); - ydown(i) -= rhs.ydown(i); + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_subtraction_inplace(rhs.yup(i)); + ydown(i).update_subtraction_inplace(rhs.ydown(i)); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs.yup(i); + ydown(i) -= rhs.ydown(i); + } } } else { clearParallelSlices(); @@ -2332,9 +2674,16 @@ Field3DParallel& Field3DParallel::operator*=(const Field3DParallel& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) *= rhs; - ydown(i) *= rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_multiplication_inplace(rhs); + ydown(i).update_multiplication_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs; + ydown(i) *= rhs; + } } } else { clearParallelSlices(); @@ -2392,9 +2741,16 @@ Field3DParallel& Field3DParallel::operator/=(const Field3DParallel& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) /= rhs; - ydown(i) /= rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_division_inplace(rhs); + ydown(i).update_division_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs; + ydown(i) /= rhs; + } } } else { clearParallelSlices(); @@ -2452,9 +2808,16 @@ Field3DParallel& Field3DParallel::operator+=(const Field3DParallel& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) += rhs; - ydown(i) += rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_addition_inplace(rhs); + ydown(i).update_addition_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs; + ydown(i) += rhs; + } } } else { clearParallelSlices(); @@ -2512,9 +2875,16 @@ Field3DParallel& Field3DParallel::operator-=(const Field3DParallel& rhs) { ASSERT1_FIELDS_COMPATIBLE(*this, rhs); if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) -= rhs; - ydown(i) -= rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_subtraction_inplace(rhs); + ydown(i).update_subtraction_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs; + ydown(i) -= rhs; + } } } else { clearParallelSlices(); @@ -2573,9 +2943,16 @@ Field3DParallel& Field3DParallel::operator*=(const BoutReal rhs) { if (data.unique() or isRef) { if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) *= rhs; - ydown(i) *= rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_multiplication_inplace(rhs); + ydown(i).update_multiplication_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) *= rhs; + ydown(i) *= rhs; + } } } else { clearParallelSlices(); @@ -2636,9 +3013,16 @@ Field3DParallel& Field3DParallel::operator/=(const BoutReal rhs) { if (data.unique() or isRef) { if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) /= rhs; - ydown(i) /= rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_division_inplace(rhs); + ydown(i).update_division_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) /= rhs; + ydown(i) /= rhs; + } } } else { clearParallelSlices(); @@ -2698,9 +3082,16 @@ Field3DParallel& Field3DParallel::operator+=(const BoutReal rhs) { if (data.unique() or isRef) { if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) += rhs; - ydown(i) += rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_addition_inplace(rhs); + ydown(i).update_addition_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) += rhs; + ydown(i) += rhs; + } } } else { clearParallelSlices(); @@ -2760,9 +3151,16 @@ Field3DParallel& Field3DParallel::operator-=(const BoutReal rhs) { if (data.unique() or isRef) { if (this->isFci()) { - for (size_t i{0}; i < yup_fields.size(); ++i) { - yup(i) -= rhs; - ydown(i) -= rhs; + if (isRef) { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i).update_subtraction_inplace(rhs); + ydown(i).update_subtraction_inplace(rhs); + } + } else { + for (size_t i{0}; i < yup_fields.size(); ++i) { + yup(i) -= rhs; + ydown(i) -= rhs; + } } } else { clearParallelSlices(); From 00048e0ccc1988fe30e03784ca9ad8c66d6d1e4e Mon Sep 17 00:00:00 2001 From: David Bold Date: Mon, 2 Feb 2026 12:06:32 +0100 Subject: [PATCH 51/98] Add const version --- include/bout/field2d.hxx | 1 + 1 file changed, 1 insertion(+) diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index a28c8bfa8f..3549df4ef9 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -274,6 +274,7 @@ public: int size() const override { return nx * ny; } Field2D& asField3DParallel() { return *this; } + const Field2D& asField3DParallel() const { return *this; } private: /// Internal data array. Handles allocation/freeing of memory From 2dd233593e64f91731b99d062a2f155ff8592271 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Feb 2026 12:36:34 +0100 Subject: [PATCH 52/98] Only compute parallel slices in filledFrom for FCI --- include/bout/field3d.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index f57a098430..af33ce0f5d 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -854,7 +854,7 @@ inline Field3DParallel filledFrom(const Field3DParallel& f, std::function func) { auto result{emptyFrom(f)}; - if (f.hasParallelSlices()) { + if (f.isFci()) { BOUT_FOR(i, result.getRegion("RGN_NOY")) { result[i] = func(0, i); } for (size_t i = 0; i < result.numberParallelSlices(); ++i) { From 4f2f60a04a5efa35846146c68ac2d3430112581e Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 13 Feb 2026 14:42:08 +0100 Subject: [PATCH 53/98] floor only if allocated --- include/bout/field.hxx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index 1e54a4f96b..cac0bdb150 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -684,7 +684,36 @@ inline T floor(const T& var, BoutReal f, const std::string& rgn = "RGN_ALL") { result[d] = f; } } - + if constexpr (std::is_same_v) { + if (var.hasParallelSlices()) { + for (size_t i = 0; i < result.numberParallelSlices(); ++i) { + if (result.yup(i).isAllocated()) { + BOUT_FOR(d, result.yup(i).getRegion(rgn)) { + if (result.yup(i)[d] < f) { + result.yup(i)[d] = f; + } + } + } else { + if (result.isFci()) { + throw BoutException("Expected parallel slice to be allocated"); + } + } + if (result.ydown(i).isAllocated()) { + BOUT_FOR(d, result.ydown(i).getRegion(rgn)) { + if (result.ydown(i)[d] < f) { + result.ydown(i)[d] = f; + } + } + } else { + if (result.isFci()) { + throw BoutException("Expected parallel slice to be allocated"); + } + } + } + } + } else { + result.clearParallelSlices(); + } return result; } From 1adc1fc2fefbdfe31dc9df1bb99d89a47398038e Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 13 Feb 2026 14:42:23 +0100 Subject: [PATCH 54/98] Use allocate only to make sure data is unique --- include/bout/field3d.hxx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index af33ce0f5d..c0bfba5d7b 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -813,10 +813,16 @@ private: }; Field3DParallel Field3D::asField3DParallel() { - allocate(); - for (size_t i = 0; i < numberParallelSlices(); ++i) { - yup(i).allocate(); - ydown(i).allocate(); + if (isAllocated()) { + allocate(); + for (size_t i = 0; i < numberParallelSlices(); ++i) { + if (yup(i).isAllocated()) { + yup(i).allocate(); + } + if (ydown(i).isAllocated()) { + ydown(i).allocate(); + } + } } return Field3DParallel(*this, true); } From a46781ae2c3446d5ba830baa868e6a829dabc98d Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 11:58:05 +0100 Subject: [PATCH 55/98] Do not use numberParallelSlices() --- include/bout/field3d.hxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index c0bfba5d7b..ea1a1ce24b 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -739,7 +739,8 @@ std::ostream& operator<<(std::ostream& out, const Field3D& value); inline Field3D copy(const Field3D& f) { Field3D result{f}; result.allocate(); - for (size_t i = 0; i < result.numberParallelSlices(); ++i) { + const size_t numSlices = result.hasParallelSlices() ? result.fieldmesh->ystart : 0; + for (size_t i = 0; i < numSlices; ++i) { result.yup(i).allocate(); result.ydown(i).allocate(); } From 80d7d34213b37ad79cc58245034d492ccd6ffb12 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 12:02:10 +0100 Subject: [PATCH 56/98] Add regionID to f3d ctor --- include/bout/field3d.hxx | 3 ++- src/field/field3d.cxx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index ea1a1ce24b..f542a5462f 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -175,7 +175,8 @@ public: */ Field3D(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, DirectionTypes directions_in = {YDirectionType::Standard, - ZDirectionType::Standard}); + ZDirectionType::Standard}, + std::optional regionID = {}); /*! * Copy constructor diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index 0a1dae3929..1b10546df3 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -52,8 +52,9 @@ #include "fmt/format.h" /// Constructor -Field3D::Field3D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in) - : Field(localmesh, location_in, directions_in) { +Field3D::Field3D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in, + std::optional regionID) + : Field(localmesh, location_in, directions_in), regionID(regionID) { #if BOUT_USE_TRACK name = ""; #endif From 40e1c2e5c2201e45b9a850541907216bc11cb6c5 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 12:08:29 +0100 Subject: [PATCH 57/98] add regionID to f2d ctor --- include/bout/field2d.hxx | 3 ++- src/field/field2d.cxx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/include/bout/field2d.hxx b/include/bout/field2d.hxx index 3549df4ef9..9996d1dbda 100644 --- a/include/bout/field2d.hxx +++ b/include/bout/field2d.hxx @@ -68,7 +68,8 @@ public: */ Field2D(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, DirectionTypes directions_in = {YDirectionType::Standard, - ZDirectionType::Average}); + ZDirectionType::Average}, + std::optional regionID = {}); /*! * Copy constructor. After this both fields diff --git a/src/field/field2d.cxx b/src/field/field2d.cxx index a046078585..b4bd83ed71 100644 --- a/src/field/field2d.cxx +++ b/src/field/field2d.cxx @@ -39,7 +39,8 @@ #include -Field2D::Field2D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in) +Field2D::Field2D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in, + [[maybe_unused]] std::optional regionID) : Field(localmesh, location_in, directions_in) { if (fieldmesh) { From ee6b03e6184d7fa602dea5d26a98757430b0b74c Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 12:08:48 +0100 Subject: [PATCH 58/98] Add numberParallelSlices to all fields --- include/bout/field.hxx | 1 + include/bout/field3d.hxx | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/include/bout/field.hxx b/include/bout/field.hxx index cac0bdb150..b6a2c8ec7d 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -137,6 +137,7 @@ public: virtual void calcParallelSlices() {} virtual void splitParallelSlices() {} virtual void clearParallelSlices() {} + virtual size_t numberParallelSlices() const { return 0; } private: /// Labels for the type of coordinate system this field is defined over diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index f542a5462f..8644fd61a2 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -267,6 +267,13 @@ public: #endif } + size_t numberParallelSlices() const override { +#if CHECK > 0 + hasParallelSlices(); +#endif + return yup_fields.size(); + } + /// Check if this field has yup and ydown fields /// Return reference to yup field Field3D& yup(size_t index = 0) { From 9501f7a6b1368c8a77e0df1dac07f275a78a509e Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 12:10:46 +0100 Subject: [PATCH 59/98] use numberParallelSlices --- include/bout/field3d.hxx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 8644fd61a2..86fe03186c 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -747,8 +747,7 @@ std::ostream& operator<<(std::ostream& out, const Field3D& value); inline Field3D copy(const Field3D& f) { Field3D result{f}; result.allocate(); - const size_t numSlices = result.hasParallelSlices() ? result.fieldmesh->ystart : 0; - for (size_t i = 0; i < numSlices; ++i) { + for (size_t i = 0; i < result.numberParallelSlices(); ++i) { result.yup(i).allocate(); result.ydown(i).allocate(); } From ddb9132bc92dceb778c9fa4f6415b3a126a0a3df Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 12:14:55 +0100 Subject: [PATCH 60/98] Fix usage of coords --- src/mesh/coordinates.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 64c67ade1d..5b8a26cb69 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1570,7 +1570,7 @@ Field3D Coordinates::Div_par(const Field3DParallel& f, CELL_LOC outloc, } // Need to modify yup and ydown fields - Field3D Jg = coords->J / sqrt(coords->g_22.asField3DParallel()); + Field3D Jg = J / sqrt(g_22.asField3DParallel()); return Jg * Grad_par(f / Jg, outloc, method); } From b5a2c3b7fb47e52032023fe402769fa6574b94d8 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 12 Mar 2026 09:21:00 +0100 Subject: [PATCH 61/98] FCI test only works with 3D metrics --- tests/MMS/spatial/fci/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/MMS/spatial/fci/CMakeLists.txt b/tests/MMS/spatial/fci/CMakeLists.txt index dcdb183fdd..48721a8199 100644 --- a/tests/MMS/spatial/fci/CMakeLists.txt +++ b/tests/MMS/spatial/fci/CMakeLists.txt @@ -3,5 +3,6 @@ bout_add_mms_test( SOURCES fci_mms.cxx USE_RUNTEST USE_DATA_BOUT_INP REQUIRES zoidberg_FOUND + REQUIRES BOUT_USE_METRIC_3D PROCESSORS 2 ) From 2e7dccfa8ef3bccdb9d433c4837171171a1fc6b3 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 10 Oct 2024 12:45:35 +0200 Subject: [PATCH 62/98] Add code to load parallel metric slices --- include/bout/paralleltransform.hxx | 4 +++ src/mesh/coordinates.cxx | 5 ++++ src/mesh/parallel/fci.cxx | 41 ++++++++++++++++-------------- src/mesh/parallel/fci.hxx | 2 ++ 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/include/bout/paralleltransform.hxx b/include/bout/paralleltransform.hxx index 49dea67743..8c151b61e1 100644 --- a/include/bout/paralleltransform.hxx +++ b/include/bout/paralleltransform.hxx @@ -90,6 +90,10 @@ public: /// require a twist-shift at branch cuts on closed field lines? virtual bool requiresTwistShift(bool twist_shift_enabled, YDirectionType ytype) = 0; + /// Can be implemented to load parallel metrics + /// Needed by FCI + virtual void loadParallelMetrics(MAYBE_UNUSED(Coordinates* coords)) {} + protected: /// This method should be called in the constructor to check that if the grid /// has a 'parallel_transform' variable, it has the correct value diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index d2634d3f88..ce7406a518 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -591,6 +591,9 @@ Coordinates::Coordinates(Mesh* mesh, Options* options) // IntShiftTorsion will not be used, but set to zero to avoid uninitialized field IntShiftTorsion = 0.; } + + // Allow transform to fix things up + transform->loadParallelMetrics(this); } Coordinates::Coordinates(Mesh* mesh, Options* options, const CELL_LOC loc, @@ -879,6 +882,8 @@ Coordinates::Coordinates(Mesh* mesh, Options* options, const CELL_LOC loc, true, true, false, transform.get()); } } + // Allow transform to fix things up + transform->loadParallelMetrics(this); } void Coordinates::outputVars(Options& output_options) { diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 152ffaed2d..9d35b3af8e 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -64,6 +64,18 @@ using namespace std::string_view_literals; +namespace { +// Get a unique name for a field based on the sign/magnitude of the offset +std::string parallel_slice_field_name(std::string field, int offset) { + const std::string direction = (offset > 0) ? "forward" : "backward"; + // We only have a suffix for parallel slices beyond the first + // This is for backwards compatibility + const std::string slice_suffix = + (std::abs(offset) > 1) ? "_" + std::to_string(std::abs(offset)) : ""; + return direction + "_" + field + slice_suffix; +} +} // namespace + FCIMap::FCIMap(Mesh& mesh, [[maybe_unused]] const Coordinates::FieldMetric& dy, Options& options, int offset, const std::shared_ptr& inner_boundary, @@ -102,38 +114,29 @@ FCIMap::FCIMap(Mesh& mesh, [[maybe_unused]] const Coordinates::FieldMetric& dy, map_mesh->get(R, "R", 0.0, false); map_mesh->get(Z, "Z", 0.0, false); - // Get a unique name for a field based on the sign/magnitude of the offset - const auto parallel_slice_field_name = [&](std::string_view field) -> std::string { - const auto direction = (offset_ > 0) ? "forward"sv : "backward"sv; - // We only have a suffix for parallel slices beyond the first - // This is for backwards compatibility - if (std::abs(offset_) == 1) { - return fmt::format("{}_{}", direction, field); - } - return fmt::format("{}_{}_{}", direction, field, std::abs(offset_)); - }; - // If we can't read in any of these fields, things will silently not // work, so best throw - if (map_mesh->get(xt_prime, parallel_slice_field_name("xt_prime"), 0.0, false) != 0) { + if (map_mesh->get(xt_prime, parallel_slice_field_name("xt_prime", offset), 0.0, false) + != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("xt_prime")); + parallel_slice_field_name("xt_prime", offset)); } - if (map_mesh->get(zt_prime, parallel_slice_field_name("zt_prime"), 0.0, false) != 0) { + if (map_mesh->get(zt_prime, parallel_slice_field_name("zt_prime", offset), 0.0, false) + != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("zt_prime")); + parallel_slice_field_name("zt_prime", offset)); } - if (map_mesh->get(R_prime, parallel_slice_field_name("R"), 0.0, false) != 0) { + if (map_mesh->get(R_prime, parallel_slice_field_name("R", offset), 0.0, false) != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("R")); + parallel_slice_field_name("R", offset)); } - if (map_mesh->get(Z_prime, parallel_slice_field_name("Z"), 0.0, false) != 0) { + if (map_mesh->get(Z_prime, parallel_slice_field_name("Z", offset), 0.0, false) != 0) { throw BoutException("Could not read {:s} from grid file!\n" " Either add it to the grid file, or reduce MYG", - parallel_slice_field_name("Z")); + parallel_slice_field_name("Z", offset)); } // Cell corners diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index 65529a4c4e..71ac35a192 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -125,6 +125,8 @@ public: return false; } + void loadParallelMetrics(Coordinates* coords) override; + protected: void checkInputGrid() override; From 6e21bb8c4f6bc66de826d6390e2bf7f0a811eb87 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 11:37:50 +0100 Subject: [PATCH 63/98] Prefer [[maybe_unused]] --- include/bout/paralleltransform.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/paralleltransform.hxx b/include/bout/paralleltransform.hxx index 8c151b61e1..0b425de209 100644 --- a/include/bout/paralleltransform.hxx +++ b/include/bout/paralleltransform.hxx @@ -92,7 +92,7 @@ public: /// Can be implemented to load parallel metrics /// Needed by FCI - virtual void loadParallelMetrics(MAYBE_UNUSED(Coordinates* coords)) {} + virtual void loadParallelMetrics([[maybe_unused]] Coordinates* coords) {} protected: /// This method should be called in the constructor to check that if the grid From e5eb01af8ee8a4a8fc66f4914eb964de70ca588c Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 10 Oct 2024 12:45:35 +0200 Subject: [PATCH 64/98] Add code to load parallel metric slices --- src/mesh/parallel/fci.cxx | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 9d35b3af8e..badcdf6906 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -73,7 +73,40 @@ std::string parallel_slice_field_name(std::string field, int offset) { const std::string slice_suffix = (std::abs(offset) > 1) ? "_" + std::to_string(std::abs(offset)) : ""; return direction + "_" + field + slice_suffix; +}; + +void load_parallel_metric_component(std::string name, Field3D& component, int offset) { + Mesh* mesh = component.getMesh(); + Field3D tmp{mesh}; + const auto pname = parallel_slice_field_name(name, offset); + if (mesh->get(tmp, pname, 0.0, false) != 0) { + throw BoutException("Could not read {:s} from grid file!\n" + " Fix it up with `zoidberg-update-parallel-metrics `", + pname); + } + if (!component.hasParallelSlices()) { + component.splitParallelSlices(); + component.allowCalcParallelSlices = false; + } + auto& pcom = component.ynext(offset); + pcom.allocate(); + BOUT_FOR(i, component.getRegion("RGN_NOBNDRY")) { pcom[i.yp(offset)] = tmp[i]; } +} + +void load_parallel_metric_components(Coordinates* coords, int offset) { +#define LOAD_PAR(var) load_parallel_metric_component(#var, coords->var, offset) + LOAD_PAR(g11); + LOAD_PAR(g22); + LOAD_PAR(g33); + LOAD_PAR(g13); + LOAD_PAR(g_11); + LOAD_PAR(g_22); + LOAD_PAR(g_33); + LOAD_PAR(g_13); + LOAD_PAR(J); +#undef LOAD_PAR } + } // namespace FCIMap::FCIMap(Mesh& mesh, [[maybe_unused]] const Coordinates::FieldMetric& dy, @@ -418,3 +451,10 @@ void FCITransform::outputVars(Options& output_options) { output_options["R"].force(R, "FCI"); output_options["Z"].force(Z, "FCI"); } + +void FCITransform::loadParallelMetrics(Coordinates* coords) { + for (int i = 1; i <= mesh.ystart; ++i) { + load_parallel_metric_components(coords, -i); + load_parallel_metric_components(coords, i); + } +} From 763ce7809483f6467a5626815c11cb5d3c863cda Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Mar 2026 14:14:17 +0100 Subject: [PATCH 65/98] Update to new interface --- src/mesh/parallel/fci.cxx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index badcdf6906..78e101ff66 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -75,6 +75,7 @@ std::string parallel_slice_field_name(std::string field, int offset) { return direction + "_" + field + slice_suffix; }; +#if BOUT_USE_METRIC_3D void load_parallel_metric_component(std::string name, Field3D& component, int offset) { Mesh* mesh = component.getMesh(); Field3D tmp{mesh}; @@ -86,7 +87,7 @@ void load_parallel_metric_component(std::string name, Field3D& component, int of } if (!component.hasParallelSlices()) { component.splitParallelSlices(); - component.allowCalcParallelSlices = false; + component.disallowCalcParallelSlices(); } auto& pcom = component.ynext(offset); pcom.allocate(); @@ -106,6 +107,7 @@ void load_parallel_metric_components(Coordinates* coords, int offset) { LOAD_PAR(J); #undef LOAD_PAR } +#endif } // namespace @@ -453,8 +455,10 @@ void FCITransform::outputVars(Options& output_options) { } void FCITransform::loadParallelMetrics(Coordinates* coords) { +#if BOUT_USE_METRIC_3D for (int i = 1; i <= mesh.ystart; ++i) { load_parallel_metric_components(coords, -i); load_parallel_metric_components(coords, i); } +#endif } From 02aafdadc01cbbc1995ca84b58a1acfcdb617d36 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 12 Mar 2026 10:47:31 +0100 Subject: [PATCH 66/98] Add headers from clang-tidy --- include/bout/index_derivs_interface.hxx | 2 ++ src/field/field2d.cxx | 2 ++ src/field/generated_fieldops.cxx | 3 +++ 3 files changed, 7 insertions(+) diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index 03cc480323..c63a388948 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -29,6 +29,8 @@ #ifndef __INDEX_DERIVS_INTERFACE_HXX__ #define __INDEX_DERIVS_INTERFACE_HXX__ +#include "bout/boutexception.hxx" +#include "bout/field3d.hxx" #include "bout/traits.hxx" #include #include diff --git a/src/field/field2d.cxx b/src/field/field2d.cxx index b4bd83ed71..2043d91b34 100644 --- a/src/field/field2d.cxx +++ b/src/field/field2d.cxx @@ -38,6 +38,8 @@ #include #include +#include +#include Field2D::Field2D(Mesh* localmesh, CELL_LOC location_in, DirectionTypes directions_in, [[maybe_unused]] std::optional regionID) diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 98cb1a0286..3e10aabefe 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -1,4 +1,7 @@ // This file is autogenerated - see gen_fieldops.py +#include "fmt/format.h" +#include "bout/assert.hxx" +#include "bout/build_defines.hxx" #include #include #include From f98fe9dc6774d121b0bd8a828add6cb84d11a986 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 12 Mar 2026 10:50:29 +0100 Subject: [PATCH 67/98] Remove useless std::move --- include/bout/field3d.hxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 86fe03186c..9321ec5fc4 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -764,11 +764,11 @@ public: explicit Field3DParallel(Types... args) : Field3D(std::move(args)...) { ensureFieldAligned(); } - Field3DParallel(const Field3D& f) : Field3D(std::move(f)) { ensureFieldAligned(); } - Field3DParallel(const Field3D& f, bool isRef) : Field3D(std::move(f)), isRef(isRef) { + Field3DParallel(const Field3D& f) : Field3D(f) { ensureFieldAligned(); } + Field3DParallel(const Field3D& f, bool isRef) : Field3D(f), isRef(isRef) { ensureFieldAligned(); } - Field3DParallel(const Field2D& f) : Field3D(std::move(f)) { ensureFieldAligned(); } + Field3DParallel(const Field2D& f) : Field3D(f) { ensureFieldAligned(); } // Explicitly needed, as DirectionTypes is sometimes constructed from a // brace enclosed list explicit Field3DParallel(Mesh* localmesh = nullptr, CELL_LOC location_in = CELL_CENTRE, From bf6743f25d5e14996689fc9fd0c55b8132380a3c Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 12 Mar 2026 10:51:33 +0100 Subject: [PATCH 68/98] Prefer const ref --- include/bout/field3d.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 9321ec5fc4..c7a83f3c93 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -866,7 +866,7 @@ inline Field3D operator/(const Field3DParallel& lhs, const Field2D& rhs) { inline Field3DParallel filledFrom(const Field3DParallel& f, - std::function func) { + const std::function& func) { auto result{emptyFrom(f)}; if (f.isFci()) { BOUT_FOR(i, result.getRegion("RGN_NOY")) { result[i] = func(0, i); } From 85fb0fd4047e2850f4647e117041f56645c4d092 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 12 Mar 2026 10:54:14 +0100 Subject: [PATCH 69/98] prefer const --- src/mesh/coordinates.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 5b8a26cb69..699e1cd440 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1570,7 +1570,7 @@ Field3D Coordinates::Div_par(const Field3DParallel& f, CELL_LOC outloc, } // Need to modify yup and ydown fields - Field3D Jg = J / sqrt(g_22.asField3DParallel()); + const Field3D Jg = J / sqrt(g_22.asField3DParallel()); return Jg * Grad_par(f / Jg, outloc, method); } From a9c5059cdc0e859ce49a290a5e3cf530a4aff5db Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 12 Mar 2026 11:22:36 +0100 Subject: [PATCH 70/98] fix DDY with parallel slices with FA --- include/bout/index_derivs_interface.hxx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/include/bout/index_derivs_interface.hxx b/include/bout/index_derivs_interface.hxx index c63a388948..cbb1e5ac53 100644 --- a/include/bout/index_derivs_interface.hxx +++ b/include/bout/index_derivs_interface.hxx @@ -206,7 +206,7 @@ template T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", const std::string& region = "RGN_NOBNDRY") { - if (f.isFci()) { + if (f.isFci() or f.hasParallelSlices()) { ASSERT1(f.getDirectionY() == YDirectionType::Standard); if (!f.hasParallelSlices()) { throw BoutException( @@ -215,13 +215,12 @@ T DDY(const T& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "D } return standardDerivative(f, outloc, method, region); - } else { - const bool is_unaligned = (f.getDirectionY() == YDirectionType::Standard); - const T f_aligned = is_unaligned ? toFieldAligned(f, "RGN_NOX") : f; - T result = standardDerivative(f_aligned, outloc, - method, region); - return is_unaligned ? fromFieldAligned(result, region) : result; } + const bool is_unaligned = (f.getDirectionY() == YDirectionType::Standard); + const T f_aligned = is_unaligned ? toFieldAligned(f, "RGN_NOX") : f; + T result = standardDerivative(f_aligned, outloc, + method, region); + return is_unaligned ? fromFieldAligned(result, region) : result; } inline Field3D DDY(const Field3DParallel& f, CELL_LOC outloc = CELL_DEFAULT, const std::string& method = "DEFAULT", From d5272c43e19fb7231b63ae25ade03338ddffdc41 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 12 Mar 2026 12:11:42 +0100 Subject: [PATCH 71/98] Fixes for prek --- include/bout/coordinates.hxx | 10 +++++----- include/bout/field.hxx | 2 +- include/bout/field3d.hxx | 4 ++-- src/field/field3d.cxx | 6 +++--- src/field/gen_fieldops.py | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/include/bout/coordinates.hxx b/include/bout/coordinates.hxx index 8a4bfb5ef6..6fc97a03dd 100644 --- a/include/bout/coordinates.hxx +++ b/include/bout/coordinates.hxx @@ -3,16 +3,16 @@ * * ChangeLog * ========= - * + * * 2014-11-10 Ben Dudson * * Created by separating metric from Mesh * - * + * ************************************************************************** * Copyright 2014-2025 BOUT++ contributors * * Contact: Ben Dudson, dudson2@llnl.gov - * + * * This file is part of BOUT++. * * BOUT++ is free software: you can redistribute it and/or modify @@ -256,10 +256,10 @@ private: class TokamakCoordinates : public Coordinates { public: TokamakCoordinates(Mesh *mesh) : Coordinates(mesh) { - + } private: - + }; */ diff --git a/include/bout/field.hxx b/include/bout/field.hxx index b6a2c8ec7d..f8b07afb82 100644 --- a/include/bout/field.hxx +++ b/include/bout/field.hxx @@ -6,7 +6,7 @@ * Copyright 2010 B.D.Dudson, S.Farley, M.V.Umansky, X.Q.Xu * * Contact: Ben Dudson, bd512@york.ac.uk - * + * * This file is part of BOUT++. * * BOUT++ is free software: you can redistribute it and/or modify diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index c7a83f3c93..438e28c25d 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -2,7 +2,7 @@ * Copyright 2010 B.D.Dudson, S.Farley, M.V.Umansky, X.Q.Xu * * Contact: Ben Dudson, bd512@york.ac.uk - * + * * This file is part of BOUT++. * * BOUT++ is free software: you can redistribute it and/or modify @@ -372,7 +372,7 @@ public: * Direct access to the underlying data array * * If CHECK > 2 then bounds checking is performed - * + * * If CHECK <= 2 then no checks are performed, to * allow inlining and optimisation of inner loops */ diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index e4198cdd2c..975a918f89 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -7,7 +7,7 @@ * Copyright 2010 - 2025 BOUT++ developers * * Contact: Ben Dudson, dudson2@llnl.gov - * + * * This file is part of BOUT++. * * BOUT++ is free software: you can redistribute it and/or modify @@ -244,7 +244,7 @@ const Region& Field3D::getRegion2D(const std::string& region_name) const } /*************************************************************** - * OPERATORS + * OPERATORS ***************************************************************/ /////////////////// ASSIGNMENT //////////////////// @@ -743,7 +743,7 @@ Field3D lowPass(const Field3D& var, int zmax, bool keep_zonal, const std::string return result; } -/* +/* * Use FFT to shift by an angle in the Z direction */ void shiftZ(Field3D& var, int jx, int jy, double zangle) { diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index e44b0645fe..2bcb144c47 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -16,7 +16,6 @@ from builtins import object import argparse -from collections import OrderedDict import contextlib from copy import deepcopy as copy import itertools From 360358eff6eedecc640fe80afb3245f1809d918f Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 17 Mar 2026 10:03:46 +0100 Subject: [PATCH 72/98] CI: install pre-release zoidberg --- .ci_fedora.sh | 1 + .github/workflows/tests.yml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci_fedora.sh b/.ci_fedora.sh index e1dc563c6c..2339ff1b4f 100755 --- a/.ci_fedora.sh +++ b/.ci_fedora.sh @@ -47,6 +47,7 @@ test . != ".$1" && mpi="$1" || mpi=openmpi export OMP_NUM_THREADS=1 cd cd BOUT-dev + python -m pip install git+https://github.com/boutproject/zoidberg@better-metric echo "starting configure" time cmake -S . -B build -DBOUT_USE_PETSC=ON \ -DBOUT_UPDATE_GIT_SUBMODULE=OFF \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 02f018b873..8919f82df1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -166,6 +166,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools python -m pip install -r requirements.txt + python -m pip install git+https://github.com/boutproject/zoidberg@better-metric - name: Cache Zenodo test data uses: actions/cache@v5 @@ -187,7 +188,7 @@ jobs: 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 + 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 }} From 99439569c6c1f3e4572d9fd7fa4103a21a3df99f Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 11:44:20 +0100 Subject: [PATCH 73/98] Add missing headers also to gen_fieldops --- src/field/gen_fieldops.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 2bcb144c47..7cec44855d 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -61,6 +61,9 @@ def smart_open(filename, mode="r"): header = """// This file is autogenerated - see gen_fieldops.py +#include "fmt/format.h" +#include "bout/assert.hxx" +#include "bout/build_defines.hxx" #include #include #include From 01582cd336adf97a7b7bee4218c7d47458e2e3c2 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 11:45:53 +0100 Subject: [PATCH 74/98] Fix whitespace --- src/field/gen_fieldops.jinja | 64 ++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index ba2e4d65c8..06db5d315c 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -19,8 +19,8 @@ {% endif %} {% if out == "Field3DParallel" %} if ({{out.name}}.isFci()) { - {{ lhs.assertParallelSlices }} - {{ rhs.assertParallelSlices }} + {{ lhs.assertParallelSlices }} + {{ rhs.assertParallelSlices }} {{out.name}}.splitParallelSlices(); {% if lhs.region_type == "3D" %} for (size_t i{0} ; i < {{lhs.name}}.numberParallelSlices() ; ++i) { @@ -42,17 +42,17 @@ {% else %} {{region_loop}}({{index_var}}, {{rhs.name}}.getRegion({{region_name}})) { {% endif %} - const auto {{mixed_base_ind}} = localmesh->ind2Dto3D({{index_var}}); - {% if (operator == "/") and (rhs == "Field2D") %} + const auto {{mixed_base_ind}} = localmesh->ind2Dto3D({{index_var}}); + {% if (operator == "/") and (rhs == "Field2D") %} const auto tmp = 1.0 / {{rhs.mixed_index}}; - for (int {{jz_var}} = 0; {{jz_var}} < localmesh->LocalNz; ++{{jz_var}}){ - {{out.mixed_index}} = {{lhs.mixed_index}} * tmp; + for (int {{jz_var}} = 0; {{jz_var}} < localmesh->LocalNz; ++{{jz_var}}){ + {{out.mixed_index}} = {{lhs.mixed_index}} * tmp; {% else %} - for (int {{jz_var}} = 0; {{jz_var}} < localmesh->LocalNz; ++{{jz_var}}){ - {{out.mixed_index}} = {{lhs.mixed_index}} {{operator}} {{rhs.mixed_index}}; + for (int {{jz_var}} = 0; {{jz_var}} < localmesh->LocalNz; ++{{jz_var}}){ + {{out.mixed_index}} = {{lhs.mixed_index}} {{operator}} {{rhs.mixed_index}}; {% endif %} - } - } + } + } {% elif out == "FieldPerp" and (lhs == "Field2D" or lhs == "Field3D" or rhs == "Field2D" or rhs == "Field3D")%} Mesh *localmesh = {{lhs.name if lhs.field_type != "BoutReal" else rhs.name}}.getMesh(); @@ -60,11 +60,11 @@ int yind = {{lhs.name if lhs == "FieldPerp" else rhs.name}}.getIndex(); const auto {{mixed_base_ind}} = localmesh->indPerpto3D({{index_var}}, yind); {% if lhs != "FieldPerp" %} - {{out.index}} = {{lhs.base_index}} {{operator}} {{rhs.index}}; + {{out.index}} = {{lhs.base_index}} {{operator}} {{rhs.index}}; {% else %} - {{out.index}} = {{lhs.index}} {{operator}} {{rhs.base_index}}; + {{out.index}} = {{lhs.index}} {{operator}} {{rhs.base_index}}; {% endif %} - } + } {% elif (operator == "/") and (rhs == "BoutReal") %} const auto tmp = 1.0 / {{rhs.index}}; {{region_loop}}({{index_var}}, {{out.name}}.getValidRegionWithDefault({{region_name}})) { @@ -72,8 +72,8 @@ } {% else %} {{region_loop}}({{index_var}}, {{out.name}}.getValidRegionWithDefault({{region_name}})) { - {{out.index}} = {{lhs.index}} {{operator}} {{rhs.index}}; - } + {{out.index}} = {{lhs.index}} {{operator}} {{rhs.index}}; + } {% endif %} checkData({{out.name}}); @@ -102,17 +102,17 @@ {% if (lhs == "Field3D") and (rhs =="Field2D") %} {{region_loop}}({{index_var}}, {{rhs.name}}.getRegion({{region_name}})) { - const auto {{mixed_base_ind}} = fieldmesh->ind2Dto3D({{index_var}}); - {% if (operator == "/") and (rhs == "Field2D") %} + const auto {{mixed_base_ind}} = fieldmesh->ind2Dto3D({{index_var}}); + {% if (operator == "/") and (rhs == "Field2D") %} const auto tmp = 1.0 / {{rhs.mixed_index}}; - for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ - (*this)[{{mixed_base_ind}} + {{jz_var}}] *= tmp; + for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ + (*this)[{{mixed_base_ind}} + {{jz_var}}] *= tmp; {% else %} for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ - (*this)[{{mixed_base_ind}} + {{jz_var}}] {{operator}}= {{rhs.index}}; + (*this)[{{mixed_base_ind}} + {{jz_var}}] {{operator}}= {{rhs.index}}; {% endif %} - } - } + } + } {% elif rhs == "FieldPerp" and (lhs == "Field3D" or lhs == "Field2D")%} Mesh *localmesh = this->getMesh(); @@ -120,7 +120,7 @@ int yind = {{rhs.name}}.getIndex(); const auto {{mixed_base_ind}} = localmesh->indPerpto3D({{index_var}}, yind); (*this)[{{base_ind_var}}] {{operator}}= {{rhs.index}}; - } + } {% elif (operator == "/") and (lhs == "Field3D" or lhs == "Field2D") and (rhs =="BoutReal") %} const auto tmp = 1.0 / {{rhs.index}}; {{region_loop}}({{index_var}}, this->getRegion({{region_name}})) { @@ -192,17 +192,17 @@ {% if (lhs == "Field3D") and (rhs =="Field2D") %} {{region_loop}}({{index_var}}, {{rhs.name}}.getRegion({{region_name}})) { - const auto {{mixed_base_ind}} = fieldmesh->ind2Dto3D({{index_var}}); - {% if (operator == "/") and (rhs == "Field2D") %} + const auto {{mixed_base_ind}} = fieldmesh->ind2Dto3D({{index_var}}); + {% if (operator == "/") and (rhs == "Field2D") %} const auto tmp = 1.0 / {{rhs.mixed_index}}; - for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ - (*this)[{{mixed_base_ind}} + {{jz_var}}] *= tmp; + for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ + (*this)[{{mixed_base_ind}} + {{jz_var}}] *= tmp; {% else %} for (int {{jz_var}} = 0; {{jz_var}} < fieldmesh->LocalNz; ++{{jz_var}}){ - (*this)[{{mixed_base_ind}} + {{jz_var}}] {{operator}}= {{rhs.index}}; + (*this)[{{mixed_base_ind}} + {{jz_var}}] {{operator}}= {{rhs.index}}; {% endif %} - } - } + } + } {% elif lhs == "FieldPerp" and (rhs == "Field3D" or rhs == "Field2D")%} Mesh *localmesh = this->getMesh(); @@ -210,7 +210,7 @@ int yind = this->getIndex(); const auto {{mixed_base_ind}} = localmesh->indPerpto3D({{index_var}}, yind); (*this)[{{index_var}}] {{operator}}= {{rhs.base_index}}; - } + } {% elif rhs == "FieldPerp" and (lhs == "Field3D" or lhs == "Field2D")%} Mesh *localmesh = this->getMesh(); @@ -218,7 +218,7 @@ int yind = {{rhs.name}}.getIndex(); const auto {{mixed_base_ind}} = localmesh->indPerpto3D({{index_var}}, yind); (*this)[{{base_ind_var}}] {{operator}}= {{rhs.index}}; - } + } {% elif (operator == "/") and (lhs == "Field3D" or lhs == "Field2D") and (rhs =="BoutReal") %} const auto tmp = 1.0 / {{rhs.index}}; {{region_loop}}({{index_var}}, this->getRegion({{region_name}})) { From f5291581b8cff4d20d72a20e0a88bae4f554d5cd Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 11:48:25 +0100 Subject: [PATCH 75/98] Improve asserts for parallel slices --- src/field/gen_fieldops.jinja | 1 + src/field/gen_fieldops.py | 9 +- src/field/generated_fieldops.cxx | 340 +++++++++++++++++++++++++++---- 3 files changed, 305 insertions(+), 45 deletions(-) diff --git a/src/field/gen_fieldops.jinja b/src/field/gen_fieldops.jinja index 06db5d315c..ca171eaf8d 100644 --- a/src/field/gen_fieldops.jinja +++ b/src/field/gen_fieldops.jinja @@ -30,6 +30,7 @@ {{out.name}}.yup(i) = {{lhs.yup}} {{operator}} {{rhs.yup}}; {{out.name}}.ydown(i) = {{lhs.ydown}} {{operator}} {{rhs.ydown}}; } + {{ out.assertParallelSlices }} } {% endif %} {% endif %} diff --git a/src/field/gen_fieldops.py b/src/field/gen_fieldops.py index 7cec44855d..91fd4082f0 100755 --- a/src/field/gen_fieldops.py +++ b/src/field/gen_fieldops.py @@ -177,7 +177,14 @@ def ydown(self): def assertParallelSlices(self): if self.field_type == "BoutReal": return "" - return f"ASSERT2({self.name}.hasParallelSlices());" + + return f""" + ASSERT2({self.name}.hasParallelSlices()); + for (size_t i{{0}} ; i < {self.name}.numberParallelSlices() ; ++i) {{ + ASSERT2({self.ydown}.isAllocated()); + ASSERT2({self.yup}.isAllocated()); + }}\ + """ def __eq__(self, other): try: diff --git a/src/field/generated_fieldops.cxx b/src/field/generated_fieldops.cxx index 3e10aabefe..f718d156fe 100644 --- a/src/field/generated_fieldops.cxx +++ b/src/field/generated_fieldops.cxx @@ -2276,13 +2276,29 @@ Field3DParallel operator*(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2303,13 +2319,29 @@ Field3DParallel operator/(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2330,13 +2362,29 @@ Field3DParallel operator+(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2357,13 +2405,29 @@ Field3DParallel operator-(const Field3D& lhs, const Field3DParallel& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2384,13 +2448,29 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2451,13 +2531,29 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2518,13 +2614,29 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2585,13 +2697,29 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3D& rhs) { result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2652,13 +2780,29 @@ Field3DParallel operator*(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs.yup(i); result.ydown(i) = lhs.ydown(i) * rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2719,13 +2863,29 @@ Field3DParallel operator/(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs.yup(i); result.ydown(i) = lhs.ydown(i) / rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2786,13 +2946,29 @@ Field3DParallel operator+(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs.yup(i); result.ydown(i) = lhs.ydown(i) + rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2853,13 +3029,29 @@ Field3DParallel operator-(const Field3DParallel& lhs, const Field3DParallel& rhs result.setRegion(lhs.getMesh()->getCommonRegion(lhs.getRegionID(), rhs.getRegionID())); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs.yup(i); result.ydown(i) = lhs.ydown(i) - rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -2919,22 +3111,30 @@ Field3DParallel operator*(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) * rhs; result.ydown(i) = lhs.ydown(i) * rhs; } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] * rhs; } -#if BOUT_USE_TRACK - result.name = fmt::format("{:s} * {:s}", lhs.name, "BR"); -#endif checkData(result); return result; } @@ -2966,9 +3166,6 @@ Field3DParallel& Field3DParallel::operator*=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] *= rhs; } track(rhs, "operator*="); -#if BOUT_USE_TRACK - name = fmt::format("{:s} *= {:s}", this->name, "BR"); -#endif checkData(*this); @@ -2988,13 +3185,24 @@ Field3DParallel operator/(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) / rhs; result.ydown(i) = lhs.ydown(i) / rhs; } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } const auto tmp = 1.0 / rhs; @@ -3002,9 +3210,6 @@ Field3DParallel operator/(const Field3DParallel& lhs, const BoutReal rhs) { result[index] = lhs[index] * tmp; } -#if BOUT_USE_TRACK - result.name = fmt::format("{:s} / {:s}", lhs.name, "BR"); -#endif checkData(result); return result; } @@ -3036,9 +3241,6 @@ Field3DParallel& Field3DParallel::operator/=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] /= rhs; } track(rhs, "operator/="); -#if BOUT_USE_TRACK - name = fmt::format("{:s} /= {:s}", this->name, "BR"); -#endif checkData(*this); @@ -3058,22 +3260,30 @@ Field3DParallel operator+(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) + rhs; result.ydown(i) = lhs.ydown(i) + rhs; } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] + rhs; } -#if BOUT_USE_TRACK - result.name = fmt::format("{:s} + {:s}", lhs.name, "BR"); -#endif checkData(result); return result; } @@ -3105,9 +3315,6 @@ Field3DParallel& Field3DParallel::operator+=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] += rhs; } track(rhs, "operator+="); -#if BOUT_USE_TRACK - name = fmt::format("{:s} += {:s}", this->name, "BR"); -#endif checkData(*this); @@ -3127,22 +3334,30 @@ Field3DParallel operator-(const Field3DParallel& lhs, const BoutReal rhs) { result.setRegion(lhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); + ASSERT2(lhs.hasParallelSlices()); + for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { + ASSERT2(lhs.ydown(i).isAllocated()); + ASSERT2(lhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < lhs.numberParallelSlices(); ++i) { result.yup(i) = lhs.yup(i) - rhs; result.ydown(i) = lhs.ydown(i) - rhs; } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { result[index] = lhs[index] - rhs; } -#if BOUT_USE_TRACK - result.name = fmt::format("{:s} - {:s}", lhs.name, "BR"); -#endif checkData(result); return result; } @@ -3174,9 +3389,6 @@ Field3DParallel& Field3DParallel::operator-=(const BoutReal rhs) { BOUT_FOR(index, this->getRegion("RGN_ALL")) { (*this)[index] -= rhs; } track(rhs, "operator-="); -#if BOUT_USE_TRACK - name = fmt::format("{:s} -= {:s}", this->name, "BR"); -#endif checkData(*this); @@ -3196,13 +3408,23 @@ Field3DParallel operator*(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs * rhs.yup(i); result.ydown(i) = lhs * rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -3222,13 +3444,23 @@ Field3DParallel operator/(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs / rhs.yup(i); result.ydown(i) = lhs / rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -3248,13 +3480,23 @@ Field3DParallel operator+(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs + rhs.yup(i); result.ydown(i) = lhs + rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { @@ -3274,13 +3516,23 @@ Field3DParallel operator-(const BoutReal lhs, const Field3DParallel& rhs) { result.setRegion(rhs.getRegionID()); if (result.isFci()) { - result.splitParallelSlices(); ASSERT2(rhs.hasParallelSlices()); + for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { + ASSERT2(rhs.ydown(i).isAllocated()); + ASSERT2(rhs.yup(i).isAllocated()); + } + result.splitParallelSlices(); for (size_t i{0}; i < rhs.numberParallelSlices(); ++i) { result.yup(i) = lhs - rhs.yup(i); result.ydown(i) = lhs - rhs.ydown(i); } + + ASSERT2(result.hasParallelSlices()); + for (size_t i{0}; i < result.numberParallelSlices(); ++i) { + ASSERT2(result.ydown(i).isAllocated()); + ASSERT2(result.yup(i).isAllocated()); + } } BOUT_FOR(index, result.getValidRegionWithDefault("RGN_ALL")) { From 8ad5817399053624f2c9168d54d1d8dc24eac235 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 11:48:42 +0100 Subject: [PATCH 76/98] Do not split parallel slices by default --- include/bout/field3d.hxx | 1 - 1 file changed, 1 deletion(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index 438e28c25d..b95617feaf 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -776,7 +776,6 @@ public: ZDirectionType::Standard}, std::optional regionID = {}) : Field3D(localmesh, location_in, directions_in, regionID) { - splitParallelSlices(); ensureFieldAligned(); } explicit Field3DParallel(Array data, Mesh* localmesh, From 8d6e1a9a66e5b4bf22dfcdf5d1cee1a7e24f534e Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 11:49:39 +0100 Subject: [PATCH 77/98] Simplify Div_par Also for FCI, the coordinate system is locally field aligned --- src/mesh/coordinates.cxx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 699e1cd440..ef2ee2a375 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1563,15 +1563,7 @@ Field3D Coordinates::Div_par(const Field3DParallel& f, CELL_LOC outloc, // Coordinates object const auto& Bxy_floc = f.getCoordinates()->Bxy; - if (!f.hasParallelSlices()) { - // No yup/ydown fields. The Grad_par operator will - // shift to field aligned coordinates - return Bxy * Grad_par(f / Bxy_floc, outloc, method); - } - - // Need to modify yup and ydown fields - const Field3D Jg = J / sqrt(g_22.asField3DParallel()); - return Jg * Grad_par(f / Jg, outloc, method); + return Bxy * Grad_par(f / Bxy_floc, outloc, method); } ///////////////////////////////////////////////////////// From de707f33796cc245438f9cbb8d05a675cb6cb457 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 12:25:15 +0100 Subject: [PATCH 78/98] Do not compute Christoffel symbol for FCI --- src/mesh/coordinates.cxx | 216 ++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 103 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index ef2ee2a375..9f9c47c2e2 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -957,109 +957,119 @@ int Coordinates::geometry(bool recalculate_staggered, // Note: This calculation is completely general: metric // tensor can be 2D or 3D. For 2D, all DDZ terms are zero - G1_11 = 0.5 * g11 * DDX(g_11) + g12 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g13 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G1_22 = g11 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g12 * DDY(g_22) - + g13 * (DDY(g_23) - 0.5 * DDZ(g_22)); - G1_33 = g11 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g12 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g13 * DDZ(g_33); - G1_12 = 0.5 * g11 * DDY(g_11) + 0.5 * g12 * DDX(g_22) - + 0.5 * g13 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G1_13 = 0.5 * g11 * DDZ(g_11) + 0.5 * g12 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - + 0.5 * g13 * DDX(g_33); - G1_23 = 0.5 * g11 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) - + 0.5 * g12 * (DDZ(g_22) + DDY(g_23) - DDY(g_23)) - // + 0.5 *g13*(DDZ(g_32) + DDY(g_33) - DDZ(g_23)); - // which equals - + 0.5 * g13 * DDY(g_33); - - G2_11 = 0.5 * g12 * DDX(g_11) + g22 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g23 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G2_22 = g12 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g22 * DDY(g_22) - + g23 * (DDY(g23) - 0.5 * DDZ(g_22)); - G2_33 = g12 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g22 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g23 * DDZ(g_33); - G2_12 = 0.5 * g12 * DDY(g_11) + 0.5 * g22 * DDX(g_22) - + 0.5 * g23 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G2_13 = - // 0.5 *g21*(DDZ(g_11) + DDX(g_13) - DDX(g_13)) - // which equals - 0.5 * g12 * (DDZ(g_11) + DDX(g_13) - DDX(g_13)) - // + 0.5 *g22*(DDZ(g_21) + DDX(g_23) - DDY(g_13)) - // which equals - + 0.5 * g22 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - // + 0.5 *g23*(DDZ(g_31) + DDX(g_33) - DDZ(g_13)); - // which equals - + 0.5 * g23 * DDX(g_33); - G2_23 = 0.5 * g12 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g22 * DDZ(g_22) - + 0.5 * g23 * DDY(g_33); - - G3_11 = 0.5 * g13 * DDX(g_11) + g23 * (DDX(g_12) - 0.5 * DDY(g_11)) - + g33 * (DDX(g_13) - 0.5 * DDZ(g_11)); - G3_22 = g13 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g23 * DDY(g_22) - + g33 * (DDY(g_23) - 0.5 * DDZ(g_22)); - G3_33 = g13 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g23 * (DDZ(g_23) - 0.5 * DDY(g_33)) - + 0.5 * g33 * DDZ(g_33); - G3_12 = - // 0.5 *g31*(DDY(g_11) + DDX(g_12) - DDX(g_12)) - // which equals to - 0.5 * g13 * DDY(g_11) - // + 0.5 *g32*(DDY(g_21) + DDX(g_22) - DDY(g_12)) - // which equals to - + 0.5 * g23 * DDX(g_22) - //+ 0.5 *g33*(DDY(g_31) + DDX(g_32) - DDZ(g_12)); - // which equals to - + 0.5 * g33 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); - G3_13 = 0.5 * g13 * DDZ(g_11) + 0.5 * g23 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) - + 0.5 * g33 * DDX(g_33); - G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) - + 0.5 * g33 * DDY(g_33); - - G1 = (DDX(J * g11) + DDY(J.asField3DParallel() * g12) + DDZ(J * g13)) / J; - G2 = (DDX(J * g12) + DDY(J.asField3DParallel() * g22) + DDZ(J * g23)) / J; - G3 = (DDX(J * g13) + DDY(J.asField3DParallel() * g23) + DDZ(J * g33)) / J; - - // Communicate christoffel symbol terms - output_progress.write("\tCommunicating connection terms\n"); - - localmesh->communicate_no_slices(G1_11, G1_22, G1_33, G1_12, G1_13, G1_23, G2_11, G2_22, - G2_33, G2_12, G2_13, G2_23, G3_11, G3_22, G3_33, G3_12, - G3_13, G3_23, G1, G2, G3); - - // Set boundary guard cells of Christoffel symbol terms - // Ideally, when location is staggered, we would set the upper/outer boundary point - // correctly rather than by extrapolating here: e.g. if location==CELL_YLOW and we are - // at the upper y-boundary the x- and z-derivatives at yend+1 at the boundary can be - // calculated because the guard cells are available, while the y-derivative could be - // calculated from the CELL_CENTRE metric components (which have guard cells available - // past the boundary location). This would avoid the problem that the y-boundary on the - // CELL_YLOW grid is at a 'guard cell' location (yend+1). - // However, the above would require lots of special handling, so just extrapolate for - // now. - G1_11 = interpolateAndExtrapolate(G1_11, location, true, true, true, transform.get()); - G1_22 = interpolateAndExtrapolate(G1_22, location, true, true, true, transform.get()); - G1_33 = interpolateAndExtrapolate(G1_33, location, true, true, true, transform.get()); - G1_12 = interpolateAndExtrapolate(G1_12, location, true, true, true, transform.get()); - G1_13 = interpolateAndExtrapolate(G1_13, location, true, true, true, transform.get()); - G1_23 = interpolateAndExtrapolate(G1_23, location, true, true, true, transform.get()); - - G2_11 = interpolateAndExtrapolate(G2_11, location, true, true, true, transform.get()); - G2_22 = interpolateAndExtrapolate(G2_22, location, true, true, true, transform.get()); - G2_33 = interpolateAndExtrapolate(G2_33, location, true, true, true, transform.get()); - G2_12 = interpolateAndExtrapolate(G2_12, location, true, true, true, transform.get()); - G2_13 = interpolateAndExtrapolate(G2_13, location, true, true, true, transform.get()); - G2_23 = interpolateAndExtrapolate(G2_23, location, true, true, true, transform.get()); - - G3_11 = interpolateAndExtrapolate(G3_11, location, true, true, true, transform.get()); - G3_22 = interpolateAndExtrapolate(G3_22, location, true, true, true, transform.get()); - G3_33 = interpolateAndExtrapolate(G3_33, location, true, true, true, transform.get()); - G3_12 = interpolateAndExtrapolate(G3_12, location, true, true, true, transform.get()); - G3_13 = interpolateAndExtrapolate(G3_13, location, true, true, true, transform.get()); - G3_23 = interpolateAndExtrapolate(G3_23, location, true, true, true, transform.get()); - - G1 = interpolateAndExtrapolate(G1, location, true, true, true, transform.get()); - G2 = interpolateAndExtrapolate(G2, location, true, true, true, transform.get()); - G3 = interpolateAndExtrapolate(G3, location, true, true, true, transform.get()); + if (!g11.isFci()) { + G1_11 = 0.5 * g11 * DDX(g_11) + g12 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g13 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G1_22 = g11 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g12 * DDY(g_22) + + g13 * (DDY(g_23) - 0.5 * DDZ(g_22)); + G1_33 = g11 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g12 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g13 * DDZ(g_33); + G1_12 = 0.5 * g11 * DDY(g_11) + 0.5 * g12 * DDX(g_22) + + 0.5 * g13 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G1_13 = 0.5 * g11 * DDZ(g_11) + 0.5 * g12 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + + 0.5 * g13 * DDX(g_33); + G1_23 = 0.5 * g11 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + + 0.5 * g12 * (DDZ(g_22) + DDY(g_23) - DDY(g_23)) + // + 0.5 *g13*(DDZ(g_32) + DDY(g_33) - DDZ(g_23)); + // which equals + + 0.5 * g13 * DDY(g_33); + + G2_11 = 0.5 * g12 * DDX(g_11) + g22 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g23 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G2_22 = g12 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g22 * DDY(g_22) + + g23 * (DDY(g23) - 0.5 * DDZ(g_22)); + G2_33 = g12 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g22 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g23 * DDZ(g_33); + G2_12 = 0.5 * g12 * DDY(g_11) + 0.5 * g22 * DDX(g_22) + + 0.5 * g23 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G2_13 = + // 0.5 *g21*(DDZ(g_11) + DDX(g_13) - DDX(g_13)) + // which equals + 0.5 * g12 * (DDZ(g_11) + DDX(g_13) - DDX(g_13)) + // + 0.5 *g22*(DDZ(g_21) + DDX(g_23) - DDY(g_13)) + // which equals + + 0.5 * g22 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + // + 0.5 *g23*(DDZ(g_31) + DDX(g_33) - DDZ(g_13)); + // which equals + + 0.5 * g23 * DDX(g_33); + G2_23 = 0.5 * g12 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g22 * DDZ(g_22) + + 0.5 * g23 * DDY(g_33); + + G3_11 = 0.5 * g13 * DDX(g_11) + g23 * (DDX(g_12) - 0.5 * DDY(g_11)) + + g33 * (DDX(g_13) - 0.5 * DDZ(g_11)); + G3_22 = g13 * (DDY(g_12) - 0.5 * DDX(g_22)) + 0.5 * g23 * DDY(g_22) + + g33 * (DDY(g_23) - 0.5 * DDZ(g_22)); + G3_33 = g13 * (DDZ(g_13) - 0.5 * DDX(g_33)) + g23 * (DDZ(g_23) - 0.5 * DDY(g_33)) + + 0.5 * g33 * DDZ(g_33); + G3_12 = + // 0.5 *g31*(DDY(g_11) + DDX(g_12) - DDX(g_12)) + // which equals to + 0.5 * g13 * DDY(g_11) + // + 0.5 *g32*(DDY(g_21) + DDX(g_22) - DDY(g_12)) + // which equals to + + 0.5 * g23 * DDX(g_22) + //+ 0.5 *g33*(DDY(g_31) + DDX(g_32) - DDZ(g_12)); + // which equals to + + 0.5 * g33 * (DDY(g_13) + DDX(g_23) - DDZ(g_12)); + G3_13 = 0.5 * g13 * DDZ(g_11) + 0.5 * g23 * (DDZ(g_12) + DDX(g_23) - DDY(g_13)) + + 0.5 * g33 * DDX(g_33); + G3_23 = 0.5 * g13 * (DDZ(g_12) + DDY(g_13) - DDX(g_23)) + 0.5 * g23 * DDZ(g_22) + + 0.5 * g33 * DDY(g_33); + + G1 = (DDX(J * g11) + DDY(J.asField3DParallel() * g12) + DDZ(J * g13)) / J; + G2 = (DDX(J * g12) + DDY(J.asField3DParallel() * g22) + DDZ(J * g23)) / J; + G3 = (DDX(J * g13) + DDY(J.asField3DParallel() * g23) + DDZ(J * g33)) / J; + + // Communicate christoffel symbol terms + output_progress.write("\tCommunicating connection terms\n"); + + localmesh->communicate_no_slices(G1_11, G1_22, G1_33, G1_12, G1_13, G1_23, G2_11, + G2_22, G2_33, G2_12, G2_13, G2_23, G3_11, G3_22, + G3_33, G3_12, G3_13, G3_23, G1, G2, G3); + + // Set boundary guard cells of Christoffel symbol terms + // Ideally, when location is staggered, we would set the upper/outer boundary point + // correctly rather than by extrapolating here: e.g. if location==CELL_YLOW and we are + // at the upper y-boundary the x- and z-derivatives at yend+1 at the boundary can be + // calculated because the guard cells are available, while the y-derivative could be + // calculated from the CELL_CENTRE metric components (which have guard cells available + // past the boundary location). This would avoid the problem that the y-boundary on the + // CELL_YLOW grid is at a 'guard cell' location (yend+1). + // However, the above would require lots of special handling, so just extrapolate for + // now. + G1_11 = interpolateAndExtrapolate(G1_11, location, true, true, true, transform.get()); + G1_22 = interpolateAndExtrapolate(G1_22, location, true, true, true, transform.get()); + G1_33 = interpolateAndExtrapolate(G1_33, location, true, true, true, transform.get()); + G1_12 = interpolateAndExtrapolate(G1_12, location, true, true, true, transform.get()); + G1_13 = interpolateAndExtrapolate(G1_13, location, true, true, true, transform.get()); + G1_23 = interpolateAndExtrapolate(G1_23, location, true, true, true, transform.get()); + + G2_11 = interpolateAndExtrapolate(G2_11, location, true, true, true, transform.get()); + G2_22 = interpolateAndExtrapolate(G2_22, location, true, true, true, transform.get()); + G2_33 = interpolateAndExtrapolate(G2_33, location, true, true, true, transform.get()); + G2_12 = interpolateAndExtrapolate(G2_12, location, true, true, true, transform.get()); + G2_13 = interpolateAndExtrapolate(G2_13, location, true, true, true, transform.get()); + G2_23 = interpolateAndExtrapolate(G2_23, location, true, true, true, transform.get()); + + G3_11 = interpolateAndExtrapolate(G3_11, location, true, true, true, transform.get()); + G3_22 = interpolateAndExtrapolate(G3_22, location, true, true, true, transform.get()); + G3_33 = interpolateAndExtrapolate(G3_33, location, true, true, true, transform.get()); + G3_12 = interpolateAndExtrapolate(G3_12, location, true, true, true, transform.get()); + G3_13 = interpolateAndExtrapolate(G3_13, location, true, true, true, transform.get()); + G3_23 = interpolateAndExtrapolate(G3_23, location, true, true, true, transform.get()); + + G1 = interpolateAndExtrapolate(G1, location, true, true, true, transform.get()); + G2 = interpolateAndExtrapolate(G2, location, true, true, true, transform.get()); + G3 = interpolateAndExtrapolate(G3, location, true, true, true, transform.get()); + } else { + G1_11 = G1_22 = G1_33 = G1_12 = G1_13 = G1_23 = + + G2_11 = G2_22 = G2_33 = G2_12 = G2_13 = G2_23 = + + G3_11 = G3_22 = G3_33 = G3_12 = G3_13 = G3_23 = + + G1 = G2 = G3 = BoutNaN; + } ////////////////////////////////////////////////////// /// Non-uniform meshes. Need to use DDX, DDY From bd7dd2dd7880a76ee3734e70f1fc09ac95e99cdf Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 12:31:57 +0100 Subject: [PATCH 79/98] Do not differentiate dy for FCI --- src/mesh/coordinates.cxx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 9f9c47c2e2..c88e5010d9 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1106,11 +1106,15 @@ int Coordinates::geometry(bool recalculate_staggered, if (localmesh->get(d2y, "d2y" + suffix, 0.0, false, location)) { output_warn.write( "\tWARNING: differencing quantity 'd2y' not found. Calculating from dy\n"); - d1_dy = DDY(1. / dy.asField3DParallel()); // d/di(1/dy) + if (dy.isFci()) { + d1_dy = BoutNaN; + } else { + d1_dy = DDY(1. / dy.asField3DParallel()); // d/di(1/dy) - localmesh->communicate_no_slices(d1_dy); - d1_dy = - interpolateAndExtrapolate(d1_dy, location, true, true, true, transform.get()); + localmesh->communicate_no_slices(d1_dy); + d1_dy = + interpolateAndExtrapolate(d1_dy, location, true, true, true, transform.get()); + } } else { d2y.setLocation(location); // set boundary cells if necessary From 481179f0cfc53ebd5f6c7f73c2407b2e4d87ecda Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 17 Mar 2026 10:03:46 +0100 Subject: [PATCH 80/98] CI: install pre-release zoidberg --- .ci_fedora.sh | 1 + .github/workflows/tests.yml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci_fedora.sh b/.ci_fedora.sh index e1dc563c6c..2339ff1b4f 100755 --- a/.ci_fedora.sh +++ b/.ci_fedora.sh @@ -47,6 +47,7 @@ test . != ".$1" && mpi="$1" || mpi=openmpi export OMP_NUM_THREADS=1 cd cd BOUT-dev + python -m pip install git+https://github.com/boutproject/zoidberg@better-metric echo "starting configure" time cmake -S . -B build -DBOUT_USE_PETSC=ON \ -DBOUT_UPDATE_GIT_SUBMODULE=OFF \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 02f018b873..8919f82df1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -166,6 +166,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools python -m pip install -r requirements.txt + python -m pip install git+https://github.com/boutproject/zoidberg@better-metric - name: Cache Zenodo test data uses: actions/cache@v5 @@ -187,7 +188,7 @@ jobs: 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 + 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 }} From 1a5b922cf53bb0718337c212298a9a97983492b4 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 12:52:32 +0100 Subject: [PATCH 81/98] CI: fix fedora CI --- .ci_fedora.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci_fedora.sh b/.ci_fedora.sh index 2339ff1b4f..a4ead5eb63 100755 --- a/.ci_fedora.sh +++ b/.ci_fedora.sh @@ -47,7 +47,7 @@ test . != ".$1" && mpi="$1" || mpi=openmpi export OMP_NUM_THREADS=1 cd cd BOUT-dev - python -m pip install git+https://github.com/boutproject/zoidberg@better-metric + python3 -m pip install git+https://github.com/boutproject/zoidberg@better-metric echo "starting configure" time cmake -S . -B build -DBOUT_USE_PETSC=ON \ -DBOUT_UPDATE_GIT_SUBMODULE=OFF \ From f89333da4e53e4916a321c96e9125c4247c37d55 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 12:52:32 +0100 Subject: [PATCH 82/98] CI: fix fedora CI --- .ci_fedora.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci_fedora.sh b/.ci_fedora.sh index 2339ff1b4f..a4ead5eb63 100755 --- a/.ci_fedora.sh +++ b/.ci_fedora.sh @@ -47,7 +47,7 @@ test . != ".$1" && mpi="$1" || mpi=openmpi export OMP_NUM_THREADS=1 cd cd BOUT-dev - python -m pip install git+https://github.com/boutproject/zoidberg@better-metric + python3 -m pip install git+https://github.com/boutproject/zoidberg@better-metric echo "starting configure" time cmake -S . -B build -DBOUT_USE_PETSC=ON \ -DBOUT_UPDATE_GIT_SUBMODULE=OFF \ From cbacecc2f5d51ab59b242b6e1db959a2790a1ba0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 12:58:30 +0100 Subject: [PATCH 83/98] Split slices for FCI --- include/bout/field3d.hxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index b95617feaf..9537bf1fa7 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -776,6 +776,9 @@ public: ZDirectionType::Standard}, std::optional regionID = {}) : Field3D(localmesh, location_in, directions_in, regionID) { + if (isFci()) { + splitParallelSlices(); + } ensureFieldAligned(); } explicit Field3DParallel(Array data, Mesh* localmesh, From d4e137c7536a0e8ae183a65e73e1ea0608af0b67 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 13:08:03 +0100 Subject: [PATCH 84/98] CI: ensure pip is installed --- .ci_fedora.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci_fedora.sh b/.ci_fedora.sh index a4ead5eb63..8214a40ccc 100755 --- a/.ci_fedora.sh +++ b/.ci_fedora.sh @@ -47,6 +47,7 @@ test . != ".$1" && mpi="$1" || mpi=openmpi export OMP_NUM_THREADS=1 cd cd BOUT-dev + python3 -m ensurepip python3 -m pip install git+https://github.com/boutproject/zoidberg@better-metric echo "starting configure" time cmake -S . -B build -DBOUT_USE_PETSC=ON \ From 3d184a66a55a4f53655b37891101b8fa2293d24d Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 13:08:03 +0100 Subject: [PATCH 85/98] CI: ensure pip is installed --- .ci_fedora.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci_fedora.sh b/.ci_fedora.sh index a4ead5eb63..8214a40ccc 100755 --- a/.ci_fedora.sh +++ b/.ci_fedora.sh @@ -47,6 +47,7 @@ test . != ".$1" && mpi="$1" || mpi=openmpi export OMP_NUM_THREADS=1 cd cd BOUT-dev + python3 -m ensurepip python3 -m pip install git+https://github.com/boutproject/zoidberg@better-metric echo "starting configure" time cmake -S . -B build -DBOUT_USE_PETSC=ON \ From 2abbd6c633537e1dc3d988b7842f2c5f97247e18 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 13:48:13 +0100 Subject: [PATCH 86/98] Load parallel B rather then J --- 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 e20b63d0ea..b6f92ee39b 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -104,7 +104,8 @@ void load_parallel_metric_components(Coordinates* coords, int offset) { LOAD_PAR(g_22); LOAD_PAR(g_33); LOAD_PAR(g_13); - LOAD_PAR(J); + LOAD_PAR(Bxy); + #undef LOAD_PAR } #endif From 243632612120dbd560fbe552319763be7d54f92c Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 13:48:27 +0100 Subject: [PATCH 87/98] Set parallel J --- src/mesh/parallel/fci.cxx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index b6f92ee39b..a885957ea0 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -457,9 +457,19 @@ void FCITransform::outputVars(Options& output_options) { void FCITransform::loadParallelMetrics(Coordinates* coords) { #if BOUT_USE_METRIC_3D + const auto JB0 = mesh.J * mesh.B; + mesh.J.splitParallelSlices(); + mesh.J.disallowCalcParallelSlices(); for (int i = 1; i <= mesh.ystart; ++i) { load_parallel_metric_components(coords, -i); load_parallel_metric_components(coords, i); + + mesh.J.ynext(i).allocate(); + mesh.J.ynext(-i).allocate(); + BOUT_FOR(j, JB0.getRegion("RGN_NO_BNDRY")) { + mesh.J.ynext(i)[j.yp(i)] = JB0[j] / mesh.B.ynext(i)[j.yp(i)]; + mesh.J.ynext(-i)[j.yp(-i)] = JB0[j] / mesh.B.ynext(-i)[j.yp(-i)]; + } } #endif } From c9868d3cba8136a33f59eed316cb5a9a70a6fae5 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 18 Mar 2026 14:01:45 +0100 Subject: [PATCH 88/98] Fixup J computation --- src/mesh/parallel/fci.cxx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index a885957ea0..e29937df09 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -457,18 +457,18 @@ void FCITransform::outputVars(Options& output_options) { void FCITransform::loadParallelMetrics(Coordinates* coords) { #if BOUT_USE_METRIC_3D - const auto JB0 = mesh.J * mesh.B; - mesh.J.splitParallelSlices(); - mesh.J.disallowCalcParallelSlices(); + const auto JB0 = coords->J * coords->Bxy; + coords->J.splitParallelSlices(); + coords->J.disallowCalcParallelSlices(); for (int i = 1; i <= mesh.ystart; ++i) { load_parallel_metric_components(coords, -i); load_parallel_metric_components(coords, i); - mesh.J.ynext(i).allocate(); - mesh.J.ynext(-i).allocate(); + coords->J.ynext(i).allocate(); + coords->J.ynext(-i).allocate(); BOUT_FOR(j, JB0.getRegion("RGN_NO_BNDRY")) { - mesh.J.ynext(i)[j.yp(i)] = JB0[j] / mesh.B.ynext(i)[j.yp(i)]; - mesh.J.ynext(-i)[j.yp(-i)] = JB0[j] / mesh.B.ynext(-i)[j.yp(-i)]; + coords->J.ynext(i)[j.yp(i)] = JB0[j] / coords->Bxy.ynext(i)[j.yp(i)]; + coords->J.ynext(-i)[j.yp(-i)] = JB0[j] / coords->Bxy.ynext(-i)[j.yp(-i)]; } } #endif From a50c53166305cb12714a149dc4197d2983fc85cb Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 19 Mar 2026 08:15:17 +0100 Subject: [PATCH 89/98] Fix region name --- src/mesh/parallel/fci.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index e29937df09..6b7dfaa26b 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -466,7 +466,7 @@ void FCITransform::loadParallelMetrics(Coordinates* coords) { coords->J.ynext(i).allocate(); coords->J.ynext(-i).allocate(); - BOUT_FOR(j, JB0.getRegion("RGN_NO_BNDRY")) { + BOUT_FOR(j, JB0.getRegion("RGN_NOBNDRY")) { coords->J.ynext(i)[j.yp(i)] = JB0[j] / coords->Bxy.ynext(i)[j.yp(i)]; coords->J.ynext(-i)[j.yp(-i)] = JB0[j] / coords->Bxy.ynext(-i)[j.yp(-i)]; } From b57ea103c2d432224e39ccb53a4f60dce0f560db Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 19 Mar 2026 09:42:08 +0100 Subject: [PATCH 90/98] Do not attempt to communicate Bxy --- tests/MMS/spatial/fci/fci_mms.cxx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/MMS/spatial/fci/fci_mms.cxx b/tests/MMS/spatial/fci/fci_mms.cxx index 7967452f3d..90bf107f3c 100644 --- a/tests/MMS/spatial/fci/fci_mms.cxx +++ b/tests/MMS/spatial/fci/fci_mms.cxx @@ -46,12 +46,6 @@ int main(int argc, char** argv) { Field3D K{FieldFactory::get()->create3D("K", Options::getRoot(), mesh)}; // Communicate to calculate parallel transform. - if constexpr (bout::build::use_metric_3d) { - // Div_par operators require B parallel slices: - // Coordinates::geometry doesn't ensure this (yet) - auto& Bxy = mesh->getCoordinates()->Bxy; - mesh->communicate(Bxy); - } mesh->communicate(input, K); Options dump; From be8bae53e8c3d4397a19fd598b66cd17fd5cd4f6 Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 19 Mar 2026 09:42:37 +0100 Subject: [PATCH 91/98] Update to newer mesh with parallel metrics --- tests/integrated/test-fci-mpi/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-fci-mpi/CMakeLists.txt b/tests/integrated/test-fci-mpi/CMakeLists.txt index 0abbbce099..ba126d7e76 100644 --- a/tests/integrated/test-fci-mpi/CMakeLists.txt +++ b/tests/integrated/test-fci-mpi/CMakeLists.txt @@ -4,7 +4,7 @@ bout_add_mms_test( USE_RUNTEST USE_DATA_BOUT_INP PROCESSORS 6 DOWNLOAD - https://zenodo.org/records/7614499/files/W7X-conf4-36x8x128.fci.nc?download=1 + https://zenodo.org/records/17571607/files/W7X-conf0-36x8x128.fci.nc?download=1 DOWNLOAD_NAME grid.fci.nc REQUIRES BOUT_HAS_PETSC ) From 202e51d8b55bd2417c573700db962458b67c527f Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 19 Mar 2026 12:47:57 +0100 Subject: [PATCH 92/98] Set the regionID for parallel slices --- src/mesh/parallel/fci.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 6b7dfaa26b..7a63dbf30a 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -88,6 +88,7 @@ void load_parallel_metric_component(std::string name, Field3D& component, int of if (!component.hasParallelSlices()) { component.splitParallelSlices(); component.disallowCalcParallelSlices(); + component.resetRegionParallel(); } auto& pcom = component.ynext(offset); pcom.allocate(); @@ -460,6 +461,7 @@ void FCITransform::loadParallelMetrics(Coordinates* coords) { const auto JB0 = coords->J * coords->Bxy; coords->J.splitParallelSlices(); coords->J.disallowCalcParallelSlices(); + coords->J.resetRegionParallel(); for (int i = 1; i <= mesh.ystart; ++i) { load_parallel_metric_components(coords, -i); load_parallel_metric_components(coords, i); From f828a9c7a5c1db1356c38a2bf1954ebd853641fb Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 19 Mar 2026 13:07:44 +0100 Subject: [PATCH 93/98] Allow to override isFci check --- include/bout/field3d.hxx | 2 +- src/field/field3d.cxx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/bout/field3d.hxx b/include/bout/field3d.hxx index f88e84f131..68dc84d1a7 100644 --- a/include/bout/field3d.hxx +++ b/include/bout/field3d.hxx @@ -342,7 +342,7 @@ public: const Region& getValidRegionWithDefault(const std::string& region_name) const; void setRegion(const std::string& region_name) override; void resetRegion() override { regionID.reset(); }; - void resetRegionParallel(); + void resetRegionParallel(bool force = false); void setRegion(size_t id) override { regionID = id; }; void setRegion(std::optional id) override { regionID = id; }; std::optional getRegionID() const override { return regionID; }; diff --git a/src/field/field3d.cxx b/src/field/field3d.cxx index a123074572..38b2682606 100644 --- a/src/field/field3d.cxx +++ b/src/field/field3d.cxx @@ -883,8 +883,8 @@ void Field3D::setRegion(const std::string& region_name) { regionID = fieldmesh->getRegionID(region_name); } -void Field3D::resetRegionParallel() { - if (isFci()) { +void Field3D::resetRegionParallel(const bool force) { + if (force or isFci()) { for (int i = 0; i < fieldmesh->ystart; ++i) { yup_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", i + 1)); ydown_fields[i].setRegion(fmt::format("RGN_YPAR_{:+d}", -i - 1)); From 1d7b9f0cca654f1479475c3952b42583b4f0498d Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 19 Mar 2026 13:09:35 +0100 Subject: [PATCH 94/98] Parallel transform is not yet set, so we need to overwrite the isFci check --- src/mesh/parallel/fci.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 7a63dbf30a..c23185460f 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -88,7 +88,7 @@ void load_parallel_metric_component(std::string name, Field3D& component, int of if (!component.hasParallelSlices()) { component.splitParallelSlices(); component.disallowCalcParallelSlices(); - component.resetRegionParallel(); + component.resetRegionParallel(true); } auto& pcom = component.ynext(offset); pcom.allocate(); @@ -461,7 +461,7 @@ void FCITransform::loadParallelMetrics(Coordinates* coords) { const auto JB0 = coords->J * coords->Bxy; coords->J.splitParallelSlices(); coords->J.disallowCalcParallelSlices(); - coords->J.resetRegionParallel(); + coords->J.resetRegionParallel(true); for (int i = 1; i <= mesh.ystart; ++i) { load_parallel_metric_components(coords, -i); load_parallel_metric_components(coords, i); From 4b49458166705550e757fbca6b47e5e686392e7e Mon Sep 17 00:00:00 2001 From: David Bold Date: Thu, 19 Mar 2026 13:10:37 +0100 Subject: [PATCH 95/98] Revert "Do not differentiate dy for FCI" This reverts commit bd7dd2dd7880a76ee3734e70f1fc09ac95e99cdf. --- src/mesh/coordinates.cxx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 798a62b64f..1157f3a823 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1111,15 +1111,11 @@ int Coordinates::geometry(bool recalculate_staggered, if (localmesh->get(d2y, "d2y" + suffix, 0.0, false, location)) { output_warn.write( "\tWARNING: differencing quantity 'd2y' not found. Calculating from dy\n"); - if (dy.isFci()) { - d1_dy = BoutNaN; - } else { - d1_dy = DDY(1. / dy.asField3DParallel()); // d/di(1/dy) + d1_dy = DDY(1. / dy.asField3DParallel()); // d/di(1/dy) - localmesh->communicate_no_slices(d1_dy); - d1_dy = - interpolateAndExtrapolate(d1_dy, location, true, true, true, transform.get()); - } + localmesh->communicate_no_slices(d1_dy); + d1_dy = + interpolateAndExtrapolate(d1_dy, location, true, true, true, transform.get()); } else { d2y.setLocation(location); // set boundary cells if necessary From c865d1b408f3818789bfb186f0010d349ebfa2b0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 20 Mar 2026 12:07:58 +0100 Subject: [PATCH 96/98] Load parallel dy --- src/mesh/parallel/fci.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index c23185460f..ce391c2bdc 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -105,6 +105,7 @@ void load_parallel_metric_components(Coordinates* coords, int offset) { LOAD_PAR(g_22); LOAD_PAR(g_33); LOAD_PAR(g_13); + LOAD_PAR(dy); LOAD_PAR(Bxy); #undef LOAD_PAR From 07afd43874e40715c59ede8936c70454348a0245 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 20 Mar 2026 12:08:17 +0100 Subject: [PATCH 97/98] Output that we load the parallel metrics --- src/mesh/parallel/fci.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index ce391c2bdc..5b23ce7fc6 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -459,6 +459,7 @@ void FCITransform::outputVars(Options& output_options) { void FCITransform::loadParallelMetrics(Coordinates* coords) { #if BOUT_USE_METRIC_3D + output_info.write("\tLoading parallel metrics\n"); const auto JB0 = coords->J * coords->Bxy; coords->J.splitParallelSlices(); coords->J.disallowCalcParallelSlices(); From b2a73839911b3f247b1800d7563307667eebfb34 Mon Sep 17 00:00:00 2001 From: David Bold Date: Fri, 20 Mar 2026 21:01:44 +0100 Subject: [PATCH 98/98] prek fixes --- include/bout/difops.hxx | 28 +++++++++---------- include/bout/fv_ops.hxx | 12 ++++---- src/mesh/interpolation/hermite_spline_xz.cxx | 1 - .../MMS/spatial/finite-volume/CMakeLists.txt | 6 ++-- tests/MMS/spatial/finite-volume/mms.py | 2 +- tests/MMS/spatial/finite-volume/runtest | 9 +----- 6 files changed, 25 insertions(+), 33 deletions(-) diff --git a/include/bout/difops.hxx b/include/bout/difops.hxx index 2cd99f8d33..2070cc30d3 100644 --- a/include/bout/difops.hxx +++ b/include/bout/difops.hxx @@ -1,11 +1,11 @@ /*!****************************************************************************** * \file difops.hxx - * + * * Differential operators * * Changelog: * - * 2009-01 Ben Dudson + * 2009-01 Ben Dudson * * Added two optional parameters which can be put in any order * These determine the method to use (DIFF_METHOD) * and CELL_LOC location of the result. @@ -15,7 +15,7 @@ * Copyright 2010 B.D.Dudson, S.Farley, M.V.Umansky, X.Q.Xu * * Contact: Ben Dudson, bd512@york.ac.uk - * + * * This file is part of BOUT++. * * BOUT++ is free software: you can redistribute it and/or modify @@ -30,7 +30,7 @@ * * You should have received a copy of the GNU Lesser General Public License * along with BOUT++. If not, see . - * + * *******************************************************************************/ #ifndef BOUT_DIFOPS_H @@ -81,7 +81,7 @@ Field3D Grad_parP(const Field3D& apar, const Field3D& f); * \f[ * v\mathbf{b}_0 \cdot \nabla f * \f] - * + * * * @param[in] v The velocity in y direction * @param[in] f The scalar field to be differentiated @@ -175,7 +175,7 @@ inline Field3D Grad2_par2(const Field3D& f, CELL_LOC outloc, DIFF_METHOD method) /*! * Parallel divergence of diffusive flux, K*Grad_par - * + * * \f[ * \nabla \cdot ( \mathbf{b}_0 kY (\mathbf{b}_0 \cdot \nabla) f ) * \f] @@ -203,8 +203,8 @@ Field3D Div_par_K_Grad_par_mod(const Field3D& k, const Field3D& f, Field3D& flow * Perpendicular Laplacian operator * * This version only includes terms in X and Z, dropping - * derivatives in Y. This is the inverse operation to - * the Laplacian inversion class. + * derivatives in Y. This is the inverse operation to + * the Laplacian inversion class. * * For the full perpendicular Laplacian, use Laplace_perp */ @@ -216,7 +216,7 @@ FieldPerp Delp2(const FieldPerp& f, CELL_LOC outloc = CELL_DEFAULT, bool useFFT /*! * Perpendicular Laplacian, keeping y derivatives * - * + * */ Coordinates::FieldMetric Laplace_perp(const Field2D& f, CELL_LOC outloc = CELL_DEFAULT, @@ -250,18 +250,18 @@ Field2D Laplace_perpXY(const Field2D& A, const Field2D& f); /*! * Terms of form b0 x Grad(phi) dot Grad(A) - * + * */ Coordinates::FieldMetric b0xGrad_dot_Grad(const Field2D& phi, const Field2D& A, CELL_LOC outloc = CELL_DEFAULT); /*! - * Terms of form + * Terms of form * * \f[ * \mathbf{b}_0 \times \nabla \phi \cdot \nabla A * \f] - * + * * @param[in] phi The scalar potential * @param[in] A The field being advected * @param[in] outloc The cell location where the result is defined. By default the same as A. @@ -295,13 +295,13 @@ constexpr BRACKET_METHOD BRACKET_CTU = BRACKET_METHOD::ctu; * \f[ * [f, g] = (1/B) \mathbf{b}_0 \times \nabla f \cdot \nabla g * \f] - * + * * @param[in] f The potential * @param[in] g The field being advected * @param[in] method The method to use * @param[in] outloc The cell location where the result is defined. Default is the same as g * @param[in] solver Pointer to the time integration solver - * + * */ Coordinates::FieldMetric bracket(const Field2D& f, const Field2D& g, BRACKET_METHOD method = BRACKET_STD, diff --git a/include/bout/fv_ops.hxx b/include/bout/fv_ops.hxx index 07a2c2976b..027a1ef77b 100644 --- a/include/bout/fv_ops.hxx +++ b/include/bout/fv_ops.hxx @@ -115,7 +115,7 @@ struct Fromm { /*! * Second order slope limiter method - * + * * Limits slope to minimum absolute value * of left and right gradients. If at a maximum * or minimum slope set to zero, i.e. reverts @@ -152,8 +152,8 @@ private: /*! * Monotonised Central (MC) second order slope limiter (Van Leer) - * - * Limits the slope based on taking the slope with + * + * Limits the slope based on taking the slope with * the minimum absolute value from central, 2*left and * 2*right. If any of these slopes have different signs * then the slope reverts to zero (i.e. 1st-order upwinding). @@ -256,7 +256,7 @@ Field3D Div_par(const Field3D& f_in, const Field3D& v_in, const Field3D& wave_sp ASSERT1_FIELDS_COMPATIBLE(f_in, v_in); ASSERT1_FIELDS_COMPATIBLE(f_in, wave_speed_in); - Mesh const* mesh = f_in.getMesh(); + const Mesh* mesh = f_in.getMesh(); CellEdges cellboundary; @@ -423,9 +423,9 @@ Field3D Div_par(const Field3D& f_in, const Field3D& v_in, const Field3D& wave_sp * Div ( n * v ) -- Magnetic drifts * * This uses the expression - * + * * Div( A ) = 1/J * d/di ( J * A^i ) - * + * * Hence the input vector should be contravariant * * Note: Uses to/from FieldAligned diff --git a/src/mesh/interpolation/hermite_spline_xz.cxx b/src/mesh/interpolation/hermite_spline_xz.cxx index e2760ac0e3..5b9271c93f 100644 --- a/src/mesh/interpolation/hermite_spline_xz.cxx +++ b/src/mesh/interpolation/hermite_spline_xz.cxx @@ -509,7 +509,6 @@ Field3D XZHermiteSplineBase::interpolate( f_interp[iyp] = +f_z * h00_z[i] + f_zp1 * h01_z[i] + fz_z * h10_z[i] + fz_zp1 * h11_z[i]; - if constexpr (monotonic) { const auto corners = {(*gf)[IndG3D(g3dinds[i][0])], (*gf)[IndG3D(g3dinds[i][1])], (*gf)[IndG3D(g3dinds[i][2])], (*gf)[IndG3D(g3dinds[i][3])]}; diff --git a/tests/MMS/spatial/finite-volume/CMakeLists.txt b/tests/MMS/spatial/finite-volume/CMakeLists.txt index 6d9c839a05..7180eb7d98 100644 --- a/tests/MMS/spatial/finite-volume/CMakeLists.txt +++ b/tests/MMS/spatial/finite-volume/CMakeLists.txt @@ -1,6 +1,6 @@ -bout_add_mms_test(MMS-spatial-finite-volume +bout_add_mms_test( + MMS-spatial-finite-volume SOURCES fv_mms.cxx - USE_RUNTEST - USE_DATA_BOUT_INP + USE_RUNTEST USE_DATA_BOUT_INP PROCESSORS 2 ) diff --git a/tests/MMS/spatial/finite-volume/mms.py b/tests/MMS/spatial/finite-volume/mms.py index dfcfce9a09..a95ecc1328 100755 --- a/tests/MMS/spatial/finite-volume/mms.py +++ b/tests/MMS/spatial/finite-volume/mms.py @@ -32,7 +32,7 @@ v = fv / f # Substitute back to get input y coordinates -replace = [ (metric.y, y*Ly/(2*pi) ) ] +replace = [(metric.y, y * Ly / (2 * pi))] def Grad2_par2(f: Expr) -> Expr: diff --git a/tests/MMS/spatial/finite-volume/runtest b/tests/MMS/spatial/finite-volume/runtest index b38a6359ac..bcd4672545 100755 --- a/tests/MMS/spatial/finite-volume/runtest +++ b/tests/MMS/spatial/finite-volume/runtest @@ -22,23 +22,18 @@ OPERATORS = { "FV_Div_par_MC": 1.5, "FV_Div_par_mod_MC": 1.5, "FV_Div_par_fvv_MC": 1.5, - "FV_Div_par_Upwind": 1, "FV_Div_par_mod_Upwind": 1, "FV_Div_par_fvv_Upwind": 1, - "FV_Div_par_Fromm": 1.5, "FV_Div_par_mod_Fromm": 1.5, "FV_Div_par_fvv_Fromm": 1.5, - "FV_Div_par_MinMod": 1.5, "FV_Div_par_mod_MinMod": 1.5, "FV_Div_par_fvv_MinMod": 1.5, - "FV_Div_par_Superbee": 1.5, "FV_Div_par_mod_Superbee": 1.5, "FV_Div_par_fvv_Superbee": 1.5, - "FV_Div_par_K_Grad_par": 2, "FV_Div_par_K_Grad_par_mod": 2, } @@ -131,9 +126,7 @@ def test_fv_operators() -> bool: final_errors = transpose(all_errors) for operator, order in OPERATORS.items(): - success = assert_convergence( - final_errors[operator]["l_2"], dx, operator, order - ) + success = assert_convergence(final_errors[operator]["l_2"], dx, operator, order) if not success: failures.append(operator)