[Possibly breaking] Require all containers with the same base name in a DataCollection be made from the same field set - #1449
Conversation
…les and introspect containers in solvers
|
@par-hermes format |
DataCollection be made from the same field setDataCollection be made from the same field set
Yurlungur
left a comment
There was a problem hiding this comment.
Some nitpicks below, but otherwise LGTM.
Aside: I find myself nitpicking AI comments a lot these days because I find them so frequently overly verbose that they confuse rather than clarify.
| PARTHENON_THROW( | ||
| "Container \"" + name + | ||
| "\" is being created from different field lists on different sources. All " | ||
| "instances sharing a name must be created from the same field list."); | ||
| } |
There was a problem hiding this comment.
Might be useful for debugging to print the source label (if we know it) and both sets of fields.
| // containers built by hand or through a different DataCollection); an empty list | ||
| // means "all fields"/"don't check" and always passes. | ||
| if (fields.size() && !(it->second)->CreatedFrom(fields)) | ||
| PARTHENON_THROW(key + " already exists in collection but fields do not match."); |
There was a problem hiding this comment.
Might be useful for debugging to print the source label (if we know it) and both sets of fields.
|
Also I agree we should enforce this behavior. I think it was probably a mistake that it didn't in the first place. |
Co-authored-by: Jonah Miller <DrLoco3000@gmail.com> Co-authored-by: Luke Roberts <lfroberts@lanl.gov>
|
@par-hermes format |
|
@c-prather and @pgrete: Can you give a review on this? I think it is reasonable that we enforce all containers with the same base name contain the same set of fields, but I want to make sure that this doesn't conflict with anything in Kharma or AthenaPK. |
c-prather
left a comment
There was a problem hiding this comment.
Tried this & it doesn't break anything in KHARMA. I agree with enforcing it generally.
pgrete
left a comment
There was a problem hiding this comment.
Given that uid are now used internally, does this mean that if I construct sth from a string vector, the resulting pack is not guaranteed to be in the same order of the passed vector any more?
Thinking in terms of a migration path for AthenaPK here and a potential hiccup for "simple" downstream implementations.
Also Codex identified a separate issue due to different defaults for empty lists{} in Initialization paths between MeshBlockData and ``MeshBlock`:
An empty fields list is source-dependent: copying MeshData preserves its current subset, while creating from a BlockListPartition selects all package fields. Recording {} in both cases
lets disjoint sources with different subsets pass the same-name check. It also makes GetCreationFields() return {} for a subset, allowing solver-created partitions to expand it to all
fields. Please resolve empty lists to the effective field set before recording and comparing them.
A reproducer unit test is here:
diff --git a/tst/unit/test_data_collection.cpp b/tst/unit/test_data_collection.cpp
index 013807d55..988b505cb 100644
--- a/tst/unit/test_data_collection.cpp
+++ b/tst/unit/test_data_collection.cpp
@@ -10,7 +10,10 @@
// license in this material to reproduce, prepare derivative works, distribute copies to
// the public, perform publicly and display publicly, and to permit others to do so.
//========================================================================================
+// This file was made in part with generative AI.
+
#include <memory>
+#include <set>
#include <string>
#include <vector>
@@ -18,6 +21,7 @@
#include "basic_types.hpp"
#include "interface/data_collection.hpp"
+#include "interface/mesh_data.hpp"
#include "interface/meshblock_data.hpp"
#include "interface/metadata.hpp"
#include "kokkos_abstraction.hpp"
@@ -149,3 +153,78 @@ TEST_CASE("Adding MeshBlockData objects to a DataCollection", "[DataCollection]"
}
}
}
+
+TEST_CASE("MeshData with the same base name must have the same effective field set",
+ "[DataCollection]") {
+ DataCollection<MeshData<Real>> d;
+
+ std::vector<int> size(6, 1);
+ Metadata m({Metadata::Independent}, size);
+
+ auto pkg = std::make_shared<StateDescriptor>("DataCollection MeshData field set test");
+ pkg->AddField("var1", m);
+ pkg->AddField("var2", m);
+ pkg->AddField("var3", m);
+
+ // Use different gids so that the two MeshData sources have distinct DataCollection
+ // cache keys despite using the same destination base name.
+ auto pmb0 = std::make_shared<MeshBlock>();
+ pmb0->gid = 0;
+ pmb0->resolved_packages = pkg;
+
+ auto pmb1 = std::make_shared<MeshBlock>();
+ pmb1->gid = 1;
+ pmb1->resolved_packages = pkg;
+
+ auto part0 = std::make_shared<parthenon::BlockListPartition>(
+ 0, parthenon::GridIdentifier::leaf(), parthenon::BlockList_t{pmb0}, nullptr);
+ auto part1 = std::make_shared<parthenon::BlockListPartition>(
+ 1, parthenon::GridIdentifier::leaf(), parthenon::BlockList_t{pmb1}, nullptr);
+
+ const std::vector<std::string> fields0{"var1", "var2"};
+ const std::vector<std::string> fields1{"var1", "var3"};
+
+ // Different base names may use different field subsets.
+ auto &src0 = d.Add("src0", part0, fields0);
+ auto &src1 = d.Add("src1", part1, fields1);
+
+ REQUIRE(src0->NumBlocks() == 1);
+ REQUIRE(src1->NumBlocks() == 1);
+ REQUIRE(src0->ContainsExactly(fields0));
+ REQUIRE(src1->ContainsExactly(fields1));
+
+ // Omitting the field list copies the source subset.
+ auto &work0 = d.Add("work", src0);
+ REQUIRE(work0->ContainsExactly(fields0));
+
+ SECTION("Implicit copies of different subsets are rejected across partitions") {
+ REQUIRE_THROWS_WITH(
+ d.Add("work", src1),
+ Catch::Matchers::Contains("different field lists on different sources"));
+ }
+
+ SECTION("Implicit copies of matching subsets are accepted across partitions") {
+ auto &matching = d.Add("matching", part1, fields0);
+ auto &work1 = d.Add("work", matching);
+ REQUIRE(work1->NumBlocks() == 1);
+ REQUIRE(work1->ContainsExactly(fields0));
+ }
+
+ SECTION("Creation fields describe the copied subset") {
+ const std::set<parthenon::Uid_t> expected{src0->GetBlockData(0)->UniqueID("var1"),
+ src0->GetBlockData(0)->UniqueID("var2")};
+ REQUIRE(d.GetCreationFields("work") == expected);
+ }
+
+ SECTION("Creation fields preserve the subset on a new block partition") {
+ auto &work1 = d.AddFromSet("work", part1, d.GetCreationFields("work"));
+ REQUIRE(work1->NumBlocks() == 1);
+ REQUIRE(work1->ContainsExactly(fields0));
+ }
+
+ SECTION("An omitted field list still reuses an existing subset container") {
+ auto &subset = d.Add("subset", part0, fields0);
+ REQUIRE(d.Add("subset", part0) == subset);
+ REQUIRE(subset->ContainsExactly(fields0));
+ }
+}I verified this locally but am wondering how relevant this path is in practice for us.
| auto &md_base = | ||
| pmesh->mesh_data.AddFromSet(container_base, partitions[partition], | ||
| pmesh->mesh_data.GetCreationFields(container_base)); | ||
| // Container in which the solution is stored and with which the downstream user can |
There was a problem hiding this comment.
Does this imply that this is the recommend downstream pattern now, too?
If so, it might be worth to also mention this in the doc (and in general add a two-liner in the existing doc to reflect this change).
There was a problem hiding this comment.
No, I don't think this is the generally recommended pattern unless you need to do something specifically like what is done in multigrid where someone creates containers in one place from a set of partitions/meshdata and then somewhere else you need to create them from a different set of partitions/meshdata.
| @@ -122,6 +135,54 @@ class DataCollection { | |||
| void clear() { containers_.clear(); } | |||
There was a problem hiding this comment.
Should the clear also clear name_creation_fields_?
There was a problem hiding this comment.
yes, good catch.
PR Summary
[Made with assistance from generative AI]
This PR updates
DataCollectionto check that every container with a given base name is created from the same field list. Previously, code likewould run fine if there was no overlap between
partition1andpartition2, now it will throw an error.I think that we should be enforcing this because boundary communication on meshdata that is built similarly to
my_containerwill cause deadlocks in the task list. We ran into this issue in multigrid, where thereMeshDatawith the same base name are built by both downstream code and internal to the solver on different partitions.To make it easier to build containers in multiple places without keeping the field lists synchronized by hand, we also add the ability to query the
DataCollectionfor the list of fields used to create a partition on a given base name, allowingGetCreationFieldsreturns an empty vector if no container has been previously created with the requested base name, so the example above would include all fields.It is still valid to do something like
where the second call without a field list will still return the pre-existing container with the more limited field set.
PR Checklist