Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning

### Changed

- Generated physical-flux bricks now make their qualified provider requirements executable native
ABI evidence: the binder validates every row at compile time and reads only its declared storage
slots instead of scanning the model's complete auxiliary width.
- AMR checkpoint capability reports now distinguish same-rank bit-identical replay from
non-bit-identical rank-count rematerialization with Dense persisted histories. The M3 gate
executes the persisted two-rank to one-rank restart proof. The explicit `RegridOnRestart()`
Expand Down
5 changes: 5 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,11 @@ model-qualified `FaceTrace` values plus `FaceContext` and returns a typed densit
`SpatialOperator` alone applies face and cell measures. Provider packs are selected from exact
`(owner, space kind, space name, component)` identities. Missing, unavailable or contract-mismatched
providers fail during selection; homonymous components from different owners never alias.
Generated physical models carry those qualified rows as `flux_provider_requirements`. The native
binder validates their count, qualification, availability, unique in-range storage slots and then
loads only those declared slots into the model-qualified device pack. Hand-written C++ test models
that do not declare this generated ABI retain the full-width fixture path; generated PoPS models
never use that route.

## Limitations

Expand Down
80 changes: 76 additions & 4 deletions include/pops/numerics/fv/flux_interfaces.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
#include <pops/core/state/state.hpp>

#include <concepts>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <type_traits>
#include <utility>

namespace pops {

Expand Down Expand Up @@ -127,6 +129,46 @@ inline constexpr int flux_provider_count = [] {
return kAuxBaseComps;
}();

template <class Model>
inline constexpr bool has_qualified_flux_provider_requirements = requires {
Model::n_flux_providers;
Model::flux_provider_requirements;
};

/// Authenticate the generated logical provider ABI before a device pack can be instantiated.
///
/// Hand-written C++ test models may omit both members. Generated models must provide both, and
/// every selected provider must be available, fully qualified, and backed by one in-range native
/// storage slot. The binder consumes exactly these rows; they are not inspection-only metadata.
template <class Model>
consteval bool qualified_flux_provider_requirements_valid() {
constexpr bool has_count = requires { Model::n_flux_providers; };
constexpr bool has_rows = requires { Model::flux_provider_requirements; };
if constexpr (has_count != has_rows) {
return false;
} else if constexpr (!has_count) {
return true;
} else {
if (Model::n_flux_providers < 0 || static_cast<std::size_t>(Model::n_flux_providers) !=
Model::flux_provider_requirements.size())
return false;
const auto nonempty = [](const char* value) { return value != nullptr && value[0] != '\0'; };
for (std::size_t index = 0; index < Model::flux_provider_requirements.size(); ++index) {
const auto& row = Model::flux_provider_requirements[index];
if (!row.available || row.storage_slot < 0 ||
row.storage_slot >= flux_provider_count<Model> || !nonempty(row.owner_qid) ||
!nonempty(row.space_kind) || !nonempty(row.space_name) || !nonempty(row.component) ||
!nonempty(row.representation) || !nonempty(row.centering) || !nonempty(row.layout) ||
!nonempty(row.producer))
return false;
for (std::size_t previous = 0; previous < index; ++previous)
if (Model::flux_provider_requirements[previous].storage_slot == row.storage_slot)
return false;
}
return true;
}
}

/// Exact, model-qualified values before they are sealed into a bound device pack.
///
/// Unlike the historical global Aux object this type has exactly the width requested by Model.
Expand All @@ -136,6 +178,8 @@ inline constexpr int flux_provider_count = [] {
template <class Model>
struct FluxProviderValues {
static constexpr int size = flux_provider_count<Model>;
static_assert(qualified_flux_provider_requirements_valid<Model>(),
"generated physical flux provider requirements are invalid");
static_assert(size >= kAuxBaseComps,
"physical flux provider packs must declare the required base providers");
static_assert(size <= kAuxMaxComps,
Expand Down Expand Up @@ -176,15 +220,43 @@ POPS_HD BoundFluxProviders<Model> bind_flux_providers(const FluxProviderValues<M
return BoundFluxProviders<Model>(values);
}

namespace detail {

template <class Model, std::size_t Index>
inline constexpr int qualified_flux_provider_storage_slot =
Model::flux_provider_requirements[Index].storage_slot;

template <class Model, class Storage, std::size_t... Indices>
POPS_HD BoundFluxProviders<Model> bind_qualified_flux_providers_at(
const Storage& storage, int i, int j, std::index_sequence<Indices...>) {
FluxProviderValues<Model> values{};
((values[qualified_flux_provider_storage_slot<Model, Indices>] =
storage(i, j, qualified_flux_provider_storage_slot<Model, Indices>)),
...);
return bind_flux_providers<Model>(values);
}

} // namespace detail

/// Bind one exact provider pack directly from native field storage. The caller supplies a
/// model-qualified component count at compile time; there is no global Aux object, truncation, or
/// zero-on-missing branch on this path.
template <class Model, class Storage>
POPS_HD BoundFluxProviders<Model> bind_flux_providers_at(const Storage& storage, int i, int j) {
FluxProviderValues<Model> values{};
for (int component = 0; component < FluxProviderValues<Model>::size; ++component)
values[component] = storage(i, j, component);
return bind_flux_providers<Model>(values);
if constexpr (has_qualified_flux_provider_requirements<Model>) {
static_assert(qualified_flux_provider_requirements_valid<Model>(),
"generated physical flux provider requirements are invalid");
constexpr std::size_t count = qualified_flux_provider_requirements_valid<Model>()
? static_cast<std::size_t>(Model::n_flux_providers)
: 0;
return detail::bind_qualified_flux_providers_at<Model>(storage, i, j,
std::make_index_sequence<count>{});
} else {
FluxProviderValues<Model> values{};
for (int component = 0; component < FluxProviderValues<Model>::size; ++component)
values[component] = storage(i, j, component);
return bind_flux_providers<Model>(values);
}
}

template <class State, class ProviderPack>
Expand Down
68 changes: 68 additions & 0 deletions tests/cpp/unit/numerics/test_flux_interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <pops/numerics/fv/flux_failure.hpp>
#include <pops/numerics/fv/numerical_flux.hpp>

#include <array>
#include <cmath>
#include <initializer_list>
#include <limits>
Expand Down Expand Up @@ -68,6 +69,49 @@ struct ProviderStorage {
}
};

struct QualifiedProviderAdvect : ProviderAdvect {
static constexpr int n_flux_providers = 1;
inline static constexpr std::array<pops::QualifiedProviderRequirement, 1>
flux_provider_requirements{{
{"model::qualified", "field", "electric", "grad_x", "scalar", "cell", "",
"layout::primary", "", "field::electric", true, 1},
}};
};

struct UnavailableQualifiedProviderAdvect : ProviderAdvect {
static constexpr int n_flux_providers = 1;
inline static constexpr std::array<pops::QualifiedProviderRequirement, 1>
flux_provider_requirements{{
{"model::unavailable", "field", "electric", "grad_x", "scalar", "cell", "",
"layout::primary", "", "field::electric", false, 1},
}};
};

struct IncompleteQualifiedProviderAdvect : ProviderAdvect {
static constexpr int n_flux_providers = 1;
};

struct DuplicateQualifiedProviderAdvect : ProviderAdvect {
static constexpr int n_flux_providers = 2;
inline static constexpr std::array<pops::QualifiedProviderRequirement, 2>
flux_provider_requirements{{
{"model::duplicate", "field", "electric", "grad_x", "scalar", "cell", "",
"layout::primary", "", "field::electric", true, 1},
{"model::duplicate", "field", "magnetic", "grad_x", "scalar", "cell", "",
"layout::primary", "", "field::magnetic", true, 1},
}};
};

struct CountingProviderStorage {
pops::Real values[3]{pops::Real(11), pops::Real(4), pops::Real(13)};
mutable int reads[3]{};

POPS_HD pops::Real operator()(int, int, int component) const {
++reads[component];
return values[component];
}
};

template <class Model>
auto providers(std::initializer_list<pops::Real> values = {}) {
pops::FluxProviderValues<Model> resolved{};
Expand Down Expand Up @@ -253,6 +297,30 @@ TEST(test_flux_interfaces, provider_pack_is_model_qualified_and_failure_action_i
pops::TransactionFailureAction::kAbortRun);
}

TEST(test_flux_interfaces, generated_provider_requirements_own_native_slot_reads) {
static_assert(pops::has_qualified_flux_provider_requirements<QualifiedProviderAdvect>);
static_assert(pops::qualified_flux_provider_requirements_valid<QualifiedProviderAdvect>());
static_assert(
!pops::qualified_flux_provider_requirements_valid<UnavailableQualifiedProviderAdvect>());
static_assert(
!pops::qualified_flux_provider_requirements_valid<IncompleteQualifiedProviderAdvect>());
static_assert(
!pops::qualified_flux_provider_requirements_valid<DuplicateQualifiedProviderAdvect>());

const CountingProviderStorage storage{};
const auto bound = pops::bind_flux_providers_at<QualifiedProviderAdvect>(storage, 0, 0);
EXPECT_EQ(storage.reads[0], 0);
EXPECT_EQ(storage.reads[1], 1);
EXPECT_EQ(storage.reads[2], 0);

const QualifiedProviderAdvect::State state{pops::Real(3)};
const auto trace = pops::make_face_trace(state, bound);
const auto density =
pops::PhysicalFluxView<QualifiedProviderAdvect>{QualifiedProviderAdvect{}}.evaluate(
trace, pops::FaceContext::axis_aligned(0));
EXPECT_DOUBLE_EQ(density.value[0], pops::Real(12));
}

TEST(test_flux_interfaces, failed_evaluation_never_publishes_a_density) {
const Advect physical{};
const Advect::State state{pops::Real(3)};
Expand Down
8 changes: 8 additions & 0 deletions tests/python/architecture/test_flux_interface_fences.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ def test_bound_native_flux_pack_is_exact_and_does_not_store_global_aux():
assert "FluxDensity<State> checked_density() const" in header


def test_generated_flux_pack_metadata_controls_native_storage_reads():
header = _behavior(ROOT / "include/pops/numerics/fv/flux_interfaces.hpp")
assert "qualified_flux_provider_requirements_valid" in header
assert "qualified_flux_provider_storage_slot<Model, Indices>" in header
assert "std::make_index_sequence<count>" in header
assert "generated physical flux provider requirements are invalid" in header


def test_provider_selection_is_qualified_and_never_returns_a_neutral_value():
source = (ROOT / "python/pops/model/provider_pack.py").read_text(encoding="utf-8")
assert "def select(" in source
Expand Down
2 changes: 2 additions & 0 deletions tests/python/unit/codegen/test_compiler_model_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def test_facade_and_formula_carrier_share_one_minimal_flux_provider_pack():
assert rows[0]["key"]["owner_qid"] in source
assert '"grad_x"' in source
assert "true, 1" in source
assert "static constexpr int n_flux_providers = 1;" in source
assert "flux_provider_requirements" in source


def test_field_dependent_flux_without_provider_fails_before_native_source():
Expand Down
Loading