Skip to content

Conversation

@trisyoungs
Copy link
Member

@trisyoungs trisyoungs commented Oct 17, 2025

This PR implements a LoopGraph node - a simple Graph-type node which iterates over its children a number of times. It changes the InputsNode to mirror its proxy output parameters to identically-named input parameters, allowing edges to be created to transfer data back to the beginning of the loop.

The actual data flow in this PR is worth explicitly stating. In the unit test, when the node y_ is pulled to start the processing the LoopGraph is activated and processes it's input edges (only one, from the node i_). It then proceeds to iterate its subgraph for the required number of cycles. Loopback edges are not pulled on the first iteration, only on every subsequent iteration in order to provide any new input data. This means that, unless the node i_ is modified, calling the graph again (as is performed in the unit test) will result in it being Unchanged.

The loopback of data is achieved in practice by essentially disconnecting the InputsNode from natural node behaviour, principally overriding it's run() function to not automatically pull input edges (which would cause an infinite loop) and let LoopGraph handle this when necessary. Furthermore, propagation of "update required" flags is deliberately stopped at edges connecting to an input on an InputsNode as again this would cause an infinite loop.

Complicated, but hopefully a solid solution. Thoughts please!

@trisyoungs trisyoungs changed the base branch from develop to develop2 October 17, 2025 11:09
Copy link
Contributor

@RobBuchananCompPhys RobBuchananCompPhys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good progress! I think it would be worth investing some time while we're developing more tests to try and make test graph initialisation more efficient i.e. via some sort of TestGraph class hierarchy as commented above.

* ----------------------------------
*/
root_.x = 0;
root_.y = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we need to do often when setting up test graph? It may be good to have a base class for these tests which takes ownership of root_ and zeroes the coordinates behind the scenes, so we don't have to think about it.

namespace UnitTest
{
class LoopGraphTest :  public GraphTest // derives from public ::testing::Test
{
    public:
    LoopGraphTest() : GraphTest(), dissolve_(coreData_) {} // GraphTest could have default xtor args like centered = true //which give us a centered (x0, y0) graph

    // Create a graph for testing
    void createGraph()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suspicion would be that, in the longer term, most of these test graphs would be loaded from a TOML file instead of crafting the graphs by hand.

Copy link
Contributor

@rprospero rprospero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a solid design to me and answers most of the questions that I had in the stand-up. I'm particularly please that we know that the loop won't have to be re-run if the user changes and downstream analysis nodes.

Comment on lines -57 to +60
Error
Error,
Debug
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watching this grow, I'm wondering if we might be better off with a third party logging library. We've already written half of one and we might get some free functionality from one that is pre-built

Comment on lines +35 to +64
for (auto n = 0; n < iterations_.asInteger(); ++n)
{
debug("{}LoopGraph({})::process() - iteration {}", GraphDebug::indent(), name(), n);

// Pull edges connected to our InputsNode *if*
if (n > 0)
{
for (auto &[inputName, edges] : proxyInputs_->inputEdges())
{
for (const auto edge : edges)
{
debug("{}LoopGraph::process() - Pulling edge {}...\n", GraphDebug::indent(), edge->definition().asString());
switch (edge->pull())
{
case (NodeConstants::ProcessResult::Failed):
case (NodeConstants::ProcessResult::InputsNotSatisfied):
return NodeConstants::ProcessResult::Failed;
case (NodeConstants::ProcessResult::Success):
case (NodeConstants::ProcessResult::Unchanged):
break;
}
}
}
}

// Process child nodes, returning early if we encounter an error
auto result = Graph::process();
if (result == NodeConstants::ProcessResult::Failed || result == NodeConstants::ProcessResult::InputsNotSatisfied)
return NodeConstants::ProcessResult::Failed;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we had discussed your proposal during the stand-up, I had misunderstood and thought that you were putting this logic into the input nodes. Having this in the node answers 95% of my concerns.

* ----------------------------------
*/
root_.x = 0;
root_.y = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suspicion would be that, in the longer term, most of these test graphs would be loaded from a TOML file instead of crafting the graphs by hand.

// first iteration of the loop. So, we only pull from inputs to our InputsNode (proxyInputs_) after the first
// iteration. Furthermore, we do this manually here rather than let InputsNode pull its own edges as otherwise we
// get into an infinite loop when the graph tries to run. In this sense the

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like there was more to come here!

Copy link
Contributor

@RobBuchananCompPhys RobBuchananCompPhys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My feeling is that there are some similarities we have converged on, but also differences in the complexity and robustness of the looping strategies. Personally, my preference would be to implement something simpler, which the other PR demonstrates (in about half the lines of code)! This economy, I find, is easier to reason about. However, there are short comings there which I think you have covered here particularly with regards to robustness of version indexing, and as you mentioned in the stand up, ensuring that the loop graph isn't re-traversed when an external node is changed, which I hadn't taken into account. So, ideally I'd like to go with the simpler option and see how it holds up for now, but if you and Adam think this is the one that's fine also.

@trisyoungs trisyoungs force-pushed the dissolve2/loop-subgraph branch from fd93228 to 4452f29 Compare November 11, 2025 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants