# Designing and Using Models¶

## Introduction¶

The concept of a **model** is key to the use of QInfer. A model defines the
probability distribution over experimental data given hypotheses about the
system of interest, and given a description of the measurement performed. This
distribution is called the *likelihood function*, and it encapsulates the
definition of the model.

In QInfer, likelihood functions are represented as classes inheriting from either
`Model`

, when the likelihood function can be numerically
evaluated, or `Simulatable`

when only samples from the
function can be efficiently generated.

## Using Models and Simulations¶

### Basic Functionality¶

Both `Model`

and `Simulatable`

offer
basic functionality to describe how they are parameterized, what outcomes are
possible, etc. For this example, we will use a premade model from `test_models`

,
`SimplePrecessionModel`

. This model implements the likelihood
function

as can be derived from Born’s Rule for a spin-½ particle prepared and measured in the \(\left|+\right\rangle\) state, and evolved under \(H = \omega \sigma_z / 2\) for some time \(t\).

In this way, we see that by defining the likelihood function in terms of the hypothetical outcome \(d\), the model parameter \(\omega\), and the experimental parameter \(t\), we can reason about the experimental data that we would extract from the system.

In order to use this likelihood function, we must instantiate the model that
implements the likelihood. Since `SimplePrecessionModel`

is
provided with QInfer, we can simply import it and make an instance.

```
>>> from qinfer.test_models import SimplePrecessionModel
>>> m = SimplePrecessionModel()
```

Once a model or simulator has been created, you can query how many model parameters it admits and how many outcomes a given experiment can have.

```
>>> print m.n_modelparams
1
>>> print m.modelparam_names
['\\omega']
>>> print m.is_n_outcomes_constant
True
>>> print m.n_outcomes(expparams=0)
2
```

### Model and Experiment Parameters¶

The division between unknown parameters that we are trying to learn (\(\omega\)
in the `SimplePrecessionModel`

example) and the controls that we can use to
design measurements (\(t\)) is generic, and is key to how QInfer handles
the problem of parameter estimation.
Roughly speaking, model parameters are real numbers that represent properties
of the system that we would like to learn, whereas experiment parameters
represent the choices we get to make in performing measurements.

Model parameters are represented by NumPy arrays of dtype `float`

and that
have two indices, one representing which model is being considered and one
representing which parameter. That is, model parameters are defined by matrices
such that the element \(X_ij\) is the \(j^{\text{th}}\) parameter of
the model parameter vector \(\vec{x}_i\).

By contrast, since not all experiment parameters are best represented by
the data type `float`

, we cannot use an array of homogeneous dtype unless there
is only one experimental parameter. The alternative is to use NumPy’s
record array functionality to specify the
*heterogeneous* type of the experiment parameters. To do so, instead of using
a second index to refer to specific experiment parameters, we use *fields*.
Each field then has its own dtype.

For instance, a dtype of `[('t', 'float'), ('basis', 'int')]`

specifies that
an array has two fields, named `t`

and `basis`

, having dtypes of `float`

and `int`

, respectively. Such arrays are initialized by passing lists of
*tuples*, one for each field:

```
>>> import numpy as np
>>> eps = np.array([
... (12.3, 2),
... (14.1, 1)
... ], dtype=[('t', 'float'), ('basis', 'int')])
>>> print eps
[(12.3, 2) (14.1, 1)]
>>> print eps.shape
(2,)
```

Once we have made a record array, we can then index by field names to get out each field as an array of that field’s value in each record, or we can index by record to get all fields.

```
>>> print eps['t']
[ 12.3 14.1]
>>> print eps['basis']
[2 1]
>>> print eps[0]
(12.3, 2)
```

Model classes specify the dtypes of their experimental parameters with the
property `expparams_dtype`

. Thus, a common
idiom is to pass this property to the dtype keyword of NumPy functions. For
example, the model class `BinomialModel`

adds an `int`

field specifying how many times a two-outcome measurement is repeated, so to
specify that we can use its `expparams_dtype`

:

```
>>> from qinfer.derived_models import BinomialModel
>>> bm = BinomialModel(m)
>>> print bm.expparams_dtype
[('x', 'float'), ('n_meas', 'uint')]
>>> eps = np.array([
... (11.0, 20)
... ], dtype=bm.expparams_dtype)
```

### Simulation¶

Both models and simulators allow for simulated data to be drawn from the
model distribution using the `simulate_experiment()`

method. This method takes a matrix of model parameters and a vector of experiment
parameter records or scalars (depending on the model or simulator),
then returns an array of sample data, one sample for each combination of model
and experiment parameters.

```
>>> import numpy as np
>>> modelparams = np.linspace(0, 1, 100)
>>> expparams = np.arange(1, 10) * np.pi / 2
>>> D = m.simulate_experiment(modelparams, expparams, repeat=3)
>>> print type(D)
<type 'numpy.ndarray'>
>>> print D.shape
(3, 100, 9)
```

If exactly one datum is requested, `simulate_experiment()`

will return a scalar:

```
>>> print m.simulate_experiment(np.array([0.5]), np.array([3.5 * np.pi]), repeat=1).shape
()
```

Note that in NumPy, a shape tuple of length zero indicates a scalar value, as such an array has no indices.

### Likelihooods¶

The core functionality of `Model`

, however, is the
`likelihood()`

method. This takes vectors of outcomes,
model parameters and experiment parameters, then returns for each combination
of the three the corresponding probability \(\Pr(d | \vec{x}; \vec{e})\).

```
>>> import numpy as np
>>> modelparams = np.linspace(0, 1, 100)
>>> expparams = np.arange(1, 10) * np.pi / 2
>>> outcomes = np.array([0], dtype=int)
>>> L = m.likelihood(outcomes, modelparams, expparams)
```

The return value of `likelihood()`

is a three-index
array of probabilities whose shape is given by the lengths of `outcomes`

,
`modelparams`

and `expparams`

.
In particular, `likelihood()`

returns a rank-three
tensor \(L_{ijk} := \Pr(d_i | \vec{x}_j; \vec{e}_k)\).

```
>>> print type(L)
<type 'numpy.ndarray'>
>>> print L.shape
(1, 100, 9)
```

## Implementing Custom Simulators and Models¶

In order to implement a custom simulator or model, one must specify metadata describing the number of outcomes, model parameters, experimental parameters, etc. in addition to implementing the simulation and/or likelihood methods.

Here, we demonstrate how to do so by walking through a simple subclass of
`Model`

. For more detail, please see the
API Reference.

Suppose we wish to implement the likelihood function

as may arise in looking, for instance, at an experiment expired by 2D NMR.
This model has two model parameters, \(\omega_1\) and \(\omega_2\), and
so we start by creating a new class and declaring the number of model
parameters as a `property`

:

```
class MultiCosModel(Model):
@property
def n_modelparams(self):
return 2
```

Next, we proceed to add a property and method indicating that this model always admits two outcomes, irrespective of what measurement is performed.

```
@property
def is_n_outcomes_constant(self):
return True
def n_outcomes(self, expparams):
return 2
```

We indicate the valid range for model parameters by returning an array of
dtype `bool`

for each of an input matrix of model parameters, specifying whether
each model vector is valid or not. Typically, this will look like some typical
bounds checking, combined using `logical_and`

and `all`

. Here,
we follow that model by inisting that *all* elements of each model parameter
vector must be at least 0, *and* must not exceed 1.

```
def are_models_valid(self, modelparams):
return np.all(np.logical_and(modelparams > 0, modelparams <= 1), axis=1)
```

Next, we specify what a measurement looks like by defining `expparams_dtype`

.
In this case, we want one field that is an array of two `float`

elements:

```
@property
def expparams_dtype(self):
return [('ts', '2float')]
```

Finally, we write the likelihood itself. Since this is a two-outcome model,
we can calculate the rank-two tensor
\(p_{jk} = \Pr(0 | \vec{x}_j; \vec{e}_k)\) and let
`pr0_to_likelihood_array()`

add an index over
outcomes for us.
To compute \(p_{jk}\) efficiently, it is helpful to do a bit of index
gymnastics using NumPy’s powerful broadcasting rules. In this example, we
set up the calculation to produce terms of the form
\(\cos^2(x_{j,l} e_{k,l} / 2)\) for \(l \in \{0, 1\}\) indicating
whether we’re referring to \(\omega_1\) or \(\omega_2\), respectively.
Multiplying along this axis then gives us the product of the two cosine
functions, and in a way that very nicely generalizes to likelihood functions of
the form

Running through the index gymnastics, we can implement the likelihood function as:

```
def likelihood(self, outcomes, modelparams, expparams):
# We first call the superclass method, which basically
# just makes sure that call count diagnostics are properly
# logged.
super(MultiCosModel, self).likelihood(outcomes, modelparams, expparams)
# Next, since we have a two-outcome model, everything is defined by
# Pr(0 | modelparams; expparams), so we find the probability of 0
# for each model and each experiment.
#
# We do so by taking a product along the modelparam index (len 2,
# indicating omega_1 or omega_2), then squaring the result.
pr0 = np.prod(
np.cos(
# shape (n_models, 1, 2)
modelparams[:, np.newaxis, :] *
# shape (n_experiments, 2)
expparams['ts']
), # <- broadcasts to shape (n_models, n_experiments, 2).
axis=2 # <- product over the final index (len 2)
) ** 2 # square each element
# Now we use pr0_to_likelihood_array to turn this two index array
# above into the form expected by SMCUpdater and other consumers
# of likelihood().
return Model.pr0_to_likelihood_array(outcomes, pr0)
```

Our new custom model is now ready to use!

## Adding Functionality to Models with Other Models¶

QInfer also provides model classes which add functionality or otherwise modify
other models. For instance, the `BinomialModel`

class accepts instances
of two-outcome models and then represents the likelihood for many repeated
measurements of that model. This is especially useful in cases where
experimental concerns make switching experiments costly, such that repeated
measurements make sense.

To use `BinomialModel`

, simply provide an instance of another model
class:

```
>>> from qinfer.test_models import SimplePrecessionModel
>>> from qinfer.derived_models import BinomialModel
>>> bin_model = BinomialModel(SimplePrecessionModel())
```

Experiments for `BinomialModel`

have an additional field from the
underlying models, called `n_meas`

. If the original model used scalar
experiment parameters (e.g.: `expparams_dtype`

is `float`

), then the original
scalar will be referred to by a field `x`

.

```
>>> import numpy as np
>>> eps = np.array([(12.1, 10)], dtype=bin_model.expparams_dtype)
>>> print eps['x'], eps['n_meas']
[ 12.1] [10]
```

Another model which *decorates* other models in this way is `PoisonedModel`

,
which is discussed in more detail in Robustness Testing. Roughly,
this model causes the likeihood functions calculated by its underlying model
to be subject to random noise, so that the robustness of an inference algorithm
against such noise can be tested.