## Functions from a Linear Algebra Perspective

In mathematics a function is a relation between a set of inputs (domain) and a set of outputs (range) with the property that each input is related to (mapped to) exactly one output. This means that for functions that are represented in $\mathbb{R}^2$ a function has to pass what is known as the "vertical line test." The vertical line test means that if you were to pass a vertical line over the function it would only be touching the function in one spot at any given time

Which of the graphs below are functions?

I want to re-emphasize that a function is a mapping from a set of inputs to a set of outputs where no two inputs is mapped to the same output.

## Relations as Functions

Imagine that we were trying to plot the line that created by the function $f(x) = 2x + 3$

How would we do it? Well, we would probably pick some simple $x$ values that exist near the origin and plug them in to see what their corresponding $y$ values were. It's clear that this is the equation for a line and we know that we're plotting these in $/mathbb{R}^2$ we would probably make some kind of a table or list of points that are on the line and then "connect the dots" to plot it:

Help me create some points that we would use to graph this line

\begin{align}
\text{Points: } (-2, y), (-1, y), (0, y), (1, y), (2, y)
\end{align}

These points do not completely represent that function, but we know that lines have a constant slope, so we don't need to find every ordered pair in the line in order to be able to plot it.

But say that we had some function where the only 5 inputs were the 5 that I gave you above and the only 5 outputs were the 5 that I gave you above. Tthis would be an odd function, and I don't know how we would define an equation for it. But what if we just let the ordered pairs themselves represent the function -the mapping from domain values to range values. We could create a set of ordered pairs that shows a mapping from domain values (x) to range values (y) where no domain value maps to the same range value twice. This set of ordered pairs is called a "relation"

\begin{align}
\text{Relation: } \{(-2, -1), (-1, 1), (0, 3), (1, 5), (2, 7)\}
\end{align}

Although this relation isn't represented by a specific equation (because it's just 5 points). However it meets our definition of a function, so this set of ordered pairs which show a valid mapping of domain values to range values is a function. If we plot these 5 points it would be a bit sparse but it would pass the vertical line test.

Relations that satisfy these conditions are functions. (albeit very simple ones)

### Which of the following relations are functions?

\begin{align}
\text{Relation 1: } \{(-2, -1), (-2, 1), (2, 3)\}
\\
\text{Relation 2: } \{(-2, 5), (-1, 5), (0, 5)\}
\\
\text{Relation 3: } \{(0, -1), (-1, 1), (0, 3)\}
\end{align}

## Linear Transformations (vector transformations)

Lets look at the function $f(x) = 2x + 3$ again.

This is how lower level math classes deal with functions, they put one number in as input and they get one output, but there's no reason why we need to limit functions to having just one input.

What if I defined a function like this $g(x_1, x_2) = x_1+x_2$

**_Side Note:_** I hope that if you weren't already comfortable with it that this kind of function notation, that it makes a lot more sense since you've been writing python functions and you recognize the things in the parenthesis as being the parameters of a function just like how we would write it in code.

The above function $g$ requires a two-dimensional tuple of inputs (two inputs) and gives a single output. We would say that this function is a mapping from $\mathbb{R}^2$ to $\mathbb{R}$.

Here's another function: $h(x_1, x_2, x_3) = (x_1 + 2, x_2 - x_3 )$

What is the dimensionality of the domain compared to the range (codomain)? Can you see that this mapping takes sets of values points from a certain dimensionality and maps them to sets of values with a smaller dimensionality?

## Vector Transformations

For today, we're going to go easy on you and imagine a mapping of vectors from $\mathbb{R}^2$ to $\mathbb{R}^2$.  We're just going to work with transforming 2D vectors to some other vector in two dimensions.

In linear algebra for a linear transformation to be valid it must meet the following two requirements:

\begin{align}
T(u+v) = T(u)+T(v)
\\
T(cu) = cT(u)
\end{align}

What these two properties mean is that scaling vectors is still valid and that a transformation of two vectors can be seen as the composition of smaller transformations (like scaled unit vectors added together). See 3Blue1Brown videos for more intuition on this.

## Linear Transformations As Matrix-Vector Products

What if I defined a mapping from $\mathbb{R}^2$ to $\mathbb{R}^2$ like so:

\begin{align}
f(\begin{bmatrix}x_1 \\ x_2\end{bmatrix}) = \begin{bmatrix} 2x_1 + x_2 \\ x_2 - x_1 \\  \end{bmatrix}
\end{align}

Well, remember how we can write any vector as a linear combination of scalars and basis vectors? How would I rewrite my input vector in those terms?

\begin{align}
\\
\begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = x_1\begin{bmatrix} 1 \\ 0 \end{bmatrix} + x_2\begin{bmatrix} 0 \\ 1 \end{bmatrix}
\\
\end{align}

You might remember from the 3Blue1Brown video about linear transformations that they can be described as the composition of transformations of our basis vectors. rewriting our input vector in this way will help us see that.

What if we evaluated our expression by passing in the unit vectors $\hat{i}$ and $hat{j}$?

\begin{align}
f(\begin{bmatrix} 1 \\ 0 \end{bmatrix}) = \begin{bmatrix} 2 \\ -1 \end{bmatrix}
\\
f(\begin{bmatrix} 0 \\ 1 \end{bmatrix}) = \begin{bmatrix} 1 \\ 1 \end{bmatrix}
\end{align}

By passing in our unit vectors individually, We are essentially asking: Tell me what happens to all of the x values, and then tell me what happens to all of the y values individually. And the resulting vector will be a composition of how both the y values and x values change.

The coolest part about passing in the unit vectors is that we've got two new vectors back that we can compose into a transformation matrix:

\begin{align}
T = \begin{bmatrix} 2 & 1 \\ -1 & 1 \end{bmatrix}
\end{align}

We can now describe the same function above as the product of matrix-vector multiplication between our resulting transformation vector and any vector in $\mathbb{R^2}$.

Lets test it! First lets use the original function to calculate the result of passing in

\begin{align}
f(\begin{bmatrix} 3 \\ 4 \end{bmatrix}) = \begin{bmatrix} 10 \\ 1 \end{bmatrix}
\end{align}

Now lets do it via matrix multiplication:

\begin{align}
\begin{bmatrix} 2 & 1 \\ -1 & 1 \end{bmatrix}\begin{bmatrix} 3 \\ 4 \end{bmatrix} = \begin{bmatrix} 10 \\ 1 \end{bmatrix}
\end{align}

Wild right???

Because of the properties of linear transformations, we can describe any linear transformation as matrix vector multiplication where the columns of the transformation matrix come from the result of passing in the dimension appropriate unit vectors into the function. So cool that we could represent a much more traditional function (mapping) as matrix-vector multiplication because of the way that we can represent vectors as linear combinations of scalars and unit vectors.

```
import matplotlib.pyplot as plt

# Vectors
input_vector = [3, 4]
output_vector = [10, 1]

# Plot the Vectors
plt.arrow(0,0, input_vector[0], input_vector[1],head_width=.05, head_length=0.05, color ='red')
plt.arrow(0,0, output_vector[0], output_vector[1],head_width=.05, head_length=0.05, color ='blue')
plt.xlim(0,11)
plt.ylim(-2,6)
plt.title("Transformed Vector")
plt.show()
```

## Intro to Eigenvalues and Eigenvectors (Intuition)

All vector-matrix multiplication can be thought of as a multi-dimensional transformation of space. (I hope that by now you're realizing how critical linear algebra is for representing multi-dimensional data.) This is hard for me to demonstrate without some cool animations so I'll have to rely upon 3Blue1Brown videos to help get you that intuition. Those videos are really the only place where I've seen that principle demonstrated even a little bit effectively.

One thing that might not be immediately clear however, is that not all points in space get transformed in exactly the same way. Think about a transformation in $\mathbb{R^3}$ where we rotate 3D space. You could imagine a globe spinning on its axis.

When a globe rotates, does every point on the globe move the same distance in space? No, points closer to the pole move less than points near the equator (kinda weird if you think about it that people living near the equator are hurtling through space at a different rate than other latitudes).

What about the vector represented by the earth's axis? Does it move?

We could call this vector the **eigenvector** of the earth's rotation brecause the transformation does not reorient it. Any transformation that you could conceive of possesses eigenvectors - dimensions of space that are not pointed in a new direction by the transformation described by a certain transformation matrix.

## So what's an eigenvalue?

An eigenvector is any vector that doesn't change its orientation during a given transformation, but an eigenvector might still get scaled by some scalar. It can get stretched or squished, it just can't change the direction in which it is pointing. An **eigenvalue** is the scalar that represents how its corresponding eigenvector was scaled during the transformation. Eigenvectors and eigenvalues always come in pairs.

What is the eigenvalue of the earth's axis when the earth rotates?

## Finding Eigenvalues and Eigenvectors

We don't have time to go over this today and it's one of the more complex processes in Linear Algebra. It usually involves a combination of a process called "Matrix Diagonalization" and Gaussian Elimination. If you're interested, you can look it up but that process is too complex for the time that we have today and I think you will be better served by focusing on an application of all of these concepts: **Principle Component Analysis (PCA)**

## One More Thing

What's the symbol commonly used to denote eigenvalues? - $\lambda$

\begin{align}
T(v) = \lambda v
\end{align}

# The Curse of Dimensionality

The Curse of Dimensionality is a term that is used to refer to some of the challenges and limitations that arise from trying to process or model datasets with a large number of features. (hundreds or thousands)

## What is a Dimension?

Imagine that we have a dataset with 50 numeric datapoints. How would we graph a single column of data? Well, we have nothing else in the dataset to compare it to, so all that we can do is really plot the values on a one-dimensional number line.

```
import pandas as pd
import random
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np

# Create a list of 50 random floats and add them to a dataframe
N = 50
x = np.random.rand(N)*100

# Crete Dataframe
data = {"x": x}
df = pd.DataFrame(data)
df.head()
```

      x

      0
      91.661852

      1
      57.389340

      2
      80.622474

      3
      40.581956

      4
      80.409636

```
# Plot the floats on a number line

# Setup a plot such that only the bottom spine is shown
def setup(ax):
    ax.spines['right'].set_color('none')
    ax.spines['left'].set_color('none')
    ax.yaxis.set_major_locator(ticker.NullLocator())
    ax.spines['top'].set_color('none')
    ax.xaxis.set_ticks_position('bottom')
    ax.tick_params(which='major', width=1.00)
    ax.tick_params(which='major', length=5)
    ax.tick_params(which='minor', width=0.75)
    ax.tick_params(which='minor', length=2.5)
    ax.set_xlim(0, 100)
    ax.set_ylim(0, 1)
    ax.patch.set_alpha(0.0)

plt.figure(figsize=(8, 6))
n = 8

# Create some y values to satisfy matplotlib
df['y'] = pd.Series(list(np.zeros(50)))

# Multiple Locator tick mark settings
ax = plt.subplot(n, 1, 2)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(5))
ax.text(0.0, 0.5, "Number Line", fontsize=14,
        transform=ax.transAxes)

# Push the top of the top axes outside the figure because we only show the
# bottom spine.
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05)
plt.scatter(df.x, df.y, alpha=0.5)

plt.show()
```

## JUST LET ME PLOT POINTS ON A NUMBER LINE IS THAT TOO MUCH TO ASK?!?

### Anyway... back to dimensions.

## Two Dimensions
What if I wanted to plot plot these points on a two-dimensional graph like a scatter plot? Well, I would need y values to tell the points where to go, or else they would just sit on the x-axis of the 2D graph. If we don't have any y values, the additional axis of the coordinate plane serves no purpose.

The number line is a 1D graph with 1D data. Scatter plots are 2Ds graphs that facilitate 2D data.

```
plt.scatter(df.x, df.y, alpha=0.5)
plt.title("Not a good use of a 2D Graph")
plt.show()
```

```
# Generate 50 random y values and add them to the dataframe
df['y'] = pd.Series(list(np.random.rand(N)*100))
df.head()
```

      x
      y

      0
      87.224509
      0.828650

      1
      22.433375
      59.655565

      2
      54.646833
      69.856257

      3
      49.150725
      30.249769

      4
      72.895643
      7.607457

```
# Graph 2D data on a 2D plot
plt.scatter(df['x'], df['y'], alpha=0.5)
plt.title("A Better Use of a 2D Graph")
plt.show()
```

### That's how easy the number line should have been... Somebody fix it and submit a Pull Request to Matplotlib

## Lets do 3-Dimensional Data on a 3-Dimensional Graph!

I hope you've realized that the number of dimensions that we want to plot, the number of columns that is required in the dataframe. This is why you'll hear people refer to Dataframe columns: Columns, Features, and Dimensions, interchangeably.

```
from mpl_toolkits.mplot3d import Axes3D

# Add Z dimension to dataframe
df['z'] = pd.Series(list(np.random.rand(N)*100))

threedee = plt.figure().gca(projection='3d')
threedee.scatter(df['x'], df['y'], df['z'])
threedee.set_xlabel('X')
threedee.set_ylabel('Y')
threedee.set_zlabel('Z')
plt.show()
```

## 3D Graphs on 2D screens are a lie... unless they can rotate.

Unless this graph can rotate like a hologram it's a 2D graph that's trying really hard, yet failing to be a 3D graph. Look more closely at the pionts and you'll realize that you actually have no idea where they are at. You have no depth perception unless you can rotate the graph. This is something that's possible to do in iPython notebooks, but that I didn't want to mess with right now. You can explore it on your own. Some libraries will try and change the diameter of the points so that ones "closer" to the viewer have some kind of depth, but this is an imperfect solution.

## What if we tried a spatially represent 4-Dimensional Data in a 4D graph?

While it **is** technically possible to plot points on a 4-Dimensional graph, the result is by no means intuitive and suffers from even worse interpretation problems than the 3D graph.

A cube is 3-dimensional, so any representation larger than 3 dimensions is referred to as an n-dimensional hypercube in n-dimensional hyperspace. This is why in space movies/books they always zoom to "hyperspace" when they're trying to take a shortcut through the 3 spatial dimensions of our reality.

## Now Lets do a 1000 dimensional dataset!

You see the problem?

While the inability for the human brain to spatially comprehend more than 3 dimensions is not **really** the curse of dimensionality, this limitation of our human brain is related to some of the challenges of dealing with high-dimensional data.

## A thought experiment

A great explanation from a Quora user: (Sorry, I can't find the actual Quora post that this originated from) I've modified it a little bit as well.

```
Let's say you have a straight line 100 yards long and you dropped 10 pennies somewhere on it. It wouldn't be too hard to find one. You walk along the line and it takes a few seconds, maybe as long as a couple of minutes if you're unlucky.

Now let's say you have a square 100 yards on each side and you dropped 10 pennies somewhere on it. It would be harder. The pennies are more spread out now. This is like searching across two football fields stuck together. It could take hours or days to find one. (on average, 10 times longer than the first time)

Now imagine a cube 100 yards across. That's like searching a 30-story building the size of a football stadium. Ugh. (It's now 100 times harder to find a penny in this space than in the first example)

The difficulty of searching through the space gets exponentially harder as you have more dimensions.
```

## Challenges of High Dimensional Spaces

### More difficult to explore the data and create visualizations

This pairplot shows us all of the possible scatterplots that could be created from a 4-dimensional dataset. Where the scatter plots would be plotted against themselves they have been replaced with histograms to show the distribution of a single variable.

Question: How many scatter plots would you have to look at if you had 1000 dimensions in a dataset?

- Remember that there are a lot of redundant scatterplots with pairplots

### Diminishing returns to adding features (increased redundancy)
Not every feature is as important as every other feature when it comes to capturing relationships in data. he likelihood of adding a feature that contributes meaningful signal to a particular predictive endeavor decreases with each additional dimension.

A good example of this is Deep Learning image recognition tasks:

How does increasing the number of pixels (dimensions of an image) increase our ability to detect what the image is?

If an extra dimension is more expensive to process but adds less value to a model, then at some point it isn't cost effective to add additional dimensions.

### Increased computational workload

Just like how it takes more time to find the pennies in the 100x100x100 yard cube.

### Increased sparcity of data - Measures of distance (Euclidean) lose meaning

As we add dimensions, our datapoints become more spread out. Just look at the difference between the 1-dimensional numberline and the 2-dimensional scatterplot that we did above. With each dimension that we add our datapoints spread out further, but at a decreasing rate. Eventually, in extremely high dimensional settings all datapoints appear to be more or less equidistant to one another. This is crippling to algorithms that rely upon measures of distance (Like Clustering for example). We'll talk more about this on Thursday!

### Low number of observations relative to dimensions increases the risk of overfitting

The the degree to which increased dimensions can lead to overfitting varies from model to model. The above images are examples of a linear regression model that is attempting to fit a good line through the data. We'll talk about this extensively next week, but linear regression models can be curved, You've typically seen a straight line cutting through a cloud of points, but just no that fitting a curve to a cloud of points doesn't violate the linearity of the model equation. Don't worry about this too much right now.

With a linear regression you can create a bend in the line of best fit by introducing another parameter. This parameter has to come from your dataset as another feature (or dimension). If my cloud of points is made up of only 9 data points, then I can curve my line of best fit exactly through each point by including 9 parameters. When discussing this aspect of the Curse of Dimensionality we will abbreviate the number of parameters that we are including in the model (dimensions, features) as **P** and the number of observations (rows in a dataset) as **N**.

If **P** $\geq$ **N** then I am at extreme risk of overfitting my model. Overfitting means that my model has fit to the noise or variance or the data rather than the acutal signal or phenomenon that we are trying to model. This damages the predictive accuracy of a model and makes it so that our model will not be able to generalize (perform well) to new data that it hasn't seen before.

### Rules of Thumb

While these rules of thumb can vary from model to model, some say that you should always make sure that you have 5 times the number of observations as you have parameters in your model. This is an easier task for some models than others. Some practitioners hold a more extreme view and say that the number of observations should be the square of the number of parameters. I think this is silly once you get to truly high numbers of dimensions, but some people are adamant about it (past professors of mine who really didn't deal with really high numbers of dimensions in their work and were blind to the existence of any other statistical approaches).

## Remember This:

### You're in grave danger of overfitting if P $\geq$ N
### Make sure you have 5N = P (as a minimum)
### Having more observations is almost always better
### More data in terms of additional dimensions has diminishing value

## Solutions

### Discard Redundant or Irrelevant Features
### Get More Data - LOTS more data
### Don't measure similarity via Euclidean Distance
### Get More Data - more observations not more features

More data covereth a multitude of sins.

# Dimensionality Reduction Techniques:

The Curse of Dimensionality means that as "Big Data" gets bigger and bigger the challenges associated with it become more difficult. Not only is it difficult to explore and visualize high dimensional data, but also certain intuitions that exist in 2d or 3d space begin to betray us (distance between points for example). Also models that have a high feature to observation ratio are more prone to overfitting.

What if we could take Big Data and make it smaller while still retaining most of its substance? In Machine Learning there are a whole host of Dimensionality Reduction Techniques that enable us to do exactly that. (with some tradeoffs - like always.)

## Feacture Selection:

Say we have 100 features in our dataset. Instead with fighting with all of them, what if we just used the ones that contributed the most. Feature Selection refers to methods of filtering irrelevant or redundant features from our dataset.

## Feature Extraction:

Feature Extraction creates a new, smaller set of features that stills captures most of the useful information. Feature selection finds a suitable subset of a dataset's features, feature extraction creates new ones altogether. PCA is a Feature Extraction method.

# Principal Component Analysis (PCA)

PCA is a technique that transforms a high dimensional dataset into a new lower dimensional dataset while preserving the maximum amount of information from the original data

# Variance ~~ Information

Imagine you were making shadow puppets by putting your hand in front of some light source that was projecting onto a wall.

If you want the shadow that was being cast onto the wall to be very recognizable as a hand which way would you orient your hand? (kill screenshare)

Why is that orientation more recognizeable as a hand than other orientations?

What is the following image an image of?

What about with clouds of data points?

<http://setosa.io/ev/principal-component-analysis/>

```
import pandas as pd
import matplotlib.pyplot as plt

x = [-2.2,-2,-2,-1,-1,0,0,1,1,2,2,2.2]
y = [0,.5,-.5,.8,-.8,.9,-.9,.8,-.8,.5,-.5,0]

print(len(x))
print(len(y))

data = {"x": x, "y": y}

df = pd.DataFrame(data)

axes = plt.gca()
plt.axes().set_aspect('equal')
plt.scatter(df['x'], df['y'])
```

Imagine that the data pictured above is a scatterplot of two explanatory variables. Lets pretend that we could only keep one of them. To remove the one that we don't need, we're going to collapse this data along one of its dimensions.

We could choose to project all of these datapoints onto the x-axis or onto the y-axis, which axis should we project the data points onto? Which one preserves more information?

```
import math

x1 = [-2.2,-2,-2,-1,-1,0,0,1,1,2,2,2.2]
y1 = [0,.5,-.5,.8,-.8,.9,-.9,.8,-.8,.5,-.5,0]

data = {"x": x1, "y": y1}

df1 = pd.DataFrame(data)

df1["x_rotate"] = df1.apply(lambda x: (x.x+x.y)/math.sqrt(2), axis=1)
df1["y_rotate"] = df1.apply(lambda x: (x.y-x.x)/math.sqrt(2), axis=1)

axes = plt.gca()
plt.axes().set_aspect('equal')
plt.scatter(df1['x_rotate'], df1['y_rotate'])
```

What if our data didn't lie as cleanly on the x or y axis? How would we then know along what dimension to perform our linear projection?

Well, the dimension that cuts nicely through this cloud of points is the dimension that maximizes the variance of points along it.

```

```

# The PCA Process:

## 1) Separate your data into X and Y, variables.

Remember that the y variable is the "label" or the "target" it's the thing that we want to predict. our X variables are all of the features of our data that are going ot help us to predict Y.

## 2) Center each column at 0 by subtracting its mean.

Take the matrix of independent variables $X$ and, for each column, subtract the mean of that column from each entry. (This ensures that each column has a mean of zero.)

If we don't do this then it will appear as if one component explains most of the variance in the data and the other components explain very little.

## 3) Divide each column by its standard deviation.

This completes the process of "standardizing" our data - giving it a mean of 0 and a standard deviation of 1. Just like we standardized our variance and covariance, this now makes the variances of our different features comparable and removes some of the concerns that were brought up when we talked about the rotated elipse of points.

We'll call the new standardized matrix the $Z$ matrix.

## 4) Calculate the Variance-Covariance matrix of Z.

Do this by calculating $Z^{T}Z$ Mind blown! - Multiplying a matrix by its transpose yields the variance-covariance matrix.

## 5) Calculate the Eigenvectors and Eigenvalues of the Variance-Covariance Matrix.

What are the Eigenvectors and Eigenvalues of the Variance-Covariance Matrix? The eigenvectors are those that run along the dimensions that maximize the variance along that dimension. Remember our rotated elipse example? When we moved our elipse off of the x and y axis we then didn't know what line we should project the points onto so that they we could collapse the dimension of least variance. Well now we do!

## 6) Sort the Eigenvalue and Eigenvector Pairs.

We'll sort our eigenvalue eigenvector pairs that resulted from the eigendecomposition of the variance-covariance matrix. We'll sort them by their eigenvalues in descending order. Since the eigenvalues tell us how to scale an eigenvector. The largest eigenvalue corresponds to the direction of greatest variance in the data. The eigenvalue eigenvector pair with the largest eigenvalue will be Principal Component 1

## 7) Use a matrix transformation to project the datapoints onto our eigenvector subspaces.

This final step is what makes it so that if we plot our transformed data our principal components will be alined with the axis of our graph.

```
from numpy import array
from numpy import mean
from numpy import cov
from numpy.linalg import eig

# define a matrix (we'll pretend that we've already separated our X and Y matrices)
X = array([[1, 2], [3, 4], [5, 6]])
print("Data: ", X)
# calculate the mean of each column
means = mean(X.T, axis=1)
print("\n Means: \n", means)
# center columns by subtracting column means
centered_data = X - means
print("\n Centered Data: \n", centered_data)
# calculate covariance matrix of centered matrix
# We're not going to find the covaraicne matrix by the (Z^T)(Z) method, I trust this function more
covariance_matrix = cov(centered_data.T)
print("\n Covariance Matrix: \n", covariance_matrix)
# eigendecomposition of covariance matrix
values, vectors = eig(covariance_matrix)
print("\n Eigenvectors: \n", vectors)
print("\n Eigenvalues: \n", values)
# project data
P = vectors.T.dot(centered_data.T)
print("\n Projected Data: \n", P.T)
```

    [[1 2]
     [3 4]
     [5 6]]

     Means:
     [3. 4.]

     Centered Data
     [[-2. -2.]
     [ 0.  0.]
     [ 2.  2.]]

     Covariance Matrix:
     [[4. 4.]
     [4. 4.]]

     Eigenvectors:
     [[ 0.70710678 -0.70710678]
     [ 0.70710678  0.70710678]]

     Eigenvalues:
     [8. 0.]

     Projected Data:
     [[-2.82842712  0.        ]
     [ 0.          0.        ]
     [ 2.82842712  0.        ]]

```
x = [1,3,5]
y = [2,4,6]
data = {"x": x, "y": y}

df = pd.DataFrame(data)

plt.scatter(df['x'], df['y'])
plt.title("Data Before PCA")
plt.show()
```

```
x = [-2.82842712,0,2.82842712]
y = [0,0,0]
data = {"x": x, "y": y}

df = pd.DataFrame(data)

plt.scatter(df['x'], df['y'])
plt.title("Data After PCA")
plt.xlabel('PC1')
plt.show()
```

# Lets use a library to prove to ourselves how smart we are

```
# Principal Component Analysis
from numpy import array
from sklearn.decomposition import PCA
# define a matrix
X = array([[1, 2], [3, 4], [5, 6]])
print("Data: \n", X)
# create the PCA instance
pca = PCA(2)
# fit on data
pca.fit(X)
# access values and vectors
print("\n Eigenvectors: \n", pca.components_)
print("\n Eigenvalues: \n",pca.explained_variance_)
# transform data
B = pca.transform(X)
print("\n Projected Data: \n", B)
```

    Data:
     [[1 2]
     [3 4]
     [5 6]]

     Eigenvectors:
     [[ 0.70710678  0.70710678]
     [-0.70710678  0.70710678]]

     Eigenvalues:
     [8. 0.]

     Projected Data:
     [[-2.82842712e+00 -2.22044605e-16]
     [ 0.00000000e+00  0.00000000e+00]
     [ 2.82842712e+00  2.22044605e-16]]

```

```


# Reference

```python
%matplotlib widget
import rad.css as css
import rad.example as ex
import rad.quiz as qz
from rad.const import c, k
from rad.radar import to_db, from_db, deg2rad, rad2deg
from math import sqrt, sin, asin, cos, acos, tan, atan2, pi, log, log10
css.add_custom_css()
```

To find material quickly, use the *Table of Contents*  button on the left sidebar.

## Aperture Beamwidth

The beamwidth of an aperture is lower bounded by:

$$
\theta \geq \mathrm{Shape~Factor}~\cdot\frac{\lambda}{D}
$$

where the shape factor is dictated by the aperture shape, $\lambda$ is the transmit wavelength (in $\mathrm{m}$), and $D$ is the extent of the aperture in the dimension under consideration.

### Circular Aperture

The beamwidth of a circular aperture (in degrees) of diameter $D$ is lower bounded by:

$$
\theta \geq 70^\circ\cdot\frac{\lambda}{D}
$$

where $\lambda$ is the transmit wavelength (in $\mathrm{m}$).

### Rectangular Aperture

The vertical beamwidth, $\Delta \theta_v$ , and horizontal beamwidth, $\Delta \theta_h$ , of a rectangular aperture (in degrees) of height $a$ (in $\mathrm{m}$) and width $b$ (in $\mathrm{m}$) is lower bounded by:

$$
\Delta\theta_v \geq 57.3^\circ\cdot\frac{\lambda}{a}
$$

$$
\Delta\theta_h \geq 57.3^\circ\cdot\frac{\lambda}{b}
$$

where $\lambda$ is the transmit wavelength (in $\mathrm{m}$).

## Aperture Gain

The gain of an aperture in direction $\theta$, $G(\theta)$ , is upper bounded by:

$$
G(\theta) \leq \frac{4\pi A}{\lambda^2}
$$

where $A$ is the aperture area (in $\mathrm{m^2}$) and $\lambda$ is the transmit wavelength (in $\mathrm{m}$).

## Constants

| Constant           | Symbol |
|--------------------|--------|
| Boltzmann constant | `k`    |
| $\pi$              | `pi`   |
| Speed of light     | `c`    |

## Decibels

Converting a number $x$ to its decibel equivalent, $x_d$ :

$$
x_d = 10\log_{10}(x)
$$

To return to original units:

$$
x = 10^{x_d/10}
$$

The function `to_db()` can be used to convert to decibels, and the function `from_db()` can be used to convert back to original units in notebook calculations.

### Multiplication

Conversion to decibels turns multiplication into addition:

$$
\begin{align}
z &= x\cdot y\\
z_d &= x_d + y_d
\end{align}
$$

Likewise, division becomes subtraction:

$$
\begin{align}
z &= x/ y\\
z_d &= x_d - y_d
\end{align}
$$

### Exponentiation

Conversion to decibel will change exponents into multiplication:

$$
\begin{align}
z &= x^a\\
z_d &= a \cdot x_d
\end{align}
$$

## Doppler

The Doppler frequency shift, $f_D$ , incurred by a target moving at a range rate $\dot{r}$ relative to a radar transmitting at frequency $f$ is:

$$
f_D = -\frac{2\dot{r}f}{c}
$$

## Operators and Functions

### Mathematical Operators

| Operation           | Symbol |
|---------------------|--------|
| Addition            | `+`    |
| Subtraction         | `-`    |
| Multiplication      | `*`    |
| Division            | `/`    |
| Exponent            | `**`   |
| Square root         | `sqrt` |
| Logarithm           | `log`  |
| Logarithm (Base 10) | `log10`|

### Trigonometric Functions

| Function       | Symbol    |
|----------------|-----------|
| Sine           | `sin`     |
| Arcsine        | `asin`    |
| Cosine         | `cos`     |
| Arccosine      | `acos`    |
| Tangent        | `tan`     |
| Arctangent     | `atan2`   |

## Radar Range Equation

### Received Power

Received power, $P_r$ , (in watts) from a radar transmission is:

$$
P_r = \frac{P_t G_t(\theta) G_r(\theta) \lambda^2 \sigma}{(4\pi)^3 r^4}
$$

where

- $P_t$ is the transmitted power $(\mathrm{W})$
- $G_t(\theta)$ is the transmit gain in the transmit direction $\theta$
- $G_r(\theta)$ is the receive gain in the receive direction $\theta$
- $\lambda$ is the transmission wavelength ($\mathrm{m}$)
- $\sigma$ is the radar cross section of the target ($\mathrm{m^2}$)
- $r$ is the range to the target ($\mathrm{m}$)

### Received Energy

Received energy, $\mathcal{E}_r$ , (in joules) from a radar transmission is:

$$
\mathcal{E}_r = \frac{\mathcal{E}_t G_t(\theta) G_r(\theta) \lambda^2 \sigma}{(4\pi)^3 r^4}
$$

where

- $\mathcal{E}_t$ is the transmitted energy $(\mathrm{J})$
- $G_t(\theta)$ is the transmit gain in the transmit direction $\theta$
- $G_r(\theta)$ is the receive gain in the receive direction $\theta$
- $\lambda$ is the transmission wavelength ($\mathrm{m}$)
- $\sigma$ is the radar cross section of the target ($\mathrm{m^2}$)
- $r$ is the range to the target ($\mathrm{m}$)

### Average Noise Energy

Average noise energy, $\mathcal{E}_n$ , (in joules) is:

$$
\mathcal{E}_n = k T_s
$$

where

- $k = 1.38064852 \times 10^{-23} \frac{J} {^\circ\mathrm{K}}$ is the Boltzmann constant
- $T_s$ is the noise temperature of the sensor $(^\circ\mathrm{K})$

### Signal-to-Noise Ratio

The received signal-to-noise ratio is then:

$$
\mathrm{SNR} = \frac{\mathcal{E}_r}{\mathcal{E}_n}
$$

## Scale

Here is a list of the scale prefixes, their values, and code snippets to use them in notebook calculations:

| Symbol            | Prefix       | Value               | Code           |
|-------------------|--------------|---------------------|----------------|
| $T$               | tera         | $1 \times 10^{12}$  | `1E12`         |
| $G$               | giga         | $1 \times 10^9$     | `1E9`          |
| $M$               | mega         | $1 \times 10^6$     | `1E6`          |
| $k$               | kilo         | $1 \times 10^3$     | `1E3`          |
| $m$               | milli        | $1 \times 10^{-3}$  | `1E-3`         |
| $\mu$             | micro        | $1 \times 10^{-6}$  | `1E-6`         |
| $n$               | nano         | $1 \times 10^{-9}$  | `1E-9`         |
| $p$               | pico         | $1 \times 10^{-12}$ | `1E-12`        |

## Symbols

Here is a list of the mathematical symbols used throughout the course and their respective meanings:

| Symbol            | Definition                                             |
|-------------------|--------------------------------------------------------|
| $a$               | Amplitude                                              |
| $A$               | Area, $\mathrm{m^2}$                                   |
| $A_t$             | Area of transmit aperture, $\mathrm{m^2}$              |
| $A_r$             | Area of receive aperture, $\mathrm{m^2}$               |
| $B$               | Transmit bandwidth, $\mathrm{Hz}$                      |
| $c$               | Speed of light, $\mathrm{m/s}$                         |
| $\delta_\phi$     | Target angle resolution, $\mathrm{deg}$                |
| $\delta_r$        | Target range resolution, $\mathrm{m}$                  |
| $\delta_\dot{r}$  | Target range rate resolution, $\mathrm{m/s}$           |
| $\mathcal{E}$     | Energy, $\mathrm{J}$                                   |
| $\mathcal{E}_n$   | Noise energy, $\mathrm{J}$                             |
| $\mathcal{E}_r$   | Received energy, $\mathrm{J}$                          |
| $\mathcal{E}_t$   | Transmit energy, $\mathrm{J}$                          |
| $f$               | Transmit frequency, $\mathrm{Hz}$                      |
| $f_D$             | Doppler shift, $\mathrm{Hz}$                           |
| $f_r$             | Received frequency, $\mathrm{Hz}$                      |
| $f_{rep}$         | Pulse repetition frequency, $\mathrm{Hz}$              |
| $\theta$          | Transmit azimuth angle, $\mathrm{deg}$                 |
| $\Delta\theta$    | Transmit azimuth angle beamwidth, $\mathrm{deg}$       |
| $P$               | Power, $\mathrm{W}$                                    |
| $P_i$             | Incident power, $\mathrm{W}$                           |
| $P_n$             | Noise power, $\mathrm{W}$                              |
| $P_r$             | Received power, $\mathrm{W}$                           |
| $P_t$             | Transmit power, $\mathrm{W}$                           |
| $\phi$            | Target azimuth angle, $\mathrm{deg}$                   |
| $\hat{\phi}$      | Target azimuth angle estimate, $\mathrm{deg}$          |
| $r$               | Target range, $\mathrm{m}$                             |
| $\hat{r}$         | Target range estimate, $\mathrm{m}$                    |
| $\sigma$          | Target radar cross section, $\mathrm{m}^2$             |
| $\hat{\sigma}$    | Target radar cross section estimate, $\mathrm{m}^2$    |
| $\tau$            | Propagation delay, $\mathrm{s}$                        |
| $\hat{\tau}$      | Propagation delay estimate, $\mathrm{s}$               |
| $t$               | Time, $\mathrm{s}$                                     |
| $t_p$             | Pulsewidth, $\mathrm{s}$                               |
| $t_{rep}$         | Pulse repetition interval, $\mathrm{s}$                |
| $v_p$             | Propagation velocity, $\mathrm{m/s}$                   |

## Target Parameter Estimates

### Accuracy and SNR

Each of the observable attributes of target can be estimated with a certain level of accuracy[1](#foot_acc). It can be shown[[1]](#ref_skolnik) that the accuracy, $\alpha$, of each estimate is approximately:

$$
\alpha \approx \frac{\delta}{\sqrt{\mathrm{SNR}}}
$$

where $\delta$ is the resolution in that dimension and $\mathrm{SNR}$ is the signal-to-noise ratio.

### Range

The range estimate of a target, $\hat{r}$ , is given by
$$
\hat{r} = \frac{c\cdot \hat{\tau}}{2}
$$

where $\hat{\tau}$ is the estimated propagation delay from the detection. Range resolution, $\delta r$ , is

$$
\delta r = \frac{c}{2B}
$$

where $B$ is the bandwidth of the transmitted waveform. Thus, the accuracy of a range estimate from a detection, $\alpha_r$ , is

$$
\alpha_r = \frac{c}{2B\sqrt{\mathrm{SNR}}}
$$

### Angle

Coarse angle estimation can be done using *mainlobe angle estimation* which places the observed target within the main beam of the radar system. In this case, the target angle estimate, $\hat{\phi}$, is simply the transmit-receive steering direction, $\theta$, or

$$
\hat{\phi} = \theta
$$

The accuracy is then roughly the beamwidth of the radar aperture:

$$
\alpha_\phi = \Delta \theta
$$

Fine angle measurement can be performed using multiple different techniques, e.g., *beamcenter angle estimation*, *monopulse amplitude comparison*, or *monopulse phase comparison*. With these, the target angle estimate accuracy is increased approximately to

$$
\alpha_\phi = \frac{\Delta \theta}{\sqrt{\mathrm{SNR}}}
$$

### Range Rate

Range rate measurements are derived from measuring the frequency shift caused by the Doppler effect, $f_D$. The range rate estimate, $\hat{\dot{r}}$ , can be calculated as

$$
\hat{\dot{r}} = -\frac{c f_D}{2f}
$$

Similar to the range and angle measurements dicussed above, the range rate accuracy of a radar system, $\alpha_{\dot{r}}$, can be approximated by:

$$
\alpha_{\dot{r}} = \frac{c}{2f\Delta t\sqrt{\mathrm{SNR}}}
$$

where $\Delta t$ is the observation time. The highest measureable Doppler shift is given by the rate that the multiple pulses are transmitted. For a set of pulses with pulse repetition frequency, $f_{rep}$ , the magnitude of Doppler shifts that can be unambiguously measured is bounded by:

$$
|f_D| < \frac{f_{rep}}{2}
$$

This can be translated to the span of range rates that can be unambiguously observed:

$$
|\dot{r}| < \frac{c f_{rep}}{4f}
$$

### Radar Cross Section

Radar cross section can be calculated from the estimated received energy, $\hat{\mathcal{E}}_r$ , and range, $\hat{r}$ , by:

$$
\hat{\sigma} = \frac{(4\pi)^3}{\mathcal{E}_t G_t(\theta) G_r(\theta) \lambda^2} \cdot \hat{\mathcal{E}}_r \cdot \hat{r}^4
$$

where

- $\mathcal{E}_t$ is the transmitted energy $(\mathrm{J})$
- $G_t(\theta)$ is the transmit gain in the transmit direction $\theta$
- $G_r(\theta)$ is the receive gain in the receive direction $\theta$
- $\lambda$ is the transmission wavelength ($\mathrm{m}$)
- $r$ is the range to the target ($\mathrm{m}$)

Likewise, it can be calculated efficiently using a reference SNR by:

$$
\hat{\sigma} = \frac{\mathrm{SNR}}{\mathrm{SNR}_0} \cdot \left(\frac{\hat{r}}{r_0}\right)^4 \cdot {\sigma_0}
$$

## Units

| Symbol              | Unit           | Definition                                             |
|---------------------|----------------|--------------------------------------------------------|
| $\mathrm{Hz}$       | hertz          | Unit of frequency                                      |
| $\mathrm{J}$        | joule          | Unit of energy                                         |
| $^\circ \mathrm{K}$ | degree kelvin  | Unit of temperature                                    |
| $\mathrm{m}$        | meter          | Unit of length                                         |
| $\mathrm{s}$        | second         | Unit of duration                                       |
| $\mathrm{W}$        | watt           | Unit of power                                          |

## Wavelength

The frequency and wavelength of a transmitted wave are related by the following:

$$ f = \frac{v_p}{\lambda} $$

and

$$ \lambda = \frac{v_p}{f} $$

where $v_p$ is the propagation velocity.


#
Mathematical Formulation

The economical dispach of thermal generation units can be formulated as an objetive function, which should be minimized considering the limits of each power plant and also the supply of the demanded power. The following system of equations formulate the problem:

$$
\begin{equation}
    \begin{aligned}
       \text{Min } & F_t = \sum_{i=1}^{n_g} F_i(P_i) \\
       \text{s.t}  & \\
                   & \sum_{i=1}^{n_g} P_i = P_D \\
                   & P_i^{min} \leq P_i \leq P_i^{max} \\
    \end{aligned}
\end{equation}
$$

Onde:
 - $F_t$ = total financial cost of the dispach.
 - $F_i$ = financial cost function for the the generator $i$.
 - $P_i$ = power generated by the $i$.
 - $P_D$ = power demand.
 - $P_i^{min}$ = minimum generation limit of the generator $i$.
 - $P_i^{max}$ = maximum generation limit of the generator $i$;
 - $n_g$ = number of generatio units.

A quadratic function can be used as a proxy of the fuel cost for each generation unit (Zhu, 2015).

$$
\begin{equation}
    F_i(P_i) = c_iP_i^2 + b_iP_i + a_i
\end{equation}
$$

Bringin a more realistic approach to the economic dispach, a thermal generation unit might also have operative zones (Orero e Irving, 1996), which are directly related to auxiliary systems such as boilers and feed pumps. Such operative zones should be avoided as they bring risk to a safe operation of the plant, hence a discontinuous mathematical formulation rises:

$$
\begin{equation}
        P_i^{min} \leq P_i \leq P_i^{n_1} \\
        P_i^{n_2} \leq P_i \leq P_i^{n_3} \\
        P_i^{n_4} \leq P_i \leq P_i^{max} \\
\end{equation}
$$

---

# Solver

In order to solve the problem the [CVXOPT](https://cvxopt.org/) library will be used. The library offers a complete set of mathematical solvers implemented.

### Loading data

From now on we can use the `systems` module and initialize a `PowerSystem`, which should provide all of the necessary data for the solver.

```python
from acopoweropt import system

PSystem = system.PowerSystem(name='s10')
```

### Solving

Defining a function to handle the solution of the PowerSystem

```python
from cvxopt import matrix, solvers

import pandas as pd
import numpy as np

def solve(demand:float, operation: pd.DataFrame,
                        maxiters: int=15,
                        show_progress: bool=False):
    """Returns a solution to a specific operation configuration

    Given a specific configuration to be solved, this function uses cvxopt
    quadratic programing to solve the system check:
        https://cvxopt.org/examples/tutorial/qp.html

    One possible source of configuration data can be obtained by using the
    provided `PowerSystem.sample_operation()` method, which randomly creates
    a possible configuration for the system to be operated

    Parameters
    ----------
    operation : pd.DataFrame
        DataFrame representing the operation of the system
    maxiters=15 : int
        Maximum number of iterations to be performed by the method.
    show_progress=False : bool
        Iteractively show progress during computation

    Returns
    -------
    dict
        A dictionary containing all of the solution results

    """

    # CVXOPT uses matrix like objects in order to model
    # a system of equations. Numpy can be used to prepare
    # the data so that the solver can be used.

    solvers.options['show_progress'] = show_progress
    # solvers.options['refinement'] = 2
    solvers.options['maxiters'] = maxiters

    demand = np.array([demand], dtype="double")

    # Equation parameters cP^2 + bP + a
    a = operation.a.sum()
    b = operation.b.to_numpy(dtype="double")
    c = operation.c.to_numpy(dtype="double")

    # CVXOPT needs a system of equations:
    Pmin = operation.Pmin.to_numpy(dtype="double")
    Pmax = operation.Pmax.to_numpy(dtype="double")

    P = matrix(2 * (c[..., None] * np.eye(operation.shape[0])))
    q = matrix(b)

    G_min = -1 * np.eye(operation.shape[0])
    G_max = np.eye(operation.shape[0])
    G = matrix(np.concatenate((G_min,G_max)))
    h = matrix(np.concatenate((-1 * Pmin,Pmax)))

    A = matrix(np.ones(operation.shape[0]), (1, c.shape[0]))
    b = matrix(demand)

    # Solving using Quadratic Programing
    solution = solvers.qp(P, q, G, h, A, b)

    # Interpreting the solution:
    Ft = solution.get('dual objective') + a
    Pg = pd.DataFrame([{'tgu': i+1,
                        'Pg': Pg} for i,Pg in enumerate(solution.get('x'))]
                      ).set_index('tgu')

    Fi = (((Pg.Pg ** 2) * operation.c) + (Pg.Pg * operation.b) + operation.c)
    Fi_df = pd.DataFrame({'tgu':Fi.index, 'Fi':Fi.values}).set_index('tgu')

    return {'status': solution.get('status'),
            'Ft': Ft,
            'operation': pd.concat([operation,Pg,Fi_df], axis=1)}
```


# Nosecone Designs

This notbook provides descriptions, equations and renderings of several nose cone shapes commonly used in hobbly rocketry. The notebook leverages serveral common packages such as matplotlib, numpy for data handling and 2D graphing and some less-common packages viewscad, solidpython and OpenSCAD for 3D rendering.

Our first step is to import these packages.

```python
import matplotlib.pyplot as plt
import numpy as np
import viewscad
from solid import *
```

L and R define the length and radius of the nosecone and S defines the diameter of the shoulder.

```python
L = 43.7
R = 18.7/2
S = 18/2
M = 3/2
```

The smoothing factor defines the number of segments used to render objects

```python
SMOOTH=100
```

This function rotates the nosecone profile around the z-axis and outputs an .stl file

```python
def build_3d(points, name, shoulder=0, mount=0):
    r = viewscad.Renderer()
    p = rotate_extrude(360, segments=SMOOTH)(polygon(points))
    if shoulder != 0:
        p += translate([0, 0, -shoulder])(cylinder(r=shoulder, h=shoulder, segments=SMOOTH))

        if mount != 0:
            p -= translate([0, 0, -shoulder])(cylinder(r=mount, h=shoulder, segments=SMOOTH))

    r.render(p, outfile='/Users/hatcherr/' + name + '.stl')
```

This function adds origin coordinates to the arrays passed and plots a graph of the nosecone profile in 2 dimensions. The arrays are combined into an array of points and returned.

```python
def build_2d(x, y):
    zero = np.array([0])
    xplt = np.concatenate((zero, x, zero))
    yplt = np.concatenate((zero, y, zero))

    plt.axes().set_aspect("equal")
    plt.plot(xplt, yplt)

    return np.vstack((yplt, xplt)).T
```

We define a set of values for X from 0 to the nosecone length.

```python
x = np.linspace(0, L, int(L))
```

## Elliptical

The elliptical nose cone shape is one-half of an ellipse, with the major axis being the centerline and the minor axis being the base of the nose cone. A rotation of a full ellipse about its major axis is called a prolate spheroid, so an elliptical nose shape would properly be known as a prolate hemispheroid. This shape is popular in subsonic flight (such as model rocketry) due to the blunt nose and tangent base and are generally considered superior for model rocketry altitude optimisation use. This is not a shape normally found in professional rocketry, which almost always flies at much higher velocities where other designs are more suitable.

The profile is defined as $y=R{\sqrt  {1-{x^{2} \over L^{2}}}}$ If $R = L$, this is a hemisphere.

We define our function, apply that to the values of x to generate an array of y values, which are used to generate a plot of the nosecone profile. The points returned combine the x and y arrays with the addition of the origin point to enure a closed shape.

```python
f = lambda x: R * np.sqrt(1-(x**2/L**2))
y = f(x)

points = build_2d(x, y)
```

rotate the profile around the z-axis to create a solid object and output an .stl for subsequent use.

```python
build_3d(points, 'elliptical', S, M)
```

    VBox(children=(HTML(value=''), Renderer(background='#cccc88', background_opacity=0.0, camera=PerspectiveCamera…

## Conical

A very common nose-cone shape is a simple cone. This shape is often chosen for its ease of manufacture, and is also often chosen for its drag and radar cross section characteristics. A lower drag cone would be more streamlined, with the most optimal shape being a Sears-Haack body. The sides of a conical profile are straight lines, so the diameter equation is simply $y={xR \over L}$

Cones are sometimes defined by their half angle, $\phi$

$\phi = \arctan \Bigl({R \over L}\Bigr)$ and $y = x \tan (\phi) $

In practical applications, a conical nose is often blunted by capping it with a segment of a sphere. The tangency point where the sphere meets the cone can be found from

$x_t = \frac{L^2}{R}\sqrt{\frac{r_n^2}{R^2+L^2}}$

$y_t = \frac{x_tR}{L}$

where

$r_n$ is the radius of the spherical nose cap.

$x_o = x_t + \sqrt{r_n^2 - y_t^2}$

$x_a = x_o - r_n$

```python
f = lambda x: x*R/L
y = f(x)

points = build_2d(x, y[::-1])
```

```python
build_3d(points, 'conical', S, M)
```

    VBox(children=(HTML(value=''), Renderer(background='#cccc88', background_opacity=0.0, camera=PerspectiveCamera…

## Parabolic
This nose shape is not the blunt shape that is envisioned when people commonly refer to a "parabolic" nose cone. The parabolic series nose shape is generated by rotating a segment of a parabola around a line parallel to its latus rectum. This construction is similar to that of the tangent ogive, except that a parabola is the defining shape rather than a circle. Just as it does on an ogive, this construction produces a nose shape with a sharp tip. For the blunt shape typically associated with a parabolic nose, see power series below. (The parabolic shape is also often confused with the elliptical shape.)

For $0 \leq K^\prime \leq 1 :  y=R \Biggl({2({x \over L})-K^\prime({x \over L})^{2} \over 2-K^\prime}\Biggr)$

$K^\prime$ can vary anywhere between $0$ and $1$, but the most common values used for nose cone shapes are:

| Parabola Type | $K^\prime$ Value |
| --- | --- |
| Cone | $0$ |
| Half | $\frac {1}{2}$ |
| Three Quarter| $3 \over 4$ |
| Full | $1$ |

For the case of the full parabola $(K^\prime = 1)$ the shape is tangent to the body at its base, and the base is on the axis of the parabola. Values of $K^\prime \lt 1$ result in a slimmer shape, whose appearance is similar to that of the secant ogive. The shape is no longer tangent at the base, and the base is parallel to, but offset from, the axis of the parabola.

```python
K = .75

f = lambda x: R*(((2*(x/L))-(K*(x/L)**2))/(2-K))
y = f(x)

points = build_2d(x, y[::-1])
```

```python
build_3d(points, 'parabolic', S, M)
```

    VBox(children=(HTML(value=''), Renderer(background='#cccc88', background_opacity=0.0, camera=PerspectiveCamera…

## Ogive
### Tangent ogive
Next to a simple cone, the tangent ogive shape is the most familiar in model/hobby rocketry. The profile of this shape is formed by a segment of a circle such that the rocket body is tangent to the curve of the nose cone at its base, and the base is on the radius of the circle. The popularity of this shape is largely due to the ease of constructing its profile, as it is simply a circular section.

The radius of the circle that forms the ogive is called the ''ogive radius'', $\rho$, and it is related to the length and base radius of the nose cone as expressed by the formula

$\rho = {R^2 + L^2\over 2R}$

The radius $y$ at any point $x$, as $x$ varies from $0$ to $L$ is

$y = \sqrt{\rho^2 - (L - x)^2}+R - \rho$

The nose cone length, $L$, must be $\leq \rho$. If they are equal, then the shape is a hemisphere.

### Spherically blunted tangent ogive
A tangent ogive nose is often blunted by capping it with a segment of a [[sphere]].  The tangency point where the sphere meets the tangent ogive can be found from:
\begin{align}
  x_o &= L - \sqrt{\left(\rho - r_n\right)^2 - (\rho - R)^2} \\
  y_t &= \frac{r_n(\rho - R)}{\rho - r_n} \\
  x_t &= x_o - \sqrt{r_n^2 - y_t^2}
\end{align}
where $r_n$ is the radius and $x_o$ is the center of the spherical nose cap.

Finally, the apex point can be found from:
$x_a = x_o - r_n$

```python
rho = ((R**2+L**2)/2*R)
print(rho)
f = lambda x: np.sqrt((rho**2 - (x-L)**2)) + (R - rho)
y = f(x)
print(x, y)
yplt = y[::-1]
plt.axes().set_aspect("auto")
plt.plot(x, yplt)

```

## Haack series

Unlike all of the nose cone shapes above, the Haack Series shapes are not constructed from geometric figures. The shapes are instead mathematically derived for the purpose of minimizing drag; see also Sears–Haack body. While the series is a continuous set of shapes determined by the value of $C$ in the equations below, two values of $C$ have particular significance: when $C = 0$, the notation $LD$ signifies minimum drag for the given length and diameter, and when $C = {1 \over 3}$, $LV$ indicates minimum drag for a given length and volume. The Haack series nose cones are not perfectly tangent to the body at their base except for the case where $C = {2 \over 3}$. However, the discontinuity is usually so slight as to be imperceptible. For $C > {2 \over 3}$, Haack nose cones bulge to a maximum diameter greater than the base diameter. Haack nose tips do not come to a sharp point, but are slightly rounded.

$\theta = \arccos \Bigl(1 - {2X \over L}\Bigr)$

$y = {R \over \sqrt{\pi}} \sqrt{\theta-{\sin({2\theta})\over2}+C \sin^3({\theta})}$

Where:

$C = {1 \over 3}$ for LV-Haack

$C = 0$ for LD-Haack

```python
C = 1/3

f = lambda x: (R/np.sqrt(np.pi))*np.sqrt((np.arccos(1 - (2*x)/L)) - (np.sin(2 * (np.arccos(1 - (2*x)/L))))/2 + C * np.sin((np.arccos(1 - (2*x)/L)))**3)
y = f(x)

points = build_2d(x, y[::-1])
```

```python
build_3d(points, 'haak', S, M)
```

    VBox(children=(HTML(value=''), Renderer(background='#cccc88', background_opacity=0.0, camera=PerspectiveCamera…

```python

```


```python
import numpy as np
import scipy.misc
from scipy.fftpack import dct, idct
import sys
from PIL import Image
import matplotlib
import matplotlib.pyplot as plt
import random
from tqdm._tqdm_notebook import tqdm_notebook
from scipy.fftpack import dct, idct
import seaborn as sns
from skimage.metrics import structural_similarity as ssim
import pandas as pd
import sympy

%matplotlib inline

class ImageLoader:
    def __init__(self, FILE_PATH):
        self.img = np.array(Image.open(FILE_PATH))
        # 行数
        self.row_blocks_count = self.img.shape[0] // 8
        # 列数
        self.col_blocks_count = self.img.shape[1] // 8

    def get_points(self, POINT):
        Row = random.randint(0, len(self.img) - POINT - 1)
        Col = random.randint(0, len(self.img) - 1)
        return self.img[Row : Row + POINT, Col]

    def get_block(self, col, row):
        return self.img[col * 8 : (col + 1) * 8, row * 8 : (row + 1) * 8]

# plt.rcParams['font.family'] ='sans-serif'#使用するフォント
# plt.rcParams["font.sans-serif"] = "Source Han Sans"
plt.rcParams["font.family"] = "Source Han Sans JP"  # 使用するフォント
plt.rcParams["xtick.direction"] = "in"  # x軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["ytick.direction"] = "in"  # y軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["xtick.major.width"] = 1.0  # x軸主目盛り線の線幅
plt.rcParams["ytick.major.width"] = 1.0  # y軸主目盛り線の線幅
plt.rcParams["font.size"] = 12  # フォントの大きさ
plt.rcParams["axes.linewidth"] = 1.0  # 軸の線幅edge linewidth。囲みの太さ
matplotlib.font_manager._rebuild()

MONO_DIR_PATH = "../../Mono/"
AIRPLANE = ImageLoader(MONO_DIR_PATH + "airplane512.bmp")
BARBARA = ImageLoader(MONO_DIR_PATH + "barbara512.bmp")
BOAT = ImageLoader(MONO_DIR_PATH + "boat512.bmp")
GOLDHILL = ImageLoader(MONO_DIR_PATH + "goldhill512.bmp")
LENNA = ImageLoader(MONO_DIR_PATH + "lenna512.bmp")
MANDRILL = ImageLoader(MONO_DIR_PATH + "mandrill512.bmp")
MILKDROP = ImageLoader(MONO_DIR_PATH + "milkdrop512.bmp")
SAILBOAT = ImageLoader(MONO_DIR_PATH + "sailboat512.bmp")

IMAGES = [
    AIRPLANE,
    BARBARA,
    BOAT,
    GOLDHILL,
    LENNA,
    MANDRILL,
    MILKDROP,
    SAILBOAT
]
```

```python
class DMLCT:
    def __init__(self, n_bar, N):
        self.n_bar = n_bar
        self.N = N

        self.x_l = (2 * np.arange(N) + 1) / (2 * N)
        self.s_l = np.arange(n_bar) / (n_bar - 1)
        self.xi = (np.arange(n_bar + 1) - 0.5) / (n_bar - 1)

        self.lambda_kh = self.get_lambda_kh(self.n_bar)

        self.w_k_j = self.get_w_k_j(self.n_bar, self.N)
        self.W_L_k_kh = self.get_W_L_k_kh(self.n_bar, self.N)
        self.W_k_kh = self.get_W_k_kh(self.n_bar, self.N)
        self.W_R_k_kh = self.get_W_R_k_kh(self.n_bar, self.N)

    def Lagrange_j(self, j):
        x = sympy.Symbol("x")
        L_x = 1.0
        for l in range(self.n_bar):
            if l != j:
                L_x *= (x - self.s_l[l]) / (self.s_l[j] - self.s_l[l])
        return sympy.integrate(L_x)

    def get_lambda_kh(self, n_bar):
        lambda_kh = np.ones(n_bar)
        lambda_kh[0] = np.sqrt(1 / 2)
        return lambda_kh

    def get_w_k_j(self, n_bar, N):
        L_j = np.zeros((n_bar, N))
        x = sympy.Symbol("x")
        for j in range(n_bar):
            temp = []
            Lj = self.Lagrange_j(j)
            for k in range(N):
                temp.append(Lj.subs(x, self.x_l[k]))
            L_j[j] = np.array(temp)

        w_k_j = np.zeros((n_bar, N))
        for j in range(n_bar):
            w_k_j[j] = scipy.fftpack.dct(L_j[j], norm="ortho")
        return w_k_j

    def get_W_L_k_kh(self, n_bar, N):
        W_L_k_kh = np.zeros((n_bar - 1, N))
        lambda_kh = self.get_lambda_kh(n_bar)

        for kh in range(n_bar - 1):
            W_L_k_kh[kh] = (
                (1 - n_bar)
                * np.sqrt(2 / N)
                * lambda_kh[kh]
                * np.cos(np.pi * kh * (self.xi[0] + 1))
                * self.w_k_j[0]
            )
        return W_L_k_kh

    def get_W_k_kh(self, n_bar, N):
        W_k_kh = np.zeros((n_bar - 1, N))
        for kh in range(n_bar - 1):
            sum_sin = np.zeros(N)
            for j in range(1, n_bar - 2 + 1):
                sum_sin += np.sin(np.pi * kh * self.s_l[j]) * self.w_k_j[j]

            W_k_kh[kh] = (
                (n_bar - 1)
                * np.sqrt(2 / N)
                * self.lambda_kh[kh]
                * (
                    np.cos(np.pi * kh * self.xi[1])
                    * (self.w_k_j[0] - (-1) ** (kh) * self.w_k_j[n_bar - 1])
                    - 2 * np.sin((np.pi * kh) / (2 * (n_bar - 1))) * sum_sin
                )
            )
        return W_k_kh

    def get_W_R_k_kh(self, n_bar, N):
        W_R_k_kh = np.zeros((n_bar - 1, N))
        for kh in range(n_bar - 1):
            W_R_k_kh[kh] = (
                (n_bar - 1)
                * np.sqrt(2 / N)
                * self.lambda_kh[kh]
                * np.cos(np.pi * kh * (self.xi[n_bar] - 1))
                * self.w_k_j[n_bar - 1]
            )
        return W_R_k_kh
```

```python
def get_F_L_k_horizontal(arr, N, row, col):
    # w
    if col == 0:
        w_block = np.zeros(N)
    else:
        w_block = arr[row, (col - 1) * N : col * N]

    return w_block
```

```python
def get_F_R_k_horizontal(arr, N, row, col):
    # e
    if col == arr.shape[1] // N - 1:
        e_block = np.zeros(N)
    else:
        e_block = arr[row, (col + 1) * N : (col + 2) * N]

    return e_block
```

```python
def get_F_L_k_vertical(arr, N, row, col):
    # n
    if row == 0:
        n_block = np.zeros(N)
    else:
        n_block = arr[(row - 1) * N : row * N, col]

    return n_block
```

```python
def get_F_R_k_vertical(arr, N, row, col):
    # s
    if row == arr.shape[0] // N - 1:
        s_block = np.zeros(N)
    else:
        s_block = arr[(row + 1) * N : (row + 2) * N, col]

    return s_block
```

```python
# n_bar = 4
N = 16
```

```python
# dmlct = DMLCT(n_bar, N)
```

```python
IMG = LENNA
```

```python
Fk = np.zeros(IMG.img.shape)
```

# 順変換

```python
def DMLCT_forward(IMG,n_bar,N):
    Fk = np.zeros(IMG.img.shape)
    ## 縦方向
    ### DCT
    for row in range(IMG.img.shape[0] // N):
        for col in range(IMG.img.shape[1]):
            eight_points = IMG.img[N * row : N * (row + 1), col]
            c = scipy.fftpack.dct(eight_points, norm="ortho")
            Fk[N * row : N * (row + 1), col] = c
    ### 残差
    dmlct = DMLCT(n_bar,N)
    for row in range(IMG.img.shape[0] // N):
        for col in range(IMG.img.shape[1]):
            # ビューなら直接いじっちゃう
            F = Fk[N * row : N * (row + 1), col]
            F_L = get_F_L_k_vertical(Fk, N, row, col)
            F_R = get_F_R_k_vertical(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)
            for kh in range(n_bar - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
            F[n_bar - 2 + 1 :] -= U_k_n_bar[n_bar - 2 + 1 :]
    # 0を残す
    for k in reversed(range(1, n_bar - 2 + 1)):
        dmlct = DMLCT(k+1, N)
        for row in range(IMG.img.shape[0] // N):
            for col in range(IMG.img.shape[1]):
                # ビューなら直接いじっちゃう
                F = Fk[N * row : N * (row + 1), col]
                F_L = get_F_L_k_vertical(Fk, N, row, col)
                F_R = get_F_R_k_vertical(Fk, N, row, col)

                U_k_n_bar = np.zeros(N)

                for kh in range((k + 1) - 2 + 1):
                    U_k_n_bar += (
                        F_L[kh] * dmlct.W_L_k_kh[kh]
                        + F[kh] * dmlct.W_k_kh[kh]
                        + F_R[kh] * dmlct.W_R_k_kh[kh]
                    )

                F[k] -= U_k_n_bar[k]
    ## 横方向
    ### DCT
    for row in range(Fk.shape[0]):
        for col in range(Fk.shape[1] // N):
            eight_points = Fk[row, N * col : N * (col + 1)]
            c = scipy.fftpack.dct(eight_points, norm="ortho")
            Fk[row, N * col : N * (col + 1)] = c

    ### 残差
    dmlct = DMLCT(n_bar,N)
    for row in range(IMG.img.shape[0]):
        for col in range(IMG.img.shape[1] // N):
            F = Fk[row, N * col : N * (col + 1)]
            F_L = get_F_L_k_horizontal(Fk, N, row, col)
            F_R = get_F_R_k_horizontal(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)
            for kh in range(n_bar - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
            F[n_bar - 2 + 1 :] -= U_k_n_bar[n_bar - 2 + 1 :]
    # 0を残す
    for k in reversed(range(1, n_bar - 2 + 1)):
        dmlct = DMLCT(k+1, N)
        for row in range(IMG.img.shape[0]):
            for col in range(IMG.img.shape[1] // N):
                F = Fk[row, N * col : N * (col + 1)]
                F_L = get_F_L_k_horizontal(Fk, N, row, col)
                F_R = get_F_R_k_horizontal(Fk, N, row, col)

                U_k_n_bar = np.zeros(N)

                for kh in range((k + 1) - 2 + 1):
                    U_k_n_bar += (
                        F_L[kh] * dmlct.W_L_k_kh[kh]
                        + F[kh] * dmlct.W_k_kh[kh]
                        + F_R[kh] * dmlct.W_R_k_kh[kh]
                    )

                F[k] -= U_k_n_bar[k]

    return Fk
```

```python
# DCT係数の平均を求める
```

```python
Fk_values = np.zeros((512,512))
```

```python
for IMG in tqdm_notebook(IMAGES):
    values = np.zeros((25,4))
    Fk = np.zeros(IMG.img.shape)
    for row in range(IMG.img.shape[0] // N):
        for col in range(IMG.img.shape[1] // N):
            block = IMG.img[row * N : (row + 1) * N, col * N : (col + 1) * N]
            c = scipy.fftpack.dct(
                scipy.fftpack.dct(block, axis=0, norm="ortho"), axis=1, norm="ortho"
            )
            Fk[row * N : (row + 1) * N, col * N : (col + 1) * N] = c

    Fk_values += np.abs(Fk)
```

    HBox(children=(IntProgress(value=0, max=8), HTML(value='')))

```python
Fk_values /= len(IMAGES)
pd.DataFrame(Fk_values).to_csv("DCT_coef_ave.csv",header=False,index=False)
```

```python
# 各n_barの残差係数の平均を求める
```

```python
Vk_values = np.zeros((512,512))
```

```python
n_bar = 5
for n_bar in tqdm_notebook(range(2,n_bar+1)):
    Vk_values = np.zeros((512,512))
    for IMG in IMAGES:
        Fk = DMLCT_forward(IMG,n_bar,N)
        Vk_values += np.abs(Fk)

    pd.DataFrame(Vk_values / len(IMAGES)).to_csv("DMLCT_" + str(n_bar) + "_coef_ave.csv",header=False,index=False)
```

    HBox(children=(IntProgress(value=0, max=4), HTML(value='')))

```python
# DCT係数を読み込む
```

```python
Fk_values = pd.read_csv("DCT_coef_ave.csv",header=None).values
```

```python
# 残差係数を読み込む
```

```python
Vk_values_arr = []
for i in range(2,n_bar+1,1):
    Vk_values_arr.append(pd.read_csv("DMLCT_" + str(i) + "_coef_ave.csv",header=None).values)
```

```python
# NxNブロック1個当たりの係数の平均を求める
```

```python
Fk_block_ave_values = np.zeros((N,N))
for row in range(IMG.img.shape[0] // N):
    for col in range(IMG.img.shape[1] // N):
        if col == 0:
            continue
        if col == IMG.img.shape[1] // N -1:
            continue
        if row == 0:
            continue
        if row == IMG.img.shape[0] // N -1:
            continue
        block = Fk_values[row * N : (row + 1) * N, col * N : (col + 1) * N]
        Fk_block_ave_values += np.abs(block)

Fk_block_ave_values /= (IMG.img.shape[0]//N)**2
```

```python
Vk_block_ave_values_arr = []
for Vk_values in Vk_values_arr:
    Vk_block_ave_values = np.zeros((N,N))
    for row in range(IMG.img.shape[0] // N):
        for col in range(IMG.img.shape[1] // N):
            if col == 0:
                continue
            if col == IMG.img.shape[1] // N -1:
                continue
            if row == 0:
                continue
            if row == IMG.img.shape[0] // N -1:
                continue
            block = Vk_values[row * N : (row + 1) * N, col * N : (col + 1) * N]
            Vk_block_ave_values += np.abs(block)

    Vk_block_ave_values /= (IMG.img.shape[0]//N)**2
    Vk_block_ave_values_arr.append(Vk_block_ave_values)
```

```python
columns = []
for n in reversed(range(2,n_bar+1,1)):
    columns.append("n=" + str(n))
df = pd.DataFrame(columns=columns)
```

```python
k_max = 5
for index in range(1,k_max,1):
    for i in range(index):
        Gk1k2_1 = []
        Gk1k2_2 = []
        for n in reversed(range(2,n_bar+1,1)):
            Vk_block = Vk_block_ave_values_arr[n-2]
            Vk = Vk_block[i,index]
            Fk = Fk_block_ave_values[i,index]
            Gk1k2_1.append(100 * (1 - Vk/Fk))

            Vk = Vk_block[index,i]
            Fk = Fk_block_ave_values[index,i]
            Gk1k2_2.append(100 * (1 - Vk/Fk))

        df.loc["(" + str(i) + "," + str(index) + ")"] = Gk1k2_1
        df.loc["(" + str(index) + "," + str(i) + ")"] = Gk1k2_2

    Gk1k2 = []
    for n in reversed(range(2,n_bar+1,1)):
        Vk_block = Vk_block_ave_values_arr[n-2]
        Vk = Vk_block[index,index]
        Fk = Fk_block_ave_values[index,index]
        Gk1k2.append(100 * (1 - Vk/Fk))

    df.loc["(" + str(index) + "," + str(index) + ")"] = Gk1k2

Gk1k2 = []
for n in reversed(range(2,n_bar+1,1)):
    Vk_block = Vk_block_ave_values_arr[n-2]

    Vk_sum = 0
    Fk_sum = 0
    for row in range(N):
        for col in range(N):
            if row > k_max-1 or col > k_max-1:
                Vk_sum += Vk_block[row,col]
                Fk_sum += Fk_block_ave_values[row,col]
    Gk1k2.append(100 * (1 - Vk_sum/Fk_sum))
df.loc["others"] = Gk1k2
```

```python
df.to_csv("DMLCT_high_freq_comp.csv")
df
```

      n=5
      n=4
      n=3
      n=2

      (0,1)
      18.310981
      18.310981
      18.310981
      18.310981

      (1,0)
      21.832817
      21.832817
      21.832817
      21.832817

      (1,1)
      14.575232
      14.575232
      14.575232
      14.575232

      (0,2)
      17.877560
      17.877560
      17.877560
      9.336089

      (2,0)
      16.195630
      16.195630
      16.195630
      8.333530

      (1,2)
      11.987230
      11.987230
      11.987230
      7.455402

      (2,1)
      12.711960
      12.711960
      12.711960
      7.439948

      (2,2)
      12.214650
      12.214650
      12.214650
      5.681808

      (0,3)
      12.405071
      12.405071
      8.628719
      3.997779

      (3,0)
      10.430801
      10.430801
      7.309138
      3.667256

      (1,3)
      7.824086
      7.824086
      5.045261
      2.374177

      (3,1)
      8.051693
      8.051693
      5.461716
      2.826230

      (2,3)
      8.804471
      8.804471
      6.944298
      2.749921

      (3,2)
      7.777447
      7.777447
      5.828321
      2.416075

      (3,3)
      6.360546
      6.360546
      3.840187
      0.963791

      (0,4)
      9.997110
      8.267106
      7.113365
      3.912433

      (4,0)
      7.706722
      6.083833
      5.606183
      3.186106

      (1,4)
      4.039410
      2.487527
      2.614220
      1.213619

      (4,1)
      4.838427
      3.434484
      3.363089
      1.906202

      (2,4)
      5.953920
      4.662677
      4.576186
      2.363430

      (4,2)
      4.229894
      3.112613
      3.123368
      1.878314

      (3,4)
      5.651344
      4.630047
      3.272790
      0.999370

      (4,3)
      4.845592
      3.681487
      2.855581
      0.987264

      (4,4)
      3.622517
      1.985599
      2.198705
      0.982007

      others
      1.054861
      1.299350
      0.865533
      0.328743

```python

```


# Imports

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Arrow
from matplotlib.animation import FuncAnimation
from mpl_toolkits.axes_grid.inset_locator import inset_axes
%matplotlib notebook

import re
from scipy.integrate import odeint, solve_ivp
```

# Decalaring Paths

```python
results_path = '../Notebook Results/Chapter 1/'
```

# Initializations

```python
%%html
<style>
.output_wrapper button.btn.btn-default,
.output_wrapper .ui-dialog-titlebar {
  display: none;
}
</style>
```

<style>
.output_wrapper button.btn.btn-default,
.output_wrapper .ui-dialog-titlebar {
  display: none;
}
</style>

```python
plt.rcParams['figure.dpi'] = 80
plt.rcParams['figure.figsize'] = (12.5,6)
```

# Body Mass Simulation

**Plane Surface Without Air Drag**

\begin{equation}
\begin{aligned}
F &= ma \\
-\mu mg &\Leftrightarrow ma \\
ma &= -\mu mg \\
\frac{dv}{dt} &= -\mu g \\
\int_{\mathrm{t}=0}^{\mathrm{t}=t} dv &= \int_{\mathrm{t}=0}^{\mathrm{t}=t}-\mu g dt \\
v - v_{0}&= - \mu gt\\
v &= v_{0} - \mu gt\\
\end{aligned}
\end{equation}

**Plane Surface With Air Drag**

\begin{equation}
\begin{aligned}
F &= ma \\
-\mu mg &\Leftrightarrow ma \\
ma &= -\mu mg \\ \\ \\
Adding\ the\ Air\ Drag\ Factor\ -\delta \\
\frac{dv}{dt} &= -\mu g - \delta v\\
\int_{\mathrm{v}=v_{0}}^{\mathrm{v}=v} \frac{dv}{-\mu g - \delta v} &= \int_{\mathrm{t}=0}^{\mathrm{t}=t}dt \\
-\frac{1}{\delta} ln \left(\frac{-\mu g - \delta v}{-\mu g - \delta v_{0}}\right) &= t - 0 \\
\frac{-\mu g - \delta v(t)}{-\mu g - \delta v_{0}} &= e^{-\delta t} \\
v(t) &= (\frac{\delta}{\mu} g + v_{0})e^{-\delta t} - \frac{\delta}{\mu} g\\
\end{aligned}
\end{equation}

```python
class PlaneSimulator:

    def __init__(self, mass_pos_0 = (0,0), vel_0 = 20, mu_s = 0.01,
                 g_force = 9.8, air_drag = 0.01, body_mass = 2,
                 timesteps = 30, vector_skips=5, interval=5):

        self.plane = 0.2

        self.mass_pos_x_0 = mass_pos_0[0]
        self.mass_pos_y_0 = mass_pos_0[1] + self.plane
        self.vel_0        = vel_0

        self.mass_height  = 0.03
        self.mass_width   = 2.0
        self.mass_pos_x_0 = self.mass_pos_x_0 + self.mass_width/2
        self.mass_pos_y_0 = self.mass_pos_y_0

        self.mu_s = mu_s
        self.g_force = g_force
        self.air_drag = air_drag
        self.body_mass = body_mass

        self.timesteps = timesteps
        self.sim_time = np.linspace(0, self.timesteps, 20*(self.timesteps)+1)
        self.interval = interval

    @staticmethod
    def get_deriv(t, state, g, mu, ad):

        _pos_x = state[0]
        _pos_y = state[1]
        _vel = state[2]

        d_pos_x = _vel
        d_pos_y = 0
        d_vel = - mu*g*t - ad*_vel if _vel > 0 else 0.0# Infinitesmly Small Acceleration

        next_sate = [d_pos_x, d_pos_y, d_vel]

        return next_sate

    def calculate_deriv(self):

        state_0 = [self.mass_pos_x_0, self.mass_pos_y_0, self.vel_0]
        oderes = odeint(self.get_deriv, state_0 , self.sim_time,
                        args=(self.g_force, self.mu_s, self.air_drag),
                        tfirst=True)

        self._pos_x, self._pos_y, self._vel = oderes.T
        self._accel = -np.array([self.g_force*self.mu_s]*self._vel.shape[0]) - self.air_drag*self._vel
        self.oderes = np.c_[self._pos_x, self._pos_y, self._vel, self._accel]
        self.velocity_vector = self.oderes[:,2]/max(self.oderes[:,2])

    def init_plot(self):

        # Basic Axes setup
        if self.air_drag == 0.0:
            self.axes.set_title(r'$Dynamics\ Of\ Motion\ in\ Plane$')
        else:
            self.axes.set_title(r'$Dynamics\ Of\ Motion\ in\ Plane\ With\ Air\ Drag$')
        self.axes.set_facecolor('k')
        self.axes.get_yaxis().set_ticks([])
        self.axes.set_xlim(0, np.max(self.oderes[:,0])*1.2)
        self.axes.set_ylim(0, self.plane*1.5)

        # Create plane
        self.axes.fill_between(np.linspace(0, np.max(self.oderes[:,0])*1.2, 10000),
                               self.plane, 0,
                               color='#e8a864')

        # Set up the mass
        pos_x, pos_y = self.oderes[:,0][0]+self.mass_width/4, self.oderes[:,1][0]+self.mass_height/4
        vel_0 = self.oderes[:,2][0]
        bboxprop = dict(facecolor='wheat',boxstyle='round', color='w', lw=2)
        self.masstext = self.axes.text(pos_x, pos_y, r'$m$', color='#63492a', fontsize = 20, bbox = bboxprop)
        self.velocity_vector = self.axes.annotate('$Velocity$ = '+str(vel_0) + '$ms^{-1}$', xy=(pos_x+15,pos_y), xytext=(50,pos_y-.003), color='w',
                                                   arrowprops = {'arrowstyle': "<|-", 'color':'w'})

        # Set up the Time Patch
        bbox_prop = dict(facecolor='wheat',boxstyle='round', edgecolor='w', lw=3)
        self.time_text = r'$Time : {0}$'.format(self.sim_time[0])
        self.timetext = self.fig.text(0.8, 0.85, self.time_text,
                                       color='#63492a', fontsize = 20,
                                       bbox = bbox_prop)

        # Setup the Velocity and Acceleration axes
        self.va_axes = inset_axes(self.axes, width="40%", height="30%", loc=4,
                                  bbox_to_anchor=(-0.12,0.1,1,1), bbox_transform=self.axes.transAxes)
        self.va_axes.patch.set_alpha(0.5)
        self.va_axes.set_facecolor('#ffefd6')
        for spine in self.va_axes.spines.values(): spine.set_edgecolor('white')
        self.va_axes.grid(color='w', linestyle='-.', linewidth=0.3)
        self.va_axes.tick_params(axis='both', colors='w')
        self.va_axes.set_xlabel(r'$Time \longrightarrow$', color='w')
        self.va_axes.set_ylabel(r'$Velocity(ms^{-1}) \longrightarrow$', color='#2278b3')
        self.va_axes_ty = self.va_axes.twinx()
        for spine in self.va_axes_ty.spines.values(): spine.set_edgecolor('white')
        self.va_axes_ty.tick_params(axis='both', colors='w')
        self.va_axes_ty.set_ylabel(r'$Acceleration(ms^{-2}) \longrightarrow$', color='#944601')

        self.velocityplt, = self.va_axes.plot((self.sim_time[0], self.oderes[:,2][0]), linestyle = '-.')
        self.velocitysct = self.va_axes.scatter(self.sim_time[0], self.oderes[:,2][0], edgecolor = 'w')
        self.velocityplt.set_data(self.sim_time[:10], self.oderes[:,2][:10])

        self.accelplt, = self.va_axes_ty.plot((self.sim_time[0], self.oderes[:,3][0]), linestyle = '-.')
        self.accelsct = self.va_axes_ty.scatter(self.sim_time[0], self.oderes[:,3][0],
                                                color='#944601', edgecolor = 'w')

        return self.fig,

    def update_plot(self, i):

        curr_vel = np.round(np.clip(self.oderes[:,2][i-1], 0, self.vel_0), 2)
        curr_accel = self.oderes[:,3][i-1]
        curr_x = self.oderes[:,0][i-1]
        curr_y =  self.oderes[:,1][i-1]
        prev_time, curr_time = self.sim_time[i], self.sim_time[i-1]

        # Update the Time Text
        if curr_vel > 0.0:
            self.timetext.set_text(r'$Time : {0}$'.format(np.round(curr_time, 1)))
        elif float(curr_vel) == 0.0 and i != 0 :
            self.timetext.set_color('r')

        # Update the block Position
        pos_x, pos_y = curr_x+self.mass_width/4, curr_y+self.mass_height/4
        self.masstext.set_position((pos_x, pos_y))

        # Update the Velocity Vector for the mass block
        mul_factor = (curr_vel/self.vel_0)
        self.velocity_vector.set_text('$Velocity$ = '+ str(curr_vel)+ '$ms^{-1}$')
        self.velocity_vector.xy = (pos_x+12*(self.vel_0/20), pos_y)
        self.velocity_vector.set_position((pos_x + 12*(self.vel_0/10) + 15*mul_factor, pos_y-0.003*mul_factor))
        self.velocity_vector.set_fontsize(10*mul_factor)

        # Update the Velocity and Acceleration Plot
        self.va_axes.set_xlim(0, curr_time*1.2)
        self.velocityplt.set_data(self.sim_time[:i], self.oderes[:,2][:i])
        self.accelplt.set_data(self.sim_time[:i], self.oderes[:,3][:i])
        self.velocitysct.set_offsets((prev_time, curr_vel))
        self.accelsct.set_offsets((prev_time, curr_accel))

        return self.fig,

    def start_simulation(self):

        self.fig, self.axes = plt.subplots()
        self.fig.tight_layout()
        self.calculate_deriv()
        self.ani=FuncAnimation(fig=self.fig, func=self.update_plot, init_func=self.init_plot, repeat = False,
                               interval=self.interval, blit=True, save_count = 1000, frames = 1000)
        plt.show()

```

```python
planeSim = PlaneSimulator(vel_0=20,air_drag = 0.0, timesteps=30)
planeSim.start_simulation()
```

    <IPython.core.display.Javascript object>

```python

```

```python

```


# [DRAFT] Precomputing near-field volumetric integrals.

## Setting up a test problem

```
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp

%config InlineBackend.figure_format='retina'
```

### Lagrange basis functions

```
N = 7
chebyshev_pts = [sp.cos(sp.pi * i / (N - 1)) for i in range(N)][::-1]
chebyshev_pts_np = np.array([float(p) for p in chebyshev_pts])
x = sp.var("x")
```

```
basis_functions = []
for i in range(N):
    xi = chebyshev_pts[i]
    prod = 1
    # The definition of the Lagrange interpolating polynomial.
    # In a numerical context, this definition is troublesome
    # and it's better to use the barycentric Lagrange formulas.
    # But this simple definition works fantastically well in
    # a symbolic setting.
    for j in range(N):
        if j == i:
            continue
        xj = chebyshev_pts[j]
        prod *= (x - xj) / (xi - xj)
    basis_functions.append(prod.simplify().expand())
```

```
basis_functions
```

### Precomputing coincident integrals with polar integration

```
C = 1.0 / (4 * np.pi)

def fundamental_solution(obsx, obsy, srcx, srcy):
    r2 = ((obsx - srcx) ** 2) + ((obsy - srcy) ** 2)
    return C * np.log(r2)
```

```
sx, sy = sp.var("sx, sy")
```

```
import scipy.integrate

def to_corner(ox, oy, cx, cy):
    t = np.arctan2(cy - oy, cx - ox)
    r = np.sqrt((cx - ox) ** 2 + (cy - oy) ** 2)
    return [t, r]

def compute_coincident(obsx, obsy, basis):
    tol = 1e-16

    def F(srcR, srcT):
        if srcR == 0:
            return 0
        srcx = obsx + np.cos(srcT) * srcR
        srcy = obsy + np.sin(srcT) * srcR
        out = srcR * basis(srcx, srcy) * fundamental_solution(obsx, obsy, srcx, srcy)
        return out

    corner_vecs = [
        to_corner(obsx, obsy, 1, 1),
        to_corner(obsx, obsy, -1, 1),
        to_corner(obsx, obsy, -1, -1),
        to_corner(obsx, obsy, 1, -1),
    ]
    # Normally the theta value for corner idx 2 is negative because it
    # is greater than Pi and the output range of arctan2 is [-pi,pi]
    # But, if the observation point is on the bottom edge of the domain (y=-1)
    # then it's possible for the the theta value to be exactly pi. If this is the
    # case it will be positive and will mess up the integration domains for
    # integrals 2 and 3. So, if it's positive here, we loop around and make
    # it negative.
    if corner_vecs[2][0] > 0:
        corner_vecs[2][0] -= 2 * np.pi

    subdomain = [
        [corner_vecs[0][0], corner_vecs[1][0], lambda t: (1.0 - obsy) / np.sin(t)],
        [
            corner_vecs[1][0],
            corner_vecs[2][0] + 2 * np.pi,
            lambda t: (-1.0 - obsx) / np.cos(t),
        ],
        [corner_vecs[2][0], corner_vecs[3][0], lambda t: (-1.0 - obsy) / np.sin(t)],
        [corner_vecs[3][0], corner_vecs[0][0], lambda t: (1.0 - obsx) / np.cos(t)],
    ]

    Is = []
    for d in subdomain:
        I = scipy.integrate.dblquad(F, d[0], d[1], 0.0, d[2], epsabs=tol, epsrel=tol)
        Is.append(I)

    result = sum([I[0] for I in Is])
    err = sum([I[1] for I in Is])
    return result, err
```

```
import pickle

with open("data/constant_test_integral.pkl", "rb") as f:
    coincident, nearfield = pickle.load(f)

ox, oy = sp.symbols("ox, oy")
constant_soln_coincident = sp.lambdify((ox, oy), coincident, "numpy")
constant_soln_nearfield = sp.lambdify((ox, oy), nearfield, "numpy")
```

```
%%time
est = compute_coincident(-0.5, -0.5, lambda sx, sy: 1.0)
true = constant_soln_coincident(-0.5, -0.5)
est[0], true, est[0] - true
```

```
est = compute_coincident(1, 1, lambda sx, sy: 1.0)
true = constant_soln_coincident(1 - 1e-7, 1 - 1e-7)
est[0], true, est[0] - true
```

```
est = compute_coincident(0, 1, lambda sx, sy: 1.0)
true = constant_soln_coincident(0, -1 + 1e-7)
est[0], true, est[0] - true
```

```
import multiprocessing

def mp_compute_coincident(obsx, obsy, srci, srcj):
    basis_sxsy = basis_functions[srci].subs(x, sx) * basis_functions[srcj].subs(x, sy)
    basis = sp.lambdify((sx, sy), basis_sxsy, "numpy")
    return compute_coincident(obsx, obsy, basis)

def get_inputs(obs_scale, obs_offsetx, obs_offsety):
    inputs = []
    for obsi in range(N):
        for obsj in range(N):
            obsx = obs_scale * chebyshev_pts_np[obsi] + obs_offsetx
            obsy = obs_scale * chebyshev_pts_np[obsj] + obs_offsety
            for srci in range(N):
                for srcj in range(N):
                    inputs.append((obsx, obsy, srci, srcj))
    return inputs

multiprocessing.set_start_method('fork')
def coincident_grid():
    inputs = get_inputs(1, 0, 0)
    p = multiprocessing.Pool(5)
    return np.array(p.starmap(mp_compute_coincident, inputs))
```

```
%%time
np.save("data/coincident_grid.npy", coincident_grid())
```

```
# integrals_and_err = compute_grid(1, 0, 0)
integrals_and_err = np.load("data/coincident_grid.npy", allow_pickle=True)
integrals = integrals_and_err[:, 0].reshape((N, N, N, N))
error = integrals_and_err[:, 1].reshape((N, N, N, N))
```

There are no estimated errors greated than `5e-15`:

```
np.where(error > 5e-15)[0].shape[0]
```

```
for i in range(1, N - 1):
    for j in range(1, N - 1):
        err = (
            constant_soln_coincident(chebyshev_pts_np[i], chebyshev_pts_np[j])
            - integrals[i, j, :, :].sum()
        )
        print(err)
```

```
with open("data/xy_test_integral.pkl", "rb") as f:
    coincident, nearfield = pickle.load(f)
xy_soln_coincident = sp.lambdify((ox, oy), coincident, "numpy")
xy_soln_nearfield = sp.lambdify((ox, oy), nearfield, "numpy")
xy_laplacian = lambda x, y: (1 - (1 - x) ** 3) * (1 - (y + 1) ** 2)
```

```
cheb2dX, cheb2dY = np.meshgrid(chebyshev_pts_np, chebyshev_pts_np)
cheb2d = np.array([cheb2dX, cheb2dY]).T.reshape((-1, 2)).copy()
```

```
f = xy_laplacian(cheb2d[:, 0], cheb2d[:, 1])

for i in range(1, N - 1):
    for j in range(1, N - 1):
        true = xy_soln_coincident(chebyshev_pts_np[i], chebyshev_pts_np[j])
        est = integrals[i, j, :, :].ravel().dot(f.ravel())
        err = true - est
        print(err)
```

## Pre-computing adjacent integrals

```
def is_on_source_edge(obsx, obsy):
    on_left_right_edges = np.abs(obsx) == 1 and np.abs(obsy) <= 1
    on_top_bottom_edges = np.abs(obsy) == 1 and np.abs(obsx) <= 1
    return on_left_right_edges or on_top_bottom_edges

def compute_nearfield(obsx, obsy, basis):
    if is_on_source_edge(obsx, obsy):
        return compute_coincident(obsx, obsy, basis)

    tol = 1e-16

    def F(srcy, srcx):
        return basis(srcx, srcy) * fundamental_solution(obsx, obsy, srcx, srcy)

    I = scipy.integrate.dblquad(F, -1, 1, -1, 1, epsabs=tol, epsrel=tol)
    return I
```

```
est = compute_nearfield(1.1, 1.1, lambda x, y: 1.0)
true = constant_soln_nearfield(1.1, 1.1)
est[0], true, est[0] - true
```

```
est = compute_nearfield(-1.1, -1.1, xy_laplacian)
true = xy_soln_nearfield(-1.1, -1.1)
est[0], true, est[0] - true
```

```
est = compute_nearfield(1.0, 1.0, lambda x, y: 1.0)
true = constant_soln_nearfield(1.0 + 1e-7, 1.0 + 1e-7)
est[0], true, est[0] - true
```

```
def mp_compute_nearfield(obsx, obsy, srci, srcj):
    basis_sxsy = basis_functions[srci].subs(x, sx) * basis_functions[srcj].subs(x, sy)
    basis = sp.lambdify((sx, sy), basis_sxsy, "numpy")
    return compute_nearfield(obsx, obsy, basis)

def compute_grid(obs_scale, obs_offsetx, obs_offsety):
    inputs = get_inputs(obs_scale, obs_offsetx, obs_offsety)
    p = multiprocessing.Pool()
    return np.array(p.starmap(mp_compute_nearfield, inputs))
```

```
import matplotlib.patches as patches

xrange = [-1.5, 6]
yrange = [-1.5, 6]

def size_and_aspect():
    plt.xlim(*xrange)
    plt.ylim(*yrange)
    # plt.axis("off")
    ax = plt.gca()
    # ax.set_axis_off()
    plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0)
    plt.margins(0, 0)
    ax.xaxis.set_major_locator(plt.NullLocator())
    ax.yaxis.set_major_locator(plt.NullLocator())
    # plt.axis('equal')

plt.figure(figsize=(4, 8))

plt.subplot(3, 2, 1)
plt.gca().add_patch(patches.Rectangle((-1, -1), 2, 2, linewidth=1, edgecolor="k"))
plt.gca().add_patch(
    patches.Rectangle((1, 1), 2, 2, linewidth=1, edgecolor="k", facecolor="none")
)
plt.text(1.75, 1.65, "1", fontsize=30)
size_and_aspect()

plt.subplot(3, 2, 2)
plt.gca().add_patch(patches.Rectangle((-1, -1), 2, 2, linewidth=1, edgecolor="k"))
plt.gca().add_patch(
    patches.Rectangle((1, -1), 2, 2, linewidth=1, edgecolor="k", facecolor="none")
)
plt.text(1.75, -0.35, "2", fontsize=30)
size_and_aspect()

plt.subplot(3, 2, 3)
plt.gca().add_patch(patches.Rectangle((-1, -1), 2, 2, linewidth=1, edgecolor="k"))
plt.gca().add_patch(
    patches.Rectangle((1, 1), 4, 4, linewidth=1, edgecolor="k", facecolor="none")
)
plt.text(2.65, 2.65, "3", fontsize=30)
size_and_aspect()

plt.subplot(3, 2, 4)
plt.gca().add_patch(patches.Rectangle((-1, -1), 2, 2, linewidth=1, edgecolor="k"))
plt.gca().add_patch(
    patches.Rectangle((1, -1), 4, 4, linewidth=1, edgecolor="k", facecolor="none")
)
plt.text(2.65, 0.65, "4", fontsize=30)
size_and_aspect()

plt.subplot(3, 2, 5)
plt.gca().add_patch(patches.Rectangle((-1, -1), 2, 2, linewidth=1, edgecolor="k"))
plt.gca().add_patch(
    patches.Rectangle((1, 1), 1, 1, linewidth=1, edgecolor="k", facecolor="none")
)
plt.text(1.25, 1.13, "5", fontsize=30)
size_and_aspect()

plt.subplot(3, 2, 6)
plt.gca().add_patch(patches.Rectangle((-1, -1), 2, 2, linewidth=1, edgecolor="k"))
plt.gca().add_patch(
    patches.Rectangle((1, 0), 1, 1, linewidth=1, edgecolor="k", facecolor="none")
)
plt.text(1.25, 0.13, "6", fontsize=30)
size_and_aspect()

plt.show()
```

```
%%time
np.save("data/adj1_grid.npy", compute_grid(1, 2, 2))
```

```
%%time
np.save("data/adj2_grid.npy", compute_grid(1, 2, 0))
```

```
%%time
np.save("data/adj3_grid.npy", compute_grid(2, 3, 3))
```

```
%%time
np.save("data/adj4_grid.npy", compute_grid(2, 3, 1))
```

```
%%time
np.save("data/adj5_grid.npy", compute_grid(0.5, 1.5, 1.5))
```

```
%%time
np.save("data/adj6_grid.npy", compute_grid(0.5, 1.5, 0.5))
```

```
grid_filenames = [
    "data/coincident_grid.npy",
    "data/adj1_grid.npy",
    "data/adj2_grid.npy",
    "data/adj3_grid.npy",
    "data/adj4_grid.npy",
    "data/adj5_grid.npy",
    "data/adj6_grid.npy",
]
raw_grids = np.array([np.load(g, allow_pickle=True) for g in grid_filenames])
```

```
[raw_grids[i].shape for i in range(7)]
```

The estimated error is extremely small for all the integrals!

```
np.where(raw_grids[:, :, 1] > 5e-15)
```

```
all_integrals = raw_grids[:, :, 0].reshape((7, N, N, N ** 2))
```

## Rotations

```
def get_test_values(
    soln_fnc, obs_scale, obs_offsetx, obs_offsety, src_center=[0, 0], src_size=[2, 2]
):
    correct = np.zeros((N, N))
    for i in range(N):
        for j in range(N):
            obsx = obs_offsetx + obs_scale * chebyshev_pts_np[i]
            obsy = obs_offsety + obs_scale * chebyshev_pts_np[j]
            is_x_edge = np.abs(np.abs(obsx - src_center[0]) - (src_size[0] / 2)) < 1e-8
            is_y_edge = np.abs(np.abs(obsy - src_center[1]) - (src_size[1] / 2)) < 1e-8
            if is_x_edge or is_y_edge:
                correct[i, j] = np.nan
            else:
                correct[i, j] = soln_fnc(obsx, obsy)
    return correct
```

### Type 1

```
correct_upper_right = get_test_values(xy_soln_nearfield, 1.0, 2.0, 2.0)
correct_upper_left = get_test_values(xy_soln_nearfield, 1.0, -2.0, 2.0)
correct_lower_left = get_test_values(xy_soln_nearfield, 1.0, -2.0, -2.0)
correct_lower_right = get_test_values(xy_soln_nearfield, 1.0, 2.0, -2.0)
```

```
correct_upper_right
```

```
correct_upper_left
```

```
correct_lower_left
```

```
correct_lower_right
```

```
def nearfield_box(I, Fv, flipx, flipy, rotxy):
    Fv = Fv.reshape((N,N))

    n_rot = {
        (1, 1): 0,
        (1, -1): 1,
        (-1, -1): 2,
        (-1, 1): 3
    }[(flipx, flipy)]
    n_transpose = ((n_rot % 2) == 1) + rotxy

    # Rotate from input coordinates into position
    Fv = np.rot90(Fv, n_rot)
    if n_transpose % 2 == 1:
        Fv = Fv.T

    est = I.dot(Fv.ravel())
    # Reverse the transformation back to the original input space
    if n_transpose % 2 == 1:
        est = est.T
    est = np.rot90(est, -n_rot)
    return est

for C, flipx, flipy in [
    (correct_upper_right, 1, 1),
    (correct_upper_left, -1, 1),
    (correct_lower_left, -1, -1),
    (correct_lower_right, 1, -1),
]:
    Fv = xy_laplacian(cheb2d[:,0], cheb2d[:,1]).reshape((N,N))
    est = nearfield_box(all_integrals[1], Fv, flipx, flipy, 0)
    print(np.max(np.abs((C - est)[~np.isnan(C)])))
```

### Type 2

```
correct_middle_right = get_test_values(xy_soln_nearfield, 1.0, 2.0, 0.0)
correct_top_center = get_test_values(xy_soln_nearfield, 1.0, 0.0, 2.0)
correct_middle_left = get_test_values(xy_soln_nearfield, 1.0, -2.0, 0.0)
correct_bottom_center = get_test_values(xy_soln_nearfield, 1.0, 0.0, -2.0)
```

```
for C, flipx, flipy, rotxy in [
    (correct_middle_right, 1, 1, 0),
    (correct_top_center, 1, 1, 1),
    (correct_middle_left, -1, 1, 0),
    (correct_bottom_center, 1, -1, 1),
]:
    Fv = xy_laplacian(cheb2d[:,0], cheb2d[:,1])
    est = nearfield_box(all_integrals[2], Fv, flipx, flipy, rotxy)
    print(np.max(np.abs((C - est)[~np.isnan(C)])))
```

```
boxes = {
    # Type 0 (coincident)
    (1, 0, 0): (0, 1, 1, 0),
    # Type 1
    (1, 2, 2): (1, 1, 1, 0),
    (1, -2, 2): (1, -1, 1, 0),
    (1, -2, -2): (1, -1, -1, 0),
    (1, 2, -2): (1, 1, -1, 0),
    # Type 2
    (1, 2, 0): (2, 1, 1, 0),
    (1, 0, 2): (2, 1, 1, 1),
    (1, -2, 0): (2, -1, 1, 0),
    (1, 0, -2): (2, 1, -1, 1),
    # Type 3
    (2, 3, 3): (3, 1, 1, 0),
    (2, -3, 3): (3, -1, 1, 0),
    (2, -3, -3): (3, -1, -1, 0),
    (2, 3, -3): (3, 1, -1, 0),
    # Type 4
    (2, 1, 3): (4, 1, 1, 1),
    (2, -1, 3): (4, -1, 1, 1),
    (2, -3, 1): (4, -1, 1, 0),
    (2, -3, -1): (4, -1, -1, 0),
    (2, -1, -3): (4, -1, -1, 1),
    (2, 1, -3): (4, 1, -1, 1),
    (2, 3, -1): (4, 1, -1, 0),
    (2, 3, 1): (4, 1, 1, 0),
    # Type 5
    (0.5, 1.5, 1.5): (5, 1, 1, 0),
    (0.5, -1.5, 1.5): (5, -1, 1, 0),
    (0.5, -1.5, -1.5): (5, -1, -1, 0),
    (0.5, 1.5, -1.5): (5, 1, -1, 0),
    # Type 6
    (0.5, 0.5, 1.5): (6, 1, 1, 1),
    (0.5, -0.5, 1.5): (6, -1, 1, 1),
    (0.5, -1.5, 0.5): (6, -1, 1, 0),
    (0.5, -1.5, -0.5): (6, -1, -1, 0),
    (0.5, -0.5, -1.5): (6, -1, -1, 1),
    (0.5, 0.5, -1.5): (6, 1, -1, 1),
    (0.5, 1.5, -0.5): (6, 1, -1, 0),
    (0.5, 1.5, 0.5): (6, 1, 1, 0),
}
```

```
for box_loc, rot_params in boxes.items():
    soln_fnc = xy_soln_coincident if rot_params[0] == 0 else xy_soln_nearfield
    C = get_test_values(soln_fnc, *box_loc)
    Fv = xy_laplacian(cheb2d[:,0], cheb2d[:,1])
    est = nearfield_box(all_integrals[rot_params[0]], Fv, *rot_params[1:])
    print(np.max(np.abs((C - est)[~np.isnan(C)])))
```

## Scaling

The remaining piece is to scale the source and observation boxes so that they fit into the rotation scheme above.

```
def get_test_values(
    soln_fnc, obs_scale, obs_offsetx, obs_offsety, src_center=[0, 0], src_size=2
):
    correct = np.zeros((N, N))
    for i in range(N):
        for j in range(N):
            obsx = obs_offsetx + obs_scale * chebyshev_pts_np[i]
            obsy = obs_offsety + obs_scale * chebyshev_pts_np[j]
            is_x_edge = np.abs(np.abs(obsx - src_center[0]) - (src_size / 2)) < 1e-8
            is_y_edge = np.abs(np.abs(obsy - src_center[1]) - (src_size / 2)) < 1e-8
            if is_x_edge or is_y_edge:
                correct[i, j] = np.nan
            else:
                correct[i, j] = soln_fnc(obsx, obsy)
    return correct
```

```
with open("data/constant_transformed_test_integral.pkl", "rb") as f:
    constant_soln_shifted = [sp.lambdify((ox, oy), I, "numpy") for I in pickle.load(f)]
with open("data/xy_transformed_test_integral.pkl", "rb") as f:
    xy_soln_shifted = [sp.lambdify((ox, oy), I, "numpy") for I in pickle.load(f)]
```

```
basis_integrals = np.empty((N, N))
for srci in range(N):
    for srcj in range(N):
        basis_sxsy = basis_functions[srci].subs(x, sx) * basis_functions[srcj].subs(x, sy)
        basis = sp.lambdify((sx, sy), basis_sxsy, "numpy")
        I = scipy.integrate.dblquad(basis, -1, 1, -1, 1, epsabs=1e-16, epsrel=1e-16)
        basis_integrals[srci, srcj] = I[0]
```

```
def scale_integral(I, basis_dot_F, src_s):
    scale_T = src_s / 2.0
    C = scale_T ** 2
    log_factor = C * (1 / (2 * np.pi)) * np.log(scale_T)
    return C * I + log_factor * basis_dot_F

F_xy = lambda x, y: (1 - x) * (1 - y ** 2)
for i, mult in enumerate([1, 2, 4, 8, 16]):
    src_c = np.array([0, 0])
    src_s = mult / 4.0
    obs_c = np.array([src_c[0] + 0.5 * src_s, src_c[1] - 1.5 * src_s])
    #obs_c = np.array([src_c[0] - 0.5 * src_s, src_c[1] - 1.5 * src_s])

    obs_s = src_s * 2
    transformed_obs_center = np.round(2 * (obs_c - src_c) / src_s, decimals=1)
    transformed_obs_size = np.round(obs_s / src_s, decimals=1)

    correct_xy = get_test_values(
        xy_soln_shifted[i],
        obs_s / 2.0,
        obs_c[0],
        obs_c[1],
        src_center=src_c,
        src_size=src_s,
    )

    src_box_pts = cheb2d * 0.5 * src_s + src_c[None,:]
    Fv_xy = F_xy(src_box_pts[:,0], src_box_pts[:,1])

    nearfield_info = boxes[(transformed_obs_size, *transformed_obs_center)]
    integral_type, flipx, flipy, rotxy = nearfield_info

    basis_I_xy = basis_integrals.ravel().dot(Fv_xy.ravel())

    I_xy = nearfield_box(all_integrals[integral_type], Fv_xy, flipx, flipy, rotxy)

    est_xy = scale_integral(I_xy, basis_I_xy, src_s)

    print(f'\nfor source size={src_s}')
    print("xy error: ", np.max(np.abs(correct_xy - est_xy)[~np.isnan(correct_xy)]))
```

### Computing for an arbitrary box pair.

1. Scale the source box to have width and length 2.
2. Center the source box at `(0,0)`
3. Perform the same transformations on the observation box.
4. Because adjacent boxes are at most one level apart, they will fall into one of the 33 categories defined above.
5. Retrieve the integral type and rotation information and compute the integral!
6. Reverse the rotations.
7. Reverse the scaling via the equations above.

```
to_save = np.empty(2, object)
to_save[0] = basis_integrals
to_save[1] = all_integrals
np.save("data/nearfield_integrals.npy", to_save)
```