Advanced Topics

Parallelization

By default, with the exception of simulations, SPINS computes the computational graph serially and does not exploit any parallelism. Simulations, on the other hand, are parallelized as much as possible by default. Because function and gradient evaluation is dominated by the simulation time, you typically do not need to change these defaults. However, if you may choose to change this behavior on any node by calling the parallelize method on any ProblemGraphNode:

# Parallelize computation of the node.
node.parallelize()

# Disables parallelization.
node.parallelize(False)

Note that turning on parallelization may actually cause a decrease in performance due to the overhead in the setup. Keep in mind that the parallelization operates by grouping together and executing in parallel operations that (1) are marked for parallel computing and (2) can be executed independently (i.e. no direct or indirect dependency between the nodes. Therefore, there may not be any true parallelization in effect if these conditons are never met during the execution of the computational graph.

Array Flows

An array flow is, in essence, a list of other flows. Array flows are used to group together multiple flows into a single flow. Working with array flows is similar to working with arrays:

# Array flow is created by passing an array of flows.
flow = goos.ArrayFlow([goos.NumericFlow(4), goos.ShapeFlow()])

# Use indexing to set and get nth flow.
# Prints 4.
print(flow[0].array)
flow[1].pos = np.array([3, 4, 5])

# Prints 2.
print(len(flow))

ArrayFlow.Grad works similarly:

# Array flow is created by passing an array of flows.
flow = goos.ArrayFlow.Grad([goos.NumericFlow.Grad(4), goos.ShapeFlow.Grad()])

# Use indexing to set and get nth flow.
# Prints 4.
print(flow[0].array_array_grad)
flow[1].pos_grad = np.array([3, 4, 5])

Additionally, ArrayFlow.Grad supports adding multiple array flows together. When doing this summation, a flow added to None is just the flow itself:

flow1 = goos.ArrayFlow.Grad([goos.NumericFlow.Grad(1),
                             goos.NumericFlow.Grad(2)])
flow2 = goos.ArrayFlow.Grad([goos.NumericFlow.Grad(3),
                             goos.NumericFlow.Grad(4)])
flow3 = goos.ArrayFlow.Grad([None, goos.NumericFlow.Grad(5)])
flow4 = goos.ArrayFlow.Grad([None, None])

flow1 + flow2 == goos.ArrayFlow.Grad([goos.NumericFlow.Grad(4),
                                      goos.NumericFlow.Grad(6)])

flow1 + flow3 == goos.ArrayFlow.Grad([goos.NumericFlow.Grad(1),
                                      goos.NumericFlow.Grad(5)])

flow1 + flow4 == flow1

Using ArrayFlowOpMixin

For any node that produces an array flow, it is recommended that the node inherits ArrayFlowOpMixin. This mixin overloads the indexing operator so that individual elements of the output array flow can be easily accessed. Suppose we have a node MyNode that produces an array flow with two elements. Then, by inheriting from ArrayFlowOpMixin, we can compute the sum as follows:

# Computes the next two elements in the Fibonacci sequence.
class FibonacciNode(goos.ArrayFlowOpMixin, goos.ProblemGraphNode):

  def __init__(self, in1: goos.Function, in2: goos.Function) -> None:
    super().__init__([in1, in2], flow_types=[goos.Function, goos.Function])
    ...

  def eval(self, inputs: List[goos.NumericFlow]) -> goos.ArrayFlow:
    fib_next = inputs[0].array + inputs[1].array
    fib_next_next = inputs[1].array + fib_next
    return goos.ArrayFlow([goos.NumericFlow(fib_next),
                           goos.NumericFlow(fib_next_next)])

  ...

node = FibonacciNode(...)
out_sum = node[0] * node[1]

Note that order of inheritance. Because it is a mixin, you should inherit from ArrayFlowOpMixin before ProblemGraphNode (or any other node class). Additionally, we pass an array flow_types to the mixin constructor. This array sets the type of node that is returned when performing the indexing operation.

You may also choose to set flow_names, which enables indexing by name instead of by number:

class FibonacciNode(goos.ArrayFlowOpMixin, goos.ProblemGraphNode):

  def __init__(self, ...) -> None:
    super().__init__(...,
                     flow_types=[goos.Function, goos.Function],
                     flow_names=["first", "second"])
    ...

  ...

node = FibonacciNode(...)
out_sum = node["first"] * node["second"]

Using IndexOp

You can also “manually” extract an element from an ArrayFlow node by using the IndexOp node:

# Computes the next two elements in the Fibonacci sequence.
class FibonacciNode(goos.ProblemGraphNode):

  def __init__(self, in1: goos.Function, in2: goos.Function) -> None:
    super().__init__([in1, in2])
    ...

  def eval(self, inputs: List[goos.NumericFlow]) -> goos.ArrayFlow:
    fib_next = inputs[0].array + inputs[1].array
    fib_next_next = inputs[1].array + fib_next
    return goos.ArrayFlow([goos.NumericFlow(fib_next),
                           goos.NumericFlow(fib_next_next)])

  ...

node = FibonacciNode(...)
out_sum = (goos.cast(IndexOp(node, 0), goos.Function)
           * goos.cast(IndexOp(node, 1), goos.Function)

Note that we had to cast the output node into goos.Function before being able to use arithemetic operations. This arises from the fact that IndexOp inherits directly from ProblemGraphNode, so arithmetic operations, which can only operate on Function cannot be directly performed.