---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.17.3
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---

# Parameter Bounds
This tutorial shows how to place simple bounds on the values of a model's parameters.  This can be especially useful when building your own objects (see the [custom operator tutorial](CustomOperator)) that might have restrictions on their allowed parameters.

First, we construct a model.  It doesn't really matter what type of model this is, as the procedure we outline below is the same regardless of the model type.  We choose to create a simple 1-qubit model.

```{code-cell} ipython3
import pygsti
from pygsti.modelpacks import smq1Q_XYI

mdl = smq1Q_XYI.target_model("H+S")
```

Next, we choose which parameter(s) to add bounds for.  Let's suppose that we want to restrict the Hamiltonian (coherent) Z error rate on the $X(\pi/2)$ gate to be between 0 and 0.2, restricting the gate error to being *over*-rotation of magnitude at most 0.2 radians.  First, let's find the modelmember and parameter value we want to bound.

```{code-cell} ipython3
# Here's the X(pi/2) gate:
print(mdl.operations[('Gxpi2', 0)])
```

```{code-cell} ipython3
# this is the error generator whose parameters we want to bound
eg = mdl.operations[('Gxpi2', 0)].factorops[1].errorgen
for i, lbl in enumerate(eg.parameter_labels):
    print(i, lbl)
```

This shows us that we want to set bounds for the parameter with index 2.  Currently the bounds are set to `None`, which means there aren't any:

```{code-cell} ipython3
print(eg.parameter_bounds)
```

We can set the `parameter_bounds` attribute of a model member to be a 2D NumPy array of shape (num_params, 2), that has rows equal to (min, max) values for each parameter.  We can use `numpy.inf` and `-numpy.inf` for parameters where we don't want one or both bounds.

```{code-cell} ipython3
import numpy as np
bounds = np.empty((eg.num_params, 2), 'd')
bounds[:, 0] = -np.inf  # initial lower bounds
bounds[:, 1] = np.inf   # initial upper bounds
bounds[2, :] = (0, 0.2) # bounds for "Z Hamiltonian error coefficient" parameter
eg.parameter_bounds = bounds
```

After we've set the bounds on constituent members, we should retrieve the model's number of parameters (`.num_params`) to ensure that the bounds get propagated throughout the model:

```{code-cell} ipython3
print("Model bounds before getting the number of parameters:\n",mdl.parameter_bounds)
mdl.num_params  # triggers re-building of model's parameters and parameter bounds based on its members.
print("\nModel bounds after getting the number of parameters:\n",mdl.parameter_bounds)
```

Now, when model `mdl` is optimized, the optimizer will limit range of this parameter as desired.


