diff --git a/.github/ci-hpc-config.yml b/.github/ci-hpc-config.yml index d23f54c..586856f 100644 --- a/.github/ci-hpc-config.yml +++ b/.github/ci-hpc-config.yml @@ -1,12 +1,41 @@ -build: - modules: - - ninja - dependencies: - - ecmwf/ecbuild@develop - - ecmwf/eckit@develop - - ecmwf/fckit@develop - - ecmwf/atlas@develop - - ecmwf/eccodes@develop - dependency_cmake_options: - - ecmwf/atlas:-DENABLE_FORTRAN=ON - parallel: 64 +matrix: + - mpi_on + - mpi_off + +mpi_on: + build: + modules: + - ninja + - openmpi + modules_package: + - atlas:openmpi + - eckit:openmpi + dependencies: + - ecmwf/ecbuild@develop + - ecmwf/eckit@develop + - ecmwf/fckit@develop + - ecmwf/atlas@develop + - ecmwf/eccodes@develop + dependency_cmake_options: + - ecmwf/atlas:-DENABLE_FORTRAN=ON + parallel: 64 + ntasks: 16 + env: + - CTEST_PARALLEL_LEVEL=1 + - OMPI_MCA_rmaps_base_oversubscribe=1 + - ECCODES_SAMPLES_PATH=$ECCODES_DIR/share/eccodes/samples + - ECCODES_DEFINITION_PATH=$ECCODES_DIR/share/eccodes/definitions + +mpi_off: + build: + modules: + - ninja + dependencies: + - ecmwf/ecbuild@develop + - ecmwf/eckit@develop + - ecmwf/fckit@develop + - ecmwf/atlas@develop + - ecmwf/eccodes@develop + dependency_cmake_options: + - ecmwf/atlas:-DENABLE_FORTRAN=ON + parallel: 64 diff --git a/src/nwp_emulator/README.md b/src/nwp_emulator/README.md index cddcdc7..3e38685 100644 --- a/src/nwp_emulator/README.md +++ b/src/nwp_emulator/README.md @@ -65,6 +65,7 @@ emulator: step: area: [71.5, -25, 34.5, 45] # rectangle represented by NW and SE (lat,lon) coordinates value: 10.0 + variation: 1.0 translation: [1.0, 1.0] # degrees of translation of the area per time step (lat, lon) "2": sinc: diff --git a/src/nwp_emulator/grib_file_reader.cc b/src/nwp_emulator/grib_file_reader.cc index dd2b4af..494a0f1 100644 --- a/src/nwp_emulator/grib_file_reader.cc +++ b/src/nwp_emulator/grib_file_reader.cc @@ -44,19 +44,25 @@ GRIBFileReader::GRIBFileReader(const eckit::PathName& inputPath, size_t rank, si gridName_ = std::string(gridNameBuffer.begin(), gridNameBuffer.end()); gridName_.resize(strlen(gridName_.c_str())); } - // 2. Field names (parameters) - size_t paramCount = params_.size(); - eckit::mpi::comm().broadcast(paramCount, root_); - std::vector paramBuffer(64); // Params short name are typically 1-4 chars - for (size_t i = 0; i < paramCount; ++i) { - paramBuffer.resize(64, '\0'); // clean param buffer - if (rank_ == root_) { - paramBuffer.assign(params_[i].begin(), params_[i].end()); + // 2. Field names & metadata (parameters) + std::vector paramBuffer; + if (rank_ == root_) { + for (size_t i = 0; i < params_.size(); i++) { + std::copy(params_[i].begin(), params_[i].end(), std::back_inserter(paramBuffer)); + paramBuffer.push_back(';'); } - eckit::mpi::comm().broadcast(paramBuffer, root_); - if (rank_ != root_) { - std::string param(paramBuffer.begin(), paramBuffer.end()); - param.resize(strlen(param.c_str())); // Trim trailing '\0's + paramBuffer.pop_back(); // remove last ';' separator + } + size_t paramSize = paramBuffer.size(); + eckit::mpi::comm().broadcast(paramSize, root_); + paramBuffer.resize(paramSize); + eckit::mpi::comm().broadcast(paramBuffer, root_); // broadcast a single string to limit communication + if (rank_ != root_) { + std::string paramBufferStr(paramBuffer.begin(), paramBuffer.end()); + paramBufferStr.resize(strlen(paramBufferStr.c_str())); + std::stringstream ss(paramBufferStr); + std::string param; + while (std::getline(ss, param, ';')) { params_.push_back(param); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 909b748..df3b438 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,5 +7,10 @@ # granted to it by virtue of its status as an intergovernmental organisation nor # does it submit to any jurisdiction. +if( NOT DEFINED MPI_SLOTS ) + set( MPI_SLOTS 9999 ) +endif() + add_subdirectory(core) -add_subdirectory(api) \ No newline at end of file +add_subdirectory(api) +add_subdirectory(nwp_emulator) \ No newline at end of file diff --git a/tests/nwp_emulator/CMakeLists.txt b/tests/nwp_emulator/CMakeLists.txt new file mode 100644 index 0000000..6b6d0be --- /dev/null +++ b/tests/nwp_emulator/CMakeLists.txt @@ -0,0 +1,63 @@ +# (C) Copyright 2025- ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation nor +# does it submit to any jurisdiction. +# Test files repository on Nexus +set(ECBUILD_DOWNLOAD_BASE_URL https://get.ecmwf.int/repository/plume-test-data) + +set(test_nwp_emulator_files_dir ${CMAKE_BINARY_DIR}/tests/nwp_emulator/data) +add_custom_target( + make_nwp_emulator_test_data_dir ALL COMMAND ${CMAKE_COMMAND} -E make_directory ${test_nwp_emulator_files_dir} +) + +ecbuild_get_test_multidata( + TARGET test_nwp_emulator_files + NAMES + model_data_1.grib + model_data_2.grib + DIRNAME + nwp_emulator + DIRLOCAL + ${test_nwp_emulator_files_dir} + NOCHECK +) + +ecbuild_add_test( + TARGET plume_test_nwp_grib + SOURCES test_grib_reader.cc + LIBS plume_nwp_emulator + ENVIRONMENT TEST_DATA_DIR=${test_nwp_emulator_files_dir} + MPI 3 + CONDITION eckit_HAVE_MPI + TEST_DEPENDS test_nwp_emulator_files +) + +ecbuild_add_test( + TARGET plume_test_nwp_config + SOURCES test_config_reader.cc + LIBS plume_nwp_emulator + ENVIRONMENT TEST_DATA_DIR=${CMAKE_CURRENT_SOURCE_DIR}/data/ + MPI 3 + CONDITION eckit_HAVE_MPI +) + +ecbuild_add_library( + TARGET nwp_emulator_test_plugin + SOURCES + nwp_emulator_plugin.h + nwp_emulator_plugin.cc + PRIVATE_LIBS + plume_plugin +) + +ecbuild_add_test( + TARGET plume_test_nwp_tool + LIBS simple_plugin nwp_emulator_test_plugin + COMMAND nwp_emulator_run.x + ARGS --config-src=${CMAKE_CURRENT_SOURCE_DIR}/data/valid_config.yml + --plume-cfg=${CMAKE_CURRENT_SOURCE_DIR}/data/plume_config.yml +) \ No newline at end of file diff --git a/tests/nwp_emulator/data/invalid_config.yml b/tests/nwp_emulator/data/invalid_config.yml new file mode 100644 index 0000000..83742c9 --- /dev/null +++ b/tests/nwp_emulator/data/invalid_config.yml @@ -0,0 +1 @@ +no_emulator_key: -1 \ No newline at end of file diff --git a/tests/nwp_emulator/data/plume_config.yml b/tests/nwp_emulator/data/plume_config.yml new file mode 100644 index 0000000..544090d --- /dev/null +++ b/tests/nwp_emulator/data/plume_config.yml @@ -0,0 +1,14 @@ +{ + "plugins": [ + { + "name": "SimplePlugin", + "lib": "simple_plugin", + "plugincore-config": {} + }, + { + "name": "NWPEmulatorPlugin", + "lib": "nwp_emulator_test_plugin", + "plugincore-config": {} + } + ] +} \ No newline at end of file diff --git a/tests/nwp_emulator/data/valid_config.yml b/tests/nwp_emulator/data/valid_config.yml new file mode 100644 index 0000000..9cc7550 --- /dev/null +++ b/tests/nwp_emulator/data/valid_config.yml @@ -0,0 +1,39 @@ +emulator: + n_steps: 2 + grid_identifier: "N80" + vertical_levels: 5 + fields: + 100u: + levtype: "sfc" + apply: + vortex_rollup: + area: [71.5, -25, 34.5, 45] + time_variation: 1.1 + u: + apply: + levels: + "2": + random: + distribution: "uniform" + min: 1.0 + max: 2.0 + step: + area: [71.5, -25, 34.5, 45] + value: 10.0 + variation: 1.0 + translation: [1.0, 1.0] + "1,3": + sinc: + modes: 3 + min: -1.0 + max: 10.0 + spread: 10.0 + sink: false + "4:": + gaussian: + modes: 2 + min: 1.0 + max: 2.0 + max_stddev: 3.0 + sink: true + v: "u" \ No newline at end of file diff --git a/tests/nwp_emulator/nwp_emulator_plugin.cc b/tests/nwp_emulator/nwp_emulator_plugin.cc new file mode 100644 index 0000000..0153f7b --- /dev/null +++ b/tests/nwp_emulator/nwp_emulator_plugin.cc @@ -0,0 +1,46 @@ +/* + * (C) Copyright 2025- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ +#include "nwp_emulator_plugin.h" +#include + + +namespace nwp_emulator_test_plugin { + +REGISTER_LIBRARY(NWPEmulatorPlugin) + +NWPEmulatorPlugin::NWPEmulatorPlugin() : Plugin("NWPEmulatorPlugin"){}; + +NWPEmulatorPlugin::~NWPEmulatorPlugin(){}; + +const NWPEmulatorPlugin& NWPEmulatorPlugin::instance() { + static NWPEmulatorPlugin instance; + return instance; +} +//-------------------------------------------------------------- + + +// NWPEmulatorPluginCore +static plume::PluginCoreBuilder runnable_plugincore_FooBuilder_; + +NWPEmulatorPluginCore::NWPEmulatorPluginCore(const eckit::Configuration& conf) : PluginCore(conf) {} + +NWPEmulatorPluginCore::~NWPEmulatorPluginCore() {} + +void NWPEmulatorPluginCore::run() { + eckit::Log::info() << "Consuming parameters " << modelData().getAtlasFieldShared("100u").name() << ", " + << modelData().getAtlasFieldShared("u").name() << ", " + << modelData().getAtlasFieldShared("v").name() << std::endl; +} + +//-------------------------------------------------------------- + + +} // namespace nwp_emulator_test_plugin diff --git a/tests/nwp_emulator/nwp_emulator_plugin.h b/tests/nwp_emulator/nwp_emulator_plugin.h new file mode 100644 index 0000000..79e41c7 --- /dev/null +++ b/tests/nwp_emulator/nwp_emulator_plugin.h @@ -0,0 +1,55 @@ +/* + * (C) Copyright 2025- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ +#include +#include "plume/Plugin.h" +#include "plume/PluginCore.h" + +namespace nwp_emulator_test_plugin { + + +// ------ Foo runnable plugincore that self-registers! ------- +class NWPEmulatorPluginCore : public plume::PluginCore { +public: + NWPEmulatorPluginCore(const eckit::Configuration& conf); + ~NWPEmulatorPluginCore(); + void run() override; + constexpr static const char* type() { return "nwpemulator-plugincore"; } +}; +// ------------------------------------------------------ + +// ------------------------------------------------------ +class NWPEmulatorPlugin : public plume::Plugin { + +public: + NWPEmulatorPlugin(); + ~NWPEmulatorPlugin(); + + plume::Protocol negotiate() override { + plume::Protocol protocol; + protocol.requireAtlasField("100u"); + protocol.requireAtlasField("u"); + protocol.requireAtlasField("v"); + + return protocol; + } + + // Return the static instance + static const NWPEmulatorPlugin& instance(); + + std::string version() const override { return "0.0.1-NWPEmulator"; } + + std::string gitsha1(unsigned int count) const override { return "undefined"; } + + virtual std::string plugincoreName() const override { return NWPEmulatorPluginCore::type(); } +}; +// ------------------------------------------------------ + +} // namespace nwp_emulator_test_plugin diff --git a/tests/nwp_emulator/test_config_reader.cc b/tests/nwp_emulator/test_config_reader.cc new file mode 100644 index 0000000..0f489cf --- /dev/null +++ b/tests/nwp_emulator/test_config_reader.cc @@ -0,0 +1,117 @@ +/* + * (C) Copyright 2025- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ +#include +#include + +#include "atlas/array/ArrayShape.h" +#include "atlas/util/function/VortexRollup.h" +#include "eckit/filesystem/PathName.h" +#include "eckit/mpi/Comm.h" +#include "eckit/testing/Test.h" + +#include "nwp_emulator/nwp_data_provider.h" +#include "nwp_emulator/nwp_definitions.h" + +using namespace eckit::testing; +using namespace nwp_emulator; + +namespace eckit { +namespace test { +CASE("test_focus_area") { + atlas::PointLonLat p1{10.12, 65.59}, p2{187.10, 50.73}; + + // area crossing the longitude 0/360 + FocusArea areaA{326.70, 47.91, 48.35, 72.98}; + EXPECT(areaA.contains(p1)); + EXPECT_NOT(areaA.contains(p2)); + + // area not crossing the longitude 0/360 + FocusArea areaB{155.0, 225.0, 34.5, 71.5}; + EXPECT(areaB.contains(p2)); + EXPECT_NOT(areaB.contains(p1)); +} +CASE("test_reader_setup") { + std::string configPath(std::getenv("TEST_DATA_DIR")); + ConfigReader* dataReader; + EXPECT_NO_THROW( + dataReader = new ConfigReader(eckit::PathName{configPath + "valid_config.yml"}, eckit::mpi::comm().rank(), 0);); + std::vector expectedParams{"100u,sfc,0", "u,ml,1", "u,ml,2", "u,ml,3", "u,ml,4", "u,ml,5", + "v,ml,1", "v,ml,2", "v,ml,3", "v,ml,4", "v,ml,5"}; + EXPECT_EQUAL(dataReader->getGridName(), "N80"); + EXPECT_EQUAL(dataReader->getParams(), expectedParams); + delete dataReader; +} +CASE("test_invalid_config") { + std::string configPath(std::getenv("TEST_DATA_DIR")); + EXPECT_THROWS( + ConfigReader dataReader(eckit::PathName{configPath + "invalid_config.yml"}, eckit::mpi::comm().rank(), 0)); +} +CASE("test_data_generation") { + std::string configPath(std::getenv("TEST_DATA_DIR")); + NWPDataProvider dataProvider(DataSourceType::CONFIG, eckit::PathName{configPath + "valid_config.yml"}, + eckit::mpi::comm().rank(), 0, eckit::mpi::comm().size()); + + std::vector expectedFields{"100u", "u", "v"}; + EXPECT_EQUAL(dataProvider.getModelFieldSet().field_names(), expectedFields); + + // With N80 there is 35718 grid points, so 11906 per partition + EXPECT_EQUAL(dataProvider.getModelFieldSet().field("u").shape(), atlas::array::make_shape(11906, 5)); + EXPECT_EQUAL(dataProvider.getModelFieldSet().field("v").shape(), atlas::array::make_shape(11906, 5)); + EXPECT_EQUAL(dataProvider.getModelFieldSet().field("100u").shape(), atlas::array::make_shape(11906, 1)); + + auto lonlat = + atlas::array::make_view(dataProvider.getModelFieldSet().field("u").functionspace().lonlat()); + FocusArea europe{155.0, 225.0, 34.5, 71.5}; + + int iterCount = 0; // avoid infinite while loop + while (dataProvider.getStepData()) { + iterCount++; + EXPECT_MSG(iterCount < 3, [=]() { + std::cerr << "The data provider has iterated through all steps, it should return false now..." << std::endl; + };); + + // 1. Vortex rollup & area mask + auto field100u = atlas::array::make_view(dataProvider.getModelFieldSet().field("100u")); + // Point outside of the focus area should remain zeros + EXPECT_NOT(europe.contains(atlas::PointLonLat{lonlat(0, 0), lonlat(0, 1)})); + EXPECT(std::abs(field100u(0, 0)) < 1e-4); + + if (eckit::mpi::comm().rank() == 0) { + // Europe is entirely owned by partition 0 + EXPECT(europe.contains(atlas::PointLonLat{lonlat(1132, 0), lonlat(1132, 1)})); + EXPECT(std::abs(field100u(1132, 0) - atlas::util::function::vortex_rollup(lonlat(1132, 0), lonlat(1132, 1), + (iterCount - 1) * 1.1)) < 1e-4); + } + auto fieldu = atlas::array::make_view(dataProvider.getModelFieldSet().field("u")); + // 2. Random + EXPECT(fieldu(0, 1) - 1 > 1e-6); + EXPECT(2 - fieldu(0, 1) > 1e-6); + // 3. Step + if (eckit::mpi::comm().rank() == 0) { + EXPECT(std::abs(fieldu(1132, 1) - 10 - (iterCount - 1)) < 1e-6); + } + // 4. Sinc + EXPECT(fieldu(0, 0) + 1 > 1e-6); + EXPECT(10 - fieldu(0, 0) > 1e-6); + // 5. Gaussian + EXPECT(fieldu(0, 4) > 1); + EXPECT(fieldu(0, 4) < 2 + 1e-6); + + eckit::mpi::comm().barrier(); + } + EXPECT_EQUAL(dataProvider.getStep(), 2); +} +} // namespace test +} // namespace eckit + +int main(int argc, char** argv) { + return run_tests(argc, argv); +} \ No newline at end of file diff --git a/tests/nwp_emulator/test_grib_reader.cc b/tests/nwp_emulator/test_grib_reader.cc new file mode 100644 index 0000000..2a2b210 --- /dev/null +++ b/tests/nwp_emulator/test_grib_reader.cc @@ -0,0 +1,74 @@ +/* + * (C) Copyright 2025- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ +#include +#include + +#include "atlas/array/ArrayShape.h" +#include "eckit/filesystem/PathName.h" +#include "eckit/mpi/Comm.h" +#include "eckit/testing/Test.h" + +#include "nwp_emulator/nwp_data_provider.h" +#include "nwp_emulator/nwp_definitions.h" + +using namespace eckit::testing; +using namespace nwp_emulator; + +namespace eckit { +namespace test { +CASE("test_reader_setup") { + GRIBFileReader* dataReader; + EXPECT_NO_THROW( + dataReader = new GRIBFileReader(eckit::PathName{std::getenv("TEST_DATA_DIR")}, eckit::mpi::comm().rank(), 0)); + std::vector expectedParams{"u,ml,1", "u,ml,28", "u,ml,55", "u,ml,82", + "u,ml,109", "u,ml,136", "100u,sfc,0"}; + // Ensure that the main and secondary readers all share the same setup + EXPECT_EQUAL(dataReader->getGridName(), "N80"); + EXPECT_EQUAL(dataReader->getParams(), expectedParams); + delete dataReader; +} +CASE("test_data_reading") { + NWPDataProvider dataProvider(DataSourceType::GRIB, eckit::PathName{std::getenv("TEST_DATA_DIR")}, + eckit::mpi::comm().rank(), 0, eckit::mpi::comm().size()); + + std::vector expectedFields{"100u", "u"}; + EXPECT_EQUAL(dataProvider.getModelFieldSet().field_names(), expectedFields); + + // With N80 there is 35718 grid points, so 11906 per partition + EXPECT_EQUAL(dataProvider.getModelFieldSet().field("u").shape(), atlas::array::make_shape(11906, 6)); + EXPECT_EQUAL(dataProvider.getModelFieldSet().field("100u").shape(), atlas::array::make_shape(11906, 1)); + + std::vector expectedValuesU{14.5716, 8.74349, 6.9388, -5.17533, 6.08444, 11.8501}; + std::vector expectedValues100U{1.48735, -3.94234, -1.19038, 1.22401, -7.02989, 1.72304}; + + int iterCount = 0; // avoid infinite while loop + while (dataProvider.getStepData()) { + iterCount++; + EXPECT_MSG(iterCount < 3, [=]() { + std::cerr << "The data provider has iterated through all steps, it should return false now..." << std::endl; + };); + + auto field100u = atlas::array::make_view(dataProvider.getModelFieldSet().field("100u")); + auto fieldu = atlas::array::make_view(dataProvider.getModelFieldSet().field("u")); + EXPECT(std::abs(fieldu(0, 0) - expectedValuesU[(dataProvider.getStep() - 1) * eckit::mpi::comm().size() + + eckit::mpi::comm().rank()]) < 1e-4); + EXPECT(std::abs(field100u(0, 0) - expectedValues100U[(dataProvider.getStep() - 1) * eckit::mpi::comm().size() + + eckit::mpi::comm().rank()]) < 1e-4); + eckit::mpi::comm().barrier(); + } + EXPECT_EQUAL(dataProvider.getStep(), 2); +} +} // namespace test +} // namespace eckit + +int main(int argc, char** argv) { + return run_tests(argc, argv); +} \ No newline at end of file