Simulations - alternative ways to distribute ticks

I'm using node-red 3 to simulate processes.
I find myself distributing a setup and a tick signal to almost all nodes. That makes the flows visually cluttered.
Are there alternatives to handle global signals that many nodes use ?
Having events available (+ the possibility to handle events in function) nodes would be really nice. I've read the comments that this would break the wiring paradigm of node-red, but in this case that would actually be a relief. The wires would still be used for 'normal' information-carrying signals.
I've been searching the docs for event-handling but without success so far.

When I have done this in the past I have used the message itself to be the tick. So every second (or whatever the simulation period) a message is injected with the latest process values and it flows through the simulation and produces a new value at the output.
Or even just let the messages flow round, with a delay in the loop to simulate the passing of time. For instance, here is a subflow that simulates a process delay and a couple of RC constants. The technique may not be appropriate for more complex processes though.

[{"id":"1156e9cbb379d09c","type":"subflow","name":"Process Simulation (2)","info":"","in":[{"x":37,"y":103,"wires":[{"id":"5f7bb53fd5e8503c"}]}],"out":[{"x":728.5,"y":294,"wires":[{"id":"f423d62b0901b0c3","port":0}]}]},{"id":"a78eda7549e10a97","type":"function","z":"1156e9cbb379d09c","name":"30 sec RC + 20","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 30*1000;       // time constant in milliseconds\n\nvar lastValue = context.get('lastValue');\nif (typeof lastValue == \"undefined\") lastValue = msg.payload;\nvar lastTime = context.get('lastTime') || null;\nvar now = new Date();\nvar currentValue = msg.payload;\nif (lastTime === null) {\n    // first time through\n    newValue = currentValue;\n} else {\n    var dt = now - lastTime;\n    var newValue;\n    \n    if (dt > 0) {\n        var dtotc = dt / tc;\n        newValue = lastValue * (1 - dtotc) + currentValue * dtotc;\n    } else {\n        // no time has elapsed leave output the same as last time\n        newValue = lastValue;\n    }\n}\ncontext.set('lastValue', newValue);\ncontext.set('lastTime', now);\n\nmsg.payload = newValue + 20;\nreturn msg;","outputs":1,"noerr":0,"x":626.5,"y":207,"wires":[["f423d62b0901b0c3"]]},{"id":"87d30eb526f1da83","type":"inject","z":"1156e9cbb379d09c","name":"Inject -0.2 at start","repeat":"","crontab":"","once":true,"topic":"","payload":"-0.2","payloadType":"num","x":134.5,"y":30,"wires":[["5f7bb53fd5e8503c"]]},{"id":"4d29fb5c58ae7a0d","type":"function","z":"1156e9cbb379d09c","name":"10 sec RC","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 10*1000;       // time constant in milliseconds\n\nvar lastValue = context.get('lastValue');\nif (typeof lastValue == \"undefined\") lastValue = msg.payload;\nvar lastTime = context.get('lastTime') || null;\nvar now = new Date();\nvar currentValue = msg.payload;\nif (lastTime === null) {\n    // first time through\n    newValue = currentValue;\n} else {\n    var dt = now - lastTime;\n    var newValue;\n    \n    if (dt > 0) {\n        var dtotc = dt / tc;\n        newValue = lastValue * (1 - dtotc) + currentValue * dtotc;\n    } else {\n        // no time has elapsed leave output the same as last time\n        newValue = lastValue;\n    }\n}\ncontext.set('lastValue', newValue);\ncontext.set('lastTime', now);\n\nmsg.payload = newValue;\nreturn msg;","outputs":1,"noerr":0,"x":451,"y":207,"wires":[["a78eda7549e10a97"]]},{"id":"5f7bb53fd5e8503c","type":"delay","z":"1156e9cbb379d09c","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":268,"y":104,"wires":[["1d73e8daa47ff9a2"]]},{"id":"e3afd7e9a2a0baa0","type":"function","z":"1156e9cbb379d09c","name":"2 msg transport delay","func":"// stores messages in a fifo until the specified number have been received, \n// then releases them as new messages are received.\n// during the filling phase the earliest message is passed on each time \n// a message is received, but it is also left in the fifo\nvar fifoMaxLength = 2;\nvar fifo = context.get('fifo') || [];\n// push the new message onto the top of the array, messages are shifted down and\n// drop off the front\nvar length = fifo.push(msg);  // returns new length\nif (length > fifoMaxLength) {\n    newMsg = fifo.shift();\n} else {\n    // not full yet, make a copy of the msg and pass it on\n    var newMsg = JSON.parse(JSON.stringify(fifo[0]));\n}\ncontext.set('fifo', fifo);\nreturn newMsg;","outputs":1,"noerr":0,"x":258,"y":208,"wires":[["4d29fb5c58ae7a0d"]]},{"id":"f423d62b0901b0c3","type":"function","z":"1156e9cbb379d09c","name":"Clear all except payload","func":"msg2 = {payload: msg.payload};\nreturn msg2;","outputs":1,"noerr":0,"x":545,"y":293,"wires":[[]]},{"id":"1d73e8daa47ff9a2","type":"range","z":"1156e9cbb379d09c","minin":"0","maxin":"1","minout":"0","maxout":"100","action":"scale","round":false,"name":"","x":87,"y":208,"wires":[["e3afd7e9a2a0baa0"]]}]

@Colin
thank for the tip, I had actually tried that but it gives me two problems

  • I still need to disperse the tick and reset housekeeping signals to all the first nodes in every chain which clutters the flows.
  • In my energy management simulation, when linking two or more (let's say n) consumer nodes - which can have different power ratings - to a storage node, the energy consumption messages do arrive at n times the usual speed, but the tick signal itself also arrives at n times the normal rate so I lose the capability to discern how much total consumption is linked to the storage.

If you think about it, the global context is obviously a way to distribute information (variables) outside of the node-and-wires paradigm. So it is not that far fetched to have the same option for events

I found this work-around but it is not very pretty
on tick startup function node with thisstartup code

setInterval(() => {
    const timerArray = global.get('timer')
    timerArray.forEach(callback);
}, 1000)

This startup code in the regular function nodes (producers, consumers, storage, power meters, ...)

const myCallback = () => { node.send({ topic: 'consumer', payload: 200 })}
global.set('timer', (global.get('timer') ?? []).push(myCallback))

I don't have a clear idea of your problem but especially because it is an event you can use the link nodes to unclutter.

Your ticks and init node can go to a link in node. And you can distribute this with a link out node. 1- n is possible here.