diff --git a/docs/design/SPECIFICATION_TECHNIQUE_FINALE_POPS_ARCHITECTURE.md b/docs/design/SPECIFICATION_TECHNIQUE_FINALE_POPS_ARCHITECTURE.md index e8bb1baf1..d279bfee2 100644 --- a/docs/design/SPECIFICATION_TECHNIQUE_FINALE_POPS_ARCHITECTURE.md +++ b/docs/design/SPECIFICATION_TECHNIQUE_FINALE_POPS_ARCHITECTURE.md @@ -705,9 +705,13 @@ doivent couvrir exactement la hiérarchie. Le provider natif livré matérialise le coeur maillage/stockage en 2D et ses kernels de transfert, correction conservative et sous-cyclage AMR exigent un ratio de transition égal à 2. La correction -coarse/fine reste l'unique ledger de flux détenu par PoPS : aucune interface externe `Reflux` -n'existe, car déléguer ce dépôt créerait une seconde autorité conservative. Une autre dimension ou un autre -ratio est refusé pendant la résolution ou le bind avec les capacités observées. Le coeur de +coarse/fine reste l'unique ledger de flux détenu par PoPS. L'interface native `Reflux` ne peut +déléguer qu'un kernel local et non collectif : PoPS lui fournit les flux coarse/fine déjà intégrés +dans le temps et ramenés sur la même face coarse ; le kernel écrit la correction locale +`side * (fine - coarse) / dx`. PoPS conserve exclusivement la topologie d'interface, le ledger, la +réduction MPI, la transaction et l'application à l'état. Un provider `Reflux` ne devient donc jamais +une seconde autorité conservative. Une autre dimension ou un autre ratio est refusé pendant la +résolution ou le bind avec les capacités observées. Le coeur de planification ne normalise jamais la demande vers ce sous-ensemble. Défensivement, `AmrProgramContext` revalide aussi chaque transition à sa construction et refuse un ratio différent de 2 avant le premier pas : cette limite appartient au provider natif reflux/average-down installé, @@ -1405,14 +1409,23 @@ paramètres, interfaces, requirements, capabilities, effets, layouts, clocks, d restart et points d'entrée. Le même catalogue génère les IDs et tables C/POD versionnées des interfaces natives (flux numérique, -ghost boundary, closure de champ, tagging, clustering, transfert, solveur de champ, writer et -topologie de champ). Le reflux conservatif reste une autorité interne pilotée par le flux ledger ; -aucune table externe `Reflux` n'est annoncée. Chaque famille possède sa propre version d'interface, indépendante de la version +ghost boundary, closure de champ, tagging, clustering, transfert, kernel local de reflux, solveur de +champ, writer et topologie de champ). Le reflux conservatif complet reste une autorité interne +pilotée par le flux ledger ; la table externe `Reflux` ne couvre que la transformation locale, +non collective, de flux intégrés en correction non appliquée. Chaque famille possède sa propre version d'interface, indépendante de la version du protocole enveloppe. Le loader authentifie identité sémantique, manifest, digest du catalogue, taille/header de table et opérations requises avant de conserver le handle de bibliothèque. Les tables sont résolues une fois à l'installation ; aucun `dlsym`, nom de classe ou dispatch Python n'entre dans une boucle de cellules. +Le contrat `Reflux` v1 est volontairement livré avant son branchement dans +`PreparedAmrProgramRefluxTransition` : catalogue, manifest, loader et consumer typé peuvent qualifier +un conformer, mais le runtime AMR continue d'utiliser son kernel interne tant qu'un adaptateur préparé +ne peut pas fournir les vues locales sans dupliquer le ledger ni transférer l'autorité collective. Une +configuration AMR ne prétend donc pas encore avoir sélectionné un provider `Reflux` externe. Cette +première qualification est limitée à la cible 2D, `float64`, CPU déjà admise par le loader de +composants ; elle ne constitue pas une promesse GPU. + Les champs sémantiques inconnus, capacités sans preuve, collisions d'identité et entry points manquants sont refusés. Un vieux manifest n'est pas « réparé » silencieusement. diff --git a/include/pops/runtime/config/generated_component_abi.hpp b/include/pops/runtime/config/generated_component_abi.hpp index 2403ce736..bc042a6f9 100644 --- a/include/pops/runtime/config/generated_component_abi.hpp +++ b/include/pops/runtime/config/generated_component_abi.hpp @@ -15,7 +15,7 @@ extern "C" { #endif #define POPS_COMPONENT_API_SYMBOL_V1 "pops_component_interface_v1" -#define POPS_COMPONENT_CATALOG_SHA256_V1 "84c68fcee96663f71e7e7fa7589ec1ddee0d1037e741a678e4afd83c9749620a" +#define POPS_COMPONENT_CATALOG_SHA256_V1 "5c991781aadafd3165dccb4642086c8b20fbd4a83ee8e462f1e29078ecb0d1c4" #define POPS_COMPONENT_PROTOCOL_ABI_V1 1u #define POPS_COMPONENT_COMMON_ABI_V1 1u @@ -26,6 +26,7 @@ typedef enum PopsNativeInterfaceIdV1 { POPS_NATIVE_INTERFACE_TAGGER_V2 = 3, POPS_NATIVE_INTERFACE_CLUSTERING_V1 = 4, POPS_NATIVE_INTERFACE_TRANSFER_V1 = 5, + POPS_NATIVE_INTERFACE_REFLUX_V1 = 6, POPS_NATIVE_INTERFACE_FIELD_SOLVER_V2 = 7, POPS_NATIVE_INTERFACE_WRITER_V1 = 8, POPS_NATIVE_INTERFACE_FIELD_TOPOLOGY_V2 = 9, @@ -446,6 +447,41 @@ typedef struct PopsTransferApiV1 { PopsTransferApplyFnV1 apply; } PopsTransferApiV1; +// Reflux providers are patch-local numerical kernels only. PoPS retains sole ownership of the +// time-integrated flux ledger, interface topology, MPI reduction, transaction and state update. +// Each face contains coarse/fine fluxes already integrated in time and averaged onto the same +// coarse face. The provider writes, but never applies, side*(fine-coarse)/dx into `correction`. +typedef enum PopsRefluxFaceSideV1 { + POPS_REFLUX_FACE_LOW_V1 = -1, + POPS_REFLUX_FACE_HIGH_V1 = 1 +} PopsRefluxFaceSideV1; +typedef struct PopsRefluxFaceV1 { + uint32_t struct_size; + const char* interface_identity; + int32_t axis; + PopsRefluxFaceSideV1 side; + double inverse_coarse_cell_spacing; + PopsConstFieldViewV1 coarse_integrated_flux; + PopsConstFieldViewV1 fine_integrated_flux; + PopsFieldViewV1 correction; +} PopsRefluxFaceV1; +typedef struct PopsRefluxRequestV1 { + uint32_t struct_size; + const char* transition_identity; + int32_t parent_level; + int32_t child_level; + size_t face_count; + const PopsRefluxFaceV1* faces; + PopsLogicalTimeV1 logical_time; + PopsExecutionContextV1 execution; +} PopsRefluxRequestV1; +typedef int32_t (*PopsRefluxApplyInterfaceBatchFnV1)( + void*, const PopsRefluxRequestV1*, PopsComponentStatusV1*); +typedef struct PopsRefluxApiV1 { + PopsComponentTableHeaderV1 header; + PopsRefluxApplyInterfaceBatchFnV1 apply_interface_batch; +} PopsRefluxApiV1; + typedef struct PopsFieldPatchMetadataV1 { uint32_t struct_size; size_t global_patch_index; @@ -724,6 +760,7 @@ inline constexpr size_t generated_native_interface_table_size( case POPS_NATIVE_INTERFACE_TAGGER_V2: return sizeof(PopsTaggerApiV2); case POPS_NATIVE_INTERFACE_CLUSTERING_V1: return sizeof(PopsClusteringApiV1); case POPS_NATIVE_INTERFACE_TRANSFER_V1: return sizeof(PopsTransferApiV1); + case POPS_NATIVE_INTERFACE_REFLUX_V1: return sizeof(PopsRefluxApiV1); case POPS_NATIVE_INTERFACE_FIELD_SOLVER_V2: return sizeof(PopsFieldSolverApiV2); case POPS_NATIVE_INTERFACE_WRITER_V1: return sizeof(PopsWriterApiV1); case POPS_NATIVE_INTERFACE_FIELD_TOPOLOGY_V2: return sizeof(PopsFieldTopologyApiV2); @@ -739,12 +776,71 @@ inline constexpr const char* generated_native_interface_table_name( case POPS_NATIVE_INTERFACE_TAGGER_V2: return "PopsTaggerApiV2"; case POPS_NATIVE_INTERFACE_CLUSTERING_V1: return "PopsClusteringApiV1"; case POPS_NATIVE_INTERFACE_TRANSFER_V1: return "PopsTransferApiV1"; + case POPS_NATIVE_INTERFACE_REFLUX_V1: return "PopsRefluxApiV1"; case POPS_NATIVE_INTERFACE_FIELD_SOLVER_V2: return "PopsFieldSolverApiV2"; case POPS_NATIVE_INTERFACE_WRITER_V1: return "PopsWriterApiV1"; case POPS_NATIVE_INTERFACE_FIELD_TOPOLOGY_V2: return "PopsFieldTopologyApiV2"; } return nullptr; } +inline bool generated_native_interface_table_is_complete( + PopsNativeInterfaceIdV1 id, const void* table, size_t table_size) noexcept { + if (table == nullptr) + return false; + switch (id) { + case POPS_NATIVE_INTERFACE_NUMERICAL_FLUX_V1: { + if (table_size < sizeof(PopsNumericalFluxApiV1)) return false; + const auto* api = static_cast(table); + return api->evaluate_faces != nullptr; + } + case POPS_NATIVE_INTERFACE_GHOST_BOUNDARY_V1: { + if (table_size < sizeof(PopsGhostBoundaryApiV1)) return false; + const auto* api = static_cast(table); + return api->apply_region_batch != nullptr; + } + case POPS_NATIVE_INTERFACE_FIELD_BOUNDARY_CLOSURE_V1: { + if (table_size < sizeof(PopsFieldBoundaryClosureApiV1)) return false; + const auto* api = static_cast(table); + return api->residual != nullptr && api->jvp != nullptr; + } + case POPS_NATIVE_INTERFACE_TAGGER_V2: { + if (table_size < sizeof(PopsTaggerApiV2)) return false; + const auto* api = static_cast(table); + return api->tag_batch != nullptr; + } + case POPS_NATIVE_INTERFACE_CLUSTERING_V1: { + if (table_size < sizeof(PopsClusteringApiV1)) return false; + const auto* api = static_cast(table); + return api->cluster != nullptr; + } + case POPS_NATIVE_INTERFACE_TRANSFER_V1: { + if (table_size < sizeof(PopsTransferApiV1)) return false; + const auto* api = static_cast(table); + return api->apply != nullptr; + } + case POPS_NATIVE_INTERFACE_REFLUX_V1: { + if (table_size < sizeof(PopsRefluxApiV1)) return false; + const auto* api = static_cast(table); + return api->apply_interface_batch != nullptr; + } + case POPS_NATIVE_INTERFACE_FIELD_SOLVER_V2: { + if (table_size < sizeof(PopsFieldSolverApiV2)) return false; + const auto* api = static_cast(table); + return api->solve != nullptr; + } + case POPS_NATIVE_INTERFACE_WRITER_V1: { + if (table_size < sizeof(PopsWriterApiV1)) return false; + const auto* api = static_cast(table); + return api->verify != nullptr && api->publish != nullptr && api->discard != nullptr && api->rollback != nullptr; + } + case POPS_NATIVE_INTERFACE_FIELD_TOPOLOGY_V2: { + if (table_size < sizeof(PopsFieldTopologyApiV2)) return false; + const auto* api = static_cast(table); + return api->prepare_topology != nullptr; + } + } + return false; +} } // namespace pops::component #endif // clang-format on diff --git a/include/pops/runtime/config/generated_component_catalog.hpp b/include/pops/runtime/config/generated_component_catalog.hpp index 2c4cb1c08..b1c996aa2 100644 --- a/include/pops/runtime/config/generated_component_catalog.hpp +++ b/include/pops/runtime/config/generated_component_catalog.hpp @@ -301,9 +301,9 @@ inline constexpr int kComponentCatalogSchemaVersion = 1; inline constexpr int kComponentManifestSchemaVersion = 2; inline constexpr int kRouteRegistryVersion = 2; inline constexpr int kCapabilityVocabularyVersion = 2; -inline constexpr const char* kComponentCatalogSha256 = "84c68fcee96663f71e7e7fa7589ec1ddee0d1037e741a678e4afd83c9749620a"; -inline constexpr const char* kComponentCatalogSemanticSha256 = "c0e14d4a3dd082612d052b2ce293e17de712c6916e39f430d0ff311ed0f24ef8"; -inline constexpr const char* kRouteRegistrySignature = "v2:c0e14d4a3dd082612d052b2ce293e17de712c6916e39f430d0ff311ed0f24ef8"; +inline constexpr const char* kComponentCatalogSha256 = "5c991781aadafd3165dccb4642086c8b20fbd4a83ee8e462f1e29078ecb0d1c4"; +inline constexpr const char* kComponentCatalogSemanticSha256 = "b29e5d1d811234bd55bfce83ffdf810ed95868392d1ac1da5644f8886620e129"; +inline constexpr const char* kRouteRegistrySignature = "v2:b29e5d1d811234bd55bfce83ffdf810ed95868392d1ac1da5644f8886620e129"; inline constexpr const char* kComponentManifestSemanticFields[] = { "schema_version", "uri", diff --git a/include/pops/runtime/config/generated_route_accessors.inc b/include/pops/runtime/config/generated_route_accessors.inc index 886506256..8d847fc57 100644 --- a/include/pops/runtime/config/generated_route_accessors.inc +++ b/include/pops/runtime/config/generated_route_accessors.inc @@ -1,4 +1,4 @@ -// Generated from component catalog 84c68fcee96663f71e7e7fa7589ec1ddee0d1037e741a678e4afd83c9749620a; DO NOT EDIT. +// Generated from component catalog 5c991781aadafd3165dccb4642086c8b20fbd4a83ee8e462f1e29078ecb0d1c4; DO NOT EDIT. // POPS_DEFINE_ROUTE_ACCESSORS must be defined by the including behavior header. POPS_DEFINE_ROUTE_ACCESSORS(riemann, RiemannRouteId, kRiemannRoutes, kRiemann); POPS_DEFINE_ROUTE_ACCESSORS(limiter, LimiterRouteId, kLimiterRoutes, kLimiter); diff --git a/include/pops/runtime/dynamic/component_consumers.hpp b/include/pops/runtime/dynamic/component_consumers.hpp index ccdaeb506..586eae878 100644 --- a/include/pops/runtime/dynamic/component_consumers.hpp +++ b/include/pops/runtime/dynamic/component_consumers.hpp @@ -596,6 +596,81 @@ inline int apply_transfer(const PopsTransferApiV1& api, void* state, return api.apply(state, &request, &status); } +template +inline bool same_reflux_face_shape(const Left& left, const Right& right) { + if (left.dimension != right.dimension || left.component_count != right.component_count || + left.scalar_type != right.scalar_type || left.memory_space != right.memory_space) + return false; + for (std::int32_t axis = 0; axis < 3; ++axis) + if (left.extents[axis] != right.extents[axis] || + left.ghost_lower[axis] != right.ghost_lower[axis] || + left.ghost_upper[axis] != right.ghost_upper[axis]) + return false; + return true; +} + +inline int apply_reflux_interface_batch(const PopsRefluxApiV1& api, void* state, + const PopsRefluxRequestV1& request, + PopsComponentStatusV1& status) { + require_operation(api.apply_interface_batch != nullptr, "apply_interface_batch"); + if (request.struct_size < sizeof(PopsRefluxRequestV1) || + !component_text(request.transition_identity) || request.parent_level < 0 || + request.child_level != request.parent_level + 1 || request.face_count == 0 || + request.faces == nullptr || request.logical_time.level != request.parent_level) + throw std::invalid_argument("reflux request is incomplete"); + validate_logical_time(request.logical_time); + validate_noncollective_execution_context(request.execution); + + std::unordered_set identities; + for (std::size_t index = 0; index < request.face_count; ++index) { + const auto& face = request.faces[index]; + if (face.struct_size < sizeof(PopsRefluxFaceV1) || !component_text(face.interface_identity) || + !identities.insert(face.interface_identity).second || face.axis < 0 || face.axis >= 2 || + (face.side != POPS_REFLUX_FACE_LOW_V1 && face.side != POPS_REFLUX_FACE_HIGH_V1) || + !std::isfinite(face.inverse_coarse_cell_spacing) || face.inverse_coarse_cell_spacing <= 0.0) + throw std::invalid_argument("reflux face descriptor is incomplete"); + + validate_execution_field(request.execution, face.coarse_integrated_flux, + "reflux coarse integrated flux"); + validate_execution_field(request.execution, face.fine_integrated_flux, + "reflux fine integrated flux"); + validate_execution_field(request.execution, face.correction, "reflux correction"); + const auto centering_axis = 1u << static_cast(face.axis); + if (face.coarse_integrated_flux.centering != POPS_FIELD_CENTERING_FACE_V1 || + face.fine_integrated_flux.centering != POPS_FIELD_CENTERING_FACE_V1 || + face.coarse_integrated_flux.centering_axes != centering_axis || + face.fine_integrated_flux.centering_axes != centering_axis || + face.correction.centering != POPS_FIELD_CENTERING_CELL_V1 || + face.correction.centering_axes != 0 || + face.coarse_integrated_flux.ownership != POPS_FIELD_OWNERSHIP_RUNTIME_BORROWED_V1 || + face.fine_integrated_flux.ownership != POPS_FIELD_OWNERSHIP_RUNTIME_BORROWED_V1 || + face.correction.ownership != POPS_FIELD_OWNERSHIP_RUNTIME_BORROWED_V1 || + !same_reflux_face_shape(face.coarse_integrated_flux, face.fine_integrated_flux) || + !same_reflux_face_shape(face.coarse_integrated_flux, face.correction) || + face.coarse_integrated_flux.extents[face.axis] != 1 || + std::string(face.coarse_integrated_flux.layout_identity) != + face.correction.layout_identity || + std::string(face.coarse_integrated_flux.patch_identity) != face.correction.patch_identity) + throw std::invalid_argument( + "reflux face fluxes and correction disagree on shape, centering or ownership"); + for (std::int32_t axis = 0; axis < face.coarse_integrated_flux.dimension; ++axis) + if (face.coarse_integrated_flux.ghost_lower[axis] != 0 || + face.coarse_integrated_flux.ghost_upper[axis] != 0) + throw std::invalid_argument("reflux face views cannot carry ghost cells"); + } + + status = unwritten_component_status(); + const int code = api.apply_interface_batch(state, &request, &status); + if (!component_status_is_well_formed(status)) + throw std::runtime_error("native Reflux component returned an invalid status"); + if ((code == 0) != (status.code == 0) || + (code == 0 && status.action != POPS_COMPONENT_CONTINUE_V1) || + (code != 0 && status.action == POPS_COMPONENT_CONTINUE_V1) || + (code != 0 && !component_text(status.reason))) + throw std::runtime_error("native Reflux component returned an inconsistent outcome"); + return code; +} + inline std::string writer_geometry_key(const char* layout, std::int32_t level) { return std::string(layout) + "\n" + std::to_string(level); } diff --git a/include/pops/runtime/dynamic/component_loader.hpp b/include/pops/runtime/dynamic/component_loader.hpp index 0cb9ffc46..ce9483ae0 100644 --- a/include/pops/runtime/dynamic/component_loader.hpp +++ b/include/pops/runtime/dynamic/component_loader.hpp @@ -392,6 +392,9 @@ class LoadedComponent final { if ((header->prepare == nullptr) != (header->destroy == nullptr)) throw std::runtime_error( "native component interface prepare/destroy callbacks must be paired"); + if (!generated_native_interface_table_is_complete(row.interface_id, row.table, + row.table_size)) + throw std::runtime_error("native component interface table misses a required operation"); } for (const auto& required : expected.interfaces) { bool found = false; diff --git a/python/bindings/core/init/generated_component_invokers.inc b/python/bindings/core/init/generated_component_invokers.inc index cc1600dc8..d9ddca9d6 100644 --- a/python/bindings/core/init/generated_component_invokers.inc +++ b/python/bindings/core/init/generated_component_invokers.inc @@ -1,4 +1,4 @@ -// Generated by scripts/generate_component_catalog.py from catalog 84c68fcee96663f71e7e7fa7589ec1ddee0d1037e741a678e4afd83c9749620a; DO NOT EDIT. +// Generated by scripts/generate_component_catalog.py from catalog 5c991781aadafd3165dccb4642086c8b20fbd4a83ee8e462f1e29078ecb0d1c4; DO NOT EDIT. // This file is the sole Python/native request marshaller. init_component_loader.cpp only registers it. #include diff --git a/python/pops/_generated_component_interfaces.py b/python/pops/_generated_component_interfaces.py index 66d02552c..2e99f6ef2 100644 --- a/python/pops/_generated_component_interfaces.py +++ b/python/pops/_generated_component_interfaces.py @@ -3,8 +3,8 @@ NATIVE_COMPONENT_ABI_VERSION = 1 NATIVE_COMPONENT_COMMON_ABI_VERSION = 1 -NATIVE_COMPONENT_CATALOG_SHA256 = '84c68fcee96663f71e7e7fa7589ec1ddee0d1037e741a678e4afd83c9749620a' -NATIVE_COMPONENT_CATALOG_SEMANTIC_SHA256 = 'c0e14d4a3dd082612d052b2ce293e17de712c6916e39f430d0ff311ed0f24ef8' +NATIVE_COMPONENT_CATALOG_SHA256 = '5c991781aadafd3165dccb4642086c8b20fbd4a83ee8e462f1e29078ecb0d1c4' +NATIVE_COMPONENT_CATALOG_SEMANTIC_SHA256 = 'b29e5d1d811234bd55bfce83ffdf810ed95868392d1ac1da5644f8886620e129' NATIVE_TAGGING_PROGRAM_ABI = {'version': 1, 'execution_modes': {'native_backend': 1, 'host': 2}, 'collective_scopes': {'none': 0}, @@ -72,6 +72,14 @@ 'hot_path': True, 'facets': ('stencil', 'lowering'), 'operations': ('apply',)}, + {'id': 6, + 'name': 'reflux', + 'uri': 'pops://interfaces/reflux', + 'version': 1, + 'cpp_table': 'PopsRefluxApiV1', + 'hot_path': True, + 'facets': ('stencil', 'lowering', 'effects'), + 'operations': ('apply_interface_batch',)}, {'id': 7, 'name': 'field_solver', 'uri': 'pops://interfaces/field-solver', diff --git a/python/pops/interfaces.py b/python/pops/interfaces.py index fdb7533f2..4662da304 100644 --- a/python/pops/interfaces.py +++ b/python/pops/interfaces.py @@ -170,6 +170,7 @@ def resolve(name: str) -> ComponentInterface: Tagger = resolve("tagger") Clustering = resolve("clustering") Transfer = resolve("transfer") +Reflux = resolve("reflux") FieldSolver = resolve("field_solver") Writer = resolve("writer") FieldTopology = resolve("field_topology") @@ -177,6 +178,6 @@ def resolve(name: str) -> ComponentInterface: __all__ = [ "ComponentInterface", "resolve", "NumericalFlux", "GhostBoundary", - "FieldBoundaryClosure", "Tagger", "Clustering", "Transfer", + "FieldBoundaryClosure", "Tagger", "Clustering", "Transfer", "Reflux", "FieldSolver", "Writer", "FieldTopology", ] diff --git a/python/pops/model/_generated_component_schema.py b/python/pops/model/_generated_component_schema.py index 84612fb23..7c829113c 100644 --- a/python/pops/model/_generated_component_schema.py +++ b/python/pops/model/_generated_component_schema.py @@ -3,8 +3,8 @@ COMPONENT_CATALOG_SCHEMA_VERSION = 1 COMPONENT_MANIFEST_SCHEMA_VERSION = 2 -COMPONENT_CATALOG_SHA256 = '84c68fcee96663f71e7e7fa7589ec1ddee0d1037e741a678e4afd83c9749620a' -COMPONENT_CATALOG_SEMANTIC_SHA256 = 'c0e14d4a3dd082612d052b2ce293e17de712c6916e39f430d0ff311ed0f24ef8' +COMPONENT_CATALOG_SHA256 = '5c991781aadafd3165dccb4642086c8b20fbd4a83ee8e462f1e29078ecb0d1c4' +COMPONENT_CATALOG_SEMANTIC_SHA256 = 'b29e5d1d811234bd55bfce83ffdf810ed95868392d1ac1da5644f8886620e129' COMPONENT_INTERFACE_SPECS = ({'name': 'requirement', 'method': 'requirements', 'required_args': 0}, {'name': 'lowering', 'method': 'lower', 'required_args': 1}, {'name': 'stencil', 'method': 'stencil', 'required_args': 0}, diff --git a/python/pops/runtime/_generated_component_routes.py b/python/pops/runtime/_generated_component_routes.py index 41b38a9e8..787947838 100644 --- a/python/pops/runtime/_generated_component_routes.py +++ b/python/pops/runtime/_generated_component_routes.py @@ -9,11 +9,11 @@ CAPABILITY_VOCAB_VERSION = 2 -COMPONENT_CATALOG_SHA256 = '84c68fcee96663f71e7e7fa7589ec1ddee0d1037e741a678e4afd83c9749620a' +COMPONENT_CATALOG_SHA256 = '5c991781aadafd3165dccb4642086c8b20fbd4a83ee8e462f1e29078ecb0d1c4' -COMPONENT_CATALOG_SEMANTIC_SHA256 = 'c0e14d4a3dd082612d052b2ce293e17de712c6916e39f430d0ff311ed0f24ef8' +COMPONENT_CATALOG_SEMANTIC_SHA256 = 'b29e5d1d811234bd55bfce83ffdf810ed95868392d1ac1da5644f8886620e129' -ROUTE_REGISTRY_SIGNATURE = 'v2:c0e14d4a3dd082612d052b2ce293e17de712c6916e39f430d0ff311ed0f24ef8' +ROUTE_REGISTRY_SIGNATURE = 'v2:b29e5d1d811234bd55bfce83ffdf810ed95868392d1ac1da5644f8886620e129' ROUTE_TABLES = {'riemann': (('rusanov', 'pops::RusanovFlux', diff --git a/schemas/component_catalog.v2.json b/schemas/component_catalog.v2.json index c48a03d1b..178196ea7 100644 --- a/schemas/component_catalog.v2.json +++ b/schemas/component_catalog.v2.json @@ -158,6 +158,16 @@ "facets": ["stencil", "lowering"], "operations": ["apply"] }, + { + "id": 6, + "name": "reflux", + "uri": "pops://interfaces/reflux", + "version": 1, + "cpp_table": "PopsRefluxApiV1", + "hot_path": true, + "facets": ["stencil", "lowering", "effects"], + "operations": ["apply_interface_batch"] + }, { "id": 7, "name": "field_solver", diff --git a/scripts/generate_component_catalog.py b/scripts/generate_component_catalog.py index 655377024..8a6a0cb75 100644 --- a/scripts/generate_component_catalog.py +++ b/scripts/generate_component_catalog.py @@ -649,6 +649,22 @@ def _render_component_abi(catalog: dict[str, Any], digest: str) -> str: % (row["name"].upper(), row["version"], row["cpp_table"]) for row in catalog["native_interface_abis"] ) + table_complete_rows = "\n".join( + """ case POPS_NATIVE_INTERFACE_%s_V%d: { + if (table_size < sizeof(%s)) return false; + const auto* api = static_cast(table); + return %s; + }""" + % ( + row["name"].upper(), + row["version"], + row["cpp_table"], + row["cpp_table"], + " && ".join("api->%s != nullptr" % operation + for operation in row["operations"]), + ) + for row in catalog["native_interface_abis"] + ) return f'''#pragma once // Generated by scripts/generate_component_catalog.py; DO NOT EDIT. @@ -1081,6 +1097,41 @@ def _render_component_abi(catalog: dict[str, Any], digest: str) -> str: PopsTransferApplyFnV1 apply; }} PopsTransferApiV1; +// Reflux providers are patch-local numerical kernels only. PoPS retains sole ownership of the +// time-integrated flux ledger, interface topology, MPI reduction, transaction and state update. +// Each face contains coarse/fine fluxes already integrated in time and averaged onto the same +// coarse face. The provider writes, but never applies, side*(fine-coarse)/dx into `correction`. +typedef enum PopsRefluxFaceSideV1 {{ + POPS_REFLUX_FACE_LOW_V1 = -1, + POPS_REFLUX_FACE_HIGH_V1 = 1 +}} PopsRefluxFaceSideV1; +typedef struct PopsRefluxFaceV1 {{ + uint32_t struct_size; + const char* interface_identity; + int32_t axis; + PopsRefluxFaceSideV1 side; + double inverse_coarse_cell_spacing; + PopsConstFieldViewV1 coarse_integrated_flux; + PopsConstFieldViewV1 fine_integrated_flux; + PopsFieldViewV1 correction; +}} PopsRefluxFaceV1; +typedef struct PopsRefluxRequestV1 {{ + uint32_t struct_size; + const char* transition_identity; + int32_t parent_level; + int32_t child_level; + size_t face_count; + const PopsRefluxFaceV1* faces; + PopsLogicalTimeV1 logical_time; + PopsExecutionContextV1 execution; +}} PopsRefluxRequestV1; +typedef int32_t (*PopsRefluxApplyInterfaceBatchFnV1)( + void*, const PopsRefluxRequestV1*, PopsComponentStatusV1*); +typedef struct PopsRefluxApiV1 {{ + PopsComponentTableHeaderV1 header; + PopsRefluxApplyInterfaceBatchFnV1 apply_interface_batch; +}} PopsRefluxApiV1; + typedef struct PopsFieldPatchMetadataV1 {{ uint32_t struct_size; size_t global_patch_index; @@ -1364,6 +1415,15 @@ def _render_component_abi(catalog: dict[str, Any], digest: str) -> str: }} return nullptr; }} +inline bool generated_native_interface_table_is_complete( + PopsNativeInterfaceIdV1 id, const void* table, size_t table_size) noexcept {{ + if (table == nullptr) + return false; + switch (id) {{ +{table_complete_rows} + }} + return false; +}} }} // namespace pops::component #endif // clang-format on diff --git a/tests/cpp/unit/runtime/test_component_interfaces.cpp b/tests/cpp/unit/runtime/test_component_interfaces.cpp index 717c42601..57d4cc2ff 100644 --- a/tests/cpp/unit/runtime/test_component_interfaces.cpp +++ b/tests/cpp/unit/runtime/test_component_interfaces.cpp @@ -53,6 +53,12 @@ struct TransferComponent { std::string restart() const { return "stateless"; } }; +struct RefluxComponent { + int stencil() const { return 1; } + std::string lower(Context&) const { return "integrated-interface-correction"; } + std::vector effects() const { return {"local-correction"}; } +}; + struct SolverComponent { pops::component::EvaluationOutcome evaluate(Context&) const { return pops::component::EvaluationOutcome::reject("non-converged"); @@ -89,6 +95,9 @@ static_assert(pops::component::Lowering); static_assert(pops::component::Effects); static_assert(pops::component::Stencil); static_assert(pops::component::Restart); +static_assert(pops::component::Stencil); +static_assert(pops::component::Lowering); +static_assert(pops::component::Effects); static_assert(pops::component::FallibleEvaluation); static_assert(pops::component::Restart); static_assert(pops::component::Format); @@ -731,6 +740,78 @@ TEST(ComponentInterfaces, ExactAbiConsumersExecuteEveryClosedScientificFamily) { EXPECT_THROW(pops::component::apply_transfer(transfer_api, nullptr, wrong_transfer_shape, status), std::invalid_argument); + std::array coarse_integrated_flux{1.0, 2.0}; + std::array fine_integrated_flux{3.0, 6.0}; + std::array reflux_correction{}; + PopsRefluxApiV1 reflux_api{ + abi_header(sizeof(PopsRefluxApiV1), POPS_NATIVE_INTERFACE_REFLUX_V1), + +[](void*, const PopsRefluxRequestV1* request, PopsComponentStatusV1* result) { + for (std::size_t face_index = 0; face_index < request->face_count; ++face_index) { + const auto& face = request->faces[face_index]; + const auto* coarse = static_cast(face.coarse_integrated_flux.data); + const auto* fine = static_cast(face.fine_integrated_flux.data); + auto* correction = static_cast(face.correction.data); + const std::size_t points = + pops::component::field_point_count(face.coarse_integrated_flux); + for (std::size_t point = 0; point < points; ++point) + correction[point] = static_cast(face.side) * (fine[point] - coarse[point]) * + face.inverse_coarse_cell_spacing; + } + *result = ok_status(); + return 0; + }}; + auto coarse_face = abi::const_field_view(coarse_integrated_flux.data(), 1, 2, 1, "parent::layout", + "parent::patch"); + coarse_face.centering = POPS_FIELD_CENTERING_FACE_V1; + coarse_face.centering_axes = 1u; + auto fine_face = + abi::const_field_view(fine_integrated_flux.data(), 1, 2, 1, "child::layout", "child::patch"); + fine_face.centering = POPS_FIELD_CENTERING_FACE_V1; + fine_face.centering_axes = 1u; + PopsRefluxFaceV1 reflux_face{ + sizeof(PopsRefluxFaceV1), + "transition::0-to-1/x-low", + 0, + POPS_REFLUX_FACE_LOW_V1, + 2.0, + coarse_face, + fine_face, + abi::field_view(reflux_correction.data(), 1, 2, 1, "parent::layout", "parent::patch")}; + PopsRefluxRequestV1 reflux_request{sizeof(PopsRefluxRequestV1), + "transition::0-to-1", + 0, + 1, + 1, + &reflux_face, + abi::logical_time(), + abi::noncollective_host_execution_context()}; + EXPECT_TRUE(pops::component::generated_native_interface_table_is_complete( + POPS_NATIVE_INTERFACE_REFLUX_V1, &reflux_api, sizeof(reflux_api))); + EXPECT_EQ( + pops::component::apply_reflux_interface_batch(reflux_api, nullptr, reflux_request, status), + 0); + EXPECT_EQ(reflux_correction, (std::array{-4.0, -8.0})); + + auto incomplete_reflux_api = reflux_api; + incomplete_reflux_api.apply_interface_batch = nullptr; + EXPECT_FALSE(pops::component::generated_native_interface_table_is_complete( + POPS_NATIVE_INTERFACE_REFLUX_V1, &incomplete_reflux_api, sizeof(incomplete_reflux_api))); + EXPECT_THROW(pops::component::apply_reflux_interface_batch(incomplete_reflux_api, nullptr, + reflux_request, status), + std::runtime_error); + auto collective_reflux = reflux_request; + collective_reflux.execution = execution; + EXPECT_THROW( + pops::component::apply_reflux_interface_batch(reflux_api, nullptr, collective_reflux, status), + std::invalid_argument); + auto malformed_reflux = reflux_request; + auto malformed_face = reflux_face; + malformed_face.correction.layout_identity = "other::parent-layout"; + malformed_reflux.faces = &malformed_face; + EXPECT_THROW( + pops::component::apply_reflux_interface_batch(reflux_api, nullptr, malformed_reflux, status), + std::invalid_argument); + auto overflowing_ghosts = abi::const_field_view(tag_values.data(), 2, 2); overflowing_ghosts.ghost_lower[0] = std::numeric_limits::max(); overflowing_ghosts.ghost_upper[0] = 1; diff --git a/tests/python/integration/native_loader/test_external_interface_backend.py b/tests/python/integration/native_loader/test_external_interface_backend.py index 07617eaa9..6d510c6a8 100644 --- a/tests/python/integration/native_loader/test_external_interface_backend.py +++ b/tests/python/integration/native_loader/test_external_interface_backend.py @@ -5,23 +5,63 @@ import json from pathlib import Path +import pytest + from pops import interfaces from pops import _generated_component_interfaces as generated +from pops.model import ComponentManifest def test_all_required_native_families_are_generated_data_only_contracts(): expected = { "numerical_flux", "ghost_boundary", "field_boundary_closure", "tagger", - "clustering", "transfer", "field_solver", "writer", "field_topology", + "clustering", "transfer", "reflux", "field_solver", "writer", "field_topology", } resolved = {name: interfaces.resolve(name) for name in expected} assert set(resolved) == expected assert len({value.abi_id for value in resolved.values()}) == len(expected) + assert {name: value.abi_id for name, value in resolved.items()} == { + "numerical_flux": 0, + "ghost_boundary": 1, + "field_boundary_closure": 2, + "tagger": 3, + "clustering": 4, + "transfer": 5, + "reflux": 6, + "field_solver": 7, + "writer": 8, + "field_topology": 9, + } assert all(value.table_symbol == "pops_component_interface_v1" for value in resolved.values()) assert all(value.operations for value in resolved.values()) +def test_reflux_is_exact_generated_id_6_and_incomplete_conformer_is_refused(): + interface = interfaces.Reflux + assert interface.abi_id == 6 + assert interface.uri == "pops://interfaces/reflux" + assert interface.cpp_table == "PopsRefluxApiV1" + assert interface.operations == ("apply_interface_batch",) + + incomplete_signature = interface.signature_declaration() + incomplete_signature["operations"] = () + manifest = ComponentManifest( + uri="pops://external.test/reflux/incomplete", + component_type="reflux", + version="1.0.0", + facets=interface.facets, + signature={"native_interface": incomplete_signature}, + interfaces=interface.manifest_declarations(), + target={"variants": [{ + "dimension": 2, "scalar": "float64", "device": "cpu", "features": [], + }]}, + entry_points={"interface_table": "pops_component_interface_v1"}, + ) + with pytest.raises(ValueError, match="does not carry the generated native interface identity"): + interface.require_manifest(manifest) + + def test_python_native_component_boundary_has_no_ffi_or_test_owned_backend(): root = Path(__file__).resolve().parents[4] production = (