# Assignment 7

Name 1: Sangeet Sagar

Student id 1: 7009050

Email 1: sasa00001@stud.uni-saarland.de

Name 2: Nikhil Paliwal

Student id 2: 7009915

Email 2: nipa00002@stud.uni-saarland.de

**Instructions:** Read each question carefully.

Make sure you appropriately comment your code wherever required. Your final submission should contain the completed Notebook and the Python files for exercises 1 and 2. There is no need to submit the data files.

Upload the zipped folder in Teams. Make sure to click on "Turn-in" after your upload your submission, otherwise the assignment will not be considered as submitted. Only one from the group should make the submisssion.

---

```python

from IPython.core.display import display, HTML
display(HTML("<style>.container { width:100% !important; }</style>"))
import matplotlib.pyplot as plt
import seaborn
import matplotlib as mpl
seaborn.set()
mpl.rcParams['figure.dpi'] = 120
```

## Exercise 1: Count-Trees (5 points)

Your task is to implement a count-tree with variable maximum history. It is a memory-efficient way to store n-gram counts which can also be used to create an intuitive back-off after the tree is pruned.

The tree object should support the following four operations:
- Increment a count of a specific n-gram (even if not present)
- Retrieve counts given the history (variable length)
- Retrieve the conditional probability of a word given the history. (Proportion count between branches)
- Pruning all nodes with counts less or equal to $k$

**1.1 (2 points)**

Make sure your implementation is correct by passing the asserts in the first code cell.

**1.2 (1 point)**

The next cell will incrementally add a quad-gram to the tree. Plot the perplexity of trigram language model (induced by this count tree) against the number of added n-grams. Comment on the curve shape. Smooth this language model with a zerogram distribution using a linear combination ($0.75\times p_4 + 0.25\times p_0$).

**1.3 (1 point)**

For the given range of thresholds, prune your tree and see how the threshold affects the performance. Plot the results (perplexity vs. threshold).

**1.4 (1 point)**

1. If you first prune with threshold $k_1$ and get tree $t_1$, then prune with $k_2$ and get $t_2$ what will be the relationship between $t_1$ and $t_2$ if $k_1 \ge k_2$? (0.25 points)
2. What is the memory benefit of count trees, in comparison to storing the counts as a dictionary `{n-gram:freq}`? (0.25 points)
3. If we pruned the tree so that only the first level is preserved, what distribution could we model with this tree? (0.25 points)
4. Pruning the count tree is said to be a dynamic way of smoothing the language model. Elaborate on how this smoothing happens. (0.25 points)

```python
from importlib import reload
import exercise_1
exercise_1 = reload(exercise_1)

tree = exercise_1.CountTree(n=4)

assert tree.get("") == 0
tree.add("ABCE")
tree.add("ABCD")
tree.add("ABCD")
tree.add("QBCD")
tree.add("QQCD")
tree.add("BCDA")
tree.add("1234")
tree.add("1234")
tree.add("1234")
tree.add("1234")
tree.add("1234")
tree.add("5634")
assert tree.get("ABCD") == 2
assert tree.get("ABCX") == 0
assert tree.get("BCD") == 3
assert tree.get("D") == 4
assert tree.get("CD") == 4
assert tree.get("1234") == 5
assert tree.get("5634") == 1
```

```python
tree.prune(4)
assert tree.get("ABCD") == 4
assert tree.get("XXCD") == 4
assert tree.get("D") == 4
assert tree.get("1234") == 5
assert tree.get("5634") == 1
```

```python
import matplotlib.pyplot as plt

plot_x = []
plot_y = []

ngrams = []
tree = exercise_1.CountTree(n=4)
with open("data/alice_in_wonderland.txt", "r") as f:
  tokens = f.read().lower().split()
  for i in range(len(tokens)-4):
    ngrams.append(tokens[i:i+4])

vocab = set(tokens)
for i,ngram in enumerate(ngrams):
  tree.add(ngram)
  if i % 1000 == 0:
    plot_x.append(i)
    plot_y.append(tree.perplexity(ngrams, vocab))

plt.plot(plot_x, plot_y)
```

```python
plot_x = []
plot_y = []

for threshold in [1,2,3,4,5,10,25,50,75,100]:
  tree.prune(threshold)
  plot_x.append(threshold)
  plot_y.append(tree.perplexity(ngrams, vocab))

print(plot_x)
print(plot_y)

plt.plot(plot_x, plot_y)
```

## Exercise 2: Kneser-Ney Smoothing (5 points)

This exercise aims to provide a basic understanding of Kneser-Ney Smoothing. Kneser-Ney Smoothing makes use of *continuation counts* of words for lower order n-grams, given as

\begin{equation}
C_{KN} =
\begin{cases}
\text{count}(\bullet) & \text{for highest order} \\
\text{continuationcount}(\bullet) & \text{for lower orders}
\end{cases}
\end{equation}

For a trigram distribution, Kneser-Ney Smoothing is implemented using the following equations:

$$P_{KN}(w_3|w_1, w_2) = \frac{\max\{N(w_1 w_2 w_3)-d,0\}}{N(w_1 w_2)} + \lambda(w_1, w_2)P_{KN}(w_3|w_2)$$

$$P_{KN}(w_3|w_2) = \frac{\max\{N_{+}(\bullet w_2 w_3)-d,0\}}{N_{+}(\bullet w_2 \bullet)} + \lambda(w_2)P_{KN}(w_3)$$

\begin{equation}
P_{KN}(w_3) = \begin{cases}
\frac{N_{+}(\bullet w_3)}{N_{+}(\bullet \bullet)} & \text{if $w_3 \in$ V} \\
\frac{1}{V} & \text{otherwise}
\end{cases}
\end{equation}

$\lambda$ is used to normalise the discounted probability mass and is given by

$$\lambda(w_1, w_2) = \frac{d}{N(w_1 w_2)} \cdot N_{+}(w_1 w_2 \bullet)$$

$$\lambda(w_2) = \frac{d}{N(w_2)} \cdot N_{+}(w_2 \bullet)$$

**2.1 (4.5 points)**

* Your first task is to understand what these terms represent and fill it in the table below (4-5 words each).

* Create a trigram-level model on the given text, `alice_in_wonderland.txt`. Write your implementation in the file `exercise_2.py`. Preprocess the text by punctuation removal, lowercasing, and tokenisation. There is no need to split the data into train and test sets. (0.5 points)

```python
from importlib import reload
import exercise_2
exercise_2 = reload(exercise_2)

file = open("data/alice_in_wonderland.txt", "r")
text = file.read()

# TODO: Preprocess text
tokens = exercise_2.preprocess(text)
```

*
Write a simple class `KneserNey` in `exercise_2` that calculates the different parameters required for finding the trigram conditional probability. You may modify the function signature and add other functionality as required.

Now, consider the trigrams `"alice said nothing"` and `"alice said nichts"`. For these trigrams, estimate the values mentioned in the table given below and fill in the obtained results. The discounting parameter *d* = 0.75. (3 points)

```python
KN_model = exercise_2.KneserNey(tokens, d=0.75, N=3)

t1 = "alice said nothing"
t2 = "alice said nichts"

# TODO
# Get the required parameters
KN_model.global_counter(tokens)

params_t1 = KN_model.get_params(t1)
print(params_t1)
print('\n')
params_t2 = KN_model.get_params(t2)
print(params_t2)
```

    V=  2749
    {'Nw1_w2_w3': 2, 'Nw1_w2': 11, 'Nplus_dot_w2_w3': 5, 'Nplus_dot_w2_dot': 319, 'Nplus_dot_w3': 23, 'Nplus_dot_dot': 14713, 'Nplus_w1_w2_dot': 6, 'Nplus_w2_dot': 56, 'lambda_w1_w2': 0.40909090909090906, 'lambda_w2': 0.09090909090909091}

    V=  2749
    {'Nw1_w2_w3': 0, 'Nw1_w2': 11, 'Nplus_dot_w2_w3': 0, 'Nplus_dot_w2_dot': 319, 'Nplus_dot_w3': 0, 'Nplus_dot_dot': 14713, 'Nplus_w1_w2_dot': 6, 'Nplus_w2_dot': 56, 'lambda_w1_w2': 0.40909090909090906, 'lambda_w2': 0.09090909090909091}

| Term in Kneser-Ney |   Value t1  | Value t2 | Description |
|---|---|---| --- |
|$N(w_1w_2w_3)$|2  |0  |number of trigrams of the
 combination $w_1 w_2 w_3$ |
|$N(w_1 w_2)$| 11 |11  | number of bigrams of the
 combination $w_1 w_2$|
|$N_{+}( \bullet w_2 w_3)$|5  |0  |number of words that precede
 $w_2 w_3$ at least once in the corpus |
|$N_{+}( \bullet w_2 \bullet)$|319  |319  |number of trigrams that precede $w_2$
 and also has history $w_2$|
|$N_{+}( \bullet w_3)$|23  | 0 |unique number of words that precede
 $w_3$ at least once in the corpus |
|$N_{+}( \bullet \bullet)$|14713  |14713  |all bigrams appearing
 atleast once |
|$N_{+}(w_1 w_2 \bullet)$|6 |6 |number of words observed
 after $w_1 w_2$ atleast once|
|$N_{+}(w_2 \bullet)$|56  | 56 | number of words observed
 after $w_2$ atleast once|
|$\lambda(w_1 w_2)$|0.409 |0.409 |scaling factor for bigram $w_1 w_2$ |
|$\lambda(w_2)$|0.090 |0.090 | scaling factor for unigram $w_1$|

* Using the values obtained above, manually calculate $P_{KN}(w_3)$, $P_{KN}(w_3|w_2)$, and $P_{KN}(w_3|w_1, w_2)$ for the given trigrams. (1 point)

**Answers**

\begin{align*}
P_{KN}(w_3=\text{nothing}) &= 23/14713 = 0.001563 \\
P_{KN}(w_3=\text{nothing}|w_2=\text{said}) &= \frac{max(5-0.75, 0)}{319} + 0.09090 \cdot 0.001563 = 0.01346496 \\
P_{KN}(w_3=\text{nothing}|w_1=\text{alice}, w_2=\text{said}) &= \frac{max(2-0.75, 0)}{11} + 0.40909 \cdot 0.01346496 = 0.1191447 \\
\\
P_{KN}(w3=\text{nichts}) &= 1/2749 = 0.000363 \\
P_{KN}(w_3=\text{nichts}|w_2=\text{said}) &= \frac{max(0-0.75, 0)}{319} + 0.09090 \cdot 0.000363 = 3.29e-05 \\
P_{KN}(w_3=\text{nichts}|w_1=\text{alice}, w_2=\text{said}) &= \frac{max(0-0.75, 0)}{11} + 0.40909 \cdot 3.29e-05 = 1.34e-05
\end{align*}

**2.2 (0.5 points)**

Take a look at this [video](https://www.youtube.com/watch?v=cbAxvpBFyNU) on Kneser-Ney smoothing by Dan Jurafksy. Make sure to undestand his *San Francisco* example.

How will Kneser-Ney Smoothing handle the following bigrams (answer in 3-4 sentences)?

* Abu Dhabi

* Game Over

**Answer**

In the bigram `Abu Dhabi`, the word `Dhabi` appears often but only after `Abu` while in `Start Over`, the word `Over` doesnt appear only after `Over` but also after many other possible words like `pull`, `talk`,`stop` etc. So, the unigram probablity should only be based on number of different words it follows. Therefore, the continuation probablity for `Game over` would be greater than `Abu Dhabi`.

Also, the quantity $P_{KN}(w_i|w_{i-1})$
$$ \frac{C(w_i=\text{Dhabi},w_{i-1}=\text{Abu})}{C(w_i=\text{Abu})}  > \frac{C(w_i=\text{Over},w_{i-1}=\text{Game})}{C(w_i=\text{Game})} $$
Since counts of `Dhabi` would be almost same as counts of `Abu Dhabi` while counts of `Game` would be greater than `Game Over`

## Bonus (2 points)

For each of the smoothing techniques below,

1. Laplace/add-1 smoothing (0.3 points)
2. Add-$\alpha$ smoothing (0.3 points)
3. Linear interpolation (0.3 points)
4. Absolute discounting (0.3 points)
5. Good-Turing (0.3 points)
6. Kneser-Ney smoothing (0.3 point)

* Give the intuition behind it
* State at least one drawback and
* Explain how the ensuing smoothing technique accounts for this drawback.

You can do so in continuous text or in bullet points. Write 3-5 sentences for each technique. For Kneser-Ney smoothing, you should suggest *and explain* an improved version from the literature, e.g. [here](http://nrs.harvard.edu/urn-3:HUL.InstRepos:25104739) (this tutorial may also be helpful for the rest of the exercise).

Please note that while the points for this bonus exercise are the immediate motivation, your self-made comparison will be highly beneficial for the exam.

1.
- In order to deal with the 0 probability of an n-gram, we pretend to see each word an extra number of times. So we add 1 to all counts
- It sometimes assigns too much probability mass from available ngrams in training data to ngrams absent in training data
- Since adding 1 to all counts leads to the probability mass getting assigned too much (to absent ngrams), how about we control this number. We can change to a float value less than 1.

2.
- In add $\alpha$ smoothing, we fix the problem of 0 probability by adding $\alpha<1$ to each count. So the formula becomes:
$$ P_{+\alpha}(w_i|w_{i-1}) = \frac{C(w_{i-1},w_i)+\alpha}{C(w_{i-1})+\alpha \cdot V}$$
- Even if we optimize $\alpha$ to a suitable optimized value, this smoothing technique makes pretty bad predictions for word sequences.
- Solution? we can try a different approach. until now we have been using counts of unigrams and bigrams to compute the conditional probability of bigrams and trigrams respectively. We cant use e.g. trigrams if we have very low evidence of it. Idea? What if we use all of the unigrams, bigrams, and trigrams. Let's see this approach.

3.
- Linear interpolation suggests that we use evidences from all of the unigrams, bigrams, and trigrams and multiply a factor ($\lambda_1$, $\lambda_2$, $\lambda_3$) to each of them to control their properties that are favorable for our language model.
- The optimal $\lambda_i$ depends on the context but we cant tune all lambdas separately. We will need to bucket them.
-

4.
- In absolute discounting  a constant value $ m$ is subtracted from each count. The effect of this is that the ngrams with the lowest counts are discounted relatively more than those with higher counts.
- The problem with this method is that if we haven't seen a bigram at all. We are going to reply only on the unigram probability.
- Good turing addresses this problem

5.
- The intitution behind Good-Turning estimation is to use the counts of ngrams we have seen once to estimate the counts of ngrams we have never seen.
$$
c^*_k = (c_k+1)\frac{N_{k+1}}{N_k}
$$
- when $k$ is very large i.e. its values is equal to maximum occurrence in the training corpus (for eg. $k$=3 and there are no items are that appear $k+1$ times), then there is a possible conflict with the defined formula ($N_{k+1}=0$). We also have to make sure that the probabilities are always normalized

6.
- It is a modified version of absolute discounting. In this, we optimize the calculation of the lower-order n-gram probabilities in case the higher-order ngram was unseen in the corpus
- The given article suggests not to use the same $d$ discounting parameter for all non-zero counts. It writes that we can have three different $D_1$, $D_2$, $D_{3+}$ that can be applied to unigrams, bigrams, and trigrams respectively


```python
import numpy as np
import matplotlib.pyplot as plt
import math
from sympy import *
import random as rnd
import hashlib as hash
```

**Liniowy generator kongruencyjny**

a - mnożnik

c - przyrost

m - moduł

x0 - pierwsza liczba pseudolosowa

ilosc_liczb - ilość licz pseudolosowych do wygenerowania

```python
def LCG(a,c,m,x0,ilosc_liczb):
  tab = []
  iteracja = [0]
  tab.append(x0)

  for i in range(0,ilosc_liczb-1):
    x = (a*tab[i] + c) % m
    tab.append(x)
    next_i = i+1
    iteracja.append(next_i)

  return tab
```

**Średnia i odchylenie standardowe**

```python
x = LCG(5,7,8,2,30)

y = sum(x)/30
print(y)

X = 0
for i in range(0,30):
  X += (x[i]-y)**2
X = sqrt(X/30)
print(X)
```

    3.5
    2.18708329364323

**Szyfr podstawieniowy**

```python
def key(text, m, a, c, x0):
  Key = LCG(a,c,m,x0,len(text)+1)
  return Key

Cipher = "VPDDWLOGLKJBNTMGMOQNXPNHAAOZTPUEAW"
m = 3956280000
a = 1978141
c = 1309
x0 = 13
Key = key(Cipher,m,a,c,x0)
print(Key)
```

    [13, 25717142, 2284754331, 2939519980, 2899958489, 3867270258, 421351687, 3758475176, 2128489125, 397617934, 267062003, 272997732, 3699137521, 2751609770, 2296638879, 79145248, 2547865277, 1117671366, 3819811915, 3651671324, 2314449993, 2300604322, 2937566711, 387745560, 3473645269, 2027626238, 2498424867, 257193556, 3335180705, 773490714, 2185883983, 2152256912, 41901, 3760487350, 2474697659]

Deszyfrowanie

```python
def decrypt(cipher, key):
  cipherA = []
  for i in cipher:
    cipherA.append(ord(i))
  decryptA = []
  t = 1
  for i in cipherA:
    a = 65 + (i - 39 - (key[t]) % 26) % 26
    decryptA.append(a)
    t += 1
  plaintext = "".join([chr(i) for i in decryptA])
  plaintext = plaintext.replace("X", " ")
  return plaintext

text = decrypt(Cipher,Key)
print(text)
```

    ZAJECIA Z PANEM GROCHOTEM SA SUPER

Szyfrowanie

```python
def encrypt(text,key):
  plaintext = text.replace(" ", "X")
  textA = []
  for i in plaintext:
    textA.append(ord(i))
  encryptA = []
  t = 1
  for i in textA:
    a = 65 + (i - 65 + (key[t]) % 26) % 26
    encryptA.append(a)
    t += 1
  ciphertext = "".join([chr(i) for i in encryptA])
  return ciphertext

ciphertext = encrypt(text,Key)
print(ciphertext)
```

    VPDDWLOGLKJBNTMGMOQNXPNHAAOZTPUEAW

**Hashowanie**

```python
a = b"Ala ma kota"
b = b"ROD"
c = b"fajraaant"

d = hash.sha256(b).hexdigest()
print(d)
e = b"9e5b0ee018f932bceb77851b1ba997b824b562f8ae765bd8020da726efce34ca"
print(hash.sha256(e).hexdigest())

```

    9e5b0ee018f932bceb77851b1ba997b824b562f8ae765bd8020da726efce34ca
    1e2b35ff6724e2ecf156d15b7050332261a638ee1442a76e308b694480055180

**RSA**

Szyfrowanie

```python
def RSA_encrypt(key_public,plaintext):
  text_padding = []
  for i in plaintext:
    text_padding.append(ord(i))

  encryptedA = []
  for i in text_padding:
    a = pow(i, key_public[0], key_public[1])
    encryptedA.append(a)

  return encryptedA
```

Deszyfrowanie

```python
def RSA_decrypt(key_private,cipher):
  decryptedA = []
  for i in cipher:
    a = pow(i, key_private[0], key_private[1])
    decryptedA.append(a)

  plaintext = "".join([chr(i) for i in decryptedA])

  return plaintext
```

Zadanie 4

Odszukanie mojej listy

```python
Cipher = "MSBWKDAYPLR"
m = 75
a = 402
c = 77
x0 = 0
Key = key(Cipher,m,a,c,x0)
name = decrypt(Cipher,Key)
print(name)
```

    KONRAD PUSZ

Wczytanie listy i odszyfrowanie znaków

```python
Cipher_text = [512000, 1367631, 1000000, 1481544, 1367631, 1815848, 1331000, 1771561, 32768, 1000000, 1367631, 32768, 1061208, 1367631, 1259712, 1685159, 912673, 1481544, 1225043, 1601613, 32768, 1331000, 1157625, 1030301, 32768, 941192, 1157625, 1030301, 1092727, 1259712, 32768, 1520875, 1259712, 1601613, 1092727, 32768, 1815848, 912673, 1404928, 1771561, 1560896, 912673, 970299]
key_private = [5971645067, 8957657851]

text = RSA_decrypt(key_private, Cipher_text)
print(text)
```

    Podrozny do folwarku nie biegl slug zapytac

Hashowanie ciągu znaków

```python
text = "KONRAD"
h_text = hash.sha256(text.encode()).hexdigest()
print(h_text)
```

    0e9bfddb608c5e183ea77e91edc8ba1a2b790c287f5bf283acef77e70186ace5

Szyfrowanie hashu za pomocą RSA i klucza publicznego  (5, 4472424641)

```python
key_public = [5, 4472424641]

cipher_hash = RSA_encrypt(key_public, h_text)
print(cipher_hash)
```

    [254803968, 1565251219, 601692057, 94358686, 2095958750, 1055150718, 1055150718, 94358686, 459165024, 254803968, 550731776, 565051217, 418195493, 1565251219, 282475249, 550731776, 345025251, 1565251219, 4114915616, 503284375, 503284375, 1565251219, 601692057, 282475249, 1565251219, 1055150718, 565051217, 550731776, 94358686, 4114915616, 282475249, 4114915616, 312500000, 94358686, 503284375, 601692057, 254803968, 565051217, 312500000, 550731776, 503284375, 2095958750, 418195493, 94358686, 2095958750, 312500000, 550731776, 345025251, 4114915616, 565051217, 1565251219, 2095958750, 503284375, 503284375, 1565251219, 503284375, 254803968, 282475249, 550731776, 459165024, 4114915616, 565051217, 1565251219, 418195493]

Mój klucz publiczny: (3, 9523)

Klucz prywatny: (6219, 9523)


Construction d'un schéma exponentiel pour Vlasov-Maxwell hybride linéarisé
===

On regardera ici comment calculer l'exponentielle de matrice nécessaire pour construire un schéma de Lawson.

```python
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt
import scipy as sc
from IPython.display import display
sp.init_printing(use_latex='mathjax')
```

On travaille sur le système :

$$
  \begin{cases}
    \partial_t j_{c,x} = \Omega_{pe}^2E_x - j_{c,y}B_0 \\
    \partial_t j_{c,y} = \Omega_{pe}^2E_y + j_{c,x}B_0 \\
    \partial_t B_x     =  \partial_zE_y \\
    \partial_t B_y     = -\partial_zE_x \\
    \partial_t E_x     = -\partial_zB_y - j_x + \int v_xf\,\mathrm{d}v \\
    \partial_t E_y     =  \partial_zB_x - j_y + \int v_yf\,\mathrm{d}v \\
    \partial_t f       = - v_z\partial_t f + (E_x+v_yB_0-v_zB_y)\partial_{v_x}f + (E_y-v_xB_0+v_zB_x)\partial_{v_y}f + (v_xB_y-v_yB_x)\partial_{v_z}f
  \end{cases}
$$

Ce qui nous donne, sous forme matricielle (après une transformée de Fourier en $z$ et sans changement dans les notations) :

$$
  \partial_t \begin{pmatrix}
    j_{c,x} \\ j_{c_y} \\ B_x \\ B_y \\ E_x \\ E_y \\ f
  \end{pmatrix}
  =
  \begin{pmatrix}
     0 & -1 & 0  &  0  &  \Omega_{pe}^2 & 0             & 0 \\
     1 &  0 & 0  &  0  &  0             & \Omega_{pe}^2 & 0 \\
     0 &  0 & 0  &  0  &  0             & ik            & 0 \\
     0 &  0 & 0  &  0  & -ik            & 0             & 0 \\
    -1 &  0 & 0  & -ik &  0             & 0             & 0 \\
     0 & -1 & ik &  0  &  0             & 0             & 0 \\
     0 &  0 & 0  &  0  &  0             & 0             & -ikv_z
  \end{pmatrix}
  \begin{pmatrix}
    j_{c,x} \\ j_{c_y} \\ B_x \\ B_y \\ E_x \\ E_y \\ f
  \end{pmatrix}
  +
  \begin{pmatrix}
    0 \\ 0 \\ 0 \\ 0 \\ \int v_xf\,\mathrm{d}v \\ \int v_yf\,\mathrm{d}v \\ (E+v\times(B_0+B))\cdot\partial_vf
  \end{pmatrix}
$$

Sauf qu'il n'est pas possible de calculer l'exponentielle de cette matrice pour tout $\Delta t$ de manière exacte. Il est donc décidé de transférer le transport en $z$ (les "$ik$" dans la matrice) dans le terme non linéaire pour enfin écrire :

$$
  \partial_t U = AU + F(U)
$$

avec $U = (j_{c,x} , j_{c_y} , B_x , B_y , E_x , E_y , f)^\textsf{T}$ et :

$$
  A = \begin{pmatrix}
     0 & -1 & 0  &  0  &  \Omega_{pe}^2 & 0             & 0 \\
     1 &  0 & 0  &  0  &  0             & \Omega_{pe}^2 & 0 \\
     0 &  0 & 0  &  0  &  0             & 0             & 0 \\
     0 &  0 & 0  &  0  &  0             & 0             & 0 \\
    -1 &  0 & 0  &  0  &  0             & 0             & 0 \\
     0 & -1 & 0  &  0  &  0             & 0             & 0 \\
     0 &  0 & 0  &  0  &  0             & 0             & -ikv_z
  \end{pmatrix}
  \quad,\quad
  F(U) = \begin{pmatrix}
    0 \\ 0 \\ ikE_y \\ -ikE_x \\ -ikB_y + \int v_xf\,\mathrm{d}v \\ ikB_x + \int v_yf\,\mathrm{d}v \\ (E+v\times(B_0+B))\cdot\partial_vf
  \end{pmatrix}
$$

```python
jx,jy = sp.symbols(r"j_{c\,x} j_{c\,y}")
Bx,By = sp.symbols(r"B_x B_y")
Ex,Ey = sp.symbols(r"E_x E_y")
f = sp.symbols(r"\hat{f}")
wpe = sp.symbols(r"\Omega_{pe}",real=True,positive=True)
dt = sp.symbols(r"\Delta\ t",real=True,positive=True)
k = sp.symbols("k")
vx,vy,vz = sp.symbols("v_x v_y v_z",real=True)

U = sp.Matrix([jx,jy,Bx,By,Ex,Ey,f])
U
```

```python
A = sp.Matrix([[ 0 , -1 , 0 , 0 , wpe**2 , 0      ,  0         ],
               [ 1 ,  0 , 0 , 0 , 0      , wpe**2 ,  0         ],
               [ 0 ,  0 , 0 , 0 , 0      , 0      ,  0         ],
               [ 0 ,  0 , 0 , 0 , 0      , 0      ,  0         ],
               [-1 ,  0 , 0 , 0 , 0      , 0      ,  0         ],
               [ 0 , -1 , 0 , 0 , 0      , 0      ,  0         ],
               [ 0 ,  0 , 0 , 0 , 0      , 0      , -sp.I*k*vz ]
              ])
A
```

```python
eA = sp.exp(A[:-1,:-1]*dt)
eA.subs(wpe,2).evalf().simplify()
```

Il n'est pas nécessaire de calculer la matrice pour une valeur de $\Omega_{pe}$ quelconque. Cette grandeur est liée à des constantes physiques et on a $\Omega_{pe}=2$.

Pour s'assurer de la non nécessité numérique de calculer pour une valeur quelconque de $\Omega_{pe}$, regardons le premier terme de la matrice.

```python
eA[0,0]
```

```python
Ix = sp.Function(r"\int_\mathbb{R}\ v_x")
Iy = sp.Function(r"\int_\mathbb{R}\ v_y")
df = sp.Function(r"(E+v\times B)\cdot\nabla_v")
def F(U):
  return sp.Matrix([
     0,
     0,
     sp.I*k*U[5],
    -sp.I*k*U[4],
    -sp.I*k*U[3]+Ix(U[6]),
     sp.I*k*U[2]+Iy(U[6]),
     df(U[6])
  ])
F(U)
```

Pour calculer l'exponentielle de $\Delta t A$ rapidement, je calcule l'exponentielle de la sous matrice (sans la dernière colonne et denière ligne) et je construis ensuite la matrice exponentielle par bloc.

```python
def expM(A):
  A = A.subs(wpe,2)
  eA = sp.exp(A[:-1,:-1]).evalf().simplify().row_join(sp.zeros(6,1)).col_join(sp.Matrix(1,7,[0]*6+[sp.exp(A[-1,-1])]))
  return eA.simplify()

expM(A*dt)
```

On écrit maintenant le schéma Lawson induit par la méthode RK(3,3) de Shu-Osher.

```python
jxn,jyn = sp.symbols(r"j_{c\,x}^n j_{c\,y}^n")
Bxn,Byn = sp.symbols(r"B_x^n B_y^n")
Exn,Eyn = sp.symbols(r"E_x^n E_y^n")
fn = sp.symbols(r"\hat{f}^n")
Un = sp.Matrix([jxn,jyn,Bxn,Byn,Exn,Eyn,fn])

jx1,jy1 = sp.symbols(r"j_{c\,x}^{(1)} j_{c\,y}^{(1)}")
Bx1,By1 = sp.symbols(r"B_x^{(1)} B_y^{(1)}")
Ex1,Ey1 = sp.symbols(r"E_x^{(1)} E_y^{(1)}")
f1 = sp.symbols(r"\hat{f}^{(1)}")
Us1 = sp.Matrix([jx1,jy1,Bx1,By1,Ex1,Ey1,f1])

jx2,jy2 = sp.symbols(r"j_{c\,x}^{(2)} j_{c\,y}^{(2)}")
Bx2,By2 = sp.symbols(r"B_x^{(2)} B_y^{(2)}")
Ex2,Ey2 = sp.symbols(r"E_x^{(2)} E_y^{(2)}")
f2 = sp.symbols(r"\hat{f}^{(2)}")
Us2 = sp.Matrix([jx2,jy2,Bx2,By2,Ex2,Ey2,f2])

U1 = expM(A*dt)*Un + dt*expM(A*dt)*F(Un)
U2 = sp.Rational(3,4)*expM(sp.Rational(1,2)*A*dt)*Un + sp.Rational(1,4)*expM(-sp.Rational(1,2)*dt*A)*Us1 + sp.Rational(1,4)*dt*expM(-sp.Rational(1,2)*dt*A)*F(Us1)
Un1 = sp.Rational(1,3)*expM(A*dt)*Un + sp.Rational(2,3)*expM(sp.Rational(1,2)*dt*A)*Us2 + sp.Rational(2,3)*dt*expM(sp.Rational(1,2)*dt*A)*F(Us2)

display(U1,U2,Un1)
```

Il faut maintenant remplacer tous les symbols par des expressions qui peuvent être des noms de variable, ainsi que les fonctions. Certaines optimisations, comme le remplacement des divisions par 2 par une multiplication par 0.5, sont également faites.

```python
def expr_to_code (expr,symbols_replace,function_replace):
  math_to_stl = [(f,"std::"+f) for f in ("sin","cos","exp","sqrt")]

  a = sp.Wild('a')
  def func(expr):
    print(expr is sp.S.Half,expr)
    return sp.Float(0.5)

  tmp = expr.subs(symbols_replace)
  for old,new in function_replace:
    tmp = tmp.replace(old,new)
  #tmp = tmp.replace(lambda expr:expr is sp.S.Half,lambda _:sp.Float(0.5)).simplify().evalf()
  tmp = tmp.replace(a/2,0.5*a,exact=True).simplify()
  tmp = tmp.replace(lambda expr:expr is sp.S.One or expr == 1.0,lambda _:sp.S.One).simplify().evalf()

  tmp = tmp.replace(lambda expr:expr.is_integer and (expr is not sp.S.One or expr is not sp.S.NegativeOne),lambda expr:sp.symbols(str(expr)+"."))
  stmp = str(tmp)
  #display(gv.Source(sp.dotprint(tmp)))
  for old,new in math_to_stl:
    stmp = stmp.replace(old,new)
  return stmp
```

```python
sym_to_code = [(dt,sp.symbols("dt")),(k,sp.symbols("Kz[i]")),
               (Exn,sp.symbols("hEx[i]")),(Eyn,sp.symbols("hEy[i]")),
               (jxn,sp.symbols("hjcx[i]")),(jyn,sp.symbols("hjcy[i]")),
               (Bxn,sp.symbols("hBx[i]")),(Byn,sp.symbols("hBy[i]")),
               (fn,sp.symbols(r"hf[k_x][k_y][k_z][i]")),
               (Ex1,sp.symbols("hEx1[i]")),(Ey1,sp.symbols("hEy1[i]")),
               (jx1,sp.symbols("hjcx1[i]")),(jy1,sp.symbols("hjcy1[i]")),
               (Bx1,sp.symbols("hBx1[i]")),(By1,sp.symbols("hBy1[i]")),
               (f1,sp.symbols(r"hf1[k_x][k_y][k_z][i]")),
               (Ex2,sp.symbols("hEx2[i]")),(Ey2,sp.symbols("hEy2[i]")),
               (jx2,sp.symbols("hjcx2[i]")),(jy2,sp.symbols("hjcy2[i]")),
               (Bx2,sp.symbols("hBx2[i]")),(By2,sp.symbols("hBy2[i]")),
               (f2,sp.symbols(r"hf2[k_x][k_y][k_z][i]")),
               #(sp.sqrt(2),sp.symbols("sqrt2_v"))
              ]
fun_to_code = [(Ix,lambda _:sp.symbols("hjhx[i]")),(Iy,lambda _:sp.symbols("hjhy[i]")),(df,lambda _:sp.symbols("hfvxvyvz[i]"))]

for stage,nextU in zip([U1,U2,Un1],[Us1,Us2,Un]) :
  code_nextU = nextU.subs(sym_to_code)
  for i,l in enumerate(stage):
    print(str(code_nextU[i])+" = "+expr_to_code(l,sym_to_code,fun_to_code)+";")
    print("// ---")
  print("\n/* ------ */\n")
```

J'ai essayé plusieurs fois de supprimer les `1.0` qui apparaissent dans les formules, mais je n'y suis jamais parvenu.

On teste maintenant du Lawson(RK(4,4))

```python
jx1,jy1 = sp.symbols(r"j_{c\,x}^{(1)} j_{c\,y}^{(1)}")
Bx1,By1 = sp.symbols(r"B_x^{(1)} B_y^{(1)}")
Ex1,Ey1 = sp.symbols(r"E_x^{(1)} E_y^{(1)}")
f1 = sp.symbols(r"\hat{f}^{(1)}")
Us1 = sp.Matrix([jx1,jy1,Bx1,By1,Ex1,Ey1,f1])

jx2,jy2 = sp.symbols(r"j_{c\,x}^{(2)} j_{c\,y}^{(2)}")
Bx2,By2 = sp.symbols(r"B_x^{(2)} B_y^{(2)}")
Ex2,Ey2 = sp.symbols(r"E_x^{(2)} E_y^{(2)}")
f2 = sp.symbols(r"\hat{f}^{(2)}")
Us2 = sp.Matrix([jx2,jy2,Bx2,By2,Ex2,Ey2,f2])

jx3,jy3 = sp.symbols(r"j_{c\,x}^{(3)} j_{c\,y}^{(3)}")
Bx3,By3 = sp.symbols(r"B_x^{(3)} B_y^{(3)}")
Ex3,Ey3 = sp.symbols(r"E_x^{(3)} E_y^{(3)}")
f3 = sp.symbols(r"\hat{f}^{(3)}")
Us3 = sp.Matrix([jx3,jy3,Bx3,By3,Ex3,Ey3,f3])

U1 = expM(sp.Rational(1,2)*A*dt)*Un + sp.Rational(1,2)*dt*expM(sp.Rational(1,2)*A*dt)*F(Un)
U2 = expM(sp.Rational(1,2)*A*dt)*Un + sp.Rational(1,2)*dt*F(Us1)
U3 = expM(A*dt)*Un + dt*expM(sp.Rational(1,2)*A*dt)*F(Us2)
Un1 = -sp.Rational(1,3)*expM(A*dt)*Un + sp.Rational(1,3)*expM(sp.Rational(1,2)*dt*A)*Us1 + sp.Rational(2,3)*expM(sp.Rational(1,2)*dt*A)*Us2 + sp.Rational(1,3)*Us3 + sp.Rational(1,6)*dt*F(Us3)

display(U1,U2,Us3,Un1)
```

```python
sym_to_code = [(dt,sp.symbols("dt")),(k,sp.symbols("Kz[i]")),
               (Exn,sp.symbols("hEx[i]")),(Eyn,sp.symbols("hEy[i]")),
               (jxn,sp.symbols("hjcx[i]")),(jyn,sp.symbols("hjcy[i]")),
               (Bxn,sp.symbols("hBx[i]")),(Byn,sp.symbols("hBy[i]")),
               (fn,sp.symbols(r"hf[k_x][k_y][k_z][i]")),
               (Ex1,sp.symbols("hEx1[i]")),(Ey1,sp.symbols("hEy1[i]")),
               (jx1,sp.symbols("hjcx1[i]")),(jy1,sp.symbols("hjcy1[i]")),
               (Bx1,sp.symbols("hBx1[i]")),(By1,sp.symbols("hBy1[i]")),
               (f1,sp.symbols(r"hf1[k_x][k_y][k_z][i]")),
               (Ex2,sp.symbols("hEx2[i]")),(Ey2,sp.symbols("hEy2[i]")),
               (jx2,sp.symbols("hjcx2[i]")),(jy2,sp.symbols("hjcy2[i]")),
               (Bx2,sp.symbols("hBx2[i]")),(By2,sp.symbols("hBy2[i]")),
               (f2,sp.symbols(r"hf2[k_x][k_y][k_z][i]")),
               (Ex3,sp.symbols("hEx3[i]")),(Ey3,sp.symbols("hEy3[i]")),
               (jx3,sp.symbols("hjcx3[i]")),(jy3,sp.symbols("hjcy3[i]")),
               (Bx3,sp.symbols("hBx3[i]")),(By3,sp.symbols("hBy3[i]")),
               (f3,sp.symbols(r"hf3[k_x][k_y][k_z][i]")),
               #(sp.sqrt(2),sp.symbols("sqrt2_v"))
              ]
fun_to_code = [(Ix,lambda _:sp.symbols("hjhx[i]")),(Iy,lambda _:sp.symbols("hjhy[i]")),(df,lambda _:sp.symbols("hfvxvyvz[i]"))]

for stage,nextU in zip([U1,U2,U3,Un1],[Us1,Us2,Us3,Un]) :
  code_nextU = nextU.subs(sym_to_code)
  for i,l in enumerate(stage):
    print(str(code_nextU[i])+" = "+expr_to_code(l,sym_to_code,fun_to_code)+";")
    print("// ---")
  print("\n/* ------ */\n")
```

Dans le code généré il est encore nécessaire d'écrire les boucles qui vont autour, de la manière suivante :

1. Calcul des variables $\hat{j}_{c,x}^{(1)}$, $\hat{j}_{c,y}^{(1)}$, $\hat{E}_x^{(1)}$, $\hat{E}_y^{(1)}$, $\hat{B}_x^{(1)}$ et $\hat{B}_y^{(1)}$ :
    + calcul des intégrales $\int v_x \hat{f}\,\mathrm{d}v$ et $\int v_y \hat{f}\,\mathrm{d}v$ :
        - $\left(\hat{j}_{h,x}\right)_i \gets \sum_{k_x,k_y,k_z} v_{k_x}\,\hat{f}_{i,k_x,k_y,k_z}\,\Delta v$
        - $\left(\hat{j}_{h,y}\right)_i \gets \sum_{k_x,k_y,k_z} v_{k_y}\,\hat{f}_{i,k_x,k_y,k_z}\,\Delta v$
    + pour tout $x_i$ (code donné par `sympy`) :
        - $\hat{j}_{c,x}^{(1)}\gets \cdots$
        - $\hat{j}_{c,y}^{(1)}\gets \cdots$
        - $\hat{E}_{c,x}^{(1)}\gets \cdots$
        - $\hat{E}_{c,y}^{(1)}\gets \cdots$
        - $\hat{B}_{c,x}^{(1)}\gets \cdots$
        - $\hat{B}_{c,y}^{(1)}\gets \cdots$
2. Calcul de la variable $\hat{f}^{(1)}$ :
    + transformée inverse de $\hat{f}^{n}$ :
        - $\left(f\right)_{i,k_x,k_z,k_z} \gets \text{iFFT}(\hat{f}^n_{\cdot,k_x,k_z,k_z})_i$ (boucle en $v_x$, $v_y$ et $v_z$)
    + calcul de l'approximation de $(E_x+v_yB_0 + v_zB_y)\partial_{v_x}f + (E_y-v_xB_0+v_zB_x)\partial_{v_y}f +(v_xB_y - v_yB_x)\partial_{v_z}f$ :
        - pour tout $v_{k_x}$ :
            - pour tout $v_{k_y}$ :
                - pour tout $v_{k_y}$ :
                    - pour tout $v_{k_z}$ :
                        - pour tout $k_i$ :
                            - $\texttt{velocity_vx} \gets E_{x,i}+v_{k_y}B_0 + v_{k_z}B_{y,i}$
                            - $\texttt{velocity_vy} \gets E_{y,i}-v_{k_x}B_0 + v_{k_z}B_{x,i}$
                            - $\texttt{velocity_vz} \gets v_{k_x}B_{y,i} - v_{k_y}B_{x,i}$
                            - $\partial_vf_{i,k_x,k_y,k_z}\gets \text{WENO}(\texttt{velocity_vx},f_{i,k_x-3:k_x+3,k_y,k_z}) + \text{WENO}(\texttt{velocity_vy},f_{i,k_x,k_y-3:k_y+3,k_z}) + \text{WENO}(\texttt{velocity_vz},f_{i,k_x,k_y,k_z-3:k_z+3})$
    + incrémentation de $\hat{f}^{(1)}$ :
        - pour tout $v_{k_x}$ :
            - pour tout $v_{k_y}$ :
                - pour tout $v_{k_y}$ :
                    - pour tout $v_{k_z}$ :
                        - $\left(\widehat{\partial_vf}\right)_i\gets\text{FFT}(\partial_vf_{\cdot,k_x,k_y,k_z})_i$
                        - pour tout $k_i$ :
                            - $\hat{f}^{(1)}_{i,k_x,k_y,k_z} \gets \hat{f}^n_{i,k_x,k_y,k_z} + \Delta t\widehat{\partial_vf}_i$

où $\text{WENO}$ est une fonction qui calcule une approximation de $a\partial_xu(x_i)$ en prenant en argument $a$ et le stencil $u_{i-3:i+3}$. Cette fonction WENO diffère de ce que j'avais déjà pu implémenter avant, car il s'agissait alors d'une fonction renvoyant le flux numérique $\hat{f}^\pm_{i+\frac{1}{2}}$. C'est-à-dire $\text{WENO}(a,u_{i-3},u_{i-2},u_{i-1},u_{i},u_{i+1},u_{i+2},u_{i+3})$ est une fonction renvoyant l'approximation : $$a^+\frac{u^+_{i+\frac{1}{2}}-u^+_{i-\frac{1}{2}}}{\Delta x} + a^-\frac{u^-_{i+\frac{1}{2}}-u^-_{i-\frac{1}{2}}}{\Delta x}$$ donc les flux numériques sont calculés 2 fois, ce qui engendre des pertes en temps de calcul, mais un gain important en utilisation de la mémoire (puisqu'il n'est pas nécessaire de sauvergarder tous les flux numériques avant calcul des différences).

Il existe un module `codegen` dans `sympy` qui ressemble à ce que je fais, mais celui-ci génère des fonctions, suppose que le symbole est directement convertible en nom de variable, etc. et fait toujours apparaître des `1.0` un peu partout dans ses expressions. Par exemple pour chaque "ligne" d'une méthode d'Euler explicite.

```python
from sympy.utilities.codegen import codegen

for l in expM(A*dt)*Un + dt*expM(A*dt)*F(U) :
  print(codegen(("osef",l),"C")[0][1])
  print("---")
```

Il ne serait pas inintéressant de s'intéresser un peu plus au module, mais je ne sais pas comment définir proprement un tableau (car il faut remplacer tous les symbols par des tableaux) avec `sympy`... donc à voir...

Ce module présente l'avantage d'utiliser les variables du module `math.h` de `C` (par exemple `M_SQRT2`). Ces variables ont leur équivalent `C++` uniquement à partir de `C++20` dans le module `cmath`, avant il faut utiliser ceux de `C`.

> **Nota Bene :** j'ai remarqué récemment qu'il était possible d'indiquer que tous mes tableaux sont des objets indicés (`sp.IndexedBase`) et cela permet, lors de la génération de code, de dérouler les boucles. Mais vu la lourdeur d'utilisation pour moi, par rapport à ce que j'ai pu coder... je pense que je ne vais pas explorer plus cette piste tout de suite.

### une énième tentative de mettre le plus de choses dans la partie linéaire

```python
B = sp.Matrix([[ 0 , -1 , 0      ,  0      ,  wpe**2 , 0      ,  0         ],
               [ 1 ,  0 , 0      ,  0      ,  0      , wpe**2 ,  0         ],
               [ 0 ,  0 , 0      ,  0      ,  0      , sp.I*k ,  0         ],
               [ 0 ,  0 , 0      ,  0      , -sp.I*k , 0      ,  0         ],
               [-1 ,  0 , 0      , -sp.I*k ,  0      , 0      ,  0         ],
               [ 0 , -1 , sp.I*k ,  0      ,  0      , 0      ,  0         ],
               [ 0 ,  0 , 0      ,  0      ,  0      , 0      , -sp.I*k*vz ]
              ])
try:
  sp.exp(B.subs(wpe,2)*dt)
except(NotImplementedError):
  print("sympy gère pas cette matrice")
```

```python
for e in (B[:-1,:-1].subs(wpe,2)*dt).eigenvals():
  display(e)
  print("---")
```

Les valeurs propres de la sous-matrice (sans le transport en $z$ de $f$) sont relativement compliquées, il est donc très compliqué d'obtenir l'exponentielle de la matrice. `sympy` semble malgré tout calculer quelque chose, donc il est peut-être envisageable d'effectuer le calcul sur le serveur de calcul... à voir...

Une méthode itérative est aussi envisageable pour avoir une méthode approchée, mais je ne sais pas si cela est vraiment possible avec du calcul formel (nécessaire pour le faire pour tout mode $k$ et pour tout $\Delta t$).

Les tentatives de calcul direct pour tout mode $k$ et tout $\Delta t>0$ sur le serveur (donc sans contrainte de temps de calcul) ont échoué, même en fixant le mode $k$ (donc en voulant faire une trentaine de calcul pour chaque mode de Fourier dans la discrétisation en $z$).

```python

```


# Joint Measurements Kata

**Joint Measurements** quantum kata is a series of exercises designed to get you familiar with programming in Q#. It covers the joint parity measurements and using them for distinguishing quantum states or for performing multi-qubit gates.

* In Q# joint measurements are implemented as the [Measure](https://docs.microsoft.com/qsharp/api/qsharp/microsoft.quantum.intrinsic.measure) operation.
* You can read more about measurements of multi-qubit Pauli operators in the [Q# documentation](https://docs.microsoft.com/quantum/concepts/pauli-measurements).

Each task is wrapped in one operation preceded by the description of the task.  Your goal is to fill in the blank (marked with `// ...` comments)
with some Q# code that solves the task. To verify your answer, run the cell using Ctrl/⌘+Enter.

The tasks are given in approximate order of increasing difficulty; harder ones are marked with asterisks.

To begin, first prepare this notebook for execution (if you skip this step, you'll get "Syntax does not match any known patterns" error when you try to execute Q# code in the next cells):

```qsharp
%package Microsoft.Quantum.Katas::0.10.1911.1607
```

Microsoft.Quantum.Standard::0.10.1911.1607

Microsoft.Quantum.Katas::0.10.1911.1607

> The package versions in the output of the cell above should always match. If you are running the Notebooks locally and the versions do not match, please install the IQ# version that matches the version of the `Microsoft.Quantum.Katas` package.
>
> How to install the right IQ# version
> For example, if the version of `Microsoft.Quantum.Katas` package above is 0.1.2.3, the installation steps are as follows:
>
> 1. Stop the kernel.
> 2. Uninstall the existing version of IQ#:
>        dotnet tool uninstall microsoft.quantum.iqsharp -g
> 3. Install the matching version:
>        dotnet tool install microsoft.quantum.iqsharp -g --version 0.1.2.3
> 4. Reinstall the kernel:
>        dotnet iqsharp install
> 5. Restart the Notebook.
>

```qsharp
%workspace reload
```

Quantum.Kata.JointMeasurements.CNOTWrapper

Quantum.Kata.JointMeasurements.ControlledX

Quantum.Kata.JointMeasurements.ControlledX_General

Quantum.Kata.JointMeasurements.ControlledX_General_Reference

Quantum.Kata.JointMeasurements.ControlledX_Reference

Quantum.Kata.JointMeasurements.DifferentBasis

Quantum.Kata.JointMeasurements.DifferentBasis_Reference

Quantum.Kata.JointMeasurements.DistinguishStates_MultiQubit

Quantum.Kata.JointMeasurements.GetMultiQubitNonMeasurementOpCount

Quantum.Kata.JointMeasurements.GHZOrGHZWithX

Quantum.Kata.JointMeasurements.GHZOrGHZWithX_Reference

Quantum.Kata.JointMeasurements.GHZOrWState

Quantum.Kata.JointMeasurements.GHZOrWState_Reference

Quantum.Kata.JointMeasurements.ParityMeasurement

Quantum.Kata.JointMeasurements.ParityMeasurement_Reference

Quantum.Kata.JointMeasurements.SingleQubitMeasurement

Quantum.Kata.JointMeasurements.SingleQubitMeasurement_Reference

Quantum.Kata.JointMeasurements.StatePrep_A

Quantum.Kata.JointMeasurements.StatePrep_DifferentBasis

Quantum.Kata.JointMeasurements.StatePrep_GHZOrWState

Quantum.Kata.JointMeasurements.StatePrep_ParityMeasurement

Quantum.Kata.JointMeasurements.StatePrep_WState_Arbitrary

Quantum.Kata.JointMeasurements.T01_SingleQubitMeasurement_Test

Quantum.Kata.JointMeasurements.T02_ParityMeasurement_Test

Quantum.Kata.JointMeasurements.T03_GHZOrGHZWithX_Test

Quantum.Kata.JointMeasurements.T04_GHZOrWState_Test

Quantum.Kata.JointMeasurements.T05_DifferentBasis_Test

Quantum.Kata.JointMeasurements.T06_ControlledX_Test

Quantum.Kata.JointMeasurements.T07_ControlledX_General_Test

### Task 1. Single-qubit measurement

**Input:** Two qubits (stored in an array) which are guaranteed to be either in superposition of states $|00\rangle$ and $|11\rangle$ or in superposition of states $|01\rangle$ and $|10\rangle$.

**Output:**  0 if qubits were in the first superposition, 1 if they were in the second superposition.
*The state of the qubits at the end of the operation does not matter.*

  Need a hint? Click here
    Use two single-qubit measurements.

📝 `CNOT` を掛けると、`qs[1]` が必ず $|0\rangle$ or 必ず $|1\rangle$ になる。

```qsharp
%kata T01_SingleQubitMeasurement_Test

operation SingleQubitMeasurement (qs : Qubit[]) : Int {
    CNOT(qs[0], qs[1]);
    return M(qs[1]) == Zero ? 0 | 1;
}
```

    Success!

### Task 2. Parity measurement

**Inputs**: Two qubits (stored in an array) which are guaranteed to be either in superposition of states $|00\rangle$ and $|11\rangle$ or in superposition of states $|01\rangle$ and $|10\rangle$.

**Output**: 0 if qubits were in the first superposition, 1 if they were in the second superposition.
*The state of the qubits at the end of the operation should be the same as the starting state.*

📝 元の状態に戻さないといけないので、`CNOT` の逆操作（= `CNOT`）を測定後に行う。

```qsharp
%kata T02_ParityMeasurement_Test

operation ParityMeasurement (qs : Qubit[]) : Int {
    CNOT(qs[0], qs[1]);
    let result = M(qs[1]) == Zero ? 0 | 1;
    CNOT(qs[0], qs[1]);
    return result;
}
```

    Success!

📝 [Pauli Measurements](https://docs.microsoft.com/en-us/quantum/concepts/pauli-measurements?view=qsharp-preview) のドキュメントによれば、計算基底に限らず、好きな基底で測定する（Pauli measurement）ことが [`Measure`](https://docs.microsoft.com/en-us/qsharp/api/qsharp/microsoft.quantum.intrinsic.measure) でできるらしい。複数キュビットの測定にも適用でき、その場合も結果は `Zero` か `One` になる。

```qsharp
%kata T02_ParityMeasurement_Test

operation ParityMeasurement (qs : Qubit[]) : Int {
    return Measure([PauliZ, PauliZ], qs) == Zero ? 0 | 1;
}
```

    Success!

📝 上のコードでは、$Z \otimes Z$ で測定した。これは

$$
Z \otimes Z = \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & -1 & 0 & 0 \\
0 & 0 & -1 & 0 \\
0 & 0 & 0 & 1 \\
\end{bmatrix}
$$

なので、

$$
\begin{align}
(Z \otimes Z)|00\rangle &= |00\rangle, \\
(Z \otimes Z)|11\rangle &= |11\rangle
\end{align}
$$

となり、これらに対する固有値は 1。よって、測定結果として `Zero` が返ってくる。また、

$$
\begin{align}
(Z \otimes Z)|01\rangle &= - |01\rangle, \\
(Z \otimes Z)|10\rangle &= - |10\rangle
\end{align}
$$

となり、これらに対する固有値は -1。よって、測定結果として `One` が返ってくる。

### Task 3. $|0000\rangle + |1111\rangle$ or $|0011\rangle + |1100\rangle$  ?
**Inputs**: Four qubits (stored in an array) which are guaranteed to be either in superposition of states $|0000\rangle$ and $|1111\rangle$ or in superposition of states $|0011\rangle$ and $|1100\rangle$.

**Output** : 0 if qubits were in the first superposition, 1 if they were in the second superposition.
*The state of the qubits at the end of the operation should be the same as the starting state.*

📝 真ん中の2キュビットだけ見ればと前問と同じ。

```qsharp
%kata T03_GHZOrGHZWithX_Test

operation GHZOrGHZWithX (qs : Qubit[]) : Int {
    return ParityMeasurement(qs[1..2]);
}
```

    Success!

### Task 4. $|0..0\rangle + |1..1\rangle$ or W state ?

**Inputs:** An even number of qubits (stored in an array) which are guaranteed to be either in a superposition of states |0..0$\rangle$ and |1..1$\rangle$ or in the [W state](https://en.wikipedia.org/wiki/W_state).

**Output:** 0 if qubits were in the first superposition, 1 if they were in the second superposition.
*The state of the qubits at the end of the operation should be the same as the starting state.*

📝 Task 2 の $Z \otimes Z$ での測定結果は、状態の偶奇に対応していた。キュビット数が増えても、$Z \otimes \dots \otimes Z$ で同様に偶奇が区別できる。今回、キュビット数は偶数なので、$|0\dots 0\rangle$ や $|1\dots 1\rangle$ は必ず偶になる。逆に、W state はつねに奇。

```qsharp
%kata T04_GHZOrWState_Test

open Microsoft.Quantum.Characterization;

operation GHZOrWState (qs : Qubit[]) : Int {
    return MeasureAllZ(qs) == Zero ? 0 | 1;
}
```

    Success!

### Task 5*. Parity measurement in different basis

**Inputs:** Two qubits (stored in an array) which are guaranteed to be
* either in superposition $\alpha |00\rangle + \beta |01\rangle + \beta |10\rangle + \alpha |11\rangle$
* or in superposition $\alpha |00\rangle - \beta |01\rangle + \beta |10\rangle - \alpha |11\rangle$

**Output:** 0 if qubits were in the first superposition, 1 if they were in the second superposition.
*The state of the qubits at the end of the operation should be the same as the starting state.*

📝 前者は $|\mathord{++}\rangle$ と $|\mathord{--}\rangle$ の重ね合わせ、後者は $|\mathord{+-}\rangle$ と $|\mathord{-+}\rangle$ の重ね合わせで表せるので、$X \otimes X$ で測定すればいいらしい。思いつかない…

実際に、

$$
\begin{align}
a |\mathord{++}\rangle + b |\mathord{--}\rangle
&= \frac{a}{2} \big(|00\rangle + |01\rangle + |10\rangle + |11\rangle \big)
 + \frac{b}{2} \big(|00\rangle - |01\rangle - |10\rangle + |11\rangle \big) \\
&= \frac{a + b}{2} |00\rangle + \frac{a - b}{2} |00\rangle + \frac{a - b}{2} |10\rangle + \frac{a + b}{2} |11\rangle
\end{align}
$$

なので、

$$
\alpha = \frac{a + b}{2},\quad \beta = \frac{a - b}{2}
$$

となるように $a, b$ を決めれば $\alpha |00\rangle + \beta |01\rangle + \beta |10\rangle + \alpha |11\rangle$ となる。後者も同様。

```qsharp
%kata T05_DifferentBasis_Test

operation DifferentBasis (qs : Qubit[]) : Int {
    return Measure([PauliX, PauliX], qs) == Zero ? 0 | 1;
}
```

    Success!

### Task 6*. Controlled X gate with $|0\rangle$ target

**Input:** Two unentangled qubits (stored in an array of length 2). The first qubit (`qs[0]`) will be in state $|\psi\rangle = \alpha |0\rangle + \beta |1\rangle$ , the second (`qs[1]`) - in state $|0\rangle$ (this can be written as two-qubit state $\big(\alpha |0\rangle + \beta |1\rangle\big) \otimes |0\rangle$).

**Output:** Change the two-qubit state to $\alpha |00\rangle + \beta |11\rangle$ using only single-qubit gates and joint measurements. Do not use two-qubit gates. You do not need to allocate extra qubits.

📝 Task 2 の測定では偶奇が判別できた。また、この測定を行っても、キュビットの状態には影響を与えなかった。

$\big(\alpha |0\rangle + \beta |1\rangle\big) \otimes |0\rangle = \alpha |00\rangle + \beta |10\rangle$ なので、奇だった場合にのみ `Z(qs[1])` を適用すれば、$\alpha |00\rangle + \beta |11\rangle$ になる。

…かと思ったが、「キュビットの状態には影響を与えなかった」というのは Task 2 の入力が「$|00\rangle$ と $|11\rangle$ の重ね合わせ」or「$|01\rangle$ と $|10\rangle$ の重ね合わせ」という条件があったから成り立っただけだった。

```qsharp
%kata T06_ControlledX_Test

operation ControlledX (qs : Qubit[]) : Unit {
    if (Measure([PauliZ, PauliZ], qs) == One) {
        Z(qs[1]);
    }
}
```

    Qubit in invalid state. Expecting: Zero
    	Expected:	0
    	Actual:	0.030153689607045803
    Try again!

📝 解答を見ると、$H$ を使うらしい。（以下、係数の $1/\sqrt{2}$ は省略）

$$
\begin{align}
\big(\alpha |0\rangle + \beta |1\rangle\big) \otimes |0\rangle
&\xrightarrow{I \otimes H}
  \big(\alpha |0\rangle + \beta |1\rangle\big) \otimes \big( |0\rangle + |1\rangle \big) \\
&= \big(\alpha|00\rangle + \beta|11\rangle\big) + \big(\alpha|01\rangle + \beta|10\rangle\big)
\end{align}
$$

$Z \otimes Z$ で測定すると、偶なら第1項、奇なら第2項の状態になる。偶の場合、そのままで $\alpha |00\rangle + \beta |11\rangle$。奇の場合、後ろのキュビットを反転させれば $\alpha |00\rangle + \beta |11\rangle$ になる。

```qsharp
%kata T06_ControlledX_Test

operation ControlledX (qs : Qubit[]) : Unit {
    H(qs[1]);
    if (Measure([PauliZ, PauliZ], qs) == One) {
        X(qs[1]);
    }
}
```

    Success!

### Task 7**. Controlled X gate with arbitrary target

**Input:** Two qubits (stored in an array of length 2) in an arbitrary two-qubit state $\alpha |00\rangle + \beta |01\rangle + \color{blue}\gamma |10\rangle  + \color{blue}\delta |11\rangle$.

**Goal:** Change the two-qubit state to $\alpha |00\rangle + \beta |01\rangle + \color{red}\delta |10\rangle  + \color{red}\gamma |11\rangle$ using only single-qubit gates and joint measurements.  Do not use two-qubit gates.

> A general-case implementation of CNOT gate via joint measurements is described in [this paper](https://arxiv.org/pdf/1201.5734.pdf).

  Need a hint? Click here
  You can use an extra qubit to perform this operation.

```qsharp
%kata T07_ControlledX_General_Test

operation ControlledX_General (qs : Qubit[]) : Unit {
    // ...
}
```


# NgspiceX - Monte Carlo simulation example

1.0 | 22.08.2021 | jchabloz | First draft

1.1 | 21.12.2021 | jchabloz | New run with NgSpice 35

Table of Contents

NgspiceX - Monte Carlo simulation example

Introduction

Simulation preparation

Expected results

Monte Carlo simulation loop

Simulation results

Comparison with expected results

Conclusion

## Introduction

This notebook intends to demonstrate how to use [ngspice-connect](https://github.com/jchabloz/ngspice-connect) to run efficiently a [Monte Carlo simulation](https://www.wikiwand.com/en/Monte_Carlo_method) with [ngspice](http://ngspice.sourceforge.net/) by using Python instead of resorting to the simulator internal scripting language.

In this demo, we create a simple resistive divider circuit with the value of the resistances including some statistical variations. We distinguish a *lot (or process) variation*, that would affect the nominal value of the resistances in a similar manner for all the resistances in the circuit from a *device variation* that affects separately each resistor and that models the mismatch between them.

## Simulation preparation

The demo start by importing all the required packages and creating an instance of the ``NgSpice`` class to access the ngspice shared library.

```python
from ngspicex import NgSpice
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from math import sqrt
from tqdm import tqdm
from time import perf_counter
```

```python
ngx = NgSpice()
```

     ******
     ** ngspice-35 shared library
     ** Creation Date: Tue Dec 21 19:51:18 UTC 2021
     ******

We define a few parameters for the simulation:

```python
V1 = 1.0            #[V]    Input voltage
sigma_R_lot = 100   #[Ohms] Standard deviation (lot)
sigma_R_dev = 200   #[Ohms] Standard deviation (device)
R = 1e4             #[Ohms] Resistances nominal value
lot = 0             #[-]    0: don't use lot variations, 1: use lot variations
dev = 1             #[-]    0: don't use device variations, 1: use device variations
niter = 500         #[-]    Number of iterations
```

The next step is to create the circuit in ngspice. If we want to use parameters, the easiest is to use the ``send_circ()`` function, especially for a small netlist. The netlist is sent line after line to ngspice.
The statistical variations of the resistors value are introduced via parameters and the built-in ngspice `agauss` function.

```python
ngx.send_circ(
    "*Test Monte-Carlo of resistive divider",
    ".param __LOT__ = {:d}".format(lot),
    ".param __DEV__ = {:d}".format(dev),
    ".param r0 = {}".format(R),
    ".param dr0 = agauss(0.0, '{}*__LOT__', 1) rval='r0 + dr0'".format(sigma_R_lot),
    ".param dr1 = agauss(0.0, '{}*__DEV__', 1)".format(sigma_R_dev),
    "R1 n1 n2 'rval + dr1'",
    "R2 n2 0 'rval + dr1'",
    "V1 n1 0 DC {}".format(V1),
    ".op",
    ".end")
```

    Source Deck
     No compatibility mode selected!
     Circuit: *test monte-carlo of resistive divider

### Expected results

Let us consider the case for ``lot = 0, dev = 1``.
What are the expected statistics for the voltage at the middle point of the resistive divider and the current through it?

Since we consider the case for which both resistances of the divider are nominally equal $R_1 = R_2 = R$, we can calculate the expected standard deviations for both the output voltage $\sigma_{V_2}$ and the current $\sigma_I$ as

$$\begin{align}
\sigma_{V_2} &= \frac{1}{\sqrt{2}}\frac{\sigma_R}{R}\cdot\frac{V_1}{2}\\
\sigma_{I} &= \frac{1}{\sqrt{2}}\frac{\sigma_R}{R}\cdot\frac{V_1}{2R}\\
\end{align}$$.

```python
sigma_V2 = 1/sqrt(2)*sigma_R_dev/R*V1/2
sigma_I = 1/sqrt(2)*sigma_R_dev/R*V1/2/R

print("Expected standard deviation on V(n2) (lot=0, dev=1) = {:.3f}mV".format(sigma_V2*1e3))
print("Expected standard deviation on I(V1) (lot=0, dev=1) = {:.3f}uA".format(sigma_I*1e6))
```

    Expected standard deviation on V(n2) (lot=0, dev=1) = 7.071mV
    Expected standard deviation on I(V1) (lot=0, dev=1) = 0.707uA

## Monte Carlo simulation loop

Using Python, it is actually quite easy to run a simulation loop, collect the results and concatenate them into a single [pandas](https://pandas.pydata.org) DataFrame. Using [tqdm](https://tqdm.github.io/) to show a progress bar makes for a nice way to monitor the progress of the loop.
Note that in order to make sure that the values of the parameters are recomputed for each iteration, the `reset` commands needs to be used.

```python
dfx=[]
t0 = perf_counter()
for i in tqdm(range(niter)):
    ngx.reset()
    ngx.run(silent=True)
    dfx.append(ngx.get_all_vectors())
df_out = pd.concat(dfx)
t1 = perf_counter()
print("Monte Carlo loop time: {:.3f}s".format(t1 - t0))
```

    100%|████████████████████████████████████████| 500/500 [00:01<00:00, 461.65it/s]

    Monte Carlo loop time: 1.180s

## Simulation results

Having directly the simulation results in a pandas DataFrame allows to use all the available powerful tools of pandas and associated libraries to analyze them.

For example, using the `describe()` method allows to get a quick overview of the content and statistics of a DataFrame.

```python
df_out.describe().T
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      i(v1)
      500.0
      -0.000050
      7.358976e-07
      -0.000052
      -0.000051
      -0.000050
      -0.000049
      -0.000048

      n2
      500.0
      0.500115
      7.120330e-03
      0.480827
      0.495229
      0.499762
      0.505097
      0.520268

      n1
      500.0
      1.000000
      0.000000e+00
      1.000000
      1.000000
      1.000000
      1.000000
      1.000000

We can also plot the distributions for the variables of interest. For example using the nice [seaborn library](https://seaborn.pydata.org/).

```python
sns.set_theme(context='notebook', style='whitegrid')
```

```python
f, axs = plt.subplots(1, 2, figsize=(14,6), sharey=True)
sns.histplot(data=df_out, x="i(v1)", ax=axs[0], stat='probability', kde=True)
_ = sns.histplot(data=df_out, x="n2", ax=axs[1], stat='probability', kde=True)
```

### Comparison with expected results

```python
sigma_V2_sim = df_out.n2.std()
print("Expected standard deviation on V(n2) = {:.3f}mV".format(sigma_V2*1e3))
print("Simulated standard deviation on V(n2) = {:.3f}mV".format(sigma_V2_sim*1e3))
print("Error = {:.1f}%".format((sigma_V2_sim/sigma_V2 - 1)*100.0))
```

    Expected standard deviation on V(n2) = 7.071mV
    Simulated standard deviation on V(n2) = 7.120mV
    Error = 0.7%

```python
sigma_I_sim = df_out['i(v1)'].std()
print("Expected standard deviation on I(V1) = {:.3f}uA".format(sigma_I*1e6))
print("Simulated standard deviation on I(V1) = {:.3f}uA".format(sigma_I_sim*1e6))
print("Error = {:.1f}%".format((sigma_I_sim/sigma_I - 1)*100.0))
```

    Expected standard deviation on I(V1) = 0.707uA
    Simulated standard deviation on I(V1) = 0.736uA
    Error = 4.1%

## Conclusion

We have demonstrated that using the ngspice-connect Python wrapper to ngspice shared library allows to efficiently run a Monte Carlo simulation loop with very little overhead.
Using an example based on a very simple resistive divider netlist, we have seen that the obtained results using the ngspice built-in `agauss` function are consistent with the theoretical results.