From 3ec736e1607392e52dfc80a9a5c72f260aa1e747 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 13 Apr 2026 08:13:36 +0000 Subject: [PATCH 01/17] mars2grib: infrastructure for iteration concept - Add new iteration concept under backend/concepts/iteration/ following the standard 4-file layout (Enum, Matcher, Encoding, ConceptDescriptor); single Default variant, applicable at (StagePreset, SecLocalUseSection), with LocalDefinitionNumber allow-list {20, 38}; encoder sets iterationNumber and (optionally) totalNumberOfIterations. - Add the supporting deductions backend/deductions/iterationNumber.h (resolve_IterationNumber_or_throw) and backend/deductions/totalNumberOfIterations.h (resolve_TotalNumberOfIterations_opt) used by IterationOp. - Register IterationConcept in AllConcepts.h: include added alphabetically between generating-process and level; type appended to the AllConcepts typelist after LongrangeConcept. - Implement iterationMatcher: returns IterationType::Default when mars has the "iteration" key, MISSING otherwise. - Extend analysisEncoding.h LocalDefinitionNumber allow-list from {36} to {36, 38} so AnalysisOp accepts the new combined template 38 (4i analysis products). - Add Section 2 recipes in section2Recipes.h: S2_R20 (Mars + Iteration) and S2_R38 (Mars + Iteration + Analysis); both registered in the Section2Recipes aggregator in numerical order. --- .../mars2grib/backend/concepts/AllConcepts.h | 11 +- .../concepts/analysis/analysisEncoding.h | 2 +- .../iteration/iterationConceptDescriptor.h | 160 ++++++++++++++++ .../concepts/iteration/iterationEncoding.h | 176 ++++++++++++++++++ .../concepts/iteration/iterationEnum.h | 136 ++++++++++++++ .../concepts/iteration/iterationMatcher.h | 25 +++ .../backend/deductions/iterationNumber.h | 130 +++++++++++++ .../deductions/totalNumberOfIterations.h | 171 +++++++++++++++++ .../section-recipes/impl/section2Recipes.h | 17 ++ 9 files changed, 822 insertions(+), 6 deletions(-) create mode 100644 src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h create mode 100644 src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h create mode 100644 src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h create mode 100644 src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h create mode 100644 src/metkit/mars2grib/backend/deductions/iterationNumber.h create mode 100644 src/metkit/mars2grib/backend/deductions/totalNumberOfIterations.h diff --git a/src/metkit/mars2grib/backend/concepts/AllConcepts.h b/src/metkit/mars2grib/backend/concepts/AllConcepts.h index f4b46c1e..fb9f23c7 100644 --- a/src/metkit/mars2grib/backend/concepts/AllConcepts.h +++ b/src/metkit/mars2grib/backend/concepts/AllConcepts.h @@ -99,6 +99,7 @@ #include "metkit/mars2grib/backend/concepts/destine/destineConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/ensemble/ensembleConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/generating-process/generatingProcessConceptDescriptor.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/level/levelConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/longrange/longrangeConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/mars/marsConceptDescriptor.h" @@ -168,10 +169,10 @@ using TypeList = metkit::mars2grib::backend::compile_time_registry_engine::TypeL /// Higher-level code should interact with concepts exclusively through /// registry APIs, not by iterating this list directly. /// -using AllConcepts = - TypeList; +using AllConcepts = TypeList; } // namespace metkit::mars2grib::backend::concepts_::detail \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h index ce8140cc..b47e9c62 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h @@ -150,7 +150,7 @@ void AnalysisOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& o MARS2GRIB_LOG_CONCEPT(analysis); // Structural validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L}); + validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L, 38L}); // Deductions long offsetToEndOf4DvarWindowVal = deductions::resolve_offsetToEndOf4DvarWindow_or_throw(mars, par, opt); diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h new file mode 100644 index 00000000..6a8777f3 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h @@ -0,0 +1,160 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file IterationConcept.h +/// @brief Compile-time registry entry for the GRIB `iteration` concept. +/// +/// This header defines `IterationConcept`, the **compile-time descriptor** +/// that registers the GRIB `iteration` concept into the mars2grib +/// compile-time registry engine. +/// +/// The descriptor provides: +/// - The concept name +/// - The mapping between variants and their symbolic names +/// - The set of callbacks associated with each encoding phase +/// - The entry-level matcher used to activate the concept +/// +/// This file contains **no runtime logic**. All decisions are resolved +/// at compile time through template instantiation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System include +#include + +// Registry engine +#include "metkit/mars2grib/backend/compile-time-registry-engine/RegisterEntryDescriptor.h" +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Core concept includes +#include "metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationEnum.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h" + +namespace metkit::mars2grib::backend::concepts_ { + +// Importing the compile-time registry engine namespace locally to avoid +// excessive verbosity in template-heavy code. This is restricted to an +// internal scope and not exposed through public headers. +using namespace metkit::mars2grib::backend::compile_time_registry_engine; + +/// +/// @brief Compile-time descriptor for the `iteration` concept. +/// +/// `IterationConcept` registers the GRIB `iteration` concept into the +/// compile-time registry engine. +/// +/// The descriptor defines: +/// - The canonical concept name +/// - The mapping from variant enum values to symbolic names +/// - The callbacks associated with each encoding phase +/// - The entry-level matcher used to detect applicability +/// +/// All functions in this descriptor are `constexpr` and are evaluated +/// entirely at compile time. +/// +struct IterationConcept : RegisterEntryDescriptor { + + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - Registry identification + /// - Diagnostics and logging + /// - Debug and introspection facilities + /// + static constexpr std::string_view entryName() { return iterationName; } + + /// + /// @brief Return the symbolic name of a concept variant. + /// + /// @tparam T Variant enumeration value + /// + /// @return String view representing the variant name + /// + template + static constexpr std::string_view variantName() { + return iterationTypeName(); + } + + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This function is queried by the registry engine to obtain the + /// callback implementing the `iteration` concept for a given: + /// + /// - Capability + /// - Encoding stage + /// - GRIB section + /// - Concept variant + /// + /// The function returns: + /// - A valid function pointer if the concept is applicable + /// - `nullptr` otherwise + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// @tparam MarsDict_t Type of MARS dictionary + /// @tparam ParDict_t Type of parameter dictionary + /// @tparam OptDict_t Type of options dictionary + /// @tparam OutDict_t Type of output GRIB dictionary + /// + /// @return Function pointer implementing the phase, or `nullptr` + /// + template + static constexpr Fn phaseCallbacks() { + + if constexpr (Capability == 0) { + + if constexpr (iterationApplicable()) { + return &IterationOp; + } + else { + return nullptr; + } + } + else { + return nullptr; + } + + mars2gribUnreachable(); + } + + /// + /// @brief Variant-specific callbacks (not used for this concept). + /// + template + static constexpr Fn variantCallbacks() { + return nullptr; + } + + /// + /// @brief Entry-level matcher callback. + /// + template + static constexpr Fm entryCallbacks() { + if constexpr (Capability == 0) { + return &iterationMatcher; + } + else { + return nullptr; + } + } +}; + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h new file mode 100644 index 00000000..cc0900d7 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h @@ -0,0 +1,176 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file iterationOp.h +/// @brief Implementation of the GRIB `iteration` concept operation. +/// +/// This header defines the applicability rules and execution logic for the +/// **iteration concept** within the mars2grib backend. +/// +/// The iteration concept is responsible for encoding GRIB keys associated with +/// *long-range forecast metadata* stored in the Local Use Section, specifically: +/// +/// - `methodNumber` +/// - `systemNumber` +/// +/// These fields are used to identify the forecasting method and system +/// used for long-range or seasonal products. +/// +/// The implementation follows the standard mars2grib concept model: +/// - Compile-time applicability via `iterationApplicable` +/// - Runtime validation of Local Definition Number +/// - Explicit deduction of required values +/// - Strict error handling with contextual concept exceptions +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of `concepts` +/// to avoid ambiguity and potential conflicts with the C++20 `concept` language +/// feature and related standard headers. +/// +/// This is a deliberate design choice and must not be changed. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationEnum.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Deductions +#include "metkit/mars2grib/backend/deductions/iterationNumber.h" +#include "metkit/mars2grib/backend/deductions/totalNumberOfIterations.h" + +// checks +#include "metkit/mars2grib/backend/checks/matchLocalDefinitionNumber.h" + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +/// +/// @brief Compile-time applicability predicate for the `iteration` concept. +/// +/// This predicate determines whether the iteration concept is applicable +/// for a given combination of: +/// - encoding stage +/// - GRIB section +/// - concept variant +/// +/// Applicability is evaluated entirely at compile time and is used by the +/// concept dispatcher to control instantiation and execution. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant Iteration concept variant +/// +/// @return `true` if the concept is applicable for the given parameters, +/// `false` otherwise. +/// +/// @note +/// The default applicability rule enables the concept only when: +/// - `Variant == IterationType::Default` +/// - `Stage == StagePreset` +/// - `Section == SecLocalUseSection` +/// +template +constexpr bool iterationApplicable() { + return ((Variant == IterationType::Default) && (Stage == StagePreset) && (Section == SecLocalUseSection)); +} + + +/// +/// @brief Execute the `iteration` concept operation. +/// +/// This function implements the runtime logic of the GRIB `iteration` concept. +/// When applicable, it: +/// +/// 1. Validates that the Local Use Section matches the expected definition. +/// 2. Deduces the long-range forecasting method and system identifiers. +/// 3. Encodes the corresponding GRIB keys in the output dictionary. +/// +/// If the concept is invoked when not applicable, a +/// `Mars2GribConceptException` is thrown. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant Iteration concept variant +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam ParDict_t Type of the parameter dictionary +/// @tparam OptDict_t Type of the options dictionary +/// @tparam OutDict_t Type of the GRIB output dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] par Parameter dictionary +/// @param[in] opt Options dictionary +/// @param[out] out Output GRIB dictionary to be populated +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribConceptException +/// If: +/// - the Local Definition Number does not match expectations, +/// - required deductions fail, +/// - any GRIB key cannot be set, +/// - the concept is invoked when not applicable. +/// +/// @note +/// - All runtime errors are wrapped with full concept context +/// (concept name, variant, stage, section). +/// - This concept does not rely on pre-existing GRIB header state. +/// +/// @see iterationApplicable +/// +template +void IterationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt, OutDict_t& out) { + + using metkit::mars2grib::utils::dict_traits::set_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribConceptException; + + if constexpr (iterationApplicable()) { + + + try { + + MARS2GRIB_LOG_CONCEPT(iteration); + + // Preconditions / contracts + validation::match_LocalDefinitionNumber_or_throw(opt, out, {20L, 38L}); + + // Deductions + auto iterationNumberVal = deductions::resolve_IterationNumber_or_throw(mars, par, opt); + auto totalNumberOfIterationsVal = deductions::resolve_TotalNumberOfIterations_opt(mars, par, opt); + + // Encoding + set_or_throw(out, "iterationNumber", iterationNumberVal); + if (totalNumberOfIterationsVal.has_value()) { + set_or_throw(out, "totalNumberOfIterations", totalNumberOfIterationsVal.value()); + } + } + catch (...) { + MARS2GRIB_CONCEPT_RETHROW(iteration, "Unable to set `iteration` concept..."); + } + + // Successful operation + return; + } + + // Concept invoked outside its applicability domain + MARS2GRIB_CONCEPT_THROW(iteration, "Concept called when not applicable..."); + + // Remove compiler warning + mars2gribUnreachable(); +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h new file mode 100644 index 00000000..5a8fae4f --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h @@ -0,0 +1,136 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file iterationEnum.h +/// @brief Definition of the `iteration` concept variants and compile-time metadata. +/// +/// This header defines the **static description** of the GRIB `iteration` concept +/// used by the mars2grib backend. It contains: +/// +/// - the canonical concept name (`iterationName`) +/// - the enumeration of supported long-range variants (`IterationType`) +/// - a compile-time typelist of all variants (`IterationList`) +/// - a compile-time mapping from variant to string identifier +/// +/// This file intentionally contains **no runtime logic** and **no encoding +/// behavior**. Its sole purpose is to provide compile-time metadata used by: +/// +/// - the concept registry +/// - compile-time table generation +/// - logging and diagnostics +/// - static validation of concept variants +/// +/// @note +/// This header is part of the **concept definition layer**. +/// Runtime behavior is implemented separately in the corresponding +/// `iteration.h` / `iterationOp` implementation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System includes +#include +#include + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +using ValueList = metkit::mars2grib::backend::compile_time_registry_engine::ValueList; + +/// +/// @brief Canonical name of the `iteration` concept. +/// +/// This identifier is used: +/// - as the logical concept key in the concept registry +/// - for logging and debugging output +/// - to associate variants and capabilities with the `iteration` concept +/// +/// The value must remain stable across releases. +/// +inline constexpr std::string_view iterationName{"iteration"}; + + +/// +/// @brief Enumeration of all supported `iteration` concept variants. +/// +/// Each enumerator represents a specific long-range forecasting +/// classification or processing mode handled by the encoder. +/// +/// The numeric values of the enumerators are **not semantically relevant**; +/// they are required only to: +/// - provide a stable compile-time identifier +/// - allow array indexing and table generation +/// +/// @note +/// This enumeration is intentionally minimal. Additional variants may be +/// introduced in the future as the long-range concept evolves. +/// +/// @warning +/// Do not reorder existing enumerators, as they are used in compile-time +/// tables and registries. +/// +enum class IterationType : std::size_t { + Default = 0 +}; + + +/// +/// @brief Compile-time list of all `iteration` concept variants. +/// +/// This typelist is used to: +/// - generate concept capability tables at compile time +/// - register all supported variants in the concept registry +/// - enable static iteration over variants without runtime overhead +/// +/// @note +/// The order of this list must match the intended iteration order +/// for registry construction and diagnostics. +/// +using IterationList = ValueList; + + +/// +/// @brief Compile-time mapping from `IterationType` to human-readable name. +/// +/// This function returns the canonical string identifier associated +/// with a given long-range variant. +/// +/// The returned value is used for: +/// - logging and debugging output +/// - error reporting +/// - concept registry diagnostics +/// +/// @tparam T Long-range variant +/// @return String view identifying the variant +/// +/// @note +/// The returned string must remain stable across releases, as it may +/// appear in logs, tests, and diagnostic output. +/// +template +constexpr std::string_view iterationTypeName(); + +#define DEF(T, NAME) \ + template <> \ + constexpr std::string_view iterationTypeName() { \ + return NAME; \ + } + +DEF(IterationType::Default, "default"); + +#undef DEF + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h new file mode 100644 index 00000000..39fd3f7d --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h @@ -0,0 +1,25 @@ +#pragma once + +// System include +#include + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationEnum.h" +#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +std::size_t iterationMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::has; + + if (has(mars, "iteration")) { + return static_cast(IterationType::Default); + } + + return compile_time_registry_engine::MISSING; +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/deductions/iterationNumber.h b/src/metkit/mars2grib/backend/deductions/iterationNumber.h new file mode 100644 index 00000000..fe023ef8 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/iterationNumber.h @@ -0,0 +1,130 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file iterationNumber.h +/// @brief Deduction of the offset to the end of the 4D-Var analysis window. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **offset to the end of the 4D-Var assimilation window** +/// from input dictionaries. +/// +/// The deduction retrieves the offset explicitly from the MARS dictionary. +/// No inference, defaulting, normalization, or validation of temporal +/// semantics is performed. +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved from one or more input dictionaries +/// +/// @section References +/// Concept: +/// - @ref analysisEncoding.h +/// +/// Related deductions: +/// - @ref lengthOfTimeWindow.h +/// +/// @ingroup mars2grib_backend_deductions +/// +#pragma once + +// System includes +#include + +// Core deduction includes +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the offset to the end of the 4D-Var analysis window. +/// +/// @section Deduction contract +/// - Reads: `mars["iteration"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction resolves the temporal offset between the analysis +/// reference time and the end of the 4D-Var assimilation window. +/// +/// The returned value is treated as an opaque numeric quantity. Its unit +/// and interpretation are defined by upstream MARS/IFS conventions and +/// are not interpreted by this deduction. +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary. Must provide the key `iteration`. +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary (unused). +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary from which the offset is resolved. +/// +/// @param[in] par +/// Parameter dictionary (unused). +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The offset to the end of the 4D-Var analysis window. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If the key `iteration` is missing, cannot be converted to `long`, +/// or if any unexpected error occurs during deduction. +/// +/// @note +/// This deduction assumes that the offset is explicitly provided by +/// MARS and does not attempt any inference or defaulting. +/// +template +long resolve_IterationNumber_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Retrieve mandatory MARS iteration + auto iterationNumber = get_or_throw(mars, "iteration"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`iterationNumber` resolved from input dictionaries: value='"; + logMsg += std::to_string(iterationNumber) + "'"; + return logMsg; + }()); + + // Success exit point + return iterationNumber; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `iterationNumber` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/totalNumberOfIterations.h b/src/metkit/mars2grib/backend/deductions/totalNumberOfIterations.h new file mode 100644 index 00000000..f15b8328 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/totalNumberOfIterations.h @@ -0,0 +1,171 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file totalNumberOfIterations.h +/// @brief Deduction of the GRIB totalNumberOfIterations (in seconds). +/// +/// This header defines the deduction used by the mars2grib backend to resolve +/// the GRIB totalNumberOfIterations key from input dictionaries. +/// +/// The deduction reads the parameter dictionary entry totalNumberOfIterations, +/// interprets it as hours, and converts it to seconds. If the key is missing, +/// the deduction returns `std::nullopt`. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - applying explicit, deterministic deduction logic +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys +/// - do NOT infer units or values beyond the documented rule +/// - do NOT perform GRIB table validation +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or malformed inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value derived from the parameter dictionary +/// - DEFAULT: value defaulted to the GRIB missing code +/// +/// @section References +/// Concept: +/// - @ref analysisEncoding.h +/// +/// Related deductions: +/// - @ref offsetToEndOf4DvarWindow.h +/// +/// @ingroup mars2grib_backend_deductions +/// +#pragma once + +// System Include +#include +#include +#include + +// Other project includes +#include "eckit/log/Log.h" + +// Core deduction includes +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the GRIB `totalNumberOfIterations` expressed in seconds. +/// +/// This deduction determines the value of the GRIB `totalNumberOfIterations` +/// (in seconds) based on the parameter dictionary key `totalNumberOfIterations`. +/// +/// The deduction follows these rules: +/// +/// - If the key `totalNumberOfIterations` is present in the parameter dictionary, +/// its value is interpreted as **hours** and converted to seconds. +/// - If the key is absent, `std::nullopt` is used, which should be handled +/// by the encoding layer as the GRIB missing code (0xFFFF). +/// +/// @important +/// This deduction currently relies on **implicit assumptions** about +/// units and defaults that are not explicitly encoded in MARS metadata. +/// These assumptions are documented but not enforced via validation. +/// +/// @assumptions +/// - `par::totalNumberOfIterations` is expressed in **hours** +/// - Default value is `std::nullopt` when the key is missing +/// +/// @warning +/// - These assumptions may not be valid for all datasets. +/// - Relying on implicit defaults may lead to non-reproducible GRIB output +/// if upstream conventions change. +/// +/// @tparam MarsDict_t Type of the MARS dictionary (unused) +/// @tparam ParDict_t Type of the parameter dictionary +/// @tparam OptDict_t Type of the options dictionary (unused) +/// +/// @param[in] mars MARS dictionary (unused) +/// @param[in] par Parameter dictionary +/// @param[in] opt Options dictionary (unused) +/// +/// @return The length of time window in seconds. If `par::totalNumberOfIterations` is missing, +/// returns `std::nullopt`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If: +/// - access to the parameter dictionary fails +/// - the retrieved value cannot be interpreted as a valid integer +/// - any unexpected error occurs during deduction +/// +/// @todo [owner: mds,dgov][scope: deduction][reason: correctness][prio: medium] +/// - Make the unit of `totalNumberOfIterations` explicit instead of assuming hours. +/// - Add explicit validation of allowed ranges and units. +/// +/// @note +/// - This deduction does not rely on any pre-existing GRIB header state. +/// - Logging intentionally emits RESOLVE/DEFAULT entries to highlight implicit assumptions. +/// + +template +std::optional resolve_TotalNumberOfIterations_opt(const MarsDict_t& mars, const ParDict_t& par, + const OptDict_t& opt) { + + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Big assumption here: + // - totalNumberOfIterations is in hours + if (has(par, "totalNumberOfIterations")) { + long totalNumberOfIterationsVal = get_or_throw(par, "totalNumberOfIterations"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`totalNumberOfIterations` resolved from input dictionaries: value='"; + logMsg += std::to_string(totalNumberOfIterationsVal); + return logMsg; + }()); + + // Success exit point + return {totalNumberOfIterationsVal}; // Convert hours to seconds + } + else { + + // Emit DEFAULT log entry + MARS2GRIB_LOG_DEFAULT([&]() { + std::string logMsg = "`totalNumberOfIterations` defaulted to MISSING (nullopt)"; + return logMsg; + }()); + + // Success exit point + return std::nullopt; + } + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Unable to get `totalNumberOfIterations` from Par dictionary", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +} + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index 16fcb994..7b70c5db 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -32,6 +32,13 @@ inline const Recipe S2_R15 = Select >(); +// 4i related products +inline const Recipe S2_R20 = + make_recipe<20, + Select, + Select + >(); + // Satellite-related products inline const Recipe S2_R24 = make_recipe<24, @@ -46,6 +53,14 @@ inline const Recipe S2_R36 = Select >(); +// 4i Analysis-related products +inline const Recipe S2_R38 = + make_recipe<38, + Select, + Select, + Select + >(); + //------------------------------------------------------------------------------ // Virtual (encoder-specific) templates //------------------------------------------------------------------------------ @@ -79,8 +94,10 @@ inline const Recipes Section2Recipes{ 2, std::vector{ &S2_R1, &S2_R15, + &S2_R20, &S2_R24, &S2_R36, + &S2_R38, &S2_R1001, &S2_R1002 } From 9e3111acd79ee111d578bf8fbb02b3ff76004855 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 28 Apr 2026 15:06:33 +0000 Subject: [PATCH 02/17] mars2grib: infrastructure for modelError concept - Add new modelError concept under backend/concepts/model-error/ following the standard 4-file layout (Enum, Matcher, Encoding, ConceptDescriptor); single Default variant, applicable at (StagePreset, SecLocalUseSection), with LocalDefinitionNumber allow-list {25, 39}; encoder body left as TODO (deductions / GRIB key writes to be added separately). - Register ModelErrorConcept in AllConcepts.h: include added alphabetically between mars and nil; type appended at the end of the AllConcepts typelist to preserve existing conceptIds and global variant indices. - Implement modelErrorMatcher: returns ModelErrorType::Default when mars["type"] == "eme"; throws Mars2GribMatcherException if the mandatory "number" key is missing in that case; returns MISSING otherwise. - Add Section 2 recipes in section2Recipes.h: S2_R25 (Mars + ModelError) and S2_R39 (Mars + Analysis + ModelError); both registered in the Section2Recipes aggregator in numerical order. - Extend analysisEncoding.h LocalDefinitionNumber allow-list from {36, 38} to {36, 38, 39} so AnalysisOp accepts the new template 39. - Update ensembleMatcher.h to return MISSING when mars["type"] == "eme", since in that case the "number" key identifies the model-error realization, not an ensemble member. --- .../mars2grib/backend/concepts/AllConcepts.h | 3 +- .../concepts/analysis/analysisEncoding.h | 2 +- .../concepts/ensemble/ensembleMatcher.h | 8 + .../model-error/modelErrorConceptDescriptor.h | 160 ++++++++++++++++ .../concepts/model-error/modelErrorEncoding.h | 178 ++++++++++++++++++ .../concepts/model-error/modelErrorEnum.h | 136 +++++++++++++ .../concepts/model-error/modelErrorMatcher.h | 35 ++++ .../backend/deductions/componentIndex.h | 166 ++++++++++++++++ .../backend/deductions/modelErrorType.h | 146 ++++++++++++++ .../backend/deductions/numberOfComponents.h | 146 ++++++++++++++ .../section-recipes/impl/section2Recipes.h | 17 ++ 11 files changed, 995 insertions(+), 2 deletions(-) create mode 100644 src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h create mode 100644 src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h create mode 100644 src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h create mode 100644 src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h create mode 100644 src/metkit/mars2grib/backend/deductions/componentIndex.h create mode 100644 src/metkit/mars2grib/backend/deductions/modelErrorType.h create mode 100644 src/metkit/mars2grib/backend/deductions/numberOfComponents.h diff --git a/src/metkit/mars2grib/backend/concepts/AllConcepts.h b/src/metkit/mars2grib/backend/concepts/AllConcepts.h index fb9f23c7..257e4fc5 100644 --- a/src/metkit/mars2grib/backend/concepts/AllConcepts.h +++ b/src/metkit/mars2grib/backend/concepts/AllConcepts.h @@ -103,6 +103,7 @@ #include "metkit/mars2grib/backend/concepts/level/levelConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/longrange/longrangeConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/mars/marsConceptDescriptor.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/nil/nilConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/origin/originConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/packing/packingConceptDescriptor.h" @@ -173,6 +174,6 @@ using AllConcepts = TypeList; + ShapeOfTheEarthConcept, StatisticsConcept, TablesConcept, WaveConcept, ModelErrorConcept>; } // namespace metkit::mars2grib::backend::concepts_::detail \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h index b47e9c62..1e6b08b7 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h @@ -150,7 +150,7 @@ void AnalysisOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& o MARS2GRIB_LOG_CONCEPT(analysis); // Structural validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L, 38L}); + validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L, 38L, 39L}); // Deductions long offsetToEndOf4DvarWindowVal = deductions::resolve_offsetToEndOf4DvarWindow_or_throw(mars, par, opt); diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index c82e2b60..60a3d097 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -2,6 +2,7 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.h" @@ -12,8 +13,15 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t ensembleMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; + // Skip model-error products: in that case "number" identifies the + // model-error realization, not an ensemble member. + if (has(mars, "type") && get_or_throw(mars, "type") == "eme") { + return compile_time_registry_engine::MISSING; + } + if (has(mars, "number")) { return static_cast(EnsembleType::Individual); } diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h new file mode 100644 index 00000000..5ff7c713 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h @@ -0,0 +1,160 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file ModelErrorConcept.h +/// @brief Compile-time registry entry for the GRIB `modelError` concept. +/// +/// This header defines `ModelErrorConcept`, the **compile-time descriptor** +/// that registers the GRIB `modelError` concept into the mars2grib +/// compile-time registry engine. +/// +/// The descriptor provides: +/// - The concept name +/// - The mapping between variants and their symbolic names +/// - The set of callbacks associated with each encoding phase +/// - The entry-level matcher used to activate the concept +/// +/// This file contains **no runtime logic**. All decisions are resolved +/// at compile time through template instantiation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System include +#include + +// Registry engine +#include "metkit/mars2grib/backend/compile-time-registry-engine/RegisterEntryDescriptor.h" +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Core concept includes +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h" + +namespace metkit::mars2grib::backend::concepts_ { + +// Importing the compile-time registry engine namespace locally to avoid +// excessive verbosity in template-heavy code. This is restricted to an +// internal scope and not exposed through public headers. +using namespace metkit::mars2grib::backend::compile_time_registry_engine; + +/// +/// @brief Compile-time descriptor for the `modelError` concept. +/// +/// `ModelErrorConcept` registers the GRIB `modelError` concept into the +/// compile-time registry engine. +/// +/// The descriptor defines: +/// - The canonical concept name +/// - The mapping from variant enum values to symbolic names +/// - The callbacks associated with each encoding phase +/// - The entry-level matcher used to detect applicability +/// +/// All functions in this descriptor are `constexpr` and are evaluated +/// entirely at compile time. +/// +struct ModelErrorConcept : RegisterEntryDescriptor { + + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - Registry identification + /// - Diagnostics and logging + /// - Debug and introspection facilities + /// + static constexpr std::string_view entryName() { return modelErrorName; } + + /// + /// @brief Return the symbolic name of a concept variant. + /// + /// @tparam T Variant enumeration value + /// + /// @return String view representing the variant name + /// + template + static constexpr std::string_view variantName() { + return modelErrorTypeName(); + } + + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This function is queried by the registry engine to obtain the + /// callback implementing the `modelError` concept for a given: + /// + /// - Capability + /// - Encoding stage + /// - GRIB section + /// - Concept variant + /// + /// The function returns: + /// - A valid function pointer if the concept is applicable + /// - `nullptr` otherwise + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// @tparam MarsDict_t Type of MARS dictionary + /// @tparam ParDict_t Type of parameter dictionary + /// @tparam OptDict_t Type of options dictionary + /// @tparam OutDict_t Type of output GRIB dictionary + /// + /// @return Function pointer implementing the phase, or `nullptr` + /// + template + static constexpr Fn phaseCallbacks() { + + if constexpr (Capability == 0) { + + if constexpr (modelErrorApplicable()) { + return &ModelErrorOp; + } + else { + return nullptr; + } + } + else { + return nullptr; + } + + mars2gribUnreachable(); + } + + /// + /// @brief Variant-specific callbacks (not used for this concept). + /// + template + static constexpr Fn variantCallbacks() { + return nullptr; + } + + /// + /// @brief Entry-level matcher callback. + /// + template + static constexpr Fm entryCallbacks() { + if constexpr (Capability == 0) { + return &modelErrorMatcher; + } + else { + return nullptr; + } + } +}; + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h new file mode 100644 index 00000000..94317091 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h @@ -0,0 +1,178 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file modelErrorOp.h +/// @brief Implementation of the GRIB `modelError` concept operation. +/// +/// This header defines the applicability rules and execution logic for the +/// **modelError concept** within the mars2grib backend. +/// +/// The modelError concept is responsible for encoding GRIB keys related to +/// *model-error metadata* stored in the Local Use Section, specifically: +/// +/// - `componentIndex` +/// - `numberOfComponents` +/// - `modelErrorType` +/// +/// These fields identify the realization within a model-error ensemble, +/// the total number of realizations, and the type of model error. +/// +/// The implementation follows the standard mars2grib concept model: +/// - Compile-time applicability via `modelErrorApplicable` +/// - Runtime validation of Local Definition Number +/// - Explicit deduction of required values +/// - Strict error handling with contextual concept exceptions +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of `concepts` +/// to avoid ambiguity and potential conflicts with the C++20 `concept` language +/// feature and related standard headers. +/// +/// This is a deliberate design choice and must not be changed. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Deductions +#include "metkit/mars2grib/backend/deductions/componentIndex.h" +#include "metkit/mars2grib/backend/deductions/modelErrorType.h" +#include "metkit/mars2grib/backend/deductions/numberOfComponents.h" + +// checks +#include "metkit/mars2grib/backend/checks/matchLocalDefinitionNumber.h" + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +/// +/// @brief Compile-time applicability predicate for the `modelError` concept. +/// +/// This predicate determines whether the modelError concept is applicable +/// for a given combination of: +/// - encoding stage +/// - GRIB section +/// - concept variant +/// +/// Applicability is evaluated entirely at compile time and is used by the +/// concept dispatcher to control instantiation and execution. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant ModelError concept variant +/// +/// @return `true` if the concept is applicable for the given parameters, +/// `false` otherwise. +/// +/// @note +/// The default applicability rule enables the concept only when: +/// - `Variant == ModelErrorType::Default` +/// - `Stage == StagePreset` +/// - `Section == SecLocalUseSection` +/// +template +constexpr bool modelErrorApplicable() { + return ((Variant == ModelErrorType::Default) && (Stage == StagePreset) && (Section == SecLocalUseSection)); +} + + +/// +/// @brief Execute the `modelError` concept operation. +/// +/// This function implements the runtime logic of the GRIB `modelError` concept. +/// When applicable, it: +/// +/// 1. Validates that the Local Use Section matches the expected definition. +/// 2. Deduces the model-error related identifiers. +/// 3. Encodes the corresponding GRIB keys in the output dictionary. +/// +/// If the concept is invoked when not applicable, a +/// `Mars2GribConceptException` is thrown. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant ModelError concept variant +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam ParDict_t Type of the parameter dictionary +/// @tparam OptDict_t Type of the options dictionary +/// @tparam OutDict_t Type of the GRIB output dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] par Parameter dictionary +/// @param[in] opt Options dictionary +/// @param[out] out Output GRIB dictionary to be populated +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribConceptException +/// If: +/// - the Local Definition Number does not match expectations, +/// - required deductions fail, +/// - any GRIB key cannot be set, +/// - the concept is invoked when not applicable. +/// +/// @note +/// - All runtime errors are wrapped with full concept context +/// (concept name, variant, stage, section). +/// - This concept does not rely on pre-existing GRIB header state. +/// +/// @see modelErrorApplicable +/// +template +void ModelErrorOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt, OutDict_t& out) { + + using metkit::mars2grib::utils::dict_traits::set_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribConceptException; + + if constexpr (modelErrorApplicable()) { + + + try { + + MARS2GRIB_LOG_CONCEPT(modelError); + + // Preconditions / contracts + validation::match_LocalDefinitionNumber_or_throw(opt, out, {25L, 39L}); + + // Deductions + auto componentIndexVal = deductions::resolve_ComponentIndex_or_throw(mars, par, opt); + auto numberOfComponentsVal = deductions::resolve_NumberOfComponents_or_throw(mars, par, opt); + auto modelErrorTypeVal = deductions::resolve_ModelErrorType_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "componentIndex", componentIndexVal); + set_or_throw(out, "numberOfComponents", numberOfComponentsVal); + set_or_throw(out, "modelErrorType", modelErrorTypeVal); + } + catch (...) { + MARS2GRIB_CONCEPT_RETHROW(modelError, "Unable to set `modelError` concept..."); + } + + // Successful operation + return; + } + + // Concept invoked outside its applicability domain + MARS2GRIB_CONCEPT_THROW(modelError, "Concept called when not applicable..."); + + // Remove compiler warning + mars2gribUnreachable(); +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h new file mode 100644 index 00000000..eeb96d3a --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h @@ -0,0 +1,136 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file modelErrorEnum.h +/// @brief Definition of the `modelError` concept variants and compile-time metadata. +/// +/// This header defines the **static description** of the GRIB `modelError` concept +/// used by the mars2grib backend. It contains: +/// +/// - the canonical concept name (`modelErrorName`) +/// - the enumeration of supported model-error variants (`ModelErrorType`) +/// - a compile-time typelist of all variants (`ModelErrorList`) +/// - a compile-time mapping from variant to string identifier +/// +/// This file intentionally contains **no runtime logic** and **no encoding +/// behavior**. Its sole purpose is to provide compile-time metadata used by: +/// +/// - the concept registry +/// - compile-time table generation +/// - logging and diagnostics +/// - static validation of concept variants +/// +/// @note +/// This header is part of the **concept definition layer**. +/// Runtime behavior is implemented separately in the corresponding +/// `modelError.h` / `modelErrorOp` implementation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System includes +#include +#include + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +using ValueList = metkit::mars2grib::backend::compile_time_registry_engine::ValueList; + +/// +/// @brief Canonical name of the `modelError` concept. +/// +/// This identifier is used: +/// - as the logical concept key in the concept registry +/// - for logging and debugging output +/// - to associate variants and capabilities with the `modelError` concept +/// +/// The value must remain stable across releases. +/// +inline constexpr std::string_view modelErrorName{"modelError"}; + + +/// +/// @brief Enumeration of all supported `modelError` concept variants. +/// +/// Each enumerator represents a specific model-error +/// classification or processing mode handled by the encoder. +/// +/// The numeric values of the enumerators are **not semantically relevant**; +/// they are required only to: +/// - provide a stable compile-time identifier +/// - allow array indexing and table generation +/// +/// @note +/// This enumeration is intentionally minimal. Additional variants may be +/// introduced in the future as the model-error concept evolves. +/// +/// @warning +/// Do not reorder existing enumerators, as they are used in compile-time +/// tables and registries. +/// +enum class ModelErrorType : std::size_t { + Default = 0 +}; + + +/// +/// @brief Compile-time list of all `modelError` concept variants. +/// +/// This typelist is used to: +/// - generate concept capability tables at compile time +/// - register all supported variants in the concept registry +/// - enable static iteration over variants without runtime overhead +/// +/// @note +/// The order of this list must match the intended iteration order +/// for registry construction and diagnostics. +/// +using ModelErrorList = ValueList; + + +/// +/// @brief Compile-time mapping from `ModelErrorType` to human-readable name. +/// +/// This function returns the canonical string identifier associated +/// with a given model-error variant. +/// +/// The returned value is used for: +/// - logging and debugging output +/// - error reporting +/// - concept registry diagnostics +/// +/// @tparam T Model-error variant +/// @return String view identifying the variant +/// +/// @note +/// The returned string must remain stable across releases, as it may +/// appear in logs, tests, and diagnostic output. +/// +template +constexpr std::string_view modelErrorTypeName(); + +#define DEF(T, NAME) \ + template <> \ + constexpr std::string_view modelErrorTypeName() { \ + return NAME; \ + } + +DEF(ModelErrorType::Default, "default"); + +#undef DEF + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h new file mode 100644 index 00000000..96ce9248 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h @@ -0,0 +1,35 @@ +#pragma once + +// System include +#include +#include + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h" +#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +std::size_t modelErrorMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + + // Concept does not apply unless "type" is present and equals "eme" + if (!has(mars, "type") || get_or_throw(mars, "type") != "eme") { + return compile_time_registry_engine::MISSING; + } + + // At this point the request is a model-error request: "number" is mandatory + if (!has(mars, "number")) { + throw utils::exceptions::Mars2GribMatcherException( + "modelError concept requires MARS key \"number\" when type=\"eme\"", Here()); + } + + return static_cast(ModelErrorType::Default); +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/deductions/componentIndex.h b/src/metkit/mars2grib/backend/deductions/componentIndex.h new file mode 100644 index 00000000..1200c365 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/componentIndex.h @@ -0,0 +1,166 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file componentIndex.h +/// @brief Deduction of the model-error realization identifier. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **model-error realization identifier** (`componentIndex`) +/// from MARS metadata. +/// +/// The deduction retrieves the realization identifier explicitly from the +/// MARS dictionary and returns it verbatim, without applying inference, +/// defaulting, or semantic interpretation. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - enforcing deterministic resolution rules +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys directly +/// - do NOT apply heuristic or data-driven inference +/// - do NOT validate against GRIB code tables unless explicitly required +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved directly from input dictionaries +/// +/// @section References +/// Concept: +/// - @ref modelErrorEncoding.h +/// +/// Related deductions: +/// - @ref numberOfComponents.h +/// - @ref modelErrorType.h +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +#include + +#include "eckit/log/Log.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the model-error realization identifier. +/// +/// @section Deduction contract +/// - Reads: `mars["type"]`, `mars["number"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction retrieves the model-error realization identifier from +/// the MARS dictionary. For requests with `type=eme`, the MARS key +/// `number` identifies the realization within the model-error ensemble +/// (not an ensemble-forecast member). +/// +/// The value is treated as mandatory and is returned verbatim as a +/// numeric identifier. No inference, defaulting, or validation against +/// GRIB code tables is performed. +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary. Must provide the key `number`. +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary (unused). +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary from which the realization identifier is retrieved. +/// +/// @param[in] par +/// Parameter dictionary (unused). +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The model-error realization identifier. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If: +/// - the key `type` is missing or not equal to `"eme"` (defence-in-depth: +/// `componentIndex` is only meaningful for model-error products), +/// - the key `number` is missing, cannot be converted to `long`, +/// - or any unexpected error occurs during deduction. +/// +/// @note +/// This deduction assumes that the realization identifier is explicitly +/// provided by the MARS dictionary and does not attempt any semantic +/// interpretation or consistency checking. +/// +template +long resolve_ComponentIndex_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Defence-in-depth: componentIndex is only meaningful for type=eme + // (model-error products). Resolving it for any other request type + // indicates a serious upstream contract violation (wrong recipe, + // matcher bypass, etc.) and must be surfaced as a hard failure + // with an unambiguous diagnostic. + const std::string typeVal = get_or_throw(mars, "type"); + if (typeVal != "eme") { + throw Mars2GribDeductionException( + std::string("`componentIndex` requested for a non-`eme` request: " + "`mars[\"type\"]` is `") + + typeVal + + "` but only `eme` is supported. This is a serious upstream " + "contract violation: the model-error deduction was reached " + "for a request that is not a model-error product. Check " + "recipe selection and matcher dispatch.", + Here()); + } + + // Retrieve mandatory MARS number (model-error realization id) + long componentIndex = get_or_throw(mars, "number"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`componentIndex` resolved from input dictionaries: value="; + logMsg += std::to_string(componentIndex); + return logMsg; + }()); + + // Success exit point + return componentIndex; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `componentIndex` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/modelErrorType.h b/src/metkit/mars2grib/backend/deductions/modelErrorType.h new file mode 100644 index 00000000..24125089 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/modelErrorType.h @@ -0,0 +1,146 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file modelErrorType.h +/// @brief Deduction of the model-error type identifier. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **model-error type identifier** (`modelErrorType`) from +/// the parameter dictionary. +/// +/// The value is not derivable from MARS alone. It must be supplied via +/// the parameter dictionary by the upstream tool, typically read from +/// the input GRIB1 handle being re-encoded. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - enforcing deterministic resolution rules +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys directly +/// - do NOT apply heuristic or data-driven inference +/// - do NOT validate against GRIB code tables unless explicitly required +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved directly from input dictionaries +/// +/// @section References +/// Concept: +/// - @ref modelErrorEncoding.h +/// +/// Related deductions: +/// - @ref componentIndex.h +/// - @ref numberOfComponents.h +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +#include + +#include "eckit/log/Log.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the model-error type identifier. +/// +/// @section Deduction contract +/// - Reads: `par["modelErrorType"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction retrieves the model-error type identifier from the +/// parameter dictionary. +/// +/// The value is treated as mandatory: it cannot be derived from MARS +/// metadata alone and must be supplied by the upstream tool that +/// populates the parameter dictionary (typically read from the input +/// GRIB1 handle being re-encoded). +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary (unused). +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary. Must provide the key +/// `modelErrorType`. +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary (unused). +/// +/// @param[in] par +/// Parameter dictionary from which the model-error type is retrieved. +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The model-error type identifier. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If the key `modelErrorType` is missing from the parameter dictionary, +/// cannot be converted to `long`, or if any unexpected error occurs +/// during deduction. +/// +/// @note +/// This deduction does not infer or default the value. Absence of the +/// key in the parameter dictionary is considered a contract violation +/// by the upstream tool. +/// +template +long resolve_ModelErrorType_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Retrieve mandatory parameter-dictionary modelErrorType + long modelErrorType = get_or_throw(par, "modelErrorType"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`modelErrorType` resolved from input dictionaries: value="; + logMsg += std::to_string(modelErrorType); + return logMsg; + }()); + + // Success exit point + return modelErrorType; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `modelErrorType` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/numberOfComponents.h b/src/metkit/mars2grib/backend/deductions/numberOfComponents.h new file mode 100644 index 00000000..f831dca6 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/numberOfComponents.h @@ -0,0 +1,146 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// +/// @file numberOfComponents.h +/// @brief Deduction of the model-error ensemble size. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **total number of model-error realizations** +/// (`numberOfComponents`) from the parameter dictionary. +/// +/// The value is not derivable from MARS alone. It must be supplied via +/// the parameter dictionary by the upstream tool, typically read from +/// the input GRIB1 handle being re-encoded. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - enforcing deterministic resolution rules +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys directly +/// - do NOT apply heuristic or data-driven inference +/// - do NOT validate against GRIB code tables unless explicitly required +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved directly from input dictionaries +/// +/// @section References +/// Concept: +/// - @ref modelErrorEncoding.h +/// +/// Related deductions: +/// - @ref componentIndex.h +/// - @ref modelErrorType.h +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +#include + +#include "eckit/log/Log.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the total number of model-error realizations. +/// +/// @section Deduction contract +/// - Reads: `par["numberOfComponents"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction retrieves the total number of realizations in the +/// model-error ensemble from the parameter dictionary. +/// +/// The value is treated as mandatory: it cannot be derived from MARS +/// metadata alone and must be supplied by the upstream tool that +/// populates the parameter dictionary (typically read from the input +/// GRIB1 handle being re-encoded). +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary (unused). +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary. Must provide the key +/// `numberOfComponents`. +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary (unused). +/// +/// @param[in] par +/// Parameter dictionary from which the ensemble size is retrieved. +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The total number of model-error realizations. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If the key `numberOfComponents` is missing from the parameter +/// dictionary, cannot be converted to `long`, or if any unexpected error +/// occurs during deduction. +/// +/// @note +/// This deduction does not infer or default the value. Absence of the +/// key in the parameter dictionary is considered a contract violation +/// by the upstream tool. +/// +template +long resolve_NumberOfComponents_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Retrieve mandatory parameter-dictionary numberOfComponents + long numberOfComponents = get_or_throw(par, "numberOfComponents"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`numberOfComponents` resolved from input dictionaries: value="; + logMsg += std::to_string(numberOfComponents); + return logMsg; + }()); + + // Success exit point + return numberOfComponents; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `numberOfComponents` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index 7b70c5db..1d6f3648 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -46,6 +46,13 @@ inline const Recipe S2_R24 = Select >(); +// Model-error products +inline const Recipe S2_R25 = + make_recipe<25, + Select, + Select + >(); + // Analysis-related products inline const Recipe S2_R36 = make_recipe<36, @@ -61,6 +68,14 @@ inline const Recipe S2_R38 = Select >(); +// Analysis model-error products +inline const Recipe S2_R39 = + make_recipe<39, + Select, + Select, + Select + >(); + //------------------------------------------------------------------------------ // Virtual (encoder-specific) templates //------------------------------------------------------------------------------ @@ -96,8 +111,10 @@ inline const Recipes Section2Recipes{ 2, &S2_R15, &S2_R20, &S2_R24, + &S2_R25, &S2_R36, &S2_R38, + &S2_R39, &S2_R1001, &S2_R1002 } From 7550624fa688ee688d9989d1d6a1b9a3c0fb3f04 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 28 Apr 2026 16:49:05 +0000 Subject: [PATCH 03/17] mars2grib: split level concept Hybrid into ModelSingleLevel and ModelMultipleLevel - Replace the single LevelType::Hybrid variant with two variants: ModelSingleLevel for 2D fields published on the model-level system (no vertical column, no PV array) and ModelMultipleLevel for full vertical columns of model-level data, which require allocation and population of the PV array describing the hybrid coordinate transformation. Both variants map to GRIB typeOfLevel "hybrid", so encoded output is bit-identical for cases that previously used Hybrid; only encoder behaviour (PV allocation) differs between the two new variants. - Update the LevelList typelist to reflect the new variants and keep it in sync with the LevelType enumeration. - Update needPv to fire only on ModelMultipleLevel; update needLevel to cover both ModelSingleLevel and ModelMultipleLevel. - Add a new AbstractLevel variant carrying a numeric level value, sitting alongside the existing AbstractSingleLevel and AbstractMultipleLevel opaque variants. AbstractLevel is included in needLevel. - Rewrite matchML to dispatch single-level model paramIds (22, 127, 128, 129, 152) to ModelSingleLevel and the remaining multi-level set to ModelMultipleLevel. ERA6 paramIds 127 and 128 on ML, which were previously rejected, are now accepted as ModelSingleLevel. - Refresh Doxygen for the level concept to document the three orthogonal predicates needPv, needLevel, needTopBottomLevel and to describe the rationale for splitting Hybrid into single-level and multi-level model variants. - Fix typo in the level encoder header comment: "Se of typeOfLevel" becomes "Setting of typeOfLevel". --- .../backend/concepts/level/levelEncoding.h | 51 ++++++++++++++----- .../backend/concepts/level/levelEnum.h | 51 ++++++++++++++----- .../backend/concepts/level/levelMatcher.h | 16 ++++-- 3 files changed, 89 insertions(+), 29 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h b/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h index 3d004351..20cae4f8 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h @@ -20,12 +20,29 @@ /// /// - `typeOfLevel` /// - `level` -/// - hybrid vertical coordinate parameters (`pv` array) +/// - model-level coordinates and PV array (multi-level model layouts only) +/// - layered levels expressed via `topLevel` / `bottomLevel` +/// +/// The encoder behaviour is governed by three orthogonal compile-time +/// predicates over the variant: +/// +/// - `needPv()` : the variant requires a PV array +/// (currently only `ModelMultipleLevel`). +/// - `needLevel()` : the variant carries a numeric `level` +/// value to be written. +/// - `needTopBottomLevel()` : the variant is expressed as a +/// layer with explicit `topLevel` and +/// `bottomLevel` GRIB keys. +/// +/// These three axes are independent: any subset may apply to a given +/// variant. A small number of variants additionally have hard-coded +/// special cases inside `LevelOp` (for example the `*At2M` / `*At10M` +/// shortcuts and the `IsobaricInHpa` Pa->hPa conversion). /// /// Depending on the selected level variant, the concept may: /// - set only the level type, /// - set both level type and numeric level, -/// - allocate and populate the PV array (hybrid levels). +/// - allocate and populate the PV array (multi-level model layouts). /// /// The implementation follows the standard mars2grib concept model: /// - Compile-time applicability via `levelApplicable` @@ -67,8 +84,10 @@ namespace metkit::mars2grib::backend::concepts_ { /// /// @brief Compile-time predicate indicating whether a PV array is required. /// -/// Only hybrid vertical coordinates require a PV array describing the -/// vertical transformation. +/// Only multi-level model-level layouts require a PV array describing +/// the vertical hybrid coordinate transformation. Single-level model +/// fields share the GRIB `typeOfLevel="hybrid"` string but do not carry +/// a vertical column and therefore do not need a PV array. /// /// @tparam Variant Level concept variant /// @@ -77,7 +96,7 @@ namespace metkit::mars2grib::backend::concepts_ { /// template constexpr bool needPv() { - if constexpr (Variant == LevelType::Hybrid) { + if constexpr (Variant == LevelType::ModelMultipleLevel) { return true; } else { @@ -91,8 +110,13 @@ constexpr bool needPv() { /// /// @brief Compile-time predicate indicating whether a numeric `level` value is required. /// -/// Some level types require an associated numeric level (e.g. pressure, height), -/// while others encode only the level type. +/// Some level types require an associated numeric level (e.g. pressure, +/// height, model-level number). Both model-level variants +/// (`ModelSingleLevel` and `ModelMultipleLevel`) require it; they differ +/// only in whether a PV array is also needed. +/// +/// `AbstractLevel` carries an opaque numeric `level` value (in contrast +/// to `AbstractSingleLevel` and `AbstractMultipleLevel`, which do not). /// /// @tparam Variant Level concept variant /// @@ -104,10 +128,11 @@ constexpr bool needLevel() { if constexpr (Variant == LevelType::HeightAboveGroundAt10M || Variant == LevelType::HeightAboveGroundAt2M || Variant == LevelType::HeightAboveGround || Variant == LevelType::HeightAboveSeaAt10M || Variant == LevelType::HeightAboveSeaAt2M || Variant == LevelType::HeightAboveSea || - Variant == LevelType::Hybrid || Variant == LevelType::IsobaricInHpa || - Variant == LevelType::IsobaricInPa || Variant == LevelType::Isothermal || - Variant == LevelType::PotentialVorticity || Variant == LevelType::Theta || - Variant == LevelType::OceanModel) { + Variant == LevelType::ModelSingleLevel || Variant == LevelType::ModelMultipleLevel || + Variant == LevelType::IsobaricInHpa || Variant == LevelType::IsobaricInPa || + Variant == LevelType::Isothermal || Variant == LevelType::PotentialVorticity || + Variant == LevelType::Theta || Variant == LevelType::OceanModel || + Variant == LevelType::AbstractLevel) { return true; } else { @@ -156,7 +181,7 @@ constexpr bool needTopBottomLevel() { /// Applicability is evaluated entirely at compile time and is used by the /// concept dispatcher to control instantiation and execution. /// -/// Hybrid levels require special handling: +/// Multi-level model layouts require special handling: /// - during allocation stage to reserve space for the PV array, /// - during preset/runtime stages to set the level type and parameters. /// @@ -223,7 +248,7 @@ constexpr bool levelApplicable() { /// - All runtime errors are wrapped with full concept context /// (concept name, variant, stage, section). /// - The concept does not rely on pre-existing GRIB header state. -/// - Se of typeOfLevel is happening at both preset and runtime stages because +/// - Setting of typeOfLevel is happening at both preset and runtime stages because /// sometimes due to sideeffects in eccodes the typeOfLevel set at preset stage /// can be overwritten before runtime stage. /// diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h index c88dc368..1d8db7bf 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h @@ -81,9 +81,26 @@ inline constexpr std::string_view levelName{"level"}; /// - concrete GRIB levels (e.g. isobaric, hybrid, heightAboveGround) /// - abstract or logical levels used internally by the encoder /// +/// @note +/// Model-level coordinates are split into two variants: +/// - `ModelSingleLevel` for fields published as a single 2D layer on the +/// model-level system (no vertical column, no PV array). +/// - `ModelMultipleLevel` for full vertical columns of model-level data, +/// which require allocation and population of the PV array describing +/// the vertical hybrid coordinate transformation. +/// Both variants share the GRIB `typeOfLevel` string `"hybrid"`; they +/// differ only in encoder behaviour (PV allocation). +/// +/// @note +/// Three abstract variants exist: +/// - `AbstractSingleLevel` and `AbstractMultipleLevel`: opaque level +/// identifiers without an associated numeric `level` value. +/// - `AbstractLevel`: opaque level identifier that carries a numeric +/// `level` value (encoded via the `level` GRIB key). +/// /// @warning -/// Do not reorder existing enumerators, as they are used in compile-time -/// tables and registries. +/// Do not reorder existing enumerators in future changes, as their +/// numeric values are used in compile-time tables and registries. /// enum class LevelType : std::size_t { Surface = 0, @@ -103,7 +120,8 @@ enum class LevelType : std::size_t { MeanSea, HeightAboveSea, HeightAboveGround, - Hybrid, + ModelSingleLevel, ///< 2D field on model-level system; no PV array. + ModelMultipleLevel, ///< Full vertical column of model-level data; requires PV array. Theta, PotentialVorticity, SnowLayer, @@ -124,6 +142,7 @@ enum class LevelType : std::size_t { EntireMeltPond, WaterSurfaceToIsothermalOceanLayer, AbstractSingleLevel, + AbstractLevel, ///< Opaque level identifier carrying a numeric `level` value. AbstractMultipleLevel, HeightAboveSeaAt10M, HeightAboveSeaAt2M, @@ -150,15 +169,16 @@ using LevelList = LevelType::Tropopause, LevelType::NominalTop, LevelType::MostUnstableParcel, LevelType::MixedLayerParcel, LevelType::Isothermal, LevelType::IsobaricInPa, LevelType::IsobaricInHpa, LevelType::LowCloudLayer, LevelType::MediumCloudLayer, LevelType::HighCloudLayer, LevelType::MeanSea, LevelType::HeightAboveSea, - LevelType::HeightAboveGround, LevelType::Hybrid, LevelType::Theta, LevelType::PotentialVorticity, - LevelType::SnowLayer, LevelType::SoilLayer, LevelType::SeaIceLayer, LevelType::OceanSurface, - LevelType::DepthBelowSeaLayer, LevelType::OceanSurfaceToBottom, LevelType::LakeBottom, - LevelType::MixingLayer, LevelType::OceanModel, LevelType::OceanModelLayer, - LevelType::MixedLayerDepthByDensity, LevelType::MixedLayerDepthByTemperature, - LevelType::SnowLayerOverIceOnWater, LevelType::IceTopOnWater, LevelType::IceLayerOnWater, - LevelType::EntireMeltPond, LevelType::WaterSurfaceToIsothermalOceanLayer, LevelType::AbstractSingleLevel, - LevelType::AbstractMultipleLevel, LevelType::HeightAboveSeaAt10M, LevelType::HeightAboveSeaAt2M, - LevelType::HeightAboveGroundAt10M, LevelType::HeightAboveGroundAt2M, LevelType::Default>; + LevelType::HeightAboveGround, LevelType::ModelSingleLevel, LevelType::ModelMultipleLevel, + LevelType::Theta, LevelType::PotentialVorticity, LevelType::SnowLayer, LevelType::SoilLayer, + LevelType::SeaIceLayer, LevelType::OceanSurface, LevelType::DepthBelowSeaLayer, + LevelType::OceanSurfaceToBottom, LevelType::LakeBottom, LevelType::MixingLayer, LevelType::OceanModel, + LevelType::OceanModelLayer, LevelType::MixedLayerDepthByDensity, + LevelType::MixedLayerDepthByTemperature, LevelType::SnowLayerOverIceOnWater, LevelType::IceTopOnWater, + LevelType::IceLayerOnWater, LevelType::EntireMeltPond, LevelType::WaterSurfaceToIsothermalOceanLayer, + LevelType::AbstractSingleLevel, LevelType::AbstractLevel, LevelType::AbstractMultipleLevel, + LevelType::HeightAboveSeaAt10M, LevelType::HeightAboveSeaAt2M, LevelType::HeightAboveGroundAt10M, + LevelType::HeightAboveGroundAt2M, LevelType::Default>; /// @@ -205,7 +225,11 @@ DEF(LevelType::HighCloudLayer, "highCloudLayer"); DEF(LevelType::MeanSea, "meanSea"); DEF(LevelType::HeightAboveSea, "heightAboveSea"); DEF(LevelType::HeightAboveGround, "heightAboveGround"); -DEF(LevelType::Hybrid, "hybrid"); +// ModelSingleLevel and ModelMultipleLevel both encode as GRIB +// `typeOfLevel="hybrid"`; they differ only in encoder behaviour +// (PV array allocation, see needPv in levelEncoding.h). +DEF(LevelType::ModelSingleLevel, "hybrid"); +DEF(LevelType::ModelMultipleLevel, "hybrid"); DEF(LevelType::Theta, "theta"); DEF(LevelType::PotentialVorticity, "potentialVorticity"); DEF(LevelType::SnowLayer, "snowLayer"); @@ -226,6 +250,7 @@ DEF(LevelType::IceLayerOnWater, "iceLayerOnWater"); DEF(LevelType::EntireMeltPond, "entireMeltPond"); DEF(LevelType::WaterSurfaceToIsothermalOceanLayer, "waterSurfaceToIsothermalOceanLayer"); DEF(LevelType::AbstractSingleLevel, "abstractSingleLevel"); +DEF(LevelType::AbstractLevel, "abstractLevel"); DEF(LevelType::AbstractMultipleLevel, "abstractMultipleLevel"); DEF(LevelType::HeightAboveSeaAt10M, "heightAboveSeaAt10m"); DEF(LevelType::HeightAboveSeaAt2M, "heightAboveSeaAt2m"); diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index 3d23158c..f7b8d628 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -162,9 +162,19 @@ inline std::size_t matchML(const long param) { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, range(21, 23), range(75, 77), range(129, 133), 135, 138, 152, range(155, 157), 203, - range(246, 248), range(162100, 162113), 260290, 260292, 260293)) { - return static_cast(LevelType::Hybrid); + // Single-level subset of ML params: 2D fields published on the + // model-level levtype but not requiring a vertical PV array. This + // guard fires before the multi-level rule below; params listed here + // are removed from the multi-level set. + if (matchAny(param, 22, 127, 128, 129, 152)) { + return static_cast(LevelType::ModelSingleLevel); + } + + // Multi-level model fields: full vertical column, require allocation + // and population of the PV array describing the hybrid coordinate. + if (matchAny(param, 21, 23, range(75, 77), range(130, 133), 135, 138, range(155, 157), 203, range(246, 248), + range(162100, 162113), 260290, 260292, 260293)) { + return static_cast(LevelType::ModelMultipleLevel); } throw utils::exceptions::Mars2GribMatcherException( From e96b35c0121a4f3cbffebf9cfac3afa2414c3875 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 28 Apr 2026 19:15:52 +0000 Subject: [PATCH 04/17] mars2grib: Register new section2 templates {20,25,38,39} --- .../mars2grib/backend/sections/initializers/sectionRegistry.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h index 14e3ed1a..c99419eb 100644 --- a/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h +++ b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h @@ -77,8 +77,12 @@ template inline constexpr Entry Sec2Reg[] = { {1, &allocateTemplateNumber2<2, 1, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {15, &allocateTemplateNumber2<2, 15, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {20, &allocateTemplateNumber2<2, 20, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {24, &allocateTemplateNumber2<2, 24, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {25, &allocateTemplateNumber2<2, 25, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {36, &allocateTemplateNumber2<2, 36, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {38, &allocateTemplateNumber2<2, 38, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {39, &allocateTemplateNumber2<2, 39, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1000, &allocateTemplateNumber2<2, 1000, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1001, &allocateTemplateNumber2<2, 1001, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1002, &allocateTemplateNumber2<2, 1002, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, From 6f94fb973958d606d83a2fbb4714aca89d2d9463 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Wed, 29 Apr 2026 14:20:38 +0000 Subject: [PATCH 05/17] mars2grib: add ERA6 re-encoding support (covariance params, ensemble fc detection, model-error types) - params.yaml: register ECMWF covariance paramIds 254001..254017 on levtype=sfc. - levelMatcher: map paramIds 254001..254017 to LevelType::AbstractLevel (typeOfFirstFixedSurface=254); extend matchO2D with ocean paramIds 262146/262147 (DepthBelowSeaLayer) and 262148 (OceanSurfaceToBottom). - pointInTimeMatcher: add 254001..254017 and 262146..262148 to the default point-in-time set so Section 4 recipe selection succeeds for these params. - significanceOfReferenceTime: recognize MARS type "est" as a forecast type. - typeOfGeneratingProcess: * For type=fc, detect ensemble evidence (numberOfForecastsInEnsemble>1, typeOfEnsembleForecast present, or mars.number>0) and resolve to EnsembleForecast instead of Forecast; default behavior is preserved when no ensemble evidence is present. Adds detail to RESOLVE log. * Map type=eme/me (4D-Var model errors) to Analysis, matching the existing {4i,4v,me,eme} grouping in significanceOfReferenceTime. --- share/metkit/params.yaml | 18 ++++++ .../backend/concepts/level/levelMatcher.h | 12 +++- .../point-in-time/pointInTimeMatcher.h | 12 +++- .../deductions/significanceOfReferenceTime.h | 5 +- .../deductions/typeOfGeneratingProcess.h | 61 ++++++++++++++++++- 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/share/metkit/params.yaml b/share/metkit/params.yaml index 8d76016e..c0afde68 100644 --- a/share/metkit/params.yaml +++ b/share/metkit/params.yaml @@ -3603,6 +3603,24 @@ - 228240 - 228246 - 228247 +- - levtype: sfc + - - 254001 + - 254002 + - 254003 + - 254004 + - 254005 + - 254006 + - 254007 + - 254008 + - 254009 + - 254010 + - 254011 + - 254012 + - 254013 + - 254014 + - 254015 + - 254016 + - 254017 - - class: od levtype: al stream: elda diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index f7b8d628..2f05349c 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -141,6 +141,14 @@ inline std::size_t matchSFC(const long param) { return compile_time_registry_engine::MISSING; } + // ECMWF covariance paramIds (254001..254017) are defined in + // eccodes/definitions/grib2/localConcepts/{ecmf,era6}/paramId.def with + // typeOfFirstFixedSurface=254, which maps to the eccodes typeOfLevel + // concept "abstractLevel". + if (matchAny(param, range(254001, 254017))) { + return static_cast(LevelType::AbstractLevel); + } + throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SFC", Here()); } @@ -283,10 +291,10 @@ inline std::size_t matchO2D(const long param) { if (matchAny(param, 262116)) { return static_cast(LevelType::MixedLayerDepthByTemperature); } - if (matchAny(param, 262118, 262119, 262121, 262122)) { + if (matchAny(param, 262118, 262119, 262121, 262122, 262146, 262147)) { return static_cast(LevelType::DepthBelowSeaLayer); } - if (matchAny(param, 262120, 262123)) { + if (matchAny(param, 262120, 262123, 262148)) { return static_cast(LevelType::OceanSurfaceToBottom); } if (matchAny(param, 262141)) { diff --git a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h index 85fc7b42..2794e736 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h @@ -34,7 +34,7 @@ std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { 260509, 260688, 261001, 261002, range(261014, 261016), 261018, 261023, range(262000, 262009), 262011, 262014, 262015, 262017, 262018, 262023, 262024, range(262100, 262106), range(262108, 262112), range(262113, 262116), range(262118, 262125), 262130, range(262139, 262141), 262143, 262144, - range(262500, 262502), range(262505, 262507), 262900, 262906, 262907)) { + range(262146, 262149), range(262500, 262502), range(262505, 262507), 262900, 262906, 262907)) { return static_cast(PointInTimeType::Default); } @@ -53,6 +53,16 @@ std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(PointInTimeType::Default); } + // ECMWF covariance / analysis-uncertainty paramIds (254001..254017). + // These are point-in-time products living on the abstractLevel + // (typeOfFirstFixedSurface=254) and are used with MARS type=est + // (individual ensemble member, PDT=1) as well as with non-ensemble + // analyses (PDT=0). Without this mapping, PointInTimeConcept is left + // inactive and Section 4 recipe selection fails with "No matching recipe". + if (matchAny(param, range(254001, 254017))) { + return static_cast(PointInTimeType::Default); + } + return compile_time_registry_engine::MISSING; } diff --git a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h index ecbf50c2..d9e602ac 100644 --- a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h +++ b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h @@ -118,10 +118,11 @@ tables::SignificanceOfReferenceTime resolve_SignificanceOfReferenceTime_or_throw constexpr std::array analysisTypes = { {"an", "ia", "oi", "3v", "3g", "4g", "ea", "pa", "tpa", "ga", "gai", "ai", "af", "ab", "oai", "ga", "gai"}}; - constexpr std::array forecastTypes = { + constexpr std::array forecastTypes = { {"fc", "cf", "pf", "cm", "fp", "em", "ep", "es", "fa", "efi", "efic", "bf", "cd", "wem", "wes", "cr", "ses", "taem", "taes", "sg", "sf", "if", - "fcmean", "fcmax", "fcmin", "fcstdev", "ssd", "tf", "bf", "cd", "hcmean", "s3", "si"}}; + "fcmean", "fcmax", "fcmin", "fcstdev", "ssd", "tf", "bf", "cd", "hcmean", "s3", "si", + "est"}}; constexpr std::array startOfDataAssimilationTypes = {{"4i", "4v", "me", "eme"}}; diff --git a/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h b/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h index d18eca2f..b8542a7a 100644 --- a/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h +++ b/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h @@ -94,10 +94,11 @@ namespace metkit::mars2grib::backend::deductions { /// template std::optional resolve_TypeOfGeneratingProcess_opt( - const MarsDict_t& mars, [[maybe_unused]] const ParDict_t& par, [[maybe_unused]] const OptDict_t& opt) { + const MarsDict_t& mars, const ParDict_t& par, [[maybe_unused]] const OptDict_t& opt) { using metkit::mars2grib::backend::tables::TypeOfGeneratingProcess; using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; // N.B. Sometimes this is overwritten by eccodes as a side effect of setting `param` @@ -142,13 +143,69 @@ std::optional resolve_TypeOfGeneratingProcess_o } else if (marsTypeVal == "fc") { - tables::TypeOfGeneratingProcess result = TypeOfGeneratingProcess::Forecast; + // Detect ensemble evidence even when MARS `type` is the generic + // `fc`. Legacy GRIB1 data (and some rewritten streams) may carry + // `type=fc` together with ensemble-describing keys; in that case + // the correct GRIB2 `typeOfGeneratingProcess` is EnsembleForecast + // (4), not Forecast (2). + // + // Signals (any of): + // - par.numberOfForecastsInEnsemble > 1 + // - par.typeOfEnsembleForecast present + // - mars.number > 0 + const bool hasEnsembleSize = + has(par, "numberOfForecastsInEnsemble") && (get_or_throw(par, "numberOfForecastsInEnsemble") > 1); + const bool hasEnsembleType = has(par, "typeOfEnsembleForecast"); + const bool hasEnsembleNumber = has(mars, "number") && (get_or_throw(mars, "number") > 0); + + const bool isEnsemble = hasEnsembleSize || hasEnsembleType || hasEnsembleNumber; + + tables::TypeOfGeneratingProcess result = + isEnsemble ? TypeOfGeneratingProcess::EnsembleForecast : TypeOfGeneratingProcess::Forecast; // Emit RESOLVE log entry MARS2GRIB_LOG_RESOLVE([&]() { std::string logMsg = "`typeOfGeneratingProcess` resolved from input dictionaries: value='"; logMsg += tables::enum2name_TypeOfGeneratingProcess_or_throw(result); logMsg += "'"; + if (isEnsemble) { + logMsg += " (type='fc' with ensemble evidence:"; + if (hasEnsembleSize) { + logMsg += " numberOfForecastsInEnsemble>1"; + } + if (hasEnsembleType) { + logMsg += " typeOfEnsembleForecast-present"; + } + if (hasEnsembleNumber) { + logMsg += " number>0"; + } + logMsg += ")"; + } + else { + logMsg += " (type='fc', no ensemble evidence)"; + } + return logMsg; + }()); + + // Success exit point + return {result}; + } + else if (marsTypeVal == "eme" || marsTypeVal == "me") { + + // 4D-Var model-error fields (eme = ensemble model errors, + // me = model errors). Generated as part of the analysis + // system; the canonical ECMWF GRIB2 value is Analysis (0). + // Without an explicit mapping here the encoder fell back to the + // GRIB sample default, which only happened to be 0 by accident. + // Grouped with eme to mirror the {4i, 4v, me, eme} grouping + // already used in significanceOfReferenceTime. + tables::TypeOfGeneratingProcess result = TypeOfGeneratingProcess::Analysis; + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`typeOfGeneratingProcess` resolved from input dictionaries: value='"; + logMsg += tables::enum2name_TypeOfGeneratingProcess_or_throw(result); + logMsg += "' (type=eme/me)"; return logMsg; }()); From e8a67452a76afc31bf61f3de8f2dd6840344697b Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Wed, 29 Apr 2026 15:16:31 +0000 Subject: [PATCH 06/17] Run clang-format --- .../backend/concepts/level/levelEnum.h | 14 +++++++------- .../backend/deductions/componentIndex.h | 17 ++++++++--------- .../deductions/significanceOfReferenceTime.h | 7 +++---- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h index 1d8db7bf..bf625840 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h @@ -142,7 +142,7 @@ enum class LevelType : std::size_t { EntireMeltPond, WaterSurfaceToIsothermalOceanLayer, AbstractSingleLevel, - AbstractLevel, ///< Opaque level identifier carrying a numeric `level` value. + AbstractLevel, ///< Opaque level identifier carrying a numeric `level` value. AbstractMultipleLevel, HeightAboveSeaAt10M, HeightAboveSeaAt2M, @@ -173,12 +173,12 @@ using LevelList = LevelType::Theta, LevelType::PotentialVorticity, LevelType::SnowLayer, LevelType::SoilLayer, LevelType::SeaIceLayer, LevelType::OceanSurface, LevelType::DepthBelowSeaLayer, LevelType::OceanSurfaceToBottom, LevelType::LakeBottom, LevelType::MixingLayer, LevelType::OceanModel, - LevelType::OceanModelLayer, LevelType::MixedLayerDepthByDensity, - LevelType::MixedLayerDepthByTemperature, LevelType::SnowLayerOverIceOnWater, LevelType::IceTopOnWater, - LevelType::IceLayerOnWater, LevelType::EntireMeltPond, LevelType::WaterSurfaceToIsothermalOceanLayer, - LevelType::AbstractSingleLevel, LevelType::AbstractLevel, LevelType::AbstractMultipleLevel, - LevelType::HeightAboveSeaAt10M, LevelType::HeightAboveSeaAt2M, LevelType::HeightAboveGroundAt10M, - LevelType::HeightAboveGroundAt2M, LevelType::Default>; + LevelType::OceanModelLayer, LevelType::MixedLayerDepthByDensity, LevelType::MixedLayerDepthByTemperature, + LevelType::SnowLayerOverIceOnWater, LevelType::IceTopOnWater, LevelType::IceLayerOnWater, + LevelType::EntireMeltPond, LevelType::WaterSurfaceToIsothermalOceanLayer, LevelType::AbstractSingleLevel, + LevelType::AbstractLevel, LevelType::AbstractMultipleLevel, LevelType::HeightAboveSeaAt10M, + LevelType::HeightAboveSeaAt2M, LevelType::HeightAboveGroundAt10M, LevelType::HeightAboveGroundAt2M, + LevelType::Default>; /// diff --git a/src/metkit/mars2grib/backend/deductions/componentIndex.h b/src/metkit/mars2grib/backend/deductions/componentIndex.h index 1200c365..525e51fa 100644 --- a/src/metkit/mars2grib/backend/deductions/componentIndex.h +++ b/src/metkit/mars2grib/backend/deductions/componentIndex.h @@ -128,15 +128,14 @@ long resolve_ComponentIndex_or_throw(const MarsDict_t& mars, const ParDict_t& pa // with an unambiguous diagnostic. const std::string typeVal = get_or_throw(mars, "type"); if (typeVal != "eme") { - throw Mars2GribDeductionException( - std::string("`componentIndex` requested for a non-`eme` request: " - "`mars[\"type\"]` is `") - + typeVal - + "` but only `eme` is supported. This is a serious upstream " - "contract violation: the model-error deduction was reached " - "for a request that is not a model-error product. Check " - "recipe selection and matcher dispatch.", - Here()); + throw Mars2GribDeductionException(std::string("`componentIndex` requested for a non-`eme` request: " + "`mars[\"type\"]` is `") + + typeVal + + "` but only `eme` is supported. This is a serious upstream " + "contract violation: the model-error deduction was reached " + "for a request that is not a model-error product. Check " + "recipe selection and matcher dispatch.", + Here()); } // Retrieve mandatory MARS number (model-error realization id) diff --git a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h index d9e602ac..4d64f45e 100644 --- a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h +++ b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h @@ -119,10 +119,9 @@ tables::SignificanceOfReferenceTime resolve_SignificanceOfReferenceTime_or_throw {"an", "ia", "oi", "3v", "3g", "4g", "ea", "pa", "tpa", "ga", "gai", "ai", "af", "ab", "oai", "ga", "gai"}}; constexpr std::array forecastTypes = { - {"fc", "cf", "pf", "cm", "fp", "em", "ep", "es", "fa", "efi", "efic", - "bf", "cd", "wem", "wes", "cr", "ses", "taem", "taes", "sg", "sf", "if", - "fcmean", "fcmax", "fcmin", "fcstdev", "ssd", "tf", "bf", "cd", "hcmean", "s3", "si", - "est"}}; + {"fc", "cf", "pf", "cm", "fp", "em", "ep", "es", "fa", "efi", "efic", "bf", + "cd", "wem", "wes", "cr", "ses", "taem", "taes", "sg", "sf", "if", "fcmean", "fcmax", + "fcmin", "fcstdev", "ssd", "tf", "bf", "cd", "hcmean", "s3", "si", "est"}}; constexpr std::array startOfDataAssimilationTypes = {{"4i", "4v", "me", "eme"}}; From 46e02e8a860699dc8610b5ef72eb68f05d37dc8b Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 12 May 2026 12:54:02 +0000 Subject: [PATCH 07/17] mars2grib: add brightnessTemperature variant to satellite concept --- .../concepts/satellite/satelliteEncoding.h | 29 +++++++++--- .../concepts/satellite/satelliteEnum.h | 6 ++- .../concepts/satellite/satelliteMatcher.h | 7 ++- .../backend/deductions/numberOfFrequencies.h | 47 +++++++++++++++++++ .../docs/doxygen/mars2grib.config.in | 1 + .../section-recipes/impl/section2Recipes.h | 10 +++- 6 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h index 2ac3f05c..8862c79c 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h @@ -117,6 +117,7 @@ // Deductions #include "metkit/mars2grib/backend/deductions/channel.h" #include "metkit/mars2grib/backend/deductions/instrumentType.h" +#include "metkit/mars2grib/backend/deductions/numberOfFrequencies.h" #include "metkit/mars2grib/backend/deductions/satelliteNumber.h" #include "metkit/mars2grib/backend/deductions/satelliteSeries.h" #include "metkit/mars2grib/backend/deductions/scaleFactorOfCentralWaveNumber.h" @@ -214,14 +215,30 @@ void SatelliteOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& if constexpr (Section == SecLocalUseSection && Stage == StagePreset) { - // Check/Validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {24}); + if constexpr (Variant == SatelliteType::BrightnessTemperature) { - // Deductions - long channel = deductions::resolve_Channel_or_throw(mars, par, opt); + // Check/Validation + validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); - // Encoding - set_or_throw(out, "channel", channel); + // Deductions + long channelNumber = deductions::resolve_Channel_or_throw(mars, par, opt); + long numberOfFrequencies = deductions::resolve_NumberOfFrequencies_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "channelNumber", channelNumber); + set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); + } + else { + + // Check/Validation + validation::match_LocalDefinitionNumber_or_throw(opt, out, {24}); + + // Deductions + long channel = deductions::resolve_Channel_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "channel", channel); + } } if constexpr (Section == SecProductDefinitionSection && Stage == StageAllocate) { diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h index 2f52d1f0..dea8784d 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h @@ -85,7 +85,8 @@ inline constexpr std::string_view satelliteName{"satellite"}; /// tables and registries. /// enum class SatelliteType : std::size_t { - Default = 0 + Default = 0, + BrightnessTemperature = 1 }; @@ -101,7 +102,7 @@ enum class SatelliteType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using SatelliteList = ValueList; +using SatelliteList = ValueList; /// @@ -132,6 +133,7 @@ constexpr std::string_view satelliteTypeName(); } DEF(SatelliteType::Default, "default"); +DEF(SatelliteType::BrightnessTemperature, "brightnessTemperature"); #undef DEF diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h index 07dfba36..b7869f79 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -12,10 +12,15 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t satelliteMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; if (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) { - return static_cast(SatelliteType::Default); + if (has(mars, "param") && get_or_throw(mars, "param") == 194) { + return static_cast(SatelliteType::BrightnessTemperature); + } + + return static_cast(SatelliteType::Default); } return compile_time_registry_engine::MISSING; diff --git a/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h new file mode 100644 index 00000000..cc059ce3 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h @@ -0,0 +1,47 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +#pragma once + +#include + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +template +long resolve_NumberOfFrequencies_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + long numberOfFrequencies = get_or_throw(par, "numberOfFrequencies"); + + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`numberOfFrequencies` resolved from input dictionaries: value="; + logMsg += std::to_string(numberOfFrequencies); + return logMsg; + }()); + + return numberOfFrequencies; + } + catch (...) { + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `numberOfFrequencies` from input dictionaries", Here())); + }; + + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in b/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in index 001b3d7b..f0df1a50 100644 --- a/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in +++ b/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in @@ -1155,6 +1155,7 @@ INPUT = \@MARS2GRIB_SOURCE_DIRECTORY@/utils/configConverter.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/generatingProcessIdentifier.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/offsetToEndOf4DvarWindow.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/numberOfForecastsInEnsemble.h \ +@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/numberOfFrequencies.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/type.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/satelliteNumber.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/checks/matchDataRepresentationTemplateNumber.h \ diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index 1d6f3648..3918c940 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -43,7 +43,7 @@ inline const Recipe S2_R20 = inline const Recipe S2_R24 = make_recipe<24, Select, - Select + Select >(); // Model-error products @@ -60,6 +60,13 @@ inline const Recipe S2_R36 = Select >(); +// Brightness temperature satellite products +inline const Recipe S2_R37 = + make_recipe<37, + Select, + Select + >(); + // 4i Analysis-related products inline const Recipe S2_R38 = make_recipe<38, @@ -113,6 +120,7 @@ inline const Recipes Section2Recipes{ 2, &S2_R24, &S2_R25, &S2_R36, + &S2_R37, &S2_R38, &S2_R39, &S2_R1001, From 9d9ba347a981e28f966f069ad95075efc237c8db Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 12 May 2026 13:05:22 +0000 Subject: [PATCH 08/17] mars2grib: improve documentation --- .../mars2grib/backend/concepts/concepts.md | 73 ++++++++++++++++--- src/metkit/mars2grib/copilot-instructions.md | 65 +++++++++++++++++ .../docs/doxygen/mars2grib_concepts.md | 35 ++++++++- 3 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 src/metkit/mars2grib/copilot-instructions.md diff --git a/src/metkit/mars2grib/backend/concepts/concepts.md b/src/metkit/mars2grib/backend/concepts/concepts.md index 5d6037d1..c3535f75 100644 --- a/src/metkit/mars2grib/backend/concepts/concepts.md +++ b/src/metkit/mars2grib/backend/concepts/concepts.md @@ -47,7 +47,60 @@ The ordered list of variants for a concept defines its **local variant index spa --- -## 3. Concept Descriptor Contract +## 3. New Concept vs New Variant + +When changing the concept system, first decide whether the requested behavior is +a new concept or a new variant of an existing concept. + +Use a **new concept** when the feature is an independent semantic axis that must +be composable with other concepts. Use a **new variant** when the feature is an +alternative realization inside an existing semantic axis and does not require +independent composability. + +This distinction is often a domain decision and is usually not reliably +deducible from the code alone. If a request does not explicitly state whether a +new concept or a new variant is required, ask before implementing. + +--- + +## 4. Level Concept Guardrail + +The `level` concept is one of the most constrained concepts in mars2grib. +Although GRIB ultimately represents vertical levels through six low-level +fixed-surface keys: + +* `typeOfFirstFixedSurface` +* `scaleFactorOfFirstFixedSurface` +* `scaledValueOfFirstFixedSurface` +* `typeOfSecondFixedSurface` +* `scaleFactorOfSecondFixedSurface` +* `scaledValueOfSecondFixedSurface` + +mars2grib must not set these keys directly. Many combinations of these keys are +syntactically possible but semantically meaningless for ECMWF products. + +Instead, the encoder must rely on the official level abstraction: + +* `typeOfLevel` +* `level`, when required +* `topLevel` / `bottomLevel`, when required +* PV-array data, when required + +Each supported `typeOfLevel` corresponds to a `LevelType` variant, apart from a +few virtual type-of-level values kept in mars2grib because they cannot be added +to ecCodes for backward-compatibility reasons. Each variant maps to a prescribed +configuration of the low-level fixed-surface keys. + +Do not implement level fixes by injecting `typeOfFirstFixedSurface`, +`scaleFactorOfFirstFixedSurface`, `scaledValueOfFirstFixedSurface`, +`typeOfSecondFixedSurface`, `scaleFactorOfSecondFixedSurface`, or +`scaledValueOfSecondFixedSurface`. If a new level behavior is required, add or +adjust the appropriate `LevelType` variant, matcher mapping, or deduction so the +level remains encoded through `typeOfLevel` and the official level interface. + +--- + +## 5. Concept Descriptor Contract Each concept is implemented as a **descriptor type** that conforms to the `RegisterEntryDescriptor` interface. @@ -68,7 +121,7 @@ The descriptor contains **no runtime state** and no virtual functions. --- -## 4. Capabilities +## 6. Capabilities Concepts may expose multiple independent *capabilities*. @@ -87,7 +140,7 @@ independent dispatch planes. --- -## 5. The Concept Universe (`AllConcepts`) +## 7. The Concept Universe (`AllConcepts`) All concepts known to the system are aggregated into a single ordered typelist: @@ -107,7 +160,7 @@ Changing this order is a **breaking structural change**. --- -## 6. Concept Identifiers +## 8. Concept Identifiers Each concept is assigned a **stable numeric identifier** based on its position in `AllConcepts`. @@ -130,7 +183,7 @@ They are used as indices into: --- -## 7. Variant Index Spaces +## 9. Variant Index Spaces Variants are indexed in two ways: @@ -158,7 +211,7 @@ The global variant index is the primary key used by: --- -## 8. Matching Phase +## 10. Matching Phase Matching determines **which concepts and variants are active** for a given input request. @@ -179,7 +232,7 @@ The result is an `ActiveConceptsData` structure. --- -## 9. Encoding Phases +## 11. Encoding Phases Encoding is divided into **logical stages**, such as: @@ -201,7 +254,7 @@ All dispatch tables are generated **entirely at compile time**. --- -## 10. Design Principles +## 12. Design Principles The concept system is designed around the following principles: @@ -217,7 +270,7 @@ Execution code performs *only iteration and invocation*. --- -## 11. Adding a New Concept +## 13. Adding a New Concept To add a new concept: @@ -231,7 +284,7 @@ No registry code needs to be modified. --- -## 12. Summary +## 14. Summary Concepts are the **semantic backbone** of the mars2grib backend. diff --git a/src/metkit/mars2grib/copilot-instructions.md b/src/metkit/mars2grib/copilot-instructions.md new file mode 100644 index 00000000..69aee07e --- /dev/null +++ b/src/metkit/mars2grib/copilot-instructions.md @@ -0,0 +1,65 @@ +# Copilot Instructions for mars2grib + +These instructions apply to AI-assisted changes under `src/metkit/mars2grib`. + +## Concept Or Variant Decision + +Before implementing any modification in the mars2grib concept system, determine whether the requested behavior is a new concept or a new variant of an existing concept. + +- Use a new concept when the feature is an independent semantic axis that must be composable with other concepts. +- Use a new variant when the feature is an alternative realization inside an existing semantic axis and does not need independent composability. +- This distinction is usually not reliably deducible from code structure alone. +- If the user has not explicitly said whether the change is a new concept or a variant, ask the user before implementing. + +## Level Concept Guardrail + +The `level` concept is intentionally constrained. In the GRIB header, vertical level information is ultimately represented by these six low-level fixed-surface keys: + +- `typeOfFirstFixedSurface` +- `scaleFactorOfFirstFixedSurface` +- `scaledValueOfFirstFixedSurface` +- `typeOfSecondFixedSurface` +- `scaleFactorOfSecondFixedSurface` +- `scaledValueOfSecondFixedSurface` + +Do not set these keys directly as a shortcut or workaround. + +mars2grib encodes only official level definitions by relying on `typeOfLevel` plus, when needed, `level`, `topLevel`, `bottomLevel`, and PV-array data. Each supported `typeOfLevel` maps to a prescribed fixed-surface configuration. Some virtual `typeOfLevel` values exist because they cannot be introduced in ecCodes for backward-compatibility reasons. + +If a requested change appears to require direct writes to fixed-surface keys, do not implement that approach. Instead, add or adjust the appropriate `LevelType` variant, matcher mapping, or deduction so the level remains encoded through the official level abstraction. + + +## Documentation synchronization rule + +When working on a pull request: + +1. Determine the set of files modified by the PR. +2. From that set, consider only files under: + - `src/metkit/mars2grib` + - Changes under `tests/mars2grib` require documentation updates only if they affect documented public behaviour + +3. For each of those files: + - Verify that the related documentation is present in the concrete doc locations for mars2grib, including (where applicable): + - `src/metkit/mars2grib/docs/**` + - Any module-level `.md` files or Doxygen pages associated with the modified code + - Verify that the documentation in these locations is up to date with the code changes + - If documentation is missing or outdated in these locations, propose the required updates + +4. Do not request documentation changes for files outside these paths. + +## Definition of “documentation in sync” + +Documentation must: +- Describe the current public behavior and interfaces +- Reflect any new parameters, options, or outputs +- Remove references to deleted functionality + +## PR review behavior + +During PR reviews: +- Explicitly list the impacted files in the two target directories +- State whether documentation is: + - ✅ in sync + - ❌ missing + - ❌ outdated +- Suggest concrete doc patches when needed, referencing the specific lines or sections that require updates. \ No newline at end of file diff --git a/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md b/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md index 8cc63b95..09d72877 100644 --- a/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md +++ b/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md @@ -32,6 +32,40 @@ Concept::Variant Each `(Concept, Variant)` pair represents a distinct semantic realization and is treated as an independent entity by the Encoder. +@subsection concepts_new_concept_or_variant New Concept or New Variant + +Before changing the concept system, decide whether the behavior belongs to a new +concept or to a new variant of an existing concept. + +- A new concept is appropriate for an independent semantic axis that must be + composable with other concepts. +- A new variant is appropriate for an alternative realization inside an existing + semantic axis when independent composability is not required. + +This is a domain decision and is not always apparent from the code structure. + +@section concepts_level_guardrail Level Concept Guardrail + +The `level` concept deliberately hides the raw fixed-surface representation used +by GRIB. GRIB vertical levels are ultimately represented by: + +- `typeOfFirstFixedSurface` +- `scaleFactorOfFirstFixedSurface` +- `scaledValueOfFirstFixedSurface` +- `typeOfSecondFixedSurface` +- `scaleFactorOfSecondFixedSurface` +- `scaledValueOfSecondFixedSurface` + +These keys must not be set directly by mars2grib concept changes. Although many +combinations are technically possible, most are not meaningful ECMWF levels. + +Level encoding must go through the official abstraction: `typeOfLevel` plus, when +needed, `level`, `topLevel`, `bottomLevel`, and PV-array data. Each supported +`typeOfLevel` corresponds to a `LevelType` variant or to a small number of +virtual type-of-level values maintained in mars2grib for backward-compatibility +reasons. If new level behavior is required, update the `LevelType` variant, +matcher mapping, or deduction instead of writing fixed-surface keys directly. + @section concepts_registration_value Registration Value The value associated with each `Concept::Variant` key is a **dense, fixed-size @@ -132,4 +166,3 @@ Concepts intentionally do not: Concepts are purely declarative, statically registered contributors to the encoding process. - From 3bb7dbc8154b4aeec7985cdc414c022ade657519 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 12 May 2026 13:33:18 +0000 Subject: [PATCH 09/17] mars2grib: fix recipe for LocalSectionNumbe=37 --- .../frontend/resolution/section-recipes/impl/section2Recipes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index 3918c940..8e48ca43 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -64,6 +64,7 @@ inline const Recipe S2_R36 = inline const Recipe S2_R37 = make_recipe<37, Select, + Select, Select >(); From 1f6a8fd435629dc3bbe24c536309e014046a454c Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 14 May 2026 23:13:07 +0000 Subject: [PATCH 10/17] mars2grib: add default for numberOfFrequencies --- .../backend/deductions/numberOfFrequencies.h | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h index cc059ce3..40faf47f 100644 --- a/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h +++ b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h @@ -23,18 +23,33 @@ template long resolve_NumberOfFrequencies_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; try { - long numberOfFrequencies = get_or_throw(par, "numberOfFrequencies"); - MARS2GRIB_LOG_RESOLVE([&]() { - std::string logMsg = "`numberOfFrequencies` resolved from input dictionaries: value="; - logMsg += std::to_string(numberOfFrequencies); - return logMsg; - }()); + if (has(par, "numberOfFrequencies")) { + long numberOfFrequencies = get_or_throw(par, "numberOfFrequencies"); - return numberOfFrequencies; + MARS2GRIB_LOG_OVERRIDE([&]() { + std::string logMsg = "`numberOfFrequencies` resolved from input dictionaries: value="; + logMsg += std::to_string(numberOfFrequencies); + return logMsg; + }()); + + return numberOfFrequencies; + } + else { + long numberOfFrequencies = 54; + + MARS2GRIB_LOG_DEFAULT([&]() { + std::string logMsg = "`numberOfFrequencies` resolved from input dictionaries: value="; + logMsg += std::to_string(numberOfFrequencies); + return logMsg; + }()); + + return numberOfFrequencies; + } } catch (...) { std::throw_with_nested( From 97055b9fb98a47b46f4ffad17d81ecf48f3e1b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domokos=20S=C3=A1rm=C3=A1ny?= Date: Wed, 13 May 2026 21:40:04 +0100 Subject: [PATCH 11/17] mars2grib: resolve typeOfGeneratingProcess=4 for ensemble statistics types (est, es, em, ses) --- .../deductions/typeOfGeneratingProcess.h | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h b/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h index b8542a7a..1f78c3bd 100644 --- a/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h +++ b/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h @@ -190,6 +190,26 @@ std::optional resolve_TypeOfGeneratingProcess_o // Success exit point return {result}; } + else if (marsTypeVal == "est" || marsTypeVal == "es" || marsTypeVal == "em" || marsTypeVal == "ses") { + + // Ensemble-derived statistical products (ensemble statistics, + // ensemble standard deviation, ensemble mean, ensemble spread + // of estimation). No dedicated code table entry exists for + // "ensemble-derived analysis"; EnsembleForecast (4) is the + // established convention to signal ensemble provenance. + tables::TypeOfGeneratingProcess result = TypeOfGeneratingProcess::EnsembleForecast; + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`typeOfGeneratingProcess` resolved from input dictionaries: value='"; + logMsg += tables::enum2name_TypeOfGeneratingProcess_or_throw(result); + logMsg += "' (type=" + marsTypeVal + ")"; + return logMsg; + }()); + + // Success exit point + return {result}; + } else if (marsTypeVal == "eme" || marsTypeVal == "me") { // 4D-Var model-error fields (eme = ensemble model errors, From ee28c428248d0d72dc5bf42f08194c0000007836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domokos=20S=C3=A1rm=C3=A1ny?= Date: Wed, 13 May 2026 21:45:02 +0100 Subject: [PATCH 12/17] mars2grib: route type=ses to PDT=2 (derived ensemble forecast) --- src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index 815ed738..3d94ad2f 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -18,6 +18,7 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { const auto& type = get_or_throw(mars, "type"); if (type == "em" || // Ensemble mean type == "es" || // Ensemble standard deviation + type == "ses" || // Ensemble spread of estimation type == "taem" || // Time-averaged ensemble mean type == "taes" || // Time-averaged ensemble standard deviation type == "efi" || // Extreme forecast index From 3d5e8c9337481e904f95d31579e0f54ce9c15ee5 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 26 May 2026 15:24:11 +0000 Subject: [PATCH 13/17] Fix multiple bugs for encoding of BrightnessTemperature --- .../concepts/analysis/analysisEncoding.h | 2 +- .../concepts/derived/derivedEncoding.h | 39 ++++++++++++++----- .../backend/concepts/derived/derivedEnum.h | 38 ++---------------- .../backend/concepts/derived/derivedMatcher.h | 7 ++++ .../backend/concepts/level/levelMatcher.h | 13 ++----- .../concepts/satellite/satelliteMatcher.h | 14 +++++-- .../backend/deductions/derivedForecast.h | 2 +- .../sections/initializers/sectionRegistry.h | 1 + .../section-recipes/impl/section2Recipes.h | 12 +++++- 9 files changed, 67 insertions(+), 61 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h index 1e6b08b7..39de4284 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h @@ -150,7 +150,7 @@ void AnalysisOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& o MARS2GRIB_LOG_CONCEPT(analysis); // Structural validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L, 38L, 39L}); + validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L, 37L, 38L, 39L}); // Deductions long offsetToEndOf4DvarWindowVal = deductions::resolve_offsetToEndOf4DvarWindow_or_throw(mars, par, opt); diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h index 30597585..ad895476 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h @@ -51,8 +51,10 @@ #include "metkit/mars2grib/utils/generalUtils.h" // Deductions +#include "metkit/mars2grib/backend/deductions/channel.h" #include "metkit/mars2grib/backend/deductions/derivedForecast.h" #include "metkit/mars2grib/backend/deductions/numberOfForecastsInEnsemble.h" +#include "metkit/mars2grib/backend/deductions/numberOfFrequencies.h" // Tables #include "metkit/mars2grib/backend/tables/derivedForecast.h" @@ -96,7 +98,7 @@ namespace metkit::mars2grib::backend::concepts_ { /// template constexpr bool derivedApplicable() { - return (Stage == StagePreset) && (Section == SecProductDefinitionSection); + return (Stage == StagePreset) && ((Section == SecProductDefinitionSection) || (Section == SecLocalUseSection)); } /// @@ -158,16 +160,35 @@ void DerivedOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op MARS2GRIB_LOG_CONCEPT(derived); - // Structural validation - validation::check_DerivedProductDefinitionSection_or_throw(opt, out); + if constexpr (Section == SecLocalUseSection && Stage == StagePreset && + Variant == DerivedType::BrightnessTemperature) { - // Deductions - tables::DerivedForecast derivedForecast = deductions::resolve_DerivedForecast_or_throw(mars, par, opt); - long numberOfForecastsInEnsemble = deductions::resolve_NumberOfForecastsInEnsemble_or_throw(mars, par, opt); + // Check/Validation + validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); - // Encoding - set_or_throw(out, "derivedForecast", static_cast(derivedForecast)); - set_or_throw(out, "numberOfForecastsInEnsemble", numberOfForecastsInEnsemble); + // Deductions + long channelNumber = deductions::resolve_Channel_or_throw(mars, par, opt); + long numberOfFrequencies = deductions::resolve_NumberOfFrequencies_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "channelNumber", channelNumber); + set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); + } + + if constexpr (Section == SecProductDefinitionSection && Stage == StagePreset && + Variant != DerivedType::BrightnessTemperature) { + // Structural validation + validation::check_DerivedProductDefinitionSection_or_throw(opt, out); + + // Deductions + tables::DerivedForecast derivedForecast = deductions::resolve_DerivedForecast_or_throw(mars, par, opt); + long numberOfForecastsInEnsemble = + deductions::resolve_NumberOfForecastsInEnsemble_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "derivedForecast", static_cast(derivedForecast)); + set_or_throw(out, "numberOfForecastsInEnsemble", numberOfForecastsInEnsemble); + } } catch (...) { diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h index 5971d7e9..0d84bc3a 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h @@ -83,21 +83,7 @@ inline constexpr std::string_view derivedName{"derived"}; /// tables and registries. /// enum class DerivedType : std::size_t { - Individual = 0, - Derived, - PerturbedParameters, - RandomPatterns, - MeanUnweightedAll, - MeanWeightedAll, - StddevCluster, - StddevClusterNorm, - SpreadAll, - LargeAnomalyIndex, - MeanUnweightedCluster, - Iqr, - MinAll, - MaxAll, - VarianceAll, + BrightnessTemperature, // Special variant for satellite brightness temperature products Default }; @@ -114,11 +100,7 @@ enum class DerivedType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using DerivedList = ValueList; +using DerivedList = ValueList; /// @@ -148,21 +130,7 @@ constexpr std::string_view derivedTypeName(); return NAME; \ } -DEF(DerivedType::Individual, "individual"); -DEF(DerivedType::Derived, "derived"); -DEF(DerivedType::PerturbedParameters, "perturbedParameters"); -DEF(DerivedType::RandomPatterns, "randomPatterns"); -DEF(DerivedType::MeanUnweightedAll, "meanUnweightedAll"); -DEF(DerivedType::MeanWeightedAll, "meanWeightedAll"); -DEF(DerivedType::StddevCluster, "stddevCluster"); -DEF(DerivedType::StddevClusterNorm, "stddevClusterNorm"); -DEF(DerivedType::SpreadAll, "spreadAll"); -DEF(DerivedType::LargeAnomalyIndex, "largeAnomalyIndex"); -DEF(DerivedType::MeanUnweightedCluster, "meanUnweightedCluster"); -DEF(DerivedType::Iqr, "iqr"); -DEF(DerivedType::MinAll, "minAll"); -DEF(DerivedType::MaxAll, "maxAll"); -DEF(DerivedType::VarianceAll, "varianceAll"); +DEF(DerivedType::BrightnessTemperature, "brightnessTemperature"); DEF(DerivedType::Default, "default"); #undef DEF diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index 3d94ad2f..722db81b 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -13,7 +13,9 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; const auto& type = get_or_throw(mars, "type"); if (type == "em" || // Ensemble mean @@ -27,6 +29,11 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(DerivedType::Default); } + if (has(mars, "channel") && has(mars, "param") && get_or_throw(mars, "param") == 194 && has(mars, "stream") && + get_or_throw(mars, "stream") == "elda") { + return static_cast(DerivedType::BrightnessTemperature); + } + return compile_time_registry_engine::MISSING; } diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index 2f05349c..07dfb589 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -33,10 +33,10 @@ inline std::size_t matchSFC(const long param) { if (matchAny(param, 262118)) { return static_cast(LevelType::DepthBelowSeaLayer); } - if (matchAny(param, 59, 78, 79, 136, 137, 164, 206, range(162059, 162063), 162071, 162072, 162093, 228001, 228044, - 228050, 228052, range(228088, 228090), 228164, 235087, 235088, 235136, 235137, 235287, 235288, 235290, - 235326, 235383, 237087, 237088, 237137, 237287, 237288, 237290, 237326, 238087, 238088, 238137, 238287, - 238288, 238290, 238326, 239087, 239088, 239137, 239287, 239288, 239290, 239326, 260132)) { + if (matchAny(param, 59, 78, 79, 136, 137, 164, 194, 206, range(162059, 162063), 162071, 162072, 162093, 228001, + 228044, 228050, 228052, range(228088, 228090), 228164, 235087, 235088, 235136, 235137, 235287, 235288, + 235290, 235326, 235383, 237087, 237088, 237137, 237287, 237288, 237290, 237326, 238087, 238088, 238137, + 238287, 238288, 238290, 238326, 239087, 239088, 239137, 239287, 239288, 239290, 239326, 260132)) { return static_cast(LevelType::EntireAtmosphere); } if (matchAny(param, 228007, 228011)) { @@ -126,11 +126,6 @@ inline std::size_t matchSFC(const long param) { return static_cast(LevelType::Tropopause); } - // Satellite - if (matchAny(param, 194)) { - return static_cast(LevelType::Surface); - } - // Chemical if (matchAny(param, range(228080, 228085), range(233032, 233035), range(235062, 235064))) { return static_cast(LevelType::Surface); diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h index b7869f79..5c164eea 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -15,11 +15,17 @@ std::size_t satelliteMatcher(const MarsDict_t& mars, const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; - if (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) { - if (has(mars, "param") && get_or_throw(mars, "param") == 194) { - return static_cast(SatelliteType::BrightnessTemperature); - } + // BrightnessTemperature (paramId=194): only requires channel. + // Section 2 (local def 37) encodes channelNumber + numberOfFrequencies. + // Section 4 satellite band metadata (ident, instrument, series, waveNumber) + // is only present for PDT 32/33 — the encoding handles this conditionally. + if (has(mars, "channel") && has(mars, "param") && get_or_throw(mars, "param") == 194 && has(mars, "stream") && + get_or_throw(mars, "stream") == "oper") { + return static_cast(SatelliteType::BrightnessTemperature); + } + // Default satellite: requires full satellite identification keys + if (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) { return static_cast(SatelliteType::Default); } diff --git a/src/metkit/mars2grib/backend/deductions/derivedForecast.h b/src/metkit/mars2grib/backend/deductions/derivedForecast.h index 98e474f0..eae25c5c 100644 --- a/src/metkit/mars2grib/backend/deductions/derivedForecast.h +++ b/src/metkit/mars2grib/backend/deductions/derivedForecast.h @@ -146,7 +146,7 @@ tables::DerivedForecast resolve_DerivedForecast_or_throw(const MarsDict_t& mars, if (marsType == "em" || marsType == "taem") { derivedForecast = tables::DerivedForecast::UnweightedMeanAllMembers; } - else if (marsType == "es" || marsType == "taes") { + else if (marsType == "es" || marsType == "ses" || marsType == "taes") { derivedForecast = tables::DerivedForecast::SpreadAllMembers; } else { diff --git a/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h index c99419eb..a15606f8 100644 --- a/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h +++ b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h @@ -81,6 +81,7 @@ inline constexpr Entry Sec2Reg[] = {24, &allocateTemplateNumber2<2, 24, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {25, &allocateTemplateNumber2<2, 25, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {36, &allocateTemplateNumber2<2, 36, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {37, &allocateTemplateNumber2<2, 37, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {38, &allocateTemplateNumber2<2, 38, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {39, &allocateTemplateNumber2<2, 39, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1000, &allocateTemplateNumber2<2, 1000, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index 8e48ca43..e9d23fa7 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -61,13 +61,20 @@ inline const Recipe S2_R36 = >(); // Brightness temperature satellite products -inline const Recipe S2_R37 = +inline const Recipe S2_R37A = make_recipe<37, Select, Select, Select >(); +inline const Recipe S2_R37B = + make_recipe<37, + Select, + Select, + Select + >(); + // 4i Analysis-related products inline const Recipe S2_R38 = make_recipe<38, @@ -121,7 +128,8 @@ inline const Recipes Section2Recipes{ 2, &S2_R24, &S2_R25, &S2_R36, - &S2_R37, + &S2_R37A, + &S2_R37B, &S2_R38, &S2_R39, &S2_R1001, From 2713be39f892a253de9f5e63e7e5afa706a7b55b Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Wed, 27 May 2026 14:01:59 +0000 Subject: [PATCH 14/17] mars2grib: bugfix, fix BrightnessTemperatureEnsembleMean --- .../backend/concepts/derived/derivedEncoding.h | 5 ++--- .../backend/concepts/derived/derivedEnum.h | 6 +++--- .../backend/concepts/derived/derivedMatcher.h | 14 +++++++++----- .../section-recipes/impl/section2Recipes.h | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h index ad895476..c1fb40cc 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h @@ -161,7 +161,7 @@ void DerivedOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op MARS2GRIB_LOG_CONCEPT(derived); if constexpr (Section == SecLocalUseSection && Stage == StagePreset && - Variant == DerivedType::BrightnessTemperature) { + Variant == DerivedType::BrightnessTemperatureEnsembleMean) { // Check/Validation validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); @@ -175,8 +175,7 @@ void DerivedOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); } - if constexpr (Section == SecProductDefinitionSection && Stage == StagePreset && - Variant != DerivedType::BrightnessTemperature) { + if constexpr (Section == SecProductDefinitionSection && Stage == StagePreset) { // Structural validation validation::check_DerivedProductDefinitionSection_or_throw(opt, out); diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h index 0d84bc3a..5b062f03 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h @@ -83,7 +83,7 @@ inline constexpr std::string_view derivedName{"derived"}; /// tables and registries. /// enum class DerivedType : std::size_t { - BrightnessTemperature, // Special variant for satellite brightness temperature products + BrightnessTemperatureEnsembleMean, // Special variant for satellite brightness temperature products Default }; @@ -100,7 +100,7 @@ enum class DerivedType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using DerivedList = ValueList; +using DerivedList = ValueList; /// @@ -130,7 +130,7 @@ constexpr std::string_view derivedTypeName(); return NAME; \ } -DEF(DerivedType::BrightnessTemperature, "brightnessTemperature"); +DEF(DerivedType::BrightnessTemperatureEnsembleMean, "brightnessTemperatureEnsembleMean"); DEF(DerivedType::Default, "default"); #undef DEF diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index 722db81b..e1a98fef 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -18,8 +18,7 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::has; const auto& type = get_or_throw(mars, "type"); - if (type == "em" || // Ensemble mean - type == "es" || // Ensemble standard deviation + if (type == "es" || // Ensemble standard deviation type == "ses" || // Ensemble spread of estimation type == "taem" || // Time-averaged ensemble mean type == "taes" || // Time-averaged ensemble standard deviation @@ -29,9 +28,14 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(DerivedType::Default); } - if (has(mars, "channel") && has(mars, "param") && get_or_throw(mars, "param") == 194 && has(mars, "stream") && - get_or_throw(mars, "stream") == "elda") { - return static_cast(DerivedType::BrightnessTemperature); + if (type == "em") { // Ensemble mean (special handling for brightness temperature products) + if (has(mars, "channel") && has(mars, "param") && get_or_throw(mars, "param") == 194 && + has(mars, "stream") && get_or_throw(mars, "stream") == "elda") { + return static_cast(DerivedType::BrightnessTemperatureEnsembleMean); + } + else { + return static_cast(DerivedType::Default); + } } return compile_time_registry_engine::MISSING; diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index e9d23fa7..52f2d7bf 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -72,7 +72,7 @@ inline const Recipe S2_R37B = make_recipe<37, Select, Select, - Select + Select >(); // 4i Analysis-related products From b62d228bf8e1eacfd48cd54abca3bcf526faade5 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 12:58:30 +0000 Subject: [PATCH 15/17] mars2grib: split brightness temperature into its own concept Represent brightness-temperature metadata as a dedicated concept so it can be selected independently of the surrounding satellite or derived product family. This removes the brightness-temperature variants from satellite and derived handling and routes section 2 recipes through the new concept. --- .../mars2grib/backend/concepts/AllConcepts.h | 3 +- .../brightnessTemperatureConceptDescriptor.h | 174 +++++++++++++++++ .../brightnessTemperatureEncoding.h | 181 ++++++++++++++++++ .../brightnessTemperatureEnum.h | 126 ++++++++++++ .../brightnessTemperatureMatcher.h | 86 +++++++++ .../concepts/derived/derivedEncoding.h | 15 -- .../backend/concepts/derived/derivedEnum.h | 4 +- .../backend/concepts/derived/derivedMatcher.h | 13 +- .../concepts/satellite/satelliteEncoding.h | 29 +-- .../concepts/satellite/satelliteEnum.h | 7 +- .../concepts/satellite/satelliteMatcher.h | 9 - .../section-recipes/impl/section2Recipes.h | 7 +- 12 files changed, 586 insertions(+), 68 deletions(-) create mode 100644 src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h create mode 100644 src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h create mode 100644 src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h create mode 100644 src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h diff --git a/src/metkit/mars2grib/backend/concepts/AllConcepts.h b/src/metkit/mars2grib/backend/concepts/AllConcepts.h index 257e4fc5..6edf8b14 100644 --- a/src/metkit/mars2grib/backend/concepts/AllConcepts.h +++ b/src/metkit/mars2grib/backend/concepts/AllConcepts.h @@ -116,6 +116,7 @@ #include "metkit/mars2grib/backend/concepts/statistics/statisticsConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/tables/tablesConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/wave/waveConceptDescriptor.h" +#include "metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h" namespace metkit::mars2grib::backend::concepts_::detail { @@ -174,6 +175,6 @@ using AllConcepts = TypeList; + ShapeOfTheEarthConcept, StatisticsConcept, TablesConcept, WaveConcept, ModelErrorConcept, BrightnessTemperatureConcept>; } // namespace metkit::mars2grib::backend::concepts_::detail \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h new file mode 100644 index 00000000..250f39c4 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h @@ -0,0 +1,174 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// @file brightnessTemperatureConceptDescriptor.h +/// @brief Compile-time registry entry for the GRIB `brightnessTemperature` concept. +/// +/// This header defines `BrightnessTemperatureConcept`, the **compile-time +/// descriptor** that registers the GRIB `brightnessTemperature` concept into +/// the mars2grib compile-time registry engine. +/// +/// The descriptor provides: +/// - The concept name +/// - The mapping between variants and their symbolic names +/// - The set of callbacks associated with each encoding phase +/// - The entry-level matcher used to activate the concept +/// +/// This file contains **no runtime logic**. All decisions are resolved at +/// compile time through template instantiation. +/// +/// @ingroup mars2grib_backend_concepts + +#pragma once + +// System includes +#include + +// Registry engine +#include "metkit/mars2grib/backend/compile-time-registry-engine/RegisterEntryDescriptor.h" +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Core concept includes +#include "metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h" +#include "metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h" +#include "metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h" + +namespace metkit::mars2grib::backend::concepts_ { + +// Importing the compile-time registry engine namespace locally to avoid +// excessive verbosity in template-heavy code. This is restricted to an +// internal scope and not exposed through public headers. +using namespace metkit::mars2grib::backend::compile_time_registry_engine; + +/// @brief Compile-time descriptor for the `brightnessTemperature` concept. +/// +/// `BrightnessTemperatureConcept` registers the GRIB `brightnessTemperature` +/// concept into the compile-time registry engine. +/// +/// The descriptor defines: +/// - The canonical concept name +/// - The mapping from variant enum values to symbolic names +/// - The callbacks associated with each encoding phase +/// - The entry-level matcher used to detect applicability +/// +/// All functions in this descriptor are `constexpr` and are evaluated +/// entirely at compile time. +struct BrightnessTemperatureConcept + : RegisterEntryDescriptor { + + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - Registry identification + /// - Diagnostics and logging + /// - Debug and introspection facilities + static constexpr std::string_view entryName() { + return brightnessTemperatureName; + } + + /// @brief Return the symbolic name of a concept variant. + /// + /// @tparam T Variant enumeration value + /// + /// @return String view representing the variant name + template + static constexpr std::string_view variantName() { + return brightnessTemperatureTypeName(); + } + + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This function is queried by the registry engine to obtain the callback + /// implementing the `brightnessTemperature` concept for a given: + /// + /// - Capability + /// - Encoding stage + /// - GRIB section + /// - Concept variant + /// + /// The function returns: + /// - A valid function pointer if the concept is applicable + /// - `nullptr` otherwise + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// @tparam MarsDict_t Type of MARS dictionary + /// @tparam ParDict_t Type of parameter dictionary + /// @tparam OptDict_t Type of options dictionary + /// @tparam OutDict_t Type of output GRIB dictionary + /// + /// @return Function pointer implementing the phase, or `nullptr` + template + static constexpr Fn phaseCallbacks() { + if constexpr (Capability == 0) { + if constexpr (brightnessTemperatureApplicable()) { + return &BrightnessTemperatureOp; + } + else { + return nullptr; + } + } + else { + return nullptr; + } + + mars2gribUnreachable(); + } + + /// @brief Variant-specific callbacks. + /// + /// The brightnessTemperature concept does not currently require + /// variant-specific runtime callbacks. All runtime behavior is handled + /// through phase callbacks. + /// + /// @return Always `nullptr` + template + static constexpr Fn variantCallbacks() { + return nullptr; + } + + /// @brief Entry-level matcher callback. + /// + /// This callback is used by the registry engine to decide whether the + /// brightnessTemperature concept is active for a given MARS request. + /// + /// @tparam Capability Encoding capability index + /// @tparam MarsDict_t Type of MARS dictionary + /// @tparam OptDict_t Type of options dictionary + /// + /// @return Function pointer to the matcher, or `nullptr` + template + static constexpr Fm entryCallbacks() { + if constexpr (Capability == 0) { + return &brightnessTemperatureMatcher; + } + else { + return nullptr; + } + } +}; + +} // namespace metkit::mars2grib::backend::concepts_ \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h new file mode 100644 index 00000000..03097519 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h @@ -0,0 +1,181 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// @file brightnessTemperatureEncoding.h +/// @brief Implementation of the GRIB `brightnessTemperature` concept operation. +/// +/// This header defines the applicability rules and execution logic for the +/// **brightnessTemperature concept** within the mars2grib backend. +/// +/// The brightnessTemperature concept is responsible for encoding GRIB keys +/// associated with brightness-temperature metadata stored in the Local Use +/// Section, specifically: +/// +/// - `channelNumber` +/// - `numberOfFrequencies` +/// +/// These fields identify the satellite channel and the number of frequencies +/// associated with the brightness-temperature product. +/// +/// The implementation follows the standard mars2grib concept model: +/// - Compile-time applicability via `brightnessTemperatureApplicable` +/// - Runtime validation of Local Definition Number +/// - Explicit deduction of required values +/// - Strict error handling with contextual concept exceptions +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of `concepts` +/// to avoid ambiguity and potential conflicts with the C++20 `concept` +/// language feature and related standard headers. +/// +/// This is a deliberate design choice and must not be changed. +/// +/// @ingroup mars2grib_backend_concepts + +#pragma once + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Deductions +#include "metkit/mars2grib/backend/deductions/channel.h" +#include "metkit/mars2grib/backend/deductions/numberOfFrequencies.h" + +// Checks +#include "metkit/mars2grib/backend/checks/matchLocalDefinitionNumber.h" + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +/// @brief Compile-time applicability predicate for the `brightnessTemperature` concept. +/// +/// This predicate determines whether the brightnessTemperature concept is +/// applicable for a given combination of: +/// - encoding stage +/// - GRIB section +/// - concept variant +/// +/// Applicability is evaluated entirely at compile time and is used by the +/// concept dispatcher to control instantiation and execution. +/// +/// @tparam Stage Encoding stage +/// @tparam Section GRIB section index +/// @tparam Variant Brightness-temperature concept variant +/// +/// @return `true` if the concept is applicable for the given parameters, +/// `false` otherwise. +/// +/// @note +/// The default applicability rule enables the concept only when: +/// - `Variant == BrightnessTemperatureType::Default` +/// - `Stage == StagePreset` +/// - `Section == SecLocalUseSection` +template +constexpr bool brightnessTemperatureApplicable() { + return ((Stage == StagePreset) && + (Section == SecLocalUseSection)); +} + +/// @brief Execute the `brightnessTemperature` concept operation. +/// +/// This function implements the runtime logic of the GRIB +/// `brightnessTemperature` concept. +/// +/// When applicable, it: +/// +/// 1. Validates that the Local Use Section matches the expected definition. +/// 2. Deduces the brightness-temperature related identifiers. +/// 3. Encodes the corresponding GRIB keys in the output dictionary. +/// +/// If the concept is invoked when not applicable, a +/// `Mars2GribConceptException` is thrown. +/// +/// @tparam Stage Encoding stage +/// @tparam Section GRIB section index +/// @tparam Variant Brightness-temperature concept variant +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam ParDict_t Type of the parameter dictionary +/// @tparam OptDict_t Type of the options dictionary +/// @tparam OutDict_t Type of the GRIB output dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] par Parameter dictionary +/// @param[in] opt Options dictionary +/// @param[out] out Output GRIB dictionary to be populated +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribConceptException +/// If: +/// - the Local Definition Number does not match expectations, +/// - required deductions fail, +/// - any GRIB key cannot be set, +/// - the concept is invoked when not applicable. +/// +/// @note +/// - All runtime errors are wrapped with full concept context +/// concept name, variant, stage, section. +/// - This concept does not decide whether the surrounding product is encoded +/// through the satellite path or the derived-product path. +/// +/// @see brightnessTemperatureApplicable +template +void BrightnessTemperatureOp(const MarsDict_t& mars, + const ParDict_t& par, + const OptDict_t& opt, + OutDict_t& out) { + using metkit::mars2grib::utils::dict_traits::set_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribConceptException; + + if constexpr (brightnessTemperatureApplicable()) { + try { + MARS2GRIB_LOG_CONCEPT(brightnessTemperature); + + // Preconditions / contracts + validation::match_LocalDefinitionNumber_or_throw(opt, out, {37L}); + + // number of frequencies is always 1 for brightness temperature products + auto numberOfFrequenciesVal = deductions::resolve_NumberOfFrequencies_or_throw(mars, par, opt); + set_or_throw(out, "numberOfFrequencies", numberOfFrequenciesVal); + + // In Ensemble Mean variant, channel number is required; in Default variant it is already set by the satellite concept + if constexpr (Variant == BrightnessTemperatureType::EnsembleMean) { + auto channelNumberVal = deductions::resolve_Channel_or_throw(mars, par, opt); + set_or_throw(out, "channelNumber", channelNumberVal); + } + } + catch (...) { + MARS2GRIB_CONCEPT_RETHROW(brightnessTemperature, + "Unable to set `brightnessTemperature` concept..."); + } + + // Successful operation + return; + } + + // Concept invoked outside its applicability domain + MARS2GRIB_CONCEPT_THROW(brightnessTemperature, "Concept called when not applicable..."); + + // Remove compiler warning + mars2gribUnreachable(); +} + +} // namespace metkit::mars2grib::backend::concepts_ \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h new file mode 100644 index 00000000..4b017d61 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h @@ -0,0 +1,126 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * 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. + */ + +/// @file brightnessTemperatureEnum.h +/// @brief Definition of the `brightnessTemperature` concept variants and compile-time metadata. +/// +/// This header defines the **static description** of the GRIB +/// `brightnessTemperature` concept used by the mars2grib backend. +/// It contains: +/// +/// - the canonical concept name (`brightnessTemperatureName`) +/// - the enumeration of supported brightness-temperature variants +/// - a compile-time typelist of all variants +/// - a compile-time mapping from variant to string identifier +/// +/// This file intentionally contains **no runtime logic** and **no encoding +/// behavior**. Its sole purpose is to provide compile-time metadata used by: +/// +/// - the concept registry +/// - compile-time table generation +/// - logging and diagnostics +/// - static validation of concept variants +/// +/// @note +/// Brightness temperature is represented as an independent concept because +/// it is orthogonal to the surrounding product-family concept. Depending on +/// the stream, the same parameter may coexist with either the satellite path +/// or the derived-product path. +/// +/// @ingroup mars2grib_backend_concepts + +#pragma once + +// System includes +#include +#include + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +using ValueList = metkit::mars2grib::backend::compile_time_registry_engine::ValueList; + +/// @brief Canonical name of the `brightnessTemperature` concept. +/// +/// This identifier is used: +/// - as the logical concept key in the concept registry +/// - for logging and debugging output +/// - to associate variants and capabilities with the concept +/// +/// The value must remain stable across releases. +inline constexpr std::string_view brightnessTemperatureName{"brightnessTemperature"}; + +/// @brief Enumeration of all supported `brightnessTemperature` concept variants. +/// +/// The concept currently has a single variant because both supported streams +/// share the same Local Use Section encoding logic. +/// +/// The numeric values of the enumerators are **not semantically relevant**; +/// they are required only to: +/// - provide a stable compile-time identifier +/// - allow array indexing and table generation +/// +/// @warning +/// Do not reorder existing enumerators, as they are used in compile-time +/// tables and registries. +enum class BrightnessTemperatureType : std::size_t +{ + EnsembleMean = 0, + Default +}; + +/// @brief Compile-time list of all `brightnessTemperature` concept variants. +/// +/// This typelist is used to: +/// - generate concept capability tables at compile time +/// - register all supported variants in the concept registry +/// - enable static iteration over variants without runtime overhead +/// +/// @note +/// The order of this list must match the intended iteration order for +/// registry construction and diagnostics. +using BrightnessTemperatureList = ValueList; + +/// @brief Compile-time mapping from `BrightnessTemperatureType` to human-readable name. +/// +/// This function returns the canonical string identifier associated with a +/// given brightness-temperature variant. +/// +/// The returned value is used for: +/// - logging and debugging output +/// - error reporting +/// - concept registry diagnostics +/// +/// @tparam T Brightness-temperature variant +/// @return String view identifying the variant +/// +/// @note +/// The returned string must remain stable across releases, as it may appear +/// in logs, tests, and diagnostic output. +template +constexpr std::string_view brightnessTemperatureTypeName(); + +#define DEF(T, NAME) \ + template <> \ + constexpr std::string_view brightnessTemperatureTypeName() { \ + return NAME; \ + } + +DEF(BrightnessTemperatureType::EnsembleMean, "ensembleMean"); +DEF(BrightnessTemperatureType::Default, "default"); + +#undef DEF + +} // namespace metkit::mars2grib::backend::concepts_ \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h new file mode 100644 index 00000000..b9d09b57 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h @@ -0,0 +1,86 @@ +#pragma once + +// System includes +#include +#include + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h" +#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +/// @brief Entry-level matcher for the `brightnessTemperature` concept. +/// +/// The concept is activated for brightness-temperature products identified by: +/// +/// - `param == 194` +/// - `stream == "oper"` or `stream == "elda"` +/// - presence of the MARS key `channel` +/// +/// The stream is intentionally **not** represented as a concept variant. +/// It only determines which surrounding product-family concept is active: +/// +/// - `elda` may coexist with the satellite concept +/// - `oper` may coexist with the derived-product concept +/// +/// The brightness-temperature concept itself owns only the common +/// brightness-temperature Local Use Section metadata. +/// +/// @tparam MarsDict_t Type of the MARS dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Index of the selected concept variant, or +/// `compile_time_registry_engine::MISSING` if the concept does not apply +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If the request is identified as a brightness-temperature request but the +/// mandatory `channel` key is missing. +template +std::size_t brightnessTemperatureMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + + // Concept does not apply unless "param" is present and equals 194 + if (!has(mars, "param") || get_or_throw(mars, "param") != 194) { + return compile_time_registry_engine::MISSING; + } + + // Concept does not apply unless the stream is explicitly supported + if (!has(mars, "stream")) { + return compile_time_registry_engine::MISSING; + } + + const auto& stream = get_or_throw(mars, "stream"); + + if (stream != "oper" && stream != "elda") { + return compile_time_registry_engine::MISSING; + } + + // At this point the request is a brightness-temperature request: + // "channel" is mandatory + if (!has(mars, "channel")) { + throw utils::exceptions::Mars2GribMatcherException( + "brightnessTemperature concept requires MARS key \"channel\" " + "when param=194 and stream is either \"oper\" or \"elda\"", + Here()); + } + + if ( stream == "elda" ) { + return static_cast(BrightnessTemperatureType::EnsembleMean); + } + else if ( stream == "oper" ) { + return static_cast(BrightnessTemperatureType::Default); + } + + return compile_time_registry_engine::MISSING; + +} + +} // namespace metkit::mars2grib::backend::concepts_ \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h index c1fb40cc..032ca20a 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h @@ -160,21 +160,6 @@ void DerivedOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op MARS2GRIB_LOG_CONCEPT(derived); - if constexpr (Section == SecLocalUseSection && Stage == StagePreset && - Variant == DerivedType::BrightnessTemperatureEnsembleMean) { - - // Check/Validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); - - // Deductions - long channelNumber = deductions::resolve_Channel_or_throw(mars, par, opt); - long numberOfFrequencies = deductions::resolve_NumberOfFrequencies_or_throw(mars, par, opt); - - // Encoding - set_or_throw(out, "channelNumber", channelNumber); - set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); - } - if constexpr (Section == SecProductDefinitionSection && Stage == StagePreset) { // Structural validation validation::check_DerivedProductDefinitionSection_or_throw(opt, out); diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h index 5b062f03..39737808 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h @@ -83,7 +83,6 @@ inline constexpr std::string_view derivedName{"derived"}; /// tables and registries. /// enum class DerivedType : std::size_t { - BrightnessTemperatureEnsembleMean, // Special variant for satellite brightness temperature products Default }; @@ -100,7 +99,7 @@ enum class DerivedType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using DerivedList = ValueList; +using DerivedList = ValueList; /// @@ -130,7 +129,6 @@ constexpr std::string_view derivedTypeName(); return NAME; \ } -DEF(DerivedType::BrightnessTemperatureEnsembleMean, "brightnessTemperatureEnsembleMean"); DEF(DerivedType::Default, "default"); #undef DEF diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index e1a98fef..8ff2d913 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -18,7 +18,8 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::has; const auto& type = get_or_throw(mars, "type"); - if (type == "es" || // Ensemble standard deviation + if (type == "em" || // Ensemble mean + type == "es" || // Ensemble standard deviation type == "ses" || // Ensemble spread of estimation type == "taem" || // Time-averaged ensemble mean type == "taes" || // Time-averaged ensemble standard deviation @@ -28,16 +29,6 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(DerivedType::Default); } - if (type == "em") { // Ensemble mean (special handling for brightness temperature products) - if (has(mars, "channel") && has(mars, "param") && get_or_throw(mars, "param") == 194 && - has(mars, "stream") && get_or_throw(mars, "stream") == "elda") { - return static_cast(DerivedType::BrightnessTemperatureEnsembleMean); - } - else { - return static_cast(DerivedType::Default); - } - } - return compile_time_registry_engine::MISSING; } diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h index 8862c79c..60082cba 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h @@ -215,30 +215,17 @@ void SatelliteOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& if constexpr (Section == SecLocalUseSection && Stage == StagePreset) { - if constexpr (Variant == SatelliteType::BrightnessTemperature) { - - // Check/Validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); - - // Deductions - long channelNumber = deductions::resolve_Channel_or_throw(mars, par, opt); - long numberOfFrequencies = deductions::resolve_NumberOfFrequencies_or_throw(mars, par, opt); - - // Encoding - set_or_throw(out, "channelNumber", channelNumber); - set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); - } - else { + // Check/Validation + validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); - // Check/Validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {24}); + // Deductions + long channelNumber = deductions::resolve_Channel_or_throw(mars, par, opt); + long numberOfFrequencies = deductions::resolve_NumberOfFrequencies_or_throw(mars, par, opt); - // Deductions - long channel = deductions::resolve_Channel_or_throw(mars, par, opt); + // Encoding + set_or_throw(out, "channelNumber", channelNumber); + set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); - // Encoding - set_or_throw(out, "channel", channel); - } } if constexpr (Section == SecProductDefinitionSection && Stage == StageAllocate) { diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h index dea8784d..b8f5308a 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h @@ -85,8 +85,7 @@ inline constexpr std::string_view satelliteName{"satellite"}; /// tables and registries. /// enum class SatelliteType : std::size_t { - Default = 0, - BrightnessTemperature = 1 + Default = 0 }; @@ -102,7 +101,7 @@ enum class SatelliteType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using SatelliteList = ValueList; +using SatelliteList = ValueList; /// @@ -133,8 +132,6 @@ constexpr std::string_view satelliteTypeName(); } DEF(SatelliteType::Default, "default"); -DEF(SatelliteType::BrightnessTemperature, "brightnessTemperature"); - #undef DEF } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h index 5c164eea..56c176d5 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -15,15 +15,6 @@ std::size_t satelliteMatcher(const MarsDict_t& mars, const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; - // BrightnessTemperature (paramId=194): only requires channel. - // Section 2 (local def 37) encodes channelNumber + numberOfFrequencies. - // Section 4 satellite band metadata (ident, instrument, series, waveNumber) - // is only present for PDT 32/33 — the encoding handles this conditionally. - if (has(mars, "channel") && has(mars, "param") && get_or_throw(mars, "param") == 194 && has(mars, "stream") && - get_or_throw(mars, "stream") == "oper") { - return static_cast(SatelliteType::BrightnessTemperature); - } - // Default satellite: requires full satellite identification keys if (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) { return static_cast(SatelliteType::Default); diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index 52f2d7bf..56a5be06 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -43,7 +43,7 @@ inline const Recipe S2_R20 = inline const Recipe S2_R24 = make_recipe<24, Select, - Select + Select >(); // Model-error products @@ -65,14 +65,15 @@ inline const Recipe S2_R37A = make_recipe<37, Select, Select, - Select + Select >(); inline const Recipe S2_R37B = make_recipe<37, Select, Select, - Select + Select, + Select >(); // 4i Analysis-related products From 73be35cd44545a44cab93b0918f550d99aef629e Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 12:58:36 +0000 Subject: [PATCH 16/17] mars2grib: extend model-error concept handling Split model-error handling into explicit component-index and Fourier-coefficient variants so recipe selection can distinguish the supported request shapes. Also treats legacy type=me requests as model-error requests alongside type=eme. --- .../concepts/ensemble/ensembleMatcher.h | 2 +- .../concepts/model-error/modelErrorEncoding.h | 35 ++++++++++----- .../concepts/model-error/modelErrorEnum.h | 9 ++-- .../concepts/model-error/modelErrorMatcher.h | 43 ++++++++++++++----- .../backend/deductions/componentIndex.h | 13 ++++-- 5 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index 60a3d097..bba73a29 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -18,7 +18,7 @@ std::size_t ensembleMatcher(const MarsDict_t& mars, const OptDict_t& opt) { // Skip model-error products: in that case "number" identifies the // model-error realization, not an ensemble member. - if (has(mars, "type") && get_or_throw(mars, "type") == "eme") { + if (has(mars, "type") && (get_or_throw(mars, "type") == "eme" || get_or_throw(mars, "type") == "me")) { return compile_time_registry_engine::MISSING; } diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h index 94317091..4335e2f0 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h @@ -89,7 +89,7 @@ namespace metkit::mars2grib::backend::concepts_ { /// template constexpr bool modelErrorApplicable() { - return ((Variant == ModelErrorType::Default) && (Stage == StagePreset) && (Section == SecLocalUseSection)); + return ( (Stage == StagePreset) && (Section == SecLocalUseSection) ); } @@ -147,18 +147,31 @@ void ModelErrorOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& MARS2GRIB_LOG_CONCEPT(modelError); - // Preconditions / contracts - validation::match_LocalDefinitionNumber_or_throw(opt, out, {25L, 39L}); + if ( Variant == ModelErrorType::ComponentIndex ) { + validation::match_LocalDefinitionNumber_or_throw(opt, out, {25L, 39L}); - // Deductions - auto componentIndexVal = deductions::resolve_ComponentIndex_or_throw(mars, par, opt); - auto numberOfComponentsVal = deductions::resolve_NumberOfComponents_or_throw(mars, par, opt); - auto modelErrorTypeVal = deductions::resolve_ModelErrorType_or_throw(mars, par, opt); + // Deductions + auto componentIndexVal = deductions::resolve_ComponentIndex_or_throw(mars, par, opt); + auto numberOfComponentsVal = deductions::resolve_NumberOfComponents_or_throw(mars, par, opt); + auto modelErrorTypeVal = deductions::resolve_ModelErrorType_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "componentIndex", componentIndexVal); + set_or_throw(out, "numberOfComponents", numberOfComponentsVal); + set_or_throw(out, "modelErrorType", modelErrorTypeVal); + + + } + else if ( Variant == ModelErrorType::FourierCoefficients ) { + validation::match_LocalDefinitionNumber_or_throw(opt, out, {45L}); + + MARS2GRIB_CONCEPT_THROW(modelError, "Variant not implemented..."); + + } + else { + MARS2GRIB_CONCEPT_THROW(modelError, "Unknown variant..."); + } - // Encoding - set_or_throw(out, "componentIndex", componentIndexVal); - set_or_throw(out, "numberOfComponents", numberOfComponentsVal); - set_or_throw(out, "modelErrorType", modelErrorTypeVal); } catch (...) { MARS2GRIB_CONCEPT_RETHROW(modelError, "Unable to set `modelError` concept..."); diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h index eeb96d3a..ef21dc32 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h @@ -83,7 +83,8 @@ inline constexpr std::string_view modelErrorName{"modelError"}; /// tables and registries. /// enum class ModelErrorType : std::size_t { - Default = 0 + ComponentIndex = 0, + FourierCoefficients }; @@ -99,7 +100,7 @@ enum class ModelErrorType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using ModelErrorList = ValueList; +using ModelErrorList = ValueList; /// @@ -129,7 +130,9 @@ constexpr std::string_view modelErrorTypeName(); return NAME; \ } -DEF(ModelErrorType::Default, "default"); +DEF(ModelErrorType::ComponentIndex, "componentIndex"); +DEF(ModelErrorType::FourierCoefficients, "fourierCoefficients"); + #undef DEF diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h index 96ce9248..a47faa9f 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h @@ -15,21 +15,42 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t modelErrorMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; - // Concept does not apply unless "type" is present and equals "eme" - if (!has(mars, "type") || get_or_throw(mars, "type") != "eme") { + + try { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + + if (!has(mars, "type") ){ + throw metkit::mars2grib::utils::exceptions::Mars2GribMatcherException( + "MARS key `type` is required to determine applicability of the `modelError` concept but is missing. This is a contract violation by the upstream tool that populates the MARS dictionary.", + Here()); + } + + // Concept does not apply unless "type" is present and equals "eme" + if ( (get_or_throw(mars, "type") != "eme" && + get_or_throw(mars, "type") != "me")) { + return compile_time_registry_engine::MISSING; + } + + // At this point the request is a model-error request: "number" is mandatory + if (has(mars, "number")) { + return static_cast(ModelErrorType::ComponentIndex); + } + + if ( has(mars,"coeffindex") ) { + return static_cast(ModelErrorType::FourierCoefficients); + } + return compile_time_registry_engine::MISSING; } - - // At this point the request is a model-error request: "number" is mandatory - if (!has(mars, "number")) { - throw utils::exceptions::Mars2GribMatcherException( - "modelError concept requires MARS key \"number\" when type=\"eme\"", Here()); + catch (...) { + // Rethrow nested exceptions with a more specific message + std::throw_with_nested( + metkit::mars2grib::utils::exceptions::Mars2GribMatcherException( + "An error occurred while matching the `modelError` concept. Check nested exception for details.", Here())); } - - return static_cast(ModelErrorType::Default); } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/deductions/componentIndex.h b/src/metkit/mars2grib/backend/deductions/componentIndex.h index 525e51fa..1199489d 100644 --- a/src/metkit/mars2grib/backend/deductions/componentIndex.h +++ b/src/metkit/mars2grib/backend/deductions/componentIndex.h @@ -127,17 +127,24 @@ long resolve_ComponentIndex_or_throw(const MarsDict_t& mars, const ParDict_t& pa // matcher bypass, etc.) and must be surfaced as a hard failure // with an unambiguous diagnostic. const std::string typeVal = get_or_throw(mars, "type"); - if (typeVal != "eme") { - throw Mars2GribDeductionException(std::string("`componentIndex` requested for a non-`eme` request: " + if (typeVal != "eme" && typeVal != "me") { + throw Mars2GribDeductionException(std::string("`componentIndex` requested for a non-`eme` or non-`me` request: " "`mars[\"type\"]` is `") + typeVal + - "` but only `eme` is supported. This is a serious upstream " + "` but only `eme` or `me` is supported. This is a serious upstream " "contract violation: the model-error deduction was reached " "for a request that is not a model-error product. Check " "recipe selection and matcher dispatch.", Here()); } + // Warning due to the hack + if ( typeVal == "me" ) { + eckit::Log::warning() << "MARS `type` value `me` is treated as a synonym for `eme` to accommodate legacy requests. " + << "This is a temporary compatibility hack and support for `me` will be removed in the future. " + << std::endl; + } + // Retrieve mandatory MARS number (model-error realization id) long componentIndex = get_or_throw(mars, "number"); From 60019ff2a35ac18da6b20d2313cf551ae2a51ef9 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 12:58:43 +0000 Subject: [PATCH 17/17] mars2grib: keep samples valid during packing setup Set packing precision during allocation using setBitsPerValue and defer spectral packing metadata to the preset stage. Initialize sample values from the representation concept using a deduced reference value so intermediate samples remain valid throughout encoding, even though this adds work to the encoder path. --- .../concepts/packing/packingEncoding.h | 56 +++++++----- .../representation/representationEncoding.h | 88 ++++++++++++++++--- .../deductions/allowedReferenceValue.h | 50 ++++++++--- 3 files changed, 148 insertions(+), 46 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h index dc0466d6..df155efe 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h @@ -81,7 +81,20 @@ namespace metkit::mars2grib::backend::concepts_ { /// template constexpr bool packingApplicable() { - return (Stage == StagePreset && Section == SecDataRepresentationSection); + + // Most packing algorithms only require configuration at the allocation stage + if constexpr (Stage == StageAllocate && Section == SecDataRepresentationSection) { + return true; + } + + if constexpr (Variant == PackingType::SpectralComplex) { + // Spectral complex packing requires some parameters to be set at the preset stage + if constexpr (Stage == StagePreset && Section == SecDataRepresentationSection) { + return true; + } + } + + return false; } @@ -159,11 +172,11 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op // Check sample structure validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {0}); - // Get bits per value - long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); - // Set bits per value - set_or_throw(out, "bitsPerValue", bitsPerValue); + if constexpr ( Stage == StageAllocate ) { + long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); + set_or_throw(out, "setBitsPerValue", bitsPerValue); + } } if constexpr (Variant == PackingType::Ccsds) { @@ -171,11 +184,11 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op // Check sample structure validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {42}); - // Get bits per value - long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); - // Set bits per value - set_or_throw(out, "bitsPerValue", bitsPerValue); + if constexpr ( Stage == StageAllocate ) { + long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); + set_or_throw(out, "setBitsPerValue", bitsPerValue); + } } if constexpr (Variant == PackingType::SpectralComplex) { @@ -183,18 +196,21 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op // Check sample structure validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {51}); - // Get bits per value - long bitsPerValue = deductions::resolve_BitsPerValueSpectral_or_throw(mars, par, opt); - double laplacianOperator = deductions::resolve_LaplacianOperator_or_throw(mars, par, opt); - long subSetTruncation = deductions::resolve_SubSetTruncation_or_throw(mars, par, opt); - // Set bits per value - set_or_throw(out, "bitsPerValue", bitsPerValue); - set_or_throw(out, "laplacianOperator", laplacianOperator); - set_or_throw(out, "subSetJ", subSetTruncation); - set_or_throw(out, "subSetK", subSetTruncation); - set_or_throw(out, "subSetM", subSetTruncation); - set_or_throw(out, "TS", (subSetTruncation + 1) * (subSetTruncation + 2)); + if constexpr ( Stage == StageAllocate ) { + long bitsPerValue = deductions::resolve_BitsPerValueSpectral_or_throw(mars, par, opt); + set_or_throw(out, "setBitsPerValue", bitsPerValue); + } + + if constexpr ( Stage == StagePreset ) { + double laplacianOperator = deductions::resolve_LaplacianOperator_or_throw(mars, par, opt); + long subSetTruncation = deductions::resolve_SubSetTruncation_or_throw(mars, par, opt); + set_or_throw(out, "laplacianOperator", laplacianOperator); + set_or_throw(out, "subSetJ", subSetTruncation); + set_or_throw(out, "subSetK", subSetTruncation); + set_or_throw(out, "subSetM", subSetTruncation); + set_or_throw(out, "TS", (subSetTruncation + 1) * (subSetTruncation + 2)); + } } } catch (...) { diff --git a/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h b/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h index f9747d47..ca29c1ff 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h @@ -119,10 +119,12 @@ #pragma once // System includes +#include #include #include #include #include +#include // Eckit::geo includes #include "eckit/geo/Grid.h" @@ -135,6 +137,8 @@ #include "eckit/spec/Custom.h" #include "metkit/mars2grib/utils/generalUtils.h" +// Defintion of Span +#include "metkit/codes/api/CodesTypes.h" // Core concept includes #include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" @@ -154,6 +158,38 @@ namespace metkit::mars2grib::backend::concepts_ { + +/// This helper provides a function-local static buffer initialized with a constant value. +/// The buffer starts with size zero and is grown on demand when the requested +/// size exceeds the currently allocated size. +/// +/// If the buffer is already large enough, the existing storage is reused without +/// reallocation. The returned span is restricted to the requested size, even if +/// the underlying buffer is larger. +/// +/// The returned span is read-only and is intended to initialize encoded fields +/// with the value resolved by the caller. The active span is refreshed with +/// `std::transform` on each call. +/// +/// @param requiredSize Number of entries requested. +/// @param value Constant value used to initialize every entry. +/// +/// @return Span containing exactly `requiredSize` entries set to `value`. +/// +static metkit::codes::Span constValueSpan(std::size_t requiredSize, double value) { + static thread_local std::vector values; + + if (values.size() < requiredSize) { + values.resize(requiredSize); + } + + std::transform(values.begin(), values.begin() + requiredSize, values.begin(), + [value](double) { return value; }); + + return metkit::codes::Span{values.data(), requiredSize}; +} + + /// /// @brief Compile-time applicability predicate for the `representation` concept. /// @@ -358,6 +394,10 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic set_or_throw(out, "longitudeOfLastGridPointInDegrees", longitudeOfLastGridPointInDegrees); set_or_throw(out, "iDirectionIncrementInDegrees", iDirectionIncrementInDegrees); set_or_throw(out, "jDirectionIncrementInDegrees", jDirectionIncrementInDegrees); + + // Initialize values with the deduced reference value + std::size_t numberOfCoefficients = grid->size(); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); } else if constexpr (Variant == RepresentationType::RegularGaussian) { @@ -387,6 +427,10 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic set_or_throw(out, "latitudeOfLastGridPointInDegrees", latitudeOfLastGridPointInDegrees); set_or_throw(out, "longitudeOfLastGridPointInDegrees", longitudeOfLastGridPointInDegrees); set_or_throw(out, "iDirectionIncrementInDegrees", iDirectionIncrementInDegrees); + + // Initialize values with the deduced reference value + std::size_t numberOfCoefficients = grid->size(); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); } else if constexpr (Variant == RepresentationType::ReducedGaussian) { @@ -416,20 +460,10 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic set_or_throw(out, "latitudeOfLastGridPointInDegrees", latitudeOfLastGridPointInDegrees); set_or_throw(out, "longitudeOfLastGridPointInDegrees", longitudeOfLastGridPointInDegrees); setMissing_or_throw(out, "iDirectionIncrement"); - } - else if constexpr (Variant == RepresentationType::SphericalHarmonics) { - - // Deductions - const auto marsTruncation = get_or_throw(mars, "truncation"); - - const auto pentagonalResolutionParameterJ = marsTruncation; - const auto pentagonalResolutionParameterK = marsTruncation; - const auto pentagonalResolutionParameterM = marsTruncation; - // Encoding - set_or_throw(out, "pentagonalResolutionParameterJ", pentagonalResolutionParameterJ); - set_or_throw(out, "pentagonalResolutionParameterK", pentagonalResolutionParameterK); - set_or_throw(out, "pentagonalResolutionParameterM", pentagonalResolutionParameterM); + // Initialize values with the deduced reference value + std::size_t numberOfCoefficients = grid->size(); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); } else if constexpr (Variant == RepresentationType::Healpix) { @@ -449,6 +483,10 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic set_or_throw(out, "Nside", nside); set_or_throw(out, "orderingConvention", orderingConvention); set_or_throw(out, "longitudeOfFirstGridPointInDegrees", longitudeOfFirstGridPointInDegrees); + + // Initialize values with the deduced reference value + std::size_t numberOfCoefficients = grid->size(); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); } else if constexpr (Variant == RepresentationType::Orca) { @@ -466,9 +504,33 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic set_or_throw(out, "unstructuredGridType", gridType); set_or_throw(out, "unstructuredGridSubtype", gridSubType); set_or_throw(out, "uuidOfHGrid", uuid); + + // Initialize values with the deduced reference value + std::size_t numberOfCoefficients = grid->size(); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); } else if constexpr (Variant == RepresentationType::Fesom) { MARS2GRIB_CONCEPT_THROW(representation, "Support for Fesom representation not implemented..."); + } + else if constexpr (Variant == RepresentationType::SphericalHarmonics) { + + // Deductions + const auto marsTruncation = get_or_throw(mars, "truncation"); + + const auto pentagonalResolutionParameterJ = marsTruncation; + const auto pentagonalResolutionParameterK = marsTruncation; + const auto pentagonalResolutionParameterM = marsTruncation; + + // Encoding + set_or_throw(out, "pentagonalResolutionParameterJ", pentagonalResolutionParameterJ); + set_or_throw(out, "pentagonalResolutionParameterK", pentagonalResolutionParameterK); + set_or_throw(out, "pentagonalResolutionParameterM", pentagonalResolutionParameterM); + + // Initialize values with the deduced reference value + std::size_t numberOfCoefficients = (marsTruncation + 1) * (marsTruncation + 2); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); + + } else { MARS2GRIB_CONCEPT_THROW(representation, "Unknown `representation` variant..."); diff --git a/src/metkit/mars2grib/backend/deductions/allowedReferenceValue.h b/src/metkit/mars2grib/backend/deductions/allowedReferenceValue.h index 3d283127..5d7cb0ce 100644 --- a/src/metkit/mars2grib/backend/deductions/allowedReferenceValue.h +++ b/src/metkit/mars2grib/backend/deductions/allowedReferenceValue.h @@ -68,26 +68,32 @@ namespace metkit::mars2grib::backend::deductions { /// @brief Resolve the GRIB allowed reference value from input dictionaries. /// /// @section Deduction contract -/// - Reads: `mars["param"]` +/// - Reads: exactly one of `mars["grid"]` or `mars["truncation"]`; `mars["param"]` for grid-point products /// - Writes: none /// - Side effects: logging (RESOLVE) /// - Failure mode: throws /// -/// This deduction resolves the GRIB `allowedReferenceValue` by retrieving -/// the mandatory MARS parameter identifier (`param`) and consulting a -/// statically defined table of admissible reference value ranges. +/// This deduction resolves the GRIB `allowedReferenceValue` by first determining +/// whether the request describes a grid-point or spectral representation. Exactly +/// one of `grid` or `truncation` must be present in the MARS dictionary. /// -/// For parameters with an explicit range definition, the resolved reference -/// value is chosen as the midpoint of the corresponding `[min, max]` interval. -/// If no explicit range is defined for the parameter, a default reference -/// value of `0.0` is returned. +/// For grid-point products, the mandatory MARS parameter identifier (`param`) is +/// retrieved and used to consult a statically defined table of admissible +/// reference value ranges. For parameters with an explicit range definition, the +/// resolved reference value is chosen as the midpoint of the corresponding +/// `[min, max]` interval. If no explicit range is defined for the parameter, a +/// default reference value of `0.0` is returned. +/// +/// For spectral products identified by `truncation`, the resolved reference +/// value is `0.0`. /// /// No semantic interpretation beyond the explicit range table is applied. /// The admissible ranges are defined locally and are not validated against /// external GRIB tables. /// /// @tparam MarsDict_t -/// Type of the MARS dictionary. Must support keyed access to `param` +/// Type of the MARS dictionary. Must support presence checks for `grid` and +/// `truncation`; grid-point products must also support keyed access to `param` /// and conversion to an integral type. /// /// @tparam ParDict_t @@ -109,8 +115,9 @@ namespace metkit::mars2grib::backend::deductions { /// The resolved allowed reference value. /// /// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException -/// If the key `param` is missing, cannot be retrieved as an integral value, -/// or if any unexpected error occurs during deduction. +/// If neither or both of `grid` and `truncation` are present, if `param` is +/// missing or malformed for a grid-point product, or if any unexpected error +/// occurs during deduction. /// /// @note /// This deduction applies a local, table-driven rule and does not @@ -120,6 +127,7 @@ template double resolve_AllowedReferenceValue_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; try { @@ -190,10 +198,26 @@ double resolve_AllowedReferenceValue_or_throw(const MarsDict_t& mars, const ParD {263501, {173.0, 1000.0}}, }; - // Default reference value + const bool hasGrid = has(mars, "grid"); + const bool hasTruncation = has(mars, "truncation"); + + if (hasGrid == hasTruncation) { + throw Mars2GribDeductionException( + "`allowedReferenceValue` requires exactly one of MARS keys `grid` or `truncation`", Here()); + } + + if (hasTruncation) { + MARS2GRIB_LOG_RESOLVE([&]() { + return std::string{"`allowedReferenceValue` resolved for spectral representation: value='0.000000'"}; + }()); + + return 0.0; + } + + // Default reference value for grid-point products double ret = 0.0; - // Retrieve mandatory MARS allowedReferenceValue + // Retrieve mandatory MARS parameter identifier long marsParamVal = get_or_throw(mars, "param"); // Lookup allowed value in the mid of the allowed range