Composing and Parallelizing
Sequential Steps
When one sits down to perform uncertainty propagation, it can be overwhelming. The measurement models can be complex. The thought of computing the necessary partial derivatives can be intimidating. However, many models can be factored into a series of simple steps - compute this and then compute that from this. Taken one-by-one, it is likely to be a lot easier to compute the partial derivatives for the simple steps than for the full model.
Fortunately, we have the chain-rule of differential calculus. The chain rule says that
\[\frac{δf(g(x))}{δx} = \left. \frac{δf(y)}{δy}\right |_{y=g(x)} \frac{δg(x)}{δx}\]
The key fact here is that we can break a complex problem into a series of simpler steps. We don't need to compute $\frac{δf(g(x))}{δx}$ directly. We can compute $\frac{δf(y)}{δy}$ and $\frac{δg(x)}{δx}$ and use the chain rule to compute $\frac{δf(g(x))}{δx}$.
In the multivariate world, it gets even better. If $F(X)$ and $G(Y)$ are a vector functions of a vector variable and $J[F(X)]$ and $J[G(Y)]$ are the Jacobians of $F(X)$ and $G(Y)$ respectively then
\[J[G(F(X))] = \left. J[G(Y)] \right |_{Y=G(X)} J[F(X)]\]
That is to say, if we compute the Jacobian matrix for each step, we can compute the Jacobian for the entire calculation by taking the product of the Jacobians. All the bookkeeping necessary handle all the variables is performed by matrix products. This is far simpler than the univariate measurement model case outlined in the BIPM GUM. For comparison, consider equation 13 on page 21 of the GUM. Equation 13 is exactly equivalent to the Jacobian expression in the univariate case but the bookkeeping is so much less clear.
The equivalent of equation 13 in the multivariate case is
\[U(F(X)) = J[F(X)] U(X) J[F(X)]^T\]
Clean, simple, straighforward.
From a practical perspective, this library allows you to implementing MeasurementModel
types to represent each step in a calculation. So let's say we implement measurement models MM1
, MM2
and MM3
. The output of MM1
is a superset of the values required as input to MM2
and the output of MM2
is a superset of the values required as input to MM3
. We can create an object to represent the composition of these models using the "compose operator" ∘ - MM1to3 = MM3 ∘ MM2 ∘ MM1
. MM1to3(X)
is conceptually equivalent to MM3(MM2(MM1(X)))
. (The ∘ operator is syntactic sugar for the ComposedMeasurementModel
type.)
Like we did on the Getting Started page, it is possible to apply the composed MeasurementModel
MM1to3
to either input represented by an UncertainValues
object (the full uncertainty calculation) or a LabeledValues
object (just the function evaluation). The computational cost of the entire calculation is roughly the sum of the cost of the individual steps plus the matrix products of the Jacobians.
One subtlety - sometimes it is necessary to pass variables unmodified from one step to the next. This is of course handled trivially by the identity function with derivative of unity. Since this is common, there is a special mechanism to handle this using the MaintainInputs
or AllInputs
MeasurementModel
s combined with the next concept - parallel steps.
See the "Resistor Network Example" for a (somewhat) simple multi-step computation.
Parallel Steps
In some measurement models, the same calculation is repeated on multiple sets of input data. In this case, it is useful to be able to apply the same MeasurementModel
to different sub-sets of the data. For example, if you want to compute the X-ray mass absorption coefficient (MAC) for a series of different X-rays lines, you might define a generic MeasurementModel
ComputeMAC
that computes the MAC for a single X-ray line. You could apply the model sequentially as described in "sequential steps". However, it would often be more efficient to compute the MACs in parallel as a single step.
There are two operators to handle steps that can be calculated in parallel and combined to look like a single step = "^" and "|". The first of these, "^", uses @threads
to parallelize the calculation. The second performs the calculation sequentially. If the calculation is simple and quick use "|". If the calculation is longer and more complex, "^" may be faster depending upon the relative cost of the calculation and the cost of spinning up multiple threads. "|" and "^" are syntactic sugar for the "ParallelMeasurementModel" type.
There is a second use for the "|" and "^" operators (or the ParallelMeasurementModel
type) is to pass variables unmodified from one step to the next using the MaintainInputs
and AllInputs
MeasurementModel
types.