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/AllConcepts.h b/src/metkit/mars2grib/backend/concepts/AllConcepts.h index f4b46c1ea..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 @@ -93,15 +97,18 @@ #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" #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" +#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" @@ -170,8 +177,9 @@ using TypeList = metkit::mars2grib::backend::compile_time_registry_engine::TypeL /// using AllConcepts = TypeList; + GeneratingProcessConcept, LevelConcept, LongrangeConcept, IterationConcept, MarsConcept, NilConcept, + OriginConcept, PackingConcept, ParamConcept, PointInTimeConcept, ReferenceTimeConcept, + RepresentationConcept, SatelliteConcept, 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/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 ce8140cce..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 @@ -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, 37L, 38L, 39L}); // Deductions long offsetToEndOf4DvarWindowVal = deductions::resolve_offsetToEndOf4DvarWindow_or_throw(mars, par, opt); 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/analysis/analysisMatcher.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h index fdfaeb756..02f6ad327 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisMatcher.h @@ -1,24 +1,74 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/brightness-temperature/brightnessTemperatureConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h new file mode 100644 index 000000000..aa557f9d9 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureConceptDescriptor.h @@ -0,0 +1,161 @@ +/* + * (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..4492f05b1 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureEncoding.h @@ -0,0 +1,172 @@ +/* + * (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..3928a63ba --- /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..66f030716 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/brightness-temperature/brightnessTemperatureMatcher.h @@ -0,0 +1,114 @@ +/* + * (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 +#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/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/composition/compositionMatcher.h b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h index 0e06491f9..80e84aac1 100644 --- a/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/composition/compositionMatcher.h @@ -1,7 +1,33 @@ +/* + * (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 #include +#include // Utils #include "metkit/mars2grib/backend/concepts/composition/compositionEnum.h" @@ -12,100 +38,128 @@ 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) { - 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); - } - } - else if (matchAny(param, 458000, 459000, 460000, 461000, 462000, 472000)) { - if (matchAny(chem, 922)) { - 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, 401000)) { - if (matchAny(chem, range(900, 916))) { - return static_cast(CompositionType::Aerosol); + + 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); + } } - 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); + else if (matchAny(param, 458000, 459000, 460000, 461000, 462000, 472000)) { + if (matchAny(chem, 922)) { + return static_cast(CompositionType::AerosolOptical); + } } } - else if (matchAny(param, 402000)) { - if (matchAny(chem, range(900, 917), 924)) { - return static_cast(CompositionType::Aerosol); + 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); + } } - 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, 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); + } } - } - 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/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/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/data-type/dataTypeMatcher.h b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h index 14f3fdee3..ffa8c10a6 100644 --- a/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/data-type/dataTypeMatcher.h @@ -1,17 +1,67 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/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 305975850..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 @@ -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,19 @@ 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 == SecProductDefinitionSection && Stage == StagePreset) { + // 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); + // 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); + // 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..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 /// @@ -83,21 +83,6 @@ 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, Default }; @@ -114,11 +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; /// @@ -148,21 +129,6 @@ 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::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 815ed738e..0201d5e4c 100644 --- a/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/derived/derivedMatcher.h @@ -1,32 +1,88 @@ +/* + * (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 #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_ { +/// +/// @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) { - using metkit::mars2grib::utils::dict_traits::get_or_throw; - - const auto& type = get_or_throw(mars, "type"); - if (type == "em" || // Ensemble mean - type == "es" || // Ensemble standard deviation - 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; + + 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())); + } } } // namespace metkit::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/destine/destineMatcher.h b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h index a394e091d..d4e1b0302 100644 --- a/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/destine/destineMatcher.h @@ -1,7 +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 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 #include +#include #include // Project includes @@ -12,32 +37,59 @@ 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) { - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; - using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; - - 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); + 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); + } + 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/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/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index c82e2b604..98cab54bb 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -1,24 +1,82 @@ +/* + * (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 #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_ { +/// +/// @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) { - using metkit::mars2grib::utils::dict_traits::has; - if (has(mars, "number")) { - return static_cast(EnsembleType::Individual); - } + // Skip model-error products: in that case "number" identifies the + // model-error realization, not an ensemble member. + try { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; - return compile_time_registry_engine::MISSING; + 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( + utils::exceptions::Mars2GribMatcherException("Unable to match `ensemble` concept", Here())); + } } } // namespace metkit::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/generating-process/generatingProcessMatcher.h b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h index 2bb32c7fb..af7f71d4e 100644 --- a/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/generating-process/generatingProcessMatcher.h @@ -1,17 +1,67 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/iteration/iterationConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h new file mode 100644 index 000000000..1e3488b4a --- /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 iterationConceptDescriptor.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..b5c03fe56 --- /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 iterationEncoding.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..70771badc --- /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 +/// `iterationEncoding.h` 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..c496a9afd --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h @@ -0,0 +1,54 @@ +/* + * (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 +#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/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 b06b44278..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 @@ -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..ea351bdbe 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 /// @@ -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..1b8e0b517 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -7,10 +7,37 @@ * 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 #include +#include +#include // Utils #include "metkit/mars2grib/backend/concepts/level/levelEnum.h" @@ -23,352 +50,629 @@ 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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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, 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); + } - // 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); + } - // 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); + } - 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( + param, "sfc", "Unable to match `level` concept for levtype \"sfc\"", Here())); + } } +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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( + param, "hl", "Unable to match `level` concept for levtype \"hl\"", Here())); + } } +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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); + } - 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); - } - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype ML", Here()); + // 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()); + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "ml", "Unable to match `level` concept for levtype \"ml\"", Here())); + } } +/// +/// @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) { - 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, - 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); + try { + 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, 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); + } } - } - 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())); + } } + +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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); + } - 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()); + 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())); + } } +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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( + param, "pv", "Unable to match `level` concept for levtype \"pv\"", Here())); + } } +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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, 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()); } - if (matchAny(param, 183, 235077, 260199, 260360)) { - return static_cast(LevelType::SoilLayer); + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "sol", "Unable to match `level` concept for levtype \"sol\"", Here())); } - - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SOL", Here()); } +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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( + param, "al", "Unable to match `level` concept for levtype \"al\"", Here())); + } } +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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)) { - return static_cast(LevelType::DepthBelowSeaLayer); - } - if (matchAny(param, 262120, 262123)) { - return static_cast(LevelType::OceanSurfaceToBottom); + 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()); } - if (matchAny(param, 262141)) { - return static_cast(LevelType::WaterSurfaceToIsothermalOceanLayer); + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "o2d", "Unable to match `level` concept for levtype \"o2d\"", Here())); } - - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O2D", Here()); } +/// +/// @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) { - using metkit::mars2grib::util::param_matcher::matchAny; - using metkit::mars2grib::util::param_matcher::range; + try { + 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, 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()); } - if (matchAny(param, 262507)) { - return static_cast(LevelType::OceanModel); + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException( + param, "o3d", "Unable to match `level` concept for levtype \"o3d\"", Here())); } - - throw utils::exceptions::Mars2GribMatcherException( - "No mapping exists for param \"" + std::to_string(param) + "\" on levtype O3D", Here()); } } // 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) { - 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; - } + 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; + } - 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 == "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()); } - if (levtype == "o3d") { - return impl::matchO3D(param); + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `level` concept", Here())); } - - throw utils::exceptions::Mars2GribMatcherException("Unknown levtype \"" + levtype + "\"", Here()); }; } // namespace metkit::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/longrange/longrangeMatcher.h b/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h index 056938d58..e42d39422 100644 --- a/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/longrange/longrangeMatcher.h @@ -1,25 +1,76 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/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/mars/marsMatcher.h b/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h index 9dcbbf28e..44f68a45b 100644 --- a/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/mars/marsMatcher.h @@ -1,24 +1,74 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/model-error/modelErrorConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h new file mode 100644 index 000000000..a68bf60df --- /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 modelErrorConceptDescriptor.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..57940521e --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h @@ -0,0 +1,187 @@ +/* + * (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 modelErrorEncoding.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 ((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); + + 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); + + // 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..."); + } + } + 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..1b4b50f31 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h @@ -0,0 +1,139 @@ +/* + * (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 +/// `modelErrorEncoding.h` 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 { + ComponentIndex = 0, + FourierCoefficients +}; + + +/// +/// @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::ComponentIndex, "componentIndex"); +DEF(ModelErrorType::FourierCoefficients, "fourierCoefficients"); + + +#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..abd5e28f3 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h @@ -0,0 +1,84 @@ +/* + * (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 +#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) { + + + 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; + } + 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())); + } +} + +} // namespace metkit::mars2grib::backend::concepts_ 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/nil/nilMatcher.h b/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h index bfd12acf9..35a35b8f0 100644 --- a/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/nil/nilMatcher.h @@ -1,17 +1,65 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/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/origin/originMatcher.h b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h index 7a200702a..6ccd604c5 100644 --- a/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/origin/originMatcher.h @@ -1,17 +1,66 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/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 dc0466d6f..18cd8491d 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 @@ -81,7 +81,12 @@ namespace metkit::mars2grib::backend::concepts_ { /// template constexpr bool packingApplicable() { - return (Stage == StagePreset && Section == SecDataRepresentationSection); + + if constexpr (Stage == StagePreset && Section == SecDataRepresentationSection) { + return true; + } + + return false; } @@ -159,10 +164,8 @@ 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 + long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); set_or_throw(out, "bitsPerValue", bitsPerValue); } @@ -171,10 +174,8 @@ 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 + long bitsPerValue = deductions::resolve_BitsPerValueGridded_or_throw(mars, par, opt); set_or_throw(out, "bitsPerValue", bitsPerValue); } @@ -183,14 +184,13 @@ 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 + long bitsPerValue = deductions::resolve_BitsPerValueSpectral_or_throw(mars, par, opt); set_or_throw(out, "bitsPerValue", bitsPerValue); - set_or_throw(out, "laplacianOperator", laplacianOperator); + + // 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); 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/packing/packingMatcher.h b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h index b2b91f647..14aa0c23d 100644 --- a/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/packing/packingMatcher.h @@ -1,7 +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 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 #include +#include #include // Project includes @@ -12,23 +37,48 @@ 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) { - 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/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/param/paramMatcher.h b/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h index 064729919..0e78d8209 100644 --- a/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/param/paramMatcher.h @@ -1,17 +1,65 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/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 41911e1a8..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 @@ -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/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/point-in-time/pointInTimeMatcher.h b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h index 17c546b87..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,59 +1,122 @@ +/* + * (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 #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_ { +/// +/// @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 { - 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, - 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(262500, 262502), range(262505, 262507), 262900, 262906, 262907)) { - return static_cast(PointInTimeType::Default); - } + using metkit::mars2grib::util::param_matcher::matchAny; + using metkit::mars2grib::util::param_matcher::range; + using metkit::mars2grib::utils::dict_traits::get_or_throw; - // Wave products - if (matchAny(param, range(140114, 140120), 140251)) { - return static_cast(PointInTimeType::Default); - } + 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)) { + return static_cast(PointInTimeType::Default); + } - // Satellite products - if (matchAny(param, 194, range(260510, 260512))) { - return static_cast(PointInTimeType::Default); - } + // Wave products + if (matchAny(param, range(140114, 140120), 140251)) { + return static_cast(PointInTimeType::Default); + } - // Chemical products - if (matchAny(param, range(228083, 228085), range(400000, 499999))) { - return static_cast(PointInTimeType::Default); - } + // Satellite products + if (matchAny(param, 194, range(260510, 260512))) { + return static_cast(PointInTimeType::Default); + } - return compile_time_registry_engine::MISSING; + // 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); + } + + 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/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 189bdc9c5..aafb0f65d 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 @@ -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::labelDateTime (from date / + // time). The ProductDefinitionSection branch below writes + // the model-version date from referenceDateTime instead. + const eckit::DateTime& referenceDateTime = pt.labelDateTime; // 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 initialConditionsDateTime. + const eckit::DateTime& dateTime = pt.referenceDateTime; // Encoding set_or_throw(out, "YearOfModelVersion", dateTime.date().year()); 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/reference-time/referenceTimeMatcher.h b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h index 65da076b9..e0e94eb46 100644 --- a/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/reference-time/referenceTimeMatcher.h @@ -1,23 +1,73 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/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 f9747d478..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 @@ -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,37 @@ 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 +393,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 +426,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 +459,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 +482,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,10 +503,32 @@ 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/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/representation/representationMatcher.h b/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h index aa8a4d534..1fd6420fa 100644 --- a/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/representation/representationMatcher.h @@ -1,7 +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 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 #include +#include // Utils #include "eckit/geo/Grid.h" @@ -13,35 +38,61 @@ 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) { - using metkit::mars2grib::utils::dict_traits::get_or_throw; - using metkit::mars2grib::utils::dict_traits::has; + 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); - } + 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); + 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 == "healpix") { - return static_cast(RepresentationType::Healpix); + catch (...) { + std::throw_with_nested( + utils::exceptions::Mars2GribMatcherException("Unable to match `representation` concept", Here())); } - - 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/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 2ac3f05c5..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 @@ -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" @@ -215,13 +216,15 @@ 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}); + validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); // Deductions - long channel = deductions::resolve_Channel_or_throw(mars, par, opt); + 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, "channel", channel); + 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 2f52d1f0a..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 /// @@ -132,7 +132,6 @@ constexpr std::string_view satelliteTypeName(); } DEF(SatelliteType::Default, "default"); - #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 07dfba360..cd6a4eaf6 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -1,24 +1,79 @@ +/* + * (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 #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_ { +/// +/// @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) { - using metkit::mars2grib::utils::dict_traits::has; - 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; - return compile_time_registry_engine::MISSING; + // 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; + } + 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/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/shape-of-the-earth/shapeOfTheEarthMatcher.h b/src/metkit/mars2grib/backend/concepts/shape-of-the-earth/shapeOfTheEarthMatcher.h index 0134b485d..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,27 +1,78 @@ +/* + * (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 #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_ { +/// +/// @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 { - 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/impl/statisticsDescriptor.h b/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h new file mode 100644 index 000000000..bdb58f94a --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/statistics/impl/statisticsDescriptor.h @@ -0,0 +1,271 @@ +/* + * (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/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/statisticsEncoding.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h index 69ba80c44..a7b649c0b 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEncoding.h @@ -9,39 +9,38 @@ */ /// -/// @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 +/// - Encodes the time-dependent keys from the resolved `ProductTime`: +/// - `forecastTime` (hours between `pt.referenceDateTime` and `pt.windowStart`) +/// - `OfEndOfOverallTimeInterval` (from `pt.windowEnd`) /// -/// @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 +48,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 +77,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 +89,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 +106,81 @@ 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()); - + auto pt = deductions::resolve_ProductTime_or_throw(mars, par, opt); + auto inner = typeOfStatisticalProcessingEnum(); + auto types = deductions::resolve_TypeOfStatisticalProcessing_or_throw(inner, mars, par, opt); - // Test WIP - deductions::StatisticalProcessing statsDesc = deductions::getTimeDescriptorFromMars_orThrow( - mars, par, opt, typeOfStatisticalProcessing()); + auto desc = impl::compute_StatisticalProcessing(pt, types); - if (numberOfTimeRangesVal > 1) { - MARS2GRIB_CONCEPT_THROW( - statistics, - "`statistics` concept with multiple time ranges not yet supported at preset stage..."); - } - } + 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, "timeIncrement", desc.lengthOfTimeIncrement); } + // ============================================================= + // StageRuntime + // + // Time-dependent keys: forecastTime and end-of-interval date/time. + // ============================================================= if constexpr (Stage == StageRuntime) { + 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()); + } - // 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 (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); - if (numberOfTimeRangesVal > 1) { - MARS2GRIB_CONCEPT_THROW( - statistics, - "`statistics` concept with multiple time ranges not yet supported at runtime stage..."); - } + 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/concepts/statistics/statisticsEnum.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsEnum.h index c5cbcb2a3..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 /// @@ -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/concepts/statistics/statisticsMatcher.h b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h index dee0a18d5..d376be73b 100644 --- a/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/statistics/statisticsMatcher.h @@ -1,7 +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 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 #include +#include #include // Utils @@ -13,68 +38,98 @@ 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) { - 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/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/tables/tablesMatcher.h b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h index db902bd67..e8de6f57f 100644 --- a/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/tables/tablesMatcher.h @@ -1,24 +1,73 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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/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 /// diff --git a/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h b/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h index 9150d20dc..4fbebe3cf 100644 --- a/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/wave/waveMatcher.h @@ -1,37 +1,93 @@ +/* + * (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 #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_ { +/// +/// @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) { - 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; + using metkit::mars2grib::utils::exceptions::Mars2GribMatcherException; - const auto param = get_or_throw(mars, "param"); + const auto param = get_or_throw(mars, "param"); - if (matchAny(param, range(140114, 140120))) { - return static_cast(WaveType::Period); - } + if (matchAny(param, range(140114, 140120))) { + return static_cast(WaveType::Period); + } - if (matchAny(param, 140251)) { - ASSERT(has(mars, "frequency")); - ASSERT(has(mars, "direction")); - return static_cast(WaveType::Spectra); - } + 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; + return compile_time_registry_engine::MISSING; + } + catch (...) { + std::throw_with_nested(utils::exceptions::Mars2GribMatcherException("Unable to match `wave` concept", Here())); + } } } // namespace metkit::mars2grib::backend::concepts_ 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 diff --git a/src/metkit/mars2grib/backend/deductions/componentIndex.h b/src/metkit/mars2grib/backend/deductions/componentIndex.h new file mode 100644 index 000000000..a501f7ff0 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/componentIndex.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 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" && 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()); + } + + // 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"); + + // 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/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/deductions/detail/ProductTime.h b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h new file mode 100644 index 000000000..79ea01adc --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/detail/ProductTime.h @@ -0,0 +1,915 @@ +/* + * (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) +/// - `make_ProductTime_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`. +/// +/// 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 +/// + +#pragma once + +// System includes +#include +#include +#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-initial-conditions invariant (§5.3) +/// - per-consumer field-access table (§15) +/// +struct ProductTime { + + const eckit::DateTime labelDateTime; + const eckit::DateTime initialConditionsDateTime; + 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 `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. +/// +struct ProductTimeInput { + + eckit::DateTime labelDateTime; + + std::optional initialConditionsDateTime; ///< 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 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::Mars2GribGenericException; + + try { + 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()); + } + } + catch (...) { + + 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::Mars2GribGenericException; + + try { + 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 (...) { + + 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::Mars2GribGenericException; + + try { + 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 (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to convert HHMMSS value to eckit::Time", Here())); + } + + mars2gribUnreachable(); +} + +// ============================================================= +// 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). +/// +/// @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) { + 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) { + 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(); +} + +// ============================================================= +// 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). +/// +/// @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) { + 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 (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to subtract calendar months from DateTime", Here())); + } + + mars2gribUnreachable(); +} + +/// +/// @brief Subtract `count` calendar days from a midnight `DateTime`. +/// +/// 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) { + 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) { + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + // 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)); + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to subtract seconds from DateTime", Here())); + } + + mars2gribUnreachable(); +} + +/// +/// @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. +/// +/// @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::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(); +} + +// ============================================================= +// 8. Alignment checks (§4.2, §4.3, §10.9, §10.10) +// ============================================================= + +/// +/// @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) { + 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) { + 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(); +} + +// ============================================================= +// 9. Inline string formatting (§12, §13) +// ============================================================= + +/// +/// @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) { + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + return dt.iso(true); + } + catch (...) { + + std::throw_with_nested(Mars2GribGenericException("Failed to format DateTime", Here())); + } + + mars2gribUnreachable(); +} + +/// +/// @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. +/// +/// +/// @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) { + 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) { + 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) { + 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(); +} + +// ============================================================= +// 10. Factory: make_ProductTime_or_throw (§6, §9, §10) +// ============================================================= + +/// +/// @brief Validate input invariants and construct an immutable `ProductTime`. +/// +/// Performs (in order): +/// 1. Default resolution for `initialConditionsDateTime` 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 metkit::mars2grib::utils::exceptions::Mars2GribGenericException +/// If any ProductTime rule or invariant is violated, or if any unexpected +/// error occurs. The original exception is preserved as a nested exception; +/// rule-violation check sites remain tagged with the corresponding §10 entry +/// number in the nested cause. +/// +inline ProductTime make_ProductTime_or_throw(const ProductTimeInput& input) { + using metkit::mars2grib::utils::exceptions::Mars2GribGenericException; + + try { + + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + // --------------------------------------------------------- + // Default resolution (§7.3, §7.4) + // --------------------------------------------------------- + + const eckit::DateTime labelDateTime = input.labelDateTime; + + // §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()); + } + + // --------------------------------------------------------- + // 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.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}", + 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()); + } + + /// @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 \ No newline at end of file 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..de5d8b017 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/detail/StatType.h @@ -0,0 +1,254 @@ +/* + * (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 +/// `make_ProductTime_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..fe6c7b29c --- /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 `make_ProductTime_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 `make_ProductTime_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/pv_137_be.h b/src/metkit/mars2grib/backend/deductions/detail/pv_137_be.h index fd7e15e8d..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,17 +1,23 @@ -#pragma once -#include "metkit/mars2grib/utils/generalUtils.h" - -// namespace metkit::mars2grib::backend::deductions::pv_detail::data { +/* + * (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, @@ -53,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}, @@ -556,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/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/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/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/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/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/backend/deductions/numberOfFrequencies.h b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h new file mode 100644 index 000000000..64a9ca427 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h @@ -0,0 +1,96 @@ +/* + * (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 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 + +#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 `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) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + if (has(par, "numberOfFrequencies")) { + long numberOfFrequencies = get_or_throw(par, "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( + Mars2GribDeductionException("Failed to resolve `numberOfFrequencies` from input dictionaries", Here())); + }; + + 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/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) { diff --git a/src/metkit/mars2grib/backend/deductions/productTime.h b/src/metkit/mars2grib/backend/deductions/productTime.h new file mode 100644 index 000000000..b4d7c7b55 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/productTime.h @@ -0,0 +1,454 @@ +/* + * (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 +#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 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`; additionally `class` and +/// `stream` when validating single-loop statistics +/// - 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::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). +/// +/// @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::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::exceptions::Mars2GribDeductionException; + + // Suppress "unused parameter" warning while preserving the documented + // signature (§8: opt is reserved). + (void)opt; + + try { + + // ========================================================= + // §7.1 / §7.2: labelDateTime 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 labelDateTime; + 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)); + } + else if (!hasDate && !hasTime && hasFcYear && hasFcMonth) { + // 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 { + 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 labelDateTime: " + 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: initialConditionsDateTime from (hdate, htime) + // ========================================================= + + const bool hasHdate = has(mars, "hdate"); + const bool hasHtime = has(mars, "htime"); + + std::optional initialConditionsDateTime; + if (!hasHdate && !hasHtime) { + // initialConditionsDateTime defaults to labelDateTime in the factory. + } + else if (hasHdate && !hasHtime) { + const long marsHdate = get_or_throw(mars, "hdate"); + 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"); + initialConditionsDateTime = 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 initialConditionsDateTime 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: 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; + + if ((timespanKind == detail::TimespanKind::None && stattypeWindowCount == 1) || + timespanKind == detail::TimespanKind::Duration) { + tInc = timeIncrementInSeconds_opt(mars, par); + } + + // ========================================================= + // Assemble the input bundle and call the factory. + // ========================================================= + + 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; + + detail::ProductTime pt = detail::make_ProductTime_or_throw(input); + + // ========================================================= + // §12: composite RESOLVE log line (success path only) + // ========================================================= + + MARS2GRIB_LOG_RESOLVE([&]() { + std::string msg = "`ProductTime` resolved from input dictionaries: "; + 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) + "'"; + 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 \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/deductions/productTime.md b/src/metkit/mars2grib/backend/deductions/productTime.md new file mode 100644 index 000000000..8ad718f2b --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/productTime.md @@ -0,0 +1,1614 @@ +# 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 (see §18.4). + +--- + +## 2. Architecture + +``` +MARS par opt + \ | / + \ | / + v v v + resolve_ProductTime_or_throw (deductions/productTime.h) + | + | builds ProductTimeInput, calls factory + v + make_ProductTime_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) + (concepts/statistics/impl/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" + +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 + +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 `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). + +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 labelDateTime; + const eckit::DateTime initialConditionsDateTime; + 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 >= initialConditionsDateTime (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 labelDateTime; + + std::optional initialConditionsDateTime; // 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) 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; +}; +``` + +Factory: + +```cpp +ProductTime make_ProductTime_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 | +| `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` + +Resolution rule for `labelDateTime`: + +```text +date present and time present: + labelDateTime := DateTime(date, time) + +date missing and time missing and fcyear present and fcmonth present: + labelDateTime := DateTime(Date(fcyear, fcmonth, 1), Time(00:00:00)) + (this is the same value §7.4 produces for referenceDateTime; + consequently labelDateTime == referenceDateTime in this case) + +any other combination -> hard error (§10.1) +``` + +`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 `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 -> 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) +``` + +### 7.4 `fcyear` / `fcmonth` + +Both must be present together or both absent. + +```text +both missing -> referenceDateTime := initialConditionsDateTime +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 the legacy `detail/timeUtils.h::parseStatType_or_throw`, +now `detail/StatType.h::parse_StatType_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 (now in `detail/StatType.h` as `parse_StatType_or_throw`) used +by both deductions, so that the two never drift. + +### 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"]` 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.14) +- 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. 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 +used in new code. + +Header location: `deductions/productTime.h` (public). The factory +`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). + +--- + +## 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` (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) | +| any other combination | — | — | — | — | hard error (§10.6, §10.7, §10.8) | + +In all rows, `labelDateTime`, `initialConditionsDateTime`, `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 (resolver-forced; par value is not read) +``` + +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. + +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 +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. + +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)] +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 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 +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`: + +```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 `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. + +| # | 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 < 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 | +| 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` 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 +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. `typeOfStatisticalProcessing.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: labelDateTime='...' \ +initialConditionsDateTime='...' 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 +`typeOfStatisticalProcessing.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. +- `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 + 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` | +|-----------------------------|:---------------:|:-------------:|:------------:| +| `labelDateTime` | R | — | R | +| `initialConditionsDateTime` | R | — | R | +| `referenceDateTime` | R | R | R | +| `windowStart` | — | — | R | +| `windowEnd` | — | R | R | +| `statisticalWindows` | — | — | R | +| `statisticalWindowCount` | — | — | R | +| `timeIncrementInSeconds` | — | — | R | + +--- + +## 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 +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 +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 +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 +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 +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 == initialConditionsDateTime`. + +### 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 +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 +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 +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 +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 + (`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 + `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/concepts/statistics/impl/statisticsDescriptor.h` + — pure function `computeStatisticDescription(productTime, + typeOfStatisticalProcessing)`. The existing `StatisticalProcessing` struct + 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 + +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/numberOfTimeRanges.h` +- `src/metkit/mars2grib/backend/deductions/referenceDateTime.h` +- `src/metkit/mars2grib/backend/deductions/hindcastDateTime.h` + +`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 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` + +### 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 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) | +|----------|---------------------------| +| 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.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 | +| 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) +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::detail { + +struct StatisticalWindow { + tables::TimeUnit unit{tables::TimeUnit::Second}; + long count{0}; +}; + +} // namespace metkit::mars2grib::backend::deductions::detail +``` + +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 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. + +--- + +## 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`). \ No newline at end of file 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/significanceOfReferenceTime.h b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h index 8ef181e7c..bea442b57 100644 --- a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h +++ b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h @@ -41,8 +41,7 @@ /// - @ref referenceTimeEncoding.h /// /// Related deductions: -/// - @ref standardReferenceDateTime.h -/// - @ref hindcastReferenceDateTime.h +/// - @ref productTime.h /// /// @ingroup mars2grib_backend_deductions /// @@ -118,10 +117,10 @@ tables::SignificanceOfReferenceTime resolve_SignificanceOfReferenceTime_or_throw constexpr std::array analysisTypes = { {"an", "ia", "oi", "3v", "3g", "4g", "ea", "pa", "tpa", "ga", "gai", "ai", "af", "ab", "oai", "ga", "gai"}}; - constexpr std::array forecastTypes = { + constexpr std::array forecastTypes = { {"fc", "cf", "pf", "cm", "fp", "em", "ep", "es", "fa", "efi", "efic", "bf", "cd", "wem", "wes", "cr", "ses", "taem", "taes", "sg", "sf", "if", "fcmean", "fcmax", - "fcmin", "fcstdev", "ssd", "tf", "bf", "cd", "hcmean", "s3", "si", "gbf", "gwt"}}; + "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/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/timeIncrementInSeconds.h b/src/metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h index bb0ebae80..1a3726e2e 100644 --- a/src/metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h +++ b/src/metkit/mars2grib/backend/deductions/timeIncrementInSeconds.h @@ -1,21 +1,88 @@ +/* + * (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 timeIncrementInSeconds.h +/// @brief Deduction of the optional `timeIncrementInSeconds` parameter. +/// +/// Resolves the `timeIncrementInSeconds` key from the parameter dictionary. +/// The value, when present and strictly positive, denotes the inner time +/// increment (in seconds) of a statistical product (the spacing between +/// the samples consumed by the innermost statistical operation, e.g. the +/// 1-hour spacing of an hourly accumulation). +/// +/// Normalization rules: +/// - absent key -> `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/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/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/backend/deductions/typeOfGeneratingProcess.h b/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h index d18eca2f7..1f78c3bde 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,89 @@ 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 == "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, + // 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; }()); diff --git a/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h b/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h new file mode 100644 index 000000000..c32905003 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/typeOfStatisticalProcessing.h @@ -0,0 +1,319 @@ +/* + * (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/backend/sections/initializers/sectionRegistry.h b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h index 14e3ed1a7..a15606f85 100644 --- a/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h +++ b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h @@ -77,8 +77,13 @@ 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>}, + {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>}, {1001, &allocateTemplateNumber2<2, 1001, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1002, &allocateTemplateNumber2<2, 1002, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, 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.config.in b/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in index 4e4bbd70a..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 \ @@ -1156,6 +1154,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/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. - 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..56a5be063 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, @@ -39,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, @@ -46,6 +60,38 @@ inline const Recipe S2_R36 = Select >(); +// Brightness temperature satellite products +inline const Recipe S2_R37A = + make_recipe<37, + Select, + Select, + Select + >(); + +inline const Recipe S2_R37B = + make_recipe<37, + Select, + Select, + Select, + Select + >(); + +// 4i Analysis-related products +inline const Recipe S2_R38 = + make_recipe<38, + Select, + Select, + Select + >(); + +// Analysis model-error products +inline const Recipe S2_R39 = + make_recipe<39, + Select, + Select, + Select + >(); + //------------------------------------------------------------------------------ // Virtual (encoder-specific) templates //------------------------------------------------------------------------------ @@ -79,8 +125,14 @@ inline const Recipes Section2Recipes{ 2, std::vector{ &S2_R1, &S2_R15, + &S2_R20, &S2_R24, + &S2_R25, &S2_R36, + &S2_R37A, + &S2_R37B, + &S2_R38, + &S2_R39, &S2_R1001, &S2_R1002 }