From 604cf9b0577b79b3a2cd50913ae9fd510d887022 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 13 Apr 2026 08:13:36 +0000 Subject: [PATCH 01/35] 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 f4b46c1ea..fb9f23c70 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 ce8140cce..b47e9c623 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 000000000..6a8777f33 --- /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 000000000..cc0900d77 --- /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 000000000..5a8fae4fc --- /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 000000000..39fd3f7d8 --- /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 000000000..fe023ef86 --- /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 000000000..f15b8328c --- /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 16fcb9946..7b70c5dbb 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 109e143c44737e973d8c3680d43e4cf5768d50dc Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 28 Apr 2026 15:06:33 +0000 Subject: [PATCH 02/35] 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 fb9f23c70..257e4fc5a 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 b47e9c623..1e6b08b70 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 c82e2b604..60a3d097b 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 000000000..5ff7c713d --- /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 000000000..94317091d --- /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 000000000..eeb96d3aa --- /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 000000000..96ce92484 --- /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 000000000..1200c3653 --- /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 000000000..241250898 --- /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 000000000..f831dca60 --- /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 7b70c5dbb..1d6f36481 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 b2cbc19b923a173c3a914de7b0bfbf678cb88682 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 28 Apr 2026 16:49:05 +0000 Subject: [PATCH 03/35] 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 | 52 ++++++++++++---- .../backend/concepts/level/levelEnum.h | 61 +++++++++++++------ .../backend/concepts/level/levelMatcher.h | 16 ++++- 3 files changed, 95 insertions(+), 34 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h b/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h index b06b44278..dba35acc0 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,12 @@ 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::FlightLevel) { + 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 || Variant == LevelType::FlightLevel) { + return true; } else { @@ -156,7 +182,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 +249,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 a0e544034..3624de8cc 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, @@ -146,20 +165,21 @@ enum class LevelType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using LevelList = ValueList< - LevelType::Surface, LevelType::EntireAtmosphere, LevelType::EntireLake, LevelType::CloudBase, 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::FlightLevel, LevelType::Default>; - +using LevelList = + ValueList; /// /// @brief Compile-time mapping from `LevelType` to human-readable name. @@ -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 5600ee5bf..f36d7ae7e 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, range(400000, 499999))) { - 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), range(400000, 499999)) { + return static_cast(LevelType::ModelMultipleLevel); } throw utils::exceptions::Mars2GribMatcherException( From 1978270d20e342a89fff35ec4590aab6728023f7 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 28 Apr 2026 19:15:52 +0000 Subject: [PATCH 04/35] 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 14e3ed1a7..c99419eb3 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 4aed79fcc10dde7b2d98f16404b99d2cb44b06e8 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Wed, 29 Apr 2026 14:20:38 +0000 Subject: [PATCH 05/35] 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 | 9 +-- .../deductions/typeOfGeneratingProcess.h | 61 ++++++++++++++++++- 5 files changed, 103 insertions(+), 9 deletions(-) diff --git a/share/metkit/params.yaml b/share/metkit/params.yaml index 8d76016ed..c0afde682 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 f36d7ae7e..336059826 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()); } @@ -297,10 +305,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 17c546b87..872fea0b7 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 8ef181e7c..3e3953f50 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 = { - {"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", "gbf", "gwt"}}; + 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", + "gbf", "gwt", "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 d18eca2f7..b8542a7ae 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 d6c1298b984e10cc8385cb402e2c3bf9e6781bb5 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Wed, 29 Apr 2026 15:16:31 +0000 Subject: [PATCH 06/35] Run clang-format --- .../backend/concepts/level/levelEnum.h | 2 +- .../backend/deductions/componentIndex.h | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h index 3624de8cc..8f58af0e6 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, diff --git a/src/metkit/mars2grib/backend/deductions/componentIndex.h b/src/metkit/mars2grib/backend/deductions/componentIndex.h index 1200c3653..525e51fac 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) From 0b41ca960de5de1cdf3f6a9cd80ff97e89409f75 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 12 May 2026 12:54:02 +0000 Subject: [PATCH 07/35] 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 2ac3f05c5..8862c79ca 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 2f52d1f0a..dea8784d4 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 07dfba360..b7869f796 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 000000000..cc059ce3a --- /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 4e4bbd70a..a11a12ac9 100644 --- a/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in +++ b/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in @@ -1156,6 +1156,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 1d6f36481..3918c9408 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 a568cec6c5e3c9448fd6c95ba3c7107f939b7462 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 12 May 2026 13:05:22 +0000 Subject: [PATCH 08/35] 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 5d6037d18..c3535f759 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 000000000..69aee07e9 --- /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 8cc63b954..09d72877d 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 555a277fe35c9edd30e6ee6bac2a2bbba5baf5d8 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 12 May 2026 13:33:18 +0000 Subject: [PATCH 09/35] 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 3918c9408..8e48ca430 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 5ccf2e1b610b15cecfebf28b49c78248586ea57e Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 14 May 2026 23:13:07 +0000 Subject: [PATCH 10/35] 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 cc059ce3a..40faf47fa 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 93b95799a805b63f14b0ad1c335c125c53bb175c 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/35] 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 b8542a7ae..1f78c3bde 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 d72e34e64e0472b424799a6bf319d0ed6cfa5e8f 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/35] 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 815ed738e..3d94ad2fa 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 48bb5ccc7cbce344d1d035c0e57eab256b98ae20 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 26 May 2026 15:24:11 +0000 Subject: [PATCH 13/35] 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 1e6b08b70..39de42840 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 305975850..ad8954769 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 5971d7e98..0d84bc3ae 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 3d94ad2fa..722db81b2 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 336059826..e3819d2ba 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), range(400000, 499999))) { 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 b7869f796..5c164eea9 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 98e474f0c..eae25c5c3 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 c99419eb3..a15606f85 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 8e48ca430..e9d23fa79 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 86a700e0eb6c485b136eddd1a51a3cffa1ca28d3 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Wed, 27 May 2026 14:01:59 +0000 Subject: [PATCH 14/35] 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 ad8954769..c1fb40ccd 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 0d84bc3ae..5b062f03e 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 722db81b2..e1a98fef9 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 e9d23fa79..52f2d7bf7 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 abd6b8df584b98c9284b956757c4b65292e61e6e Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 12:58:30 +0000 Subject: [PATCH 15/35] 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 257e4fc5a..6edf8b148 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 000000000..250f39c4e --- /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 000000000..03097519c --- /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 000000000..4b017d61f --- /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 000000000..b9d09b57b --- /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 c1fb40ccd..032ca20aa 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 5b062f03e..397378083 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 e1a98fef9..8ff2d913f 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 8862c79ca..60082cba5 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 dea8784d4..b8f5308ad 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 5c164eea9..56c176d5a 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 52f2d7bf7..56a5be063 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 560817b9f0915424503c6e49f120f9d6aa4d7612 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 12:58:36 +0000 Subject: [PATCH 16/35] 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 60a3d097b..bba73a29f 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 94317091d..4335e2f0c 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 eeb96d3aa..ef21dc327 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 96ce92484..a47faa9f6 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 525e51fac..1199489d0 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 a9d9d4c4e9c1ffdc5c6398fc26513b7ce76cc9f8 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 12:58:43 +0000 Subject: [PATCH 17/35] 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 dc0466d6f..df155efe5 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 f9747d478..ca29c1ff6 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 3d2831276..5d7cb0ce3 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 From 2be0e6dd9dd5787a7a4350c9f8c3a22059d8e6ba Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 5 May 2026 05:30:12 +0000 Subject: [PATCH 18/35] WIP: route temporal concepts through unified ProductTime - Introduce ProductTime as the single source of truth for all temporal data; remove per-concept re-parsing of raw MARS keys. - Collapse the statistics encoder to a uniform SoA path (no instant / single-window / multi-window branching). - Migrate referenceTime, pointInTime, statistics concepts onto resolve_ProductTime_or_throw. - Drop superseded legacy deductions (forecastTimeInSeconds, timeSpanInSeconds, referenceDateTime, hindcastDateTime, numberOfTimeRanges, statisticsDescriptor, detail/timeUtils). Skeleton for design review; runtime stage of statistics deliberately left empty pending follow-up. --- .../point-in-time/pointInTimeEncoding.h | 13 +- .../reference-time/referenceTimeEncoding.h | 31 +- .../statistics/impl/statisticsDescriptor.h | 277 +++ .../concepts/statistics/statisticsEncoding.h | 254 +-- .../concepts/statistics/statisticsEnum.h | 52 + .../backend/deductions/detail/ProductTime.h | 671 +++++++ .../backend/deductions/detail/StatType.h | 270 +++ .../deductions/detail/StatisticalWindow.h | 71 + .../backend/deductions/detail/timeUtils.h | 325 ---- .../deductions/forecastTimeInSeconds.h | 115 -- .../backend/deductions/hindcastDateTime.h | 130 -- .../backend/deductions/numberOfTimeRanges.h | 175 -- .../backend/deductions/productTime.h | 387 +++++ .../backend/deductions/referenceDateTime.h | 121 -- .../backend/deductions/statisticsDescriptor.h | 183 -- .../backend/deductions/timeProducts.md | 1545 +++++++++++++++++ .../backend/deductions/timeSpanInSeconds.h | 110 -- .../deductions/typeOfStatisticalProcessing.h | 330 ++++ .../docs/doxygen/mars2grib.config.in | 12 +- 19 files changed, 3720 insertions(+), 1352 deletions(-) create mode 100644 src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h create mode 100644 src/metkit/mars2grib/backend/deductions/detail/ProductTime.h create mode 100644 src/metkit/mars2grib/backend/deductions/detail/StatType.h create mode 100644 src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h delete mode 100644 src/metkit/mars2grib/backend/deductions/detail/timeUtils.h delete mode 100644 src/metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h delete mode 100644 src/metkit/mars2grib/backend/deductions/hindcastDateTime.h delete mode 100644 src/metkit/mars2grib/backend/deductions/numberOfTimeRanges.h create mode 100644 src/metkit/mars2grib/backend/deductions/productTime.h delete mode 100644 src/metkit/mars2grib/backend/deductions/referenceDateTime.h delete mode 100644 src/metkit/mars2grib/backend/deductions/statisticsDescriptor.h create mode 100644 src/metkit/mars2grib/backend/deductions/timeProducts.md delete mode 100644 src/metkit/mars2grib/backend/deductions/timeSpanInSeconds.h create mode 100644 src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h diff --git a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h index 41911e1a8..ff6bcf6c7 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h @@ -53,7 +53,7 @@ #include "metkit/mars2grib/backend/tables/timeUnits.h" // Deductions -#include "metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h" +#include "metkit/mars2grib/backend/deductions/productTime.h" // Utils #include "metkit/config/LibMetkit.h" @@ -154,8 +154,15 @@ void PointInTimeOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t MARS2GRIB_LOG_CONCEPT(pointInTime); - // Deductions - long marsStepInSeconds = deductions::resolve_ForecastTimeInSeconds_or_throw(mars, par, opt); + // Deductions: temporal data is sourced exclusively from the + // resolved `ProductTime` (§15 of timeProducts.md). + auto pt = deductions::resolve_ProductTime_or_throw(mars, par, opt); + + // For an instant product `windowStart == windowEnd`; the + // forecast-time offset is `windowEnd - referenceDateTime` (in + // seconds, returned by eckit::DateTime::operator-). + const long marsStepInSeconds = + static_cast(static_cast(pt.windowEnd - pt.referenceDateTime)); // Basic checks if (marsStepInSeconds % 3600 != 0) { diff --git a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h index 189bdc9c5..8387f1cfe 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h @@ -62,8 +62,7 @@ #include "metkit/mars2grib/utils/generalUtils.h" // Deductions -#include "metkit/mars2grib/backend/deductions/hindcastDateTime.h" -#include "metkit/mars2grib/backend/deductions/referenceDateTime.h" +#include "metkit/mars2grib/backend/deductions/productTime.h" #include "metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h" // Tables @@ -178,12 +177,18 @@ void ReferenceTimeOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict MARS2GRIB_LOG_CONCEPT(referenceTime); + // Resolve the canonical ProductTime once per concept invocation. + // All three branches below source their date/time exclusively + // from this object (§15 of timeProducts.md). + auto pt = deductions::resolve_ProductTime_or_throw(mars, par, opt); + // ============================================================= // Variant-specific logic // ============================================================= if constexpr (Section == SecIdentificationSection) { - // Deductions + // Deductions: significanceOfReferenceTime is orthogonal to + // ProductTime (driven by mars.type / mars.stream). tables::SignificanceOfReferenceTime significanceOfReferenceTime = deductions::resolve_SignificanceOfReferenceTime_or_throw(mars, par, opt); @@ -193,8 +198,9 @@ void ReferenceTimeOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict if constexpr ((Section == SecIdentificationSection) && (Variant == ReferenceTimeType::Standard)) { - // Deductions - eckit::DateTime dateTime = deductions::resolve_ReferenceDateTime_or_throw(mars, par, opt); + // For a standard product, the GRIB "reference date/time" + // is the canonical ProductTime::referenceDateTime. + const eckit::DateTime& dateTime = pt.referenceDateTime; // Encoding set_or_throw(out, "year", dateTime.date().year()); @@ -207,8 +213,12 @@ void ReferenceTimeOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict if constexpr ((Section == SecIdentificationSection) && (Variant == ReferenceTimeType::Reforecast)) { - // Deductions - eckit::DateTime referenceDateTime = deductions::resolve_HindcastDateTime_or_throw(mars, par, opt); + // For a reforecast product, the Identification Section's + // reference date/time is the hindcast date — i.e. the + // canonical ProductTime::simulationDateTime (from date / + // time). The ProductDefinitionSection branch below writes + // the model-version date from referenceDateTime instead. + const eckit::DateTime& referenceDateTime = pt.simulationDateTime; // Encoding set_or_throw(out, "year", referenceDateTime.date().year()); @@ -224,9 +234,10 @@ void ReferenceTimeOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict // Validation validation::match_ProductDefinitionTemplateNumber_or_throw(opt, out, {60L, 61L}); - - // Deduction - eckit::DateTime dateTime = deductions::resolve_ReferenceDateTime_or_throw(mars, par, opt); + // Model-version date/time = ProductTime::referenceDateTime + // (derived from date/time or year/month per §7.4). + // TODO: Need to clarify with DGOV if this is reference or simulatedDateTime. + const eckit::DateTime& dateTime = pt.referenceDateTime; // Encoding set_or_throw(out, "YearOfModelVersion", dateTime.date().year()); diff --git a/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h b/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h new file mode 100644 index 000000000..126c38cd3 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h @@ -0,0 +1,277 @@ +/* + * (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 impl/statisticsDescriptor.h +/// @brief Pure SoA builder for the GRIB Section-4 statistical processing keys. +/// +/// Defines: +/// - `StatisticalProcessing`: a transparent shadow of the GRIB header keys +/// `typeOfStatisticalProcessing`, `typeOfTimeIncrement`, +/// `indicatorOfUnitForTimeRange`, `lengthOfTimeRange`, +/// `indicatorOfUnitForTimeIncrement`, `lengthOfTimeIncrement`. All +/// members are `long` / `vector` so the struct can be written +/// verbatim to eccodes. +/// +/// - `compute_StatisticalProcessing`: a pure function turning a +/// resolved `ProductTime` plus the per-loop typeOfStatisticalProcessing +/// vector into a fully-populated `StatisticalProcessing`. +/// +/// All semantic decisions (instant vs single-window vs multi-window, AIFS +/// no-increment hack, calendar-month length) live inside this single +/// function. The encoding layer remains a thin write-out. +/// +/// @ingroup mars2grib_backend_concepts +/// + +#pragma once + +#include +#include +#include +#include + +#include "eckit/exception/Exceptions.h" + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/deductions/detail/ProductTime.h" +#include "metkit/mars2grib/backend/tables/timeUnits.h" +#include "metkit/mars2grib/backend/tables/typeOfStatisticalProcessing.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_::impl { + +/// +/// @brief Transparent SoA shadow of the GRIB Section-4 statistical keys. +/// +/// All vectors have the same length: `numberOfTimeRanges`. Ordering is +/// outermost → innermost, matching `ProductTime::statisticalWindows`. +/// +/// All members are `long` / `vector` because eccodes does not +/// understand `tables::TimeUnit` or `tables::TypeOfStatisticalProcessing` +/// as types — it expects raw numeric codes. Callers MUST cast the table +/// enums to `static_cast` before populating the vectors (the builder +/// in this file does this internally). +/// +struct StatisticalProcessing { + long numberOfTimeRanges = 0; + + std::vector typeOfStatisticalProcessing; + std::vector typeOfTimeIncrement; + std::vector indicatorOfUnitForTimeRange; + std::vector lengthOfTimeRange; + std::vector indicatorOfUnitForTimeIncrement; + std::vector lengthOfTimeIncrement; +}; + +namespace detail { + +/// +/// @brief Length in hours of the calendar month that ENDS at `(year, month)`. +/// +/// Mirrors the legacy `previousMonthLengthHours` from +/// `deductions/detail/timeUtils.h`. A monthly statistical window whose +/// `windowEnd` is the first of the month covers the *previous* calendar +/// month, hence the helper is named accordingly. +/// +inline long previousMonthLengthHours(long year, long month) { + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + if (month < 1 || month > 12) { + throw Mars2GribGenericException("Invalid month (must be 1..12)", Here()); + } + + switch (month) { + case 1: + case 2: + case 4: + case 6: + case 8: + case 9: + case 11: + return 31 * 24; + + case 5: + case 7: + case 10: + case 12: + return 30 * 24; + + case 3: + return ((year % 4) == 0 ? 29 : 28) * 24; + } + + throw Mars2GribGenericException("Unreachable", Here()); +} + +/// +/// @brief Convert a `StatisticalWindow` to its length in hours. +/// +/// - `Second`: `count` must be a multiple of 3600, returns `count / 3600`. +/// - `Day`: returns `count * 24`. +/// - `Month`: returns `count * previousMonthLengthHours(endYear, endMonth)`. +/// +/// `endYear`/`endMonth` are taken from `pt.windowEnd` for monthly windows. +/// +inline long windowLengthInHours(const deductions::detail::StatisticalWindow& w, + long endYear, long endMonth) { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + switch (w.unit) { + case tables::TimeUnit::Second: { + if (w.count % 3600 != 0) { + throw Mars2GribDeductionException( + "StatisticalWindow with unit=Second must have count divisible by 3600 " + "to be expressed in hours; actual count=" + std::to_string(w.count), + Here()); + } + return w.count / 3600; + } + case tables::TimeUnit::Day: + return w.count * 24; + case tables::TimeUnit::Month: + return w.count * previousMonthLengthHours(endYear, endMonth); + default: + throw Mars2GribDeductionException( + "Unsupported StatisticalWindow unit for hour conversion: '" + + tables::enum2name_TimeUnit_or_throw(w.unit) + "'", + Here()); + } +} + +} // namespace detail + +/// +/// @brief Build the GRIB statistical-processing SoA from a `ProductTime`. +/// +/// Single uniform loop over `pt.statisticalWindows[0 .. statisticalWindowCount)`. +/// No instant / single-window / multi-window branching: every slot is +/// filled identically except for the AIFS no-increment edge case detailed +/// below. +/// +/// **Per-slot semantics**: +/// - `typeOfTimeIncrement[i] = 2` (multIO fixed value) +/// - `indicatorOfUnitForTimeRange[i] = Hour (1)` (legacy convention) +/// - `indicatorOfUnitForTimeIncrement[i] = Second (13)` (legacy convention) +/// - `typeOfStatisticalProcessing[i] = static_cast(types[i])` +/// - `lengthOfTimeRange[i] = windowLengthInHours(window[i])` +/// - `lengthOfTimeIncrement[i]`: +/// * inner slot (`i == n - 1`): `pt.timeIncrementInSeconds.value()` +/// (or AIFS hack — see below) +/// * outer slot: `lengthOfTimeRange[i+1] * 3600` (i.e. +/// the hour-length of the next-deeper loop expressed in seconds). +/// +/// **AIFS single-window no-increment hack** (§9.4): +/// when `pt.statisticalWindowCount == 1` AND +/// `!pt.timeIncrementInSeconds.has_value()`, the inner slot is filled with +/// - `lengthOfTimeIncrement[0] = 0` +/// - `indicatorOfUnitForTimeIncrement[0] = static_cast(TimeUnit::Missing)` (255) +/// to mark the increment as undefined. +/// +/// **Preconditions**: +/// - `types.size() == pt.statisticalWindowCount`. +/// - For `pt.statisticalWindowCount >= 2`, `pt.timeIncrementInSeconds` +/// MUST have a value (the AIFS hack is single-window only). +/// +/// @param[in] pt Resolved `ProductTime`. +/// @param[in] types Per-loop GRIB type-of-statistical-processing codes +/// (outermost → innermost), same length as +/// `pt.statisticalWindowCount`. +/// +/// @return Fully-populated `StatisticalProcessing` ready to be written to +/// the GRIB header verbatim. +/// +/// @throws Mars2GribDeductionException on size mismatch, missing required +/// increment for multi-window products, or unsupported window unit. +/// +inline StatisticalProcessing compute_StatisticalProcessing( + const deductions::detail::ProductTime& pt, + const std::vector& types) { + + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + const std::size_t n = pt.statisticalWindowCount; + + if (types.size() != n) { + throw Mars2GribDeductionException( + "compute_StatisticalProcessing: types.size()=" + std::to_string(types.size()) + + " != pt.statisticalWindowCount=" + std::to_string(n), + Here()); + } + + StatisticalProcessing out; + out.numberOfTimeRanges = static_cast(n); + out.typeOfStatisticalProcessing .resize(n); + out.typeOfTimeIncrement .resize(n); + out.indicatorOfUnitForTimeRange .resize(n); + out.lengthOfTimeRange .resize(n); + out.indicatorOfUnitForTimeIncrement.resize(n); + out.lengthOfTimeIncrement .resize(n); + + if (n == 0) { + // Instant product: nothing to encode. Caller (Allocate stage) will + // still write numberOfTimeRanges=0; Preset will write 6 empty + // vectors which eccodes treats as no-op for these keys. + return out; + } + + // AIFS single-window no-increment detection (§9.4). + const bool aifsSingleWindowHack = + (n == 1) && !pt.timeIncrementInSeconds.has_value(); + + if (n >= 2 && !pt.timeIncrementInSeconds.has_value()) { + throw Mars2GribDeductionException( + "compute_StatisticalProcessing: multi-window product (n=" + + std::to_string(n) + ") requires pt.timeIncrementInSeconds; missing.", + Here()); + } + + const long endYear = pt.windowEnd.date().year(); + const long endMonth = pt.windowEnd.date().month(); + + // First pass: fill typeOfStatisticalProcessing, fixed indicators, and + // lengthOfTimeRange. lengthOfTimeIncrement is filled in a second pass + // so outer slots can reference the inner slot's lengthOfTimeRange. + for (std::size_t i = 0; i < n; ++i) { + out.typeOfStatisticalProcessing[i] = static_cast(types[i]); + out.typeOfTimeIncrement[i] = 2; + out.indicatorOfUnitForTimeRange[i] = static_cast(tables::TimeUnit::Hour); + out.indicatorOfUnitForTimeIncrement[i] = static_cast(tables::TimeUnit::Second); + out.lengthOfTimeRange[i] = + detail::windowLengthInHours(pt.statisticalWindows[i], endYear, endMonth); + } + + // Second pass: lengthOfTimeIncrement. + for (std::size_t i = 0; i < n; ++i) { + const bool isInner = (i + 1 == n); + if (isInner) { + if (aifsSingleWindowHack) { + out.lengthOfTimeIncrement[i] = 0; + out.indicatorOfUnitForTimeIncrement[i] = + static_cast(tables::TimeUnit::Missing); + } + else { + // Guaranteed by the multi-window precondition check above + // (and by single-window non-AIFS path). + out.lengthOfTimeIncrement[i] = pt.timeIncrementInSeconds.value(); + } + } + else { + // Outer slot: increment equals the next-deeper window length + // expressed in seconds (lengthOfTimeRange is in hours). + out.lengthOfTimeIncrement[i] = out.lengthOfTimeRange[i + 1] * 3600; + } + } + + return out; +} + +} // namespace metkit::mars2grib::backend::concepts_::impl diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h index 69ba80c44..b30c4e15f 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h @@ -9,39 +9,40 @@ */ /// -/// @file statisticsOp.h +/// @file statisticsEncoding.h /// @brief Implementation of the GRIB `statistics` concept operation. /// -/// This header defines the applicability rules and execution logic for the -/// **statistics concept** within the mars2grib backend. +/// The `statistics` concept encodes GRIB Section-4 keys describing +/// statistical processing over time (PDT 4.8 / 4.11). It runs in three +/// stages: /// -/// The `statistics` concept is responsible for encoding GRIB metadata -/// related to statistical processing over time, including: -/// - type of statistical processing (e.g. mean, accumulation, extremes), -/// - time range structure, -/// - time increment and span, -/// - start and end steps for statistical intervals. -/// -/// The concept operates exclusively in the *Product Definition Section* -/// (Section 4) and is executed across multiple encoding stages -/// (`Allocate`, `Preset`, `Runtime`), each contributing a well-defined -/// subset of the GRIB keys. +/// ### StageAllocate +/// - Validates that the Product Definition Section supports statistics. +/// - Encodes `numberOfTimeRanges` (from `ProductTime`). +/// - Sets `hoursAfterDataCutoff` / `minutesAfterDataCutoff` to missing. /// -/// The implementation follows the standard mars2grib concept model: -/// - Compile-time applicability via `statisticsApplicable` -/// - Stage-dependent encoding logic -/// - Centralized deduction of time descriptors from MARS metadata -/// - Strict validation of GRIB structural constraints -/// - Context-rich error handling +/// ### StagePreset +/// - Computes the per-loop statistical processing descriptor from a +/// `ProductTime` and writes the 6 SoA vectors verbatim: +/// * `typeOfStatisticalProcessing` +/// * `typeOfTimeIncrement` +/// * `indicatorOfUnitForTimeRange` +/// * `lengthOfTimeRange` +/// * `indicatorOfUnitForTimeIncrement` +/// * `lengthOfTimeIncrement` +/// No instant / single-window / multi-window branching: a single uniform +/// path covers all cases (including the AIFS no-increment hack which is +/// handled inside `compute_StatisticalProcessing`). /// -/// @note -/// Support for multiple time ranges is currently **incomplete** and -/// explicitly rejected at both preset and runtime stages. -/// This limitation is documented and enforced at runtime. +/// ### StageRuntime +/// - **Intentionally empty in this revision**. The time-dependent keys +/// (`forecastTime` from `pt.windowStart` offset relative to +/// `pt.referenceDateTime`, and the +/// `OfEndOfOverallTimeInterval` set +/// from `pt.windowEnd`) will be populated in a follow-up. /// -/// @note -/// The namespace name `concepts_` is intentionally used instead of -/// `concepts` to avoid conflicts with the C++20 `concepts` language feature. +/// All temporal data is sourced exclusively from the `ProductTime` +/// produced by `resolve_ProductTime_or_throw` (§15 of `timeProducts.md`). /// /// @ingroup mars2grib_backend_concepts /// @@ -49,28 +50,23 @@ // System includes #include +#include // Core concept includes #include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h" #include "metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h" #include "metkit/mars2grib/utils/generalUtils.h" -// Deductions -#include "metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h" -#include "metkit/mars2grib/backend/deductions/numberOfTimeRanges.h" -#include "metkit/mars2grib/backend/deductions/statisticsDescriptor.h" -#include "metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h" -#include "metkit/mars2grib/backend/deductions/timeSpanInSeconds.h" +// Deductions (ProductTime + per-loop type-of-statistical-processing resolver) +#include "metkit/mars2grib/backend/deductions/productTime.h" +#include "metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h" -// checks +// Checks #include "metkit/mars2grib/backend/checks/checkStatisticsProductDefinitionSection.h" // Tables -#include "metkit/mars2grib/backend/tables/timeUnits.h" -#include "metkit/mars2grib/backend/tables/typeOfTimeIntervals.h" - -// Deduction helpers -#include "metkit/mars2grib/backend/deductions/detail/timeUtils.h" +#include "metkit/mars2grib/backend/tables/typeOfStatisticalProcessing.h" // Utils #include "metkit/config/LibMetkit.h" @@ -83,18 +79,8 @@ namespace metkit::mars2grib::backend::concepts_ { /// /// @brief Compile-time applicability predicate for the `statistics` concept. /// -/// This predicate determines whether the `statistics` concept is applicable -/// for a given encoding stage, GRIB section, and concept variant. -/// -/// The concept is applicable for: -/// - any encoding stage -/// - the *Product Definition Section* (Section 4) -/// -/// @tparam Stage Encoding stage (compile-time constant) -/// @tparam Section GRIB section index -/// @tparam Variant Statistics concept variant -/// -/// @return `true` if the concept is applicable, `false` otherwise. +/// The concept is applicable for the *Product Definition Section* (Section 4) +/// at any encoding stage. /// template constexpr bool statisticsApplicable() { @@ -105,61 +91,12 @@ constexpr bool statisticsApplicable() { /// /// @brief Execute the `statistics` concept operation. /// -/// This function implements the runtime logic of the GRIB `statistics` concept. -/// Depending on the encoding stage, it performs the following actions: -/// -/// ### StageAllocate -/// - Validates that the Product Definition Section supports statistics. -/// - Encodes the number of statistical time ranges. -/// -/// ### StagePreset -/// - Encodes the statistical processing type. -/// - Encodes time unit metadata for ranges and increments. -/// - Handles special cases where the time increment is missing -/// (legacy AIFS behavior). -/// - Rejects unsupported multi-range configurations. -/// -/// ### StageRuntime -/// - Computes and encodes `startStep` and `endStep`. -/// - Resolves time span and forecast step from MARS metadata. -/// - Explicitly rejects multiple time ranges. -/// -/// The concept relies on multiple time-related deductions and helper -/// utilities to interpret MARS time semantics consistently. -/// -/// @tparam Stage Encoding stage (compile-time constant) -/// @tparam Section GRIB section index -/// @tparam Variant Statistics 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 Product Definition Section is incompatible with statistics -/// - unsupported multi-range configurations are detected -/// - any deduction or encoding step fails -/// -/// @note -/// - Time units are currently normalized to **hours** at GRIB level. -/// - Time increment handling contains legacy logic and known hacks. -/// - Multiple time ranges are not yet supported. -/// -/// @see statisticsApplicable -/// @see deductions::numberOfTimeRanges -/// @see deductions::getTimeDescriptorFromMars_orThrow +/// See file-level documentation for stage-by-stage semantics. /// template void StatisticsOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt, OutDict_t& out) { - using metkit::mars2grib::backend::tables::TimeUnit; using metkit::mars2grib::utils::dict_traits::set_or_throw; using metkit::mars2grib::utils::dict_traits::setMissing_or_throw; using metkit::mars2grib::utils::exceptions::Mars2GribConceptException; @@ -171,95 +108,66 @@ void StatisticsOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& MARS2GRIB_LOG_CONCEPT(statistics); - // Global deduction used in multiple stages - long numberOfTimeRangesVal = deductions::numberOfTimeRanges(mars, par); - // ============================================================= - // Variant-specific logic + // StageAllocate // ============================================================= if constexpr (Stage == StageAllocate) { + auto pt = deductions::resolve_ProductTime_or_throw(mars, par, opt); + // Checks/Validation validation::check_StatisticsProductDefinitionSection_or_throw(opt, out); // Encoding setMissing_or_throw(out, "hoursAfterDataCutoff"); setMissing_or_throw(out, "minutesAfterDataCutoff"); - set_or_throw(out, "numberOfTimeRanges", numberOfTimeRangesVal); + set_or_throw(out, "numberOfTimeRanges", + deductions::numberOfTimeRanges(pt)); } + // ============================================================= + // StagePreset + // + // Single uniform path. No branching for instant / single-window + // / multi-window: `compute_StatisticalProcessing` handles all + // cases (including the AIFS no-increment hack) internally. + // ============================================================= if constexpr (Stage == StagePreset) { - // Deductions - std::optional timeIncrementInSecondsOpt = deductions::timeIncrementInSeconds_opt(mars, par); - - // Encoding - set_or_throw(out, "typeOfStatisticalProcessing", typeOfStatisticalProcessing()); - set_or_throw(out, "indicatorOfUnitOfTimeRange", static_cast(TimeUnit::Hour)); - set_or_throw(out, "indicatorOfUnitForTimeRange", static_cast(TimeUnit::Hour)); - - - // HACK: handle special case for AIFS (MUL-227) - if (numberOfTimeRangesVal == 1 && !timeIncrementInSecondsOpt.has_value()) { - - // Encoding - set_or_throw( - out, "typeOfTimeIncrement", - static_cast(tables::TypeOfTimeIntervals::SameStartTimeForecastIncremented)); - set_or_throw(out, "indicatorOfUnitForTimeIncrement", static_cast(TimeUnit::Missing)); - set_or_throw(out, "timeIncrement", 0L); - } - else { - - // Encoding - set_or_throw( - out, "typeOfTimeIncrement", - static_cast(tables::TypeOfTimeIntervals::SameStartTimeForecastIncremented)); - set_or_throw(out, "indicatorOfUnitForTimeIncrement", static_cast(TimeUnit::Second)); - set_or_throw(out, "timeIncrement", timeIncrementInSecondsOpt.value()); - - - // Test WIP - deductions::StatisticalProcessing statsDesc = deductions::getTimeDescriptorFromMars_orThrow( - mars, par, opt, typeOfStatisticalProcessing()); - - if (numberOfTimeRangesVal > 1) { - MARS2GRIB_CONCEPT_THROW( - statistics, - "`statistics` concept with multiple time ranges not yet supported at preset stage..."); - } - } + auto pt = deductions::resolve_ProductTime_or_throw(mars, par, opt); + auto inner = typeOfStatisticalProcessingEnum(); + auto types = deductions::resolve_TypeOfStatisticalProcessing_or_throw( + inner, mars, par, opt); + + auto desc = impl::compute_StatisticalProcessing(pt, types); + + set_or_throw>(out, "typeOfStatisticalProcessing", + desc.typeOfStatisticalProcessing); + set_or_throw>(out, "typeOfTimeIncrement", + desc.typeOfTimeIncrement); + set_or_throw>(out, "indicatorOfUnitForTimeRange", + desc.indicatorOfUnitForTimeRange); + set_or_throw>(out, "lengthOfTimeRange", + desc.lengthOfTimeRange); + set_or_throw>(out, "indicatorOfUnitForTimeIncrement", + desc.indicatorOfUnitForTimeIncrement); + set_or_throw>(out, "lengthOfTimeIncrement", + desc.lengthOfTimeIncrement); } + // ============================================================= + // StageRuntime + // + // Intentionally empty. Time-dependent keys (forecastTime, + // OfEndOfOverallTimeInterval) + // will be populated in a follow-up using pt.windowStart / + // pt.windowEnd. + // ============================================================= if constexpr (Stage == StageRuntime) { - - // Deductions - long stepInHour = deductions::resolve_ForecastTimeInSeconds_or_throw(mars, par, opt) / 3600; - long timeSpanInHour = deductions::resolve_TimeSpanInSeconds_or_throw(mars, par, opt) / 3600; - - // Get the length of timestep in seconds - std::optional timeIncrementInSecondsOpt = deductions::timeIncrementInSeconds_opt(mars, par); - - long tmp = stepInHour - timeSpanInHour; - long startStep = (tmp >= 0) ? tmp : 0; - long endStep = stepInHour; - - // Encoding - set_or_throw(out, "startStep", startStep); - set_or_throw(out, "endStep", endStep); - - // Test WIP - if (timeIncrementInSecondsOpt.has_value()) { - deductions::StatisticalProcessing statsDesc = deductions::getTimeDescriptorFromMars_orThrow( - mars, par, opt, typeOfStatisticalProcessing()); - }; - - - if (numberOfTimeRangesVal > 1) { - MARS2GRIB_CONCEPT_THROW( - statistics, - "`statistics` concept with multiple time ranges not yet supported at runtime stage..."); - } + (void)mars; + (void)par; + (void)opt; + (void)out; } } catch (...) { diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h index c5cbcb2a3..ce3aff707 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h @@ -45,6 +45,7 @@ // Core concept includes #include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/backend/tables/typeOfStatisticalProcessing.h" #include "metkit/mars2grib/utils/generalUtils.h" namespace metkit::mars2grib::backend::concepts_ { @@ -226,4 +227,55 @@ DEF(StatisticsType::Default, 255); #undef DEF +/// +/// @brief Compile-time mapping from `StatisticsType` to +/// `tables::TypeOfStatisticalProcessing` (GRIB Code Table 4.10). +/// +/// Parallel to `typeOfStatisticalProcessing()` (which returns the raw +/// `long` GRIB code). Use this overload when the call site needs the +/// strongly-typed enum value — e.g. when passing the inner statistical +/// operation to `resolve_TypeOfStatisticalProcessing_or_throw`. +/// +/// The two specialization sets MUST stay in numerical lock-step; any +/// future addition to `StatisticsType` MUST update both. +/// +/// @note +/// Three enumerator names differ between the concept-side `StatisticsType` +/// and the table-side `tables::TypeOfStatisticalProcessing` (the numeric +/// codes match exactly): +/// - `StatisticsType::DifferenceFromStart` → `DifferenceEndMinusStart` (4) +/// - `StatisticsType::DifferenceFromEnd` → `DifferenceStartMinusEnd` (8) +/// - `StatisticsType::Default` → `Missing` (255) +/// +template +constexpr tables::TypeOfStatisticalProcessing typeOfStatisticalProcessingEnum(); + +#define DEF(T, VAL) \ + template <> \ + constexpr tables::TypeOfStatisticalProcessing typeOfStatisticalProcessingEnum() { \ + return tables::TypeOfStatisticalProcessing::VAL; \ + } + +DEF(StatisticsType::Average, Average); +DEF(StatisticsType::Accumulation, Accumulation); +DEF(StatisticsType::Maximum, Maximum); +DEF(StatisticsType::Minimum, Minimum); +DEF(StatisticsType::DifferenceFromStart, DifferenceEndMinusStart); +DEF(StatisticsType::RootMeanSquare, RootMeanSquare); +DEF(StatisticsType::StandardDeviation, StandardDeviation); +DEF(StatisticsType::Covariance, Covariance); +DEF(StatisticsType::DifferenceFromEnd, DifferenceStartMinusEnd); +DEF(StatisticsType::Ratio, Ratio); +DEF(StatisticsType::StandardizedAnomaly, StandardizedAnomaly); +DEF(StatisticsType::Summation, Summation); +DEF(StatisticsType::ReturnPeriod, ReturnPeriod); +DEF(StatisticsType::Median, Median); +DEF(StatisticsType::Severity, Severity); +DEF(StatisticsType::Mode, Mode); +DEF(StatisticsType::IndexProcessing, IndexProcessing); +DEF(StatisticsType::Default, Missing); + +#undef DEF + + } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h new file mode 100644 index 000000000..027a3e4b1 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -0,0 +1,671 @@ +/* + * (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 detail/ProductTime.h +/// @brief Implementation detail of the `ProductTime` deduction. +/// +/// Defines: +/// - `TimespanKind` (§6) +/// - `ProductTimeInput` (§6) +/// - `ProductTime` (§5) +/// - `makeProductTime_or_throw` (§6) +/// - shared helpers (calendar arithmetic, alignment checks, signed-second +/// shifts, fmt overloads) +/// +/// Types extracted to dedicated shared headers: +/// - `StatisticalWindow` → `detail/StatisticalWindow.h` (§21) +/// - `parse_StatType_or_throw`, +/// `ParsedStatTypeBlock` → `detail/StatType.h` (§22) +/// +/// This is the **detail** half of the `ProductTime` module. The public +/// resolver `resolve_ProductTime_or_throw` lives in +/// `deductions/productTime.h` (function-primary, lowercase initial per +/// §20.1; this file is type-primary and uses UpperCamelCase initial). +/// +/// All rules in this file are normative; see `deductions/timeProducts.md`. +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +// System includes +#include +#include +#include +#include +#include +#include + +// eckit +#include "eckit/exception/Exceptions.h" +#include "eckit/types/Date.h" +#include "eckit/types/DateTime.h" +#include "eckit/types/Time.h" + +// Project +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h" +#include "metkit/mars2grib/backend/tables/timeUnits.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions::detail { + +// ============================================================= +// 1. Constants +// ============================================================= + +/// +/// @brief Maximum number of statistical windows a `ProductTime` may carry. +/// +/// Matches the maximum producible by the locked `stattype` grammar +/// (§7.7): one `mo` block + one `da` block + one inner `timespan` window. +/// +inline constexpr std::size_t maxStatisticalWindows = 3; + +// ============================================================= +// 2. ProductTime (§5) +// ============================================================= + +/// +/// @brief Canonical, immutable representation of all temporal information +/// associated with a single MARS product. +/// +/// All members are `const`; the struct is copyable but not assignable. +/// +/// See `deductions/timeProducts.md` §5 for the full contract, including: +/// - tri-equivalent instant invariant (§5.1) +/// - window-end ordering invariant (§5.2) +/// - reference-vs-simulated invariant (§5.3) +/// - per-consumer field-access table (§15) +/// +struct ProductTime { + + const eckit::DateTime simulationDateTime; + const eckit::DateTime simulatedDateTime; + const eckit::DateTime referenceDateTime; + + /// Internal convention: `[windowStart, windowEnd)` when + /// `windowStart < windowEnd`. For instant products + /// `windowStart == windowEnd` (the point in time at `windowEnd`). + const eckit::DateTime windowStart; + const eckit::DateTime windowEnd; + + /// Valid entries: `statisticalWindows[0 .. statisticalWindowCount)`. + /// Ordering: outermost → innermost. + const std::array statisticalWindows; + const std::size_t statisticalWindowCount; + + /// Sampling increment of the innermost statistical loop. + /// `std::nullopt` for instant products (§9.1). + /// Optionally absent for single-window statistical products (AIFS path, + /// §9.4). Required and `> 0` for multi-window statistical products. + const std::optional timeIncrementInSeconds; +}; + +// ============================================================= +// 3. TimespanKind (§6) +// ============================================================= + +/// +/// @brief Origin of the MARS `timespan` keyword in a `ProductTimeInput`. +/// +enum class TimespanKind { + Missing, ///< MARS keyword absent. + Duration, ///< MARS keyword carries a duration value. + None ///< MARS keyword equals literal `"none"` (fakeDoubleLoop, §9.4). +}; + +// ============================================================= +// 4. ProductTimeInput (§6) +// ============================================================= + +/// +/// @brief Resolver-side input bundle for `makeProductTime_or_throw`. +/// +/// The resolver normalizes raw MARS / par keys into this struct before +/// invoking the factory. The factory does not consume raw MARS keys. +/// +struct ProductTimeInput { + + eckit::DateTime simulationDateTime; + + std::optional simulatedDateTime; ///< from `hdate` / `htime` + std::optional referenceDateTime; ///< from `fcyear` / `fcmonth` + + /// Offset from `referenceDateTime` to `ProductTime::windowEnd`. + long stepInSeconds{0}; + + TimespanKind timespanKind{TimespanKind::Missing}; + + /// Valid only when `timespanKind == TimespanKind::Duration`. + StatisticalWindow timespan{}; + + /// Temporal windows decoded from `stattype` (period part only), + /// ordered outermost → innermost. + std::array stattypeWindows{}; + std::size_t stattypeWindowCount{0}; + + /// From `deductions::timeIncrementInSeconds_opt(mars, par)`, i.e. + /// `par["timeIncrementInSeconds"]`. Absent for instants and for the AIFS + /// single-window path (§9.4). + std::optional timeIncrementInSeconds; +}; + +// ============================================================= +// 5. Conversion helpers (§7.5, §7.6) +// ============================================================= + +/// +/// @brief Parse a duration string (`step` / `timespan` syntax) to seconds. +/// +/// Bare numeric `N` parses as **N hours**. Suffixed numeric supports: +/// - `h` hours +/// - `m` minutes +/// - `s` seconds +/// - `d` days +/// +/// @throws Mars2GribDeductionException on malformed input. +/// +inline long toSeconds_or_throw(std::string_view step) { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + if (step.empty()) { + throw Mars2GribDeductionException("Empty duration string", Here()); + } + + std::size_t pos = 0; + while (pos < step.size() && std::isdigit(static_cast(step[pos]))) { + ++pos; + } + + if (pos == 0) { + throw Mars2GribDeductionException( + "Invalid duration format (no numeric part): '" + std::string(step) + "'", + Here()); + } + + long value = 0; + try { + value = std::stol(std::string(step.substr(0, pos))); + } + catch (...) { + throw Mars2GribDeductionException( + "Invalid numeric value in duration: '" + std::string(step) + "'", Here()); + } + + char unit = 'h'; // default unit + if (pos < step.size()) { + if (pos + 1 != step.size()) { + throw Mars2GribDeductionException( + "Invalid duration format (trailing characters): '" + std::string(step) + "'", + Here()); + } + unit = step[pos]; + } + + switch (unit) { + case 'h': + return value * 3600L; + case 'm': + return value * 60L; + case 's': + return value; + case 'd': + return value * 86400L; + default: + throw Mars2GribDeductionException( + std::string("Unknown duration unit: '") + unit + + "', expected={h,m,s,d}", + Here()); + } +} + +/// +/// @brief Convert a MARS-encoded `YYYYMMDD` integer to `eckit::Date`. +/// +inline eckit::Date convert_YYYYMMDD2Date_or_throw(long YYYYMMDD) { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + long YYYY = YYYYMMDD / 10000; + long MM = (YYYYMMDD / 100) % 100; + long DD = YYYYMMDD % 100; + + try { + return eckit::Date(YYYY, MM, DD); + } + catch (const eckit::Exception& e) { + throw Mars2GribDeductionException( + "Invalid date value '" + std::to_string(YYYYMMDD) + "': " + e.what(), Here()); + } +} + +/// +/// @brief Convert a MARS-encoded `HHMMSS` integer to `eckit::Time`. +/// +inline eckit::Time convert_hhmmss2Time_or_throw(long hhmmss) { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + long hh = hhmmss / 10000; + long mm = (hhmmss / 100) % 100; + long ss = hhmmss % 100; + + try { + return eckit::Time(hh, mm, ss); + } + catch (const eckit::Exception& e) { + throw Mars2GribDeductionException( + "Invalid time value '" + std::to_string(hhmmss) + "': " + e.what(), Here()); + } +} + +// ============================================================= +// 6. Allow-list / classification helpers (§3.1, §3.3) +// ============================================================= + +/// +/// @brief Test whether a `tables::TimeUnit` value belongs to the +/// `StatisticalWindow` allow-list `{Second, Day, Month}` (§3.1). +/// +inline bool isAllowedWindowUnit(tables::TimeUnit u) { + return u == tables::TimeUnit::Second + || u == tables::TimeUnit::Day + || u == tables::TimeUnit::Month; +} + +/// +/// @brief Test whether a `tables::TimeUnit` value is calendar-aligned +/// (i.e. `Day` or `Month`) per the classification table in §3.3. +/// +inline bool isCalendarUnit(tables::TimeUnit u) { + return u == tables::TimeUnit::Day || u == tables::TimeUnit::Month; +} + +// ============================================================= +// 7. Calendar arithmetic (§9.6) +// ============================================================= + +/// +/// @brief Subtract `count` calendar months from a day=1, midnight `DateTime`. +/// +/// Precondition: `dt` is on day=1 at 00:00:00 (verified by alignment check +/// (§10.10) before this function is called). +/// +inline eckit::DateTime subtractCalendarMonths(const eckit::DateTime& dt, long count) { + long year = dt.date().year(); + long month = dt.date().month(); + + // Convert to (year * 12 + (month-1)) basis so subtraction is trivial. + long total = year * 12L + (month - 1L) - count; + long newYear = total / 12L; + long newMonthIdx = total % 12L; + if (newMonthIdx < 0) { + newMonthIdx += 12L; + newYear -= 1L; + } + long newMonth = newMonthIdx + 1L; + + return eckit::DateTime(eckit::Date(newYear, newMonth, 1), eckit::Time(0, 0, 0)); +} + +/// +/// @brief Subtract `count` calendar days from a midnight `DateTime`. +/// +/// Precondition: `dt` is at 00:00:00 (verified by alignment check (§10.9)). +/// +inline eckit::DateTime subtractCalendarDays(const eckit::DateTime& dt, long count) { + eckit::Date d = dt.date(); + d -= count; + return eckit::DateTime(d, eckit::Time(0, 0, 0)); +} + +/// +/// @brief Subtract `count` seconds from a `DateTime`. +/// +inline eckit::DateTime subtractSeconds(const eckit::DateTime& dt, long count) { + return dt + (-static_cast(count)); +} + +/// +/// @brief Apply `windowStart := windowEnd - window` per §9.6. +/// +/// Dispatches on `window.unit`. Precondition: `window.unit` is in the +/// allow-list and any required alignment has been verified by the caller. +/// +inline eckit::DateTime applyWindowSubtraction(const eckit::DateTime& windowEnd, + const StatisticalWindow& window) { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + switch (window.unit) { + case tables::TimeUnit::Second: + return subtractSeconds(windowEnd, window.count); + case tables::TimeUnit::Day: + return subtractCalendarDays(windowEnd, window.count); + case tables::TimeUnit::Month: + return subtractCalendarMonths(windowEnd, window.count); + default: + throw Mars2GribDeductionException( + "Internal error: applyWindowSubtraction called with disallowed unit", + Here()); + } + mars2gribUnreachable(); +} + +// ============================================================= +// 8. Alignment checks (§4.2, §4.3, §10.9, §10.10) +// ============================================================= + +/// +/// @brief Test whether a `DateTime` is at hh=00, mm=00, ss=00. +/// +inline bool isAtMidnight(const eckit::DateTime& dt) { + const eckit::Time& t = dt.time(); + return t.hours() == 0 && t.minutes() == 0 && t.seconds() == 0; +} + +/// +/// @brief Test whether a `DateTime` is on day=1 at midnight. +/// +inline bool isOnFirstOfMonthMidnight(const eckit::DateTime& dt) { + return isAtMidnight(dt) && dt.date().day() == 1; +} + +// ============================================================= +// 9. Inline string formatting (§12, §13) +// ============================================================= + +/// +/// @brief Format an `eckit::DateTime` as an ISO-8601 string for logs/errors. +/// +inline std::string fmt(const eckit::DateTime& dt) { + return dt.iso(true); +} + +/// +/// @brief Format a `tables::TimeUnit` value as a short symbolic name. +/// +/// Uses the canonical names produced by `tables::enum2name_TimeUnit_or_throw` +/// for the allow-list values, with a numeric fallback for any other value. +/// +inline std::string fmt(tables::TimeUnit u) { + switch (u) { + case tables::TimeUnit::Second: + return "second"; + case tables::TimeUnit::Day: + return "day"; + case tables::TimeUnit::Month: + return "month"; + default: + return "TimeUnit(" + std::to_string(static_cast(u)) + ")"; + } +} + +/// +/// @brief Format a `StatisticalWindow` as `{unit,count}`. +/// +inline std::string fmt(const StatisticalWindow& w) { + return "{" + fmt(w.unit) + "," + std::to_string(w.count) + "}"; +} + +/// +/// @brief Format the populated prefix of a `StatisticalWindow` array. +/// +inline std::string fmt(const std::array& a, + std::size_t count) { + std::string s{"["}; + for (std::size_t i = 0; i < count; ++i) { + if (i) + s += ","; + s += fmt(a[i]); + } + s += "]"; + return s; +} + +// ============================================================= +// 10. Factory: makeProductTime_or_throw (§6, §9, §10) +// ============================================================= + +/// +/// @brief Validate input invariants and construct an immutable `ProductTime`. +/// +/// Performs (in order): +/// 1. Default resolution for `simulatedDateTime` and `referenceDateTime` +/// (§7.3, §7.4). +/// 2. Computes `windowEnd := referenceDateTime + stepInSeconds` (§7.5). +/// 3. Assembles `statisticalWindows` per the case table (§9). +/// 4. Computes `windowStart` per §9. +/// 5. Validates all invariants (§5.1, §5.2, §5.3) and all hard errors (§10). +/// +/// @throws Mars2GribDeductionException on any rule violation; check sites +/// are tagged with the corresponding §10 entry number. +/// +inline ProductTime makeProductTime_or_throw(const ProductTimeInput& input) { + + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + // --------------------------------------------------------- + // Default resolution (§7.3, §7.4) + // --------------------------------------------------------- + + const eckit::DateTime simulationDateTime = input.simulationDateTime; + + // §7.3: hdate / htime defaulting (the resolver has already enforced + // §10.2; here we just apply the fall-through to simulationDateTime). + const eckit::DateTime simulatedDateTime = + input.simulatedDateTime.value_or(simulationDateTime); + + // §7.4: fcyear / fcmonth defaulting. + const eckit::DateTime referenceDateTime = + input.referenceDateTime.value_or(simulatedDateTime); + + // --------------------------------------------------------- + // §5.3: referenceDateTime >= simulatedDateTime (§10.4) + // --------------------------------------------------------- + if (referenceDateTime < simulatedDateTime) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.4]: referenceDateTime ('" + + fmt(referenceDateTime) + "') < simulatedDateTime ('" + + fmt(simulatedDateTime) + "')", + Here()); + } + + // --------------------------------------------------------- + // windowEnd (§7.5) + // --------------------------------------------------------- + const eckit::DateTime windowEnd = + referenceDateTime + static_cast(input.stepInSeconds); + + // --------------------------------------------------------- + // §9: window assembly + // --------------------------------------------------------- + std::array windows{}; + std::size_t windowCount = 0; + eckit::DateTime windowStart = windowEnd; + + const bool hasTimespanDuration = (input.timespanKind == TimespanKind::Duration); + const bool hasTimespanNone = (input.timespanKind == TimespanKind::None); + const bool hasTimespanMissing = (input.timespanKind == TimespanKind::Missing); + const std::size_t nStat = input.stattypeWindowCount; + + // Case dispatch (§9). + if (hasTimespanMissing && nStat == 0) { + // ----- §9.1: Instant product ----- + windowCount = 0; + windowStart = windowEnd; + } + else if (hasTimespanDuration && nStat == 0) { + // ----- §9.2: Old-style single-loop statistic ----- + windows[0] = input.timespan; + windowCount = 1; + // windowStart computed after allow-list / positivity / alignment. + } + else if (hasTimespanDuration && nStat >= 1) { + // ----- §9.3: Old-style multi-loop statistic ----- + if (nStat + 1 > maxStatisticalWindows) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.15]: statisticalWindowCount (" + + std::to_string(nStat + 1) + ") > maxStatisticalWindows (" + + std::to_string(maxStatisticalWindows) + ")", + Here()); + } + for (std::size_t i = 0; i < nStat; ++i) { + windows[i] = input.stattypeWindows[i]; + } + windows[nStat] = input.timespan; + windowCount = nStat + 1; + } + else if (hasTimespanNone && nStat == 1) { + // ----- §9.4: New-style fakeDoubleLoop ----- + windows[0] = input.stattypeWindows[0]; + windowCount = 1; + } + else if (hasTimespanNone && nStat == 0) { + // §10.7: timespan = none but stattype missing. + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.7]: timespan='none' requires " + "exactly one stattype block, got 0", + Here()); + } + else if (hasTimespanNone && nStat > 1) { + // §10.8: timespan = none with more than one stattype block. + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.8]: timespan='none' requires " + "exactly one stattype block, got " + std::to_string(nStat), + Here()); + } + else if (hasTimespanMissing && nStat >= 1) { + // §10.6: stattype present but timespan missing. + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.6]: stattype present (" + std::to_string(nStat) + + " block(s)) but timespan is missing", + Here()); + } + else { + // Defensive: all combinations should be covered above. + throw Mars2GribDeductionException( + "ProductTime internal error: unhandled (timespanKind, stattypeCount) combination", + Here()); + } + + // --------------------------------------------------------- + // Per-window validation (§3.1, §3.2 → §10.11, §10.18 (b)) + // --------------------------------------------------------- + for (std::size_t i = 0; i < windowCount; ++i) { + const StatisticalWindow& w = windows[i]; + + if (w.count <= 0) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.11]: statisticalWindows[" + std::to_string(i) + + "] has non-positive count (" + std::to_string(w.count) + ")", + Here()); + } + + if (!isAllowedWindowUnit(w.unit)) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.18(b)]: statisticalWindows[" + std::to_string(i) + + "] uses disallowed TimeUnit '" + fmt(w.unit) + + "', expected one of {second, day, month}", + Here()); + } + } + + // --------------------------------------------------------- + // Outermost-window alignment (§4.4, §10.9, §10.10) and windowStart + // --------------------------------------------------------- + if (windowCount > 0) { + const StatisticalWindow& outermost = windows[0]; + + if (outermost.unit == tables::TimeUnit::Day) { + if (!isAtMidnight(windowEnd)) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.9]: outermost window is " + "calendar-day-aligned but windowEnd ('" + fmt(windowEnd) + + "') is not at hh=00,mm=00,ss=00", + Here()); + } + } + else if (outermost.unit == tables::TimeUnit::Month) { + if (!isOnFirstOfMonthMidnight(windowEnd)) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.10]: outermost window is " + "calendar-month-aligned but windowEnd ('" + fmt(windowEnd) + + "') is not on day=1 at hh=00,mm=00,ss=00", + Here()); + } + } + // tables::TimeUnit::Second: no alignment required. + + windowStart = applyWindowSubtraction(windowEnd, outermost); + } + + // --------------------------------------------------------- + // §5.2: windowStart <= windowEnd (defensive) + // --------------------------------------------------------- + if (windowStart > windowEnd) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§5.2]: windowStart ('" + fmt(windowStart) + + "') > windowEnd ('" + fmt(windowEnd) + "')", + Here()); + } + + // --------------------------------------------------------- + // timeIncrementInSeconds validation (§7.8, §9.5, §10.13, §10.14) + // --------------------------------------------------------- + std::optional tInc = input.timeIncrementInSeconds; + + if (tInc.has_value() && tInc.value() < 0) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.14]: timeIncrementInSeconds < 0 ('" + + std::to_string(tInc.value()) + "')", + Here()); + } + + if (windowCount >= 2 && !tInc.has_value()) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.13]: statisticalWindowCount (" + + std::to_string(windowCount) + + ") >= 2 requires timeIncrementInSeconds to be present", + Here()); + } + + // --------------------------------------------------------- + // §5.1: tri-equivalent instant invariant (§10.5) + // --------------------------------------------------------- + const bool a = (windowStart == windowEnd); + const bool b = (windowCount == 0); + const bool c = !tInc.has_value(); + if (!((a == b) && (b == c))) { + throw Mars2GribDeductionException( + std::string("ProductTime invariant violated [§10.5]: tri-equivalence broken: ") + + "(windowStart==windowEnd)=" + (a ? "true" : "false") + + ", (statisticalWindowCount==0)=" + (b ? "true" : "false") + + ", (timeIncrementInSeconds==nullopt)=" + (c ? "true" : "false"), + Here()); + } + + // --------------------------------------------------------- + // Construct the immutable ProductTime + // --------------------------------------------------------- + return ProductTime{ + simulationDateTime, + simulatedDateTime, + referenceDateTime, + windowStart, + windowEnd, + windows, + windowCount, + tInc + }; +} + +} // namespace metkit::mars2grib::backend::deductions::detail diff --git a/src/metkit/mars2grib/backend/deductions/detail/StatType.h b/src/metkit/mars2grib/backend/deductions/detail/StatType.h new file mode 100644 index 000000000..ced8057a8 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/detail/StatType.h @@ -0,0 +1,270 @@ +/* + * (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 detail/StatType.h +/// @brief Shared `stattype` parser used by both `ProductTime` and +/// `typeOfStatisticalProcessing`. +/// +/// This header is **shared infrastructure**, not a deduction. It exposes: +/// - the `ParsedStatTypeBlock` value type (per-block, pre-mapped to GRIB +/// table values); +/// - the `parse_StatType_or_throw` function, which decodes the locked +/// `stattype` grammar (§7.7) into a vector of blocks in MARS textual +/// order (outermost → innermost). +/// +/// **Why shared**: both `ProductTime` (which needs the *period* part) and +/// `typeOfStatisticalProcessing` (which needs the *operation* part) consume +/// the same `stattype` string. A single shared parser eliminates the drift +/// risk between them — both deductions size their output arrays in lock-step +/// from the same parse result. +/// +/// **Allow-lists enforced by this parser** (parser-level hard errors): +/// - period unit ∈ `{mo, da}` → §10.16 / §10.18 (a) +/// - operation ∈ `{av, mn, mx, sd}` → §10.16 +/// - block ordering: `mo` precedes `da` → §10.17 +/// - block count ∈ `{1, 2}` → §10.16 +/// +/// The extended `Second`-inclusive allow-list applies only to the assembled +/// `ProductTime::statisticalWindows` array (where the innermost window may +/// originate from `timespan`); that check lives in +/// `makeProductTime_or_throw`. +/// +/// See `deductions/timeProducts.md` §22 for the full normative +/// specification. +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +// System includes +#include +#include +#include +#include + +// eckit +#include "eckit/exception/Exceptions.h" + +// Project +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h" +#include "metkit/mars2grib/backend/tables/timeUnits.h" +#include "metkit/mars2grib/backend/tables/typeOfStatisticalProcessing.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions::detail { + +// ============================================================= +// 1. ParsedStatTypeBlock (§22.2) +// ============================================================= + +/// +/// @brief One decoded `stattype` block, pre-mapped to GRIB table values. +/// +/// - `timeWindow` carries the **period** part of the block (the +/// stattype-grammar pair `(period_unit, count)`). For locked grammar +/// tokens `mo` / `da`, the parser emits `count = 1` and +/// `unit = tables::TimeUnit::Month` / `tables::TimeUnit::Day`. +/// - `typeOfStatisticalProcessing` carries the **operation** part of the +/// block, already mapped to a value of GRIB Code Table 4.10: +/// - `av` → `Average` +/// - `mn` → `Minimum` +/// - `mx` → `Maximum` +/// - `sd` → `StandardDeviation` +/// +struct ParsedStatTypeBlock { + StatisticalWindow timeWindow; + tables::TypeOfStatisticalProcessing typeOfStatisticalProcessing; +}; + +// ============================================================= +// 2. Single-token decoders (private helpers) +// ============================================================= + +namespace impl { + +/// +/// @brief Decode a single period token to a `StatisticalWindow` +/// with `count = 1`. +/// +/// @throws Mars2GribDeductionException on unknown token (§10.16) or +/// narrow-allow-list violation (§10.18 (a)). +/// +inline StatisticalWindow decodePeriod_or_throw(std::string_view s, + const std::string& fullStatType) { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + if (s == "da") + return StatisticalWindow{tables::TimeUnit::Day, 1}; + if (s == "mo") + return StatisticalWindow{tables::TimeUnit::Month, 1}; + + throw Mars2GribDeductionException( + "Invalid stattype period token [§10.16/§10.18(a)]: actual='" + + std::string(s) + "', expected={'da','mo'} (in stattype='" + + fullStatType + "')", + Here()); +} + +/// +/// @brief Decode a single operation token to a +/// `tables::TypeOfStatisticalProcessing`. +/// +/// @throws Mars2GribDeductionException on unknown token (§10.16). +/// +inline tables::TypeOfStatisticalProcessing +decodeOp_or_throw(std::string_view s, const std::string& fullStatType) { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + if (s == "av") + return tables::TypeOfStatisticalProcessing::Average; + if (s == "mn") + return tables::TypeOfStatisticalProcessing::Minimum; + if (s == "mx") + return tables::TypeOfStatisticalProcessing::Maximum; + if (s == "sd") + return tables::TypeOfStatisticalProcessing::StandardDeviation; + + throw Mars2GribDeductionException( + "Invalid stattype operation token [§10.16]: actual='" + + std::string(s) + "', expected={'av','mn','mx','sd'} (in stattype='" + + fullStatType + "')", + Here()); +} + +} // namespace impl + +// ============================================================= +// 3. parse_StatType_or_throw (§22.3) +// ============================================================= + +/// +/// @brief Parse a `stattype` string per the locked grammar (§7.7). +/// +/// Grammar: +/// @code +/// stattype := block ('_' block)* +/// block := period operation (4 chars) +/// period := 'mo' | 'da' +/// operation := 'av' | 'mn' | 'mx' | 'sd' +/// @endcode +/// +/// Semantic constraints (parser-level hard errors): +/// - block count MUST be in `{1, 2}` (§10.16); +/// - if both `mo` and `da` blocks are present, `mo` MUST precede `da` +/// (§10.17); +/// - all unit and operation tokens MUST belong to the locked allow-lists +/// (§10.16, §10.18 (a)). +/// +/// @param[in] stattype Raw textual value of the MARS `stattype` keyword. +/// MUST be non-empty; callers MUST NOT invoke the +/// parser when the `stattype` key is absent from the +/// MARS dictionary. +/// +/// @return A vector of size 1 or 2, in MARS textual order (outermost first, +/// innermost last). +/// +/// @throws Mars2GribDeductionException on any grammar (§10.16), +/// ordering (§10.17), or narrow-allow-list (§10.18 (a)) violation. +/// +/// @note Each block's `typeOfStatisticalProcessing` is already mapped to a +/// value of GRIB Code Table 4.10; consumers do not need to perform +/// any further mapping. +/// +inline std::vector +parse_StatType_or_throw(const std::string& stattype) { + + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + if (stattype.empty()) { + throw Mars2GribDeductionException( + "Invalid stattype: empty string (caller MUST NOT invoke " + "parse_StatType_or_throw when 'stattype' is absent)", + Here()); + } + + std::vector blocks; + + std::size_t pos = 0; + while (pos < stattype.size()) { + + if (pos + 4 > stattype.size()) { + throw Mars2GribDeductionException( + "Invalid stattype format [§10.16]: incomplete 4-char block " + "at position " + std::to_string(pos) + " in '" + stattype + "'", + Here()); + } + + StatisticalWindow window = + impl::decodePeriod_or_throw(stattype.substr(pos, 2), stattype); + tables::TypeOfStatisticalProcessing op = + impl::decodeOp_or_throw(stattype.substr(pos + 2, 2), stattype); + + blocks.push_back(ParsedStatTypeBlock{window, op}); + + pos += 4; + if (pos < stattype.size()) { + if (stattype[pos] != '_') { + throw Mars2GribDeductionException( + "Invalid stattype separator [§10.16] at position " + + std::to_string(pos) + " in '" + stattype + + "': expected '_'", + Here()); + } + ++pos; + } + } + + // Block-count limit: 1 or 2 blocks (§22.4 / §10.16). + if (blocks.empty() || blocks.size() > 2) { + throw Mars2GribDeductionException( + "Invalid stattype [§10.16]: block count (" + std::to_string(blocks.size()) + + ") outside allowed range {1, 2} (in stattype='" + stattype + "')", + Here()); + } + + // Semantic validation (§10.17): at most one mo, at most one da, mo precedes da. + int moIndex = -1; + int daIndex = -1; + for (std::size_t i = 0; i < blocks.size(); ++i) { + if (blocks[i].timeWindow.unit == tables::TimeUnit::Month) { + if (moIndex != -1) { + throw Mars2GribDeductionException( + "Invalid stattype [§10.17] '" + stattype + + "': more than one 'mo' block", + Here()); + } + moIndex = static_cast(i); + } + if (blocks[i].timeWindow.unit == tables::TimeUnit::Day) { + if (daIndex != -1) { + throw Mars2GribDeductionException( + "Invalid stattype [§10.17] '" + stattype + + "': more than one 'da' block", + Here()); + } + daIndex = static_cast(i); + } + } + + if (moIndex != -1 && daIndex != -1 && moIndex > daIndex) { + throw Mars2GribDeductionException( + "Invalid stattype [§10.17] '" + stattype + + "': blocks not in outermost-to-innermost order " + "('mo' must precede 'da')", + Here()); + } + + return blocks; +} + +} // namespace metkit::mars2grib::backend::deductions::detail diff --git a/src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h b/src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h new file mode 100644 index 000000000..b735cf791 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h @@ -0,0 +1,71 @@ +/* + * (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 detail/StatisticalWindow.h +/// @brief Shared `StatisticalWindow` type used by `ProductTime` and the +/// shared `stattype` parser. +/// +/// `StatisticalWindow` represents one statistical window as a +/// `(unit, count)` pair, reusing the GRIB-aligned `tables::TimeUnit` enum +/// (GRIB2 Code Table 4.4) rather than introducing a parallel "kind" enum. +/// +/// This header is **shared infrastructure**, not a deduction. It is consumed +/// by: +/// - `deductions/detail/ProductTime.h` (stores values inside `ProductTime`) +/// - `deductions/detail/StatType.h` (the shared `stattype` parser) +/// +/// The two-level allow-list enforcement is documented in +/// `deductions/timeProducts.md` §10.18: +/// - the parser (§22) enforces the narrow `stattype`-grammar allow-list +/// `{Day, Month}` at parse time; +/// - the factory `makeProductTime_or_throw` (§detail/ProductTime.h) +/// enforces the extended assembled-window allow-list `{Second, Day, +/// Month}` after window assembly. +/// +/// See `deductions/timeProducts.md` §3, §21 for the full normative +/// specification. +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +// Project +#include "metkit/mars2grib/backend/tables/timeUnits.h" + +namespace metkit::mars2grib::backend::deductions::detail { + +/// +/// @brief One statistical window: a `tables::TimeUnit` + a count. +/// +/// Reuses the GRIB-aligned `tables::TimeUnit` enum (GRIB2 Code Table 4.4) +/// rather than introducing a parallel "kind" enum. +/// +/// **Allow-list (assembled `ProductTime::statisticalWindows` entry)**: +/// only `Second`, `Day`, `Month` are legal. Any other `TimeUnit` value (in +/// particular `Hour`, `Minute`, `Hours3/6/12`, `Year`, `Decade`, `Normal`, +/// `Century`, `Missing`) is rejected by the factory `makeProductTime_or_throw` +/// (§10.18 (b)). +/// +/// **Allow-list (parser-produced entry)**: only `Day`, `Month` are legal. +/// The narrower `stattype`-grammar allow-list is enforced inside +/// `parse_StatType_or_throw` (§10.18 (a)). +/// +/// **Invariant**: `count > 0` for any window stored inside a `ProductTime` +/// (§3.2 / §10.11). The default-constructed value (`count == 0`) is legal at +/// the C++ language level but illegal as a `ProductTime` member. +/// +struct StatisticalWindow { + tables::TimeUnit unit{tables::TimeUnit::Second}; + long count{0}; +}; + +} // namespace metkit::mars2grib::backend::deductions::detail diff --git a/src/metkit/mars2grib/backend/deductions/detail/timeUtils.h b/src/metkit/mars2grib/backend/deductions/detail/timeUtils.h deleted file mode 100644 index 6d17769b1..000000000 --- a/src/metkit/mars2grib/backend/deductions/detail/timeUtils.h +++ /dev/null @@ -1,325 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include "eckit/exception/Exceptions.h" -#include "metkit/mars2grib/utils/generalUtils.h" - -namespace metkit::mars2grib::backend::deductions::detail { - - -enum class Period { - Daily, - Monthly -}; - -enum class StatOp { - Average, - Minimum, - Maximum, - StdDev -}; - - -// ============================================================= -// Decoded block -// ============================================================= - -struct StatTypeBlock { - Period period; - StatOp op; -}; - -// ============================================================= -// Utilities -// ============================================================= - -// Count number of blocks in stattype (Fortran-equivalent logic) -inline std::size_t countBlocks(std::string_view stattype) { - if (stattype.empty()) - return 0; - - std::size_t blocks = 1; - for (char c : stattype) { - if (c == '_') - ++blocks; - } - return blocks; -} - -// Compute month length in hours (Julian / truncated-Gregorian rule) -inline long previousMonthLengthHours(int year, int month) { - - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - if (month < 1 || month > 12) { - throw Mars2GribGenericException("Invalid month (must be 1..12)", Here()); - } - - switch (month) { - case 1: - case 2: - case 4: - case 6: - case 8: - case 9: - case 11: - return 31 * 24; - - case 5: - case 7: - case 10: - case 12: - return 30 * 24; - - case 3: - return ((year % 4) == 0 ? 29 : 28) * 24; - } - - throw Mars2GribGenericException("Unreachable", Here()); -} - -// Compute month length in hours (Julian / truncated-Gregorian rule) -inline long monthLengthHours(int year, int month) { - - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - if (month < 1 || month > 12) { - throw Mars2GribGenericException("Invalid month (must be 1..12)", Here()); - } - - switch (month) { - case 1: - case 3: - case 5: - case 7: - case 8: - case 10: - case 12: - return 31 * 24; - - case 4: - case 6: - case 9: - case 11: - return 30 * 24; - - case 2: - return ((year % 4) == 0 ? 29 : 28) * 24; - } - - throw Mars2GribGenericException("Unreachable", Here()); -} - -// ============================================================= -// Decoding helpers -// ============================================================= - -inline Period decodePeriod_orThrow(std::string_view s) { - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - if (s == "da") - return Period::Daily; - if (s == "mo") - return Period::Monthly; - throw Mars2GribGenericException("Invalid period token: " + std::string(s), Here()); -} - -inline StatOp decodeOp_orThrow(std::string_view s) { - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - if (s == "av") - return StatOp::Average; - if (s == "mn") - return StatOp::Minimum; - if (s == "mx") - return StatOp::Maximum; - if (s == "sd") - return StatOp::StdDev; - throw Mars2GribGenericException("Invalid operation token: " + std::string(s), Here()); -} - -// ============================================================= -// Parser + semantic validation -// ============================================================= - -inline std::vector parseStatType_or_throw(const std::string& stattype) { - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - std::vector blocks; - - std::size_t pos = 0; - while (pos < stattype.size()) { - - if (pos + 4 > stattype.size()) { - throw Mars2GribGenericException("Invalid stattype format", Here()); - } - - auto period = decodePeriod_orThrow(stattype.substr(pos, 2)); - auto op = decodeOp_orThrow(stattype.substr(pos + 2, 2)); - - blocks.push_back({period, op}); - - pos += 4; - if (pos < stattype.size()) { - if (stattype[pos] != '_') { - throw Mars2GribGenericException("Invalid stattype separator (expected '_')", Here()); - } - ++pos; - } - } - - // Semantic validation: only one mo, one da, correct order - int moIndex = -1; - int daIndex = -1; - - for (std::size_t i = 0; i < blocks.size(); ++i) { - if (blocks[i].period == Period::Monthly) { - if (moIndex != -1) - throw Mars2GribGenericException("Invalid stattype: more than one 'mo'", Here()); - moIndex = static_cast(i); - } - if (blocks[i].period == Period::Daily) { - if (daIndex != -1) - throw Mars2GribGenericException("Invalid stattype: more than one 'da'", Here()); - daIndex = static_cast(i); - } - } - - if (moIndex != -1 && daIndex != -1 && moIndex > daIndex) { - throw Mars2GribGenericException("Invalid stattype order: 'mo' must precede 'da'", Here()); - } - - return blocks; -} - -// ============================================================= -// Pretty printing (test/debug) -// ============================================================= - -inline const char* toString(Period p) { - switch (p) { - case Period::Daily: - return "Daily"; - case Period::Monthly: - return "Monthly"; - } - return "Unknown"; -} - -inline const char* toString(StatOp op) { - switch (op) { - case StatOp::Average: - return "Average"; - case StatOp::Minimum: - return "Minimum"; - case StatOp::Maximum: - return "Maximum"; - case StatOp::StdDev: - return "StandardDeviation"; - } - return "Unknown"; -} - -inline void prettyPrint(const std::vector& blocks) { - std::cout << "Decoded stattype (" << blocks.size() << " block(s)):\n"; - for (std::size_t i = 0; i < blocks.size(); ++i) { - std::cout << " [" << i << "] " - << "Period = " << toString(blocks[i].period) << ", Operation = " << toString(blocks[i].op) << '\n'; - } -} - - -// If unit is missing default is hours!!! -inline long toSeconds_or_throw(std::string_view step) { - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - if (step.empty()) { - throw Mars2GribGenericException("Empty step string", Here()); - } - - // Split numeric part and optional unit - std::size_t pos = 0; - while (pos < step.size() && std::isdigit(static_cast(step[pos]))) { - ++pos; - } - - if (pos == 0) { - throw Mars2GribGenericException("Invalid step format (no numeric part): " + std::string(step), Here()); - } - - long value = 0; - try { - value = std::stol(std::string(step.substr(0, pos))); - } - catch (...) { - throw Mars2GribGenericException("Invalid numeric value in step: " + std::string(step), Here()); - } - - // Default unit: hours - char unit = 'h'; - if (pos < step.size()) { - if (pos + 1 != step.size()) { - throw Mars2GribGenericException("Invalid step format (trailing characters): " + std::string(step), Here()); - } - unit = step[pos]; - } - - switch (unit) { - case 'h': // hours - return value * 3600L; - case 'm': // minutes - return value * 60L; - case 's': // seconds - return value; - case 'd': // days - return value * 86400L; - default: - throw Mars2GribGenericException(std::string("Unknown step unit: '") + unit + "'", Here()); - } -} - -inline eckit::Date convert_YYYYMMDD2Date_or_throw(long YYYYMMDD) { - - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - long YYYY = YYYYMMDD / 10000; - long MM = (YYYYMMDD / 100) % 100; - long DD = YYYYMMDD % 100; - - // @todo Validate YYYY, MM, DD ranges? - - try { - return eckit::Date(YYYY, MM, DD); - } - catch (const eckit::Exception& e) { - throw Mars2GribGenericException("Invalid date value: " + std::string(e.what()), Here()); - } -} - - -inline eckit::Time convert_hhmmss2Time_or_throw(long hhmmss) { - - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - long hh = hhmmss / 10000; - long mm = (hhmmss / 100) % 100; - long ss = hhmmss % 100; - - // @todo Validate hh, mm, ss ranges? - - try { - return eckit::Time(hh, mm, ss); - } - catch (const eckit::Exception& e) { - throw Mars2GribGenericException("Invalid time value: " + std::string(e.what()), Here()); - } -} - - -} // namespace metkit::mars2grib::backend::deductions::detail diff --git a/src/metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h b/src/metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h deleted file mode 100644 index bbf0e3968..000000000 --- a/src/metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h +++ /dev/null @@ -1,115 +0,0 @@ -/* - * (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 "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 forecast valid time from the MARS dictionary using a step expressed in seconds. -/// -/// This deduction computes the forecast valid time by retrieving the MARS key -/// `step` and interpreting it as a forecast lead time expressed in hours. -/// The lead time is converted to seconds and added to the reference time -/// to obtain the forecastTime in seconds as a `long`. -/// -/// The conversion follows the conventional MARS interpretation: -/// - `step` is assumed to be expressed in hours, -/// - the corresponding number of seconds is obtained as -/// \f$ \text{step} \times 3600 \f$. -/// -/// The resolved forecast lead time (in seconds) is logged for diagnostic -/// and traceability purposes. -/// -/// @tparam MarsDict_t -/// Type of the MARS dictionary, expected to contain the key `step`. -/// -/// @tparam ParDict_t -/// Type of the parameter dictionary (unused by this deduction). -/// -/// @tparam OptDict_t -/// Type of the options dictionary (unused by this deduction). -/// -/// @param[in] mars -/// MARS dictionary from which the forecast step is retrieved. -/// -/// @param[in] par -/// Parameter dictionary (unused). -/// -/// @param[in] opt -/// Options dictionary (unused). -/// -/// @return -/// The forecast valid time as a `long`, representing the number of seconds, -/// obtained by converting the step to seconds. -/// -/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException -/// If: -/// - the key `step` is not present in the MARS dictionary, -/// - the associated value cannot be converted to `long`, -/// - any unexpected error occurs during conversion or time computation. -/// -/// @note -/// This deduction assumes that the MARS `step` value is expressed in -/// hours. Alternative units (e.g. minutes or seconds) are not supported. -/// -/// @note -/// The reference time to which the forecast step is applied is assumed -/// to be available in the surrounding context. This function does not -/// resolve the reference time itself. -/// -/// @note -/// The function follows a fail-fast strategy and uses nested exception -/// propagation to preserve full error provenance across API boundaries. -/// -template -long resolve_ForecastTimeInSeconds_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 { - - // Get the mars.step - long marsStep = get_or_throw(mars, "step"); - - // Convert step in seconds (Assumed to be in hours) - /// @todo add constant to avoid hardcoding 3600 - long marsStepInSecondsVal = marsStep * 3600; - - // Logging of the forecastTime - MARS2GRIB_LOG_RESOLVE([&]() { - std::string logMsg = "forecastTime: deduced from mars dictionary with value: "; - logMsg += std::to_string(marsStepInSecondsVal) + " [seconds]"; - return logMsg; - }()); - - // Success exit point - return marsStepInSecondsVal; - } - catch (...) { - - // Rethrow nested exceptions - std::throw_with_nested(Mars2GribDeductionException("Unable to compute forecast time", Here())); - }; - - // Remove compiler warning - mars2gribUnreachable(); -}; - -} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/hindcastDateTime.h b/src/metkit/mars2grib/backend/deductions/hindcastDateTime.h deleted file mode 100644 index f5b74775f..000000000 --- a/src/metkit/mars2grib/backend/deductions/hindcastDateTime.h +++ /dev/null @@ -1,130 +0,0 @@ -/* - * (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 "eckit/log/Log.h" -#include "eckit/types/Date.h" -#include "eckit/types/DateTime.h" -#include "eckit/types/Time.h" -#include "metkit/mars2grib/utils/generalUtils.h" - -#include "metkit/mars2grib/backend/deductions/detail/timeUtils.h" - -#include "metkit/config/LibMetkit.h" -#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" -#include "metkit/mars2grib/utils/logUtils.h" -#include "metkit/mars2grib/utils/mars2gribExceptions.h" - -namespace metkit::mars2grib::backend::deductions { - -/// -/// @brief Resolve the hindcast reference date and time from the MARS dictionary. -/// -/// This deduction retrieves the hindcast reference date and time from the -/// MARS dictionary entries `hdate` (mandatory) and `htime` (optional) and -/// combines them into an `eckit::DateTime` object. -/// -/// The values are expected to follow the standard MARS integer encodings: -/// - `hdate`: calendar date encoded as `YYYYMMDD` -/// - `htime`: clock time encoded as `HHMMSS` (defaulting to `000000` if missing) -/// -/// These fields are typically used for hindcast or reforecast products, -/// where the reference time of the forecast differs from the nominal -/// analysis or forecast reference time. -/// -/// The resolved hindcast date and time (if present) are logged for diagnostic and -/// traceability purposes. -/// -/// @tparam MarsDict_t -/// Type of the MARS dictionary, expected to contain the key `hdate`, -/// `htime` instead is optional and defaulted to 0 when missing. -/// -/// @tparam ParDict_t -/// Type of the parameter dictionary (unused by this deduction). -/// -/// @tparam OptDict_t -/// Type of the options dictionary (unused by this deduction). -/// -/// @param[in] mars -/// MARS dictionary from which the hindcast date and time are retrieved. -/// -/// @param[in] par -/// Parameter dictionary (unused). -/// -/// @param[in] opt -/// Options dictionary (unused). -/// -/// @return -/// The hindcast reference date and time resolved from the MARS dictionary, -/// returned as an `eckit::DateTime` object. -/// -/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException -/// If: -/// - either `hdate` is missing from the MARS dictionary, -/// - the associated values cannot be converted to `long`, -/// - the integer values do not represent a valid calendar date or time, -/// - any unexpected error occurs during dictionary access or conversion. -/// -/// @note -/// This deduction assumes standard MARS integer encodings for hindcast -/// date (`YYYYMMDD`) and time (`HHMMSS`) if present. Validation and normalization -/// are expected to be handled by the underlying conversion utilities. -/// -/// @note -/// A future enhancement may retrieve hindcast date and time as strings -/// and rely on higher-level Metkit parsing utilities for improved -/// normalization and validation. -/// -/// @note -/// This function follows a fail-fast strategy and uses nested exception -/// propagation to preserve full error provenance across API boundaries. -/// -template -eckit::DateTime resolve_HindcastDateTime_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { - - using metkit::mars2grib::utils::dict_traits::get_opt; - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; - - try { - - // TODO MIVAL: get as string and parse/normalize with metkit utilities - - // Get the mars.date and mars.time - long marsDate = get_or_throw(mars, "hdate"); - long marsTime = get_opt(mars, "htime").value_or(0); - - // Convert to canonical format - eckit::Date date = detail::convert_YYYYMMDD2Date_or_throw(marsDate); - eckit::Time time = detail::convert_hhmmss2Time_or_throw(marsTime); - - // Logging of the resolution - MARS2GRIB_LOG_RESOLVE([&]() { - std::string logMsg = "hindcast[date,time]: deduced from mars dictionary with value: "; - logMsg += std::to_string(marsDate) + "," + std::to_string(marsTime); - return logMsg; - }()); - - return eckit::DateTime(date, time); - } - catch (...) { - - // Rethrow nested exceptions - std::throw_with_nested(Mars2GribDeductionException( - "Unable to get `date` and `time` from Mars dictionary to deduce `dateTime`", Here())); - }; - - // Remove compiler warning - mars2gribUnreachable(); -}; - -} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/numberOfTimeRanges.h b/src/metkit/mars2grib/backend/deductions/numberOfTimeRanges.h deleted file mode 100644 index 0d0b38c76..000000000 --- a/src/metkit/mars2grib/backend/deductions/numberOfTimeRanges.h +++ /dev/null @@ -1,175 +0,0 @@ -/* - * (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 numberOfTimeRanges.h -/// @brief Deduction of the GRIB `numberOfTimeRanges` key. -/// -/// This header defines deduction utilities used by the mars2grib backend -/// to resolve the **number of time ranges** associated with statistical -/// processing. -/// -/// The deduction determines the number of time ranges based on the -/// presence and structure of MARS statistical metadata. -/// -/// In particular: -/// - the MARS key `timespan` is mandatory -/// - the MARS key `stattype` is used to determine the number of -/// statistical blocks when present -/// -/// 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 statisticsEncoding.h -/// -/// Related deductions: -/// - @ref timeSpanInSeconds.h -/// - @ref timeIncrementInSeconds.h -/// -/// @ingroup mars2grib_backend_deductions -/// -#pragma once - -// System includes -#include - -// Details -#include "metkit/mars2grib/backend/deductions/detail/timeUtils.h" -#include "metkit/mars2grib/utils/generalUtils.h" - -// Core deduction includes -#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 number of time ranges for statistical processing. -/// -/// @section Deduction contract -/// - Reads: `mars["timespan"]`, optionally `mars["stattype"]` -/// - Writes: none -/// - Side effects: logging (RESOLVE) -/// - Failure mode: throws -/// -/// This deduction computes the number of time ranges required by GRIB -/// statistical processing templates. -/// -/// Resolution rules: -/// - if `timespan` is missing → failure -/// - if `stattype` is missing → returns `1` -/// - otherwise: -/// - the number of time ranges is computed as -/// `countBlocks(stattype) + 1` -/// -/// @tparam MarsDict_t -/// Type of the MARS dictionary. Must support access to `timespan` -/// and optionally `stattype`. -/// -/// @tparam ParDict_t -/// Type of the parameter dictionary (unused by this deduction). -/// -/// @param[in] mars -/// MARS dictionary providing statistical metadata. -/// -/// @param[in] par -/// Parameter dictionary (unused). -/// -/// @return -/// The number of time ranges. -/// -/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException -/// If `timespan` is missing or if any unexpected error occurs during -/// deduction. -/// -/// @note -/// This deduction is deterministic and does not rely on pre-existing -/// GRIB header state. -/// -template -long numberOfTimeRanges(const MarsDict_t& mars, const ParDict_t& par) { - - using metkit::mars2grib::backend::deductions::detail::countBlocks; - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; - using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; - - try { - - // Get the mars.levelist - bool hasTimespan = has(mars, "timespan"); - bool hasStatType = has(mars, "stattype"); - - // Error if timespan is missing - if (!hasTimespan) { - throw Mars2GribDeductionException("`timespan` is required to compute number of time ranges", Here()); - } - - // Handle trivial case - if (!hasStatType) { - - // Retrieve number Of Timeranges - long numberOfTimeRanges = 1; - - // Emit RESOLVE log entry - MARS2GRIB_LOG_RESOLVE([&]() { - std::string logMsg = "`numberOfTimeRanges` resolved from input dictionaries: value='"; - logMsg += std::to_string(numberOfTimeRanges) + "'"; - return logMsg; - }()); - - return numberOfTimeRanges; - } - - if (hasStatType) { - - // Retrieve MARS stattype - std::string statTypeVal = get_or_throw(mars, "stattype"); - - // Count number of blocks in stattype - long numberOfBlocks = static_cast(countBlocks(statTypeVal)); - - // Compute number of time ranges - long numberOfTimeRanges = numberOfBlocks + 1; - - // Emit RESOLVE log entry - MARS2GRIB_LOG_RESOLVE([&]() { - std::string logMsg = "`numberOfTimeRanges` resolved from input dictionaries: value='"; - logMsg += std::to_string(numberOfTimeRanges) + "'"; - return logMsg; - }()); - - // Number of time ranges = number of blocks + 1 - return numberOfTimeRanges; - } - - // Remove compiler warning - mars2gribUnreachable(); - } - catch (...) { - - // Rethrow nested exceptions - std::throw_with_nested( - Mars2GribDeductionException("Failed to resolve `numberOfTimeRanges` from input dictionaries", Here())); - }; - - // Remove compiler warning - mars2gribUnreachable(); -}; - -} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/productTime.h b/src/metkit/mars2grib/backend/deductions/productTime.h new file mode 100644 index 000000000..25348c559 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/productTime.h @@ -0,0 +1,387 @@ +/* + * (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 productTime.h +/// @brief Public deduction header for `ProductTime`. +/// +/// Exposes `resolve_ProductTime_or_throw`, the canonical entry point that +/// produces a `ProductTime` from MARS / par / opt input dictionaries. +/// +/// All temporal consumers (`referenceTime`, `pointInTime`, `statistics`) +/// MUST obtain their temporal data exclusively from a `ProductTime` +/// produced by this function. They MUST NOT reinterpret raw MARS keys +/// (`date`, `time`, `hdate`, `htime`, `fcyear`, `fcmonth`, `step`, +/// `timespan`, `stattype`) independently. +/// +/// The implementation detail (types, factory, helpers) lives in +/// `deductions/detail/ProductTime.h` (UpperCamelCase initial per §20.1 +/// because the file is type-primary). The shared `stattype` parser lives +/// in `deductions/detail/StatType.h` (§22). The shared `StatisticalWindow` +/// type lives in `deductions/detail/StatisticalWindow.h` (§21). +/// +/// See `deductions/timeProducts.md` for the full normative specification. +/// +/// @section References +/// Concept consumers: +/// - referenceTimeEncoding.h +/// - pointInTimeEncoding.h +/// - statisticsEncoding.h +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +// System includes +#include +#include +#include +#include +#include + +// eckit +#include "eckit/exception/Exceptions.h" +#include "eckit/types/DateTime.h" + +// Project utilities (must precede timeIncrementInSeconds.h, which uses +// dict_traits but does not include the corresponding header itself). +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +// Detail +#include "metkit/mars2grib/backend/deductions/detail/ProductTime.h" +#include "metkit/mars2grib/backend/deductions/detail/StatType.h" + +// Sibling deductions +#include "metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the canonical `ProductTime` for one MARS product. +/// +/// @section Deduction contract +/// - Reads (MARS): `date`, `time`, `hdate`, `htime`, `fcyear`, `fcmonth`, +/// `step`, `timespan`, `stattype` +/// - Reads (par): `timeIncrementInSeconds` (via `timeIncrementInSeconds_opt`) +/// - Reads (opt): none (signature-only, reserved) +/// - Writes: none +/// - Side effects: one `MARS2GRIB_LOG_RESOLVE` line on success +/// - Failure mode: throws `Mars2GribDeductionException` (nested-with) +/// +/// Resolution proceeds in two stages: +/// 1. **Resolver** (this function): reads the input dictionaries and +/// normalizes them into a `ProductTimeInput`. +/// 2. **Factory** (`detail::makeProductTime_or_throw`): validates all +/// invariants and returns the immutable `ProductTime`. +/// +/// On success, exactly one composite RESOLVE log line is emitted listing +/// every resolved field in a stable, greppable form (§12). +/// +/// @tparam MarsDict_t MARS dictionary type. +/// @tparam ParDict_t Parameter dictionary type. +/// @tparam OptDict_t Options dictionary type (currently unused). +/// +/// @param[in] mars MARS dictionary providing temporal keys. +/// @param[in] par Parameter dictionary providing `timeIncrementInSeconds`. +/// @param[in] opt Options dictionary (signature-only). +/// +/// @return The resolved, immutable `ProductTime`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// on any rule violation (§10.x), with the original cause attached +/// via `std::throw_with_nested`. +/// +/// @note This function is thread-safe given thread-safe dictionary reads +/// (§14). +/// +template +detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, + const ParDict_t& par, + const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_opt; + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + // Suppress "unused parameter" warning while preserving the documented + // signature (§8: opt is reserved). + (void)opt; + + try { + + // ========================================================= + // §7.1 / §7.2: simulationDateTime from (date, time) or + // defaulted from (fcyear, fcmonth) + // ========================================================= + + const bool hasDate = has(mars, "date"); + const bool hasTime = has(mars, "time"); + const bool hasFcYear = has(mars, "fcyear"); + const bool hasFcMonth = has(mars, "fcmonth"); + + eckit::DateTime simulationDateTime; + if (hasDate && hasTime) { + const long marsDate = get_or_throw(mars, "date"); + const long marsTime = get_or_throw(mars, "time"); + simulationDateTime = eckit::DateTime( + detail::convert_YYYYMMDD2Date_or_throw(marsDate), + detail::convert_hhmmss2Time_or_throw(marsTime)); + } + else if (!hasDate && !hasTime && hasFcYear && hasFcMonth) { + // R2 default: simulationDateTime := DateTime(fcyear, fcmonth, 1, 00:00:00). + const long fcYear = get_or_throw(mars, "fcyear"); + const long fcMonth = get_or_throw(mars, "fcmonth"); + try { + simulationDateTime = eckit::DateTime( + eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); + } + catch (const eckit::Exception& e) { + throw Mars2GribDeductionException( + "Invalid (fcyear, fcmonth) for default simulationDateTime: " + + std::string(e.what()), + Here()); + } + } + else { + // §10.1: date or time missing without an applicable fallback. + throw Mars2GribDeductionException( + std::string("ProductTime invariant violated [§10.1]: ") + + "missing 'date'/'time' without (fcyear,fcmonth) fallback. " + + "has(date)=" + (hasDate ? "true" : "false") + + ", has(time)=" + (hasTime ? "true" : "false") + + ", has(fcyear)=" + (hasFcYear ? "true" : "false") + + ", has(fcmonth)=" + (hasFcMonth ? "true" : "false"), + Here()); + } + + // ========================================================= + // §7.3: simulatedDateTime from (hdate, htime) + // ========================================================= + + const bool hasHdate = has(mars, "hdate"); + const bool hasHtime = has(mars, "htime"); + + std::optional simulatedDateTime; + if (!hasHdate && !hasHtime) { + // simulatedDateTime defaults to simulationDateTime in the factory. + } + else if (hasHdate && !hasHtime) { + const long marsHdate = get_or_throw(mars, "hdate"); + simulatedDateTime = eckit::DateTime( + detail::convert_YYYYMMDD2Date_or_throw(marsHdate), + eckit::Time(0, 0, 0)); + } + else if (hasHdate && hasHtime) { + const long marsHdate = get_or_throw(mars, "hdate"); + const long marsHtime = get_or_throw(mars, "htime"); + simulatedDateTime = eckit::DateTime( + detail::convert_YYYYMMDD2Date_or_throw(marsHdate), + detail::convert_hhmmss2Time_or_throw(marsHtime)); + } + else { + // §10.2: htime present without hdate. + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.2]: 'htime' present without 'hdate'", + Here()); + } + + // ========================================================= + // §7.4: referenceDateTime from (fcyear, fcmonth) + // ========================================================= + + std::optional referenceDateTime; + if (!hasFcYear && !hasFcMonth) { + // referenceDateTime defaults to simulatedDateTime in the factory. + } + else if (hasFcYear && hasFcMonth) { + const long fcYear = get_or_throw(mars, "fcyear"); + const long fcMonth = get_or_throw(mars, "fcmonth"); + try { + referenceDateTime = eckit::DateTime( + eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); + } + catch (const eckit::Exception& e) { + throw Mars2GribDeductionException( + "Invalid (fcyear, fcmonth) for referenceDateTime: " + + std::string(e.what()), + Here()); + } + } + else { + // §10.3: exactly one of fcyear / fcmonth present. + throw Mars2GribDeductionException( + std::string("ProductTime invariant violated [§10.3]: ") + + "exactly one of 'fcyear'/'fcmonth' present. " + + "has(fcyear)=" + (hasFcYear ? "true" : "false") + + ", has(fcmonth)=" + (hasFcMonth ? "true" : "false"), + Here()); + } + + // ========================================================= + // §7.5: stepInSeconds from step (default 0 if missing) + // ========================================================= + + long stepInSeconds = 0; + if (has(mars, "step")) { + // MARS may expose 'step' either as a long (legacy hours) or as a + // string (suffixed). Prefer a string read so the parser handles + // both bare-numeric and unit-suffixed forms uniformly. + std::optional stepStr = get_opt(mars, "step"); + if (stepStr.has_value()) { + stepInSeconds = detail::toSeconds_or_throw(stepStr.value()); + } + else { + // Fallback: numeric step interpreted as hours (§7.5 bare-numeric rule). + const long marsStep = get_or_throw(mars, "step"); + stepInSeconds = marsStep * 3600L; + } + } + + // ========================================================= + // §7.6: timespan + // ========================================================= + + detail::TimespanKind timespanKind = detail::TimespanKind::Missing; + detail::StatisticalWindow timespan{}; + + if (has(mars, "timespan")) { + std::optional tsStr = get_opt(mars, "timespan"); + if (tsStr.has_value()) { + if (tsStr.value() == "none") { + timespanKind = detail::TimespanKind::None; + } + else { + timespanKind = detail::TimespanKind::Duration; + timespan.unit = tables::TimeUnit::Second; + timespan.count = detail::toSeconds_or_throw(tsStr.value()); + } + } + else { + // Numeric-only timespan: interpret as hours per §7.6 (→ §7.5). + const long tsNum = get_or_throw(mars, "timespan"); + timespanKind = detail::TimespanKind::Duration; + timespan.unit = tables::TimeUnit::Second; + timespan.count = tsNum * 3600L; + } + } + + // ========================================================= + // §7.7: stattype → stattypeWindows (period part only) + // + // Parser is shared with resolve_TypeOfStatisticalProcessing_or_throw + // (§22). This deduction consumes only the `timeWindow` field of + // each parsed block; the `typeOfStatisticalProcessing` field is + // consumed by the sibling deduction. + // ========================================================= + + std::array stattypeWindows{}; + std::size_t stattypeWindowCount = 0; + + if (has(mars, "stattype")) { + const std::string statTypeVal = get_or_throw(mars, "stattype"); + const std::vector blocks = + detail::parse_StatType_or_throw(statTypeVal); + + if (blocks.size() > detail::maxStatisticalWindows) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.15]: stattype yields " + + std::to_string(blocks.size()) + " block(s) > maxStatisticalWindows (" + + std::to_string(detail::maxStatisticalWindows) + ")", + Here()); + } + + for (std::size_t i = 0; i < blocks.size(); ++i) { + stattypeWindows[i] = blocks[i].timeWindow; + } + stattypeWindowCount = blocks.size(); + } + + // ========================================================= + // §7.8: timeIncrementInSeconds (par) + // ========================================================= + + const std::optional tInc = timeIncrementInSeconds_opt(mars, par); + + // ========================================================= + // Assemble the input bundle and call the factory. + // ========================================================= + + detail::ProductTimeInput input; + input.simulationDateTime = simulationDateTime; + input.simulatedDateTime = simulatedDateTime; + input.referenceDateTime = referenceDateTime; + input.stepInSeconds = stepInSeconds; + input.timespanKind = timespanKind; + input.timespan = timespan; + input.stattypeWindows = stattypeWindows; + input.stattypeWindowCount = stattypeWindowCount; + input.timeIncrementInSeconds = tInc; + + detail::ProductTime pt = detail::makeProductTime_or_throw(input); + + // ========================================================= + // §12: composite RESOLVE log line (success path only) + // ========================================================= + + MARS2GRIB_LOG_RESOLVE([&]() { + std::string msg = "`ProductTime` resolved from input dictionaries: "; + msg += "simulationDateTime='" + detail::fmt(pt.simulationDateTime) + "'"; + msg += " simulatedDateTime='" + detail::fmt(pt.simulatedDateTime) + "'"; + msg += " referenceDateTime='" + detail::fmt(pt.referenceDateTime) + "'"; + msg += " windowStart='" + detail::fmt(pt.windowStart) + "'"; + msg += " windowEnd='" + detail::fmt(pt.windowEnd) + "'"; + msg += " statisticalWindowCount='" + + std::to_string(pt.statisticalWindowCount) + "'"; + msg += " statisticalWindows=" + + detail::fmt(pt.statisticalWindows, pt.statisticalWindowCount); + msg += " timeIncrementInSeconds='" + + (pt.timeIncrementInSeconds.has_value() + ? std::to_string(pt.timeIncrementInSeconds.value()) + : std::string("missing")) + + "'"; + return msg; + }()); + + return pt; + } + catch (...) { + + // §11: nested rethrow with context. + std::throw_with_nested( + Mars2GribDeductionException("Unable to resolve ProductTime", Here())); + } + + // Remove compiler warning + mars2gribUnreachable(); +} + +/// +/// @brief Number of statistical time ranges encoded in a `ProductTime`. +/// +/// This is a thin accessor: it returns `pt.statisticalWindowCount` cast to +/// `long`, which is the GRIB-side `numberOfTimeRanges` value (PDT 4.8 / 4.11). +/// +/// Provided as a free function so that consumers do not need to access +/// the `ProductTime` field directly nor reach into `std::size_t` arithmetic. +/// +/// @param[in] pt Resolved `ProductTime`. +/// @return Number of statistical loops (`0` for instant products). +/// +inline long numberOfTimeRanges(const detail::ProductTime& pt) { + return static_cast(pt.statisticalWindowCount); +} + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/referenceDateTime.h b/src/metkit/mars2grib/backend/deductions/referenceDateTime.h deleted file mode 100644 index 4069b1646..000000000 --- a/src/metkit/mars2grib/backend/deductions/referenceDateTime.h +++ /dev/null @@ -1,121 +0,0 @@ -/* - * (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 "eckit/log/Log.h" -#include "eckit/types/Date.h" -#include "eckit/types/DateTime.h" -#include "eckit/types/Time.h" -#include "metkit/mars2grib/utils/generalUtils.h" - -#include "metkit/mars2grib/backend/deductions/detail/timeUtils.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 reference date and time from the MARS dictionary. -/// -/// This deduction constructs an `eckit::DateTime` object from the MARS -/// dictionary entries `date` and `time`. Both values are treated as -/// mandatory and are expected to be provided in the conventional MARS -/// integer formats: -/// -/// - `date`: calendar date encoded as `YYYYMMDD` -/// - `time`: clock time encoded as `HHMMSS` -/// -/// The raw integer values are first converted into canonical `eckit::Date` -/// and `eckit::Time` objects using dedicated conversion utilities, and are -/// then combined into a single `eckit::DateTime` instance. -/// -/// The resolved date and time are logged for diagnostic and traceability -/// purposes. -/// -/// @tparam MarsDict_t -/// Type of the MARS dictionary, expected to contain the keys `date` and `time`. -/// -/// @tparam ParDict_t -/// Type of the parameter dictionary (unused by this deduction). -/// -/// @param[in] mars -/// MARS dictionary from which the reference date and time are retrieved. -/// -/// @param[in] par -/// Parameter dictionary (unused). -/// -/// @return -/// The reference date and time resolved from the MARS dictionary, returned -/// as an `eckit::DateTime` object. -/// -/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException -/// If: -/// - either `date` or `time` is missing from the MARS dictionary, -/// - the associated values cannot be converted to `long`, -/// - the integer values do not represent a valid calendar date or time, -/// - any unexpected error occurs during conversion or dictionary access. -/// -/// @note -/// The conversion assumes standard MARS integer encodings for date -/// (`YYYYMMDD`) and time (`HHMMSS`). Validation and normalization are -/// delegated to the underlying conversion utilities. -/// -/// @note -/// A future enhancement may retrieve date and time as strings and rely -/// on higher-level Metkit parsing utilities for normalization and -/// validation. -/// -/// @note -/// This deduction follows a fail-fast strategy and uses nested exception -/// propagation to preserve full error provenance across API boundaries. -/// -template -eckit::DateTime resolve_ReferenceDateTime_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 { - - // TODO MIVAL: get as string and parse/normalize with metkit utilities - - // Get the mars.date and mars.time - long marsDate = get_or_throw(mars, "date"); - long marsTime = get_or_throw(mars, "time"); - - // Convert to canonical format - eckit::Date date = detail::convert_YYYYMMDD2Date_or_throw(marsDate); - eckit::Time time = detail::convert_hhmmss2Time_or_throw(marsTime); - - // Logging of the resolution - MARS2GRIB_LOG_RESOLVE([&]() { - std::string logMsg = "date,time: deduced from mars dictionary with value: "; - logMsg += std::to_string(marsDate) + "," + std::to_string(marsTime); - return logMsg; - }()); - - return eckit::DateTime(date, time); - } - catch (...) { - - // Rethrow nested exceptions - std::throw_with_nested(Mars2GribDeductionException( - "Unable to get `date` and `time` from Mars dictionary to deduce `dateTime`", Here())); - }; - - // Remove compiler warning - mars2gribUnreachable(); -}; - -} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/statisticsDescriptor.h b/src/metkit/mars2grib/backend/deductions/statisticsDescriptor.h deleted file mode 100644 index a8de45f61..000000000 --- a/src/metkit/mars2grib/backend/deductions/statisticsDescriptor.h +++ /dev/null @@ -1,183 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - - -#include "eckit/exception/Exceptions.h" -#include "eckit/log/Log.h" -#include "metkit/mars2grib/utils/generalUtils.h" - -// Deductions -#include "metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h" -#include "metkit/mars2grib/backend/deductions/numberOfTimeRanges.h" -#include "metkit/mars2grib/backend/deductions/timeSpanInSeconds.h" - -// Utils -#include "metkit/mars2grib/backend/deductions/detail/timeUtils.h" -#include "metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h" - -// Exceptions -#include "metkit/config/LibMetkit.h" -#include "metkit/mars2grib/utils/mars2gribExceptions.h" - - -namespace metkit::mars2grib::backend::deductions { - -struct StatisticalProcessing { - - long numberOfTimeRanges = 0; - - std::vector typeOfStatisticalProcessing; - std::vector typeOfTimeIncrement; - std::vector indicatorOfUnitForTimeRange; - std::vector lengthOfTimeRange; - std::vector indicatorOfUnitForTimeIncrement; - std::vector lengthOfTimeIncrement; -}; - -template -inline StatisticalProcessing getTimeDescriptorFromMars_orThrow( - const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt, - long outerStatOp // typeOfStatisticalProcessing for inner loop -) { - - using metkit::mars2grib::backend::deductions::detail::parseStatType_or_throw; - using metkit::mars2grib::backend::deductions::detail::Period; - using metkit::mars2grib::backend::deductions::detail::previousMonthLengthHours; - using metkit::mars2grib::backend::deductions::detail::StatOp; - using metkit::mars2grib::backend::deductions::detail::StatTypeBlock; - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; - - try { - StatisticalProcessing out{}; - - // --------------------------------------------------------------------- - // Number of loops - // --------------------------------------------------------------------- - long numberOfTimeRangesVal = numberOfTimeRanges(mars, par); - - // Validate number of time ranges - if (numberOfTimeRangesVal < 1 || numberOfTimeRangesVal > 3) { - throw Mars2GribDeductionException("Unexpected number of time loops", Here()); - } - - // Initialize output structure - out.numberOfTimeRanges = numberOfTimeRangesVal; - out.typeOfStatisticalProcessing.reserve(numberOfTimeRangesVal); - out.typeOfTimeIncrement.reserve(numberOfTimeRangesVal); - out.indicatorOfUnitForTimeRange.reserve(numberOfTimeRangesVal); - out.lengthOfTimeRange.reserve(numberOfTimeRangesVal); - out.indicatorOfUnitForTimeIncrement.reserve(numberOfTimeRangesVal); - out.lengthOfTimeIncrement.reserve(numberOfTimeRangesVal); - - // --------------------------------------------------------------------- - // Parse stattype blocks (outer loops) - // --------------------------------------------------------------------- - - std::vector blocks; - if (numberOfTimeRangesVal > 1) { - // Get the stattype from mars - std::string statTypeVal = get_or_throw(mars, "stattype"); - - // Parse stattype - blocks = parseStatType_or_throw(statTypeVal); - } - - // --------------------------------------------------------------------- - // End date (needed for monthly length) - // --------------------------------------------------------------------- - eckit::DateTime forecastTime = resolve_ForecastTimeInSeconds_or_throw(mars, par, opt); - - long endYear = forecastTime.date().year(); - long endMonth = forecastTime.date().month(); - - const long timeStepSeconds = timeIncrementInSeconds_or_throw(mars, par); - - const long timeSpanInSeconds = resolve_TimeSpanInSeconds_or_throw(mars, par, opt); - - if (timeSpanInSeconds % 3600 != 0) { - throw Mars2GribDeductionException("`timespan` must be multiple of 3600 seconds", Here()); - } - const long timeSpanHours = timeSpanInSeconds / 3600; - - - // --------------------------------------------------------------------- - // Fill SoA (exactly like Fortran) - // --------------------------------------------------------------------- - - for (long i = 0; i < numberOfTimeRangesVal; ++i) { - - const bool isInnerLoop = (i == numberOfTimeRangesVal - 1); - - // Common fields (hard-coded in Fortran) - out.typeOfTimeIncrement[i] = 2; // multIO fixed value - out.indicatorOfUnitForTimeRange[i] = 1; // hours - out.indicatorOfUnitForTimeIncrement[i] = 13; // seconds - - if (isInnerLoop) { - // ------------------------------------------------------------- - // Inner loop (timespan) - // ------------------------------------------------------------- - out.typeOfStatisticalProcessing[i] = outerStatOp; - out.lengthOfTimeRange[i] = timeSpanHours; - out.lengthOfTimeIncrement[i] = timeStepSeconds; - } - else { - // ------------------------------------------------------------- - // Outer loop(s) from stattype - // ------------------------------------------------------------- - const auto& blk = blocks[i]; - - out.typeOfStatisticalProcessing[i] = static_cast(blk.op); - - // lengthOfTimeRange - if (blk.period == Period::Daily) { - out.lengthOfTimeRange[i] = 24; - } - else if (blk.period == Period::Monthly) { - out.lengthOfTimeRange[i] = previousMonthLengthHours(endYear, endMonth); - } - else { - throw std::runtime_error("Unsupported period"); - } - - // lengthOfTimeIncrement - if (i == numberOfTimeRangesVal - 2) { - // Next loop is inner → use timespan - out.lengthOfTimeIncrement[i] = timeSpanHours * 3600; - } - else { - // Next loop is another stattype block - const auto& nextBlk = blocks[i + 1]; - - if (nextBlk.period == Period::Daily) { - out.lengthOfTimeIncrement[i] = 24 * 3600; - } - else if (nextBlk.period == Period::Monthly) { - out.lengthOfTimeIncrement[i] = previousMonthLengthHours(endYear, endMonth) * 3600; - } - else { - throw Mars2GribDeductionException("Unsupported next period", Here()); - } - } - } - - return out; - } - } - catch (...) { - // Rethrow nested exceptions - std::throw_with_nested( - Mars2GribDeductionException("Unable to compute statistics descriptor from Mars dictionary", Here())); - }; - - // Remove compiler warning - mars2gribUnreachable(); -} - -} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/timeProducts.md b/src/metkit/mars2grib/backend/deductions/timeProducts.md new file mode 100644 index 000000000..9bf6cf7dc --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/timeProducts.md @@ -0,0 +1,1545 @@ +# ProductTime — Specification + +> Status: **normative**. This document supersedes all prior design notes for time +> handling in `mars2grib`. It contains no open questions and no historical +> commentary; every rule is prescriptive (`MUST` / `MUST NOT` / `MAY`). + +--- + +## 1. Purpose and scope + +`ProductTime` is the **single canonical representation** of all temporal +information associated with a MARS product within `mars2grib`. + +The temporal consumers `referenceTime`, `pointInTime`, and `statistics` MUST +obtain their temporal data exclusively from a `ProductTime` instance produced by +`resolve_ProductTime_or_throw`. They MUST NOT reinterpret raw MARS keys +(`date`, `time`, `hdate`, `htime`, `fcyear`, `fcmonth`, `step`, `timespan`, +`stattype`) independently. + +Out of scope for this document: +- The MARS-side parsing of request strings. +- The `paramId` → statistical-processing-type lookup table. +- The GRIB-side encoding of fields outside the temporal domain. +- The migration of consumer concept files (tracked separately). + +--- + +## 2. Architecture + +``` +MARS par opt + \ | / + \ | / + v v v + resolve_ProductTime_or_throw (deductions/productTime.h) + | + | builds ProductTimeInput, calls factory + v + makeProductTime_or_throw (deductions/detail/productTime.h) + | + | validates invariants, returns immutable ProductTime + v + +----------------+ + | ProductTime | + +----------------+ + | + +--> referenceTime consumer + +--> pointInTime consumer + +--> statistics consumer + | + +--> resolve_TypeOfStatisticalProcessing_or_throw + | (deductions/typeOfStatisticalProcessing.h, §23; + | self-contained, depends on detail/StatType.h, §22) + +--> computeStatisticDescription(productTime, types) + (deductions/statisticsDescriptor.h) +``` + +`computeStatisticDescription` is a **pure function**: it depends only on its +arguments. No MARS / par / opt access at descriptor-build time. + +--- + +## 3. `StatisticalWindow` model + +`ProductTime` represents each statistical window as a `(unit, count)` pair, +reusing the existing GRIB-aligned `tables::TimeUnit` enum +(`backend/tables/timeUnits.h`, GRIB2 Code Table 4.4). No parallel "kind" enum +is introduced. + +The `StatisticalWindow` type is defined in the shared header +`deductions/detail/StatisticalWindow.h` (§21) and is consumed by +`detail/ProductTime.h`, `detail/StatType.h` (the shared `stattype` parser, +§22), and `typeOfStatisticalProcessing.h` (§23). + +```cpp +#include "metkit/mars2grib/backend/tables/timeUnits.h" + +struct StatisticalWindow { + tables::TimeUnit unit{tables::TimeUnit::Second}; + long count{0}; +}; +``` + +### 3.1 Allowed `TimeUnit` subset + +Although `tables::TimeUnit` enumerates the full GRIB2 code table, the only +values legal inside a `ProductTime::statisticalWindows` entry are: + +```text +tables::TimeUnit::Second +tables::TimeUnit::Day +tables::TimeUnit::Month +``` + +The factory `makeProductTime_or_throw` MUST reject any other value (including +but not limited to `Minute`, `Hour`, `Hours3`, `Hours6`, `Hours12`, `Year`, +`Decade`, `Normal`, `Century`, `Missing`) as a hard error (§10.18). + +This restriction reflects the locked production paths: +- `timespan` is always pre-converted to fixed seconds → produces + `tables::TimeUnit::Second`; +- the locked `stattype` grammar (§7.7) emits only `mo` and `da` periods → + produce `tables::TimeUnit::Month` and `tables::TimeUnit::Day` respectively. + +Any future widening of this allow-list is an explicit spec amendment, not an +implementation freedom. + +### 3.2 Invariants + +- For any `StatisticalWindow` stored inside a `ProductTime` (i.e. inside + `statisticalWindows[0 .. statisticalWindowCount)`), `count > 0` MUST hold. +- The factory rejects any input that contains a `StatisticalWindow` with + `count <= 0` (hard error §10.11). +- The factory rejects any input that contains a `StatisticalWindow` with + `unit` outside the §3.1 allow-list (hard error §10.18). +- The default-constructed `StatisticalWindow{}` has `count == 0`; it is legal + at the C++ language level but illegal as a `ProductTime` member; it serves + only as a temporary. + +### 3.3 Calendar-vs-fixed classification + +For the purposes of windowing semantics (§4) and calendar subtraction (§9.6), +the units in §3.1 are classified as: + +| `unit` | Classification | +|------------------------------|----------------| +| `tables::TimeUnit::Second` | fixed-duration | +| `tables::TimeUnit::Day` | calendar-aligned | +| `tables::TimeUnit::Month` | calendar-aligned | + +The classification is implicit in the unit value; no separate field encodes +it. + +### 3.4 Examples + +```text +1h -> StatisticalWindow{tables::TimeUnit::Second, 3600} +24h -> StatisticalWindow{tables::TimeUnit::Second, 86400} +1 calendar day -> StatisticalWindow{tables::TimeUnit::Day, 1} +1 calendar month -> StatisticalWindow{tables::TimeUnit::Month, 1} +``` + +`tables::TimeUnit::Year` is intentionally excluded from the allow-list: it is +unreachable through the locked `stattype` grammar (§7.7) and not produced by +any other path. + +--- + +## 4. Calendar-window semantics and strict alignment + +### 4.1 Half-open interval convention + +A product temporal support is the half-open interval `[windowStart, windowEnd)`, +**when** `windowStart < windowEnd`. The degenerate case `windowStart == +windowEnd` denotes the **point in time** at `windowEnd` and is reserved for +instant products (§9.1). + +### 4.2 Calendar day + +A `StatisticalWindow{tables::TimeUnit::Day, N}` window is a window of N +**named** calendar days, internally represented as + +```text +[YYYY-MM-DD 00:00:00, YYYY-MM-(DD+N) 00:00:00) +``` + +(with appropriate calendar carry into the next month / year). + +Alignment rule for a `tables::TimeUnit::Day` outermost window: + +```text +windowEnd MUST be at hh=00, mm=00, ss=00. +``` + +### 4.3 Calendar month + +A `StatisticalWindow{tables::TimeUnit::Month, N}` window is a window of N +**named** calendar months. Its length depends on the concrete months +involved. + +Alignment rule for a `tables::TimeUnit::Month` outermost window: + +```text +windowEnd MUST be on day=1 at hh=00, mm=00, ss=00. +``` + +### 4.4 Strict alignment policy + +When the outermost window in `statisticalWindows` is calendar-aligned (i.e. +its `unit` is `tables::TimeUnit::Day` or `tables::TimeUnit::Month`, per +§3.3), the factory MUST reject any input whose `windowEnd` violates the +corresponding alignment rule (hard errors §10.9, §10.10). + +This is a deliberate behavioural change relative to legacy `timeUtils.h`-based +code, which silently tolerated misaligned ends. There is no opt-in flag and no +warn-only mode. Misaligned operational requests fail loudly and must be fixed +upstream. + +--- + +## 5. `ProductTime` struct + +```cpp +inline constexpr std::size_t maxStatisticalWindows = 3; + +struct ProductTime { + const eckit::DateTime simulationDateTime; + const eckit::DateTime simulatedDateTime; + const eckit::DateTime referenceDateTime; + + // Internal convention: [windowStart, windowEnd) when windowStart < windowEnd. + // For instant products: windowStart == windowEnd (the point in time at windowEnd). + const eckit::DateTime windowStart; + const eckit::DateTime windowEnd; + + // Valid entries: statisticalWindows[0 .. statisticalWindowCount). + // Ordering: outermost -> innermost. + const std::array statisticalWindows; + const std::size_t statisticalWindowCount; + + // Sampling increment of the innermost statistical loop. + // std::nullopt for instant products (§9.1). + // Optionally absent for single-window statistical products (AIFS path, §9.4). + // Required and > 0 for multi-window statistical products. + const std::optional timeIncrementInSeconds; +}; +``` + +All members are `const`. The struct is therefore immutable after construction, +copyable, and not assignable. `maxStatisticalWindows = 3` matches the maximum +producible by the locked `stattype` grammar (§7.6): one `mo` block + one `da` +block + one inner `timespan` window. + +### 5.1 Tri-equivalent instant invariant + +The factory MUST enforce, as a single equivalence: + +```text +windowStart == windowEnd + <=> +statisticalWindowCount == 0 + <=> +timeIncrementInSeconds == std::nullopt +``` + +i.e. all three conditions hold together or none does. Any other state is a +hard error (§10.5). + +### 5.2 Window-end ordering invariant + +```text +windowStart <= windowEnd (always) +``` + +Strictly `<` for statistical products; equality reserved for instants. + +### 5.3 Reference-vs-simulated invariant + +```text +referenceDateTime >= simulatedDateTime (always) +``` + +Violation is a hard error (§10.4). The corner case where reference < simulated +is officially out-of-spec MARS semantics and must be fixed upstream. + +--- + +## 6. `ProductTimeInput` + +The factory does not consume raw MARS keys. The resolver (§8) normalizes them +into `ProductTimeInput` first. + +```cpp +enum class TimespanKind { + Missing, // MARS keyword absent + Duration, // MARS keyword has a duration value + None // MARS keyword equals literal "none" (fakeDoubleLoop, §9.4) +}; + +struct ProductTimeInput { + eckit::DateTime simulationDateTime; + + std::optional simulatedDateTime; // from hdate/htime + std::optional referenceDateTime; // from fcyear/fcmonth + + // Offset from referenceDateTime to ProductTime::windowEnd. + long stepInSeconds{0}; + + TimespanKind timespanKind{TimespanKind::Missing}; + + // Valid only when timespanKind == TimespanKind::Duration. + StatisticalWindow timespan{}; + + // Temporal windows decoded from stattype (period part only), + // ordered outermost -> innermost. + std::array stattypeWindows{}; + std::size_t stattypeWindowCount{0}; + + // Source: deductions::timeIncrementInSeconds_opt(mars, par) — i.e. par["timeIncrementInSeconds"]. + // Absent for instants and for the AIFS single-window path (§9.4). + std::optional timeIncrementInSeconds; +}; +``` + +Factory: + +```cpp +ProductTime makeProductTime_or_throw(const ProductTimeInput& input); +``` + +The factory: +1. Resolves defaults (simulated, reference) per §7. +2. Computes `windowEnd = referenceDateTime + stepInSeconds`. +3. Assembles `statisticalWindows` per the case table in §9. +4. Computes `windowStart` per §9. +5. Validates all invariants (§5.1, §5.2, §5.3) and all hard errors (§10). +6. Returns the immutable `ProductTime`. + +--- + +## 7. MARS keywords and field-by-field resolution + +### 7.1 Keyword reference + +| Key | Required | Source | +|------------|----------------------|----------------------------------------------| +| `date` | conditional (§7.2) | MARS | +| `time` | conditional (§7.2) | MARS | +| `hdate` | no | MARS | +| `htime` | no | MARS | +| `fcyear` | no | MARS | +| `fcmonth` | no | MARS | +| `step` | no, default `0` (§7.5) | MARS | +| `timespan` | no | MARS | +| `stattype` | no | MARS | +| `timeIncrementInSeconds` | no | par (parameter dictionary), via `deductions::timeIncrementInSeconds_opt` | + +### 7.2 `date` / `time` + +Resolution rule for `simulationDateTime`: + +```text +date present and time present: + simulationDateTime := DateTime(date, time) + +date missing and time missing and fcyear present and fcmonth present: + simulationDateTime := DateTime(Date(fcyear, fcmonth, 1), Time(00:00:00)) + (this is the same value §7.4 produces for referenceDateTime; + consequently simulationDateTime == referenceDateTime in this case) + +any other combination -> hard error (§10.1) +``` + +`simulationDateTime` is a label for the simulation; it is not used for `step` +arithmetic. The `hdate`/`htime` rules in §7.3 are unchanged regardless of +whether the R2 default in §7.2 is taken: if `hdate` is present it still +drives `simulatedDateTime`, and the §5.3 invariant +`referenceDateTime >= simulatedDateTime` continues to apply (it may now +constrain `(fcyear, fcmonth, 1)` against `hdate`). + +### 7.3 `hdate` / `htime` + +```text +hdate missing, htime missing -> simulatedDateTime := simulationDateTime +hdate present, htime missing -> simulatedDateTime := DateTime(hdate, 00:00:00) +hdate present, htime present -> simulatedDateTime := DateTime(hdate, htime) +hdate missing, htime present -> hard error (§10.2) +``` + +### 7.4 `fcyear` / `fcmonth` + +Both must be present together or both absent. + +```text +both missing -> referenceDateTime := simulatedDateTime +both present -> referenceDateTime := DateTime(Date(fcyear, fcmonth, 1), Time(00:00:00)) +exactly one present -> hard error (§10.3) +``` + +The day component is `1` (Gregorian-valid). No day-zero variant exists. + +### 7.5 `step` + +`step` is optional with default `0`. + +```text +step missing -> stepInSeconds := 0 +step present -> stepInSeconds := toSeconds_or_throw(step) +``` + +`toSeconds_or_throw` (signature locked from existing `detail/timeUtils.h`) +parses: + +- bare numeric `N` as **N hours**; +- suffixed numeric with `h` (hours), `m` (minutes), `s` (seconds), `d` (days). + +Then: + +```text +windowEnd := referenceDateTime + stepInSeconds +``` + +`step` missing is observationally indistinguishable from `step = 0`; both +produce the same `ProductTime`. The internal type is `long`, not +`std::optional`. + +Whether the resulting `ProductTime` is then a legal instant or a misaligned +calendar window is determined by the case table (§9) and the alignment rule +(§4.4) — no special-case logic for the absent / zero step. + +### 7.6 `timespan` + +Three states (`TimespanKind`): + +- **`Missing`** — keyword absent. +- **`Duration`** — keyword carries a duration. Parsed by `toSeconds_or_throw` + (same rules as `step`, §7.5). The result is wrapped in + `StatisticalWindow{tables::TimeUnit::Second, seconds}`. **Note**: in + production the resolver always converts `timespan` to fixed seconds before + entering the factory; the spec deliberately leaves no path for + calendar-aligned `timespan`. If future needs require it, this section is + the single point that must be amended. +- **`None`** — keyword equals the literal string `"none"`. Valid only in the + fakeDoubleLoop case (§9.4); any other use is a hard error (§10.6, §10.7). + +### 7.7 `stattype` + +Grammar (locked, identical to existing `detail/timeUtils.h::parseStatType_or_throw`): + +```text +stattype := block ('_' block)* +block := period operation +period := 'mo' | 'da' +operation := 'av' | 'mn' | 'mx' | 'sd' +``` + +Block length is exactly 4 characters; separator is exactly one underscore. + +Semantic constraints (also locked from existing parser): + +- at most one `mo` block; +- at most one `da` block; +- if both present, `mo` MUST precede `da` (outermost-to-innermost order). + +The `ProductTime` factory uses the **period prefix only** for window assembly: + +```text +period 'mo' -> StatisticalWindow{tables::TimeUnit::Month, 1} +period 'da' -> StatisticalWindow{tables::TimeUnit::Day, 1} +``` + +The operation suffix is consumed by `resolveTypeOfStatisticalProcessing` (a +sibling deduction, out of scope here). The `stattype` parser MUST be a single +shared helper (currently `detail/timeUtils.h::parseStatType_or_throw`, +relocated to `detail/productTime.h`) used by both deductions, so that the two +never drift. + +### 7.8 `timeIncrementInSeconds` + +Source: `deductions::timeIncrementInSeconds_opt(mars, par)`, which reads +`par["timeIncrementInSeconds"]`. Existing normalization is preserved: + +- absent -> `std::nullopt` +- present, value `0` -> `std::nullopt` (legacy normalization) +- present, value < 0 -> hard error (§10.10) +- present, value > 0 -> the value + +The forwarded `std::optional` becomes `ProductTime::timeIncrementInSeconds` +subject to the case table (§9) and the tri-equivalent invariant (§5.1). + +--- + +## 8. Resolver + +```cpp +template +ProductTime resolve_ProductTime_or_throw( + const MarsDict_t& mars, + const ParDict_t& par, + const OptDict_t& opt); +``` + +- `opt` is accepted for signature consistency with sibling deductions but + reads no keys at present. Reserved for future options. +- The resolver: + 1. Reads MARS keys per §7. + 2. Reads `par["timeIncrementInSeconds"]` via the existing + `timeIncrementInSeconds_opt` helper. + 3. Builds a `ProductTimeInput`. + 4. Calls `makeProductTime_or_throw`. + 5. On success, emits exactly one `MARS2GRIB_LOG_RESOLVE` line (§12). + 6. On failure, rethrows-with-nested per §11. + +`resolve_ProductTime_or_throw` is the canonical name. Any additional spelling +(e.g. `resolveProductTime_or_throw`) is a deprecated alias and SHOULD NOT be +used in new code. + +Header location: `deductions/productTime.h` (public). The factory +`makeProductTime_or_throw` called by this resolver is declared in +`deductions/detail/productTime.h`. Both files share the basename +`productTime.h`; they are distinguished by the `detail/` subdirectory per the +codebase convention for implementation-detail headers (matching the existing +`detail/timeUtils.h` precedent and consistent with the convention that public +deduction files are named after the concept they produce). + +--- + +## 9. Window-assembly cases + +Four mutually-exclusive cases, distinguished by `timespanKind` and presence of +`stattype` blocks. Each row of the table specifies the **complete** state of +the resulting `ProductTime`. + +| Case | `timespanKind` | `stattype` blocks | `statisticalWindows` (out→in) | `windowStart` | `timeIncrementInSeconds` | +|------|----------------|-------------------|-------------------------------|---------------|--------------------------| +| §9.1 Instant | `Missing` | 0 | (empty) | `windowEnd` | `std::nullopt` (required) | +| §9.2 Old single-loop | `Duration`| 0 | `[timespan]` | `windowEnd - timespan` | optional or required (§9.5) | +| §9.3 Old multi-loop | `Duration`| 1 or 2 | `[parse(stattype) ..., timespan]` | `windowEnd - statisticalWindows[0]` | required and > 0 | +| §9.4 New fakeDoubleLoop | `None` | exactly 1 | `[parse(stattype)]` | `windowEnd - statisticalWindows[0]` | optional or required (§9.5) | +| any other combination | — | — | — | — | hard error (§10.6, §10.7, §10.8) | + +In all rows, `simulationDateTime`, `simulatedDateTime`, `referenceDateTime`, +and `windowEnd` are computed per §7. The strict-alignment rule (§4.4) is +applied after `windowStart` is computed. + +### 9.1 Instant product + +```text +windowEnd := referenceDateTime + stepInSeconds +windowStart := windowEnd +statisticalWindows := (empty array, statisticalWindowCount = 0) +timeIncrementInSeconds := std::nullopt (MUST; else §10.5) +``` + +Used primarily by `referenceTime` and `pointInTime`. `statistics` MUST NOT be +invoked on an instant `ProductTime`. + +### 9.2 Old-style single-loop statistic + +```text +windowEnd := referenceDateTime + stepInSeconds +statisticalWindows := [timespan] +statisticalWindowCount := 1 +windowStart := windowEnd - timespan (= windowEnd - timespan.count seconds) +``` + +`timespan.unit` is `tables::TimeUnit::Second` (§7.6). `windowStart` is +therefore a simple seconds subtraction; no calendar arithmetic is involved on +this path. + +### 9.3 Old-style multi-loop statistic + +```text +windowEnd := referenceDateTime + stepInSeconds +statisticalWindows := [ , timespan ] +statisticalWindowCount := stattypeWindowCount + 1 +windowStart := windowEnd - statisticalWindows[0] + (calendar subtraction; see §9.6) +``` + +### 9.4 New-style fakeDoubleLoop ("`timespan = none`") + +A deliberately redundant encoding that exists in production. Single statistical +loop only. The single `stattype` block carries both: + +- a **period** (`mo` or `da`) consumed by `ProductTime`; +- an **operation** (`av` / `mn` / `mx` / `sd`) consumed by + `resolveTypeOfStatisticalProcessing`. + +The same statistical operation is also implied by `paramId`. The two MUST +agree; disagreement is a hard error (§10.12). This contract is enforced inside +the deduction `resolve_TypeOfStatisticalProcessing_or_throw` (§23), which +receives the `paramId`-derived operation as its +`innerTypeOfStatisticalProcessing` argument and compares it against the parsed +`stattype` block operation in this case. + +```text +windowEnd := referenceDateTime + stepInSeconds +statisticalWindows := [parse(single stattype block)] +statisticalWindowCount := 1 +windowStart := windowEnd - statisticalWindows[0] + (calendar subtraction; see §9.6) +``` + +### 9.5 `timeIncrementInSeconds` per case + +| Case | Required value of `timeIncrementInSeconds` | +|------|--------------------------------------------| +| §9.1 Instant | MUST be `std::nullopt` | +| §9.2 Old single-loop | MAY be `std::nullopt` (AIFS path); if present MUST be > 0 | +| §9.3 Old multi-loop | MUST be present and > 0 | +| §9.4 New fakeDoubleLoop | MAY be `std::nullopt` (AIFS path); if present MUST be > 0 | + +Violation is a hard error (§10.5 for instants, §10.10 for non-positive, +§10.13 for missing-where-required). + +### 9.6 Calendar subtraction + +For a window with `unit == tables::TimeUnit::Day` and `count == N`: + +```text +windowStart := DateTime(windowEnd.date() - N days, 00:00:00) +``` + +For a window with `unit == tables::TimeUnit::Month` and `count == N`: + +```text +windowStart := DateTime(Date(year, month, 1) shifted back N months, 00:00:00) +``` + +For a window with `unit == tables::TimeUnit::Second` and `count == N`: + +```text +windowStart := windowEnd - N seconds (no calendar arithmetic) +``` + +The calendar paths presuppose `windowEnd` already satisfies the corresponding +alignment rule (§4); the factory verifies alignment before performing the +subtraction. + +--- + +## 10. Hard errors + +The factory `makeProductTime_or_throw` and the resolver +`resolve_ProductTime_or_throw` MUST throw `Mars2GribDeductionException` on any +of the following conditions. Each entry corresponds to exactly one check site +in the implementation. + +| # | Condition | +|-------|------------------------------------------------------------------| +| 10.1 | `date` or `time` missing without fallback: `date` missing OR `time` missing, **except** when both `date` AND `time` are missing AND both `fcyear` AND `fcmonth` are present (in which case §7.2 substitutes the default). Note: `step` missing is NOT an error (§7.5 default 0). | +| 10.2 | `htime` present without `hdate` | +| 10.3 | exactly one of `fcyear` / `fcmonth` present (must be both or neither) | +| 10.4 | `referenceDateTime < simulatedDateTime` | +| 10.5 | tri-equivalence broken: `windowStart == windowEnd` XOR `statisticalWindowCount == 0` XOR `timeIncrementInSeconds == nullopt` | +| 10.6 | `stattype` present but `timespan` missing | +| 10.7 | `timespan = none` but `stattype` missing | +| 10.8 | `timespan = none` with more than one `stattype` block | +| 10.9 | outermost window has `unit == tables::TimeUnit::Day` and `windowEnd` is not at hh=00,mm=00,ss=00 | +| 10.10 | outermost window has `unit == tables::TimeUnit::Month` and `windowEnd` is not on day=1 at hh=00,mm=00,ss=00 | +| 10.11 | any `StatisticalWindow` in `statisticalWindows[0..count)` has `count <= 0` | +| 10.12 | (deduction-side, `resolve_TypeOfStatisticalProcessing_or_throw`, §23) in the §9.4 fakeDoubleLoop case, the parsed `stattype` block operation disagrees with the `innerTypeOfStatisticalProcessing` argument supplied by the caller | +| 10.13 | `statisticalWindowCount >= 2` and `timeIncrementInSeconds == nullopt` | +| 10.14 | `timeIncrementInSeconds` raw input value < 0 | +| 10.15 | `statisticalWindowCount > maxStatisticalWindows` (= 3) | +| 10.16 | `stattype` block parsed with unknown period or operation token (raised by the shared parser, §22; period MUST be in `{mo, da}` and operation MUST be in `{av, mn, mx, sd}`) | +| 10.17 | `stattype` blocks not in outermost-to-innermost order (e.g. `da_mo`) (raised by the shared parser, §22) | +| 10.18 | any `StatisticalWindow` in `statisticalWindows[0..count)` has `unit` outside the §3.1 allow-list `{Second, Day, Month}` (e.g. `Hour`, `Hours6`, `Year`, `Missing`). **Two-level enforcement**: (a) the shared parser (§22) enforces the narrow `stattype`-grammar allow-list `{Day, Month}` at parse time; (b) the factory `makeProductTime_or_throw` enforces the extended assembled-window allow-list `{Second, Day, Month}` after window assembly (the `Second` extension covers the innermost window when it originates from `timespan` rather than `stattype`). | + +The tri-equivalence check (10.5) subsumes several otherwise-separate checks +(e.g. "instant with non-null increment", "statistical with zero-length +window"); they are aggregated into a single invariant for clarity. + +--- + +## 11. Error contract + +All failures use `Mars2GribDeductionException` with `Here()` for source +location. The pattern matches sibling deductions +(e.g. `numberOfTimeRanges.h`): + +```cpp +try { + /* internal logic, possibly throwing other deductions' exceptions */ +} +catch (...) { + std::throw_with_nested( + Mars2GribDeductionException("Unable to resolve ProductTime", Here())); +} +``` + +Every check in §10 throws a short imperative message at its precise check +site. The outer wrapper adds context. Inner exceptions form a chain visible at +the catch boundary. + +No other exception type is introduced. No subclassing per failure category. + +--- + +## 12. Logging contract + +`resolve_ProductTime_or_throw` MUST emit exactly **one** log line on +successful completion, via `MARS2GRIB_LOG_RESOLVE`. The payload is built +inline (no `operator<<` is provided; see §13) and lists every resolved field +in a stable, greppable form. + +Indicative payload shape: + +```text +`ProductTime` resolved from input dictionaries: simulationDateTime='...' \ +simulatedDateTime='...' referenceDateTime='...' windowStart='...' windowEnd='...' \ +statisticalWindowCount='N' statisticalWindows=['...','...'] timeIncrementInSeconds='...|missing' +``` + +No log emissions on intermediate sub-steps. No log emissions on failure +(failures travel via the exception chain). + +--- + +## 13. Serialization + +No `operator<<` is provided for `ProductTime` or `StatisticalWindow`. Both +the RESOLVE log line (§12) and any inline error-message text build their +string representation locally, in the same lambda style as +`numberOfTimeRanges.h`. Tests assert on individual fields rather than on +whole-struct equality. + +--- + +## 14. Thread-safety + +- `ProductTime` is immutable and trivially safe to share across threads. +- `makeProductTime_or_throw` and `resolve_ProductTime_or_throw` access no + shared mutable state. Concurrent invocation on **disjoint** inputs is safe. +- Concurrent invocation that shares the same `MarsDict_t` / `ParDict_t` / + `OptDict_t` instances is safe iff those dictionary types' read operations + are themselves thread-safe (this is the dictionary author's contract, not + this module's). + +--- + +## 15. Per-consumer field-access table + +The following table is **normative**: each consumer MUST read only the fields +marked `R` and MUST NOT read any field marked `—`. The table is not enforced at +the language level; reviewers and consumer-side tests are responsible. + +| Field | `referenceTime` | `pointInTime` | `statistics` | +|-----------------------------|:---------------:|:-------------:|:------------:| +| `simulationDateTime` | R | — | R | +| `simulatedDateTime` | R | — | R | +| `referenceDateTime` | R | R | R | +| `windowStart` | — | — | R | +| `windowEnd` | — | R | R | +| `statisticalWindows` | — | — | R | +| `statisticalWindowCount` | — | — | R | +| `timeIncrementInSeconds` | — | — | R | + +The exact mapping between the legacy deductions used today by each consumer +(`resolve_ForecastTimeInSeconds_or_throw`, `resolve_ReferenceDateTime_or_throw`, +`resolve_HindcastDateTime_or_throw`, `resolve_SignificanceOfReferenceTime_or_throw`, +`timeIncrementInSeconds_opt`, etc.) and the `ProductTime` field accesses above +is part of the consumer-migration step (out of scope for this document). + +--- + +## 16. Test plan + +The implementation MUST ship the following tests, alongside the existing +`tests/mars2grib/...` layout (exact harness to match the in-place style). + +### 16.1 Unit tests — happy paths + +One test per worked example in §17. All §17 examples use concrete numeric +values, so each maps directly to a fixture. + +### 16.2 Unit tests — hard errors + +One negative test per entry in §10. Each test name MUST cite the §10 entry +number for traceability. + +### 16.3 Operational regression sweep (pre-merge gate) + +A curated set of real MARS requests is processed by both the legacy +`timeUtils.h`-based pipeline and the new `ProductTime` pipeline; outputs are +compared at the GRIB byte level. + +- Requests that today produce **misaligned** calendar windows are EXPECTED to + fail under the new pipeline (§4.4). Such failures are flagged in the test + set, triaged, and forwarded upstream — they are not regressions. +- All other requests MUST produce **bit-identical** GRIB output. Any byte + difference is a regression that MUST be fixed before merge. + +The fixture loader is a placeholder; populating it with operational request +samples is an environment task, not a source-code task. + +--- + +## 17. Worked examples + +All values are concrete; each example is a unit-test fixture (§16.1). + +### 17.1 Instant forecast product + +Input: + +```text +date = 20260501 +time = 000000 +step = 24 +timespan = (missing) +stattype = (missing) +``` + +Output: + +```text +simulationDateTime = 2026-05-01 00:00:00 +simulatedDateTime = 2026-05-01 00:00:00 +referenceDateTime = 2026-05-01 00:00:00 +windowEnd = 2026-05-02 00:00:00 +windowStart = 2026-05-02 00:00:00 +statisticalWindows = [] +statisticalWindowCount = 0 +timeIncrementInSeconds = std::nullopt +``` + +### 17.2 Hindcast/reforecast product + +Input: + +```text +date = 20260501 +time = 000000 +hdate = 19930501 +htime = (missing) +step = 24 +``` + +Output: + +```text +simulationDateTime = 2026-05-01 00:00:00 +simulatedDateTime = 1993-05-01 00:00:00 +referenceDateTime = 1993-05-01 00:00:00 +windowEnd = 1993-05-02 00:00:00 +windowStart = 1993-05-02 00:00:00 +statisticalWindows = [] +statisticalWindowCount = 0 +timeIncrementInSeconds = std::nullopt +``` + +### 17.3 Climate / reforecast anchor + +Input: + +```text +date = 20260501 +time = 000000 +hdate = 19930501 +fcyear = 1993 +fcmonth = 5 +step = 24 +``` + +Output: + +```text +simulationDateTime = 2026-05-01 00:00:00 +simulatedDateTime = 1993-05-01 00:00:00 +referenceDateTime = 1993-05-01 00:00:00 # Date(1993, 5, 1) +windowEnd = 1993-05-02 00:00:00 +windowStart = 1993-05-02 00:00:00 +``` + +Invariant §5.3 holds: `referenceDateTime == simulatedDateTime`. + +### 17.4 Old-style single-loop statistic — hourly accumulation over 1h + +Input: + +```text +date = 20260501 +time = 000000 +step = 24 +timespan = 1h +stattype = (missing) +timeIncrementInSeconds (par) = 3600 +``` + +Output: + +```text +windowEnd = 2026-05-02 00:00:00 +windowStart = 2026-05-01 23:00:00 +statisticalWindows = [StatisticalWindow{tables::TimeUnit::Second, 3600}] +statisticalWindowCount = 1 +timeIncrementInSeconds = 3600 +``` + +### 17.5 Old-style multi-loop — monthly average of daily minimum of hourly accumulation + +Input (concrete values; replaces the previous textual placeholder): + +```text +date = 20260501 +time = 000000 +step = 744 # 31 days * 24 hours, end of May 2026 +timespan = 1h +stattype = moav_damn +timeIncrementInSeconds (par) = 3600 +``` + +Output: + +```text +referenceDateTime = 2026-05-01 00:00:00 +windowEnd = 2026-06-01 00:00:00 # day=1, alignment satisfied (§4.3) +statisticalWindows = [ + StatisticalWindow{tables::TimeUnit::Month, 1}, # outermost + StatisticalWindow{tables::TimeUnit::Day, 1}, + StatisticalWindow{tables::TimeUnit::Second, 3600} # innermost (= timespan) +] +statisticalWindowCount = 3 +windowStart = 2026-05-01 00:00:00 # windowEnd minus 1 calendar month +timeIncrementInSeconds = 3600 +``` + +### 17.6 New-style fakeDoubleLoop — monthly average + +Input: + +```text +date = 20260501 +time = 000000 +step = 744 +timespan = none +stattype = moav +timeIncrementInSeconds (par) = 86400 +paramId-implied operation = average # required to match 'av' (§9.4, §10.12); enforced inside resolve_TypeOfStatisticalProcessing_or_throw (§23) +``` + +Output: + +```text +referenceDateTime = 2026-05-01 00:00:00 +windowEnd = 2026-06-01 00:00:00 # alignment satisfied +statisticalWindows = [StatisticalWindow{tables::TimeUnit::Month, 1}] +statisticalWindowCount = 1 +windowStart = 2026-05-01 00:00:00 +timeIncrementInSeconds = 86400 +``` + +### 17.7 Invalid new-style multi-loop — must throw + +Input: + +```text +timespan = none +stattype = moav_damn +``` + +Result: hard error per §10.8. + +### 17.8 Analysis-time product (R1: step missing) + +Input: + +```text +date = 20260501 +time = 000000 +step = (missing) +timespan = (missing) +stattype = (missing) +``` + +Output: + +```text +simulationDateTime = 2026-05-01 00:00:00 +simulatedDateTime = 2026-05-01 00:00:00 +referenceDateTime = 2026-05-01 00:00:00 +windowEnd = 2026-05-01 00:00:00 # step defaults to 0 +windowStart = 2026-05-01 00:00:00 +statisticalWindows = [] +statisticalWindowCount = 0 +timeIncrementInSeconds = std::nullopt +``` + +### 17.9 Default date/time from fcyear/fcmonth (R2) + +Input: + +```text +date = (missing) +time = (missing) +fcyear = 1993 +fcmonth = 5 +step = (missing) +``` + +Output: + +```text +simulationDateTime = 1993-05-01 00:00:00 # defaulted from (fcyear, fcmonth, 1, 00:00:00) +simulatedDateTime = 1993-05-01 00:00:00 # from simulationDateTime, hdate absent +referenceDateTime = 1993-05-01 00:00:00 # from (fcyear, fcmonth, 1, 00:00:00) +windowEnd = 1993-05-01 00:00:00 # step defaults to 0 +windowStart = 1993-05-01 00:00:00 +statisticalWindows = [] +statisticalWindowCount = 0 +timeIncrementInSeconds = std::nullopt +``` + +All three datetime fields collapse to the same value — the natural +consequence of the defaults. + +--- + +## 18. Migration & cleanup plan + +### 18.1 Files added + +- `src/metkit/mars2grib/backend/deductions/productTime.h` — public deduction + header, exposing `resolve_ProductTime_or_throw`. Named after the concept + produced (`ProductTime`), consistent with sibling deduction files in this + directory. Lowercase initial per the §20 naming convention + (function-primary). +- `src/metkit/mars2grib/backend/deductions/detail/ProductTime.h` — types + (`TimespanKind`, `ProductTimeInput`, `ProductTime`), factory + (`makeProductTime_or_throw`), and helpers (calendar arithmetic, alignment + checks, signed-second shifts). UpperCamelCase initial per the §20 naming + convention (type-primary). The shared `StatisticalWindow` type is no longer + defined here; it lives in `detail/StatisticalWindow.h` (§21). The shared + `stattype` parser is no longer defined here; it lives in `detail/StatType.h` + (§22). +- `src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h` — shared + header exposing the `StatisticalWindow` type (§21). +- `src/metkit/mars2grib/backend/deductions/detail/StatType.h` — shared + `stattype` parser exposing `parse_StatType_or_throw` and the + `ParsedStatTypeBlock` type (§22). +- `src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h` — + public deduction header, exposing `resolve_TypeOfStatisticalProcessing_or_throw` + (§23). Lowercase initial per the §20 naming convention (function-primary). +- Unit tests per §16.1 and §16.2. + +### 18.2 Files rewritten + +- `src/metkit/mars2grib/backend/deductions/statisticsDescriptor.h` — becomes + the pure function `computeStatisticDescription(productTime, + typeOfStatisticalProcessing)`. The existing `StatisticalProcessing` struct + is preserved. The current `reserve(...)` bug (vectors are indexed without + being resized) is fixed by switching to `resize(...)`. + +### 18.3 Files deleted + +The following deductions are subsumed by `ProductTime` and removed: + +- `src/metkit/mars2grib/backend/deductions/detail/timeUtils.h` +- `src/metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h` +- `src/metkit/mars2grib/backend/deductions/timeSpanInSeconds.h` +- `src/metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h` +- `src/metkit/mars2grib/backend/deductions/numberOfTimeRanges.h` +- `src/metkit/mars2grib/backend/deductions/referenceDateTime.h` +- `src/metkit/mars2grib/backend/deductions/hindcastDateTime.h` + +`significanceOfReferenceTime.h` is removed **conditionally**: if its logic is +purely temporal it folds into the new pipeline; if it carries non-temporal +semantics (type/stream/etc.) it is preserved untouched. Determined by direct +inspection at code-generation time. + +### 18.4 Files NOT modified in the deductions step + +- `src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h` +- `src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h` +- `src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h` + +These three consumer files reference deductions that are deleted in §18.3 and +will therefore **fail to compile** after the deductions step. This is +intentional and matches the explicit scoping of the deductions-only work +package. A separate consumer-migration step (out of scope here) restores the +build by routing all temporal accesses through `resolve_ProductTime_or_throw` +and the per-consumer field-access table (§15). + +A `STEP4_CONSUMER_MIGRATION_CHECKLIST.md` MUST be produced alongside the +deductions step, listing for each consumer file the exact symbol replacements +required. + +### 18.5 Build system + +`CMakeLists.txt` (or equivalent) entries that reference any deleted file are +updated in the same change set as §18.3. + +### 18.6 Breaking-change call-out + +The strict alignment rule (§4.4) is a behavioural change. Operational requests +that today silently produce misaligned calendar windows will start throwing. +This is the intended improvement; it MUST be flagged in release notes. + +--- + +## 19. Locked-decision cross-reference + +Each numbered design decision from the Step 0 / Step 1 design discussion maps +to one or more sections of this specification. The cross-reference is +maintained for traceability; future amendments SHOULD update both columns. + +| Decision | Section(s) | +|----------|---------------------------| +| A1 (timespan=none semantics) | §6, §7.6, §9.4, §10.7, §10.8 | +| A2 / B8 (strict calendar alignment) | §4.4, §10.9, §10.10 | +| A3 (reference >= simulated) | §5.3, §10.4 | +| A4 / C6 (instant tri-equivalence) | §4.1, §5.1, §9.1, §10.5 | +| A5 / B1 (timeIncrementInSeconds in PT) | §5, §6, §7.8, §9.5 | +| Q1 (fakeDoubleLoop paramId↔stattype, deduction-side) | §9.4, §10.12, §23 | +| B2 / C9 (stattype grammar) | §7.7, §10.16, §10.17, §22 | +| B3 (bare numeric units = hours) | §7.5, §7.6 | +| B4 (Date(fcyear, fcmonth, 1)) | §7.4 | +| B5 (reuse Period/StatOp/StatTypeBlock) | §7.7, §18.1, §22 | +| B6 (no sub-day calendar units) | §3.1 | +| B7 (maxStatisticalWindows = 3) | §5, §10.15 | +| B11 / B12 (per-consumer access table) | §15 | +| B13 (StatisticalWindow.count > 0) | §3.2, §10.11 | +| C13 (drop CalendarYears / restrict TimeUnit subset) | §3.1, §3.4, §10.18 | +| StatisticalWindow uses tables::TimeUnit | §3, §3.1, §10.18, §21 | +| StatisticalWindow shared header | §3, §21 | +| Shared stattype parser (Option A) | §22 | +| typeOfStatisticalProcessing deduction | §23 | +| Filename / function naming convention | §20 | +| D1 (OptDict_t pass-through, unused) | §8 | +| D2 (nested Mars2GribDeductionException) | §11 | +| D3 (single composite RESOLVE log) | §12 | +| D4 (thread-safety paragraph) | §14 | +| D5 (no operator<<) | §13 | +| D6 (full test coverage) | §16, §17 | +| D7 (hard cut for strict alignment) | §4.4, §18.6 | +| D10 (step = 0 allowed uniformly) | §7.5 | +| R1 (step optional, default 0) | §7.1, §7.5, §10.1, §17.8 | +| R2 (date/time defaulted from fcyear/fcmonth when both missing) | §7.1, §7.2, §10.1, §17.9 | +| C-list (cosmetic cleanups) | applied throughout | + +--- + +## 20. Filename and function-name naming convention + +This section is **normative** and applies to every header and function added +or renamed by this specification. + +### 20.1 Filenames + +Headers under `deductions/` and `deductions/detail/` follow a single rule +based on what the file **primarily exposes**: + +| Primary export | Filename initial | Examples | +|----------------|------------------|--------------------------------------------------------------------| +| A function | lowercase | `productTime.h`, `typeOfStatisticalProcessing.h` | +| A type | UpperCamelCase | `detail/ProductTime.h`, `detail/StatisticalWindow.h`, `detail/StatType.h` | + +Public deduction headers under `deductions/` are typically function-primary +(they expose `resolve__or_throw`) and therefore lowercase. Detail +headers under `deductions/detail/` are typically type-primary (they expose the +data type the public header resolves to) and therefore UpperCamelCase. + +A file MUST NOT be renamed once published; this convention applies only to +files added or replaced by this specification. + +### 20.2 Function names + +Functions in the deductions layer follow the pattern: + +``` +__ +``` + +where: + +- `` is the operation: `resolve`, `parse`, `make`, `convert`, + … +- `` is the produced concept or type, in + UpperCamelCase: `ProductTime`, `StatType`, `TypeOfStatisticalProcessing`, + `Date`, … +- `` is the failure policy: `or_throw`, `opt`, … + +Examples already in the codebase: + +``` +resolve_ProductTime_or_throw +make_ProductTime_or_throw (factory; legacy spelling makeProductTime_or_throw is grandfathered) +parse_StatType_or_throw +resolve_TypeOfStatisticalProcessing_or_throw +convert_YYYYMMDD2Date_or_throw +``` + +The convention is **observed**, not enforced by tooling; reviewers are +responsible. + +--- + +## 21. Shared `StatisticalWindow` type header + +### 21.1 Header + +``` +src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h +``` + +UpperCamelCase initial per §20.1 (type-primary). + +### 21.2 Definition + +```cpp +#include "metkit/mars2grib/backend/tables/timeUnits.h" + +namespace metkit::mars2grib::backend::deductions { + +struct StatisticalWindow { + tables::TimeUnit unit{tables::TimeUnit::Second}; + long count{0}; +}; + +} // namespace metkit::mars2grib::backend::deductions +``` + +The type carries no methods, no `operator<<` (per §13), and no validation; +all validation is performed by the consumers (the parser at parse time, §22; +the factory at assembly time, §10.18). + +### 21.3 Consumers + +- `deductions/detail/StatType.h` (§22) — produces values of this type. +- `deductions/detail/ProductTime.h` — stores values of this type inside + `ProductTime::statisticalWindows`. +- `deductions/typeOfStatisticalProcessing.h` (§23) — does NOT consume this + type (it consumes only the operation field of `ParsedStatTypeBlock`). + +### 21.4 Rationale + +The type is a shared primitive consumed by both `ProductTime` and the +`stattype` parser. It cannot live inside `detail/ProductTime.h` because that +would force the parser to depend on a deduction's detail header, inverting +the dependency direction. It cannot live inside `detail/StatType.h` because +`ProductTime`'s §9.2 single-loop case constructs `StatisticalWindow` +*without* invoking the parser. A shared header is the only correct +placement. + +--- + +## 22. Shared `stattype` parser + +### 22.1 Header + +``` +src/metkit/mars2grib/backend/deductions/detail/StatType.h +``` + +UpperCamelCase initial per §20.1 (type-primary; the file exposes the +`ParsedStatTypeBlock` type alongside the parser function). + +### 22.2 Type + +```cpp +#include "metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h" +#include "metkit/mars2grib/backend/tables/typeOfStatisticalProcessing.h" + +namespace metkit::mars2grib::backend::deductions::detail { + +struct ParsedStatTypeBlock { + StatisticalWindow timeWindow; + tables::TypeOfStatisticalProcessing typeOfStatisticalProcessing; +}; + +} // namespace metkit::mars2grib::backend::deductions::detail +``` + +The `timeWindow` field carries the **period** part of the block (the +stattype-grammar pair `(period_unit, count)`). The +`typeOfStatisticalProcessing` field carries the **operation** part, already +mapped to a value of GRIB Code Table 4.10. + +### 22.3 Function + +```cpp +namespace metkit::mars2grib::backend::deductions::detail { + +std::vector +parse_StatType_or_throw(std::string_view value); + +} // namespace metkit::mars2grib::backend::deductions::detail +``` + +### 22.4 Contract + +- **Input**: the raw textual value of the MARS `stattype` keyword. Callers + MUST NOT invoke the parser when the `stattype` key is absent from the MARS + dictionary; the absent case is handled at the caller. +- **Output**: a `std::vector` of length **1 or 2**, in + MARS textual order. Index 0 is the leftmost block, which corresponds to + the **outermost** loop in `ProductTime::statisticalWindows` (matching the + spec-wide outer→inner convention). +- **Block-count limit**: the parser MUST reject values with zero blocks or + more than two blocks. The cap of two blocks is the locked grammar limit; + widening it is an explicit spec amendment. + +### 22.5 Allow-lists (parser-level enforcement) + +The parser MUST reject blocks that violate the locked `stattype` grammar: + +| Field | Allow-list | Mapped to | +|------------|-------------------------------------------------|-------------------------------------------------| +| period unit| `mo`, `da` | `tables::TimeUnit::Month`, `tables::TimeUnit::Day` | +| operation | `av`, `mn`, `mx`, `sd` | `tables::TypeOfStatisticalProcessing::Average`, `::Minimum`, `::Maximum`, `::StandardDeviation` | +| count | positive integer | `long` | + +Period units `Second`, `Hour`, `Year`, `Missing`, etc. are **not** legal in +the `stattype` grammar and the parser MUST reject them at parse time. (The +extended `Second`-inclusive allow-list applies only to the assembled +`statisticalWindows` array inside `ProductTime`, where the innermost window +may originate from `timespan`; see §10.18 for the two-level enforcement +narrative.) + +### 22.6 Ordering check + +The parser MUST reject `stattype` values whose blocks are not in +outermost-to-innermost period-unit order — i.e., a `da` block MUST NOT +precede a `mo` block. This is hard error §10.17. + +### 22.7 Hard errors raised by the parser + +| Spec entry | Condition | +|------------|-------------------------------------------------------------------| +| §10.16 | unknown period or operation token | +| §10.17 | blocks not in outermost-to-innermost order | +| §10.18 (a) | period unit outside the narrow `stattype` allow-list `{Day, Month}` | + +All other §10 errors are raised by the factory, not the parser. + +### 22.8 Rationale + +A single shared parser eliminates the drift risk between `productTime` and +`typeOfStatisticalProcessing` (which would otherwise need to size their +output arrays in lock-step from independent parses of the same string). +The parser is shared infrastructure, not a deduction; "self-contained +deduction" means *no dependency on other deductions*, not *no dependency on +shared parsing primitives*. + +--- + +## 23. `typeOfStatisticalProcessing` deduction + +### 23.1 Header + +``` +src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h +``` + +Lowercase initial per §20.1 (function-primary). Named after the GRIB key +produced (`typeOfStatisticalProcessing`), consistent with the codebase +convention "filename = concept produced". + +The header is **self-contained**: it depends on `detail/StatType.h` (§22) +and `tables/typeOfStatisticalProcessing.h`, but does NOT depend on +`ProductTime`, `detail/ProductTime.h`, or any other deduction. + +### 23.2 Prototype + +```cpp +#include "metkit/mars2grib/backend/tables/typeOfStatisticalProcessing.h" + +namespace metkit::mars2grib::backend::deductions { + +template +std::vector +resolve_TypeOfStatisticalProcessing_or_throw( + tables::TypeOfStatisticalProcessing innerTypeOfStatisticalProcessing, + const MarsDict_t& mars, + const ParDict_t& par, + const OptDict_t& opt); + +} // namespace metkit::mars2grib::backend::deductions +``` + +`OptDict_t` is passed for signature symmetry with sibling deductions; it is +not currently read. + +### 23.3 Implicit invariant — array length and ordering + +**Normative.** The output `std::vector` +has length and order **identical** to `ProductTime::statisticalWindows` +resolved from the same MARS input — outermost loop at index 0, innermost +loop at index `size() - 1`. This invariant is **not** verified at runtime +(this deduction never reads `ProductTime`); callers MUST rely on it +when zipping the two vectors. + +The invariant is preserved by the locked `stattype` grammar, the locked +window-assembly cases (§9), and the case-by-case output sizing rules in +§23.5. + +### 23.4 Precondition (discharged by the type system) + +The function takes a `tables::TypeOfStatisticalProcessing` argument. +A caller cannot construct this argument without having already identified +the product as statistical (i.e., having decoded the inner statistical +operation from `paramId`). Therefore the deduction is **structurally +unreachable** for instant products (§9.1). No runtime check is performed for +this case. + +### 23.5 Output sizing per case + +The deduction inspects MARS locally (no `ProductTime` dependency) to classify +the input into one of the §9 cases, then sizes its output accordingly: + +| MARS state | §9 case | Output size | Output content | +|---------------------------------------------------------|---------|------------:|---------------------------------------------------------------------------------| +| `stattype` absent, `timespan` is a Duration | §9.2 | 1 | `[innerTypeOfStatisticalProcessing]` | +| `stattype` has N blocks (N ∈ {1, 2}), `timespan` is a Duration | §9.3 | N + 1 | `[op(block_0), …, op(block_{N-1}), innerTypeOfStatisticalProcessing]` | +| `stattype` has exactly 1 block, `timespan = "none"` | §9.4 | 1 | `[innerTypeOfStatisticalProcessing]` (after equality assertion, §23.6) | +| `stattype` absent, `timespan` absent | §9.1 | unreachable | (precondition violated, see §23.4 — no defensive check) | + +Where: + +- `op(block_i)` is the `typeOfStatisticalProcessing` field of the `i`-th + `ParsedStatTypeBlock` returned by `parse_StatType_or_throw` (§22). +- The innermost slot is **always** filled by the + `innerTypeOfStatisticalProcessing` argument — never by a parsed block — + except for the §9.4 fakeDoubleLoop case where the single slot is filled + by the argument *after* the equality assertion in §23.6. + +### 23.6 fakeDoubleLoop equality assertion (§9.4) + +In the §9.4 case, the single parsed `stattype` block carries a redundantly +encoded operation. The deduction MUST assert: + +```text +parsedBlocks[0].typeOfStatisticalProcessing == innerTypeOfStatisticalProcessing +``` + +Disagreement is hard error §10.12. After the assertion succeeds, the single +output slot is filled by `innerTypeOfStatisticalProcessing`. The two values +are equal in this branch by construction; using the argument keeps a single +authoritative source for the inner operation across all cases (§9.2, §9.3, +§9.4). + +### 23.7 Outer/inner asymmetry + +**Normative.** + +- **Outer slots** (indices `0 .. size()-2` in the §9.3 case) are constrained + to `{Average, Minimum, Maximum, StandardDeviation}` by the parser-level + op allow-list (§22.5). No other GRIB Code Table 4.10 value can appear in + an outer slot via the `stattype` path. +- **Innermost slot** (always the last index) is **unrestricted** within + GRIB Code Table 4.10. It can be any value the caller passes — typically + `Accumulation`, `Difference`, `Ratio`, `Average`, etc. — derived from + `paramId` outside this deduction's scope. + +Callers consuming the resulting vector against GRIB encoding rules MUST +take this asymmetry into account. + +### 23.8 Hard errors + +| Spec entry | Condition | +|------------|----------------------------------------------------------------------------------------------| +| §10.12 | §9.4 case with parsed block operation ≠ `innerTypeOfStatisticalProcessing` (§23.6) | +| §10.16 | propagated from `parse_StatType_or_throw` (§22.7) | +| §10.17 | propagated from `parse_StatType_or_throw` (§22.7) | +| §10.18 (a) | propagated from `parse_StatType_or_throw` (§22.7) — narrow `stattype` unit allow-list | +| §10.6 | `stattype` present but `timespan` missing (caller-side / consumer-side; this deduction does NOT classify timespan presence beyond what is needed for §23.5; the `timespan`-only checks remain on the `ProductTime` resolver) | + +This deduction does **not** raise §10.6, §10.7, or §10.8 directly — those +are `ProductTime`-resolver responsibilities. Misclassification of cases at +this level (e.g., MARS that would fail §10.7 in `ProductTime`) is undefined +behavior here; callers are expected to invoke this deduction only on inputs +that also resolve cleanly through `resolve_ProductTime_or_throw`. + +### 23.9 Error contract + +Identical to §11. All failures use `Mars2GribDeductionException` with +`Here()` for source location and a nested wrapper: + +```cpp +try { + /* parser call, classification, equality assertion */ +} +catch (...) { + std::throw_with_nested( + Mars2GribDeductionException( + "Unable to resolve typeOfStatisticalProcessing", Here())); +} +``` + +### 23.10 Logging contract + +`resolve_TypeOfStatisticalProcessing_or_throw` MUST emit exactly **one** log +line on successful completion, via `MARS2GRIB_LOG_RESOLVE`. Indicative +payload: + +```text +`typeOfStatisticalProcessing` resolved from input dictionaries: \ +innerTypeOfStatisticalProcessing='...' size='N' \ +typesOfStatisticalProcessing=['...','...','...'] +``` + +No log emissions on intermediate sub-steps. No log emissions on failure. + +### 23.11 Worked examples + +#### 23.11.1 §9.2 — single-loop hourly accumulation + +Input: + +```text +mars.stattype = (missing) +mars.timespan = "1h" +innerTypeOfStatisticalProcessing = Accumulation +``` + +Output: + +```text +[Accumulation] # size 1; matches ProductTime::statisticalWindows = [{Second, 3600}] +``` + +#### 23.11.2 §9.3 — multi-loop monthly average of daily minimum of hourly accumulation + +Input: + +```text +mars.stattype = "moav_damn" +mars.timespan = "1h" +innerTypeOfStatisticalProcessing = Accumulation +``` + +Output: + +```text +[Average, Minimum, Accumulation] # size 3; matches ProductTime::statisticalWindows = [{Month,1}, {Day,1}, {Second,3600}] +``` + +#### 23.11.3 §9.4 — fakeDoubleLoop monthly average + +Input (success): + +```text +mars.stattype = "moav" +mars.timespan = "none" +innerTypeOfStatisticalProcessing = Average +``` + +Output: + +```text +[Average] # size 1; equality assertion (§23.6) passes +``` + +Input (failure): + +```text +mars.stattype = "moav" +mars.timespan = "none" +innerTypeOfStatisticalProcessing = Maximum +``` + +Result: hard error §10.12 (parsed `Average` ≠ argument `Maximum`). + diff --git a/src/metkit/mars2grib/backend/deductions/timeSpanInSeconds.h b/src/metkit/mars2grib/backend/deductions/timeSpanInSeconds.h deleted file mode 100644 index e9db8ef8a..000000000 --- a/src/metkit/mars2grib/backend/deductions/timeSpanInSeconds.h +++ /dev/null @@ -1,110 +0,0 @@ -/* - * (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 "eckit/log/Log.h" -#include "metkit/mars2grib/utils/generalUtils.h" - -// Exceptions -#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 time span from the MARS dictionary and convert it to seconds. -/// -/// This deduction retrieves the value associated with the key `timespan` -/// from the MARS dictionary (`mars`). The value is expected to be -/// convertible to a `long` and is treated as mandatory. -/// -/// The retrieved value is interpreted according to standard MARS -/// conventions as a time span expressed in **hours**. It is converted -/// to seconds by applying a fixed scaling factor: -/// -/// \f[ -/// \text{timeSpanInSeconds} = \text{timespan} \times 3600 -/// \f] -/// -/// The resolved time span (in seconds) is logged for diagnostic and -/// traceability purposes. -/// -/// @tparam MarsDict_t -/// Type of the MARS dictionary, expected to contain the key `timespan`. -/// -/// @tparam ParDict_t -/// Type of the parameter dictionary (unused by this deduction). -/// -/// @tparam OptDict_t -/// Type of the options dictionary (unused by this deduction). -/// -/// @param[in] mars -/// MARS dictionary from which the time span is retrieved. -/// -/// @param[in] par -/// Parameter dictionary (unused). -/// -/// @param[in] opt -/// Options dictionary (unused). -/// -/// @return -/// The time span expressed in seconds, returned as a `long`. -/// -/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException -/// If: -/// - the key `timespan` is not present in the MARS dictionary, -/// - the associated value cannot be converted to `long`, -/// - any unexpected error occurs during dictionary access or conversion. -/// -/// @note -/// This deduction assumes that the MARS `timespan` value is expressed -/// in hours. No alternative units (e.g. minutes or seconds) are -/// currently supported. -/// -/// @note -/// The function follows a fail-fast strategy and uses nested exception -/// propagation to preserve full error provenance across API boundaries. -/// -template -long resolve_TimeSpanInSeconds_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 { - - // Get the mars.timespan - long marsTimespanVal = get_or_throw(mars, "timespan"); - - long timeSpanInSeconds = marsTimespanVal * 3600; - - // Logging of the timeSpan - MARS2GRIB_LOG_RESOLVE([&]() { - std::string logMsg = "timeSpan: deduced from mars dictionary with value: "; - logMsg += std::to_string(timeSpanInSeconds) + " [seconds]"; - return logMsg; - }()); - - return timeSpanInSeconds; - } - catch (...) { - - // Rethrow nested exceptions - std::throw_with_nested(Mars2GribDeductionException("Unable to get `timespan` from Mars dictionary", Here())); - }; - - // Remove compiler warning - mars2gribUnreachable(); -}; - -} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h b/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h new file mode 100644 index 000000000..29947dfb9 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h @@ -0,0 +1,330 @@ +/* + * (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 typeOfStatisticalProcessing.h +/// @brief Public deduction header for `typeOfStatisticalProcessing`. +/// +/// Exposes `resolve_TypeOfStatisticalProcessing_or_throw`, which produces a +/// `std::vector` describing the +/// statistical processing applied at each time loop of a statistical +/// product, in outer→inner order. +/// +/// **Self-contained**: this deduction does NOT depend on `ProductTime`. +/// It depends only on: +/// - `detail/StatType.h` — the shared `stattype` parser (§22) +/// - `tables/typeOfStatisticalProcessing.h` — GRIB Code Table 4.10 enum +/// +/// **Implicit invariant** (well-documented; not enforced at runtime): +/// the output `std::vector` has length and order **identical** to +/// `ProductTime::statisticalWindows` resolved from the same MARS input — +/// outermost loop at index 0, innermost loop at index `size() - 1`. +/// Callers MUST rely on this invariant when zipping the two vectors. +/// +/// **Precondition discharged by the type system**: the function takes a +/// `tables::TypeOfStatisticalProcessing` argument; a caller cannot +/// construct this argument without having already identified the product as +/// statistical (i.e., having decoded the inner statistical operation from +/// `paramId`). Therefore this deduction is structurally unreachable for +/// instant products (§9.1); no runtime check is performed for that case. +/// +/// See `deductions/timeProducts.md` §23 for the full normative +/// specification. +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +// System includes +#include +#include +#include +#include + +// eckit +#include "eckit/exception/Exceptions.h" + +// Project utilities +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +// Detail (shared parser) +#include "metkit/mars2grib/backend/deductions/detail/StatType.h" + +// Tables +#include "metkit/mars2grib/backend/tables/typeOfStatisticalProcessing.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the per-loop GRIB `typeOfStatisticalProcessing` array for +/// one MARS statistical product. +/// +/// @section Deduction contract +/// - Reads (MARS): `stattype`, `timespan` (presence only — for case +/// classification per §23.5) +/// - Reads (par): none +/// - Reads (opt): none (signature-only, reserved) +/// - Writes: none +/// - Side effects: one `MARS2GRIB_LOG_RESOLVE` line on success +/// - Failure mode: throws `Mars2GribDeductionException` (nested-with) +/// +/// @section Output sizing per case (§23.5) +/// +/// Local classification of MARS into one of the §9 cases (no `ProductTime` +/// dependency): +/// +/// | MARS state | §9 case | Output size | Output content | +/// |---------------------------------------------------------|---------|------------:|---------------------------------------------------------------------------------| +/// | `stattype` absent, `timespan` is a Duration | §9.2 | 1 | `[innerTypeOfStatisticalProcessing]` | +/// | `stattype` has N blocks, `timespan` is a Duration | §9.3 | N + 1 | `[op(block_0), …, op(block_{N-1}), innerTypeOfStatisticalProcessing]` | +/// | `stattype` has 1 block, `timespan = "none"` | §9.4 | 1 | `[innerTypeOfStatisticalProcessing]` (after equality assertion, §23.6) | +/// | `stattype` absent, `timespan` absent | §9.1 | unreachable | (precondition violated, see §23.4 — no defensive check) | +/// +/// The innermost slot is **always** filled by the +/// `innerTypeOfStatisticalProcessing` argument — never by a parsed block. +/// +/// @section fakeDoubleLoop equality assertion (§23.6) +/// +/// In the §9.4 case, the single parsed `stattype` block carries a +/// redundantly encoded operation. This deduction asserts: +/// +/// parsedBlocks[0].typeOfStatisticalProcessing +/// == innerTypeOfStatisticalProcessing +/// +/// Disagreement is hard error §10.12. +/// +/// @section Outer/inner asymmetry (§23.7) +/// +/// - **Outer slots** (indices 0..size()-2 in the §9.3 case) are +/// constrained to `{Average, Minimum, Maximum, StandardDeviation}` by +/// the parser-level op allow-list (§22.5). +/// - **Innermost slot** is **unrestricted** within GRIB Code Table 4.10; +/// it can be any value the caller passes. +/// +/// @tparam MarsDict_t MARS dictionary type. +/// @tparam ParDict_t Parameter dictionary type (currently unused). +/// @tparam OptDict_t Options dictionary type (currently unused). +/// +/// @param[in] innerTypeOfStatisticalProcessing +/// The pre-resolved inner statistical processing type, derived by +/// the caller from `paramId`. +/// @param[in] mars +/// MARS dictionary providing `stattype` and `timespan`. +/// @param[in] par +/// Parameter dictionary (signature-only). +/// @param[in] opt +/// Options dictionary (signature-only). +/// +/// @return A `std::vector` whose length +/// and order match `ProductTime::statisticalWindows` for the same +/// MARS input (implicit invariant, §23.3). +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// on: +/// - §10.12 — fakeDoubleLoop equality violation; +/// - §10.16, §10.17, §10.18 (a) — propagated from +/// `parse_StatType_or_throw` (§22.7). +/// All failures are wrapped via `std::throw_with_nested`. +/// +/// @note This function does NOT classify `timespan` exhaustively against the +/// §9 case table; it only distinguishes the three presence-states +/// (absent, Duration, "none") needed for §23.5 sizing. Misclassification +/// at this level (e.g. inputs that would fail §10.7 at the `ProductTime` +/// resolver) is undefined behavior here; callers are expected to invoke +/// this deduction only on inputs that also resolve cleanly through +/// `resolve_ProductTime_or_throw`. +/// +template +std::vector +resolve_TypeOfStatisticalProcessing_or_throw( + tables::TypeOfStatisticalProcessing innerTypeOfStatisticalProcessing, + const MarsDict_t& mars, + const ParDict_t& par, + const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_opt; + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + // Suppress "unused parameter" warnings while preserving the documented + // signature (par and opt are reserved for future use). + (void)par; + (void)opt; + + try { + + // ========================================================= + // Local case classification (§23.5) + // + // We look at presence of `stattype` and the textual value of + // `timespan` to distinguish §9.2 / §9.3 / §9.4. Full §9-case + // dispatch (with hard errors §10.6 / §10.7 / §10.8) remains the + // responsibility of the `ProductTime` resolver; this deduction + // only needs enough classification to size its output. + // ========================================================= + + const bool hasStatType = has(mars, "stattype"); + + // Classify timespan: missing | "none" | Duration. + // We do NOT parse the duration value here; only its presence and + // the literal "none" matter for §23.5 sizing. + bool hasTimespanNone = false; + bool hasTimespanDuration = false; + if (has(mars, "timespan")) { + std::optional tsStr = get_opt(mars, "timespan"); + if (tsStr.has_value()) { + if (tsStr.value() == "none") { + hasTimespanNone = true; + } + else { + hasTimespanDuration = true; + } + } + else { + // Numeric-only timespan: a Duration per §7.6. + hasTimespanDuration = true; + } + } + + // ========================================================= + // Parse `stattype` once (shared parser, §22). + // ========================================================= + + std::vector blocks; + if (hasStatType) { + const std::string statTypeVal = get_or_throw(mars, "stattype"); + blocks = detail::parse_StatType_or_throw(statTypeVal); + } + + // ========================================================= + // Output assembly per §23.5. + // + // Default-fallback path: when MARS does not match any of the + // statistical cases (§9.2 / §9.3 / §9.4) we still emit a size-1 + // vector containing the inner type. Other case-table violations + // (e.g. §10.6 / §10.7 / §10.8) are not raised here; they are + // raised by the `ProductTime` resolver when the caller invokes it + // on the same MARS input (per the contract in §23.8 and the + // class-level note above). + // ========================================================= + + std::vector out; + + if (!hasStatType && hasTimespanDuration) { + // ----- §9.2: Old-style single-loop ----- + out.reserve(1); + out.push_back(innerTypeOfStatisticalProcessing); + } + else if (hasStatType && hasTimespanDuration) { + // ----- §9.3: Old-style multi-loop ----- + out.reserve(blocks.size() + 1); + for (const detail::ParsedStatTypeBlock& b : blocks) { + out.push_back(b.typeOfStatisticalProcessing); + } + out.push_back(innerTypeOfStatisticalProcessing); + } + else if (hasStatType && hasTimespanNone) { + // ----- §9.4: New-style fakeDoubleLoop ----- + // + // Equality assertion (§23.6 / §10.12). Block count is + // structurally guaranteed to be 1 here under the spec; if the + // caller has supplied input that would fail §10.8 at the + // `ProductTime` resolver (more than one block), the assertion + // below acts on blocks[0] only — the multi-block error itself + // is reported by the `ProductTime` resolver. + if (blocks.empty()) { + // §10.7: timespan='none' with no stattype block. + // Defensive only — the parser rejects empty `stattype`, + // and `hasStatType` is true here, so blocks.size() >= 1. + throw Mars2GribDeductionException( + "typeOfStatisticalProcessing invariant violated [§10.7]: " + "timespan='none' requires exactly one stattype block, got 0", + Here()); + } + + const tables::TypeOfStatisticalProcessing parsedOp = + blocks[0].typeOfStatisticalProcessing; + + if (parsedOp != innerTypeOfStatisticalProcessing) { + throw Mars2GribDeductionException( + std::string("typeOfStatisticalProcessing invariant violated " + "[§10.12]: fakeDoubleLoop disagreement: parsed " + "stattype block operation ('") + + tables::enum2name_TypeOfStatisticalProcessing_or_throw(parsedOp) + + "') != innerTypeOfStatisticalProcessing argument ('" + + tables::enum2name_TypeOfStatisticalProcessing_or_throw( + innerTypeOfStatisticalProcessing) + + "')", + Here()); + } + + out.reserve(1); + out.push_back(innerTypeOfStatisticalProcessing); + } + else { + // Fallback: any other MARS shape (instant, or shapes that + // would fail at the `ProductTime` resolver). Per §23.4 the + // function is not expected to be called on such inputs; + // produce a size-1 vector with the inner type so the caller + // sees a defined result if they invoke us defensively. + // + // Strict §9-case enforcement is the `ProductTime` resolver's + // responsibility; we deliberately do not duplicate §10.6 / + // §10.7 / §10.8 here. + out.reserve(1); + out.push_back(innerTypeOfStatisticalProcessing); + } + + // ========================================================= + // §23.10: composite RESOLVE log line (success path only) + // ========================================================= + + MARS2GRIB_LOG_RESOLVE([&]() { + std::string msg = + "`typeOfStatisticalProcessing` resolved from input dictionaries: "; + msg += "innerTypeOfStatisticalProcessing='" + + tables::enum2name_TypeOfStatisticalProcessing_or_throw( + innerTypeOfStatisticalProcessing) + + "'"; + msg += " size='" + std::to_string(out.size()) + "'"; + msg += " typesOfStatisticalProcessing=["; + for (std::size_t i = 0; i < out.size(); ++i) { + if (i) { + msg += ","; + } + msg += "'" + + tables::enum2name_TypeOfStatisticalProcessing_or_throw(out[i]) + + "'"; + } + msg += "]"; + return msg; + }()); + + return out; + } + catch (...) { + + // §11 / §23.9: nested rethrow with context. + std::throw_with_nested( + Mars2GribDeductionException( + "Unable to resolve typeOfStatisticalProcessing", Here())); + } + + // Remove compiler warning + 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 a11a12ac9..b8abf04c7 100644 --- a/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in +++ b/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in @@ -1098,18 +1098,14 @@ INPUT = \@MARS2GRIB_SOURCE_DIRECTORY@/utils/configConverter.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/compile-time-registry-engine/common.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/compile-time-registry-engine/makeVariantCallbacksRegistry.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/compile-time-registry-engine/makeEntryCallbacksRegistry.h \ -@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/forecastTimeInSeconds.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/productionStatusOfProcessedData.h \ -@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/referenceDateTime.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/generation.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/typeOfProcessedData.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/backgroundProcess.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/waveFrequencyGrid.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/scaleFactorOfCentralWaveNumber.h \ -@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/statisticsDescriptor.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/paramId.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/methodNumber.h \ -@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/numberOfTimeRanges.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/typeOfGeneratingProcess.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/allowedReferenceValue.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/activity.h \ @@ -1119,13 +1115,14 @@ INPUT = \@MARS2GRIB_SOURCE_DIRECTORY@/utils/configConverter.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/perturbationNumber.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/localTablesVersion.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/model.h \ -@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/hindcastDateTime.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/waveFrequencyNumber.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/shapeOfTheEarth.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/typeOfEnsembleForecast.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/pvArray.h \ -@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/detail/timeUtils.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/detail/pv_137_be.h \ +@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/detail/ProductTime.h \ +@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/detail/StatType.h \ +@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/detail/StatisticalWindow.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/dataset.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/lengthOfTimeWindow.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/scaledValueOfCentralWaveNumber.h \ @@ -1134,7 +1131,8 @@ INPUT = \@MARS2GRIB_SOURCE_DIRECTORY@/utils/configConverter.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/subCentre.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/tablesVersion.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/bitsPerValue.h \ -@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/timeSpanInSeconds.h \ +@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/productTime.h \ +@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/typeOfStatisticalProcessing.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/systemNumber.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/resolution.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/experiment.h \ From bee813cd0add29ef2e3e5ffefac03852ad5384a1 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 5 May 2026 07:05:04 +0000 Subject: [PATCH 19/35] WIP: align deductions docs with as-built ProductTime migration - timeProducts.md: 18 surgical edits reconciling spec with reality (factory rename to make_ProductTime_or_throw, statisticsDescriptor.h relocated to concepts/statistics/impl/, timeIncrementInSeconds.h preserved, significanceOfReferenceTime.h preserved, consumer-migration marked complete, dropped STEP4 checklist mandate, fixed deductions/ detail/ProductTime.h casing, swapped numberOfTimeRanges.h exemplars) - timeIncrementInSeconds.h: add full ECMWF doxygen header (file/brief/ingroup, per-function param/return/throws), drop misleading copy-paste comment and implemented-already TODO - significanceOfReferenceTime.h: replace two broken @ref lines pointing at never-existing files with @ref productTime.h - detail/ProductTime.h, productTime.h, detail/StatType.h, detail/ StatisticalWindow.h: rename factory makeProductTime_or_throw to make_ProductTime_or_throw (definition + call site + 5 doxygen refs) - detail/pv_137_be.h: delete dangling commented-out namespace open - timeProducts.md section 9.5: document new numberOfTimeRanges helper - timeProducts.md section 19: drop Step 0/1 design-discussion wording --- .../backend/deductions/detail/ProductTime.h | 8 +- .../backend/deductions/detail/StatType.h | 2 +- .../deductions/detail/StatisticalWindow.h | 4 +- .../backend/deductions/detail/pv_137_be.h | 2 - .../backend/deductions/productTime.h | 4 +- .../deductions/significanceOfReferenceTime.h | 3 +- .../deductions/timeIncrementInSeconds.h | 91 ++++++++++++++- .../backend/deductions/timeProducts.md | 107 +++++++++--------- 8 files changed, 150 insertions(+), 71 deletions(-) diff --git a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h index 027a3e4b1..2bbfc7cce 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -16,7 +16,7 @@ /// - `TimespanKind` (§6) /// - `ProductTimeInput` (§6) /// - `ProductTime` (§5) -/// - `makeProductTime_or_throw` (§6) +/// - `make_ProductTime_or_throw` (§6) /// - shared helpers (calendar arithmetic, alignment checks, signed-second /// shifts, fmt overloads) /// @@ -130,7 +130,7 @@ enum class TimespanKind { // ============================================================= /// -/// @brief Resolver-side input bundle for `makeProductTime_or_throw`. +/// @brief Resolver-side input bundle for `make_ProductTime_or_throw`. /// /// The resolver normalizes raw MARS / par keys into this struct before /// invoking the factory. The factory does not consume raw MARS keys. @@ -432,7 +432,7 @@ inline std::string fmt(const std::array `std::nullopt` +/// - present, value `0` -> `std::nullopt` (legacy normalization) +/// - present, value `> 0` -> `value` +/// - present, value `< 0` -> hard error +/// +/// Deductions: +/// - extract value from the parameter dictionary +/// - apply normalization rule above +/// +/// Deductions do NOT: +/// - infer missing values +/// - apply defaults or fallbacks +/// - validate against external GRIB code tables +/// +/// Error handling follows a strict fail-fast strategy with nested +/// exception propagation to preserve full diagnostic context. +/// +/// @section References +/// Consumer: +/// - @ref productTime.h (consumes `timeIncrementInSeconds_opt` to populate +/// the `ProductTime::timeIncrementInSeconds` field) +/// +/// Specification: +/// - `deductions/timeProducts.md` §7.8 (`timeIncrementInSeconds` keyword) +/// +/// @ingroup mars2grib_backend_deductions +/// #pragma once +// System includes #include #include #include #include #include - +// Core deduction includes #include "eckit/exception/Exceptions.h" #include "eckit/log/Log.h" -#include "metkit/mars2grib/utils/generalUtils.h" - #include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/generalUtils.h" #include "metkit/mars2grib/utils/mars2gribExceptions.h" namespace metkit::mars2grib::backend::deductions { +/// +/// @brief Resolve `timeIncrementInSeconds` as an optional value. +/// +/// Reads the `timeIncrementInSeconds` key from the parameter dictionary and +/// applies the normalization rules described in the file-level +/// documentation. +/// +/// @tparam MarsDict_t MARS dictionary type (unused; kept for symmetry with +/// sibling deductions). +/// @tparam ParDict_t Parameter dictionary type. +/// @param mars MARS dictionary (unused). +/// @param par Parameter dictionary; queried for +/// `timeIncrementInSeconds`. +/// @return `std::optional` holding the strictly-positive seconds value +/// when present, `std::nullopt` otherwise (key absent or value `0`). +/// @throws Mars2GribDeductionException if the key is present and the value +/// is strictly negative, or if any underlying dictionary access +/// throws (rethrown nested with diagnostic context). +/// template std::optional timeIncrementInSeconds_opt(const MarsDict_t& mars, const ParDict_t& par) { @@ -24,10 +91,8 @@ std::optional timeIncrementInSeconds_opt(const MarsDict_t& mars, const Par try { - // Get the mars.expver auto lengthOfTimeStepInSeconds_opt = get_opt(par, "timeIncrementInSeconds"); - // TODO MIVAL: Validate (if present needs to be > 0) if (lengthOfTimeStepInSeconds_opt.has_value()) { if (lengthOfTimeStepInSeconds_opt.value() < 0) { throw Mars2GribDeductionException("`timeIncrementInSeconds` must be > 0 if present", Here()); @@ -51,6 +116,22 @@ std::optional timeIncrementInSeconds_opt(const MarsDict_t& mars, const Par }; +/// +/// @brief Resolve `timeIncrementInSeconds` or throw if absent. +/// +/// Thin wrapper around `timeIncrementInSeconds_opt` that converts a +/// `std::nullopt` result into a hard error. Use when the consumer requires +/// a definite value. +/// +/// @tparam MarsDict_t MARS dictionary type. +/// @tparam ParDict_t Parameter dictionary type. +/// @param mars MARS dictionary (forwarded). +/// @param par Parameter dictionary (forwarded). +/// @return The strictly-positive seconds value. +/// @throws Mars2GribDeductionException if the value is absent (key missing +/// or normalized to `std::nullopt`), strictly negative, or if any +/// underlying dictionary access throws. +/// template long timeIncrementInSeconds_or_throw(const MarsDict_t& mars, const ParDict_t& par) { diff --git a/src/metkit/mars2grib/backend/deductions/timeProducts.md b/src/metkit/mars2grib/backend/deductions/timeProducts.md index 9bf6cf7dc..f830d21dc 100644 --- a/src/metkit/mars2grib/backend/deductions/timeProducts.md +++ b/src/metkit/mars2grib/backend/deductions/timeProducts.md @@ -21,7 +21,7 @@ Out of scope for this document: - The MARS-side parsing of request strings. - The `paramId` → statistical-processing-type lookup table. - The GRIB-side encoding of fields outside the temporal domain. -- The migration of consumer concept files (tracked separately). +- The migration of consumer concept files (see §18.4). --- @@ -36,7 +36,7 @@ MARS par opt | | builds ProductTimeInput, calls factory v - makeProductTime_or_throw (deductions/detail/productTime.h) + make_ProductTime_or_throw (deductions/detail/ProductTime.h) | | validates invariants, returns immutable ProductTime v @@ -52,7 +52,7 @@ MARS par opt | (deductions/typeOfStatisticalProcessing.h, §23; | self-contained, depends on detail/StatType.h, §22) +--> computeStatisticDescription(productTime, types) - (deductions/statisticsDescriptor.h) + (concepts/statistics/impl/statisticsDescriptor.h) ``` `computeStatisticDescription` is a **pure function**: it depends only on its @@ -92,7 +92,7 @@ tables::TimeUnit::Day tables::TimeUnit::Month ``` -The factory `makeProductTime_or_throw` MUST reject any other value (including +The factory `make_ProductTime_or_throw` MUST reject any other value (including but not limited to `Minute`, `Hour`, `Hours3`, `Hours6`, `Hours12`, `Year`, `Decade`, `Normal`, `Century`, `Missing`) as a hard error (§10.18). @@ -305,7 +305,7 @@ struct ProductTimeInput { Factory: ```cpp -ProductTime makeProductTime_or_throw(const ProductTimeInput& input); +ProductTime make_ProductTime_or_throw(const ProductTimeInput& input); ``` The factory: @@ -425,7 +425,8 @@ Three states (`TimespanKind`): ### 7.7 `stattype` -Grammar (locked, identical to existing `detail/timeUtils.h::parseStatType_or_throw`): +Grammar (locked, identical to the legacy `detail/timeUtils.h::parseStatType_or_throw`, +now `detail/StatType.h::parse_StatType_or_throw`): ```text stattype := block ('_' block)* @@ -451,9 +452,8 @@ period 'da' -> StatisticalWindow{tables::TimeUnit::Day, 1} The operation suffix is consumed by `resolveTypeOfStatisticalProcessing` (a sibling deduction, out of scope here). The `stattype` parser MUST be a single -shared helper (currently `detail/timeUtils.h::parseStatType_or_throw`, -relocated to `detail/productTime.h`) used by both deductions, so that the two -never drift. +shared helper (now in `detail/StatType.h` as `parse_StatType_or_throw`) used +by both deductions, so that the two never drift. ### 7.8 `timeIncrementInSeconds` @@ -487,7 +487,7 @@ ProductTime resolve_ProductTime_or_throw( 2. Reads `par["timeIncrementInSeconds"]` via the existing `timeIncrementInSeconds_opt` helper. 3. Builds a `ProductTimeInput`. - 4. Calls `makeProductTime_or_throw`. + 4. Calls `make_ProductTime_or_throw`. 5. On success, emits exactly one `MARS2GRIB_LOG_RESOLVE` line (§12). 6. On failure, rethrows-with-nested per §11. @@ -496,12 +496,11 @@ ProductTime resolve_ProductTime_or_throw( used in new code. Header location: `deductions/productTime.h` (public). The factory -`makeProductTime_or_throw` called by this resolver is declared in -`deductions/detail/productTime.h`. Both files share the basename -`productTime.h`; they are distinguished by the `detail/` subdirectory per the -codebase convention for implementation-detail headers (matching the existing -`detail/timeUtils.h` precedent and consistent with the convention that public -deduction files are named after the concept they produce). +`make_ProductTime_or_throw` called by this resolver is declared in +`deductions/detail/ProductTime.h`. The two files are distinguished by both +the `detail/` subdirectory and the case of the basename per the §20.1 +naming convention (function-primary public headers use lowercase initials; +type-primary detail headers use UpperCamelCase initials). --- @@ -594,6 +593,17 @@ windowStart := windowEnd - statisticalWindows[0] Violation is a hard error (§10.5 for instants, §10.10 for non-positive, §10.13 for missing-where-required). +The free helper `numberOfTimeRanges(const detail::ProductTime& pt)` declared +in `productTime.h` returns the number of statistical loops contributing to +the product, defined as `pt.statisticalWindowCount`. For instant products +(§9.1) it returns `0`; for old-style single-loop and new-style +fakeDoubleLoop statistics (§9.2, §9.4) it returns `1`; for old-style +multi-loop statistics (§9.3) it returns the number of nested loops (`1` or +`2`, matching the §3.1 allow-list and the §22.6 ordering). Consumers (e.g. +the GRIB `numberOfTimeRange` key) MUST use this helper rather than recompute +the count from `statisticalWindows.size()` to preserve the +`statisticalWindowCount` invariant (§3.2). + ### 9.6 Calendar subtraction For a window with `unit == tables::TimeUnit::Day` and `count == N`: @@ -622,7 +632,7 @@ subtraction. ## 10. Hard errors -The factory `makeProductTime_or_throw` and the resolver +The factory `make_ProductTime_or_throw` and the resolver `resolve_ProductTime_or_throw` MUST throw `Mars2GribDeductionException` on any of the following conditions. Each entry corresponds to exactly one check site in the implementation. @@ -646,7 +656,7 @@ in the implementation. | 10.15 | `statisticalWindowCount > maxStatisticalWindows` (= 3) | | 10.16 | `stattype` block parsed with unknown period or operation token (raised by the shared parser, §22; period MUST be in `{mo, da}` and operation MUST be in `{av, mn, mx, sd}`) | | 10.17 | `stattype` blocks not in outermost-to-innermost order (e.g. `da_mo`) (raised by the shared parser, §22) | -| 10.18 | any `StatisticalWindow` in `statisticalWindows[0..count)` has `unit` outside the §3.1 allow-list `{Second, Day, Month}` (e.g. `Hour`, `Hours6`, `Year`, `Missing`). **Two-level enforcement**: (a) the shared parser (§22) enforces the narrow `stattype`-grammar allow-list `{Day, Month}` at parse time; (b) the factory `makeProductTime_or_throw` enforces the extended assembled-window allow-list `{Second, Day, Month}` after window assembly (the `Second` extension covers the innermost window when it originates from `timespan` rather than `stattype`). | +| 10.18 | any `StatisticalWindow` in `statisticalWindows[0..count)` has `unit` outside the §3.1 allow-list `{Second, Day, Month}` (e.g. `Hour`, `Hours6`, `Year`, `Missing`). **Two-level enforcement**: (a) the shared parser (§22) enforces the narrow `stattype`-grammar allow-list `{Day, Month}` at parse time; (b) the factory `make_ProductTime_or_throw` enforces the extended assembled-window allow-list `{Second, Day, Month}` after window assembly (the `Second` extension covers the innermost window when it originates from `timespan` rather than `stattype`). | The tri-equivalence check (10.5) subsumes several otherwise-separate checks (e.g. "instant with non-null increment", "statistical with zero-length @@ -658,7 +668,7 @@ window"); they are aggregated into a single invariant for clarity. All failures use `Mars2GribDeductionException` with `Here()` for source location. The pattern matches sibling deductions -(e.g. `numberOfTimeRanges.h`): +(e.g. `typeOfStatisticalProcessing.h`): ```cpp try { @@ -703,7 +713,7 @@ No log emissions on intermediate sub-steps. No log emissions on failure No `operator<<` is provided for `ProductTime` or `StatisticalWindow`. Both the RESOLVE log line (§12) and any inline error-message text build their string representation locally, in the same lambda style as -`numberOfTimeRanges.h`. Tests assert on individual fields rather than on +`typeOfStatisticalProcessing.h`. Tests assert on individual fields rather than on whole-struct equality. --- @@ -711,7 +721,7 @@ whole-struct equality. ## 14. Thread-safety - `ProductTime` is immutable and trivially safe to share across threads. -- `makeProductTime_or_throw` and `resolve_ProductTime_or_throw` access no +- `make_ProductTime_or_throw` and `resolve_ProductTime_or_throw` access no shared mutable state. Concurrent invocation on **disjoint** inputs is safe. - Concurrent invocation that shares the same `MarsDict_t` / `ParDict_t` / `OptDict_t` instances is safe iff those dictionary types' read operations @@ -737,12 +747,6 @@ the language level; reviewers and consumer-side tests are responsible. | `statisticalWindowCount` | — | — | R | | `timeIncrementInSeconds` | — | — | R | -The exact mapping between the legacy deductions used today by each consumer -(`resolve_ForecastTimeInSeconds_or_throw`, `resolve_ReferenceDateTime_or_throw`, -`resolve_HindcastDateTime_or_throw`, `resolve_SignificanceOfReferenceTime_or_throw`, -`timeIncrementInSeconds_opt`, etc.) and the `ProductTime` field accesses above -is part of the consumer-migration step (out of scope for this document). - --- ## 16. Test plan @@ -1009,7 +1013,7 @@ consequence of the defaults. (function-primary). - `src/metkit/mars2grib/backend/deductions/detail/ProductTime.h` — types (`TimespanKind`, `ProductTimeInput`, `ProductTime`), factory - (`makeProductTime_or_throw`), and helpers (calendar arithmetic, alignment + (`make_ProductTime_or_throw`), and helpers (calendar arithmetic, alignment checks, signed-second shifts). UpperCamelCase initial per the §20 naming convention (type-primary). The shared `StatisticalWindow` type is no longer defined here; it lives in `detail/StatisticalWindow.h` (§21). The shared @@ -1027,11 +1031,13 @@ consequence of the defaults. ### 18.2 Files rewritten -- `src/metkit/mars2grib/backend/deductions/statisticsDescriptor.h` — becomes - the pure function `computeStatisticDescription(productTime, +- `src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h` + — pure function `computeStatisticDescription(productTime, typeOfStatisticalProcessing)`. The existing `StatisticalProcessing` struct - is preserved. The current `reserve(...)` bug (vectors are indexed without - being resized) is fixed by switching to `resize(...)`. + is preserved. The legacy `reserve(...)` bug (vectors are indexed without + being resized) is fixed by switching to `resize(...)`. Header was relocated + out of `deductions/` because it is a `concepts/`-side consumer-stage helper, + not a deduction. ### 18.3 Files deleted @@ -1040,33 +1046,28 @@ The following deductions are subsumed by `ProductTime` and removed: - `src/metkit/mars2grib/backend/deductions/detail/timeUtils.h` - `src/metkit/mars2grib/backend/deductions/forecastTimeInSeconds.h` - `src/metkit/mars2grib/backend/deductions/timeSpanInSeconds.h` -- `src/metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h` - `src/metkit/mars2grib/backend/deductions/numberOfTimeRanges.h` - `src/metkit/mars2grib/backend/deductions/referenceDateTime.h` - `src/metkit/mars2grib/backend/deductions/hindcastDateTime.h` -`significanceOfReferenceTime.h` is removed **conditionally**: if its logic is -purely temporal it folds into the new pipeline; if it carries non-temporal -semantics (type/stream/etc.) it is preserved untouched. Determined by direct -inspection at code-generation time. +`significanceOfReferenceTime.h` is **preserved**: it carries non-temporal +semantics (derived from `mars::type`) orthogonal to `ProductTime` and remains +a standalone deduction. + +`timeIncrementInSeconds.h` is **preserved**: its `timeIncrementInSeconds_opt` +helper is consumed by the `ProductTime` resolver to populate the +`ProductTime::timeIncrementInSeconds` field (§5). -### 18.4 Files NOT modified in the deductions step +### 18.4 Consumer files migrated + +The three consumer headers below were migrated to source all temporal data +from `resolve_ProductTime_or_throw` per the per-consumer field-access table +(§15): - `src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h` - `src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h` - `src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h` -These three consumer files reference deductions that are deleted in §18.3 and -will therefore **fail to compile** after the deductions step. This is -intentional and matches the explicit scoping of the deductions-only work -package. A separate consumer-migration step (out of scope here) restores the -build by routing all temporal accesses through `resolve_ProductTime_or_throw` -and the per-consumer field-access table (§15). - -A `STEP4_CONSUMER_MIGRATION_CHECKLIST.md` MUST be produced alongside the -deductions step, listing for each consumer file the exact symbol replacements -required. - ### 18.5 Build system `CMakeLists.txt` (or equivalent) entries that reference any deleted file are @@ -1082,9 +1083,9 @@ This is the intended improvement; it MUST be flagged in release notes. ## 19. Locked-decision cross-reference -Each numbered design decision from the Step 0 / Step 1 design discussion maps -to one or more sections of this specification. The cross-reference is -maintained for traceability; future amendments SHOULD update both columns. +Each numbered design decision recorded in this specification maps to one or +more sections. The cross-reference is maintained for traceability; future +amendments SHOULD update both columns. | Decision | Section(s) | |----------|---------------------------| @@ -1166,7 +1167,7 @@ Examples already in the codebase: ``` resolve_ProductTime_or_throw -make_ProductTime_or_throw (factory; legacy spelling makeProductTime_or_throw is grandfathered) +make_ProductTime_or_throw (factory) parse_StatType_or_throw resolve_TypeOfStatisticalProcessing_or_throw convert_YYYYMMDD2Date_or_throw From e4a7b9945ea36bf7d083bbee8c255f1ea4528650 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 5 May 2026 12:28:34 +0000 Subject: [PATCH 20/35] mars2grib: Add nested error handling for all matchers --- .../concepts/analysis/analysisMatcher.h | 17 +- .../concepts/composition/compositionMatcher.h | 154 ++++++++++-------- .../concepts/data-type/dataTypeMatcher.h | 9 +- .../backend/concepts/derived/derivedMatcher.h | 38 +++-- .../backend/concepts/destine/destineMatcher.h | 43 +++-- .../concepts/ensemble/ensembleMatcher.h | 11 +- .../generatingProcessMatcher.h | 10 +- .../backend/concepts/level/levelMatcher.h | 67 ++++++++ .../concepts/longrange/longrangeMatcher.h | 18 +- .../backend/concepts/mars/marsMatcher.h | 17 +- .../backend/concepts/nil/nilMatcher.h | 9 +- .../backend/concepts/origin/originMatcher.h | 9 +- .../backend/concepts/packing/packingMatcher.h | 30 ++-- .../backend/concepts/param/paramMatcher.h | 9 +- .../point-in-time/pointInTimeMatcher.h | 67 ++++---- .../reference-time/referenceTimeMatcher.h | 16 +- .../representation/representationMatcher.h | 57 ++++--- .../concepts/satellite/satelliteMatcher.h | 23 ++- .../shapeOfTheEarthMatcher.h | 20 ++- .../concepts/statistics/statisticsMatcher.h | 120 +++++++------- .../backend/concepts/tables/tablesMatcher.h | 15 +- .../backend/concepts/wave/waveMatcher.h | 44 +++-- 22 files changed, 514 insertions(+), 289 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h index fdfaeb756..830a463fc 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h @@ -2,23 +2,30 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/analysis/analysisEnum.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 analysisMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::utils::dict_traits::has; + try { + using metkit::mars2grib::utils::dict_traits::has; - if (has(mars, "anoffset")) { - return static_cast(AnalysisType::Default); - } + if (has(mars, "anoffset")) { + return static_cast(AnalysisType::Default); + } - return compile_time_registry_engine::MISSING; + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `analysis` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h index 0e06491f9..510f49173 100644 --- a/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h @@ -2,6 +2,7 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/composition/compositionEnum.h" @@ -14,98 +15,107 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t compositionMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; - using metkit::mars2grib::utils::dict_traits::get_opt; - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; - using metkit::mars2grib::utils::exceptions::Mars2GribMatcherException; - const auto param = get_or_throw(mars, "param"); + try { - // TODO: This is the range for CAMS, there are some unmapped parameters that may need to be supported for ERA6, etc. - if (param < 400000 || param >= 500000) { - return compile_time_registry_engine::MISSING; - } + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::utils::dict_traits::get_opt; + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribMatcherException; - const auto chem = get_or_throw(mars, "chem"); - const auto hasWavelength = has(mars, "wavelength"); + const auto param = get_or_throw(mars, "param"); - if (hasWavelength) { - if (matchAny(param, 457000)) { - if (matchAny(chem, range(900, 914), 918, 922, 923, range(933, 936))) { - return static_cast(CompositionType::AerosolOptical); - } + // TODO: This is the range for CAMS, there are some unmapped parameters that may need to be supported for ERA6, etc. + if (param < 400000 || param >= 500000) { + return compile_time_registry_engine::MISSING; } - else if (matchAny(param, 458000, 459000, 460000, 461000, 462000, 472000)) { - if (matchAny(chem, 922)) { - return static_cast(CompositionType::AerosolOptical); + + const auto chem = get_or_throw(mars, "chem"); + const auto hasWavelength = has(mars, "wavelength"); + + if (hasWavelength) { + if (matchAny(param, 457000)) { + if (matchAny(chem, range(900, 914), 918, 922, 923, range(933, 936))) { + return static_cast(CompositionType::AerosolOptical); + } } - } - } - else { - if (matchAny(param, 401000)) { - if (matchAny(chem, range(900, 916))) { - return static_cast(CompositionType::Aerosol); + else if (matchAny(param, 458000, 459000, 460000, 461000, 462000, 472000)) { + if (matchAny(chem, 922)) { + return static_cast(CompositionType::AerosolOptical); + } } - if (matchAny(chem, 2, 3, range(5, 24), range(26, 30), range(32, 50), 52, 53, range(55, 58), range(63, 80), + } + else { + if (matchAny(param, 401000)) { + if (matchAny(chem, range(900, 916))) { + return static_cast(CompositionType::Aerosol); + } + if (matchAny(chem, 2, 3, range(5, 24), range(26, 30), range(32, 50), 52, 53, range(55, 58), range(63, 80), 82, 83, 85, 86, range(99, 101), 107, 112, 159, 161, 169, range(173, 178), range(186, 204), 222, range(224, 231), 233, 311, 359, 404, 917)) { - return static_cast(CompositionType::Chem); + return static_cast(CompositionType::Chem); + } } - } - else if (matchAny(param, 402000)) { - if (matchAny(chem, range(900, 917), 924)) { - return static_cast(CompositionType::Aerosol); + else if (matchAny(param, 402000)) { + if (matchAny(chem, range(900, 917), 924)) { + return static_cast(CompositionType::Aerosol); + } + if (matchAny(chem, range(2, 24), range(26, 30), range(32, 50), 52, 53, range(55, 59), range(63, 80), 82, 83, + 85, 86, range(99, 101), 107, 112, 118, 159, 161, 169, range(173, 178), range(186, 204), 222, + range(224, 230), 233, 236, 311)) { + return static_cast(CompositionType::Chem); + } } - if (matchAny(chem, range(2, 24), range(26, 30), range(32, 50), 52, 53, range(55, 59), range(63, 80), 82, 83, - 85, 86, range(99, 101), 107, 112, 118, 159, 161, 169, range(173, 178), range(186, 204), 222, - range(224, 230), 233, 236, 311)) { - return static_cast(CompositionType::Chem); - } - } - else if (matchAny(param, 406000, 407000, 410000, 411000, 451000)) { - if (matchAny(chem, range(901, 916))) { - return static_cast(CompositionType::Aerosol); + else if (matchAny(param, 406000, 407000, 410000, 411000, 451000)) { + if (matchAny(chem, range(901, 916))) { + return static_cast(CompositionType::Aerosol); + } } - } - else if (matchAny(param, 453000)) { - if (matchAny(chem, range(901, 916), 922)) { - return static_cast(CompositionType::Aerosol); + else if (matchAny(param, 453000)) { + if (matchAny(chem, range(901, 916), 922)) { + return static_cast(CompositionType::Aerosol); + } } - } - else if (matchAny(param, 400000)) { - if (matchAny(chem, range(929, 931))) { - return static_cast(CompositionType::Aerosol); + else if (matchAny(param, 400000)) { + if (matchAny(chem, range(929, 931))) { + return static_cast(CompositionType::Aerosol); + } } - } - else if (matchAny(param, 444000)) { - if (matchAny(chem, 6, 8, 13, 15, 17, 19, 26, 27, 33)) { - return static_cast(CompositionType::Chem); + else if (matchAny(param, 444000)) { + if (matchAny(chem, 6, 8, 13, 15, 17, 19, 26, 27, 33)) { + return static_cast(CompositionType::Chem); + } } - } - else if (matchAny(param, 445000)) { - if (matchAny(chem, 6, 8, 13, 15, 17, 19, 27, 33, 236)) { - return static_cast(CompositionType::Chem); + else if (matchAny(param, 445000)) { + if (matchAny(chem, 6, 8, 13, 15, 17, 19, 27, 33, 236)) { + return static_cast(CompositionType::Chem); + } } - } - else if (matchAny(param, 479000)) { - if (matchAny(chem, 404)) { - return static_cast(CompositionType::Chem); + else if (matchAny(param, 479000)) { + if (matchAny(chem, 404)) { + return static_cast(CompositionType::Chem); + } } - } - else if (matchAny(param, 469000)) { - if (matchAny(chem, 2, 5, 9, 10, 12, 16, 18, 19, 42, range(45, 48), 52, 99, 100, 129, 224, 226, 233, 311, - 933, 934)) { - return static_cast(CompositionType::ChemicalSource); + else if (matchAny(param, 469000)) { + if (matchAny(chem, 2, 5, 9, 10, 12, 16, 18, 19, 42, range(45, 48), 52, 99, 100, 129, 224, 226, 233, 311, + 933, 934)) { + return static_cast(CompositionType::ChemicalSource); + } } } - } - throw Mars2GribMatcherException( - "compositionMatcher: matching logic is not implemented for param=" + std::to_string(param) + - ", chem=" + std::to_string(chem) + ", hasWavelength=" + (hasWavelength ? "true" : "false"), - Here()); + throw Mars2GribMatcherException( + "compositionMatcher: matching logic is not implemented for param=" + std::to_string(param) + + ", chem=" + std::to_string(chem) + ", hasWavelength=" + (hasWavelength ? "true" : "false"), + Here()); + + } + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `composition` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h index 14f3fdee3..a813cf8ff 100644 --- a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h @@ -2,16 +2,23 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/data-type/dataTypeEnum.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t dataTypeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - return static_cast(DataTypeType::Default); + try { + return static_cast(DataTypeType::Default); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `dataType` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index 8ff2d913f..01d4b9f7e 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -2,34 +2,44 @@ // System include #include +#include #include // Utils #include "metkit/mars2grib/backend/concepts/derived/derivedEnum.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 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 - 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 - type == "sot" // Shift of tails - ) { - return static_cast(DerivedType::Default); + + try { + + 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 + 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 + type == "sot" // Shift of tails + ) { + return static_cast(DerivedType::Default); + } + + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `derived` concept", Here())); } - return compile_time_registry_engine::MISSING; } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h index a394e091d..abadce384 100644 --- a/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h @@ -2,6 +2,7 @@ // System include #include +#include #include // Project includes @@ -14,30 +15,36 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t destineMatcher(const MarsDict_t& mars, 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::Mars2GribGenericException; + try { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribMatcherException; - if (!has(mars, "anoffset") && get_or_throw(mars, "class") == "d1") { - if (has(mars, "dataset")) { - if (get_or_throw(mars, "dataset") == "extremes-dt") { - return static_cast(DestineType::ExtremesDT); - } - else if (get_or_throw(mars, "dataset") == "climate-dt") { - return static_cast(DestineType::ClimateDT); + if (!has(mars, "anoffset") && get_or_throw(mars, "class") == "d1") { + if (has(mars, "dataset")) { + if (get_or_throw(mars, "dataset") == "extremes-dt") { + return static_cast(DestineType::ExtremesDT); + } + else if (get_or_throw(mars, "dataset") == "climate-dt") { + return static_cast(DestineType::ClimateDT); + } + else { + throw Mars2GribMatcherException{"Unknown value \"" + get_or_throw(mars, "dataset") + + "\" for mars keyword \"dataset\"!", + Here()}; + } } else { - throw Mars2GribGenericException{"Unknown value \"" + get_or_throw(mars, "dataset") + - "\" for mars keyword \"dataset\"!"}; + throw Mars2GribMatcherException{ + "Missing required mars keyword \"dataset\" for class \"d1\" without \"anoffset\"!", Here()}; } } - else { - throw Mars2GribGenericException{ - "Missing required mars keyword \"dataset\" for class \"d1\" without \"anoffset\"!"}; - } - } - return compile_time_registry_engine::MISSING; + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `destine` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index bba73a29f..ed33817df 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -3,30 +3,27 @@ // System include #include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.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 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" || get_or_throw(mars, "type") == "me")) { return compile_time_registry_engine::MISSING; } - - if (has(mars, "number")) { - return static_cast(EnsembleType::Individual); + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `ensemble` concept", Here())); } - - return compile_time_registry_engine::MISSING; } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h index 2bb32c7fb..3fe16ea70 100644 --- a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h @@ -2,16 +2,24 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/generating-process/generatingProcessEnum.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t generatingProcessMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - return static_cast(GeneratingProcessType::Default); + try { + return static_cast(GeneratingProcessType::Default); + } + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `generatingProcess` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index e3819d2ba..e9d4bd3e6 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -11,6 +11,8 @@ // System include #include +#include +#include // Utils #include "metkit/mars2grib/backend/concepts/level/levelEnum.h" @@ -24,6 +26,7 @@ namespace metkit::mars2grib::backend::concepts_ { namespace impl { inline std::size_t matchSFC(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -146,9 +149,15 @@ inline std::size_t matchSFC(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SFC", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "sfc", "Unable to match `level` concept for levtype \"sfc\"", Here())); + } } inline std::size_t matchHL(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -159,9 +168,15 @@ inline std::size_t matchHL(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype HL", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "hl", "Unable to match `level` concept for levtype \"hl\"", Here())); + } } inline std::size_t matchML(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -182,9 +197,15 @@ inline std::size_t matchML(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype ML", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "ml", "Unable to match `level` concept for levtype \"ml\"", Here())); + } } inline std::size_t matchPL(const long param, const long level) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -201,6 +222,11 @@ inline std::size_t matchPL(const long param, const long level) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PL", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "pl", "Unable to match `level` concept for levtype \"pl\" and levelist \"" + std::to_string(level) + "\"", Here())); + } } inline std::size_t matchFL(const long param) { @@ -216,6 +242,7 @@ inline std::size_t matchFL(const long param) { } inline std::size_t matchPT(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -226,10 +253,16 @@ inline std::size_t matchPT(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PT", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "pt", "Unable to match `level` concept for levtype \"pt\"", Here())); + } } inline std::size_t matchPV(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -239,9 +272,15 @@ inline std::size_t matchPV(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PV", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "pv", "Unable to match `level` concept for levtype \"pv\"", Here())); + } } inline std::size_t matchSOL(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -257,9 +296,15 @@ inline std::size_t matchSOL(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SOL", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "sol", "Unable to match `level` concept for levtype \"sol\"", Here())); + } } inline std::size_t matchAL(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -269,9 +314,15 @@ inline std::size_t matchAL(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype AL", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "al", "Unable to match `level` concept for levtype \"al\"", Here())); + } } inline std::size_t matchO2D(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -312,9 +363,15 @@ inline std::size_t matchO2D(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O2D", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "o2d", "Unable to match `level` concept for levtype \"o2d\"", Here())); + } } inline std::size_t matchO3D(const long param) { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -327,12 +384,18 @@ inline std::size_t matchO3D(const long param) { throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O3D", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "o3d", "Unable to match `level` concept for levtype \"o3d\"", Here())); + } } } // namespace impl template std::size_t levelMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + try { using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; @@ -382,6 +445,10 @@ std::size_t levelMatcher(const MarsDict_t& mars, const OptDict_t& opt) { } throw utils::exceptions::Mars2GribMatcherException("Unknown levtype \"" + levtype + "\"", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `level` concept", Here())); + } }; } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h b/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h index 056938d58..9e5577f36 100644 --- a/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h @@ -2,24 +2,32 @@ // System include #include +#include // Utils #include "metkit/config/LibMetkit.h" #include "metkit/mars2grib/backend/concepts/longrange/longrangeEnum.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 longrangeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::utils::dict_traits::has; + try { + using metkit::mars2grib::utils::dict_traits::has; - if (has(mars, "method") && has(mars, "system")) { - return static_cast(LongrangeType::Default); - } + if (has(mars, "method") && has(mars, "system")) { + return static_cast(LongrangeType::Default); + } - return compile_time_registry_engine::MISSING; + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `longrange` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h b/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h index 9dcbbf28e..4dfcbf008 100644 --- a/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h @@ -2,23 +2,30 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/mars/marsEnum.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 marsMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::utils::dict_traits::has; + try { + using metkit::mars2grib::utils::dict_traits::has; - if (has(mars, "class") && has(mars, "type") && has(mars, "stream") && has(mars, "expver")) { - return static_cast(MarsType::Default); - } + if (has(mars, "class") && has(mars, "type") && has(mars, "stream") && has(mars, "expver")) { + return static_cast(MarsType::Default); + } - return compile_time_registry_engine::MISSING; + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `mars` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h b/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h index bfd12acf9..098c04679 100644 --- a/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h @@ -2,16 +2,23 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/nil/nilEnum.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t nilMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - return static_cast(NilType::Default); + try { + return static_cast(NilType::Default); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `nil` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h index 7a200702a..cfd4e6c65 100644 --- a/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h @@ -2,16 +2,23 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/origin/originEnum.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t originMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - return static_cast(OriginType::Default); + try { + return static_cast(OriginType::Default); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `origin` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h index b2b91f647..3fdb5fb01 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h @@ -2,6 +2,7 @@ // System include #include +#include #include // Project includes @@ -14,21 +15,26 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t packingMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + try { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribMatcherException; - const auto& packing = get_or_throw(mars, "packing"); - if (packing == "simple") { - return static_cast(PackingType::Simple); - } - if (packing == "ccsds") { - return static_cast(PackingType::Ccsds); + const auto& packing = get_or_throw(mars, "packing"); + if (packing == "simple") { + return static_cast(PackingType::Simple); + } + if (packing == "ccsds") { + return static_cast(PackingType::Ccsds); + } + if (packing == "complex") { + return static_cast(PackingType::SpectralComplex); + } + + throw Mars2GribMatcherException{"Unknown value \"" + packing + "\" for mars keyword \"packing\"!", Here()}; } - if (packing == "complex") { - return static_cast(PackingType::SpectralComplex); + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `packing` concept", Here())); } - - throw Mars2GribGenericException{"Unknown value \"" + packing + "\" for mars keyword \"packing\"!"}; } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h b/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h index 064729919..6f79a981c 100644 --- a/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h @@ -2,16 +2,23 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/param/paramEnum.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t paramMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - return static_cast(ParamType::ParamId); + try { + return static_cast(ParamType::ParamId); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `param` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ 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 872fea0b7..4dba19bcb 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h @@ -2,24 +2,27 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEnum.h" #include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" #include "metkit/mars2grib/utils/paramMatcher.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; - using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::utils::dict_traits::get_or_throw; - const auto param = get_or_throw(mars, "param"); - if (matchAny(param, range(1, 3), 10, range(15, 18), range(21, 23), range(26, 43), 53, 54, 59, 60, 66, 67, + const auto param = get_or_throw(mars, "param"); + if (matchAny(param, range(1, 3), 10, range(15, 18), range(21, 23), range(26, 43), 53, 54, 59, 60, 66, 67, range(74, 79), range(129, 139), 141, 148, 151, 152, range(155, 157), range(159, 168), 170, range(172, 174), 183, range(186, 188), 198, 203, 206, 207, range(229, 232), range(234, 236), 238, range(243, 248), 3020, 3031, 3067, range(3073, 3075), 129172, range(131074, 131077), @@ -35,35 +38,41 @@ std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { 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(262146, 262149), range(262500, 262502), range(262505, 262507), 262900, 262906, 262907)) { - return static_cast(PointInTimeType::Default); - } + return static_cast(PointInTimeType::Default); + } - // Wave products - if (matchAny(param, range(140114, 140120), 140251)) { - return static_cast(PointInTimeType::Default); - } + // Wave products + if (matchAny(param, range(140114, 140120), 140251)) { + return static_cast(PointInTimeType::Default); + } - // Satellite products - if (matchAny(param, 194, range(260510, 260512))) { - return static_cast(PointInTimeType::Default); - } + // Satellite products + if (matchAny(param, 194, range(260510, 260512))) { + return static_cast(PointInTimeType::Default); + } - // Chemical products - if (matchAny(param, range(228083, 228085), range(400000, 499999))) { - return static_cast(PointInTimeType::Default); - } + // Chemical products + if (matchAny(param, range(228083, 228085), range(400000, 499999))) { + 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); - } + // 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; - return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `pointInTime` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h index 65da076b9..3c9135cb2 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h @@ -2,22 +2,30 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/reference-time/referenceTimeEnum.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 referenceTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::utils::dict_traits::has; + try { + using metkit::mars2grib::utils::dict_traits::has; - if (has(mars, "hdate")) { - return static_cast(ReferenceTimeType::Reforecast); + if (has(mars, "hdate")) { + return static_cast(ReferenceTimeType::Reforecast); + } + return static_cast(ReferenceTimeType::Standard); + } + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `referenceTime` concept", Here())); } - return static_cast(ReferenceTimeType::Standard); } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h b/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h index aa8a4d534..386a7ad7f 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h @@ -2,6 +2,7 @@ // System include #include +#include // Utils #include "eckit/geo/Grid.h" @@ -15,33 +16,39 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t representationMatcher(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, "truncation")) { - return static_cast(RepresentationType::SphericalHarmonics); - } - - const auto marsGrid = get_or_throw(mars, "grid"); - const auto gridType = eckit::geo::GridFactory::build(eckit::spec::Custom{{"grid", marsGrid}})->type(); - if (gridType == "regular-gg") { - return static_cast(RepresentationType::RegularGaussian); - } - else if (gridType == "reduced-gg") { - return static_cast(RepresentationType::ReducedGaussian); + try { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + + if (has(mars, "truncation")) { + return static_cast(RepresentationType::SphericalHarmonics); + } + + const auto marsGrid = get_or_throw(mars, "grid"); + const auto gridType = eckit::geo::GridFactory::build(eckit::spec::Custom{{"grid", marsGrid}})->type(); + if (gridType == "regular-gg") { + return static_cast(RepresentationType::RegularGaussian); + } + else if (gridType == "reduced-gg") { + return static_cast(RepresentationType::ReducedGaussian); + } + else if (gridType == "regular-ll") { + return static_cast(RepresentationType::Latlon); + } + else if (gridType == "ORCA") { + return static_cast(RepresentationType::Orca); + } + else if (gridType == "healpix") { + return static_cast(RepresentationType::Healpix); + } + + throw utils::exceptions::Mars2GribMatcherException( + "Cannot match grid \"" + marsGrid + "\" with grid type \"" + gridType + "\"! ", Here()); } - else if (gridType == "regular-ll") { - return static_cast(RepresentationType::Latlon); + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `representation` concept", Here())); } - else if (gridType == "ORCA") { - return static_cast(RepresentationType::Orca); - } - else if (gridType == "healpix") { - return static_cast(RepresentationType::Healpix); - } - - throw utils::exceptions::Mars2GribMatcherException( - "Cannot match grid \"" + marsGrid + "\" with grid type \"" + gridType + "\"! ", Here()); } } // 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 56c176d5a..8ca0c4030 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -2,25 +2,34 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/satellite/satelliteEnum.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 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; - // Default satellite: requires full satellite identification keys - if (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) { - return static_cast(SatelliteType::Default); - } + try { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + + // Default satellite: requires full satellite identification keys + if (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) { + return static_cast(SatelliteType::Default); + } - return compile_time_registry_engine::MISSING; + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `satellite` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h index 0134b485d..c2a568c82 100644 --- a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h @@ -2,26 +2,34 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" #include "metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEnum.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 shapeOfTheEarthMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + try { - using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::dict_traits::has; - // NOTE: Spherical harmonics is encoded without shape of the earth - if (has(mars, "truncation")) { - return compile_time_registry_engine::MISSING; - } + // NOTE: Spherical harmonics is encoded without shape of the earth + if (has(mars, "truncation")) { + return compile_time_registry_engine::MISSING; + } - return static_cast(ShapeOfTheEarthType::Default); + return static_cast(ShapeOfTheEarthType::Default); + } + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `shapeOfTheEarth` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h index dee0a18d5..1f9bb8784 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h @@ -2,6 +2,7 @@ // System include #include +#include #include // Utils @@ -15,66 +16,75 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t statisticsMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; + try { + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; - const auto param = get_or_throw(mars, "param"); + const auto param = get_or_throw(mars, "param"); - if (matchAny(param, 8, 9, 20, 44, 45, 47, 50, 57, 58, range(142, 147), 169, range(175, 182), 189, range(195, 197), - 205, range(208, 213), 228, 239, 240, 3062, 3099, range(162100, 162113), range(222001, 222256), 228021, - 228022, 228129, 228130, 228143, 228144, 228216, 228228, 228251, range(231001, 231003), 231005, 231010, - 231012, 231057, 231058, range(233000, 233031), 260259)) { - return static_cast(StatisticsType::Accumulation); - } - if (matchAny(param, range(141101, 141105), 141208, 141209, 141215, 141216, 141220, 141229, 141232, 141233, 141245, - 228004, 228005, 228051, 228053, range(228057, 228060), 235020, 235021, range(235029, 235031), - range(235033, 235043), range(235048, 235053), 235055, 235058, range(235077, 235080), 235083, 235084, - 235087, 235088, 235090, 235091, 235093, 235094, 235097, 235098, 235100, 235108, range(235129, 235138), - 235151, 235152, 235155, 235157, 235159, 235165, 235166, 235168, 235189, 235203, 235246, 235263, 235269, - 235283, 235287, 235288, 235290, 235305, 235309, 235322, 235326, 235339, 235383, 263024, 263107)) { - return static_cast(StatisticsType::Average); - } - if (matchAny(param, 49, 121, 123, 201, range(143101, 143105), 143208, 143209, 143215, 143216, 143220, 143229, - 143232, 143233, 143245, 228026, 228028, 228035, 228036, 228222, 228224, 228226, 237013, 237041, 237042, - 237055, 237077, 237078, 237080, 237083, 237084, 237087, 237088, 237090, 237091, 237093, 237094, 237097, - 237108, 237117, 237131, 237132, 237134, 237137, 237151, 237159, range(237165, 237168), 237203, 237207, - 237263, 237287, 237288, 237290, 237305, 237309, 237318, 237321, 237322, 237326, 265024)) { - return static_cast(StatisticsType::Maximum); - } - if (matchAny(param, 122, 202, range(144101, 144105), 144208, 144209, 144215, 144216, 144220, 144229, 144232, 144233, - 144245, 228027, 228223, 228225, 228227, 238013, 238041, 238042, 238055, 238077, 238078, 238080, 238083, - 238084, 238087, 238088, 238090, 238091, 238093, 238094, 238097, 238108, 238131, 238132, 238134, 238137, - 238151, 238159, range(238165, 238168), 238203, 238207, 238263, 238287, 238288, 238290, 238305, 238309, - 238322, 238326, 266024)) { - return static_cast(StatisticsType::Minimum); - } - if (matchAny(param, 260320, 260321, 260339, 260683)) { - return static_cast(StatisticsType::Mode); - } - if (matchAny(param, 260318, 260319, 260338, 260682)) { - return static_cast(StatisticsType::Severity); - } - if (matchAny(param, range(145101, 145105), 145208, 145209, 145215, 145216, 145220, 145229, 145232, 145233, 145245, - 239041, 239042, 239077, 239078, 239080, 239083, 239084, 239087, 239088, 239090, 239091, 239093, 239094, - 239097, 239108, 239131, 239132, 239134, 239137, 239151, 239159, range(239165, 239168), 239203, 239207, - 239263, 239287, 239288, 239290, 239305, 239309, 239322, 239326, 267024)) { - return static_cast(StatisticsType::StandardDeviation); - } + if (matchAny(param, 8, 9, 20, 44, 45, 47, 50, 57, 58, range(142, 147), 169, range(175, 182), 189, + range(195, 197), 205, range(208, 213), 228, 239, 240, 3062, 3099, range(162100, 162113), + range(222001, 222256), 228021, 228022, 228129, 228130, 228143, 228144, 228216, 228228, 228251, + range(231001, 231003), 231005, 231010, 231012, 231057, 231058, range(233000, 233031), 260259)) { + return static_cast(StatisticsType::Accumulation); + } + if (matchAny(param, range(141101, 141105), 141208, 141209, 141215, 141216, 141220, 141229, 141232, 141233, + 141245, 228004, 228005, 228051, 228053, range(228057, 228060), 235020, 235021, + range(235029, 235031), range(235033, 235043), range(235048, 235053), 235055, 235058, + range(235077, 235080), 235083, 235084, 235087, 235088, 235090, 235091, 235093, 235094, 235097, + 235098, 235100, 235108, range(235129, 235138), 235151, 235152, 235155, 235157, 235159, 235165, + 235166, 235168, 235189, 235203, 235246, 235263, 235269, 235283, 235287, 235288, 235290, 235305, + 235309, 235322, 235326, 235339, 235383, 263024, 263107)) { + return static_cast(StatisticsType::Average); + } + if (matchAny(param, 49, 121, 123, 201, range(143101, 143105), 143208, 143209, 143215, 143216, 143220, 143229, + 143232, 143233, 143245, 228026, 228028, 228035, 228036, 228222, 228224, 228226, 237013, 237041, + 237042, 237055, 237077, 237078, 237080, 237083, 237084, 237087, 237088, 237090, 237091, 237093, + 237094, 237097, 237108, 237117, 237131, 237132, 237134, 237137, 237151, 237159, + range(237165, 237168), 237203, 237207, 237263, 237287, 237288, 237290, 237305, 237309, 237318, + 237321, 237322, 237326, 265024)) { + return static_cast(StatisticsType::Maximum); + } + if (matchAny(param, 122, 202, range(144101, 144105), 144208, 144209, 144215, 144216, 144220, 144229, 144232, + 144233, 144245, 228027, 228223, 228225, 228227, 238013, 238041, 238042, 238055, 238077, 238078, + 238080, 238083, 238084, 238087, 238088, 238090, 238091, 238093, 238094, 238097, 238108, 238131, + 238132, 238134, 238137, 238151, 238159, range(238165, 238168), 238203, 238207, 238263, 238287, + 238288, 238290, 238305, 238309, 238322, 238326, 266024)) { + return static_cast(StatisticsType::Minimum); + } + if (matchAny(param, 260320, 260321, 260339, 260683)) { + return static_cast(StatisticsType::Mode); + } + if (matchAny(param, 260318, 260319, 260338, 260682)) { + return static_cast(StatisticsType::Severity); + } + if (matchAny(param, range(145101, 145105), 145208, 145209, 145215, 145216, 145220, 145229, 145232, 145233, + 145245, 239041, 239042, 239077, 239078, 239080, 239083, 239084, 239087, 239088, 239090, 239091, + 239093, 239094, 239097, 239108, 239131, 239132, 239134, 239137, 239151, 239159, + range(239165, 239168), 239203, 239207, 239263, 239287, 239288, 239290, 239305, 239309, 239322, + 239326, 267024)) { + return static_cast(StatisticsType::StandardDeviation); + } - // Chemical products - if (matchAny(param, range(228080, 228082), range(233032, 233035), range(235062, 235064))) { - return static_cast(StatisticsType::Accumulation); - } + // Chemical products + if (matchAny(param, range(228080, 228082), range(233032, 233035), range(235062, 235064))) { + return static_cast(StatisticsType::Accumulation); + } - // TODO: Don't handle products with timespan as non-statistical if they are not handled above! - // if (has(mars, "timespan")) { - // throw utils::exceptions::Mars2GribMatcherException("MARS contains `timespan` but typeOfStatisticalProcessing - // is defined for param " + std::to_string(param), Here()); - // } + // TODO: Don't handle products with timespan as non-statistical if they are not handled above! + // if (has(mars, "timespan")) { + // throw utils::exceptions::Mars2GribMatcherException("MARS contains `timespan` but typeOfStatisticalProcessing + // is defined for param " + std::to_string(param), Here()); + // } - return compile_time_registry_engine::MISSING; + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `statistics` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h index db902bd67..a15e4ce39 100644 --- a/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h @@ -2,23 +2,30 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/tables/tablesEnum.h" #include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" #include "metkit/mars2grib/utils/paramMatcher.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t tablesMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; - using metkit::mars2grib::utils::dict_traits::get_or_throw; + try { + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::utils::dict_traits::get_or_throw; - return static_cast(TablesType::Default); + return static_cast(TablesType::Default); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `tables` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h b/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h index 9150d20dc..51a41abee 100644 --- a/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h @@ -2,36 +2,48 @@ // System include #include +#include // Utils #include "eckit/exception/Exceptions.h" #include "metkit/mars2grib/backend/concepts/wave/waveEnum.h" #include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" #include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" #include "metkit/mars2grib/utils/paramMatcher.h" namespace metkit::mars2grib::backend::concepts_ { template std::size_t waveMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; - - const auto param = get_or_throw(mars, "param"); - - if (matchAny(param, range(140114, 140120))) { - return static_cast(WaveType::Period); + try { + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribMatcherException; + + const auto param = get_or_throw(mars, "param"); + + if (matchAny(param, range(140114, 140120))) { + return static_cast(WaveType::Period); + } + + if (matchAny(param, 140251)) { + if (!has(mars, "frequency")) { + throw Mars2GribMatcherException("Missing required mars keyword `frequency` for wave spectra", Here()); + } + if (!has(mars, "direction")) { + throw Mars2GribMatcherException("Missing required mars keyword `direction` for wave spectra", Here()); + } + return static_cast(WaveType::Spectra); + } + + return compile_time_registry_engine::MISSING; } - - if (matchAny(param, 140251)) { - ASSERT(has(mars, "frequency")); - ASSERT(has(mars, "direction")); - return static_cast(WaveType::Spectra); + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `wave` concept", Here())); } - - return compile_time_registry_engine::MISSING; } } // namespace metkit::mars2grib::backend::concepts_ From 32d3e92016da92bff7eef47c9f2c6ea3d6ada9d5 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 5 May 2026 12:30:01 +0000 Subject: [PATCH 21/35] mars2grib: Run clang format --- .../concepts/analysis/analysisMatcher.h | 3 +- .../concepts/data-type/dataTypeMatcher.h | 3 +- .../backend/concepts/derived/derivedMatcher.h | 3 +- .../backend/concepts/destine/destineMatcher.h | 3 +- .../concepts/ensemble/ensembleMatcher.h | 5 +- .../backend/concepts/level/levelMatcher.h | 578 +++++++++--------- .../backend/concepts/origin/originMatcher.h | 3 +- .../backend/concepts/packing/packingMatcher.h | 3 +- .../point-in-time/pointInTimeMatcher.h | 32 +- .../concepts/satellite/satelliteMatcher.h | 3 +- .../statistics/impl/statisticsDescriptor.h | 46 +- .../concepts/statistics/statisticsEncoding.h | 21 +- .../concepts/statistics/statisticsEnum.h | 40 +- .../concepts/statistics/statisticsMatcher.h | 4 +- .../backend/concepts/tables/tablesMatcher.h | 3 +- .../backend/deductions/detail/ProductTime.h | 161 ++--- .../backend/deductions/detail/StatType.h | 70 +-- .../deductions/detail/StatisticalWindow.h | 2 +- .../backend/deductions/productTime.h | 108 ++-- .../deductions/typeOfStatisticalProcessing.h | 53 +- 20 files changed, 537 insertions(+), 607 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h index 830a463fc..33a8f3ce4 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h @@ -24,7 +24,8 @@ std::size_t analysisMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return compile_time_registry_engine::MISSING; } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `analysis` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `analysis` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h index a813cf8ff..745b2dc08 100644 --- a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h @@ -17,7 +17,8 @@ std::size_t dataTypeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(DataTypeType::Default); } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `dataType` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `dataType` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index 01d4b9f7e..87c97def4 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -37,7 +37,8 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return compile_time_registry_engine::MISSING; } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `derived` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `derived` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h index abadce384..f300f6d5a 100644 --- a/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h @@ -43,7 +43,8 @@ std::size_t destineMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return compile_time_registry_engine::MISSING; } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `destine` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `destine` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index ed33817df..3f6e852af 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -2,8 +2,8 @@ // System include #include -#include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.h" @@ -22,7 +22,8 @@ std::size_t ensembleMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return compile_time_registry_engine::MISSING; } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `ensemble` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `ensemble` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index e9d4bd3e6..26e2a2591 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -27,128 +27,130 @@ namespace impl { inline std::size_t matchSFC(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 228023)) { - return static_cast(LevelType::CloudBase); - } - if (matchAny(param, 262118)) { - return static_cast(LevelType::DepthBelowSeaLayer); - } - 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)) { - return static_cast(LevelType::EntireLake); - } - if (matchAny(param, 129172)) { - return static_cast(LevelType::HeightAboveGround); - } - if (matchAny(param, 49, 123, 165, 166, 207, 228005, 228028, 228029, 228131, 228132, 235165, 235166, 237165, 237166, - 237207, 237318, 238165, 238166, 238207, 239165, 239166, 239207, 260260)) { - return static_cast(LevelType::HeightAboveGroundAt10M); - } - if (matchAny(param, 121, 122, 167, 168, 201, 202, 174096, 228004, 228037, 235168, 237167, 237168, 238167, 238168, - 239167, 239168, 260242)) { - return static_cast(LevelType::HeightAboveGroundAt2M); - } - if (matchAny(param, 140233, 140245, 140249, 141233, 141245, 143233, 143245, 144233, 144245, 145233, 145245)) { - return static_cast(LevelType::HeightAboveSeaAt10M); - } - if (matchAny(param, 188, 3075)) { - return static_cast(LevelType::HighCloudLayer); - } - if (matchAny(param, 228014, 235309, 237309, 238309, 239309)) { - return static_cast(LevelType::IceLayerOnWater); - } - if (matchAny(param, 228013)) { - return static_cast(LevelType::IceTopOnWater); - } - if (matchAny(param, 262104)) { - return static_cast(LevelType::Isothermal); - } - if (matchAny(param, 228010, 235305, 237305, 238305, 239305)) { - return static_cast(LevelType::LakeBottom); - } - if (matchAny(param, 186, 3073, 235108, 237108, 238108, 239108)) { - return static_cast(LevelType::LowCloudLayer); - } - if (matchAny(param, 151, 235151, 237151, 238151, 239151)) { - return static_cast(LevelType::MeanSea); - } - if (matchAny(param, 187, 3074)) { - return static_cast(LevelType::MediumCloudLayer); - } - if (matchAny(param, range(228231, 228234))) { - return static_cast(LevelType::MixedLayerParcel); - } - if (matchAny(param, 228008, 228009, 235090, 235091, 237090, 237091, 238090, 238091, 239090, 239091)) { - return static_cast(LevelType::MixingLayer); - } - if (matchAny(param, range(228235, 228237))) { - return static_cast(LevelType::MostUnstableParcel); - } - if (matchAny(param, 178, 179, 208, 209, 212, 235039, 235040, 235049, 235050, 235053)) { - return static_cast(LevelType::NominalTop); - } - if (matchAny(param, 263024, 265024, 266024, 267024)) { - return static_cast(LevelType::SeaIceLayer); - } - if (matchAny(param, 235077, 235094, 237077, 237094, 238077, 238094, 239077, 239094)) { - return static_cast(LevelType::SoilLayer); - } - if (matchAny(param, 8, 9, range(15, 18), 20, range(26, 45), 47, 50, 57, 58, 66, 67, 74, 129, 134, 139, - range(141, 148), range(159, 163), 169, 170, range(172, 177), range(180, 182), 189, range(195, 198), - 205, 210, 211, 213, range(228, 232), range(234, 236), range(238, 240), range(243, 245), 3020, 3062, - 3067, 3099, range(131074, 131077), range(140098, 140105), 140112, 140113, range(140121, 140129), - range(140131, 140134), range(140207, 140209), 140211, 140212, range(140214, 140232), - range(140234, 140239), 140244, range(140246, 140248), range(140252, 140254), range(141101, 141105), - 141208, 141209, 141215, 141216, 141220, 141229, 141232, range(143101, 143105), 143208, 143209, 143215, - 143216, 143220, 143229, 143232, range(144101, 144105), 144208, 144209, 144215, 144216, 144220, 144229, - 144232, range(145101, 145105), 145208, 145209, 145215, 145216, 145220, 145229, 145232, 160198, - range(162100, 162113), 200199, range(210186, 210191), range(210198, 210202), range(210260, 210264), - range(222001, 222256), 228002, 228003, 228012, range(228015, 228022), 228024, 228026, 228027, 228032, - 228035, 228036, range(228046, 228048), 228051, 228053, range(228057, 228060), 228129, 228130, 228141, - 228143, 228144, range(228216, 228228), 228251, 229001, 229007, range(231001, 231003), 231005, 231010, - 231012, 231057, 231058, range(233000, 233031), 235020, 235021, range(235029, 235031), - range(235033, 235038), range(235041, 235043), 235048, 235051, 235052, 235055, 235058, - range(235078, 235080), 235083, 235084, 235093, 235134, 235159, 235189, 235263, 235283, 235339, 237013, - 237041, 237042, 237055, 237078, 237080, 237083, 237084, 237093, 237117, 237134, 237159, 237263, 237321, - 238013, 238041, 238042, 238055, 238078, 238080, 238083, 238084, 238093, 238134, 238159, 238263, 239041, - 239042, 239078, 239080, 239083, 239084, 239093, 239134, 239159, 239263, 260004, 260005, 260015, 260038, - 260048, 260109, 260121, 260123, 260255, 260259, 260289, 260292, 260293, range(260318, 260321), 260338, - 260339, 260509, 260682, 260683, 260688, 261001, 261002, range(261014, 261016), 261018, 261023, 262000, - 262100, 262124, 262139, 262140, 262144)) { - return static_cast(LevelType::Surface); - } - if (matchAny(param, 228045, 235322, 237322, 238322, 239322)) { - return static_cast(LevelType::Tropopause); - } + if (matchAny(param, 228023)) { + return static_cast(LevelType::CloudBase); + } + if (matchAny(param, 262118)) { + return static_cast(LevelType::DepthBelowSeaLayer); + } + 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)) { + return static_cast(LevelType::EntireLake); + } + if (matchAny(param, 129172)) { + return static_cast(LevelType::HeightAboveGround); + } + if (matchAny(param, 49, 123, 165, 166, 207, 228005, 228028, 228029, 228131, 228132, 235165, 235166, 237165, + 237166, 237207, 237318, 238165, 238166, 238207, 239165, 239166, 239207, 260260)) { + return static_cast(LevelType::HeightAboveGroundAt10M); + } + if (matchAny(param, 121, 122, 167, 168, 201, 202, 174096, 228004, 228037, 235168, 237167, 237168, 238167, + 238168, 239167, 239168, 260242)) { + return static_cast(LevelType::HeightAboveGroundAt2M); + } + if (matchAny(param, 140233, 140245, 140249, 141233, 141245, 143233, 143245, 144233, 144245, 145233, 145245)) { + return static_cast(LevelType::HeightAboveSeaAt10M); + } + if (matchAny(param, 188, 3075)) { + return static_cast(LevelType::HighCloudLayer); + } + if (matchAny(param, 228014, 235309, 237309, 238309, 239309)) { + return static_cast(LevelType::IceLayerOnWater); + } + if (matchAny(param, 228013)) { + return static_cast(LevelType::IceTopOnWater); + } + if (matchAny(param, 262104)) { + return static_cast(LevelType::Isothermal); + } + if (matchAny(param, 228010, 235305, 237305, 238305, 239305)) { + return static_cast(LevelType::LakeBottom); + } + if (matchAny(param, 186, 3073, 235108, 237108, 238108, 239108)) { + return static_cast(LevelType::LowCloudLayer); + } + if (matchAny(param, 151, 235151, 237151, 238151, 239151)) { + return static_cast(LevelType::MeanSea); + } + if (matchAny(param, 187, 3074)) { + return static_cast(LevelType::MediumCloudLayer); + } + if (matchAny(param, range(228231, 228234))) { + return static_cast(LevelType::MixedLayerParcel); + } + if (matchAny(param, 228008, 228009, 235090, 235091, 237090, 237091, 238090, 238091, 239090, 239091)) { + return static_cast(LevelType::MixingLayer); + } + if (matchAny(param, range(228235, 228237))) { + return static_cast(LevelType::MostUnstableParcel); + } + if (matchAny(param, 178, 179, 208, 209, 212, 235039, 235040, 235049, 235050, 235053)) { + return static_cast(LevelType::NominalTop); + } + if (matchAny(param, 263024, 265024, 266024, 267024)) { + return static_cast(LevelType::SeaIceLayer); + } + if (matchAny(param, 235077, 235094, 237077, 237094, 238077, 238094, 239077, 239094)) { + return static_cast(LevelType::SoilLayer); + } + if (matchAny(param, 8, 9, range(15, 18), 20, range(26, 45), 47, 50, 57, 58, 66, 67, 74, 129, 134, 139, + range(141, 148), range(159, 163), 169, 170, range(172, 177), range(180, 182), 189, range(195, 198), + 205, 210, 211, 213, range(228, 232), range(234, 236), range(238, 240), range(243, 245), 3020, 3062, + 3067, 3099, range(131074, 131077), range(140098, 140105), 140112, 140113, range(140121, 140129), + range(140131, 140134), range(140207, 140209), 140211, 140212, range(140214, 140232), + range(140234, 140239), 140244, range(140246, 140248), range(140252, 140254), range(141101, 141105), + 141208, 141209, 141215, 141216, 141220, 141229, 141232, range(143101, 143105), 143208, 143209, + 143215, 143216, 143220, 143229, 143232, range(144101, 144105), 144208, 144209, 144215, 144216, + 144220, 144229, 144232, range(145101, 145105), 145208, 145209, 145215, 145216, 145220, 145229, + 145232, 160198, range(162100, 162113), 200199, range(210186, 210191), range(210198, 210202), + range(210260, 210264), range(222001, 222256), 228002, 228003, 228012, range(228015, 228022), + 228024, 228026, 228027, 228032, 228035, 228036, range(228046, 228048), 228051, 228053, + range(228057, 228060), 228129, 228130, 228141, 228143, 228144, range(228216, 228228), 228251, + 229001, 229007, range(231001, 231003), 231005, 231010, 231012, 231057, 231058, + range(233000, 233031), 235020, 235021, range(235029, 235031), range(235033, 235038), + range(235041, 235043), 235048, 235051, 235052, 235055, 235058, range(235078, 235080), 235083, + 235084, 235093, 235134, 235159, 235189, 235263, 235283, 235339, 237013, 237041, 237042, 237055, + 237078, 237080, 237083, 237084, 237093, 237117, 237134, 237159, 237263, 237321, 238013, 238041, + 238042, 238055, 238078, 238080, 238083, 238084, 238093, 238134, 238159, 238263, 239041, 239042, + 239078, 239080, 239083, 239084, 239093, 239134, 239159, 239263, 260004, 260005, 260015, 260038, + 260048, 260109, 260121, 260123, 260255, 260259, 260289, 260292, 260293, range(260318, 260321), + 260338, 260339, 260509, 260682, 260683, 260688, 261001, 261002, range(261014, 261016), 261018, + 261023, 262000, 262100, 262124, 262139, 262140, 262144)) { + return static_cast(LevelType::Surface); + } + if (matchAny(param, 228045, 235322, 237322, 238322, 239322)) { + return static_cast(LevelType::Tropopause); + } - // Chemical - if (matchAny(param, range(228080, 228085), range(233032, 233035), range(235062, 235064), range(400000, 499999))) { - return static_cast(LevelType::Surface); - } + // Chemical + if (matchAny(param, range(228080, 228085), range(233032, 233035), range(235062, 235064), range(400000, 499999))) { + return static_cast(LevelType::Surface); + } - // Wave period - if (matchAny(param, range(140114, 140120))) { - return compile_time_registry_engine::MISSING; - } + // Wave period + if (matchAny(param, range(140114, 140120))) { + 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); - } + // 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()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SFC", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -158,16 +160,16 @@ inline std::size_t matchSFC(const long param) { inline std::size_t matchHL(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 10, 54, range(130, 132), 157, 246, 247, 3031, 235097, 235131, 235132, 237097, 237131, 237132, - 238097, 238131, 238132, 239097, 239131, 239132)) { - return static_cast(LevelType::HeightAboveGround); - } + if (matchAny(param, 10, 54, range(130, 132), 157, 246, 247, 3031, 235097, 235131, 235132, 237097, 237131, + 237132, 238097, 238131, 238132, 239097, 239131, 239132)) { + return static_cast(LevelType::HeightAboveGround); + } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype HL", Here()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype HL", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -177,26 +179,28 @@ inline std::size_t matchHL(const long param) { inline std::size_t matchML(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + + // 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); + } - // 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), range(400000, 499999)) { - return static_cast(LevelType::ModelMultipleLevel); - } + // 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), range(400000, 499999)) { + return static_cast(LevelType::ModelMultipleLevel); + } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype ML", Here()); + + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype ML", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -206,26 +210,29 @@ inline std::size_t matchML(const long param) { inline std::size_t matchPL(const long param, const long level) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + - if (matchAny(param, 1, 2, 10, 60, 75, 76, range(129, 135), 138, 152, range(155, 157), 203, range(246, 248), 235100, + if (matchAny(param, 1, 2, 10, 60, 75, 76, range(129, 135), 138, 152, range(155, 157), 203, range(246, 248), 235100, range(235129, 235133), 235135, 235138, 235152, 235155, 235157, 235203, 235246, 260290, 263107, range(400000, 499999))) { - if (level >= 100) { - return static_cast(LevelType::IsobaricInHpa); - } - else { - return static_cast(LevelType::IsobaricInPa); + if (level >= 100) { + return static_cast(LevelType::IsobaricInHpa); + } + else { + return static_cast(LevelType::IsobaricInPa); + } } - } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PL", Here()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PL", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( - param, "pl", "Unable to match `level` concept for levtype \"pl\" and levelist \"" + std::to_string(level) + "\"", Here())); + param, "pl", + "Unable to match `level` concept for levtype \"pl\" and levelist \"" + std::to_string(level) + "\"", + Here())); } } @@ -243,16 +250,18 @@ inline std::size_t matchFL(const long param) { inline std::size_t matchPT(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 53, 54, 60, range(131, 133), 138, 155, 203, 235100, 235203, 237203, 238203, 239203, - range(400000, 499999))) { - return static_cast(LevelType::Theta); - } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PT", Here()); + if (matchAny(param, 53, 54, 60, range(131, 133), 138, 155, 203, 235100, 235203, 237203, 238203, 239203, + range(400000, 499999))) { + return static_cast(LevelType::Theta); + } + + + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PT", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -263,15 +272,17 @@ inline std::size_t matchPT(const long param) { inline std::size_t matchPV(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 3, 54, 129, range(131, 133), 203, 235098, 235269, range(400000, 499999))) { - return static_cast(LevelType::PotentialVorticity); - } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PV", Here()); + if (matchAny(param, 3, 54, 129, range(131, 133), 203, 235098, 235269, range(400000, 499999))) { + return static_cast(LevelType::PotentialVorticity); + } + + + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype PV", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -281,21 +292,21 @@ inline std::size_t matchPV(const long param) { inline std::size_t matchSOL(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 262000, 262024)) { - return static_cast(LevelType::SeaIceLayer); - } - if (matchAny(param, 33, 74, 238, 228038, 228141, 235078, 235080, 237080, 238080, 239080)) { - return static_cast(LevelType::SnowLayer); - } - if (matchAny(param, 183, 235077, 260199, 260360)) { - return static_cast(LevelType::SoilLayer); - } + if (matchAny(param, 262000, 262024)) { + return static_cast(LevelType::SeaIceLayer); + } + if (matchAny(param, 33, 74, 238, 228038, 228141, 235078, 235080, 237080, 238080, 239080)) { + return static_cast(LevelType::SnowLayer); + } + if (matchAny(param, 183, 235077, 260199, 260360)) { + return static_cast(LevelType::SoilLayer); + } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SOL", Here()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SOL", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -305,15 +316,15 @@ inline std::size_t matchSOL(const long param) { inline std::size_t matchAL(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, range(213101, 213160))) { - return static_cast(LevelType::AbstractSingleLevel); - } + if (matchAny(param, range(213101, 213160))) { + return static_cast(LevelType::AbstractSingleLevel); + } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype AL", Here()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype AL", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -323,46 +334,46 @@ inline std::size_t matchAL(const long param) { inline std::size_t matchO2D(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 262000, 262003, 262004, 262008, 262014, 262023)) { - return static_cast(LevelType::IceLayerOnWater); - } - if (matchAny(param, 262001, 262005, 262006, 262906, 262907)) { - return static_cast(LevelType::IceTopOnWater); - } - if (matchAny(param, 262002, 262009, 262011, 262015)) { - return static_cast(LevelType::SnowLayerOverIceOnWater); - } - if (matchAny(param, 262017, 262018)) { - return static_cast(LevelType::EntireMeltPond); - } - if (matchAny(param, 262100, 262101, range(262108, 262112), 262124, 262125, 262130, 262139, 262140, 262143, - 262900)) { - return static_cast(LevelType::OceanSurface); - } - if (matchAny(param, range(262102, 262106))) { - return static_cast(LevelType::Isothermal); - } - if (matchAny(param, range(262113, 262115))) { - return static_cast(LevelType::MixedLayerDepthByDensity); - } - if (matchAny(param, 262116)) { - return static_cast(LevelType::MixedLayerDepthByTemperature); - } - if (matchAny(param, 262118, 262119, 262121, 262122, 262146, 262147)) { - return static_cast(LevelType::DepthBelowSeaLayer); - } - if (matchAny(param, 262120, 262123, 262148)) { - return static_cast(LevelType::OceanSurfaceToBottom); - } - if (matchAny(param, 262141)) { - return static_cast(LevelType::WaterSurfaceToIsothermalOceanLayer); - } + if (matchAny(param, 262000, 262003, 262004, 262008, 262014, 262023)) { + return static_cast(LevelType::IceLayerOnWater); + } + if (matchAny(param, 262001, 262005, 262006, 262906, 262907)) { + return static_cast(LevelType::IceTopOnWater); + } + if (matchAny(param, 262002, 262009, 262011, 262015)) { + return static_cast(LevelType::SnowLayerOverIceOnWater); + } + if (matchAny(param, 262017, 262018)) { + return static_cast(LevelType::EntireMeltPond); + } + if (matchAny(param, 262100, 262101, range(262108, 262112), 262124, 262125, 262130, 262139, 262140, 262143, + 262900)) { + return static_cast(LevelType::OceanSurface); + } + if (matchAny(param, range(262102, 262106))) { + return static_cast(LevelType::Isothermal); + } + if (matchAny(param, range(262113, 262115))) { + return static_cast(LevelType::MixedLayerDepthByDensity); + } + if (matchAny(param, 262116)) { + return static_cast(LevelType::MixedLayerDepthByTemperature); + } + if (matchAny(param, 262118, 262119, 262121, 262122, 262146, 262147)) { + return static_cast(LevelType::DepthBelowSeaLayer); + } + if (matchAny(param, 262120, 262123, 262148)) { + return static_cast(LevelType::OceanSurfaceToBottom); + } + if (matchAny(param, 262141)) { + return static_cast(LevelType::WaterSurfaceToIsothermalOceanLayer); + } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O2D", Here()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O2D", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -372,18 +383,18 @@ inline std::size_t matchO2D(const long param) { inline std::size_t matchO3D(const long param) { try { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, range(262500, 262502), 262505, 262506)) { - return static_cast(LevelType::OceanModelLayer); - } - if (matchAny(param, 262507)) { - return static_cast(LevelType::OceanModel); - } + if (matchAny(param, range(262500, 262502), 262505, 262506)) { + return static_cast(LevelType::OceanModelLayer); + } + if (matchAny(param, 262507)) { + return static_cast(LevelType::OceanModel); + } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O3D", Here()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O3D", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( @@ -396,55 +407,56 @@ inline std::size_t matchO3D(const long param) { template std::size_t levelMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; - - // Skip wave spectra and satellite products - if ((has(mars, "frequency") && has(mars, "direction")) || // Wave spectra - (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) // Satellite - ) { - return compile_time_registry_engine::MISSING; - } + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + + // Skip wave spectra and satellite products + if ((has(mars, "frequency") && has(mars, "direction")) || // Wave spectra + (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) // Satellite + ) { + return compile_time_registry_engine::MISSING; + } - const auto param = get_or_throw(mars, "param"); - const auto levtype = get_or_throw(mars, "levtype"); + const auto param = get_or_throw(mars, "param"); + const auto levtype = get_or_throw(mars, "levtype"); + + if (levtype == "sfc") { + return impl::matchSFC(param); + } + if (levtype == "hl") { + return impl::matchHL(param); + } + if (levtype == "ml") { + return impl::matchML(param); + } + if (levtype == "pl") { + const auto level = get_or_throw(mars, "levelist"); + return impl::matchPL(param, level); + } + if (levtype == "fl") { + return impl::matchFL(param); + } + if (levtype == "pt") { + return impl::matchPT(param); + } + if (levtype == "pv") { + return impl::matchPV(param); + } + if (levtype == "sol") { + return impl::matchSOL(param); + } + if (levtype == "al") { + return impl::matchAL(param); + } + if (levtype == "o2d") { + return impl::matchO2D(param); + } + if (levtype == "o3d") { + return impl::matchO3D(param); + } - if (levtype == "sfc") { - return impl::matchSFC(param); - } - if (levtype == "hl") { - return impl::matchHL(param); - } - if (levtype == "ml") { - return impl::matchML(param); - } - if (levtype == "pl") { - const auto level = get_or_throw(mars, "levelist"); - return impl::matchPL(param, level); - } - if (levtype == "fl") { - return impl::matchFL(param); - } - if (levtype == "pt") { - return impl::matchPT(param); - } - if (levtype == "pv") { - return impl::matchPV(param); - } - if (levtype == "sol") { - return impl::matchSOL(param); - } - if (levtype == "al") { - return impl::matchAL(param); - } - if (levtype == "o2d") { - return impl::matchO2D(param); - } - if (levtype == "o3d") { - return impl::matchO3D(param); - } - throw utils::exceptions::Mars2GribMatcherException("Unknown levtype \"" + levtype + "\"", Here()); + throw utils::exceptions::Mars2GribMatcherException("Unknown levtype \"" + levtype + "\"", Here()); } catch (...) { std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `level` concept", Here())); diff --git a/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h index cfd4e6c65..f7919c777 100644 --- a/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h @@ -17,7 +17,8 @@ std::size_t originMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(OriginType::Default); } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `origin` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `origin` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h index 3fdb5fb01..35d7fd85f 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h @@ -33,7 +33,8 @@ std::size_t packingMatcher(const MarsDict_t& mars, const OptDict_t& opt) { throw Mars2GribMatcherException{"Unknown value \"" + packing + "\" for mars keyword \"packing\"!", Here()}; } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `packing` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `packing` concept", Here())); } } 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 4dba19bcb..1b5453e14 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h @@ -23,21 +23,22 @@ std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { const auto param = get_or_throw(mars, "param"); if (matchAny(param, range(1, 3), 10, range(15, 18), range(21, 23), range(26, 43), 53, 54, 59, 60, 66, 67, - range(74, 79), range(129, 139), 141, 148, 151, 152, range(155, 157), range(159, 168), 170, - range(172, 174), 183, range(186, 188), 198, 203, 206, 207, range(229, 232), range(234, 236), 238, - range(243, 248), 3020, 3031, 3067, range(3073, 3075), 129172, range(131074, 131077), - range(140098, 140105), 140112, 140113, range(140121, 140129), range(140131, 140134), - range(140207, 140209), 140211, 140212, range(140214, 140239), range(140244, 140249), - range(140252, 140254), 160198, range(162059, 162063), 162071, 162072, 162093, 174096, 200199, - range(210186, 210191), range(210198, 210202), range(210260, 210264), range(213101, 213160), - range(228001, 228003), range(228007, 228020), 228023, 228024, 228029, 228032, 228037, 228038, - range(228044, 228048), 228050, 228052, range(228088, 228090), 228131, 228132, 228141, 228164, - range(228217, 228221), range(228231, 228237), 229001, 229007, 260004, 260005, 260015, 260038, 260048, - 260109, 260121, 260123, 260132, 260199, 260242, 260255, 260260, 260289, 260290, 260292, 260293, 260360, - 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(262146, 262149), range(262500, 262502), range(262505, 262507), 262900, 262906, 262907)) { + range(74, 79), range(129, 139), 141, 148, 151, 152, range(155, 157), range(159, 168), 170, + range(172, 174), 183, range(186, 188), 198, 203, 206, 207, range(229, 232), range(234, 236), 238, + range(243, 248), 3020, 3031, 3067, range(3073, 3075), 129172, range(131074, 131077), + range(140098, 140105), 140112, 140113, range(140121, 140129), range(140131, 140134), + range(140207, 140209), 140211, 140212, range(140214, 140239), range(140244, 140249), + range(140252, 140254), 160198, range(162059, 162063), 162071, 162072, 162093, 174096, 200199, + range(210186, 210191), range(210198, 210202), range(210260, 210264), range(213101, 213160), + range(228001, 228003), range(228007, 228020), 228023, 228024, 228029, 228032, 228037, 228038, + range(228044, 228048), 228050, 228052, range(228088, 228090), 228131, 228132, 228141, 228164, + range(228217, 228221), range(228231, 228237), 229001, 229007, 260004, 260005, 260015, 260038, + 260048, 260109, 260121, 260123, 260132, 260199, 260242, 260255, 260260, 260289, 260290, 260292, + 260293, 260360, 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(262146, 262149), range(262500, 262502), + range(262505, 262507), 262900, 262906, 262907)) { return static_cast(PointInTimeType::Default); } @@ -67,7 +68,6 @@ std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { } return compile_time_registry_engine::MISSING; - } catch (...) { std::throw_with_nested( diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h index 8ca0c4030..d5f746e3e 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -28,7 +28,8 @@ std::size_t satelliteMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return compile_time_registry_engine::MISSING; } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `satellite` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `satellite` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h b/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h index 126c38cd3..bdb58f94a 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h @@ -121,8 +121,7 @@ inline long previousMonthLengthHours(long year, long month) { /// /// `endYear`/`endMonth` are taken from `pt.windowEnd` for monthly windows. /// -inline long windowLengthInHours(const deductions::detail::StatisticalWindow& w, - long endYear, long endMonth) { +inline long windowLengthInHours(const deductions::detail::StatisticalWindow& w, long endYear, long endMonth) { using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; switch (w.unit) { @@ -130,7 +129,8 @@ inline long windowLengthInHours(const deductions::detail::StatisticalWindow& w, if (w.count % 3600 != 0) { throw Mars2GribDeductionException( "StatisticalWindow with unit=Second must have count divisible by 3600 " - "to be expressed in hours; actual count=" + std::to_string(w.count), + "to be expressed in hours; actual count=" + + std::to_string(w.count), Here()); } return w.count / 3600; @@ -140,10 +140,9 @@ inline long windowLengthInHours(const deductions::detail::StatisticalWindow& w, case tables::TimeUnit::Month: return w.count * previousMonthLengthHours(endYear, endMonth); default: - throw Mars2GribDeductionException( - "Unsupported StatisticalWindow unit for hour conversion: '" - + tables::enum2name_TimeUnit_or_throw(w.unit) + "'", - Here()); + throw Mars2GribDeductionException("Unsupported StatisticalWindow unit for hour conversion: '" + + tables::enum2name_TimeUnit_or_throw(w.unit) + "'", + Here()); } } @@ -193,8 +192,7 @@ inline long windowLengthInHours(const deductions::detail::StatisticalWindow& w, /// increment for multi-window products, or unsupported window unit. /// inline StatisticalProcessing compute_StatisticalProcessing( - const deductions::detail::ProductTime& pt, - const std::vector& types) { + const deductions::detail::ProductTime& pt, const std::vector& types) { using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; @@ -202,19 +200,19 @@ inline StatisticalProcessing compute_StatisticalProcessing( if (types.size() != n) { throw Mars2GribDeductionException( - "compute_StatisticalProcessing: types.size()=" + std::to_string(types.size()) - + " != pt.statisticalWindowCount=" + std::to_string(n), + "compute_StatisticalProcessing: types.size()=" + std::to_string(types.size()) + + " != pt.statisticalWindowCount=" + std::to_string(n), Here()); } StatisticalProcessing out; out.numberOfTimeRanges = static_cast(n); - out.typeOfStatisticalProcessing .resize(n); - out.typeOfTimeIncrement .resize(n); - out.indicatorOfUnitForTimeRange .resize(n); - out.lengthOfTimeRange .resize(n); + out.typeOfStatisticalProcessing.resize(n); + out.typeOfTimeIncrement.resize(n); + out.indicatorOfUnitForTimeRange.resize(n); + out.lengthOfTimeRange.resize(n); out.indicatorOfUnitForTimeIncrement.resize(n); - out.lengthOfTimeIncrement .resize(n); + out.lengthOfTimeIncrement.resize(n); if (n == 0) { // Instant product: nothing to encode. Caller (Allocate stage) will @@ -224,14 +222,12 @@ inline StatisticalProcessing compute_StatisticalProcessing( } // AIFS single-window no-increment detection (§9.4). - const bool aifsSingleWindowHack = - (n == 1) && !pt.timeIncrementInSeconds.has_value(); + const bool aifsSingleWindowHack = (n == 1) && !pt.timeIncrementInSeconds.has_value(); if (n >= 2 && !pt.timeIncrementInSeconds.has_value()) { - throw Mars2GribDeductionException( - "compute_StatisticalProcessing: multi-window product (n=" - + std::to_string(n) + ") requires pt.timeIncrementInSeconds; missing.", - Here()); + throw Mars2GribDeductionException("compute_StatisticalProcessing: multi-window product (n=" + + std::to_string(n) + ") requires pt.timeIncrementInSeconds; missing.", + Here()); } const long endYear = pt.windowEnd.date().year(); @@ -245,8 +241,7 @@ inline StatisticalProcessing compute_StatisticalProcessing( out.typeOfTimeIncrement[i] = 2; out.indicatorOfUnitForTimeRange[i] = static_cast(tables::TimeUnit::Hour); out.indicatorOfUnitForTimeIncrement[i] = static_cast(tables::TimeUnit::Second); - out.lengthOfTimeRange[i] = - detail::windowLengthInHours(pt.statisticalWindows[i], endYear, endMonth); + out.lengthOfTimeRange[i] = detail::windowLengthInHours(pt.statisticalWindows[i], endYear, endMonth); } // Second pass: lengthOfTimeIncrement. @@ -255,8 +250,7 @@ inline StatisticalProcessing compute_StatisticalProcessing( if (isInner) { if (aifsSingleWindowHack) { out.lengthOfTimeIncrement[i] = 0; - out.indicatorOfUnitForTimeIncrement[i] = - static_cast(tables::TimeUnit::Missing); + out.indicatorOfUnitForTimeIncrement[i] = static_cast(tables::TimeUnit::Missing); } else { // Guaranteed by the multi-window precondition check above diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h index b30c4e15f..f6ad899b7 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h @@ -121,8 +121,7 @@ void StatisticsOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& // Encoding setMissing_or_throw(out, "hoursAfterDataCutoff"); setMissing_or_throw(out, "minutesAfterDataCutoff"); - set_or_throw(out, "numberOfTimeRanges", - deductions::numberOfTimeRanges(pt)); + set_or_throw(out, "numberOfTimeRanges", deductions::numberOfTimeRanges(pt)); } // ============================================================= @@ -136,23 +135,17 @@ void StatisticsOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& auto pt = deductions::resolve_ProductTime_or_throw(mars, par, opt); auto inner = typeOfStatisticalProcessingEnum(); - auto types = deductions::resolve_TypeOfStatisticalProcessing_or_throw( - inner, mars, par, opt); + auto types = deductions::resolve_TypeOfStatisticalProcessing_or_throw(inner, mars, par, opt); auto desc = impl::compute_StatisticalProcessing(pt, types); - set_or_throw>(out, "typeOfStatisticalProcessing", - desc.typeOfStatisticalProcessing); - set_or_throw>(out, "typeOfTimeIncrement", - desc.typeOfTimeIncrement); - set_or_throw>(out, "indicatorOfUnitForTimeRange", - desc.indicatorOfUnitForTimeRange); - set_or_throw>(out, "lengthOfTimeRange", - desc.lengthOfTimeRange); + set_or_throw>(out, "typeOfStatisticalProcessing", desc.typeOfStatisticalProcessing); + set_or_throw>(out, "typeOfTimeIncrement", desc.typeOfTimeIncrement); + set_or_throw>(out, "indicatorOfUnitForTimeRange", desc.indicatorOfUnitForTimeRange); + set_or_throw>(out, "lengthOfTimeRange", desc.lengthOfTimeRange); set_or_throw>(out, "indicatorOfUnitForTimeIncrement", desc.indicatorOfUnitForTimeIncrement); - set_or_throw>(out, "lengthOfTimeIncrement", - desc.lengthOfTimeIncrement); + set_or_throw>(out, "lengthOfTimeIncrement", desc.lengthOfTimeIncrement); } // ============================================================= diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h index ce3aff707..3dd04e876 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h @@ -250,30 +250,30 @@ DEF(StatisticsType::Default, 255); template constexpr tables::TypeOfStatisticalProcessing typeOfStatisticalProcessingEnum(); -#define DEF(T, VAL) \ - template <> \ - constexpr tables::TypeOfStatisticalProcessing typeOfStatisticalProcessingEnum() { \ - return tables::TypeOfStatisticalProcessing::VAL; \ +#define DEF(T, VAL) \ + template <> \ + constexpr tables::TypeOfStatisticalProcessing typeOfStatisticalProcessingEnum() { \ + return tables::TypeOfStatisticalProcessing::VAL; \ } -DEF(StatisticsType::Average, Average); -DEF(StatisticsType::Accumulation, Accumulation); -DEF(StatisticsType::Maximum, Maximum); -DEF(StatisticsType::Minimum, Minimum); +DEF(StatisticsType::Average, Average); +DEF(StatisticsType::Accumulation, Accumulation); +DEF(StatisticsType::Maximum, Maximum); +DEF(StatisticsType::Minimum, Minimum); DEF(StatisticsType::DifferenceFromStart, DifferenceEndMinusStart); -DEF(StatisticsType::RootMeanSquare, RootMeanSquare); -DEF(StatisticsType::StandardDeviation, StandardDeviation); -DEF(StatisticsType::Covariance, Covariance); -DEF(StatisticsType::DifferenceFromEnd, DifferenceStartMinusEnd); -DEF(StatisticsType::Ratio, Ratio); +DEF(StatisticsType::RootMeanSquare, RootMeanSquare); +DEF(StatisticsType::StandardDeviation, StandardDeviation); +DEF(StatisticsType::Covariance, Covariance); +DEF(StatisticsType::DifferenceFromEnd, DifferenceStartMinusEnd); +DEF(StatisticsType::Ratio, Ratio); DEF(StatisticsType::StandardizedAnomaly, StandardizedAnomaly); -DEF(StatisticsType::Summation, Summation); -DEF(StatisticsType::ReturnPeriod, ReturnPeriod); -DEF(StatisticsType::Median, Median); -DEF(StatisticsType::Severity, Severity); -DEF(StatisticsType::Mode, Mode); -DEF(StatisticsType::IndexProcessing, IndexProcessing); -DEF(StatisticsType::Default, Missing); +DEF(StatisticsType::Summation, Summation); +DEF(StatisticsType::ReturnPeriod, ReturnPeriod); +DEF(StatisticsType::Median, Median); +DEF(StatisticsType::Severity, Severity); +DEF(StatisticsType::Mode, Mode); +DEF(StatisticsType::IndexProcessing, IndexProcessing); +DEF(StatisticsType::Default, Missing); #undef DEF diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h index 1f9bb8784..969c75b30 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h @@ -75,8 +75,8 @@ std::size_t statisticsMatcher(const MarsDict_t& mars, const OptDict_t& opt) { // TODO: Don't handle products with timespan as non-statistical if they are not handled above! // if (has(mars, "timespan")) { - // throw utils::exceptions::Mars2GribMatcherException("MARS contains `timespan` but typeOfStatisticalProcessing - // is defined for param " + std::to_string(param), Here()); + // throw utils::exceptions::Mars2GribMatcherException("MARS contains `timespan` but + // typeOfStatisticalProcessing is defined for param " + std::to_string(param), Here()); // } return compile_time_registry_engine::MISSING; diff --git a/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h index a15e4ce39..e184921ef 100644 --- a/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h @@ -24,7 +24,8 @@ std::size_t tablesMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(TablesType::Default); } catch (...) { - std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `tables` concept", Here())); + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `tables` concept", Here())); } } diff --git a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h index 2bbfc7cce..80992d09b 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -103,7 +103,7 @@ struct ProductTime { /// Valid entries: `statisticalWindows[0 .. statisticalWindowCount)`. /// Ordering: outermost → innermost. const std::array statisticalWindows; - const std::size_t statisticalWindowCount; + const std::size_t statisticalWindowCount; /// Sampling increment of the innermost statistical loop. /// `std::nullopt` for instant products (§9.1). @@ -139,8 +139,8 @@ struct ProductTimeInput { eckit::DateTime simulationDateTime; - std::optional simulatedDateTime; ///< from `hdate` / `htime` - std::optional referenceDateTime; ///< from `fcyear` / `fcmonth` + std::optional simulatedDateTime; ///< from `hdate` / `htime` + std::optional referenceDateTime; ///< from `fcyear` / `fcmonth` /// Offset from `referenceDateTime` to `ProductTime::windowEnd`. long stepInSeconds{0}; @@ -153,7 +153,7 @@ struct ProductTimeInput { /// Temporal windows decoded from `stattype` (period part only), /// ordered outermost → innermost. std::array stattypeWindows{}; - std::size_t stattypeWindowCount{0}; + std::size_t stattypeWindowCount{0}; /// From `deductions::timeIncrementInSeconds_opt(mars, par)`, i.e. /// `par["timeIncrementInSeconds"]`. Absent for instants and for the AIFS @@ -189,9 +189,8 @@ inline long toSeconds_or_throw(std::string_view step) { } if (pos == 0) { - throw Mars2GribDeductionException( - "Invalid duration format (no numeric part): '" + std::string(step) + "'", - Here()); + throw Mars2GribDeductionException("Invalid duration format (no numeric part): '" + std::string(step) + "'", + Here()); } long value = 0; @@ -199,16 +198,14 @@ inline long toSeconds_or_throw(std::string_view step) { value = std::stol(std::string(step.substr(0, pos))); } catch (...) { - throw Mars2GribDeductionException( - "Invalid numeric value in duration: '" + std::string(step) + "'", Here()); + throw Mars2GribDeductionException("Invalid numeric value in duration: '" + std::string(step) + "'", Here()); } char unit = 'h'; // default unit if (pos < step.size()) { if (pos + 1 != step.size()) { throw Mars2GribDeductionException( - "Invalid duration format (trailing characters): '" + std::string(step) + "'", - Here()); + "Invalid duration format (trailing characters): '" + std::string(step) + "'", Here()); } unit = step[pos]; } @@ -223,10 +220,8 @@ inline long toSeconds_or_throw(std::string_view step) { case 'd': return value * 86400L; default: - throw Mars2GribDeductionException( - std::string("Unknown duration unit: '") + unit - + "', expected={h,m,s,d}", - Here()); + throw Mars2GribDeductionException(std::string("Unknown duration unit: '") + unit + "', expected={h,m,s,d}", + Here()); } } @@ -244,8 +239,7 @@ inline eckit::Date convert_YYYYMMDD2Date_or_throw(long YYYYMMDD) { return eckit::Date(YYYY, MM, DD); } catch (const eckit::Exception& e) { - throw Mars2GribDeductionException( - "Invalid date value '" + std::to_string(YYYYMMDD) + "': " + e.what(), Here()); + throw Mars2GribDeductionException("Invalid date value '" + std::to_string(YYYYMMDD) + "': " + e.what(), Here()); } } @@ -263,8 +257,7 @@ inline eckit::Time convert_hhmmss2Time_or_throw(long hhmmss) { return eckit::Time(hh, mm, ss); } catch (const eckit::Exception& e) { - throw Mars2GribDeductionException( - "Invalid time value '" + std::to_string(hhmmss) + "': " + e.what(), Here()); + throw Mars2GribDeductionException("Invalid time value '" + std::to_string(hhmmss) + "': " + e.what(), Here()); } } @@ -277,9 +270,7 @@ inline eckit::Time convert_hhmmss2Time_or_throw(long hhmmss) { /// `StatisticalWindow` allow-list `{Second, Day, Month}` (§3.1). /// inline bool isAllowedWindowUnit(tables::TimeUnit u) { - return u == tables::TimeUnit::Second - || u == tables::TimeUnit::Day - || u == tables::TimeUnit::Month; + return u == tables::TimeUnit::Second || u == tables::TimeUnit::Day || u == tables::TimeUnit::Month; } /// @@ -310,7 +301,7 @@ inline eckit::DateTime subtractCalendarMonths(const eckit::DateTime& dt, long co long newMonthIdx = total % 12L; if (newMonthIdx < 0) { newMonthIdx += 12L; - newYear -= 1L; + newYear -= 1L; } long newMonth = newMonthIdx + 1L; @@ -341,8 +332,7 @@ inline eckit::DateTime subtractSeconds(const eckit::DateTime& dt, long count) { /// Dispatches on `window.unit`. Precondition: `window.unit` is in the /// allow-list and any required alignment has been verified by the caller. /// -inline eckit::DateTime applyWindowSubtraction(const eckit::DateTime& windowEnd, - const StatisticalWindow& window) { +inline eckit::DateTime applyWindowSubtraction(const eckit::DateTime& windowEnd, const StatisticalWindow& window) { using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; switch (window.unit) { @@ -353,9 +343,8 @@ inline eckit::DateTime applyWindowSubtraction(const eckit::DateTime& windowEnd, case tables::TimeUnit::Month: return subtractCalendarMonths(windowEnd, window.count); default: - throw Mars2GribDeductionException( - "Internal error: applyWindowSubtraction called with disallowed unit", - Here()); + throw Mars2GribDeductionException("Internal error: applyWindowSubtraction called with disallowed unit", + Here()); } mars2gribUnreachable(); } @@ -419,8 +408,7 @@ inline std::string fmt(const StatisticalWindow& w) { /// /// @brief Format the populated prefix of a `StatisticalWindow` array. /// -inline std::string fmt(const std::array& a, - std::size_t count) { +inline std::string fmt(const std::array& a, std::size_t count) { std::string s{"["}; for (std::size_t i = 0; i < count; ++i) { if (i) @@ -461,36 +449,32 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { // §7.3: hdate / htime defaulting (the resolver has already enforced // §10.2; here we just apply the fall-through to simulationDateTime). - const eckit::DateTime simulatedDateTime = - input.simulatedDateTime.value_or(simulationDateTime); + const eckit::DateTime simulatedDateTime = input.simulatedDateTime.value_or(simulationDateTime); // §7.4: fcyear / fcmonth defaulting. - const eckit::DateTime referenceDateTime = - input.referenceDateTime.value_or(simulatedDateTime); + const eckit::DateTime referenceDateTime = input.referenceDateTime.value_or(simulatedDateTime); // --------------------------------------------------------- // §5.3: referenceDateTime >= simulatedDateTime (§10.4) // --------------------------------------------------------- if (referenceDateTime < simulatedDateTime) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.4]: referenceDateTime ('" - + fmt(referenceDateTime) + "') < simulatedDateTime ('" - + fmt(simulatedDateTime) + "')", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.4]: referenceDateTime ('" + + fmt(referenceDateTime) + "') < simulatedDateTime ('" + + fmt(simulatedDateTime) + "')", + Here()); } // --------------------------------------------------------- // windowEnd (§7.5) // --------------------------------------------------------- - const eckit::DateTime windowEnd = - referenceDateTime + static_cast(input.stepInSeconds); + const eckit::DateTime windowEnd = referenceDateTime + static_cast(input.stepInSeconds); // --------------------------------------------------------- // §9: window assembly // --------------------------------------------------------- std::array windows{}; - std::size_t windowCount = 0; - eckit::DateTime windowStart = windowEnd; + std::size_t windowCount = 0; + eckit::DateTime windowStart = windowEnd; const bool hasTimespanDuration = (input.timespanKind == TimespanKind::Duration); const bool hasTimespanNone = (input.timespanKind == TimespanKind::None); @@ -505,18 +489,17 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { } else if (hasTimespanDuration && nStat == 0) { // ----- §9.2: Old-style single-loop statistic ----- - windows[0] = input.timespan; + windows[0] = input.timespan; windowCount = 1; // windowStart computed after allow-list / positivity / alignment. } else if (hasTimespanDuration && nStat >= 1) { // ----- §9.3: Old-style multi-loop statistic ----- if (nStat + 1 > maxStatisticalWindows) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.15]: statisticalWindowCount (" - + std::to_string(nStat + 1) + ") > maxStatisticalWindows (" - + std::to_string(maxStatisticalWindows) + ")", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.15]: statisticalWindowCount (" + + std::to_string(nStat + 1) + ") > maxStatisticalWindows (" + + std::to_string(maxStatisticalWindows) + ")", + Here()); } for (std::size_t i = 0; i < nStat; ++i) { windows[i] = input.stattypeWindows[i]; @@ -540,21 +523,20 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { // §10.8: timespan = none with more than one stattype block. throw Mars2GribDeductionException( "ProductTime invariant violated [§10.8]: timespan='none' requires " - "exactly one stattype block, got " + std::to_string(nStat), + "exactly one stattype block, got " + + std::to_string(nStat), Here()); } else if (hasTimespanMissing && nStat >= 1) { // §10.6: stattype present but timespan missing. - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.6]: stattype present (" + std::to_string(nStat) - + " block(s)) but timespan is missing", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.6]: stattype present (" + + std::to_string(nStat) + " block(s)) but timespan is missing", + Here()); } else { // Defensive: all combinations should be covered above. throw Mars2GribDeductionException( - "ProductTime internal error: unhandled (timespanKind, stattypeCount) combination", - Here()); + "ProductTime internal error: unhandled (timespanKind, stattypeCount) combination", Here()); } // --------------------------------------------------------- @@ -564,18 +546,17 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { const StatisticalWindow& w = windows[i]; if (w.count <= 0) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.11]: statisticalWindows[" + std::to_string(i) - + "] has non-positive count (" + std::to_string(w.count) + ")", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.11]: statisticalWindows[" + + std::to_string(i) + "] has non-positive count (" + + std::to_string(w.count) + ")", + Here()); } if (!isAllowedWindowUnit(w.unit)) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.18(b)]: statisticalWindows[" + std::to_string(i) - + "] uses disallowed TimeUnit '" + fmt(w.unit) - + "', expected one of {second, day, month}", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.18(b)]: statisticalWindows[" + + std::to_string(i) + "] uses disallowed TimeUnit '" + fmt(w.unit) + + "', expected one of {second, day, month}", + Here()); } } @@ -589,8 +570,8 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { if (!isAtMidnight(windowEnd)) { throw Mars2GribDeductionException( "ProductTime invariant violated [§10.9]: outermost window is " - "calendar-day-aligned but windowEnd ('" + fmt(windowEnd) - + "') is not at hh=00,mm=00,ss=00", + "calendar-day-aligned but windowEnd ('" + + fmt(windowEnd) + "') is not at hh=00,mm=00,ss=00", Here()); } } @@ -598,8 +579,8 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { if (!isOnFirstOfMonthMidnight(windowEnd)) { throw Mars2GribDeductionException( "ProductTime invariant violated [§10.10]: outermost window is " - "calendar-month-aligned but windowEnd ('" + fmt(windowEnd) - + "') is not on day=1 at hh=00,mm=00,ss=00", + "calendar-month-aligned but windowEnd ('" + + fmt(windowEnd) + "') is not on day=1 at hh=00,mm=00,ss=00", Here()); } } @@ -612,10 +593,9 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { // §5.2: windowStart <= windowEnd (defensive) // --------------------------------------------------------- if (windowStart > windowEnd) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§5.2]: windowStart ('" + fmt(windowStart) - + "') > windowEnd ('" + fmt(windowEnd) + "')", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§5.2]: windowStart ('" + fmt(windowStart) + + "') > windowEnd ('" + fmt(windowEnd) + "')", + Here()); } // --------------------------------------------------------- @@ -624,18 +604,16 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { std::optional tInc = input.timeIncrementInSeconds; if (tInc.has_value() && tInc.value() < 0) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.14]: timeIncrementInSeconds < 0 ('" - + std::to_string(tInc.value()) + "')", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.14]: timeIncrementInSeconds < 0 ('" + + std::to_string(tInc.value()) + "')", + Here()); } if (windowCount >= 2 && !tInc.has_value()) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.13]: statisticalWindowCount (" - + std::to_string(windowCount) - + ") >= 2 requires timeIncrementInSeconds to be present", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.13]: statisticalWindowCount (" + + std::to_string(windowCount) + + ") >= 2 requires timeIncrementInSeconds to be present", + Here()); } // --------------------------------------------------------- @@ -646,26 +624,17 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { const bool c = !tInc.has_value(); if (!((a == b) && (b == c))) { throw Mars2GribDeductionException( - std::string("ProductTime invariant violated [§10.5]: tri-equivalence broken: ") - + "(windowStart==windowEnd)=" + (a ? "true" : "false") - + ", (statisticalWindowCount==0)=" + (b ? "true" : "false") - + ", (timeIncrementInSeconds==nullopt)=" + (c ? "true" : "false"), + std::string("ProductTime invariant violated [§10.5]: tri-equivalence broken: ") + + "(windowStart==windowEnd)=" + (a ? "true" : "false") + ", (statisticalWindowCount==0)=" + + (b ? "true" : "false") + ", (timeIncrementInSeconds==nullopt)=" + (c ? "true" : "false"), Here()); } // --------------------------------------------------------- // Construct the immutable ProductTime // --------------------------------------------------------- - return ProductTime{ - simulationDateTime, - simulatedDateTime, - referenceDateTime, - windowStart, - windowEnd, - windows, - windowCount, - tInc - }; + return ProductTime{simulationDateTime, simulatedDateTime, referenceDateTime, windowStart, + windowEnd, windows, windowCount, tInc}; } } // namespace metkit::mars2grib::backend::deductions::detail diff --git a/src/metkit/mars2grib/backend/deductions/detail/StatType.h b/src/metkit/mars2grib/backend/deductions/detail/StatType.h index 9692128c9..de5d8b017 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/StatType.h +++ b/src/metkit/mars2grib/backend/deductions/detail/StatType.h @@ -82,8 +82,8 @@ namespace metkit::mars2grib::backend::deductions::detail { /// - `sd` → `StandardDeviation` /// struct ParsedStatTypeBlock { - StatisticalWindow timeWindow; - tables::TypeOfStatisticalProcessing typeOfStatisticalProcessing; + StatisticalWindow timeWindow; + tables::TypeOfStatisticalProcessing typeOfStatisticalProcessing; }; // ============================================================= @@ -99,8 +99,7 @@ namespace impl { /// @throws Mars2GribDeductionException on unknown token (§10.16) or /// narrow-allow-list violation (§10.18 (a)). /// -inline StatisticalWindow decodePeriod_or_throw(std::string_view s, - const std::string& fullStatType) { +inline StatisticalWindow decodePeriod_or_throw(std::string_view s, const std::string& fullStatType) { using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; if (s == "da") @@ -108,11 +107,9 @@ inline StatisticalWindow decodePeriod_or_throw(std::string_view s, if (s == "mo") return StatisticalWindow{tables::TimeUnit::Month, 1}; - throw Mars2GribDeductionException( - "Invalid stattype period token [§10.16/§10.18(a)]: actual='" - + std::string(s) + "', expected={'da','mo'} (in stattype='" - + fullStatType + "')", - Here()); + throw Mars2GribDeductionException("Invalid stattype period token [§10.16/§10.18(a)]: actual='" + std::string(s) + + "', expected={'da','mo'} (in stattype='" + fullStatType + "')", + Here()); } /// @@ -121,8 +118,7 @@ inline StatisticalWindow decodePeriod_or_throw(std::string_view s, /// /// @throws Mars2GribDeductionException on unknown token (§10.16). /// -inline tables::TypeOfStatisticalProcessing -decodeOp_or_throw(std::string_view s, const std::string& fullStatType) { +inline tables::TypeOfStatisticalProcessing decodeOp_or_throw(std::string_view s, const std::string& fullStatType) { using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; if (s == "av") @@ -134,11 +130,9 @@ decodeOp_or_throw(std::string_view s, const std::string& fullStatType) { if (s == "sd") return tables::TypeOfStatisticalProcessing::StandardDeviation; - throw Mars2GribDeductionException( - "Invalid stattype operation token [§10.16]: actual='" - + std::string(s) + "', expected={'av','mn','mx','sd'} (in stattype='" - + fullStatType + "')", - Here()); + throw Mars2GribDeductionException("Invalid stattype operation token [§10.16]: actual='" + std::string(s) + + "', expected={'av','mn','mx','sd'} (in stattype='" + fullStatType + "')", + Here()); } } // namespace impl @@ -180,8 +174,7 @@ decodeOp_or_throw(std::string_view s, const std::string& fullStatType) { /// value of GRIB Code Table 4.10; consumers do not need to perform /// any further mapping. /// -inline std::vector -parse_StatType_or_throw(const std::string& stattype) { +inline std::vector parse_StatType_or_throw(const std::string& stattype) { using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; @@ -200,25 +193,22 @@ parse_StatType_or_throw(const std::string& stattype) { if (pos + 4 > stattype.size()) { throw Mars2GribDeductionException( "Invalid stattype format [§10.16]: incomplete 4-char block " - "at position " + std::to_string(pos) + " in '" + stattype + "'", + "at position " + + std::to_string(pos) + " in '" + stattype + "'", Here()); } - StatisticalWindow window = - impl::decodePeriod_or_throw(stattype.substr(pos, 2), stattype); - tables::TypeOfStatisticalProcessing op = - impl::decodeOp_or_throw(stattype.substr(pos + 2, 2), stattype); + StatisticalWindow window = impl::decodePeriod_or_throw(stattype.substr(pos, 2), stattype); + tables::TypeOfStatisticalProcessing op = impl::decodeOp_or_throw(stattype.substr(pos + 2, 2), stattype); blocks.push_back(ParsedStatTypeBlock{window, op}); pos += 4; if (pos < stattype.size()) { if (stattype[pos] != '_') { - throw Mars2GribDeductionException( - "Invalid stattype separator [§10.16] at position " - + std::to_string(pos) + " in '" + stattype - + "': expected '_'", - Here()); + throw Mars2GribDeductionException("Invalid stattype separator [§10.16] at position " + + std::to_string(pos) + " in '" + stattype + "': expected '_'", + Here()); } ++pos; } @@ -226,10 +216,9 @@ parse_StatType_or_throw(const std::string& stattype) { // Block-count limit: 1 or 2 blocks (§22.4 / §10.16). if (blocks.empty() || blocks.size() > 2) { - throw Mars2GribDeductionException( - "Invalid stattype [§10.16]: block count (" + std::to_string(blocks.size()) - + ") outside allowed range {1, 2} (in stattype='" + stattype + "')", - Here()); + throw Mars2GribDeductionException("Invalid stattype [§10.16]: block count (" + std::to_string(blocks.size()) + + ") outside allowed range {1, 2} (in stattype='" + stattype + "')", + Here()); } // Semantic validation (§10.17): at most one mo, at most one da, mo precedes da. @@ -239,29 +228,24 @@ parse_StatType_or_throw(const std::string& stattype) { if (blocks[i].timeWindow.unit == tables::TimeUnit::Month) { if (moIndex != -1) { throw Mars2GribDeductionException( - "Invalid stattype [§10.17] '" + stattype - + "': more than one 'mo' block", - Here()); + "Invalid stattype [§10.17] '" + stattype + "': more than one 'mo' block", Here()); } moIndex = static_cast(i); } if (blocks[i].timeWindow.unit == tables::TimeUnit::Day) { if (daIndex != -1) { throw Mars2GribDeductionException( - "Invalid stattype [§10.17] '" + stattype - + "': more than one 'da' block", - Here()); + "Invalid stattype [§10.17] '" + stattype + "': more than one 'da' block", Here()); } daIndex = static_cast(i); } } if (moIndex != -1 && daIndex != -1 && moIndex > daIndex) { - throw Mars2GribDeductionException( - "Invalid stattype [§10.17] '" + stattype - + "': blocks not in outermost-to-innermost order " - "('mo' must precede 'da')", - Here()); + throw Mars2GribDeductionException("Invalid stattype [§10.17] '" + stattype + + "': blocks not in outermost-to-innermost order " + "('mo' must precede 'da')", + Here()); } return blocks; diff --git a/src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h b/src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h index eb3867d10..fe6c7b29c 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h +++ b/src/metkit/mars2grib/backend/deductions/detail/StatisticalWindow.h @@ -65,7 +65,7 @@ namespace metkit::mars2grib::backend::deductions::detail { /// struct StatisticalWindow { tables::TimeUnit unit{tables::TimeUnit::Second}; - long count{0}; + long count{0}; }; } // namespace metkit::mars2grib::backend::deductions::detail diff --git a/src/metkit/mars2grib/backend/deductions/productTime.h b/src/metkit/mars2grib/backend/deductions/productTime.h index a89572fd1..74c9c2c30 100644 --- a/src/metkit/mars2grib/backend/deductions/productTime.h +++ b/src/metkit/mars2grib/backend/deductions/productTime.h @@ -107,9 +107,7 @@ namespace metkit::mars2grib::backend::deductions { /// (§14). /// template -detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, - const ParDict_t& par, - const OptDict_t& opt) { +detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::get_opt; using metkit::mars2grib::utils::dict_traits::get_or_throw; @@ -136,35 +134,30 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, if (hasDate && hasTime) { const long marsDate = get_or_throw(mars, "date"); const long marsTime = get_or_throw(mars, "time"); - simulationDateTime = eckit::DateTime( - detail::convert_YYYYMMDD2Date_or_throw(marsDate), - detail::convert_hhmmss2Time_or_throw(marsTime)); + simulationDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsDate), + detail::convert_hhmmss2Time_or_throw(marsTime)); } else if (!hasDate && !hasTime && hasFcYear && hasFcMonth) { // R2 default: simulationDateTime := DateTime(fcyear, fcmonth, 1, 00:00:00). const long fcYear = get_or_throw(mars, "fcyear"); const long fcMonth = get_or_throw(mars, "fcmonth"); try { - simulationDateTime = eckit::DateTime( - eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); + simulationDateTime = eckit::DateTime(eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); } catch (const eckit::Exception& e) { throw Mars2GribDeductionException( - "Invalid (fcyear, fcmonth) for default simulationDateTime: " - + std::string(e.what()), - Here()); + "Invalid (fcyear, fcmonth) for default simulationDateTime: " + std::string(e.what()), Here()); } } else { // §10.1: date or time missing without an applicable fallback. - throw Mars2GribDeductionException( - std::string("ProductTime invariant violated [§10.1]: ") - + "missing 'date'/'time' without (fcyear,fcmonth) fallback. " - + "has(date)=" + (hasDate ? "true" : "false") - + ", has(time)=" + (hasTime ? "true" : "false") - + ", has(fcyear)=" + (hasFcYear ? "true" : "false") - + ", has(fcmonth)=" + (hasFcMonth ? "true" : "false"), - Here()); + throw Mars2GribDeductionException(std::string("ProductTime invariant violated [§10.1]: ") + + "missing 'date'/'time' without (fcyear,fcmonth) fallback. " + + "has(date)=" + (hasDate ? "true" : "false") + + ", has(time)=" + (hasTime ? "true" : "false") + + ", has(fcyear)=" + (hasFcYear ? "true" : "false") + + ", has(fcmonth)=" + (hasFcMonth ? "true" : "false"), + Here()); } // ========================================================= @@ -180,22 +173,19 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, } else if (hasHdate && !hasHtime) { const long marsHdate = get_or_throw(mars, "hdate"); - simulatedDateTime = eckit::DateTime( - detail::convert_YYYYMMDD2Date_or_throw(marsHdate), - eckit::Time(0, 0, 0)); + simulatedDateTime = + eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsHdate), eckit::Time(0, 0, 0)); } else if (hasHdate && hasHtime) { const long marsHdate = get_or_throw(mars, "hdate"); const long marsHtime = get_or_throw(mars, "htime"); - simulatedDateTime = eckit::DateTime( - detail::convert_YYYYMMDD2Date_or_throw(marsHdate), - detail::convert_hhmmss2Time_or_throw(marsHtime)); + simulatedDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsHdate), + detail::convert_hhmmss2Time_or_throw(marsHtime)); } else { // §10.2: htime present without hdate. - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.2]: 'htime' present without 'hdate'", - Here()); + throw Mars2GribDeductionException("ProductTime invariant violated [§10.2]: 'htime' present without 'hdate'", + Here()); } // ========================================================= @@ -210,24 +200,20 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const long fcYear = get_or_throw(mars, "fcyear"); const long fcMonth = get_or_throw(mars, "fcmonth"); try { - referenceDateTime = eckit::DateTime( - eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); + referenceDateTime = eckit::DateTime(eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); } catch (const eckit::Exception& e) { throw Mars2GribDeductionException( - "Invalid (fcyear, fcmonth) for referenceDateTime: " - + std::string(e.what()), - Here()); + "Invalid (fcyear, fcmonth) for referenceDateTime: " + std::string(e.what()), Here()); } } else { // §10.3: exactly one of fcyear / fcmonth present. - throw Mars2GribDeductionException( - std::string("ProductTime invariant violated [§10.3]: ") - + "exactly one of 'fcyear'/'fcmonth' present. " - + "has(fcyear)=" + (hasFcYear ? "true" : "false") - + ", has(fcmonth)=" + (hasFcMonth ? "true" : "false"), - Here()); + throw Mars2GribDeductionException(std::string("ProductTime invariant violated [§10.3]: ") + + "exactly one of 'fcyear'/'fcmonth' present. " + + "has(fcyear)=" + (hasFcYear ? "true" : "false") + + ", has(fcmonth)=" + (hasFcMonth ? "true" : "false"), + Here()); } // ========================================================= @@ -254,7 +240,7 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, // §7.6: timespan // ========================================================= - detail::TimespanKind timespanKind = detail::TimespanKind::Missing; + detail::TimespanKind timespanKind = detail::TimespanKind::Missing; detail::StatisticalWindow timespan{}; if (has(mars, "timespan")) { @@ -264,8 +250,8 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, timespanKind = detail::TimespanKind::None; } else { - timespanKind = detail::TimespanKind::Duration; - timespan.unit = tables::TimeUnit::Second; + timespanKind = detail::TimespanKind::Duration; + timespan.unit = tables::TimeUnit::Second; timespan.count = detail::toSeconds_or_throw(tsStr.value()); } } @@ -288,18 +274,16 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, // ========================================================= std::array stattypeWindows{}; - std::size_t stattypeWindowCount = 0; + std::size_t stattypeWindowCount = 0; if (has(mars, "stattype")) { - const std::string statTypeVal = get_or_throw(mars, "stattype"); - const std::vector blocks = - detail::parse_StatType_or_throw(statTypeVal); + const std::string statTypeVal = get_or_throw(mars, "stattype"); + const std::vector blocks = detail::parse_StatType_or_throw(statTypeVal); if (blocks.size() > detail::maxStatisticalWindows) { throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.15]: stattype yields " - + std::to_string(blocks.size()) + " block(s) > maxStatisticalWindows (" - + std::to_string(detail::maxStatisticalWindows) + ")", + "ProductTime invariant violated [§10.15]: stattype yields " + std::to_string(blocks.size()) + + " block(s) > maxStatisticalWindows (" + std::to_string(detail::maxStatisticalWindows) + ")", Here()); } @@ -339,19 +323,16 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, MARS2GRIB_LOG_RESOLVE([&]() { std::string msg = "`ProductTime` resolved from input dictionaries: "; msg += "simulationDateTime='" + detail::fmt(pt.simulationDateTime) + "'"; - msg += " simulatedDateTime='" + detail::fmt(pt.simulatedDateTime) + "'"; - msg += " referenceDateTime='" + detail::fmt(pt.referenceDateTime) + "'"; - msg += " windowStart='" + detail::fmt(pt.windowStart) + "'"; - msg += " windowEnd='" + detail::fmt(pt.windowEnd) + "'"; - msg += " statisticalWindowCount='" - + std::to_string(pt.statisticalWindowCount) + "'"; - msg += " statisticalWindows=" - + detail::fmt(pt.statisticalWindows, pt.statisticalWindowCount); - msg += " timeIncrementInSeconds='" - + (pt.timeIncrementInSeconds.has_value() - ? std::to_string(pt.timeIncrementInSeconds.value()) - : std::string("missing")) - + "'"; + msg += " simulatedDateTime='" + detail::fmt(pt.simulatedDateTime) + "'"; + msg += " referenceDateTime='" + detail::fmt(pt.referenceDateTime) + "'"; + msg += " windowStart='" + detail::fmt(pt.windowStart) + "'"; + msg += " windowEnd='" + detail::fmt(pt.windowEnd) + "'"; + msg += " statisticalWindowCount='" + std::to_string(pt.statisticalWindowCount) + "'"; + msg += " statisticalWindows=" + detail::fmt(pt.statisticalWindows, pt.statisticalWindowCount); + msg += " timeIncrementInSeconds='" + + (pt.timeIncrementInSeconds.has_value() ? std::to_string(pt.timeIncrementInSeconds.value()) + : std::string("missing")) + + "'"; return msg; }()); @@ -360,8 +341,7 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, catch (...) { // §11: nested rethrow with context. - std::throw_with_nested( - Mars2GribDeductionException("Unable to resolve ProductTime", Here())); + std::throw_with_nested(Mars2GribDeductionException("Unable to resolve ProductTime", Here())); } // Remove compiler warning diff --git a/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h b/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h index 29947dfb9..c32905003 100644 --- a/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h +++ b/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h @@ -85,12 +85,14 @@ namespace metkit::mars2grib::backend::deductions { /// Local classification of MARS into one of the §9 cases (no `ProductTime` /// dependency): /// -/// | MARS state | §9 case | Output size | Output content | +/// | MARS state | §9 case | Output size | Output content | /// |---------------------------------------------------------|---------|------------:|---------------------------------------------------------------------------------| -/// | `stattype` absent, `timespan` is a Duration | §9.2 | 1 | `[innerTypeOfStatisticalProcessing]` | -/// | `stattype` has N blocks, `timespan` is a Duration | §9.3 | N + 1 | `[op(block_0), …, op(block_{N-1}), innerTypeOfStatisticalProcessing]` | -/// | `stattype` has 1 block, `timespan = "none"` | §9.4 | 1 | `[innerTypeOfStatisticalProcessing]` (after equality assertion, §23.6) | -/// | `stattype` absent, `timespan` absent | §9.1 | unreachable | (precondition violated, see §23.4 — no defensive check) | +/// | `stattype` absent, `timespan` is a Duration | §9.2 | 1 | +/// `[innerTypeOfStatisticalProcessing]` | | `stattype` has N blocks, +/// `timespan` is a Duration | §9.3 | N + 1 | `[op(block_0), …, op(block_{N-1}), +/// innerTypeOfStatisticalProcessing]` | | `stattype` has 1 block, `timespan = "none"` | §9.4 | +/// 1 | `[innerTypeOfStatisticalProcessing]` (after equality assertion, §23.6) | | `stattype` absent, +/// `timespan` absent | §9.1 | unreachable | (precondition violated, see §23.4 — no defensive check) | /// /// The innermost slot is **always** filled by the /// `innerTypeOfStatisticalProcessing` argument — never by a parsed block. @@ -147,12 +149,9 @@ namespace metkit::mars2grib::backend::deductions { /// `resolve_ProductTime_or_throw`. /// template -std::vector -resolve_TypeOfStatisticalProcessing_or_throw( - tables::TypeOfStatisticalProcessing innerTypeOfStatisticalProcessing, - const MarsDict_t& mars, - const ParDict_t& par, - const OptDict_t& opt) { +std::vector resolve_TypeOfStatisticalProcessing_or_throw( + tables::TypeOfStatisticalProcessing innerTypeOfStatisticalProcessing, const MarsDict_t& mars, const ParDict_t& par, + const OptDict_t& opt) { using metkit::mars2grib::utils::dict_traits::get_opt; using metkit::mars2grib::utils::dict_traits::get_or_throw; @@ -206,7 +205,7 @@ resolve_TypeOfStatisticalProcessing_or_throw( std::vector blocks; if (hasStatType) { const std::string statTypeVal = get_or_throw(mars, "stattype"); - blocks = detail::parse_StatType_or_throw(statTypeVal); + blocks = detail::parse_StatType_or_throw(statTypeVal); } // ========================================================= @@ -255,19 +254,16 @@ resolve_TypeOfStatisticalProcessing_or_throw( Here()); } - const tables::TypeOfStatisticalProcessing parsedOp = - blocks[0].typeOfStatisticalProcessing; + const tables::TypeOfStatisticalProcessing parsedOp = blocks[0].typeOfStatisticalProcessing; if (parsedOp != innerTypeOfStatisticalProcessing) { throw Mars2GribDeductionException( std::string("typeOfStatisticalProcessing invariant violated " "[§10.12]: fakeDoubleLoop disagreement: parsed " - "stattype block operation ('") - + tables::enum2name_TypeOfStatisticalProcessing_or_throw(parsedOp) - + "') != innerTypeOfStatisticalProcessing argument ('" - + tables::enum2name_TypeOfStatisticalProcessing_or_throw( - innerTypeOfStatisticalProcessing) - + "')", + "stattype block operation ('") + + tables::enum2name_TypeOfStatisticalProcessing_or_throw(parsedOp) + + "') != innerTypeOfStatisticalProcessing argument ('" + + tables::enum2name_TypeOfStatisticalProcessing_or_throw(innerTypeOfStatisticalProcessing) + "')", Here()); } @@ -293,21 +289,16 @@ resolve_TypeOfStatisticalProcessing_or_throw( // ========================================================= MARS2GRIB_LOG_RESOLVE([&]() { - std::string msg = - "`typeOfStatisticalProcessing` resolved from input dictionaries: "; - msg += "innerTypeOfStatisticalProcessing='" - + tables::enum2name_TypeOfStatisticalProcessing_or_throw( - innerTypeOfStatisticalProcessing) - + "'"; + std::string msg = "`typeOfStatisticalProcessing` resolved from input dictionaries: "; + msg += "innerTypeOfStatisticalProcessing='" + + tables::enum2name_TypeOfStatisticalProcessing_or_throw(innerTypeOfStatisticalProcessing) + "'"; msg += " size='" + std::to_string(out.size()) + "'"; msg += " typesOfStatisticalProcessing=["; for (std::size_t i = 0; i < out.size(); ++i) { if (i) { msg += ","; } - msg += "'" - + tables::enum2name_TypeOfStatisticalProcessing_or_throw(out[i]) - + "'"; + msg += "'" + tables::enum2name_TypeOfStatisticalProcessing_or_throw(out[i]) + "'"; } msg += "]"; return msg; @@ -318,9 +309,7 @@ resolve_TypeOfStatisticalProcessing_or_throw( catch (...) { // §11 / §23.9: nested rethrow with context. - std::throw_with_nested( - Mars2GribDeductionException( - "Unable to resolve typeOfStatisticalProcessing", Here())); + std::throw_with_nested(Mars2GribDeductionException("Unable to resolve typeOfStatisticalProcessing", Here())); } // Remove compiler warning From 5c612c6e66f20ee70a40b195e57b48a01cdcd36e Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 5 May 2026 12:37:50 +0000 Subject: [PATCH 22/35] mars2grib: Add documentation for all matchers --- .../concepts/analysis/analysisMatcher.h | 42 ++++ .../concepts/composition/compositionMatcher.h | 44 ++++ .../concepts/data-type/dataTypeMatcher.h | 42 ++++ .../backend/concepts/derived/derivedMatcher.h | 43 ++++ .../backend/concepts/destine/destineMatcher.h | 44 ++++ .../concepts/ensemble/ensembleMatcher.h | 43 ++++ .../generatingProcessMatcher.h | 42 ++++ .../backend/concepts/level/levelMatcher.h | 225 +++++++++++++++++- .../concepts/longrange/longrangeMatcher.h | 43 ++++ .../backend/concepts/mars/marsMatcher.h | 43 ++++ .../backend/concepts/nil/nilMatcher.h | 41 ++++ .../backend/concepts/origin/originMatcher.h | 41 ++++ .../backend/concepts/packing/packingMatcher.h | 43 ++++ .../backend/concepts/param/paramMatcher.h | 41 ++++ .../point-in-time/pointInTimeMatcher.h | 44 ++++ .../reference-time/referenceTimeMatcher.h | 42 ++++ .../representation/representationMatcher.h | 44 ++++ .../concepts/satellite/satelliteMatcher.h | 43 ++++ .../shapeOfTheEarthMatcher.h | 43 ++++ .../concepts/statistics/statisticsMatcher.h | 45 ++++ .../backend/concepts/tables/tablesMatcher.h | 41 ++++ .../backend/concepts/wave/waveMatcher.h | 44 ++++ 22 files changed, 1116 insertions(+), 7 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h index 33a8f3ce4..02f6ad327 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 analysisMatcher.h +/// @brief Entry-level matcher for the GRIB `analysis` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether the **analysis concept** is active for a MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -12,6 +36,24 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `analysis` concept variant. +/// +/// The concept is active when the MARS request contains `anoffset`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `AnalysisType::Default`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t analysisMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h index 510f49173..80bcfadc7 100644 --- a/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h @@ -1,3 +1,28 @@ +/* + * (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 compositionMatcher.h +/// @brief Entry-level matcher for the GRIB `composition` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether chemical or aerosol composition metadata is active for a +/// MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +38,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `composition` concept variant. +/// +/// The concept is active as `CompositionType::Chem` when `chem` is present, +/// and as `CompositionType::Aerosol` when `wavelength` is present. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local composition variant index, or +/// `compile_time_registry_engine::MISSING` when no composition metadata is active. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t compositionMatcher(const MarsDict_t& mars, const OptDict_t& opt) { diff --git a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h index 745b2dc08..ffa8c10a6 100644 --- a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 dataTypeMatcher.h +/// @brief Entry-level matcher for the GRIB `dataType` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// activate the default data-type concept variant. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -11,6 +35,24 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `dataType` concept variant. +/// +/// The data-type concept is always active and resolves to +/// `DataTypeType::Default`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `DataTypeType::Default`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t dataTypeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index 87c97def4..b49499f76 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 derivedMatcher.h +/// @brief Entry-level matcher for the GRIB `derived` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether derived ensemble-product metadata is active for a request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `derived` concept variant. +/// +/// The concept is active for ensemble-derived MARS `type` values such as +/// ensemble mean, spread, EFI, and shift-of-tails products. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `DerivedType::Default`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { diff --git a/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h index f300f6d5a..d4e1b0302 100644 --- a/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 destineMatcher.h +/// @brief Entry-level matcher for the GRIB `destine` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether DestinE digital-twin metadata is active for a MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,26 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `destine` concept variant. +/// +/// The concept is active for class `d1` requests without `anoffset`. The +/// `dataset` keyword selects the concrete DestinE variant. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local DestinE variant index, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If required DestinE metadata is missing, unsupported `dataset` values are +/// encountered, or lower-level matcher evaluation fails. Lower-level exceptions +/// are preserved through `std::throw_with_nested`. +/// template std::size_t destineMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index 3f6e852af..89d3663a9 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 ensembleMatcher.h +/// @brief Entry-level matcher for the GRIB `ensemble` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether ensemble-member metadata is active for a MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `ensemble` concept variant. +/// +/// The concept is active as `EnsembleType::Individual` when the MARS request +/// contains `number`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `EnsembleType::Individual`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t ensembleMatcher(const MarsDict_t& mars, const OptDict_t& opt) { diff --git a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h index 3fe16ea70..af7f71d4e 100644 --- a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 generatingProcessMatcher.h +/// @brief Entry-level matcher for the GRIB `generatingProcess` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// activate the default generating-process concept variant. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -11,6 +35,24 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `generatingProcess` concept variant. +/// +/// The generating-process concept is always active and resolves to +/// `GeneratingProcessType::Default`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `GeneratingProcessType::Default`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t generatingProcessMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index 26e2a2591..b36228456 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -7,6 +7,31 @@ * granted to it by virtue of its status as an intergovernmental organisation nor * does it submit to any jurisdiction. */ + +/// +/// @file levelMatcher.h +/// @brief Entry-level matcher for the GRIB `level` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// map MARS `levtype`, `param`, and, where required, `levelist` metadata onto +/// GRIB level concept variants. +/// +/// The implementation is split into levtype-specific helper functions in the +/// internal `impl` namespace and a public entry-level matcher. Each helper is +/// responsible for one MARS level type and returns a local `LevelType` variant +/// index. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// Nested error handling is intentionally applied both at the entry point and +/// inside the levtype-specific helpers so diagnostics retain the full matcher +/// call chain and the levtype-specific mapping context. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -25,6 +50,23 @@ namespace metkit::mars2grib::backend::concepts_ { namespace impl { +/// +/// @brief Match surface-level MARS parameters. +/// +/// Maps `levtype=sfc` parameters onto their GRIB level concept variant. +/// Wave-period parameters deliberately return `MISSING` because they are handled +/// by the wave concept rather than by level encoding. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index, or +/// `compile_time_registry_engine::MISSING` for surface products owned by another +/// concept. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no surface-level mapping exists. Lower-level exceptions are preserved +/// through `std::throw_with_nested`. +/// inline std::size_t matchSFC(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -158,6 +200,19 @@ inline std::size_t matchSFC(const long param) { } } +/// +/// @brief Match height-level MARS parameters. +/// +/// Maps `levtype=hl` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no height-level mapping exists. Lower-level exceptions are preserved +/// through `std::throw_with_nested`. +/// inline std::size_t matchHL(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -177,6 +232,19 @@ inline std::size_t matchHL(const long param) { } } +/// +/// @brief Match model-level MARS parameters. +/// +/// Maps `levtype=ml` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no model-level mapping exists. Lower-level exceptions are preserved +/// through `std::throw_with_nested`. +/// inline std::size_t matchML(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -208,6 +276,22 @@ inline std::size_t matchML(const long param) { } } +/// +/// @brief Match pressure-level MARS parameters. +/// +/// Maps `levtype=pl` parameters onto pressure-level GRIB variants. The MARS +/// `levelist` value determines whether the pressure level is interpreted in hPa +/// or Pa. +/// +/// @param[in] param MARS parameter identifier +/// @param[in] level MARS pressure level value from `levelist` +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no pressure-level mapping exists. Lower-level exceptions are preserved +/// through `std::throw_with_nested`. +/// inline std::size_t matchPL(const long param, const long level) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -236,18 +320,57 @@ inline std::size_t matchPL(const long param, const long level) { } } + +/// +/// @brief Match flight-level MARS parameters. +/// +/// Maps `levtype=fl` parameters onto their GRIB level concept variant. +/// The MARS `levelist` value is not relevant for flight levels because the +/// conversion from flight level to pressure level is not one-to-one and requires additional +/// metadata (e.g. temperature profile) that is not available at the point of level concept matching. +/// Therefore, all flight levels map to the same GRIB level concept variant regardless of `levelist`. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no flight-level mapping exists. Lower-level exceptions are preserved +/// through `std::throw_with_nested`. +/// inline std::size_t matchFL(const long param) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 260290)) { - return static_cast(LevelType::FlightLevel); - } + try { + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + + if (matchAny(param, 260290)) { + return static_cast(LevelType::FlightLevel); + } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype FL", Here()); + throw utils::exceptions::Mars2GribMatcherException( + "No mapping exists for param \"" + std::to_string(param) + "\" on levtype FL", Here()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "fl", "Unable to match `level` concept for levtype \"fl\"", Here())); + } } + +/// +/// @brief Match potential-temperature-level MARS parameters. +/// +/// Maps `levtype=pt` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no potential-temperature-level mapping exists. Lower-level exceptions are +/// preserved through `std::throw_with_nested`. +/// inline std::size_t matchPT(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -270,6 +393,19 @@ inline std::size_t matchPT(const long param) { } +/// +/// @brief Match potential-vorticity-level MARS parameters. +/// +/// Maps `levtype=pv` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no potential-vorticity-level mapping exists. Lower-level exceptions are +/// preserved through `std::throw_with_nested`. +/// inline std::size_t matchPV(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -290,6 +426,19 @@ inline std::size_t matchPV(const long param) { } } +/// +/// @brief Match soil-level MARS parameters. +/// +/// Maps `levtype=sol` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no soil-level mapping exists. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// inline std::size_t matchSOL(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -314,6 +463,19 @@ inline std::size_t matchSOL(const long param) { } } +/// +/// @brief Match abstract-level MARS parameters. +/// +/// Maps `levtype=al` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no abstract-level mapping exists. Lower-level exceptions are preserved +/// through `std::throw_with_nested`. +/// inline std::size_t matchAL(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -332,6 +494,19 @@ inline std::size_t matchAL(const long param) { } } +/// +/// @brief Match two-dimensional ocean-level MARS parameters. +/// +/// Maps `levtype=o2d` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no two-dimensional ocean-level mapping exists. Lower-level exceptions are +/// preserved through `std::throw_with_nested`. +/// inline std::size_t matchO2D(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -381,6 +556,19 @@ inline std::size_t matchO2D(const long param) { } } +/// +/// @brief Match three-dimensional ocean-level MARS parameters. +/// +/// Maps `levtype=o3d` parameters onto their GRIB level concept variant. +/// +/// @param[in] param MARS parameter identifier +/// +/// @return Local `LevelType` variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If no three-dimensional ocean-level mapping exists. Lower-level exceptions +/// are preserved through `std::throw_with_nested`. +/// inline std::size_t matchO3D(const long param) { try { using metkit::mars2grib::util::param_matcher::matchAny; @@ -404,6 +592,29 @@ inline std::size_t matchO3D(const long param) { } // namespace impl +/// +/// @brief Match the `level` concept variant. +/// +/// The matcher first excludes products owned by other concepts, such as wave +/// spectra and satellite products. For remaining products, it reads `param` and +/// `levtype` and dispatches to the corresponding levtype-specific helper. +/// Pressure-level products additionally require `levelist`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local level variant index, or +/// `compile_time_registry_engine::MISSING` when the level concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If required MARS metadata is missing, `levtype` is unsupported, no mapping +/// exists for the `(levtype, param)` combination, or lower-level matcher +/// evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t levelMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h b/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h index 9e5577f36..e42d39422 100644 --- a/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 longrangeMatcher.h +/// @brief Entry-level matcher for the GRIB `longrange` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether long-range forecast system metadata is active for a request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `longrange` concept variant. +/// +/// The concept is active as `LongrangeType::Default` when both `method` and +/// `system` are present in the MARS request. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `LongrangeType::Default`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t longrangeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h b/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h index 4dfcbf008..44f68a45b 100644 --- a/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 marsMatcher.h +/// @brief Entry-level matcher for the GRIB `mars` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether MARS provenance metadata is active for a request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -12,6 +36,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `mars` concept variant. +/// +/// The concept is active as `MarsType::Default` when `class`, `type`, `stream`, +/// and `expver` are present in the MARS request. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `MarsType::Default`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t marsMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h b/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h index 098c04679..35a35b8f0 100644 --- a/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 nilMatcher.h +/// @brief Entry-level matcher for the GRIB `nil` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// activate the default nil concept variant. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -11,6 +35,23 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `nil` concept variant. +/// +/// The nil concept is always active and resolves to `NilType::Default`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `NilType::Default`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t nilMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h index f7919c777..6ccd604c5 100644 --- a/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 originMatcher.h +/// @brief Entry-level matcher for the GRIB `origin` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// activate the default originating-centre concept variant. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -11,6 +35,23 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `origin` concept variant. +/// +/// The origin concept is always active and resolves to `OriginType::Default`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `OriginType::Default`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t originMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h index 35d7fd85f..14aa0c23d 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 packingMatcher.h +/// @brief Entry-level matcher for the GRIB `packing` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// select the GRIB data packing variant requested by MARS metadata. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `packing` concept variant. +/// +/// The matcher maps the MARS `packing` keyword onto the corresponding local +/// packing concept variant. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local packing variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If the `packing` keyword is missing, has an unsupported value, or lower-level +/// matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t packingMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h b/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h index 6f79a981c..0e78d8209 100644 --- a/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 paramMatcher.h +/// @brief Entry-level matcher for the GRIB `param` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// activate the parameter-identity concept variant. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -11,6 +35,23 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `param` concept variant. +/// +/// The parameter concept is always active and resolves to `ParamType::ParamId`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `ParamType::ParamId`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t paramMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { 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 1b5453e14..330a21d45 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 pointInTimeMatcher.h +/// @brief Entry-level matcher for the GRIB `pointInTime` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// identify products that use point-in-time forecast semantics. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,26 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `pointInTime` concept variant. +/// +/// The matcher classifies known non-statistical, wave, satellite, and chemical +/// parameters as `PointInTimeType::Default`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `PointInTimeType::Default`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If the required parameter metadata cannot be read or lower-level matcher +/// evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h index 3c9135cb2..e0e94eb46 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 referenceTimeMatcher.h +/// @brief Entry-level matcher for the GRIB `referenceTime` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// choose between standard and reforecast reference-time handling. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -12,6 +36,24 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `referenceTime` concept variant. +/// +/// The matcher selects `ReferenceTimeType::Reforecast` when `hdate` is present +/// in the MARS request, and `ReferenceTimeType::Standard` otherwise. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for the selected reference-time variant. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t referenceTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h b/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h index 386a7ad7f..1fd6420fa 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 representationMatcher.h +/// @brief Entry-level matcher for the GRIB `representation` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// select the GRIB grid representation variant from MARS metadata. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -14,6 +38,26 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `representation` concept variant. +/// +/// Spherical harmonics are selected when `truncation` is present. Otherwise the +/// matcher builds the eckit geometry from MARS `grid` and maps the resulting +/// grid type onto the corresponding representation variant. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local representation variant index. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If the grid cannot be resolved, is unsupported, or lower-level matcher +/// evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t representationMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h index d5f746e3e..cd6a4eaf6 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 satelliteMatcher.h +/// @brief Entry-level matcher for the GRIB `satellite` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether satellite product metadata is active for a MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -12,6 +36,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `satellite` concept variant. +/// +/// The concept is active as `SatelliteType::Default` when `channel`, `ident`, +/// and `instrument` are present in the MARS request. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `SatelliteType::Default`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t satelliteMatcher(const MarsDict_t& mars, const OptDict_t& opt) { diff --git a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h index c2a568c82..47d707189 100644 --- a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 shapeOfTheEarthMatcher.h +/// @brief Entry-level matcher for the GRIB `shapeOfTheEarth` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether shape-of-the-earth metadata should be encoded. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,25 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `shapeOfTheEarth` concept variant. +/// +/// The concept is active as `ShapeOfTheEarthType::Default` for grid-point +/// fields. It is inactive for spherical harmonics, identified by `truncation`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `ShapeOfTheEarthType::Default`, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t shapeOfTheEarthMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h index 969c75b30..d376be73b 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 statisticsMatcher.h +/// @brief Entry-level matcher for the GRIB `statistics` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// identify statistical-processing semantics from the MARS parameter code. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -14,6 +38,27 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `statistics` concept variant. +/// +/// The matcher maps known statistical MARS parameter codes onto the appropriate +/// local `StatisticsType` variant. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local statistics variant index, or +/// `compile_time_registry_engine::MISSING` when no statistical-processing +/// concept is active. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If the required parameter metadata cannot be read or lower-level matcher +/// evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t statisticsMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h index e184921ef..e8de6f57f 100644 --- a/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 tablesMatcher.h +/// @brief Entry-level matcher for the GRIB `tables` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// activate the default GRIB tables concept variant. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -13,6 +37,23 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `tables` concept variant. +/// +/// The tables concept is always active and resolves to `TablesType::Default`. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local variant index for `TablesType::Default`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If matcher evaluation fails. Lower-level exceptions are preserved through +/// `std::throw_with_nested`. +/// template std::size_t tablesMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { diff --git a/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h b/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h index 51a41abee..4fbebe3cf 100644 --- a/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h @@ -1,3 +1,27 @@ +/* + * (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 waveMatcher.h +/// @brief Entry-level matcher for the GRIB `wave` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// identify wave-period and wave-spectra products. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active, +/// - wrap runtime failures as nested `Mars2GribMatcherException` instances. +/// +/// @ingroup mars2grib_backend_concepts +/// #pragma once // System include @@ -14,6 +38,26 @@ namespace metkit::mars2grib::backend::concepts_ { +/// +/// @brief Match the `wave` concept variant. +/// +/// Wave-period parameters resolve to `WaveType::Period`. Wave spectra resolve +/// to `WaveType::Spectra` and require both `frequency` and `direction` keys. +/// +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam OptDict_t Type of the options dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] opt Options dictionary +/// +/// @return Local wave variant index, or +/// `compile_time_registry_engine::MISSING` when the concept is inactive. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribMatcherException +/// If wave spectra metadata is incomplete, the required parameter metadata +/// cannot be read, or lower-level matcher evaluation fails. Lower-level +/// exceptions are preserved through `std::throw_with_nested`. +/// template std::size_t waveMatcher(const MarsDict_t& mars, const OptDict_t& opt) { try { From 45d943b62e683a94ba8e28b715ff5f3c0b9c5154 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 17:37:44 +0000 Subject: [PATCH 23/35] Run clang-format --- .../mars2grib/backend/concepts/AllConcepts.h | 13 +++++----- .../brightnessTemperatureConceptDescriptor.h | 25 +++++------------- .../brightnessTemperatureEncoding.h | 25 ++++++------------ .../brightnessTemperatureEnum.h | 6 ++--- .../brightnessTemperatureMatcher.h | 5 ++-- .../backend/concepts/derived/derivedMatcher.h | 1 - .../concepts/ensemble/ensembleMatcher.h | 3 ++- .../concepts/model-error/modelErrorEncoding.h | 10 +++---- .../concepts/model-error/modelErrorMatcher.h | 15 +++++------ .../concepts/packing/packingEncoding.h | 8 +++--- .../representation/representationEncoding.h | 17 +++++------- .../concepts/satellite/satelliteEncoding.h | 1 - .../concepts/satellite/satelliteEnum.h | 2 +- .../backend/deductions/componentIndex.h | 26 ++++++++++--------- 14 files changed, 64 insertions(+), 93 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/AllConcepts.h b/src/metkit/mars2grib/backend/concepts/AllConcepts.h index 6edf8b148..229cf79ce 100644 --- a/src/metkit/mars2grib/backend/concepts/AllConcepts.h +++ b/src/metkit/mars2grib/backend/concepts/AllConcepts.h @@ -93,6 +93,7 @@ #include "metkit/mars2grib/utils/generalUtils.h" #include "metkit/mars2grib/backend/concepts/analysis/analysisConceptDescriptor.h" +#include "metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/composition/compositionConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/data-type/dataTypeConceptDescriptor.h" #include "metkit/mars2grib/backend/concepts/derived/derivedConceptDescriptor.h" @@ -116,7 +117,6 @@ #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 { @@ -171,10 +171,11 @@ 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/brightness-temperature/brightnessTemperatureConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h index 250f39c4e..aa557f9d9 100644 --- a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h @@ -62,8 +62,7 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// /// All functions in this descriptor are `constexpr` and are evaluated /// entirely at compile time. -struct BrightnessTemperatureConcept - : RegisterEntryDescriptor { +struct BrightnessTemperatureConcept : RegisterEntryDescriptor { /// @brief Return the canonical name of the concept. /// @@ -71,9 +70,7 @@ struct BrightnessTemperatureConcept /// - Registry identification /// - Diagnostics and logging /// - Debug and introspection facilities - static constexpr std::string_view entryName() { - return brightnessTemperatureName; - } + static constexpr std::string_view entryName() { return brightnessTemperatureName; } /// @brief Return the symbolic name of a concept variant. /// @@ -109,14 +106,8 @@ struct BrightnessTemperatureConcept /// @tparam OutDict_t Type of output GRIB dictionary /// /// @return Function pointer implementing the phase, or `nullptr` - template + template static constexpr Fn phaseCallbacks() { if constexpr (Capability == 0) { if constexpr (brightnessTemperatureApplicable()) { @@ -140,12 +131,8 @@ struct BrightnessTemperatureConcept /// through phase callbacks. /// /// @return Always `nullptr` - template + template static constexpr Fn variantCallbacks() { return nullptr; } diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h index 03097519c..4492f05b1 100644 --- a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h @@ -86,8 +86,7 @@ namespace metkit::mars2grib::backend::concepts_ { /// - `Section == SecLocalUseSection` template constexpr bool brightnessTemperatureApplicable() { - return ((Stage == StagePreset) && - (Section == SecLocalUseSection)); + return ((Stage == StagePreset) && (Section == SecLocalUseSection)); } /// @brief Execute the `brightnessTemperature` concept operation. @@ -131,17 +130,9 @@ constexpr bool brightnessTemperatureApplicable() { /// 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) { +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; @@ -156,15 +147,15 @@ void BrightnessTemperatureOp(const MarsDict_t& mars, 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 + // 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); + 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..."); + MARS2GRIB_CONCEPT_RETHROW(brightnessTemperature, "Unable to set `brightnessTemperature` concept..."); } // Successful operation diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h index 4b017d61f..3928a63ba 100644 --- a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEnum.h @@ -75,8 +75,7 @@ inline constexpr std::string_view brightnessTemperatureName{"brightnessTemperatu /// @warning /// Do not reorder existing enumerators, as they are used in compile-time /// tables and registries. -enum class BrightnessTemperatureType : std::size_t -{ +enum class BrightnessTemperatureType : std::size_t { EnsembleMean = 0, Default }; @@ -91,7 +90,8 @@ enum class BrightnessTemperatureType : std::size_t /// @note /// The order of this list must match the intended iteration order for /// registry construction and diagnostics. -using BrightnessTemperatureList = ValueList; +using BrightnessTemperatureList = + ValueList; /// @brief Compile-time mapping from `BrightnessTemperatureType` to human-readable name. /// diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h index b9d09b57b..521614047 100644 --- a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h @@ -72,15 +72,14 @@ std::size_t brightnessTemperatureMatcher(const MarsDict_t& mars, const OptDict_t Here()); } - if ( stream == "elda" ) { + if (stream == "elda") { return static_cast(BrightnessTemperatureType::EnsembleMean); } - else if ( stream == "oper" ) { + 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/derivedMatcher.h b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h index b49499f76..0201d5e4c 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -83,7 +83,6 @@ std::size_t derivedMatcher(const MarsDict_t& mars, const OptDict_t& opt) { std::throw_with_nested( utils::exceptions::Mars2GribMatcherException("Unable to match `derived` concept", Here())); } - } } // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index 89d3663a9..d0d2e6359 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -61,7 +61,8 @@ 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" || get_or_throw(mars, "type") == "me")) { + if (has(mars, "type") && + (get_or_throw(mars, "type") == "eme" || get_or_throw(mars, "type") == "me")) { return compile_time_registry_engine::MISSING; } catch (...) { diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h index 4335e2f0c..8ded03e69 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 ( (Stage == StagePreset) && (Section == SecLocalUseSection) ); + return ((Stage == StagePreset) && (Section == SecLocalUseSection)); } @@ -147,7 +147,7 @@ void ModelErrorOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& MARS2GRIB_LOG_CONCEPT(modelError); - if ( Variant == ModelErrorType::ComponentIndex ) { + if (Variant == ModelErrorType::ComponentIndex) { validation::match_LocalDefinitionNumber_or_throw(opt, out, {25L, 39L}); // Deductions @@ -159,19 +159,15 @@ void ModelErrorOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& set_or_throw(out, "componentIndex", componentIndexVal); set_or_throw(out, "numberOfComponents", numberOfComponentsVal); set_or_throw(out, "modelErrorType", modelErrorTypeVal); - - } - else if ( Variant == ModelErrorType::FourierCoefficients ) { + 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..."); } - } catch (...) { MARS2GRIB_CONCEPT_RETHROW(modelError, "Unable to set `modelError` concept..."); diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h index a47faa9f6..7f07333d0 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h @@ -22,15 +22,15 @@ 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; - if (!has(mars, "type") ){ + 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.", + "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")) { + if ((get_or_throw(mars, "type") != "eme" && get_or_throw(mars, "type") != "me")) { return compile_time_registry_engine::MISSING; } @@ -39,7 +39,7 @@ std::size_t modelErrorMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(ModelErrorType::ComponentIndex); } - if ( has(mars,"coeffindex") ) { + if (has(mars, "coeffindex")) { return static_cast(ModelErrorType::FourierCoefficients); } @@ -47,9 +47,8 @@ std::size_t modelErrorMatcher(const MarsDict_t& mars, const OptDict_t& opt) { } 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())); + std::throw_with_nested(metkit::mars2grib::utils::exceptions::Mars2GribMatcherException( + "An error occurred while matching the `modelError` concept. Check nested exception for details.", Here())); } } diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h index df155efe5..8bfc535f3 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h @@ -173,7 +173,7 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {0}); // Set bits per value - if constexpr ( Stage == StageAllocate ) { + if constexpr (Stage == StageAllocate) { long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); set_or_throw(out, "setBitsPerValue", bitsPerValue); } @@ -185,7 +185,7 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {42}); // Set bits per value - if constexpr ( Stage == StageAllocate ) { + if constexpr (Stage == StageAllocate) { long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); set_or_throw(out, "setBitsPerValue", bitsPerValue); } @@ -197,12 +197,12 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {51}); // Set bits per value - if constexpr ( Stage == StageAllocate ) { + if constexpr (Stage == StageAllocate) { long bitsPerValue = deductions::resolve_BitsPerValueSpectral_or_throw(mars, par, opt); set_or_throw(out, "setBitsPerValue", bitsPerValue); } - if constexpr ( Stage == StagePreset ) { + 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); diff --git a/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h b/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h index ca29c1ff6..6404f7e53 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h @@ -183,8 +183,7 @@ static metkit::codes::Span constValueSpan(std::size_t requiredSize values.resize(requiredSize); } - std::transform(values.begin(), values.begin() + requiredSize, values.begin(), - [value](double) { return value; }); + std::transform(values.begin(), values.begin() + requiredSize, values.begin(), [value](double) { return value; }); return metkit::codes::Span{values.data(), requiredSize}; } @@ -397,7 +396,7 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic // Initialize values with the deduced reference value std::size_t numberOfCoefficients = grid->size(); - set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue)); } else if constexpr (Variant == RepresentationType::RegularGaussian) { @@ -430,7 +429,7 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic // Initialize values with the deduced reference value std::size_t numberOfCoefficients = grid->size(); - set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue)); } else if constexpr (Variant == RepresentationType::ReducedGaussian) { @@ -463,7 +462,7 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic // Initialize values with the deduced reference value std::size_t numberOfCoefficients = grid->size(); - set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue)); } else if constexpr (Variant == RepresentationType::Healpix) { @@ -486,7 +485,7 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic // Initialize values with the deduced reference value std::size_t numberOfCoefficients = grid->size(); - set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue)); } else if constexpr (Variant == RepresentationType::Orca) { @@ -507,7 +506,7 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic // Initialize values with the deduced reference value std::size_t numberOfCoefficients = grid->size(); - set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue)); } else if constexpr (Variant == RepresentationType::Fesom) { MARS2GRIB_CONCEPT_THROW(representation, "Support for Fesom representation not implemented..."); @@ -528,9 +527,7 @@ void RepresentationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDic // Initialize values with the deduced reference value std::size_t numberOfCoefficients = (marsTruncation + 1) * (marsTruncation + 2); - set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue) ); - - + set_or_throw(out, "values", constValueSpan(numberOfCoefficients, allowedReferenceValue)); } else { MARS2GRIB_CONCEPT_THROW(representation, "Unknown `representation` variant..."); diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h index 60082cba5..0bb9a10f5 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h @@ -225,7 +225,6 @@ void SatelliteOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& // Encoding set_or_throw(out, "channelNumber", channelNumber); set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); - } 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 b8f5308ad..d4076abcc 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h @@ -85,7 +85,7 @@ inline constexpr std::string_view satelliteName{"satellite"}; /// tables and registries. /// enum class SatelliteType : std::size_t { - Default = 0 + Default = 0 }; diff --git a/src/metkit/mars2grib/backend/deductions/componentIndex.h b/src/metkit/mars2grib/backend/deductions/componentIndex.h index 1199489d0..a501f7ff0 100644 --- a/src/metkit/mars2grib/backend/deductions/componentIndex.h +++ b/src/metkit/mars2grib/backend/deductions/componentIndex.h @@ -128,21 +128,23 @@ 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" && typeVal != "me") { - throw Mars2GribDeductionException(std::string("`componentIndex` requested for a non-`eme` or non-`me` request: " - "`mars[\"type\"]` is `") + - typeVal + - "` 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()); + throw Mars2GribDeductionException( + std::string("`componentIndex` requested for a non-`eme` or non-`me` request: " + "`mars[\"type\"]` is `") + + typeVal + + "` 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; + 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) From bce25b625845f59f11f279458de3c819563a5b3a Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 17:44:08 +0000 Subject: [PATCH 24/35] Bugfix: fix code deleted during rebase --- .../backend/concepts/ensemble/ensembleMatcher.h | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index d0d2e6359..3a63a5f11 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -61,9 +61,18 @@ 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" || get_or_throw(mars, "type") == "me")) { - return compile_time_registry_engine::MISSING; + try { + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::dict_traits::get_or_throw; + + if (has(mars, "number") && !(has(mars, "type") && + (get_or_throw(mars, "type") == "me" || + get_or_throw(mars, "type") == "eme"))) { + return static_cast(EnsembleType::Individual); + } + else { + return compile_time_registry_engine::MISSING; + } } catch (...) { std::throw_with_nested( From f316cf54013d21230ca980959439734a666a571a Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 22:27:02 +0000 Subject: [PATCH 25/35] mars2grib: documentation alignment for concepts --- ...zXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json | 8 ++++ .../mars2grib/backend/concepts/AllConcepts.h | 4 ++ .../concepts/EncodingCallbacksRegistry.h | 4 ++ .../backend/concepts/GeneralRegistry.h | 4 ++ .../concepts/MatchingCallbacksRegistry.h | 4 ++ .../analysis/analysisConceptDescriptor.h | 2 +- .../concepts/analysis/analysisEncoding.h | 2 +- .../backend/concepts/analysis/analysisEnum.h | 2 +- .../brightnessTemperatureMatcher.h | 29 +++++++++++++ .../compositionConceptDescriptor.h | 2 +- .../composition/compositionEncoding.h | 2 +- .../concepts/composition/compositionEnum.h | 2 +- .../data-type/dataTypeConceptDescriptor.h | 2 +- .../concepts/data-type/dataTypeEncoding.h | 2 +- .../backend/concepts/data-type/dataTypeEnum.h | 2 +- .../derived/derivedConceptDescriptor.h | 2 +- .../concepts/derived/derivedEncoding.h | 2 +- .../backend/concepts/derived/derivedEnum.h | 2 +- .../destine/destineConceptDescriptor.h | 2 +- .../concepts/destine/destineEncoding.h | 2 +- .../backend/concepts/destine/destineEnum.h | 2 +- .../ensemble/ensembleConceptDescriptor.h | 2 +- .../concepts/ensemble/ensembleEncoding.h | 2 +- .../backend/concepts/ensemble/ensembleEnum.h | 2 +- .../generatingProcessConceptDescriptor.h | 2 +- .../generatingProcessEncoding.h | 2 +- .../generatingProcessEnum.h | 2 +- .../iteration/iterationConceptDescriptor.h | 2 +- .../concepts/iteration/iterationEncoding.h | 2 +- .../concepts/iteration/iterationEnum.h | 2 +- .../concepts/iteration/iterationMatcher.h | 29 +++++++++++++ .../concepts/level/levelConceptDescriptor.h | 2 +- .../backend/concepts/level/levelEncoding.h | 2 +- .../backend/concepts/level/levelEnum.h | 2 +- .../longrange/longrangeConceptDescriptor.h | 2 +- .../concepts/longrange/longrangeEncoding.h | 2 +- .../concepts/longrange/longrangeEnum.h | 2 +- .../concepts/mars/marsConceptDescriptor.h | 2 +- .../backend/concepts/mars/marsEncoding.h | 2 +- .../backend/concepts/mars/marsEnum.h | 2 +- .../model-error/modelErrorConceptDescriptor.h | 2 +- .../concepts/model-error/modelErrorEncoding.h | 2 +- .../concepts/model-error/modelErrorEnum.h | 2 +- .../concepts/model-error/modelErrorMatcher.h | 29 +++++++++++++ .../concepts/nil/nilConceptDescriptor.h | 2 +- .../backend/concepts/nil/nilEncoding.h | 2 +- .../mars2grib/backend/concepts/nil/nilEnum.h | 2 +- .../concepts/origin/originConceptDescriptor.h | 43 ++++++++++++++++++- .../backend/concepts/origin/originEncoding.h | 2 +- .../backend/concepts/origin/originEnum.h | 2 +- .../packing/packingConceptDescriptor.h | 43 ++++++++++++++++++- .../concepts/packing/packingEncoding.h | 2 +- .../backend/concepts/packing/packingEnum.h | 2 +- .../concepts/param/paramConceptDescriptor.h | 43 ++++++++++++++++++- .../backend/concepts/param/paramEncoding.h | 2 +- .../backend/concepts/param/paramEnum.h | 2 +- .../pointInTimeConceptDescriptor.h | 43 ++++++++++++++++++- .../point-in-time/pointInTimeEncoding.h | 2 +- .../concepts/point-in-time/pointInTimeEnum.h | 2 +- .../referenceTimeConceptDescriptor.h | 43 ++++++++++++++++++- .../reference-time/referenceTimeEncoding.h | 2 +- .../reference-time/referenceTimeEnum.h | 2 +- .../representationConceptDescriptor.h | 43 ++++++++++++++++++- .../representation/representationEncoding.h | 2 +- .../representation/representationEnum.h | 2 +- .../satellite/satelliteConceptDescriptor.h | 43 ++++++++++++++++++- .../concepts/satellite/satelliteEncoding.h | 2 +- .../concepts/satellite/satelliteEnum.h | 2 +- .../shapeOfTheEarthConceptDescriptor.h | 43 ++++++++++++++++++- .../shapeOfTheEarthEncoding.h | 2 +- .../shape-of-the-earth/shapeOfTheEarthEnum.h | 2 +- .../statistics/statisticsConceptDescriptor.h | 43 ++++++++++++++++++- .../concepts/statistics/statisticsEnum.h | 2 +- .../concepts/tables/tablesConceptDescriptor.h | 43 ++++++++++++++++++- .../backend/concepts/tables/tablesEncoding.h | 2 +- .../backend/concepts/tables/tablesEnum.h | 2 +- .../concepts/wave/waveConceptDescriptor.h | 43 ++++++++++++++++++- .../backend/concepts/wave/waveEncoding.h | 2 +- .../backend/concepts/wave/waveEnum.h | 2 +- 79 files changed, 633 insertions(+), 71 deletions(-) create mode 100644 .weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json diff --git a/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json b/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json new file mode 100644 index 000000000..8fd0e69f2 --- /dev/null +++ b/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json @@ -0,0 +1,8 @@ +{ + "session_id": "ses_17b97b786ffe9FVOFQidc4qyZV", + "foreground_agent": "build", + "mode": "ad_hoc", + "execution_ref": null, + "status": "running", + "updated_at": "2026-06-01T18:51:14.699Z" +} \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/AllConcepts.h b/src/metkit/mars2grib/backend/concepts/AllConcepts.h index 229cf79ce..011693d08 100644 --- a/src/metkit/mars2grib/backend/concepts/AllConcepts.h +++ b/src/metkit/mars2grib/backend/concepts/AllConcepts.h @@ -83,6 +83,10 @@ /// compile-time registry engine. /// +/// +/// @ingroup mars2grib_backend_concepts +/// + #pragma once // System includes diff --git a/src/metkit/mars2grib/backend/concepts/EncodingCallbacksRegistry.h b/src/metkit/mars2grib/backend/concepts/EncodingCallbacksRegistry.h index c02c0ec4e..1525b5a3e 100644 --- a/src/metkit/mars2grib/backend/concepts/EncodingCallbacksRegistry.h +++ b/src/metkit/mars2grib/backend/concepts/EncodingCallbacksRegistry.h @@ -83,6 +83,10 @@ /// /// This header is safe to include in performance-critical translation units. /// +/// +/// @ingroup mars2grib_backend_concepts +/// + #pragma once // System includes diff --git a/src/metkit/mars2grib/backend/concepts/GeneralRegistry.h b/src/metkit/mars2grib/backend/concepts/GeneralRegistry.h index 91d59045d..cc22e928a 100644 --- a/src/metkit/mars2grib/backend/concepts/GeneralRegistry.h +++ b/src/metkit/mars2grib/backend/concepts/GeneralRegistry.h @@ -94,6 +94,10 @@ /// /// This header must remain lightweight and safe to include transitively. /// +/// +/// @ingroup mars2grib_backend_concepts +/// + #pragma once // System includes diff --git a/src/metkit/mars2grib/backend/concepts/MatchingCallbacksRegistry.h b/src/metkit/mars2grib/backend/concepts/MatchingCallbacksRegistry.h index bff38d545..18e41ea67 100644 --- a/src/metkit/mars2grib/backend/concepts/MatchingCallbacksRegistry.h +++ b/src/metkit/mars2grib/backend/concepts/MatchingCallbacksRegistry.h @@ -87,6 +87,10 @@ /// /// This header is safe to include transitively in performance-critical code. /// +/// +/// @ingroup mars2grib_backend_concepts +/// + #pragma once // System includes diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisConceptDescriptor.h index 1232b9341..bf9bac1ef 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file AnalysisConcept.h +/// @file analysisConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `analysis` concept. /// /// This header defines `AnalysisConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h index 39de42840..14096a2a6 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file analysisOp.h +/// @file analysisEncoding.h /// @brief Implementation of the GRIB `analysis` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisEnum.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisEnum.h index c2b8b6a5e..ded09865b 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisEnum.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `analysis.h` / `analysisOp` implementation. +/// `analysisEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h index 521614047..66f030716 100644 --- a/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h @@ -1,3 +1,32 @@ +/* + * (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 brightnessTemperatureMatcher.h +/// @brief Entry-level matcher for the GRIB `brightnessTemperature` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether the **brightnessTemperature concept** is active for a MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active. +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of +/// `concepts` to avoid conflicts with the C++20 `concept` language +/// feature. +/// +/// @ingroup mars2grib_backend_concepts +/// + #pragma once // System includes diff --git a/src/metkit/mars2grib/backend/concepts/composition/compositionConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/composition/compositionConceptDescriptor.h index 81c4bc1e3..136ceb211 100644 --- a/src/metkit/mars2grib/backend/concepts/composition/compositionConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/composition/compositionConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file CompositionConcept.h +/// @file compositionConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `composition` concept. /// /// This header defines `CompositionConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/composition/compositionEncoding.h b/src/metkit/mars2grib/backend/concepts/composition/compositionEncoding.h index 499c2d8fb..6e6208b91 100644 --- a/src/metkit/mars2grib/backend/concepts/composition/compositionEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/composition/compositionEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file compositionOp.h +/// @file compositionEncoding.h /// @brief Implementation of the GRIB `composition` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/composition/compositionEnum.h b/src/metkit/mars2grib/backend/concepts/composition/compositionEnum.h index 77ffcd534..a0e72ed2e 100644 --- a/src/metkit/mars2grib/backend/concepts/composition/compositionEnum.h +++ b/src/metkit/mars2grib/backend/concepts/composition/compositionEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `composition.h` / `compositionOp` implementation. +/// `compositionEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeConceptDescriptor.h index 88cb68325..82debea7b 100644 --- a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file DataTypeConcept.h +/// @file dataTypeConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `dataType` concept. /// /// This header defines `DataTypeConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEncoding.h b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEncoding.h index 63fc433c9..45bee7343 100644 --- a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file dataTypeOp.h +/// @file dataTypeEncoding.h /// @brief Implementation of the GRIB `dataType` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEnum.h b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEnum.h index 7d7b7726f..c8a3e5af4 100644 --- a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEnum.h +++ b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `dataType.h` / `dataTypeOp` implementation. +/// `dataTypeEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/derived/derivedConceptDescriptor.h index c0f54669d..9a9d1b65c 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file DerivedConcept.h +/// @file derivedConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `derived` concept. /// /// This header defines `DerivedConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h index 032ca20aa..f60d0ded2 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file derivedOp.h +/// @file derivedEncoding.h /// @brief Implementation of the GRIB `derived` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h index 397378083..8afdfa6b9 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `derived.h` / `derivedOp` implementation. +/// `derivedEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/destine/destineConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/destine/destineConceptDescriptor.h index 6416df99a..1185bf2c3 100644 --- a/src/metkit/mars2grib/backend/concepts/destine/destineConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/destine/destineConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file DestineConcept.h +/// @file destineConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `destine` concept. /// /// This header defines `DestineConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/destine/destineEncoding.h b/src/metkit/mars2grib/backend/concepts/destine/destineEncoding.h index d91115ad6..a4d3a611d 100644 --- a/src/metkit/mars2grib/backend/concepts/destine/destineEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/destine/destineEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file destineOp.h +/// @file destineEncoding.h /// @brief Implementation of the GRIB `destine` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/destine/destineEnum.h b/src/metkit/mars2grib/backend/concepts/destine/destineEnum.h index fbda7e41a..e2ac625b7 100644 --- a/src/metkit/mars2grib/backend/concepts/destine/destineEnum.h +++ b/src/metkit/mars2grib/backend/concepts/destine/destineEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `destine.h` / `destineOp` implementation. +/// `destineEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleConceptDescriptor.h index c0da44bb8..4ecad219b 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file EnsembleConcept.h +/// @file ensembleConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `ensemble` concept. /// /// This header defines `EnsembleConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEncoding.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEncoding.h index 7cd71f5c8..db8e25e73 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file ensembleOp.h +/// @file ensembleEncoding.h /// @brief Implementation of the GRIB `ensemble` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.h index 370683653..70f2f1c4b 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `ensemble.h` / `ensembleOp` implementation. +/// `ensembleEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessConceptDescriptor.h index f60a6fd7b..e6c2d39d4 100644 --- a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file GeneratingProcessConcept.h +/// @file generatingProcessConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `generatingProcess` concept. /// /// This header defines `GeneratingProcessConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEncoding.h b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEncoding.h index e02f5b976..945f8613e 100644 --- a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file generatingProcessOp.h +/// @file generatingProcessEncoding.h /// @brief Implementation of the GRIB `generatingProcess` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEnum.h b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEnum.h index 608c7d154..9d76e88b1 100644 --- a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEnum.h +++ b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `generatingProcess.h` / `generatingProcessOp` implementation. +/// `generatingProcessEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h index 6a8777f33..1e3488b4a 100644 --- a/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file IterationConcept.h +/// @file iterationConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `iteration` concept. /// /// This header defines `IterationConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h index cc0900d77..b5c03fe56 100644 --- a/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file iterationOp.h +/// @file iterationEncoding.h /// @brief Implementation of the GRIB `iteration` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h index 5a8fae4fc..70771badc 100644 --- a/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `iteration.h` / `iterationOp` implementation. +/// `iterationEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h index 39fd3f7d8..c496a9afd 100644 --- a/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h @@ -1,3 +1,32 @@ +/* + * (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 iterationMatcher.h +/// @brief Entry-level matcher for the GRIB `iteration` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether the **iteration concept** is active for a MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active. +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of +/// `concepts` to avoid conflicts with the C++20 `concept` language +/// feature. +/// +/// @ingroup mars2grib_backend_concepts +/// + #pragma once // System include diff --git a/src/metkit/mars2grib/backend/concepts/level/levelConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/level/levelConceptDescriptor.h index a2d50e637..667c0c043 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file LevelConcept.h +/// @file levelConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `level` concept. /// /// This header defines `LevelConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h b/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h index dba35acc0..65ffaaca9 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file levelOp.h +/// @file levelEncoding.h /// @brief Implementation of the GRIB `level` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h index 8f58af0e6..619d217ef 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `level.h` / `levelOp` implementation. +/// `levelEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/longrange/longrangeConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/longrange/longrangeConceptDescriptor.h index fb037647a..a8ab12a05 100644 --- a/src/metkit/mars2grib/backend/concepts/longrange/longrangeConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/longrange/longrangeConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file LongrangeConcept.h +/// @file longrangeConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `longrange` concept. /// /// This header defines `LongrangeConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/longrange/longrangeEncoding.h b/src/metkit/mars2grib/backend/concepts/longrange/longrangeEncoding.h index 0a424bfe2..66ab4b49b 100644 --- a/src/metkit/mars2grib/backend/concepts/longrange/longrangeEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/longrange/longrangeEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file longrangeOp.h +/// @file longrangeEncoding.h /// @brief Implementation of the GRIB `longrange` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/longrange/longrangeEnum.h b/src/metkit/mars2grib/backend/concepts/longrange/longrangeEnum.h index d1bea0d75..4493f2e23 100644 --- a/src/metkit/mars2grib/backend/concepts/longrange/longrangeEnum.h +++ b/src/metkit/mars2grib/backend/concepts/longrange/longrangeEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `longrange.h` / `longrangeOp` implementation. +/// `longrangeEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/mars/marsConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/mars/marsConceptDescriptor.h index 910924355..755641824 100644 --- a/src/metkit/mars2grib/backend/concepts/mars/marsConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/mars/marsConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file MarsConcept.h +/// @file marsConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `mars` concept. /// /// This header defines `MarsConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/mars/marsEncoding.h b/src/metkit/mars2grib/backend/concepts/mars/marsEncoding.h index 1815121bc..17c00aed0 100644 --- a/src/metkit/mars2grib/backend/concepts/mars/marsEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/mars/marsEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file marsOp.h +/// @file marsEncoding.h /// @brief Implementation of the GRIB `mars` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/mars/marsEnum.h b/src/metkit/mars2grib/backend/concepts/mars/marsEnum.h index 993290bd8..18d36acf3 100644 --- a/src/metkit/mars2grib/backend/concepts/mars/marsEnum.h +++ b/src/metkit/mars2grib/backend/concepts/mars/marsEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `mars.h` / `marsOp` implementation. +/// `marsEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h index 5ff7c713d..a68bf60df 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file ModelErrorConcept.h +/// @file modelErrorConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `modelError` concept. /// /// This header defines `ModelErrorConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h index 8ded03e69..57940521e 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file modelErrorOp.h +/// @file modelErrorEncoding.h /// @brief Implementation of the GRIB `modelError` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h index ef21dc327..1b4b50f31 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `modelError.h` / `modelErrorOp` implementation. +/// `modelErrorEncoding.h` implementation. /// /// @ingroup 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 index 7f07333d0..abd5e28f3 100644 --- a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h @@ -1,3 +1,32 @@ +/* + * (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 modelErrorMatcher.h +/// @brief Entry-level matcher for the GRIB `modelError` concept. +/// +/// This header defines the runtime matcher used by the concept registry to +/// decide whether the **modelError concept** is active for a MARS request. +/// +/// The matcher follows the standard mars2grib matching contract: +/// - return a local concept variant index when the concept is active, +/// - return `compile_time_registry_engine::MISSING` when it is not active. +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of +/// `concepts` to avoid conflicts with the C++20 `concept` language +/// feature. +/// +/// @ingroup mars2grib_backend_concepts +/// + #pragma once // System include diff --git a/src/metkit/mars2grib/backend/concepts/nil/nilConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/nil/nilConceptDescriptor.h index c5085a92c..10a12768b 100644 --- a/src/metkit/mars2grib/backend/concepts/nil/nilConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/nil/nilConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file NilConcept.h +/// @file nilConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `nil` concept. /// /// This header defines `NilConcept`, the **compile-time descriptor** diff --git a/src/metkit/mars2grib/backend/concepts/nil/nilEncoding.h b/src/metkit/mars2grib/backend/concepts/nil/nilEncoding.h index 547444b0a..49d3a261e 100644 --- a/src/metkit/mars2grib/backend/concepts/nil/nilEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/nil/nilEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file nilOp.h +/// @file nilEncoding.h /// @brief Implementation of the GRIB `nil` concept operation. /// /// This header defines the **nil concept**, a sentinel / placeholder concept diff --git a/src/metkit/mars2grib/backend/concepts/nil/nilEnum.h b/src/metkit/mars2grib/backend/concepts/nil/nilEnum.h index de2ed95b3..bf0cd3138 100644 --- a/src/metkit/mars2grib/backend/concepts/nil/nilEnum.h +++ b/src/metkit/mars2grib/backend/concepts/nil/nilEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `nil.h` / `nilOp` implementation. +/// `nilEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/origin/originConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/origin/originConceptDescriptor.h index b03f44fc1..b4e7bfec5 100644 --- a/src/metkit/mars2grib/backend/concepts/origin/originConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/origin/originConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file OriginConcept.h +/// @file originConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `origin` concept. /// /// This header defines `OriginConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct OriginConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return originName; } + /// + /// @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 originTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `origin` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct OriginConcept : RegisterEntryDescriptor { return nullptr; } + /// + /// @brief Entry-level matcher callback. + /// + /// This hook is invoked to determine whether the `origin` concept should be + /// activated for a given request. + /// + /// @tparam Capability Matching/encoding capability index + /// @return Matcher function pointer, or `nullptr` if not participating. + /// template static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/origin/originEncoding.h b/src/metkit/mars2grib/backend/concepts/origin/originEncoding.h index a651414f1..b3f9574e2 100644 --- a/src/metkit/mars2grib/backend/concepts/origin/originEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/origin/originEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file originOp.h +/// @file originEncoding.h /// @brief Implementation of the GRIB `origin` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/origin/originEnum.h b/src/metkit/mars2grib/backend/concepts/origin/originEnum.h index a672fee57..f03674a3c 100644 --- a/src/metkit/mars2grib/backend/concepts/origin/originEnum.h +++ b/src/metkit/mars2grib/backend/concepts/origin/originEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `origin.h` / `originOp` implementation. +/// `originEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/packing/packingConceptDescriptor.h index d76c50833..1304afc42 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file PackingConcept.h +/// @file packingConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `packing` concept. /// /// This header defines `PackingConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct PackingConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return packingName; } + /// + /// @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 packingTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `packing` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct PackingConcept : RegisterEntryDescriptor { return nullptr; } + /// + /// @brief Entry-level matcher callback. + /// + /// This hook is invoked to determine whether the `packing` concept should be + /// activated for a given request. + /// + /// @tparam Capability Matching/encoding capability index + /// @return Matcher function pointer, or `nullptr` if not participating. + /// template static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h index 8bfc535f3..47b950210 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file packingOp.h +/// @file packingEncoding.h /// @brief Implementation of the GRIB `packing` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingEnum.h b/src/metkit/mars2grib/backend/concepts/packing/packingEnum.h index 49d1b38bb..f1e24903d 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingEnum.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `packing.h` / `packingOp` implementation. +/// `packingEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/param/paramConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/param/paramConceptDescriptor.h index 7950c79c4..1f9ab0581 100644 --- a/src/metkit/mars2grib/backend/concepts/param/paramConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/param/paramConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file ParamConcept.h +/// @file paramConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `param` concept. /// /// This header defines `ParamConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct ParamConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return paramName; } + /// + /// @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 paramTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `param` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct ParamConcept : RegisterEntryDescriptor { return nullptr; } + /// + /// @brief Entry-level matcher callback. + /// + /// This hook is invoked to determine whether the `param` concept should be + /// activated for a given request. + /// + /// @tparam Capability Matching/encoding capability index + /// @return Matcher function pointer, or `nullptr` if not participating. + /// template static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/param/paramEncoding.h b/src/metkit/mars2grib/backend/concepts/param/paramEncoding.h index 81c0af846..0c4cc4668 100644 --- a/src/metkit/mars2grib/backend/concepts/param/paramEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/param/paramEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file paramOp.h +/// @file paramEncoding.h /// @brief Implementation of the GRIB `param` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/param/paramEnum.h b/src/metkit/mars2grib/backend/concepts/param/paramEnum.h index 816ac5ee6..10e093089 100644 --- a/src/metkit/mars2grib/backend/concepts/param/paramEnum.h +++ b/src/metkit/mars2grib/backend/concepts/param/paramEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `param.h` / `paramOp` implementation. +/// `paramEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeConceptDescriptor.h index 90d645a01..1b0c55e7c 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file PointInTimeConcept.h +/// @file pointInTimeConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `pointInTime` concept. /// /// This header defines `PointInTimeConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct PointInTimeConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return pointInTimeName; } + /// + /// @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 pointInTimeTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `pointInTime` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct PointInTimeConcept : RegisterEntryDescriptor static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h index ff6bcf6c7..e509f1925 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file pointInTimeOp.h +/// @file pointInTimeEncoding.h /// @brief Implementation of the GRIB `pointInTime` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEnum.h b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEnum.h index 1a209a052..6829bfaec 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEnum.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `pointInTime.h` / `pointInTimeOp` implementation. +/// `pointInTimeEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeConceptDescriptor.h index ad6d57772..9c7c8ce85 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file ReferenceTimeConcept.h +/// @file referenceTimeConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `referenceTime` concept. /// /// This header defines `ReferenceTimeConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct ReferenceTimeConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return referenceTimeName; } + /// + /// @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 referenceTimeTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `referenceTime` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct ReferenceTimeConcept : RegisterEntryDescriptor static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h index 8387f1cfe..5883ce3c1 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file referenceTimeOp.h +/// @file referenceTimeEncoding.h /// @brief Implementation of the GRIB `referenceTime` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEnum.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEnum.h index 8be8a7838..ba323b35f 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEnum.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `referenceTime.h` / `referenceTimeOp` implementation. +/// `referenceTimeEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/representation/representationConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/representation/representationConceptDescriptor.h index e0597b8d9..9b7ddf18c 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file RepresentationConceptDescriptor.h +/// @file representationConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `representation` concept. /// /// This header defines `RepresentationConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct RepresentationConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return representationName; } + /// + /// @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 representationTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `representation` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct RepresentationConcept : RegisterEntryDescriptor static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h b/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h index 6404f7e53..0b23677dc 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file representationOp.h +/// @file representationEncoding.h /// @brief Implementation of the GRIB `representation` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/representation/representationEnum.h b/src/metkit/mars2grib/backend/concepts/representation/representationEnum.h index b4f031e1f..6013ff501 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationEnum.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `representation.h` / `representationOp` implementation. +/// `representationEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteConceptDescriptor.h index ed43bc276..77f02bbdf 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file SatelliteConcept.h +/// @file satelliteConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `satellite` concept. /// /// This header defines `SatelliteConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct SatelliteConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return satelliteName; } + /// + /// @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 satelliteTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `satellite` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct SatelliteConcept : RegisterEntryDescriptor return nullptr; } + /// + /// @brief Entry-level matcher callback. + /// + /// This hook is invoked to determine whether the `satellite` concept should be + /// activated for a given request. + /// + /// @tparam Capability Matching/encoding capability index + /// @return Matcher function pointer, or `nullptr` if not participating. + /// template static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h index 0bb9a10f5..7925216c6 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file satelliteOp.h +/// @file satelliteEncoding.h /// @brief Implementation of the GRIB `satellite` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h index d4076abcc..47f92a07c 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `satellite.h` / `satelliteOp` implementation. +/// `satelliteEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthConceptDescriptor.h index 65a82d5c8..6db45003b 100644 --- a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file ShapeOfTheEarthConcept.h +/// @file shapeOfTheEarthConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `shapeOfTheEarth` concept. /// /// This header defines `ShapeOfTheEarthConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct ShapeOfTheEarthConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return shapeOfTheEarthName; } + /// + /// @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 shapeOfTheEarthTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `shapeOfTheEarth` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct ShapeOfTheEarthConcept : RegisterEntryDescriptor static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEncoding.h b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEncoding.h index aeb43fc00..1188e8ddf 100644 --- a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file shapeOfTheEarthOp.h +/// @file shapeOfTheEarthEncoding.h /// @brief Implementation of the GRIB `shapeOfTheEarth` concept operation. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEnum.h b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEnum.h index af23d1eee..ff523f7b1 100644 --- a/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEnum.h +++ b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `shapeOfTheEarth.h` / `shapeOfTheEarthOp` implementation. +/// `shapeOfTheEarthEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsConceptDescriptor.h index 0d107aa26..44f2cb6a0 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file StatisticsConcept.h +/// @file statisticsConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `statistics` concept. /// /// This header defines `StatisticsConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct StatisticsConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return statisticsName; } + /// + /// @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 statisticsTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `statistics` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct StatisticsConcept : RegisterEntryDescriptor static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h index 3dd04e876..e0e5505a8 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h @@ -33,7 +33,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `statistics.h` / `statisticsOp` implementation. +/// `statisticsEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/tables/tablesConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/tables/tablesConceptDescriptor.h index 5c7e78df4..115328c76 100644 --- a/src/metkit/mars2grib/backend/concepts/tables/tablesConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/tables/tablesConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file TablesConcept.h +/// @file tablesConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `tables` concept. /// /// This header defines `TablesConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct TablesConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return tablesName; } + /// + /// @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 tablesTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `tables` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct TablesConcept : RegisterEntryDescriptor { return nullptr; } + /// + /// @brief Entry-level matcher callback. + /// + /// This hook is invoked to determine whether the `tables` concept should be + /// activated for a given request. + /// + /// @tparam Capability Matching/encoding capability index + /// @return Matcher function pointer, or `nullptr` if not participating. + /// template static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/tables/tablesEncoding.h b/src/metkit/mars2grib/backend/concepts/tables/tablesEncoding.h index 7ee0e0043..eac081d41 100644 --- a/src/metkit/mars2grib/backend/concepts/tables/tablesEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/tables/tablesEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file tablesOp.h +/// @file tablesEncoding.h /// @brief Implementation of the GRIB tables-versioning concept (`tables`). /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/tables/tablesEnum.h b/src/metkit/mars2grib/backend/concepts/tables/tablesEnum.h index 3ab97b11e..1b7ab06da 100644 --- a/src/metkit/mars2grib/backend/concepts/tables/tablesEnum.h +++ b/src/metkit/mars2grib/backend/concepts/tables/tablesEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `tables.h` / `tablesOp` implementation. +/// `tablesEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// diff --git a/src/metkit/mars2grib/backend/concepts/wave/waveConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/wave/waveConceptDescriptor.h index 2b2484584..0591918ad 100644 --- a/src/metkit/mars2grib/backend/concepts/wave/waveConceptDescriptor.h +++ b/src/metkit/mars2grib/backend/concepts/wave/waveConceptDescriptor.h @@ -9,7 +9,7 @@ */ /// -/// @file WaveConcept.h +/// @file waveConceptDescriptor.h /// @brief Compile-time registry entry for the GRIB `wave` concept. /// /// This header defines `WaveConcept`, the **compile-time descriptor** @@ -57,13 +57,45 @@ using namespace metkit::mars2grib::backend::compile_time_registry_engine; /// struct WaveConcept : RegisterEntryDescriptor { + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - registry identification + /// - diagnostics and logging + /// - debug and introspection utilities + /// static constexpr std::string_view entryName() { return waveName; } + /// + /// @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 waveTypeName(); } + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This hook is queried by the registry engine to obtain the function + /// implementing the `wave` concept for a given: + /// - capability + /// - stage + /// - GRIB section + /// - variant + /// + /// The function returns either a valid function pointer or `nullptr` if the + /// combination is not applicable. + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// template static constexpr Fn phaseCallbacks() { @@ -90,6 +122,15 @@ struct WaveConcept : RegisterEntryDescriptor { return nullptr; } + /// + /// @brief Entry-level matcher callback. + /// + /// This hook is invoked to determine whether the `wave` concept should be + /// activated for a given request. + /// + /// @tparam Capability Matching/encoding capability index + /// @return Matcher function pointer, or `nullptr` if not participating. + /// template static constexpr Fm entryCallbacks() { if constexpr (Capability == 0) { diff --git a/src/metkit/mars2grib/backend/concepts/wave/waveEncoding.h b/src/metkit/mars2grib/backend/concepts/wave/waveEncoding.h index 4a5cb3f20..aad5f081b 100644 --- a/src/metkit/mars2grib/backend/concepts/wave/waveEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/wave/waveEncoding.h @@ -9,7 +9,7 @@ */ /// -/// @file waveOp.h +/// @file waveEncoding.h /// @brief Implementation of the GRIB `wave` concept. /// /// This header defines the applicability rules and execution logic for the diff --git a/src/metkit/mars2grib/backend/concepts/wave/waveEnum.h b/src/metkit/mars2grib/backend/concepts/wave/waveEnum.h index b6a6331ac..b662e606a 100644 --- a/src/metkit/mars2grib/backend/concepts/wave/waveEnum.h +++ b/src/metkit/mars2grib/backend/concepts/wave/waveEnum.h @@ -31,7 +31,7 @@ /// @note /// This header is part of the **concept definition layer**. /// Runtime behavior is implemented separately in the corresponding -/// `wave.h` / `waveOp` implementation. +/// `waveEncoding.h` implementation. /// /// @ingroup mars2grib_backend_concepts /// From 1f03bf73b4b49270e4d1bbf6425fcfd397509183 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Mon, 1 Jun 2026 23:16:37 +0000 Subject: [PATCH 26/35] mars2grib: Fix documentation of deductions --- ...zXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json | 2 +- .../backend/deductions/detail/pv_137_be.h | 25 ++++++++-- .../deductions/generatingProcessIdentifier.h | 16 +++++++ .../backend/deductions/numberOfFrequencies.h | 34 ++++++++++++++ .../backend/deductions/perturbationNumber.h | 47 ++----------------- 5 files changed, 74 insertions(+), 50 deletions(-) diff --git a/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json b/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json index 8fd0e69f2..ae6fb66f5 100644 --- a/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json +++ b/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json @@ -4,5 +4,5 @@ "mode": "ad_hoc", "execution_ref": null, "status": "running", - "updated_at": "2026-06-01T18:51:14.699Z" + "updated_at": "2026-06-01T23:12:01.763Z" } \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/deductions/detail/pv_137_be.h b/src/metkit/mars2grib/backend/deductions/detail/pv_137_be.h index 3520427d8..fe70d89a0 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/pv_137_be.h +++ b/src/metkit/mars2grib/backend/deductions/detail/pv_137_be.h @@ -1,15 +1,23 @@ -#pragma once -#include "metkit/mars2grib/utils/generalUtils.h" +/* + * (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 detail/pv_137_be.h /// @brief Predefined PV coefficient table extracted from IFS binary output. /// /// This file contains a statically defined table of PV coefficients used by /// the PV lookup and decoding infrastructure in the mars2grib backend. /// /// The values in this table have been obtained by performing a **binary dump -/// in the IFS** of the PV array associated -/// with a specific hybrid vertical coordinate configuration. +/// in the IFS** of the PV array associated with a specific hybrid vertical +/// coordinate configuration. /// /// The dumped binary values were: /// - captured verbatim from IFS runtime memory, @@ -51,6 +59,13 @@ /// No attempt must be made to modify, regenerate, or reinterpret the /// coefficients manually. /// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +#include "metkit/mars2grib/utils/generalUtils.h" + static constexpr std::array pv_137_1002_be = { {{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, {0x40, 0x00, 0x00, 0xbf, 0x60, 0x00, 0x00, 0x00}, {0x40, 0x08, 0xd1, 0x63, 0xc0, 0x00, 0x00, 0x00}, {0x40, 0x12, 0xaa, 0x11, 0xe0, 0x00, 0x00, 0x00}, @@ -554,4 +569,4 @@ static constexpr std::array pv_137_1002_be = { {0x00, 0x00, 0x15, 0x3e, 0x5f, 0x5b, 0x76, 0x30}, {0x00, 0x00, 0x15, 0x3d, 0x2c, 0x82, 0x72, 0x10}, {0x00, 0x00, 0x15, 0x3e, 0x5f, 0x5b, 0x76, 0x30}, {0x00, 0x00, 0x15, 0x3d, 0x2c, 0x82, 0x70, 0x50}}}; -// } // namespace metkit::mars2grib::backend::deductions::pv_detail::data \ No newline at end of file +// } // namespace metkit::mars2grib::backend::deductions::pv_detail::data diff --git a/src/metkit/mars2grib/backend/deductions/generatingProcessIdentifier.h b/src/metkit/mars2grib/backend/deductions/generatingProcessIdentifier.h index aeebbb536..23a62bab5 100644 --- a/src/metkit/mars2grib/backend/deductions/generatingProcessIdentifier.h +++ b/src/metkit/mars2grib/backend/deductions/generatingProcessIdentifier.h @@ -7,6 +7,22 @@ * granted to it by virtue of its status as an intergovernmental organisation nor * does it submit to any jurisdiction. */ + +/// +/// @file generatingProcessIdentifier.h +/// @brief Optional deduction of the GRIB `generatingProcessIdentifier` key. +/// +/// This header provides an optional passthrough deduction for the GRIB +/// `generatingProcessIdentifier` key. +/// +/// @section Deduction contract +/// - Reads: `par["generatingProcessIdentifier"]` (optional) +/// - Writes: none +/// - Side effects: logging (RESOLVE when present, otherwise DEFAULT-style skip message) +/// - Failure mode: returns `std::nullopt` when absent; throws on unexpected errors +/// +/// @ingroup mars2grib_backend_deductions +/// #pragma once #include diff --git a/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h index 40faf47fa..64a9ca427 100644 --- a/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h +++ b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h @@ -8,6 +8,24 @@ * does it submit to any jurisdiction. */ +/// +/// @file numberOfFrequencies.h +/// @brief Deduction of the GRIB `numberOfFrequencies` key. +/// +/// Resolve the wave spectrum discretization size (`numberOfFrequencies`). +/// +/// @section Deduction contract +/// - Reads: `par["numberOfFrequencies"]` (optional) +/// - Writes: none +/// - Side effects: logging (OVERRIDE or DEFAULT) +/// - Failure mode: throws on unexpected errors +/// +/// If the parameter dictionary provides `numberOfFrequencies`, the value is +/// used verbatim. Otherwise a documented default (`54`) is applied. +/// +/// @ingroup mars2grib_backend_deductions +/// + #pragma once #include @@ -19,6 +37,22 @@ namespace metkit::mars2grib::backend::deductions { +/// +/// @brief Resolve the GRIB `numberOfFrequencies` key. +/// +/// @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; may contain `numberOfFrequencies`. +/// @param[in] opt Options dictionary (unused). +/// +/// @return Resolved number of frequencies. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If an unexpected error occurs while accessing the parameter dictionary. +/// template long resolve_NumberOfFrequencies_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { diff --git a/src/metkit/mars2grib/backend/deductions/perturbationNumber.h b/src/metkit/mars2grib/backend/deductions/perturbationNumber.h index c02066269..cd55b8dde 100644 --- a/src/metkit/mars2grib/backend/deductions/perturbationNumber.h +++ b/src/metkit/mars2grib/backend/deductions/perturbationNumber.h @@ -9,6 +9,7 @@ */ /// +/// @file perturbationNumber.h /// @brief Resolve the perturbation (ensemble member) number from the MARS dictionary. /// /// This deduction retrieves the value associated with the MARS key `number`, @@ -60,6 +61,8 @@ /// - @ref numberOfForecastsInEnsemble.h /// - @ref typeOfEnsembleForecast.h /// +/// @ingroup mars2grib_backend_deductions +/// #pragma once // System includes @@ -73,50 +76,6 @@ namespace metkit::mars2grib::backend::deductions { -/// -/// @brief Resolve the perturbation number (`number`) from the MARS dictionary. -/// -/// @section Deduction contract -/// - Reads: `mars["number"]` -/// - Writes: none -/// - Side effects: logging (RESOLVE) -/// - Failure mode: throws on error -/// -/// This deduction retrieves the perturbation number associated with the -/// current field from the MARS dictionary. -/// -/// The value uniquely identifies the ensemble member within an ensemble -/// forecast. Its interpretation (e.g. control vs perturbed members) is -/// handled elsewhere and is not enforced here. -/// -/// @tparam MarsDict_t -/// Type of the MARS dictionary. Must contain `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 perturbation number is retrieved. -/// -/// @param[in] par -/// Parameter dictionary (unused). -/// -/// @param[in] opt -/// Options dictionary (unused). -/// -/// @return -/// The perturbation number resolved from the MARS dictionary. -/// -/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException -/// If the key `number` is missing, cannot be converted to `long`, or if -/// any unexpected error occurs during deduction. -/// -/// @note -/// This deduction performs no range checks or consistency validation. -/// template long resolve_PerturbationNumber_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { From 2c9128048cef1ae49ad119f10c50dfb04b987839 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Tue, 2 Jun 2026 01:07:03 +0000 Subject: [PATCH 27/35] mars2grib: improvements in time management --- ...zXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json | 8 --- .../reference-time/referenceTimeEncoding.h | 6 +- .../concepts/statistics/statisticsEncoding.h | 46 +++++++++---- .../backend/deductions/detail/ProductTime.h | 31 ++++----- .../backend/deductions/productTime.h | 36 +++++----- .../backend/deductions/timeProducts.md | 67 +++++++++---------- 6 files changed, 103 insertions(+), 91 deletions(-) delete mode 100644 .weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json diff --git a/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json b/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json deleted file mode 100644 index ae6fb66f5..000000000 --- a/.weave/runtime/sessions/c2VzXzE3Yjk3Yjc4NmZmZTlGVk9GUWlkYzRxeVpW.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "session_id": "ses_17b97b786ffe9FVOFQidc4qyZV", - "foreground_agent": "build", - "mode": "ad_hoc", - "execution_ref": null, - "status": "running", - "updated_at": "2026-06-01T23:12:01.763Z" -} \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h index 5883ce3c1..aafb0f65d 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeEncoding.h @@ -215,10 +215,10 @@ void ReferenceTimeOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict // For a reforecast product, the Identification Section's // reference date/time is the hindcast date — i.e. the - // canonical ProductTime::simulationDateTime (from date / + // canonical ProductTime::labelDateTime (from date / // time). The ProductDefinitionSection branch below writes // the model-version date from referenceDateTime instead. - const eckit::DateTime& referenceDateTime = pt.simulationDateTime; + const eckit::DateTime& referenceDateTime = pt.labelDateTime; // Encoding set_or_throw(out, "year", referenceDateTime.date().year()); @@ -236,7 +236,7 @@ void ReferenceTimeOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict // Model-version date/time = ProductTime::referenceDateTime // (derived from date/time or year/month per §7.4). - // TODO: Need to clarify with DGOV if this is reference or simulatedDateTime. + // TODO: Need to clarify with DGOV if this is reference or initialConditionsDateTime. const eckit::DateTime& dateTime = pt.referenceDateTime; // Encoding diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h index f6ad899b7..3519eb3df 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h @@ -35,11 +35,9 @@ /// handled inside `compute_StatisticalProcessing`). /// /// ### StageRuntime -/// - **Intentionally empty in this revision**. The time-dependent keys -/// (`forecastTime` from `pt.windowStart` offset relative to -/// `pt.referenceDateTime`, and the -/// `OfEndOfOverallTimeInterval` set -/// from `pt.windowEnd`) will be populated in a follow-up. +/// - Encodes the time-dependent keys from the resolved `ProductTime`: +/// - `forecastTime` (hours between `pt.referenceDateTime` and `pt.windowStart`) +/// - `OfEndOfOverallTimeInterval` (from `pt.windowEnd`) /// /// All temporal data is sourced exclusively from the `ProductTime` /// produced by `resolve_ProductTime_or_throw` (§15 of `timeProducts.md`). @@ -151,16 +149,38 @@ void StatisticsOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& // ============================================================= // StageRuntime // - // Intentionally empty. Time-dependent keys (forecastTime, - // OfEndOfOverallTimeInterval) - // will be populated in a follow-up using pt.windowStart / - // pt.windowEnd. + // Time-dependent keys: forecastTime and end-of-interval date/time. // ============================================================= if constexpr (Stage == StageRuntime) { - (void)mars; - (void)par; - (void)opt; - (void)out; + auto pt = deductions::resolve_ProductTime_or_throw(mars, par, opt); + + // forecastTime is the distance in hours between reference time + // and WindowBegin (pt.windowStart). It must be a non-negative + // integer number of hours. + const long deltaSeconds = + static_cast(static_cast(pt.windowStart - pt.referenceDateTime)); + + if (deltaSeconds < 0) { + throw Mars2GribConceptException( + std::string(statisticsName), std::string(statisticsTypeName()), std::to_string(Stage), + std::to_string(Section), "statistics: forecastTime must be non-negative", Here()); + } + + if (deltaSeconds % 3600 != 0) { + throw Mars2GribConceptException( + std::string(statisticsName), std::string(statisticsTypeName()), std::to_string(Stage), + std::to_string(Section), "statistics: forecastTime must be a whole number of hours", Here()); + } + + set_or_throw(out, "forecastTime", deltaSeconds / 3600); + + const eckit::DateTime& end = pt.windowEnd; + set_or_throw(out, "yearOfEndOfOverallTimeInterval", end.date().year()); + set_or_throw(out, "monthOfEndOfOverallTimeInterval", end.date().month()); + set_or_throw(out, "dayOfEndOfOverallTimeInterval", end.date().day()); + set_or_throw(out, "hourOfEndOfOverallTimeInterval", end.time().hours()); + set_or_throw(out, "minuteOfEndOfOverallTimeInterval", end.time().minutes()); + set_or_throw(out, "secondOfEndOfOverallTimeInterval", end.time().seconds()); } } catch (...) { diff --git a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h index 80992d09b..614b1ac6a 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -85,13 +85,13 @@ inline constexpr std::size_t maxStatisticalWindows = 3; /// See `deductions/timeProducts.md` §5 for the full contract, including: /// - tri-equivalent instant invariant (§5.1) /// - window-end ordering invariant (§5.2) -/// - reference-vs-simulated invariant (§5.3) +/// - reference-vs-initial-conditions invariant (§5.3) /// - per-consumer field-access table (§15) /// struct ProductTime { - const eckit::DateTime simulationDateTime; - const eckit::DateTime simulatedDateTime; + const eckit::DateTime labelDateTime; + const eckit::DateTime initialConditionsDateTime; const eckit::DateTime referenceDateTime; /// Internal convention: `[windowStart, windowEnd)` when @@ -137,9 +137,9 @@ enum class TimespanKind { /// struct ProductTimeInput { - eckit::DateTime simulationDateTime; + eckit::DateTime labelDateTime; - std::optional simulatedDateTime; ///< from `hdate` / `htime` + std::optional initialConditionsDateTime; ///< from `hdate` / `htime` std::optional referenceDateTime; ///< from `fcyear` / `fcmonth` /// Offset from `referenceDateTime` to `ProductTime::windowEnd`. @@ -427,7 +427,7 @@ inline std::string fmt(const std::array= simulatedDateTime (§10.4) + // §5.3: referenceDateTime >= initialConditionsDateTime (§10.4) // --------------------------------------------------------- - if (referenceDateTime < simulatedDateTime) { + if (referenceDateTime < initialConditionsDateTime) { throw Mars2GribDeductionException("ProductTime invariant violated [§10.4]: referenceDateTime ('" + - fmt(referenceDateTime) + "') < simulatedDateTime ('" + - fmt(simulatedDateTime) + "')", + fmt(referenceDateTime) + "') < initialConditionsDateTime ('" + + fmt(initialConditionsDateTime) + "')", Here()); } @@ -633,7 +634,7 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { // --------------------------------------------------------- // Construct the immutable ProductTime // --------------------------------------------------------- - return ProductTime{simulationDateTime, simulatedDateTime, referenceDateTime, windowStart, + return ProductTime{labelDateTime, initialConditionsDateTime, referenceDateTime, windowStart, windowEnd, windows, windowCount, tInc}; } diff --git a/src/metkit/mars2grib/backend/deductions/productTime.h b/src/metkit/mars2grib/backend/deductions/productTime.h index 74c9c2c30..b8c98cf27 100644 --- a/src/metkit/mars2grib/backend/deductions/productTime.h +++ b/src/metkit/mars2grib/backend/deductions/productTime.h @@ -121,7 +121,7 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P try { // ========================================================= - // §7.1 / §7.2: simulationDateTime from (date, time) or + // §7.1 / §7.2: labelDateTime from (date, time) or // defaulted from (fcyear, fcmonth) // ========================================================= @@ -130,23 +130,23 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P const bool hasFcYear = has(mars, "fcyear"); const bool hasFcMonth = has(mars, "fcmonth"); - eckit::DateTime simulationDateTime; + eckit::DateTime labelDateTime; if (hasDate && hasTime) { const long marsDate = get_or_throw(mars, "date"); const long marsTime = get_or_throw(mars, "time"); - simulationDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsDate), - detail::convert_hhmmss2Time_or_throw(marsTime)); + labelDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsDate), + detail::convert_hhmmss2Time_or_throw(marsTime)); } else if (!hasDate && !hasTime && hasFcYear && hasFcMonth) { - // R2 default: simulationDateTime := DateTime(fcyear, fcmonth, 1, 00:00:00). + // R2 default: labelDateTime := DateTime(fcyear, fcmonth, 1, 00:00:00). const long fcYear = get_or_throw(mars, "fcyear"); const long fcMonth = get_or_throw(mars, "fcmonth"); try { - simulationDateTime = eckit::DateTime(eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); + labelDateTime = eckit::DateTime(eckit::Date(fcYear, fcMonth, 1), eckit::Time(0, 0, 0)); } catch (const eckit::Exception& e) { throw Mars2GribDeductionException( - "Invalid (fcyear, fcmonth) for default simulationDateTime: " + std::string(e.what()), Here()); + "Invalid (fcyear, fcmonth) for default labelDateTime: " + std::string(e.what()), Here()); } } else { @@ -161,26 +161,26 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P } // ========================================================= - // §7.3: simulatedDateTime from (hdate, htime) + // §7.3: initialConditionsDateTime from (hdate, htime) // ========================================================= const bool hasHdate = has(mars, "hdate"); const bool hasHtime = has(mars, "htime"); - std::optional simulatedDateTime; + std::optional initialConditionsDateTime; if (!hasHdate && !hasHtime) { - // simulatedDateTime defaults to simulationDateTime in the factory. + // initialConditionsDateTime defaults to labelDateTime in the factory. } else if (hasHdate && !hasHtime) { const long marsHdate = get_or_throw(mars, "hdate"); - simulatedDateTime = + initialConditionsDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsHdate), eckit::Time(0, 0, 0)); } else if (hasHdate && hasHtime) { const long marsHdate = get_or_throw(mars, "hdate"); const long marsHtime = get_or_throw(mars, "htime"); - simulatedDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsHdate), - detail::convert_hhmmss2Time_or_throw(marsHtime)); + initialConditionsDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsHdate), + detail::convert_hhmmss2Time_or_throw(marsHtime)); } else { // §10.2: htime present without hdate. @@ -194,7 +194,7 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P std::optional referenceDateTime; if (!hasFcYear && !hasFcMonth) { - // referenceDateTime defaults to simulatedDateTime in the factory. + // referenceDateTime defaults to initialConditionsDateTime in the factory. } else if (hasFcYear && hasFcMonth) { const long fcYear = get_or_throw(mars, "fcyear"); @@ -304,8 +304,8 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P // ========================================================= detail::ProductTimeInput input; - input.simulationDateTime = simulationDateTime; - input.simulatedDateTime = simulatedDateTime; + input.labelDateTime = labelDateTime; + input.initialConditionsDateTime = initialConditionsDateTime; input.referenceDateTime = referenceDateTime; input.stepInSeconds = stepInSeconds; input.timespanKind = timespanKind; @@ -322,8 +322,8 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P MARS2GRIB_LOG_RESOLVE([&]() { std::string msg = "`ProductTime` resolved from input dictionaries: "; - msg += "simulationDateTime='" + detail::fmt(pt.simulationDateTime) + "'"; - msg += " simulatedDateTime='" + detail::fmt(pt.simulatedDateTime) + "'"; + msg += "labelDateTime='" + detail::fmt(pt.labelDateTime) + "'"; + msg += " initialConditionsDateTime='" + detail::fmt(pt.initialConditionsDateTime) + "'"; msg += " referenceDateTime='" + detail::fmt(pt.referenceDateTime) + "'"; msg += " windowStart='" + detail::fmt(pt.windowStart) + "'"; msg += " windowEnd='" + detail::fmt(pt.windowEnd) + "'"; diff --git a/src/metkit/mars2grib/backend/deductions/timeProducts.md b/src/metkit/mars2grib/backend/deductions/timeProducts.md index f830d21dc..23486daa6 100644 --- a/src/metkit/mars2grib/backend/deductions/timeProducts.md +++ b/src/metkit/mars2grib/backend/deductions/timeProducts.md @@ -204,8 +204,8 @@ upstream. inline constexpr std::size_t maxStatisticalWindows = 3; struct ProductTime { - const eckit::DateTime simulationDateTime; - const eckit::DateTime simulatedDateTime; + const eckit::DateTime labelDateTime; + const eckit::DateTime initialConditionsDateTime; const eckit::DateTime referenceDateTime; // Internal convention: [windowStart, windowEnd) when windowStart < windowEnd. @@ -257,7 +257,7 @@ Strictly `<` for statistical products; equality reserved for instants. ### 5.3 Reference-vs-simulated invariant ```text -referenceDateTime >= simulatedDateTime (always) +referenceDateTime >= initialConditionsDateTime (always) ``` Violation is a hard error (§10.4). The corner case where reference < simulated @@ -278,9 +278,9 @@ enum class TimespanKind { }; struct ProductTimeInput { - eckit::DateTime simulationDateTime; + eckit::DateTime labelDateTime; - std::optional simulatedDateTime; // from hdate/htime + std::optional initialConditionsDateTime; // from hdate/htime std::optional referenceDateTime; // from fcyear/fcmonth // Offset from referenceDateTime to ProductTime::windowEnd. @@ -337,33 +337,33 @@ The factory: ### 7.2 `date` / `time` -Resolution rule for `simulationDateTime`: +Resolution rule for `labelDateTime`: ```text date present and time present: - simulationDateTime := DateTime(date, time) + labelDateTime := DateTime(date, time) date missing and time missing and fcyear present and fcmonth present: - simulationDateTime := DateTime(Date(fcyear, fcmonth, 1), Time(00:00:00)) + labelDateTime := DateTime(Date(fcyear, fcmonth, 1), Time(00:00:00)) (this is the same value §7.4 produces for referenceDateTime; - consequently simulationDateTime == referenceDateTime in this case) + consequently labelDateTime == referenceDateTime in this case) any other combination -> hard error (§10.1) ``` -`simulationDateTime` is a label for the simulation; it is not used for `step` +`labelDateTime` is a label for the simulation; it is not used for `step` arithmetic. The `hdate`/`htime` rules in §7.3 are unchanged regardless of whether the R2 default in §7.2 is taken: if `hdate` is present it still -drives `simulatedDateTime`, and the §5.3 invariant -`referenceDateTime >= simulatedDateTime` continues to apply (it may now +drives `initialConditionsDateTime`, and the §5.3 invariant +`referenceDateTime >= initialConditionsDateTime` continues to apply (it may now constrain `(fcyear, fcmonth, 1)` against `hdate`). ### 7.3 `hdate` / `htime` ```text -hdate missing, htime missing -> simulatedDateTime := simulationDateTime -hdate present, htime missing -> simulatedDateTime := DateTime(hdate, 00:00:00) -hdate present, htime present -> simulatedDateTime := DateTime(hdate, htime) +hdate missing, htime missing -> initialConditionsDateTime := labelDateTime +hdate present, htime missing -> initialConditionsDateTime := DateTime(hdate, 00:00:00) +hdate present, htime present -> initialConditionsDateTime := DateTime(hdate, htime) hdate missing, htime present -> hard error (§10.2) ``` @@ -372,7 +372,7 @@ hdate missing, htime present -> hard error (§10.2) Both must be present together or both absent. ```text -both missing -> referenceDateTime := simulatedDateTime +both missing -> referenceDateTime := initialConditionsDateTime both present -> referenceDateTime := DateTime(Date(fcyear, fcmonth, 1), Time(00:00:00)) exactly one present -> hard error (§10.3) ``` @@ -518,7 +518,7 @@ the resulting `ProductTime`. | §9.4 New fakeDoubleLoop | `None` | exactly 1 | `[parse(stattype)]` | `windowEnd - statisticalWindows[0]` | optional or required (§9.5) | | any other combination | — | — | — | — | hard error (§10.6, §10.7, §10.8) | -In all rows, `simulationDateTime`, `simulatedDateTime`, `referenceDateTime`, +In all rows, `labelDateTime`, `initialConditionsDateTime`, `referenceDateTime`, and `windowEnd` are computed per §7. The strict-alignment rule (§4.4) is applied after `windowStart` is computed. @@ -642,7 +642,7 @@ in the implementation. | 10.1 | `date` or `time` missing without fallback: `date` missing OR `time` missing, **except** when both `date` AND `time` are missing AND both `fcyear` AND `fcmonth` are present (in which case §7.2 substitutes the default). Note: `step` missing is NOT an error (§7.5 default 0). | | 10.2 | `htime` present without `hdate` | | 10.3 | exactly one of `fcyear` / `fcmonth` present (must be both or neither) | -| 10.4 | `referenceDateTime < simulatedDateTime` | +| 10.4 | `referenceDateTime < initialConditionsDateTime` | | 10.5 | tri-equivalence broken: `windowStart == windowEnd` XOR `statisticalWindowCount == 0` XOR `timeIncrementInSeconds == nullopt` | | 10.6 | `stattype` present but `timespan` missing | | 10.7 | `timespan = none` but `stattype` missing | @@ -698,8 +698,8 @@ in a stable, greppable form. Indicative payload shape: ```text -`ProductTime` resolved from input dictionaries: simulationDateTime='...' \ -simulatedDateTime='...' referenceDateTime='...' windowStart='...' windowEnd='...' \ +`ProductTime` resolved from input dictionaries: labelDateTime='...' \ +initialConditionsDateTime='...' referenceDateTime='...' windowStart='...' windowEnd='...' \ statisticalWindowCount='N' statisticalWindows=['...','...'] timeIncrementInSeconds='...|missing' ``` @@ -738,8 +738,8 @@ the language level; reviewers and consumer-side tests are responsible. | Field | `referenceTime` | `pointInTime` | `statistics` | |-----------------------------|:---------------:|:-------------:|:------------:| -| `simulationDateTime` | R | — | R | -| `simulatedDateTime` | R | — | R | +| `labelDateTime` | R | — | R | +| `initialConditionsDateTime` | R | — | R | | `referenceDateTime` | R | R | R | | `windowStart` | — | — | R | | `windowEnd` | — | R | R | @@ -800,8 +800,8 @@ stattype = (missing) Output: ```text -simulationDateTime = 2026-05-01 00:00:00 -simulatedDateTime = 2026-05-01 00:00:00 +labelDateTime = 2026-05-01 00:00:00 +initialConditionsDateTime = 2026-05-01 00:00:00 referenceDateTime = 2026-05-01 00:00:00 windowEnd = 2026-05-02 00:00:00 windowStart = 2026-05-02 00:00:00 @@ -825,8 +825,8 @@ step = 24 Output: ```text -simulationDateTime = 2026-05-01 00:00:00 -simulatedDateTime = 1993-05-01 00:00:00 +labelDateTime = 2026-05-01 00:00:00 +initialConditionsDateTime = 1993-05-01 00:00:00 referenceDateTime = 1993-05-01 00:00:00 windowEnd = 1993-05-02 00:00:00 windowStart = 1993-05-02 00:00:00 @@ -851,14 +851,14 @@ step = 24 Output: ```text -simulationDateTime = 2026-05-01 00:00:00 -simulatedDateTime = 1993-05-01 00:00:00 +labelDateTime = 2026-05-01 00:00:00 +initialConditionsDateTime = 1993-05-01 00:00:00 referenceDateTime = 1993-05-01 00:00:00 # Date(1993, 5, 1) windowEnd = 1993-05-02 00:00:00 windowStart = 1993-05-02 00:00:00 ``` -Invariant §5.3 holds: `referenceDateTime == simulatedDateTime`. +Invariant §5.3 holds: `referenceDateTime == initialConditionsDateTime`. ### 17.4 Old-style single-loop statistic — hourly accumulation over 1h @@ -962,8 +962,8 @@ stattype = (missing) Output: ```text -simulationDateTime = 2026-05-01 00:00:00 -simulatedDateTime = 2026-05-01 00:00:00 +labelDateTime = 2026-05-01 00:00:00 +initialConditionsDateTime = 2026-05-01 00:00:00 referenceDateTime = 2026-05-01 00:00:00 windowEnd = 2026-05-01 00:00:00 # step defaults to 0 windowStart = 2026-05-01 00:00:00 @@ -987,8 +987,8 @@ step = (missing) Output: ```text -simulationDateTime = 1993-05-01 00:00:00 # defaulted from (fcyear, fcmonth, 1, 00:00:00) -simulatedDateTime = 1993-05-01 00:00:00 # from simulationDateTime, hdate absent +labelDateTime = 1993-05-01 00:00:00 # defaulted from (fcyear, fcmonth, 1, 00:00:00) +initialConditionsDateTime = 1993-05-01 00:00:00 # from labelDateTime, hdate absent referenceDateTime = 1993-05-01 00:00:00 # from (fcyear, fcmonth, 1, 00:00:00) windowEnd = 1993-05-01 00:00:00 # step defaults to 0 windowStart = 1993-05-01 00:00:00 @@ -1543,4 +1543,3 @@ innerTypeOfStatisticalProcessing = Maximum ``` Result: hard error §10.12 (parsed `Average` ≠ argument `Maximum`). - From c89db8d80656f2a6396f82f6448b146258037fc3 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 11 Jun 2026 21:36:43 +0000 Subject: [PATCH 28/35] mars2grib: revert packing modifications due to ecCodes inconsistency --- .../concepts/packing/packingEncoding.h | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h index 47b950210..0a36148df 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h @@ -82,18 +82,10 @@ namespace metkit::mars2grib::backend::concepts_ { template constexpr bool packingApplicable() { - // Most packing algorithms only require configuration at the allocation stage - if constexpr (Stage == StageAllocate && Section == SecDataRepresentationSection) { + if constexpr (Stage == StagePreset && 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; } @@ -173,10 +165,9 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {0}); // Set bits per value - if constexpr (Stage == StageAllocate) { - long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); - set_or_throw(out, "setBitsPerValue", bitsPerValue); - } + long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); + set_or_throw(out, "bitsPerValue", bitsPerValue); + } if constexpr (Variant == PackingType::Ccsds) { @@ -185,10 +176,8 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {42}); // Set bits per value - if constexpr (Stage == StageAllocate) { - long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); - set_or_throw(out, "setBitsPerValue", bitsPerValue); - } + long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); + set_or_throw(out, "bitsPerValue", bitsPerValue); } if constexpr (Variant == PackingType::SpectralComplex) { @@ -197,20 +186,17 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op validation::match_DataRepresentationTemplateNumber_or_throw(opt, out, {51}); // Set bits per value - 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)); - } + long bitsPerValue = deductions::resolve_BitsPerValueSpectral_or_throw(mars, par, opt); + set_or_throw(out, "bitsPerValue", bitsPerValue); + + // 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 (...) { From f30d02160930eee1de8a7b1c8bf2426e39f6a917 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 11 Jun 2026 21:39:18 +0000 Subject: [PATCH 29/35] mars2grib: fix typo in grib key name --- .../mars2grib/backend/concepts/statistics/statisticsEncoding.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h index 3519eb3df..a7b649c0b 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h @@ -143,7 +143,7 @@ void StatisticsOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& set_or_throw>(out, "lengthOfTimeRange", desc.lengthOfTimeRange); set_or_throw>(out, "indicatorOfUnitForTimeIncrement", desc.indicatorOfUnitForTimeIncrement); - set_or_throw>(out, "lengthOfTimeIncrement", desc.lengthOfTimeIncrement); + set_or_throw>(out, "timeIncrement", desc.lengthOfTimeIncrement); } // ============================================================= From f25b1c9011b699908753cf2feae79324b752e75d Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 11 Jun 2026 21:41:41 +0000 Subject: [PATCH 30/35] mars2grib: Fix productTime bugs --- .../backend/deductions/detail/ProductTime.h | 41 +++++++++++++++---- .../backend/deductions/productTime.h | 10 ++++- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h index 614b1ac6a..c4cefde78 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -323,7 +323,28 @@ inline eckit::DateTime subtractCalendarDays(const eckit::DateTime& dt, long coun /// @brief Subtract `count` seconds from a `DateTime`. /// inline eckit::DateTime subtractSeconds(const eckit::DateTime& dt, long count) { - return dt + (-static_cast(count)); + // assert(count >= 0); + + static constexpr long secondsPerDay = 24 * 60 * 60; + + eckit::Date d = dt.date(); + + // eckit::Time has operator eckit::Second(), and eckit::Second is double. + eckit::Second t = dt.time(); // seconds since midnight + + const long wholeDays = count / secondsPerDay; + const long remSecs = count % secondsPerDay; + + d -= wholeDays; + + if (t < static_cast(remSecs)) { + d -= 1; + t += static_cast(secondsPerDay); + } + + t -= static_cast(remSecs); + + return eckit::DateTime(d, eckit::Time(t)); } /// @@ -540,19 +561,21 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { "ProductTime internal error: unhandled (timespanKind, stattypeCount) combination", Here()); } - // --------------------------------------------------------- - // Per-window validation (§3.1, §3.2 → §10.11, §10.18 (b)) - // --------------------------------------------------------- + // ----------------------------------------------------------------- + // Per-window validation (§3.1, §3.2 → §10.11a, §10.11b, §10.18 (b)) + // ----------------------------------------------------------------- for (std::size_t i = 0; i < windowCount; ++i) { const StatisticalWindow& w = windows[i]; if (w.count <= 0) { - throw Mars2GribDeductionException("ProductTime invariant violated [§10.11]: statisticalWindows[" + + throw Mars2GribDeductionException("ProductTime invariant violated [§10.11a]: statisticalWindows[" + std::to_string(i) + "] has non-positive count (" + std::to_string(w.count) + ")", Here()); } + + if (!isAllowedWindowUnit(w.unit)) { throw Mars2GribDeductionException("ProductTime invariant violated [§10.18(b)]: statisticalWindows[" + std::to_string(i) + "] uses disallowed TimeUnit '" + fmt(w.unit) + @@ -586,7 +609,6 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { } } // tables::TimeUnit::Second: no alignment required. - windowStart = applyWindowSubtraction(windowEnd, outermost); } @@ -623,7 +645,7 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { const bool a = (windowStart == windowEnd); const bool b = (windowCount == 0); const bool c = !tInc.has_value(); - if (!((a == b) && (b == c))) { + if (!((a == b) && (b == c) )) { throw Mars2GribDeductionException( std::string("ProductTime invariant violated [§10.5]: tri-equivalence broken: ") + "(windowStart==windowEnd)=" + (a ? "true" : "false") + ", (statisticalWindowCount==0)=" + @@ -631,6 +653,11 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { Here()); } + /// @todo: Handle the case of zero windows with stepInSeconds == 0 (instant product) + /// but timeIncrementInSeconds present. This is technically a violation of the + /// tri-equivalence invariant (§10.5) but we may want to allow it as scientists + /// wants do avoid 2 requests for fields that have an increasing window. + // --------------------------------------------------------- // Construct the immutable ProductTime // --------------------------------------------------------- diff --git a/src/metkit/mars2grib/backend/deductions/productTime.h b/src/metkit/mars2grib/backend/deductions/productTime.h index b8c98cf27..a8a766375 100644 --- a/src/metkit/mars2grib/backend/deductions/productTime.h +++ b/src/metkit/mars2grib/backend/deductions/productTime.h @@ -112,6 +112,7 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P using metkit::mars2grib::utils::dict_traits::get_opt; using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::dict_traits::dict_to_json; using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; // Suppress "unused parameter" warning while preserving the documented @@ -291,13 +292,18 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P stattypeWindows[i] = blocks[i].timeWindow; } stattypeWindowCount = blocks.size(); + } // ========================================================= // §7.8: timeIncrementInSeconds (par) // ========================================================= - - const std::optional tInc = timeIncrementInSeconds_opt(mars, par); + std::optional tInc = std::nullopt; + + if ( (timespanKind == detail::TimespanKind::None && stattypeWindowCount ==1) || + timespanKind == detail::TimespanKind::Duration ) { + tInc = timeIncrementInSeconds_opt(mars, par); + } // ========================================================= // Assemble the input bundle and call the factory. From aed9772431411147c02142e5c85b16eecffcc12a Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 11 Jun 2026 23:16:23 +0000 Subject: [PATCH 31/35] mars2grib: Add check for fakeDoubleLoop --- .../backend/deductions/productTime.h | 88 ++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/src/metkit/mars2grib/backend/deductions/productTime.h b/src/metkit/mars2grib/backend/deductions/productTime.h index a8a766375..4a9f2011f 100644 --- a/src/metkit/mars2grib/backend/deductions/productTime.h +++ b/src/metkit/mars2grib/backend/deductions/productTime.h @@ -43,6 +43,7 @@ // System includes #include #include +#include #include #include #include @@ -68,12 +69,46 @@ namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Test whether a single-loop statistic requires fakeDoubleLoop representation. +/// +/// FakeDoubleLoop is a compatibility representation for selected single-loop +/// statistics where `timespan="none"` and the single `stattype` block carries +/// the statistical period. Products outside this allow-list must use the +/// standard single-loop representation (`timespan` duration, no `stattype`). +/// +/// @param[in] klass MARS `class` value. +/// @param[in] stream MARS `stream` value. +/// @return `true` when fakeDoubleLoop representation is required. +/// +inline bool requiresFakeDoubleLoopRepresentation(const std::string& klass, const std::string& stream) { + + // Rule valid for ERA6 products. + if (klass == "e6" && (stream == "sttd" || stream == "stte")) { + return true; + } + + // Rule valid for SEAS6 products. + if ((klass == "od" || klass == "rd" || klass == "c3") && (stream == "sfmd" || stream == "shmd")) { + return true; + } + + // Other explicitly enabled products. + if ((klass == "gh" || klass == "eh") && (stream == "msmm" || stream == "rfsd")) { + return true; + } + + return false; +} + /// /// @brief Resolve the canonical `ProductTime` for one MARS product. /// /// @section Deduction contract /// - Reads (MARS): `date`, `time`, `hdate`, `htime`, `fcyear`, `fcmonth`, -/// `step`, `timespan`, `stattype` +/// `step`, `timespan`, `stattype`; additionally `class` and +/// `stream` when validating single-loop statistics /// - Reads (par): `timeIncrementInSeconds` (via `timeIncrementInSeconds_opt`) /// - Reads (opt): none (signature-only, reserved) /// - Writes: none @@ -86,6 +121,11 @@ namespace metkit::mars2grib::backend::deductions { /// 2. **Factory** (`detail::make_ProductTime_or_throw`): validates all /// invariants and returns the immutable `ProductTime`. /// +/// For single-loop statistics, the resolver also validates whether the product +/// must use the standard representation (`timespan` duration, no `stattype`) or +/// the fakeDoubleLoop representation (`timespan="none"`, one `stattype` block), +/// according to the `(class, stream)` allow-list. +/// /// On success, exactly one composite RESOLVE log line is emitted listing /// every resolved field in a stable, greppable form (§12). /// @@ -296,7 +336,49 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P } // ========================================================= - // §7.8: timeIncrementInSeconds (par) + // §7.8: single-loop representation policy + // ========================================================= + + const bool isStandardSingleLoopStatistic = + (timespanKind == detail::TimespanKind::Duration && stattypeWindowCount == 0); + const bool isFakeDoubleLoopSingleLoopStatistic = + (timespanKind == detail::TimespanKind::None && stattypeWindowCount == 1); + + if (isStandardSingleLoopStatistic || isFakeDoubleLoopSingleLoopStatistic) { + std::string klass; + std::string stream; + try { + klass = get_or_throw(mars, "class"); + stream = get_or_throw(mars, "stream"); + } + catch (...) { + std::throw_with_nested(Mars2GribDeductionException( + "ProductTime invariant violated [§10.19]: single-loop statistic requires MARS 'class' and " + "'stream' to validate the standard/fakeDoubleLoop representation policy", + Here())); + } + + const bool requiresFakeDoubleLoop = requiresFakeDoubleLoopRepresentation(klass, stream); + + if (isStandardSingleLoopStatistic && requiresFakeDoubleLoop) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.20]: standard single-loop statistic is not valid for " + "class='" + + klass + "', stream='" + stream + "'; fakeDoubleLoop representation is required", + Here()); + } + + if (isFakeDoubleLoopSingleLoopStatistic && !requiresFakeDoubleLoop) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.21]: fakeDoubleLoop single-loop statistic is not valid for " + "class='" + + klass + "', stream='" + stream + "'; standard single-loop representation is required", + Here()); + } + } + + // ========================================================= + // §7.9: timeIncrementInSeconds (par) // ========================================================= std::optional tInc = std::nullopt; @@ -370,4 +452,4 @@ inline long numberOfTimeRanges(const detail::ProductTime& pt) { return static_cast(pt.statisticalWindowCount); } -} // namespace metkit::mars2grib::backend::deductions +} // namespace metkit::mars2grib::backend::deductions \ No newline at end of file From 8b23de161d1048f4da29473270e70d4f60c2d57c Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 11 Jun 2026 23:17:17 +0000 Subject: [PATCH 32/35] mars2grib: Add error handling to all the relevant code for productTime --- .../backend/deductions/detail/ProductTime.h | 865 +++++++++++------- 1 file changed, 555 insertions(+), 310 deletions(-) diff --git a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h index c4cefde78..0390247e5 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -32,6 +32,14 @@ /// /// All rules in this file are normative; see `deductions/timeProducts.md`. /// +/// Error handling follows the mars2grib generic fail-fast pattern: +/// - each helper/factory executes its complete body inside a `try` block +/// - all exceptions are caught at the function boundary +/// - failures are rethrown with `std::throw_with_nested` as +/// `Mars2GribGenericException` +/// - the original low-level exception is preserved as the nested cause +/// - successful, non-exception execution is left unchanged +/// /// @ingroup mars2grib_backend_deductions /// @@ -39,6 +47,8 @@ // System includes #include +#include +#include #include #include #include @@ -174,91 +184,133 @@ struct ProductTimeInput { /// - `s` seconds /// - `d` days /// -/// @throws Mars2GribDeductionException on malformed input. +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the duration string is malformed or cannot be converted. The original +/// exception is preserved as a nested exception. /// inline long toSeconds_or_throw(std::string_view step) { - using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - if (step.empty()) { - throw Mars2GribDeductionException("Empty duration string", Here()); - } + try { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; - std::size_t pos = 0; - while (pos < step.size() && std::isdigit(static_cast(step[pos]))) { - ++pos; - } + if (step.empty()) { + throw Mars2GribDeductionException("Empty duration string", Here()); + } - if (pos == 0) { - throw Mars2GribDeductionException("Invalid duration format (no numeric part): '" + std::string(step) + "'", - Here()); - } + std::size_t pos = 0; + while (pos < step.size() && std::isdigit(static_cast(step[pos]))) { + ++pos; + } - long value = 0; - try { - value = std::stol(std::string(step.substr(0, pos))); + if (pos == 0) { + throw Mars2GribDeductionException("Invalid duration format (no numeric part): '" + std::string(step) + "'", + Here()); + } + + long value = 0; + try { + value = std::stol(std::string(step.substr(0, pos))); + } + catch (...) { + throw Mars2GribDeductionException("Invalid numeric value in duration: '" + std::string(step) + "'", Here()); + } + + char unit = 'h'; // default unit + if (pos < step.size()) { + if (pos + 1 != step.size()) { + throw Mars2GribDeductionException( + "Invalid duration format (trailing characters): '" + std::string(step) + "'", Here()); + } + unit = step[pos]; + } + + switch (unit) { + case 'h': + return value * 3600L; + case 'm': + return value * 60L; + case 's': + return value; + case 'd': + return value * 86400L; + default: + throw Mars2GribDeductionException(std::string("Unknown duration unit: '") + unit + "', expected={h,m,s,d}", + Here()); + } } catch (...) { - throw Mars2GribDeductionException("Invalid numeric value in duration: '" + std::string(step) + "'", Here()); - } - char unit = 'h'; // default unit - if (pos < step.size()) { - if (pos + 1 != step.size()) { - throw Mars2GribDeductionException( - "Invalid duration format (trailing characters): '" + std::string(step) + "'", Here()); - } - unit = step[pos]; - } - - switch (unit) { - case 'h': - return value * 3600L; - case 'm': - return value * 60L; - case 's': - return value; - case 'd': - return value * 86400L; - default: - throw Mars2GribDeductionException(std::string("Unknown duration unit: '") + unit + "', expected={h,m,s,d}", - Here()); + std::throw_with_nested(Mars2GribGenericException("Failed to convert duration string to seconds", Here())); } + + mars2gribUnreachable(); } /// /// @brief Convert a MARS-encoded `YYYYMMDD` integer to `eckit::Date`. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline eckit::Date convert_YYYYMMDD2Date_or_throw(long YYYYMMDD) { - using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; - - long YYYY = YYYYMMDD / 10000; - long MM = (YYYYMMDD / 100) % 100; - long DD = YYYYMMDD % 100; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; try { - return eckit::Date(YYYY, MM, DD); + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + long YYYY = YYYYMMDD / 10000; + long MM = (YYYYMMDD / 100) % 100; + long DD = YYYYMMDD % 100; + + try { + return eckit::Date(YYYY, MM, DD); + } + catch (const eckit::Exception& e) { + throw Mars2GribDeductionException("Invalid date value '" + std::to_string(YYYYMMDD) + "': " + e.what(), Here()); + } } - catch (const eckit::Exception& e) { - throw Mars2GribDeductionException("Invalid date value '" + std::to_string(YYYYMMDD) + "': " + e.what(), Here()); + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to convert YYYYMMDD value to eckit::Date", Here())); } + + mars2gribUnreachable(); } /// /// @brief Convert a MARS-encoded `HHMMSS` integer to `eckit::Time`. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline eckit::Time convert_hhmmss2Time_or_throw(long hhmmss) { - using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; - - long hh = hhmmss / 10000; - long mm = (hhmmss / 100) % 100; - long ss = hhmmss % 100; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; try { - return eckit::Time(hh, mm, ss); + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + long hh = hhmmss / 10000; + long mm = (hhmmss / 100) % 100; + long ss = hhmmss % 100; + + try { + return eckit::Time(hh, mm, ss); + } + catch (const eckit::Exception& e) { + throw Mars2GribDeductionException("Invalid time value '" + std::to_string(hhmmss) + "': " + e.what(), Here()); + } } - catch (const eckit::Exception& e) { - throw Mars2GribDeductionException("Invalid time value '" + std::to_string(hhmmss) + "': " + e.what(), Here()); + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to convert HHMMSS value to eckit::Time", Here())); } + + mars2gribUnreachable(); } // ============================================================= @@ -269,16 +321,46 @@ inline eckit::Time convert_hhmmss2Time_or_throw(long hhmmss) { /// @brief Test whether a `tables::TimeUnit` value belongs to the /// `StatisticalWindow` allow-list `{Second, Day, Month}` (§3.1). /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline bool isAllowedWindowUnit(tables::TimeUnit u) { - return u == tables::TimeUnit::Second || u == tables::TimeUnit::Day || u == tables::TimeUnit::Month; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + return u == tables::TimeUnit::Second || u == tables::TimeUnit::Day || u == tables::TimeUnit::Month; + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to classify TimeUnit allow-list membership", Here())); + } + + mars2gribUnreachable(); } /// /// @brief Test whether a `tables::TimeUnit` value is calendar-aligned /// (i.e. `Day` or `Month`) per the classification table in §3.3. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline bool isCalendarUnit(tables::TimeUnit u) { - return u == tables::TimeUnit::Day || u == tables::TimeUnit::Month; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + return u == tables::TimeUnit::Day || u == tables::TimeUnit::Month; + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to classify TimeUnit calendar alignment", Here())); + } + + mars2gribUnreachable(); } // ============================================================= @@ -291,21 +373,36 @@ inline bool isCalendarUnit(tables::TimeUnit u) { /// Precondition: `dt` is on day=1 at 00:00:00 (verified by alignment check /// (§10.10) before this function is called). /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline eckit::DateTime subtractCalendarMonths(const eckit::DateTime& dt, long count) { - long year = dt.date().year(); - long month = dt.date().month(); + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + long year = dt.date().year(); + long month = dt.date().month(); + + // Convert to (year * 12 + (month-1)) basis so subtraction is trivial. + long total = year * 12L + (month - 1L) - count; + long newYear = total / 12L; + long newMonthIdx = total % 12L; + if (newMonthIdx < 0) { + newMonthIdx += 12L; + newYear -= 1L; + } + long newMonth = newMonthIdx + 1L; + + return eckit::DateTime(eckit::Date(newYear, newMonth, 1), eckit::Time(0, 0, 0)); + } + catch (...) { - // Convert to (year * 12 + (month-1)) basis so subtraction is trivial. - long total = year * 12L + (month - 1L) - count; - long newYear = total / 12L; - long newMonthIdx = total % 12L; - if (newMonthIdx < 0) { - newMonthIdx += 12L; - newYear -= 1L; + std::throw_with_nested(Mars2GribGenericException("Failed to subtract calendar months from DateTime", Here())); } - long newMonth = newMonthIdx + 1L; - return eckit::DateTime(eckit::Date(newYear, newMonth, 1), eckit::Time(0, 0, 0)); + mars2gribUnreachable(); } /// @@ -313,38 +410,68 @@ inline eckit::DateTime subtractCalendarMonths(const eckit::DateTime& dt, long co /// /// Precondition: `dt` is at 00:00:00 (verified by alignment check (§10.9)). /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline eckit::DateTime subtractCalendarDays(const eckit::DateTime& dt, long count) { - eckit::Date d = dt.date(); - d -= count; - return eckit::DateTime(d, eckit::Time(0, 0, 0)); + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + eckit::Date d = dt.date(); + d -= count; + return eckit::DateTime(d, eckit::Time(0, 0, 0)); + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to subtract calendar days from DateTime", Here())); + } + + mars2gribUnreachable(); } /// /// @brief Subtract `count` seconds from a `DateTime`. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline eckit::DateTime subtractSeconds(const eckit::DateTime& dt, long count) { - // assert(count >= 0); + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - static constexpr long secondsPerDay = 24 * 60 * 60; + try { + // assert(count >= 0); + + static constexpr long secondsPerDay = 24 * 60 * 60; + + eckit::Date d = dt.date(); - eckit::Date d = dt.date(); + // eckit::Time has operator eckit::Second(), and eckit::Second is double. + eckit::Second t = dt.time(); // seconds since midnight - // eckit::Time has operator eckit::Second(), and eckit::Second is double. - eckit::Second t = dt.time(); // seconds since midnight + const long wholeDays = count / secondsPerDay; + const long remSecs = count % secondsPerDay; - const long wholeDays = count / secondsPerDay; - const long remSecs = count % secondsPerDay; + d -= wholeDays; - d -= wholeDays; + if (t < static_cast(remSecs)) { + d -= 1; + t += static_cast(secondsPerDay); + } + + t -= static_cast(remSecs); - if (t < static_cast(remSecs)) { - d -= 1; - t += static_cast(secondsPerDay); + return eckit::DateTime(d, eckit::Time(t)); } + catch (...) { - t -= static_cast(remSecs); + std::throw_with_nested(Mars2GribGenericException("Failed to subtract seconds from DateTime", Here())); + } - return eckit::DateTime(d, eckit::Time(t)); + mars2gribUnreachable(); } /// @@ -353,20 +480,35 @@ inline eckit::DateTime subtractSeconds(const eckit::DateTime& dt, long count) { /// Dispatches on `window.unit`. Precondition: `window.unit` is in the /// allow-list and any required alignment has been verified by the caller. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline eckit::DateTime applyWindowSubtraction(const eckit::DateTime& windowEnd, const StatisticalWindow& window) { - using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; - - switch (window.unit) { - case tables::TimeUnit::Second: - return subtractSeconds(windowEnd, window.count); - case tables::TimeUnit::Day: - return subtractCalendarDays(windowEnd, window.count); - case tables::TimeUnit::Month: - return subtractCalendarMonths(windowEnd, window.count); - default: - throw Mars2GribDeductionException("Internal error: applyWindowSubtraction called with disallowed unit", - Here()); + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + switch (window.unit) { + case tables::TimeUnit::Second: + return subtractSeconds(windowEnd, window.count); + case tables::TimeUnit::Day: + return subtractCalendarDays(windowEnd, window.count); + case tables::TimeUnit::Month: + return subtractCalendarMonths(windowEnd, window.count); + default: + throw Mars2GribDeductionException("Internal error: applyWindowSubtraction called with disallowed unit", + Here()); + } + mars2gribUnreachable(); + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to apply statistical-window subtraction", Here())); } + mars2gribUnreachable(); } @@ -377,16 +519,46 @@ inline eckit::DateTime applyWindowSubtraction(const eckit::DateTime& windowEnd, /// /// @brief Test whether a `DateTime` is at hh=00, mm=00, ss=00. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline bool isAtMidnight(const eckit::DateTime& dt) { - const eckit::Time& t = dt.time(); - return t.hours() == 0 && t.minutes() == 0 && t.seconds() == 0; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + const eckit::Time& t = dt.time(); + return t.hours() == 0 && t.minutes() == 0 && t.seconds() == 0; + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to test DateTime midnight alignment", Here())); + } + + mars2gribUnreachable(); } /// /// @brief Test whether a `DateTime` is on day=1 at midnight. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline bool isOnFirstOfMonthMidnight(const eckit::DateTime& dt) { - return isAtMidnight(dt) && dt.date().day() == 1; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + return isAtMidnight(dt) && dt.date().day() == 1; + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to test DateTime first-of-month midnight alignment", Here())); + } + + mars2gribUnreachable(); } // ============================================================= @@ -396,8 +568,23 @@ inline bool isOnFirstOfMonthMidnight(const eckit::DateTime& dt) { /// /// @brief Format an `eckit::DateTime` as an ISO-8601 string for logs/errors. /// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// +/// inline std::string fmt(const eckit::DateTime& dt) { - return dt.iso(true); + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + return dt.iso(true); + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to format DateTime", Here())); + } + + mars2gribUnreachable(); } /// @@ -406,38 +593,83 @@ inline std::string fmt(const eckit::DateTime& dt) { /// Uses the canonical names produced by `tables::enum2name_TimeUnit_or_throw` /// for the allow-list values, with a numeric fallback for any other value. /// +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// inline std::string fmt(tables::TimeUnit u) { - switch (u) { - case tables::TimeUnit::Second: - return "second"; - case tables::TimeUnit::Day: - return "day"; - case tables::TimeUnit::Month: - return "month"; - default: - return "TimeUnit(" + std::to_string(static_cast(u)) + ")"; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + switch (u) { + case tables::TimeUnit::Second: + return "second"; + case tables::TimeUnit::Day: + return "day"; + case tables::TimeUnit::Month: + return "month"; + default: + return "TimeUnit(" + std::to_string(static_cast(u)) + ")"; + } } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to format TimeUnit", Here())); + } + + mars2gribUnreachable(); } /// /// @brief Format a `StatisticalWindow` as `{unit,count}`. /// +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// inline std::string fmt(const StatisticalWindow& w) { - return "{" + fmt(w.unit) + "," + std::to_string(w.count) + "}"; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + return "{" + fmt(w.unit) + "," + std::to_string(w.count) + "}"; + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to format StatisticalWindow", Here())); + } + + mars2gribUnreachable(); } /// /// @brief Format the populated prefix of a `StatisticalWindow` array. /// +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If the helper fails. The original exception is preserved as a nested +/// exception. +/// inline std::string fmt(const std::array& a, std::size_t count) { - std::string s{"["}; - for (std::size_t i = 0; i < count; ++i) { - if (i) - s += ","; - s += fmt(a[i]); - } - s += "]"; - return s; + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + std::string s{"["}; + for (std::size_t i = 0; i < count; ++i) { + if (i) + s += ","; + s += fmt(a[i]); + } + s += "]"; + return s; + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to format StatisticalWindow array", Here())); + } + + mars2gribUnreachable(); } // ============================================================= @@ -455,214 +687,227 @@ inline std::string fmt(const std::array= initialConditionsDateTime (§10.4) - // --------------------------------------------------------- - if (referenceDateTime < initialConditionsDateTime) { - throw Mars2GribDeductionException("ProductTime invariant violated [§10.4]: referenceDateTime ('" + - fmt(referenceDateTime) + "') < initialConditionsDateTime ('" + - fmt(initialConditionsDateTime) + "')", - Here()); - } - - // --------------------------------------------------------- - // windowEnd (§7.5) - // --------------------------------------------------------- - const eckit::DateTime windowEnd = referenceDateTime + static_cast(input.stepInSeconds); - - // --------------------------------------------------------- - // §9: window assembly - // --------------------------------------------------------- - std::array windows{}; - std::size_t windowCount = 0; - eckit::DateTime windowStart = windowEnd; - - const bool hasTimespanDuration = (input.timespanKind == TimespanKind::Duration); - const bool hasTimespanNone = (input.timespanKind == TimespanKind::None); - const bool hasTimespanMissing = (input.timespanKind == TimespanKind::Missing); - const std::size_t nStat = input.stattypeWindowCount; - - // Case dispatch (§9). - if (hasTimespanMissing && nStat == 0) { - // ----- §9.1: Instant product ----- - windowCount = 0; - windowStart = windowEnd; - } - else if (hasTimespanDuration && nStat == 0) { - // ----- §9.2: Old-style single-loop statistic ----- - windows[0] = input.timespan; - windowCount = 1; - // windowStart computed after allow-list / positivity / alignment. - } - else if (hasTimespanDuration && nStat >= 1) { - // ----- §9.3: Old-style multi-loop statistic ----- - if (nStat + 1 > maxStatisticalWindows) { - throw Mars2GribDeductionException("ProductTime invariant violated [§10.15]: statisticalWindowCount (" + - std::to_string(nStat + 1) + ") > maxStatisticalWindows (" + - std::to_string(maxStatisticalWindows) + ")", + // §7.3: hdate / htime defaulting (the resolver has already enforced + // §10.2; here we just apply the fall-through to labelDateTime). + const eckit::DateTime initialConditionsDateTime = + input.initialConditionsDateTime.value_or(labelDateTime); + + // §7.4: fcyear / fcmonth defaulting. + const eckit::DateTime referenceDateTime = input.referenceDateTime.value_or(initialConditionsDateTime); + + // --------------------------------------------------------- + // §5.3: referenceDateTime >= initialConditionsDateTime (§10.4) + // --------------------------------------------------------- + if (referenceDateTime < initialConditionsDateTime) { + throw Mars2GribDeductionException("ProductTime invariant violated [§10.4]: referenceDateTime ('" + + fmt(referenceDateTime) + "') < initialConditionsDateTime ('" + + fmt(initialConditionsDateTime) + "')", Here()); } - for (std::size_t i = 0; i < nStat; ++i) { - windows[i] = input.stattypeWindows[i]; + + // --------------------------------------------------------- + // windowEnd (§7.5) + // --------------------------------------------------------- + const eckit::DateTime windowEnd = referenceDateTime + static_cast(input.stepInSeconds); + + // --------------------------------------------------------- + // §9: window assembly + // --------------------------------------------------------- + std::array windows{}; + std::size_t windowCount = 0; + eckit::DateTime windowStart = windowEnd; + + const bool hasTimespanDuration = (input.timespanKind == TimespanKind::Duration); + const bool hasTimespanNone = (input.timespanKind == TimespanKind::None); + const bool hasTimespanMissing = (input.timespanKind == TimespanKind::Missing); + const std::size_t nStat = input.stattypeWindowCount; + + // Case dispatch (§9). + if (hasTimespanMissing && nStat == 0) { + // ----- §9.1: Instant product ----- + windowCount = 0; + windowStart = windowEnd; } - windows[nStat] = input.timespan; - windowCount = nStat + 1; - } - else if (hasTimespanNone && nStat == 1) { - // ----- §9.4: New-style fakeDoubleLoop ----- - windows[0] = input.stattypeWindows[0]; - windowCount = 1; - } - else if (hasTimespanNone && nStat == 0) { - // §10.7: timespan = none but stattype missing. - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.7]: timespan='none' requires " - "exactly one stattype block, got 0", - Here()); - } - else if (hasTimespanNone && nStat > 1) { - // §10.8: timespan = none with more than one stattype block. - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.8]: timespan='none' requires " - "exactly one stattype block, got " + - std::to_string(nStat), - Here()); - } - else if (hasTimespanMissing && nStat >= 1) { - // §10.6: stattype present but timespan missing. - throw Mars2GribDeductionException("ProductTime invariant violated [§10.6]: stattype present (" + - std::to_string(nStat) + " block(s)) but timespan is missing", - Here()); - } - else { - // Defensive: all combinations should be covered above. - throw Mars2GribDeductionException( - "ProductTime internal error: unhandled (timespanKind, stattypeCount) combination", Here()); - } - - // ----------------------------------------------------------------- - // Per-window validation (§3.1, §3.2 → §10.11a, §10.11b, §10.18 (b)) - // ----------------------------------------------------------------- - for (std::size_t i = 0; i < windowCount; ++i) { - const StatisticalWindow& w = windows[i]; - - if (w.count <= 0) { - throw Mars2GribDeductionException("ProductTime invariant violated [§10.11a]: statisticalWindows[" + - std::to_string(i) + "] has non-positive count (" + - std::to_string(w.count) + ")", + else if (hasTimespanDuration && nStat == 0) { + // ----- §9.2: Old-style single-loop statistic ----- + windows[0] = input.timespan; + windowCount = 1; + // windowStart computed after allow-list / positivity / alignment. + } + else if (hasTimespanDuration && nStat >= 1) { + // ----- §9.3: Old-style multi-loop statistic ----- + if (nStat + 1 > maxStatisticalWindows) { + throw Mars2GribDeductionException("ProductTime invariant violated [§10.15]: statisticalWindowCount (" + + std::to_string(nStat + 1) + ") > maxStatisticalWindows (" + + std::to_string(maxStatisticalWindows) + ")", + Here()); + } + for (std::size_t i = 0; i < nStat; ++i) { + windows[i] = input.stattypeWindows[i]; + } + windows[nStat] = input.timespan; + windowCount = nStat + 1; + } + else if (hasTimespanNone && nStat == 1) { + // ----- §9.4: New-style fakeDoubleLoop ----- + windows[0] = input.stattypeWindows[0]; + windowCount = 1; + } + else if (hasTimespanNone && nStat == 0) { + // §10.7: timespan = none but stattype missing. + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.7]: timespan='none' requires " + "exactly one stattype block, got 0", + Here()); + } + else if (hasTimespanNone && nStat > 1) { + // §10.8: timespan = none with more than one stattype block. + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.8]: timespan='none' requires " + "exactly one stattype block, got " + + std::to_string(nStat), + Here()); + } + else if (hasTimespanMissing && nStat >= 1) { + // §10.6: stattype present but timespan missing. + throw Mars2GribDeductionException("ProductTime invariant violated [§10.6]: stattype present (" + + std::to_string(nStat) + " block(s)) but timespan is missing", Here()); } + else { + // Defensive: all combinations should be covered above. + throw Mars2GribDeductionException( + "ProductTime internal error: unhandled (timespanKind, stattypeCount) combination", Here()); + } + + // ----------------------------------------------------------------- + // Per-window validation (§3.1, §3.2 → §10.11a, §10.11b, §10.18 (b)) + // ----------------------------------------------------------------- + for (std::size_t i = 0; i < windowCount; ++i) { + const StatisticalWindow& w = windows[i]; + + if (w.count <= 0) { + throw Mars2GribDeductionException("ProductTime invariant violated [§10.11a]: statisticalWindows[" + + std::to_string(i) + "] has non-positive count (" + + std::to_string(w.count) + ")", + Here()); + } - if (!isAllowedWindowUnit(w.unit)) { - throw Mars2GribDeductionException("ProductTime invariant violated [§10.18(b)]: statisticalWindows[" + - std::to_string(i) + "] uses disallowed TimeUnit '" + fmt(w.unit) + - "', expected one of {second, day, month}", + if (!isAllowedWindowUnit(w.unit)) { + throw Mars2GribDeductionException("ProductTime invariant violated [§10.18(b)]: statisticalWindows[" + + std::to_string(i) + "] uses disallowed TimeUnit '" + fmt(w.unit) + + "', expected one of {second, day, month}", + Here()); + } + } + + // --------------------------------------------------------- + // Outermost-window alignment (§4.4, §10.9, §10.10) and windowStart + // --------------------------------------------------------- + if (windowCount > 0) { + const StatisticalWindow& outermost = windows[0]; + + if (outermost.unit == tables::TimeUnit::Day) { + if (!isAtMidnight(windowEnd)) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.9]: outermost window is " + "calendar-day-aligned but windowEnd ('" + + fmt(windowEnd) + "') is not at hh=00,mm=00,ss=00", + Here()); + } + } + else if (outermost.unit == tables::TimeUnit::Month) { + if (!isOnFirstOfMonthMidnight(windowEnd)) { + throw Mars2GribDeductionException( + "ProductTime invariant violated [§10.10]: outermost window is " + "calendar-month-aligned but windowEnd ('" + + fmt(windowEnd) + "') is not on day=1 at hh=00,mm=00,ss=00", + Here()); + } + } + // tables::TimeUnit::Second: no alignment required. + windowStart = applyWindowSubtraction(windowEnd, outermost); + } + + // --------------------------------------------------------- + // §5.2: windowStart <= windowEnd (defensive) + // --------------------------------------------------------- + if (windowStart > windowEnd) { + throw Mars2GribDeductionException("ProductTime invariant violated [§5.2]: windowStart ('" + fmt(windowStart) + + "') > windowEnd ('" + fmt(windowEnd) + "')", Here()); } - } - // --------------------------------------------------------- - // Outermost-window alignment (§4.4, §10.9, §10.10) and windowStart - // --------------------------------------------------------- - if (windowCount > 0) { - const StatisticalWindow& outermost = windows[0]; + // --------------------------------------------------------- + // timeIncrementInSeconds validation (§7.8, §9.5, §10.13, §10.14) + // --------------------------------------------------------- + std::optional tInc = input.timeIncrementInSeconds; - if (outermost.unit == tables::TimeUnit::Day) { - if (!isAtMidnight(windowEnd)) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.9]: outermost window is " - "calendar-day-aligned but windowEnd ('" + - fmt(windowEnd) + "') is not at hh=00,mm=00,ss=00", - Here()); - } + if (tInc.has_value() && tInc.value() < 0) { + throw Mars2GribDeductionException("ProductTime invariant violated [§10.14]: timeIncrementInSeconds < 0 ('" + + std::to_string(tInc.value()) + "')", + Here()); } - else if (outermost.unit == tables::TimeUnit::Month) { - if (!isOnFirstOfMonthMidnight(windowEnd)) { - throw Mars2GribDeductionException( - "ProductTime invariant violated [§10.10]: outermost window is " - "calendar-month-aligned but windowEnd ('" + - fmt(windowEnd) + "') is not on day=1 at hh=00,mm=00,ss=00", - Here()); - } + + if (windowCount >= 2 && !tInc.has_value()) { + throw Mars2GribDeductionException("ProductTime invariant violated [§10.13]: statisticalWindowCount (" + + std::to_string(windowCount) + + ") >= 2 requires timeIncrementInSeconds to be present", + Here()); } - // tables::TimeUnit::Second: no alignment required. - windowStart = applyWindowSubtraction(windowEnd, outermost); - } - - // --------------------------------------------------------- - // §5.2: windowStart <= windowEnd (defensive) - // --------------------------------------------------------- - if (windowStart > windowEnd) { - throw Mars2GribDeductionException("ProductTime invariant violated [§5.2]: windowStart ('" + fmt(windowStart) + - "') > windowEnd ('" + fmt(windowEnd) + "')", - Here()); - } - - // --------------------------------------------------------- - // timeIncrementInSeconds validation (§7.8, §9.5, §10.13, §10.14) - // --------------------------------------------------------- - std::optional tInc = input.timeIncrementInSeconds; - - if (tInc.has_value() && tInc.value() < 0) { - throw Mars2GribDeductionException("ProductTime invariant violated [§10.14]: timeIncrementInSeconds < 0 ('" + - std::to_string(tInc.value()) + "')", - Here()); - } - - if (windowCount >= 2 && !tInc.has_value()) { - throw Mars2GribDeductionException("ProductTime invariant violated [§10.13]: statisticalWindowCount (" + - std::to_string(windowCount) + - ") >= 2 requires timeIncrementInSeconds to be present", - Here()); - } - - // --------------------------------------------------------- - // §5.1: tri-equivalent instant invariant (§10.5) - // --------------------------------------------------------- - const bool a = (windowStart == windowEnd); - const bool b = (windowCount == 0); - const bool c = !tInc.has_value(); - if (!((a == b) && (b == c) )) { - throw Mars2GribDeductionException( - std::string("ProductTime invariant violated [§10.5]: tri-equivalence broken: ") + - "(windowStart==windowEnd)=" + (a ? "true" : "false") + ", (statisticalWindowCount==0)=" + - (b ? "true" : "false") + ", (timeIncrementInSeconds==nullopt)=" + (c ? "true" : "false"), - Here()); - } - - /// @todo: Handle the case of zero windows with stepInSeconds == 0 (instant product) - /// but timeIncrementInSeconds present. This is technically a violation of the - /// tri-equivalence invariant (§10.5) but we may want to allow it as scientists - /// wants do avoid 2 requests for fields that have an increasing window. - - // --------------------------------------------------------- - // Construct the immutable ProductTime - // --------------------------------------------------------- - return ProductTime{labelDateTime, initialConditionsDateTime, referenceDateTime, windowStart, - windowEnd, windows, windowCount, tInc}; + + // --------------------------------------------------------- + // §5.1: tri-equivalent instant invariant (§10.5) + // --------------------------------------------------------- + const bool a = (windowStart == windowEnd); + const bool b = (windowCount == 0); + const bool c = !tInc.has_value(); + if (!((a == b) && (b == c) )) { + throw Mars2GribDeductionException( + std::string("ProductTime invariant violated [§10.5]: tri-equivalence broken: ") + + "(windowStart==windowEnd)=" + (a ? "true" : "false") + ", (statisticalWindowCount==0)=" + + (b ? "true" : "false") + ", (timeIncrementInSeconds==nullopt)=" + (c ? "true" : "false"), + Here()); + } + + /// @todo: Handle the case of zero windows with stepInSeconds == 0 (instant product) + /// but timeIncrementInSeconds present. This is technically a violation of the + /// tri-equivalence invariant (§10.5) but we may want to allow it as scientists + /// wants do avoid 2 requests for fields that have an increasing window. + + // --------------------------------------------------------- + // Construct the immutable ProductTime + // --------------------------------------------------------- + return ProductTime{labelDateTime, initialConditionsDateTime, referenceDateTime, windowStart, + windowEnd, windows, windowCount, tInc}; + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to construct ProductTime", Here())); + } + + mars2gribUnreachable(); } -} // namespace metkit::mars2grib::backend::deductions::detail +} // namespace metkit::mars2grib::backend::deductions::detail \ No newline at end of file From 70f7956b9ed4262bd5cf7fdd78ddd3f7c7d67e16 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Thu, 11 Jun 2026 23:17:49 +0000 Subject: [PATCH 33/35] mars2grib: Update the markdown specification of productTime --- .../{timeProducts.md => productTime.md} | 123 ++++++++++++++---- 1 file changed, 96 insertions(+), 27 deletions(-) rename src/metkit/mars2grib/backend/deductions/{timeProducts.md => productTime.md} (92%) diff --git a/src/metkit/mars2grib/backend/deductions/timeProducts.md b/src/metkit/mars2grib/backend/deductions/productTime.md similarity index 92% rename from src/metkit/mars2grib/backend/deductions/timeProducts.md rename to src/metkit/mars2grib/backend/deductions/productTime.md index 23486daa6..8ad718f2b 100644 --- a/src/metkit/mars2grib/backend/deductions/timeProducts.md +++ b/src/metkit/mars2grib/backend/deductions/productTime.md @@ -75,10 +75,14 @@ The `StatisticalWindow` type is defined in the shared header ```cpp #include "metkit/mars2grib/backend/tables/timeUnits.h" +namespace metkit::mars2grib::backend::deductions::detail { + struct StatisticalWindow { tables::TimeUnit unit{tables::TimeUnit::Second}; long count{0}; }; + +} // namespace metkit::mars2grib::backend::deductions::detail ``` ### 3.1 Allowed `TimeUnit` subset @@ -296,8 +300,10 @@ struct ProductTimeInput { std::array stattypeWindows{}; std::size_t stattypeWindowCount{0}; - // Source: deductions::timeIncrementInSeconds_opt(mars, par) — i.e. par["timeIncrementInSeconds"]. - // Absent for instants and for the AIFS single-window path (§9.4). + // Source: deductions::timeIncrementInSeconds_opt(mars, par) when the resolver invokes it (§7.9). + // Resolver-forced std::nullopt for instant products and for invalid + // timespan/stattype combinations that fail before the value is used. + // Also optionally absent for the AIFS single-window path (§9.4). std::optional timeIncrementInSeconds; }; ``` @@ -333,7 +339,9 @@ The factory: | `step` | no, default `0` (§7.5) | MARS | | `timespan` | no | MARS | | `stattype` | no | MARS | -| `timeIncrementInSeconds` | no | par (parameter dictionary), via `deductions::timeIncrementInSeconds_opt` | +| `class` | conditional (§7.8) | MARS | +| `stream` | conditional (§7.8) | MARS | +| `timeIncrementInSeconds` | no | par (parameter dictionary), conditionally via `deductions::timeIncrementInSeconds_opt` (§7.9) | ### 7.2 `date` / `time` @@ -455,14 +463,63 @@ sibling deduction, out of scope here). The `stattype` parser MUST be a single shared helper (now in `detail/StatType.h` as `parse_StatType_or_throw`) used by both deductions, so that the two never drift. -### 7.8 `timeIncrementInSeconds` +### 7.8 Single-loop representation policy + +When the resolver detects a single-loop statistic, it MUST read MARS `class` +and `stream` and validate whether the product must use the standard +single-loop representation or the fakeDoubleLoop representation. + +The two single-loop representations are: + +```text +standard single-loop: + timespanKind == Duration AND stattypeWindowCount == 0 + +fakeDoubleLoop single-loop: + timespanKind == None AND stattypeWindowCount == 1 +``` + +The fakeDoubleLoop representation is required exactly for the following +`(class, stream)` combinations: + +| `class` | `stream` values | +|---------|-----------------| +| `e6` | `sttd`, `stte` | +| `od` | `sfmd`, `shmd` | +| `rd` | `sfmd`, `shmd` | +| `c3` | `sfmd`, `shmd` | +| `gh` | `msmm`, `rfsd` | +| `eh` | `msmm`, `rfsd` | + +For these combinations, standard single-loop statistics are a hard error +(§10.20). For all other combinations, fakeDoubleLoop single-loop statistics are +a hard error (§10.21). If `class` or `stream` cannot be read while validating a +single-loop statistic, this is a hard error (§10.19). + +This rule applies only to single-loop statistics. It does not apply to instant +products or old-style multi-loop statistics. + +### 7.9 `timeIncrementInSeconds` Source: `deductions::timeIncrementInSeconds_opt(mars, par)`, which reads -`par["timeIncrementInSeconds"]`. Existing normalization is preserved: +`par["timeIncrementInSeconds"]` when invoked. The resolver invokes it only +for inputs that are statistical candidates: + +```text +timespanKind == Duration +OR +timespanKind == None AND stattypeWindowCount == 1 +``` + +For instant products, and for invalid `timespan` / `stattype` combinations that +fail before the value is used, the resolver forwards `std::nullopt` without +reading `par["timeIncrementInSeconds"]`. + +When the helper is invoked, existing normalization is preserved: - absent -> `std::nullopt` - present, value `0` -> `std::nullopt` (legacy normalization) -- present, value < 0 -> hard error (§10.10) +- present, value < 0 -> hard error (§10.14) - present, value > 0 -> the value The forwarded `std::optional` becomes `ProductTime::timeIncrementInSeconds` @@ -484,12 +541,14 @@ ProductTime resolve_ProductTime_or_throw( reads no keys at present. Reserved for future options. - The resolver: 1. Reads MARS keys per §7. - 2. Reads `par["timeIncrementInSeconds"]` via the existing - `timeIncrementInSeconds_opt` helper. - 3. Builds a `ProductTimeInput`. - 4. Calls `make_ProductTime_or_throw`. - 5. On success, emits exactly one `MARS2GRIB_LOG_RESOLVE` line (§12). - 6. On failure, rethrows-with-nested per §11. + 2. Validates the standard/fakeDoubleLoop representation policy for + single-loop statistics according to §7.8. + 3. Conditionally reads `par["timeIncrementInSeconds"]` via the existing + `timeIncrementInSeconds_opt` helper, according to §7.9. + 4. Builds a `ProductTimeInput`. + 5. Calls `make_ProductTime_or_throw`. + 6. On success, emits exactly one `MARS2GRIB_LOG_RESOLVE` line (§12). + 7. On failure, rethrows-with-nested per §11. `resolve_ProductTime_or_throw` is the canonical name. Any additional spelling (e.g. `resolveProductTime_or_throw`) is a deprecated alias and SHOULD NOT be @@ -512,7 +571,7 @@ the resulting `ProductTime`. | Case | `timespanKind` | `stattype` blocks | `statisticalWindows` (out→in) | `windowStart` | `timeIncrementInSeconds` | |------|----------------|-------------------|-------------------------------|---------------|--------------------------| -| §9.1 Instant | `Missing` | 0 | (empty) | `windowEnd` | `std::nullopt` (required) | +| §9.1 Instant | `Missing` | 0 | (empty) | `windowEnd` | `std::nullopt` (resolver-forced) | | §9.2 Old single-loop | `Duration`| 0 | `[timespan]` | `windowEnd - timespan` | optional or required (§9.5) | | §9.3 Old multi-loop | `Duration`| 1 or 2 | `[parse(stattype) ..., timespan]` | `windowEnd - statisticalWindows[0]` | required and > 0 | | §9.4 New fakeDoubleLoop | `None` | exactly 1 | `[parse(stattype)]` | `windowEnd - statisticalWindows[0]` | optional or required (§9.5) | @@ -528,7 +587,7 @@ applied after `windowStart` is computed. windowEnd := referenceDateTime + stepInSeconds windowStart := windowEnd statisticalWindows := (empty array, statisticalWindowCount = 0) -timeIncrementInSeconds := std::nullopt (MUST; else §10.5) +timeIncrementInSeconds := std::nullopt (resolver-forced; par value is not read) ``` Used primarily by `referenceTime` and `pointInTime`. `statistics` MUST NOT be @@ -547,6 +606,9 @@ windowStart := windowEnd - timespan (= windowEnd - timespan therefore a simple seconds subtraction; no calendar arithmetic is involved on this path. +This representation is valid only when the `(class, stream)` policy in §7.8 +does not require fakeDoubleLoop representation. + ### 9.3 Old-style multi-loop statistic ```text @@ -573,6 +635,9 @@ receives the `paramId`-derived operation as its `innerTypeOfStatisticalProcessing` argument and compares it against the parsed `stattype` block operation in this case. +This representation is valid only when the `(class, stream)` policy in §7.8 +requires fakeDoubleLoop representation. + ```text windowEnd := referenceDateTime + stepInSeconds statisticalWindows := [parse(single stattype block)] @@ -590,8 +655,10 @@ windowStart := windowEnd - statisticalWindows[0] | §9.3 Old multi-loop | MUST be present and > 0 | | §9.4 New fakeDoubleLoop | MAY be `std::nullopt` (AIFS path); if present MUST be > 0 | -Violation is a hard error (§10.5 for instants, §10.10 for non-positive, -§10.13 for missing-where-required). +Violation is a hard error (§10.5 for the constructed `ProductTime` invariant, +§10.14 for negative values when the helper is invoked, §10.13 for +missing-where-required). For §9.1, the resolver forces the constructed field to +`std::nullopt` and does not consult the par value. The free helper `numberOfTimeRanges(const detail::ProductTime& pt)` declared in `productTime.h` returns the number of statistical loops contributing to @@ -652,11 +719,14 @@ in the implementation. | 10.11 | any `StatisticalWindow` in `statisticalWindows[0..count)` has `count <= 0` | | 10.12 | (deduction-side, `resolve_TypeOfStatisticalProcessing_or_throw`, §23) in the §9.4 fakeDoubleLoop case, the parsed `stattype` block operation disagrees with the `innerTypeOfStatisticalProcessing` argument supplied by the caller | | 10.13 | `statisticalWindowCount >= 2` and `timeIncrementInSeconds == nullopt` | -| 10.14 | `timeIncrementInSeconds` raw input value < 0 | +| 10.14 | `timeIncrementInSeconds` value < 0 after resolver normalization, or raw input value < 0 when `timeIncrementInSeconds_opt` is invoked (§7.9) | | 10.15 | `statisticalWindowCount > maxStatisticalWindows` (= 3) | | 10.16 | `stattype` block parsed with unknown period or operation token (raised by the shared parser, §22; period MUST be in `{mo, da}` and operation MUST be in `{av, mn, mx, sd}`) | | 10.17 | `stattype` blocks not in outermost-to-innermost order (e.g. `da_mo`) (raised by the shared parser, §22) | | 10.18 | any `StatisticalWindow` in `statisticalWindows[0..count)` has `unit` outside the §3.1 allow-list `{Second, Day, Month}` (e.g. `Hour`, `Hours6`, `Year`, `Missing`). **Two-level enforcement**: (a) the shared parser (§22) enforces the narrow `stattype`-grammar allow-list `{Day, Month}` at parse time; (b) the factory `make_ProductTime_or_throw` enforces the extended assembled-window allow-list `{Second, Day, Month}` after window assembly (the `Second` extension covers the innermost window when it originates from `timespan` rather than `stattype`). | +| 10.19 | single-loop statistic detected, but MARS `class` or `stream` cannot be read, so the standard/fakeDoubleLoop representation policy cannot be evaluated (§7.8) | +| 10.20 | standard single-loop statistic (`timespanKind == Duration`, no `stattype`) used for a `(class, stream)` combination that requires fakeDoubleLoop representation (§7.8) | +| 10.21 | fakeDoubleLoop single-loop statistic (`timespan = none`, exactly one `stattype` block) used for a `(class, stream)` combination that does not require fakeDoubleLoop representation (§7.8) | The tri-equivalence check (10.5) subsumes several otherwise-separate checks (e.g. "instant with non-null increment", "statistical with zero-length @@ -1093,8 +1163,9 @@ amendments SHOULD update both columns. | A2 / B8 (strict calendar alignment) | §4.4, §10.9, §10.10 | | A3 (reference >= simulated) | §5.3, §10.4 | | A4 / C6 (instant tri-equivalence) | §4.1, §5.1, §9.1, §10.5 | -| A5 / B1 (timeIncrementInSeconds in PT) | §5, §6, §7.8, §9.5 | +| A5 / B1 (timeIncrementInSeconds in PT) | §5, §6, §7.9, §9.5 | | Q1 (fakeDoubleLoop paramId↔stattype, deduction-side) | §9.4, §10.12, §23 | +| Single-loop standard/fakeDoubleLoop representation policy | §7.8, §9.2, §9.4, §10.19, §10.20, §10.21 | | B2 / C9 (stattype grammar) | §7.7, §10.16, §10.17, §22 | | B3 (bare numeric units = hours) | §7.5, §7.6 | | B4 (Date(fcyear, fcmonth, 1)) | §7.4 | @@ -1193,14 +1264,14 @@ UpperCamelCase initial per §20.1 (type-primary). ```cpp #include "metkit/mars2grib/backend/tables/timeUnits.h" -namespace metkit::mars2grib::backend::deductions { +namespace metkit::mars2grib::backend::deductions::detail { struct StatisticalWindow { tables::TimeUnit unit{tables::TimeUnit::Second}; long count{0}; }; -} // namespace metkit::mars2grib::backend::deductions +} // namespace metkit::mars2grib::backend::deductions::detail ``` The type carries no methods, no `operator<<` (per §13), and no validation; @@ -1218,12 +1289,10 @@ the factory at assembly time, §10.18). ### 21.4 Rationale The type is a shared primitive consumed by both `ProductTime` and the -`stattype` parser. It cannot live inside `detail/ProductTime.h` because that -would force the parser to depend on a deduction's detail header, inverting -the dependency direction. It cannot live inside `detail/StatType.h` because -`ProductTime`'s §9.2 single-loop case constructs `StatisticalWindow` -*without* invoking the parser. A shared header is the only correct -placement. +`stattype` parser. It lives in its own detail header, rather than inside +`detail/ProductTime.h` or `detail/StatType.h`, so both detail components can +include the shared primitive without coupling either one to the other's +implementation header. --- @@ -1542,4 +1611,4 @@ mars.timespan = "none" innerTypeOfStatisticalProcessing = Maximum ``` -Result: hard error §10.12 (parsed `Average` ≠ argument `Maximum`). +Result: hard error §10.12 (parsed `Average` ≠ argument `Maximum`). \ No newline at end of file From e7891df55fc19cf6b6d74ed2954bb9a1e05bd395 Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Fri, 12 Jun 2026 00:17:52 +0000 Subject: [PATCH 34/35] mars2grib: fix rebase typo --- src/metkit/mars2grib/backend/concepts/level/levelMatcher.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index b36228456..0ac0cee2a 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -261,8 +261,8 @@ inline std::size_t matchML(const long param) { // 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), range(400000, 499999)) { + 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, range(400000, 499999))) { return static_cast(LevelType::ModelMultipleLevel); } From 36f2f3b7981f82ed5443650ab7c1b8209375f09a Mon Sep 17 00:00:00 2001 From: Mirco Valentini Date: Fri, 12 Jun 2026 01:21:57 +0000 Subject: [PATCH 35/35] mars2grib: Run clang-format --- .../concepts/composition/compositionMatcher.h | 18 +++++----- .../concepts/ensemble/ensembleMatcher.h | 7 ++-- .../backend/concepts/level/levelEnum.h | 12 +++---- .../backend/concepts/level/levelMatcher.h | 11 +++--- .../concepts/packing/packingEncoding.h | 4 +-- .../backend/deductions/detail/ProductTime.h | 34 +++++++++--------- .../backend/deductions/productTime.h | 35 +++++++++---------- .../deductions/significanceOfReferenceTime.h | 7 ++-- 8 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h index 80bcfadc7..80e84aac1 100644 --- a/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h @@ -60,7 +60,7 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t compositionMatcher(const MarsDict_t& mars, const OptDict_t& opt) { - try { + try { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; @@ -71,7 +71,8 @@ std::size_t compositionMatcher(const MarsDict_t& mars, const OptDict_t& opt) { const auto param = get_or_throw(mars, "param"); - // TODO: This is the range for CAMS, there are some unmapped parameters that may need to be supported for ERA6, etc. + // TODO: This is the range for CAMS, there are some unmapped parameters that may need to be supported for ERA6, + // etc. if (param < 400000 || param >= 500000) { return compile_time_registry_engine::MISSING; } @@ -96,9 +97,9 @@ std::size_t compositionMatcher(const MarsDict_t& mars, const OptDict_t& opt) { if (matchAny(chem, range(900, 916))) { return static_cast(CompositionType::Aerosol); } - if (matchAny(chem, 2, 3, range(5, 24), range(26, 30), range(32, 50), 52, 53, range(55, 58), range(63, 80), - 82, 83, 85, 86, range(99, 101), 107, 112, 159, 161, 169, range(173, 178), range(186, 204), 222, - range(224, 231), 233, 311, 359, 404, 917)) { + if (matchAny(chem, 2, 3, range(5, 24), range(26, 30), range(32, 50), 52, 53, range(55, 58), + range(63, 80), 82, 83, 85, 86, range(99, 101), 107, 112, 159, 161, 169, range(173, 178), + range(186, 204), 222, range(224, 231), 233, 311, 359, 404, 917)) { return static_cast(CompositionType::Chem); } } @@ -106,9 +107,9 @@ std::size_t compositionMatcher(const MarsDict_t& mars, const OptDict_t& opt) { if (matchAny(chem, range(900, 917), 924)) { return static_cast(CompositionType::Aerosol); } - if (matchAny(chem, range(2, 24), range(26, 30), range(32, 50), 52, 53, range(55, 59), range(63, 80), 82, 83, - 85, 86, range(99, 101), 107, 112, 118, 159, 161, 169, range(173, 178), range(186, 204), 222, - range(224, 230), 233, 236, 311)) { + if (matchAny(chem, range(2, 24), range(26, 30), range(32, 50), 52, 53, range(55, 59), range(63, 80), 82, + 83, 85, 86, range(99, 101), 107, 112, 118, 159, 161, 169, range(173, 178), range(186, 204), + 222, range(224, 230), 233, 236, 311)) { return static_cast(CompositionType::Chem); } } @@ -154,7 +155,6 @@ std::size_t compositionMatcher(const MarsDict_t& mars, const OptDict_t& opt) { "compositionMatcher: matching logic is not implemented for param=" + std::to_string(param) + ", chem=" + std::to_string(chem) + ", hasWavelength=" + (hasWavelength ? "true" : "false"), Here()); - } catch (...) { std::throw_with_nested( diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index 3a63a5f11..98cab54bb 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -62,12 +62,11 @@ 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. try { - using metkit::mars2grib::utils::dict_traits::has; using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; - if (has(mars, "number") && !(has(mars, "type") && - (get_or_throw(mars, "type") == "me" || - get_or_throw(mars, "type") == "eme"))) { + if (has(mars, "number") && !(has(mars, "type") && (get_or_throw(mars, "type") == "me" || + get_or_throw(mars, "type") == "eme"))) { return static_cast(EnsembleType::Individual); } else { diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h index 619d217ef..ea351bdbe 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h @@ -174,12 +174,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::FlightLevel, 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::FlightLevel, LevelType::Default>; /// /// @brief Compile-time mapping from `LevelType` to human-readable name. diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index 0ac0cee2a..1b8e0b517 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -174,7 +174,8 @@ inline std::size_t matchSFC(const long param) { } // Chemical - if (matchAny(param, range(228080, 228085), range(233032, 233035), range(235062, 235064), range(400000, 499999))) { + if (matchAny(param, range(228080, 228085), range(233032, 233035), range(235062, 235064), + range(400000, 499999))) { return static_cast(LevelType::Surface); } @@ -261,7 +262,7 @@ inline std::size_t matchML(const long param) { // 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), + 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, range(400000, 499999))) { return static_cast(LevelType::ModelMultipleLevel); } @@ -298,9 +299,9 @@ inline std::size_t matchPL(const long param, const long level) { using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, 1, 2, 10, 60, 75, 76, range(129, 135), 138, 152, range(155, 157), 203, range(246, 248), 235100, - range(235129, 235133), 235135, 235138, 235152, 235155, 235157, 235203, 235246, 260290, 263107, - range(400000, 499999))) { + if (matchAny(param, 1, 2, 10, 60, 75, 76, range(129, 135), 138, 152, range(155, 157), 203, range(246, 248), + 235100, range(235129, 235133), 235135, 235138, 235152, 235155, 235157, 235203, 235246, 260290, + 263107, range(400000, 499999))) { if (level >= 100) { return static_cast(LevelType::IsobaricInHpa); } diff --git a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h index 0a36148df..18cd8491d 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingEncoding.h @@ -167,7 +167,6 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op // Set bits per value long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); set_or_throw(out, "bitsPerValue", bitsPerValue); - } if constexpr (Variant == PackingType::Ccsds) { @@ -190,13 +189,12 @@ void PackingOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& op set_or_throw(out, "bitsPerValue", bitsPerValue); // double laplacianOperator = deductions::resolve_LaplacianOperator_or_throw(mars, par, opt); - long subSetTruncation = deductions::resolve_SubSetTruncation_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/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h index 0390247e5..79ea01adc 100644 --- a/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -47,10 +47,10 @@ // System includes #include -#include -#include #include #include +#include +#include #include #include #include @@ -150,7 +150,7 @@ struct ProductTimeInput { eckit::DateTime labelDateTime; std::optional initialConditionsDateTime; ///< from `hdate` / `htime` - std::optional referenceDateTime; ///< from `fcyear` / `fcmonth` + std::optional referenceDateTime; ///< from `fcyear` / `fcmonth` /// Offset from `referenceDateTime` to `ProductTime::windowEnd`. long stepInSeconds{0}; @@ -235,8 +235,8 @@ inline long toSeconds_or_throw(std::string_view step) { case 'd': return value * 86400L; default: - throw Mars2GribDeductionException(std::string("Unknown duration unit: '") + unit + "', expected={h,m,s,d}", - Here()); + throw Mars2GribDeductionException( + std::string("Unknown duration unit: '") + unit + "', expected={h,m,s,d}", Here()); } } catch (...) { @@ -269,7 +269,8 @@ inline eckit::Date convert_YYYYMMDD2Date_or_throw(long YYYYMMDD) { return eckit::Date(YYYY, MM, DD); } catch (const eckit::Exception& e) { - throw Mars2GribDeductionException("Invalid date value '" + std::to_string(YYYYMMDD) + "': " + e.what(), Here()); + throw Mars2GribDeductionException("Invalid date value '" + std::to_string(YYYYMMDD) + "': " + e.what(), + Here()); } } catch (...) { @@ -302,7 +303,8 @@ inline eckit::Time convert_hhmmss2Time_or_throw(long hhmmss) { return eckit::Time(hh, mm, ss); } catch (const eckit::Exception& e) { - throw Mars2GribDeductionException("Invalid time value '" + std::to_string(hhmmss) + "': " + e.what(), Here()); + throw Mars2GribDeductionException("Invalid time value '" + std::to_string(hhmmss) + "': " + e.what(), + Here()); } } catch (...) { @@ -555,7 +557,8 @@ inline bool isOnFirstOfMonthMidnight(const eckit::DateTime& dt) { } catch (...) { - std::throw_with_nested(Mars2GribGenericException("Failed to test DateTime first-of-month midnight alignment", Here())); + std::throw_with_nested( + Mars2GribGenericException("Failed to test DateTime first-of-month midnight alignment", Here())); } mars2gribUnreachable(); @@ -708,8 +711,7 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { // §7.3: hdate / htime defaulting (the resolver has already enforced // §10.2; here we just apply the fall-through to labelDateTime). - const eckit::DateTime initialConditionsDateTime = - input.initialConditionsDateTime.value_or(labelDateTime); + const eckit::DateTime initialConditionsDateTime = input.initialConditionsDateTime.value_or(labelDateTime); // §7.4: fcyear / fcmonth defaulting. const eckit::DateTime referenceDateTime = input.referenceDateTime.value_or(initialConditionsDateTime); @@ -813,7 +815,6 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { } - if (!isAllowedWindowUnit(w.unit)) { throw Mars2GribDeductionException("ProductTime invariant violated [§10.18(b)]: statisticalWindows[" + std::to_string(i) + "] uses disallowed TimeUnit '" + fmt(w.unit) + @@ -854,8 +855,8 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { // §5.2: windowStart <= windowEnd (defensive) // --------------------------------------------------------- if (windowStart > windowEnd) { - throw Mars2GribDeductionException("ProductTime invariant violated [§5.2]: windowStart ('" + fmt(windowStart) + - "') > windowEnd ('" + fmt(windowEnd) + "')", + throw Mars2GribDeductionException("ProductTime invariant violated [§5.2]: windowStart ('" + + fmt(windowStart) + "') > windowEnd ('" + fmt(windowEnd) + "')", Here()); } @@ -883,7 +884,7 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { const bool a = (windowStart == windowEnd); const bool b = (windowCount == 0); const bool c = !tInc.has_value(); - if (!((a == b) && (b == c) )) { + if (!((a == b) && (b == c))) { throw Mars2GribDeductionException( std::string("ProductTime invariant violated [§10.5]: tri-equivalence broken: ") + "(windowStart==windowEnd)=" + (a ? "true" : "false") + ", (statisticalWindowCount==0)=" + @@ -899,8 +900,9 @@ inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { // --------------------------------------------------------- // Construct the immutable ProductTime // --------------------------------------------------------- - return ProductTime{labelDateTime, initialConditionsDateTime, referenceDateTime, windowStart, - windowEnd, windows, windowCount, tInc}; + return ProductTime{ + labelDateTime, initialConditionsDateTime, referenceDateTime, windowStart, windowEnd, windows, windowCount, + tInc}; } catch (...) { diff --git a/src/metkit/mars2grib/backend/deductions/productTime.h b/src/metkit/mars2grib/backend/deductions/productTime.h index 4a9f2011f..b4d7c7b55 100644 --- a/src/metkit/mars2grib/backend/deductions/productTime.h +++ b/src/metkit/mars2grib/backend/deductions/productTime.h @@ -149,10 +149,10 @@ inline bool requiresFakeDoubleLoopRepresentation(const std::string& klass, const template detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::dict_to_json; using metkit::mars2grib::utils::dict_traits::get_opt; using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; - using metkit::mars2grib::utils::dict_traits::dict_to_json; using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; // Suppress "unused parameter" warning while preserving the documented @@ -175,8 +175,8 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P if (hasDate && hasTime) { const long marsDate = get_or_throw(mars, "date"); const long marsTime = get_or_throw(mars, "time"); - labelDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsDate), - detail::convert_hhmmss2Time_or_throw(marsTime)); + labelDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsDate), + detail::convert_hhmmss2Time_or_throw(marsTime)); } else if (!hasDate && !hasTime && hasFcYear && hasFcMonth) { // R2 default: labelDateTime := DateTime(fcyear, fcmonth, 1, 00:00:00). @@ -218,8 +218,8 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsHdate), eckit::Time(0, 0, 0)); } else if (hasHdate && hasHtime) { - const long marsHdate = get_or_throw(mars, "hdate"); - const long marsHtime = get_or_throw(mars, "htime"); + const long marsHdate = get_or_throw(mars, "hdate"); + const long marsHtime = get_or_throw(mars, "htime"); initialConditionsDateTime = eckit::DateTime(detail::convert_YYYYMMDD2Date_or_throw(marsHdate), detail::convert_hhmmss2Time_or_throw(marsHtime)); } @@ -332,7 +332,6 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P stattypeWindows[i] = blocks[i].timeWindow; } stattypeWindowCount = blocks.size(); - } // ========================================================= @@ -381,9 +380,9 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P // §7.9: timeIncrementInSeconds (par) // ========================================================= std::optional tInc = std::nullopt; - - if ( (timespanKind == detail::TimespanKind::None && stattypeWindowCount ==1) || - timespanKind == detail::TimespanKind::Duration ) { + + if ((timespanKind == detail::TimespanKind::None && stattypeWindowCount == 1) || + timespanKind == detail::TimespanKind::Duration) { tInc = timeIncrementInSeconds_opt(mars, par); } @@ -392,15 +391,15 @@ detail::ProductTime resolve_ProductTime_or_throw(const MarsDict_t& mars, const P // ========================================================= detail::ProductTimeInput input; - input.labelDateTime = labelDateTime; - input.initialConditionsDateTime = initialConditionsDateTime; - input.referenceDateTime = referenceDateTime; - input.stepInSeconds = stepInSeconds; - input.timespanKind = timespanKind; - input.timespan = timespan; - input.stattypeWindows = stattypeWindows; - input.stattypeWindowCount = stattypeWindowCount; - input.timeIncrementInSeconds = tInc; + input.labelDateTime = labelDateTime; + input.initialConditionsDateTime = initialConditionsDateTime; + input.referenceDateTime = referenceDateTime; + input.stepInSeconds = stepInSeconds; + input.timespanKind = timespanKind; + input.timespan = timespan; + input.stattypeWindows = stattypeWindows; + input.stattypeWindowCount = stattypeWindowCount; + input.timeIncrementInSeconds = tInc; detail::ProductTime pt = detail::make_ProductTime_or_throw(input); diff --git a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h index 67ecd5481..bea442b57 100644 --- a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h +++ b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h @@ -118,10 +118,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", - "gbf", "gwt", "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", "gbf", "gwt", "est"}}; constexpr std::array startOfDataAssimilationTypes = {{"4i", "4v", "me", "eme"}};