# Job Shop Scheduling

Keywords: job shop, scheduling, cbc usage, neos usage, cplex, gdp, disjunctive programming, batch processes

## Imports

The following cell specifies the solver to used in the subsequent calculations. Some of these problems can become quite larger, and therefore the `gurobi` solver has been set as a default. If you don't have the `gurobi` solver then adjust the code to use the `glpk` solver, but know the calculations may take longer (and the benchmark problem will not solve at all). If you do have the `gurobi` solver, edit the location of the executable to match the location on your computer.

```python
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib as mpl
import pandas as pd

import shutil
import sys
import os.path

if not shutil.which("pyomo"):
    !pip install -q pyomo
    assert(shutil.which("pyomo"))

if not (shutil.which("cbc") or os.path.isfile("cbc")):
    if "google.colab" in sys.modules:
        !apt-get install -y -qq coinor-cbc
    else:
        try:
            !conda install -c conda-forge coincbc
        except:
            pass

assert(shutil.which("cbc") or os.path.isfile("cbc"))
from pyomo.environ import *
from pyomo.gdp import *
```

## Background

A job shop consists of a set of distinct machines that process jobs. Each job is a series of tasks that require use of particular machines for known durations, and which must be completed in specified order. The job shop scheduling problem is to schedule the jobs on the machines to minimize the time necessary to process all jobs (i.e, the makespan) or some other metric of productivity. Job shop scheduling is one of the classic problems in Operations Research.

Data consists of two tables. The first table is decomposition of the jobs into a series of tasks. Each task lists a job name, name of the required machine, and task duration. The second table list task pairs where the first task must be completed before the second task can be started. This formulation is quite general, but can also specify situations with no feasible solutions.

## Job shop example

The following example of a job shop is from from Christelle Gueret, Christian Prins, Marc Sevaux, "Applications of Optimization with Xpress-MP," Dash Optimization, 2000.

In this example, there are three printed paper products that must pass through color printing presses in a particular order. The given data consists of a flowsheet showing the order in which each job passes through the color presses

and a table of data showing, in minutes, the amount of time each job requires on each machine.

| Machine | Color | Paper 1 | Paper 2 | Paper 3 |
| :-----: | :---: | :-----: | :-----: | :-----: |
| 1       | Blue  | 45      | 20      | 12      |
| 2       | Green | -       | 10      | 17      |
| 3       | Yellow| 10      | 34      | 28      |

What is the minimum amount of time (i.e, what is the makespan) for this set of jobs?

## Task decomposition

The first step in the analysis is to decompose the process into a series of tasks. Each task is a (job,machine) pair. Some tasks cannot start until a prerequisite task is completed.

| Task (Job,Machine) | Duration | Prerequisite Task |
| :----------------: | :------: | :---------------: |
| (Paper 1, Blue)    | 45       | -                 |
| (Paper 1, Yellow)  | 10       | (Paper 1,Blue)    |
| (Paper 2, Blue)    | 20       | (Paper 2, Green)  |
| (Paper 2, Green)   | 10       | -                 |
| (Paper 2, Yellow)  | 34       | (Paper 2, Blue)   |
| (Paper 3, Blue)    | 12       | (Paper 3, Yellow) |
| (Paper 3, Green)   | 17       | (Paper 3, Blue)   |
| (Paper 3, Yellow)  | 28       | -                 |

We convert this to a JSON style representation where tasks are denoted by (Job,Machine) tuples in Python. The task data is stored in a Python dictionary indexed by (Job,Machine) tuples. The task data conists of a dictionary with duration ('dur') and (Job,Machine) pair for any prerequisite task.

```python
TASKS = {
    ('Paper_1','Blue')   : {'dur': 45, 'prec': None},
    ('Paper_1','Yellow') : {'dur': 10, 'prec': ('Paper_1','Blue')},
    ('Paper_2','Blue')   : {'dur': 20, 'prec': ('Paper_2','Green')},
    ('Paper_2','Green')  : {'dur': 10, 'prec': None},
    ('Paper_2','Yellow') : {'dur': 34, 'prec': ('Paper_2','Blue')},
    ('Paper_3','Blue')   : {'dur': 12, 'prec': ('Paper_3','Yellow')},
    ('Paper_3','Green')  : {'dur': 17, 'prec': ('Paper_3','Blue')},
    ('Paper_3','Yellow') : {'dur': 28, 'prec': None},
}
```

## Model formulation

Each task is indexed by an ordered pair $(j,m)$ where $j$ is a job, and $m$ is a machine. Associated with each task is data describing the time needed to perform the task, and a preceeding task that must be completed before the index task can start.

| Parameter | Description |
| :-------- | :-----------|
| $\text{dur}_{j,m}$ | Duration of task $(j,m)$ |
| $\text{prec}_{j,m}$ | A task $(k,n) = \text{prec}_{j,m}$ that must be completed before task $(j,m)$|

The choice of decision variables for this problem are key to modeling. We introduce $makespan$ as the time needed to complete all tasks. $makespan$ is a candidate objective function. Variable $start_{j,m}$ denotes the time when task $(j,m)$ begins.

| Decision Variables | Description |
| :-------- | :-----------|
| $\text{makespan}$  | Completion of all jobs |
| $\text{start}_{j,m}$ | Start time for task $(j,m)$ |

The constraints include lower bounda on the start and an upper bound on the completion of each task $(j,m)$

\begin{align}
\text{start}_{j,m} & \geq 0\\
\text{start}_{j,m}+\text{dur}_{j,m} & \leq \text{makespan}
\end{align}

Any preceeding tasks must be completed before task $(j,m)$ can start.

\begin{align}
\text{start}_{k,n}+\text{dur}_{k,n}\leq\text{start}_{j,m}\ \ \ \ \text{for } (k,n) =\text{prec}_{j,m}
\end{align}

Finally, for every task performed on machine $m$, there can be no overlap among those tasks. This leads to a set of pair-wise disjunctive constraints for each machine.

\begin{align}
\left[\text{start}_{j,m}+\text{dur}_{j,m} \leq \text{start}_{k,m}\right] \vee \left[\text{start}_{k,m}+\text{dur}_{k,m} \leq \text{start}_{j,m}\right]
\end{align}

avoids conflicts for use of the same machine.

## Pyomo implementation

The job shop scheduling problem is implemented below in Pyomo. The implementation consists of of a function JobShopModel(TASKS) that accepts a dictionary of tasks and returns a Pyomo model.

```python
def jobshop_model(TASKS):

    model = ConcreteModel()

    # tasks is a two dimensional set of (j,m) constructed from the dictionary keys
    model.TASKS = Set(initialize = TASKS.keys(), dimen=2)

    # the set of jobs is constructed from a python set
    model.JOBS = Set(initialize = list(set([j for (j,m) in model.TASKS])))

    # set of machines is constructed from a python set
    model.MACHINES = Set(initialize = list(set([m for (j,m) in model.TASKS])))

    # the order of tasks is constructed as a cross-product of tasks and filtering
    model.TASKORDER = Set(initialize = model.TASKS * model.TASKS, dimen=4,
        filter = lambda model, j, m, k, n: (k,n) == TASKS[(j,m)]['prec'])

    # the set of disjunctions is cross-product of jobs, jobs, and machines
    model.DISJUNCTIONS = Set(initialize = model.JOBS * model.JOBS * model.MACHINES, dimen=3,
        filter = lambda model, j, k, m: j < k and (j,m) in model.TASKS and (k,m) in model.TASKS)

    # load duration data into a model parameter for later access
    model.dur = Param(model.TASKS, initialize=lambda model, j, m: TASKS[(j,m)]['dur'])

    # establish an upper bound on makespan
    ub = sum([model.dur[j, m] for (j,m) in model.TASKS])

    # create decision variables
    model.makespan = Var(bounds=(0, ub))
    model.start = Var(model.TASKS, bounds=(0, ub))

    model.objective = Objective(expr = model.makespan, sense = minimize)

    model.finish = Constraint(model.TASKS, rule=lambda model, j, m:
        model.start[j,m] + model.dur[j,m] <= model.makespan)

    model.preceding = Constraint(model.TASKORDER, rule=lambda model, j, m, k, n:
        model.start[k,n] + model.dur[k,n] <= model.start[j,m])

    model.disjunctions = Disjunction(model.DISJUNCTIONS, rule=lambda model,j,k,m:
        [model.start[j,m] + model.dur[j,m] <= model.start[k,m],
         model.start[k,m] + model.dur[k,m] <= model.start[j,m]])

    TransformationFactory('gdp.hull').apply_to(model)
    return model

jobshop_model(TASKS)
```

    <pyomo.core.base.PyomoModel.ConcreteModel at 0x7fcedaa091b0>

```python
def jobshop_solve(model):
    SolverFactory('cbc').solve(model)
    results = [{'Job': j,
                'Machine': m,
                'Start': model.start[j, m](),
                'Duration': model.dur[j,m],
                'Finish': model.start[(j, m)]() + model.dur[j,m]}
               for j,m in model.TASKS]
    return results

def jobshop(TASKS):
    return jobshop_solve(jobshop_model(TASKS))

results = jobshop(TASKS)
results
```

    [{'Duration': 45,
      'Finish': 87.0,
      'Job': 'Paper_1',
      'Machine': 'Blue',
      'Start': 42.0},
     {'Duration': 10,
      'Finish': 97.0,
      'Job': 'Paper_1',
      'Machine': 'Yellow',
      'Start': 87.0},
     {'Duration': 20,
      'Finish': 30.0,
      'Job': 'Paper_2',
      'Machine': 'Blue',
      'Start': 10.0},
     {'Duration': 10,
      'Finish': 10.0,
      'Job': 'Paper_2',
      'Machine': 'Green',
      'Start': 0.0},
     {'Duration': 34,
      'Finish': 64.0,
      'Job': 'Paper_2',
      'Machine': 'Yellow',
      'Start': 30.0},
     {'Duration': 12,
      'Finish': 42.0,
      'Job': 'Paper_3',
      'Machine': 'Blue',
      'Start': 30.0},
     {'Duration': 17,
      'Finish': 59.0,
      'Job': 'Paper_3',
      'Machine': 'Green',
      'Start': 42.0},
     {'Duration': 28,
      'Finish': 28.0,
      'Job': 'Paper_3',
      'Machine': 'Yellow',
      'Start': 0.0}]

## Printing schedules

```python
schedule = pd.DataFrame(results)

print('\nSchedule by Job')
print(schedule.sort_values(by=['Job','Start']).set_index(['Job', 'Machine']))

print('\nSchedule by Machine')
print(schedule.sort_values(by=['Machine','Start']).set_index(['Machine', 'Job']))
```

    Schedule by Job
                     Start  Duration  Finish
    Job     Machine
    Paper_1 Blue      42.0        45    87.0
            Yellow    87.0        10    97.0
    Paper_2 Green      0.0        10    10.0
            Blue      10.0        20    30.0
            Yellow    30.0        34    64.0
    Paper_3 Yellow     0.0        28    28.0
            Blue      30.0        12    42.0
            Green     42.0        17    59.0

    Schedule by Machine
                     Start  Duration  Finish
    Machine Job
    Blue    Paper_2   10.0        20    30.0
            Paper_3   30.0        12    42.0
            Paper_1   42.0        45    87.0
    Green   Paper_2    0.0        10    10.0
            Paper_3   42.0        17    59.0
    Yellow  Paper_3    0.0        28    28.0
            Paper_2   30.0        34    64.0
            Paper_1   87.0        10    97.0

## Visualizing Results with Gantt Charts

```python
def visualize(results):

    schedule = pd.DataFrame(results)
    JOBS = sorted(list(schedule['Job'].unique()))
    MACHINES = sorted(list(schedule['Machine'].unique()))
    makespan = schedule['Finish'].max()

    bar_style = {'alpha':1.0, 'lw':25, 'solid_capstyle':'butt'}
    text_style = {'color':'white', 'weight':'bold', 'ha':'center', 'va':'center'}
    colors = mpl.cm.Dark2.colors

    schedule.sort_values(by=['Job', 'Start'])
    schedule.set_index(['Job', 'Machine'], inplace=True)

    fig, ax = plt.subplots(2,1, figsize=(12, 5+(len(JOBS)+len(MACHINES))/4))

    for jdx, j in enumerate(JOBS, 1):
        for mdx, m in enumerate(MACHINES, 1):
            if (j,m) in schedule.index:
                xs = schedule.loc[(j,m), 'Start']
                xf = schedule.loc[(j,m), 'Finish']
                ax[0].plot([xs, xf], [jdx]*2, c=colors[mdx%7], **bar_style)
                ax[0].text((xs + xf)/2, jdx, m, **text_style)
                ax[1].plot([xs, xf], [mdx]*2, c=colors[jdx%7], **bar_style)
                ax[1].text((xs + xf)/2, mdx, j, **text_style)

    ax[0].set_title('Job Schedule')
    ax[0].set_ylabel('Job')
    ax[1].set_title('Machine Schedule')
    ax[1].set_ylabel('Machine')

    for idx, s in enumerate([JOBS, MACHINES]):
        ax[idx].set_ylim(0.5, len(s) + 0.5)
        ax[idx].set_yticks(range(1, 1 + len(s)))
        ax[idx].set_yticklabels(s)
        ax[idx].text(makespan, ax[idx].get_ylim()[0]-0.2, "{0:0.1f}".format(makespan), ha='center', va='top')
        ax[idx].plot([makespan]*2, ax[idx].get_ylim(), 'r--')
        ax[idx].set_xlabel('Time')
        ax[idx].grid(True)

    fig.tight_layout()

visualize(results)
```

## Application to the scheduling of batch processes

We will now turn our attention to the application of the job shop scheduling problem to the short term scheduling of batch processes. We illustrate these techniques using Example II from Dunn (2013).

| Recipe  | Mixer | Reactor | Separator | Packaging |
| :-----: | :---: | :-----: | :-------: | :-------: |
| A       | 1.0   | 5.0     | 4.0       | 1.5       |
| B       | -     | -       | 4.5       | 1.0       |
| C       | -     | 3.0     | 5.0       | 1.5       |

### Single product strategies

Before going further, we create a function to streamline the generation of the TASKS dictionary.

```python
def recipe_to_tasks(jobs, machines, durations):
    TASKS = {}
    for j in jobs:
        prec = (None,None)
        for m,d in zip(machines,durations):
            task = (j,m)
            if prec == (None,None):
                TASKS.update({(j,m): {'dur': d, 'prec': None}})
            else:
                TASKS.update({(j,m): {'dur': d, 'prec': prec}})
            prec = task
    return TASKS

recipeA = recipe_to_tasks('A', ['Mixer', 'Reactor', 'Separator', 'Packaging'], [1, 5, 4, 1.5])
visualize(jobshop(recipeA))
```

```python
recipeB = recipe_to_tasks('B', ['Separator', 'Packaging'], [4.5, 1])
visualize(jobshop(recipeB))
```

```python
recipeC = recipe_to_tasks('C', ['Separator', 'Reactor', 'Packaging'], [5, 3, 1.5])
visualize(jobshop(recipeC))
```

### Multiple Overlapping tasks

Let's now consider an optimal scheduling problem where we are wish to make two batches of Product A.

```python
TASKS = recipe_to_tasks(['A1','A2','A3', 'A4'],['Mixer','Reactor','Separator','Packaging'],[1,5,4,1.5])
results = jobshop(TASKS)
visualize(results)
print("Makespan =", max([task['Finish'] for task in results]))
```

Earlier we found it tood 11.5 hours to produce one batch of product A.  As we see here, we can produce a second batch with only 5.0 additional hours because some of the tasks overlap.  The overlapping of tasks is the key to gaining efficiency in batch processing facilities.

Let's next consider production of a single batch each of products A, B, and C.

```python
# update is used to append dictionaries
TASKS = recipeA
TASKS.update(recipeB)
TASKS.update(recipeC)

for k, v in TASKS.items():
    print(k, v)

results = jobshop(TASKS)
visualize(results)
print("Makespan =", max([task['Finish'] for task in results]))
```

The individual production of A, B, and C required 11.5, 5.5, and 9.5 hours, respectively, for a total of 25.5 hours. As we see here, by scheduling the production simultaneously, we can get all three batches done in just 15 hours.

As we see below, each additional set of three products takes an additionl 13 hours.  So there is considerable efficiency gained by scheduling over longer intervals whenever possible.

```python
TASKS = recipe_to_tasks(['A1','A2'],['Mixer','Reactor','Separator','Packaging'],[1,5,4,1.5])
TASKS.update(recipe_to_tasks(['B1','B2'],['Separator','Packaging'],[4.5,1]))
TASKS.update(recipe_to_tasks(['C1','C2'],['Separator','Reactor','Packaging'],[5,3,1.5]))

results = jobshop(TASKS)
visualize(results)
print("Makespan =", max([task['Finish'] for task in results]))
```

### Adding time for unit clean out

A common feature of batch unit operations is a requirement that equipment be cleaned prior to reuse.

In most cases the time needed for clean out would be specific to the equipment and product. But for the purposes this notebook, we implement can implement a simple clean out policy with a single non-negative parameter $t_{clean} \geq 0$ which, if specified, requires a period no less than $t_{clean}$ between the finish of one task and the start of another on every piece of equipment.

This policy is implemented by modifying the usual disjunctive constraints to avoid machine conflicts to read

\begin{align}
\left[\text{start}_{j,m}+\text{dur}_{j,m} + t_{clean} \leq \text{start}_{k,m}\right] \vee \left[\text{start}_{k,m}+\text{dur}_{k,m} + t_{clean} \leq \text{start}_{j,m}\right]
\end{align}

For this purpose, we write a new JobShopModel_Clean

```python
def jobshop_model_clean(TASKS, tclean=0):

    model = ConcreteModel()

    # tasks is a two dimensional set of (j,m) constructed from the dictionary keys
    model.TASKS = Set(initialize = TASKS.keys(), dimen=2)

    # the set of jobs is constructed from a python set
    model.JOBS = Set(initialize = list(set([j for (j,m) in model.TASKS])))

    # set of machines is constructed from a python set
    model.MACHINES = Set(initialize = list(set([m for (j,m) in model.TASKS])))

    # the order of tasks is constructed as a cross-product of tasks and filtering
    model.TASKORDER = Set(initialize = model.TASKS * model.TASKS, dimen=4,
        filter = lambda model, j, m, k, n: (k,n) == TASKS[(j,m)]['prec'])

    # the set of disjunctions is cross-product of jobs, jobs, and machines
    model.DISJUNCTIONS = Set(initialize = model.JOBS * model.JOBS * model.MACHINES, dimen=3,
        filter = lambda model, j, k, m: j < k and (j,m) in model.TASKS and (k,m) in model.TASKS)

    # load duration data into a model parameter for later access
    model.dur = Param(model.TASKS, initialize=lambda model, j, m: TASKS[(j,m)]['dur'])

    # establish an upper bound on makespan
    ub = sum([model.dur[j,m] for (j,m) in model.TASKS])

    model.makespan = Var(bounds=(0, ub))

    model.start = Var(model.TASKS, bounds=(0, ub))

    model.objective = Objective(expr = model.makespan, sense = minimize)

    model.finish = Constraint(model.TASKS, rule=lambda model, j, m:
        model.start[j,m] + model.dur[j,m] <= model.makespan)

    model.preceding = Constraint(model.TASKORDER, rule=lambda model, j, m, k, n:
        model.start[k,n] + model.dur[k,n] <= model.start[j,m])

    model.disjunctions = Disjunction(model.DISJUNCTIONS, rule=lambda model,j,k,m:
        [model.start[j,m] + model.dur[j,m] + tclean <= model.start[k,m],
         model.start[k,m] + model.dur[k,m] + tclean <= model.start[j,m]])

    TransformationFactory('gdp.hull').apply_to(model)
    return model

model = jobshop_model_clean(TASKS, tclean=0.5)
results = jobshop_solve(model)
visualize(results)
print("Makespan =", max([task['Finish'] for task in results]))
```

### Adding a zero wait policy

One of the issues in the use of job shop scheduling for chemical process operations are situations where there it is not possible to store intermediate materials. If there is no way to store intermediates, either in the processing equipment or in external vessels, then a **zero-wait** policy may be required.

A zero-wait policy requires subsequent processing machines to be available immediately upon completion of any task. To implement this policy, the usual precident sequencing constraint of a job shop scheduling problem, i.e.,

\begin{align*}
\text{start}_{k,n}+\text{Dur}_{k,n} \leq \text{start}_{j,m}\ \ \ \ \text{for } (k,n) =\text{Prec}_{j,m}
\end{align*}

is changed to

\begin{align*}
\text{start}_{k,n}+\text{Dur}_{k,n} = \text{start}_{j,m}\ \ \ \ \text{for } (k,n) =\text{Prec}_{j,m}\text{ and ZW is True}
\end{align*}

if the zero-wait policy is in effect.

While this could be implemented on an equipment or product specific basis, here we add an optional ZW flag to the JobShop function that, by default, is set to False.

```python
def jobshop_model_clean_zw(TASKS, tclean=0, ZW=False):

    model = ConcreteModel()

    # tasks is a two dimensional set of (j,m) constructed from the dictionary keys
    model.TASKS = Set(initialize = TASKS.keys(), dimen=2)

    # the set of jobs is constructed from a python set
    model.JOBS = Set(initialize = list(set([j for (j,m) in model.TASKS])))

    # set of machines is constructed from a python set
    model.MACHINES = Set(initialize = list(set([m for (j,m) in model.TASKS])))

    # the order of tasks is constructed as a cross-product of tasks and filtering
    model.TASKORDER = Set(initialize = model.TASKS * model.TASKS, dimen=4,
        filter = lambda model, j, m, k, n: (k,n) == TASKS[(j,m)]['prec'])

    # the set of disjunctions is cross-product of jobs, jobs, and machines
    model.DISJUNCTIONS = Set(initialize = model.JOBS * model.JOBS * model.MACHINES, dimen=3,
        filter = lambda model, j, k, m: j < k and (j,m) in model.TASKS and (k,m) in model.TASKS)

    # load duration data into a model parameter for later access
    model.dur = Param(model.TASKS, initialize=lambda model, j, m: TASKS[(j,m)]['dur'])

    # establish an upper bound on makespan
    ub = sum([model.dur[j,m] for (j,m) in model.TASKS])

    # to implement a zero-wait policy
    model.bigM = Param(initialize=ub if ZW else 0)

    model.makespan = Var(bounds=(0, ub))

    model.start = Var(model.TASKS, bounds=(0, ub))

    model.objective = Objective(expr = model.makespan, sense = minimize)

    model.finish = Constraint(model.TASKS, rule=lambda model, j, m:
        model.start[j,m] + model.dur[j,m] <= model.makespan)

    def _preceding(model, j, m, k, n):
        if ZW:
            return  model.start[k,n] + model.dur[k,n] == model.start[j,m]
        else:
            return model.start[k,n] + model.dur[k,n] <= model.start[j,m]

    model.preceding = Constraint(model.TASKORDER, rule=_preceding)

    model.disjunctions = Disjunction(model.DISJUNCTIONS, rule=lambda model,j,k,m:
        [model.start[j,m] + model.dur[j,m] + tclean <= model.start[k,m],
         model.start[k,m] + model.dur[k,m] + tclean <= model.start[j,m]])

    TransformationFactory('gdp.hull').apply_to(model)
    return model

model = jobshop_model_clean_zw(TASKS, tclean=0.5, ZW=True)
results = jobshop_solve(model)
visualize(results)
print("Makespan =", max([task['Finish'] for task in results]))
```

## Solving the LA19 benchmark problem with NEOS

The file [`jobshop1.txt` contains 82 benchmark problems](http://people.brunel.ac.uk/~mastjjb/jeb/orlib/files/jobshop1.txt) from a well-known collection of job shop scheduling problems in the [OR-Library maintained by J. E. Beasley](http://people.brunel.ac.uk/~mastjjb/jeb/info.html). The data format for each example consists of a single line for each job.  The data on each line is a sequence of (machine number, time) pairs showing the order in which machines process each job.

LA19 is a benchmark problem for job shop scheduling introduced by Lawrence in 1984, and a solution presented by Cook and Applegate in 1991. The following cell may take many minutes to hours to run, depending on the choice of solver and hardware. To run, uncomment the the last lines in the cell.

```python
data = """
2  44  3   5  5  58  4  97  0   9  7  84  8  77  9  96  1  58  6  89
4  15  7  31  1  87  8  57  0  77  3  85  2  81  5  39  9  73  6  21
9  82  6  22  4  10  3  70  1  49  0  40  8  34  2  48  7  80  5  71
1  91  2  17  7  62  5  75  8  47  4  11  3   7  6  72  9  35  0  55
6  71  1  90  3  75  0  64  2  94  8  15  4  12  7  67  9  20  5  50
7  70  5  93  8  77  2  29  4  58  6  93  3  68  1  57  9   7  0  52
6  87  1  63  4  26  5   6  2  82  3  27  7  56  8  48  9  36  0  95
0  36  5  15  8  41  9  78  3  76  6  84  4  30  7  76  2  36  1   8
5  88  2  81  3  13  6  82  4  54  7  13  8  29  9  40  1  78  0  75
9  88  4  54  6  64  7  32  0  52  2   6  8  54  5  82  3   6  1  26
"""

TASKS = {}
for job, line in enumerate(data.splitlines()[1:]):
    nums = line.split()
    prec = None
    for m, dur in zip(nums[::2], nums[1::2]):
        task = (f"J{job}",f"M{m}")
        TASKS[task] = {'dur':int(dur), 'prec':prec}
        prec = task

#pd.DataFrame(TASKS).T
```

Depending on the choice of solver, this benchmark example may require from minutes to hours of computational effort on a laptop. An alternative to solving on a laptop is to submit the job to [NEOS](https://neos-server.org/neos/), a free internet-based service for solving optimization problems hosted by the University of Wisconsin and utilizing high performance servers at locations across the globe.

The following cell shows how to solve a model using [CPLEX](https://www.ibm.com/analytics/cplex-optimizer), a high performance commericial solver, on NEOS. The solution may take several minutes, and depends on the current length of the NEOS job queue.

```python
def jobshop_solve_neos(model):
    solver_manager = SolverManagerFactory('neos')
    solver_manager.solve(model, opt='cplex')
    results = [{'Job': j,
                'Machine': m,
                'Start': model.start[j, m](),
                'Duration': model.dur[j,m],
                'Finish': model.start[(j, m)]() + model.dur[j,m]}
               for j,m in model.TASKS]
    return results

model = jobshop_model(TASKS)
results = jobshop_solve_neos(model)
visualize(results)
```

## References

* Applegate, David, and William Cook. ["A computational study of the job-shop scheduling problem."](https://doi.org/10.1287/ijoc.3.2.149) ORSA Journal on computing 3, no. 2 (1991): 149-156. [pdf available](http://www.math.uwaterloo.ca/~bico/papers/jobshop.pdf)
* Beasley, John E. ["OR-Library: distributing test problems by electronic mail."](https://www.jstor.org/stable/pdf/2582903.pdf?casa_token=RWUXQ6e2VngAAAAA:g-dwMXjJfCUWqoLJk5y-MpDDhLmJKSGE36YU5kpkXZ71dmos25vntN0RlDFLxDme_QsOw8Oc3oi6juXCkVCfm2gcycCK0SYNoOnOZqRYDp3EqSrXP6nDeQ) Journal of the operational research society 41, no. 11 (1990): 1069-1072. [OR-Library](http://people.brunel.ac.uk/~mastjjb/jeb/info.html)
* Guéret, Christelle, Christian Prins, and Marc Sevaux. ["Applications of optimization with Xpress-MP."](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.69.9634&rep=rep1&type=pdf) contract (1999): 00034.
* Manne, Alan S. ["On the job-shop scheduling problem."](https://doi.org/10.1287/opre.8.2.219) Operations Research 8, no. 2 (1960): 219-223.

## Exercises

### Task specific cleanout

Clean out operations are often slow and time consuming. Further more, the time required to perform a clean out frequently depends on the type of machine, and the task performed by the machine. For this exercise, create a data format to include task-specific clean out times, and model the job shop model to accomodate this additional informaton.

### Computational impact of a zero-wait policy

Repeat the benchmark problem calculation, but with a zero-wait policy. Does the execution time increase or descrease as a consequence of specifying zero-wait?


```python
import os
import sys

# add the path of packages to system path
nb_dir = os.path.split(os.getcwd())[0]
if nb_dir not in sys.path:
    sys.path.append(nb_dir)

# add packages for computation
import numpy as np
import pandas as pd
from arbitragerepair import constraints, repair

# add plot packages
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
```

## Load and normalise data

*Note: The data contained in the file sample.csv are collected from the real world option market, and are pre-processed to remove sensitive information. The pre-processing includes removing contract details, adding small noise to the raw prices, scaling the raw prices and strike prices, and so on.*

### Prepare price data and option contract specifications $(T,K)$ as 1D arrays

Consider $N$ European call options that have expiries $0 < T_1 < T_2 < \cdots < T_m$. For a given expiry $T_i$, available strikes are $0 < K_1^i < K_2^i < \cdots < K_{n_i}^i$. The $(i,j)$-th option has price $C^i_{j}$ at present time $0$, and terminal payoff $H^i_j := (S_{T_i} - K^i_j)^+$, where $S_t$ denotes the price of the underlying asset at time $t$. Hence $N = \sum_{i=1}^m n_i$.

We stack data of $T$, $K$ and $C$ (bid, ask and mid) to get 1-dimensional arrays.

```python
data_dir = '../data/'
fname = data_dir + 'sample.csv'

df = pd.read_csv(fname)

# extract mid prices and option specifications
T = df[df['quote']=='mid']['expiry'].values
K = df[df['quote']=='mid']['strike'].values
C = df[df['quote']=='mid']['call_fv'].values
F = df[df['quote']=='mid']['forward'].values
iv = df[df['quote']=='mid']['imp_vol'].values

# extract bid and ask prices
C_bid = df[df['quote']=='bid']['call_fv'].values
C_ask = df[df['quote']=='ask']['call_fv'].values
```

### Plot forward curve, call price surface and implied volatility surface

```python
fig = plt.figure(figsize=(12,6))
spec = gridspec.GridSpec(ncols=2, nrows=3, figure=fig)

ax = fig.add_subplot(spec[0,:])  # plot the forward curve
ax.set_title('Forward curve')
unq_Ts, idx_T = np.unique(T, return_index=True)
ax.plot(unq_Ts, F[idx_T], '--ok')
ax.set_xlabel(r'$T$')
ax.set_ylabel(r'$F(T)$')

ax = fig.add_subplot(spec[1:,0], projection='3d')
ax.set_title('Call price surface')
ax.scatter3D(T, K, C, s=3, color='k')
ax.set_xlabel(r'$T$')
ax.set_ylabel(r'$K$')
ax.view_init(30,170)

ax = fig.add_subplot(spec[1:,1], projection='3d')
ax.set_title('Implied volatility surface')
ax.scatter3D(T, K, iv, s=3, color='k')
ax.set_xlabel(r'$T$')
ax.set_ylabel(r'$K$')
ax.view_init(30,20)

plt.tight_layout()
plt.show()
```

### Normalise the data

Let us define $M_{T} = S_{T_i} / F_i$, $k^i_j = K^i_j/F_i$, $c^i_j = C^i_j / (D_i F_i)$, for all $i,j$. To have no static arbitrage, there exists $\mathbb{Q}$ such that
$$
c^i_j = \mathbb{E}^\mathbb{Q} \left[ \left. \left( \frac{S_{T_i}}{F_i} - \frac{K^i_j}{F_i} \right)^+ \right| \mathscr{F}_0 \right] = \mathbb{E}^\mathbb{Q} \left[ \left. \left( M_{T_i} - k^i_j \right)^+ \right| \mathscr{F}_0 \right],  \quad \forall i,j.
$$

Note that the call price data we have are the future values, or undiscounted values, of call options. Therefore, we do not require discount factor (i.e. zero rate curve) inputs.

```python
# normalise strikes and prices
normaliser = constraints.Normalise()
normaliser.fit(T, K, C, F)
T1, K1, C1 = normaliser.transform(T, K, C)

_, _, C1_bid = normaliser.transform(T, K, C_bid)
_, _, C1_ask = normaliser.transform(T, K, C_ask)
```

##  Detect arbitrage

The static arbitrage constraints are linear inequalities of at most three call prices. Therefore, we can write these constraints in the form $A \mathbf{c} \geq \mathbf{b}$, where $\mathbf{c} = [c^1_1 \cdots c^1_{n_1} \cdots c^m_{n_m}]^\top \in \mathbb{R}^N$, $A  = (a_{ij}) \in \mathbb{R}^{R \times N}$ and $\mathbf{b} = (b_j) \in \mathbb{R}^{R}$ are a constant matrix and a vector corresponding to coefficients and bounds of the inequalities, respectively. Here, $R$ is the number of no-arbitrage constraints, where $R \sim \mathcal{O}(m^2 N)$.

When some row of the system of inequalities $A \mathbf{c} \geq \mathbf{b}$ is not satisfied, there is arbitrage. We identify, in our sample data, which types of arbitrage constraints are violated.

```python
# construct arbitrage constraints and detect violation
mat_A, vec_b, _, _ = constraints.detect(T1, K1, C1, verbose=True)
```

    Number of violations to non-negative outright price:                   0/13
    Number of violations to non-negative and unit-bounded vertical spread: 0/130
    Number of violations to non-negative butterfly spread:                 0/104
    Number of violations to non-negative calendar (horizontal) spread:     0/0
    Number of violations to non-negative calendar vertical spread:         9/513
    Number of violations to non-negative calendar butterfly spread:        126/3070

## Repair arbitrage from the price data

We define $\boldsymbol{\varepsilon}$ to be the vector of perturbations added to the vector of call prices $\mathbf{c}$ such that the perturbed prices are arbitrage-free, i.e. $A (\mathbf{c} + \boldsymbol{\varepsilon}) \geq \mathbf{b}$. Hence, to remove arbitrage from the call price data, we seek the ``minimal'' repair subject to no-arbitrage constraints:

\begin{equation}
\underset{\boldsymbol\varepsilon \in \mathbb{R}^N}{\text{min}} f(\boldsymbol\varepsilon), \quad \text{subject to } A \boldsymbol{\varepsilon} \geq \mathbf{b} - A \mathbf{c},
\end{equation}

### Repair using two objective functions

We propose the following two objective functions:

- The $\ell^1$-norm repair (no bid-ask spread inputs are needed)

\begin{equation}
f(\boldsymbol{\varepsilon}) := ||\boldsymbol{\varepsilon}||_{\ell^1}.
\end{equation}

- The $\ell^1$-BA repair

\begin{equation}
f(\boldsymbol{\varepsilon}) = \sum_{j=1}^N \max \left( -\mathbf{e}_j^\top \boldsymbol{\varepsilon} -\delta^b_j+\delta_0, -\frac{\delta_0}{\delta^b_j} \mathbf{e}_j^\top \boldsymbol{\varepsilon}, \frac{\delta_0}{\delta^a_j} \mathbf{e}_j^\top \boldsymbol{\varepsilon}, \mathbf{e}_j^\top \boldsymbol{\varepsilon} - \delta^a_j + \delta_0 \right),
\end{equation}

where
- $\delta^b_j$ is the bid-mid spread
- $\delta^a_j$ is the ask-mid spread
- $\delta_0$ is a small non-negative constant
\begin{equation}
\delta_0 = \frac{1}{N} \wedge \min_{j=1,\dots,N} \left( \delta^a_j \wedge \delta^b_j \right).
\label{eq:delta_0}
\end{equation}

```python
# repair arbitrage - l1-norm objective
epsilon1 = repair.l1(mat_A, vec_b, C1)

# repair arbitrage - l1ba objective
spread_ask = C1_ask - C1
spread_bid = C1 - C1_bid
spread = [spread_ask, spread_bid]

epsilon2 = repair.l1ba(mat_A, vec_b, C1, spread=spread)
```

### Compare the $\ell^1$-norm repair and the $\ell^1$-BA repair

```python
tol = 1e-8
n_quote = len(C1)
unq_Ts, idx_T = np.unique(T1, return_index=True)

expiry_str = ['1D', '1W', '2W', '3W',
              '1M', '2M', '3M', '4M', '6M', '9M', '1Y', '18M', '2Y']

fig = plt.figure(figsize=(12,6))

ax = fig.add_subplot(211)
ax.plot(epsilon1/C1, 'ko', markersize=3, alpha=.8)
mask1 = epsilon1[epsilon1>0] - spread_ask[epsilon1>0] > tol
ax.plot(np.arange(n_quote)[epsilon1>0][mask1],
        ((epsilon1/C1)[epsilon1>0])[mask1], 'ro', markersize=8, alpha=.5)
mask2 = epsilon1[epsilon1<0] + spread_bid[epsilon1<0] < -tol
ax.plot(np.arange(n_quote)[epsilon1<0][mask2],
        ((epsilon1/C1)[epsilon1<0])[mask2], 'ro', markersize=8, alpha=.5)
for t in unq_Ts:
    mask_t = T1==t
    ax.fill_between(np.arange(n_quote)[mask_t],
                    (-spread_bid/C1)[mask_t], (spread_ask/C1)[mask_t], color='C0', alpha=.2)
vals = ax.get_yticks()
ax.set_yticklabels(['{:3.0f}%'.format(x*100) for x in vals])
ax.set_title(r"$\ell^1$")
ax.set_xticks(np.ceil((idx_T + np.hstack([idx_T[1:], len(T1)-1]))/2))
ax.set_xticklabels(expiry_str)

ax = fig.add_subplot(212)
ax.plot(epsilon2/C1, 'ko', markersize=3, alpha=.8, label='Perturbation')
mask1 = epsilon2[epsilon2>0] - spread_ask[epsilon2>0] > tol
ax.plot(np.arange(n_quote)[epsilon2>0][mask1],
        ((epsilon2/C1)[epsilon2>0])[mask1], 'ro', markersize=8, alpha=.5)
mask2 = epsilon2[epsilon2<0] + spread_bid[epsilon2<0] < -tol
ax.plot(np.arange(n_quote)[epsilon2<0][mask2],
        ((epsilon2/C1)[epsilon2<0])[mask2], 'ro', markersize=8, alpha=.5,
        label='Perturbation out of bid-ask price bounds')
for t in unq_Ts:
    mask_t = T1==t
    ax.fill_between(np.arange(n_quote)[mask_t],
                    (-spread_bid/C1)[mask_t], (spread_ask/C1)[mask_t], color='C0', alpha=.2)
vals = ax.get_yticks()
ax.set_yticklabels(['{:3.0f}%'.format(x*100) for x in vals])
ax.set_title(r"$\ell^1$-BA")
ax.set_xticks(np.ceil((idx_T + np.hstack([idx_T[1:], len(T1)-1]))/2))
ax.set_xticklabels(expiry_str)
ax.legend()

plt.tight_layout()
plt.show()
```

From left to right, the displayed data are ordered by increasing strikes grouped by expiry. The light blue area are confined by bid-ask spread as a percentage of option prices (green lines). We see that ITM and OTM options have wider bid-ask spreads than ATM options do. The $\ell^1$-BA repair method results in fewer effective perturbations. First, there is one less effective perturbation of 1M option prices, at the cost of perturbing a few 2W, 3W and 1M option prices to their bid or ask prices. Second, all four effective perturbations of 4M option prices by the $\ell^1$-norm repair are replaced by six ineffective perturbations of 6M option prices by the $\ell^1$-BA repair.

In the following plot, we take a closer look at how these two repair methods perturb prices for a subset of expiries.

```python
idx_T_sub = [2,3,4,7,8]

fig = plt.figure(figsize=(12,4))

ax = fig.add_subplot(131)
ax.set_title('Raw prices')
for i in idx_T_sub:
    mask_t = T1==unq_Ts[i]
    ax.plot(K1[mask_t], C1[mask_t], '--o', label=r'$T=$' + expiry_str[i])
ax.legend()

ax = fig.add_subplot(132)
ax.set_title(r'Repaired prices by the $\ell^1$-norm')
for i in idx_T_sub:
    mask_t = T1==unq_Ts[i]
    ax.plot(K1[mask_t], (C1+epsilon1)[mask_t], '--o', label=r'$T=$' + expiry_str[i])
ax.legend()

ax = fig.add_subplot(133)
ax.set_title(r'Repaired prices by the $\ell^1$-BA')
for i in idx_T_sub:
    mask_t = T1==unq_Ts[i]
    ax.plot(K1[mask_t], (C1+epsilon2)[mask_t], '--o', label=r'$T=$' + expiry_str[i])
ax.legend()

plt.tight_layout()
plt.show()
```

```python

```


# Shell-model project for Nuclear Talent course

**European Center for Theoretical Studies in Nuclear Physics and Related Areas, Trento, Italy, 3-21 July, 2017**

Date: **This text will be updated with more material**

## Introduction

The project is divided in three main parts. The first part deals with a simple pairing model and the development of a shell-model program related to this model. This program can then be developed into a more general shell-model program that allows you to study general nuclear structure problems. That is the second part of the project. In parallel, we will also use NushellX in order to perform more advanced shell-model studies and compare the results obtained with your own shell-model code to those of NushellX.  We expect you to form working groups consisting of typically three (or more) participants. Every group should establish its own Github or Gitlab repository for the project.

## Part 1, pairing problem

In the first part of the  project we will thus work with a simplified Hamiltonian consisting of a one-body operator and a so-called
 pairing interaction term. It is a model which to a large extent mimicks some central features of
atomic nuclei, certain atoms and systems which exhibit superfluiditity or superconductivity.
Pairing plays a central role in nuclear physics, in particular, for identical particles it makes up large fractions of the correlations among particles. The partial wave $^{1}S_0$ of the nucleon-nucleon force plays a central role in setting up pairing correlations in nuclei. Without this particular partial wave, the $J=0$ ground state spin assignment for many nuclei with even numbers of particles would not be possible.

We define first the Hamiltonian, with a definition of the model space and
the single-particle basis. Thereafter, we present the various steps which are needed to develop a shell-model program for studying the pairing problem.

The Hamiltonian acting in the complete Hilbert space (usually infinite
dimensional) consists of an unperturbed one-body part, $\hat{H}_0$,
and a perturbation $\hat{H}_I$.

We limit ourselves to at most two-body interactions, our Hamiltonian  is
then represented by the following operators

$$
\begin{equation}
\hat{H} = \hat{H}_0 +\hat{H}_I=\sum_{pq}\langle p |h_0|q\rangle a_{p}^{\dagger}a_{q} +\frac{1}{4}\sum_{pqrs}\langle pq| V|rs\rangle a_{p}^{\dagger}a_{q}^{\dagger}a_{s}a_{r},
\label{eq:hamiltonian} \tag{1}
\end{equation}
$$

where $a_{p}^{\dagger}$ and $a_{q}$ etc are standard fermion creation and annihilation operators, respectively,
and $pqrs$ represent all possible single-particle quantum numbers.
The full single-particle space is defined by the completeness relation
$\hat{1} = \sum_{p =1}^{\infty}|p \rangle \langle p|$.
In our calculations  we will let  the single-particle states $|p\rangle$
be eigenfunctions of  the one-particle operator $\hat{h}_0$.

The above Hamiltonian
acts in turn on various many-body Slater determinants constructed from the single-basis defined by the one-body
operator $\hat{h}_0$.

Our specific model consists of only $2$ doubly-degenerate and equally spaced
single-particle levels labeled by $p=1,2,\dots$ and spin $\sigma=\pm
1$.
In Eq. ([eq:hamiltonian](#eq:hamiltonian)) the labels $pqrs$ could also include spin $\sigma$. From now and for the rest of this project, labels like $pqrs$ represent the states without spin. The spin quantum numbers need to be accounted for explicitely.

We write
the Hamiltonian as

$$
\hat{H} = \hat{H}_0 +\hat{H}_I=\hat{H}_0 + \hat{V} ,
$$

where

$$
\hat{H}_0=\xi\sum_{p\sigma}(p-1)a_{p\sigma}^{\dagger}a_{p\sigma}.
$$

Here, $H_0$ is the unperturbed Hamiltonian with a spacing between
successive single-particle states given by $\xi$, which we will set to
a constant value $\xi=1$ without loss of generality.

The two-body
operator $\hat{V}$ has one term only. It represents the
pairing contribution and carries a constant strength $g$
and is  given by

$$
\langle q+q-| V|s+s-\rangle  = -g
$$

where $g$ is a constant. The above labeling means that for a  general matrix elements
$\langle pq| V|rs\rangle$ we require that the states $p$ and $q$ (and $r$ and $s$) have the same number
quantum number $q$ but opposite spins. The two spins values are
$\sigma = \pm 1$.
When setting up the Hamiltonian matrix you need to figure out how to make the two-body interaction antisymmetric.
The variables $\sigma=\pm$ represent the two possible spin values. The
interaction can only couple pairs and excites therefore only two
particles at the time.

In our model we have kept both the interaction strength and the single-particle level as constants.
In a realistic system like the atomic  nucleus this is not the case.

The   unperturbed Hamiltonian  $\hat{H}_0$ and $\hat{V}$  commute
with  the spin projection $\hat{S}_z$ and the total spin
$\hat{S}^2$.
This is an important feature of our system that allows us to block-diagonalize
the full Hamiltonian. In this project we will focus only on total spin $S=0$, this case is normally called the no-broken pair case.

### Part 1a: Paper and pencil gym while we wait for the more serious stuff

Show that the
unperturbed Hamiltonian  $\hat{H}_0$ and $\hat{V}$ commute
with both the spin projection $\hat{S}_z$ and the total spin
$\hat{S}^2$, given by

$$
\hat{S}_z := \frac{1}{2}\sum_{p\sigma} \sigma a^{\dagger}_{p\sigma}a_{p\sigma}
$$

and

$$
\hat{S}^2 := \hat{S}_z^2 + \frac{1}{2}(\hat{S}_+\hat{S}_- +
  \hat{S}_-\hat{S}_+),
$$

where

$$
\hat{S}_\pm := \sum_{p} a^{\dagger}_{p\pm} a_{p\mp}.
$$

This is an important feature of our system that allows us to block-diagonalize
the full Hamiltonian. We will focus on total spin $S=0$.
In this case, it is convenient to define the so-called pair creation and pair
annihilation operators

$$
\hat{P}^{+}_p = a^{\dagger}_{p+}a^{\dagger}_{p-},
$$

and

$$
\hat{P}^{-}_p = a_{p-}a_{p+},
$$

respectively.

The Hamiltonian (with $\xi=1$) we will use can be written as

$$
\hat{H}=\sum_{p\sigma}(p-1)a_{p\sigma}^{\dagger}a_{p\sigma}
-g\sum_{pq}\hat{P}^{+}_p\hat{P}^{-}_q.
$$

Show  that Hamiltonian commutes with the product of the pair creation and annihilation operators.
This model corresponds to a system with no broken pairs. This means that the Hamiltonian can only link two-particle states in so-called spin-reversed states.

### Part 1b: Simpler case

Assume now that the effective Hilbert space consists only of the two lowest single-particle states and that we have two particles only.
Set up the possible two-particle configurations when we have only two single-particle states, that is $p=1$ and $p=2$.
Construct thereafter the Hamiltonian matrix using second quantization and for example Wick's theorem
for a system with no broken pairs and spin $S=0$ (with projection $S_z=0$) for the case of the two lowest single-particle levels  and two particles only.  This gives you a
 $2\times 2$ matrix to be diagonalized.

Find the eigenvalues by diagonalizing the Hamiltonian matrix.
Vary your results for selected values of $g\in [-1,1]$ and comment your results.

### Part 1c: Setting up the Hamiltonian matrix

Construct thereafter the Hamiltonian matrix for a system with no broken pairs and spin $S=0$ for the case of the four lowest single-particle levels. Our system consists of four particles only.
Our single-particle space consists of only the four lowest levels
$p=1,2,3,4$.  You need to set up all possible Slater determinants and the Hamiltonian matrix using second quantization and
find all eigenvalues by diagonalizing the Hamiltonian matrix.
Vary your results for values of $g\in [-1,1]$.  Your Hamiltonian matrix is a $6\times 6$ matrix.
These results will serve as a benchmark for the construction  of our shell-model program.
We  refer to this as the exact results. Comment the behavior of the ground state as function of $g$.

### Part 1d: Diagonalizing the Hamiltonian matrix

Our next step is to develop a code which sets up the above Hamiltonian matrices for two and four particles in 2 and 4 single-particles states (the same as what you did in exercises b) and c) and obtain the eigenvalues.
To achieve this you should

* Decide whether you want to read from file the single-particle data and the matrix elements in $m$-scheme, or set them up internally in your code. The latter is the simplest possibility for the pairing model, whereas the first option gives you a more general code which can be extended to the more realistic cases discussed in the second part.

* Based on the single-particle basis, write a function which sets up all possible Slater determinants which have total $M=0$.  Test that this function  reproduces the cases in b) and c). If you make this function more general, it can then be reused for say a shell-model calculation of $sd$-shell nuclei in the second part.

* Use the Slater determinant basis from the previous step to set up the Hamiltonian matrix.

* With the Hamiltonian matrix, you can finally diagonalize the matrix and obtain the final eigenvalues and test against the results of b) and c).

Codes to diagonalize in C++ or Fortran can be provided. For Python, numpy contains eigenvalue solvers based on for example Householder's and Givens' algorithms.   These are topics which can we discuss separately. The lecture slides contain a rather detailed recipe
on how to construct a Slater determinant basis and how to set up the Hamiltonian matrix to diagonalize.

### Part 1e: Further benchmarks

In developing the code it also useful to test against cases which have closed-form solutions. One obvious case is that of removing the
two-body interaction. Then we have only the single-particle energies.
For the case of degenerate single-particle orbits, that is one value of total single-particle angular momentum only $j$, with degeneracy $\Omega=2j+1$, one can show that the ground state energy $E_0$ is with $n$ particles

$$
E_0= -\frac{g}{4}n\left(\Omega-n+2\right).
$$

Enlarge now your system to six and eight fermions and to $p=6$ and $p=8$ single-particle states, respectively. Run your program for a degenerate single-particle state with degeneracy $\Omega$ and test
against the exact result for the ground state. Introduce thereafter a finite single-particle spacing and study the results as you vary $g$,  as done in b) and c). Comment your results.

## Part 2, building your own shell-model program

The way we will set up the Slater determinants here follows a simple odometric recipe. The way it is done in more professional codes, is to use bitwise manipulations. The latter is a possible extension/challenge for those interested.

Part two of our project consists of at developing your own shell-model code that can perform shell-model studies of the oxygen isotopes using standard
effective interactions (provided by us) using as example the $1s0d$ shell as model space.
You may also need to consider a bit representation and manipulation of Slater determinants and to implement
the Lanczos algorithm. These details will be discussed during our lectures.

  * For the shell-model part you need now to read in your data from file, both the single-particle states and the effective interaction. We will provide you with the [USDB](https://journals.aps.org/prc/abstract/10.1103/PhysRevC.74.034315) ($1s0d$-shell effective interaction).  If you have not done so, rewrite your code from the project so that you can read in this interaction.

  * For the oxygen isotopes you can actually use your previous program and perform shell-model calculations of the oxygen isotopes using the $1s0d$ shell. Your results should agree with those obtained using Alex Brown's code Nushellx. Compute the spectra of the 3-4 lowest lying states of the oxygen isotopes from ${}^{18}\mbox{O}$ to ${}^{28}\mbox{O}$ and compare with data and the Alex Brown's results.

  * The code we wrote in the project  was however not very efficient, unless you already implemented the bit representation. As an optional challenge, you may now wish to consider the inclusion of a bit representation along the lines discussed in the lecture slides, and inserted below in the appendix here as well.  Note that this part may quickly become time consuming. You may also consider implementing the Lanczos' algorithm as discussed by [Whitehead et al.](https://link.springer.com/chapter/10.1007%2F978-1-4615-8234-2_2)

  * We will also provide you with effective $1s0d$-shell interactions derived using Coupled cluster theory and the aim is to reproduce the published results of [Jansen et al](https://journals.aps.org/prc/abstract/10.1103/PhysRevC.94.011301), see also [their Physical Review Letters article as well](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.113.142502). The derivation of these interactions, with pertaining codes will be discussed by Gustav in his lectures at this course.

## Part 3

The aim of this project is to study the structure of selected
low-lying states of the oxygen and fluorine isotopes towards their
respective dripline. These chains of isotopes have been studied
extensively during the last years, with many
efforts toward the understanding of their dripline properties,
involving studies of low-lying excited states and electromagnetic
transitions. For the oxygen isotopes, ${}^{24}\mbox{O}$ is the last
particle-stable nucleus, and for the fluorine isotopes ${}^{31}\mbox{F}$ is
assumed to the last stable one.  This part can also be used to benchmark your shell-model program from the second part.

The task here is to study these isotopic chains, extract excitation
energies and selected observables and compare with available data. To achieve this you will
need to use an effective interaction designed for the $1s0d$ shell
first and then, for nuclei beyond $A=24$ you may need to consider
degrees of freedom from the $1p0f$ shell. Since a full calculation in
these two major shells becomes quickly time-consuming for the fluorine
isotopes, you will need to truncate the number of particles which can
leave/occupy selected single-particle states. In the file which
contains the single-particle data, you can reduce the size of the
total space of Slater determinants by limiting the number of particles
which can populate the $1p0f$ shell. Here you could limit yourselves
to consider only the single-particle states $0f_{7/2}$ and $1p_{3/2}$.

* Test your effective interaction and setup of single-particle energies by computing the spectra of ${}^{18}\mbox{O}$ and ${}^{18}\mbox{F}$ in order to see that your $1s0d$-shell calculations where set up correctly.  Compare the spectra with available data. Use the [USDA and USDB interactions](https://journals.aps.org/prc/abstract/10.1103/PhysRevC.74.034315) in the NushellX directory over interactions.

* Perform shell-model studies using Nushellx for all oxygen isotopes from ${}^{18}\mbox{O}$ to ${}^{28}\mbox{O}$, plot the lowest-lying 3-4 states and compare with data where available. Comment your results.

* Perform also shell-model studies using Nushellx for all oxygen isotopes from ${}^{18}\mbox{F}$ to ${}^{29}\mbox{F}$, plot the lowest-lying 3-4 states and compare with data where available. Comment your results. Try also to compute ${}^{30}\mbox{F}$ and ${}^{31}\mbox{F}$. Here you need to include the $0f_{7/2}$ and $1p_{3/2}$ single-particle states.

* See also if you can find  excited states in ${}^{25}\mbox{O}$ and ${}^{25}\mbox{F}$  with negative parity.

* Use the monopole interactions to calculate the energies for the ground states of the four nuclei ${}^{22-25}\mbox{O}$ assuming a single Slater determinant for each. The USDB two-body matrix elements are assumed to scale like $\mbox{18/A}^{0.3}$.

* Compare the results in the last problem to the full $1s0d$ model space results and to experiment.

* Calculate the spectroscopic factors from the ground state of ${}^{23}\mbox{O}$ to all states in  ${}^{22}\mbox{O}$ in the full $1s0d$ model space. Use the sum rule to obtain the orbital occupations in ${}^{23}\mbox{O}$ for $0d_{5/2}$, $1s_{1/2}$ and $0d_{3/2}$. Compare these to those given in the so-called xxx.occ file.

* Calculate the spectroscopic factors from the ground state of ${}^{23}\mbox{O}$ to all states in  ${}^{24}\mbox{O}$ in the full $1s0d$ model space. Use the sum rule to obtain the number of holes in those three orbits in ${}^{23}\mbox{O}$. Compare these to those given in the xxx.occ file.

* Calculate the ${}^{23}\mbox{O}$  $\mbox{5/2}^+_1$ to ${}^{22}\mbox{O}$  $\mbox{0}^+_1$ spectroscopic factor. Explain why it is so small.

* Use the interaction wspot to obtain the single-particle decay width for the ${}^{23}\mbox{O}$  $\mbox{5/2}^{+}_{1}$ state using the experimental neutron separation energy as a constraint. Combine this with the result of the last problem  to obtain its neutron decay width. Compare to experiment.

* Calculate the neutron decay width of the ${}^{25}\mbox{O}$  $\mbox{3/2}^{+}_{1}$ state and compare to experiment. Use the experimental neutron separation energy as a constraint.

* Calculate the gamma decay of ${}^{22}\mbox{O}$ for levels up to 6 MeV and compare to experiment. Calculate the B(E2) for Coulex to the $\mbox{2}^{+}_{1}$ state in ${}^{22}\mbox{O}$ and compare with experiment.

* Calculate the magnetic moment for the  $\mbox{1/2}^+_1$ ground state of ${}^{23}\mbox{O}$ and compare to the single-particle (Schmidt) value.

* Calculate the Fermi ($F$) and Gamow-Teller ($GT$) beta decay of ${}^{22}\mbox{O}$. The experimental energy of the lowest $1^{+}$ state in ${}^{22}\mbox{F}$ is 1.627 MeV. You will need to put this in the xxx.beq file and rerun the beta program (see the end of the xxx.bat file for how to do this). Compare the summed $B(F)$ and $B(GT)$ values to that expected from the sum-rules. What fraction of the $GT$ sum-rule is in the transition to the lowest energy $1^{+}$ state?

# Appendix: Bit representation

In the build-up of a shell model code that is meant to tackle large dimensionalities
is the action of the Hamiltonian $\hat{H}$ on a
Slater determinant represented in second quantization as

$$
\vert \alpha_1\dots \alpha_n\rangle = a_{\alpha_1}^\dagger a_{\alpha_2}^\dagger \dots a_{\alpha_n}^\dagger \vert 0\rangle.
$$

The time consuming part stems from the action of the Hamiltonian
on the above determinant,

$$
\left(\sum_{\alpha\beta} \langle \alpha\vert \hat{t}+\hat{u}\vert \beta\rangle a_\alpha^\dagger a_\beta + \frac{1}{4} \sum_{\alpha\beta\gamma\delta}
\langle\alpha \beta\vert \hat{V}\vert \gamma \delta\rangle a_\alpha^\dagger a_\beta^\dagger a_\delta a_\gamma\right)a_{\alpha_1}^\dagger a_{\alpha_2}^\dagger \dots a_{\alpha_n}^\dagger \vert 0\rangle.
$$

A practically useful way to implement this action is to encode a Slater determinant as a bit pattern.
Assume that we have at our disposal $n$ different single-particle orbits
$\alpha_0,\alpha_2,\dots,\alpha_{n-1}$ and that we can distribute  among these orbits $N\le n$ particles.

A Slater  determinant can then be coded as an integer of $n$ bits. As an example, if we have $n=16$ single-particle states
$\alpha_0,\alpha_1,\dots,\alpha_{15}$ and $N=4$ fermions occupying the states $\alpha_3$, $\alpha_6$, $\alpha_{10}$ and $\alpha_{13}$
we could write this Slater determinant as

$$
\Phi_{\Lambda} = a_{\alpha_3}^\dagger a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle .
$$

The unoccupied single-particle states have bit value $0$ while the occupied ones are represented by bit state $1$.
In the binary notation we would write this   16 bits long integer as

$$
\begin{array}{cccccccccccccccc}
{\alpha_0}&{\alpha_1}&{\alpha_2}&{\alpha_3}&{\alpha_4}&{\alpha_5}&{\alpha_6}&{\alpha_7} & {\alpha_8} &{\alpha_9} & {\alpha_{10}} &{\alpha_{11}} &{\alpha_{12}} &{\alpha_{13}} &{\alpha_{14}} & {\alpha_{15}} \\
{0} & {0} &{0} &{1} &{0} &{0} &{1} &{0} &{0} &{0} &{1} &{0} &{0} &{1} &{0} & {0} \\
\end{array}
$$

which translates into the decimal number

$$
2^3+2^6+2^{10}+2^{13}=9288.
$$

We can thus encode a Slater determinant as a bit pattern.
With $N$ particles that can be distributed over $n$ single-particle states, the total number of Slater determinats (and defining thereby the dimensionality of the system) is

$$
\mathrm{dim}(\mathcal{H}) = \left(\begin{array}{c} n \\N\end{array}\right).
$$

The total number of bit patterns is $2^n$.
We assume again that we have at our disposal $n$ different single-particle orbits
$\alpha_0,\alpha_2,\dots,\alpha_{n-1}$ and that we can distribute  among these orbits $N\le n$ particles.
The ordering among these states is important as it defines the order of the creation operators.
We will write the determinant

$$
\Phi_{\Lambda} = a_{\alpha_3}^\dagger a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle ,
$$

in a more compact way as

$$
\Phi_{3,6,10,13} = |0001001000100100\rangle.
$$

The action of a creation operator is thus

$$
a^\dagger_{\alpha_4}\Phi_{3,6,10,13} = a^\dagger_{\alpha_4}|0001001000100100\rangle=a^\dagger_{\alpha_4}a_{\alpha_3}^\dagger a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle ,
$$

which becomes

$$
-a_{\alpha_3}^\dagger a^\dagger_{\alpha_4} a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle =-|0001101000100100\rangle.
$$

Similarly

$$
a^\dagger_{\alpha_6}\Phi_{3,6,10,13} = a^\dagger_{\alpha_6}|0001001000100100\rangle=a^\dagger_{\alpha_6}a_{\alpha_3}^\dagger a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle ,
$$

which becomes

$$
-a^\dagger_{\alpha_4} (a_{\alpha_6}^\dagger)^ 2 a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle =0!
$$

This gives a simple recipe:

  * If one of the bits $b_j$ is $1$ and we act with a creation operator on this bit, we return a null vector

  * If $b_j=0$, we set it to $1$ and return a sign factor $(-1)^l$, where $l$ is the number of bits set before bit $j$.

Consider the action of $a^\dagger_{\alpha_2}$ on various slater determinants:

$$
\begin{array}{ccc}
a^\dagger_{\alpha_2}\Phi_{00111}& = a^\dagger_{\alpha_2}|00111\rangle&=0\times |00111\rangle\\
a^\dagger_{\alpha_2}\Phi_{01011}& = a^\dagger_{\alpha_2}|01011\rangle&=(-1)\times |01111\rangle\\
a^\dagger_{\alpha_2}\Phi_{01101}& = a^\dagger_{\alpha_2}|01101\rangle&=0\times |01101\rangle\\
a^\dagger_{\alpha_2}\Phi_{01110}& = a^\dagger_{\alpha_2}|01110\rangle&=0\times |01110\rangle\\
a^\dagger_{\alpha_2}\Phi_{10011}& = a^\dagger_{\alpha_2}|10011\rangle&=(-1)\times |10111\rangle\\
a^\dagger_{\alpha_2}\Phi_{10101}& = a^\dagger_{\alpha_2}|10101\rangle&=0\times |10101\rangle\\
a^\dagger_{\alpha_2}\Phi_{10110}& = a^\dagger_{\alpha_2}|10110\rangle&=0\times |10110\rangle\\
a^\dagger_{\alpha_2}\Phi_{11001}& = a^\dagger_{\alpha_2}|11001\rangle&=(+1)\times |11101\rangle\\
a^\dagger_{\alpha_2}\Phi_{11010}& = a^\dagger_{\alpha_2}|11010\rangle&=(+1)\times |11110\rangle\\
\end{array}
$$

What is the simplest way to obtain the phase when we act with one annihilation(creation) operator
on the given Slater determinant representation?
We have an SD representation

$$
\Phi_{\Lambda} = a_{\alpha_0}^\dagger a_{\alpha_3}^\dagger a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle ,
$$

in a more compact way as

$$
\Phi_{0,3,6,10,13} = |1001001000100100\rangle.
$$

The action of

$$
a^\dagger_{\alpha_4}a_{\alpha_0}\Phi_{0,3,6,10,13} = a^\dagger_{\alpha_4}|0001001000100100\rangle=a^\dagger_{\alpha_4}a_{\alpha_3}^\dagger a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle ,
$$

which becomes

$$
-a_{\alpha_3}^\dagger a^\dagger_{\alpha_4} a_{\alpha_6}^\dagger a_{\alpha_{10}}^\dagger a_{\alpha_{13}}^\dagger \vert 0 \rangle =-|0001101000100100\rangle.
$$

The action

$$
a_{\alpha_0}\Phi_{0,3,6,10,13} = |0001001000100100\rangle,
$$

can be obtained by subtracting the logical sum (AND operation) of $\Phi_{0,3,6,10,13}$ and
a word which represents only $\alpha_0$, that is

$$
|1000000000000000\rangle,
$$

from $\Phi_{0,3,6,10,13}= |1001001000100100\rangle$.

This operation gives $|0001001000100100\rangle$.

Similarly, we can form $a^\dagger_{\alpha_4}a_{\alpha_0}\Phi_{0,3,6,10,13}$, say, by adding
$|0000100000000000\rangle$ to $a_{\alpha_0}\Phi_{0,3,6,10,13}$, first checking that their logical sum
is zero in order to make sure that orbital $\alpha_4$ is not already occupied.
It is trickier however to get the phase $(-1)^l$.
One possibility is as follows

  * Let $S_1$ be a word that represents the $1-$bit to be removed and all others set to zero. In the previous example $S_1=|1000000000000000\rangle$

  * Define $S_2$ as the similar word that represents the bit to be added, that is in our case $S_2=|0000100000000000\rangle$.

  * Compute then $S=S_1-S_2$, which here becomes

$$
S=|0111000000000000\rangle
$$

* Perform then the logical AND operation of $S$ with the word containing

$$
\Phi_{0,3,6,10,13} = |1001001000100100\rangle,
$$

which results in $|0001000000000000\rangle$. Counting the number of $1-$bits gives the phase.  Here you need however an algorithm for bitcounting. Several efficient ones available.


<center>
    <h1> ILI285/INF285 Computación Científica </h1>
    <h1> Repaso de álgebra lineal </h1>
</center>

```python
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from ipywidgets import interact, interactive, fixed, interact_manual
```

# Definiciones básicas y operaciones

**Vector** ($\vec{u}$ \\ $\mathbf{u}$): Definiremos como un vector, un arreglo unidimensional de números. Encontraremos dos tipos de vectores:

\begin{equation}
    \text{Vector Columna} =
    \begin{bmatrix}
        u_1 \\
        u_2 \\
        u_3
    \end{bmatrix}_{\,n\times 1}
    \qquad\qquad
    \text{Vector Fila} =
    [u_1, u_2, u_3]_{1\times n}
\end{equation}

```python
v = np.array([1,2,3])
print(v)
print(v.shape)
```

**Matriz** ($\mathbf{A}$): Una matriz $A\in\Re^{m \times n}$ es un arreglo bidimensional de $m$ filas y $n$ columnas, con numeros en $\Re$.

\begin{equation}
    \mathbf {A} =
    \begin{bmatrix}
        a_{11}&a_{12}&\cdots &a_{1n} \\
        a_{21}&a_{22}&\cdots &a_{2n} \\
        \vdots &\vdots &\ddots &\vdots \\
        a_{m1}&a_{m2}&\cdots &a_{mn}
    \end{bmatrix}_{\,m \times n}
\end{equation}

```python
A = np.array( [ [1,2,3,4],[4,5,6,5],[7,8,9,2] ] )
print("Dim:",A.shape)
print(A)
```

## Producto Matriz-Vector

El producto Matriz-Vector entre $\mathbf{A}\in\Re^{m \times n}$ y $\mathbf{u}\in \Re^{n}$ se calcula como:

\begin{equation}
    \mathbf {A} \mathbf{u} =
    \begin{bmatrix}
        a_{11}&a_{12}&\cdots &a_{1n} \\
        a_{21}&a_{22}&\cdots &a_{2n} \\
        \vdots &\vdots &\ddots &\vdots \\
        a_{m1}&a_{m2}&\cdots &a_{mn}
     \end{bmatrix}
     \cdot
     \begin{bmatrix}
         u_{1} \\
         u_{2} \\
         \vdots \\
         u_{n} \\
     \end{bmatrix}=
     \begin{bmatrix}
         a_{11}u_{1} + a_{12}u_{2} + \cdots + a_{1n} u_{n} \\
         a_{21}u_{1} + a_{22}u_{2} + \cdots + a_{2n} u_{n} \\
         \vdots \\a_{m1}u_{1} + a_{m2}u_{2} + \cdots + a_{mn} u_{n} \\
     \end{bmatrix}
\end{equation}

$A_{m\times n} \cdot u_{n\times1} = v_{m\times 1}$

```python
A = np.array([[1,2,3],[4,5,6],[7,8,9]])
v = np.array([1,2,3])
print(A)
print(v)
print(np.dot(A,v))
```

Notar que podemos ver una matriz como una colección de vectores columnas, entonces:

\begin{equation}
    \mathbf {A} =
    \begin{bmatrix}
        a_{11}&a_{12}&\cdots &a_{1n} \\
        a_{21}&a_{22}&\cdots &a_{2n} \\
        \vdots &\vdots &\ddots &\vdots \\
        a_{m1}&a_{m2}&\cdots &a_{mn}
    \end{bmatrix}=
    [\vec{a}_{1}|\vec{a}_{2}|\dots|\vec{a}_{n}], \quad \vec{a}_{i} \in \Re^{m}, \, i \in \{1,\dots,n\}
\end{equation}

Así, el producto matriz-vector se calcula como:

\begin{equation}
    \mathbf {A} \mathbf{u} = [\vec{a}_{1}|\vec{a}_{2}|\dots|\vec{a}_{n}] \cdot \vec{v}
    = u_{1} \vec{a}_{1} + u_{2} \vec{a}_{2}+\cdots+u_{n}\vec{a}_{n}
    = \sum_{i=1}^{n} u_{i}\vec{a}_{i} =\vec{b}
\end{equation}

\begin{equation}
    \mathbf {A} \mathbf {B} =
    \begin{bmatrix}
        a_{11}&a_{12}&\cdots &a_{1n} \\
        a_{21}&a_{22}&\cdots &a_{2n} \\
        \vdots &\vdots &\ddots &\vdots \\
        a_{m1}&a_{m2}&\cdots &a_{mn}\\
    \end{bmatrix}
    \cdot
    \begin{bmatrix}
        b_{11}&b_{12}&\cdots &b_{1p} \\
        b_{21}&b_{22}&\cdots &b_{2p} \\
        \vdots &\vdots &\ddots &\vdots \\
        b_{n1}&b_{n2}&\cdots &b_{np}\\
    \end{bmatrix} =
    \begin{bmatrix}
        a_{11}b_{11}+\cdots +a_{1n}b_{n1}&a_{11}b_{12}+\cdots
            +a_{1n}b_{n2}&\cdots &a_{11}b_{1p}+\cdots +a_{1n}b_{np}\\
        a_{21}b_{11}+\cdots +a_{2n}b_{n1}&a_{21}b_{12}+\cdots
            +a_{2n}b_{n2}&\cdots &a_{21}b_{1p}+\cdots +a_{2n}b_{np}\\
        \vdots &\vdots &\ddots &\vdots \\
        a_{m1}b_{11}+\cdots +a_{mn}b_{n1}&a_{m1}b_{12}+\cdots
            +a_{mn}b_{n2}&\cdots &a_{m1}b_{1p}+\cdots +a_{mn}b_{np}\\
     \end{bmatrix}
\end{equation}

```python
A = np.array([[7,2,3],[4,5,6],[7,8,9]])
B = np.array([[2,3,4],[4,1,6],[7,8,1]])
np.dot(A,B)
```

Organizando las matrices por sus vectores columnas, podemos calcular el producto matriz-matriz como:

$$
[\vec{a}_{1}|\vec{a}_{2}|\dots|\vec{a}_{n}] \cdot [\vec{b}_{1}|\vec{b}_{2}|\dots|\vec{b}_{n}] = C \in \Re^{m \times l} \Rightarrow \vec{c}_{i} = \displaystyle \sum_{k=1}^{n} \vec{b}_{ki}\cdot\vec{a}_{k} = A \cdot \vec{b}_{i} \\ C = [A \vec{b}_{1}|A \vec{b}_{2}|\dots|A \vec{b}_{n}]
$$

# Espacios vectoriales y álgebra lineal

**Espacio vectorial**: Un espacio vectorial $V$ corresponde a una colección de objetos (vectores) que posee las siguientes operaciones:

* Adición: Se define como +: $ V \times V \rightarrow V$, es decir, tomamos dos elementos $v,w \in V$ y de resultado es $v+w \in V$.
* Multiplicación escalar: Se define como ·: $ F \times V \rightarrow V$, es decir, tomamos un escalar $\alpha \in F$ y $v \in V$ y el resultado de $\alpha v \in V$.

Además, se deben cumplir los siguientes axiomas; $\forall u,v,w \in V$, $\forall a,b \in F$:

* a) Asociatividad de la adición: $u+(v+w) = (u+v)+w$
* b) Conmutatividad de la adición: $ u + v = v + u $
* c) Identidad aditiva: $\exists \emptyset \in V:\, u+\emptyset = \emptyset + u = u$
* d) Inverso aditivo: $\forall u \in V, \exists -u \in V: u + (-u) = \emptyset$
* e) Compatibilidad del producto escalar con el producto del campo: $a\cdot (b\cdot u) = (a \cdot b)\cdot u$
* f) Identidad en escalar multiplicativo: $\exists 1 \in F: 1\cdot u = u \cdot 1 = u$
* g) Distributividad del producto escalar respecto a la adición de vectores: $a\cdot(u+v) = au + av$
* h) Distributividad del producto escalar respecto a adición en el campo: $(a+b)u = au + bu$

Los elementos de V se llaman vectores y los elementos de F se denominan como escalares.

**Ejemplo**:
* Verificar si $P_{3} = \{a_{0} + a_{1}x + a_{2}x^{2} + a_{3}x^{3}|a_{0},\cdots,a_{3}\in\Re\}$ es un espacio vectorial.

**Subespacio**: Se define como sub espacio vectorial H de V como un subconjunto H de V que cumple con:
* El vector nulo de V está en H
* H es cerrado frente a a la adición de vectores en H
* H es cerrado frente a la multiplicación de escalares

**Transformación lineal**: Sea V y W espacios vectoriales, una función $f:V\rightarrow W$ se dice que es una transformación lineal si para todo vector $\vec{u},\vec{v}\in V$ y escalar $c\in K$, las siguientes condiciones se cumplen:
* $f(u+v) = f(u) + f(v)$
* $f(cu) = c\,f(u)$

**Combinación lineal**: Sea $ \{\vec{v}_{1},\vec{v}_{2},\cdots,\vec{v}_{n}\}$ vectores de un espacio vectorial y $c_{1},c_{2},\cdots,c_{n}$ escalares, entonces se denomina como una combinación lineal a los vectores de la forma:

\begin{equation}
    \vec{u} = c_{1} \vec{v}_{1} + c_{2} \vec{v}_{2}+\cdots+c_{n}\vec{v}_{n} = \sum_{i=1}^{n} c_{i}\vec{v}_{i}
\end{equation}

span($v_{1},v_{2},\cdots,v_{n}$) = V

**Conjunto generador**: Sea $ \{\vec{v}_{1},\vec{v}_{2},\cdots,\vec{v}_{n}\}$ un conjunto de los vectores de un espacio(o subespacio) vectorial V. Si todo vector en V puede ser escrito como una combinación de los vectores $\{v_{1},v_{2},\cdots,v_{n}\}$, entonces se dice que el conjunto $S=\{v_{1},v_{2},\cdots,v_{n}\}$ genera (span) a $V$, es decir, S es el conjunto generador de $V$.

**Dependencia lineal**: Sea $S=\{\vec{v}_{1},\vec{v}_{2},\cdots,\vec{v}_{n}\}$ un conjunto de vectores de un espacio vectorial $V$, se dice que los vectores en S son linealmente dependientes si existen escalares $c_{1},c_{2},\cdots,c_{n}$, no todos nulos, tal que:

\begin{equation}
 c_{1} \vec{v}_{1} + c_{2} \vec{v}_{2}+\cdots+c_{n}\vec{v}_{n} = \vec{0}
\end{equation}

**Independencia lineal**: Un conjunto $T=\{\vec{v}_{1},\vec{v}_{2},\cdots,\vec{v}_{n}\}$ de vectores de un espacio vectorial se dice linealmente independiente si la siguiente ecuacion se cumple:

\begin{equation}
 c_{1} \vec{v}_{1} + c_{2} \vec{v}_{2}+\cdots+c_{n}\vec{v}_{n} = \vec{0}
\end{equation}

si y solo si $c_{1} = c_{2} = \cdots = c_{n} = 0$.

**Base**: Un conjunto $B$ de vectores de un espacio vectorial $V$ se le denomina base de $V$ si:
* $B$ es un conjunto de vectores linealmente independientes
* $B$ genera $V$, es decir, $V$ = span(B)

**Ejemplo**: Resolvamos:

\begin{aligned}
    x+y =1\\
    ax+y = b
\end{aligned}

¿Cómo podriamos relacionarlo con lo anterior?

\begin{equation}
    \begin{bmatrix}
        1&1 \\
        a&1 \\
     \end{bmatrix}
     \begin{bmatrix}
         x \\
         y \\
     \end{bmatrix} =
     \begin{bmatrix}
         1 \\
         b \\
     \end{bmatrix}
\end{equation}

\begin{equation}
    x \begin{bmatrix}1\\a\\\end{bmatrix} + y \begin{bmatrix}1\\1\\\end{bmatrix} = \begin{bmatrix}1\\b\\\end{bmatrix}
\end{equation}

```python
def plot(a,b):
    plt.figure(figsize=(8, 6))
    f = lambda x: 1-x
    g = lambda a,b, x: b - a*x
    f_e = f(np.linspace(-5,5,100))
    g_e = g(a,b,np.linspace(-5,5,100))
    plt.plot(np.linspace(-5,5,100),f_e, label=r"$y=1-x$")
    plt.plot(np.linspace(-5,5,100),g_e, label=r"$y=b-ax$")
    plt.grid(True)
    plt.xlim(-5,5)
    plt.ylim(-15,25)
    plt.legend()
    plt.show()
interact(plot, a = (0,4), b = (0,4));
```

# Propiedades de matrices

**Matriz identidad** : La matriz $I_{n}\in\Re^{n\times n}$ se llama matriz identidad si cumple:
\begin{equation}
    I_{n}=
    \begin{bmatrix}
        1&0&0& \cdots &0 \\
        0&1&0& \cdots &0 \\
        0&0&1& \cdots &0 \\
        \vdots &\vdots &\vdots &\ddots &\vdots \\
        0&0&0&\cdots &1
    \end{bmatrix}.
\end{equation}

\begin{equation}
    I_{n} A = A I_{n} = A, \forall A \in \Re^{n\times n}
\end{equation}

```python
A = np.array([[1,2,3],[4,5,6],[7,8,9]])
I = np.identity(3)
print(A)
print(I)
print(np.dot(A,I))
```

**Matriz inversa**: Sea $A\in\Re^{n\times n}$, se denomina matriz inversa de A, denotada como $A^{-1}$, a la matriz que cumple:

\begin{equation}
    AA^{-1} = A^{-1}A = I_{n}
\end{equation}

```python
A = np.array([[7,2,3],[4,5,6],[7,8,9]])
AA = np.linalg.inv(A)
print(np.dot(A,AA))
```

Si A tiene inversa, se dice que es invertible o no singular. SI no tiene inversa, se dice no invertible o singular.

Algunas propiedades de la inversa son:
* $(A^{-1})^{-1} = A$
* $(AB)^{-1} = B^{-1}A^{-1}$
* $(\alpha A)^{-1} = \frac{1}{\alpha} A^{-1}, \alpha \in \Re, \alpha \neq 0$
* $(A^{n})^{-1} = (A^{-1})^{n}$

Tengamos en cuenta lo siguiente:

\begin{equation}
    \begin{split}
        A\vec{x} & = \vec{b} \\
        A^{-1} A \vec{x} & = A^{-1} \vec{b} \\
        \vec{x} & = A^{-1} \vec{b}
    \end{split}
\end{equation}

Consideremos el siguiente problema:
\begin{equation}
    \begin{bmatrix}
        7&2&3 \\
        4&5&6 \\
        7&8&9
    \end{bmatrix}
    \begin{bmatrix}
        x \\
        y \\
        z
    \end{bmatrix} =
    \begin{bmatrix}
        4 \\
        2 \\
        3
    \end{bmatrix}
\end{equation}

```python
A = np.array([[7,2,3],[4,5,6],[7,8,9]])
b = np.array([4,2,3])
AA = np.linalg.inv(A) # O(n^3)
np.dot(AA,b) # O(n^3)
```

```python
np.linalg.solve(A,b)
```

**Matriz transpuesta**: Sea $A\in\Re^{n\times n}$, la matriz transpuesta se define como:

\begin{equation}
    a^{T}_{ij} = a_{ji}
\end{equation}

\begin{equation}
    \begin{bmatrix}
        a&b&c \\
        d&e&f \\
        g&h&i
    \end{bmatrix}^{\operatorname {\,T} }=
    \begin{bmatrix}
       a&d&g \\
       b&e&h \\
       c&f&i
    \end{bmatrix}
\end{equation}

```python
A = np.array([[7,2,3],[4,5,6],[7,8,9]])
print(A)
print(A.T)
```

Propiedades de la transpuesta:
* $(A^{T})^{T} = A$
* $(A+B)^{T} = A^{T} + B^{T}$
* $(AB)^{T} = B^{T} A^{T}$
* $(\alpha A)^{T} = \alpha A^{T}$
* $(A^{T})^{-1} = (A^{-1})^{T}$

```python
np.transpose(np.transpose(A)) == A
```

**Transpuesta conjugada**: Sea $A\in \mathbb{C}^{m \times n}$ la matriz adjunta o transpuesta conjugada de A, denotada por $A^{*}\in \mathbb{C}^{n \times m}$ cuyos coeficientes son:

\begin{equation}
a^{*}_{ij} = \overline{a_{ji}}
\end{equation}

\begin{equation}
    \begin{bmatrix}
        1&-2-i&5\\
        1+i&i&4-2i
    \end{bmatrix}^{*} =
    \begin{bmatrix}
        1&1-i\\
        -2+i&-i\\
        5&4+2i
    \end{bmatrix}
\end{equation}

**Otras matrices**:
* **Matriz simétrica**: $A=A^{T}$
* **Matriz hermitiana**: $A=A^{*}$
* **Matriz unitaria**: $A^{*} = A^{-1}$
* **Matriz ortogonal**: $A^{T} = A^{-1}$

**Rango**: Dada una matriz $A\in\Re^{m\times n}$ se llama column space, range o rango al espacio generado por las columnas de A, denotado por Range(A).

**Nullspace**: El nullspace de $A\in\mathbb{C}^{m \times n}$, denotado por null(A), se define como:
\begin{equation}
    null(A) = \{\vec{x} \in \mathbb{C}^{n} | \, A\vec{x} = \vec{0},\vec{x} \neq \vec{0}\}
\end{equation}

**Rank**: Se define:
* Column-rank: Dimensión del column space(columnas linealmente independientes) denotado por rank(A)
* Row-rank: Dimensión del row-space.

**Full rank**:
* ($N \times N$): Para una matriz cuadrada se dice que es full rank si todos los vectores columnas y filas son linealmente independiente.
* ($N \times M$): Para una matriz no cuadrada se dice que es full rank si el column-rank y el row-rank tienen los valores más grandes posibles dada las dimensiones de la matriz.

Las siguientes condiciones son equivalentes:
* $A$ tiene inversa $A^{-1}$
* $Rank(A) = m$
* $Range(A) = \Re^{m}$
* $Null(A) = \emptyset$
* $\emptyset$ no es un valor propio de $A$
* $\emptyset$ no es valor singular de $A$
* $det(A) \neq 0$

# Operatorias vectoriales

**Producto interno**: Un producto interno o "inner product" es un mapeo $\langle\bullet,\bullet\rangle:V\times V \rightarrow F$, con V un espacio vectorial y F un campo, y que cumple los siguientes axiomas para todo vector $\vec{x},\vec{y},\vec{z} \in V$ y $\alpha \in F$.

* $\langle x,y\rangle = \overline{\langle y,x\rangle}$
* Linealidad en el primer argumento:
    * $\langle\alpha x,y\rangle = \alpha \langle x,y\rangle$
    * $\langle x+z,y\rangle = \langle x,y\rangle + \langle y,z\rangle$
* $\langle x,x\rangle \geq 0$
* $\langle x,x\rangle = 0 \iff x = 0$

**Producto interno a nuestro caso**: Sea $x,y\in \mathbb{C}^{m}$ dos vectores columnas, se define el producto interno como:
\begin{equation}
    \langle x,y \rangle = x^{*} y = \displaystyle \sum_{i=1}^{m} \overline{x_{i}}y_{i}
\end{equation}

```python
x = np.array([1, 2, 3])
y = np.array([3, 3, 3])
print(np.dot(x, y), np.sum(x * y))
```

Una norma inducida por esta definición es la **Norma Euclidiana**:

\begin{equation}
    ||x||_{2} = \sqrt{\langle x,x \rangle} = \sqrt{x^{*}x} = \left( \displaystyle \sum_{i=1}^{m} x_{i}^{2} \right) ^{1/2}
\end{equation}

```python
print(np.linalg.norm(x), np.sqrt(np.dot(x, x)))
```

**Norma**: Dado un espacio vectorial $V$ sobre un campo $F$, una norma es un mapeo no-negativo $p:V \rightarrow [0,\infty)$ que cumple:
* Sea $u,v \in V$, $p(u+v)\leq p(u) + p(v)$
* $p(\alpha u) = |\alpha|p(u)$, $\alpha \in F$
* Si $p(u) = 0$, entonces $u=\vec{0}$

Propiedad: $\langle x,y \rangle = x^{*}y = ||x||_{2} ||y||_{2}\cos(\alpha) $ con $\alpha$ ángulo entre $x$ e $y$.

```python
v1 = np.array([1, 0])
v2 = np.array([0, 1])
print(np.linalg.norm(v1) * np.linalg.norm(v2) * np.cos(np.pi/2), np.dot(v1, v2))
```

¿Qué pasa en el primer caso?

**Ortogonalidad**: Sea $V$ un "Inner product space", dos vectores $u,v\in V$ se dicen que son ortogonales si $\langle u,v\rangle = 0$. Además, si la norma de cada vector es igual a $1$, se dicen que son ortonormales.

**Producto exterior**: Sean $u$ y $v$ vectores columnas, el producto exterior esta definido como:
\begin{equation}
    \mathbf {u} \otimes \mathbf {v} = u\,v^{T} =
    \begin{bmatrix}
        u_{1} \\
        u_{2} \\
        \vdots \\
        u_{n} \\
    \end{bmatrix}
    \begin{bmatrix}
        v_{1}, \, v_{2}, \, \cdots, \, v_{n}
    \end{bmatrix}   =
    \begin{bmatrix}
        u_{1}v_{1} &u_{1}v_{2}&\dots &u_{1}v_{n} \\
        u_{2}v_{1}&u_{2}v_{2}&\dots &u_{2}v_{n} \\
        \vdots &\vdots &\ddots &\vdots \\
        u_{m}v_{1}&u_{m}v_{2}&\dots &u_{m}v_{n}
     \end{bmatrix}
\end{equation}

```python
np.outer(x, y)
```

Algunas normas usuales:

### Norma 1
\begin{equation}
    ||x||_{1} = \displaystyle \sum_{i=1}^{n} |x_{i}|
\end{equation}

```python
np.linalg.norm(x, ord=1)
```

### Norma 2
\begin{equation}
    ||x||_{2} = \left( \sum_{i=1}^{n} x_{i}^{2}\right)^{1/2} = \sqrt{x^{*}x} = \sqrt{\langle x,x\rangle}
\end{equation}

```python
np.linalg.norm(x, ord=2)
```

### Norma infinita
\begin{equation}
    ||x||_{\infty} = \max\{|x_{i}|\}
\end{equation}

```python
np.linalg.norm(x, ord=np.inf)
```

### Norma-p
\begin{equation}
    ||x||_{p} = \left(\displaystyle \sum_{i=1}^{n} |x_{i}|^{p} \right)^{\frac{1}{p}}
\end{equation}

### Weighted p-norm
Cada coeficiente tiene un peso. Sea $W$ la matriz diagonal donde $w_{ii} = p_{i}$ (i-esimo peso), se define como:
\begin{equation}
    ||x||_{W} = ||W\vec{x}||_{p}
\end{equation}

**Norma Matricial**: Sea $K$ un campo y $K^{m \times n}$ el espacio vectorial de las matrices $m \times n$ con coeficientes en $K$. Una norma matricial es una norma en el espacio vectorial $K^{m \times n}$, es decir, es una función $||\cdot||: K^{m \times n} \rightarrow R^{+}$ que satisfaces las siguientes propeidades para todo $\alpha \in K$ y $A,B\in K^{m \times n}$

* $||\alpha A|| = |\alpha| ||A||$
* $||A+B|| \leq ||A|| + ||B||$
* $||A|| \geq 0$
* $ ||A|| = 0 \iff A=0_{mn}$

Adicionalmente, para matrices cuadradas se cumple la siguiente condición:

* $||AB|| \leq ||A||||B||$

**Norma inducida por norma vectorial**: Dado $A\in K^{m\times n}$, $||\cdot||_{m}$ una norma en $K^{m}$ y $||\cdot||_{m}$ una norma en $K^{n}$, la norma matricial inducida $||A||_{(m,n)}$ es el número C más pequeño que cumple:

\begin{equation}
    ||A\vec{x}||_{m} \leq C ||\vec{x}||_{n}, \quad \forall \vec{x} \in K^{n}, \vec{x} \neq \vec{0}
\end{equation}

\begin{equation}
    ||A|| = \sup\left\{\frac{||A\vec{x}||_{m}}{||\vec{x}||_{n}}| ~ \vec{x} \in K^{n}, \,\vec{x} \neq \vec{0}\right\}
\end{equation}

**Norma de Frobenius**: Sea $A\in K^{m \times n}$, se define la norma de Frobenius como:

\begin{equation}
    ||A||_{F} = \left[\displaystyle \sum_{i=1}^{m} \sum_{j=1}^{n} |a_{ij}|^{2} \right]^{\frac{1}{2}}
\end{equation}


# Hello, many worlds

```python
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import numpy as np
import sympy

%matplotlib inline
from matplotlib import style
style.use('bmh')
style.use('dark_background')

import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
import pydot, graphviz
```

### The basics

```python
a, b = sympy.symbols('a b')
```

```python
q0, q1 = cirq.GridQubit.rect(1, 2)

circuit = cirq.Circuit(
                    cirq.rx(a).on(q0),
                    cirq.ry(b).on(q1), cirq.CNOT(q0, q1))

SVGCircuit(circuit)
# print(circuit)
```

```python
resolver = cirq.ParamResolver({a : 0.5, b : -0.5})
output_state_vector = cirq.Simulator().simulate(circuit, resolver).final_state
output_state_vector
```

    array([ 0.9387913 +0.j        , -0.23971277+0.j        ,
            0.        +0.06120872j,  0.        -0.23971277j], dtype=complex64)

```python
z0 = cirq.Z(q0)

qubit_map = {q0 : 0, q1 : 1}

z0.expectation_from_wavefunction(output_state_vector, qubit_map).real
```

    0.8775825500488281

```python
z0x1 = 0.5 * z0 + cirq.X(q1)

z0x1.expectation_from_wavefunction(output_state_vector, qubit_map).real
```

    -0.04063427448272705

```python
circuit_tensor = tfq.convert_to_tensor([circuit])

print(circuit_tensor.shape)
print(circuit_tensor.dtype)
```

    (1,)
    <dtype: 'string'>

```python
pauli_tensor = tfq.convert_to_tensor([z0, z0x1])
pauli_tensor.shape
```

    TensorShape([2])

### Batching circuit simulation

```python
batch_vals = np.array(np.random.uniform(0, 2 * np.pi, (5, 2)), dtype=np.float32)
```

```python
cirq_results = []
cirq_simulator = cirq.Simulator()

for vals in batch_vals:
    resolver = cirq.ParamResolver({a : vals[0], b : vals[1]})
    final_state = cirq_simulator.simulate(circuit, resolver).final_state
    cirq_results.append(
        [z0.expectation_from_wavefunction(final_state, {
            q0 : 0,
            q1 : 1
        }).real])

print('cirq batch results: \n {}'.format(np.array(cirq_results)))
```

    cirq batch results:
     [[0.83953464]
     [0.44405019]
     [0.2281633 ]
     [0.99464834]
     [0.52329826]]

```python
tfq.layers.Expectation()(circuit, symbol_names=[a, b], symbol_values=batch_vals, operators=z0)
```

    <tf.Tensor: shape=(5, 1), dtype=float32, numpy=
    array([[0.83953476],
           [0.44405025],
           [0.2281632 ],
           [0.9946483 ],
           [0.52329826]], dtype=float32)>

# Hybrid Quantum-Classical Optimization

### Controlled circuit definition

```python
# Prameters which classical NN will feed values into
control_params = sympy.symbols('t1 t2 t3')

# Create the parameterized circuit
qubit = cirq.GridQubit(0, 0)
model_circuit = cirq.Circuit(
    cirq.rz(control_params[0])(qubit),
    cirq.ry(control_params[1])(qubit),
    cirq.rx(control_params[2])(qubit))

SVGCircuit(model_circuit)
# print(model_circuit)
```

### The controller

```python
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])
```

```python
controller(tf.constant([[0.0], [1.0]])).numpy()
```

    array([[ 0.        ,  0.        ,  0.        ],
           [-0.23752365, -0.09134974, -0.17483209]], dtype=float32)

### Connect the controller to the circuit

```python
# This input is the simulated miscalibration that the model will learn to correct.
circuits_input = tf.keras.Input(shape=(),
                               dtype=tf.string, # circuit_tensor has dtype=tf.string
                               name='circuits_input')

# Commands will be either `0` or `1`, specifying the state to set the qubit to.
commands_input = tf.keras.Input(shape=(1,),
                                dtype=tf.dtypes.float32,
                                name='commands_input')
```

```python
dense_2 = controller(commands_input)

# TFQ layer for classically controlled circuits
expectation_layer = tfq.layers.ControlledPQC(model_circuit,
                                            operators=cirq.Z(qubit))

expectation = expectation_layer([circuits_input, dense_2])
```

```python
# The full Keras model is built from our layers.
model = tf.keras.Model(inputs=[circuits_input, commands_input],
                      outputs=expectation)
```

```python
tf.keras.utils.plot_model(model, show_shapes=True, dpi=70)
```

    Failed to import pydot. You must install pydot and graphviz for `pydotprint` to work.

### The dataset

```python
# The command input values to classical NN
commands = np.array([[0], [1]], dtype=np.float32)

# The desired Z expectation value at outpu t of the quantum circuit
expected_outputs = np.array([[1], [-1]], dtype=np.float32)
```

### Input circuit definition

```python
random_rotations = np.random.uniform(0, 2 * np.pi, 3)
noisy_prep = cirq.Circuit(
    cirq.rx(random_rotations[0])(qubit),
    cirq.ry(random_rotations[1])(qubit),
    cirq.rz(random_rotations[2])(qubit))

datapoint_circuits = tfq.convert_to_tensor([
    noisy_prep
] * 2) # Make two copies of this circuit
```

```python
datapoint_circuits.shape
```

    TensorShape([2])

### Training

```python
model([datapoint_circuits, commands]).numpy()
```

    array([[-0.5957383 ],
           [-0.58646244]], dtype=float32)

```python
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()
model.compile(optimizer=optimizer, loss=loss)
history = model.fit(x=[datapoint_circuits, commands],
                    y=expected_outputs,
                    epochs=30,
                    verbose=1)
```

    Train on 2 samples
    Epoch 1/30
    2/2 [==============================] - 1s 670ms/sample - loss: 1.3587
    Epoch 2/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.9834
    Epoch 3/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.6221
    Epoch 4/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.3341
    Epoch 5/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.1533
    Epoch 6/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0641
    Epoch 7/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0267
    Epoch 8/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0116
    Epoch 9/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0058
    Epoch 10/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0042
    Epoch 11/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0043
    Epoch 12/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0045
    Epoch 13/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0037
    Epoch 14/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0022
    Epoch 15/30
    2/2 [==============================] - 0s 3ms/sample - loss: 0.0010
    Epoch 16/30
    2/2 [==============================] - 0s 2ms/sample - loss: 9.5948e-04
    Epoch 17/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0016
    Epoch 18/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0024
    Epoch 19/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0038
    Epoch 20/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0066
    Epoch 21/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0096
    Epoch 22/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0095
    Epoch 23/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0068
    Epoch 24/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0046
    Epoch 25/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0037
    Epoch 26/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0038
    Epoch 27/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0044
    Epoch 28/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0044
    Epoch 29/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0033
    Epoch 30/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0018

```python
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
```

### Verify outputs

```python
def check_error(command_values, desired_values):
    """Based on the value in `command_value` see how well you could prepare
    the full circuit to have `desired_value` when taking expectation w.r.t. Z."""
    params_to_prepare_output = controller(command_values).numpy()
    full_circuit = noisy_prep + model_circuit

    # Test how well you can prepare a state to get the expectation
    # value in `desired_values`
    for index in [0, 1]:
        state = cirq_simulator.simulate(full_circuit,
                                       {s : v for (s, v) in zip(control_params, params_to_prepare_output[index])}
                                       ).final_state
        expectation = z0.expectation_from_wavefunction(state, {qubit : 0}).real

        print(f'For a desired output (expectation) of {desired_values[index]} with'
          f' noisy preparation, the controller\nnetwork found the following '
          f'values for theta: {params_to_prepare_output[index]}\nWhich gives an'
          f' actual expectation of: {expectation}\n')

check_error(commands, expected_outputs)
```

    For a desired output (expectation) of [1.] with noisy preparation, the controller
    network found the following values for theta: [ 2.0013022   4.23094    -0.16599931]
    Which gives an actual expectation of: 0.9637539386749268

    For a desired output (expectation) of [-1.] with noisy preparation, the controller
    network found the following values for theta: [-0.92347455 -1.0410511  -0.19210549]
    Which gives an actual expectation of: -0.9929053783416748

```python
model([datapoint_circuits, commands])
```

    <tf.Tensor: shape=(2, 1), dtype=float32, numpy=
    array([[ 0.96375436],
           [-0.99290544]], dtype=float32)>

# Learning to prepare eigenstates of different operators

### New model definition

```python
# Define inputs.
commands_input = tf.keras.layers.Input(shape=(1),
                                       dtype=tf.dtypes.float32,
                                       name='commands_input')
circuits_input = tf.keras.Input(shape=(),
                                # The circuit-tensor has dtype `tf.string`
                                dtype=tf.dtypes.string,
                                name='circuits_input')
operators_input = tf.keras.Input(shape=(1,),
                                 dtype=tf.dtypes.string,
                                 name='operators_input')
```

```python
# Define classical NN.
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])
```

```python
dense_2 = controller(commands_input)

# Since you aren't using a PQC or ControlledPQC you must append
# your model circuit onto the datapoint circuit tensor manually.
full_circuit = tfq.layers.AddCircuit()(circuits_input, append=model_circuit)
expectation_output = tfq.layers.Expectation()(full_circuit,
                                              symbol_names=control_params,
                                              symbol_values=dense_2,
                                              operators=operators_input)

# Contruct your Keras model.
two_axis_control_model = tf.keras.Model(
    inputs=[circuits_input, commands_input, operators_input],
    outputs=[expectation_output])
```

### The dataset

```python
# The operators to measure, for each command.
operator_data = tfq.convert_to_tensor([[cirq.X(qubit)], [cirq.Z(qubit)]])

# The command input values to the classical NN.
commands = np.array([[0], [1]], dtype=np.float32)

# The desired expectation value at output of quantum circuit.
expected_outputs = np.array([[1], [-1]], dtype=np.float32)
```

### Training

```python
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()

two_axis_control_model.compile(optimizer=optimizer, loss=loss)

history = two_axis_control_model.fit(
    x=[datapoint_circuits, commands, operator_data],
    y=expected_outputs,
    epochs=30,
    verbose=1)
```

    Train on 2 samples
    Epoch 1/30
    2/2 [==============================] - 1s 293ms/sample - loss: 1.3097
    Epoch 2/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.9362
    Epoch 3/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.5553
    Epoch 4/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.2600
    Epoch 5/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.1199
    Epoch 6/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0929
    Epoch 7/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.1186
    Epoch 8/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.1770
    Epoch 9/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.2077
    Epoch 10/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.1858
    Epoch 11/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.1315
    Epoch 12/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0747
    Epoch 13/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0368
    Epoch 14/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0216
    Epoch 15/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0224
    Epoch 16/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0230
    Epoch 17/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0166
    Epoch 18/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0108
    Epoch 19/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0087
    Epoch 20/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0073
    Epoch 21/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0044
    Epoch 22/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0020
    Epoch 23/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0012
    Epoch 24/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0013
    Epoch 25/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0016
    Epoch 26/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0019
    Epoch 27/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0021
    Epoch 28/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0020
    Epoch 29/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.0017
    Epoch 30/30
    2/2 [==============================] - 0s 3ms/sample - loss: 0.0013

```python
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
```

```python
controller.predict(np.array([0,1]))
```

    array([[ 2.679348  , -0.4228346 ,  0.36209983],
           [ 3.813424  , -2.719168  ,  3.995017  ]], dtype=float32)

```python

```