# Optimization towards a Perfect Entangler

```python
# NBVAL_IGNORE_OUTPUT
%load_ext watermark
import qutip
import numpy as np
import scipy
import matplotlib
import matplotlib.pylab as plt
import krotov
from IPython.display import display
import weylchamber as wc
from weylchamber.visualize import WeylChamber
from weylchamber.coordinates import from_magic

%watermark -v --iversions
```

    Python implementation: CPython
    Python version       : 3.8.1
    IPython version      : 7.24.1

    scipy      : 1.6.3
    qutip      : 4.6.1
    weylchamber: 0.3.2
    matplotlib : 3.4.2
    krotov     : 1.2.1+dev
    numpy      : 1.20.3

$\newcommand{tr}[0]{\operatorname{tr}}
\newcommand{diag}[0]{\operatorname{diag}}
\newcommand{abs}[0]{\operatorname{abs}}
\newcommand{pop}[0]{\operatorname{pop}}
\newcommand{aux}[0]{\text{aux}}
\newcommand{opt}[0]{\text{opt}}
\newcommand{tgt}[0]{\text{tgt}}
\newcommand{init}[0]{\text{init}}
\newcommand{lab}[0]{\text{lab}}
\newcommand{rwa}[0]{\text{rwa}}
\newcommand{bra}[1]{\langle#1\vert}
\newcommand{ket}[1]{\vert#1\rangle}
\newcommand{Bra}[1]{\left\langle#1\right\vert}
\newcommand{Ket}[1]{\left\vert#1\right\rangle}
\newcommand{Braket}[2]{\left\langle #1\vphantom{#2} \mid
#2\vphantom{#1}\right\rangle}
\newcommand{op}[1]{\hat{#1}}
\newcommand{Op}[1]{\hat{#1}}
\newcommand{dd}[0]{\,\text{d}}
\newcommand{Liouville}[0]{\mathcal{L}}
\newcommand{DynMap}[0]{\mathcal{E}}
\newcommand{identity}[0]{\mathbf{1}}
\newcommand{Norm}[1]{\lVert#1\rVert}
\newcommand{Abs}[1]{\left\vert#1\right\vert}
\newcommand{avg}[1]{\langle#1\rangle}
\newcommand{Avg}[1]{\left\langle#1\right\rangle}
\newcommand{AbsSq}[1]{\left\vert#1\right\vert^2}
\newcommand{Re}[0]{\operatorname{Re}}
\newcommand{Im}[0]{\operatorname{Im}}$

This example demonstrates the optimization with an "unconventional"
optimization target. Instead of a state-to-state transition, or the realization
of a specific quantum gate, we optimize for an arbitrary perfectly entangling
gate. See

* P. Watts, et al., Phys. Rev. A 91, 062306 (2015)

* M. H. Goerz, et al., Phys. Rev. A 91, 062307 (2015)

for details.

## Hamiltonian

We consider a generic two-qubit Hamiltonian (motivated from the example of two
superconducting transmon qubits, truncated to the logical subspace),

$$
\begin{equation}
  \op{H}(t)
    = - \frac{\omega_1}{2} \op{\sigma}_{z}^{(1)}
      - \frac{\omega_2}{2} \op{\sigma}_{z}^{(2)}
      + 2 J \left(
            \op{\sigma}_{x}^{(1)} \op{\sigma}_{x}^{(2)}
            + \op{\sigma}_{y}^{(1)} \op{\sigma}_{y}^{(2)}
        \right)
      + u(t) \left(
            \op{\sigma}_{x}^{(1)} + \lambda \op{\sigma}_{x}^{(2)}
        \right),
\end{equation}
$$

where $\omega_1$ and $\omega_2$ are the energy level splitting of the
respective qubit, $J$ is the effective coupling strength and $u(t)$ is the
control field. $\lambda$ defines the strength of the qubit-control coupling for
qubit 2, relative to qubit 1.

We use the following parameters:

```python
w1 = 1.1  # qubit 1 level splitting
w2 = 2.1  # qubit 2 level splitting
J = 0.2  # effective qubit coupling
u0 = 0.3  # initial driving strength
la = 1.1  # relative pulse coupling strength of second qubit
T = 25.0  # final time
nt = 250  # number of time steps

tlist = np.linspace(0, T, nt)
```

These are for illustrative purposes only, and do not correspond to any
particular physical system.

The initial guess is defined as

```python
def eps0(t, args):
    return u0 * krotov.shapes.flattop(
        t, t_start=0, t_stop=T, t_rise=(T / 20), t_fall=(T / 20), func='sinsq'
    )
```

```python
def plot_pulse(pulse, tlist):
    fig, ax = plt.subplots()
    if callable(pulse):
        pulse = np.array([pulse(t, args=None) for t in tlist])
    ax.plot(tlist, pulse)
    ax.set_xlabel('time')
    ax.set_ylabel('pulse amplitude')
    plt.show(fig)
```

```python
plot_pulse(eps0, tlist)
```

We instantiate the Hamiltonian with this guess pulse

```python
def hamiltonian(w1=w1, w2=w2, J=J, la=la, u0=u0):
    """Two qubit Hamiltonian

    Args:
        w1 (float): energy separation of the first qubit levels
        w2 (float): energy separation of the second qubit levels
        J (float): effective coupling between both qubits
        la (float): factor that pulse coupling strength differs for second qubit
        u0 (float): constant amplitude of the driving field
    """
    # local qubit Hamiltonians
    Hq1 = 0.5 * w1 * np.diag([-1, 1])
    Hq2 = 0.5 * w2 * np.diag([-1, 1])

    # lift Hamiltonians to joint system operators
    H0 = np.kron(Hq1, np.identity(2)) + np.kron(np.identity(2), Hq2)

    # define the interaction Hamiltonian
    sig_x = np.array([[0, 1], [1, 0]])
    sig_y = np.array([[0, -1j], [1j, 0]])
    Hint = 2 * J * (np.kron(sig_x, sig_x) + np.kron(sig_y, sig_y))
    H0 = H0 + Hint

    # define the drive Hamiltonian
    H1 = np.kron(np.array([[0, 1], [1, 0]]), np.identity(2)) + la * np.kron(
        np.identity(2), np.array([[0, 1], [1, 0]])
    )

    # convert Hamiltonians to QuTiP objects
    H0 = qutip.Qobj(H0)
    H1 = qutip.Qobj(H1)

    return [H0, [H1, eps0]]

H = hamiltonian(w1=w1, w2=w2, J=J, la=la, u0=u0)
```

As well as the canonical two-qubit logical basis,

```python
psi_00 = qutip.Qobj(np.kron(np.array([1, 0]), np.array([1, 0])))
psi_01 = qutip.Qobj(np.kron(np.array([1, 0]), np.array([0, 1])))
psi_10 = qutip.Qobj(np.kron(np.array([0, 1]), np.array([1, 0])))
psi_11 = qutip.Qobj(np.kron(np.array([0, 1]), np.array([0, 1])))
```

with the corresponding projectors to calculate population dynamics below.

```python
proj_00 = qutip.ket2dm(psi_00)
proj_01 = qutip.ket2dm(psi_01)
proj_10 = qutip.ket2dm(psi_10)
proj_11 = qutip.ket2dm(psi_11)
```

## Objectives for a perfect entangler

Our optimization target is the closest perfectly entangling gate, quantified by
the perfect-entangler functional

$$
\begin{equation}
  F_{PE} = g_3 \sqrt{g_1^2 + g_2^2} - g_1,
\end{equation}
$$

where $g_1, g_2, g_3$ are the local invariants of the implemented gate that
uniquely identify its non-local content. The local invariants are closely
related to the Weyl coordinates $c_1, c_2, c_3$, which provide a useful
geometric visualization in the Weyl chamber. The perfectly entangling gates lie
within a polyhedron in the Weyl chamber and $F_{PE}$ becomes zero at its
boundaries. We define $F_{PE} \equiv 0$ for *all* perfect entanglers (inside
the polyhedron)

A list of four objectives that encode the minimization of $F_{PE}$ are
generated by calling the `gate_objectives` function with the canonical basis,
and `"PE"` as target "gate".

```python
objectives = krotov.gate_objectives(
    basis_states=[psi_00, psi_01, psi_10, psi_11], gate="PE", H=H
)
```

```python
objectives
```

    [Objective[|Ψ₀(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]],
     Objective[|Ψ₁(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]],
     Objective[|Ψ₂(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]],
     Objective[|Ψ₃(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]]]

The initial states in these objectives are not the canonical basis states, but a Bell
basis,

```python
# NBVAL_IGNORE_OUTPUT
for obj in objectives:
    display(obj.initial_state)
```

Quantum object: dims = [[4], [1]], shape = (4, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}0.707\\0.0\\0.0\\0.707\\\end{array}\right)\end{equation*}

Quantum object: dims = [[4], [1]], shape = (4, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}0.0\\0.707j\\0.707j\\0.0\\\end{array}\right)\end{equation*}

Quantum object: dims = [[4], [1]], shape = (4, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}0.0\\0.707\\-0.707\\0.0\\\end{array}\right)\end{equation*}

Quantum object: dims = [[4], [1]], shape = (4, 1), type = ket\begin{equation*}\left(\begin{array}{*{11}c}0.707j\\0.0\\0.0\\-0.707j\\\end{array}\right)\end{equation*}

Since we don't know *which* perfect entangler the optimization result will
implement, we cannot associate any "target state" with each objective, and the
`target` attribute is set to the string 'PE'.

We can treat the above objectives as a "black box"; the only important
consideration is that the `chi_constructor` that we will pass to
`optimize_pulses` to calculating the boundary condition for the backwards
propagation,

$$
\begin{equation}
  \ket{\chi_{k}} = \frac{\partial F_{PE}}{\partial \bra{\phi_k}} \Bigg|_{\ket{\phi_{k}(T)}}\,,
\end{equation}
$$

must be consistent with how the `objectives` are set up. For the perfect
entanglers functional, the calculation of the $\ket{\chi_{k}}$ is relatively
complicated. The `weylchamber` package
(https://github.com/qucontrol/weylchamber) contains a suitable routine that
works on the `objectives` exactly as defined above (specifically, under the
assumption that the $\ket{\phi_k}$ are the appropriate Bell states):

```python
help(wc.perfect_entanglers.make_PE_krotov_chi_constructor)
```

    Help on function make_PE_krotov_chi_constructor in module weylchamber.perfect_entanglers:

    make_PE_krotov_chi_constructor(canonical_basis, unitarity_weight=0)
        Return a constructor for the χ's in a PE optimization.

        Return a `chi_constructor` that determines the boundary condition of the
        backwards propagation in an optimization towards a perfect entangler in
        Krotov's method, based on the foward-propagtion of the Bell states. In
        detail, the function returns a callable function that calculates

        .. math::

            \ket{\chi_{i}}
            =
            \frac{\partial F_{PE}}{\partial \bra{\phi_i}}
            \Bigg|_{\ket{\phi_{i}(T)}}

        for all $i$ with $\ket{\phi_{0}(T)}, ..., \ket{\phi_{3}(T)}$ the forward
        propagated Bell states at final time $T$, cf. Eq. (33b) in Ref. [1].
        $F_{PE}$ is the perfect-entangler functional
        :func:`~weylchamber.perfect_entanglers.F_PE`. For the details of the
        derivative see Appendix G in Ref. [2].

        References:

        [1] `M. H. Goerz, et al., Phys. Rev. A 91, 062307 (2015)
        <https://doi.org/10.1103/PhysRevA.91.062307>`_

        [2] `M. H. Goerz, Optimizing Robust Quantum Gates in Open Quantum Systems.
        PhD thesis, University of Kassel, 2015
        <https://michaelgoerz.net/research/diss_goerz.pdf>`_

        Args:
            canonical_basis (list[qutip.Qobj]): A list of four basis states that
                define the canonical basis $\ket{00}$, $\ket{01}$, $\ket{10}$, and
                $\ket{11}$ of the logical subspace.
            unitarity_weight (float): A weight in [0, 1] that determines how much
                emphasis is placed on  maintaining population in the logical
                subspace.

        Returns:
            callable: a function ``chi_constructor(fw_states_T, **kwargs)`` that
            receives the result of a foward propagation of the Bell states
            (obtained from `canonical_basis` via
            :func:`weylchamber.gates.bell_basis`), and returns a list of statex
            $\ket{\chi_{i}}$ that are the boundary condition for the backward
            propagation in Krotov's method. Positional arguments beyond
            `fw_states_T` are ignored.

```python
chi_constructor = wc.perfect_entanglers.make_PE_krotov_chi_constructor(
    [psi_00, psi_01, psi_10, psi_11]
)
```

Again, the key point to take from this is that when defining a new or unusual
functional, **the `chi_constructor` must be congruent with the way the
objectives are defined**. As a user, you can choose whatever definition of
objectives and implementation of `chi_constructor` is most suitable, as long
they are compatible.

## Second Order Update Equation

As the perfect-entangler functional $F_{PE}$ is non-linear in
the states, Krotov's method requires the second-order contribution in
order to guarantee monotonic convergence (see D. M. Reich, et al., J. Chem.
Phys. 136, 104103 (2012) for details). The second order update equation
reads

$$
\begin{align}
  \epsilon^{(i+1)}(t)
  & =
  \epsilon^{ref}(t) + \frac{S(t)}{\lambda_a} \Im \Bigg\{
    \sum_{k=1}^{N}
    \Bigg\langle
      \chi_k^{(i)}(t)
    \Bigg\vert
    \left.\frac{\partial \Op{H}}{\partial \epsilon}\right\vert_{{\scriptsize \begin{matrix}\phi^{(i+1)}(t) \\\epsilon^{(i+1)}(t)\end{matrix}}}
    \Bigg\vert
    \phi_k^{(i+1)}(t)
    \Bigg\rangle
    \\
    & \qquad +
    \frac{1}{2} \sigma(t)
    \Bigg\langle
      \Delta\phi_k(t)
      \Bigg\vert
              \left.\frac{\partial \Op{H}}{\partial \epsilon}\right\vert_{{\scriptsize \begin{matrix}\phi^{(i+1)}(t)\\\epsilon^{(i+1)}(t)\end{matrix}}}
      \Bigg\vert
      \phi_k^{(i+1)}(t)
     \Bigg\rangle
  \Bigg\}\,,
\end{align}
$$

where the term proportional to $\sigma(t)$ defines the second-order
contribution. In order to use the second-order term, we need to pass
a function to evaluate this $\sigma(t)$ as `sigma` to `optimize_pulses`. We use
the equation

$$
\begin{equation}
   \sigma(t) = -\max\left(\varepsilon_A,2A+\varepsilon_A\right)
\end{equation}
$$

with $\varepsilon_A$ a small non-negative number, and $A$ a parameter that can
be recalculated numerically after each iteration (see D. M. Reich, et al., J.
Chem. Phys. 136, 104103 (2012) for details).

Generally, $\sigma(t)$ has parametric dependencies like $A$ in this example,
which should be refreshed for each iteration. Thus, since `sigma` holds
internal state, it must be implemented as an object subclassing from
`krotov.second_order.Sigma`:

```python
class sigma(krotov.second_order.Sigma):
    def __init__(self, A, epsA=0):
        self.A = A
        self.epsA = epsA

    def __call__(self, t):
        ϵ, A = self.epsA, self.A
        return -max(ϵ, 2 * A + ϵ)

    def refresh(
        self,
        forward_states,
        forward_states0,
        chi_states,
        chi_norms,
        optimized_pulses,
        guess_pulses,
        objectives,
        result,
    ):
        try:
            Delta_J_T = result.info_vals[-1][0] - result.info_vals[-2][0]
        except IndexError:  # first iteration
            Delta_J_T = 0
        self.A = krotov.second_order.numerical_estimate_A(
            forward_states, forward_states0, chi_states, chi_norms, Delta_J_T
        )
```

This combines the evaluation of the function, `sigma(t)`, with the recalculation of
$A$ (or whatever parametrizations another $\sigma(t)$ function might contain)
in `sigma.refresh`, which `optimize_pulses` invokes automatically at the
end of each iteration.

## Optimization

Before running the optimization, we define the shape function $S(t)$ to
maintain the smooth switch-on and switch-off, and the $\lambda_a$ parameter
that determines the overall magnitude of the pulse update in each iteration:

```python
def S(t):
    """Shape function for the field update"""
    return krotov.shapes.flattop(
        t, t_start=0, t_stop=T, t_rise=T / 20, t_fall=T / 20, func='sinsq'
    )

pulse_options = {H[1][1]: dict(lambda_a=1.0e2, update_shape=S)}
```

In previous examples, we have used `info_hook` routines that display and store
the value of the functional $J_T$. Here, we will also want to analyze the
optimization in terms of the Weyl chamber coordinates $(c_1, c_2, c_3)$. We
therefore write a custom `print_fidelity` routine that prints $F_{PE}$ as well
as the gate concurrence (as an alternative measure for the entangling power of
quantum gates), and results in the storage of a nested tuple `(F_PE, (c1, c2,
c3))` for each iteration, in `Result.info_vals`.

```python
def print_fidelity(**args):
    basis = [objectives[i].initial_state for i in [0, 1, 2, 3]]
    states = [args['fw_states_T'][i] for i in [0, 1, 2, 3]]
    U = wc.gates.gate(basis, states)
    c1, c2, c3 = wc.coordinates.c1c2c3(from_magic(U))
    g1, g2, g3 = wc.local_invariants.g1g2g3_from_c1c2c3(c1, c2, c3)
    conc = wc.perfect_entanglers.concurrence(c1, c2, c3)
    F_PE = wc.perfect_entanglers.F_PE(g1, g2, g3)
    print("    F_PE: %f\n    gate conc.: %f" % (F_PE, conc))
    return F_PE, [c1, c2, c3]
```

This structure must be taken into account in a `check_convergence` routine. This would
affect routines like `krotov.convergence.value_below` that assume that
`Result.info_vals` contains the values of $J_T$ only. Here, we define a check
that stops the optimization as soon as we reach a perfect entangler:

```python
def check_PE(result):
    # extract F_PE from (F_PE, [c1, c2, c3])
    F_PE = result.info_vals[-1][0]
    if F_PE <= 0:
        return "achieved perfect entangler"
    else:
        return None
```

```python
opt_result = krotov.optimize_pulses(
    objectives,
    pulse_options=pulse_options,
    tlist=tlist,
    propagator=krotov.propagators.expm,
    chi_constructor=chi_constructor,
    info_hook=krotov.info_hooks.chain(
        krotov.info_hooks.print_debug_information, print_fidelity
    ),
    check_convergence=check_PE,
    sigma=sigma(A=0.0),
    iter_stop=20,
)
```

    Iteration 0
        objectives:
            1:|Ψ₀(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]
            2:|Ψ₁(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]
            3:|Ψ₂(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]
            4:|Ψ₃(4)⟩ to PE via [H₀[4,4], [H₁[4,4], u₁(t)]]
        adjoint objectives:
            1:⟨Ψ₀(4)| to PE via [H₂[4,4], [H₃[4,4], u₁(t)]]
            2:⟨Ψ₁(4)| to PE via [H₄[4,4], [H₅[4,4], u₁(t)]]
            3:⟨Ψ₂(4)| to PE via [H₆[4,4], [H₇[4,4], u₁(t)]]
            4:⟨Ψ₃(4)| to PE via [H₈[4,4], [H₉[4,4], u₁(t)]]
        propagator: expm
        chi_constructor: chi_constructor
        mu: derivative_wrt_pulse
        sigma: sigma
        S(t) (ranges): [0.000000, 1.000000]
        iter_start: 0
        iter_stop: 20
        duration: 1.5 secs (started at 2021-11-07 05:56:40)
        optimized pulses (ranges): [0.00, 0.30]
        ∫gₐ(t)dt: 0.00e+00
        λₐ: 1.00e+02
        storage (bw, fw, fw0): None, [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 1.447335
        gate conc.: 0.479492
    Iteration 1
        duration: 3.7 secs (started at 2021-11-07 05:56:42)
        optimized pulses (ranges): [0.00, 0.32]
        ∫gₐ(t)dt: 2.58e-01
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 0.998102
        gate conc.: 0.645579
    Iteration 2
        duration: 3.7 secs (started at 2021-11-07 05:56:46)
        optimized pulses (ranges): [0.00, 0.34]
        ∫gₐ(t)dt: 2.20e-01
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 0.582566
        gate conc.: 0.784635
    Iteration 3
        duration: 3.7 secs (started at 2021-11-07 05:56:49)
        optimized pulses (ranges): [0.00, 0.35]
        ∫gₐ(t)dt: 1.46e-01
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 0.304191
        gate conc.: 0.892999
    Iteration 4
        duration: 3.7 secs (started at 2021-11-07 05:56:53)
        optimized pulses (ranges): [0.00, 0.37]
        ∫gₐ(t)dt: 7.72e-02
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 0.153498
        gate conc.: 0.951891
    Iteration 5
        duration: 3.7 secs (started at 2021-11-07 05:56:57)
        optimized pulses (ranges): [0.00, 0.37]
        ∫gₐ(t)dt: 3.88e-02
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 0.076100
        gate conc.: 0.980700
    Iteration 6
        duration: 3.7 secs (started at 2021-11-07 05:57:00)
        optimized pulses (ranges): [0.00, 0.38]
        ∫gₐ(t)dt: 2.03e-02
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 0.035062
        gate conc.: 0.993840
    Iteration 7
        duration: 3.7 secs (started at 2021-11-07 05:57:04)
        optimized pulses (ranges): [0.00, 0.39]
        ∫gₐ(t)dt: 1.13e-02
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: 0.012118
        gate conc.: 0.998971
    Iteration 8
        duration: 3.7 secs (started at 2021-11-07 05:57:08)
        optimized pulses (ranges): [0.00, 0.39]
        ∫gₐ(t)dt: 6.68e-03
        λₐ: 1.00e+02
        storage (bw, fw, fw0): [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB), [4 * ndarray(250)] (0.5 MB)
        fw_states_T norm: 1.000000, 1.000000, 1.000000, 1.000000
        F_PE: -0.001422
        gate conc.: 1.000000

```python
opt_result
```

    Krotov Optimization Result
    --------------------------
    - Started at 2021-11-07 05:56:40
    - Number of objectives: 4
    - Number of iterations: 8
    - Reason for termination: Reached convergence: achieved perfect entangler
    - Ended at 2021-11-07 05:57:11 (0:00:31)

We can visualize how each iteration of the optimization brings the dynamics
closer to the polyhedron of perfect entanglers (using the Weyl chamber
coordinates that we calculated in the `info_hook` routine `print_fidelity`, and
that were stored in `Result.info_vals`).

```python
w = WeylChamber()
c1c2c3 = [opt_result.info_vals[i][1] for i in range(len(opt_result.iters))]
for i in range(len(opt_result.iters)):
    w.add_point(c1c2c3[i][0], c1c2c3[i][1], c1c2c3[i][2])
w.plot()
```

The final optimized control field looks like this:

```python
plot_pulse(opt_result.optimized_controls[0], tlist)
```


```python
import networkx as nx
from networkx.algorithms.shortest_paths.weighted import dijkstra_path
from networkx.algorithms.shortest_paths.generic import shortest_path_length

import geopandas as gpd
from geopy.distance import geodesic
from shapely.geometry import Point, LineString
import geojson
import osmnx as ox

import pandas as pd
import numpy as np
import math
import json

import gurobipy as gp
from gurobipy import GRB

from tqdm import tqdm
from tqdm._tqdm_notebook import tqdm_notebook
tqdm_notebook.pandas()

from keplergl import KeplerGl
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

import warnings
warnings.filterwarnings('ignore')
```

```python
zones = gpd.read_file('taxi_zones/taxi_zones.shp')
table = pd.read_csv('taxi+_zone_lookup.csv')
zones = zones.to_crs("EPSG:4326")
```

```python
def dist(pt1, pt2):
    return geodesic(pt1, pt2).miles
```

```python
zones['Centroid'] = zones['geometry'].apply(lambda x: x.centroid)
x_list = [(lon, lat) for (lon, lat) in zones['Centroid'].apply(lambda x: (x.xy[1][0], x.xy[0][0]))]
pi_list = [geometry.area for geometry in zones['geometry']]
dQ = dist

epsilon = 0.5
delta = 1.1
```

## Greedy Geometric Spanner Algorithm

```python
G = nx.Graph()
n = len(x_list)
G.add_nodes_from(range(n))
weighted_edges = [(i, j, dQ(x_list[i], x_list[j])) for i in range(n) for j in range(n)]
G.add_weighted_edges_from(weighted_edges)

sorted_edges = sorted(weighted_edges, key=lambda x: x[2])
spanner = nx.Graph()
spanner.add_nodes_from(range(n))
stretch = 1.1
for edge in tqdm(sorted_edges):
    source, target, weight = edge
    if not nx.has_path(spanner, source, target):
        spanner.add_weighted_edges_from([edge])
    else:
        spanner_weight = shortest_path_length(spanner, source, target, weight='weight')
        if spanner_weight < weight * stretch:
            continue
        else:
            spanner.add_weighted_edges_from([edge])
```

    100%|██████████| 69169/69169 [01:26<00:00, 800.31it/s]

```python
# Verify the constructed graph spanner
for edge in weighted_edges:
    source, target, weight = edge
    spanner_weight = shortest_path_length(spanner, source, target, weight='weight')
    if spanner_weight > weight * 1.1:
        print('invalid')
```

```python
len(G.edges)
```

    34716

```python
len(spanner.edges)
```

    1373

```python
num_edge_list = []
for stretch in tqdm(np.arange(1, 2.01, 0.01)):
    G = nx.Graph()
    n = len(x_list)
    G.add_nodes_from(range(n))
    weighted_edges = [(i, j, dQ(x_list[i], x_list[j])) for i in range(n) for j in range(n)]
    G.add_weighted_edges_from(weighted_edges)

    sorted_edges = sorted(weighted_edges, key=lambda x: x[2])
    spanner = nx.Graph()
    spanner.add_nodes_from(range(n))
    for edge in sorted_edges:
        source, target, weight = edge
        if not nx.has_path(spanner, source, target):
            spanner.add_weighted_edges_from([edge])
        else:
            spanner_weight = shortest_path_length(spanner, source, target, weight='weight')
            if spanner_weight < weight * stretch:
                continue
            else:
                spanner.add_weighted_edges_from([edge])
    num_edge_list.append(len(spanner.edges))
```

```python
plt.figure(figsize=(8, 6))
sns.lineplot(x=np.arange(1, 2.01, 0.01), y=num_edge_list)
plt.xlabel('Delta (stretch factor)', fontsize=16);
plt.ylabel('Number of edges in the spanner', fontsize=16);
```

## Graph Spanner Version for approximate optimal utility
\begin{align}
Minimize: \ &\sum_{x, z\in \mathcal{X}} \pi_x k_{xz} d_Q(x, z) \\
Subject\ to:\ & k_{xz} \leq e^{\frac{\epsilon}{\delta} d_G(x, x')} k_{x' z}  & z\in \mathcal{X}, (x, x') \in E  \tag{1} \\
& \sum_{x \in \mathcal{X}} k_{xz} = 1 & x\in\mathcal{X} \tag{2} \\
& k_{xz} \geq 0  &x,z\in \mathcal{X} \tag{3}
\end{align}

**The algorithm we used for constructing the graph spanner is from** *Althöfer, I., Das, G., Dobkin, D. et al. On sparse spanners of weighted graphs. Discrete Comput Geom 9, 81–100 (1993). https://doi.org/10.1007/BF02189308* **implemented by** *Ao Qu, github: https://github.com/quao627/GeoDifferentialPrivacy*

```python
def optql_graph_spanner(x_list, pi_list, spanner, dQ, epsilon=0.5):
    print(f'Start building a linear program for {len(x_list)} locations...')
    pre_prob = np.array(pi_list) / sum(pi_list) # normalize probability distribution
    threshold = math.exp(epsilon / delta)

    # define a model
    model = gp.Model('OptQL')

    # add variables accessed as (0, 0), (0, 1), (1, 1), ...
    variables = model.addVars(n, n, lb=0.0, ub=1.0, name='k')

    # set objective function
    model.setObjective(gp.quicksum(pre_prob[i] * variables[i, j] * dQ(x_list[i], x_list[j]) \
                                   for i in range(n) for j in range(n)), GRB.MINIMIZE)

    # add constraints (1)
    print('Adding differential privacy constraints...')
    model.addConstrs(variables[i, k] <= pow(threshold, dQ(x_list[i], x_list[j])) * variables[j, k] \
                     for (i, j) in spanner.edges for k in range(n))
    model.addConstrs(variables[i, k] <= pow(threshold, dQ(x_list[i], x_list[j])) * variables[j, k] \
                     for (j, i) in spanner.edges for k in range(n))

    # add constraints (2)
    print('Add probability sum constraints...')
    model.addConstrs(gp.quicksum(variables.select(i, '*')) == 1 for i in range(n))

    # constriants (3) are already satisfied

    # optimize the model
    model.optimize()

    # build a matrix to store the stochastic matrix
    variables = model.getAttr('x', variables)
    matrix = np.zeros((n, n))
    for key, value in variables.items():
        matrix[key] = value

    # get post-process probability distribution
    post_prob = pre_prob @ matrix

    return matrix, pre_prob, post_prob
```

```python
p_matrix, pre_prob, post_prob = optql_graph_spanner(x_list, pi_list, spanner, dQ, epsilon=epsilon)
```

    Start building a linear program for 263 locations...
    Adding differential privacy constraints...
    Add probability sum constraints...
    Gurobi Optimizer version 9.1.1 build v9.1.1rc0 (mac64)
    Thread count: 4 physical cores, 8 logical processors, using up to 8 threads
    Optimize a model with 722461 rows, 69169 columns and 1236889 nonzeros
    Model fingerprint: 0x478468b7
    Coefficient statistics:
      Matrix range     [1e+00, 2e+02]
      Objective range  [3e-05, 7e-01]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+00, 1e+00]

    Concurrent LP optimizer: dual simplex and barrier
    Showing barrier log only...

    Presolve removed 138338 rows and 0 columns
    Presolve removed 0 rows and 138338 columns
    Presolve time: 2.30s
    Presolved: 69169 rows, 653292 columns, 1306058 nonzeros

    Ordering time: 0.40s

    Barrier statistics:
     Dense cols : 263
     Free vars  : 263
     AA' NZ     : 3.611e+05
     Factor NZ  : 5.201e+06 (roughly 300 MBytes of memory)
     Factor Ops : 8.375e+08 (less than 1 second per iteration)
     Threads    : 3

                      Objective                Residual
    Iter       Primal          Dual         Primal    Dual     Compl     Time
       0  -4.62418304e+04 -0.00000000e+00  8.89e-01 1.60e+01  3.08e-01     4s
       1  -2.03107232e+03  8.91592160e+00  1.20e-02 2.63e-01  6.93e-03     4s
       2  -1.07898758e+03  1.02132602e+01  5.27e-03 4.05e-02  2.14e-03     4s
       3  -1.20914209e+02  1.04274830e+01  1.03e-04 9.41e-14  1.97e-04     5s
       4  -8.51164205e+01  1.01167444e+01  7.44e-05 4.62e-14  1.43e-04     5s
       5  -6.05486177e+01  9.76574648e+00  5.49e-05 6.04e-14  1.06e-04     5s
       6  -5.17965529e+01  9.60765748e+00  4.76e-05 3.91e-14  9.22e-05     6s
       7  -4.86841654e+01  9.49779956e+00  4.50e-05 4.09e-14  8.73e-05     6s
       8  -2.55234257e+01  8.95106172e+00  2.62e-05 6.39e-14  5.18e-05     6s
       9  -1.91086428e+01  8.45086998e+00  2.09e-05 4.97e-14  4.14e-05     7s
      10  -1.28849806e+01  7.89753830e+00  1.57e-05 5.68e-14  3.12e-05     7s
      11  -7.37732188e+00  7.35963528e+00  1.09e-05 5.86e-14  2.21e-05     7s
      12  -4.38154612e+00  6.74337646e+00  8.11e-06 4.80e-14  1.67e-05     8s
      13  -1.51677505e+00  5.76590589e+00  5.22e-06 7.11e-14  1.09e-05     8s
      14   3.29424585e-01  5.03095719e+00  3.27e-06 5.51e-14  7.06e-06     8s
      15   1.64508468e+00  4.46899586e+00  1.88e-06 4.26e-14  4.24e-06     9s
      16   1.90931288e+00  4.21063538e+00  1.54e-06 3.55e-14  3.45e-06     9s
      17   2.18350278e+00  3.99557685e+00  1.14e-06 4.26e-14  2.72e-06     9s
      18   2.39777286e+00  3.79166799e+00  8.31e-07 3.91e-14  2.09e-06     9s
      19   2.63037230e+00  3.50524344e+00  5.15e-07 4.62e-14  1.31e-06    10s
      20   2.81528154e+00  3.33574956e+00  2.84e-07 5.33e-14  7.81e-07    10s
      21   2.89618832e+00  3.25050559e+00  1.92e-07 6.22e-14  5.32e-07    11s
      22   2.93811919e+00  3.21498000e+00  1.46e-07 3.73e-14  4.16e-07    11s
      23   2.96482342e+00  3.18913781e+00  1.19e-07 3.20e-14  3.37e-07    11s
      24   2.98494446e+00  3.17130297e+00  9.83e-08 3.55e-14  2.80e-07    11s
      25   2.99118481e+00  3.15844699e+00  9.21e-08 4.80e-14  2.51e-07    12s
      26   3.01801247e+00  3.13577287e+00  6.62e-08 3.91e-14  1.77e-07    12s
      27   3.02961938e+00  3.12690380e+00  5.51e-08 5.33e-14  1.46e-07    12s
      28   3.04219996e+00  3.12065376e+00  4.31e-08 6.75e-14  1.18e-07    13s
      29   3.05074763e+00  3.11138769e+00  3.49e-08 5.51e-14  9.10e-08    13s
      30   3.05992307e+00  3.10622606e+00  2.61e-08 6.04e-14  6.95e-08    13s
      31   3.06435444e+00  3.10338445e+00  2.18e-08 6.93e-14  5.86e-08    14s
      32   3.06829356e+00  3.10029914e+00  1.79e-08 9.24e-14  4.80e-08    14s
      33   3.07090723e+00  3.09798948e+00  1.53e-08 9.06e-14  4.07e-08    15s
      34   3.07429797e+00  3.09537409e+00  1.19e-08 1.74e-13  3.16e-08    15s
      35   3.07633768e+00  3.09380124e+00  9.86e-09 1.40e-13  2.62e-08    15s
      36   3.07861333e+00  3.09272578e+00  7.59e-09 8.35e-14  2.12e-08    16s
      37   3.07985927e+00  3.09117210e+00  6.27e-09 6.39e-14  1.70e-08    16s
      38   3.08074146e+00  3.09082195e+00  5.35e-09 9.77e-14  1.51e-08    16s
      39   3.08133484e+00  3.08964740e+00  4.72e-09 1.74e-13  1.25e-08    17s
      40   3.08174335e+00  3.08911877e+00  4.28e-09 2.45e-13  1.11e-08    17s
      41   3.08242241e+00  3.08853849e+00  3.55e-09 9.77e-14  9.18e-09    17s
      42   3.08302138e+00  3.08819439e+00  2.90e-09 9.24e-14  7.76e-09    18s
      43   3.08314112e+00  3.08767644e+00  2.77e-09 2.81e-13  6.81e-09    18s
      44   3.08368264e+00  3.08744793e+00  2.20e-09 3.69e-13  5.65e-09    18s
      45   3.08390572e+00  3.08728535e+00  1.95e-09 3.94e-13  5.07e-09    19s
      46   3.08417990e+00  3.08716516e+00  1.66e-09 3.36e-13  4.48e-09    19s
      47   3.08436089e+00  3.08686723e+00  1.46e-09 1.78e-13  3.76e-09    19s
      48   3.08448817e+00  3.08666724e+00  1.32e-09 1.08e-13  3.27e-09    20s
      49   3.08462529e+00  3.08649902e+00  1.17e-09 1.28e-13  2.81e-09    20s
      50   3.08482102e+00  3.08642634e+00  9.64e-10 1.03e-13  2.41e-09    21s
      51   3.08484816e+00  3.08633061e+00  9.33e-10 1.14e-13  2.23e-09    21s
      52   3.08505572e+00  3.08625352e+00  7.05e-10 1.55e-13  1.80e-09    21s
      53   3.08516027e+00  3.08620709e+00  5.86e-10 2.03e-13  1.57e-09    22s
      54   3.08521066e+00  3.08615239e+00  5.31e-10 2.11e-13  1.41e-09    22s
      55   3.08524382e+00  3.08612420e+00  4.93e-10 1.81e-13  1.32e-09    22s
      56   3.08530695e+00  3.08607319e+00  4.20e-10 3.55e-13  1.15e-09    23s
      57   3.08534179e+00  3.08600347e+00  3.79e-10 3.20e-13  9.93e-10    23s
      58   3.08536132e+00  3.08599556e+00  3.58e-10 3.11e-13  9.52e-10    24s
      59   3.08540200e+00  3.08594623e+00  3.11e-10 2.93e-13  8.17e-10    24s
      60   3.08542995e+00  3.08590885e+00  2.79e-10 4.03e-13  7.19e-10    25s
      61   3.08545441e+00  3.08588562e+00  2.52e-10 4.39e-13  6.47e-10    25s
      62   3.08549423e+00  3.08586187e+00  2.07e-10 9.04e-13  5.52e-10    26s
      63   3.08551089e+00  3.08581835e+00  1.89e-10 3.55e-13  4.62e-10    26s
      64   3.08553288e+00  3.08580077e+00  1.64e-10 2.91e-13  4.02e-10    26s
      65   3.08555444e+00  3.08578354e+00  1.39e-10 3.43e-13  3.44e-10    27s
      66   3.08558973e+00  3.08577050e+00  9.92e-11 2.13e-13  2.71e-10    27s
      67   3.08559745e+00  3.08576519e+00  9.06e-11 3.13e-13  2.52e-10    27s
      68   3.08560798e+00  3.08574408e+00  7.89e-11 4.05e-13  2.04e-10    28s
      69   3.08561403e+00  3.08573579e+00  7.21e-11 5.29e-13  1.83e-10    28s
      70   3.08562321e+00  3.08572551e+00  6.19e-11 5.72e-13  1.54e-10    28s
      71   3.08563147e+00  3.08571854e+00  5.28e-11 4.17e-13  1.31e-10    29s
      72   3.08563911e+00  3.08570914e+00  4.45e-11 4.73e-13  1.05e-10    29s
      73   3.08564370e+00  3.08570375e+00  3.94e-11 5.10e-13  9.01e-11    30s
      74   3.08565650e+00  3.08569947e+00  2.52e-11 6.04e-13  6.45e-11    30s
      75   3.08566103e+00  3.08569637e+00  2.02e-11 1.10e-12  5.30e-11    31s
      76   3.08566512e+00  3.08569245e+00  1.58e-11 1.17e-12  4.10e-11    31s
      77   3.08566884e+00  3.08569041e+00  1.18e-11 3.76e-12  3.24e-11    32s
      78   3.08567096e+00  3.08568756e+00  1.04e-11 3.77e-12  2.49e-11    32s
      79   3.08567303e+00  3.08568598e+00  7.63e-12 3.10e-12  1.94e-11    32s
      80   3.08567420e+00  3.08568468e+00  6.46e-12 2.76e-12  1.57e-11    33s
      81   3.08567469e+00  3.08568345e+00  5.89e-12 2.67e-12  1.31e-11    33s
      82   3.08567805e+00  3.08568184e+00  5.63e-12 2.53e-12  5.68e-12    34s
      83   3.08567843e+00  3.08568128e+00  4.61e-12 2.45e-12  4.27e-12    34s
      84   3.08567869e+00  3.08568091e+00  8.13e-12 3.37e-12  3.34e-12    34s
      85   3.08567879e+00  3.08568086e+00  1.06e-11 3.24e-12  3.10e-12    35s
      86   3.08567888e+00  3.08568083e+00  8.85e-12 3.18e-12  2.93e-12    35s
      87   3.08567894e+00  3.08568073e+00  1.27e-11 2.90e-12  2.69e-12    35s
      88   3.08567911e+00  3.08568059e+00  1.75e-11 2.35e-12  2.22e-12    35s
      89   3.08567924e+00  3.08568054e+00  1.94e-11 6.79e-12  1.96e-12    36s
      90   3.08567938e+00  3.08568035e+00  2.29e-11 9.74e-11  1.46e-12    36s
      91   3.08567941e+00  3.08568027e+00  2.12e-11 8.15e-11  1.29e-12    36s
      92   3.08567952e+00  3.08568010e+00  1.59e-11 5.79e-10  8.60e-13    36s
      93   3.08567965e+00  3.08567980e+00  1.79e-11 4.70e-11  2.19e-13    37s
      94   3.08567966e+00  3.08567978e+00  1.14e-11 1.36e-11  1.68e-13    37s
      95   3.08567977e+00  3.08567977e+00  1.24e-09 8.53e-10  1.32e-14    37s

    Barrier solved model in 95 iterations and 37.27 seconds
    Optimal objective 3.08567977e+00

    Crossover log...

       12710 DPushes remaining with DInf 1.8303188e-01                38s
        3334 DPushes remaining with DInf 1.7529785e-01                40s
        1371 DPushes remaining with DInf 1.7289629e-01                45s
         477 DPushes remaining with DInf 1.6723214e-01                51s
         127 DPushes remaining with DInf 1.6104527e-01                55s
           0 DPushes remaining with DInf 1.3116419e-01                58s

      300326 PPushes remaining with PInf 4.0751134e-02                58s
      199720 PPushes remaining with PInf 5.1742165e-03                65s
      172453 PPushes remaining with PInf 1.8943377e-03                70s
      123212 PPushes remaining with PInf 1.0140784e-03                73s
      103271 PPushes remaining with PInf 5.9071235e-04                76s
       67155 PPushes remaining with PInf 1.8196838e-04                82s
       45245 PPushes remaining with PInf 1.3676637e-04                85s
       22987 PPushes remaining with PInf 5.3076216e-05                91s
        9315 PPushes remaining with PInf 3.5564546e-05                96s
        2108 PPushes remaining with PInf 6.5698968e+00               100s
           0 PPushes remaining with PInf 4.5676605e+00               103s

      Push phase complete: Pinf 4.5676605e+00, Dinf 9.3034723e+02    103s

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
      312288    3.0856766e+00   0.000000e+00   9.303472e+02    103s
      312892    3.0856837e+00   0.000000e+00   8.219638e+02    109s
      313496    3.0856898e+00   0.000000e+00   9.265972e+01    115s
      314100    3.0856914e+00   0.000000e+00   6.867347e-02    120s
      314704    3.0856916e+00   0.000000e+00   2.317480e-03    126s
      314948    3.0856796e+00   1.213708e-03   0.000000e+00    130s
    Extra simplex iterations from dual to original model: 1
      315071    3.0856796e+00   0.000000e+00   0.000000e+00    134s

    Solved with barrier
    Solved in 315071 iterations and 134.28 seconds
    Optimal objective  3.085679569e+00

```python
plt.figure(figsize=(12, 8))
sns.heatmap(p_matrix, vmin=0, vmax=0.3)
```

```python
edges = [(x_list[i], x_list[j]) for i, j in spanner.edges]
edges = list(zip(*edges))
edges = pd.DataFrame({'Source_lon': list(zip(*edges[0]))[1],
                      'Source_lat': list(zip(*edges[0]))[0],
                      'Target_lon': list(zip(*edges[1]))[1],
                      'Target_lat': list(zip(*edges[1]))[0]})

map1 = KeplerGl()

prob_diff = post_prob - pre_prob
tmp = zones.copy(deep=True).drop('Centroid', axis=1)
tmp['prob_diff'] = prob_diff
tmp['pre_prob'] = pre_prob
tmp['post_prob'] = post_prob

equalizer = ox.geocode_to_gdf('Beijing, China').iloc[0, 0]
tmp.append(pd.Series(), ignore_index=True)
tmp.loc[len(tmp), 'geometry'] = equalizer
tmp.loc[len(tmp)-1, 'pre_prob'] = max(pre_prob.max(), post_prob.max())
tmp.loc[len(tmp)-2, 'post_prob'] = max(pre_prob.max(), post_prob.max())

map1.add_data(tmp, name='Differential Privacy on Boston Postal Zones')
map1.add_data(edges, name='Edges selected by the spanner')

with open('spanner_comparison_map_config.json', 'r') as f:
    config = json.load(f)
map1.config = config

map1
```

    User Guide: https://docs.kepler.gl/docs/keplergl-jupyter

    KeplerGl(config={'version': 'v1', 'config': {'visState': {'filters': [], 'layers': [{'id': 'oj0unz', 'type': '…

```python
with open('spanner_comparison_map_config.json', 'w') as f:
    json.dump(map1.config, f)
```

## Experiments on NYC taxi data
Please replace the data directory by any taxi trip data released by NYC TLS (https://www1.nyc.gov/site/tlc/about/tlc-trip-record-data.page).

```python
trips = pd.read_csv('yellow_tripdata_2010-07.csv')
```

```python
def encrypt_trip(pickup_point, dropoff_point, p_matrix):
    n = p_matrix.shape[0]
    for index, row in zones[['zone', 'geometry']].iterrows():
        geometry = row['geometry']
        if pickup_point.within(geometry):
            pickup_zone = row['zone']
            pickup_index = index
        if dropoff_point.within(geometry):
            dropoff_zone = row['zone']
            dropoff_index = index
    encrypted_pickup_index = np.random.choice(range(n), p=p_matrix[pickup_index])
    encrypted_dropoff_index = np.random.choice(range(n), p=p_matrix[dropoff_index])
    encrypted_pickup_zone = zones.loc[encrypted_pickup_index, 'zone']
    encrypted_dropoff_zone = zones.loc[encrypted_dropoff_index, 'zone']
    print(f"Original OD Pair: {pickup_zone}, {dropoff_zone}, Encryped OD Pair: {encrypted_pickup_zone}, {encrypted_dropoff_zone}")
    return encrypted_pickup_zone, encrypted_dropoff_zone
```

```python
pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude = tuple(trips.loc[1, ['pickup_latitude', 'pickup_longitude', 'dropoff_latitude', 'dropoff_longitude']])
pickup_point = Point([pickup_longitude, pickup_latitude])
dropoff_point = Point([dropoff_longitude, dropoff_latitude])
encrypt_trip(pickup_point, dropoff_point, p_matrix)
```

    Original OD Pair: Clinton West, Clinton West, Encryped OD Pair: Lenox Hill East, UN/Turtle Bay South

    ('Lenox Hill East', 'UN/Turtle Bay South')

```python
def find_zone(lon, lat):
    point = Point([lon, lat])
    for index, zone in enumerate(zones['geometry']):
        if point.within(zone):
            return index
    return None

pickup_zones = trips.progress_apply(lambda x: find_zone(x['pickup_longitude'], x['pickup_latitude']), axis=1)
```

    HBox(children=(FloatProgress(value=0.0, max=14656519.0), HTML(value='')))

# Questions & Concerns
* How should we determine the prior distribution for the linear program?
* The zones assgined by TLC are probably not fine-grained enough.
* Are there things we need to do to verify the differential privacy algorithm? eg, post-distribution, data utility
* What is the next step after applying this algorithm to nyc taxi dataset?

# Next Step:
* Demand Prediction (OD demand) -> Compare difference

```python

```