diff --git a/share/metkit/params.yaml b/share/metkit/params.yaml index fb2afac2..eb17f6fc 100644 --- a/share/metkit/params.yaml +++ b/share/metkit/params.yaml @@ -3539,6 +3539,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 f4b46c1e..257e4fc5 100644 --- a/src/metkit/mars2grib/backend/concepts/AllConcepts.h +++ b/src/metkit/mars2grib/backend/concepts/AllConcepts.h @@ -99,9 +99,11 @@ #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" @@ -168,10 +170,10 @@ using TypeList = metkit::mars2grib::backend::compile_time_registry_engine::TypeL /// Higher-level code should interact with concepts exclusively through /// registry APIs, not by iterating this list directly. /// -using AllConcepts = - TypeList; +using AllConcepts = TypeList; } // namespace metkit::mars2grib::backend::concepts_::detail \ No newline at end of file diff --git a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h index ce8140cc..1e6b08b7 100644 --- a/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/analysis/analysisEncoding.h @@ -150,7 +150,7 @@ void AnalysisOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& o MARS2GRIB_LOG_CONCEPT(analysis); // Structural validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L}); + validation::match_LocalDefinitionNumber_or_throw(opt, out, {36L, 38L, 39L}); // Deductions long offsetToEndOf4DvarWindowVal = deductions::resolve_offsetToEndOf4DvarWindow_or_throw(mars, par, opt); diff --git a/src/metkit/mars2grib/backend/concepts/concepts.md b/src/metkit/mars2grib/backend/concepts/concepts.md index 5d6037d1..c3535f75 100644 --- a/src/metkit/mars2grib/backend/concepts/concepts.md +++ b/src/metkit/mars2grib/backend/concepts/concepts.md @@ -47,7 +47,60 @@ The ordered list of variants for a concept defines its **local variant index spa --- -## 3. Concept Descriptor Contract +## 3. New Concept vs New Variant + +When changing the concept system, first decide whether the requested behavior is +a new concept or a new variant of an existing concept. + +Use a **new concept** when the feature is an independent semantic axis that must +be composable with other concepts. Use a **new variant** when the feature is an +alternative realization inside an existing semantic axis and does not require +independent composability. + +This distinction is often a domain decision and is usually not reliably +deducible from the code alone. If a request does not explicitly state whether a +new concept or a new variant is required, ask before implementing. + +--- + +## 4. Level Concept Guardrail + +The `level` concept is one of the most constrained concepts in mars2grib. +Although GRIB ultimately represents vertical levels through six low-level +fixed-surface keys: + +* `typeOfFirstFixedSurface` +* `scaleFactorOfFirstFixedSurface` +* `scaledValueOfFirstFixedSurface` +* `typeOfSecondFixedSurface` +* `scaleFactorOfSecondFixedSurface` +* `scaledValueOfSecondFixedSurface` + +mars2grib must not set these keys directly. Many combinations of these keys are +syntactically possible but semantically meaningless for ECMWF products. + +Instead, the encoder must rely on the official level abstraction: + +* `typeOfLevel` +* `level`, when required +* `topLevel` / `bottomLevel`, when required +* PV-array data, when required + +Each supported `typeOfLevel` corresponds to a `LevelType` variant, apart from a +few virtual type-of-level values kept in mars2grib because they cannot be added +to ecCodes for backward-compatibility reasons. Each variant maps to a prescribed +configuration of the low-level fixed-surface keys. + +Do not implement level fixes by injecting `typeOfFirstFixedSurface`, +`scaleFactorOfFirstFixedSurface`, `scaledValueOfFirstFixedSurface`, +`typeOfSecondFixedSurface`, `scaleFactorOfSecondFixedSurface`, or +`scaledValueOfSecondFixedSurface`. If a new level behavior is required, add or +adjust the appropriate `LevelType` variant, matcher mapping, or deduction so the +level remains encoded through `typeOfLevel` and the official level interface. + +--- + +## 5. Concept Descriptor Contract Each concept is implemented as a **descriptor type** that conforms to the `RegisterEntryDescriptor` interface. @@ -68,7 +121,7 @@ The descriptor contains **no runtime state** and no virtual functions. --- -## 4. Capabilities +## 6. Capabilities Concepts may expose multiple independent *capabilities*. @@ -87,7 +140,7 @@ independent dispatch planes. --- -## 5. The Concept Universe (`AllConcepts`) +## 7. The Concept Universe (`AllConcepts`) All concepts known to the system are aggregated into a single ordered typelist: @@ -107,7 +160,7 @@ Changing this order is a **breaking structural change**. --- -## 6. Concept Identifiers +## 8. Concept Identifiers Each concept is assigned a **stable numeric identifier** based on its position in `AllConcepts`. @@ -130,7 +183,7 @@ They are used as indices into: --- -## 7. Variant Index Spaces +## 9. Variant Index Spaces Variants are indexed in two ways: @@ -158,7 +211,7 @@ The global variant index is the primary key used by: --- -## 8. Matching Phase +## 10. Matching Phase Matching determines **which concepts and variants are active** for a given input request. @@ -179,7 +232,7 @@ The result is an `ActiveConceptsData` structure. --- -## 9. Encoding Phases +## 11. Encoding Phases Encoding is divided into **logical stages**, such as: @@ -201,7 +254,7 @@ All dispatch tables are generated **entirely at compile time**. --- -## 10. Design Principles +## 12. Design Principles The concept system is designed around the following principles: @@ -217,7 +270,7 @@ Execution code performs *only iteration and invocation*. --- -## 11. Adding a New Concept +## 13. Adding a New Concept To add a new concept: @@ -231,7 +284,7 @@ No registry code needs to be modified. --- -## 12. Summary +## 14. Summary Concepts are the **semantic backbone** of the mars2grib backend. diff --git a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h index c82e2b60..60a3d097 100644 --- a/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/ensemble/ensembleMatcher.h @@ -2,6 +2,7 @@ // System include #include +#include // Utils #include "metkit/mars2grib/backend/concepts/ensemble/ensembleEnum.h" @@ -12,8 +13,15 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t ensembleMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; + // Skip model-error products: in that case "number" identifies the + // model-error realization, not an ensemble member. + if (has(mars, "type") && get_or_throw(mars, "type") == "eme") { + return compile_time_registry_engine::MISSING; + } + if (has(mars, "number")) { return static_cast(EnsembleType::Individual); } diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h new file mode 100644 index 00000000..6a8777f3 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationConceptDescriptor.h @@ -0,0 +1,160 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file IterationConcept.h +/// @brief Compile-time registry entry for the GRIB `iteration` concept. +/// +/// This header defines `IterationConcept`, the **compile-time descriptor** +/// that registers the GRIB `iteration` concept into the mars2grib +/// compile-time registry engine. +/// +/// The descriptor provides: +/// - The concept name +/// - The mapping between variants and their symbolic names +/// - The set of callbacks associated with each encoding phase +/// - The entry-level matcher used to activate the concept +/// +/// This file contains **no runtime logic**. All decisions are resolved +/// at compile time through template instantiation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System include +#include + +// Registry engine +#include "metkit/mars2grib/backend/compile-time-registry-engine/RegisterEntryDescriptor.h" +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Core concept includes +#include "metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationEnum.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h" + +namespace metkit::mars2grib::backend::concepts_ { + +// Importing the compile-time registry engine namespace locally to avoid +// excessive verbosity in template-heavy code. This is restricted to an +// internal scope and not exposed through public headers. +using namespace metkit::mars2grib::backend::compile_time_registry_engine; + +/// +/// @brief Compile-time descriptor for the `iteration` concept. +/// +/// `IterationConcept` registers the GRIB `iteration` concept into the +/// compile-time registry engine. +/// +/// The descriptor defines: +/// - The canonical concept name +/// - The mapping from variant enum values to symbolic names +/// - The callbacks associated with each encoding phase +/// - The entry-level matcher used to detect applicability +/// +/// All functions in this descriptor are `constexpr` and are evaluated +/// entirely at compile time. +/// +struct IterationConcept : RegisterEntryDescriptor { + + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - Registry identification + /// - Diagnostics and logging + /// - Debug and introspection facilities + /// + static constexpr std::string_view entryName() { return iterationName; } + + /// + /// @brief Return the symbolic name of a concept variant. + /// + /// @tparam T Variant enumeration value + /// + /// @return String view representing the variant name + /// + template + static constexpr std::string_view variantName() { + return iterationTypeName(); + } + + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This function is queried by the registry engine to obtain the + /// callback implementing the `iteration` concept for a given: + /// + /// - Capability + /// - Encoding stage + /// - GRIB section + /// - Concept variant + /// + /// The function returns: + /// - A valid function pointer if the concept is applicable + /// - `nullptr` otherwise + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// @tparam MarsDict_t Type of MARS dictionary + /// @tparam ParDict_t Type of parameter dictionary + /// @tparam OptDict_t Type of options dictionary + /// @tparam OutDict_t Type of output GRIB dictionary + /// + /// @return Function pointer implementing the phase, or `nullptr` + /// + template + static constexpr Fn phaseCallbacks() { + + if constexpr (Capability == 0) { + + if constexpr (iterationApplicable()) { + return &IterationOp; + } + else { + return nullptr; + } + } + else { + return nullptr; + } + + mars2gribUnreachable(); + } + + /// + /// @brief Variant-specific callbacks (not used for this concept). + /// + template + static constexpr Fn variantCallbacks() { + return nullptr; + } + + /// + /// @brief Entry-level matcher callback. + /// + template + static constexpr Fm entryCallbacks() { + if constexpr (Capability == 0) { + return &iterationMatcher; + } + else { + return nullptr; + } + } +}; + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h new file mode 100644 index 00000000..cc0900d7 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationEncoding.h @@ -0,0 +1,176 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file iterationOp.h +/// @brief Implementation of the GRIB `iteration` concept operation. +/// +/// This header defines the applicability rules and execution logic for the +/// **iteration concept** within the mars2grib backend. +/// +/// The iteration concept is responsible for encoding GRIB keys associated with +/// *long-range forecast metadata* stored in the Local Use Section, specifically: +/// +/// - `methodNumber` +/// - `systemNumber` +/// +/// These fields are used to identify the forecasting method and system +/// used for long-range or seasonal products. +/// +/// The implementation follows the standard mars2grib concept model: +/// - Compile-time applicability via `iterationApplicable` +/// - Runtime validation of Local Definition Number +/// - Explicit deduction of required values +/// - Strict error handling with contextual concept exceptions +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of `concepts` +/// to avoid ambiguity and potential conflicts with the C++20 `concept` language +/// feature and related standard headers. +/// +/// This is a deliberate design choice and must not be changed. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationEnum.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Deductions +#include "metkit/mars2grib/backend/deductions/iterationNumber.h" +#include "metkit/mars2grib/backend/deductions/totalNumberOfIterations.h" + +// checks +#include "metkit/mars2grib/backend/checks/matchLocalDefinitionNumber.h" + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +/// +/// @brief Compile-time applicability predicate for the `iteration` concept. +/// +/// This predicate determines whether the iteration concept is applicable +/// for a given combination of: +/// - encoding stage +/// - GRIB section +/// - concept variant +/// +/// Applicability is evaluated entirely at compile time and is used by the +/// concept dispatcher to control instantiation and execution. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant Iteration concept variant +/// +/// @return `true` if the concept is applicable for the given parameters, +/// `false` otherwise. +/// +/// @note +/// The default applicability rule enables the concept only when: +/// - `Variant == IterationType::Default` +/// - `Stage == StagePreset` +/// - `Section == SecLocalUseSection` +/// +template +constexpr bool iterationApplicable() { + return ((Variant == IterationType::Default) && (Stage == StagePreset) && (Section == SecLocalUseSection)); +} + + +/// +/// @brief Execute the `iteration` concept operation. +/// +/// This function implements the runtime logic of the GRIB `iteration` concept. +/// When applicable, it: +/// +/// 1. Validates that the Local Use Section matches the expected definition. +/// 2. Deduces the long-range forecasting method and system identifiers. +/// 3. Encodes the corresponding GRIB keys in the output dictionary. +/// +/// If the concept is invoked when not applicable, a +/// `Mars2GribConceptException` is thrown. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant Iteration concept variant +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam ParDict_t Type of the parameter dictionary +/// @tparam OptDict_t Type of the options dictionary +/// @tparam OutDict_t Type of the GRIB output dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] par Parameter dictionary +/// @param[in] opt Options dictionary +/// @param[out] out Output GRIB dictionary to be populated +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribConceptException +/// If: +/// - the Local Definition Number does not match expectations, +/// - required deductions fail, +/// - any GRIB key cannot be set, +/// - the concept is invoked when not applicable. +/// +/// @note +/// - All runtime errors are wrapped with full concept context +/// (concept name, variant, stage, section). +/// - This concept does not rely on pre-existing GRIB header state. +/// +/// @see iterationApplicable +/// +template +void IterationOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt, OutDict_t& out) { + + using metkit::mars2grib::utils::dict_traits::set_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribConceptException; + + if constexpr (iterationApplicable()) { + + + try { + + MARS2GRIB_LOG_CONCEPT(iteration); + + // Preconditions / contracts + validation::match_LocalDefinitionNumber_or_throw(opt, out, {20L, 38L}); + + // Deductions + auto iterationNumberVal = deductions::resolve_IterationNumber_or_throw(mars, par, opt); + auto totalNumberOfIterationsVal = deductions::resolve_TotalNumberOfIterations_opt(mars, par, opt); + + // Encoding + set_or_throw(out, "iterationNumber", iterationNumberVal); + if (totalNumberOfIterationsVal.has_value()) { + set_or_throw(out, "totalNumberOfIterations", totalNumberOfIterationsVal.value()); + } + } + catch (...) { + MARS2GRIB_CONCEPT_RETHROW(iteration, "Unable to set `iteration` concept..."); + } + + // Successful operation + return; + } + + // Concept invoked outside its applicability domain + MARS2GRIB_CONCEPT_THROW(iteration, "Concept called when not applicable..."); + + // Remove compiler warning + mars2gribUnreachable(); +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h new file mode 100644 index 00000000..5a8fae4f --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationEnum.h @@ -0,0 +1,136 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file iterationEnum.h +/// @brief Definition of the `iteration` concept variants and compile-time metadata. +/// +/// This header defines the **static description** of the GRIB `iteration` concept +/// used by the mars2grib backend. It contains: +/// +/// - the canonical concept name (`iterationName`) +/// - the enumeration of supported long-range variants (`IterationType`) +/// - a compile-time typelist of all variants (`IterationList`) +/// - a compile-time mapping from variant to string identifier +/// +/// This file intentionally contains **no runtime logic** and **no encoding +/// behavior**. Its sole purpose is to provide compile-time metadata used by: +/// +/// - the concept registry +/// - compile-time table generation +/// - logging and diagnostics +/// - static validation of concept variants +/// +/// @note +/// This header is part of the **concept definition layer**. +/// Runtime behavior is implemented separately in the corresponding +/// `iteration.h` / `iterationOp` implementation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System includes +#include +#include + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +using ValueList = metkit::mars2grib::backend::compile_time_registry_engine::ValueList; + +/// +/// @brief Canonical name of the `iteration` concept. +/// +/// This identifier is used: +/// - as the logical concept key in the concept registry +/// - for logging and debugging output +/// - to associate variants and capabilities with the `iteration` concept +/// +/// The value must remain stable across releases. +/// +inline constexpr std::string_view iterationName{"iteration"}; + + +/// +/// @brief Enumeration of all supported `iteration` concept variants. +/// +/// Each enumerator represents a specific long-range forecasting +/// classification or processing mode handled by the encoder. +/// +/// The numeric values of the enumerators are **not semantically relevant**; +/// they are required only to: +/// - provide a stable compile-time identifier +/// - allow array indexing and table generation +/// +/// @note +/// This enumeration is intentionally minimal. Additional variants may be +/// introduced in the future as the long-range concept evolves. +/// +/// @warning +/// Do not reorder existing enumerators, as they are used in compile-time +/// tables and registries. +/// +enum class IterationType : std::size_t { + Default = 0 +}; + + +/// +/// @brief Compile-time list of all `iteration` concept variants. +/// +/// This typelist is used to: +/// - generate concept capability tables at compile time +/// - register all supported variants in the concept registry +/// - enable static iteration over variants without runtime overhead +/// +/// @note +/// The order of this list must match the intended iteration order +/// for registry construction and diagnostics. +/// +using IterationList = ValueList; + + +/// +/// @brief Compile-time mapping from `IterationType` to human-readable name. +/// +/// This function returns the canonical string identifier associated +/// with a given long-range variant. +/// +/// The returned value is used for: +/// - logging and debugging output +/// - error reporting +/// - concept registry diagnostics +/// +/// @tparam T Long-range variant +/// @return String view identifying the variant +/// +/// @note +/// The returned string must remain stable across releases, as it may +/// appear in logs, tests, and diagnostic output. +/// +template +constexpr std::string_view iterationTypeName(); + +#define DEF(T, NAME) \ + template <> \ + constexpr std::string_view iterationTypeName() { \ + return NAME; \ + } + +DEF(IterationType::Default, "default"); + +#undef DEF + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h b/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h new file mode 100644 index 00000000..39fd3f7d --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/iteration/iterationMatcher.h @@ -0,0 +1,25 @@ +#pragma once + +// System include +#include + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/concepts/iteration/iterationEnum.h" +#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +std::size_t iterationMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::has; + + if (has(mars, "iteration")) { + return static_cast(IterationType::Default); + } + + return compile_time_registry_engine::MISSING; +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h b/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h index 3d004351..20cae4f8 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEncoding.h @@ -20,12 +20,29 @@ /// /// - `typeOfLevel` /// - `level` -/// - hybrid vertical coordinate parameters (`pv` array) +/// - model-level coordinates and PV array (multi-level model layouts only) +/// - layered levels expressed via `topLevel` / `bottomLevel` +/// +/// The encoder behaviour is governed by three orthogonal compile-time +/// predicates over the variant: +/// +/// - `needPv()` : the variant requires a PV array +/// (currently only `ModelMultipleLevel`). +/// - `needLevel()` : the variant carries a numeric `level` +/// value to be written. +/// - `needTopBottomLevel()` : the variant is expressed as a +/// layer with explicit `topLevel` and +/// `bottomLevel` GRIB keys. +/// +/// These three axes are independent: any subset may apply to a given +/// variant. A small number of variants additionally have hard-coded +/// special cases inside `LevelOp` (for example the `*At2M` / `*At10M` +/// shortcuts and the `IsobaricInHpa` Pa->hPa conversion). /// /// Depending on the selected level variant, the concept may: /// - set only the level type, /// - set both level type and numeric level, -/// - allocate and populate the PV array (hybrid levels). +/// - allocate and populate the PV array (multi-level model layouts). /// /// The implementation follows the standard mars2grib concept model: /// - Compile-time applicability via `levelApplicable` @@ -67,8 +84,10 @@ namespace metkit::mars2grib::backend::concepts_ { /// /// @brief Compile-time predicate indicating whether a PV array is required. /// -/// Only hybrid vertical coordinates require a PV array describing the -/// vertical transformation. +/// Only multi-level model-level layouts require a PV array describing +/// the vertical hybrid coordinate transformation. Single-level model +/// fields share the GRIB `typeOfLevel="hybrid"` string but do not carry +/// a vertical column and therefore do not need a PV array. /// /// @tparam Variant Level concept variant /// @@ -77,7 +96,7 @@ namespace metkit::mars2grib::backend::concepts_ { /// template constexpr bool needPv() { - if constexpr (Variant == LevelType::Hybrid) { + if constexpr (Variant == LevelType::ModelMultipleLevel) { return true; } else { @@ -91,8 +110,13 @@ constexpr bool needPv() { /// /// @brief Compile-time predicate indicating whether a numeric `level` value is required. /// -/// Some level types require an associated numeric level (e.g. pressure, height), -/// while others encode only the level type. +/// Some level types require an associated numeric level (e.g. pressure, +/// height, model-level number). Both model-level variants +/// (`ModelSingleLevel` and `ModelMultipleLevel`) require it; they differ +/// only in whether a PV array is also needed. +/// +/// `AbstractLevel` carries an opaque numeric `level` value (in contrast +/// to `AbstractSingleLevel` and `AbstractMultipleLevel`, which do not). /// /// @tparam Variant Level concept variant /// @@ -104,10 +128,11 @@ constexpr bool needLevel() { if constexpr (Variant == LevelType::HeightAboveGroundAt10M || Variant == LevelType::HeightAboveGroundAt2M || Variant == LevelType::HeightAboveGround || Variant == LevelType::HeightAboveSeaAt10M || Variant == LevelType::HeightAboveSeaAt2M || Variant == LevelType::HeightAboveSea || - Variant == LevelType::Hybrid || Variant == LevelType::IsobaricInHpa || - Variant == LevelType::IsobaricInPa || Variant == LevelType::Isothermal || - Variant == LevelType::PotentialVorticity || Variant == LevelType::Theta || - Variant == LevelType::OceanModel) { + Variant == LevelType::ModelSingleLevel || Variant == LevelType::ModelMultipleLevel || + Variant == LevelType::IsobaricInHpa || Variant == LevelType::IsobaricInPa || + Variant == LevelType::Isothermal || Variant == LevelType::PotentialVorticity || + Variant == LevelType::Theta || Variant == LevelType::OceanModel || + Variant == LevelType::AbstractLevel) { return true; } else { @@ -156,7 +181,7 @@ constexpr bool needTopBottomLevel() { /// Applicability is evaluated entirely at compile time and is used by the /// concept dispatcher to control instantiation and execution. /// -/// Hybrid levels require special handling: +/// Multi-level model layouts require special handling: /// - during allocation stage to reserve space for the PV array, /// - during preset/runtime stages to set the level type and parameters. /// @@ -223,7 +248,7 @@ constexpr bool levelApplicable() { /// - All runtime errors are wrapped with full concept context /// (concept name, variant, stage, section). /// - The concept does not rely on pre-existing GRIB header state. -/// - Se of typeOfLevel is happening at both preset and runtime stages because +/// - Setting of typeOfLevel is happening at both preset and runtime stages because /// sometimes due to sideeffects in eccodes the typeOfLevel set at preset stage /// can be overwritten before runtime stage. /// diff --git a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h index c88dc368..bf625840 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelEnum.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelEnum.h @@ -81,9 +81,26 @@ inline constexpr std::string_view levelName{"level"}; /// - concrete GRIB levels (e.g. isobaric, hybrid, heightAboveGround) /// - abstract or logical levels used internally by the encoder /// +/// @note +/// Model-level coordinates are split into two variants: +/// - `ModelSingleLevel` for fields published as a single 2D layer on the +/// model-level system (no vertical column, no PV array). +/// - `ModelMultipleLevel` for full vertical columns of model-level data, +/// which require allocation and population of the PV array describing +/// the vertical hybrid coordinate transformation. +/// Both variants share the GRIB `typeOfLevel` string `"hybrid"`; they +/// differ only in encoder behaviour (PV allocation). +/// +/// @note +/// Three abstract variants exist: +/// - `AbstractSingleLevel` and `AbstractMultipleLevel`: opaque level +/// identifiers without an associated numeric `level` value. +/// - `AbstractLevel`: opaque level identifier that carries a numeric +/// `level` value (encoded via the `level` GRIB key). +/// /// @warning -/// Do not reorder existing enumerators, as they are used in compile-time -/// tables and registries. +/// Do not reorder existing enumerators in future changes, as their +/// numeric values are used in compile-time tables and registries. /// enum class LevelType : std::size_t { Surface = 0, @@ -103,7 +120,8 @@ enum class LevelType : std::size_t { MeanSea, HeightAboveSea, HeightAboveGround, - Hybrid, + ModelSingleLevel, ///< 2D field on model-level system; no PV array. + ModelMultipleLevel, ///< Full vertical column of model-level data; requires PV array. Theta, PotentialVorticity, SnowLayer, @@ -124,6 +142,7 @@ enum class LevelType : std::size_t { EntireMeltPond, WaterSurfaceToIsothermalOceanLayer, AbstractSingleLevel, + AbstractLevel, ///< Opaque level identifier carrying a numeric `level` value. AbstractMultipleLevel, HeightAboveSeaAt10M, HeightAboveSeaAt2M, @@ -150,15 +169,16 @@ using LevelList = LevelType::Tropopause, LevelType::NominalTop, LevelType::MostUnstableParcel, LevelType::MixedLayerParcel, LevelType::Isothermal, LevelType::IsobaricInPa, LevelType::IsobaricInHpa, LevelType::LowCloudLayer, LevelType::MediumCloudLayer, LevelType::HighCloudLayer, LevelType::MeanSea, LevelType::HeightAboveSea, - LevelType::HeightAboveGround, LevelType::Hybrid, LevelType::Theta, LevelType::PotentialVorticity, - LevelType::SnowLayer, LevelType::SoilLayer, LevelType::SeaIceLayer, LevelType::OceanSurface, - LevelType::DepthBelowSeaLayer, LevelType::OceanSurfaceToBottom, LevelType::LakeBottom, - LevelType::MixingLayer, LevelType::OceanModel, LevelType::OceanModelLayer, - LevelType::MixedLayerDepthByDensity, LevelType::MixedLayerDepthByTemperature, + LevelType::HeightAboveGround, LevelType::ModelSingleLevel, LevelType::ModelMultipleLevel, + LevelType::Theta, LevelType::PotentialVorticity, LevelType::SnowLayer, LevelType::SoilLayer, + LevelType::SeaIceLayer, LevelType::OceanSurface, LevelType::DepthBelowSeaLayer, + LevelType::OceanSurfaceToBottom, LevelType::LakeBottom, LevelType::MixingLayer, LevelType::OceanModel, + LevelType::OceanModelLayer, LevelType::MixedLayerDepthByDensity, LevelType::MixedLayerDepthByTemperature, LevelType::SnowLayerOverIceOnWater, LevelType::IceTopOnWater, LevelType::IceLayerOnWater, LevelType::EntireMeltPond, LevelType::WaterSurfaceToIsothermalOceanLayer, LevelType::AbstractSingleLevel, - LevelType::AbstractMultipleLevel, LevelType::HeightAboveSeaAt10M, LevelType::HeightAboveSeaAt2M, - LevelType::HeightAboveGroundAt10M, LevelType::HeightAboveGroundAt2M, LevelType::Default>; + LevelType::AbstractLevel, LevelType::AbstractMultipleLevel, LevelType::HeightAboveSeaAt10M, + LevelType::HeightAboveSeaAt2M, LevelType::HeightAboveGroundAt10M, LevelType::HeightAboveGroundAt2M, + LevelType::Default>; /// @@ -205,7 +225,11 @@ DEF(LevelType::HighCloudLayer, "highCloudLayer"); DEF(LevelType::MeanSea, "meanSea"); DEF(LevelType::HeightAboveSea, "heightAboveSea"); DEF(LevelType::HeightAboveGround, "heightAboveGround"); -DEF(LevelType::Hybrid, "hybrid"); +// ModelSingleLevel and ModelMultipleLevel both encode as GRIB +// `typeOfLevel="hybrid"`; they differ only in encoder behaviour +// (PV array allocation, see needPv in levelEncoding.h). +DEF(LevelType::ModelSingleLevel, "hybrid"); +DEF(LevelType::ModelMultipleLevel, "hybrid"); DEF(LevelType::Theta, "theta"); DEF(LevelType::PotentialVorticity, "potentialVorticity"); DEF(LevelType::SnowLayer, "snowLayer"); @@ -226,6 +250,7 @@ DEF(LevelType::IceLayerOnWater, "iceLayerOnWater"); DEF(LevelType::EntireMeltPond, "entireMeltPond"); DEF(LevelType::WaterSurfaceToIsothermalOceanLayer, "waterSurfaceToIsothermalOceanLayer"); DEF(LevelType::AbstractSingleLevel, "abstractSingleLevel"); +DEF(LevelType::AbstractLevel, "abstractLevel"); DEF(LevelType::AbstractMultipleLevel, "abstractMultipleLevel"); DEF(LevelType::HeightAboveSeaAt10M, "heightAboveSeaAt10m"); DEF(LevelType::HeightAboveSeaAt2M, "heightAboveSeaAt2m"); diff --git a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h index 3d23158c..2f05349c 100644 --- a/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/level/levelMatcher.h @@ -141,6 +141,14 @@ inline std::size_t matchSFC(const long param) { return compile_time_registry_engine::MISSING; } + // ECMWF covariance paramIds (254001..254017) are defined in + // eccodes/definitions/grib2/localConcepts/{ecmf,era6}/paramId.def with + // typeOfFirstFixedSurface=254, which maps to the eccodes typeOfLevel + // concept "abstractLevel". + if (matchAny(param, range(254001, 254017))) { + return static_cast(LevelType::AbstractLevel); + } + throw utils::exceptions::Mars2GribMatcherException( "No mapping exists for param \"" + std::to_string(param) + "\" on levtype SFC", Here()); } @@ -162,9 +170,19 @@ inline std::size_t matchML(const long param) { using metkit::mars2grib::util::param_matcher::matchAny; using metkit::mars2grib::util::param_matcher::range; - if (matchAny(param, range(21, 23), range(75, 77), range(129, 133), 135, 138, 152, range(155, 157), 203, - range(246, 248), range(162100, 162113), 260290, 260292, 260293)) { - return static_cast(LevelType::Hybrid); + // Single-level subset of ML params: 2D fields published on the + // model-level levtype but not requiring a vertical PV array. This + // guard fires before the multi-level rule below; params listed here + // are removed from the multi-level set. + if (matchAny(param, 22, 127, 128, 129, 152)) { + return static_cast(LevelType::ModelSingleLevel); + } + + // Multi-level model fields: full vertical column, require allocation + // and population of the PV array describing the hybrid coordinate. + if (matchAny(param, 21, 23, range(75, 77), range(130, 133), 135, 138, range(155, 157), 203, range(246, 248), + range(162100, 162113), 260290, 260292, 260293)) { + return static_cast(LevelType::ModelMultipleLevel); } throw utils::exceptions::Mars2GribMatcherException( @@ -273,10 +291,10 @@ inline std::size_t matchO2D(const long param) { if (matchAny(param, 262116)) { return static_cast(LevelType::MixedLayerDepthByTemperature); } - if (matchAny(param, 262118, 262119, 262121, 262122)) { + if (matchAny(param, 262118, 262119, 262121, 262122, 262146, 262147)) { return static_cast(LevelType::DepthBelowSeaLayer); } - if (matchAny(param, 262120, 262123)) { + if (matchAny(param, 262120, 262123, 262148)) { return static_cast(LevelType::OceanSurfaceToBottom); } if (matchAny(param, 262141)) { diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h new file mode 100644 index 00000000..5ff7c713 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorConceptDescriptor.h @@ -0,0 +1,160 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file ModelErrorConcept.h +/// @brief Compile-time registry entry for the GRIB `modelError` concept. +/// +/// This header defines `ModelErrorConcept`, the **compile-time descriptor** +/// that registers the GRIB `modelError` concept into the mars2grib +/// compile-time registry engine. +/// +/// The descriptor provides: +/// - The concept name +/// - The mapping between variants and their symbolic names +/// - The set of callbacks associated with each encoding phase +/// - The entry-level matcher used to activate the concept +/// +/// This file contains **no runtime logic**. All decisions are resolved +/// at compile time through template instantiation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System include +#include + +// Registry engine +#include "metkit/mars2grib/backend/compile-time-registry-engine/RegisterEntryDescriptor.h" +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Core concept includes +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h" + +namespace metkit::mars2grib::backend::concepts_ { + +// Importing the compile-time registry engine namespace locally to avoid +// excessive verbosity in template-heavy code. This is restricted to an +// internal scope and not exposed through public headers. +using namespace metkit::mars2grib::backend::compile_time_registry_engine; + +/// +/// @brief Compile-time descriptor for the `modelError` concept. +/// +/// `ModelErrorConcept` registers the GRIB `modelError` concept into the +/// compile-time registry engine. +/// +/// The descriptor defines: +/// - The canonical concept name +/// - The mapping from variant enum values to symbolic names +/// - The callbacks associated with each encoding phase +/// - The entry-level matcher used to detect applicability +/// +/// All functions in this descriptor are `constexpr` and are evaluated +/// entirely at compile time. +/// +struct ModelErrorConcept : RegisterEntryDescriptor { + + /// + /// @brief Return the canonical name of the concept. + /// + /// This name is used for: + /// - Registry identification + /// - Diagnostics and logging + /// - Debug and introspection facilities + /// + static constexpr std::string_view entryName() { return modelErrorName; } + + /// + /// @brief Return the symbolic name of a concept variant. + /// + /// @tparam T Variant enumeration value + /// + /// @return String view representing the variant name + /// + template + static constexpr std::string_view variantName() { + return modelErrorTypeName(); + } + + /// + /// @brief Return the callback associated with a specific encoding phase. + /// + /// This function is queried by the registry engine to obtain the + /// callback implementing the `modelError` concept for a given: + /// + /// - Capability + /// - Encoding stage + /// - GRIB section + /// - Concept variant + /// + /// The function returns: + /// - A valid function pointer if the concept is applicable + /// - `nullptr` otherwise + /// + /// @tparam Capability Encoding capability index + /// @tparam Stage Encoding stage + /// @tparam Sec GRIB section + /// @tparam Variant Concept variant + /// @tparam MarsDict_t Type of MARS dictionary + /// @tparam ParDict_t Type of parameter dictionary + /// @tparam OptDict_t Type of options dictionary + /// @tparam OutDict_t Type of output GRIB dictionary + /// + /// @return Function pointer implementing the phase, or `nullptr` + /// + template + static constexpr Fn phaseCallbacks() { + + if constexpr (Capability == 0) { + + if constexpr (modelErrorApplicable()) { + return &ModelErrorOp; + } + else { + return nullptr; + } + } + else { + return nullptr; + } + + mars2gribUnreachable(); + } + + /// + /// @brief Variant-specific callbacks (not used for this concept). + /// + template + static constexpr Fn variantCallbacks() { + return nullptr; + } + + /// + /// @brief Entry-level matcher callback. + /// + template + static constexpr Fm entryCallbacks() { + if constexpr (Capability == 0) { + return &modelErrorMatcher; + } + else { + return nullptr; + } + } +}; + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h new file mode 100644 index 00000000..94317091 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEncoding.h @@ -0,0 +1,178 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file modelErrorOp.h +/// @brief Implementation of the GRIB `modelError` concept operation. +/// +/// This header defines the applicability rules and execution logic for the +/// **modelError concept** within the mars2grib backend. +/// +/// The modelError concept is responsible for encoding GRIB keys related to +/// *model-error metadata* stored in the Local Use Section, specifically: +/// +/// - `componentIndex` +/// - `numberOfComponents` +/// - `modelErrorType` +/// +/// These fields identify the realization within a model-error ensemble, +/// the total number of realizations, and the type of model error. +/// +/// The implementation follows the standard mars2grib concept model: +/// - Compile-time applicability via `modelErrorApplicable` +/// - Runtime validation of Local Definition Number +/// - Explicit deduction of required values +/// - Strict error handling with contextual concept exceptions +/// +/// @note +/// The namespace name `concepts_` is intentionally used instead of `concepts` +/// to avoid ambiguity and potential conflicts with the C++20 `concept` language +/// feature and related standard headers. +/// +/// This is a deliberate design choice and must not be changed. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +// Deductions +#include "metkit/mars2grib/backend/deductions/componentIndex.h" +#include "metkit/mars2grib/backend/deductions/modelErrorType.h" +#include "metkit/mars2grib/backend/deductions/numberOfComponents.h" + +// checks +#include "metkit/mars2grib/backend/checks/matchLocalDefinitionNumber.h" + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +/// +/// @brief Compile-time applicability predicate for the `modelError` concept. +/// +/// This predicate determines whether the modelError concept is applicable +/// for a given combination of: +/// - encoding stage +/// - GRIB section +/// - concept variant +/// +/// Applicability is evaluated entirely at compile time and is used by the +/// concept dispatcher to control instantiation and execution. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant ModelError concept variant +/// +/// @return `true` if the concept is applicable for the given parameters, +/// `false` otherwise. +/// +/// @note +/// The default applicability rule enables the concept only when: +/// - `Variant == ModelErrorType::Default` +/// - `Stage == StagePreset` +/// - `Section == SecLocalUseSection` +/// +template +constexpr bool modelErrorApplicable() { + return ((Variant == ModelErrorType::Default) && (Stage == StagePreset) && (Section == SecLocalUseSection)); +} + + +/// +/// @brief Execute the `modelError` concept operation. +/// +/// This function implements the runtime logic of the GRIB `modelError` concept. +/// When applicable, it: +/// +/// 1. Validates that the Local Use Section matches the expected definition. +/// 2. Deduces the model-error related identifiers. +/// 3. Encodes the corresponding GRIB keys in the output dictionary. +/// +/// If the concept is invoked when not applicable, a +/// `Mars2GribConceptException` is thrown. +/// +/// @tparam Stage Encoding stage (compile-time constant) +/// @tparam Section GRIB section index (compile-time constant) +/// @tparam Variant ModelError concept variant +/// @tparam MarsDict_t Type of the MARS input dictionary +/// @tparam ParDict_t Type of the parameter dictionary +/// @tparam OptDict_t Type of the options dictionary +/// @tparam OutDict_t Type of the GRIB output dictionary +/// +/// @param[in] mars MARS input dictionary +/// @param[in] par Parameter dictionary +/// @param[in] opt Options dictionary +/// @param[out] out Output GRIB dictionary to be populated +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribConceptException +/// If: +/// - the Local Definition Number does not match expectations, +/// - required deductions fail, +/// - any GRIB key cannot be set, +/// - the concept is invoked when not applicable. +/// +/// @note +/// - All runtime errors are wrapped with full concept context +/// (concept name, variant, stage, section). +/// - This concept does not rely on pre-existing GRIB header state. +/// +/// @see modelErrorApplicable +/// +template +void ModelErrorOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt, OutDict_t& out) { + + using metkit::mars2grib::utils::dict_traits::set_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribConceptException; + + if constexpr (modelErrorApplicable()) { + + + try { + + MARS2GRIB_LOG_CONCEPT(modelError); + + // Preconditions / contracts + validation::match_LocalDefinitionNumber_or_throw(opt, out, {25L, 39L}); + + // Deductions + auto componentIndexVal = deductions::resolve_ComponentIndex_or_throw(mars, par, opt); + auto numberOfComponentsVal = deductions::resolve_NumberOfComponents_or_throw(mars, par, opt); + auto modelErrorTypeVal = deductions::resolve_ModelErrorType_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "componentIndex", componentIndexVal); + set_or_throw(out, "numberOfComponents", numberOfComponentsVal); + set_or_throw(out, "modelErrorType", modelErrorTypeVal); + } + catch (...) { + MARS2GRIB_CONCEPT_RETHROW(modelError, "Unable to set `modelError` concept..."); + } + + // Successful operation + return; + } + + // Concept invoked outside its applicability domain + MARS2GRIB_CONCEPT_THROW(modelError, "Concept called when not applicable..."); + + // Remove compiler warning + mars2gribUnreachable(); +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h new file mode 100644 index 00000000..eeb96d3a --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h @@ -0,0 +1,136 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file modelErrorEnum.h +/// @brief Definition of the `modelError` concept variants and compile-time metadata. +/// +/// This header defines the **static description** of the GRIB `modelError` concept +/// used by the mars2grib backend. It contains: +/// +/// - the canonical concept name (`modelErrorName`) +/// - the enumeration of supported model-error variants (`ModelErrorType`) +/// - a compile-time typelist of all variants (`ModelErrorList`) +/// - a compile-time mapping from variant to string identifier +/// +/// This file intentionally contains **no runtime logic** and **no encoding +/// behavior**. Its sole purpose is to provide compile-time metadata used by: +/// +/// - the concept registry +/// - compile-time table generation +/// - logging and diagnostics +/// - static validation of concept variants +/// +/// @note +/// This header is part of the **concept definition layer**. +/// Runtime behavior is implemented separately in the corresponding +/// `modelError.h` / `modelErrorOp` implementation. +/// +/// @ingroup mars2grib_backend_concepts +/// +#pragma once + +// System includes +#include +#include + +// Core concept includes +#include "metkit/mars2grib/backend/compile-time-registry-engine/common.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +using ValueList = metkit::mars2grib::backend::compile_time_registry_engine::ValueList; + +/// +/// @brief Canonical name of the `modelError` concept. +/// +/// This identifier is used: +/// - as the logical concept key in the concept registry +/// - for logging and debugging output +/// - to associate variants and capabilities with the `modelError` concept +/// +/// The value must remain stable across releases. +/// +inline constexpr std::string_view modelErrorName{"modelError"}; + + +/// +/// @brief Enumeration of all supported `modelError` concept variants. +/// +/// Each enumerator represents a specific model-error +/// classification or processing mode handled by the encoder. +/// +/// The numeric values of the enumerators are **not semantically relevant**; +/// they are required only to: +/// - provide a stable compile-time identifier +/// - allow array indexing and table generation +/// +/// @note +/// This enumeration is intentionally minimal. Additional variants may be +/// introduced in the future as the model-error concept evolves. +/// +/// @warning +/// Do not reorder existing enumerators, as they are used in compile-time +/// tables and registries. +/// +enum class ModelErrorType : std::size_t { + Default = 0 +}; + + +/// +/// @brief Compile-time list of all `modelError` concept variants. +/// +/// This typelist is used to: +/// - generate concept capability tables at compile time +/// - register all supported variants in the concept registry +/// - enable static iteration over variants without runtime overhead +/// +/// @note +/// The order of this list must match the intended iteration order +/// for registry construction and diagnostics. +/// +using ModelErrorList = ValueList; + + +/// +/// @brief Compile-time mapping from `ModelErrorType` to human-readable name. +/// +/// This function returns the canonical string identifier associated +/// with a given model-error variant. +/// +/// The returned value is used for: +/// - logging and debugging output +/// - error reporting +/// - concept registry diagnostics +/// +/// @tparam T Model-error variant +/// @return String view identifying the variant +/// +/// @note +/// The returned string must remain stable across releases, as it may +/// appear in logs, tests, and diagnostic output. +/// +template +constexpr std::string_view modelErrorTypeName(); + +#define DEF(T, NAME) \ + template <> \ + constexpr std::string_view modelErrorTypeName() { \ + return NAME; \ + } + +DEF(ModelErrorType::Default, "default"); + +#undef DEF + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h new file mode 100644 index 00000000..96ce9248 --- /dev/null +++ b/src/metkit/mars2grib/backend/concepts/model-error/modelErrorMatcher.h @@ -0,0 +1,35 @@ +#pragma once + +// System include +#include +#include + +// Utils +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/backend/concepts/model-error/modelErrorEnum.h" +#include "metkit/mars2grib/utils/dictionary_traits/dictionary_access_traits.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::concepts_ { + +template +std::size_t modelErrorMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + + // Concept does not apply unless "type" is present and equals "eme" + if (!has(mars, "type") || get_or_throw(mars, "type") != "eme") { + return compile_time_registry_engine::MISSING; + } + + // At this point the request is a model-error request: "number" is mandatory + if (!has(mars, "number")) { + throw utils::exceptions::Mars2GribMatcherException( + "modelError concept requires MARS key \"number\" when type=\"eme\"", Here()); + } + + return static_cast(ModelErrorType::Default); +} + +} // namespace metkit::mars2grib::backend::concepts_ diff --git a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h index 85fc7b42..2794e736 100644 --- a/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/point-in-time/pointInTimeMatcher.h @@ -34,7 +34,7 @@ std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { 260509, 260688, 261001, 261002, range(261014, 261016), 261018, 261023, range(262000, 262009), 262011, 262014, 262015, 262017, 262018, 262023, 262024, range(262100, 262106), range(262108, 262112), range(262113, 262116), range(262118, 262125), 262130, range(262139, 262141), 262143, 262144, - range(262500, 262502), range(262505, 262507), 262900, 262906, 262907)) { + range(262146, 262149), range(262500, 262502), range(262505, 262507), 262900, 262906, 262907)) { return static_cast(PointInTimeType::Default); } @@ -53,6 +53,16 @@ std::size_t pointInTimeMatcher(const MarsDict_t& mars, const OptDict_t& opt) { return static_cast(PointInTimeType::Default); } + // ECMWF covariance / analysis-uncertainty paramIds (254001..254017). + // These are point-in-time products living on the abstractLevel + // (typeOfFirstFixedSurface=254) and are used with MARS type=est + // (individual ensemble member, PDT=1) as well as with non-ensemble + // analyses (PDT=0). Without this mapping, PointInTimeConcept is left + // inactive and Section 4 recipe selection fails with "No matching recipe". + if (matchAny(param, range(254001, 254017))) { + return static_cast(PointInTimeType::Default); + } + return compile_time_registry_engine::MISSING; } diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h index 2ac3f05c..8862c79c 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEncoding.h @@ -117,6 +117,7 @@ // Deductions #include "metkit/mars2grib/backend/deductions/channel.h" #include "metkit/mars2grib/backend/deductions/instrumentType.h" +#include "metkit/mars2grib/backend/deductions/numberOfFrequencies.h" #include "metkit/mars2grib/backend/deductions/satelliteNumber.h" #include "metkit/mars2grib/backend/deductions/satelliteSeries.h" #include "metkit/mars2grib/backend/deductions/scaleFactorOfCentralWaveNumber.h" @@ -214,14 +215,30 @@ void SatelliteOp(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& if constexpr (Section == SecLocalUseSection && Stage == StagePreset) { - // Check/Validation - validation::match_LocalDefinitionNumber_or_throw(opt, out, {24}); + if constexpr (Variant == SatelliteType::BrightnessTemperature) { - // Deductions - long channel = deductions::resolve_Channel_or_throw(mars, par, opt); + // Check/Validation + validation::match_LocalDefinitionNumber_or_throw(opt, out, {37}); - // Encoding - set_or_throw(out, "channel", channel); + // Deductions + long channelNumber = deductions::resolve_Channel_or_throw(mars, par, opt); + long numberOfFrequencies = deductions::resolve_NumberOfFrequencies_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "channelNumber", channelNumber); + set_or_throw(out, "numberOfFrequencies", numberOfFrequencies); + } + else { + + // Check/Validation + validation::match_LocalDefinitionNumber_or_throw(opt, out, {24}); + + // Deductions + long channel = deductions::resolve_Channel_or_throw(mars, par, opt); + + // Encoding + set_or_throw(out, "channel", channel); + } } if constexpr (Section == SecProductDefinitionSection && Stage == StageAllocate) { diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h index 2f52d1f0..dea8784d 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteEnum.h @@ -85,7 +85,8 @@ inline constexpr std::string_view satelliteName{"satellite"}; /// tables and registries. /// enum class SatelliteType : std::size_t { - Default = 0 + Default = 0, + BrightnessTemperature = 1 }; @@ -101,7 +102,7 @@ enum class SatelliteType : std::size_t { /// The order of this list must match the intended iteration order /// for registry construction and diagnostics. /// -using SatelliteList = ValueList; +using SatelliteList = ValueList; /// @@ -132,6 +133,7 @@ constexpr std::string_view satelliteTypeName(); } DEF(SatelliteType::Default, "default"); +DEF(SatelliteType::BrightnessTemperature, "brightnessTemperature"); #undef DEF diff --git a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h index 07dfba36..b7869f79 100644 --- a/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h +++ b/src/metkit/mars2grib/backend/concepts/satellite/satelliteMatcher.h @@ -12,10 +12,15 @@ namespace metkit::mars2grib::backend::concepts_ { template std::size_t satelliteMatcher(const MarsDict_t& mars, const OptDict_t& opt) { + using metkit::mars2grib::utils::dict_traits::get_or_throw; using metkit::mars2grib::utils::dict_traits::has; if (has(mars, "channel") && has(mars, "ident") && has(mars, "instrument")) { - return static_cast(SatelliteType::Default); + if (has(mars, "param") && get_or_throw(mars, "param") == 194) { + return static_cast(SatelliteType::BrightnessTemperature); + } + + return static_cast(SatelliteType::Default); } return compile_time_registry_engine::MISSING; diff --git a/src/metkit/mars2grib/backend/deductions/componentIndex.h b/src/metkit/mars2grib/backend/deductions/componentIndex.h new file mode 100644 index 00000000..525e51fa --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/componentIndex.h @@ -0,0 +1,165 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file componentIndex.h +/// @brief Deduction of the model-error realization identifier. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **model-error realization identifier** (`componentIndex`) +/// from MARS metadata. +/// +/// The deduction retrieves the realization identifier explicitly from the +/// MARS dictionary and returns it verbatim, without applying inference, +/// defaulting, or semantic interpretation. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - enforcing deterministic resolution rules +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys directly +/// - do NOT apply heuristic or data-driven inference +/// - do NOT validate against GRIB code tables unless explicitly required +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved directly from input dictionaries +/// +/// @section References +/// Concept: +/// - @ref modelErrorEncoding.h +/// +/// Related deductions: +/// - @ref numberOfComponents.h +/// - @ref modelErrorType.h +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +#include + +#include "eckit/log/Log.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the model-error realization identifier. +/// +/// @section Deduction contract +/// - Reads: `mars["type"]`, `mars["number"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction retrieves the model-error realization identifier from +/// the MARS dictionary. For requests with `type=eme`, the MARS key +/// `number` identifies the realization within the model-error ensemble +/// (not an ensemble-forecast member). +/// +/// The value is treated as mandatory and is returned verbatim as a +/// numeric identifier. No inference, defaulting, or validation against +/// GRIB code tables is performed. +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary. Must provide the key `number`. +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary (unused). +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary from which the realization identifier is retrieved. +/// +/// @param[in] par +/// Parameter dictionary (unused). +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The model-error realization identifier. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If: +/// - the key `type` is missing or not equal to `"eme"` (defence-in-depth: +/// `componentIndex` is only meaningful for model-error products), +/// - the key `number` is missing, cannot be converted to `long`, +/// - or any unexpected error occurs during deduction. +/// +/// @note +/// This deduction assumes that the realization identifier is explicitly +/// provided by the MARS dictionary and does not attempt any semantic +/// interpretation or consistency checking. +/// +template +long resolve_ComponentIndex_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Defence-in-depth: componentIndex is only meaningful for type=eme + // (model-error products). Resolving it for any other request type + // indicates a serious upstream contract violation (wrong recipe, + // matcher bypass, etc.) and must be surfaced as a hard failure + // with an unambiguous diagnostic. + const std::string typeVal = get_or_throw(mars, "type"); + if (typeVal != "eme") { + throw Mars2GribDeductionException(std::string("`componentIndex` requested for a non-`eme` request: " + "`mars[\"type\"]` is `") + + typeVal + + "` but only `eme` is supported. This is a serious upstream " + "contract violation: the model-error deduction was reached " + "for a request that is not a model-error product. Check " + "recipe selection and matcher dispatch.", + Here()); + } + + // Retrieve mandatory MARS number (model-error realization id) + long componentIndex = get_or_throw(mars, "number"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`componentIndex` resolved from input dictionaries: value="; + logMsg += std::to_string(componentIndex); + return logMsg; + }()); + + // Success exit point + return componentIndex; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `componentIndex` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/iterationNumber.h b/src/metkit/mars2grib/backend/deductions/iterationNumber.h new file mode 100644 index 00000000..fe023ef8 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/iterationNumber.h @@ -0,0 +1,130 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file iterationNumber.h +/// @brief Deduction of the offset to the end of the 4D-Var analysis window. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **offset to the end of the 4D-Var assimilation window** +/// from input dictionaries. +/// +/// The deduction retrieves the offset explicitly from the MARS dictionary. +/// No inference, defaulting, normalization, or validation of temporal +/// semantics is performed. +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved from one or more input dictionaries +/// +/// @section References +/// Concept: +/// - @ref analysisEncoding.h +/// +/// Related deductions: +/// - @ref lengthOfTimeWindow.h +/// +/// @ingroup mars2grib_backend_deductions +/// +#pragma once + +// System includes +#include + +// Core deduction includes +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the offset to the end of the 4D-Var analysis window. +/// +/// @section Deduction contract +/// - Reads: `mars["iteration"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction resolves the temporal offset between the analysis +/// reference time and the end of the 4D-Var assimilation window. +/// +/// The returned value is treated as an opaque numeric quantity. Its unit +/// and interpretation are defined by upstream MARS/IFS conventions and +/// are not interpreted by this deduction. +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary. Must provide the key `iteration`. +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary (unused). +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary from which the offset is resolved. +/// +/// @param[in] par +/// Parameter dictionary (unused). +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The offset to the end of the 4D-Var analysis window. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If the key `iteration` is missing, cannot be converted to `long`, +/// or if any unexpected error occurs during deduction. +/// +/// @note +/// This deduction assumes that the offset is explicitly provided by +/// MARS and does not attempt any inference or defaulting. +/// +template +long resolve_IterationNumber_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Retrieve mandatory MARS iteration + auto iterationNumber = get_or_throw(mars, "iteration"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`iterationNumber` resolved from input dictionaries: value='"; + logMsg += std::to_string(iterationNumber) + "'"; + return logMsg; + }()); + + // Success exit point + return iterationNumber; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `iterationNumber` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/modelErrorType.h b/src/metkit/mars2grib/backend/deductions/modelErrorType.h new file mode 100644 index 00000000..24125089 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/modelErrorType.h @@ -0,0 +1,146 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file modelErrorType.h +/// @brief Deduction of the model-error type identifier. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **model-error type identifier** (`modelErrorType`) from +/// the parameter dictionary. +/// +/// The value is not derivable from MARS alone. It must be supplied via +/// the parameter dictionary by the upstream tool, typically read from +/// the input GRIB1 handle being re-encoded. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - enforcing deterministic resolution rules +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys directly +/// - do NOT apply heuristic or data-driven inference +/// - do NOT validate against GRIB code tables unless explicitly required +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved directly from input dictionaries +/// +/// @section References +/// Concept: +/// - @ref modelErrorEncoding.h +/// +/// Related deductions: +/// - @ref componentIndex.h +/// - @ref numberOfComponents.h +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +#include + +#include "eckit/log/Log.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the model-error type identifier. +/// +/// @section Deduction contract +/// - Reads: `par["modelErrorType"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction retrieves the model-error type identifier from the +/// parameter dictionary. +/// +/// The value is treated as mandatory: it cannot be derived from MARS +/// metadata alone and must be supplied by the upstream tool that +/// populates the parameter dictionary (typically read from the input +/// GRIB1 handle being re-encoded). +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary (unused). +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary. Must provide the key +/// `modelErrorType`. +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary (unused). +/// +/// @param[in] par +/// Parameter dictionary from which the model-error type is retrieved. +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The model-error type identifier. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If the key `modelErrorType` is missing from the parameter dictionary, +/// cannot be converted to `long`, or if any unexpected error occurs +/// during deduction. +/// +/// @note +/// This deduction does not infer or default the value. Absence of the +/// key in the parameter dictionary is considered a contract violation +/// by the upstream tool. +/// +template +long resolve_ModelErrorType_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Retrieve mandatory parameter-dictionary modelErrorType + long modelErrorType = get_or_throw(par, "modelErrorType"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`modelErrorType` resolved from input dictionaries: value="; + logMsg += std::to_string(modelErrorType); + return logMsg; + }()); + + // Success exit point + return modelErrorType; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `modelErrorType` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/numberOfComponents.h b/src/metkit/mars2grib/backend/deductions/numberOfComponents.h new file mode 100644 index 00000000..f831dca6 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/numberOfComponents.h @@ -0,0 +1,146 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file numberOfComponents.h +/// @brief Deduction of the model-error ensemble size. +/// +/// This header defines deduction utilities used by the mars2grib backend +/// to resolve the **total number of model-error realizations** +/// (`numberOfComponents`) from the parameter dictionary. +/// +/// The value is not derivable from MARS alone. It must be supplied via +/// the parameter dictionary by the upstream tool, typically read from +/// the input GRIB1 handle being re-encoded. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - enforcing deterministic resolution rules +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys directly +/// - do NOT apply heuristic or data-driven inference +/// - do NOT validate against GRIB code tables unless explicitly required +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or invalid inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value resolved directly from input dictionaries +/// +/// @section References +/// Concept: +/// - @ref modelErrorEncoding.h +/// +/// Related deductions: +/// - @ref componentIndex.h +/// - @ref modelErrorType.h +/// +/// @ingroup mars2grib_backend_deductions +/// + +#pragma once + +#include + +#include "eckit/log/Log.h" +#include "metkit/mars2grib/utils/generalUtils.h" + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the total number of model-error realizations. +/// +/// @section Deduction contract +/// - Reads: `par["numberOfComponents"]` +/// - Writes: none +/// - Side effects: logging (RESOLVE) +/// - Failure mode: throws +/// +/// This deduction retrieves the total number of realizations in the +/// model-error ensemble from the parameter dictionary. +/// +/// The value is treated as mandatory: it cannot be derived from MARS +/// metadata alone and must be supplied by the upstream tool that +/// populates the parameter dictionary (typically read from the input +/// GRIB1 handle being re-encoded). +/// +/// @tparam MarsDict_t +/// Type of the MARS dictionary (unused). +/// +/// @tparam ParDict_t +/// Type of the parameter dictionary. Must provide the key +/// `numberOfComponents`. +/// +/// @tparam OptDict_t +/// Type of the options dictionary (unused). +/// +/// @param[in] mars +/// MARS dictionary (unused). +/// +/// @param[in] par +/// Parameter dictionary from which the ensemble size is retrieved. +/// +/// @param[in] opt +/// Options dictionary (unused). +/// +/// @return +/// The total number of model-error realizations. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If the key `numberOfComponents` is missing from the parameter +/// dictionary, cannot be converted to `long`, or if any unexpected error +/// occurs during deduction. +/// +/// @note +/// This deduction does not infer or default the value. Absence of the +/// key in the parameter dictionary is considered a contract violation +/// by the upstream tool. +/// +template +long resolve_NumberOfComponents_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Retrieve mandatory parameter-dictionary numberOfComponents + long numberOfComponents = get_or_throw(par, "numberOfComponents"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`numberOfComponents` resolved from input dictionaries: value="; + logMsg += std::to_string(numberOfComponents); + return logMsg; + }()); + + // Success exit point + return numberOfComponents; + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Failed to resolve `numberOfComponents` from input dictionaries", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +}; + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h new file mode 100644 index 00000000..40faf47f --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/numberOfFrequencies.h @@ -0,0 +1,62 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +#pragma once + +#include + +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + +namespace metkit::mars2grib::backend::deductions { + +template +long resolve_NumberOfFrequencies_or_throw(const MarsDict_t& mars, const ParDict_t& par, const OptDict_t& opt) { + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::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/significanceOfReferenceTime.h b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h index ecbf50c2..4d64f45e 100644 --- a/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h +++ b/src/metkit/mars2grib/backend/deductions/significanceOfReferenceTime.h @@ -118,10 +118,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 = { - {"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"}}; + constexpr std::array forecastTypes = { + {"fc", "cf", "pf", "cm", "fp", "em", "ep", "es", "fa", "efi", "efic", "bf", + "cd", "wem", "wes", "cr", "ses", "taem", "taes", "sg", "sf", "if", "fcmean", "fcmax", + "fcmin", "fcstdev", "ssd", "tf", "bf", "cd", "hcmean", "s3", "si", "est"}}; constexpr std::array startOfDataAssimilationTypes = {{"4i", "4v", "me", "eme"}}; diff --git a/src/metkit/mars2grib/backend/deductions/totalNumberOfIterations.h b/src/metkit/mars2grib/backend/deductions/totalNumberOfIterations.h new file mode 100644 index 00000000..f15b8328 --- /dev/null +++ b/src/metkit/mars2grib/backend/deductions/totalNumberOfIterations.h @@ -0,0 +1,171 @@ +/* + * (C) Copyright 2025- ECMWF and individual contributors. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// +/// @file totalNumberOfIterations.h +/// @brief Deduction of the GRIB totalNumberOfIterations (in seconds). +/// +/// This header defines the deduction used by the mars2grib backend to resolve +/// the GRIB totalNumberOfIterations key from input dictionaries. +/// +/// The deduction reads the parameter dictionary entry totalNumberOfIterations, +/// interprets it as hours, and converts it to seconds. If the key is missing, +/// the deduction returns `std::nullopt`. +/// +/// Deductions are responsible for: +/// - extracting values from MARS, parameter, and option dictionaries +/// - applying explicit, deterministic deduction logic +/// - returning strongly typed values to concept operations +/// +/// Deductions: +/// - do NOT encode GRIB keys +/// - do NOT infer units or values beyond the documented rule +/// - do NOT perform GRIB table validation +/// +/// Error handling follows a strict fail-fast strategy: +/// - missing or malformed inputs cause immediate failure +/// - errors are reported using domain-specific deduction exceptions +/// - original errors are preserved via nested exception propagation +/// +/// Logging follows the mars2grib deduction policy: +/// - RESOLVE: value derived from the parameter dictionary +/// - DEFAULT: value defaulted to the GRIB missing code +/// +/// @section References +/// Concept: +/// - @ref analysisEncoding.h +/// +/// Related deductions: +/// - @ref offsetToEndOf4DvarWindow.h +/// +/// @ingroup mars2grib_backend_deductions +/// +#pragma once + +// System Include +#include +#include +#include + +// Other project includes +#include "eckit/log/Log.h" + +// Core deduction includes +#include "metkit/config/LibMetkit.h" +#include "metkit/mars2grib/utils/generalUtils.h" +#include "metkit/mars2grib/utils/logUtils.h" +#include "metkit/mars2grib/utils/mars2gribExceptions.h" + + +namespace metkit::mars2grib::backend::deductions { + +/// +/// @brief Resolve the GRIB `totalNumberOfIterations` expressed in seconds. +/// +/// This deduction determines the value of the GRIB `totalNumberOfIterations` +/// (in seconds) based on the parameter dictionary key `totalNumberOfIterations`. +/// +/// The deduction follows these rules: +/// +/// - If the key `totalNumberOfIterations` is present in the parameter dictionary, +/// its value is interpreted as **hours** and converted to seconds. +/// - If the key is absent, `std::nullopt` is used, which should be handled +/// by the encoding layer as the GRIB missing code (0xFFFF). +/// +/// @important +/// This deduction currently relies on **implicit assumptions** about +/// units and defaults that are not explicitly encoded in MARS metadata. +/// These assumptions are documented but not enforced via validation. +/// +/// @assumptions +/// - `par::totalNumberOfIterations` is expressed in **hours** +/// - Default value is `std::nullopt` when the key is missing +/// +/// @warning +/// - These assumptions may not be valid for all datasets. +/// - Relying on implicit defaults may lead to non-reproducible GRIB output +/// if upstream conventions change. +/// +/// @tparam MarsDict_t Type of the MARS dictionary (unused) +/// @tparam ParDict_t Type of the parameter dictionary +/// @tparam OptDict_t Type of the options dictionary (unused) +/// +/// @param[in] mars MARS dictionary (unused) +/// @param[in] par Parameter dictionary +/// @param[in] opt Options dictionary (unused) +/// +/// @return The length of time window in seconds. If `par::totalNumberOfIterations` is missing, +/// returns `std::nullopt`. +/// +/// @throws metkit::mars2grib::utils::exceptions::Mars2GribDeductionException +/// If: +/// - access to the parameter dictionary fails +/// - the retrieved value cannot be interpreted as a valid integer +/// - any unexpected error occurs during deduction +/// +/// @todo [owner: mds,dgov][scope: deduction][reason: correctness][prio: medium] +/// - Make the unit of `totalNumberOfIterations` explicit instead of assuming hours. +/// - Add explicit validation of allowed ranges and units. +/// +/// @note +/// - This deduction does not rely on any pre-existing GRIB header state. +/// - Logging intentionally emits RESOLVE/DEFAULT entries to highlight implicit assumptions. +/// + +template +std::optional resolve_TotalNumberOfIterations_opt(const MarsDict_t& mars, const ParDict_t& par, + const OptDict_t& opt) { + + + using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; + using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; + + try { + + // Big assumption here: + // - totalNumberOfIterations is in hours + if (has(par, "totalNumberOfIterations")) { + long totalNumberOfIterationsVal = get_or_throw(par, "totalNumberOfIterations"); + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`totalNumberOfIterations` resolved from input dictionaries: value='"; + logMsg += std::to_string(totalNumberOfIterationsVal); + return logMsg; + }()); + + // Success exit point + return {totalNumberOfIterationsVal}; // Convert hours to seconds + } + else { + + // Emit DEFAULT log entry + MARS2GRIB_LOG_DEFAULT([&]() { + std::string logMsg = "`totalNumberOfIterations` defaulted to MISSING (nullopt)"; + return logMsg; + }()); + + // Success exit point + return std::nullopt; + } + } + catch (...) { + + // Rethrow nested exceptions + std::throw_with_nested( + Mars2GribDeductionException("Unable to get `totalNumberOfIterations` from Par dictionary", Here())); + }; + + // Remove compiler warning + mars2gribUnreachable(); +} + +} // namespace metkit::mars2grib::backend::deductions diff --git a/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h b/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h index d18eca2f..b8542a7a 100644 --- a/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h +++ b/src/metkit/mars2grib/backend/deductions/typeOfGeneratingProcess.h @@ -94,10 +94,11 @@ namespace metkit::mars2grib::backend::deductions { /// template std::optional resolve_TypeOfGeneratingProcess_opt( - const MarsDict_t& mars, [[maybe_unused]] const ParDict_t& par, [[maybe_unused]] const OptDict_t& opt) { + const MarsDict_t& mars, const ParDict_t& par, [[maybe_unused]] const OptDict_t& opt) { using metkit::mars2grib::backend::tables::TypeOfGeneratingProcess; using metkit::mars2grib::utils::dict_traits::get_or_throw; + using metkit::mars2grib::utils::dict_traits::has; using metkit::mars2grib::utils::exceptions::Mars2GribDeductionException; // N.B. Sometimes this is overwritten by eccodes as a side effect of setting `param` @@ -142,13 +143,69 @@ std::optional resolve_TypeOfGeneratingProcess_o } else if (marsTypeVal == "fc") { - tables::TypeOfGeneratingProcess result = TypeOfGeneratingProcess::Forecast; + // Detect ensemble evidence even when MARS `type` is the generic + // `fc`. Legacy GRIB1 data (and some rewritten streams) may carry + // `type=fc` together with ensemble-describing keys; in that case + // the correct GRIB2 `typeOfGeneratingProcess` is EnsembleForecast + // (4), not Forecast (2). + // + // Signals (any of): + // - par.numberOfForecastsInEnsemble > 1 + // - par.typeOfEnsembleForecast present + // - mars.number > 0 + const bool hasEnsembleSize = + has(par, "numberOfForecastsInEnsemble") && (get_or_throw(par, "numberOfForecastsInEnsemble") > 1); + const bool hasEnsembleType = has(par, "typeOfEnsembleForecast"); + const bool hasEnsembleNumber = has(mars, "number") && (get_or_throw(mars, "number") > 0); + + const bool isEnsemble = hasEnsembleSize || hasEnsembleType || hasEnsembleNumber; + + tables::TypeOfGeneratingProcess result = + isEnsemble ? TypeOfGeneratingProcess::EnsembleForecast : TypeOfGeneratingProcess::Forecast; // Emit RESOLVE log entry MARS2GRIB_LOG_RESOLVE([&]() { std::string logMsg = "`typeOfGeneratingProcess` resolved from input dictionaries: value='"; logMsg += tables::enum2name_TypeOfGeneratingProcess_or_throw(result); logMsg += "'"; + if (isEnsemble) { + logMsg += " (type='fc' with ensemble evidence:"; + if (hasEnsembleSize) { + logMsg += " numberOfForecastsInEnsemble>1"; + } + if (hasEnsembleType) { + logMsg += " typeOfEnsembleForecast-present"; + } + if (hasEnsembleNumber) { + logMsg += " number>0"; + } + logMsg += ")"; + } + else { + logMsg += " (type='fc', no ensemble evidence)"; + } + return logMsg; + }()); + + // Success exit point + return {result}; + } + else if (marsTypeVal == "eme" || marsTypeVal == "me") { + + // 4D-Var model-error fields (eme = ensemble model errors, + // me = model errors). Generated as part of the analysis + // system; the canonical ECMWF GRIB2 value is Analysis (0). + // Without an explicit mapping here the encoder fell back to the + // GRIB sample default, which only happened to be 0 by accident. + // Grouped with eme to mirror the {4i, 4v, me, eme} grouping + // already used in significanceOfReferenceTime. + tables::TypeOfGeneratingProcess result = TypeOfGeneratingProcess::Analysis; + + // Emit RESOLVE log entry + MARS2GRIB_LOG_RESOLVE([&]() { + std::string logMsg = "`typeOfGeneratingProcess` resolved from input dictionaries: value='"; + logMsg += tables::enum2name_TypeOfGeneratingProcess_or_throw(result); + logMsg += "' (type=eme/me)"; return logMsg; }()); diff --git a/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h index 14e3ed1a..c99419eb 100644 --- a/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h +++ b/src/metkit/mars2grib/backend/sections/initializers/sectionRegistry.h @@ -77,8 +77,12 @@ template inline constexpr Entry Sec2Reg[] = { {1, &allocateTemplateNumber2<2, 1, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {15, &allocateTemplateNumber2<2, 15, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {20, &allocateTemplateNumber2<2, 20, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {24, &allocateTemplateNumber2<2, 24, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {25, &allocateTemplateNumber2<2, 25, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {36, &allocateTemplateNumber2<2, 36, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {38, &allocateTemplateNumber2<2, 38, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, + {39, &allocateTemplateNumber2<2, 39, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1000, &allocateTemplateNumber2<2, 1000, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1001, &allocateTemplateNumber2<2, 1001, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, {1002, &allocateTemplateNumber2<2, 1002, MarsDict_t, ParDict_t, OptDict_t, OutDict_t>}, diff --git a/src/metkit/mars2grib/copilot-instructions.md b/src/metkit/mars2grib/copilot-instructions.md new file mode 100644 index 00000000..69aee07e --- /dev/null +++ b/src/metkit/mars2grib/copilot-instructions.md @@ -0,0 +1,65 @@ +# Copilot Instructions for mars2grib + +These instructions apply to AI-assisted changes under `src/metkit/mars2grib`. + +## Concept Or Variant Decision + +Before implementing any modification in the mars2grib concept system, determine whether the requested behavior is a new concept or a new variant of an existing concept. + +- Use a new concept when the feature is an independent semantic axis that must be composable with other concepts. +- Use a new variant when the feature is an alternative realization inside an existing semantic axis and does not need independent composability. +- This distinction is usually not reliably deducible from code structure alone. +- If the user has not explicitly said whether the change is a new concept or a variant, ask the user before implementing. + +## Level Concept Guardrail + +The `level` concept is intentionally constrained. In the GRIB header, vertical level information is ultimately represented by these six low-level fixed-surface keys: + +- `typeOfFirstFixedSurface` +- `scaleFactorOfFirstFixedSurface` +- `scaledValueOfFirstFixedSurface` +- `typeOfSecondFixedSurface` +- `scaleFactorOfSecondFixedSurface` +- `scaledValueOfSecondFixedSurface` + +Do not set these keys directly as a shortcut or workaround. + +mars2grib encodes only official level definitions by relying on `typeOfLevel` plus, when needed, `level`, `topLevel`, `bottomLevel`, and PV-array data. Each supported `typeOfLevel` maps to a prescribed fixed-surface configuration. Some virtual `typeOfLevel` values exist because they cannot be introduced in ecCodes for backward-compatibility reasons. + +If a requested change appears to require direct writes to fixed-surface keys, do not implement that approach. Instead, add or adjust the appropriate `LevelType` variant, matcher mapping, or deduction so the level remains encoded through the official level abstraction. + + +## Documentation synchronization rule + +When working on a pull request: + +1. Determine the set of files modified by the PR. +2. From that set, consider only files under: + - `src/metkit/mars2grib` + - Changes under `tests/mars2grib` require documentation updates only if they affect documented public behaviour + +3. For each of those files: + - Verify that the related documentation is present in the concrete doc locations for mars2grib, including (where applicable): + - `src/metkit/mars2grib/docs/**` + - Any module-level `.md` files or Doxygen pages associated with the modified code + - Verify that the documentation in these locations is up to date with the code changes + - If documentation is missing or outdated in these locations, propose the required updates + +4. Do not request documentation changes for files outside these paths. + +## Definition of “documentation in sync” + +Documentation must: +- Describe the current public behavior and interfaces +- Reflect any new parameters, options, or outputs +- Remove references to deleted functionality + +## PR review behavior + +During PR reviews: +- Explicitly list the impacted files in the two target directories +- State whether documentation is: + - ✅ in sync + - ❌ missing + - ❌ outdated +- Suggest concrete doc patches when needed, referencing the specific lines or sections that require updates. \ No newline at end of file diff --git a/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in b/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in index 001b3d7b..f0df1a50 100644 --- a/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in +++ b/src/metkit/mars2grib/docs/doxygen/mars2grib.config.in @@ -1155,6 +1155,7 @@ INPUT = \@MARS2GRIB_SOURCE_DIRECTORY@/utils/configConverter.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/generatingProcessIdentifier.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/offsetToEndOf4DvarWindow.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/numberOfForecastsInEnsemble.h \ +@MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/numberOfFrequencies.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/type.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/deductions/satelliteNumber.h \ @MARS2GRIB_SOURCE_DIRECTORY@/backend/checks/matchDataRepresentationTemplateNumber.h \ diff --git a/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md b/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md index 8cc63b95..09d72877 100644 --- a/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md +++ b/src/metkit/mars2grib/docs/doxygen/mars2grib_concepts.md @@ -32,6 +32,40 @@ Concept::Variant Each `(Concept, Variant)` pair represents a distinct semantic realization and is treated as an independent entity by the Encoder. +@subsection concepts_new_concept_or_variant New Concept or New Variant + +Before changing the concept system, decide whether the behavior belongs to a new +concept or to a new variant of an existing concept. + +- A new concept is appropriate for an independent semantic axis that must be + composable with other concepts. +- A new variant is appropriate for an alternative realization inside an existing + semantic axis when independent composability is not required. + +This is a domain decision and is not always apparent from the code structure. + +@section concepts_level_guardrail Level Concept Guardrail + +The `level` concept deliberately hides the raw fixed-surface representation used +by GRIB. GRIB vertical levels are ultimately represented by: + +- `typeOfFirstFixedSurface` +- `scaleFactorOfFirstFixedSurface` +- `scaledValueOfFirstFixedSurface` +- `typeOfSecondFixedSurface` +- `scaleFactorOfSecondFixedSurface` +- `scaledValueOfSecondFixedSurface` + +These keys must not be set directly by mars2grib concept changes. Although many +combinations are technically possible, most are not meaningful ECMWF levels. + +Level encoding must go through the official abstraction: `typeOfLevel` plus, when +needed, `level`, `topLevel`, `bottomLevel`, and PV-array data. Each supported +`typeOfLevel` corresponds to a `LevelType` variant or to a small number of +virtual type-of-level values maintained in mars2grib for backward-compatibility +reasons. If new level behavior is required, update the `LevelType` variant, +matcher mapping, or deduction instead of writing fixed-surface keys directly. + @section concepts_registration_value Registration Value The value associated with each `Concept::Variant` key is a **dense, fixed-size @@ -132,4 +166,3 @@ Concepts intentionally do not: Concepts are purely declarative, statically registered contributors to the encoding process. - diff --git a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h index 16fcb994..8e48ca43 100644 --- a/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h +++ b/src/metkit/mars2grib/frontend/resolution/section-recipes/impl/section2Recipes.h @@ -32,11 +32,25 @@ 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, Select, - Select + Select + >(); + +// Model-error products +inline const Recipe S2_R25 = + make_recipe<25, + Select, + Select >(); // Analysis-related products @@ -46,6 +60,30 @@ inline const Recipe S2_R36 = Select >(); +// Brightness temperature satellite products +inline const Recipe S2_R37 = + make_recipe<37, + 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 +117,13 @@ inline const Recipes Section2Recipes{ 2, std::vector{ &S2_R1, &S2_R15, + &S2_R20, &S2_R24, + &S2_R25, &S2_R36, + &S2_R37, + &S2_R38, + &S2_R39, &S2_R1001, &S2_R1002 }