diff --git a/CMakeLists.txt b/CMakeLists.txt index c05bad29db..ba19a2e7f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -372,6 +372,7 @@ set(BOUT_SOURCES ./src/sys/options/options_adios.hxx ./src/sys/optionsreader.cxx ./src/sys/output.cxx + ./src/sys/output_bout_types.cxx ./src/sys/petsclib.cxx ./src/sys/range.cxx ./src/sys/slepclib.cxx diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index b67762521b..a5c68dad9c 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -5,10 +5,21 @@ #ifndef OUTPUT_BOUT_TYPES_H #define OUTPUT_BOUT_TYPES_H +#include "bout/bout_types.hxx" +#include "bout/mesh.hxx" +#include "bout/output.hxx" // IWYU pragma: keep +#include "bout/region.hxx" +#include "bout/traits.hxx" + +#include +#include #include -#include "bout/output.hxx" -#include "bout/region.hxx" +#include +#include +#include +#include +#include template struct fmt::formatter> { @@ -17,7 +28,8 @@ struct fmt::formatter> { // Parses format specifications of the form ['c' | 'i']. constexpr auto parse(format_parse_context& ctx) { - auto it = ctx.begin(), end = ctx.end(); + const auto* it = ctx.begin(); + const auto* end = ctx.end(); if (it != end && (*it == 'c' || *it == 'i')) { presentation = *it++; } @@ -50,4 +62,222 @@ struct fmt::formatter> { } }; +namespace bout { +namespace details { +template +/// Transpose a region so that it iterates in Z first, then Y, then X +/// +/// Caution: this is the most inefficient memory order! +auto region_transpose(const Region& region) -> Region; + +auto colour(BoutReal value, BoutReal min, BoutReal max) -> fmt::text_style; + +// Parses the range [begin, end) as an unsigned integer. This function assumes +// that the range is non-empty and the first character is a digit. +// +// Taken from fmt +// Copyright (c) 2012 - present, Victor Zverovich +// SPDX-License-Identifier: MIT +constexpr auto parse_nonnegative_int(const char*& begin, const char* end, + int error_value) noexcept -> int { + const auto* p = begin; + unsigned value = *p - '0'; + unsigned prev = 0; + ++p; + + while (p != end && '0' <= *p && *p <= '9') { + prev = value; + value = (value * 10) + unsigned(*p - '0'); + ++p; + } + auto num_digits = p - begin; + begin = p; + int digits10 = static_cast(sizeof(int) * CHAR_BIT * 3 / 10); + if (num_digits <= digits10) { + return static_cast(value); + } + // Check for overflow. + unsigned max = INT_MAX; + return num_digits == digits10 + 1 and (prev * 10ULL) + unsigned(p[-1] - '0') <= max + ? static_cast(value) + : error_value; +} +} // namespace details +} // namespace bout + +/// Formatter for Fields +/// +/// Format specification: +/// +/// - ``n``: Don't show indices +/// - ``r''``: Use given region (default: ``RGN_ALL``) +/// - ``T``: Transpose field so X is first dimension +/// - ``#``: Plot slices as 2D heatmap +/// - ``e``: Number of elements at each edge to show +/// - ``f``: Show full field +template +struct fmt::formatter, char>> { +private: + fmt::formatter underlying; + + static constexpr auto default_region = "RGN_ALL"; + std::string_view region = default_region; + + bool show_indices = true; + bool transpose = false; + bool plot = false; + int edgeitems = 4; + bool show_full = false; + +public: + constexpr auto parse(format_parse_context& ctx) { + const auto* it = ctx.begin(); + const auto* end = ctx.end(); + + if (it == end) { + return underlying.parse(ctx); + } + + while (it != end and *it != ':' and *it != '}') { + // Other cases handled explicitly below + // NOLINTNEXTLINE(bugprone-switch-missing-default-case) + switch (*it) { + case 'e': + ++it; + edgeitems = bout::details::parse_nonnegative_int(it, end, -1); + if (edgeitems == -1) { + throw fmt::format_error("number is too big"); + } + break; + case 'f': + show_full = true; + ++it; + break; + case 'r': + ++it; + if (*it != '\'') { + throw fmt::format_error("invalid format for Field"); + } + { + const auto* rgn_start = ++it; + std::size_t size = 0; + while (*it != '\'') { + ++size; + ++it; + } + region = std::string_view(rgn_start, size); + } + ++it; + break; + case 'n': + show_indices = false; + ++it; + break; + case 'T': + transpose = true; + ++it; + break; + case '#': + plot = true; + show_indices = false; + ++it; + break; + } + } + + if (it != end and *it != '}') { + if (*it != ':') { + throw fmt::format_error("invalid format specifier"); + } + ++it; + } + + ctx.advance_to(it); + return underlying.parse(ctx); + } + + auto format(const T& f, format_context& ctx) const -> format_context::iterator { + using namespace bout::details; + + const auto* mesh = f.getMesh(); + + const auto rgn_str = std::string{region}; + const auto rgn_ = f.getRegion(rgn_str); + const auto rgn = transpose ? region_transpose(rgn_) : rgn_; + + const auto i = rgn.begin(); + int previous_x = i->x(); + int previous_y = i->y(); + int previous_z = i->z(); + + const auto last_i = rgn.getIndices().rbegin(); + const int last_x = last_i->x(); + const int last_y = last_i->y(); + const int last_z = last_i->z(); + + // Indices of edges + const int start_x = previous_x + edgeitems; + const int start_y = previous_y + edgeitems; + const int start_z = previous_z + edgeitems; + const int end_x = last_x - edgeitems; + const int end_y = last_y - edgeitems; + const int end_z = last_z - edgeitems; + + // Range of the data for plotting + BoutReal plot_min = 0.0; + BoutReal plot_max = 0.0; + if (plot) { + plot_min = min(f, false, rgn_str); + plot_max = max(f, false, rgn_str); + } + + // Separators + constexpr auto block_sep = "\n\n"; + const auto* const item_sep = plot ? "" : " "; + + // If we've shown the skip sep already this dim + bool shown_skip = false; + constexpr auto skip_sep = "..."; + + BOUT_FOR_SERIAL(i, rgn) { + const auto ix = i.x(); + const auto iy = i.y(); + const auto iz = i.z(); + + const bool should_show = + show_full + or ((ix < start_x or ix > end_x) and (iy < start_y or iy > end_y) + and (iz < start_z or iz > end_z)); + + if ((not shown_skip or should_show) and iz > previous_z) { + format_to(ctx.out(), transpose ? block_sep : item_sep); + } + if ((not shown_skip or should_show) and iy > previous_y) { + format_to(ctx.out(), "\n"); + } + if ((not shown_skip or should_show) and ix > previous_x) { + format_to(ctx.out(), transpose ? item_sep : block_sep); + } + + if (show_indices and should_show) { + format_to(ctx.out(), "{:c}: ", i); + } + if (plot) { + format_to(ctx.out(), "{}", styled("█", colour(f[i], plot_min, plot_max))); + } else if (should_show) { + underlying.format(f[i], ctx); + format_to(ctx.out(), ";"); + shown_skip = false; + } else if (not shown_skip) { + format_to(ctx.out(), skip_sep); + shown_skip = true; + } + previous_x = ix; + previous_y = iy; + previous_z = iz; + } + return format_to(ctx.out(), ""); + } +}; + #endif // OUTPUT_BOUT_TYPES_H diff --git a/src/sys/output_bout_types.cxx b/src/sys/output_bout_types.cxx new file mode 100644 index 0000000000..41a2055dfc --- /dev/null +++ b/src/sys/output_bout_types.cxx @@ -0,0 +1,326 @@ +#include "bout/output_bout_types.hxx" + +#include "bout/bout_types.hxx" +#include "bout/mesh.hxx" +#include "bout/region.hxx" + +#include + +#include +#include +#include +#include +#include + +namespace bout::details { + +template +auto region_transpose(const Region& region) -> Region { + auto indices = region.getIndices(); + + std::sort(indices.begin(), indices.end(), [](const T& lhs, const T& rhs) { + const auto lx = lhs.x(); + const auto ly = lhs.y(); + const auto lz = lhs.z(); + + const auto rx = rhs.x(); + const auto ry = rhs.y(); + const auto rz = rhs.z(); + + // Z is now outer scale, so put it in largest blocks + if (lz != rz) { + return lz < rz; + } + if (ly != ry) { + return ly < ry; + } + return lx < rx; + }); + + return Region{indices}; +} + +template auto region_transpose(const Region& region) -> Region; +template auto region_transpose(const Region& region) -> Region; +template auto region_transpose(const Region& region) -> Region; + +// Matplotlib viridis colourmap +// Copyright Matplotlib Development Team +// SPDX-License-Identifier: BSD +constexpr std::array, 256> viridis_data = {{ + // clang-format off + {68, 1, 84}, + {68, 2, 85}, + {69, 3, 87}, + {69, 5, 88}, + {69, 6, 90}, + {70, 8, 91}, + {70, 9, 93}, + {70, 11, 94}, + {70, 12, 96}, + {71, 14, 97}, + {71, 15, 98}, + {71, 17, 100}, + {71, 18, 101}, + {71, 20, 102}, + {72, 21, 104}, + {72, 22, 105}, + {72, 24, 106}, + {72, 25, 108}, + {72, 26, 109}, + {72, 28, 110}, + {72, 29, 111}, + {72, 30, 112}, + {72, 32, 113}, + {72, 33, 115}, + {72, 34, 116}, + {72, 36, 117}, + {72, 37, 118}, + {72, 38, 119}, + {72, 39, 120}, + {71, 41, 121}, + {71, 42, 121}, + {71, 43, 122}, + {71, 44, 123}, + {71, 46, 124}, + {70, 47, 125}, + {70, 48, 126}, + {70, 49, 126}, + {70, 51, 127}, + {69, 52, 128}, + {69, 53, 129}, + {69, 54, 129}, + {68, 56, 130}, + {68, 57, 131}, + {68, 58, 131}, + {67, 59, 132}, + {67, 60, 132}, + {67, 62, 133}, + {66, 63, 133}, + {66, 64, 134}, + {65, 65, 134}, + {65, 66, 135}, + {65, 67, 135}, + {64, 69, 136}, + {64, 70, 136}, + {63, 71, 136}, + {63, 72, 137}, + {62, 73, 137}, + {62, 74, 137}, + {61, 75, 138}, + {61, 77, 138}, + {60, 78, 138}, + {60, 79, 138}, + {59, 80, 139}, + {59, 81, 139}, + {58, 82, 139}, + {58, 83, 139}, + {57, 84, 140}, + {57, 85, 140}, + {56, 86, 140}, + {56, 87, 140}, + {55, 88, 140}, + {55, 89, 140}, + {54, 91, 141}, + {54, 92, 141}, + {53, 93, 141}, + {53, 94, 141}, + {52, 95, 141}, + {52, 96, 141}, + {51, 97, 141}, + {51, 98, 141}, + {51, 99, 141}, + {50, 100, 142}, + {50, 101, 142}, + {49, 102, 142}, + {49, 103, 142}, + {48, 104, 142}, + {48, 105, 142}, + {47, 106, 142}, + {47, 107, 142}, + {47, 108, 142}, + {46, 109, 142}, + {46, 110, 142}, + {45, 111, 142}, + {45, 112, 142}, + {45, 112, 142}, + {44, 113, 142}, + {44, 114, 142}, + {43, 115, 142}, + {43, 116, 142}, + {43, 117, 142}, + {42, 118, 142}, + {42, 119, 142}, + {41, 120, 142}, + {41, 121, 142}, + {41, 122, 142}, + {40, 123, 142}, + {40, 124, 142}, + {40, 125, 142}, + {39, 126, 142}, + {39, 127, 142}, + {38, 128, 142}, + {38, 129, 142}, + {38, 130, 142}, + {37, 131, 142}, + {37, 131, 142}, + {37, 132, 142}, + {36, 133, 142}, + {36, 134, 142}, + {35, 135, 142}, + {35, 136, 142}, + {35, 137, 142}, + {34, 138, 141}, + {34, 139, 141}, + {34, 140, 141}, + {33, 141, 141}, + {33, 142, 141}, + {33, 143, 141}, + {32, 144, 141}, + {32, 145, 140}, + {32, 146, 140}, + {32, 147, 140}, + {31, 147, 140}, + {31, 148, 140}, + {31, 149, 139}, + {31, 150, 139}, + {31, 151, 139}, + {30, 152, 139}, + {30, 153, 138}, + {30, 154, 138}, + {30, 155, 138}, + {30, 156, 137}, + {30, 157, 137}, + {30, 158, 137}, + {30, 159, 136}, + {30, 160, 136}, + {31, 161, 136}, + {31, 162, 135}, + {31, 163, 135}, + {31, 163, 134}, + {32, 164, 134}, + {32, 165, 134}, + {33, 166, 133}, + {33, 167, 133}, + {34, 168, 132}, + {35, 169, 131}, + {35, 170, 131}, + {36, 171, 130}, + {37, 172, 130}, + {38, 173, 129}, + {39, 174, 129}, + {40, 175, 128}, + {41, 175, 127}, + {42, 176, 127}, + {43, 177, 126}, + {44, 178, 125}, + {46, 179, 124}, + {47, 180, 124}, + {48, 181, 123}, + {50, 182, 122}, + {51, 183, 121}, + {53, 183, 121}, + {54, 184, 120}, + {56, 185, 119}, + {57, 186, 118}, + {59, 187, 117}, + {61, 188, 116}, + {62, 189, 115}, + {64, 190, 114}, + {66, 190, 113}, + {68, 191, 112}, + {70, 192, 111}, + {72, 193, 110}, + {73, 194, 109}, + {75, 194, 108}, + {77, 195, 107}, + {79, 196, 106}, + {81, 197, 105}, + {83, 198, 104}, + {85, 198, 102}, + {88, 199, 101}, + {90, 200, 100}, + {92, 201, 99}, + {94, 201, 98}, + {96, 202, 96}, + {98, 203, 95}, + {101, 204, 94}, + {103, 204, 92}, + {105, 205, 91}, + {108, 206, 90}, + {110, 206, 88}, + {112, 207, 87}, + {115, 208, 85}, + {117, 208, 84}, + {119, 209, 82}, + {122, 210, 81}, + {124, 210, 79}, + {127, 211, 78}, + {129, 212, 76}, + {132, 212, 75}, + {134, 213, 73}, + {137, 213, 72}, + {139, 214, 70}, + {142, 215, 68}, + {144, 215, 67}, + {147, 216, 65}, + {149, 216, 63}, + {152, 217, 62}, + {155, 217, 60}, + {157, 218, 58}, + {160, 218, 57}, + {163, 219, 55}, + {165, 219, 53}, + {168, 220, 51}, + {171, 220, 50}, + {173, 221, 48}, + {176, 221, 46}, + {179, 221, 45}, + {181, 222, 43}, + {184, 222, 41}, + {187, 223, 39}, + {189, 223, 38}, + {192, 223, 36}, + {195, 224, 35}, + {197, 224, 33}, + {200, 225, 32}, + {203, 225, 30}, + {205, 225, 29}, + {208, 226, 28}, + {211, 226, 27}, + {213, 226, 26}, + {216, 227, 25}, + {219, 227, 24}, + {221, 227, 24}, + {224, 228, 24}, + {226, 228, 24}, + {229, 228, 24}, + {232, 229, 25}, + {234, 229, 25}, + {237, 229, 26}, + {239, 230, 27}, + {242, 230, 28}, + {244, 230, 30}, + {247, 230, 31}, + {249, 231, 33}, + {251, 231, 35}, + {254, 231, 36}, +}}; +// clang-format on + +constexpr auto colour_map_scaling = viridis_data.size() - 1; + +auto colour(BoutReal value, BoutReal min, BoutReal max) -> fmt::text_style { + if (std::isnan(value)) { + return fmt::fg(fmt::color::black); + } + + // Get value in range [0, 1] + const auto x = (value - min) / (max - min); + // Convert to range [0, 255] + const auto index = static_cast(x * colour_map_scaling); + const auto colour = viridis_data[index]; // NOLINT + + return fmt::fg(fmt::rgb(colour[0], colour[1], colour[2])); +} +} // namespace bout::details diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 7cf0bb0af7..b347de7354 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -121,6 +121,7 @@ set(serial_tests_source ./fake_mesh.hxx ./fake_mesh_fixture.hxx ./src/test_bout++.cxx + ./sys/test_output_bout_types.cxx ) if(BOUT_HAS_HYPRE) diff --git a/tests/unit/sys/test_output_bout_types.cxx b/tests/unit/sys/test_output_bout_types.cxx new file mode 100644 index 0000000000..1c4d6bdfe0 --- /dev/null +++ b/tests/unit/sys/test_output_bout_types.cxx @@ -0,0 +1,320 @@ +#include "fake_mesh_fixture.hxx" +#include "test_extras.hxx" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "bout/field2d.hxx" +#include "bout/field3d.hxx" +#include "bout/fieldperp.hxx" +#include "bout/globals.hxx" +#include "bout/output_bout_types.hxx" // IWYU pragma: keep + +#include + +#include +#include + +using FormatFieldTest = FakeMeshFixture_tmpl<3, 5, 2>; + +TEST_F(FormatFieldTest, DetailsRegionTranspose) { + const auto rgn_all = bout::globals::mesh->getRegion("RGN_ALL"); + const auto rgn_transpose = bout::details::region_transpose(rgn_all); + + std::vector> points{}; + points.reserve(rgn_all.size()); + + for (const auto i : rgn_transpose) { + points.push_back({i.x(), i.y(), i.z()}); + } + + const std::vector> expected = { + // clang-format off + {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, + {0, 1, 0}, {1, 1, 0}, {2, 1, 0}, + {0, 2, 0}, {1, 2, 0}, {2, 2, 0}, + {0, 3, 0}, {1, 3, 0}, {2, 3, 0}, + {0, 4, 0}, {1, 4, 0}, {2, 4, 0}, + + {0, 0, 1}, {1, 0, 1}, {2, 0, 1}, + {0, 1, 1}, {1, 1, 1}, {2, 1, 1}, + {0, 2, 1}, {1, 2, 1}, {2, 2, 1}, + {0, 3, 1}, {1, 3, 1}, {2, 3, 1}, + {0, 4, 1}, {1, 4, 1}, {2, 4, 1}, + // clang-format on + }; + + ASSERT_EQ(points, expected); +} + +TEST_F(FormatFieldTest, Field2D) { + Field2D f{bout::globals::mesh}; + + fillField(f, {{0., 1., 2., 3., 4.}, {5., 6., 7., 8., 9.}, {10., 11., 12., 13., 14.}}); + + const auto out = fmt::format("{}", f); + + const std::string expected = + R"((0, 0): 0; +(0, 1): 1; +(0, 2): 2; +(0, 3): 3; +(0, 4): 4; + +(1, 0): 5; +(1, 1): 6; +(1, 2): 7; +(1, 3): 8; +(1, 4): 9; + +(2, 0): 10; +(2, 1): 11; +(2, 2): 12; +(2, 3): 13; +(2, 4): 14;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, Field2DSpec) { + Field2D f{bout::globals::mesh}; + + fillField(f, {{0., 1., 2., 3., 4.}, {5., 6., 7., 8., 9.}, {10., 11., 12., 13., 14.}}); + + const auto out = fmt::format("{::3.1e}", f); + + const std::string expected = + R"((0, 0): 0.0e+00; +(0, 1): 1.0e+00; +(0, 2): 2.0e+00; +(0, 3): 3.0e+00; +(0, 4): 4.0e+00; + +(1, 0): 5.0e+00; +(1, 1): 6.0e+00; +(1, 2): 7.0e+00; +(1, 3): 8.0e+00; +(1, 4): 9.0e+00; + +(2, 0): 1.0e+01; +(2, 1): 1.1e+01; +(2, 2): 1.2e+01; +(2, 3): 1.3e+01; +(2, 4): 1.4e+01;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, Field2DTranspose) { + Field2D f{bout::globals::mesh}; + + fillField(f, {{0., 1., 2., 3., 4.}, {5., 6., 7., 8., 9.}, {10., 11., 12., 13., 14.}}); + + const auto out = fmt::format("{:T}", f); + + const std::string expected = + R"((0, 0): 0; (1, 0): 5; (2, 0): 10; +(0, 1): 1; (1, 1): 6; (2, 1): 11; +(0, 2): 2; (1, 2): 7; (2, 2): 12; +(0, 3): 3; (1, 3): 8; (2, 3): 13; +(0, 4): 4; (1, 4): 9; (2, 4): 14;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, Field3D) { + Field3D f{bout::globals::mesh}; + + fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, + {{10., 11}, {12., 13}, {14., 15}, {16., 17}, {18., 19}}, + {{20., 21}, {22., 23}, {24., 25}, {26., 27}, {28., 29}}}); + + const auto out = fmt::format("{}", f); + + const std::string expected = + R"((0, 0, 0): 0; (0, 0, 1): 1; +(0, 1, 0): 2; (0, 1, 1): 3; +(0, 2, 0): 4; (0, 2, 1): 5; +(0, 3, 0): 6; (0, 3, 1): 7; +(0, 4, 0): 8; (0, 4, 1): 9; + +(1, 0, 0): 10; (1, 0, 1): 11; +(1, 1, 0): 12; (1, 1, 1): 13; +(1, 2, 0): 14; (1, 2, 1): 15; +(1, 3, 0): 16; (1, 3, 1): 17; +(1, 4, 0): 18; (1, 4, 1): 19; + +(2, 0, 0): 20; (2, 0, 1): 21; +(2, 1, 0): 22; (2, 1, 1): 23; +(2, 2, 0): 24; (2, 2, 1): 25; +(2, 3, 0): 26; (2, 3, 1): 27; +(2, 4, 0): 28; (2, 4, 1): 29;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, Field3DSpec) { + Field3D f{bout::globals::mesh}; + + fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, + {{10., 11}, {12., 13}, {14., 15}, {16., 17}, {18., 19}}, + {{20., 21}, {22., 23}, {24., 25}, {26., 27}, {28., 29}}}); + + const auto out = fmt::format("{::3.1e}", f); + + const std::string expected = + R"((0, 0, 0): 0.0e+00; (0, 0, 1): 1.0e+00; +(0, 1, 0): 2.0e+00; (0, 1, 1): 3.0e+00; +(0, 2, 0): 4.0e+00; (0, 2, 1): 5.0e+00; +(0, 3, 0): 6.0e+00; (0, 3, 1): 7.0e+00; +(0, 4, 0): 8.0e+00; (0, 4, 1): 9.0e+00; + +(1, 0, 0): 1.0e+01; (1, 0, 1): 1.1e+01; +(1, 1, 0): 1.2e+01; (1, 1, 1): 1.3e+01; +(1, 2, 0): 1.4e+01; (1, 2, 1): 1.5e+01; +(1, 3, 0): 1.6e+01; (1, 3, 1): 1.7e+01; +(1, 4, 0): 1.8e+01; (1, 4, 1): 1.9e+01; + +(2, 0, 0): 2.0e+01; (2, 0, 1): 2.1e+01; +(2, 1, 0): 2.2e+01; (2, 1, 1): 2.3e+01; +(2, 2, 0): 2.4e+01; (2, 2, 1): 2.5e+01; +(2, 3, 0): 2.6e+01; (2, 3, 1): 2.7e+01; +(2, 4, 0): 2.8e+01; (2, 4, 1): 2.9e+01;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, Field3DRegionSpec) { + Field3D f{bout::globals::mesh}; + + fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, + {{10., 11}, {12., 13}, {14., 15}, {16., 17}, {18., 19}}, + {{20., 21}, {22., 23}, {24., 25}, {26., 27}, {28., 29}}}); + + const auto out = fmt::format("{:r'RGN_NOX':3.1e}", f); + + const std::string expected = + R"((1, 0, 0): 1.0e+01; (1, 0, 1): 1.1e+01; +(1, 1, 0): 1.2e+01; (1, 1, 1): 1.3e+01; +(1, 2, 0): 1.4e+01; (1, 2, 1): 1.5e+01; +(1, 3, 0): 1.6e+01; (1, 3, 1): 1.7e+01; +(1, 4, 0): 1.8e+01; (1, 4, 1): 1.9e+01;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, Field3DNoIndices) { + Field3D f{bout::globals::mesh}; + + fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, + {{10., 11}, {12., 13}, {14., 15}, {16., 17}, {18., 19}}, + {{20., 21}, {22., 23}, {24., 25}, {26., 27}, {28., 29}}}); + + const auto out = fmt::format("{:n}", f); + + const std::string expected = + R"(0; 1; +2; 3; +4; 5; +6; 7; +8; 9; + +10; 11; +12; 13; +14; 15; +16; 17; +18; 19; + +20; 21; +22; 23; +24; 25; +26; 27; +28; 29;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, Field3DTranspose) { + Field3D f{bout::globals::mesh}; + + fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, + {{10., 11}, {12., 13}, {14., 15}, {16., 17}, {18., 19}}, + {{20., 21}, {22., 23}, {24., 25}, {26., 27}, {28., 29}}}); + + const auto out = fmt::format("{:T}", f); + + const std::string expected = + R"((0, 0, 0): 0; (1, 0, 0): 10; (2, 0, 0): 20; +(0, 1, 0): 2; (1, 1, 0): 12; (2, 1, 0): 22; +(0, 2, 0): 4; (1, 2, 0): 14; (2, 2, 0): 24; +(0, 3, 0): 6; (1, 3, 0): 16; (2, 3, 0): 26; +(0, 4, 0): 8; (1, 4, 0): 18; (2, 4, 0): 28; + +(0, 0, 1): 1; (1, 0, 1): 11; (2, 0, 1): 21; +(0, 1, 1): 3; (1, 1, 1): 13; (2, 1, 1): 23; +(0, 2, 1): 5; (1, 2, 1): 15; (2, 2, 1): 25; +(0, 3, 1): 7; (1, 3, 1): 17; (2, 3, 1): 27; +(0, 4, 1): 9; (1, 4, 1): 19; (2, 4, 1): 29;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, FieldPerp) { + Field3D f{bout::globals::mesh}; + + fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, + {{10., 11}, {12., 13}, {14., 15}, {16., 17}, {18., 19}}, + {{20., 21}, {22., 23}, {24., 25}, {26., 27}, {28., 29}}}); + + FieldPerp g = sliceXZ(f, 0); + + const auto out = fmt::format("{}", g); + + const std::string expected = + R"((0, 0): 0; (0, 1): 1; + +(1, 0): 10; (1, 1): 11; + +(2, 0): 20; (2, 1): 21;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTest, FieldPerpSpec) { + Field3D f{bout::globals::mesh}; + + fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, + {{10., 11}, {12., 13}, {14., 15}, {16., 17}, {18., 19}}, + {{20., 21}, {22., 23}, {24., 25}, {26., 27}, {28., 29}}}); + + FieldPerp g = sliceXZ(f, 0); + + const auto out = fmt::format("{::3.1e}", g); + + const std::string expected = + R"((0, 0): 0.0e+00; (0, 1): 1.0e+00; + +(1, 0): 1.0e+01; (1, 1): 1.1e+01; + +(2, 0): 2.0e+01; (2, 1): 2.1e+01;)"; + EXPECT_EQ(out, expected); +} + +using FormatFieldTestLargerMesh = FakeMeshFixture_tmpl<10, 10, 10>; + +TEST_F(FormatFieldTestLargerMesh, Field3DEdges) { + Field3D f{1., bout::globals::mesh}; + + const auto out = fmt::format("{:e1}", f); + + const std::string expected = + R"((0, 0, 0): 1; ... (0, 0, 9): 1; +... +(0, 9, 0): 1; ... (0, 9, 9): 1; + +... + +(9, 0, 0): 1; ... (9, 0, 9): 1; +... +(9, 9, 0): 1; ... (9, 9, 9): 1;)"; + EXPECT_EQ(out, expected); +} + +TEST_F(FormatFieldTestLargerMesh, Field3DFull) { + Field3D f{1., bout::globals::mesh}; + + const auto out = fmt::format("{:f}", f); + + using namespace ::testing; + EXPECT_THAT(out, Not(HasSubstr("..."))); +}