From 0d390ff1c56d2ae70d46790a1ee511a8fc7ddf0a Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 24 Feb 2026 17:46:12 +0000 Subject: [PATCH 1/8] Add basic custom formatter for fields --- include/bout/output_bout_types.hxx | 64 ++++++++- tests/unit/CMakeLists.txt | 1 + tests/unit/sys/test_output_bout_types.cxx | 151 ++++++++++++++++++++++ 3 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 tests/unit/sys/test_output_bout_types.cxx diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index b67762521b..43d1b4adfa 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -5,9 +5,13 @@ #ifndef OUTPUT_BOUT_TYPES_H #define OUTPUT_BOUT_TYPES_H +#include "fmt/base.h" #include -#include "bout/output.hxx" +#include "bout/bout_types.hxx" +#include "bout/field2d.hxx" +#include "bout/mesh.hxx" +#include "bout/output.hxx" // IWYU pragma: keep #include "bout/region.hxx" template @@ -17,7 +21,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 +55,59 @@ struct fmt::formatter> { } }; +class Field2D; +class Field3D; +class FieldPerp; + +template <> +struct fmt::formatter : fmt::formatter { + auto format(const Field2D& f, format_context& ctx) const -> format_context::iterator { + const auto* mesh = f.getMesh(); + for (int ix = 0; ix < mesh->LocalNx; ++ix) { + for (int jy = 0; jy < mesh->LocalNy; ++jy) { + format_to(ctx.out(), "({}, {}): ", ix, jy); + formatter::format(f(ix, jy), ctx); + format_to(ctx.out(), (jy < mesh->LocalNy - 1) ? "; " : ";"); + } + format_to(ctx.out(), "\n"); + } + return format_to(ctx.out(), "\n"); + } +}; + +template <> +struct fmt::formatter : fmt::formatter { + auto format(const Field3D& f, format_context& ctx) const -> format_context::iterator { + const auto* mesh = f.getMesh(); + for (int ix = 0; ix < mesh->LocalNx; ++ix) { + for (int jy = 0; jy < mesh->LocalNy; ++jy) { + for (int kz = 0; kz < mesh->LocalNz; ++kz) { + format_to(ctx.out(), "({}, {}, {}): ", ix, jy, kz); + formatter::format(f(ix, jy, kz), ctx); + format_to(ctx.out(), (kz < mesh->LocalNz - 1) ? "; " : ";"); + } + format_to(ctx.out(), "\n"); + } + format_to(ctx.out(), "\n"); + } + return format_to(ctx.out(), "\n"); + } +}; + +template <> +struct fmt::formatter : fmt::formatter { + auto format(const FieldPerp& f, format_context& ctx) const -> format_context::iterator { + const auto* mesh = f.getMesh(); + for (int ix = 0; ix < mesh->LocalNx; ++ix) { + for (int kz = 0; kz < mesh->LocalNz; ++kz) { + format_to(ctx.out(), "({}, {}): ", ix, kz); + formatter::format(f(ix, kz), ctx); + format_to(ctx.out(), (kz < mesh->LocalNz - 1) ? "; " : ";"); + } + format_to(ctx.out(), "\n"); + } + return format_to(ctx.out(), "\n"); + } +}; + #endif // OUTPUT_BOUT_TYPES_H 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..b3a7493803 --- /dev/null +++ b/tests/unit/sys/test_output_bout_types.cxx @@ -0,0 +1,151 @@ +#include "fake_mesh_fixture.hxx" +#include "test_extras.hxx" +#include "gtest/gtest.h" + +#include "bout/field2d.hxx" +#include "bout/globals.hxx" +#include "bout/output_bout_types.hxx" + +#include + +#include + +using FormatFieldTest = FakeMeshFixture_tmpl<3, 5, 2>; + +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, 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, 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); +} From ccea15a1dc33f8da9ff43b1795a86f5ac18f79ad Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 24 Feb 2026 21:08:22 +0000 Subject: [PATCH 2/8] Consolidate Field formatters into one implementation --- include/bout/output_bout_types.hxx | 82 +++++++++-------------- tests/unit/sys/test_output_bout_types.cxx | 66 +++++++++++------- 2 files changed, 75 insertions(+), 73 deletions(-) diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index 43d1b4adfa..a62f22537a 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -5,14 +5,16 @@ #ifndef OUTPUT_BOUT_TYPES_H #define OUTPUT_BOUT_TYPES_H -#include "fmt/base.h" -#include - #include "bout/bout_types.hxx" -#include "bout/field2d.hxx" #include "bout/mesh.hxx" #include "bout/output.hxx" // IWYU pragma: keep #include "bout/region.hxx" +#include "bout/traits.hxx" + +#include "fmt/base.h" +#include + +#include template struct fmt::formatter> { @@ -55,58 +57,38 @@ struct fmt::formatter> { } }; -class Field2D; -class Field3D; -class FieldPerp; - -template <> -struct fmt::formatter : fmt::formatter { - auto format(const Field2D& f, format_context& ctx) const -> format_context::iterator { +/// Formatter for Fields +template +struct fmt::formatter, char>> : fmt::formatter { + auto format(const T& f, format_context& ctx) const -> format_context::iterator { const auto* mesh = f.getMesh(); - for (int ix = 0; ix < mesh->LocalNx; ++ix) { - for (int jy = 0; jy < mesh->LocalNy; ++jy) { - format_to(ctx.out(), "({}, {}): ", ix, jy); - formatter::format(f(ix, jy), ctx); - format_to(ctx.out(), (jy < mesh->LocalNy - 1) ? "; " : ";"); - } - format_to(ctx.out(), "\n"); - } - return format_to(ctx.out(), "\n"); - } -}; + int previous_x = 0; + int previous_y = 0; + int previous_z = 0; -template <> -struct fmt::formatter : fmt::formatter { - auto format(const Field3D& f, format_context& ctx) const -> format_context::iterator { - const auto* mesh = f.getMesh(); - for (int ix = 0; ix < mesh->LocalNx; ++ix) { - for (int jy = 0; jy < mesh->LocalNy; ++jy) { - for (int kz = 0; kz < mesh->LocalNz; ++kz) { - format_to(ctx.out(), "({}, {}, {}): ", ix, jy, kz); - formatter::format(f(ix, jy, kz), ctx); - format_to(ctx.out(), (kz < mesh->LocalNz - 1) ? "; " : ";"); - } + BOUT_FOR(i, f.getRegion("RGN_ALL")) { + const auto ix = i.x(); + const auto iy = i.y(); + const auto iz = i.z(); + + if (iz > previous_z) { + format_to(ctx.out(), " "); + } + if (iy > previous_y) { format_to(ctx.out(), "\n"); } - format_to(ctx.out(), "\n"); - } - return format_to(ctx.out(), "\n"); - } -}; - -template <> -struct fmt::formatter : fmt::formatter { - auto format(const FieldPerp& f, format_context& ctx) const -> format_context::iterator { - const auto* mesh = f.getMesh(); - for (int ix = 0; ix < mesh->LocalNx; ++ix) { - for (int kz = 0; kz < mesh->LocalNz; ++kz) { - format_to(ctx.out(), "({}, {}): ", ix, kz); - formatter::format(f(ix, kz), ctx); - format_to(ctx.out(), (kz < mesh->LocalNz - 1) ? "; " : ";"); + if (ix > previous_x) { + format_to(ctx.out(), "\n\n"); } - format_to(ctx.out(), "\n"); + + format_to(ctx.out(), "{:c}: ", i); + formatter::format(f[i], ctx); + format_to(ctx.out(), ";"); + previous_x = ix; + previous_y = iy; + previous_z = iz; } - return format_to(ctx.out(), "\n"); + return format_to(ctx.out(), ""); } }; diff --git a/tests/unit/sys/test_output_bout_types.cxx b/tests/unit/sys/test_output_bout_types.cxx index b3a7493803..82eb1754d0 100644 --- a/tests/unit/sys/test_output_bout_types.cxx +++ b/tests/unit/sys/test_output_bout_types.cxx @@ -3,8 +3,10 @@ #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" +#include "bout/output_bout_types.hxx" // IWYU pragma: keep #include @@ -20,11 +22,23 @@ TEST_F(FormatFieldTest, Field2D) { 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; - -)"; + 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); } @@ -36,11 +50,23 @@ TEST_F(FormatFieldTest, Field2DSpec) { 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; - -)"; + 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); } @@ -70,10 +96,7 @@ TEST_F(FormatFieldTest, Field3D) { (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; - - -)"; +(2, 4, 0): 28; (2, 4, 1): 29;)"; EXPECT_EQ(out, expected); } @@ -103,10 +126,7 @@ TEST_F(FormatFieldTest, Field3DSpec) { (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; - - -)"; +(2, 4, 0): 2.8e+01; (2, 4, 1): 2.9e+01;)"; EXPECT_EQ(out, expected); } @@ -123,10 +143,10 @@ TEST_F(FormatFieldTest, FieldPerp) { const std::string expected = R"((0, 0): 0; (0, 1): 1; + (1, 0): 10; (1, 1): 11; -(2, 0): 20; (2, 1): 21; -)"; +(2, 0): 20; (2, 1): 21;)"; EXPECT_EQ(out, expected); } @@ -143,9 +163,9 @@ TEST_F(FormatFieldTest, FieldPerpSpec) { 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; -)"; +(2, 0): 2.0e+01; (2, 1): 2.1e+01;)"; EXPECT_EQ(out, expected); } From a0ca7b301357cc3f419d71148feacef6f81d2bcc Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 24 Feb 2026 21:45:36 +0000 Subject: [PATCH 3/8] Add region to Field format spec --- include/bout/output_bout_types.hxx | 65 ++++++++++++++++++++--- tests/unit/sys/test_output_bout_types.cxx | 25 +++++++-- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index a62f22537a..b97811751c 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -11,9 +11,12 @@ #include "bout/region.hxx" #include "bout/traits.hxx" -#include "fmt/base.h" +#include #include +#include +#include +#include #include template @@ -59,14 +62,62 @@ struct fmt::formatter> { /// Formatter for Fields template -struct fmt::formatter, char>> : fmt::formatter { +struct fmt::formatter, char>> { +private: + fmt::formatter underlying; + + static constexpr auto default_region = "RGN_ALL"; + std::string_view region = default_region; + +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); + } + + // Other cases handled explicitly below + // NOLINTNEXTLINE(bugprone-switch-missing-default-case) + switch (*it) { + 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; + } + + if (it != end && *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 { const auto* mesh = f.getMesh(); - int previous_x = 0; - int previous_y = 0; - int previous_z = 0; - BOUT_FOR(i, f.getRegion("RGN_ALL")) { + const auto rgn = f.getRegion(std::string(region)); + const auto i = rgn.begin(); + int previous_x = i->x(); + int previous_y = i->y(); + int previous_z = i->z(); + + BOUT_FOR(i, rgn) { const auto ix = i.x(); const auto iy = i.y(); const auto iz = i.z(); @@ -82,7 +133,7 @@ struct fmt::formatter, char>> : f } format_to(ctx.out(), "{:c}: ", i); - formatter::format(f[i], ctx); + underlying.format(f[i], ctx); format_to(ctx.out(), ";"); previous_x = ix; previous_y = iy; diff --git a/tests/unit/sys/test_output_bout_types.cxx b/tests/unit/sys/test_output_bout_types.cxx index 82eb1754d0..dc7292f555 100644 --- a/tests/unit/sys/test_output_bout_types.cxx +++ b/tests/unit/sys/test_output_bout_types.cxx @@ -47,7 +47,7 @@ TEST_F(FormatFieldTest, Field2DSpec) { 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 auto out = fmt::format("{::3.1e}", f); const std::string expected = R"((0, 0): 0.0e+00; @@ -107,7 +107,7 @@ TEST_F(FormatFieldTest, Field3DSpec) { {{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 auto out = fmt::format("{::3.1e}", f); const std::string expected = R"((0, 0, 0): 0.0e+00; (0, 0, 1): 1.0e+00; @@ -130,6 +130,25 @@ TEST_F(FormatFieldTest, Field3DSpec) { 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, FieldPerp) { Field3D f{bout::globals::mesh}; @@ -159,7 +178,7 @@ TEST_F(FormatFieldTest, FieldPerpSpec) { FieldPerp g = sliceXZ(f, 0); - const auto out = fmt::format("{:3.1e}", g); + const auto out = fmt::format("{::3.1e}", g); const std::string expected = R"((0, 0): 0.0e+00; (0, 1): 1.0e+00; From ef8555cf984dce3de560392c69eb53d9f8e2958c Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 25 Feb 2026 13:46:47 +0000 Subject: [PATCH 4/8] Add field format spec to not print indices --- include/bout/output_bout_types.hxx | 51 +++++++++++++++-------- tests/unit/sys/test_output_bout_types.cxx | 30 +++++++++++++ 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index b97811751c..4a6542a5eb 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -61,6 +61,11 @@ struct fmt::formatter> { }; /// Formatter for Fields +/// +/// Format specification: +/// +/// - ``n``: Don't show indices +/// - ``r''``: Use given region (default: ``RGN_ALL``) template struct fmt::formatter, char>> { private: @@ -69,6 +74,8 @@ private: static constexpr auto default_region = "RGN_ALL"; std::string_view region = default_region; + bool show_indices = true; + public: constexpr auto parse(format_parse_context& ctx) { const auto* it = ctx.begin(); @@ -78,26 +85,34 @@ public: return underlying.parse(ctx); } - // Other cases handled explicitly below - // NOLINTNEXTLINE(bugprone-switch-missing-default-case) - switch (*it) { - 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; + while (it != end and *it != ':' and *it != '}') { + // Other cases handled explicitly below + // NOLINTNEXTLINE(bugprone-switch-missing-default-case) + switch (*it) { + 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; } - region = std::string_view(rgn_start, size); - ++it; - break; } - if (it != end && *it != '}') { + if (it != end and *it != '}') { if (*it != ':') { throw fmt::format_error("invalid format specifier"); } @@ -132,7 +147,9 @@ public: format_to(ctx.out(), "\n\n"); } - format_to(ctx.out(), "{:c}: ", i); + if (show_indices) { + format_to(ctx.out(), "{:c}: ", i); + } underlying.format(f[i], ctx); format_to(ctx.out(), ";"); previous_x = ix; diff --git a/tests/unit/sys/test_output_bout_types.cxx b/tests/unit/sys/test_output_bout_types.cxx index dc7292f555..a9c447091c 100644 --- a/tests/unit/sys/test_output_bout_types.cxx +++ b/tests/unit/sys/test_output_bout_types.cxx @@ -149,6 +149,36 @@ TEST_F(FormatFieldTest, Field3DRegionSpec) { EXPECT_EQ(out, expected); } +TEST_F(FormatFieldTest, NoIndices) { + 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, FieldPerp) { Field3D f{bout::globals::mesh}; From 3605c4faac997dde634e132006aa1c41b67a5f1f Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 25 Feb 2026 16:06:29 +0000 Subject: [PATCH 5/8] Add field format spec to transpose field --- CMakeLists.txt | 1 + include/bout/output_bout_types.hxx | 24 ++++++- src/sys/output_bout_types.cxx | 39 +++++++++++ tests/unit/sys/test_output_bout_types.cxx | 82 +++++++++++++++++++++-- 4 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 src/sys/output_bout_types.cxx 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 4a6542a5eb..90b9d2789d 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -60,12 +60,23 @@ 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; +} +} // 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 template struct fmt::formatter, char>> { private: @@ -75,6 +86,7 @@ private: std::string_view region = default_region; bool show_indices = true; + bool transpose = false; public: constexpr auto parse(format_parse_context& ctx) { @@ -109,6 +121,10 @@ public: show_indices = false; ++it; break; + case 'T': + transpose = true; + ++it; + break; } } @@ -126,7 +142,9 @@ public: auto format(const T& f, format_context& ctx) const -> format_context::iterator { const auto* mesh = f.getMesh(); - const auto rgn = f.getRegion(std::string(region)); + const auto rgn_ = f.getRegion(std::string(region)); + const auto rgn = transpose ? bout::details::region_transpose(rgn_) : rgn_; + const auto i = rgn.begin(); int previous_x = i->x(); int previous_y = i->y(); @@ -138,13 +156,13 @@ public: const auto iz = i.z(); if (iz > previous_z) { - format_to(ctx.out(), " "); + format_to(ctx.out(), transpose ? "\n\n" : " "); } if (iy > previous_y) { format_to(ctx.out(), "\n"); } if (ix > previous_x) { - format_to(ctx.out(), "\n\n"); + format_to(ctx.out(), transpose ? " " : "\n\n"); } if (show_indices) { diff --git a/src/sys/output_bout_types.cxx b/src/sys/output_bout_types.cxx new file mode 100644 index 0000000000..ed76cd2b13 --- /dev/null +++ b/src/sys/output_bout_types.cxx @@ -0,0 +1,39 @@ +#include "bout/output_bout_types.hxx" + +#include "bout/mesh.hxx" +#include "bout/region.hxx" + +#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; +} // namespace bout::details diff --git a/tests/unit/sys/test_output_bout_types.cxx b/tests/unit/sys/test_output_bout_types.cxx index a9c447091c..8a9da618bf 100644 --- a/tests/unit/sys/test_output_bout_types.cxx +++ b/tests/unit/sys/test_output_bout_types.cxx @@ -8,12 +8,43 @@ #include "bout/globals.hxx" #include "bout/output_bout_types.hxx" // IWYU pragma: keep -#include - #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}; @@ -22,7 +53,7 @@ TEST_F(FormatFieldTest, Field2D) { const auto out = fmt::format("{}", f); const std::string expected = - R"((0, 0): 0; + R"((0, 0): 0; (0, 1): 1; (0, 2): 2; (0, 3): 3; @@ -50,7 +81,7 @@ TEST_F(FormatFieldTest, Field2DSpec) { const auto out = fmt::format("{::3.1e}", f); const std::string expected = - R"((0, 0): 0.0e+00; + R"((0, 0): 0.0e+00; (0, 1): 1.0e+00; (0, 2): 2.0e+00; (0, 3): 3.0e+00; @@ -70,6 +101,22 @@ TEST_F(FormatFieldTest, Field2DSpec) { 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}; @@ -130,7 +177,6 @@ TEST_F(FormatFieldTest, Field3DSpec) { EXPECT_EQ(out, expected); } - TEST_F(FormatFieldTest, Field3DRegionSpec) { Field3D f{bout::globals::mesh}; @@ -149,7 +195,7 @@ TEST_F(FormatFieldTest, Field3DRegionSpec) { EXPECT_EQ(out, expected); } -TEST_F(FormatFieldTest, NoIndices) { +TEST_F(FormatFieldTest, Field3DNoIndices) { Field3D f{bout::globals::mesh}; fillField(f, {{{0., 1}, {2., 3}, {4., 5}, {6., 7}, {8., 9}}, @@ -179,6 +225,30 @@ TEST_F(FormatFieldTest, NoIndices) { 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}; From a3665051eef26a462106f45bab1bd866585af2bc Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 25 Feb 2026 17:14:58 +0000 Subject: [PATCH 6/8] Add very basic plotting for fields --- include/bout/output_bout_types.hxx | 43 ++++- src/sys/output_bout_types.cxx | 287 +++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+), 7 deletions(-) diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index 90b9d2789d..9430262f81 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -12,6 +12,7 @@ #include "bout/traits.hxx" #include +#include #include #include @@ -67,7 +68,9 @@ template /// /// 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; +} // namespace details } // namespace bout /// Formatter for Fields @@ -77,6 +80,7 @@ auto region_transpose(const Region& region) -> Region; /// - ``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 template struct fmt::formatter, char>> { private: @@ -87,6 +91,7 @@ private: bool show_indices = true; bool transpose = false; + bool plot = false; public: constexpr auto parse(format_parse_context& ctx) { @@ -125,6 +130,11 @@ public: transpose = true; ++it; break; + case '#': + plot = true; + show_indices = false; + ++it; + break; } } @@ -140,36 +150,55 @@ public: } auto format(const T& f, format_context& ctx) const -> format_context::iterator { + using namespace bout::details; + const auto* mesh = f.getMesh(); - const auto rgn_ = f.getRegion(std::string(region)); - const auto rgn = transpose ? bout::details::region_transpose(rgn_) : rgn_; + 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(); + // 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 + const auto* const block_sep = "\n\n"; + const auto* const item_sep = plot ? "" : " "; + BOUT_FOR(i, rgn) { const auto ix = i.x(); const auto iy = i.y(); const auto iz = i.z(); if (iz > previous_z) { - format_to(ctx.out(), transpose ? "\n\n" : " "); + format_to(ctx.out(), transpose ? block_sep : item_sep); } if (iy > previous_y) { format_to(ctx.out(), "\n"); } if (ix > previous_x) { - format_to(ctx.out(), transpose ? " " : "\n\n"); + format_to(ctx.out(), transpose ? item_sep : block_sep); } if (show_indices) { format_to(ctx.out(), "{:c}: ", i); } - underlying.format(f[i], ctx); - format_to(ctx.out(), ";"); + if (plot) { + format_to(ctx.out(), "{}", styled("█", colour(f[i], plot_min, plot_max))); + } else { + underlying.format(f[i], ctx); + format_to(ctx.out(), ";"); + } previous_x = ix; previous_y = iy; previous_z = iz; diff --git a/src/sys/output_bout_types.cxx b/src/sys/output_bout_types.cxx index ed76cd2b13..41a2055dfc 100644 --- a/src/sys/output_bout_types.cxx +++ b/src/sys/output_bout_types.cxx @@ -1,9 +1,16 @@ #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 { @@ -36,4 +43,284 @@ auto region_transpose(const Region& region) -> Region { 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 From d5fa1220dc3344b0912c940f3f60876cdb8a36be Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 25 Feb 2026 18:13:46 +0000 Subject: [PATCH 7/8] Add field format truncation, like numpy --- include/bout/output_bout_types.hxx | 85 +++++++++++++++++++++-- tests/unit/sys/test_output_bout_types.cxx | 30 ++++++++ 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index 9430262f81..bf222fbec6 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -70,6 +71,37 @@ template 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 @@ -81,6 +113,8 @@ auto colour(BoutReal value, BoutReal min, BoutReal max) -> fmt::text_style; /// - ``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: @@ -92,6 +126,8 @@ private: 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) { @@ -106,6 +142,17 @@ public: // 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 != '\'') { @@ -163,6 +210,19 @@ public: 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; @@ -172,32 +232,45 @@ public: } // Separators - const auto* const block_sep = "\n\n"; + 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(i, rgn) { const auto ix = i.x(); const auto iy = i.y(); const auto iz = i.z(); - if (iz > previous_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 (iy > previous_y) { + if ((not shown_skip or should_show) and iy > previous_y) { format_to(ctx.out(), "\n"); } - if (ix > previous_x) { + if ((not shown_skip or should_show) and ix > previous_x) { format_to(ctx.out(), transpose ? item_sep : block_sep); } - if (show_indices) { + 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 { + } 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; diff --git a/tests/unit/sys/test_output_bout_types.cxx b/tests/unit/sys/test_output_bout_types.cxx index 8a9da618bf..1c4d6bdfe0 100644 --- a/tests/unit/sys/test_output_bout_types.cxx +++ b/tests/unit/sys/test_output_bout_types.cxx @@ -1,5 +1,6 @@ #include "fake_mesh_fixture.hxx" #include "test_extras.hxx" +#include "gmock/gmock.h" #include "gtest/gtest.h" #include "bout/field2d.hxx" @@ -288,3 +289,32 @@ TEST_F(FormatFieldTest, FieldPerpSpec) { (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("..."))); +} From 58b3783c5f1e5e08e5024b956ebf063360a2992d Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 26 Feb 2026 10:37:17 +0000 Subject: [PATCH 8/8] Make sure formatting is done in serial --- include/bout/output_bout_types.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/output_bout_types.hxx b/include/bout/output_bout_types.hxx index bf222fbec6..a5c68dad9c 100644 --- a/include/bout/output_bout_types.hxx +++ b/include/bout/output_bout_types.hxx @@ -239,7 +239,7 @@ public: bool shown_skip = false; constexpr auto skip_sep = "..."; - BOUT_FOR(i, rgn) { + BOUT_FOR_SERIAL(i, rgn) { const auto ix = i.x(); const auto iy = i.y(); const auto iz = i.z();