Skip to content
Merged
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
11 changes: 11 additions & 0 deletions include/dca/io/json/details/json_group.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@

namespace dca::io::details {

// A child group together with whether it was ever read. Returned by childGroupAccess() to let
// callers detect input sections the executable silently ignored (issue #300).
struct ChildGroupStatus {
std::string name;
bool accessed;
};

class JSONGroup : public JSONObject {
public:
JSONGroup() = default;
Expand All @@ -30,6 +37,10 @@ class JSONGroup : public JSONObject {
JSONGroup* addGroup(const std::string& name);
JSONGroup* getGroup(const std::string& name);

// Returns the access state of each direct child that is itself a group, sorted by name. Used to
// report sections of the input that the executable never read (issue #300).
std::vector<ChildGroupStatus> childGroupAccess() const;

template <class T>
void addEntry(const std::string& name, const T& val) {
const auto it = objects_.find(name);
Expand Down
13 changes: 13 additions & 0 deletions include/dca/io/json/details/json_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ class JSONObject {

virtual void write(std::ostream& stream, int ident) const = 0;
virtual bool read(std::istream& stream) = 0;

// Tracks whether this object was ever looked up during reading. Used to detect input that the
// executable silently ignores (issue #300). Marking is logically const (it does not change the
// parsed value), hence the mutable flag and const markAccessed().
bool wasAccessed() const noexcept {
return accessed_;
}
void markAccessed() const noexcept {
accessed_ = true;
}

private:
mutable bool accessed_ = false;
};

} // namespace dca::io::details
Expand Down
8 changes: 8 additions & 0 deletions include/dca/io/json/json_reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <iostream>
#include <stack>
#include <string>
#include <vector>

#include "dca/platform/dca_gpu.h"
#include "dca/io/json/details/json_group.hpp"
Expand Down Expand Up @@ -43,6 +44,13 @@ class JSONReader {
// the root group.
bool close_group() noexcept;

// Returns the access state of each top-level group of the currently loaded file, sorted by name.
// A group is "accessed" if it was ever opened via open_group. Lets callers detect sections the
// executable silently ignored (issue #300). Must be called before close_file().
std::vector<details::ChildGroupStatus> topLevelGroupAccess() const {
return root_.childGroupAccess();
}

long getStepCount() { return 0; }

std::string get_path() { return {}; }
Expand Down
89 changes: 89 additions & 0 deletions include/dca/phys/parameters/model_section_check.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (C) 2026 ETH Zurich
// Copyright (C) 2026 UT-Battelle, LLC
// All rights reserved.
//
// See LICENSE for terms of usage.
// See CITATION.md for citation guidelines, if DCA++ is used for scientific publications.
//
// Author: Tyler Sax (tylersax@gmail.com)
//
// Issue #300: surface model-section input mistakes at parse time.
//
// A DCA++ executable is compiled for exactly one model and reads exactly one "*-model" section.
// This standalone helper inspects the top-level groups of a parsed JSON input file (as
// ChildGroupStatus records) and reacts to the three situations:
// * Typo: one or more "*-model" sections are present but none is the one this executable
// reads -- likely a misspelled section name or the wrong input file. Throws
// std::invalid_argument, since the run would otherwise silently use default model parameters.
// * Multi-model file: the executable's section was read, but other "*-model" sections
// are also present (e.g. a shared input file used by several model builds). Warns, since this
// is a legitimate pattern currently used in testing.
// * No model section at all: some non-simulation uses of Parameters (domain/FFT setup) do not
// need model parameters. Warns rather than throwing, so those uses keep working.
//
// Out: out (warnings are written here; defaults to std::cerr)

#ifndef DCA_PHYS_PARAMETERS_MODEL_SECTION_CHECK_HPP
#define DCA_PHYS_PARAMETERS_MODEL_SECTION_CHECK_HPP

#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

#include "dca/io/json/details/json_group.hpp"

namespace dca {
namespace phys {
namespace params {

inline void checkModelSections(
const std::vector<dca::io::details::ChildGroupStatus>& top_level_groups,
const std::string& filename, std::ostream& out = std::cerr) {
constexpr std::string_view suffix = "-model";

std::vector<std::string> unused;
bool model_section_read = false;

for (const auto& [name, accessed] : top_level_groups) {
const bool is_model_section =
name.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), name.rbegin());
if (!is_model_section)
continue;

if (accessed)
model_section_read = true;
else
unused.push_back(name);
}

if (!model_section_read) {
if (!unused.empty()) {
// Model sections exist but none matches this executable's model -> typo / wrong file.
std::string message = "Input '" + filename +
"' contains model section(s) [";
for (std::size_t i = 0; i < unused.size(); ++i)
message += (i ? ", " : "") + unused[i];
message +=
"] but none matches this executable's model. Check for a typo in the model section name.";
throw std::invalid_argument(message);
}
// No model section at all: a non-simulation use of Parameters. Warn, but allow defaults.
out << "Warning: input '" << filename
<< "' contains no model section; this executable's model will use default parameters.\n";
return;
}

// The executable's model was read; flag any other model sections present but unused.
for (const auto& name : unused)
out << "Warning: input model section '" << name
<< "' is not used by this executable and will be ignored.\n";
}

} // namespace params
} // namespace phys
} // namespace dca

#endif // DCA_PHYS_PARAMETERS_MODEL_SECTION_CHECK_HPP
10 changes: 10 additions & 0 deletions include/dca/phys/parameters/parameters.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@

#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

#include "dca/io/json/json_reader.hpp"

// its expected that dca::config::McOptions will be provided in some manner before parameters.hpp is
// included
#include "dca/phys/parameters/model_section_check.hpp"
#include "dca/phys/parameters/num_traits.hpp"
#include "dca/function/domains/dmn_0.hpp"
#include "dca/phys/parameters/analysis_parameters.hpp"
Expand Down Expand Up @@ -280,6 +284,12 @@ void Parameters<Concurrency, Threading, Profiler, Model, RandomNumberGenerator,
Reader read_obj;
read_obj.open_file(filename);
this->readWrite(read_obj);
// Detect model-section input mistakes while the parsed tree is still live (before close_file).
// Only the JSON reader tracks which sections were read; HDF5 as input is realistically
// machine-generated, so the access check is not implemented for that path
if constexpr (std::is_same_v<Reader, dca::io::JSONReader>) {
checkModelSections(read_obj.topLevelGroupAccess(), filename);
}
read_obj.close_file();
OutputParameters::validate();
}
Expand Down
25 changes: 24 additions & 1 deletion src/io/json/details/json_group.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
// JSON group.

#include "dca/io/json/details/json_group.hpp"

#include <algorithm>

#include "dca/io/json/details/util.hpp"

namespace dca::io::details {
Expand All @@ -26,7 +29,27 @@ JSONGroup* JSONGroup::addGroup(const std::string& name) {
}

JSONGroup* JSONGroup::getGroup(const std::string& name) {
return dynamic_cast<JSONGroup*>(objects_[name].get());
// Note: use find() rather than operator[], which would insert a null entry on a miss.
const auto it = objects_.find(name);
if (it == objects_.end())
return nullptr;

auto* group = dynamic_cast<JSONGroup*>(it->second.get());
if (group)
group->markAccessed();
return group;
}

std::vector<ChildGroupStatus> JSONGroup::childGroupAccess() const {
std::vector<ChildGroupStatus> child_groups;
for (const auto& [name, object] : objects_) {
if (dynamic_cast<const JSONGroup*>(object.get()))
child_groups.push_back({name, object->wasAccessed()});
}
// objects_ is an unordered_map; sort for deterministic output (e.g. warning messages).
std::sort(child_groups.begin(), child_groups.end(),
[](const ChildGroupStatus& a, const ChildGroupStatus& b) { return a.name < b.name; });
return child_groups;
}

void JSONGroup::write(std::ostream& stream, int ident) const {
Expand Down
30 changes: 30 additions & 0 deletions test/unit/io/json_reader_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,36 @@ TEST(ReadTest, All) {
EXPECT_EQ(vc, vc_check);
}

// Issue #300: the reader must report which top-level sections were read, so callers can detect
// input that the executable silently ignores.
TEST(ReadTest, TopLevelGroupAccessTracking) {
dca::io::JSONReader reader;
reader.open_file(directory + "model_sections_input.json");

// Before any access, every top-level group is reported as unread.
auto groups = reader.topLevelGroupAccess();
// Sorted by name: bilayer-Hubbard-model, domains, single-band-Hubbard-model.
ASSERT_EQ(groups.size(), 3u);
EXPECT_EQ(groups[0].name, "bilayer-Hubbard-model");
EXPECT_EQ(groups[1].name, "domains");
EXPECT_EQ(groups[2].name, "single-band-Hubbard-model");
for (const auto& group : groups)
EXPECT_FALSE(group.accessed) << group.name << " should not be accessed yet";

// Open one of them, as the executable's ModelParameters would.
EXPECT_TRUE(reader.open_group("single-band-Hubbard-model"));
reader.close_group();

// A failed open (e.g. a typo'd section name) must not mark anything accessed.
EXPECT_FALSE(reader.open_group("single-band-Hubard-model"));

groups = reader.topLevelGroupAccess();
ASSERT_EQ(groups.size(), 3u);
EXPECT_FALSE(groups[0].accessed); // bilayer-Hubbard-model: present but never opened
EXPECT_FALSE(groups[1].accessed); // domains: never opened
EXPECT_TRUE(groups[2].accessed); // single-band-Hubbard-model: opened
}

TEST(ReadTest, InvalidInput) {
auto test_file = [](const std::string& name, int err_line) {
dca::io::JSONReader reader;
Expand Down
15 changes: 15 additions & 0 deletions test/unit/io/model_sections_input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"single-band-Hubbard-model" : {
"t" : 1.0,
"U" : 8.0
},

"bilayer-Hubbard-model" : {
"t" : 1.0,
"U" : 6.0
},

"domains" : {
"vec int" : [1, 2, 3]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
"U": 2
},

"FeAs-model": {
"t1": 0,
"t2": 0,
"t3": 0,
"t4": 0,
"U": 0,
"V": 0,
"J": 0,
"Jp": 0
},

"domains": {
"real-space-grids": {
"cluster": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
"phi": 0
},

"Rashba-Hubbard-model": {
"t": 1,
"h": 0,
"lambda": 0,
"U": 0
},

"single-band-Hubbard-model": {
"t": 1,
"U": 2
Expand Down
1 change: 1 addition & 0 deletions test/unit/phys/parameters/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ add_subdirectory(four_point_parameters)
add_subdirectory(mc_solver_parameters)
add_subdirectory(mci_parameters)
add_subdirectory(model_parameters)
add_subdirectory(model_section_check)
add_subdirectory(output_parameters)
add_subdirectory(physics_parameters)
4 changes: 4 additions & 0 deletions test/unit/phys/parameters/model_section_check/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Model section check unit test (issue #300)

dca_add_gtest(model_section_check_test
GTEST_MAIN)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (C) 2026 ETH Zurich
// Copyright (C) 2026 UT-Battelle, LLC
// All rights reserved.
//
// See LICENSE for terms of usage.
// See CITATION.md for citation guidelines, if DCA++ is used for scientific publications.
//
// Author: Tyler Sax (tylersax@gmail.com)
//
// Unit tests for checkModelSections (issue #300 bugs #2 and #3).

#include "dca/phys/parameters/model_section_check.hpp"

#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

#include "dca/testing/gtest_h_w_warning_blocking.h"

using dca::phys::params::checkModelSections;
using Groups = std::vector<dca::io::details::ChildGroupStatus>;

// The built model's section was read and nothing else is present: no warning, no throw.
TEST(CheckModelSectionsTest, OnlyBuiltModelRead) {
const Groups groups{{"domains", true}, {"single-band-Hubbard-model", true}};
std::ostringstream warnings;
EXPECT_NO_THROW(checkModelSections(groups, "input.json", warnings));
EXPECT_TRUE(warnings.str().empty());
}

// The built model was read, but other model sections are present and unused -> warn only.
TEST(CheckModelSectionsTest, UnusedModelSectionWarns) {
const Groups groups{{"single-band-Hubbard-model", true}, {"bilayer-Hubbard-model", false}};
std::ostringstream warnings;
EXPECT_NO_THROW(checkModelSections(groups, "input.json", warnings));
const std::string out = warnings.str();
EXPECT_NE(out.find("bilayer-Hubbard-model"), std::string::npos);
EXPECT_NE(out.find("ignored"), std::string::npos);
// The section that was actually used must not be warned about.
EXPECT_EQ(out.find("single-band-Hubbard-model"), std::string::npos);
}

// Model section(s) present but none matches the built model (typo / wrong file)
// -> throw, even though a model section exists.
TEST(CheckModelSectionsTest, WrongModelSectionThrows) {
const Groups groups{{"bilayer-Hubbard-model", false}, {"domains", true}};
std::ostringstream warnings;
try {
checkModelSections(groups, "input.json", warnings);
FAIL() << "expected std::invalid_argument";
}
catch (const std::invalid_argument& e) {
const std::string msg = e.what();
EXPECT_NE(msg.find("input.json"), std::string::npos);
// The unmatched section is surfaced as a typo hint.
EXPECT_NE(msg.find("bilayer-Hubbard-model"), std::string::npos);
}
// A throw must take precedence over emitting warnings.
EXPECT_TRUE(warnings.str().empty());
}

// No model section present at all -> warn (do not throw); model-agnostic uses keep working.
TEST(CheckModelSectionsTest, NoModelSectionPresentWarns) {
const Groups groups{{"domains", true}, {"output", false}};
std::ostringstream warnings;
EXPECT_NO_THROW(checkModelSections(groups, "input.json", warnings));
EXPECT_NE(warnings.str().find("no model section"), std::string::npos);
}

// Sections that merely contain "model" but do not end in "-model" are not model sections.
TEST(CheckModelSectionsTest, SuffixMatchIsExact) {
const Groups groups{{"model-parameters", false}, {"single-band-Hubbard-model", true}};
std::ostringstream warnings;
EXPECT_NO_THROW(checkModelSections(groups, "input.json", warnings));
EXPECT_TRUE(warnings.str().empty()); // "model-parameters" is not treated as a model section
}
Loading