# Mollow-triplets plots

```python
from functools import partial
import pathlib
import pdb

import pickle
import numpy as np
from scipy.optimize import minimize
from scipy.fftpack import fft, rfft, fftfreq, rfftfreq, fftshift
from scipy.integrate import quad
from scipy.special import factorial
import matplotlib.pyplot as plt

import seaborn as sns

import pysme.integrate as integ
import pysme.hierarchy as hier
```

```python
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
```

```python
# define Qubit operators
sx = np.array([[0, 1], [1, 0]], dtype=np.complex)
sy = np.array([[0, -1.j], [1.j, 0]], dtype=np.complex)
sz = np.array([[1, 0], [0, -1]], dtype=np.complex)
Id = np.eye(2, dtype=np.complex)
sp = (sx + 1.j * sy) / 2
sm = (sx - 1.j * sy) / 2
zero = np.zeros((2, 2), dtype=np.complex)
```

```python
plt.style.use('../paper.mplstyle')
```

A coherent drive adds a Hamiltonian term.
\begin{align}
    d\rho&=dt[\beta^*L-\beta L^\dagger,\rho]+dt\mathcal{D}[L]\rho \\
    &=-idt[i\beta^*L-i\beta L^\dagger,\rho]+dt\mathcal{D}[L]\rho \\
    H_{\text{eff}}&=i\beta^*L-i\beta L^\dagger
\end{align}
Rabi frequency $\Omega=2|\beta|$. Carmichæl notates the Rabi frequency as $2\Omega$, so $\Omega=|\beta|$.

```python
def rect(x, a, b):
    return np.where(x < a, 0, np.where(x < b, 1, 0))
```

```python
def xi_rect(t, a, b):
    return rect(t, a, b)/np.sqrt(b - a)
```

```python
def rho_from_ket(ket):
    return np.outer(ket, ket.conj())

def vac_rho(n):
    ket = np.zeros(n + 1, dtype=np.complex)
    ket[0] = 1
    return rho_from_ket(ket)

def make_squeezed_state_vec(r, mu, N, normalized=True):
    r'''Make a truncated squeezed-state vector.

    The squeezed-state vector is :math:`S(r,\mu)|0\rangle`. The truncated
    vector is renormalized by default.

    Parameters
    ----------
    N: positive integer
        The dimension of the truncated Hilbert space, basis {0, ..., N-1}
    r: real number
        Squeezing amplitude
    mu: real number
        Squeezing phase
    normalized: boolean
        Whether or not the truncated vector is renormalized

    Returns
    -------
    numpy.array
        Squeezed-state vector in the truncated Hilbert space, represented in the
        number basis

    '''
    ket = np.zeros(N, dtype=np.complex)
    for n in range(N//2):
        ket[2*n] = (1 / np.sqrt(np.cosh(r))) * ((-0.5 * np.exp(2.j * mu) * np.tanh(r))**n /
                                                factorial(n)) * np.sqrt(factorial(2 * n))
    return ket / np.linalg.norm(ket) if normalized else ket

def sqz_rho(r, mu, n):
    return rho_from_ket(make_squeezed_state_vec(r, mu, n + 1))

def Heff_fn(beta, L):
    return 1.j*np.conj(beta)*L - 1.j*beta*L.conj().T

def calc_hier_state_evo(xi_fn, L, r, mu, beta, gamma, m_max, rho_ss, t, t0=0, timesteps=2**10,
                        solve_ivp_kwargs=None):
    Id_field = np.eye(m_max + 1, dtype=np.complex)
    factory = hier.HierarchyIntegratorFactory(2, m_max)
    integrator = factory.make_uncond_integrator(xi_fn, Id, np.sqrt(gamma) * L, Heff_fn(beta, L), r, mu)
    times = np.linspace(t0, t, timesteps)
    soln_t = integrator.integrate(rho_ss, times)
    return soln_t

def calc_hier_auto_corr(xi_fn, L, r, mu, beta, gamma, m_max, taus, rho_ss, t, t0=0, timesteps=2**10,
                        solve_ivp_kwargs=None):
    sp_ss = np.trace(sp @ rho_ss)
    sm_ss = np.trace(sm @ rho_ss)
    Id_field = np.eye(m_max + 1, dtype=np.complex)
    factory = hier.HierarchyIntegratorFactory(2, m_max)
    integrator = factory.make_uncond_integrator(xi_fn, Id, np.sqrt(gamma) * L, Heff_fn(beta, L), r, mu)
    times = np.linspace(t0, t, timesteps)
    soln_t = integrator.integrate(rho_ss, times)
    sp_ss_t = soln_t.get_expectations(sp, vac_rho(m_max), idx_slice=np.s_[-1], hermitian=False)
    rho_ss_t = soln_t.get_hierarchy_density_matrices(np.s_[-1])
    L_t_t = rho_ss_t @ np.kron(sp, Id_field)
    L_t_taus = integrator.integrate_hier_init_cond(L_t_t, taus + t,
                                                   solve_ivp_kwargs=solve_ivp_kwargs)
    Expt_t_taus = L_t_taus.get_expectations(sm, vac_rho(m_max), hermitian=False)
    soln_t_taus = integrator.integrate_hier_init_cond(rho_ss_t, taus + t,
                                                      solve_ivp_kwargs=solve_ivp_kwargs)
    sm_ss_t_taus = soln_t_taus.get_expectations(sm, vac_rho(m_max), hermitian=False)
    # Subtract off a bunch of stuff that gets rid of the delta
    return (Expt_t_taus - sp_ss_t * sm_ss - sp_ss * sm_ss_t_taus + sp_ss * sm_ss,
            sp_ss, sm_ss, sp_ss_t, sm_ss_t_taus)

def calc_white_auto_corr(L, gamma, beta, r, mu, times_ss, taus, solve_ivp_kwargs=None):
    N = np.sinh(r)**2
    M_sq = -np.exp(2.j * mu) * np.sinh(r) * np.cosh(r)
    integrator = integ.UncondGaussIntegrator(np.sqrt(gamma)*L, M_sq, N, Heff_fn(beta, L))
    soln_ss = integrator.integrate(Id/2, times_ss)

    #pdb.set_trace()

    rho_ss = soln_ss.get_density_matrices(np.s_[-1])
    sp_ss = np.trace(sp @ rho_ss)
    sm_ss = np.trace(sm @ rho_ss)

    #pdb.set_trace()

    L_0_taus = integrator.integrate_non_herm(rho_ss @ sp, taus, solve_ivp_kwargs=solve_ivp_kwargs)

    #pdb.set_trace()

    Expt_t_taus = L_0_taus.get_expectations(sm, hermitian=False)

    #pdb.set_trace()
    return rho_ss, Expt_t_taus - sp_ss * sm_ss

def calc_quasi_markoff_degen_PA_auto_corr(
        gamma, Omega, omega_A, omega_L, gamma_c, eps, phi_L, phi_s, times_ss, taus, solve_ivp_kwargs=None):
    N_A, N_Om, M_A, M_Om, Delta_AL, F_A, G_A = get_degen_PA_params(
        Omega, omega_A, omega_L, gamma_c, eps, phi_L, phi_s)
    integrator = integ.QuasiMarkoff2LvlIntegrator(
        gamma, N_A, N_Om, M_A, M_Om, Delta_AL, Omega, phi_L, F_A, G_A)
    soln_ss = integrator.integrate(Id/2, times_ss)

    rho_ss = soln_ss.get_density_matrices(np.s_[-1])
    sp_ss = np.trace(sp @ rho_ss)
    sm_ss = np.trace(sm @ rho_ss)

    L_0_taus = integrator.integrate_non_herm(rho_ss @ sp, taus, solve_ivp_kwargs=solve_ivp_kwargs)

    Expt_t_taus = L_0_taus.get_expectations(sm, hermitian=False)
    return rho_ss, Expt_t_taus - sp_ss * sm_ss
```

```python
def calc_hier_auto_corr_fock(xi_fn, L, r, mu, beta, gamma, m_max, taus, rho_ss, t, t0=0, timesteps=2**10,
                             solve_ivp_kwargs=None):
    """Get the auto-correlation function for the Fock hierarchy

    Parameters
    ----------
    xi_fn:
        The wavepacket-envelope function
    L:
        The Lindblad operator
    r:
        The logarithm of the magnitude of the squeezing amplitude
    mu:
        The phase of the squeezing
    beta:
        The amplitude of the coherent drive
    gamma:
        The linewidth of the atom
    m_max:
        The maximum level of the hierarchy used
    taus:
        The time offsets for the two-time correlations
    rho_ss:
        The steady-state density matrix under the coherent drive
    t:
        The reference time for the two-time correlations
    t0:
        The initial time to start evolving the coherent-drive steady-state density matrix
        (should be when the wavepacket starts)
    timesteps:
        The number of timesteps to use for the density-matrix evolution
    """
    sp_ss = np.trace(sp @ rho_ss)
    sm_ss = np.trace(sm @ rho_ss)
    Id_field = np.eye(m_max + 1, dtype=np.complex)
    factory = hier.HierarchyIntegratorFactory(2, m_max)
    integrator = factory.make_uncond_integrator(xi_fn, Id, np.sqrt(gamma) * L, Heff_fn(beta, L), 0, 0)
    times = np.linspace(t0, t, timesteps)
    soln_t = integrator.integrate(rho_ss, times)
    sp_ss_t = soln_t.get_expectations(sp, sqz_rho(r, mu, m_max), idx_slice=np.s_[-1], hermitian=False)
    rho_ss_t = soln_t.get_hierarchy_density_matrices(np.s_[-1])
    L_t_t = rho_ss_t @ np.kron(sp, Id_field)
    L_t_taus = integrator.integrate_hier_init_cond(L_t_t, taus + t,
                                                   solve_ivp_kwargs=solve_ivp_kwargs)
    Expt_t_taus = L_t_taus.get_expectations(sm, sqz_rho(r, mu, m_max), hermitian=False)
    soln_t_taus = integrator.integrate_hier_init_cond(rho_ss_t, taus + t,
                                                      solve_ivp_kwargs=solve_ivp_kwargs)
    sm_ss_t_taus = soln_t_taus.get_expectations(sm, sqz_rho(r, mu, m_max), hermitian=False)
    # Subtract off a bunch of stuff that gets rid of the delta
    return (Expt_t_taus - sp_ss_t * sm_ss - sp_ss * sm_ss_t_taus + sp_ss * sm_ss,
            sp_ss, sm_ss, sp_ss_t, sm_ss_t_taus)
```

```python
def plot_emission_spectrum(auto_corr, d_tau, label, beta, plot_kwargs=None, ax=None, figsize=(10,3)):
    if ax is None:
        fig, ax = plt.subplots(figsize=figsize)
        return_fig = True
    else:
        return_fig = False
    if plot_kwargs is None:
        plot_kwargs = dict()
    fourier = fft(auto_corr.real)
    freqs = fftfreq(n=auto_corr.size, d=d_tau)
    ax.plot(fftshift(freqs), np.abs(fftshift(fourier)), label=label, **plot_kwargs)
    ax.legend()
    ax.set_xticks([-np.abs(beta) / np.pi, 0, np.abs(beta) / np.pi])
    ax.set_xlim(-1.5 * np.abs(beta) / np.pi, 1.5 * np.abs(beta) / np.pi)
    ax.set_xticklabels([r'$\omega_c-\Omega$', r'$\omega_c$', r'$\omega_c+\Omega$'])
    ax.set_yticks([])
    ax.set_ylim(0, None)
    ax.set_ylabel(r'$S(\omega)$')
    ax.set_xlabel(r'$\omega$')
    return (fig, ax) if return_fig else None
```

```python
rho0 = (Id - sz) / 2
S = Id
L = sm
H = zero
gamma = 0.5
beta = 8.j
T = 2**8
taus = np.linspace(0, T, 2**13)
```

```python
times_ss = np.linspace(0, 32, 2**12)
rho_ss_coh, delta_Expt_t_taus_coh = calc_white_auto_corr(L, gamma, beta, 0, 0, times_ss, taus,
                                                         solve_ivp_kwargs={'rtol': 1e-6, 'atol': 1e-9})
```

```python
fig, ax = plot_emission_spectrum(
    auto_corr=delta_Expt_t_taus_coh,
    d_tau=np.diff(taus)[0],
    label='No squeezing',
    beta=beta,
)
plt.tight_layout()
plt.show()
```

```python
def gen_save_load_data(data_gen_method, fname, overwrite=False):
    '''Get the data returned by the generating method, running the method only if the data isn't already available.

    If the given filename exists, load and return the data from that file. Otherwise generate the data using the
    supplied method and save and return it.

    Useful for notebooks you imagine running multiple times, but where some of the data is expensive to generate
    and you want to save it to disk to be reloaded for future sessions.

    '''
    try:
        with open(fname, 'xb' if not overwrite else 'wb') as f:
            data = data_gen_method()
            pickle.dump(data, f)
    except FileExistsError:
        print('Data already exist.')
        with open(fname, 'rb') as f:
            data = pickle.load(f)
    return data
```

```python
def gen_Expt_t_taus_wavepacket_fock(L, r, mu, beta, gamma, m_max, taus, rho_ss_coh, t_final, solve_ivp_kwargs=None):
    """Looks like we are squeezing at the sideband here because of the e^(2i beta t) modulation of the wavepacket...

    Also, we're using a wavepacket of length 2.
    """
    return calc_hier_auto_corr_fock(lambda t: xi_rect(t, 0, 2)*np.exp(2.j*np.abs(beta)*t),
                                    L, r, mu, beta, gamma, m_max,
                                    taus, rho_ss_coh, t=t_final,
                                    solve_ivp_kwargs=solve_ivp_kwargs)
```

```python
def gen_Expt_t_taus_square_wavepacket_freq_offset_fock(L, r, mu, beta, gamma, m_max, taus, rho_ss_coh, t_final, wavepacket_length, freq_offset=0., solve_ivp_kwargs=None):
    xi_fn = lambda t: xi_rect(t, 0, wavepacket_length) * np.exp(-1.j * freq_offset * t)
    return calc_hier_auto_corr_fock(xi_fn,
                                    L, r, mu, beta, gamma, m_max,
                                    taus, rho_ss_coh, t=t_final,
                                    solve_ivp_kwargs=solve_ivp_kwargs)
```

```python
mollow_triplet_plot_dir = pathlib.Path('mollow-triplit-plot')
if not mollow_triplet_plot_dir.exists():
    mollow_triplet_plot_dir.mkdir()
```

```python
%%time
r = np.log(2)
mu = 0
m_max = 12
t_final = 0.5
Expt_t_taus_wavepacket_fock = gen_save_load_data(
    partial(
        gen_Expt_t_taus_wavepacket_fock,
        L=L,
        r=r,
        mu=mu,
        beta=beta,
        gamma=gamma,
        m_max=m_max,
        taus=taus,
        rho_ss_coh=rho_ss_coh,
        t_final=t_final,
        solve_ivp_kwargs={'rtol': 1e-6, 'atol': 1e-9}
    ),
    mollow_triplet_plot_dir.joinpath(f'er_{np.exp(r):.2f}_mu_{mu / np.pi:.2f}pi_mmax_{m_max}_tfinal_{t_final:.2f}.pickle'),
    overwrite=False,
)
```

    Data already exist.
    CPU times: user 878 µs, sys: 39 µs, total: 917 µs
    Wall time: 2.56 ms

```python
fig, ax = plot_emission_spectrum(
    auto_corr=delta_Expt_t_taus_coh,
    d_tau=np.diff(taus)[0],
    label='No squeezing',
    beta=beta,
)
plot_emission_spectrum(
    auto_corr=Expt_t_taus_wavepacket_fock[0],
    d_tau=np.diff(taus)[0],
    label='Wavepacket',
    beta=beta,
    ax=ax,
)
plt.tight_layout()
```

The squeezing used in my dissertation (as extracted from my `2018-06-12-2` notebook, which generated the data I plotted for my dissertation in my `2018-06-13` notebook) is $r=\ln4$

```python
Expt_t_taus['Resonant'] = {'data': Expt_t_taus_wavepacket_on_res[0],
                           'gamma': 0.5,
                           'beta': 8.j,
                           'r': np.log(4),
                           'mu': 0,
                           'xi freq': 0,
                           'm_max': 12,
                           't': 0.5,
                           'T': 2**8,
                           'N taus': 2**13}
```

```python
Expt_t_taus['Resonant Phased'] = {'data': Expt_t_taus_wavepacket_on_res_phase[0],
                           'gamma': 0.5,
                           'beta': 8.j,
                           'r': np.log(4),
                           'mu': np.pi / 2,
                           'xi freq': 0,
                           'm_max': 12,
                           't': 0.5,
                           'T': 2**8,
                           'N taus': 2**13}
```

```python
Expt_t_taus['Sideband'] = {'data': Expt_t_taus_wavepacket[0],
                           'gamma': 0.5,
                           'beta': 8.j,
                           'r': np.log(4),
                           'mu': 0,
                           'xi freq': 2 * np.abs(8.j),
                           'm_max': 12,
                           't': 0.5,
                           'T': 2**8,
                           'N taus': 2**13}
```

```python
Expt_t_taus['Sideband Phased'] = {'data': Expt_t_taus_wavepacket_phase[0],
                                  'gamma': 0.5,
                                  'beta': 8.j,
                                  'r': np.log(4),
                                  'mu': np.pi / 2,
                                  'xi freq': 2 * np.abs(8.j),
                                  'm_max': 12,
                           't': 0.5,
                                  'T': 2**8,
                                  'N taus': 2**13}
```

```python
def gen_save_load_data_with_args(fname, data_gen_method, method_args=None, method_kwargs=None, overwrite=False):
    '''Get the data returned by the generating method, running the method only if the data isn't already available.

    If the given filename exists, load and return the data from that file. Otherwise generate the data using the
    supplied method and save and return it.

    Useful for notebooks you imagine running multiple times, but where some of the data is expensive to generate
    and you want to save it to disk to be reloaded for future sessions.

    '''
    if method_args is None:
        method_args = []
    if method_kwargs is None:
        methd_kwargs = dict()
    try:
        with open(fname, 'xb' if not overwrite else 'wb') as f:
            data = data_gen_method(*method_args, **method_kwargs)
            pickle.dump(data, f)
    except FileExistsError:
        print('Data already exist.')
        with open(fname, 'rb') as f:
            data = pickle.load(f)
    return data
```

```python
Expt_t_taus = dict()
```

```python
Expt_t_taus['Resonant'] = {'data': Expt_t_taus_wavepacket_on_res[0],
                           'gamma': 0.5,
                           'beta': 8.j,
                           'r': np.log(4),
                           'mu': 0,
                           'xi freq': 0,
                           'm_max': 12,
                           't': 0.5,
                           'T': 2**8,
                           'N taus': 2**13}
```

```python
%%time
r = np.log(4)
mu = 0
freq_offset = 0.
m_max = 20
t_final = 0.5
wavepacket_length = 4 * gamma

Expt_t_taus['Resonant'] = gen_save_load_data_with_args(
    fname=mollow_triplet_plot_dir.joinpath(f'er_{np.exp(r):.2f}_mu_{mu / np.pi:.2f}pi_mmax_{m_max}_tfinal_{t_final:.2f}.pickle'),
    data_gen_method=gen_Expt_t_taus_square_wavepacket_freq_offset_fock,
    method_kwargs=dict(
        L=L,
        r=r,
        mu=mu,
        beta=beta,
        gamma=gamma,
        m_max=m_max,
        taus=taus,
        rho_ss_coh=rho_ss_coh,
        t_final=t_final,
        freq_offset=freq_offset,
        wavepacket_length=wavepacket_length,
        solve_ivp_kwargs={'rtol': 1e-6, 'atol': 1e-9},
    ),
    overwrite=False,
)
```

    Data already exist.
    CPU times: user 784 µs, sys: 35 µs, total: 819 µs
    Wall time: 2 ms

```python
mollow_triplet_plot_dir.joinpath(f'er_{np.exp(r):.2f}_mu_{mu / np.pi:.2f}pi_mmax_{m_max}_tfinal_{t_final:.2f}.pickle')
```

    PosixPath('mollow-triplit-plot/er_4.00_mu_0.00pi_mmax_20_tfinal_0.50.pickle')

```python
Expt_t_taus['Resonant Phased'] = {'data': Expt_t_taus_wavepacket_on_res_phase[0],
                           'gamma': 0.5,
                           'beta': 8.j,
                           'r': np.log(4),
                           'mu': np.pi / 2,
                           'xi freq': 0,
                           'm_max': 12,
                           't': 0.5,
                           'T': 2**8,
                           'N taus': 2**13}
```

```python
%%time
gamma = 0.5
beta = 8.j
r = np.log(4)
mu = np.pi / 2
freq_offset = 0.
m_max = 20
t_final = 0.5
taus = np.linspace(0, 2**8, 2**13)
wavepacket_length = 4 * gamma

Expt_t_taus['Resonant Phased'] = gen_save_load_data_with_args(
    fname=mollow_triplet_plot_dir.joinpath(f'er_{np.exp(r):.2f}_mu_{mu / np.pi:.2f}pi_mmax_{m_max}_tfinal_{t_final:.2f}.pickle'),
    data_gen_method=gen_Expt_t_taus_square_wavepacket_freq_offset_fock,
    method_kwargs=dict(
        L=L,
        r=r,
        mu=mu,
        beta=beta,
        gamma=gamma,
        m_max=m_max,
        taus=taus,
        rho_ss_coh=rho_ss_coh,
        t_final=t_final,
        freq_offset=freq_offset,
        wavepacket_length=wavepacket_length,
        solve_ivp_kwargs={'rtol': 1e-6, 'atol': 1e-9},
    ),
    overwrite=False,
)
```

    Data already exist.
    CPU times: user 1.11 ms, sys: 0 ns, total: 1.11 ms
    Wall time: 3.25 ms

```python
Expt_t_taus['Sideband'] = {'data': Expt_t_taus_wavepacket[0],
                           'gamma': 0.5,
                           'beta': 8.j,
                           'r': np.log(4),
                           'mu': 0,
                           'xi freq': 2 * np.abs(8.j),
                           'm_max': 12,
                           't': 0.5,
                           'T': 2**8,
                           'N taus': 2**13}
```

```python
%%time
gamma = 0.5
beta = 8.j
r = np.log(4)
mu = 0.
freq_offset = 2 * np.abs(beta)
m_max = 20
t_final = 0.5
taus = np.linspace(0, 2**8, 2**13)
wavepacket_length = 4 * gamma

Expt_t_taus['Sideband'] = gen_save_load_data_with_args(
    fname=mollow_triplet_plot_dir.joinpath(f'er_{np.exp(r):.2f}_mu_{mu / np.pi:.2f}pi_mmax_{m_max}_tfinal_{t_final:.2f}_freq_offset_{freq_offset:.2f}.pickle'),
    data_gen_method=gen_Expt_t_taus_square_wavepacket_freq_offset_fock,
    method_kwargs=dict(
        L=L,
        r=r,
        mu=mu,
        beta=beta,
        gamma=gamma,
        m_max=m_max,
        taus=taus,
        rho_ss_coh=rho_ss_coh,
        t_final=t_final,
        freq_offset=freq_offset,
        wavepacket_length=wavepacket_length,
        solve_ivp_kwargs={'rtol': 1e-6, 'atol': 1e-9},
    ),
    overwrite=False,
)
```

    Data already exist.
    CPU times: user 1.02 ms, sys: 45 µs, total: 1.06 ms
    Wall time: 2.87 ms

```python
Expt_t_taus['Sideband Phased'] = {'data': Expt_t_taus_wavepacket_phase[0],
                                  'gamma': 0.5,
                                  'beta': 8.j,
                                  'r': np.log(4),
                                  'mu': np.pi / 2,
                                  'xi freq': 2 * np.abs(8.j),
                                  'm_max': 12,
                           't': 0.5,
                                  'T': 2**8,
                                  'N taus': 2**13}
```

```python
%%time
gamma = 0.5
beta = 8.j
r = np.log(4)
mu = np.pi / 2
freq_offset = 2 * np.abs(beta)
m_max = 20
t_final = 0.5
taus = np.linspace(0, 2**8, 2**13)
wavepacket_length = 4 * gamma

Expt_t_taus['Sideband Phased'] = gen_save_load_data_with_args(
    fname=mollow_triplet_plot_dir.joinpath(f'er_{np.exp(r):.2f}_mu_{mu / np.pi:.2f}pi_mmax_{m_max}_tfinal_{t_final:.2f}_freq_offset_{freq_offset:.2f}.pickle'),
    data_gen_method=gen_Expt_t_taus_square_wavepacket_freq_offset_fock,
    method_kwargs=dict(
        L=L,
        r=r,
        mu=mu,
        beta=beta,
        gamma=gamma,
        m_max=m_max,
        taus=taus,
        rho_ss_coh=rho_ss_coh,
        t_final=t_final,
        freq_offset=freq_offset,
        wavepacket_length=wavepacket_length,
        solve_ivp_kwargs={'rtol': 1e-6, 'atol': 1e-9},
    ),
    overwrite=False,
)
```

    Data already exist.
    CPU times: user 1.24 ms, sys: 54 µs, total: 1.29 ms
    Wall time: 2.84 ms

```python
fig, ax = plot_emission_spectrum(
    auto_corr=delta_Expt_t_taus_coh,
    label='No squeezing',
    d_tau=np.diff(taus)[0],
    beta=beta,
    plot_kwargs=dict(
        color='k',
        linestyle='--',
    ),
)
plot_emission_spectrum(
    auto_corr=Expt_t_taus['Resonant'][0],
    label='Wavepacket Resonant',
    d_tau=np.diff(taus)[0],
    beta=beta,
    ax=ax,
    plot_kwargs=dict(
        color='k',
    ),
)
plt.tight_layout()
plt.show()
```

```python
fig, ax = plot_emission_spectrum(
    auto_corr=delta_Expt_t_taus_coh,
    label='No squeezing',
    d_tau=np.diff(taus)[0],
    beta=beta,
    plot_kwargs=dict(
        color='k',
        linestyle='--',
    ),
)
plot_emission_spectrum(
    auto_corr=Expt_t_taus['Resonant Phased'][0],
    label='Wavepacket Resonant Phased',
    d_tau=np.diff(taus)[0],
    beta=beta,
    ax=ax,
    plot_kwargs=dict(
        color='k',
    ),
)
plt.tight_layout()
plt.show()
```

```python
fig, ax = plot_emission_spectrum(
    auto_corr=delta_Expt_t_taus_coh,
    label='No squeezing',
    d_tau=np.diff(taus)[0],
    beta=beta,
    plot_kwargs=dict(
        color='k',
        linestyle='--',
    ),
)
plot_emission_spectrum(
    auto_corr=Expt_t_taus['Sideband'][0],
    label='Wavepacket Sideband',
    d_tau=np.diff(taus)[0],
    beta=beta,
    ax=ax,
    plot_kwargs=dict(
        color='k',
    ),
)
plt.tight_layout()
plt.show()
```

```python
fig, ax = plot_emission_spectrum(
    auto_corr=delta_Expt_t_taus_coh,
    label='No squeezing',
    d_tau=np.diff(taus)[0],
    beta=beta,
    plot_kwargs=dict(
        color='k',
        linestyle='--',
        linewidth=1,
    ),
)
plot_emission_spectrum(
    auto_corr=Expt_t_taus['Sideband Phased'][0],
    label='Wavepacket Sideband Phased',
    d_tau=np.diff(taus)[0],
    beta=beta,
    ax=ax,
    plot_kwargs=dict(
        color='k',
    ),
)
plt.tight_layout()
plt.savefig('sideband-phased.png', dpi=150)
plt.show()
```

```python
SINGLE_COL_WIDTH = 3.4039 #INCHES
DOUBLE_COL_WIDTH = 7.0569 #INCHES

SMALL_SIZE = 6.5
MEDIUM_SIZE = 8.5
BIGGER_SIZE = 10.5

plt.rc('text', usetex=False)
plt.rc('font', size=SMALL_SIZE)          # controls default text sizes
plt.rc('axes', titlesize=SMALL_SIZE)     # fontsize of the axes title
plt.rc('axes', labelsize=MEDIUM_SIZE)    # fontsize of the x and y labels
plt.rc('xtick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
plt.rc('ytick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
plt.rc('legend', fontsize=SMALL_SIZE)    # legend fontsize
plt.rc('figure', titlesize=BIGGER_SIZE)  # fontsize of the figure title
plt.rc('lines',linewidth=0.75)
#plt.rcParams['ytick.major.pad']='0.5'
```

```python
fig, axs = plt.subplots(nrows=4, ncols=1, sharex='col', figsize=(SINGLE_COL_WIDTH, SINGLE_COL_WIDTH))
for ax, expt_t_taus_key in zip(axs, ['Resonant', 'Resonant Phased', 'Sideband', 'Sideband Phased']):
    plot_emission_spectrum(
        auto_corr=delta_Expt_t_taus_coh,
        label='No squeezing',
        d_tau=np.diff(taus)[0],
        beta=beta,
        ax=ax,
        plot_kwargs=dict(
            color='k',
            linestyle='--',
        ),
    )
    plot_emission_spectrum(
        auto_corr=Expt_t_taus[expt_t_taus_key][0],
        label=expt_t_taus_key,
        d_tau=np.diff(taus)[0],
        beta=beta,
        ax=ax,
        plot_kwargs=dict(
            color='k',
        ),
    )
for ax in axs[:-1]:
    ax.set_xlabel('')
plt.tight_layout()
plt.savefig(mollow_triplet_plot_dir.joinpath('reproduced-thesis-plots.pdf'))
plt.show()
```

```python

```


## Chapter 7

```python
import sys
sys.path.append("..")
from importlib import reload
```

```python
import Ch7.utilities as ch7_utils
reload(ch7_utils)
```

    <module 'Ch7.utilities' from '../Ch7/utilities.py'>

```python
from sympy import *
import numpy as np
init_printing(use_latex='mathjax')
s, w = symbols('s w', real=True)
```

### Problem 7.1

Each of the four parts of this problem would follow the same 4 steps:

##### Step 1:
Identify available and unavailable subsystems and their associated matrices: $A_{uu}, A_{ua}, A_{au}, A_{aa}, B_{u}, B_{a}, C_a$. $C_a$ is the measurement matrix of the available subsystem, assumed to be nonsingular.
##### Step 2:
Compute matrices as a function of entries of $L$:
\begin{eqnarray}
F &=& A_{uu} - L C_a A_{au} \\
\bar {\bar G} &=& (A_{ua} - L C_a A_{aa}) C_a^{-1} \\
H &=& B_u - L C_a B_a.
\end{eqnarray}
These matrices define the dynamics for $\mathbf{z} \triangleq \mathbf{\hat x_u} - Ly$: $\mathbf{\dot z} = F \mathbf{\hat x_u} + {\bar {\bar G}} y + Hu$

##### Step 3:
Identify a design objective: i.e. poles for characteristic polynomial of $F$.

##### Step 4:
Use Bass-Gura formalism on the unavailable subsystem dynamics to solve for the gain matrix, $L$, that meets the design objective from *Step 3*.

To make the solution general, I didn't consider the specific cases of a), b), c), and d) of this problem.  Rather, I have drawn block diagrams for the cases of reduced-order observers of orders 3, 2, and 1.  Steps 1-4 above can be used on the two-cart system that is the subject of Problem 7.1 with the specific measurability conditions of each of part a), b), c), and d).  Since the problem isn't to _actually_ design reduced-order observers, but rather to discuss criteria for their design, I think this approach is okay.

#### a)

With only one available measurement:

#### b) and c)
With two available measurements:

#### d)
With three available measurements:

### Problem 7.2

This problem uses the basic ideas of the Bass-Gura formalism from Chapter 6.  The full observer is designed as:

```python
# using constants from Problem 3.6
m, M, l, g, k, R, r = .1, 1., 1., 9.8, 1., 100., .02
A = np.array([[0., 1., 0., 0.],
              [0., -k**2 / (M*r**2*R), -(m*g)/M, 0.],
              [0., 0., 0., 1.],
              [0, k**2 / (M*r**2*R*l), ((M+m)*g)/(M*l), 0.]])
C = np.array([[1., 0., 0., 0.]])
B = np.array([[0.], [k/(M*R*r)], [0.], [-k/(M*R*r*l)]])
D = np.zeros((1, 1))
w0 = 5.
closedLoopPoles = np.roots(np.array([1.0, 2.613*w0, (2.+np.sqrt(2))*w0**2, 2.613*w0**3, w0**4]))
K = ch7_utils.obsBassGura(A, C, closedLoopPoles)
pprint('K = ')
pprint(K)
```

    K =
    [[  -11.935     ]
     [  394.51033906]
     [ -452.00581633]
     [-1993.61883169]]

The gain matrix for the full observer is $K = [-11.935, 394.510, -452.006, -1993.619]^T$.

### Problem 7.3

This problem is conceptually-similar to Problem 7.1 above, so I will follow the steps I outlined above; see `reducedOrderObserver` in "utilities.py" for the actual implementation.

```python
m, M, l, g, k, R, r = .1, 1., 1., 9.8, 1., 100., .02
A = np.array([[0., 1., 0., 0.],
              [0., -k**2 / (M*r**2*R), -(m*g)/M, 0.],
              [0., 0., 0., 1.],
              [0, k**2 / (M*r**2*R*l), ((M+m)*g)/(M*l), 0.]])
B = np.array([[0.],[k/(M*R*r)],[0.],[-k/(M*R*r*l)]])
# make sure to properly size the input matrices
A11 = np.array(A[0, 0]).reshape((1, 1))
A12 = np.array(A[0, 1:]).reshape((1, 3))
A21 = np.array(A[1:, 0]).reshape((3, 1))
A22 = np.array(A[1:, 1:]).reshape((3, 3))
B1 = np.array(B[0]).reshape((1, 1))
B2 = np.array(B[1:]).reshape((3, 1))
C1 = np.array([[1]]).reshape((1, 1))
desiredPoles = np.roots(np.array([1.0, 2.*5., 2.*5.**2, 5.**3]))
L, Gbb, H = ch7_utils.reducedOrderObserver(A11, A12, A21, A22, B1, B2, C1, desiredPoles)
pprint('L = ')
pprint(L)
```

    L =
     [[ -15.        ]
      [ -62.02040816]
     [-212.55102041]]

So, the desired third-order observer with the cart-position measured can be achieved with $L = [-15, -62.020, -212.551]^T$.

### Problem 7.4

#### a)

```python
Kv, Lv, J, Km, Omm, Kdp = 94.3, 1.0, 7900., 8.46e6, 45.9, 6.33e-6
A = np.array([[0., 1., 0., 0.], [0., 0., 1., 0.], [0., -Km/J, -(Km/J)*(Omm*J/Km), (Km/J)], [0., 0., -Kv*Kdp*J, -Lv*Kv]])
B = np.array([[0.], [0.], [0.], [Kv]])
C = np.array([[1., 0., 0., 0.]])
w0 = 20.
closedLoopPoles = np.roots(np.array([1.0, 2.613*w0, (2.+np.sqrt(2.))*w0**2, 2.613*w0**3, w0**4]))
Ktilde = ch7_utils.obsBassGura(A, C, closedLoopPoles)
pprint('K[disturbance-free] = ')
pprint(Ktilde)
```

    K[disturbance-free] =
     [[-8.79400000e+01]
      [ 3.24568261e+03]
      [ 3.83776582e+05]
     [-5.37765422e+04]]

#### b)

The exogenous vector is $\mathbf{x}_0 = [d_r, d_p, d_q]^T$.  Using the block diagram from Figure 4.4, the exogenous contribution to the system dynamics can be found to be as in the block below.  Part a) above gave the disturbance-free gain matrix, now we just need to apply steps from pg. 272.  _The method in the book doesn't work so well, so I'm omitting._

* Step 1: design a disturbance free observer (done in Part a) already)
* Step 2: Find matrix $V$ using Step 1.
* Step 3: Find $K_0$ so that the disturbance dynamics have the desired error decay rate.

```python
D = np.zeros((1, 3))
E = np.array([[0., 0., 0.], [1., 0., 0.], [0., Km / J, 0.], [0., 0., Kv]])
w0 = 30.
desiredExoPoles = np.roots(np.array([1.0, 2.*w0, 2.*w0**2, w0**3]))
K = ch7_utils.obsBassGura(A, C, closedLoopPoles, D, E, desiredExoPoles)
pprint('K[full] = ')
pprint(K)
```

    K[full] =
     [[-8.79400000e+01]
      [ 3.24568261e+03]
      [ 3.83776582e+05]
      [-5.37765422e+04]
      [ 0.00000000e+00]
      [ 0.00000000e+00]
     [ 0.00000000e+00]]

_After going back and forth on the r/ControlTheory subreddit, I'm convinced that something is missing in Friedland's argument.  Or, more likely, I have constructed the problem incorrectly.  Either way, it is not worth the time to continue working on this.  For the time-being, just return 0's for the gains.  I'm leaving this note so I won't go back and not know what was going on._

### Problem 7.5

```python
HoverJd, KQoverJd, KDoverJd, BoverJd = symbols('HoverJd KQoverJd KDoverJd BoverJd', real=True)
l1, l2, l3, l4 = symbols('l1 l2 l3 l4', real=True)
A = Matrix([[0., 0., 1., 0.],
            [0., 0., 0., 1.],
            [-KDoverJd, -KQoverJd, 0, -HoverJd],
            [KQoverJd, -KDoverJd, HoverJd, 0]])
B = Matrix([[0., 0.],
            [0., 0.],
            [1., 0.],
            [0., 1.]])
C = Matrix([[1., 0., 0., 0.],
            [0., 1., 0., 0.]])
L = Matrix([[l1, l2], [-l2, l1], [l3, l4], [-l4, l3]])
E = Matrix([[-1., 0.],
            [0., -1.],
            [0., 0.],
            [0., 0.]])
# the available subsystem observability is the 2x2 identity
Ca = eye(2)
Aaa = zeros(2)
Aau = Matrix([[A[0, 2], A[0, 3], E[0, 0], E[0, 1]], [A[1, 2], A[1, 3], E[1, 0], E[1, 1]]])
Aua = Matrix([[A[2, 0], A[2, 1]], [A[3, 0], A[3, 1]], [0., 0.], [0., 0.]])
Auu = Matrix([[A[2, 2], A[2, 3], E[2, 0], E[2, 1]], [A[3, 2], A[3, 3], E[3, 0], E[3, 1]], [0., 0., 0., 0.], [0., 0., 0., 0.]])
#pprint('A22 = ')
#pprint(Auu)
#pprint('A12 = ')
#pprint(Aau)
#pprint('C1 = ')
#pprint(Ca)
Ba = zeros(2)
Bu = Matrix([[1., 0.], [0., 1.], [0., 0.], [0., 0.]])
F = Matrix(Auu) - L * Matrix(Ca) * Matrix(Aau)
Gb = (Matrix(Aua) - L*Matrix(Ca)*Matrix(Aaa))*Matrix(Ca)**-1 + F*L
H = Matrix(Bu) - L*Matrix(Ca)*Matrix(Ba)
#pprint('F = ')
pprint(F)
#pprint('Gb = ')
#pprint(Gb)
#pprint('H = ')
#pprint(H)
```

    ⎡    -1.0⋅l₁       -HoverJd - 1.0⋅l₂  1.0⋅l₁   1.0⋅l₂⎤
    ⎢                                                    ⎥
    ⎢HoverJd + 1.0⋅l₂       -1.0⋅l₁       -1.0⋅l₂  1.0⋅l₁⎥
    ⎢                                                    ⎥
    ⎢    -1.0⋅l₃            -1.0⋅l₄       1.0⋅l₃   1.0⋅l₄⎥
    ⎢                                                    ⎥
    ⎣     1.0⋅l₄            -1.0⋅l₃       -1.0⋅l₄  1.0⋅l₃⎦

The problem says that I should see only four unique elements in the matrix $F$.  The result below is a little confusing then...

#### a)

```python
pprint('F = ')
pprint(F)
pprint('Gb = ')
pprint(Gb)
pprint('H = ')
pprint(H)
```

    F =
    ⎡    -1.0⋅l₁       -HoverJd - 1.0⋅l₂  1.0⋅l₁   1.0⋅l₂⎤
    ⎢                                                    ⎥
    ⎢HoverJd + 1.0⋅l₂       -1.0⋅l₁       -1.0⋅l₂  1.0⋅l₁⎥
    ⎢                                                    ⎥
    ⎢    -1.0⋅l₃            -1.0⋅l₄       1.0⋅l₃   1.0⋅l₄⎥
    ⎢                                                    ⎥
    ⎣     1.0⋅l₄            -1.0⋅l₃       -1.0⋅l₄  1.0⋅l₃⎦
    Gb =
    ⎡                  2
    ⎢-KDoverJd - 1.0⋅l₁  + 1.0⋅l₁⋅l₃ - 1.0⋅l₂⋅l₄ - l₂⋅(-HoverJd - 1.0⋅l₂)  -KQover
    ⎢
    ⎢
    ⎢KQoverJd + 1.0⋅l₁⋅l₂ - 1.0⋅l₁⋅l₄ + l₁⋅(HoverJd + 1.0⋅l₂) - 1.0⋅l₂⋅l₃   -KDove
    ⎢
    ⎢                                            2         2
    ⎢             -1.0⋅l₁⋅l₃ + 1.0⋅l₂⋅l₄ + 1.0⋅l₃  - 1.0⋅l₄
    ⎢
    ⎢
    ⎣                 1.0⋅l₁⋅l₄ + 1.0⋅l₂⋅l₃ - 2.0⋅l₃⋅l₄

                                                                   ⎤
    Jd - 1.0⋅l₁⋅l₂ + 1.0⋅l₁⋅l₄ + l₁⋅(-HoverJd - 1.0⋅l₂) + 1.0⋅l₂⋅l₃⎥
                                                                   ⎥
                2                                                  ⎥
    rJd - 1.0⋅l₁  + 1.0⋅l₁⋅l₃ - 1.0⋅l₂⋅l₄ + l₂⋅(HoverJd + 1.0⋅l₂)  ⎥
                                                                   ⎥
                                                                   ⎥
               -1.0⋅l₁⋅l₄ - 1.0⋅l₂⋅l₃ + 2.0⋅l₃⋅l₄                  ⎥
                                                                   ⎥
                                          2         2              ⎥
           -1.0⋅l₁⋅l₃ + 1.0⋅l₂⋅l₄ + 1.0⋅l₃  - 1.0⋅l₄               ⎦
    H =
    ⎡1.0   0 ⎤
    ⎢        ⎥
    ⎢ 0   1.0⎥
    ⎢        ⎥
    ⎢ 0    0 ⎥
    ⎢        ⎥
    ⎣ 0    0 ⎦

#### b)

```python
p = F.charpoly(s)
coeffs = p.all_coeffs()
pprint(coeffs)
```

    ⎡                                 2                          2
    ⎣1.0, 2.0⋅l₁ - 2.0⋅l₃, 1.0⋅HoverJd  + 2.0⋅HoverJd⋅l₂ + 1.0⋅l₁  - 2.0⋅l₁⋅l₃ + 1

         2                     2         2               2
    .0⋅l₂  - 2.0⋅l₂⋅l₄ + 1.0⋅l₃  + 1.0⋅l₄ , - 2.0⋅HoverJd ⋅l₃ + 2.0⋅HoverJd⋅l₁⋅l₄

                                    2   2              2   2⎤
    - 2.0⋅HoverJd⋅l₂⋅l₃, 1.0⋅HoverJd ⋅l₃  + 1.0⋅HoverJd ⋅l₄ ⎦

#### c)

I have put in as much time on this problem as I care to.  The characteristic equation above clearly leads to a nonlinear optimization problem for $l_1, l_2, l_3, $ and $l_4$.  I tried to solve for it below by changing my initial guess to get reasonably-small $f$ evaluation and I got a pretty good estimate for when the oscillatory modes are completely deadened, but the minimizer wouldn't converge for non-zero frequencies in the two oscillatory modes ($\omega = 3000$).  Changing the objective function below to some other convex objective did the trick.  I was unable to get a quick unit test implemented for this, so I am leaving as is.

```python
from scipy.optimize import minimize
HoverJd, KDoverJd, KQoverJd = 3000., 30., 60.
coeffs = [1.0,
          2.*(l1 - l3),
          HoverJd**2 + 2.0*HoverJd*l2 + l1**2 - 2.0*l1*l3 + l2**2 - 2.0*l2*l4 + 1.0*l3**2 + 1.0*l4**2,
          -2.0*HoverJd**2*l3 + 2.0*HoverJd*l1*l4 - 2.0*HoverJd*l2*l3,
          HoverJd**2*l3**2 + HoverJd**2*l4**2
         ]
#pprint(factor(coeffs))
A = np.array([[0., 0., 1., 0.],
             [0., 0., 0., 1.],
             [-KDoverJd, -KQoverJd, 0., -HoverJd],
             [KQoverJd, -KDoverJd, HoverJd, 0.]])
B = np.array([[0., 0.],
            [0., 0.],
            [1., 0.],
            [0., 1.]])
C = np.array([[1., 0., 0., 0.],
            [0., 1., 0., 0.]])
E = np.array([[-1., 0.],
              [0., -1.],
              [0., 0.],
              [0., 0.]])
Ca = np.eye(2)
Aaa = np.zeros((2, 2))
Aau = np.array([[A[0, 2], A[0, 3], E[0, 0], E[0, 1]], [A[1, 2], A[1, 3], E[1, 0], E[1, 1]]])
Aua = np.array([[A[2, 0], A[2, 1]], [A[3, 0], A[3, 1]], [0., 0.], [0., 0.]])
Auu = np.array([[A[2, 2], A[2, 3], E[2, 0], E[2, 1]], [A[3, 2], A[3, 3], E[3, 0], E[3, 1]], [0., 0., 0., 0.], [0., 0., 0., 0.]])
openLoopCoeffs = np.array(Auu.charpoly(s).all_coeffs())
#pprint(np.roots(openLoopCoeffs))
# keep imaginary parts as-is and shoot for real part = -1000.
#desiredCoeffs = np.poly(np.array([np.complex(-1000., 3000.), np.complex(-1000., -3000.), np.complex(-1000., 0.), np.complex(-1000., 0.)]))
r = -1000.
i = 3000.
desiredCoeffs = np.poly(np.array([np.complex(r, 0.), np.complex(r, 0.), np.complex(r, i), np.complex(r, -i)]))
pprint(desiredCoeffs)
# setup objective for the optimization - x, y, z -> l2, l3, l4
def obj(w):
    x, y, z = w
    u = desiredCoeffs[1] / 2. - y
    f2 = (u - y)**2 + (x - z)**2 + HoverJd**2 + 2.*HoverJd*x - desiredCoeffs[2]
    f3 = 2. * HoverJd * (u*z - x*y - HoverJd*y) - desiredCoeffs[3]
    f4 = HoverJd**2*(y**2 + z**2) - desiredCoeffs[4]
    return (f2 + f3 + f4)**2 # this objective works! the more intuitive (f2**2 + f3**2 + f4**2) does not!

def grad(w):
    h = 1e-8
    w1 = np.array([w[0] + h, w[1], w[2], w[3]])
    g1 = (obj(w1) - obj(w)) / h
    w2 = np.array([w[0], w[1]+h, w[2], w[3]])
    g2 = (obj(w2) - obj(w)) / h
    w3 = np.array([w[0], w[1], w[2]+h, w[3]])
    g3 = (obj(w3) - obj(w)) / h
    return np.array([g1, g2, g3])

x0 = np.array([-3000., -.004, 1000.])
sol = minimize(obj, x0=x0, method='nelder-mead', options={'maxiter': 10000})
l = list(sol.x)
#pprint(l)
l.insert(0, desiredCoeffs[1] / 2. - sol.x[1]) # l1 as a function of l1 came from first coefficient
#pprint(obj(sol.x)) # check on how close the solution found gets to actual solution (f(x) = 0), curr ~ 10e-5, not bad
pprint('l1, l2, l3, l4 = {}'.format(l))
L = np.array([[l[0], l[1]], [-l[1], l[0]], [l[2], l[3]], [-l[3], l[2]]])
F = Auu - L.dot(Ca.dot(Aau))
pprint(np.poly(np.linalg.eigvals(F))) # this doesn't match super closely, but oh well - probably a combination of user-error and numerical instability of the minimizer (shrug)
```

    [1.0e+00 4.0e+03 1.5e+07 2.2e+10 1.0e+13]
    l1, l2, l3, l4 = [2000.0039425825403, -3113.2106773237365, -0.0039425825401703
    08, 1054.5851351060987]
    [1.00000000e+00 4.00001577e+03 1.16912894e+07 1.26550439e+10
                           1.00093483e+13]

### Problem 7.6

#### a)

```python
R, _C = 2., 1.
# x == (v1, v2, v3)
A = np.array([[-3./(R*_C), 1./(R*_C), 0.],[1./(R*_C), -2./(R*_C), 1./(R*_C)], [0., 1./(R*_C), -3./(R*_C)]])
B = np.array([[2. / (R*_C)], [0.], [0.]])
C = np.array([[0., 0., 1.]])
w0 = 3.
desiredPoles = np.roots(np.array([1.0, 2.*w0, 2.*w0**2, w0**3]))
K = ch7_utils.obsBassGura(A, C, desiredPoles)
pprint('K[disturbance-free] = ')
pprint(K)
```

    K[disturbance-free] =
    [[42.5]
     [16.5]
     [ 2. ]]

#### b)

I am not drawing any more block diagrams!!

```python
D = np.zeros((1, 2))
E = np.array([[0., 0.], [0., 1./(R*_C)], [-3./(R*_C), 1./(R*_C)]])
desiredExoPoles = np.array([np.complex(-5., 5), np.complex(-5., -5)])
K = ch7_utils.obsBassGura(A, C, desiredPoles, D, E, desiredExoPoles)
pprint('K[full] = ')  # currently broken
pprint(K)
```

    K[full] =
    [[42.5]
     [16.5]
     [ 2. ]
     [ 0. ]
     [ 0. ]]

#### c)

Took a stab at this one and it didn't work out as expected.  Considering that a valid observer was found with the same desired characteristics in part b), I'm not going to work through this any further.  _The metasystem is unobservable, so I don't know how this is expected to work.  I might have setup the problem incorrectly._

```python
# XXX doesn't work
#R, C = 2., 1.
# x == (v1, v2, v3)
#A = np.array([[-3./(R*C), 1./(R*C), 0.],[1./(R*C), -2./(R*C), 1./(R*C)], [0., 1./(R*C), -3./(R*C)]])
#B = np.array([[2. / (R*C)], [0.], [0.]])
#E = np.array([[0., 0.], [0., 1./(R*C)], [-3. / (R*C), 1./(R*C)]])
#Am = np.zeros((5, 5))
#Am[:3, :3] = A
#Am[:3, 3:] = E
#Bm = np.zeros((5, 1))
#Bm[:3] = B
#Cm = np.array([[0., 0., 1., 1., 1.]])
#desiredCoeffsNoDist = np.array([1.0, 2.*w0, 2.*w0**2, w0**3])
#xRoots = np.array(np.roots(desiredCoeffsNoDist))
#x0Roots = np.array([np.complex(-5., 5), np.complex(-5., -5)])
#roots = np.array([xRoots[0], xRoots[1], xRoots[2], x0Roots[0], x0Roots[1]])
#pprint(roots)
#desiredCoeffs = np.poly(roots)
#_, K = BassGuraObsGain(Am, Cm, desiredCoeffs)
#pprint('Full K = ')
#pprint(K)
```

### Problem 7.7

Most of this problem is worked out in Example 7C, so I will just complete it.

#### a)

Solve the linear system from 7C.3 plugging in desired $\{ \hat b_i \}$.  The coefficients come from the desired poles given in the problem construction.

```python
A = np.array([[-30.3, 0., 0., 0.], [1.2e-4, -6.02, 0., 0.], [0., -3.77, 0., 0.], [0., -2.8, 0., 0.]])
C = np.array([[0., 0., -7.3, 0.], [0., 0., 0., -25.0]])
desiredCoeffs = np.poly(np.array([np.complex(-18., 0.), np.complex(-9., np.sqrt(3.)/2.), np.complex(-9., -np.sqrt(3.)/2.)]))
b = np.array([[desiredCoeffs[3]],
             [desiredCoeffs[2] - A[0, 0]*A[0, 1]],
             [36.],
             [desiredCoeffs[1] + A[0, 0] + A[0, 1]]])
P = np.array([[-A[1, 0]*A[3, 1], A[0, 0]*A[3, 1], 0., A[0, 0]*A[1, 1]],
              [0., -A[3, 1], 0., -(A[0, 0] + A[1, 1])],
              [0., 0., 1., 0.],
              [0., 0., 0., 1.]
              ])
k = np.linalg.inv(P).dot(b)
# following example -
tildeK12 = k[0] / C[1, 3]
tildeK22 = k[1] / C[1, 3]
tildeK31 = k[2] / C[0, 2]
tildeK42 = k[3] / C[1, 3]
K = np.array([[0., tildeK12[0]],
             [0., tildeK22[0]],
             [tildeK31[0], 0.],
             [0., tildeK42[0]]
             ])
pprint('K = ')
pprint(K)
```

    K =
     [[ 0.00000000e+00  6.65430000e+05]
      [ 0.00000000e+00 -2.83894286e+00]
      [-4.93150685e+00  0.00000000e+00]
     [ 0.00000000e+00 -2.28000000e-01]]

#### b)

```python
E = np.array([[0., 0.], [0., 0.], [62.2, 5.76], [0., 5.12]])
Ac = A - K.dot(C)
V = np.linalg.inv(Ac).dot(E)
pprint('V = ')
pprint(V)
```

    V =
     [[ 0.00000000e+00 -8.43444295e+05]
      [ 0.00000000e+00  1.29875686e+00]
      [-1.72777778e+00 -2.96008704e-01]
     [ 0.00000000e+00 -1.53623144e+00]]

#### c)

I'm not drawing any more block diagrams.


# Python快速入门教程

原作者：张路

修改人：谢作如

**注**：用鼠标选中In[  ] 的代码，点击菜单中的“运行”。运行成功后，括号中将出现数字，表示执行次序。

## 为什么要学Python?

第一，简单，跟写字说话差不多
第二，好玩，跟玩捉迷藏差不多
第三，有用，差不多什么都能做

## 计算机能干啥?

看似无所不能，控制各类机器、装置，加工处理各类信息。实则大计算器是也，擅长处理各种数据类型，进行各种运算，除加减乘除等简单计算外，还能三角函数、数据统计等。

### Python能干啥?

别想太多了，**Python至少能当个不错的计算器用**！

```python
# 比如，我们来算1+1
1+1
```

```python
1+2
```

```python
# 再算一个复杂一点的
1+2+3+4+5+6+7+8+9+10
```

不过，虽然计算机有把子傻力气，给多少数都能很快计算出来，但我们输入这些数据的时间就变长了，所以算得再快也没用，就好像飞机虽然跑得快，但是从市里到飞机场花的时间很长一样。这让我想起《测量世界》中的一个情节，高斯的数学老师为了偷闲吃个苹果，让孩子们一个数一个数地从1加到100，刚开始为了看孩子们的反应，还是一个数一个数地写，后来干脆就说就这样一直加到100。那么我们有办法像高斯的老师那样，告诉计算机你就从1加到100就行了？答案是有的，但是光靠列算式是解决不了的，这回我们要下一堆命令了。

首先我们要让计算机明白计算的范围是1到100，在英语里范围叫range，所以我们要给计算机出个**完形填空题**`range()`，但这里的数是要我们来填的，1到100的写法就是"1,100"，合起来1到100这句话的说法就是range(1,100)。

```python
list(range(1,101))
```

这种下命令的形式就叫函数，因为无法用数学符号表示，所以就要用词语和填空的形式，其中词语叫做命令或函数名，需填空的部分叫做参数。

知道范围怎么表达了，然后就要让计算机知道，我们是想把这个范围里的数字一个一个去加，那么如何表达一个一个这件事呢，这就用到了另一个句式，叫for什么什么in什么，也就是对于数字范围内的每个数，怎么着怎么着。

```python
# 先写要做多少次
for num in range(1,101):
    # 然后写每一次我们做什么事情
    pass
```

这里我们用到了另一个词num，它是number的缩写，因为我们要把取出来的数字先记下来，然后才能去进行运算，所以这里用了一个叫num的卡片，把取出来的数先下上去，这样计算的时候方便我们查看，取另外一个数的时候，我们再把原来的数擦掉，然后再写上新的数字，这在计算机语言里叫做存储变量或变量。很明显，我们还缺少一个表示结果的卡片sum，所谓的加法不过就是先把sum设为零，然后每取一个num，就把它和sum相加，用得到的结果再去更新sum，如此反复，这和我们手工计算的方式是一样的，只不过计算机算的比较快而已。

```latex
%%latex
\begin{align}&1\\+&2\\\hline&3\\+&3\\\hline&6\\...\end{align}
```

```python
sum = 0
for num in range(1,101):
    temp = sum + num
    sum = temp
print(sum)
```

这里我们浪费了一个存储变量temp，因为它只起到保存中间结果的作用，其实把中间结果直接保存到sum里就可以了，只是样子可起来有点令人费解。

```python
sum = 0
for num in range(1,101):
    sum = sum + num

print(sum)
```

习惯就好了。还有更偷懒的办法呢。

```python
sum = 0
for num in range(1,101):
    sum += num

print(sum)
```

记住，图省事是程序员的天性，尤其是男性程序员。程序员最怕敲入重复的东西，所以想尽一切办法来避免这种现象或问题的出现，除了采用改进语言的办法外，后面我们还会看到各种变态的招数，他们一般管这个叫做代码复用，其实就是嫌做重复的事情啰嗦、乏味，所以当你写代码时出现大量的拷贝、粘贴或出现重复的词句时就要警惕了，是不是代码写的有点啰嗦、难看了？

现在，尽管看上去我们也能计算出结果，但如果数字比较少的话，每回都要敲这么写东西去计算貌似也挺费劲的，而且因为那些英文单词记错或句子写错还容易计算出错，最好我们只写一次，让计算机记住，然后我们每次只要告诉一个词就行了。嗯，确实是个好主意，这在程序里叫做自己编一个命令，让计算机记住，以后只要告诉计算机编的那个命令就可以了，正规的说法这叫**定义**一个函数。

定义在英文里叫做define，图省事我们就把它叫def吧，定义什么呢？有两个要素，一个是命令的名字比如add，一个是命令要处理的对象(numbers)，当然，我们也可以直接定义一个名字就行了，但是填空的括号必须留着，把上面的代码直接粘进来就可以了

```python
def sum():
    sum = 0
    for num in range(1,101):
      sum += num

    print(sum)
```

这样，我们要计算从1到100的和，只要打sum()就行了

```python
sum()
```

算多少遍都可以，但是慢着，如果我们想计算1加到500怎么办？是不是还要从头写那些代码，所以看起来这个sum()看起来不错，但是好像是一锤子买卖，不堪大用，有什么好办法呢？

这时候，上面讲得参数就派上用场了，我们可以为参数设定一个存储变量numbers，这样当用户调用我们编的命令时，只需把要计算的数填到空空里，就等于给numbers赋值了，然后我们在命令代码中再把`for num in range(1,101)`中的数据范围替换成numbers就可以了。

```python
def sum(numbers):
    sum = 0
    for num in numbers:
      sum += num

    print(sum)
```

```python
sum(range(1,101))
```

```python
sum(1)
```

我们看到如果调用sum时，我们没有按要求输入一个范围就出现错误了，最好我们能给出一个提示，当然前提是我们能够知道用户所输入的数据类型，python给我们提供了一个指令叫type。

```python
type(range(1,101))
```

```python
type([1,2,3,4,5])
```

进行判断的命令叫做if，需要判断的事情是numbers的类型，也就是type(numbers)，看看它是否等于range或list，等于在python里用两个等于号表示

```python
def sum(numbers):
    if type(numbers) == list or type(numbers) == range:
      sum = 0
      for num in numbers:
        sum += num
      print(sum)
    else:
      print("参数错误，应该输入range或列表")
```

```python
sum(1)
```

```python
sum([1,2,3,4,5])
```

```python
sum(range(1,101))
```

但是如果我们还像保留以前的爽体验，却出现了错误

```python
sum()
```

不要紧，python还提供了另外一件武器，就是可以给参数设默认值!

```python
def sum(numbers = range(1,101)):
    if type(numbers) == list or type(numbers) == range:
      sum = 0
      for num in numbers:
        sum += num
      print(sum)
    else:
      print("参数错误，应该输入range或列表")
```

```python
sum()
```

这样看虽然是解决暂时的问题了，但是感觉用起来还是别扭，比如为什么我们不能用`sum(1,2,3,4,5)`来计算少数量的加法，而要多加那两个中括号呢，为什么我们不能用`sum(form=1,to=100)`来表示从1加到100，要用一个莫名其妙的命令range呢？很好的问题！
但是我们的程序或判断逻辑就因此变得复杂起来了，而且这其中还用到可变参数的技巧。

```python
def sum1(*numbers, **nameNumbers):
    pass
```

这里的星号`*`代表是可以有任意多(包括没有)的参数，它们都被存入numbers列表中，两个星号则是表示也可以使用name=value的方式输入参数，比如`from=1, to=101`，输入的参数会被存储在字典nameNumbers中，我们需要在命令的逻辑中分别处理这两种情况。

```python
def sum(numbers = range(1,101)):
    sum = 0
    if type(numbers) == list or type(numbers) == range:
      for num in numbers:
        sum += num
    return sum
```

```python
def sum1(*numbers, **nameNumbers):
    sum2 = 0
    if len(nameNumbers) == 2 and 'f' in nameNumbers and 't' in nameNumbers:
        sum2 += sum(range(nameNumbers['f'],nameNumbers['t']))
    if len(numbers) > 0:
        sum2 += sum(list(numbers))

    return sum2

```

```python
sum1(1,2,3)
```

```python
sum1(f=1,t=101)
```

```python
sum1(1,2,3,f=4,t=101)
```

```python
sum1()
```

这个功能看起来已经很完整了，那么我以后另外的程序要用怎么办？很简单，把它们做成模块，然后在需要的地方**导入**import进来，记住程序员很讨厌做拷贝粘贴哟。

比如，你可以把下面的代码，复制到一个文本文件中，命名为：mymodule.py。

def add(*numbers, **nameNumbers):
    sum2 = 0
    if len(nameNumbers) == 2 and 'f' in nameNumbers and 't' in nameNumbers:
        sum2 += sum(range(nameNumbers['f'],nameNumbers['t']))
    if len(numbers) > 0:
        sum2 += sum(list(numbers))

    return sum2

```python
from mymodule import add
add(f=1,t=101)
```

实际上已经早有高手感觉出了问题，为我们提供好了这类功能，当然这需要加入高手做好的模块NumPy。

```python
import numpy
numpy.sum(numpy.arange(1,101))
```

很方便，几乎只要一个指令就可以了，而且从性能上说，当计算所谓的大数据时，NumPy的性能优势就会显现出来了

当然为了一个求和做一个包实在有点奢侈，所以NumPy的功能远不止这些，比如它还可以求最大、最小和平均值呢。

```python
numpy.max(numpy.arange(1,101))
```

```python
numpy.min(numpy.arange(1,101))
```

```python
numpy.mean(numpy.arange(1,101))
```

还有更吓人的，可以求1到100的平方和

```python
numpy.sum(numpy.arange(1,101)**2)
```

相对于上面我们自己做的sum命令，这里出现的外部引用命令还是有些明显的变化，就是在命令之前多了一个`numpy.`，这其实是为了指明后面指令的出处，因为在我们的程序中出现了两个sum，如果不指明出处的话，计算机就会不知所措。另外，如果我们不小心又定义了一个sum指令，还会把外部引入的指令覆盖掉，从而引起程序出错，总起来说，这种通过外部引用的方式自动隔离同名指令的办法确实很实用，有点像我们把重名的人前面加一个家庭或地域的修饰那样，比如北京的张三、天津的张三等。但这也是有代价的，就是要额外增加一个文件。那么，有没有一个方法在一个文件内部也实现这种方法之间的**归类**和隔离呢？这就引入了另外一个词--**类**，英文名字叫class，顾名思义，就是要把不同的定义(包括变量和函数)按不同的特点和类型进行归类，避免混淆，比如我们可以定义一个叫MyMath的类。

```python
class MyMath:
  def sum(self,numbers = range(1,101)):
    sum = 0
    if type(numbers) == list or type(numbers) == range:
      for num in numbers:
        sum += num
    return sum

  def add(self,*numbers, **nameNumbers):
    sum2 = 0
    if len(nameNumbers) == 2 and 'f' in nameNumbers and 't' in nameNumbers:
        sum2 += sum(range(nameNumbers['f'],nameNumbers['t']))
    if len(numbers) > 0:
        sum2 += sum(list(numbers))

    return sum2
```

```python
mylib = MyMath()
mylib.add(1,2,3)
```

注意，为了使类内部的方法之间能够相互调用，在方法的参数中增加了self这一项，英文意思就是自我，看上去模模糊糊的意思是代表类本身，但是联系到前面关于函数的定义，我们可以知道类本身只是一个定义，如果不被调用的话，它是不会主动生成数据的，再结合类使用的方式`MyMath()`，很容易就猜想出，self代表执行MyMath()后所生成的数据对象，只不过我们没有在类的定义里明确return而已。之所以要采用统一的名字，是因为尽管结果已经存在内存里面，但我们不知道最终用户定义的引用名称会是什么，所以暂时先用self代替着，当执行`mylib = MyMath()`以后这个对象就变成mylib了，里面存储的内容是两个方法sum和add。

一般把这种归类的编程模式叫做面向对象的编程模式，除了能使程序的结构更清晰和便于管理外，还可以通过继承等机制实现代码功能的复用，避免单纯的函数复用造成的混乱局面，实际上这也是Python编程的常用模式。我们在课程之初，其实遇到了两种命令模式，一种是填空式的函数调用，一种是非填空的语句模式，二者的风格貌似有点不太统一，对于初学者可能觉得有点无章可循，实际我们看那些非填空的指令大多数是一句话说不清楚的指令，我们把这种指令叫做复合指令，如果用函数的方式来表示就会变得非常复杂，但其实程序界也有洁癖的语言叫做Lisp，它就坚持用填空的统一风格，甚至连函数名都一并写进去，比如一个for...in语句写起来大略是这个样子：
(for (num in nums) (print num))
虽然这种括号复括号的方式有些极端，但风格不统一看起来终究有些不舒服，解决的办法就是面向对象。比如前面我们用numpy表示1到100的平方数，采用的是`numpy.arange(1,101)**2`的形式，而没有采用中间变量和循环语句，这样整个风格看起来似乎就比较统一了，里面的机制就是`numpy.arange(1,101)`返回的是一个所谓的numpy对象，而不是传统意义上的list或range，而且这个对象中定义了`**2`所对应的方法，比如我们也可以用power这个词儿来重写上面的语句`numpy.arange(1,101).power(2)`，这样看起来是不是就顺溜多了，用人话来说就是把1到100的数每一项都做一遍乘方运算。为了实现这一点，需要在numpy中设立一个保存数值的属性，比如self.value，然后每次调用方法其实都是在修改这个属性，然后将修改后的numpy再吐出来，等待后续处理。

这给我们的启示是，如果你想要Python的语句风格像Processing那样大致统一，那么最好就用面向对象的方式把所有风格不统一的语句重新实现一遍、封装起来。

虽然如此，现实生活中的应用不仅要计算，更需要选择和判断，比如超市卖东西，除计算累积额度外，还要根据额度进行优惠和打折，这样就需要在基本的计算之上，增加额外的步骤对计算结果进行判断比较，然后对其进行加工修改，这个过程就叫程序，其基础是向计算机发出的一串指令，一般以语句或句子的方式呈现。这和我们日常生活中向孩子发出指令是一样的，如果只是用词和字谁也听不懂，比如“吃“、“买”，要成句才能搞清楚语义，比如你来吃饭、你去打瓶酱油来，对计算机也一样，必须提供的信息完整，它才能理解你到底想干什么。

计算机除了算术好，记忆力强外，其实在智力上基本白痴一个，人事基本不懂，所以写程序就跟教小孩儿一样，要有耐心，要自己的想法和意愿用对方的语言表达出来，对于计算机而言，就是想办法把所有事都转化为计算和记忆这两件事，计算机就能像一个大傻子似地不惜力地去干了。最基本的，我们把构成语言的字母进行编号，就把他们转化为数字处理了。更进一步说，是把一切事物转化为计算，再把计算转化为是非相关的逻辑运算，这样计算机就能处理了。典型的例子是，我们在教电路时对进位的处理方式，计算机其实并不懂进位，但这不妨碍我们采用逻辑和空间扩展规则进行模拟，这其实就是图灵机的基本原理吧。

总之，如果没有选择和逻辑，计算机其实和一个大计算器无异，如果逻辑简单且不需要重复使用和控制，那么计算器加人工也能简单应付，怕的是要处理和判断的事物量巨大且复杂，而且需要对其处理的准确性、精确性和处理效率有较高的要求，那么就需要把人做的事情教会计算机去做，这种教的过程就是编程。

课程汇集/虚谷号内置课程目录/11.其他课程/Python快速入门教程1.0.ipynb