7.4 Practical Implementation Considerations 295
the dependency tree in Code 7.5 and note that its complexity is O(M
2
) because
each reaction is checked against every other for its effect. When dealing with large
numbers of reactions, this version would add significantly to the overall runtime—
for practical purposes it does not scale. How can we improve on it? The key is
to recognize that dependency arises via the molecular species, and that both the
number of molecules affected by a single reaction, and the number of molecules
a reaction depends on will often remain fairly constant with increasing numbers
of reactions. So Code 7.9 offers a much more efficient approach to building the
dependency graph. For each reaction, we record which molecules it depends on in
a temporary molecule-dependency list, and then use that intermediate data structure
to determine which reactions are affected by each other. It is worth noting that we
end up with exactly the same data structure for the dependency graph as in the
original version, but the complexity of the building process is more like O(M) rather
than O(M
2
).
7.4.2 Programming Techniques—Tree Updating
We can further illustrate the impact of our implementation choices on runtime with a
look at the way in which the propensity tree of the SSA-CR method is updated. The
tree holds the group propensity values in its leaves and the cumulative propensity
value, a
0
, in its root. Each time a reaction’s propensity is affected by the occurrence
of another reaction, its group’s propensity must be adjusted. In Sect. 7.3.2 we noted
that it is best to defer updating the propensity tree until all the group changes are
known at the end of a single step, rather than updating it each time a reaction’s
propensity is adjusted. That way we can avoid multiple updates to the tree for the
same group. We suggested storing the changed groups’ identities in a set to avoid
updating the tree multiple times for the same group. The idea is that, once all the
individual reaction changes have been implemented within the groups, each leaf
value for the changed groups is updated and trickled up to the root. However, the
process of updating the tree deserves further consideration because it still potentially
involves some duplication of effort. Consider the case where two groups’ leaves
have the same parent node in the tree and both group’s propensities have changed
following a reaction. If the leaf-to-root update is made separately for each leaf,
then most of the work of the first update will be overwritten by the second. The
update process can be streamlined a little, therefore, by actually storing the set of
parent nodes of the group leaves that have been adjusted and recalculating the parent
values as the sum of their two child values, regardless of whether one or both child
values has been adjusted. However, we can take this even further by recognizing
that updating from two leaves with different parent nodes but a common grandparent
node will also involve duplication of update from the grandparent node upwards, and
the argument can be continued for different ancestry levels within the tree. What we
have actually done in our implementation, therefore, is to trickle each update only to
the next level above in the tree, and then repeated this process for each level, until all
the changes eventually accumulate in the root. Each changed node is only updated
once with this approach.