\documentclass{article}
\input{homework.sty}

\title{Homework 1}
\author{Austin Gill}

\begin{document}
\maketitle
\begingroup
\hypersetup{linkcolor=black}
\tableofcontents
\endgroup
\newpage

The Jupyter notebooks containing the work for each of the following problems, as well as the
\LaTeX{} source code for this document can be found at
\url{https://github.com/Notgnoshi/natural-computing/tree/master/homework/hw1}

\section{Function Optimization via Simulated Annealing}\label{prob:1}

\subsection{Statement}
Compare the effectiveness of the text's iterated hill climbing and simulated annealing algorithms
to find the max of
\[ f(x) = 2^{-2{\left(\frac{(x - 0.1)}{0.9}\right)}^2}{\big(\sin(5\pi x)\big)}^6\]
with $x\in [0,1]$. Use a real valued representation. Include a plot of the function with the
location of the max and a plot of the estimate as a function of the iteration number. How sensitive
are the algorithms to initial values?

\subsection{Method}

\subsubsection{Problem Setup and Gradient Methods}

For completeness and curiosity, I chose to attack the problem from a symbolic perspective first.
This gives an opportunity to get comfortable with the problem, and get my workflow setup correctly
because I've recently reinstalled Ubuntu and subsequently all of my commonly used Python libraries.

The python \mintinline{python}{sympy} library provides decent symbolic computation capabilities
that I have had good luck with in the past. The only real surprising implementation detail is in
the definition of the objective function.

\begin{minted}[highlightlines={7-8}]{python}
def f(x):
    """The objective function to evaluate.

    This function returns a sympy symbolic function, float, or np.ndarray
    depending on the type of the input.
    """
    # Use symbolic sine, pi if necessary.
    sin, pi = (sympy.sin, sympy.pi) if isinstance(x, sympy.Symbol) else (np.sin, np.pi)

    return 2 ** (-2 * ((x - 0.1) / 0.9) ** 2) * sin(5 * pi * x) ** 6
\end{minted}

If the given type is a symbolic variable, return a symbolic representation of the function rather
than the default numerical computation. Then this function can be given individual float values,
numpy arrays, and sympy symbols.

\begin{figure}[h]
    \centering
    % Make sure to run the notebook before this will compile.
    \includegraphics{prob1/figures/prob1-function.pdf}
    \caption{The objective function $f(x)$}\label{fig:prob1:function}
\end{figure}

The very first thing to do when dealing with optimizing a function is to plot anything you can get
your hands on, including the neighbor's cat. The objective function and its derivative are shown in
\autoref{fig:prob1:function} and \autoref{fig:prob1:derivative} respectively.

\begin{figure}[h]
    \centering
    \includegraphics{prob1/figures/prob1-derivative.pdf}
    \caption{The objective function's derivative}\label{fig:prob1:derivative}
\end{figure}

We can immediately pick out the local and global extrema by visual inspection. The maxima
periodically occur at $\frac{n}{5} - \frac{1}{10}$ with the global maximum occuring at
$\frac{1}{10}$. The minima periodically occur at $\frac{n}{5}$ with value $0$.

We can produce a symbolic version of the objective function easily with
\begin{minted}{python}
    x = sympy.Symbol('x')
    print(f(x))
\end{minted}
which displays\footnote{When coerced with \mintinline{python}{sympy.init_printing()} in a Jupyter
    notebook.}
\[2^{- 2 {\left(1.11 x - 0.11\right)}^{2}} \sin^{6}{\left (5 \pi x
        \right)}\]
Then we compute the atrocious derivative symbolically.
\begin{minted}{python}
    fp = sympy.diff(f(x), x)
    print(fp)
\end{minted}
which displays the trivial derivative\footnote{Coincidentally, the programmatically generated, and
    subsequently auto-formatted \LaTeX{} code for these functions is, I think, the ugliest thing
    I've ever seen.}
% This garbage is what sympy + my autoformatter gives. I'm not fixing it.
\[2^{- 2 \left(1.11 x - 0.11\right)^{2}} \left(- 4.938 x +
    0.4938\right) \log{\left (2 \right )} \sin^{6}{\left (5 \pi x \right )} + 30 \cdot
    2^{-2
            \left(1.11 x - 0.11\right)^{2}} \pi \sin^{5}{\left (5 \pi
        x\right)}\cos{\left(5 \pi x \right )}\]
which we can then set equal to $0$ and solve
\begin{minted}{python}
    # solve f'(x) = 0
    print(sympy.solveset(fp, x))
\end{minted}
to get $\frac{n}{5} - \frac{1}{10}$ after simplification:
% ditto
\[\left\{x \mid x \in \mathbb{C} \wedge \left(\left(- 4.938 x + 0.4938\right) \log{\left (2
            \right)} \sin{\left (5 \pi x \right )} + 30 \pi \cos{\left (5 \pi x \right )}\right)
    \sin^{5}{\left (5
        \pi x \right )} = 0 \right\} \setminus \left\{x \mid x \in \mathbb{C} \wedge 2^{2
            \left(1.11 x -
            0.11\right)^{2}} = 0 \right\}\]

It's also helpful to compare the results of your typical gradient optimization method against the
methods discussed later in this report.

\begin{minted}{python}
    # Convert the symbolic derivative to a numpy-compatable function for plotting.
    fp = sympy.lambdify([x], fp, 'numpy')

    # Minimizing -f(x) will maximize it.
    result = scipy.optimize.minimize(lambda x: -f(x), 0.19, bounds=[(0, 1)])
    print(result.x)
    result = scipy.optimize.minimize(lambda x: -f(x), 0.2, bounds=[(0, 1)])
    print(result.x)
    result = scipy.optimize.minimize(lambda x: -f(x), 0.21, bounds=[(0, 1)])
    print(result.x)
\end{minted}
\vspace{-1cm}
\begin{minted}{text}
    0.1
    0.2
    0.29953865
\end{minted}
\vspace{-1cm}

On either side of the minima at $x = 0.2$ the gradient optimizer\footnote{The optimization
    algorithm scipy chose for this function was the L-BFGS-B algorithm. I don't know anything about
    how well and under what conditions it performs, but I assume it works better than anything I
    could write by hand.} finds the next closest local maxima. This agrees with my intuitive
understanding of gradient methods; without some kind of correction, they too are susceptible to
finding local optima of oscillatory functions.

\subsubsection{Hill Climbing}

This hill climbing algorithm discussed in the book is listed in \autoref{alg:hill-climbing}.

\begin{algorithm}[h]
    % \begin{noindent}
    \begin{algorithmic}
        \Function{hill-climbing}{$f$}
            \State{Initialize $x$}
            \While{not done}\IComment{can be convergence or fixed iteration}
                \State{$x' = x + \text{perturbation}$}
                \If{$f(x') < f(x)$}\IComment{this minimizes $f(x)$}
                    \State{$x = x'$}
                \EndIf{}
            \EndWhile{}
            \State\Return{$x$}
        \EndFunction{}
    \end{algorithmic}
    % \end{noindent}
    \caption{The hill climbing algorithm}\label{alg:hill-climbing}
\end{algorithm}

This can easily be implemented in Python as

\begin{minted}{python}
    def hill_climbing(func, bounds, sigma, iters):
        """Minimize the given function using the Hill Climbing algorithm.

        :param func: The function to minimize.
        :param bounds: The lower and upper bounds on the feasible region.
        :param sigma: The standard deviation to use when perturbing the current guess.
        :param iters: The number of iterations to run the hill climbing algorithm for.
        :returns: The path of points visited from the initial guess to the final solution.
        """
        path = np.zeros(iters)
        x0 = np.random.uniform(*bounds)
        for i in range(iters):
            xp = perturb(x0, bounds, sigma)
            if func(xp) < func(x0):
                x0 = xp
            path[i] = x0
        return path
\end{minted}

with \mintinline{python}{perturb(x, bounds, sigma)} defined as

\begin{minted}{python}
    def perturb(x, bounds, sigma):
        """Perturb the given value by adding zero-mean white noise.

        :param x: The value to perturb.
        :param bounds: The lower and upper bounds on the feasible region.
        :param sigma: The standard deviation to use when adding white noise.
        """
        m, M = bounds
        xp = x + np.random.normal(scale=sigma)
        while xp >= M or xp <= m:
            xp = x + np.random.normal(scale=sigma)
        return xp
\end{minted}

Note that my implementation of \mintinline{python}{perturb(x)} will not perturb the given point
outside of the feasible region for the problem.

One of the easiest ways to improve the quality of the results from the hill climbing algorithm is
to run it multiple times and take the best result. This is the iterated hill climbing algorithm
from the book, listed in \autoref{alg:iterated-hill-climbing}.

\begin{algorithm}[h]
    % \begin{noindent}
    \begin{algorithmic}
        \Function{iterated-hill-climbing}{$f, n$}
            \State{$solutions = map\big(\Call{hill-climbing}{f}, \{1, \dots, n\}\big)$}\IComment{A trivially parallelizable operation}
            \State\Return{The best solution}
        \EndFunction{}
    \end{algorithmic}
    % \end{noindent}
    \caption{The iterated hill climbing algorithm}\label{alg:iterated-hill-climbing}
\end{algorithm}

This algorithm can easily (even with parallelization) be implemented in Python as follows
\begin{minted}[highlightlines={18}]{python}
import itertools
import multiprocessing
# I wonder why it's called dummy?
from multiprocessing.dummy import Pool as ThreadPool

def iterated_hill_climbing(func, bounds, sigma, inner_iters, iters):
    """Repeatedly climb the hill to find the less-local extremum.

    :param func: The function to minimize.
    :param bounds: The lower and upper bounds on the feasible region.
    :param sigma: The standard deviation to use when perturbing the current guess.
    :param inner_iters: The number of iterations to use for each run of the algorithm.
    :param iters: The number of times to run the algorithm.
    :returns: An array of solutions from each run, sorted by their fitness.
    """
    pool = ThreadPool(multiprocessing.cpu_count())
    # starmap consumes the given iterable in parallel until it is exhausted, collecting the results.
    results = pool.starmap(hill_climbing, itertools.repeat((func, bounds, sigma, inner_iters), times=iters))
    # Each result is a full path, not the optimal value
    optimums = [r[-1] for r in results]
    # Sort the optimums by their fitness.
    optimums.sort(key=func)
    return np.array(optimums)
\end{minted}
This implementation runs the desired number of iterations in parallel using as many threads as you
have processors before collecting all of the results and sorting the optimums by the objective
function.

\subsubsection{Simulated Annealing}

The simulated annealing algorithm from the book (listed in \autoref{alg:simulated-annealing}) is
far more interesting. Similar to the hill climbing algorithm, it too perturbs the current solution
in order to move around the feasible region. However, it not only accepts solutions that improve
the current solution's fitness, it accepts solutions that \textit{decrease} the current solution's
fitness with probability decreasing with the system temperature.

The simulated annealing algorithm accepts such solutions in an attempt to move out of local optima.
But in order to converge, and to a more reasonable value, it does so with a decreasing probability.

\begin{algorithm}[H]
    % \begin{noindent}
    \begin{algorithmic}
        \Function{simulated-annealing}{$f$}
            \State{Initialize $T$}
            \State{Initialize $x$}
            \While{not done}\IComment{prefer convergence over fixed iterations}
                \State{$x' = x + \text{perturbation}$ }
                \If{$\displaystyle rand(0, 1) < e^{\frac{\left(f(x) - f(x')\right)}{T}}$}
                    \State{$x = x'$}
                \EndIf{}
                \State{Update $T$}
            \EndWhile{}
            \State\Return{$x$}
        \EndFunction{}
    \end{algorithmic}
    % \end{noindent}
    \caption{The simulated annealing algorithm}\label{alg:simulated-annealing}
\end{algorithm}

\autoref{alg:simulated-annealing} too, isn't difficult to implement\footnote{Although it can be a
    bitch to tune.} in Python. The difficulty lies in the choice of the perturbation method and the
cooling schedule. I chose the same \mintinline{python}{perturb(x, bounds, sigma)} from above,
although as mentioned later, picking the proper standard deviation required significant trial and
error.

\begin{minted}{python}
    def simulated_annealing(func, bounds, sigma, temp, cooling_factor):
        """Use simulated annealing to optimize the given function.

        :param func: The function to minimize.
        :param bounds: The lower and upper bounds on the feasible region.
        :param sigma: The standard deviation to use when perturbing the current guess.
        :param temp: The initial temperature of the system.
        :param cooling_factor: How quickly the system should cool.
        :returns: An array of points *accepted* by the random condition.
        """
        # Pick a random starting point somewhere in the domain.
        x0 = np.random.uniform(*bounds)
        current = func(x0)
        path = []
        while temp > 0.001:
            xp = perturb(x0, bounds, sigma)
            potential = func(xp)
            if potential < current or np.random.random() < np.exp((current - potential) / temp):
                x0 = xp
                path.append(x0)
                current = potential
            temp *= 1 - cooling_factor

        return np.array(path)
\end{minted}

Note that my implementation does not return an array of points at each iteration of the algorithm.
It instead returns an array of points that the algorithm has accepted, whether the point improved
the current solution's fitness or not. Also, note that the number of iterations the simulated
annealing algorithm takes to converge depends on the initial temperature of the system and the
cooling schedule. Contrast this with the hill climbing algorithm, which terminates after some
predetermined number of steps.

Although not required, I implemented an iterated version of the simulated annealing algorithm,
echoing \autoref{alg:iterated-hill-climbing}.

\begin{algorithm}[H]
    % \begin{noindent}
    \begin{algorithmic}
        \Function{iterated-simulated-annealing}{$f, n$}
            \State{$solutions = map\big(\Call{simulated-annealing}{f}, \{1, \dots, n\}\big)$}\IComment{A trivially parallelizable operation}
            \State\Return{The best solution}
        \EndFunction{}
    \end{algorithmic}
    % \end{noindent}
    \caption{The iterated simulated annealing algorithm}\label{alg:iterated-simulated-annealing}
\end{algorithm}

The algorithms are essentially identical, as are their implementations.

\begin{minted}[highlightlines={14}]{python}
    def iterated_simulated_annealing(func, bounds, sigma, temp, cooling_factor, iters):
        """Repeatedly run simulated annealing to improve the quality of the results.

        :param func: The function to minimize.
        :param bounds: The lower and upper bounds on the feasible region.
        :param sigma: The standard deviation to use when perturbing the current guess.
        :param temp: The initial temperature of the system.
        :param cooling_factor: How quickly the system should cool.
        :param iters: The number of times to run the algorithm.
        :returns: An array of solutions from each run, sorted by their fitness.
        """
        pool = ThreadPool(multiprocessing.cpu_count())
        # starmap consumes the given iterable in parallel until it is exhausted, collecting the results.
        results = pool.starmap(simulated_annealing, itertools.repeat((func, bounds, sigma, temp, cooling_factor), times=iters))
        # Each result is a full path, not the optimal value
        optimums = [r[-1] for r in results]
        # Sort the optimums by their fitness.
        optimums.sort(key=func)
        return np.array(optimums)
\end{minted}

\subsection{Results}

\subsubsection{Hill Climbing}

Recall that the implementation of \autoref{alg:hill-climbing} picks a random starting point
somewhere inside the feasible region. There is also randomness in the perturbation of the current
solution as the algorithm progresses. This means one cannot run the algorithm once to get a sense
of how well it performs. Thus I ran the hill climbing algorithm a great many times\footnote{Three.
    I ran it three times.} and plotted the results, summarized in
\autoref{fig:prob1:hill-climbing-results}.

\begin{figure}[H]
    \centering
    \includegraphics[width=\textwidth]{prob1/figures/prob1-hill-climbing-results.pdf}
    \caption{Results from successive runs of the hill climbing
        algorithm}\label{fig:prob1:hill-climbing-results}
\end{figure}

In the left column, there is the accepted solution graphed as a function of the iteration number.
In the right column, there is a plot of the accepted solutions overlayed on top of the objective
value.

Interestingly, the plot of the accepted solution as a function of the iteration number was often a
very ``stairsteppy'' function, with riser height of $0.1$. That is, the accepted solutions very
often seem to jump from peak to peak, without hitting the sides of the peaks. I believe this is a
figment of my imagination, and is a consequence of the combination of the hill climbing algorithm
only accepting values that improve the current solution and the nature of the function being
optimized --- with steep, narrow peaks.

Since the iterated hill climbing algorithm in \autoref{alg:iterated-hill-climbing} takes the best
solution out of $n$ runs of the algorithm, it's useful to plot the results of all $n$ runs on the
same plot. \autoref{fig:prob1:iterated-hill-climbing-solutions} shows the results of the iterated
hill climbing algorithm.

\begin{figure}[h]
    \centering
    \includegraphics{prob1/figures/prob1-hill-climbing-solution.pdf}
    \caption{Solutions from multiple runs of the hill climbing
        algorithm}\label{fig:prob1:iterated-hill-climbing-solutions}
\end{figure}

Notice that every near\footnote{It's bad form to save a binary file in a Git repository, so I have
    to generate these plots every time I build this document from scratch. Since the algorithms are
    random, this results in different plots every time --- so the result that makes it into the
    final version may not be what I'm looking at now. There are fixes for this problem, but none
    that I care to implement.} every peak is a solution returned by at least one iteration of the
hill climbing algorithm. Also note that the solutions cluster around the peaks, rather than
converge on the actual peaks themselves. This is due to the steep nature of each peak; a small
change in $x$ results in a fairly large change in $f(x)$. This means that random perturbations of
$x$ are more likely to land below the current solution than above.

\subsubsection{Simulated Annealing}

After a considerable\footnote{As an aside, it's very frustrating to not know if you're being an
    idiot and implemented an algorithm incorrectly, or if you're being an idiot and picked
    unreasonable metaparameters.} amount of tuning, I was able to get simulated annealing to yield
better results than the hill climbing algorithm. The tunable metaparameters that I tweaked were the
standard deviation of the perturbation function, the initial temperature, and the cooling factor.

Picking an appropriate standard deviation is important because the scale of the domain is small
enough that noise following a standard normal distribution is \textit{too} noisy --- resulting in
convergence to a random peak \textit{despite exploring the entire damn feasible region}. Picking
the cooling factor is important because we need to give the algorithm enough time to get its
rebellious teenager phase out of the way before it starts settling down to a single value.

The initial temperature of the system is interesting. After finding good values for the
perturbation\footnote{Fun fact: I've had enough to drink that I first spelled this as
    ``preturnabtion'', which is bad enough that my spell-checking extension failed to detect as an
    error.} standard deviation and the cooling factor, I attempted to use temperatures of the same
order of magnitude as the example solution of the Travelling Salesman Problem. Such temperatures
\textit{did} yield correct solutions, but only after an extreme amount of exploration; it took
longer to display the plot than it did to generate the solutions due to the amount of data
involved.

\begin{figure}[H]
    \centering
    \includegraphics[width=\textwidth]{prob1/figures/prob1-simulated-annealing-results.pdf}
    \caption{Simulated annealing results after an inordinate amount of
        tuning}\label{fig:prob1:simulated-annealing-results}
\end{figure}

After figuring out appropriate values for the perturbation standard deviation and the cooling
schedule, I was able to observe that modifying the initial temperature did not seem to modify the
quality of the solution, or the value the iterated algorithm converged to, but \textit{did} impact
the rate of convergence. I attempted values ranging from $8000$ to $0.002$, all of which eventually
converged to the same value.

I eventually settled on an initial temperature of $0.01$ even though $0.002$ worked just as well,
because an iterative algorithm that converges after only $15$ iterations isn't very exciting.

\begin{figure}[h]
    \centering
    \includegraphics{prob1/figures/prob1-simulated-annealing-solutions.pdf}
    \caption{Iterated simulated annealing results}\label{fig:prob1:simulated-annealing-solutions}
\end{figure}

Observe from \autoref{fig:prob1:simulated-annealing-solutions} that, with the proper
metaparameters, the simulated annealing algorithm has \textit{much} better results than the hill
climbing results summarized in \autoref{fig:prob1:iterated-hill-climbing-solutions}. Each of the
runs of the simulated annealing algorithm results in the same peak marked as the solution. Further,
note that the solutions \textit{seem to be the same point}, as opposed to the fairly loose clusters
of the hill climbing algorithm. This is a consequence of the convergence condition of the simulated
annealing algorithm, versus the fixed number of iterations of the hill climbing algorithm.

In summary, I have several observations.
\begin{itemize}
    \item The simulated annealing algorithm converges closer to the actual peaks of the objective
          function.
    \item Simulated annealing required a considerable amount of tuning.
    \item The tunable metaparameters that affected the correctness of the solution the most were
          the standard deviation of the perturbation, and the cooling factor. The parameter that
          affected speed of convergence the most was the initial temperature.
    \item The solutions from repeated runs of the simulated annealing algorithm clustered much more
          tightly than the solutions from repeated runs of the hill climbing algorithm.
    \item When considering the standard deviation of the perturbation, you want enough noise to
          explore the feasible region, yet not so cuh that you jump all over the place.
    \item Temperatures ranging from $8000$ to $0.002$ (because 0.001 is the convergence criterion)
          worked well, but smaller temperatures converged much faster.
\end{itemize}

\section{The TSP with Mutation Only}\label{prob:2}

\subsection{Statement}
In lecture we addressed the Traveling Salesman Problem using Simulated Annealing. To speed up
convergence and increase the odds of finding the global extremal, it makes sense to try an
evolutionary algorithm. The mutation operator can be adapted from the SA algorithm. Skip
recombination in this problem. Write an evolutionary algorithm to solve the TSP as generated in the
sample program. Compare deterministic and stochastic selection operators.

\subsection{Method}
There are a number of decisions to make when implementing the standard evolutionary algorithm
listed in \autoref{alg:standard-ea}. There are a great many ways to perform each of the steps
(Initialization, Recombination, Mutation, and Selection, as well as the stopping criterion), and
picking the \textit{right} method constitutes the difficulty of the problem.

\begin{algorithm}[h]
    % \begin{noindent}
    \begin{algorithmic}
        \Function{standard-ea}{}
            \State{Initialize $population$}
            \While{not done}
                \State{Recombine $population$}
                \State{Mutate $population$}
                \State{Select $population$}
            \EndWhile{}
            \State\Return{$population$}
        \EndFunction{}
    \end{algorithmic}
    % \end{noindent}
    \caption{The standard evolutionary algorithm}\label{alg:standard-ea}
\end{algorithm}

In this problem, there will be no recombination of the population, only mutation and selection.
Recombination will be done in \autoref{prob:3}, however, so it is useful to keep recombination in
mind when implementing the solution for this problem.

\subsubsection{Solution Representation}
Of particular importance is the choice of solution representation. The representations for a
problem make an extreme effect on the performance, ease, and usability of a solution. In many cases
even, your choice of data structure makes the proper algorithm for solving the problem obvious,
while an improper choice makes implementing the solution tedious, nonintuitive, and ill-performant.

For this problem, the most obvious representation for a solution is an ordered path
\[\lbrack C_1, C_2, \dots C_n \rbrack \]
of cities. There are other representations, however. As a reference,~\cite{tsp_ea} enumerates a
great deal of variations for the representations of solutions, mutation operators, and
recombination operators. Briefly, the solution representation options that~\cite{tsp_ea} lists are
listed\footnote{I'm running out of nested \LaTeX{} subsections!} below.

\paragraph{Bitstring}
The bitstring representation is the most common solution representation in traditional evolutionary
and genetic algorithm literature. In this representation, with a problem with $n$ cities, a
particular city is encoded as a bitstring of length $\ceil*{\log_2 n}$, and a path of cities (a
solution) as a bitstring of $n\ceil*{\log_2 n}$ concatenated cities.

However, this representation provides some difficulty when implementing \autoref{alg:standard-ea}.
Namely, the mutation and recombination operators must be more sophisticated than simple bitwise
operations because some $\ceil*{\log_2 n}$ long bitstrings might not represent a valid city, and
some $n\ceil*{\log_2 n}$ long bitstrings might not represent a valid solution to the problem. There
are some solutions to these problems, which~\cite{tsp_ea} mentions, but they are more tedious than
I would prefer.

\paragraph{Matrix}
The paper~\cite{tsp_ea} lists two different binary matrix representations of a solution.
\begin{enumerate}
    \item Represent an individual as a matrix $M = \{m_{ij}\}$ where $m_{ij} = 1$ if and only if
          the city $i$ is visited before the city $j$ in the represented tour. The paper lists the
          following properties that hold for a valid tour represented in this manner.
          \begin{enumerate}
              \item $\displaystyle \sum_i^n \sum_j^n m_{ij} = \frac{n(n - 1)}{2}$
              \item $\displaystyle m_{ii} = 0$
              \item $\displaystyle (m_{ij} = 1) \wedge (m_{jk} = 1) \Rightarrow m_{ik} = 1$
          \end{enumerate}
    \item Represent an individual as a matrix $M = \{m_{ij}\}$ where $m_{ij} = 1$ if and only if
          city $j$ is visited immediately after city $i$. That is, a valid tour is represented by a
          matrix with exactly one $1$ in every row and in every column.
\end{enumerate}

\paragraph{Adjacency List}
An uncommon representation is that of an adjacency list, where city $j$ occurs in the $i$th
position of the list if and only if the tour leads from city $i$ to city $j$. However, the proposed
mutation and recombination operators good subpaths in parents, so this representation is not widely
used.

\paragraph{Ordered Path}
As mentioned above, this is the most obvious representation, and the one~\cite{tsp_ea} exclusively
deals with. Here, a path is represented as an ordered list
\[C_1, C_2, \dots C_n \]
where $C_i$ occurs at the $j$th position if it is the $j$th city to be visited.

This representation yields itself nicely to the fitness evaluation of individuals in your
population, and is useful as the final representation of a returned solution. However, the
classical mutation and recombination operators typically performed on bitstrings will not work on
this representation because they can (will) result in invalid paths.

This will be the representation I use, and is thus the only representation I attempt to define
mutation and (in \autoref{prob:3}) recombination operators on. In this representation, we can
generate a starting collection of cities with

\begin{minted}{python}
    def generate_cities(n, scale=100):
        """Generates an array of cities of the given size.

        Pick random coordinates in the `scale`x`scale` grid uniformly.

        :param n: The size of the individual to generate.
        :param scale: How much to scale the individual's coordinates by.
        """
        # Scale up the uniform values from [0, 1].
        return np.random.rand(n, 2) * scale
\end{minted}

and a random population of different orderings for a collection of cities with

\begin{minted}{python}
    def generate_population(cities, size, scale=100):
        """Generate a population of individuals with the given size.

        Each individual is an array of indices into the given cities array.

        :param cities: An unsorted array of city locations.
        :param size: The number of individuals to generate.
        :param scale: How much to scale the individual's coordinates by.
        """
        n = len(cities)
        population = np.zeros((size, n), dtype=int)
        for i in range(size):
            individual = np.arange(n, dtype=int)
            np.random.shuffle(individual)
            population[i] = individual
        return population
\end{minted}

Note that in this representation, an individual is a list of indices into the
\mintinline{python}{cities} array. This is to prevent repeatedly duplicating the array of the same
cities in different orders.

The \mintinline{python}{generate_individual()} function generates a single individual as shown in
\autoref{fig:prob2:random-individual}.

\begin{figure}[h]
    \centering
    \includegraphics{prob2/figures/prob2-random-individual.pdf}
    \caption{A randomly generated individual}\label{fig:prob2:random-individual}
\end{figure}

Note that each city is an $(x, y)$ pair so that we can more easily compute the distance between
them without explicitly saving that information in a graph datastructure.

\subsubsection{Fitness}
With this representation as an individual as an ordered list of ordered pairs, an individual's
solution is given as the sum of the pairwise distances between cities. To implement this, it is
helpful to use the
\href{https://docs.python.org/3/library/itertools.html#itertools-recipes}{itertools pairwise
    recipe} for iterating over a sequence in pairs.
\begin{minted}{python}
    import itertools

    def pairwise(iterable):
        """Iterate over the given iterable in pairs.

        pairwise([1, 2, 3, 4]) -> (1, 2), (2, 3), (3, 4)
        """
        a, b = itertools.tee(iterable)
        # Advance b one step
        next(b, None)
        return zip(a, b)
\end{minted}
Then the fitness function for an individual can be written succinctly as
\begin{minted}{python}
    def fitness(cities, path):
        """Evaluate the fitness of the given path.

        Compute the Euclidean distance between every pair of cities in the path
        and add them together.

        :param cities: The array of cities through which to compute a path.
        :param path: The path through the given cities to compute the fitness for.
        :returns: The fitness of the individual.
        """
        individual = cities[path]
        return 1 / sum(np.linalg.norm(c1 - c2) for c1, c2 in pairwise(individual))
\end{minted}

Note that the sum is inverted, because we want to \textit{minimize} the fitness. I also played
around with simply negating the fitness, which worked quite well in some cases. However, in the
stochastic selection discussed below, I wanted to further separate the good individuals from the
poor individuals. Inverting the fitness seemed to have a good result.\footnote{What happens if you
    leave the fitness alone, without negating or inverting? I couldn't even \textit{begin} to
    guess, although I'm sure a naive student might spend hours and hours wondering why their
    implementation was giving them the exact opposite of what they wanted.}

\subsubsection{Mutation Methods}\label{sec:mutation-methods}
There are a number of possible mutation operators that will work on the chosen solution
representation. Note that these implementations are sometimes not as simple as one would think,
because an array access (\mintinline{python}{v = a[i]}) returns a \textit{view} of the original
array. And when you modify a view, it modifies the original array. This has particular importance
when dealing with multidimensional arrays because the returned items are now \textit{views} rather
than direct scalar values.

\paragraph{Swap Mutation} This mutation randomly swaps two cities in the tour.
\begin{minted}{python}
    def swap_mutation(path):
        """Swap two random cities in the path.

        Returns a new mutated copy of the given array.
        """
        # Arrays are (kind of) passed by reference in Python.
        x = np.copy(path)
        # Generate two valid indices to swap.
        i = np.random.randint(0, len(x) - 2)
        j = np.random.randint(i, len(x) - 1)
        # array indexing returns views, not copies
        temp = np.copy(x[i])
        x[i] = x[j]
        x[j] = temp

        return x
\end{minted}

\paragraph{Insertion Mutation} This mutation is similar to a swap. It picks a random city, removes
it from the list, and inserts it at some randomly selected position.
\begin{minted}{python}
    def insertion_mutation(path):
        """Insert a random value in the list somewhere else in the list.

        Returns a new mutated copy of the given array.
        """
        i = np.random.randint(0, len(path) - 2)
        j = np.random.randint(i, len(path) - 1)
        # Delete the element at index j and insert it before index i.
        temp = np.delete(path, j, axis=0)
        temp = np.insert(temp, i, path[j], axis=0)

        return temp
\end{minted}

\paragraph{Displacement Mutation} This is a natural extension of the insertion mutation. Rather
than removing and inserting a single city, remove and insert an entire subpath.
\begin{minted}{python}
    def displacement_mutation(path):
        """Inserts a random subarray in the list somewhere else.

        Returns a new mutated copy of the given array.
        """
        # Pick a random subarray
        i = np.random.randint(0, len(path) - 2)
        j = np.random.randint(i, len(path) - 1)
        subarray = path[i:j]
        # Delete the given subarray
        tmp = np.delete(path, range(i, j), axis=0)
        k = np.random.randint(0, len(tmp) - 1)

        return np.insert(tmp, k, subarray, axis=0)
\end{minted}

\paragraph{Shuffle Mutation} This mutation is not used much, because it can destroy good genetic
information in a parent. It picks a random subpath in a solution and shuffles it.
\begin{minted}{python}
    def shuffle_mutation(path):
        """Shuffles a random subarray in the given path.

        Returns a new mutated copy of the given array.
        """
        # Pick a random subarray
        i = np.random.randint(0, len(path) - 2)
        j = np.random.randint(i, len(path) - 1)
        x = np.copy(path)
        np.random.shuffle(x[i:j])

        return x
\end{minted}

\paragraph{Inversion Mutation} This is the mutation used in the simulated annealing example shown
in class. It picks a random subpath in a solution, and reverses it. This is a useful operator
because it breaks only two links between cities, and leaves the rest intact. All of the other
mutations can be implemented as a sequence of inversions. A proof of this remark is given in
\autoref{app:proof}. This mutation can also be extended to act like the displacement mutation where
a random subpath is removed, inverted, and reinserted somewhere else in the tour.
\begin{minted}{python}
    def inversion_mutation(path):
        """Inverts a random subarray in the given path.

        Returns a new mutated copy of the given array.
        """
        i = np.random.randint(0, len(path) - 2)
        j = np.random.randint(i, len(path) - 1)
        x = np.copy(path)
        # Invert the subarray.
        x[i:j] = x[i:j][::-1]

        return x
\end{minted}

Given these mutation methods, I define the \mintinline{python}{mutate()} function for ease of use.

\begin{minted}{python}
    def mutate(path, method='inversion'):
        """Mutate the given individual via the given method.

        Returns a new mutated copy of the given array.

        :param method: One of 'swap', 'insertion', 'displacement',
        'shuffle', or 'inversion'. Defaults to 'inversion'.
        """
        methods = {
            'swap': swap_mutation,
            'insertion': insertion_mutation,
            'displacement': displacement_mutation,
            'shuffle': shuffle_mutation,
            'inversion': inversion_mutation,
        }
        return methods[method](path)
\end{minted}

\subsubsection{Selection Methods}
In my implementation, I will deal with two different selection methods: stochastic and
deterministic. The stochastic selection will rank the population according to their fitness and
sample the sorted population by some probability distribution where the probability of being chosen
is directly proportional to an individual's fitness. The deterministic method will rank the
population by their fitness and select the top $n$ to survive.

We can implement deterministic selection by
\begin{minted}{python}
    def deterministic_selection(population, size, func, cities):
        """Deterministically select the most fit from the given population.

        Use the given fitness function to rank the population, then pick
        the next `size` of the population to move on. This assumes that,
        for a problem without recombination, the mutated individuals have
        been mixed in with the original population.

        :param population: The population to cull.
        :param size: The desired size of the population.
        :param func: The fitness function to rank the population by.
        :param cities: The array of city locations.
        :returns: The culled population, sorted upwards in increasing fitness.
        """
        fitnesses = np.array([func(cities, p) for p in population])
        indices = np.argsort(fitnesses, axis=0)
        euthanize = len(population) - size
        return population[indices][euthanize:]
\end{minted}
stochastic selection by
\begin{minted}{python}
    def stochastic_selection(population, size, func, cities):
        """Randomly select the most fit from the given population.

        Select without replacement an individual with probability
        proportional to its fitness.

        :param population: The population to cull.
        :param size: The desired size of the population.
        :param func: The fitness function to rank the population by.
        :param cities: The array of city locations.
        :returns: The culled population, unsorted.
        """
        fitnesses = np.array([func(cities, p) for p in population])
        probabilities = fitnesses / np.sum(fitnesses)
        survivors = np.random.choice(len(population), size, replace=False, p=probabilities)
        return population[survivors]
\end{minted}
and finally, a nice helper function
\begin{minted}{python}
    def select(population, size, func, cities, method='deterministic'):
        """Select the `size` most fit from the given population.

        :param population: The population to cull.
        :param size: The desired size of the population.
        :param func: The fitness function to rank the population by.
        :param cities: The array of city locations.
        :param method: One of 'deterministic' or 'stochastic'.
        :returns: The culled population, in arbitrary order.
        """
        methods = {
            'stochastic': stochastic_selection,
            'deterministic': deterministic_selection,
        }
        return methods[method](population, size, func, cities)
\end{minted}

\subsubsection{The Evolutionary Algorithm}
With all of the above snippets defined, implementing the evolutionary algorithm listed in
\autoref{alg:standard-ea} (without recombination) can be done simply as

\begin{minted}{python}
    def simple_ea(cities, size, func, iters, mutation='inversion', selection='deterministic'):
        """Run the standard evolutionary algorithm to solve the TSP.

        This implementation does not use recombination.

        :param cities: The array of city locations.
        :param size: The population size to use.
        :param func: The fitness function to use.
        :param iters: The number of iterations (generations) to run.
        :param mutation: The type of mutation to use. One of 'swap', 'insertion',
        'displacement', 'shuffle', or 'inversion'.
        :param selection: The type of selection to use. One of 'deterministic',
        or 'stochastic'.
        """
        n = len(cities)
        population = generate_population(cities, size)
        best_fitnesses = np.zeros(iters)
        best_individuals = np.zeros((iters, n), dtype=int)
        for i in range(iters):
            # Do not recombine population.
            mutations = np.array([mutate(p, method=mutation) for p in population], dtype=int)
            # This is a $(\mu + \lambda)$ selection.
            combined = np.concatenate((population, mutations))
            population = select(combined, size, func, cities, method=selection)
            fitnesses = np.array([func(cities, p) for p in population])
            # Record the current best individual
            best = fitnesses.argmax()
            best_fitnesses[i] = fitnesses[best]
            best_individuals[i] = population[best]

        return best_fitnesses, best_individuals
\end{minted}

Note that this implementation does not have a convergence criterion. It simply runs for the
specified number of iterations.

\subsubsection{Simulated Annealing}
Since we talked about it in class, and we essentially already have the code for this problem, I
went ahead and implemented simulated annealing by tweaking the example given in class to satisfy my
sensibilities.

\begin{minted}{python}
    def evaluate(tour):
        """Evaluate the length of the given tour.

        Compute the Euclidean distance between every pair of cities in the tour
        and add them together.

        :param tour: An array of cities, where each city is n (x, y) pair.
        """
        return sum(np.linalg.norm(c1 - c2) for c1, c2 in pairwise(tour))

    def perturb(x):
        """Perturb the given array.

        Perform a sublist inversion in the interior of the given array.

        :param x: The array to perturb. Is not modified.
        """
        # Compute two random indices, avoiding the endpoints.
        i = np.random.randint(0, len(x) - 2)
        j = np.random.randint(i, len(x) - 1)
        # Produce a copy of the given array and invert a random sublist inside.
        y = np.copy(x)
        y[i:j] = y[i:j][::-1]
        return y

    def simulated_annealing(cities, temperature=800, cooling_factor=0.001):
        """Run the simulated annealing algorithm to solve the TSP.

        :param cities: An array of (x, y) city coordinates.
        :param temperature: The starting temperature of the system, defaults to 800
        :param cooling_factor: How quickly to cool the system, defaults to 0.001
        """
        current = evaluate(cities)
        energies = [current]
        while temperature > 0.001:
            new_solution = perturb(cities)
            energy = evaluate(new_solution)
            if np.random.random() < np.exp((current - energy) / temperature)
                cities = new_solution
                current = energy
                energies.append(current)
            temperature *= 1 - cooling_factor
        return cities, np.array(energies)
\end{minted}

Note that this implementation uses the uninverted fitness function, so in order to compare apples
to pears and grapefruit to plums, the returned array of fitnesses must be inverted as well.

\subsection{Results}
In \autoref{prob:1}, each run of the hill climbing and simulated annealing functions generated the
initial random starting point. Here, however, since we're comparing the results of three different
algorithms, I generated the random cities ahead of time.

\begin{figure}[h]
    \centering
    \includegraphics{prob2/figures/prob2-city-locations.pdf}
    \caption{The city locations to use for the TSP}\label{fig:prob2:city-locations}
\end{figure}

Then I chose to use only the inversion mutation and compare stochastic and deterministic selection
methods against the solution from the simulated annealing algorithm.

\begin{figure}[h]
    \centering
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob2/figures/prob2-fitness-stochastic.pdf}
        \caption{Fitness over time}\label{fig:prob2:stochastic-fitness}
    \end{subfigure}
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob2/figures/prob2-best-stochastic.pdf}
        \caption{The best solution}\label{fig:prob2:stochastic-best}
    \end{subfigure}
    \caption{The stochastic selection operator results}\label{fig:prob2:stochastic-selection}
\end{figure}

Notice in \autoref{fig:prob2:stochastic-fitness} that the fitness of the best individual in the
population over time has a lot of variability when using stochastic selection. I attempted several
variations on the stochastic selection, and I saw results similar to this in every case. Often the
best solution was the very first one attempted, and no solution past that point improved on it. In
other cases the best fitness show no overall trend upwards or downwards.

Moving from simply negating the fitness function to \textit{minimize} the tour length to leaving it
positive and inverting it seemed to have the best results. However poor results slightly improved
are still poor results.

Changing the \mintinline{python}{numpy.random.choice} function to sample with and without
replacement did not seem to have a substantial impact on the end solution, so I chose to sample
without replacement because I'm cruel and merciless.

To fix this behavior, I think the simple implementation
\begin{minted}{python}
    def stochastic_selection(population, size, func, cities):
        """Randomly select the most fit from the given population.

        Select without replacement an individual with probability
        proportional to its fitness.

        :param population: The population to cull.
        :param size: The desired size of the population.
        :param func: The fitness function to rank the population by.
        :param cities: The array of city locations.
        :returns: The culled population, unsorted.
        """
        fitnesses = np.array([func(cities, p) for p in population])
        probabilities = fitnesses / np.sum(fitnesses)
        survivors = np.random.choice(len(population), size, replace=False, p=probabilities)
        return population[survivors]
\end{minted}
should be modified to not kill the very best individuals, or should use a probability distribution
more heavily weighted towards the top than a uniform distribution weighted by an individual's
fitness. Another option would be to decrease the randomness in the selection as time progresses, as
does simulated annealing. This, combined with some kind of convergence criterion would be the best
option, but more difficult to implement than what I'm willing to tackle with only half of my
homework done and the deadline quickly approaching.

\begin{figure}[h]
    \centering
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob2/figures/prob2-fitness-deterministic.pdf}
        \caption{Fitness over time}\label{fig:prob2:deterministic-fitness}
    \end{subfigure}
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob2/figures/prob2-best-deterministic.pdf}
        \caption{The best solution}\label{fig:prob2:deterministic-best}
    \end{subfigure}
    \caption{The deterministic selection operator results}\label{fig:prob2:deterministic-selection}
\end{figure}

Notice from \autoref{fig:prob2:deterministic-fitness} that the fitness of the best individual over
time, when using deterministic selection, grows steadily before tapering off. Also notice that it
tapers off at a value almost double that of the highest peak of
\autoref{fig:prob2:stochastic-fitness}. We can plainly see that the best individual found, shown in
\autoref{fig:prob2:deterministic-best}, is a much more reasonable solution than that of stochastic
selection displayed in \autoref{fig:prob2:stochastic-best}.

However, there are still improvements that can be picked out visually.\footnote{Again, each
    run produces different figures, so this statement was true when I wrote it.} We might be able
to improve on this solution method by using a different mutation operator, but none of the ones
listed in \autoref{sec:mutation-methods} yielded any better results.

\begin{figure}[h]
    \centering
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob2/figures/prob2-simulated-annealing-fitness.pdf}
        \caption{}\label{fig:prob2:simulated-annealing-fitness}
    \end{subfigure}
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob2/figures/prob2-simulated-annealing.pdf}
        \caption{}\label{fig:prob2:simulated-annealing-best}
    \end{subfigure}
    \caption{The simulated annealing results}\label{fig:prob2:simulated-annealing}
\end{figure}

Simulated annealing produced results similar in quality to the evolutionary algorithm, with
deterministic selection, and much better results than with the stochastic selection operator.
Further, the simulated annealing algorithm terminates \textit{much} faster than the evolutionary
algorithm. This is expected, due to the amount of work being done, because the simulated annealing
algorithm works on a single individual, while the evolutionary algorithms work on an entire
population.

There are a number of tuneable parameters that I played with. There are, in my implementation,
\begin{itemize}
    \item The transformation on the fitness function
    \item The mutation operator
    \item Deterministic/stochastic selection
    \item Population size
    \item Number of generations
\end{itemize}
The transformation of the fitness function (simple negation or inversion) \textit{seemed} to have
an effect, but nothing measured. The mutation operator had a substantial impact, with the best one
being the sublist inversion. Using a deterministic selection operator consistently yielded the best
results. Being slightly impatient, I did not experiment with very large populations or large
numbers of generations. I think there is some parallelism that I could have taken advantage of to
make it easier to wait, but moving on to \autoref{prob:3} is currently more important.

\section{The TSP with Mutation and Recombination}\label{prob:3}

\subsection{Statement}
In \autoref{prob:2} we implemented EA code to solve the Traveling Salesman Problem. In this
problem, implement recombination (crossover) in your EA. For this problem you will need to use an
encoding that prevents crossover that creates an invalid candidate. As before, compare
deterministic and stochastic selection operators.

\subsection{Method}
As mentioned in \autoref{prob:2}, I found a paper~\cite{tsp_ea} listing many different mutation and
recombination operators on the simple ordered-path encoding for a solution to the TSP. I used the
same mutation and selection operators given in \autoref{sec:mutation-methods}. The interesting
stuff in this problem are the recombination operators given in~\cite{tsp_ea}.

\subsubsection{Partially-Mapped Crossover (PMX)}
According to~\cite{tsp_ea}, the PMX crossover is the most widely used recombination operator for
path-type problems. This operator combines parents $P_1$ and $P_2$ to get children $C_1$ and $C_2$.
The second child is produced in exactly the same manner as the first, but with the parents swapped.
It works as follows
\begin{enumerate}
    \item Choose two crossover points at random, and copy the sublist between them from $P_1$ into
          $C_1$ at the same location.\footnote{God damn it, these are really simple pictures, and
              were very easy to make, but to get nice colors took about 5 hours longer than it
              should have.}
          \begin{figure}[H]
              \centering
              \begin{tikzpicture}[font=\ttfamily,
                      array/.style={
                              matrix of nodes,
                              nodes={
                                      draw,
                                      anchor=center,
                                      minimum size=7mm,
                                      inner sep=0pt
                                  },
                              column sep=-\pgflinewidth,
                              row sep=-\pgflinewidth,
                              nodes in empty cells
                          }]

                  \matrix[array] (P1) {
                      1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\
                  };
                  \matrix[array, below of=P1, yshift=-1cm] (P2) {
                      9 & 3 & 7 & 8 & 2 & 6 & 5 & 1 & 4 \\
                  };

                  \matrix[array, right of=P2-1-9, anchor=west, yshift=1cm] (C1) {
                       &  &  & 4 & 5 & 6 & 7 &  & \\
                  };
                  \node[left of=P1-1-1] {$P_1$};
                  \node[left of=P2-1-1] {$P_2$};
                  \node[right of=C1-1-9] {$C_1$};

                  % Highlight the sublist.
                  \begin{scope}[on background layer]
                      \fill[__minted_background_color] (P1-1-1.north west) rectangle (P1-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P1-1-4.north west) rectangle (P1-1-7.south
                      east);
                      \fill[__minted_background_color] (P2-1-1.north west) rectangle (P2-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P2-1-4.north west) rectangle (P2-1-7.south
                      east);
                      \fill[__minted_background_color] (C1-1-1.north west) rectangle (C1-1-9.south
                      east);
                      \fill[__minted_highlight_color] (C1-1-4.north west) rectangle (C1-1-7.south
                      east);
                  \end{scope}
              \end{tikzpicture}
              \caption{Step 1, copy subarray}\label{fig:prob3:pmx-step-1}
          \end{figure}
    \item Starting from the first crossover point, look for elements of $P_2$ that were not copied
          to $C_1$.
          \begin{figure}[H]
              \centering
              \begin{tikzpicture}[font=\ttfamily,
                      array/.style={
                              matrix of nodes,
                              nodes={
                                      draw,
                                      anchor=center,
                                      minimum size=7mm,
                                      inner sep=0pt
                                  },
                              column sep=-\pgflinewidth,
                              row sep=-\pgflinewidth,
                              nodes in empty cells
                          }]

                  \matrix[array] (P1) {
                      1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\
                  };
                  \matrix[array, below of=P1, yshift=-1cm] (P2) {
                      9 & 3 & 7 & 8 & 2 & 6 & 5 & 1 & 4 \\
                  };

                  \matrix[array, right of=P2-1-9, anchor=west, yshift=1cm] (C1) {
                       &  &  & 4 & 5 & 6 & 7 &  & \\
                  };
                  \node[left of=P1-1-1] {$P_1$};
                  \node[left of=P2-1-1] {$P_2$};
                  \node[right of=C1-1-9] {$C_1$};

                  % Highlight the sublist.
                  \begin{scope}[on background layer]
                      \fill[__minted_background_color] (P1-1-1.north west) rectangle (P1-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P1-1-4.north west) rectangle (P1-1-7.south
                      east);
                      \fill[__minted_background_color] (P2-1-1.north west) rectangle (P2-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P2-1-4.north west) rectangle (P2-1-7.south
                      east);
                      \fill[__minted_background_color] (C1-1-1.north west) rectangle (C1-1-9.south
                      east);
                      %   \fill[__minted_highlight_color] (C1-1-4.north west) rectangle (C1-1-7.south
                      %   east);
                      \fill[red!30] (P2-1-4.north west) rectangle (P2-1-5.south east);
                  \end{scope}
              \end{tikzpicture}
              \caption{Step 2, find uncopied elements}\label{fig:prob3:pmx-step-2}
          \end{figure}
    \item For each of these non-copied elements $i$, find the element $j$ of the first parent that
          was copied in place of $i$. Place $i$ into the position occupied by $j$ in $P_2$.
          \begin{figure}[H]
              \centering
              \begin{tikzpicture}[
                      font=\ttfamily,
                      array/.style={
                              matrix of nodes,
                              nodes={
                                      draw,
                                      anchor=center,
                                      minimum size=7mm,
                                      inner sep=0pt
                                  },
                              column sep=-\pgflinewidth,
                              row sep=-\pgflinewidth,
                              nodes in empty cells
                          },
                      >=stealth]

                  \matrix[array] (P1) {
                      1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\
                  };
                  \matrix[array, below of=P1, yshift=-1cm] (P2) {
                      9 & 3 & 7 & 8 & 2 & 6 & 5 & 1 & 4 \\
                  };

                  \matrix[array, right of=P2-1-9, anchor=west, yshift=1cm] (C1) {
                       &  &  & 4 & 5 & 6 & 7 &  & 8 \\
                  };
                  \node[left of=P1-1-1] {$P_1$};
                  \node[left of=P2-1-1] {$P_2$};
                  \node[right of=C1-1-9] {$C_1$};

                  \draw[->] (P2-1-4) edge (P1-1-4) (P1-1-4.south) edge (P2-1-9.north);

                  % Highlight the sublist.
                  \begin{scope}[on background layer]
                      \fill[__minted_background_color] (P1-1-1.north west) rectangle (P1-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P1-1-4.north west) rectangle (P1-1-7.south
                      east);
                      \fill[__minted_background_color] (P2-1-1.north west) rectangle (P2-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P2-1-4.north west) rectangle (P2-1-7.south
                      east);
                      \fill[__minted_background_color] (C1-1-1.north west) rectangle (C1-1-9.south
                      east);
                      \fill[red!30] (P2-1-4.north west) rectangle (P2-1-5.south east);
                      \fill[__minted_highlight_color] (P2-1-9.north west) rectangle (P2-1-9.south
                      east);
                      \fill[__minted_highlight_color] (C1-1-9.north west) rectangle (C1-1-9.south
                      east);
                  \end{scope}
              \end{tikzpicture}
              \caption{Step 3, find a free spot to stuff things}\label{fig:prob3:pmx-step-3}
          \end{figure}
    \item If the position occupied by $j$ in $P_2$ has already been filled in $C_1$ by an element
          $k$, put $i$ in the position occupied by $k$ in $P_2$.
          \begin{figure}[H]
              \centering
              \begin{tikzpicture}[
                      font=\ttfamily,
                      array/.style={
                              matrix of nodes,
                              nodes={
                                      draw,
                                      anchor=center,
                                      minimum size=7mm,
                                      inner sep=0pt
                                  },
                              column sep=-\pgflinewidth,
                              row sep=-\pgflinewidth,
                              nodes in empty cells
                          },
                      >=stealth]

                  \matrix[array] (P1) {
                      1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\
                  };
                  \matrix[array, below of=P1, yshift=-1cm] (P2) {
                      9 & 3 & 7 & 8 & 2 & 6 & 5 & 1 & 4 \\
                  };

                  \matrix[array, right of=P2-1-9, anchor=west, yshift=1cm] (C1) {
                       &  & 2 & 4 & 5 & 6 & 7 &  & 8 \\
                  };
                  \node[left of=P1-1-1] {$P_1$};
                  \node[left of=P2-1-1] {$P_2$};
                  \node[right of=C1-1-9] {$C_1$};

                  \draw[->] (P2-1-5) edge (P1-1-5) (P1-1-5.south) edge (P2-1-7.north) (P2-1-7) edge
                  (P1-1-7) (P1-1-7.south) edge (P2-1-3.north);

                  % Highlight the sublist.
                  \begin{scope}[on background layer]
                      \fill[__minted_background_color] (P1-1-1.north west) rectangle (P1-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P1-1-4.north west) rectangle (P1-1-7.south
                      east);
                      \fill[__minted_background_color] (P2-1-1.north west) rectangle (P2-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P2-1-4.north west) rectangle (P2-1-7.south
                      east);
                      \fill[__minted_background_color] (C1-1-1.north west) rectangle (C1-1-9.south
                      east);
                      %   \fill[__minted_highlight_color] (C1-1-4.north west) rectangle (C1-1-7.south
                      %   east);
                      \fill[red!30] (P2-1-5.north west) rectangle (P2-1-5.south east);
                      \fill[__minted_highlight_color] (P2-1-9.north west) rectangle (P2-1-9.south
                      east);
                      %   \fill[__minted_highlight_color] (C1-1-9.north west) rectangle (C1-1-9.south
                      %   east);
                      \fill[__minted_highlight_color] (P2-1-3.north west) rectangle (P2-1-3.south
                      east);
                      \fill[__minted_highlight_color] (C1-1-3.north west) rectangle (C1-1-3.south
                      east);
                  \end{scope}
              \end{tikzpicture}
              \caption{Step 4, find better uncopied elements}\label{fig:prob3:pmx-step-4}
          \end{figure}
    \item Once each of the uncopied portions of $P_2$'s sublist have been placed, fill the
          remaining positions from $P_2$.
          \begin{figure}[H]
              \centering
              \begin{tikzpicture}[font=\ttfamily,
                      array/.style={
                              matrix of nodes,
                              nodes={
                                      draw,
                                      anchor=center,
                                      minimum size=7mm,
                                      inner sep=0pt
                                  },
                              column sep=-\pgflinewidth,
                              row sep=-\pgflinewidth,
                              nodes in empty cells
                          }]

                  \matrix[array] (P1) {
                      1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\
                  };
                  \matrix[array, below of=P1, yshift=-1cm] (P2) {
                      9 & 3 & 7 & 8 & 2 & 6 & 5 & 1 & 4 \\
                  };

                  \matrix[array, right of=P2-1-9, anchor=west, yshift=1cm] (C1) {
                      9 & 3 & 2 & 4 & 5 & 6 & 7 & 1 & 8 \\
                  };
                  \node[left of=P1-1-1] {$P_1$};
                  \node[left of=P2-1-1] {$P_2$};
                  \node[right of=C1-1-9] {$C_1$};

                  % Highlight the sublist.
                  \begin{scope}[on background layer]
                      \fill[__minted_background_color] (P1-1-1.north west) rectangle (P1-1-9.south
                      east);
                      \fill[__minted_background_color] (P2-1-1.north west) rectangle (P2-1-9.south
                      east);
                      \fill[__minted_background_color] (C1-1-1.north west) rectangle (C1-1-9.south
                      east);
                      \fill[__minted_highlight_color] (P1-1-4.north west) rectangle (P1-1-7.south
                      east);
                      \fill[__minted_highlight_color] (P2-1-3.north west) rectangle (P2-1-7.south
                      east);
                      \fill[__minted_highlight_color] (P2-1-9.north west) rectangle (P2-1-9.south
                      east);

                      \fill[__minted_highlight_color] (C1-1-1.north west) rectangle (C1-1-2.south
                      east);
                      \fill[__minted_highlight_color] (C1-1-8.north west) rectangle (C1-1-8.south
                      east);
                  \end{scope}
              \end{tikzpicture}
              \caption{Step 5, fill in the rest of $P_2$}\label{fig:prob3:pmx-step-5}
          \end{figure}
\end{enumerate}

The second child $C_2$ is created in the same manner, with parents swapped. This crossover operator
was surprisingly tricky\footnote{Want to play a game? It's called ``Find The Bug'', and I hear
    there's a prize this year.} to implement, and especially so with Numpy arrays over the usual
Python lists due to missing functionality.

\begin{minted}{python}
    def pmx(mom, dad):
        """Recombination using the Partially-Mapped Crossover algorithm.

        :param mom: The first parent.
        :type mom: list
        :param dad: The second parent.
        :type dad: list
        :returns: The recombined child.
        :rtype: list
        """
        # Pick a random chunk 1/3 the length of mom's genes to flat-out copy.
        l = len(mom) // 3
        i = np.random.randint(0, len(mom) - l)
        j = i + l
        child = [0] * len(mom)
        sublist1 = mom[i:j]
        sublist2 = dad[i:j]
        # Copy a chunk of mom's genes.
        child[i:j] = sublist1
        copied = set(sublist1)
        non_copied = (e for e in sublist2 if e not in sublist1)

        # Find a place for each element of dad's gene that won't clobber mom's.
        for elem in non_copied:
            # Get the index of the element in the dad array.
            idx = dad.index(elem)
            # Find a spot that won't be taken.
            while mom[idx] in copied:
                idx = dad.index(mom[idx])
            # Copy the element into the child.
            child[idx] = elem
            copied.add(elem)

        # Add the rest of dad's genes.
        for i in range(len(mom)):
            if dad[i] not in copied:
                child[i] = dad[i]
                copied.add(dad[i])
        return child
\end{minted}

\subsubsection{Order Crossover (OX)}
Explanation, implementation, and analysis left as an exercise to the reader.
\subsubsection{Maximal Preservative Crossover (MPX)}
\subsubsection{Cycle Crossover (CX)}
\subsubsection{Position Base Crossover (PBX)}
\subsubsection{Heuristic Crossover (HX)}
\subsubsection{Edge Recombination Crossover (ERX)}

\subsubsection{Mutation and Recombination with the Stack Encoding}
Given a genome encoded via the stack encoding, we can decode the corresponding city ordering with
\begin{minted}{python}
    def decode(n, genome):
        """Decode a given stack-encoded genome.

        :param n: The number of city locations.
        :param genome: The genome to decode into a phenome.
        :returns: The indices for a valid tour path.
        """
        cities = list(range(n))
        phenome = []
        for allele in genome:
            idx = allele % len(cities)
            phenome.append(cities[idx])
            cities.pop(idx)
        return phenome
\end{minted}
Now the traditional point mutation and splicing recombination will result in valid paths.

\begin{minted}{python}
    def stackx(mom, dad):
        """Recombination with stack encoding."""
        return mom[:len(mom) // 2] + dad[len(dad) // 2:]
\end{minted}

\begin{minted}{python}
    def recombine(mom, dad, encoding='stack'):
        methods = {
            'path': pmx,
            'stack': stackx,
        }
        return methods[encoding](mom, dad)
\end{minted}

Since I'm implementing this problem using plain Python lists, almost everything from
\autoref{prob:2} needs to be redefined.

\begin{minted}{python}
    def generate_cities(n, scale=100):
        return np.random.rand(n, 2) * scale

    def generate_population(sities, size):
        n = len(cities)
        population = [0] * size
        for i in range(size):
            individual = list(range(n))
            random.shuffle(individual)
            population[i] = individual
        return population
\end{minted}

The fitness function needs to be aware of the solution encoding.

\begin{minted}{python}
    def fitness(cities, path, encoding='path'):
        """Evaluate the fitness of the given path.

        Compute the Euclidean distance between every pair of cities in the path
        and add them together.

        :param cities: The array of cities through which to compute a path.
        :param path: The path through the given cities to compute the fitness for.
        :param encoding: One of 'path' or 'stack'.
        :returns: The fitness of the individual.
        """
        if encoding == 'path':
            individual = cities[path]
        elif encoding == 'stack':
            individual = cities[decode(path)]
        else:
            raise ValueError('invalid encoding')
        return 1 / sum(np.linalg.norm(c1 - c2) for c1, c2 in pairwise(individual))
\end{minted}

I experimented with different mutations, but with the stack encoding a simple random swap (as
opposed to an inversion) seemed to work best.

\begin{minted}{python}
    def mutate(path):
        i = np.random.randint(0, len(path) - 1)
        j = np.random.randint(i, len(path))
        x = path.copy()
        # Swap two random elements.
        x[i], x[j] = x[j], x[i]

        return x
\end{minted}

Then the selection operators need some TLC to work with the new encoding.

\begin{minted}{python}
    def deterministic_selection(population, size, func, cities, encoding='stack'):
        """Deterministically select the most fit from the given population.

        Use the given fitness function to rank the population, then pick
        the next `size` of the population to move on. This assumes that,
        for a problem without recombination, the mutated individuals have
        been mixed in with the original population.

        :param population: The population to cull.
        :param size: The desired size of the population.
        :param func: The fitness function to rank the population by.
        :param cities: The array of city locations.
        :returns: The culled population, sorted upwards in increasing fitness.
        """
        population.sort(key=lambda p: func(cities, p, encoding))
        euthanize = len(population) - size
        return population[euthanize:]
\end{minted}

\begin{minted}{python}
    def stochastic_selection(population, size, func, cities, encoding='stack'):
        """Randomly select the most fit from the given population.

        Select without replacement an individual with probability
        proportional to its fitness.

        :param population: The population to cull.
        :param size: The desired size of the population.
        :param func: The fitness function to rank the population by.
        :param cities: The array of city locations.
        :returns: The culled population, unsorted.
        """
        fitnesses = np.array([func(cities, p, encoding) for p in population])
        probabilities = fitnesses / np.sum(fitnesses)
        survivors = np.random.choice(len(population), size, replace=False, p=probabilities)
        return [population[i] for i in survivors]
\end{minted}

\begin{minted}{python}
    def select(population, size, func, cities, method='deterministic', encoding='stack'):
        """Select the `size` most fit from the given population.

        :param population: The population to cull.
        :param size: The desired size of the population.
        :param func: The fitness function to rank the population by.
        :param cities: The array of city locations.
        :param method: One of 'deterministic' or 'stochastic'.
        :param encoding: One of 'path' or 'stack'
        :returns: The culled population, in arbitrary order.
        """
        methods = {
            'stochastic': stochastic_selection,
            'deterministic': deterministic_selection,
        }
        return methods[method](population, size, func, cities, encoding)
\end{minted}

\subsubsection{The Evolutionary Algorithm}
The algorithm for this problem is not much different than that for \autoref{prob:2}. The biggest
difference is the recombination.

\begin{minted}{python}
    def ea(cities, size, func, iters, selection='deterministic', encoding='stack'):
        """Run the evolutionary algorithm to solve the TSP.

        :param cities: The array of city locations.
        :param size: The population size to use.
        :param func: The fitness function to use.
        :param iters: The number of generations to run.
        :param selection: One of 'deterministic' or 'stochastic'.
        :param encoding: One of 'path' or 'stack'. Determines recombination method.
        """
        n = len(cities)
        population = generate_population(cities, size)
        best_fitnesses = np.zeros(iters)
        best_individuals = np.zeros((iters, n), dtype=int)
        for i in range(iters):
            population.sort(key=lambda p: func(cities, p, encoding))
            population = population[n // 3:]
            random.shuffle(population)

            children = [recombine(mom, dad, encoding) for mom, dad in pairwise(population)]
            mutations = [mutate(c) for c in population]
            combined = population + children + mutations
            population = select(combined, size, func, cities, method=selection, encoding=encoding)

            fitnesses = np.array([func(cities, p, encoding) for p in population])
            best = fitnesses.argmax()
            best_fitnesses[i] = fitnesses[best]
            best_individuals[i] = decode(population[best]) if encoding == 'stack' else population[best]
        return best_fitnesses, best_individuals
\end{minted}

Notice that I am sorting the population by their fitness, and culling the bottom third before
shuffling and recombining pairwise. This is one of the simple changes I made to get better results.

\subsection{Results}
The most surprising and discouraging\footnote{Like seriously; I didn't even want to turn the rest
    of this in\dots} result was that I found my PMX recombination implementation
buggy. On occasion, it results in a child with a duplicated city, but only ever on the second
iteration of the evolutionary algorithm. This is frustrating, because I spent an inordinate amount
of time attempting to, first understand the operator, second draw the nice pictures like
\autoref{fig:prob3:pmx-step-3}, and third debug my implementation. This is particularly
disappointing because I wanted very badly to compare the ordered-path based recombination operators
to that of the simple splicing on the stack encoding.

So unfortunately I was only able to compare the results of the stochastic and deterministic
selection operators with only the stack encoding. As with \autoref{prob:2}, I first generated an
array of cities, shown in \autoref{fig:prob3:city-locations}.

\begin{figure}[h]
    \centering
    \includegraphics{prob3/figures/prob3-city-locations.pdf}
    \caption{The city locations}\label{fig:prob3:city-locations}
\end{figure}

Both of the selection operators gave wildly varying results. However, the stochastic operator
consistently beat the deterministic one, even if only by a tiny amount. Unfortunately, as
\autoref{fig:prob3:deterministic-fitness} shows, the fitness of the best individual caps out
relatively quickly, indicating that it is a locally optimal solution. This is unfortunate, because
the mutation operator does not introduce enough variation to push the population away from the
found solution.

I experimented with other mutation operators, and surprisingly the swap mutation worked the best.
With the stack encoding, I lack the intuitive understanding to understand what impact swapping two
elements in the genome has on the actual ordering of the cities.

\begin{figure}[h]
    \centering
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob3/figures/prob3-fitness-deterministic-stack.pdf}
        \caption{The best individual's fitness over time}\label{fig:prob3:deterministic-fitness}
    \end{subfigure}
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob3/figures/prob3-best-deterministic-stack.pdf}
        \caption{The overall best individual}\label{fig:prob3:deterministic-best}
    \end{subfigure}
    \caption{Results using deterministic selection}\label{fig:prob3:deterministic-results}
\end{figure}

Note also, from \autoref{fig:prob3:deterministic-best} that the best solution found is nowhere near
as good as \textit{any} of the solutions given in \autoref{prob:2}. (I think) Even manual
improvement of the solution would not beat those solutions.

I expected the stochastic selection operator to fail like it did in \autoref{prob:2}, but it
surprised me. Compare the population's best individual's fitness from
\autoref{fig:prob2:stochastic-fitness} to that shown in \autoref{fig:prob3:stochastic-fitness}. In
\autoref{fig:prob3:stochastic-fitness}, the fitness shows a steady climb, with several small peaks.
In \autoref{fig:prob2:stochastic-fitness}, the fitness shows no observable trend.

\begin{figure}[h]
    \centering
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob3/figures/prob3-fitness-stochastic-stack.pdf}
        \caption{The best individual's fitness over time}\label{fig:prob3:stochastic-fitness}
    \end{subfigure}
    \begin{subfigure}[b]{0.45\textwidth}
        \includegraphics[width=\textwidth]{prob3/figures/prob3-best-stochastic-stack.pdf}
        \caption{The overall best individual}\label{fig:prob3:stochastic-best}
    \end{subfigure}
    \caption{Results using stochastic selection}\label{fig:prob3:stochastic-results}
\end{figure}

However, the best individual found, shown in \autoref{fig:prob3:stochastic-best}, bears no visual
improvement on that of the deterministic selection operator.

Had I more time, I would be \textit{very} interested to see how the recombination operators
from~\cite{tsp_ea} perform, especially the PMX crossover, which~\cite{tsp_ea} claims is the most
common operator for path-based problems.

\appendix\appendixpage{}\addappheadtotoc{}
\section{Inversion Proof}\label{app:proof}

Since the best proofs are constructive, I thought I would get meta and programmatically construct a
proof that swaps, insertions, displacements, and shuffles can be implemented as a sequence of
inversions.

Unfortunately, I cannot claim to be the original author of such an excellent class of program. The
inspiration came from a fellow sufferer of topology,
\href{https://www.reddit.com/user/kwprules}{u/kwprules}, a year previous to my own suffering. The
illuminated discussion on their proof technique may be found
\href{https://www.reddit.com/r/math/comments/7gqhlc/what\_to\_say\_instead\_of\_trivially/}{here}.

\inputminted{python}{proof.py}

\bibliographystyle{ieeetr}
\bibliography{homework}{}

\end{document}


% Author: Bhishan Poudel
% Dates I worked:
%  Nov 2, 2017
%
%
%#*******************************************************
%#=======================================================
%# Chapter 2: Galaxy Fitting
%#=======================================================
%#*******************************************************
%
%
\section{Galaxy Fitting}\label{sec:chap2}
In this section, we want to fit the bulge and disk component fitting to the base
galaxy images provided from HST survey. We use the software called
\textit{Galfit} \footnote{https://users.obs.carnegiescience.edu/peng/work/galfit/galfit.html}
to get the reasonable components of the base galaxy. Galfit is an image analysis
algorithm that can model profiles of any given astronomical objects in the given
fits image of the data sample. For example, if an fits image contains a galaxy,
we can use galfit to fit the bulge and disk components to that galaxy.

Here, for the bulge part of the galaxy, we use the de Vaucouleurs profile in
Galfit.
The de Vaucouleurs profile describes how the surface brightness of a giant
elliptical galaxy changes as a  function of the radius R from the center of the
galaxy.

Let $R_e$ be the radius of an isophote containing the half of the total
luminosity for a galaxy, then, for a de Voucouleurs profile, the surface
brightness enclosed by the radius $R$ in that galaxy is given by:

\begin{eqnarray}\label{[eq:devauc]}
    I(R) = I_e e^{-7.669 [ (R/R_e)^{1/4} -1]  }
\end{eqnarray}

Galfit has this formula as a built-in feature to get the bulge component of the
galaxy.

Similarly, to fit the disk profile to a galaxy, we use the profile called
\textit{exponential disk profile} in Galfit. This exponential disk profile
is a special case of Sersic profile. In the Sersic
profile, the total surface brightness enclosed by the radius R around the center
of a galaxy is given by

\begin{eqnarray}\label{[eq:sersic]}
    ln \ I(R) = ln \ I_0 - k R^{1/n}
\end{eqnarray}

where, $I_0$ is the central surface brightness at $R = 0$ and the parameter n is
called \textit{Sersic index} which determines the curviness of the Sersic profile.

For the most of the spiral galaxies and dwarf elliptical galaxies, the Sersic index
is close to 1. This case when the Sersic index n is equal to 1 is called
exponential disk profile.
In exponential disk profile, the surface brightness inside the radius R is given
by

\begin{eqnarray}\label{[eq:expdisk]}
    ln \ I(R) = ln \ I_0 - k R
\end{eqnarray}

In this project we have 201 base galaxies obtained from the HST survey. As we
know that some of the galaxies have both bulge and disk components and some
do not have it. For the case, when the base galaxy has bulge-disk components,
Galfit gives nice bulge and disk components, however, when the base galaxy itself
does not have reasonable bulge-disk component, Galfit can not give bulge and disk
components of that galaxy. In that case either Galfit fails to give two
components or, gives bad parameter. We can see the whether the fitted parameters
are good or bad in the log file created from the Galfit. If a parameter is
enclosed by \* then, we can not trust the fitting, and we treat like the given
galaxy does not have reasonable bulge-disk components.

Let's say we get two successful bulge and disk components of
a base galaxy, then we choose devauc profile as the bulge and exponential profile
as the disk image. On the other hand, if either the Galfit fails to produce
two component fittings or gives non-reliable fitting parameters, then we make some
assumptions how to choose bugle and disk components.
If the single component fitting of \textit{devauc profile} gives the better fit
parameters than single component fitting of \textit{expdisk profile} ,
we choose the base galaxy as the bulge component and we
choose an empty image as the disk component.
Similarly, if the single component \textit{expdisk profile} gives better fitting than
single component \textit{devauc} fitting, we choose the base galaxy as the disk component
and we choose an empty image as the bulge component. We may notice that,
while doing the galaxy fitting to our 201 base galaxies, we found that most the galaxies
gave better fit for the \textit{devauc profile} than the \textit{expdisk profile}.
An example of input parameter file for \textit{Galfit} is shown next page.
\newpage

\verbatiminput{sections/expdisk_devauc.sh}

Another point to note is that, to use the Galfit program, we need to use a PSF image
to convolve the original galaxy with the given PSF. The PSF image is different for
different filter images of a given galaxy. That is, for F814W filter of a base
galaxy $galaxy\_f814w0.fits$ we use the psf $psf\_f814w.fits$ and for the galaxy
$galaxy\_f660w0.fits$ we use the psf $psf\_f606w.fits$. For all the F814W filter
images we use the same $psf\_f814w.fits$ and for all the F606W filter images we use
the same $psf\_f066w.fits$.

Here, in our project we use the F814W filter images taken from the HST ACS Wide Field Channel Camera.
To create the relevant PSF, we use an on-line tool called  \textit{STScI TinyTim Web Application}
\footnote{http://www.stsci.edu/hst/observatory/focus/TinyTim}.

The TinyTim web application needs some parameters to create a psf.
For this project we chose the following parameters:

\begin{table}[!h]
\centering
\caption{Tiny Tim Parameters}
\label{tiny-tim}
\begin{tabular}{ll}
Camera         & ACS - Wide Field Channel \\
Chip           & 1                        \\
Pixel Position & 301 301                  \\
Filter         & F814w                    \\
Spectrumtype   & Blackbody                \\
Spectrumvalue  & 6000                     \\
PSF diameter   & 5.0 arcsec               \\
Focus          & 0.0
\end{tabular}
\end{table}


\section{\texorpdfstring{Perfect security, secret sharing, symmetric ciphers}{Perfect security, secret sharing, symmetric ciphers}}
\vspace{5mm}
\large

\subsection{Perfect Security}
%2nd lecture, but belongs to 3rd by topic
\begin{definition}
	\textbf{One time pad (also known as Vernam's Cipher)} - is the only! cipher, we can prove secure.

	The key $k \in \{ 0, 1 \}^n $ is chosen uniformly at random. The message $x \in \{ 0, 1 \}^n$.

	cipher text is defined as
	\[ y = x \oplus k \iff y_i = x_i \oplus k_i \]

	Decryption, using the associativity of XOR.
	\[ y \oplus k = (x \oplus k) \oplus k = x \oplus (k \oplus k) = x \]
	Therefore encryption function is the same as decryption.

	From the observation \cref{random_bit}, XOR with random bits produces uniform random bit.
	Only the size of the information could be deduced from the cipher text. Extremely strong result!
	No matter how much computational power the attacker has, he cannot deduce plain text from cypher text.

	For other ciphers we can only prove that brute force attack is extremely slow, or $P = NP$ should be true to break the cipher.

	Such property is called \textbf{Informational-theoretic security} or \textbf{perfect security}.

\end{definition}

\begin{definition}\label{gen_one_time_p}
	\textbf{Generilized one-time pad}

	Consider some group additive G $(G, +, 0, -)$.

	We pick some $k \in G$ randomly, then
	\[ E(x, k) = x + k, D(y, k) = y - k = y + (k)^{-1} \]

	For example we can choose $\Z_t$.
\end{definition}

\begin{definition}
	A symmetric cipher is \emph{perfectly secure} iff
	\[ \forall x \forall y Pr_k[ E(x, k) = y ] = Pr_k[ D(y, k) = x ] = const \]

	For any plain text, all cipher texts should be equally likely.
\end{definition}

\begin{example}
	For example, for generalized one time pad \cref{gen_one_time_p}
	\[ \forall x \forall y \exists !k: x + k = y \Rightarrow k = y + x^{-1} \]

	Therefore the probability is $\frac{1}{|G|}$. As a result, one time pad is perfectly secure.
\end{example}

\begin{properties}
Usefulness of the OTP:
	\begin{enumerate}
		\item never use same key twice. Since xor of the cypher text is the same as xor of the plain text.
		\[ x_1 \oplus k = y_1 \land x_2 \oplus k = y_2 \Rightarrow y_1 \oplus y_2 = x_1 \oplus k \oplus x_2 \oplus k = x_2 \oplus x_1 \]
		\item Codebook - a long list of generated Keys.
			It has already happen during WW2, soviet agents ran out of keys in codebook and started to used same keys from the beginning.
			Not only were Americans able to read new messages, they have also decoded old ones.
		\item replace randomness by pseudorandomness. A key is produced by PRNG (pseudo random number generator) which is parametrized by another key and ideally by nonce (initialization vector).
		\item if attacker changes specific bit of the cipher text same plain text bit is changed. Since XOR is done bit by bit.
	\end{enumerate}
\end{properties}

\begin{theorem}[Perfect cipher]
	If \# of keys $<$ \# of messages $\Rightarrow$ the cipher is not perfectly secure.
\end{theorem}
\begin{proof}
	Let's take some cipher text $y$ and get all plain text that a mapped by the inverse of the encryption function to the some $T \subset $ Plain Texts. Since \# of keys $<$ \# of messages $\Rightarrow$ some of the keys $\notin T$.
	Therefore
	\[ Pr_k[x \notin T \land D(y, k) = x] = 0, Pr_k[x \in T \land D(y, k) = x] > 0 \]
	And the distribution of keys are not uniform. Cipher is not perfectly secure by the definition.
\end{proof}

\begin{note}
	Perfect security is also called \textbf{Shannon security}. He is the father of the information theory and cryptography.
\end{note}

\subsection{Secret Sharing}

Assume we have a secret information. We want to break secret into shared. The whole secret can be restored only having all the pieces.
We can use the OTP:

$S_1, ..., S_{k-1}$ are random numbers. $S_k = x \oplus S_1 \oplus ... \oplus S_{k-1}$.
Shareholders can decode x by xoring all $S_i$. Otherwise they will get random numbers.

\begin{definition}
	$(k, l)$ - threshold scheme. Split secret $x$ into $k$ shares $S_1, ..., S_k$.
	Having any $l$ shared agents can decode $x$. However $X$ cannot be decoded with $< l$ \# of shares. No info should be given.
\end{definition}

\begin{example}
	$(k, 2)$ scheme. Let F be finite field, the secret is a function $f = ax + b$ s.t.:
	\[ f(0) = x, f(1) \in_R F \]

	Geometrically, we have $x$ as the point with coordinates $(0, t)$ for some $t$.
	All $S_i$ for a straight line. As line can be deduced by any 2 points, 2 agents can decode $x$ using their points $S_i$.

	Distribution of the secrets $x$ is uniform $\Rightarrow$ perfectly secure.
\end{example}

Let's consider polynomials over field F.
According to results from Algebra, a polynomial of degree $d$ has at most $d$ roots.

Graph of the polynomial is a set of values for every point $\in F$.
Polynomials with the same graph share the roots $\Rightarrow$ should be equal.

\begin{theorem}[Lagrange interpolation]
	For every $x_1, ..., x_d, x_i \neq x_j, i \ne j, P(i) = 0$ distinct roots of P and $y_1, ..., y_d$
	\[ \exists! P, deg(P) < d: \forall i\ P(x_i) = y_i \]
\end{theorem}
\begin{proof}
	Let $ \overline{x} = (x_1, ..., x_d) \in F^d$ be a vector of coefficients of polynomial P, X is a set of all such vectors. $|X| = |F|^d$.

	Let Y be a set of all choices of $y_i$, graphs of the polynomials.

	Finally, let $f:X \to Y$ be a map from polynomials to graphs of polynomials. Which evaluates polynomial at d points.

	Since polynomials with the same graph are equal, $f$ is injective. $|X| = |Y| \Rightarrow f$ is bijection.
\end{proof}

\begin{consequence}
	Scheme based on graphs of the polynomials is perfectly secure, since according to the Lagrange theorem we have bijection between them $\Rightarrow$ uniform distribution.
\end{consequence}

\begin{example}
	$(k, l)$ scheme. Instead of lines we pick some polynomial P, $deg(P) = d$. Then we randomly pick $k$ values of the P.
	Having exactly $k$ shared agents can obtain P. Otherwise, all $X = P(0)$ are uniformly distributed.
\end{example}