From afae2251d23920c4d7847c32cfabcfd786dca060 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 26 Feb 2026 10:59:06 +0000 Subject: [PATCH 1/3] Start avgmol --- .gitignore | 1 + src/nodes/avgmol/avgmol.cpp | 13 +++++++++++++ src/nodes/avgmol/avgmol.h | 25 +++++++++++++++++++++++++ src/nodes/registry.cpp | 2 ++ 4 files changed, 41 insertions(+) create mode 100644 src/nodes/avgmol/avgmol.cpp create mode 100644 src/nodes/avgmol/avgmol.h diff --git a/.gitignore b/.gitignore index 7846fc76cb..7339a8075f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ aqtinstall.log *.sq *.rewrite tests/data/*.xml +tests/data/test.ANGLE tests/epsr/D2O-ReferenceData.q tests/epsr/D2O-ReferenceData.r tests/epsr/EPSR01-EstSQ-HW-HW.txt diff --git a/src/nodes/avgmol/avgmol.cpp b/src/nodes/avgmol/avgmol.cpp new file mode 100644 index 0000000000..1a52c96483 --- /dev/null +++ b/src/nodes/avgmol/avgmol.cpp @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "nodes/avgmol/avgmol.h" +#include "nodes/constants.h" + +AvgMolNode::AvgMolNode(Graph *parentGraph) : Node(parentGraph) {} + +std::string_view AvgMolNode::type() const { return "AvgMol"; } + +std::string_view AvgMolNode::summary() const { return "Calculate Average Molecule"; } + +NodeConstants::ProcessResult AvgMolNode::process() { return NodeConstants::ProcessResult::Failed; } diff --git a/src/nodes/avgmol/avgmol.h b/src/nodes/avgmol/avgmol.h new file mode 100644 index 0000000000..3c43921adb --- /dev/null +++ b/src/nodes/avgmol/avgmol.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#pragma once + +#include "nodes/node.h" +#include "nodes/parameter.h" + +class AvgMolNode : public Node +{ + public: + AvgMolNode(Graph *parentGraph); + ~AvgMolNode() override = default; + + public: + std::string_view type() const override; + std::string_view summary() const override; + + /* + * Processing + */ + public: + // Run main processing + NodeConstants::ProcessResult process() override; +}; diff --git a/src/nodes/registry.cpp b/src/nodes/registry.cpp index a82e77b842..6004492349 100644 --- a/src/nodes/registry.cpp +++ b/src/nodes/registry.cpp @@ -6,6 +6,7 @@ #include "nodes/add.h" #include "nodes/angle.h" #include "nodes/atomicMC/atomicMC.h" +#include "nodes/avgmol/avgmol.h" #include "nodes/bragg.h" #include "nodes/configuration.h" #include "nodes/data1DImport.h" @@ -54,6 +55,7 @@ void NodeRegistry::instantiateNodeProducers() producers_ = {{"Add", makeDerivedNode()}, {"Angle", makeDerivedNode()}, {"AtomicMC", makeDerivedNode()}, + {"AvgMol", makeDerivedNode()}, {"Bragg", makeDerivedNode()}, {"Configuration", makeDerivedNode()}, {"Data1DImport", makeDerivedNode()}, From 46ec099ae2c2185bcc01752e4035f5682f6dba8a Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 26 Feb 2026 12:07:37 +0000 Subject: [PATCH 2/3] Basic node framework set --- .gitignore | 1 + src/gui/qml/nodeGraph/GraphView.qml | 5 ++ src/io/export/species.cpp | 7 ++ src/io/export/species.h | 1 + src/nodes/avgmol/avgmol.cpp | 12 +++- src/nodes/avgmol/avgmol.h | 27 +++++++ src/nodes/avgmol/process.cpp | 107 ++++++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 src/nodes/avgmol/process.cpp diff --git a/.gitignore b/.gitignore index 7339a8075f..6db7462753 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ changes *.DS_Store msvc-env aqtinstall.log +justfile # Example files *.beat diff --git a/src/gui/qml/nodeGraph/GraphView.qml b/src/gui/qml/nodeGraph/GraphView.qml index f32edaca91..6be387213c 100644 --- a/src/gui/qml/nodeGraph/GraphView.qml +++ b/src/gui/qml/nodeGraph/GraphView.qml @@ -70,6 +70,11 @@ Pane { Menu { title: "Action" + MenuItem { + text: "Average Molecule" + + onClicked: graphRoot.rootModel.emplace_back(Math.round(ctxMenuCatcher.mouseX), Math.round(ctxMenuCatcher.mouseY), "AvgMol", "New AvgMol") + } MenuItem { text: "Atomic MC" diff --git a/src/io/export/species.cpp b/src/io/export/species.cpp index 258f04722f..5c98533307 100644 --- a/src/io/export/species.cpp +++ b/src/io/export/species.cpp @@ -16,6 +16,13 @@ SpeciesExportFileFormat::SpeciesExportFileFormat(std::string_view filename, Spec {SpeciesExportFormat::DLPOLY, "dlpoly", "DL_POLY CONFIG File"}}); } +SpeciesExportFileFormat &SpeciesExportFileFormat::operator=(const SpeciesExportFileFormat &other) +{ + formats_=other.formats_; + + return *this; +} + /* * Export Functions */ diff --git a/src/io/export/species.h b/src/io/export/species.h index 79ee06e321..bd260125da 100644 --- a/src/io/export/species.h +++ b/src/io/export/species.h @@ -20,6 +20,7 @@ class SpeciesExportFileFormat : public FileAndFormat }; SpeciesExportFileFormat(std::string_view filename = "", SpeciesExportFormat format = SpeciesExportFormat::XYZ); ~SpeciesExportFileFormat() override = default; + SpeciesExportFileFormat &operator=(const SpeciesExportFileFormat &other); /* * Formats diff --git a/src/nodes/avgmol/avgmol.cpp b/src/nodes/avgmol/avgmol.cpp index 1a52c96483..6e64e8bd30 100644 --- a/src/nodes/avgmol/avgmol.cpp +++ b/src/nodes/avgmol/avgmol.cpp @@ -4,10 +4,16 @@ #include "nodes/avgmol/avgmol.h" #include "nodes/constants.h" -AvgMolNode::AvgMolNode(Graph *parentGraph) : Node(parentGraph) {} +AvgMolNode::AvgMolNode(Graph *parentGraph) : Node(parentGraph) +{ + addInput("Configuration", "Set target configuration for the module", targetConfiguration_); + + addOption("Site", "Target site about which to calculate average species geometry", targetSite_); + addOption("ExportCoordinates", "Whether to save average coordinates to disk", exportFileAndFormat_); + + addPointerOutput("Average Species", "The species with the average coordinates", averageSpecies_); +} std::string_view AvgMolNode::type() const { return "AvgMol"; } std::string_view AvgMolNode::summary() const { return "Calculate Average Molecule"; } - -NodeConstants::ProcessResult AvgMolNode::process() { return NodeConstants::ProcessResult::Failed; } diff --git a/src/nodes/avgmol/avgmol.h b/src/nodes/avgmol/avgmol.h index 3c43921adb..b75ffcd7d8 100644 --- a/src/nodes/avgmol/avgmol.h +++ b/src/nodes/avgmol/avgmol.h @@ -3,9 +3,14 @@ #pragma once +#include "io/export/species.h" +#include "math/sampledVector.h" #include "nodes/node.h" #include "nodes/parameter.h" +class Configuration; +class SpeciesSite; + class AvgMolNode : public Node { public: @@ -16,6 +21,28 @@ class AvgMolNode : public Node std::string_view type() const override; std::string_view summary() const override; + /* + * Definition + */ + private: + // Target configuration + Configuration *targetConfiguration_{nullptr}; + // Target site + const SpeciesSite *targetSite_{nullptr}; + // Whether to save average coordinates to disk + SpeciesExportFileFormat exportFileAndFormat_; + // Species targeted by module (derived from selected site) + const Species *targetSpecies_{nullptr}; + // Local Species representing average of targeted Species + Species averageSpecies_; + + private: + SampledVector x_, y_, z_; + // Ensure arrays are the correct size for the current target Species + void updateArrays(); + // Update the local species with the coordinates from the supplied arrays + void updateSpecies(); + /* * Processing */ diff --git a/src/nodes/avgmol/process.cpp b/src/nodes/avgmol/process.cpp new file mode 100644 index 0000000000..4751a7c20b --- /dev/null +++ b/src/nodes/avgmol/process.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "nodes/avgmol/avgmol.h" +#include "nodes/constants.h" + +void AvgMolNode::updateArrays() +{ + auto requiredSize = targetSpecies_ ? targetSpecies_->nAtoms() : -1; + + if (requiredSize > 0) + { + if (x_.values().size() == requiredSize && y_.values().size() == requiredSize && z_.values().size() == requiredSize) + Messenger::print("Using existing coordinate arrays for average species.\n"); + else + { + Messenger::print("Initialising arrays for average molecule: size = {}\n", requiredSize); + x_.initialise(requiredSize); + y_.initialise(requiredSize); + z_.initialise(requiredSize); + } + } + else + { + x_.clear(); + y_.clear(); + z_.clear(); + } +} + +void AvgMolNode::updateSpecies() +{ + for (auto &&[i, rx, ry, rz] : zip(averageSpecies_.atoms(), x_.values(), y_.values(), z_.values())) + averageSpecies_.setAtomCoordinates(&i, {rx, ry, rz}); +} + +NodeConstants::ProcessResult AvgMolNode::process() +{ + // Grab Box pointer + const auto *box = targetConfiguration_->box(); + + // Get the target site + if (!targetSite_) + { + Messenger::error("No target site defined.\n"); + return NodeConstants::ProcessResult::Failed; + } + + // Get site parent species + auto *sp = targetSite_->parent(); + if (sp != targetSpecies_) + { + Messenger::error("Internal error - target site parent is not the same as the target species.\n"); + return NodeConstants::ProcessResult::Failed; + } + + Messenger::print("AvgMol: Target site (species) is {} ({}).\n", targetSite_->name(), targetSpecies_->name()); + if (exportFileAndFormat_.hasFilename()) + Messenger::print("AvgMol: Coordinates will be exported to '{}' ({}).\n", exportFileAndFormat_.filename(), + exportFileAndFormat_.formatDescription()); + + Messenger::print("\n"); + + // Update arrays + updateArrays(); + + // Get the site stack + const auto *stack = targetConfiguration_->siteStack(targetSite_); + + // Loop over sites + std::vector rx(targetSpecies_->nAtoms()), ry(targetSpecies_->nAtoms()), rz(targetSpecies_->nAtoms()); + Vector3 r; + for (auto n = 0; n < stack->nSites(); ++n) + { + const auto &s = stack->site(n); + assert(s.molecule()->species() == targetSpecies_); + + // Get axes and take inverse + auto inverseAxes = s.axes(); + inverseAxes.invert(); + + // Loop over atoms, taking delta position with origin, and rotating into local axes + for (auto &&[i, x, y, z] : zip(s.molecule()->atoms(), rx, ry, rz)) + { + r = inverseAxes * box->minimumVector(s.origin(), i->r()); + x = r.x; + y = r.y; + z = r.z; + } + + // Accumulate positions + x_ += rx; + y_ += ry; + z_ += rz; + } + + updateSpecies(); + + // Export data? + if (exportFileAndFormat_.hasFilename()) + { + if (!exportFileAndFormat_.exportData(&averageSpecies_)) + return NodeConstants::ProcessResult::Failed; + } + + return NodeConstants::ProcessResult::Success; +} From 6255f5e1697fc5d0abf3c09eec8d09ac8f13bc54 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 26 Feb 2026 13:47:35 +0000 Subject: [PATCH 3/3] Unit test for AvgMol --- src/nodes/avgmol/process.cpp | 7 +--- tests/nodes/CMakeLists.txt | 1 + tests/nodes/avgMol.cpp | 66 ++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/nodes/avgMol.cpp diff --git a/src/nodes/avgmol/process.cpp b/src/nodes/avgmol/process.cpp index 4751a7c20b..c6242a5ae9 100644 --- a/src/nodes/avgmol/process.cpp +++ b/src/nodes/avgmol/process.cpp @@ -47,12 +47,7 @@ NodeConstants::ProcessResult AvgMolNode::process() } // Get site parent species - auto *sp = targetSite_->parent(); - if (sp != targetSpecies_) - { - Messenger::error("Internal error - target site parent is not the same as the target species.\n"); - return NodeConstants::ProcessResult::Failed; - } + targetSpecies_ = targetSite_->parent(); Messenger::print("AvgMol: Target site (species) is {} ({}).\n", targetSite_->name(), targetSpecies_->name()); if (exportFileAndFormat_.hasFilename()) diff --git a/tests/nodes/CMakeLists.txt b/tests/nodes/CMakeLists.txt index 3061a83f10..f0b34792c9 100644 --- a/tests/nodes/CMakeLists.txt +++ b/tests/nodes/CMakeLists.txt @@ -1,5 +1,6 @@ dissolve_add_test(SRC angle.cpp) dissolve_add_test(SRC atomicMC.cpp) +dissolve_add_test(SRC avgMol.cpp) dissolve_add_test(SRC bragg.cpp) dissolve_add_test(SRC broadening.cpp) dissolve_add_test(SRC flow.cpp) diff --git a/tests/nodes/avgMol.cpp b/tests/nodes/avgMol.cpp new file mode 100644 index 0000000000..65b6e555e7 --- /dev/null +++ b/tests/nodes/avgMol.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "nodes/avgmol/avgmol.h" +#include "classes/speciesSite.h" +#include "io/export/species.h" +#include "io/import/trajectory.h" +#include "nodes/iterableGraph.h" +#include "tests/graphData.h" +#include "tests/testData.h" +#include + +namespace UnitTest +{ +TEST(AvgMolNodeTest, Water) +{ + GraphTestData data; + createWaterGraph(&data.graphRoot, 267); + + // Create an iterator + auto iterator = dynamic_cast(data.graphRoot.createNode("Iterator")); + ASSERT_TRUE(iterator); + + // Create a dynamic input from the graph's existing Insert node + EXPECT_TRUE(data.graphRoot.addEdge({"Insert", "Configuration", "Iterator", "Configuration"})); + + // Within the iterator create an ImportTrajectory node + auto importTrajectory = iterator->createNode("ImportConfigurationTrajectory"); + ASSERT_TRUE(importTrajectory); + ASSERT_TRUE(importTrajectory->setOption("FilePath", "dlpoly/water267-analysis/water-267-298K.xyz")); + ASSERT_TRUE(importTrajectory->setOption( + "FileFormat", TrajectoryImportFileFormat::TrajectoryImportFormat::XYZ)); + ASSERT_TRUE(iterator->addEdge({"Inputs", "Configuration", "ImportConfigurationTrajectory", "Configuration"})); + + // Add average molecule module to the iterator + auto avg = dynamic_cast(iterator->createNode("AvgMol")); + ASSERT_TRUE(avg); + + auto *water = data.graphRoot.findNode("Water")->getOutputValue("Species"); + ASSERT_TRUE(water); + auto *configuration = data.graphRoot.findNode("Bulk")->getOutputValue("Configuration"); + ASSERT_TRUE(configuration); + SpeciesExportFileFormat exporter("test.AVERAGE"); + ASSERT_TRUE(avg->setOption("ExportCoordinates", exporter)); + auto site = water->findSite("Origin"); + ASSERT_TRUE(site); + ASSERT_TRUE(avg->setOption("Site", water->findSite("Origin"))); + + ASSERT_TRUE(iterator->addEdge({"ImportConfigurationTrajectory", "Configuration", "AvgMol", "Configuration"})); + + // Run from the iterator node explicitly + ASSERT_TRUE(iterator->setOption("N", 95)); + ASSERT_EQ(iterator->run(), NodeConstants::ProcessResult::Success); + + // Data1DExportFileFormat exporter("test.ANGLE"); + // exporter.exportData(angle->rdfBC()); + // EXPECT_TRUE(DissolveSystemTest::checkData1D( + // angle->rdfBC(), "B-C RDF", + // {"dlpoly/water267-analysis/water-267-298K.aardf_21_23_inter_sum", Data1DImportFileFormat::Data1DImportFormat::XY}, + // 2.0e-2)); + // EXPECT_TRUE(DissolveSystemTest::checkData1D(angle->angleABC(), "A-B-C angle", + // {"dlpoly/water267-analysis/water-267-298K.dahist1_02_1_01_02.angle.norm", + // Data1DImportFileFormat::Data1DImportFormat::XY}, + // 6.0e-5)); +} +} // namespace UnitTest