Download the dataset from the following link:

https://drive.google.com/open?id=0B4c_CWtMboiGRHJhWWZMbGpQazFxcHNfUkhnQ1pQU09IeXFR

If you read the dataset by importing pandas library and using pd.read_csv():

```python
import pandas as pd
```

```python
data = pd.read_csv("./Datasets/data.csv")
```

```python
data.head()
```

      id
      diagnosis
      radius_mean
      texture_mean
      perimeter_mean
      area_mean
      smoothness_mean
      compactness_mean
      concavity_mean
      concave points_mean
      ...
      texture_worst
      perimeter_worst
      area_worst
      smoothness_worst
      compactness_worst
      concavity_worst
      concave points_worst
      symmetry_worst
      fractal_dimension_worst
      Unnamed: 32

      0
      842302
      M
      17.99
      10.38
      122.80
      1001.0
      0.11840
      0.27760
      0.3001
      0.14710
      ...
      17.33
      184.60
      2019.0
      0.1622
      0.6656
      0.7119
      0.2654
      0.4601
      0.11890
      NaN

      1
      842517
      M
      20.57
      17.77
      132.90
      1326.0
      0.08474
      0.07864
      0.0869
      0.07017
      ...
      23.41
      158.80
      1956.0
      0.1238
      0.1866
      0.2416
      0.1860
      0.2750
      0.08902
      NaN

      2
      84300903
      M
      19.69
      21.25
      130.00
      1203.0
      0.10960
      0.15990
      0.1974
      0.12790
      ...
      25.53
      152.50
      1709.0
      0.1444
      0.4245
      0.4504
      0.2430
      0.3613
      0.08758
      NaN

      3
      84348301
      M
      11.42
      20.38
      77.58
      386.1
      0.14250
      0.28390
      0.2414
      0.10520
      ...
      26.50
      98.87
      567.7
      0.2098
      0.8663
      0.6869
      0.2575
      0.6638
      0.17300
      NaN

      4
      84358402
      M
      20.29
      14.34
      135.10
      1297.0
      0.10030
      0.13280
      0.1980
      0.10430
      ...
      16.67
      152.20
      1575.0
      0.1374
      0.2050
      0.4000
      0.1625
      0.2364
      0.07678
      NaN

5 rows × 33 columns

Remove the first column 'id' and the last column 'Unnamed:32' from this dataset:

```python
data.drop(labels=[data.columns[0],data.columns[32]],axis=1,inplace=True)
```

```python
data.head()
```

      diagnosis
      radius_mean
      texture_mean
      perimeter_mean
      area_mean
      smoothness_mean
      compactness_mean
      concavity_mean
      concave points_mean
      symmetry_mean
      ...
      radius_worst
      texture_worst
      perimeter_worst
      area_worst
      smoothness_worst
      compactness_worst
      concavity_worst
      concave points_worst
      symmetry_worst
      fractal_dimension_worst

      0
      M
      17.99
      10.38
      122.80
      1001.0
      0.11840
      0.27760
      0.3001
      0.14710
      0.2419
      ...
      25.38
      17.33
      184.60
      2019.0
      0.1622
      0.6656
      0.7119
      0.2654
      0.4601
      0.11890

      1
      M
      20.57
      17.77
      132.90
      1326.0
      0.08474
      0.07864
      0.0869
      0.07017
      0.1812
      ...
      24.99
      23.41
      158.80
      1956.0
      0.1238
      0.1866
      0.2416
      0.1860
      0.2750
      0.08902

      2
      M
      19.69
      21.25
      130.00
      1203.0
      0.10960
      0.15990
      0.1974
      0.12790
      0.2069
      ...
      23.57
      25.53
      152.50
      1709.0
      0.1444
      0.4245
      0.4504
      0.2430
      0.3613
      0.08758

      3
      M
      11.42
      20.38
      77.58
      386.1
      0.14250
      0.28390
      0.2414
      0.10520
      0.2597
      ...
      14.91
      26.50
      98.87
      567.7
      0.2098
      0.8663
      0.6869
      0.2575
      0.6638
      0.17300

      4
      M
      20.29
      14.34
      135.10
      1297.0
      0.10030
      0.13280
      0.1980
      0.10430
      0.1809
      ...
      22.54
      16.67
      152.20
      1575.0
      0.1374
      0.2050
      0.4000
      0.1625
      0.2364
      0.07678

5 rows × 31 columns

The task is to determine the best fitting probability distribution on each column data (Sample Data of the Random Variable) of the dataset, out of the possible choices of probability distributions: Normal(Symmetric), Rayleigh(Skewed) or Binomial.

So, let's see that how are we going to do that for a single column data and then we have to repeat this process for rest of the columns:

Step 1: First, we will check whether the Random Variable is Discrete or Continous by checking that how many unique values are there in the single column of the Random Variable and does the Random Variable has string or floating point or integer values? Usually, Discrete Random Variables have either Integer or String values whereas continous random variables have floating point values and also because the values are not sparse (0s or 1s) in continous random variables and they are dense (floating point values) so values don't repeat much and hence the number of unique values are almost equal to the total number of values in the column. As an example, lets take one by one two columns: 'diagnosis' and 'radius_mean':

```python
diagnosis_unique_values = data['diagnosis'].unique()
```

```python
radius_mean_unique_values = data['radius_mean'].unique()
```

```python
diagnosis_unique_values
```

    array(['M', 'B'], dtype=object)

```python
radius_mean_unique_values
```

    array([17.99 , 20.57 , 19.69 , 11.42 , 20.29 , 12.45 , 18.25 , 13.71 ,
           13.   , 12.46 , 16.02 , 15.78 , 19.17 , 15.85 , 13.73 , 14.54 ,
           14.68 , 16.13 , 19.81 , 13.54 , 13.08 ,  9.504, 15.34 , 21.16 ,
           16.65 , 17.14 , 14.58 , 18.61 , 15.3  , 17.57 , 18.63 , 11.84 ,
           17.02 , 19.27 , 16.74 , 14.25 , 13.03 , 14.99 , 13.48 , 13.44 ,
           10.95 , 19.07 , 13.28 , 13.17 , 18.65 ,  8.196, 12.05 , 13.49 ,
           11.76 , 13.64 , 11.94 , 18.22 , 15.1  , 11.52 , 19.21 , 14.71 ,
           13.05 ,  8.618, 10.17 ,  8.598,  9.173, 12.68 , 14.78 ,  9.465,
           11.31 ,  9.029, 12.78 , 18.94 ,  8.888, 17.2  , 13.8  , 12.31 ,
           16.07 , 13.53 , 18.05 , 20.18 , 12.86 , 11.45 , 13.34 , 25.22 ,
           19.1  , 12.   , 18.46 , 14.48 , 19.02 , 12.36 , 14.64 , 14.62 ,
           15.37 , 13.27 , 13.45 , 15.06 , 20.26 , 12.18 ,  9.787, 11.6  ,
           14.42 , 13.61 ,  6.981,  9.876, 10.49 , 13.11 , 11.64 , 22.27 ,
           11.34 ,  9.777, 12.63 , 14.26 , 10.51 ,  8.726, 11.93 ,  8.95 ,
           14.87 , 17.95 , 11.41 , 18.66 , 24.25 , 14.5  , 13.37 , 13.85 ,
           19.   , 19.79 , 12.19 , 15.46 , 16.16 , 15.71 , 18.45 , 12.77 ,
           11.71 , 11.43 , 14.95 , 11.28 ,  9.738, 16.11 , 12.9  , 10.75 ,
           11.9  , 11.8  , 14.44 , 13.74 ,  8.219,  9.731, 11.15 , 13.15 ,
           12.25 , 17.68 , 16.84 , 12.06 , 10.9  , 11.75 , 19.19 , 19.59 ,
           12.34 , 23.27 , 14.97 , 10.8  , 16.78 , 17.47 , 12.32 , 13.43 ,
           11.08 , 10.66 ,  8.671,  9.904, 16.46 , 13.01 , 12.81 , 27.22 ,
           21.09 , 15.7  , 15.28 , 10.08 , 18.31 , 11.81 , 12.3  , 14.22 ,
            9.72 , 14.86 , 12.91 , 13.77 , 18.08 , 19.18 , 14.45 , 12.23 ,
           17.54 , 23.29 , 13.81 , 12.47 , 15.12 , 17.01 , 15.27 , 20.58 ,
           28.11 , 17.42 , 14.19 , 13.86 , 11.89 , 10.2  , 19.8  , 19.53 ,
           13.65 , 13.56 , 10.18 , 15.75 , 14.34 , 10.44 , 15.   , 12.62 ,
           12.83 , 17.05 , 11.32 , 11.22 , 20.51 ,  9.567, 14.03 , 23.21 ,
           20.48 , 17.46 , 12.42 , 11.3  , 13.75 , 19.4  , 10.48 , 13.2  ,
           12.89 , 10.65 , 20.94 , 11.5  , 19.73 , 17.3  , 19.45 , 13.96 ,
           19.55 , 15.32 , 15.66 , 15.53 , 20.31 , 17.35 , 17.29 , 15.61 ,
           17.19 , 20.73 , 10.6  , 13.59 , 12.87 , 10.71 , 14.29 , 11.29 ,
           21.75 ,  9.742, 17.93 , 11.33 , 18.81 , 19.16 , 11.74 , 16.24 ,
           12.58 , 11.26 , 11.37 , 14.41 , 14.96 , 12.95 , 11.85 , 12.72 ,
           10.91 , 20.09 , 11.46 ,  9.   , 13.5  , 11.7  , 14.61 , 12.76 ,
           11.54 ,  8.597, 12.49 ,  9.042, 12.43 , 10.25 , 20.16 , 20.34 ,
           12.2  , 12.67 , 14.11 , 12.03 , 16.27 , 16.26 , 16.03 , 12.98 ,
           11.25 , 17.06 , 12.99 , 18.77 , 10.05 , 23.51 ,  9.606, 11.06 ,
           19.68 , 10.26 , 14.76 , 11.47 , 11.95 , 11.66 , 25.73 , 15.08 ,
           11.14 , 12.56 , 13.87 ,  8.878,  9.436, 12.54 , 13.3  , 16.5  ,
           13.4  , 20.44 , 20.2  , 12.21 , 21.71 , 22.01 , 16.35 , 15.19 ,
           21.37 , 20.64 , 13.69 , 16.17 , 10.57 , 13.46 , 13.66 , 11.27 ,
           11.04 , 12.39 , 14.6  , 13.88 ,  8.734, 15.49 , 21.61 , 12.1  ,
           14.06 , 13.51 , 12.8  , 17.91 , 12.96 , 12.94 , 10.94 , 16.14 ,
           12.85 , 12.27 , 11.36 ,  9.397, 15.13 ,  9.405, 15.5  , 12.7  ,
           11.16 , 11.57 , 14.69 , 11.61 , 10.03 , 11.13 , 14.9  , 12.4  ,
           18.82 , 13.98 , 14.04 , 14.02 , 10.97 , 17.27 , 13.78 , 18.03 ,
           11.99 , 17.75 , 14.8  , 14.53 , 21.1  , 11.87 , 13.38 , 11.63 ,
           13.21 ,  9.755, 17.08 , 27.42 , 14.4  , 13.24 , 13.14 ,  9.668,
           17.6  , 11.62 ,  9.667, 12.04 , 14.92 , 10.88 , 14.2  , 13.9  ,
           11.49 , 16.25 , 12.16 , 13.47 , 13.7  , 15.73 , 19.44 , 11.68 ,
           16.69 , 17.85 , 18.01 , 13.16 , 12.65 , 18.49 , 20.59 , 15.04 ,
           13.82 , 23.09 ,  9.268,  9.676, 12.22 , 16.3  , 14.81 , 15.05 ,
           19.89 , 12.88 , 12.75 ,  9.295, 24.63 ,  9.847,  8.571, 13.94 ,
           12.07 , 11.67 , 13.68 , 20.47 , 10.96 , 20.55 , 14.27 , 11.69 ,
            7.729,  7.691, 14.47 , 14.74 , 13.62 , 10.32 ,  9.683, 10.82 ,
           10.86 ,  9.333, 10.29 , 10.16 ,  9.423, 14.59 , 11.51 , 14.05 ,
           11.2  , 15.22 , 20.92 , 21.56 , 20.13 , 16.6  , 20.6  ,  7.76 ])

Let's see that in each of the cases, how many unique values are there:

```python
len(diagnosis_unique_values)
```

    2

```python
len(radius_mean_unique_values)
```

    456

As you can see that in our discrete random variable of 'diagnosis', we have very limited values, only two in number and they are string in nature : ['B, 'M']

Whereas, in our continous random variable of 'radius_mean', we can see the numbe rof unique values is huge and it's 456 which is not much smaller than 569, the number of rows in a column. Moreover, the nature or datatype of values is floating point.

So, lets write this concept in the form of the code:

```python
def determine_random_variable_type(column_name):

    if (type(data[column_name]) == str or type(data[column_name]) == int) and (len(data[column_name].unique()) < len(data[column_name])):

        return 'discrete'

    else:

        return 'continous'
```

The above function will take the name of the single column of the dataset and access the data of that column from the dataset and check the datatype or nature of the values inside that column and also the total number of unique values in that column and check the conditions for the random variable or that specific column being discrete or continous and returns the the type of random variable as strings 'discrete' or 'continous'.

Now, the next task is to calculate the value of the best estimator of the population parameter of the probability distribution from which a specific column data is being sampled. In case of discrete random variables, it's pretty easy to determine that: If there are only two unique values in the column data, then it's binomial distribution and multinomial if there are more than two unique values.

But, the real challenge lies in case of continous random variables, where it is difficult to determine that from which continous probability distibution the column data is being sampled. We have two options here: Either Normal (Symmetric) or Rayleigh (Skewed). So, in this case we have to find the best fit of our data on both the distributions and then select the distribution with the BESTEST FIT.

Well, now the question arrises that how we are going to do that. We know that the Log Likelihood Function Value calculated on the data determines the quality of fit of data on the presumed distribution and if we maximize that value then we get the best fit of a specific probability distribution on the data. so, we first find the best fit of normal distibution on our continous data and then we find the best fit of Rayleigh Distribution on our continous data by maximizing the Log Likelihood Function Value for both the cases and because we have to select that distribution which gives us the bestest fit so we will end up selecting the distribution whose Log Likelihood Function value is maximum among both maxmized Log Likelihood Function Values.

The problem is that in order to find the Log Likelihood Function value evaluated in case of fit of specific distribution on the data, we need the PDF of that distribution, which we know but we dont know the values of best estimators of population parameters:
\begin{equation}
\mu, \sigma
\end{equation}
of Normal Distribution and best estimator of population parameter,
\begin{equation}
\sigma (mode)
\end{equation}

of Rayleigh Distribution.

Now, how do we get the values of the best estimators of population parameters of respective distributions. Well, we know the answer to this question and that is by Maximum Log Likelihood Estimation. We know that the data in each column of this dataset is IID (Independent and Identically Distributed) therefore, for any random column of this dataset, X where X can be 'radius_mean' or 'testure_mean' or anything else, the Log Likelihood function is evaluated as:

\begin{equation}
\log_e L(\mu,\sigma) = \log_e P(X=x_1\cap X=x_2\cap .........X=x_N)
\end{equation}
For Normal Distribution

\begin{equation}
\log_e L(\mu, \sigma) = \log_e\prod\limits_{i=0}^{N}\left(\frac{1}{\sqrt{2\pi}\sigma}e^\frac{(x_i-\mu)^2}{2\sigma^2}\right)
\end{equation}

\begin{equation}
\log_e L(\mu, \sigma) = \sum\limits_{i=0}^{N}\log_e\left(\frac{1}{\sqrt{2\pi}\sigma}e^\frac{(x_i-\mu)^2}{2\sigma^2}\right)
\end{equation}
Here, the population mean and population standard deviation estimator best estimate for normal distribution can be evaluated by taking the derivative of the above function with respect to $\mu$ and $\sigma$ and when we do that, that is:
\begin{equation}
\frac{\partial\log_e L(\mu, \sigma)}{\partial\mu} = 0
\end{equation}

\begin{equation}
\frac{\partial log_e L(\mu, \sigma)}{\partial\sigma} = 0
\end{equation}

We get,
\begin{equation}
\mu_\text{best,normal} = \frac{\sum\limits_{i=0}^{N}{X_i}}{N}
\end{equation}

And,

\begin{equation}
\sigma_\text{best,normal} = \sqrt{\frac{\sum\limits_{i=0}^{N}{(x_i-\mu_\text{best})^2}}{N}}
\end{equation}

Which is sample mean and sample standard deviation, therefore these are the best estimates of the population parameters of Normal Distribution for the best fit on our column data.

Similarly, we can find the values of the best estimators of the population parameter of Rayliegh Distribution by following the above mentioned steps, and we get the following value for the best estimate in case of Rayleigh Distribution:

\begin{equation}
\sigma_\text{best,rayleigh} = \sqrt{\frac{\sum\limits_{i=0}^{N}{x_i^2}}{2N}}
\end{equation}

So, these best estimates of the MVU Estimators of respective population parameters of respective Probability Distributions will maximize the Log Likelihood Function values for respective probability distributions and hence will result in best fit of respective distributions. Now, to determine the bestest fit among both of the distributions, we have to select that distribution for which the Log Likelihood Function is the highest among both already maximized values.

Now, in our case N = 569, so the best estimates of the MVU estimators of the population parameters of respective probability distributions are given by:

\begin{equation}
\mu_\text{best,normal} = \frac{\sum\limits_{i=0}^{569}{x_i}}{569}
\end{equation}

And,

\begin{equation}
\sigma_\text{best,normal} = \sqrt{\frac{\sum\limits_{i=0}^{569}{(x_i-\mu_\text{best})^2}}{569}}
\end{equation}

For Normal Distribution

And,

\begin{equation}
\sigma_\text{best,rayleigh} = \sqrt{\frac{\sum\limits_{i=0}^{569}{x_i^2}}{2*569}}
\end{equation}

Therefore, the maximized values of Log Likelihood Function for the case of both the probability distributions is given by:

\begin{equation}
L_\text{max,normal} = \log_e L(\mu_\text{best,normal},\sigma_\text{best,normal})
\end{equation}

\begin{equation}
L_\text{max,normal} = \sum\limits_{i=0}^{569}\log_e\left(\frac{1}{\sqrt{2\pi}\sigma_\text{best,normal}}e^\frac{(x_i-\mu_\text{best,normal})^2}{2\sigma_\text{best,normal}^2}\right)
\end{equation}

For Normal Distribution

And,

Similarly, maximized value of Log Likelihood Function for Rayleigh Distribution can be calculated as we have just calculated for Normal Distribution.
\begin{equation}
L_\text{max,rayleigh} = \log_e L(\sigma_\text{best,rayleigh})
\end{equation}

Now, the column data will be considered to be sampled from Normal Distribution if:

\begin{equation}
L_\text{max,normal} > L_\text{max,rayleigh}
\end{equation}

else:

column data will be considered to be sampled from Rayleigh Distribution.

Therefore, in order to do all that, we are going to create following set of functions:

```python
def calculate_L_max_normal(column_name):

    mu_best_normal = data[column_name].mean()

    sigma_best_normal = data[column_name].std()

    L_max_normal = #write the expression for evaluating the maximized value of Log Likelihood Function for Normal Distribution

    return L_max_normal
```

```python
def calculate_L_max_rayleigh(column_name):

    sigma_best_rayleigh = #calculate the value of best estimate of MVU Estimator of sigma of Rayleigh Distribution here

    L_max_rayleigh = #write the expression for evaluating the maximized value of Log Likelihood Function for Rayleigh Distribution

    return L_max_rayleigh
```

```python
def determine_distribution_type(column_name):

    #Write the code here to first determine type of random variable : Either Discrete or Continous
    if column_type == 'discrete':

        #if the above condition is true, then write the code to determine whether it's binomial or multinomial
        #and return the strings 'binomial' or 'multinomial' accordingly

    else:

        #write the code to find L_max_normal here

        #write the code to find the L_max_rayleigh here

        if L_max_normal > L_max_rayleigh:

            return 'normal'

        else:

            return 'rayleigh'
```


# Finite element analysis of an elastic frame

______________________________

*Date:* 2018-01-02

*Author:* Pierre Jehel (<pierre.jehel@centralesupelec.fr>)

*Course:* Gestion des Risques Structure (CentraleSupélec, option / MS "Aménagement et Construction Durables")
______________________________

This document presents the principal steps in a finite element analysis program for a simple  structure.
For any FE program, the overall procedure is the same.
The three main steps are:
- Pre-processing: the system is described by the user (inputs)
- Processing: the FE mesh is generated and the problem is numerically solved
- Post-processing: plotting and analysis of the outputs

We first import the Python packages and modules we will need:

```python
import numpy as np
import numpy.matlib
from numpy import linalg
import mesGen
import strMod
```

## 1. Description of the system

User inputs here the description of the structure she/he wants to model:
- Structure geometry
- Element section geometry
- Element material properties
- Boundary conditions (loading)
- Parameterization for the solution procedure
- Output data

### 1.1. Structure geometry

We consider a 2D frame structure.

We first create an array with, for each structural element, the following list: [node I, node J, element type (1: beam/column, 2: rotational spring), element length, global to local rotation angle] (see figure with the description of the structure):

Nodes coordinates (2D problem):

```python
Lf = 8      # bay length [m]
Hf = 4      # story height [m]
nodCoo = []
nodCoo.append([0,0])     # node 0 [X coordinate,Y coordinate]
nodCoo.append([Lf,0])    # node 1 [X,Y]
nodCoo.append([0,Hf])    # ...
nodCoo.append([Lf,Hf])
nodCoo.append([0,2*Hf])
nodCoo.append([Lf,2*Hf])
```

Elements connect a node I to a node J; we consider different:
- element types (0->beam/column; 1->rotational spring)
- element sections

```python
strEle = []
strEle.append([0,2,0,0])  # elt 0 [node I, node J, element type, section type]
strEle.append([2,4,0,0])  # elt 1
strEle.append([1,3,0,1])  # ...
strEle.append([3,5,0,1])
strEle.append([2,3,0,2])
strEle.append([4,5,0,3])
```

### 1.2. Element properties (material and section geometry)

The geometric properties of selected steel beam sections (see figure) can for instance be found here: <http://ds.arcelormittal.com/repo/Catalog/ArcelorMittal_PV_FR_RU.pdf>

*Remark:* We will consider that the beams can be described using Euler-Bernoulli kinematic assumptions. Other options are generally available in FE analysis programs such as Timoshenko kinematics.

E [N/m2]: material Young's modulus
A [m2]  : element cross-section area
Iy [m4] : element cross-section moment of inertia wrt centroid

```python
eltPro = []
eltPro.append([2.1e11,33.4e-4,2772e-8])   # section type 0  [E, A, Iy] -> IPE 220
eltPro.append([2.1e11,33.4e-4,2772e-8])   # section type 1
eltPro.append([2.1e11,33.4e-4,2772e-8])   # ...
eltPro.append([2.1e11,33.4e-4,2772e-8])
```

### 1.4. Boundary conditions

In 'fixDof' array, we indicate for every node whether the DOFs are free or not.
The line number of the matrix corresponds to the node number.
For each of the 3 DOFs $(u,v,\theta)$, there is a 1 if the DOF is free and a 0 otherwise:

```python
fixDof = []
fixDof.append([0,0,0])   # node 0
fixDof.append([0,0,0])   # node 1
fixDof.append([1,1,1])   # ...
fixDof.append([1,1,1])
fixDof.append([1,1,1])
fixDof.append([1,1,1])
```

The frame is fixed in the ground at its base. Uniformly distributed static loadings are applied on both beams; self-weight is neglected:

```python
q1 = 1.5e5   # [N] 2nd-floor beam
q2 = 1.5e5   # [N] 3rd-floor beam
```

In 'staLoa' array, we input external loading actions on nodes:

```python
Fy1 = q1*Lf/2
Mz1 = q1*Lf**2/12
Fy2 = q2*Lf/2
Mz2 = q2*Lf**2/12
bouCon = []
bouCon.append([0,0,0])         # node 0 [Dx, Dy, Rz] (displacements)
bouCon.append([0,0,0])         # node 1 [Dx, Dy, Rz] (displacements)
bouCon.append([0,-Fy1,-Mz1])   # node 2 [Fx, Fy, Mz] (forces)
bouCon.append([0,-Fy1,Mz1])    # ...
bouCon.append([0,-Fy2,-Mz2])
bouCon.append([0,-Fy2,Mz2])
```

## 2. Mesh generation

In this simple example, the only thing that has to be done for generating the mesh is computing element length and orientation ($cos(\theta)$ and $sin(\theta)$ where $\theta$ is the angle between local and global coordinate systems). We get these mesh data as:

```python
# mesh data
mesDat = mesGen.eltL0(strEle,nodCoo)

# number of free DOFs
nDof = np.sum(fixDof)
```

## 3. Resolution procedure

We recall that the element stiffness matrix with Euler-Bernoulli kinematics reads:

Let's solve the problem now:

```python
# external forces vector
fe_v = strMod.extLoa(nDof,fixDof,bouCon)

# structural stiffness matrix K
K_m = strMod.stiMat(nDof,strEle,mesDat,eltPro,fixDof)

# compute displacements at free DOFs
dis_v = linalg.solve(K_m,fe_v)
```

We now have the displacements in the global coordinates system (the primary unknowns of the problem) at the free DOFs.

Because there are 3 DOFs at each node (longitudinal and transversal translations and rotation), the $\texttt{dis_v}$ vector is of size $6*3-6=12$ (6 nodes * 3 DOFs/node - 6 frozen DOFs). The frozen DOFs correspond to the nodes fixed in the ground.

The first 3 elements of vector $\texttt{dis_v}$ are, respectively, the longitudinal and transversal translations and the rotation at node 2; then the elements 4 to 6 of the vector $\texttt{dis_v}$ correspond to the displacements at node 3; and so on.

## 4. Outputs analysis

### 4.1. Internal forces

We are now interested in computing the internal forces in the elements connected to node 2 (elements 0, 1, and 4). This can be computed from the primary unknowns of the problem (the displacements of the free DOFs in the global coordinates system) as follows:
- Retrieve in vector $\texttt{dis_v}$ the displacements at both ends of each element:

```python
# element 0 (column)
dis0_v = np.matlib.zeros((6,1))
dis0_v[0] = 0
dis0_v[1] = 0
dis0_v[2] = 0
dis0_v[3] = dis_v[0]
dis0_v[4] = dis_v[1]
dis0_v[5] = dis_v[2]

# element 1 (column)
dis1_v = np.matlib.zeros((6,1))
dis1_v[0] = dis_v[0]
dis1_v[1] = dis_v[1]
dis1_v[2] = dis_v[2]
dis1_v[3] = dis_v[6]
dis1_v[4] = dis_v[7]
dis1_v[5] = dis_v[8]

# element 4 (beam)
dis4_v = np.matlib.zeros((6,1))
dis4_v[0] = dis_v[0]
dis4_v[1] = dis_v[1]
dis4_v[2] = dis_v[2]
dis4_v[3] = dis_v[3]
dis4_v[4] = dis_v[4]
dis4_v[5] = dis_v[5]
```

- Project the displacements into the local coordinates system:

```python
# build transformation matrix
# for the columns:
#    Global    -->   Local
#   Y                      x
#   |                      |
#   |          -->         |
#   O----> X        y <----O
cos = 0
sin = 1
R_m = strMod.glo2loc_2D(cos,sin)

# displacements in local coordinates system
dis0loc_v = R_m*dis0_v   # column
dis1loc_v = R_m*dis1_v   # column
dis4loc_v = dis4_v       # beam
```

- Compute the internal forces (normal force, shear force, bendiing moment):

```python
# retrieve stiffness matrices in the local coordinates system
KeCol_m = strMod.beaSti_EB_2D_loc(2.1e11,33.4e-4,Hf,2772e-8)
KeBea_m = strMod.beaSti_EB_2D_loc(2.1e11,33.4e-4,Lf,2772e-8)

# compute internal nodal forces (F=K*d)
forInt0_v = KeCol_m*dis0loc_v   # element 0
forInt1_v = KeCol_m*dis1loc_v   # element 1
forInt4_v = KeBea_m*dis4loc_v   # element 4
```

```python
# show internal nodal forces
print('F_elmt0 =')
print(forInt0_v)
print('F_elmt1 =')
print(forInt1_v)
print('F_elmt4 =')
print(forInt4_v)
```

    F_elmt0 =
    [[ 1200000.        ]
     [  -89278.25303849]
     [ -119877.16952566]
     [-1200000.        ]
     [   89278.25303849]
     [ -237235.84262829]]
    F_elmt1 =
    [[ 600000.        ]
     [-291581.71751491]
     [-504084.8208204 ]
     [-600000.        ]
     [ 291581.71751491]
     [-662242.04923926]]
    F_elmt4 =
    [[ -2.02303464e+05]
     [  7.27595761e-12]
     [ -5.86793366e+04]
     [  2.02303464e+05]
     [ -7.27595761e-12]
     [  5.86793366e+04]]

Some comments on these values:
- In each element internal forces vector, the 3 first components are respectively the normal force $N$, the shear force $V$ and the bending moment $M$ at the left end $I$; the 3 last components are the same quantities but at the right end $J$ of the element
- In element 0 (column): $N^0_I=120.0~kN$, $V^0_I = -89.3~kN$, $M^0_I = -120.0~kN.m$, $N^0_J=-120.0~kN$, $V^0_J = 89.3~kN$, $M^0_J = -237.2~kN.m$
- Because element 0 is fixed at its left end $I$, $(-N,-V,-M)^0_I$ correspond to the reactions at node 0. This information is useful for designing the foundations of the structure and the connection at node 0.
- At node 2, the right end of element 0 is connected to the left end of both elements 1 (column) and 4 (beam). We can check that the equilibrium of node 2 is satisfied, that is that the internal forces equilibrite the external action due to the uniformly distributed loading along element 4 (beam):
    - Force along the Y-axis generated by the loading: $F^{ext}_{2,Y} = -q1 \times Lf/2 = -60~kN$
    - Moment around the Z-axis generated by the loading: $M^{ext}_{2,Y} = -q1 \times Lf^2/12 = -800~kN.m$
    - Equilibrium of the forces along the X-axis: $-V^0_J-V^1_I+N^4_I = -89.3+291.6-202.3 = 0$ -> OK
    - Equilibrium of the forces along the Y-axis: $N^0_J+N^1_I+V^4_I = -120.0+60.0+0.0 = -60.0~kN = F^{ext}_{2,Y}$ -> OK
    - Equilibrium of the moments around the Z-axis: $M^0_J+M^1_I+M^4_I = -237.2-504.1-58.7 = -800~kN.m = M^{ext}_{2,Y}$ -> OK

### 4.2. Normal stresses

We can now focus on the normal stresses $\sigma_{xx}(x,y)$ in the structural elements. In an element $e$ of cross-section area $A$, length $L$, and moment of inertia $I_z$ they can be computed from the formula:

\begin{equation}
 \sigma^e_{xx}(x,y) = \frac{N^e(x)}{A} + \frac{M^e(x) \, y}{I_z}
\end{equation}

where

\begin{eqnarray}
 & N^e(x) = \nu(x) + N^e_I\left(1-\frac{x}{L}\right) - N^e_J \frac{x}{L} \\
 & M^e(x) = \mu(x) - M^e_I\left(1-\frac{x}{L}\right) + M^e_J \frac{x}{L}
\end{eqnarray}

with $\nu(x)$ and $\mu(x)$ the normal force and bending moment in the isostatic version of the element $e$. For instance, element 4 is uniformly loaded with distributed vertical load $-q_1$, in this case:
- $\nu(x) = 0$
- $\mu(x) = q_1 x (Lf - x) / 2$

```python

```


# 随机矩阵引论：理论与实践

[**Introduction to Random Matrices: Theory and Practice**](https://arxiv.org/pdf/1712.07903.pdf) by Giacomo Livan, Marcel Novaes, Pierpaolo Vivo (2017)

该书给出了 **Matlab** 代码实现，下面是我的 **Python 2.7** 代码实现

## Chapter 0 辅助函数

+ 双阶乘函数

```python
def double_factorial(n):
    x = 1
    if n % 2 == 0:
        for k in range(int(n / 2)):
            x = x * (2 * (k + 1))
    else:
        for k in range(int((n + 1) / 2)):
            x = x * (2 * (k + 1) - 1)
    return x
```

```python
print double_factorial(5), double_factorial(6)
```

    15 48

```python
%notebook inline
```

## Chapter 1 Getting Started

+ This program plots a numerical histogram of the **eigenvalue density of Gaussian random matrix ensembles (Orthogonal, Unitary and Symplectic)**. You will be asked to select the ensemble by providing the value of the Dyson beta index (1 for GOE, 2 for GUE, 4 for GSE), to provide the matrix size N, and to choose the number of matrices to be diagonalized.

```python
import numpy as np
from numpy import histogram, mat, diff
from numpy.linalg import eigvalsh
import matplotlib.pyplot as plt
```

```python
# Choose value of beta (1 for GOE, 2 for GUE, 4 for GSE):
beta = input()
while beta != 1 and beta != 2 and beta != 4:
    print 'Error: beta has to be equal to 1, 2, or 4'
    beta = input()

# Choose matrix size:
N = 8
# Choose number of matrices to be diagonalized:
Nmatr = 50000

x = []
if beta == 1:
    # Gaussian Orthogonal Ensemble
    for nm in range(Nmatr):
        M = np.random.randn(N, N)
        M = (M + M.T) / 2
        x = np.append(x, eigvalsh(M))
elif beta == 2:
    # Gaussian Unitary Ensemble
    for nm in range(Nmatr):
        M = np.random.randn(N, N) + 1j * np.random.randn(N, N)
        M = mat(M)
        M = (M + M.H) / 2
        x = np.append(x, eigvalsh(M))
else:
    # Gaussian Symplectic Ensemble
    for nm in range(Nmatr):
        A = np.random.randn(N, N) + 1j * np.random.randn(N, N)
        B = np.random.randn(N, N) + 1j * np.random.randn(N, N)
        M1 = np.hstack((A, B))
        M2 = np.hstack((-np.conjugate(B), np.conjugate(A)))
        M = np.vstack((M1, M2))
        M = mat(M)
        M = (M + M.H) / 2
        x = np.append(x, np.unique(eigvalsh(M))) # The unique function gets rid of the double eigenvalues

plt.figure(figsize=(5, 5))
# Normalized eigenvalue histogram
n, bins = histogram(x, bins=50, normed=1)
bins_new = []
for i in range(len(bins) - 1):
    bins_new.append((bins[i] + bins[i + 1]) / 2)
plt.plot(bins_new, n, 'o-')
plt.title('Eigenvalue Density', fontsize=18)
plt.xlabel(r'$x$', fontsize=18)
plt.ylabel(r'$\rho(x)$', fontsize=18)
plt.show()
```

## Chapter 2 Value the eigenvalue

+ This program plots a numerical histogram of the **eigenvalue spacing distribution for 2x2 GOE random matrices**, and compares it with the **Wigner surmise**. You will be asked to choose the number of matrices to be diagonalized.

> Why is it defined a ’surmise’? After all, it is the result of an exact calculation! The story goes as follows: at a conference on Neutron Physics by Time-of-Flight, held at the Oak Ridge National Laboratory in 1956, people asked a question about the possible shape of the distribution of the spacings of energy levels in a heavy nucleus. E. P. Wigner, who was in the audience, walked up to the blackboard and guessed (= surmised) the answer given above.

```python
# Choose number of matrices to be diagonalized:
Nmatr = input()

x = []

for nm in range(Nmatr):
    #M = np.random.randn(2, 2) / np.sqrt(2)
    m1 = np.random.randn()
    m2 = np.random.randn()
    m3 = np.random.randn() / np.sqrt(2)
    M = np.array([[m1, m3], [m3, m2]])
    x = np.append(x, abs(diff(eigvalsh(M))))

plt.figure(figsize=(5, 5))
# Normalized spacing histogram
n, bins = histogram(x, bins=50, normed=1)
bins_new = []
for i in range(len(bins) - 1):
    bins_new.append((bins[i] + bins[i + 1]) / 2)
plt.plot(bins_new, n, 'or')

x_new = np.linspace(0, np.max(x), 100)
# Definition of the Wigner surmise function
p = lambda s: s * np.exp(- s ** 2 / 4) / 2
plt.plot(x_new, p(x_new))
plt.title('Wigner surmise', fontsize=18)
plt.xlabel(r'$s$', fontsize=18)
plt.ylabel(r'$p(s)$', fontsize=18)
plt.show()
```

## Chapter 3 Classified Material & Chapter 4 The fluid semicircle

+ This program generates the **equilibrium density of the Coulomb gas of particles (eigenvalues)** discussed in Chapters 3 and 4, and compares it to the **semicircle distribution**. Starting from an initial random particle configuration, a simple Monte Carlo scheme is applied: at each time step the program tries to change the position of a randomly selected particle by a Gaussian increment epsilon. The move is accepted with probability min(1, exp(-beta*DeltaH)), where DeltaH is the energy difference caused by the move. You will be asked to specify the number N of particles in the gas (which is equivalent to the matrix size in the corresponding ensemble), and the value of the Dyson Index beta, which in this context plays the role of the system's inverse temperature. You will also be asked to select the number of Monte Carlo sweeps, where one sweep corresponds to N attempted moves.  A good rule of thumb is to accept roughly 35-50% of the proposed Monte Carlo moves, and the standard deviation of the increments must be tuned accordingly. The program's default value is set at 10/sqrt(N).

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

from __future__ import division
```

```python
N = 500 # Number of particles (matrix size)
Nsweeps = 1000 # Number of sweeps. A sweep consists of N attempted Monte Carlo moves

beta = input() # Inverse temperature / Dyson index
while beta != 1 and beta != 2 and beta != 4:
    print 'Error: beta has to be equal to 1, 2, or 4'
    beta = input()

sigma = 10 / np.sqrt(N) # Standard deviation of the Gaussian distribution for Monte Carlo moves

# Initial uniform distribution of particles between -sqrt(2*N) and sqrt(2*N)
x = -np.sqrt(2 * N) + 2 * np.sqrt(2 * N) * np.random.rand(N)

for ns in range(Nsweeps):
    count = 0
    for n in range(N):
        k = np.random.randint(N) # Random selection of a particle
        epsilon = sigma * np.random.randn() # Size of the proposed position shift of particle k

        # The particles' positions (except for particle k) are stored in
        # an auxiliary vector aux, which is used to compute the change in
        # energy (Hamiltonian) upon moving particle k from its current
        # position x(k) to x(k)+epsilon

        aux = np.hstack((x[0:k], x[k+1:]))

        # Change in energy
        DeltaH = epsilon ** 2 + epsilon * x[k] + np.sum(np.log(abs(x[k] - aux) / abs(x[k] + epsilon - aux)))
        # The position change of particle k is accepted with probability min(1,exp(-beta*DeltaH))
        if min(1, np.exp(-beta * DeltaH)) > np.random.rand():
            x[k] = x[k] + epsilon
            count = count + 1

    # Printing to screen the number of completed sweeps, and the fraction of accepted moves in the latest sweep
    if ns % 100 == 0:
        print 'Sweep number: % d\nAccepted moves: %4.3f' % (ns, count / N)

# Plot of normalized particle density and Wigner's semicircle
plt.figure(figsize=(5, 5))

#n, bins, patches = plt.hist(x, bins=30, normed=1, facecolor='None', edgecolor='black',alpha=1, histtype='bar')
n, bins = histogram(x, bins=30, normed=1)
bins_new = []
for i in range(len(bins) - 1):
    bins_new.append((bins[i] + bins[i + 1]) / 2)
plt.scatter(bins_new, n)

# Definition of the semicircle distribution function
rho = lambda x: np.sqrt(2 * N - x ** 2) / (np.pi * N)
# Plot of the semicircle distribution (between -sqrt(2*N) and sqrt(2*N))
x_sc = np.linspace(-np.sqrt(2 * N), np.sqrt(2 * N), N)
plt.plot(x_sc, rho(x_sc), 'r')
plt.title('Particle Density', fontsize=18)
plt.xlabel('x', fontsize=18)
plt.ylabel(r'$\rho(x)$', fontsize=18)
plt.xticks([-np.sqrt(2 * N), 0, np.sqrt(2*N)], [r'$-\sqrt{2N}$','0',r'$\sqrt{2N}$'])
plt.show()
```

## Chapter 5 Saddle-point-of-view

+ This program computes the results of Eqs. (5.21) and (5.22) and prints their values to screen

+ The fact that the soft edges are symmetrically located around the origin is a consequence of the symmetry of the confining potential under the exchange $x \rightarrow -x$.

+ **Question:** If I drop the symmetry requirements on the entries of the ensemble ($H_{ij}\ne H_{ji}$), what is the resulting analogue of the semicircle law for complex eigenvalues?

> This is called the **Girko-Ginibre (or circular) law**. In essence, for any sequence of random N × N matrices whose entries are i.i.d. random variables, all with mean zero and variance equal to 1/N, the limiting spectral density is the **uniform distribution over the unit disc in the complex plane**.

```python
import numpy as np
from numpy import histogram, mat, diff
from numpy.linalg import eigvalsh
from scipy.integrate import quad
import matplotlib.pyplot as plt

from __future__ import division
```

```python
# Endpoints of semicircle density
a = -np.sqrt(2)
b = -a

# Definition of the semicircle distribution function
sc = lambda x: np.sqrt(2 - x ** 2) / np.pi

# Integrand functions of the two integrals in Eq. (5.21)
integrand1 = lambda x: sc(x) * x ** 2
integrand2 = lambda x: sc(x) * np.log(x - a)

# Computing Eq. (5.21)
f_int = quad(integrand1, a, b)[0] / 4 + a ** 2 / 4 - quad(integrand2, a, b)[0] / 2

# Computing Eq. (5.22)
f = (-9 * a ** 4 + 4 * a ** 3 * b + 2 * a ** 2 * (5 * b ** 2 + 48) + 4 * a * b * (b ** 2 + 16)
     - 256 * np.log(b - a) -9 * b ** 4 + 96 * b ** 2 + 512 * np.log(2)) / 512

print 'Result of Eq. 5.21: %5.4f' % f_int
print 'Result of Eq. 5.22: %5.4f' % f
```

    Result of Eq. 5.21: 0.5483
    Result of Eq. 5.22: 0.5483

+ This short code performs a numerical check of the **Tricomi equation** (5.14), where n*(x) is the semicircle density. The density is computed over a number Npts of points in the interval (-sqrt(2),sqrt(2)). The value in each point s is computed as a principal value integral, i.e. by splitting the integration domain (a,b) into two parts (a,s-eps) and (s+eps,b). You will be asked to enter the value of a parameter eps, which determines the accuracy with which the integral is computed, and the number of points where the integral is computed.

```python
# Reads the number of points where the Tricomi equation integral is computed
Npts = input()

# Reads the value which determines the accuracy of the principal value
# integral (values in the range 1e-1 - 1e-10 are recommended)
eps = 1e-5

# Endpoints of semicircle density
a = -np.sqrt(2)
b = -a

# Setting the interval over which the Tricomi equation integral is computed
s = np.linspace(a, b, Npts)
v = np.zeros(Npts)

# Integrand function of the Tricomi equation integral
sc = lambda x: np.sqrt(2 - x ** 2) / np.pi

for i in range(Npts):
    # Here the above integrand function is specialized to the particular
    # point s(i) where the integral is going to be computed
    integrand = lambda x: sc(x) / (s[i] - x)

    # Principal value integral
    v[i] = quad(integrand, a, s[i] - eps)[0] + quad(integrand, s[i] + eps, b)[0]

# Plotting the results of the principal value integrals and comparing them with the bisector line
plt.figure(figsize=(5, 5))
plt.plot(s, s, 'b')
plt.plot(s, v, 'or')
plt.title('Numerical check of Tricomi equation', fontsize=18)
plt.xlabel(r'$x$', fontsize=18)
plt.ylabel(r'$\mathrm{PV \ integral}$', fontsize=18)
plt.show()
```

+ This program plots a numerical histogram of the **rescaled densities for all three Gaussian Ensembles**, and compares them with the semicircle distribution. You will be asked to choose both the size and the number of matrices to be diagonalized (for each of the three ensembles).

```python
# Choose matrix size:
# N = 10
N = 100
# N = 1000

# Choose number of matrices to be diagonalized:
Nmatr = 100

# Definition of the semicircle distribution function
rho = lambda x: np.sqrt(2 - x ** 2) / np.pi

x1 = []; x2 = []; x4 = []

for nm in range(Nmatr):
    # GOE
    M = np.random.randn(N, N) / np.sqrt(N)
    M = (M + M.T) / 2
    x1 = np.append(x1, eigvalsh(M))

    # GUE
    M = (np.random.randn(N, N) + 1j * np.random.randn(N, N)) / np.sqrt(2 * N)
    M = mat(M)
    M = (M + M.H) / 2
    x2 = np.append(x2, eigvalsh(M))

    # GSE
    A = (np.random.randn(N, N) + 1j * np.random.randn(N, N)) / np.sqrt(4 * N)
    B = (np.random.randn(N, N) + 1j * np.random.randn(N, N)) / np.sqrt(4 * N)
    M1 = np.hstack((A, B))
    M2 = np.hstack((-np.conjugate(B), np.conjugate(A)))
    M = np.vstack((M1, M2))
    M = mat(M)
    M = (M + M.H) / 2
    x4 = np.append(x4, np.unique(eigvalsh(M)))

plt.figure(figsize=(10, 10))

# Plot of the semicircle distribution
x = np.linspace(-np.sqrt(2), np.sqrt(2), 100)
semicircle, = plt.plot(x, rho(x), 'b')

# Normalized eigenvalue histogram
n1, bins1 = histogram(x1, bins=50, normed=1)
n2, bins2 = histogram(x2, bins=50, normed=1)
n4, bins4 = histogram(x4, bins=50, normed=1)
bins_new1 = []
for i in range(len(bins1) - 1):
    bins_new1.append((bins1[i] + bins1[i + 1]) / 2)

bins_new2 = []
for i in range(len(bins2) - 1):
    bins_new2.append((bins2[i] + bins2[i + 1]) / 2)

bins_new4 = []
for i in range(len(bins4) - 1):
    bins_new4.append((bins4[i] + bins4[i + 1]) / 2)

GOE, = plt.plot(bins_new1, n1, 'or-')
GUE, = plt.plot(bins_new2, n2, 'ok-')
GSE, = plt.plot(bins_new4, n4, 'om-')

plt.title('Rescaled Eigenvalue Densities', fontsize=18)
plt.xlabel(r'$x$', fontsize=18)
plt.ylabel(r'$\rho(x)$', fontsize=18)
plt.xlim((-2, 2))
#plt.xticks([-np.sqrt(2), 0, np.sqrt(2)], [r'$-\sqrt{2}$','0',r'$\sqrt{2}$'])
plt.legend(handles=[semicircle, GOE, GUE, GSE], labels=['SemiCircle', 'GOE', 'GUE', 'GSE'], loc='upper right')
plt.show()
```

## Chapter 8 Resolve(nt) the semicircle

+ This program provides a quick numerical verification that the two **resolvent** branches in eq. (8.20) match eq. (8.5) for different signs. Namely, you will be asked to choose the real and imaginary parts of a complex number z (with real part such that -sqrt(2) < Real(z) < sqrt(2)). For small values of the imaginary parts you will be able to verify that G_plus matches the resolvent for -sqrt(2) < Real(z) < 0, whereas G_minus matches it for 0 < Real(z) < sqrt(2).

```python
import numpy as np
from scipy.integrate import quad
```

```python
re = input()

while abs(re) >= np.sqrt(2):
    print 'ERROR: Real(z) must be smaller than sqrt(2) in absolute value'
    re = input()

im = input()
z = re - 1j * im

# The value of the resolvent is computed in z
G = quad(lambda x: np.sqrt(2 - x ** 2) / (np.pi * (z - x)), -np.sqrt(2), np.sqrt(2))[0]

G_plus = lambda x: x + np.sqrt(x ** 2 - 2)
G_minus = lambda x: x - np.sqrt(x ** 2 - 2)

print 'Value of resolvent in z: %6.4f%+6.4fi' % (G.real, G.imag)
print 'Value of G plus in z: %6.4f%+6.4fi' % (G_plus(z).real, G_plus(z).imag)
print 'Value of G minus in z: %6.4f%+6.4fi' % (G_minus(z).real, G_minus(z).imag)
```

    0.5
    2
    Value of resolvent in z: 0.0890+0.0000i
    Value of G plus in z: 0.9110-4.4329i
    Value of G minus in z: 0.0890+0.4329i

    /Users/brucelee/miniconda2/lib/python2.7/site-packages/scipy/integrate/quadpack.py:388: ComplexWarning: Casting complex values to real discards the imaginary part
      return _quadpack._qagse(func,a,b,args,full_output,epsabs,epsrel,limit)

## Chapter 10 Finite N

+ What does in the bulk mean? The point is that Hermite polynomials (and other classical orthogonal polynomials) have two different asymptotics, according to the way their argument and parameter scale with N. This in turns corresponds to different regimes, namely different locations x where the spectrum is looked at, and different zooming resolutions.

## Chapter 11 Meet Andréief

+ This program performs an indirect numerical check of the **Andréief identity** by verifying that the equations reported in Chapter 11 (see, e.g., eq. (11.9)) correctly provide the **probability of a GUE matrix having a certain number of positive eigenvalues**. You will be asked to provide the matrix size N and the number n of positive eigenvalues. First, you will be shown the analytical prediction computed by the program for the aforementioned probability. Based on that, you will be able to choose the number of matrices to diagonalize numerically in order to obtain a numerical estimate for the same probability.

### 数值实验

```python
import numpy as np
from numpy import *
from numpy.linalg import eigvalsh
import matplotlib.pyplot as plt

from __future__ import division
```

```python
N = 5 # size of a GUE matrix
n = 2 # number of eigenvalues of a GUE matrix is positive
Nmatr = 1000 # samples of GUE matrices

count = 0
for nm in range(Nmatr):
    M = np.random.randn(N, N) + 1j * np.random.randn(N, N)
    M = mat(M)
    M = (M + M.H) / 2
    eigvals_pos = filter(lambda e: e > 0, eigvalsh(M))
    if len(eigvals_pos) == n:
        count = count + 1

p = count / Nmatr
print 'Numerically estimated probability of GUE matrix of size %d having %d positive eigenvalues: %.2f%%' % (N, n, p * 100.0)
```

    Numerically estimated probability of GUE matrix of size 5 having 2 positive eigenvalues: 45.80%

### 符号计算

+ 这里，使用sympy库进行高维矩阵的符号计算非常慢，利用**符号计算(symbolic computation)**方法应该可以大幅改进算法效率

```python
from sympy import Matrix, Symbol, zeros, diff, det, factorial, simplify, exp, sin
```

```python
N = 5 # size of a GUE matrix
n = 2 # number of eigenvalues of a GUE matrix is positive

z = Symbol('z', integer=True)
A = Matrix(N, N, lambda j, k: ((-1) ** (j + k) + z) * 2 ** (((j + k) - 3) / 2) * factorial(((j + k) - 1) / 2))
B = Matrix(N, N, lambda j, k: ((-1) ** (j + k) + 1) * 2 ** (((j + k) - 3) / 2) * factorial(((j + k) - 1) / 2))

phi = A.det() / B.det()
phi_der = diff(phi, z, n) / factorial(n)

p = phi_der.subs(z, 0)
print 'Theoretical probability of GUE matrix of size %d having %d positive eigenvalues: %.2f%%' % (N, n, p.evalf() * 100.0)
```

    Theoretical probability of GUE matrix of size 5 having 2 positive eigenvalues: 49.04%

+ This code performs a symbolic check of the **Toda recurrence** of equations (11.11) and (11.12). You will be asked to type in a function a0(x) that will be used to fill the entries of the tau matrices through derivation, i.e. a1(x) = a0'(x), a2(x) = a1'(x) = a0''(x), and so on.

```python
x = Symbol('x', integer=True)

# Reads the function a0 to be used in the example. In order for the code
# to produce a result quickly we recommend using a smooth and simple
# function, e.g. a0(x) = cos(x), a0(x) = exp(-x)
a0 = exp(x)

# Choose value of n:
n = input()

# Allocating space for matrices that will be used to derive the functions
# tau_n and tau_{n+1}
tau_n = Matrix(n, n, lambda j, k: diff(a0, x, j + k))
tau_n_plus_1 = Matrix(n + 1, n + 1, lambda j, k: diff(a0, x, j + k))

# Computing values of tau functions as determinants of tau matrices
tau_n_minus_1 = det(tau_n[0:-1, 0:-1])
tau_n = simplify(det(tau_n))
tau_n_plus_1 = simplify(det(tau_n_plus_1))

# Computing difference between the two sides of equation (11.12)
res = simplify(diff(tau_n, x, 2) * tau_n - (diff(tau_n, x, 1)) ** 2 - tau_n_minus_1 * tau_n_plus_1)
print 'The difference between the two sides of equation (11.12) is: %3.2f' % res.evalf()
```

    3
    The difference between the two sides of equation (11.12) is: 0.00

## Chapter 12 Finite N is not finished

+ This program plots a histogram of the **eigenvalue density obtained from the numerical diagonalization of random matrices from the Gaussian ensembles**, and compares it with the theoretical densities (eq. (12.22)  for the GOE, (10.21) for the GUE, (12.38) for the GSE). You will be asked to select the ensemble by providing the value of the Dyson beta index (1 for GOE, 2 for GUE, 4 for GSE), to provide the matrix size N, and to choose the number of matrices to be diagonalized.

```python
import numpy as np
from numpy import histogram, mat, diff, trapz, sign
from numpy.linalg import eigvalsh
from scipy.integrate import quad
from scipy.special import hermite, factorial
import matplotlib.pyplot as plt

from __future__ import division
```

```python
# Choose value of beta (1 for GOE, 2 for GUE, 4 for GSE):
beta = input()

# Choose matrix size:
N = input()
# N = 8

if beta == 1:
    while N % 2 != 0:
        print 'Error: N has to be even for beta = 1'
        N = input()

# Choose number of matrices to be diagonalized:
Nmatr = 50000

E = []

# The following IF condition selects the desired ensemble: a number Nmatr
# of matrices are diagonalized, and the eigenvalues are collected in the
# vector E

if beta == 1:
    # Gaussian Orthogonal Ensemble
    for nm in range(Nmatr):
        M = np.random.randn(N, N)
        M = (M + M.T) / 2
        E = np.append(E, eigvalsh(M))

    x = np.linspace(np.min(E), np.max(E), 1000)

    # Computing the GOE density for finite even N (eq. 11.21)
    rho = np.zeros(len(x))
    a_N = 1
    for k in range(int(N / 2)):
        R_2k = np.array(map(hermite(2 * k), x)) * np.sqrt(2) / (np.pi ** (1 / 4) * 2 ** k * double_factorial(2 * k))

        if k == 0:
            R_2k1 = (-np.array(map(hermite(2 * k + 1), x))) * np.sqrt(2) / (np.pi ** (1 / 4) * 2 ** (k + 2) * double_factorial(2 * k - 1))
        else:
            R_2k1 = (4 * k * np.array(map(hermite(2 * k - 1), x)) - np.array(map(hermite(2 * k + 1), x))) * np.sqrt(2) / (np.pi ** (1 / 4) * 2 ** (k + 2) * double_factorial(2 * k - 1))

        Phi_2k = np.zeros(len(x)); Phi_2k1 = np.zeros(len(x))

        for i in range(len(x)):
            Phi_2k[i] = trapz(np.multiply(np.multiply(R_2k, np.exp(-np.power(x, 2) / 2)), sign(x[i] - x)), x)
            Phi_2k1[i] = trapz(np.multiply(np.multiply(R_2k1, np.exp(-np.power(x, 2) / 2)), sign(x[i]-x)), x)

        rho = np.add(rho, np.multiply(np.exp(-np.power(x, 2) / 2), (np.multiply(R_2k, Phi_2k1) - np.multiply(R_2k1, Phi_2k))))
        a_N = a_N * factorial(2 * k)

    a_N = (-1) ** (N / 2) * 2 ** (N * (N - 2) / 4) / (np.pi ** (N / 4) * a_N)
    Z = factorial(N) * a_N * 2 ** (N/2)
    rho = factorial(N - 1) * 2 ** (N / 2 - 1) * a_N * rho
    rho = rho / Z

elif beta == 2:
    # Gaussian Unitary Ensemble
    for nm in range(Nmatr):
        M = np.random.randn(N, N) + 1j * np.random.randn(N, N)
        M = mat(M)
        M = (M + M.H) / 2
        E = np.append(E, eigvalsh(M))

    x = np.linspace(np.min(E), np.max(E), 1000)

    # Computing the GUE density for finite N (eq. 10.20)
    rho = np.zeros(len(x))

    for j in range(N):
        rho = np.add(rho, np.multiply(np.power(map(hermite(j), x / np.sqrt(2)), 2), np.exp(-np.power(x, 2) / 2)) / (2 ** j * factorial(j)))

    rho = rho / (N * np.sqrt(2 * np.pi))

else:
    # Gaussian Symplectic Ensemble
    for nm in range(Nmatr):
        A = np.random.randn(N, N) + 1j * np.random.randn(N, N)
        B = np.random.randn(N, N) + 1j * np.random.randn(N, N)
        M1 = np.hstack((A, B))
        M2 = np.hstack((-np.conjugate(B), np.conjugate(A)))
        M = np.vstack((M1, M2))
        M = mat(M)
        M = (M + M.H) / 2
        E = np.append(E, np.unique(eigvalsh(M))) # The unique function gets rid of the double eigenvalues

    x = np.linspace(np.min(E), np.max(E), 1000)

    # Computing the GSE density for finite N (eq. 11.36)
    rho = np.zeros(len(x))

    for k in range(int(N)):
        if k == 0:
            Q_2k = np.ones(len(x)) * np.sqrt(2) / (np.pi ** (1/4))
            Q_2k_tilde = np.ones(len(x))
            Q_2k_der = np.zeros(len(x))
            Q_2k_tilde_der = np.zeros(len(x))
        else:
            Q_2k_tilde = np.add(4 * k * Q_2k_tilde_old, np.array(map(hermite(2 * k), x / np.sqrt(2))))
            Q_2k = Q_2k_tilde * np.sqrt(2) / (np.pi ** (1/4) * 2 ** k * double_factorial(2 * k))
            Q_2k_tilde_der = 4 * k * np.add(Q_2k_tilde_der_old, np.array(map(hermite(2 * k - 1), x / np.sqrt(2))) / np.sqrt(2))
            Q_2k_der = Q_2k_tilde_der * np.sqrt(2) / (np.pi ** (1/4) * 2 ** k * double_factorial(2 * k))

        Q_2k1 = np.array(map(hermite(2 * k + 1), x / np.sqrt(2))) * np.sqrt(2) / (np.pi ** (1/4) * 2 ** (k + 1) * double_factorial(2 * k + 1))
        Q_2k1_der = (2 * k + 1) * np.array(map(hermite(2 * k), x / np.sqrt(2))) /(np.pi ** (1/4) * 2 ** k * double_factorial(2 * k + 1))

        rho = np.add(rho, np.multiply(np.exp(-x ** 2 / 2), np.multiply(Q_2k, Q_2k1_der) - np.multiply(Q_2k1, Q_2k_der)) / (2 * N))
        Q_2k_tilde_old = Q_2k_tilde
        Q_2k_tilde_der_old = Q_2k_tilde_der

plt.figure(figsize=(5, 5))
# Normalized eigenvalue histogram
n, bins = histogram(E, bins=50, normed=1)
bins_new = []
for i in range(len(bins) - 1):
    bins_new.append((bins[i] + bins[i + 1]) / 2)
plt.plot(bins_new, n, 'or')
plt.plot(x, rho, 'b')
plt.title('Eigenvalue Density', fontsize=18)
plt.xlabel(r'$x$', fontsize=18)
plt.ylabel(r'$\rho(x)$', fontsize=18)
plt.show()
```

## Chapter 13 Classical Ensembles: Wishart-Laguerre & Chapter 14 Meet Marčenko and Pastur

+ This program plots a histogram of the **eigenvalue densities obtained via numerical diagonalization of Wishart-Laguerre random matrices** (for beta = 1,2,4) and compares them with the **Marčenko-Pastur density** for the corresponding matrix size. You will be asked to provide the sizes of the rectangular NxM matrices H used to build Wishart-Laguerre matrices of the type W = X'*X, and the number of matrices to be diagonalized.

+ Historically, one of the **earliest appearances** of a random matrix ensemble1 occurred in **1928**, when the Scottish mathematician **John Wishart** published a paper on multivariate data analysis in the journal Biometrika.

+ In Mathematics, however, the **1897** work by **Hurwitz** on the volume form of a general unitary matrix is of historical significance.

+ The Wishart ensemble is also referred to as **“Laguerre”**, since its spectral properties involve Laguerre polynomials, and also **“chiral”** in the context of applications to **Quantum Chromodynamics (QCD)**.

```python
import numpy as np
from numpy import mat
from numpy.linalg import eigvalsh
import matplotlib.pyplot as plt

from __future__ import division
```

```python
# Choose first matrix size:
N = input()
#N = 100

# Choose second matrix size:
M = input()
# M = 200
# M = 800

while N >= M:
    print 'ERROR: N must be less than M\n'
    N = input()
    M = input()

# Defining the Marcenko-Pastur density function
c = N / M
xmin = (1 - 1 / np.sqrt(c)) ** 2
xmax = (1 + 1 / np.sqrt(c)) ** 2
rho = lambda x: np.sqrt((x - xmin) * (xmax - x)) / (2 * np.pi * x)

# Choose number of matrices to be diagonalized:
Nmatr = 5000

x1 = []; x2 = []; x4 = []

for nm in range(Nmatr):
    # beta = 1
    beta = 1
    H = np.random.randn(N, M)
    W = np.dot(H, H.T)
    x1 = np.append(x1, eigvalsh(W) / (beta * N)) # Notice the rescaling of the eigenvalues

    # beta = 2
    H = np.random.randn(N, M) + 1j * np.random.randn(N, M)
    H = mat(H)
    W = np.dot(H, H.H)
    x2 = np.append(x2, eigvalsh(W) / (beta * N)) # Notice the rescaling of the eigenvalues

    # beta = 4
    beta = 4
    A = np.random.randn(N, N) + 1j * np.random.randn(N, N)
    B = np.random.randn(N, N) + 1j * np.random.randn(N, N)
    H1 = np.hstack((A, B))
    H2 = np.hstack((-np.conjugate(B), np.conjugate(A)))
    H = np.vstack((H1, H2))
    H = mat(H)
    W = np.dot(H, H.H)
    x4 = np.append(x4, np.unique(eigvalsh(W)) / (beta * N)) # Notice the rescaling of the eigenvalues

plt.figure(figsize=(5, 5))

x = np.linspace(xmin, xmax, 1000)
MP, = plt.plot(x, rho(x), 'b')

# Plotting the histograms for the three numerical eigenvalue densities
n1, bins1 = histogram(x1, bins=15, normed=1)
n2, bins2 = histogram(x2, bins=15, normed=1)
n4, bins4 = histogram(x4, bins=15, normed=1)
bins_new1 = []
for i in range(len(bins1) - 1):
    bins_new1.append((bins1[i] + bins1[i + 1]) / 2)

bins_new2 = []
for i in range(len(bins2) - 1):
    bins_new2.append((bins2[i] + bins2[i + 1]) / 2)

bins_new4 = []
for i in range(len(bins4) - 1):
    bins_new4.append((bins4[i] + bins4[i + 1]) / 2)

beta1, = plt.plot(bins_new1, n1, 'ob')
beta2, = plt.plot(bins_new2, n2, 'or')
beta4, = plt.plot(bins_new4, n4, 'om')

plt.title(r'Mar$c\check$enko-Pastur density', fontsize=18)
plt.xlabel(r'$x$', fontsize=18)
plt.ylabel(r'$\rho(x)$', fontsize=18)
plt.legend(handles=[MP, beta1, beta2, beta4], labels=['MP density', r'$\beta$ = 1', r'$\beta$ = 2', r'$\beta$ = 4'], loc='upper right')
plt.show()
```

## Chapter 15 Replicas...

+ This program provides a numerical verification that equations (15.2) and (15.6) coincide in a 2x2 case. The program generates a random 2x2 GOE matrix H and uses it to compute the numerical value of Z(x) according to the two formulas. You will be asked to choose the real and imaginary parts of the number x (imaginary part has to be positive).

```python
import numpy as np
from numpy.linalg import eigvalsh
from scipy.integrate import nquad
```

```python
# Choose real part of argument x:
#re = 0.2
re = input()

# Choose imaginary part of argument x:
# im = 4.4
im = input()

while im <= 0:
    print 'ERROR: Imaginary part has to be positive'
    im = input()

x = re - 1j * im

# Generating 2x2 GOE matrix
H = np.random.randn(2, 2) / np.sqrt(2)
H = (H + H.T) / 2

# Eigenvalues of matrix H
E = eigvalsh(H)

# Definition of integrand function for Z
f = lambda y1, y2: np.exp(- 1j * ((x - H[0, 0]) * y1 ** 2 + 2 * H[0, 1] * y1 * y2 + (x - H[1, 1]) * y2 ** 2) / 2)

# Z function
Z = nquad(f, [[-np.Inf, np.Inf], [-np.Inf, np.Inf]])[0]

# Explicit form of Z function
Z_exp = 2 * np.pi * np.exp(-(np.log(E[0] - x) + np.log(E[1] - x)) / 2 + 1j * np.pi / 2)

print 'Value of Z function computed as integral: %6.4f%+6.4fi' % (Z.real, Z.imag)
print 'Value of Z function computed explicitly via eigenvalues of H: %6.4f%+6.4fi' % (Z_exp.real, Z_exp.imag)
```

    0.2
    4.4
    Value of Z function computed as integral: 1.4198+0.0000i
    Value of Z function computed explicitly via eigenvalues of H: 1.4198-0.0253i

    /Users/brucelee/miniconda2/lib/python2.7/site-packages/scipy/integrate/quadpack.py:390: ComplexWarning: Casting complex values to real discards the imaginary part
      return _quadpack._qagie(func,bound,infbounds,args,full_output,epsabs,epsrel,limit)

## Chapter 17 Born to be free

+ This program presents a numerical verification of the **free addition** example discussed in section 17.4, i.e. that of the addition of GOE and Wishart random matrices. You will be asked to select the size of the matrices (including the size of the rectangular matrices used to build Wishart matrices), the number of matrices to be numerically diagonalized, and the relative weight p between the two ensembles (with p=1 coinciding with the GOE and p=0 coinciding with the Wishart ensemble).

+ **Two random matrices are free if the traces of all non-commutative products of matrix polynomials, whose traces are zero, are zero.**

> 这里补充介绍Python中调用Matlab函数:

+ sympy中的solve, solveset求解复杂的代数方程异常慢, 建议选择Matlab, Mathematica或Maple等专门软件来处理这类符号计算问题.

```python
#matlab
#syms G z a w

#sol = solve('w^2*G/2 + (1-w)*(1+a)/(1-(1-w)*G) + 1/G - z', 'G');
#sol = subs(sol,[a,w],[alpha,p]);
#sol = vpa(sol);
```

+ 当然也可以在Python中调用Matlab函数, 详细说明见[MathWorks教程](https://www.mathworks.com/help/matlab/matlab-engine-for-python.html)

    + 首先进入Matlab应用中相应的python目录中: cd /Applications/MATLAB_R2014b.app/extern/engines/python
    + 在root权限下安装python支持: sudo python setup.py install

```python
import matlab
import matlab.engine as engine
```

```python
eng = engine.start_matlab()
future = eng.sqrt(2.0, async=True)
res = future.result()
print(res)
eng.quit()
```

    1.41421356237

+ Hybrid computation of numerical and symbolical Python

```python
import numpy as np
from numpy import histogram
from numpy.linalg import eigvalsh
import matplotlib.pyplot as plt

from sympy import symbols, solve

from __future__ import division
```

```python
# Choose size of GOE and Wishart matrices:
#N = 100
N = input()

# Choose second dimension of rectangular matrices used to build Wishart matrices:
#T = 200
T = input()

while N >= T:
    print 'ERROR: T must be larger than N\n'
    N = input()
    T = input()

# Parameters associated with Wishart matrices to be used in later calculations
c = N / float(T)
alpha = (1 - c) / c

# Choose relative weight of GOE matrices:
# p = 0.3
# p = 0.5
# p = 0.7
p = input()

# Choose number of matrices to be diagonalized:
Nmatr = 200

E = []

for nm in range(Nmatr):
    # Generating GOE matrix
    M = np.random.randn(N, N)
    M = (M + M.T) / 2

    # Generating Wishart matrix
    H = np.random.randn(N, T)
    W = np.dot(H, H.T)

    # Sum of rescaled GOE and rescaled Wishart
    H = p * M / np.sqrt(N) + (1 - p) * W / N

    E = np.append(E, eigvalsh(H))

# In the following the analytical solution for the eigenvalue density is
# computed on a support ranging from the minimum to the maximum of the
# numerically obtained eigenvalues. The density is computed in a number
# Npts of points
Npts = 100
x = np.linspace(np.min(E), np.max(E), Npts)

# Defining symbolic variables: G denotes the resolvent, z denotes the
# complex variable the resolvent is a function of, whereas a and w are
# the symbolic variables that will be eventually replaced, respectively,
# by the numerical values of the parameter alpha = (1-c)/c and of the
# relative weight p.
G, z, a, w = symbols('G, z, a, w')

# Here the symbolic solution for the resolvent G is obtained, initially
# as a function of z, a, and w. Then, the numerical values of alpha and p
# are used to replace a and w. The sol variable is a 3-dimensional vector
# whose components represent solutions of the 3rd degree polynomial
# equation for G.
expr = w ** 2 * G / 2 + (1 - w) * (1 + a) / (1 - (1 - w) * G) + 1 / G - z
expr = expr.subs({a:alpha, w:p})
sol = solve(expr, G)

G = []

for i in range(Npts):
    # Replacing the symbolic variable z with the numerical value of the
    # point x(i) where the resolvent is being computed
    tmp = [complex(s.subs({z: x[i]})) for s in sol]

    # The following if condition selects solutions with a non-zero
    # imaginary part (see Eq. 8.8)
    if tmp[0].imag == 0:
        G.append(abs(tmp[1].imag) / np.pi)
    else:
        G.append(abs(tmp[0].imag) / np.pi)

plt.figure(figsize=(5, 5))
# Normalized eigenvalue histogram
n, bins = histogram(E, bins=30, normed=1)
bins_new = []
for i in range(len(bins) - 1):
    bins_new.append((bins[i] + bins[i + 1]) / 2)
plt.plot(bins_new, n, 'o')

plt.plot(x, G, 'r')

plt.title('GOE + Wishart free sum', fontsize=18)
plt.xlabel(r'$x$', fontsize=18)
plt.ylabel(r'$\rho(x)$', fontsize=18)
plt.show()
```