Graph finalization

After a graph is flattened into its simple blox, but before it is turned into an ODEProblem/SDEProblem, it passes through a finalization step. Finalization is a hook for post-processing that can only be done once the global structure of the flattened graph is known — for example, neuromodulatory wiring where a dopamine receptor must connect to every glutamatergic synapse already targeting a particular neuron.

finalize(g::GraphSystem) copies the graph and walks its nodes, calling

finalize!(g, node, lookup)

where lookup is a pair of Dict mapping nodes to their incoming and outgoing connections. It is a named tuple (; incoming, outgoing), and lookup.incoming[node] is the list of connection records whose destination is node, and lookup.outgoing[node] those whose source is node.

The default behavior of finalize! is a no-op; you add behaviour by writing methods of GraphDynamics.finalize! that dispatch on the node type, exactly as you would for system_wiring_rule!. A finalize! method is free to add new connections to g, and this is the intended way to wire up structure that depends on the whole flattened graph.

Nodes of both the composite (top-level) graph and the flattened graph can have finalization steps. Action-selection blox, for instance, exist only at the composite level, but their incoming blox must be connected at finalization. When a graph is finalized, nodes of both the top-level graph and flattened graph will be traversed and finalized.