# 1. Introduction to Numpy
Numpy is a Library in python that specializes in dealing with multidimensional Arrays. The cool features of Numpy are
* **Automatic Checking :** Numpy ndArrays automatically check the consistancy of data. For instance, it is not possible to have 1st row with 2 elements and 2nd row with 3 elements
* **Contiguous Storage :** Unlike Python Lists, Numpy stores the data in contiguous Memory Locations, leading to lesser Space
* **Faster Vector Arithmatics :** Because of contiguous storage, the operations are performed faster as compared to default Python Execution for Lists

```python
# Importing Numpy
import numpy as np
```

## 1.1 Initialization of 1D array in Python
A numpy array comes with 2 important state variables. Just like Python, it automatically detects dtype (if not mentioned)
* dtype
* shape

#### 1.1.1 Initialization from python List
numpy.array( list **)**

```python
# Initializing from Python List
v = np.array([1,4,9,3,2])
print ('1D array in numpy is %s\n'%v)
print ('dtype of the numpy array is %s\n'%str(v.dtype))
print ('shape of the numpy array is %s\n'%v.shape)
```

    1D array in numpy is [1 4 9 3 2]

    dtype of the numpy array is int32

    shape of the numpy array is 5

#### 1.1.2 Initialization via arange
numpy.arange(**\[**start, **\]**stop, **\[**step, **\]** dtype=None)

```python
v1 = np.arange(5)
print ('Creating a numpy array via arange (stop=5) : %s\n'%v1)
v2 = np.arange(2,5)
print ('Creating a numpy array via arange (start=2,stop=5) : %s\n'%v2)
v3 = np.arange(0,-10,-2)
print ('Creating a numpy array via arange (start=0,stop=-10,step=-2) : %s\n'%v3)

```

    Creating a numpy array via arange (stop=5) : [0 1 2 3 4]

    Creating a numpy array via arange (start=2,stop=5) : [2 3 4]

    Creating a numpy array via arange (start=0,stop=-10,step=-2) : [ 0 -2 -4 -6 -8]

## 1.2. Initialization of 2D array in Python
When we talk about 2D array, it is important to note that Numpy stores Matrix is ***Row Major Format***. Row major format means that the the complete row will be stored first and then the next row will be stored and so on. You can choose to store a matrix in colum major format by mentioning ***order='F'*** on ndarray creation, which means ***Column Major Format***  or *Fortran Style Format*.

#### 1.2.1 Initialization from List of List
numpy.array **(** ***object***, dtype=None **)**

Here the matrix that we have taken is
$$mat = \begin{bmatrix}1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}$$

```python
pymat = [[1,2,3],[4,5,6]]
npmat = np.array(pymat)
print ('numpy matrix is \n%s\n'%npmat)
print ('Flattened version of numpy matrix is %s\n'%npmat.flatten())
# By flatten the matrix becomes row major 1D vector (This is the way in which a matrix is stored in memory).

```

    numpy matrix is
    [[1 2 3]
     [4 5 6]]

    Flattened version of numpy matrix is [1 2 3 4 5 6]

#### 1.2.2 Initialization with zero/ones
numpy.**zeros**(*shape*, dtype=float)
numpy.**ones**(*shape*, dtype=float)

```python
mat_zeros = np.zeros(shape=(3,5))
print ('Zeros Matris of shape %s is \n%s\n'%(mat_zeros.shape,mat_zeros))
mat_ones = np.ones(shape=(2,3))
print ('Ones Matris of shape %s is \n%s\n'%(mat_ones.shape,mat_ones))

```

    Zeros Matris of shape (3, 5) is
    [[0. 0. 0. 0. 0.]
     [0. 0. 0. 0. 0.]
     [0. 0. 0. 0. 0.]]

    Ones Matris of shape (2, 3) is
    [[1. 1. 1.]
     [1. 1. 1.]]

#### 1.2.3 Initialization with Random Values
**1.2.3.1** numpy.**random.random**(*size=None*,)
**1.2.3.2** numpy.**random.randint**(low, high, *size=None*, dtype='I') : The value of matrix lies between low and high-1
**1.2.3.3** numpy.**random.randn**($d_0,d_1,\cdots,d_n$)

```python
#Using numpy.random.random
mat1 = np.random.random(size=(3,4))
print ('matrix generated from numpy.random.random is \n%s\n'%mat1)
mat2 = np.random.randint(low=0,high=2,size=(3,4))
print ('matrix generated from numpy.random.random is \n%s\n'%mat2)
mat3 = np.random.randn(3,4)
print ('matrix generated from numpy.random.randn is \n%s\n'%mat3)

```

    matrix generated from numpy.random.random is
    [[0.40809381 0.92255528 0.26384768 0.18020434]
     [0.78683826 0.51616332 0.20181108 0.44676456]
     [0.40513581 0.41943719 0.14935848 0.49202397]]

    matrix generated from numpy.random.random is
    [[1 0 1 0]
     [0 1 1 1]
     [0 1 1 0]]

    matrix generated from numpy.random.randn is
    [[ 1.23093258  0.06762907  1.32948035  1.27069921]
     [-1.55068329 -0.48071094  0.85203508 -1.90068409]
     [-1.94335643  0.12680395  0.06573786 -0.66923407]]

## 1.3. Slicing and Indexing in Numpy
Just like python, numpy also has 0 indexing. Let us see some of the commonly used slicing techniques .
* Generic Slicing Operation : \[start\]:\[end\]:\[jump\]
* Only jump    **::2**
* Only end    **:5**
* Start and jump **2::-1**
* End and Jump **:5:2**
* Start, end and jump **2:7:3**

#### 1.3.1 Remove n elements
vec\[:*-n*\]

```python
vec = np.arange(10)
vec1 = vec[:-3]
print ('Result of removing last 3 elements from range(10) : \n%s\n'%vec1)
```

    Result of removing last 3 elements from range(10) :
    [0 1 2 3 4 5 6]

#### 1.3.2 Access elements at even indices in a 1D array
vec[::2]

```python

vec1 = np.arange(0,20,3)
print ('Original array is %s\n'%vec1)
vec2 = vec1[::2]
print ('Elements at even indices are %s\n'%vec2)

```

    Original array is [ 0  3  6  9 12 15 18]

    Elements at even indices are [ 0  6 12 18]

#### 1.3.3 Access elements at indices in reverse order
vec[::-1]

```python
vec1 = np.arange(0,20,3)
print ('Original array is %s\n'%vec1)
vec2 = vec1[::-1]
print ('Elements for indices in reverse is %s\n'%vec2)
```

    Original array is [ 0  3  6  9 12 15 18]

    Elements for indices in reverse is [18 15 12  9  6  3  0]

#### 1.3.4 Access elements present for a range of indices
vec[a:b]

```python
vec1 = np.arange(0,20,3)
print ('Original array is %s\n'%vec1)
vec2 = vec1[2:5]
print ('Elements for indices 2:5 is %s\n'%vec2)
```

    Original array is [ 0  3  6  9 12 15 18]

    Elements for indices 2:5 is [ 6  9 12]

#### 1.3.5 Access a particular set of index given by a list
vec[idxlist]

```python
idx=[0,1,5]
vec1 = np.arange(0,20,3)
print ('Original array is %s\n'%vec1)
vec2 = vec1[idx]
print ('Subarray constructed by indices %s is %s\n'%(idx,vec2))
```

    Original array is [ 0  3  6  9 12 15 18]

    Subarray constructed by indices [0, 1, 5] is [ 0  3 15]

#### 1.3.6 Creating a submatrix

```python
mat = np.random.randint(0,6,(3,5))
# Create a submatrix with first 2 rows and last 2 columns
submat1 = mat[0:2,-2:]
print ('Original Matrix is \n%s\n'%mat)
print ('Sub Matrix with first 2 rows and last 2 columns is \n%s\n'%submat1)
submat2 = mat[:,3:0:-1]
print ('After flipping the columns of the matrix, it looks : \n%s\n'%submat2)
```

    Original Matrix is
    [[1 2 0 0 2]
     [4 1 5 3 2]
     [2 1 3 5 0]]

    Sub Matrix with first 2 rows and last 2 columns is
    [[0 2]
     [3 2]]

    After flipping the columns of the matrix, it looks :
    [[0 0 2]
     [3 5 1]
     [5 3 1]]

#### 1.3.7 Horizontal Matrix splitting
numpy.**hsplit**(ary, *indices_or_sections*)

hsplit basically splits a matrix across the horizontal plane based on the indices. Do note that the ***number of rows*** always remains ***constant*** in each section after the horizontal splitting.

```python
mat = np.random.randint(0,6,(3,7))
sp1,sp2,sp3 = np.hsplit(mat,[4,6])
print ('Original matrix of shape %s, is \n%s\n'%(mat.shape,mat))
print ('First split of shape %s, is \n%s\n'%(sp1.shape,sp1))
print ('Second split of shape %s, is \n%s\n'%(sp2.shape,sp2))
print ('Third split of shape %s, is \n%s\n'%(sp3.shape,sp3))
```

    Original matrix of shape (3, 7), is
    [[5 4 1 2 1 5 2]
     [1 5 5 5 0 5 3]
     [2 0 1 5 4 4 4]]

    First split of shape (3, 4), is
    [[5 4 1 2]
     [1 5 5 5]
     [2 0 1 5]]

    Second split of shape (3, 2), is
    [[1 5]
     [0 5]
     [4 4]]

    Third split of shape (3, 1), is
    [[2]
     [3]
     [4]]

#### 1.3.8 Vertical Matrix Splitting
numpy.**vsplit**(ary, *indices_or_sections*)

vsplit is yet another operation which splits the matrix across the vertical plane based on the ***'rowwise split-index array'***. The ***number of columns*** always remain ***constant*** in each split.

```python
mat = np.random.randint(0,6,(3,7))
sp1,sp2 = np.vsplit(mat,[1])
print ('Original matrix of shape %s, is \n%s\n'%(mat.shape,mat))
print ('First split of shape %s, is \n%s\n'%(sp1.shape,sp1))
print ('Second split of shape %s, is \n%s\n'%(sp2.shape,sp2))
```

    Original matrix of shape (3, 7), is
    [[5 2 1 1 0 5 5]
     [5 3 0 1 5 2 1]
     [5 5 4 2 2 1 0]]

    First split of shape (1, 7), is
    [[5 2 1 1 0 5 5]]

    Second split of shape (2, 7), is
    [[5 3 0 1 5 2 1]
     [5 5 4 2 2 1 0]]

> ### *Class Assignment for 1.1 to 1.3*
>
> 1. Create a matrix of size (2,3) with random binary values
> 2. Find the sum of all 2x2 blocks (overlapping) for a random integer matrix of size (3,5)

## 1.4. Understanding - Pass By Reference
Just like python lists, Numpy also exhibits default pass-by-reference behavior

```python
mat1 = np.random.random((2,3))
pt1 = mat1[0].__array_interface__['data'][0]
print ('Memory Location of mat1[0] is : %s\n'%pt1)
pt2 = mat1[1].__array_interface__['data'][0]
print ('Memory Location of mat1[1] is : %s\n'%pt2)
print ('Difference in Memory Location for 3 elements  is : %s bytes\n'%(pt2-pt1))
print ('Memory jump = 8 bytes')

```

    Memory Location of mat1[0] is : 2041724279440

    Memory Location of mat1[1] is : 2041724279464

    Difference in Memory Location for 3 elements  is : 24 bytes

    Memory jump = 8 bytes

#### 1.4.1 Experiment : Change the value of array by Reference

```python
def identity(elem):
    return elem
v = np.array([1,2,3])
v1 = v # Copy by reference
v1[0]=10
print ('Value of v, after v1 is modified is %s\n'%v)
# Pass by Function
v = np.array([1,2,3])
v1 = identity(v) # Copy by reference
v1[0]=10
print ('Value of v after func(v) is modified is %s\n'%v)
v = np.array([1,2,3])
v1 = v.copy() # Copy by reference
v1[0]=10
print ('Value of v after v.copy() is modified is %s\n'%v)
```

    Value of v, after v1 is modified is [10  2  3]

    Value of v after func(v) is modified is [10  2  3]

    Value of v after v.copy() is modified is [1 2 3]

#### 1.4.2 Experiment : Change the value of matrices by Reference

```python
mat = np.array([[1,2,3],[4,5,6],[7,8,9]])
print ('Original Matrix is \n%s\n'%mat)

#1 Add 10 to all the even indexes in matrix
mat = np.array([[1,2,3],[4,5,6],[7,8,9]])
mat1 = mat[::2,::2]
mat1+=10
print (' Matrix with 10 added to even indices is \n%s\n'%mat1)
# Changes in mat1 are reflected in mat. This happens because the default assignment in numpy is 'pass by reference'
```

    Original Matrix is
    [[1 2 3]
     [4 5 6]
     [7 8 9]]

     Matrix with 10 added to even indices is
    [[11 13]
     [17 19]]

> **Note :** mat1+=10 is different from mat1=mat1+10, as a new space is allocated for mat1 in the latter case.

## 1.5 Broadcast Operation in Numpy
Broadcast Operation is like a razor-sharp knife, to be used with great care. It is a way to ***broadcast*** data of lower or same  dimension onto another ndarray. It is similar to ***map*** operation in python, scala, hadoop.  Broadcast rules are not limited to 2D array. There are extremely specific set of rules for nd-array broadcasting. You can visit ***[numpy documentation on broadcasting](https://docs.scipy.org/doc/numpy-1.13.0/user/basics.broadcasting.html)*** to get the complete picture. However, we will be restricting the discussion to only 2D-matrices.

```python
mat = np.random.randint(0,6,(3,5))
print ('mat is \n%s\n'%mat)
v = np.array([10,20,30,40,50])
print ('v is \n%s\n'%v)
print ('mat+1 is \n%s\n'%(mat+1)) # Similar behaviour in case of np.array([1])
print ('mat+v is \n%s\n'%(mat+v))
print ('mat*v is \n%s\n'%(mat*v))
print ('mat+mat is \n%s\n'%(mat+mat))
print ('mat*mat is \n%s\n'%(mat*mat))

```

    mat is
    [[1 1 2 1 4]
     [4 2 1 3 5]
     [3 1 1 2 5]]

    v is
    [10 20 30 40 50]

    mat+1 is
    [[2 2 3 2 5]
     [5 3 2 4 6]
     [4 2 2 3 6]]

    mat+v is
    [[11 21 32 41 54]
     [14 22 31 43 55]
     [13 21 31 42 55]]

    mat*v is
    [[ 10  20  60  40 200]
     [ 40  40  30 120 250]
     [ 30  20  30  80 250]]

    mat+mat is
    [[ 2  2  4  2  8]
     [ 8  4  2  6 10]
     [ 6  2  2  4 10]]

    mat*mat is
    [[ 1  1  4  1 16]
     [16  4  1  9 25]
     [ 9  1  1  4 25]]

## 1.6 Matrix Operations
There are two major matix Operations which are important for us.
1. Inner Product
  * Vector Vector
  * Matrix Vector
  * Matrix Matrix
2. Outer product

#### 1.6.1 Inner Product
In terms of Inner Product or Dot product, <v,w> will be be nothing but sum of element wise product.
$$<v,w> = \sum v_i*w_i = \begin{bmatrix} v_0 & v_1 & \cdots & v_n \end{bmatrix}\begin{bmatrix} w_0 \\ w_1 \\ \cdots \\ w_n \end{bmatrix}$$
So, the dot product between vector $v=[1,2,3]$ and  $w=[2,4,6]$ will be
$$\begin{align}1*2 + 2*4 + 3*6 \\ 2+8+18 \\ 28\end{align}$$
We will be covering matrix matrix multiplication in much depth in next-to-next chapter, but you can take it for granted as of now.
#### 1.6.2 Outer Product
The **outer product** on other end is
$$\begin{bmatrix} v_0 \\ v_1 \\ \cdots \\ v_n \end{bmatrix}\begin{bmatrix} w_0 & w_1 & \cdots & w_n \end{bmatrix} = \begin{bmatrix}v_0w_0 & \cdots & v_0w_j & \cdots & v_0w_n \\ \vdots && \vdots && \vdots \\ v_iw_0 & \cdots & v_iw_j & \cdots & v_iw_n \\ \vdots && \vdots && \vdots \\ v_nw_0 & \cdots & v_nw_j & \cdots & v_nw_n    \end{bmatrix} $$

```python
mat = np.arange(9).reshape(3,3)
vec1 = np.array([1,2,3])
vec2 = np.array([2,4,6])
print ('Vector v1 is %s\n'%vec1)
print ('Vector v2 is %s\n'%vec2)
print ('Vector Vector dot product is <v1,v2> is %s\n'%np.dot(vec1,vec2))
print ('---------------\n')

print ('Matrix is\n%s\n'%mat)
print ('Vector v2 is\n%s\n'%vec1)

print ('Matrix Vector product is <mat,vec1> is \n%s\n'%np.dot(mat,vec1))
print ('Matrix Matrix multiplication is <mat,mat> is \n%s\n'%np.dot(mat,mat))
print ('---------------\n')

print ('Vector v1 is %s\n'%vec1)
print ('Vector v2 is %s\n'%vec2)
print ('Vector Vector outer product is <v1,v2> is \n%s\n'%np.outer(vec1,vec2))

```

    Vector v1 is [1 2 3]

    Vector v2 is [2 4 6]

    Vector Vector dot product is <v1,v2> is 28

    ---------------

    Matrix is
    [[0 1 2]
     [3 4 5]
     [6 7 8]]

    Vector v2 is
    [1 2 3]

    Matrix Vector product is <mat,vec1> is
    [ 8 26 44]

    Matrix Matrix multiplication is <mat,mat> is
    [[ 15  18  21]
     [ 42  54  66]
     [ 69  90 111]]

    ---------------

    Vector v1 is [1 2 3]

    Vector v2 is [2 4 6]

    Vector Vector outer product is <v1,v2> is
    [[ 2  4  6]
     [ 4  8 12]
     [ 6 12 18]]

## 1.7 Statistical Functions
1. **np.mean**(data,axis=0)
2. **np.var**(data,axis=0)
3. **np.sum**(data,axis=0)
4. **np.max**(data,axis=0)
5. **np.min**(data,axis=0)
6. **np.percentile**(data, percentage,axis=0)

```python

mat = np.arange(9).reshape((3,3))
print ('Original matrix is \n%s\n'%mat)
print ('Overall mean of matrix is \n%s\n'%np.mean(mat))
print ('Row mean of matrix is \n%s\n'%np.mean(mat, axis=0))
print ('Column mean of matrix is \n%s\n'%np.mean(mat, axis=1))
print ('---------------------------\n')
print ('Overall varience of matrix is %s\n'%np.var(mat))
print ('Overall sum of matrix is %s\n'%np.sum(mat))
print ('Overall min of matrix is %s\n'%np.min(mat))
print ('Overall max of matrix is %s\n'%np.max(mat))

print ('-------------------------------------\n')
marks = np.array([30,31,32,40,90,95,97,98,99,100])
print ('Marks = %s\n'%marks)
print ('Overall 30 percent quantile of marks is %s\n'%(str(np.percentile(marks,30))))

```

    Original matrix is
    [[0 1 2]
     [3 4 5]
     [6 7 8]]

    Overall mean of matrix is
    4.0

    Row mean of matrix is
    [3. 4. 5.]

    Column mean of matrix is
    [1. 4. 7.]

    ---------------------------

    Overall varience of matrix is 6.666666666666667

    Overall sum of matrix is 36

    Overall min of matrix is 0

    Overall max of matrix is 8

    -------------------------------------

    Marks = [ 30  31  32  40  90  95  97  98  99 100]

    Overall 30 percent quantile of marks is 37.599999999999994

## 1.8 Miscellenous Functions
In this section, we will be discussing some important miscellenous Functions which come in handy for matrix manipulation.
#### 1.8.1 squeeze
numpy.**squeeze**(a,axis=None)
This function tries to reduce the excess dimensions which have only 1 element. If we print the shape of the ndary, these excess dimension will have a 1 in that particular dimension's index. For example, a nd-matrix with shape (1,2,3,1), has $0^{th}$ and $3^{rd}$ dimension as excess.

```python
ndmat = np.zeros(shape=(1,2,3,1))
print ('Original Shape of ndmat is %s\n'%str(ndmat.shape))
ndmat1 = np.squeeze(ndmat)
print ('Shape of np.squeeze(ndmat) is %s\n'%str(ndmat1.shape))
ndmat2 = np.squeeze(ndmat,axis=3)
print ('Shape of np.squeeze(ndmat,axis=3) is %s\n'%str(ndmat2.shape))

```

    Original Shape of ndmat is (1, 2, 3, 1)

    Shape of np.squeeze(ndmat) is (2, 3)

    Shape of np.squeeze(ndmat,axis=3) is (1, 2, 3)

#### 1.8.2 transpose
numpy.**transpose**(a, *axes=None*)
This function reverses the axes for 2D array.
For multidimensional array, it permutes the matrix according to the axis argument

```python
mat = np.zeros(shape=(2,3))
print ('Original Shape of 2D matrix is %s\n'%str(mat.shape))
mat_transpose = np.transpose(mat)
print ('Shape of np.transpose(mat) is %s\n'%str(mat_transpose.shape))
mat_transpose[1,0] = 1
print ('np.transpose is by yet again assignment by reference, as changes in transpose reflects in the original matrix\n')
print ('---------------------------------\n')
# NDimensional matrix
ndmat = np.zeros(shape=(2,3,4))
print ('Original Shape of nDimensional matrix is %s\n'%str(ndmat.shape))
ndmat_transpose = np.transpose(ndmat)
print ('Shape of np.transpose(ndmat) is %s\n'%str(ndmat_transpose.shape))
ndmat_special_transpose = np.transpose(ndmat, axes = [0,2,1])
print ('Shape of np.transpose(ndmat, axes=[0,2,1]) is %s\n'%str(ndmat_special_transpose.shape))

```

    Original Shape of 2D matrix is (2, 3)

    Shape of np.transpose(mat) is (3, 2)

    np.transpose is by yet again assignment by reference, as changes in transpose reflects in the original matrix

    ---------------------------------

    Original Shape of nDimensional matrix is (2, 3, 4)

    Shape of np.transpose(ndmat) is (4, 3, 2)

    Shape of np.transpose(ndmat, axes=[0,2,1]) is (2, 4, 3)

```python

```

# Chapter 1 Assignment
1. Create a null array of size 10 but the fifth value which is 1
2. Reverse a above created array (first element becomes last)
3. Create a 3x3 matrix with values ranging from 0 to 8
4. Find indices of non-zero elements from [1,2,0,0,4,0]
5. Create a 3x3x3 array with random values
6. Create a 10x10 array with random values and find the minimum and maximum values
7. Create a random vector of size 30 and find the mean value
8. Create a 2d array with 1 on the border and 0 inside
9. Multiply a 5x3 matrix by a 3x2 matrix (real matrix product)
10. Extract the integer part of a random array
11. Create a 5x5 matrix with row values ranging from 0 to 4
12. Create a random vector of size 10 and sort it
13. Consider two random array A anb B, check if they are equal
14. How to tell if a given 2D array has null columns?
15. Considering a four dimensions array, how to get sum over the last two axis at once?
16. Consider an array of dimension (5,5,3), how to mulitply it by an array with dimensions (5,5)?
17. Extract all the contiguous 3x3 blocks from a random 10x10 matrix
18. Consider a 16x16 array, how to get the block-sum (block size is 4x4)?

```python

```

1.1 NumPy/1. Introduction to Numpy.ipynb


# EVALUATING CLOSED LOOP SYSTEMS: PART 1

This notebook is a companion to the slides for understanding closed loop systems.

# Preliminaries

```python
!pip install -q control
!pip install -q tellurium
!pip install -q controlSBML

import collections
import control
import controlSBML as ctl
from IPython.display import HTML, Math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import sympy
import tellurium as te
```

```python
TIMES = ctl.makeSimulationTimes(0, 5, 500)
```

# Helpers

```python
def plotStep(tf, is_plot=True):
    """
    Plots a transfer function step response

    Parameters
    ----------
    tf: control.TransferFunction
    """
    result = control.forced_response(tf, T=TIMES, U=1)
    if is_plot:
        plt.plot(result.t, result.outputs)

# TESTS
Gs = control.TransferFunction([2], [1, 3])
plotStep(Gs, is_plot=False)
print("OK!")
```

    OK!

```python
def pp(obj):
    """
    Does a LaTex rendering of an object with a latex representation.

    Parameters
    ----------
    obj: object
    """
    display(Math(str(obj)))

# Tests
pp(r"e^{\alpha}")
```

$$e^{\alpha}$$

# Closed Loop Transfer Functions

**Transfer Functions**
\begin{eqnarray}
H_{RY}(s) & = & \frac{Y(s)}{R(s)} & = & \frac{C(s) G(s)}{1 + C(s) G(s) F(s)} \\
H_{RE}(s) & = & \frac{E(s)}{R(s)} & = & \frac{1}{1 + C(s) G(s) F(s)} \\
H_{NY}(s) & = & \frac{Y(s)}{N(s)} & = & -\frac{ F(s)}{1 + C(s) G(s) F(s)} \\
H_{DY}(s) & = & \frac{Y(s)}{D(s)} & = & \frac{ C(s)}{1 + C(s) G(s) F(s)} \\
\end{eqnarray}

# Problem Statement

We want to evaluate the transfer functions for a variety of systems $C(s)$ and filters $F(s)$.
We will use $G(s) = \frac{2}{s + 3}$, $C(s) = k_p$, and $F(s) = \frac{k_f}{s + k_f}$.

1. Create an experimental infrastructure, codes that creates a plot with a line for each of the transfer functions. You wil need to create the transfer functions C(s), G(s), F(s). The plot should have a legend.
    1. Inputs: G_tf (transfer function for G(s)), k_p, k_f
1. Construct plots such that $k_p \in \{1, 5, 10\}$, $k_f \in \{1, 5, 10\}$.
1. Discuss the tradeoffs between large and small values of $k_p$, $k_f$? Some of the simulations have oscillations. Why?

# Experimental Infrastructure

```python
# Initial start on function. Needs to be completed.
def plotTFs(Gs, k_p, k_f, times=TIMES, ylim=None, title=None, is_plot=True):
    """
    Constructs the transfer functions for the proportional controller, and filter.
    Calculates the transfer functions HRYs, HREs, HNYs, HDYs and plots them.

    Parameters
    ----------
    Gs: control.TransferFunction
    k_pf: float
    k_f: float
    times: list-float
    ylim: (float, float)
        limits of y-values
    title: str

    Returns
    -------
    dct
        key: name of transfer function
        value: control.TransferFunction
    """
    Cs = control.TransferFunction([k_p], [1])
    Fs = control.TransferFunction([k_f], [1, k_f])
    denom = 1 + Cs*Gs*Fs
    # Construct the transfer functions
    tf_dct = {
        "HRYs": Cs*Gs/denom,
        "HREs": None,  # TODO
        "HNYs": None,  # TODO
        "HDYs": None,  # TODO
    }
    # Construct the plots
    _, ax = plt.subplots(1)
    for tf in tf_dct.values():
        result = control.forced_response(tf, T=TIMES, U=1)
        # TODO: plot
    # Do plots
    # TODO: other plot stuff
    if not is_plot:
        plt.close()
    return tf_dct

# Tests
Gs = control.TransferFunction([2], [1, 3])
dct = plotTFs(Gs, 10, 10, ylim=[0, 3], title="Example", is_plot=False)
assert(len(dct) == 4)
assert("TransferFunction" in str(type(dct["HRYs"])))
print("OK!")
```

# Plots

```python
for k_p in [1, 5, 10]:
    for k_f in [1, 5, 10]:
        title = "k_p: %2.1f k_f: %2.1f" % (k_p, k_f)
        _ = plotTFs(Gs, k_p, k_f, title=title, ylim=[-1, 3])
```

# Discussion


# [HW10] Simple Linear Regression

## 1. Linear regression

Linear regression은 종속 변수 $y$와 한개 이상의 독립 변수 $X$와의 선형 관계를 모델링하는 방법론입니다. 여기서 독립 변수는 입력 값이나 원인을 나타내고, 종속 변수는 독립 변수에 의해 영향을 받는 변수입니다. 종속 변수는 보통 결과물을 나타냅니다.

선형 관계를 모델링한다는 것은 1차로 이루어진 직선을 구하는 것입니다. 우리의 데이터를 가장 잘 설명하는 최적의 직선을 찾아냄으로써 독립 변수와 종속 변수 사이의 관계를 도출해 내는 과정입니다.

이번 실습에서는 독립 변수가 1개인 simple linear regression을 진행하겠습니다. 변수가 하나인 직선을 정의하겠습니다.
$$f(x_i) = wx_i + b$$

우리의 데이터를 가장 잘 설명하는 직선은 우리가 직선을 통해 예측한 값이 실제 데이터의 값과 가장 비슷해야 합니다. 우리의 모델이 예측한 값은 위에서 알 수 있듯 $f(x_i)$입니다. 그리고 실제 데이터는 $y$ 입니다.

실제 데이터(위 그림에서 빨간 점) 과 직선 사이의 차이를 줄이는 것이 우리의 목적입니다. 그것을 바탕으로 cost function을 다음과 같이 정의해보겠습니다.

$$\text{cost function} = \frac{1}{N}\sum_{i=1}^n (y_i - f(x_i))^2$$

우리는 cost function을 최소로 하는 $w$와 $b$를 찾아야 합니다.

우리의 cost function은 이차함수입니다. 우리는 고등학교 수학시간에 이차함수의 최솟값을 구하는 방법을 배웠습니다! 고등학교 때 배웠던 방법을 다시 한번 알아보고, 새로운 gradient descent 방법도 알아보겠습니다.

### 1.1 Analytically

다음 식의 최솟값을 어떻게 찾을 수 있을까요?
$$f(w) = w^2 + 3w -5$$

고등학교 때 배운 방법은 미분한 값이 0이 되는 지점을 찾는 것입니다.

손으로 푸는 방법은 익숙하겠지만 sympy와 numpy 패키지를 사용하여 코드를 통해서 알아보도록 하겠습니다.

```python
import sympy
import numpy

from matplotlib import pyplot
%matplotlib inline
sympy.init_printing()
```

```python
w = sympy.Symbol('w', real=True)

f = w**2 + 3*w - 5
f
```

```python
sympy.plotting.plot(f);
```

1차 미분한 식은 다음과 같이 알아볼 수 있습니다.

```python
fprime = f.diff(w)
fprime

```

그리고 해당 식의 해는 다음과 같이 구할 수 있습니다.

```python
sympy.solve(fprime, w)
```

### 1.2 Gradient Descent

두번째 방법은 오늘 배운 Gradient Descent 방법으로 한번에 정답에 접근하는 것이 아닌 반복적으로 정답에 가까워지는 방법입니다.

이것도 코드를 통해서 이해해보도록 하겠습니다.

먼저 기울기값을 구하는 함수를 먼저 만들겠습니다.

```python
fpnum = sympy.lambdify(w, fprime)
type(fpnum)
```

    function

그 다음 처음 $w$ 값을 설정한 뒤, 반복적으로 최솟값을 향해서 접근해보겠습니다.

```python
w = 10.0   # starting guess for the min

for i in range(1000):
    w = w - fpnum(w)*0.01 # with 0.01 the step size

print(w)
```

    -1.4999999806458753

이처럼 첫번째 방법과 두번째 방법에서 같은 값이 나온 것을 알 수 있습니다.

Gradient descent 방법을 직접 데이터를 만들어서 적용해보겠습니다.

### 1.3 Linear regression

실제로 linear 한 관계를 가진 데이터 셋을 사용하기 위해서 직접 데이터를 만들어보도록 하겠습니다.

Numpy 패키지 안에 Normal distribution 함수를 통해서 조금의 noise 를 추가해서 생성하도록 하겠습니다.

```python
x_data = numpy.linspace(-5, 5, 100)
w_true = 2
b_true = 20

y_data = w_true*x_data + b_true + numpy.random.normal(size=len(x_data))

pyplot.scatter(x_data,y_data);
```

```python
x_data.shape
```

```python
y_data.shape
```

총 100개의 데이터를 생성하였습니다. 이제 코드를 통해 접근해보도록 하겠습니다.

먼저 cost function을 나타내보겠습니다.

```python
w, b, x, y = sympy.symbols('w b x y')

cost_function = (w*x + b - y)**2
cost_function
```

위의 gradient descent 예시에서 한 것처럼 기울기 함수를 정의합니다.

```python
grad_b = sympy.lambdify([w,b,x,y], cost_function.diff(b), 'numpy')
grad_w = sympy.lambdify([w,b,x,y], cost_function.diff(w), 'numpy')
```

이제 $w$와 $b$의 초기값을 정의하고 gradient descent 방법을 적용하여 cost function을 최소로 하는 $w$와 $b$ 값을 찾아보겠습니다.

```python
w = 0
b = 0

for i in range(1000):
    descent_b = numpy.sum(grad_b(w,b,x_data,y_data))/len(x_data)
    descent_w = numpy.sum(grad_w(w,b,x_data,y_data))/len(x_data)
    w = w - descent_w*0.01 # with 0.01 the step size
    b = b - descent_b*0.01

print(w)
print(b)
```

    1.950888967346333
    19.951345141481625

처음에 데이터를 생성할 때 정의한 $w, b$ 값과 매우 유사한 값을 구할 수 있었습니다.

```python
pyplot.scatter(x_data,y_data)
pyplot.plot(x_data, w*x_data + b, '-r');
```

우리가 구한 직선이 데이터와 잘 맞는 것을 볼 수 있습니다. 이번에는 실제 데이터에서 linear regression을 진행해보겠습니다.

## 2. Earth temperature over time

오늘 배운 linear regression 방법을 사용해서 시간 흐름에 따른 지구의 온도 변화를 분석해보겠습니다.

Global temperature anomaly라는 지표를 통해서 분석을 해볼 것입니다.

여기서 temperature anomaly는 어떠한 기준 온도 값을 정해놓고 그것과의 차이를 나타낸 것입니다. 예를 들어서 temperature anomaly가 양수의 높은 값을 가진다면 그것은 평소보다 따듯한 기온을 가졌다는 말이고, 음수의 작은 값을 가진다면 그것은 평소보다 차가운 기온을 가졌다는 말입니다.

세계 여러 지역의 온도가 각각 다 다르기 때문에 global temperature anomaly를 사용해서 분석을 하도록 하겠습니다. 자세한 내용은 아래 링크에서 확인하실 수 있습니다.

https://www.ncdc.noaa.gov/monitoring-references/faq/anomalies.php

```python
from IPython.display import YouTubeVideo
YouTubeVideo('gGOzHVUQCw0')
```

위 영상으로 기온이 점점 상승하고 있다는 것을 알 수 있습니다.

이제부터는 실제 데이터를 가져와서 분석해보도록 하겠습니다.

### Step 1 : Read a data file

NOAA(National Oceanic and Atmospheric Administration) 홈페이지에서 데이터를 가져오겠습니다.

아래 명령어로 데이터를 다운받겠습니다.

```python
from urllib.request import urlretrieve
URL = 'http://go.gwu.edu/engcomp1data5?accessType=DOWNLOAD'
urlretrieve(URL, 'land_global_temperature_anomaly-1880-2016.csv')
```

    ('land_global_temperature_anomaly-1880-2016.csv',
     <http.client.HTTPMessage at 0x7ff6860c3710>)

다운로드한 데이터를 numpy 패키지를 이용해 불러오겠습니다.

```python
import numpy
```

```python
fname = '/content/land_global_temperature_anomaly-1880-2016.csv'

year, temp_anomaly = numpy.loadtxt(fname, delimiter=',', skiprows=5, unpack=True)
```

### Step 2 : Plot the data

Matplotlib 패키지의 pyplot을 이용해서 2D plot을 찍어보도록 하겠습니다.

```python
from matplotlib import pyplot
%matplotlib inline
```

```python
pyplot.plot(year, temp_anomaly);
```

Plot 에 여러 정보를 추가해서 더 보기 좋게 출력해보겠습니다.

```python
pyplot.rc('font', family='serif', size='18')

#You can set the size of the figure by doing:
pyplot.figure(figsize=(10,5))

#Plotting
pyplot.plot(year, temp_anomaly, color='#2929a3', linestyle='-', linewidth=1)
pyplot.title('Land global temperature anomalies. \n')
pyplot.xlabel('Year')
pyplot.ylabel('Land temperature anomaly [°C]')
pyplot.grid();
```

### Step 3 : Analytically

Linear regression을 하기 위해서 먼저 직선을 정의하겠습니다.
$$f(x_i) = wx + b$$

그 다음 수업 시간에 배운 cost function을 정의하도록 하겠습니다. 우리가 최소화 해야 할 cost function은 다음과 같습니다.

$$\frac{1}{n} \sum_{i=1}^n (y_i - f(x_i))^2 = \frac{1}{n} \sum_{i=1}^n (y_i - (wx_i + b))^2$$

이제 cost function 을 구하고자 하는 변수로 미분한 뒤 0이 되도록 하는 값을 찾으면 됩니다.

먼저 $b$에 대해서 미분을 하겠습니다.

$$\frac{\partial{J(w,b)}}{\partial{b}} = \frac{1}{n}\sum_{i=1}^n -2(y_i - (wx_i+b)) = \frac{2}{n}\left(b + w\sum_{i=1}^n x_i -\sum_{i=1}^n y_i\right) = 0$$
위 식을 만족하는 $b$에 대해서 정리하면

$$b = \bar{y} - w\bar{x}$$
여기서  $\bar{x} = \frac{\sum_{i=1}^n x_i}{n}$ , $\bar{y} = \frac{\sum_{i=1}^n y_i}{n}$ 입니다.

이제 $w$에 대해서 미분을 하겠습니다.

$$\frac{\partial{J(w,b)}}{\partial{w}} =  \frac{1}{n}\sum_{i=1}^n -2(y_i - (wx_i+b))x_i = \frac{2}{n}\left(b\sum_{i=1}^nx_i + w\sum_{i=1}^n x_i^2 - \sum_{i=1}^n x_iy_i\right)$$

여기에 아까 구한 $b$를 대입한 후 0이 되는 $w$값을 구하하면

$$w = \frac{\sum_{i=1}^ny_i(x_i-\bar{x_i})}{\sum_{i=1}^nx_i(x_i-\bar{x_i})}$$

가 됩니다.

우리는 계산을 통해서 $w$와 $b$ 값을 구했습니다.

이제 코드를 통해서 적용해보도록 하겠습니다.

```python
w = numpy.sum(temp_anomaly*(year - year.mean())) / numpy.sum(year*(year - year.mean()))
b = a_0 = temp_anomaly.mean() - w*year.mean()

print(w)
print(b)
```

    0.01037028394347266
    -20.148685384658464

이제 그래프로 그려서 확인해보도록 하겠습니다.

```python
reg = b + w * year
```

```python
pyplot.figure(figsize=(10, 5))

pyplot.plot(year, temp_anomaly, color='#2929a3', linestyle='-', linewidth=1, alpha=0.5)
pyplot.plot(year, reg, 'k--', linewidth=2, label='Linear regression')
pyplot.xlabel('Year')
pyplot.ylabel('Land temperature anomaly [°C]')
pyplot.legend(loc='best', fontsize=15)
pyplot.grid();
```

오늘은 linear regression을 직접 만든 데이터와 실제로 있는 데이터로 진행해보았습니다.

년도에 따른 기온의 변화를 gradient descent 로 하는 방법은 내일 실습에서 추가로 알아보도록 하겠습니다.

질문 있으면 편하게 해주세요~~


```python
from sympy import *
init_printing(use_latex='mathjax')
Re,theta_r,D,rho,L_x,lam,tau,k,x = symbols('Re theta_r D rho L_x lambda tau k x', positive=True)
C0 = symbols('C0')
```

```python
rho = solve(Re - rho*sqrt(D/rho/theta_r)*L_x/D,rho)[0]  # density from Reynolds number Re
V_p = sqrt(D/rho/theta_r)                               # velocity scale - wave velocity
```

```python
H      = C0*exp(-lam*tau*V_p/L_x)*sin(pi*k*x/L_x)
q      = C0*exp(-lam*tau*V_p/L_x)*cos(pi*k*x/L_x)
```

```python
eq1 =     rho*diff(H,tau)     +   diff(q,x)             # mass balance
eq2 = theta_r*diff(q,tau) + q + D*diff(H,x)             # momentum balance
```

```python
disp = diff(eq1,tau)*theta_r - diff(eq2,x) + eq1
disp = expand(disp)
disp = expand(simplify(disp/disp.coeff(lam**2)))
disp
```

$\displaystyle - Re \lambda + \pi^{2} k^{2} + \lambda^{2}$

```python
sol     = solve(disp,lam)
disc    = discriminant(disp,lam)
Re_opt  = solve(disc,Re)[0].subs(k,1)
Re_opt
```

$\displaystyle 2 \pi$

```python
from sympy import maximum, lambdify
import numpy as np
import warnings
warnings.filterwarnings('ignore')
lam  = [re(s.subs(k,1)) for s in sol]
lamf = lambdify(Re,lam,"numpy")
Re1  = np.linspace(0,2*float(Re_opt),1000)
lam1 = np.stack(lamf(Re1),axis=0)
lam1 = np.nanmin(lam1,axis=0)
```

```python
%matplotlib notebook
from matplotlib import pyplot as plt
plt.rcParams.update({"text.usetex": True, "font.size": 14})
f  = plt.figure(figsize=(5,5))
ax = plt.subplot()
ax.plot(Re1,lam1)
ax.axvline(x=Re_opt.evalf(),ls='--',c='k',linewidth=2)
ax.set_xlabel("$Re$"); ax.set_ylabel("$\mathrm{min}\{\Re(\lambda_k)\}$")
ax.set_xticks([0,np.pi,2*np.pi,3*np.pi,4*np.pi])
ax.set_xticklabels(["$0$","$\pi$","$2\pi$","$3\pi$","$4\pi$"])
ax.set_xlim(0,4*np.pi)
ax.grid()
```

    <IPython.core.display.Javascript object>

```python

```


# Simple Calculations for Sweep

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import sympy as sym
```

```python
def cosd(angle):
    return np.cos(np.deg2rad(angle))

def arccosd(angle):
    return np.rad2deg(np.arccos(angle))

def cos(angle):
    return np.cos(angle)

def arccos(angle):
    return np.arccos(angle)

def tand(angle):
    return np.tan(np.deg2rad(angle))

def arctand(angle):
    return np.rad2deg(np.arctan(angle))
```

General parameters for the wing and aerofoil

```python
CL = 0.27
sweep_25 = 0
M_cruise = 0.75
M_crit = 0.7
M_dd = M_crit + 0.08
b = 12.9
semi_b = b/2
S = 22.2
taper = 0.22
```

**What drag divergence number do we want? If we are to follow boeing and airbus then our drag divergence number will be equal to our desired cruise mach number. But we can choose this to bes suit our needs.**

```python
M_dd_des = 0.8
```

```python
sweep_le_des = arccosd(M_dd / M_dd_des)
print(f"Leading edge desired angle {sweep_le_des}")
sweep_quarter_des =arccosd((M_dd / M_dd_des)**0.5)
print(f"Quater chord desired angle {sweep_quarter_des}")
```

    Leading edge desired angle 12.838568140984087
    Quater chord desired angle 9.097436169383398

**Finding the thickness to chord ratio using Torenbeek**

```python
def Torenbeek(M, angle, CL):
    M_eff = M*(cosd(angle))
    t_c = 0.3*cosd(angle) * ((1 - ((5 + M_eff**2)/(5 +  (1.2 - 0.25*CL)**2))**3.5)*(((1-M_eff**2)**0.5)/(M_eff**2)))**(2/3)
    return t_c
```

```python
MDD = np.linspace(0.7, 0.9, 100)

t_c = np.array([])
t_c_sweep_0 = np.array([])
t_c_sweep_5 = np.array([])
t_c_sweep_10 = np.array([])
t_c_sweep_15 = np.array([])
t_c_sweep_20 = np.array([])
for value in MDD:
    t_c_sweep_0 = np.append(t_c_sweep_0, Torenbeek(value, 0, CL))
    t_c_sweep_5 = np.append(t_c_sweep_5, Torenbeek(value, 5, CL))
    t_c = np.append(t_c, Torenbeek(value, sweep_quarter_des, CL))
    t_c_sweep_15 = np.append(t_c_sweep_15, Torenbeek(value, 15, CL))
    t_c_sweep_20 = np.append(t_c_sweep_20, Torenbeek(value, 20, CL))

fig, axs = plt.subplots(1,1, figsize=(6, 6))
axs.plot(MDD, t_c, "-", label = f"Sweep = {sweep_quarter_des}")
axs.plot(MDD, t_c_sweep_0, "--", label = "Sweep = 0")
axs.plot(MDD, t_c_sweep_5, "-.", label = "Sweep = 5")
axs.plot(MDD, t_c_sweep_15, ":", label = "Sweep = 15")
axs.plot(MDD, t_c_sweep_20, "_", label = "Sweep = 20")
axs.set_xlabel(r"$MDD$", fontsize = 20)
axs.set_ylabel(r"$t/c$", fontsize = 20)
axs.axvline(x = 0.78, linestyle = "--", color = "k", label = "Chosen MDD")
axs.legend(loc="upper right")
plt.show()
```

**Using the two types of sweep to calculate the taper ratio**

```python
m1 = tand(sweep_le_des)
m2 = tand(sweep_quarter_des)

tip_le = m1*semi_b
tip_quarter = m2*semi_b

theta1 = tip_le
theta2 = tip_quarter

x = np.linspace(-20,0,100)
y1 = m1*x
y2 = m2*x - 0.5

fig, axs = plt.subplots(1,1)
axs.plot(x, y1, "--")
axs.plot(x, y2, "-.")
axs.axvline(x = -semi_b)
plt.show()

# area = 0
# x = 0.5
# while abs(S/2-area) > 0.5:
#     x += 0.01
#     y = x + semi_b*tand(theta2) - tand(theta1)
#     h = ((semi_b*tand(theta1) + 4*y -4*x)**2 + semi_b**2)**0.5
#     theta3 = arctand(h/semi_b)
#     area = semi_b*x + semi_b**2/2 * tand(theta2) - semi_b**2 /2 * tand(theta1) + semi_b*3*x + semi_b**2 / 2 * tand(theta3) - semi_b**2 /2 *tand(theta2)
#     print(area)
#     time.sleep(2)
# print(f"x: {x} and area: {area}")

```

**Wing geometry.**

```python
taper = 0.45
C_root = 2*((S)/(b*(1+ taper)))
C_tip = taper*C_root
print(f"C root: {C_root} and C tip: {C_tip}")

quarter_chord_root = 0.25*C_root
quarter_chord_tip = 0.25*C_tip

length_1 = tand(sweep_quarter_des)*semi_b
point_1 = length_1 - quarter_chord_tip + quarter_chord_root
angle_1 = arctand(point_1/semi_b)
point_2 = point_1 + C_tip
angle_2 = arctand((point_2 - C_root)/semi_b)
print(f"Theta 1: {angle_1}, Theta 2: {angle_2}")

x = sym.Symbol("x")
true_wing_area = sym.integrate(tand(theta2)*x - C_root, (x, 0, -semi_b)) - sym.integrate(tand(theta1)*x, (x, 0, -semi_b))
semi_area = S/2
print(f"This is the area: {true_wing_area}, This is the semi area {semi_area} ")
```

    C root: 2.3736968724939858 and C tip: 1.0681635926222937
    Theta 1: 11.899844546997961, Theta 2: 0.4768020909093081
    This is the area: 15.1515630557345, This is the semi area 11.1

```python
HLD_chord_root = 0.3*C_root
HLD_chord_tip = 0.3*C_tip
m = (-(point_2-HLD_chord_tip) + (C_root - HLD_chord_root) )/ (-semi_b - 0)
theta_HL = arctand(m)

HLD_start = 0.1*semi_b
HLD_end = 0.65*semi_b
y = m*(-HLD_end) - (C_root - HLD_chord_root)

x = sym.Symbol("x")
area = sym.integrate(tand(theta2)*x - C_root, (x, -HLD_start, -HLD_end)) - sym.integrate(tand(theta1)*x, (x, -HLD_start, -HLD_end))
semi_area = S/2
Sflapped_ratio = area / true_wing_area
print(f"This is the area covered by the flap: {area}, This is the Sflapped/Sref ratio {Sflapped_ratio} ")

```

    This is the area covered by the flap: 8.35519217428358, This is the Sflapped/Sref ratio 0.551440940023765

```python
Cl_max = 1.42
CL_max_des = 2.1
CL_max = 0.9*Cl_max*cosd(sweep_quarter_des)
print(f"This is the desired CL_max: {CL_max_des} and this is the current CL_max: {CL_max}")
print(f"This is the hinge line sweep: {theta_HL}")
delta_CL_max = CL_max_des - CL_max
delta_HLD = delta_CL_max / (0.9*Sflapped_ratio*cosd(theta_HL))
print(f"Desired delta CL from high lift device: {delta_HLD}")
delta_CL_fowler = 1.3*(1+0.3*cosd(40))
print(f"Desired delta CL from high lift device: {delta_HLD}, from TE {delta_CL_fowler}")

```

    This is the desired CL_max: 2.1 and this is the current CL_max: 1.2619238883546027
    This is the hinge line sweep: 3.949686546375664
    Desired delta CL from high lift device: 1.69267919789550
    Desired delta CL from high lift device: 1.69267919789550, from TE 1.5987573328164013

```python

fig_wing, wing_plot = plt.subplots(1,1, figsize=(20, 4))
wing_plot.plot([0,0],[0, -C_root], "b-")
wing_plot.plot([-semi_b, -semi_b], [-point_1, -point_1 - C_tip], "b-")
wing_plot.plot([0, -semi_b], [0, -point_1], "b-")
wing_plot.plot([0, -semi_b], [-C_root, -point_2], "b")
wing_plot.plot([0, -semi_b], [-quarter_chord_root, -(point_1 + quarter_chord_tip)], "b")
wing_plot.plot([semi_b, semi_b], [-point_1, -point_1 - C_tip], "b-")
wing_plot.plot([0, semi_b], [0, -point_1], "b-")
wing_plot.plot([0, semi_b], [-C_root, -point_2], "b")
wing_plot.plot([0, semi_b], [-quarter_chord_root, -(point_1 + quarter_chord_tip)], "b")
#wing_plot.plot([HLD_start, semi_b], [-(C_root - HLD_chord_root),-(point_2 - HLD_chord_tip)], "b")
#wing_plot.plot([-HLD_start, -semi_b], [-(C_root - HLD_chord_root),-(point_2 - HLD_chord_tip)], "b")
wing_plot.plot([HLD_start, HLD_end], [-(C_root - HLD_chord_root), y], "b")
wing_plot.plot([-HLD_start, -HLD_end], [-(C_root - HLD_chord_root),y], "b")

```

# High Lift Devices

**Just some general parameters that should be considered.**

```python
Cl_max = 1.42
CL_max_des = 1.1*Cl_max

```


Presented Below are two plots in subplot configuration.  The upper plot is a sum of the cusp crossings.  The lower plot is a plot of the cusp latitude and the spacecraft latitude/longitude plotted against time.  Also included is the process of thoughts that led to that conclusion.

I used the Tsyganenko model of the cusp position.  This model describes the cusp position in the noon-midnight meridian using a cylindrical coordinate system.  The $r$ and $\theta$ components are occuring in the xz plane.  The neutral axis is therefore the $y$ axis.

Tsyganenko provides the following equation for a simplified cusp model
\begin{equation}
\phi_{c} = arcsin(\frac{\sqrt{\rho}}{\sqrt{\rho + \sin^{-2}(\phi_{1}) -1}}) + \psi(t)
\end{equation}

Here, $\rho$ is the radial of the satellite from the center of the earth.  $\phi_{c}$ is the colatitude of the cusp position.  $\psi$ is the dipole tilt of the cusp.  $\phi_{1}$ is provided by the equation $\phi_{c0} - (\alpha_{1} \psi + \alpha_{2}\psi^{2})$.  As in (Tsyganenko) the values of $\phi_{c}$, $\alpha_{1}$, and $\alpha_{2}$ are 0.24, 0.1287, and 0.0314 respectively.  ((note that that last sentence is almost a literal copy paste of the Tsyg paper)).  Note that $\psi (t)$ is my own addition and that the original notation is $\psi$.  I want to make it extremely clear that $\psi$ varies with time, requiring that the tsyganenko library be a function of time as well.

The dataset used here depicts the path of a satellite on a single day of its orbit 01-Jan-2019, at a $65^{\circ}$ inclined orbit.

```python
import tsyganenko as tsyg
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from spacepy import coordinates as coord
import spacepy.time as spt
from spacepy.time import Ticktock
import datetime as dt
from mpl_toolkits.mplot3d import Axes3D
import sys

#adding the year data here so I don't have to crush my github repo
pathname = '../../data-se3-path-planner/yearData/cuspCrossings2019/'
sys.path.append(pathname)

# find phi for a stationary cusp
r = np.linspace(0,10, 1000)
phi_c = tsyg.getPhi_c(r)
plt.plot(r,phi_c)
plt.ylabel('phi_c (radians)')
plt.xlabel('earth radii ($R_{e}$)')
plt.title('Tsyganenko Cusp Position')
plt.show()

```

Note that below, there are many ways that we could set up the subplot, but I just chose (programattically) to do it a certain way provided in http://matplotlib.org/examples/pylab_examples/subplots_demo.html

Note: The units for this system is solar magnetic degrees.  Some plots I did gave me that kind of bias to think that SM would be the best coordinate system for this. (That's probably not true but there are many ways to solve this problem).

```python
# the orbit is fairly easy to get
df = pd.read_csv('singleday.csv')
# df = pd.read_csv('5day.csv')
t = df['DefaultSC.A1ModJulian'] + 29999.5
x = df['DefaultSC.gse.X']
y = df['DefaultSC.gse.Y']
z = df['DefaultSC.gse.Z']

# set the "ticks"
cvals = coord.Coords([[i,j,k] for i,j,k in zip(x,y,z)], 'GSE', 'car')
cvals.ticks = Ticktock(t,'MJD')
# originally SM
sm = cvals.convert('SM', 'sph')

# t = np.asarray(t)
# plotting stuff
f, axarr = plt.subplots(2, sharex=True)
# axarr[0].plot(t, y)
axarr[0].set_title('Spacecraft Latitude')
axarr[0].set_ylabel('latitude (sm deg)')
axarr[0].plot(sm.ticks.MJD, sm.lati)
axarr[1].set_title('Spacecraft Longitude')
axarr[1].set_ylabel('longitude (sm deg)')
axarr[1].plot(sm.ticks.MJD, sm.long)

plt.xlabel('time (unit)')

plt.show()
```

The next thing to do is compute the cusp latitude and longitude using tsyganenko.  The cusp angular position is a function of radial distance from the earth as given in the Tsyganenko equation.  This means that different "tracks" will be given as we vary $R_{e}$.  Since I am using a very circular orbit, with $e \sim 0.02$, I will plot the cusp latitude and longitude below as a function of the spacecraft altitude in my GMAT file.  The semi major axis is given as $7191 km$, $\sim 1.127 R_{e}$.

```python
rs = 1.127 # average of the radius of the orbit
# rs = np.array([1,2,3])
phic = tsyg.getPhi_c(rs)
x,y,z= tsyg.tsygCyl2Car(phic,rs)
# print("x,y,z",xt,yt,zt)
print("x",x)
print("y",y)
print("z",z)

# next lets put these three coordinates into a spacepy coordinates object

singletrack = coord.Coords([[x,y,z]]*len(t), 'SM', 'car')
singletrack.ticks = Ticktock(t,'MJD')
singletrack = singletrack.convert('GEI','sph')
f, axarr2 = plt.subplots(2, sharex=True)
# axarr[0].plot(t, y)
axarr2[0].set_title('Cusp Longitude and Latitude')
axarr2[0].set_ylabel('latitude (sm deg)')
axarr2[0].scatter(singletrack.ticks.MJD, singletrack.lati)
axarr2[1].set_title('Cusp and Spacecraft Latitude')
axarr2[1].set_ylabel('longitude (sm deg)')
axarr2[1].scatter(singletrack.ticks.MJD, singletrack.long)
plt.xlabel('time (unit)')
f,axarr3 = plt.subplots(1)
axarr3.set_title('Lat vs Lon')
axarr3.set_ylabel('latitude')
axarr3.set_xlabel('longitude')
axarr3.plot(singletrack.long,singletrack.lati, label='r = 1.127 Re')
axarr3.scatter(sm.long,sm.lati,label='spacecraft orbit actual')
axarr3.legend(loc='lower left')
plt.show()
```

So I suppose this does show that for a region of the stationary cusp, that the satellite potentially crosses it.  However, the cusp location moves throughout the day so it's conceivable that the cusp avoids the orbit of the satellite, and I need a way to test for that.  The other big question is how do I integrate dipole tilt into this at the same time.  The first question I need to answer is whether or not the dipole tilt matters in SM.

One other approach I could take is to come up with the cusp position as $f(\rho,t)$, then compare that latitude and longitude to the spacecraft latitude and longitude. That's really what I need to do.

Step by Step what I will attempt to do:

1.  Move the cusp and prove that it is moving.  I'll use the r = 1.127Re track to do this and one years worth of data. x

2.  Once I have the cusp latitude and longitude, compare the satellite latitude and longitude.

3.  Do the cusp crossings count.

4.  Plot the lat/lon of the cusp/spacecraft and the cusp crossings subplots together.

From here one we will be using the data from a year's flight around the earth, because these trends are periodic over a year.

```python
# the orbit is fairly easy to get
#df = pd.read_csv(pathname+'zero.csv')
df = pd.read_csv('5day.csv')
t = df['DefaultSC.A1ModJulian'] + 29999.5
x = df['DefaultSC.gse.X']
y = df['DefaultSC.gse.Y']
z = df['DefaultSC.gse.Z']

# set the "ticks"
cvals = coord.Coords([[i,j,k] for i,j,k in zip(x,y,z)], 'GSM', 'car')
cvals.ticks = Ticktock(t,'MJD')
sm = cvals.convert('SM','sph')
gsm = cvals.convert('GEI', 'sph')

# t = np.asarray(t)
# plotting stuff
f, axarr = plt.subplots(2, sharex=True)
# axarr[0].plot(t, y)
axarr[0].set_title('Spacecraft Latitude')
axarr[0].set_ylabel('latitude (sm deg)')
axarr[0].plot(sm.ticks.MJD, sm.lati)
axarr[1].set_title('Spacecraft Longitude')
axarr[1].set_ylabel('longitude (sm deg)')
axarr[1].plot(sm.ticks.MJD, sm.long)

plt.xlabel('time (unit)')

plt.show()

f, a = plt.subplots(1)
plt.title('Solar Magnetic Orbit')
plt.xlabel('Solar Magnetic Longitude')
plt.ylabel('Solar Magnetic Latitude')
a.plot(sm.long,sm.lati)
plt.show()
```

```python
psi = tsyg.getTilt(t)
plt.plot(t,psi)
plt.title('Dipole Tilt')
plt.xlabel('MJD')
plt.ylabel('Dipole Tilt (gsm deg)')
plt.show()
# here the OUTPUT, psi is in degrees
```

```python
# output cusp position with the dipole tilt
r = 1.127
psi = np.deg2rad(psi)
phi_c = tsyg.getPhi_c(r,psi)
phi_c = np.rad2deg(phi_c)
plt.figure()
plt.plot(t,phi_c)
plt.title('Cusp location over time for $r = 1.127R_{e}$')
plt.xlabel('MJD')
plt.ylabel('Cusp Location (gsm deg)')
plt.show()
```

```python
# next lets get the spherical coordinates and plot the latitude and longitudes, which SHOULD
# allow us to get the cusp crossings.
xc,yc,zc = tsyg.tsygCyl2Car(phi_c,r)

#originally it was GSM
cusp_location = coord.Coords([[i,j,k] for i,j,k in zip(xc,yc,zc)], 'GSM', 'car')
cusp_location.ticks = Ticktock(t,'MJD')
cusp_location = cusp_location.convert('GEI','sph')
plt.scatter(cusp_location.long, cusp_location.lati)
plt.scatter(gsm.long,gsm.lati,color='green')
plt.xlabel('gei longitude (deg)')
plt.ylabel('gei latitude (deg)')
plt.title('Cusp Latitude vs. Longitude (gei)')
plt.show()

```

The biggest issues of this plot are that we aren't able to see where the cusp is at a certain time, it's just a ground track.  Therefore, we need to also put in the plots of lat/lon vs. time.

Close to solving this.  I think my cusp actually behaves somewhat correctly. The above plot was actually really promising, but for some reason my  Now I need to use the Tsyganenko equation as an function of the satellite's location.  It really don't make much sense to me why the cusp wouldn't seem to change in latitude.  My guess is that there's tiny circles there if you look closely at the data.

I'll deal with what I suspect are innacuracies later I feel like I've done okay thus far.  Like I "feel" like the equations are wrong.
Open Questions at this point:
1.  should the latitude go so low?
2.

```python
# plot of the latitude and the longitude of the spacecraft at each timestep,
f,ax = plt.subplots(2)
# i can probably use a list comprehension here for the cusp location
# just feel really unmotivated rn
# xk,yk,zk = tsyg.tsygCyl2Car()
# longitude vs. time
ax[0].set_title('Cusp and Satellite Longitude')
ax[1].set_title('Cusp and Satellite Latitude')
ax[0].plot(gsm.ticks.MJD, gsm.long)
ax[1].plot(gsm.ticks.MJD, gsm.lati)
plt.show()
```

```python
# testing the function for
x,y,z = tsyg.orbitalCuspLocation(cvals,t)

fig = plt.figure()
ax.plot(phi_c,r)
plt.show()
```


## Tests with different data transformations applied to XGBoost

Samples:
- original data
- linearly transformated data
- minmax scaled data
- pseudo-random data

```python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.impute import SimpleImputer
from xgboost import XGBRegressor
from sklearn.metrics import mean_absolute_error
```

```python
from sklearn.metrics import mean_squared_error
```

```python
import matplotlib.pyplot as plt
import seaborn as sns
import statistics
from IPython.display import display
```

```python
import warnings
warnings.filterwarnings("ignore")
```

```python
from sklearn.datasets import load_boston
X, y = load_boston(return_X_y=True)
```

```python
train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.2)
```

```python
# lidar com os valores ausentes
df_imputer = SimpleImputer()
train_X = df_imputer.fit_transform(train_X)
test_X = df_imputer.transform(test_X)
```

```python
def test_XGBoost(train_X, train_y, test_X, test_y, plot_residuals=True):
  # instanciar o modelo XGBoost
  model = XGBRegressor()

  # chamar o fit para o modelo
  model.fit(train_X, train_y, verbose=False)

  # fazer previsões em cima do dataset de teste
  predictions = model.predict(test_X)

  print("Mean Absolute Error: {:.2f}".format(mean_absolute_error(predictions, test_y)))
  if plot_residuals:
    plt.figure(figsize=(15,3))
    plt.subplot(1,2,1)
    plt.title("predictions")
    sns.distplot(predictions)
    plt.axvline(statistics.mean(predictions), color='red')

    plt.subplot(1,2,2)
    plt.title(r'residuals $\epsilon = |\hat{y} - y|$')
    sns.distplot(abs(predictions-test_y))
    plt.axvline(statistics.mean(abs(predictions-test_y)), color='red')

    plt.show()

    return mean_absolute_error(y_pred=predictions, y_true=test_y), mean_squared_error(y_true=test_y, y_pred=predictions)
```

### original data

```python
mae0, mse0 = test_XGBoost(train_X, train_y, test_X, test_y)
```

### linearly transformed

\begin{equation}
X_1 = \{x_j + k\}_{j=1}^N, k \in \mathbb{N}
\end{equation}

```python
k=20
mae1, mse1 = test_XGBoost(train_X+k, train_y+k, test_X+k, test_y+k)
```

### Min-Max scaled

\begin{equation}
\hat{X} = \left\{ \frac{x_j - min \{ X \} }{max \{ X \} - min \{ X \} } \right\}_{j=1}^N
\end{equation}

```python
def minmaxscale(v):
  return (v - v.min(axis=0))/(v.max(axis=0)-v.min(axis=0))

mae2, mse2 = test_XGBoost(
    minmaxscale(train_X),
    minmaxscale(train_y),
    minmaxscale(test_X),
    minmaxscale(test_y)
)
```

```python
import numpy as np

def randomize_matrix(X):
  X_ = X.copy()
  if len(X_.shape)==1: # vector
    for i in range(X_.shape[0]):
      X_[i] = np.random.randint(-20, 20)
  else: # matrix
    for lin in range(X_.shape[0]):
      for col in range(X_.shape[1]):
        X_[lin][col] = np.random.randint(-20, 20)
  return X_
```

### Pseudo-random data

```python
mae3, mse3 = test_XGBoost(
    randomize_matrix(train_X),
    randomize_matrix(train_y),
    randomize_matrix(test_X),
    randomize_matrix(test_y)
)
```

```python
erros = {
    'MAE': [mae0, mae1, mae2, mae3],
    'MSE': [mse0, mse1, mse2, mse3],
    'transf': ['original', 'linear', 'minmax', 'pseudo-random']
}

display(pd.DataFrame(erros))
```

      MAE
      MSE
      transf

      0
      1.947568
      7.555698
      original

      1
      1.960215
      8.868490
      linear

      2
      0.057456
      0.006035
      minmax

      3
      11.327111
      170.878044
      pseudo-random


Universidade Federal do Rio Grande do Sul (UFRGS)
Programa de Pós-Graduação em Engenharia Civil (PPGEC)

# Pré-Introdução à teoria das vibrações

### Aula 6 - Vibração forçada

*Daniel Barbosa Mapurunga Matos (Aluno PPGEC/UFRGS)*

```python
import numpy as np
import matplotlib.pyplot as plt
```

Nesta aula, serão apresentados exemplos de vibrações forçadas e as respectivas soluções da equação de equilíbrio dinâmico exposta abaixo.

\begin{align}
m\ddot{u}(t) + c\dot{u}(t) + ku(t) = F(t)
\end{align}

# 1. Vibração forçada harmonicamente

De forma simplificada, os carregamentos harmônicos serão definidos como funções seno e cosseno, como exposto abaixo:

\begin{align}
F = f_0 \sin{\omega t}\\
F = f_0 \cos{\omega t}
\end{align}

Usando a função seno como exemplo, é possível escrever a equação do movimento da seguinte forma:

\begin{align}
m\ddot{u}(t) + c\dot{u}(t) + ku(t) = f_0 \sin{\omega t}
\end{align}

Dessa forma, é possível resolver esta equação diferencialseparando-a em duas soluções: solução homogênea e solução particular.

\begin{align}
u(t) = u_{homogênea} + u_{particular}
\end{align}

A solução homogêna consiste na resolucão de uma vibração livre, enquanto a solução particular será feita a partir do teorema da preservação da frequência, onde a frequência da resposta terá mesma frequência de oscilação da carga.

## 1.1 Sistema não amortecido

Num sistema sem amortecimento, a equação de equilíbrio pode ser escrita por:
\begin{align}
m\ddot{u}(t) + ku(t) = f_0 \sin{\omega t}
\end{align}

 Para , a resposta homogêna ser pode ser escrita por:

 \begin{align}
 u_{homogênea} = A \cos{\omega_n t} + B \sin{\omega_n t}
 \end{align}

 Enquanto a solução particular será dada pelo teorema da preservação da frequência:

\begin{align}
u_{particular} = C \sin{\omega t}
\end{align}

Substituindo a solução particular na equação de equilíbrio, é possível descobrir a constante C:

\begin{align}
-m \omega^2C\sin{\omega t} + kC \sin{\omega t} &= f_0 \sin{\omega t} \\
-m \omega^2 C + kC &= f_0 \\
-\frac{\omega^2}{\omega_n^2} C + C &= \frac{f_0}{k} \\
\end{align}
Apresentando o termo $\beta$ como:

\begin{align}
\beta &= \frac{\omega}{\omega_n} \\
\end{align}

É possível definir a constante C:

\begin{align}
C                  &= \frac{f_0}{k} \left(\frac{1}{1 - \beta^2}\right)
\end{align}

É importante observar que o termo $ \frac{f_0}{k}$ corresponde ao deslocamento estático do sistema.

Agora, é possível escrever a equação geral como:

\begin{align}
u(t) = A \cos{\omega_n t} + B \sin{\omega_n t} + \frac{f_0}{k} \left(\frac{1}{1 - \beta^2}\right) \sin{\omega t}
\end{align}

Onde as constantes A e B podem ser definidas a partir dos valores iniciais do sistema $u(0)$ e $ \dot{u}(0)$. Para um sistema com valores iniciais nulos $ u(0) = \dot{u}(0) = 0$, as constantes serão:
\begin{align}
A &= 0 \\
B &= -\frac{f_0 \beta}{k}\left(\frac{1}{1 - \beta^2}\right)
\end{align}

Sendo possível, agora, definir a expressão do deslocamento:

\begin{align}
u(t) = \frac{f_0}{k}\left(\frac{1}{1 - \beta^2}\right) (\sin{\omega t} - \beta\sin{\omega_n t})
\end{align}

Com isso, é possível perceber que o deslocamento pode ser escrito em fução do seu deslocamento estático:

\begin{align}
u(t) = u_{est} R(t)
\end{align}

em que $R(t)$ é a função de ganho, que pode ser escrita por:

\begin{align}
R(t) = \left(\frac{1}{1 - \beta^2}\right) (\sin{\omega t} - \beta\sin{\omega_n t})
\end{align}

```python
# Fenômeno do batimento
F0 = 1000                                                      # Amplitude da força
wn = 2                                                         # Frequência natural
w  = 1.9                                                         # Frequência da oscilação
beta = w/wn
m  = 200                                                       # Massa do sistema
k  = wn**2 * m                                                 # Rigidez
ust = F0/k                                                     # Deslocamento estático
t = np.linspace(0,200,1000)
R = (1/(1-beta**2))*(np.sin(w*t) - beta*np.sin(wn*t))          # Função de ganho
u = ust*R                                                      # Deslocamento

```

```python
plt.figure(1,figsize = (8,6))
plt.plot(t,u,'b')
plt.xlabel('tempo(s)') ; plt.ylabel('deslocamento(m)')
plt.grid(True)
```

## 1.2 Sistema amortecido

No caso de um sistema amortecido, o mesmo procedimento deve ser realizado para a resolução da equação de equilíbrio dinâmico.

\begin{align}
m\ddot{u}(t) + c \dot{u} + ku(t) = f_0 \sin{\omega t}
\end{align}

A solução homogênea é igual a de um sistema em vibracão livre amortecida, podendo ser escrita como:

\begin{align}
u_{homogênea} = e^{-\zeta\omega_nt}(A\cos{\omega_nt} + B\sin{\omega_nt})
\end{align}

Já a solução particular será dada pelo teorema da preservação da frequência:

\begin{align}
u_{particular} = G_1\cos{\omega t} + G_2\sin{\omega t}
\end{align}

Substituindo a solução particular na equação de equilíbrio dinâmico, é possível obter as constantes $G_1$ e $G_2$.

\begin{align}
G_1 &= \frac{f_0}{k}\left(\frac{-2 \zeta \beta}{(1- \beta^2)^2 + (2 \zeta \beta)^2}\right) \\
G_2 &= \frac{f_0}{k}\left(\frac{1 - \beta^2}{(1- \beta^2)^2 + (2 \zeta \beta)^2}\right)
\end{align}

Portanto, a resposta total pode ser dada por:

\begin{align}
u(t) = e^{-\zeta\omega_nt}(A\cos{\omega_nt} + B\sin{\omega_nt}) + \\
\frac{f_0}{k}\left(\frac{1}{(1- \beta^2)^2 + (2 \zeta \beta)^2}\right)\left[(1- \beta^2)\sin{\omega t}- 2 \zeta \beta\cos{\omega t}\right]
\end{align}

O primeiro termo da expressão é chamado de estado transiente, que perderá sua importância com o tempo, visto que o amortecedor diminui a amplitude da resposta. Já o segundo termo é chamado de estado harmônico, que continuará indefinidamente. Ignorando a parcela transiente e transformado a soma $ \sin{\omega t} + \cos{\omega t}$ em $ \sin{(\omega t + \theta)}$, escreve-se a equação da seguinte forma:

\begin{align}
u(t) &= \rho \sin{(\omega t + \theta)} \\
\end{align}

em que $\rho$ e $\theta$ são, respectivamente a amplitude do movimento e o ângulo de fase, que podem ser escritos por:
\begin{align}
\rho   &= \frac{f_0}{k}\sqrt{\frac{1}{(1- \beta^2)^2 + (2 \zeta \beta)^2}} \\
\theta &= \tan^{-1}\left[\frac{2\zeta \beta}{1-\beta^2}\right]
\end{align}

Observando agora que o pico de deslocamento ocorrerá quando $\sin{(\omega t + \theta)} = 1$ e que $\frac{f_0}{k} = u_{est}$, pode-se calcular o máximo deslocamento do sistema a partir da relação:
\begin{align}
u_{dyn} &= A  u_{est} \\
A       &= \sqrt{\frac{1}{(1-\beta^2)^2 + (2\zeta\beta)^2)}}
\end{align}

Onde A é o fator de amplificação dinâmico.

Analisando o fator de amplificação em função de $\beta$, é possível visualizar a influência da frequência do carregamento no deslocamento máximo do sistema, assim como a influência do fator de amortecimento.

```python
wn = 3
z  = [0.000001,0.01,0.03,0.1,0.3,0.5,1]
w  = np.linspace(0,10,1000)
beta = w/wn
```

```python
plt.figure(2,figsize=(6,6))
plt.xlim(0.8,1.2) ; plt.ylim(0,55)
for i in z:
    A = np.sqrt(1/((1-beta**2)**2 + (2*i*beta)**2))
    plt.plot(beta,A)
    plt.legend(('ζ = 0','ζ = 0.01','ζ = 0.03','ζ = 0.1','ζ = 0.5','ζ = 1'))
    plt.xlabel('Frequência adimensional(β)') ; plt.ylabel('Fator de amplificação (A)')
    plt.grid(True)
```

Percebam que o máximo fator de amplificação, independente do valor de $\zeta$ ocorre quando $\omega = \omega_n$, ou seja, $\beta = 1$. Isto ocorre porque o sistema entrou em **ressonância**.

## 1.3 Ressonância

O fenômeno de ressonância ocorre quando o sistema recebe o máximo de energia proviniente da excitação externa. Nos gráficos acima, é possível observar que o pico de deslocamento ocorre quando $\beta$ é igual a 1, ou seja, quando a frequência de excitação é igual a frequência natural do sistema.

No caso de um sistema não amortecido, como não há dissipação de energia, um carregamento ressonante acarreta num fator de amplificação infinito. Para observar este fenômeno, observe o gráfico abaixo:

```python
F0 = 1000
wn = 2
w  = 1.9999999
beta = w/wn
m  = 2
k  = wn**2 * m
ust = F0/k
t = np.linspace(0,200,1000)
R = (1/(1-beta**2))*(np.sin(w*t) - beta*np.sin(wn*t))
u = ust*R

```

```python
plt.figure(3,figsize = (8,6))
plt.plot(t,u,'b')
plt.xlabel('tempo(s)') ; plt.ylabel('deslocamento(m)')
plt.grid(True)
```

No caso de um sistema amortecido em ressonância, a equação do deslocamento, considerando a parcela transiente com valores iniciais nulos $ u(0) = \dot{u}(0) = 0$, pode ser escrita por

\begin{align}
u(t) = e^{-\zeta\omega_nt}\left(\frac{f_0}{2k\zeta}\cos{\omega_nt} + \frac{f_0}{2k\sqrt{1-\zeta^2}}\sin{\omega_nt}\right) - \frac{f_0}{k} \frac{\cos{\omega t}}{2\zeta}
\end{align}

Observe o gráfico abaixo onde o fator de amplificação pode ser facilmente identificado pela relação:

\begin{align}
A = \frac{1}{2\zeta}
\end{align}

```python
t = np.linspace(0,300,1000)
z     = 0.01
wn    = 2
w     = 2
u1    = F0/2/k/z *np.cos(wn*t)
u2    = F0/2/k/np.sqrt(1-z**2)*np.sin(wn*t)
u3    = -F0/k/2/z* np.cos(w*t)

u_z   = np.exp(-z*wn*t)*(u1+u1) + u3
A     = np.ones(len(t))/2/z

```

```python
plt.figure(4,figsize=(12,4))
plt.plot(t,u_z/ust,'c')
plt.plot(t,A,'r--',t,-A,'r--')
plt.xlabel('tempo (s)')
plt.legend(('Função de amplificação','Fator de amplificação'))
plt.grid(True)
```

```python
print(' ---------------------------------------','\n'
      ' Deslocamento estático:           {}m'.format(ust),'\n'
      ' Fator de amplificação:             {0:.1f}'.format(A[0]),'\n',
      'Deslocamento dinâmico:            {0:.0f}m'.format(np.max(u_z)),'\n',
      '--------------------------------------')
```

     ---------------------------------------
     Deslocamento estático:           125.0m
     Fator de amplificação:             50.0
     Deslocamento dinâmico:            6250m
     --------------------------------------

# 2. Resposta impulsiva

Antes da realização da análise de um sistema sob ação de um carregamento impulsivo, é necessário relembrar o conceito de impulso e de quantidade de movimento, assim como a relação entre os dois.

## 2.1 Impulso
Impulso é definido fisicamente como a variação da quantidade de movimento e pode ser descrita matematicamente como a integral da força em relação ao tempo.
\begin{align}
I = \int_{0}^{td} F dt
\end{align}

Esta relação indica a área da figura num gráfico força x tempo.

## 2.2 Quantidade de movimento
A quantidade de movimento define o quanto de matéria está se movendo e a velocidade com que ela se move, sendo possível escreve-la matematicamente como o produto de massa e velocidade.

\begin{align}
Q = mv
\end{align}

## 2.3 Teorema do impulso

O teorema do impulso relaciona o impulso com a quantidade de movimento a partir da definição física de impulso, sendo assim:

\begin{align}
I &= \Delta Q \\
I &= m_2v_2 - m_1v_1
\end{align}

## 2.4 Resposta impulsiva para carregamentos de formato definido

Para certos carregamentos impulsivos em que o formato do carregamento influencia na resposta do sistema, exite um ábaco para a definição do fator de amplificação.

A partir do ábaco, tendo como dado de e entrada a razão entre o tempo de duração do impulso e o período de oscilação do sistema, retira-se o fator de amplificação dinâmica.

\begin{align}
u_{dyn} &= A  u_{est}
\end{align}

## 2.5 Aproximação para análise de carregamento impulsivo

Para carregamentos com duração menor que $\frac{T_n}{4}$, em que $T_n$ é o período de oscilação do sistema, o formato do carregamento não é de suma importância para a resposta do sistema, sendo possível considerar o impulso como uma velocidade inicial aplicada ao sistema.

Considerando que o sistema está em repouso e aplicando o teorema do impulso, representa-se o impulso por uma velocidade inicial.

\begin{align}
I &= m(v - v_0) \\
v &= \frac{I}{m}
\end{align}

Então, para uma vibração livre não-amortecida é possível escrever a equação de equilíbrio como:

\begin{align}
u(t) &= \frac{v}{\omega_n} \sin{\omega_n t} \\
u(t) &= \frac{I}{m \omega_n} \sin{\omega_n t}
\end{align}

Sendo possível calcular o deslocamento máximo por:

\begin{align}
u_{max} = \frac{I}{m \omega_n}
\end{align}

Para a vibração livre amortecida, utiliza-se o mesmo procedimento de substituir o carregamento impulsivo por uma velocidade inicial.

### Consideração importante:

E  se o projétil que colidiu com o sistema permanecer acoplado neste após a colisão? Utiliza-se a conservação da quantidade de movimento

\begin{align}
Q_{final}  &= Q_{inicial}\\
(m_p + m)v &= m_pv_p + mv_0 \\
v          &=\frac{m_pv_p + mv_0}{m_p+m} \\
u(t) &= \frac{v}{ \omega_n} \sin{\omega_n t}
\end{align}

É importante ressaltar que a frequência natural do sistema deve ser calculada com a massa total, visto que isso provocará alterações significativas no comportamento do sistema.

```python

```


Homework 5

Yutong Dai yutongd3@illinois.edu

## 5.2

Rewrite the problem in the standard form,

$$
\begin{align}
& \min \quad 2x_2  - x_3\\
& s.t \quad -x_1 + 2x_2 - x_3 + x_4 = 4\\
& \qquad x_1 + x_2 + x_3 + x_5 = 7\\
& \qquad 2x_1 -x_2 -x_3 + x_6 = 5 \\
& \qquad x_1,...,x_6\geq 0
\end{align}
$$

Note $z_j=C_B^TB^{-1}A_{.j}$

**Step 1:**

* $(c_1-z_1, c_2-z_2, c_3-z_3)=(0,2,-1)$ and $\delta=7$$\rightarrow$ Pivot on $x_5$.

|   | z | $x_4$ | $x_5$ | $x_6$ | RHS | $x_3$ |
| --- | --- | --- | --- | --- | --- | --- |
| z | -1 | 0 | 0 | 0 | 0 | -1 |
| $x_4$ | 0 | 1 | 0 | 0 | 4 | -1 |
| $x_5$ | 0 | 0 | 1 | 0 | 7 | 1 |
| $x_6$ | 0 | 0 | 0 | 1 | 5 | -1 |

**Step 2:**

* $(c_1-z_1, c_2-z_2, c_5-z_5)=(1,3,1)$. Stop

|   | z | $x_4$ | $x_3$ | $x_6$ | RHS |
| --- | --- | --- | --- | --- | --- |
| z | -1 | 0 | 1 | 0 |7 |  |
| $x_4$ | 0 | 1 | 1 | 0 | 11 |
| $x_3$ | 0 | 0 | 1 | 0 | 7 |
| $x_6$ | 0 | 0 | 1 | 1 | 12 |

So the solution is $(0,0,7)$ and the optimal value is $7$

## 5.12

Rewrite the problem in the standard form,

$$
\begin{align}
& \min \quad x_1 + 2x_2  + x_3 - x_4\\
& s.t \quad 2x_1 - x_2 + x_3 - 2x_4 + x_5 = 6\\
& \qquad -x_1 + 2x_2 - x_3 + x_4 + x_6 = 8\\
& \qquad -2x_1 -x_2 +x_3 + x_7 = -2 \\
& \qquad 0\leq x_1\leq 3, 1\leq x_2 \leq 4, 0 \leq x_3 \leq 8, 2 \leq x_4 \leq 5 \\
& \qquad x_5,x_6,x_7 \geq 0
\end{align}
$$

**Setup the tableau as**

|  | $x_1$ | $x_2$ | $x_3$ | $x_4$ | $x_5$ | $x_6$ | $x_7$ | RHS |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|  | 1 | 2 | 3 | -1 | 0 | 0 | 0 | 0 |
| $x_5$ | 2 | -1 | 1 | -2 | 1 | 0 | 0 | 11 |
| $x_6$ | -1 | 2 | -1 | 1 | 0 | 1 | 0 | 5 |
| $x_7$ | -2 | -1 | 1 | 0  | 0 | 0 | 1 | 5 |
|  | 3 | 1 | 0 | 5 | 11 | 5 | 5 |  |
| lb | 0 | 1 | 0 | 2 | 0 | 0 | 0 |  |
| ub | 3 | 4 | 8 | 5 |  |  |  |  |

We can only decrease $x_1$ from its upper bound and pivot on $x_7$, $\delta=5/2$.

|  | $x_1$ | $x_2$ | $x_3$ | $x_4$ | $x_5$ | $x_6$ | $x_7$ | RHS |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|  | 0 | 3/2 | 7/2 | -1 | 0 | 0 | 1/2 | 5/2 |
| $x_5$ | 0 | -2 | 2 | -2 | 1 | 0 | 1 | 6 |
| $x_6$ | 0 | 5/2 | -3/2 | 1 | 0 | 0 | 1/2 | 15/2 |
| $x_1$ | 1 | 1/2 | -1/2 | 0 | 0 | 0 | -1/2 | 1/2 |
|  | 1/2 | 1 | 0 | 5 | 6 | 15/2 | 10 |  |
| lb | 0 | 1 | 0 | 2 | 0 | 0 | 0 |  |
| ub | 3 | 4 | 8 | 5 |  |  |  |  |

We are now at the optimal. The solution is $(1/2, 1,0,5)$ and the optimal value is $-5/2$.

## 5.28

Rewrite the problem in the standard form,

$$
\begin{align}
& \min \quad -3x_1  - 2x_2\\
& s.t \quad x_1 + 2x_2 + x_3  = 8\\
& \qquad x_1 + x_2 + x_4 = 5\\
& \qquad 2x_1 -x_2 -x_3 + x_6 = 5 \\
& \qquad 0\leq x_1\leq 3, 0\leq x_2 \leq 4 \\
& \qquad x_3,x_4 \geq 0
\end{align}
$$

**Setup the tableau**:

|  | $x_1$ | $x_2$ | $x_3$ | $x_4$ | RHS |
| --- | --- | --- | --- | --- | --- |
|  | -3 | -2 | 0 | 0 | 8  |
| $x_3$ | 1 | 2 | 1 | 0 | 0 |
| $x_4$ | 1 | 1 | 0 | 1 | 1 |
|  | 0 | 4 | 0 | 1 |  |
| lb | 0 | 0 | 0 | 0 |   |
| ub | 3 | 4 |   |   |  |

**Step 1**
We can increase $x_1$ from its lower bound and pivot on $x_3$. Note $\delta$=0

|  | $x_1$ | $x_2$ | $x_3$ | $x_4$ | RHS |
| --- | --- | --- | --- | --- | --- |
|  | 0 | 4 | 3 | 0 |  8 |
| $x_1$ | 1 | 2 | 1 | 0 | 0 |
| $x_4$ | 0 | -1 | -1 | 1 | 1 |
|  | 0 | 4 | 0 | 1 |  |
| lb | 0 | 0 | 0 | 0 |   |
| ub | 3 | 4 |   |   |  |

**Step 2**
We can decrease $x_2$ from its upper bound and pivot on $x_4$. Note $\delta$=1

|  | $x_1$ | $x_2$ | $x_3$ | $x_4$ | RHS |
| --- | --- | --- | --- | --- | --- |
|  | 0 | 0 | -1 | 4 | 12 |
| $x_1$ | 1 | 0 | -1 | 2 | 2 |
| $x_2$ | 0 | 1 | 1 | -1 | 3 |
|  | 2 | 3 | 0 | 0 |  |
| lb | 0 | 0 | 0 | 0 |  |
| ub | 3 | 4 |  |  |  |

**Step 3**
We can decrease $x_3$ from its lower bound and pivot on $x_1$. Note $\delta$=1

|  | $x_1$ | $x_2$ | $x_3$ | $x_4$ | RHS |
| --- | --- | --- | --- | --- | --- |
|  | -1 | 0 | 0 | 2 | -13 |
| $x_3$ | -1 | 0 | 1 | -2 | 1 |
| $x_2$ | 1 | 1 | 0 | 1 | 2 |
|  | 3 | 2 | 1 | 0 |  |
| lb | 0 | 0 | 0 | 0 |  |
| ub | 3 | 4 |  |  |  |

We stop at this point. So the solution is $(2,3)$ and the optimal value is $13$.

## 5.58

**Bounded Case**

$\Rightarrow$: Suppose $x_0$ is an extreme point has a minimal objective, then by definition, it has an objective that is smaller than or equal to that of any adjacent extreme point. Otherwise, its not the minimal.

$\Leftarrow$: Since this is a linear programming problem, we assume the objective function is $c^Tx$.

Note that for any $x\in P$, we have

$$x=x_0 + \sum_{j=1}^k \mu_j(x_j-x_0),$$

where $P$ is a polytope and $x_0$ is an extreme points and $x_1,...,x_k$ are all of its adjacent extreme points.

$$
c^Tx = c^Tx_0 + \sum_{j=1}^k \mu_jc^T(x_j-x_0) \geq c^Tx_0,
$$
where the inequality holds by the assumptions that $x_0$ has an objective that is smaller than or equal to that of any adjacent extreme point.

**Unbounded Case**:

Note that for any $x\in P$, we have

$$x=x_0 + \sum_{j\in I} \mu_j(x_j-x_0) + \sum_{j\in J} \mu_jd_j$$

where

1. $P$ is a unbounded polyhedral set.

2. $x_0$ is an extreme points and $\{x_j|j\in I\}$ are its adjacent extreme points.

3. $\{d_j|j\in J\}$ are its adjacent extreme directions.

$$
c^Tx = c^Tx_0 + \sum_{j\in I} \mu_jc^T(x_j-x_0) +  \sum_{j\in J} \mu_jc^Td_j
$$

As it is not guaranteed that $\sum_{j\in J} \mu_jc^Td_j \geq 0$, this can not be generalized to the unbounded case.

## Exercise 3.33

Notice that the number of extreme points are finite and we can move from any basic feasible solution to an adjacent one in a single step, which means no cycling, therefore, we can go from the extreme point $x$ to another extreme point $y$ in finite number of stpes.

## (1)

**Lemma:** In the simplex method, if the variable $x_r$ is leaving the basis at the iteration $k$, then it cannot enter the basis at the iteration $k+1$.

Since the degree of the degeneracy is $1$, then the degenerated extreme points can have only two basis, which are adjacent. By the lemma above, the one out can not enter in the very next step, then there's no cycling issue.


### Example 5: Laplace equation

In this tutorial we will look constructing the steady-state heat example using the Laplace equation. In contrast to the previous tutorials this example is entirely driven by the prescribed Dirichlet and Neumann boundary conditions, instead of an initial condition. We will also demonstrate how to use Devito to solve a steady-state problem without time derivatives and how to switch buffers explicitly without having to re-compile the kernel.

First, we again define our governing equation:
$$\frac{\partial ^2 p}{\partial x^2} + \frac{\partial ^2 p}{\partial y^2} = 0$$

We are again discretizing second-order derivatives using a central difference scheme to construct a diffusion problem (see tutorial 3). This time we have no time-dependent term in our equation though, since there is no term $p_{i,j}^{n+1}$. This means that we are simply updating our field variable $p$ over and over again, until we have reached an equilibrium state. In a discretised form, after rearranging to update the central point $p_{i,j}^n$ we have
$$p_{i,j}^n = \frac{\Delta y^2(p_{i+1,j}^n+p_{i-1,j}^n)+\Delta x^2(p_{i,j+1}^n + p_{i,j-1}^n)}{2(\Delta x^2 + \Delta y^2)}$$

And, as always, we first re-create the original implementation to see what we are aiming for. Here we initialise the field $p$ to $0$ and apply the following bounday conditions:

$p=0$ at $x=0$

$p=y$ at $x=2$

$\frac{\partial p}{\partial y}=0$ at $y=0, \ 1$

**Developer note:**
The original tutorial stores the field data in the layout `(ny, nx)`. Until now we have used `(x, y)` notation for creating our Devito examples, but for this one we will adopt the `(y, x)` layout for compatibility reasons.

```python
from examples.cfd import plot_field
import numpy as np
%matplotlib inline

# Some variable declarations
nx = 31
ny = 31
c = 1
dx = 2. / (nx - 1)
dy = 1. / (ny - 1)
```

```python
def laplace2d(p, bc_y, dx, dy, l1norm_target):
    l1norm = 1
    pn = np.empty_like(p)

    while l1norm > l1norm_target:
        pn = p.copy()
        p[1:-1, 1:-1] = ((dy**2 * (pn[1:-1, 2:] + pn[1:-1, 0:-2]) +
                         dx**2 * (pn[2:, 1:-1] + pn[0:-2, 1:-1])) /
                        (2 * (dx**2 + dy**2)))

        p[:, 0] = 0  # p = 0 @ x = 0
        p[:, -1] = bc_right  # p = y @ x = 2
        p[0, :] = p[1, :]  # dp/dy = 0 @ y = 0
        p[-1, :] = p[-2, :]  # dp/dy = 0 @ y = 1
        l1norm = (np.sum(np.abs(p[:]) - np.abs(pn[:])) /
                  np.sum(np.abs(pn[:])))

    return p
```

```python
#NBVAL_IGNORE_OUTPUT

# Out initial condition is 0 everywhere,except at the boundary
p = np.zeros((ny, nx))

# Boundary conditions
bc_right = np.linspace(0, 1, ny)
p[:, 0] = 0  # p = 0 @ x = 0
p[:, -1] = bc_right  # p = y @ x = 2
p[0, :] = p[1, :]  # dp/dy = 0 @ y = 0
p[-1, :] = p[-2, :]  # dp/dy = 0 @ y = 1

plot_field(p, ymax=1.0, view=(30, 225))
```

```python
#NBVAL_IGNORE_OUTPUT

p = laplace2d(p, bc_right, dx, dy, 1e-4)
plot_field(p, ymax=1.0, view=(30, 225))
```

Ok, nice. Now, to re-create this example in Devito we need to look a little bit further under the hood. There are two things that make this different to the examples we covered so far:
* We have no time dependence in the `p` field, but we still need to advance the state of p in between buffers. So, instead of using `TimeFunction` objects that provide multiple data buffers for timestepping schemes, we will use `Function` objects that have no time dimension and only allocate a single buffer according to the space dimensions. However, since we are still implementing a pseudo-timestepping loop, we will need to objects, say `p` and `pn`, to act as alternating buffers.
* If we're using two different symbols to denote our buffers, any operator we create will only perform a single timestep. This is desired though, since we need to check a convergence criteria outside of the main stencil update to determine when we stop iterating. As a result we will need to call the operator repeatedly after instantiating it outside the convergence loop.

So, how do we make sure our operator doesn't accidentally overwrite values in the same buffer? Well, we can again let SymPy reorganise our Laplace equation based on `pn` to generate the stencil, but when we create the update expression, we set the LHS to our second buffer variable `p`.

```python
from devito import Grid, Function, Eq, INTERIOR
from sympy import solve

# Create two explicit buffers for pseudo-timestepping
grid = Grid(shape=(nx, ny), extent=(1., 2.))
p = Function(name='p', grid=grid, space_order=2)
pn = Function(name='pn', grid=grid, space_order=2)

# Create Laplace equation base on `pn`
eqn = Eq(pn.laplace, region=INTERIOR)
# Let SymPy solve for the central stencil point
stencil = solve(eqn, pn)[0]
# Now we let our stencil populate our second buffer `p`
eq_stencil = Eq(p, stencil)

# In the resulting stencil `pn` is exclusively used on the RHS
# and `p` on the LHS is the grid the kernel will update
print("Update stencil:\n%s\n" % eq_stencil)
```

    Update stencil:
    Eq(p(x, y), 0.5*(h_x**2*pn(x, y - h_y) + h_x**2*pn(x, y + h_y) + h_y**2*pn(x - h_x, y) + h_y**2*pn(x + h_x, y))/(h_x**2 + h_y**2))

Now we can add our boundary conditions. We have already seen how to prescribe constant Dirichlet BCs by simply setting values using the low-level notation. This time we will go a little further by setting a prescribed profile, which we create first as a custom 1D symbol and supply with the BC values. For this we need to create a `Function` object that has a different shape than our general `grid`, so instead of the grid we provide an explicit pair of dimension symbols and the according shape for the data.

```python
x, y = grid.dimensions
bc_right = Function(name='bc_right', shape=(nx, ), dimensions=(x, ))
bc_right.data[:] = np.linspace(0, 1, nx)
```

Now we can create a set of expressions for the BCs again, where we wet prescribed values on the right and left of our grid. For the Neuman BCs along the top and bottom boundaries we simply copy the second rwo from the outside into the outermost row, just as the original tutorial did. Using these expressions and our stencil update we can now create an operator.

```python
#NBVAL_IGNORE_OUTPUT
from devito import Operator

# Create boundary condition expressions
bc = [Eq(p.indexed[x, 0], 0.)]  # p = 0 @ x = 0
bc += [Eq(p.indexed[x, ny-1], bc_right.indexed[x])]  # p = y @ x = 2
bc += [Eq(p.indexed[0, y], p.indexed[1, y])]  # dp/dy = 0 @ y = 0
bc += [Eq(p.indexed[nx-1, y], p.indexed[nx-2, y])]  # dp/dy = 0 @ y = 1

# Now we can build the operator that we need
op = Operator(expressions=[eq_stencil] + bc)
```

We can now use this single-step operator repeatedly in a Python loop, where we can arbitrarily execute other code in between invocations. This allows us to update our L1 norm and check for convergence. Using our pre0compiled operator now comes down to a single function call that supplies the relevant data symbols. One thing to note is that we now do exactly the same thing as the original NumPy loop, in that we deep-copy the data between each iteration of the loop, which we will look at after this.

```python
#NBVAL_IGNORE_OUTPUT

# Silence the runtime performance logging
from devito import configuration
configuration['log_level'] = 'ERROR'

# Initialise the two buffer fields
p.data[:] = 0.
p.data[:, -1] = np.linspace(0, 1, ny)
pn.data[:] = 0.
pn.data[:, -1] = np.linspace(0, 1, ny)

# Visualize the initial condition
plot_field(p.data, ymax=1.0, view=(30, 225))

# Run the convergence loop with deep data copies
l1norm_target = 1.e-4
l1norm = 1
while l1norm > l1norm_target:
    # This call implies a deep data copy
    pn.data[:] = p.data[:]
    op(p=p, pn=pn)

    l1norm = (np.sum(np.abs(p.data[:]) - np.abs(pn.data[:])) /
              np.sum(np.abs(pn.data[:])))

# Visualize the converged steady-state
plot_field(p.data, ymax=1.0, view=(30, 225))
```

One crucial detail about the code above is that the deep data copy between iterations will really hurt performance if we were to run this on a large grid. However, we have already seen how we can match data symbols to symbolic names when calling the pre-compiled operator, which we can now use to actually switch the roles of `pn` and `p` between iterations, eg. `op(p=pn, pn=p)`. Thus, we can implement a simple buffer-switching scheme by simply testing for odd and even time-steps, without ever having to shuffle data around.

```python
#NBVAL_IGNORE_OUTPUT

# Initialise the two buffer fields
p.data[:] = 0.
p.data[:, -1] = np.linspace(0, 1, ny)
pn.data[:] = 0.
pn.data[:, -1] = np.linspace(0, 1, ny)

# Visualize the initial condition
plot_field(p.data, ymax=1.0, view=(30, 225))

# Run the convergence loop by explicitly flipping buffers
l1norm_target = 1.e-4
l1norm = 1
counter = 0
while l1norm > l1norm_target:
    # Determine buffer order
    if counter % 2 == 0:
        _p = p
        _pn = pn
    else:
        _p = pn
        _pn = p

    # Apply operator
    op(p=_p, pn=_pn)

    # Compute L1 norm
    l1norm = (np.sum(np.abs(_p.data[:]) - np.abs(_pn.data[:])) /
              np.sum(np.abs(_pn.data[:])))
    counter += 1

plot_field(p.data, ymax=1.0, view=(30, 225))
```


```python
from sympy import init_session
init_session()
```

    IPython console for SymPy 1.5.1 (Python 3.6.9-64-bit) (ground types: gmpy)

    These commands were executed:
    >>> from __future__ import division
    >>> from sympy import *
    >>> x, y, z, t = symbols('x y z t')
    >>> k, m, n = symbols('k m n', integer=True)
    >>> f, g, h = symbols('f g h', cls=Function)
    >>> init_printing()

    Documentation can be found at https://docs.sympy.org/1.5.1/

```python
#init_printing?
```

### Parameters and two Gaussians

```python
a, b, c, a1, a2 = symbols('a b c a1 a2', positive=True, real=True)
```

```python
u1=x**2*exp(-a1*x**2)
u2=x**2*exp(-a2*x**2)
u1, u2
```

### Theory  (Fingers crossed)
We don't solve the radial equation for $R(r)$, but rather for $u(r) = r \cdot R(r)$.
So, if $R$ is a s-function, $u$ is a p-function, and if $R$ is a p-function, $u$ is a d-function, etc.

$S = \int r^2dr R^2 = \int dr\, u^2$

$T = \int r^2dr R_1 \hat{T} R_2 = \int dr\, u_1\, \frac{d^2}{dr^2} \, u_2 $

### Normalization constant

```python
N=integrate(u1*u1, (x, 0, oo))
N
```

```python
1/sqrt(N)
```

```python
printing.sstrrepr(1/sqrt(N))
```

    '4*2**(3/4)*sqrt(3)*a1**(5/4)/(3*pi**(1/4))'

### Overlap integral S

```python
S=integrate(u1*u2, (x, 0, oo))
S
```

```python
S.simplify()
```

```python
printing.sstrrepr(S.simplify())
```

    '3*sqrt(pi)/(8*(a1 + a2)**(5/2))'

### Kinetic energy $T = -\frac{\hbar^2}{2m} \frac{d^2}{dx^2} = \frac{1}{2m}\left(\frac{\hbar}{i}\frac{d}{dx} \right)^2$

```python
d1=diff(u1,x)
d2=diff(u2,x)
d1, d2
```

```python
Tsym = 1/2 * integrate(d1*d2, (x, 0, oo))
#T=T.simplify()
#T=T.factor()
Tsym.factor()
Tsym.simplify()
```

```python
dd1=diff(u1,x,2)
dd2=diff(u2,x,2)
dd1, dd2
```

```python
T1 = -1/2 * integrate(u1*dd2, (x, 0, oo))
T1.factor()
```

```python
T2 = -1/2 * integrate(u2*dd1, (x, 0, oo))
T2.factor()
```

```python
2/11
```

```python
printing.sstrrepr(Tsym.simplify())
```

    'sqrt(pi)*(1.875*a1*a2 - 0.25*(a1 + a2)**2)/(a1 + a2)**(7/2)'

### Potential $V(x) = (ax^2 - b)e^{-cx^2}$

```python
v=(a*x**2-b)*exp(-c*x**2)
v
```

```python
V = integrate(u1*v*u2, (x, 0, oo))
V
```

```python
V.factor()
```

```python
printing.sstrrepr(V.factor())
```

    '3*sqrt(pi)*(5*a - 2*a1*b - 2*a2*b - 2*b*c)/(16*(a1 + a2 + c)**(7/2))'

Angular momentum potential: $V = \frac{l(l+1)}{2\mu r^2 }$; for $l=1$: $V = \frac{1}{\mu r^2 }$

```python
vL=1/x**2
vL
```

```python
VL = integrate(u1*vL*u2, (x, 0, oo))
VL.simplify()
```

```python
printing.sstrrepr(VL.simplify())
```

    'sqrt(pi)/(4*(a1 + a2)**(3/2))'

```python

```


# Linear Regression

```python
import numpy as np
import sympy as sym
```

```python
%pylab inline
plt.style.use('ggplot')
plt.rcParams['image.interpolation'] = 'nearest'
```

    Populating the interactive namespace from numpy and matplotlib

$$ f(x) = x^4-4x^3+2 $$

```python
f=lambda x: x**4-4*x**3+2
```

```python
x=np.linspace(-5,5,1000)
y= f(x)
plt.plot(x,y)
```

### Brute-force Search
Lets do a brute-force search for the given variable in the limit.

```python
minimum=+np.inf
mx=+np.inf

# Your code here

print("Local minimum occurs at {}".format(mx))
```

    Local minimum occurs at inf

### Gradient Descent

```python
def f_derivative(f):
    # Your code here
    # Calculate the derivative and write your code here
    return sym.diff(f)
```

```python
x_old = 0
x_new = np.random.rand(1,1)
eps = 0.01 # step size
precision = 0.00001

# Your code here

print ("Local minimum occurs at", x_new)
```

    Local minimum occurs at [[0.77524489]]

#### Always Check your gradient, before debugging gradient descent,

We know that $$\frac{\partial f}{\partial x}=\frac{f(x+\epsilon)-f(x)}{\epsilon}$$

So always compare the difference between computed gradient using this formula.

#### Lets Check

```python
eps=0.00001
x=1.5
dfx=(f(x+eps)-f(x))/eps
cdfx=f_derivative(x)

print (dfx, cdfx, dfx-cdfx)
```

    -13.500044999936732 0 -13.5000449999367

### Let's generate some random data

```python
X = np.arange(0,5,0.1, dtype=np.float32)
delta = np.random.uniform(-1,1, size=X.shape[0])
Y = .4 * X + 3 + delta

#making a copy for later use
rx = X
ry = Y
```

```python
plt.scatter(X,Y)
plt.xlabel('X')
plt.ylabel('Y')
```

### Let's visualize our solution space

```python
cf=lambda t0,t1,x,y: np.dot(t1*x+t0-y,t1*x+t0-y)
#square error
```

```python
t0=arange(-5,+5,0.01)
t1=arange(-5,+5,0.01)
```

```python
ecf=np.zeros((len(t0),len(t1)))

for i in range(len(t0)):
    for j in range(len(t1)):
        ecf[i,j]=cf(t0[i],t1[j],X,Y)
```

```python
print (ecf)
```

    [[25677.22633862 25613.85975657 25550.57693729 ...  2644.38232815
       2661.62473567  2678.94713073]
     [25655.98300977 25592.64084124 25529.38241119 ...  2647.56588198
       2664.83278707  2682.17968925]
     [25634.74963327 25571.43229164 25508.19857224 ...  2650.75946307
       2668.05084845  2685.42226257]
     ...
     [ 9462.99708244  9424.05735435  9385.20027957 ... 10783.37205352
      10825.04175003 10866.78985663]
     [ 9451.72380512  9412.80858967  9373.9760032  ... 10796.52583513
      10838.22003292 10879.99263935]
     [ 9440.46069555  9401.57002155  9362.76195988 ... 10809.689371
      10851.40793207 10893.20517065]]

```python
# Generate theta_0 and theta_1 values for the cost function...
tX, tY = np.meshgrid(t0, t1)
print (tX.shape, tY.shape)
```

    (1000, 1000) (1000, 1000)

```python
print (ecf.shape)
```

    (1000, 1000)

```python
#Plot the cost function...
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = Axes3D(fig)
ax.plot_surface(tX,tY,ecf.T)
# ax.set_xlabel(r'$\theta_0$(Intercept)')
# ax.set_ylabel(r'$\theta_1$(Slope)')
# ax.set_zlabel(r'$J(\theta_0,\theta_1)$(Cost Function)')
```

### Time for our regression implementation

$$
h(X, \theta) = X^T . \theta
$$

```python
def hyp(theta, X):
    # Your code here
    #take dot product
    transX=np.transpose(X)
    return np.dot(X,theta)
```

$$
cost = \frac{1}{2m} \sum_{i = 0}^m{(h(X^i, \theta)-Y^i)}^2
$$

```python
def cost_function(theta,X,Y):
    # Your code here
    #hyp-Y whole square and sum and divide by 2 and return it
    m=len(Y)
    hypothesis=hyp(theta,X)

    mse = np.sum((hypothesis - Y)**2)
    # root mean squared error
    # m is the number of training examples
    #rmse = np.sqrt(mse/m)

    return mse/(m*2) #(1/2*m) * np.sum(np.square(hypothesis-Y))
```

$$
\frac{\delta}{\delta \theta_j} = \frac{1}{m} \sum_{i = 0}^m{(h(X^i, \theta)-Y^i)} * X_j
$$

```python

```

```python
def derivative_cost_function(theta,X,Y):
    nexamples=float(X.shape[0])
    # Your code here
    # take x product and divide by n of cost_function
    hypothesis=hyp(theta,X)
    res=np.dot((hypothesis-Y).T,X)
    dtheta=(res/nexamples)
    return dtheta.T
```

```python
print (X.shape)
nx=np.hstack((X,ones(len(X),)))
nx=nx.reshape((2,X.shape[0])).T
print (nx.shape)
```

    (50,)
    (50, 2)

```python
np.random.seed(20) # To make sure you have the same value as me
eps=0.0001
nexamples=float(nx.shape[0])
thetas=np.random.rand(nx.shape[1],)
print (thetas)
```

    [0.5881308  0.89771373]

```python
cf=cost_function(thetas,nx,Y)
print (cf)
```

    1.509827527839499

```python
ad=derivative_cost_function(thetas,nx,Y)
print (ad)
```

    [-3.77643884 -1.65938466]

```python
#Gradient Descent...
def GradientDescent(X,Y,cost_function,derivative_cost_function,maxniter=20000):
    #Calculate the hypothesis h = X * theta
    #Calculate the loss = h - y and maybe the squared cost (loss^2)/2m
    #Calculate the gradient = X' * loss / m
    #Update the parameters theta = theta - alpha * gradient
    nexamples=float(X.shape[0])
    thetas=rand(X.shape[1],)
    eps=0.00001
    alpha=0.01
    cf=[]
    numiter=0
    m=len(Y)
    for numiter in range(0,maxniter):
        hypothesis=hyp(thetas,X)
        loss=hypothesis-Y
        cost = np.sum(loss ** 2) / (2 * m)
        gradient = np.dot(X.T, loss) / m
        thetas = thetas - alpha * gradient

    # Your code here

    return thetas
```

```python
theta_new=GradientDescent(nx,Y,cost_function,derivative_cost_function)
theta_new
```

    array([0.44932953, 2.89716151])

```python
plt.scatter(X,Y)
plt.xlabel('X')
plt.ylabel('Y')
plt.plot(nx[:,0],np.dot(nx,theta_new))
```

```python

```

```python

```