How to trigger updates down only selected outputs of component?

Hi everyone!

So, the high-level questions is: is there any way to select which outputs of a component trigger an update?

Example: I have a custom-made component that takes an input mutableIn whichever, and outputs mutableOut and immutableOut. Given any change in the output of mutableIn, I want the output of mutableOut to change (and trigger new solver instances down the graph), but the value of immutableOut to remain the same and not trigger solve instances down its graph.

Under this assumption, in the following example, the data recorder on immutableOut should remain with one single instance of “immutable” no matter how much we fiddled with the slider:

The big picture: I am trying to develop a WebSocket listener with periodic updates which should only trigger selected updates if the value of the stream of messages that is receiving changes over a certain threshold…

Thanks!

JL

I think heteroptera has components which do that (Event Gate, Event Switch). Not sure if they meet your requirements 100%, have not tested it.

Generally avoiding multiple outputs is not so hard, but the problem is to avoid that downstream objects get expired, which I think per default is still the case even if the data in the parameters doesn’t change.

There is a problem with the ordering of events that may make this impossible.

First the slider is moved (or any other event which invalidates the source of mutableIn), this causes the mutableIn parameter to be expired. This in turn causes the component to expire itself and all it’s output parameter, which in turn expire all their recipient objects.

Then a new solution is started and the slider provides its new value to mutableIn. At this point it is too late to not recompute the outputs if the new slider value happens to be the same as the old slider value. They are already expired.

If you override the expiration behaviour of your component to not expire the immutableOut parameter, then when the next solution comes around and it turns out a different value was inputted it is now too late to expire the output. You’re not allowed to expire objects during solutions, otherwise you could get into states where objects keep expiring each other and you’d never finish.

The only way I see is to switch to a double solution approach:

  1. The input parameter expires.
  2. You override the component expiration logic and only expire the mutableOut parameter. You leave the immutableOut in a completed state.
  3. During the next solution you straight up copy the data from mutableIn to mutableOut.
  4. During the same solution you check to see whether the current data inside mutableIn is the same as it was before. If it is, you’re done. If not, you set a boolean flag on your component and schedule a new solution with a callback.
  5. During this callback you expire the component, but this time you only expire the immutableOut and copy the data there.

It’s pretty counter to the way GH normally works so there may be a whole bunch of special cases that are problematic. I’ll try and come up with some code.

So here’s some code, using the simplest set up I could think of. Integer data only, single input, single output. The component tests whether the new data is different from the previous data, and if it is, sets a boolean and triggers a new solution just after the current one completes.

using System;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;

namespace TestComponent
{
  public sealed class SmartUpdateComponent : GH_Component
  {
    public SmartUpdateComponent()
      : base("Smart Update", "Smupdate", "Only trigger an update if a value changes.", "Test", "Test")
    {
      UpdateOutput = true;
      PreviousData = "none";
    }
    public override Guid ComponentGuid => new Guid("{60F1F671-78F5-4A23-87EA-CC2BF6B6C296}");

    protected override void RegisterInputParams(GH_InputParamManager pManager)
    {
      pManager.AddIntegerParameter("Input", "In", "Data input.", GH_ParamAccess.tree);
      pManager[0].Optional = true;
    }
    protected override void RegisterOutputParams(GH_OutputParamManager pManager)
    {
      pManager.AddIntegerParameter("Output", "Out", "Data output.", GH_ParamAccess.tree);
    }

    /// <summary>
    /// Gets or sets whether the immutable output ought to be assigned.
    /// </summary>
    private bool UpdateOutput { get; set; }
    /// <summary>
    /// Gets or sets the cached data from last time.
    /// </summary>
    private string PreviousData { get; set; }

    protected override void ExpireDownStreamObjects()
    {
      if (UpdateOutput)
        Params.Output[0].ExpireSolution(false);
    }
    protected override void SolveInstance(IGH_DataAccess access)
    {
      // This stops the component from assigning nulls 
      // if we don't assign anything to an output.
      access.DisableGapLogic();

      // Get the current tree and immediately assign it to the output.
      // Better safe than sorry. Since we only selectively expire the
      // output we should still prevent updates, however there is no
      // reason to not have the most recent data always in the output.
      access.GetDataTree(0, out GH_Structure<GH_Integer> tree);
      access.SetDataTree(0, tree);

      string currentData = tree.DataDescription(false, true);

      // If we were supposed to update the output (meaning it was expired), 
      // then we know for sure that we don't have to update again.
      if (UpdateOutput)
      {
        UpdateOutput = false;
        PreviousData = currentData;
        return;
      }

      // If the current data differs from the last time,
      // we need to remember that the output needs updating and
      // we need to schedule a new solution so we can actually do this.
      if (!string.Equals(PreviousData, currentData))
      {
        UpdateOutput = true;
        PreviousData = currentData;

        var doc = OnPingDocument();
        doc?.ScheduleSolution(5, Callback);
      }
    }
    private void Callback(GH_Document doc)
    {
      // The logic is all in our expiration method, but we do have 
      // to expire this component.
      if (UpdateOutput)
        ExpireSolution(false);
    }
  }
}

This file requires the compiled component:
smart update test.gh (14.5 KB)

1 Like

You can completely disable standart expiration logic by overriding ExpireDownstreamObjects with an empty method, but then you must handle all downstream expirations manually.

Take a look at the Gate component https://github.com/mazhuravlev/grasshopper-addons which implements custom input expiration logic as well. Input parameter of this component signals when it has been updated, so component knows, which input was expired. Output expiration is triggered on demand with ComponentExtensions methods like ExpireOutput.

public static void ExpireOutput(this IGH_Component component, int output)
{
	foreach (var receiver in component.Params.Output[output].Recipients)
	{
		receiver.ExpireSolution(true);
	}
}

gate

Wow, what an interesting thread. Thanks @DavidRutten and @mazhuravlev!

Extending on David’s example, here is some code that would work with multiple IOs, and expires the outputs whose solution needs an update. Only some additional handling on when to schedule new updates was necessary:

using System;
using System.Collections.Generic;

using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;
using Rhino.Geometry;

namespace MachinaGrasshopper.Bridge
{
    // From: https://discourse.mcneel.com/t/how-to-trigger-updates-down-only-selected-outputs-of-component/68441
    public sealed class SmartUpdateComponentMultipleInputs : GH_Component
    {
        /// <summary>
        /// Number of IOs this component will have. 
        /// Must be hardcoded in the component since RegisterInputParams runs before the constructor.
        /// </summary>
        private const int IOCount = 4;

        /// <summary>
        /// Gets or sets whether the immutable output ought to be assigned.
        /// </summary>
        public bool[] UpdateOutput { get; set; }

        /// <summary>
        /// Gets or sets the cached data from last time.
        /// </summary>
        public string[] PreviousData { get; set; }

        public SmartUpdateComponentMultipleInputs()
          : base("Smart Update Multiple Inputs", "SmupdateMI", "Only trigger an update if a value changes.", "Test", "Test")
        {
            UpdateOutput = new bool[IOCount];
            PreviousData = new string[IOCount];
            for (int i = 0; i < IOCount; i++)
            {
                UpdateOutput[i] = true;
                PreviousData[i] = "nastideplasti";
            }
        }
        public override Guid ComponentGuid => new Guid("3736547e-360a-4fff-9ed5-a05406cc43c1");

        protected override void RegisterInputParams(GH_InputParamManager pManager)
        {
            for (int i = 0; i < IOCount; i++)
            {
                pManager.AddIntegerParameter("in" + i, "in" + i, "Data Input " + i, GH_ParamAccess.tree);
                pManager[i].Optional = true;
            }
        }
        protected override void RegisterOutputParams(GH_OutputParamManager pManager)
        {
            for (int i = 0; i < IOCount; i++)
            {
                pManager.AddIntegerParameter("out" + i, "out" + i, "Data Output " + i, GH_ParamAccess.tree);
                pManager[i].Optional = true;
            }
        }


        /// <summary>
        /// Override the behavior of when outputs are expired
        /// </summary>
        protected override void ExpireDownStreamObjects()
        {
            for (int i = 0; i < IOCount; i++)
            {
                if (UpdateOutput[i])
                {
                    Params.Output[i].ExpireSolution(false);
                }
            }

        }

        protected override void SolveInstance(IGH_DataAccess access)
        {
            // This stops the component from assigning nulls 
            // if we don't assign anything to an output.
            access.DisableGapLogic();


            // Get the current tree and immediately assign it to the output.
            // Better safe than sorry. Since we only selectively expire the
            // output we should still prevent updates, however there is no
            // reason to not have the most recent data always in the output.
            bool doneWithUpdates = false;
            GH_Structure<GH_Integer> tree;
            string[] currentData = new string[IOCount];
            for (int i = 0; i < IOCount; i++)
            {
                // Check if any input was flagged for an update.
                doneWithUpdates |= UpdateOutput[i];

                access.GetDataTree(i, out tree);
                access.SetDataTree(i, tree);

                currentData[i] = tree.DataDescription(false, true);

                // Unflag inputs that were due for updates. 
                if (UpdateOutput[i])
                {
                    UpdateOutput[i] = false;
                    PreviousData[i] = currentData[i];
                }

            }

            // If any input was flagged for an update, the program is
            // executing the second solution and should stop solving,
            // e.g. if we were supposed to update the output (meaning it 
            // was expired), then we know for sure that we don't have to 
            // update again.
            if (doneWithUpdates)
            {
                return;
            }

            // If the current data differs from the last time,
            // we need to remember that the output needs updating and
            // we need to schedule a new solution so we can actually do this.
            bool scheduleSolution = false;
            for (int i = 0; i < IOCount; i++)
            {
                // Compare int trees by using string description including tree info
                if (!string.Equals(PreviousData[i], currentData[i]))
                {
                    UpdateOutput[i] = true;
                    PreviousData[i] = currentData[i];
                }

                // If flagged any UpdateOutput, we will need to schedule a new solution.
                scheduleSolution |= UpdateOutput[i];
            }

            // Schedule new solution if any ouput needed an update
            if (scheduleSolution)
            {
                var doc = OnPingDocument();
                doc?.ScheduleSolution(5, Callback);
            }
        }

        private void Callback(GH_Document doc)
        {
            // The logic is all in our expiration method, but we do have 
            // to expire this component.
            bool expire = false;
            foreach(var up in UpdateOutput)
            {
                if (up)
                {
                    expire = up;
                    break;
                }
            }

            if (expire)
                ExpireSolution(false);
        }
    }
}  

This results in something like this :slight_smile:

Sample file attached, which needs this code compiled:

smart update test multiple ios.gh (7.2 KB)