```python
import pyequion
import sympy
import scipy
%load_ext Cython
```

## Create a system and save it to a C file

```python
sys_eq = pyequion.create_equilibrium(['NaHCO3', 'CaCl2'])
pyequion.save_res_to_c_code(sys_eq, 'dummy', 'calc_cnv_res_equilibrium_NaHCO3_CaCl2',
    # fixed_temperature=25.0
)

```

Check the file system for the created header and source files

## Generating Cython module for calling the C code

Reference: https://www.sympy.org/scipy-2017-codegen-tutorial/

1. Firstly a magic cell is used to create the build configuration file
1. Nextly, the cython file that will bridge the python interpreter with the c-function is defined and make it available to python interpreter

```python
%%writefile calc_cnv_res_equilibrium_NaHCO3_CaCl2.pyxbld
import numpy

#            module name specified by `%%cython_pyximport` magic
#            |        just `modname + ".pyx"`
#            |        |
def make_ext(modname, pyxfilename):
    from setuptools.extension import Extension
    return Extension(modname,
                     sources=[pyxfilename, 'calc_cnv_res_equilibrium_NaHCO3_CaCl2.c'],
                     include_dirs=['.', numpy.get_include()])
```

    Overwriting calc_cnv_res_equilibrium_NaHCO3_CaCl2.pyxbld

```python
%%cython_pyximport calc_cnv_res_equilibrium_NaHCO3_CaCl2
import numpy as np
cimport numpy as cnp # cimport gives us access to NumPy's C API

# here we just replicate the function signature from the header
cdef extern from "calc_cnv_res_equilibrium_NaHCO3_CaCl2.h":
    void calc_cnv_res_equilibrium_NaHCO3_CaCl2(double T, double *concs, double *x, double *res)

# here is the "wrapper" signature that conforms to the odeint interface
def cy_calc_cnv_res_equilibrium_NaHCO3_CaCl2(double T, cnp.ndarray[cnp.double_t, ndim=1] concs, cnp.ndarray[cnp.double_t, ndim=1] x):
    # preallocate our output array
    cdef cnp.ndarray[cnp.double_t, ndim=1] res = np.empty(x.size, dtype=np.double)
    # now call the C function
    calc_cnv_res_equilibrium_NaHCO3_CaCl2(<double> T, <double *> concs.data, <double *> x.data, <double *> res.data)
    # return the result
    return res
```

```python
def wrapper_py_res_func(x, T, concs):
    return cy_calc_cnv_res_equilibrium_NaHCO3_CaCl2(T, concs, x)
```

```python
sol = pyequion.solve_solution({'NaHCO3': 10, 'CaCl2': 5})
cy_calc_cnv_res_equilibrium_NaHCO3_CaCl2(25.0+273.15, np.array([10.0, 5.0]), sol.x)
```

    array([-5.68434189e-14,  4.44089210e-16,  0.00000000e+00,  2.84217094e-14,
            0.00000000e+00,  7.77156117e-16,  0.00000000e+00,  1.77635684e-15,
            0.00000000e+00, -2.27373675e-13,  9.99000000e+00,  9.99000000e+00,
            4.99500000e+00,  9.99000000e+00,  0.00000000e+00])

## Generating C Code for the Jacobian

```python
pyequion.save_jacobian_of_res_to_c_code(sys_eq, 'dummy', 'calc_cnv_jac_equilibrium_NaHCO3_CaCl2',
    # fixed_temperature=25.0
)

```

```python
%%writefile calc_cnv_jac_equilibrium_NaHCO3_CaCl2.pyxbld
import numpy

#            module name specified by `%%cython_pyximport` magic
#            |        just `modname + ".pyx"`
#            |        |
def make_ext(modname, pyxfilename):
    from setuptools.extension import Extension
    return Extension(modname,
                     sources=[pyxfilename, 'calc_cnv_jac_equilibrium_NaHCO3_CaCl2.c'],
                     include_dirs=['.', numpy.get_include()])
```

    Overwriting calc_cnv_jac_equilibrium_NaHCO3_CaCl2.pyxbld

```python
%%cython_pyximport calc_cnv_jac_equilibrium_NaHCO3_CaCl2
import numpy as np
cimport numpy as cnp # cimport gives us access to NumPy's C API

# here we just replicate the function signature from the header
cdef extern from "calc_cnv_jac_equilibrium_NaHCO3_CaCl2.h":
    void calc_cnv_jac_equilibrium_NaHCO3_CaCl2(double T, double *x, double *res)

# here is the "wrapper" signature that conforms to the odeint interface
def cy_calc_cnv_jac_equilibrium_NaHCO3_CaCl2(double T, cnp.ndarray[cnp.double_t, ndim=1] x):
    # preallocate our output array
    cdef cnp.ndarray[cnp.double_t, ndim=1] J = np.empty((x.size*x.size), dtype=np.double)
    # now call the C function
    calc_cnv_jac_equilibrium_NaHCO3_CaCl2(<double> T, <double *> x.data, <double *> J.data)
    # return the result

    mat_J = J.reshape((x.size, -1))
    return mat_J
```

```python
def wrapper_py_jac_func(x, T, concs):
    return cy_calc_cnv_jac_equilibrium_NaHCO3_CaCl2(T, x)
```

```python
sol = pyequion.solve_solution({'NaHCO3': 10, 'CaCl2': 5})
```

```python
J = cy_calc_cnv_jac_equilibrium_NaHCO3_CaCl2(25, np.full(15, -3.0))
J.shape
J
```

    array([[ 0.        , -0.97797761, -0.97797761,  0.02202239,  0.        ,
             0.02202239,  0.08808954,  0.02202239,  0.        ,  0.        ,
             0.08808954,  0.02202239,  0.        ,  0.02202239,  0.02202239],
           [-1.        , -0.02361803, -0.02361803,  0.97638197,  0.        ,
             0.97638197, -0.09447213, -0.02361803,  0.        ,  0.        ,
            -0.09447213, -0.02361803,  0.        , -0.02361803, -0.02361803],
           [ 0.        , -0.0029818 , -1.0029818 ,  0.9970182 , -1.        ,
            -0.0029818 , -0.01192722, -0.0029818 ,  0.        ,  0.        ,
            -0.01192722, -0.0029818 ,  0.        , -0.0029818 , -0.0029818 ],
           [ 0.        , -0.04311272,  0.95688728, -0.04311272,  0.        ,
            -1.04311272,  0.8275491 , -0.04311272,  0.        ,  0.        ,
            -0.1724509 , -0.04311272,  0.        , -0.04311272, -0.04311272],
           [ 0.        , -0.0447669 , -0.0447669 ,  0.9552331 ,  0.        ,
            -0.0447669 ,  0.82093239, -1.0447669 ,  0.        ,  0.        ,
            -0.17906761, -0.0447669 ,  0.        , -0.0447669 , -0.0447669 ],
           [ 0.        , -0.06971256, -0.06971256,  1.93028744,  0.        ,
            -0.06971256,  0.72114975, -0.06971256, -1.        ,  0.        ,
            -0.27885025, -0.06971256,  0.        , -0.06971256, -0.06971256],
           [ 0.        ,  0.02075136, -0.97924864,  0.02075136,  0.        ,
            -0.97924864,  0.08300543,  0.02075136,  0.        ,  1.        ,
             0.08300543,  0.02075136,  0.        ,  0.02075136,  0.02075136],
           [ 0.        , -0.02411852, -1.02411852, -0.02411852,  0.        ,
            -0.02411852, -0.09647406, -0.02411852,  0.        ,  0.        ,
             0.90352594, -1.02411852,  0.        , -0.02411852, -0.02411852],
           [ 0.        , -0.09138075, -0.09138075, -0.09138075,  0.        ,
            -0.09138075,  0.634477  , -0.09138075,  0.        ,  0.        ,
             0.634477  , -0.09138075, -1.        , -0.09138075, -0.09138075],
           [ 0.        , -0.0464731 , -0.0464731 , -0.0464731 ,  0.        ,
             0.9535269 , -0.1858924 , -0.0464731 ,  0.        ,  0.        ,
             0.8141076 , -0.0464731 ,  0.        , -1.0464731 , -0.0464731 ],
           [-0.00230259,  0.        ,  0.        , -0.00230259, -0.00230259,
             0.        ,  0.        , -0.00230259, -0.00460517,  0.        ,
             0.        ,  0.        ,  0.        ,  0.        ,  0.        ],
           [-0.00230259,  0.        ,  0.        ,  0.        ,  0.        ,
            -0.00230259, -0.00230259, -0.00230259, -0.00230259, -0.00230259,
             0.        ,  0.        , -0.00230259, -0.00230259,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
            -0.00230259, -0.00230259, -0.00230259, -0.00230259,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ,  0.        , -0.00230259],
           [ 0.        , -0.00230259,  0.00230259,  0.00230259,  0.        ,
            -0.00230259, -0.00460517, -0.00230259,  0.        ,  0.        ,
             0.00460517,  0.00230259,  0.        ,  0.00230259, -0.00230259]])

```python
## Solving the system

```

```python
np.full(15, -3)
```

    array([-3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3])

```python

```

    ['NaHCO3',
     'OH-',
     'H+',
     'Na+',
     'NaOH',
     'HCO3-',
     'CO3--',
     'NaCO3-',
     'Na2CO3',
     'CO2',
     'Ca++',
     'CaOH+',
     'CaCO3',
     'CaHCO3+',
     'Cl-',
     'H2O']

```python
concs = np.array([10e-3,5e-3])
root_sol = scipy.optimize.root(wrapper_py_res_func, sol.x, args=(25+273.15, concs), jac=wrapper_py_jac_func)
root_sol
```

        fjac: array([[ 0.00000000e+00,  9.99999992e-01,  0.00000000e+00,
             0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
             0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
             0.00000000e+00,  8.87383607e-05,  8.87383607e-05,
             0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
           [-1.00000000e+00, -4.07451850e-14, -3.41143460e-07,
            -4.53142697e-06, -4.79142557e-06, -7.45642413e-06,
             2.36144688e-06, -2.45265355e-06, -9.71015202e-06,
            -4.83215964e-06,  2.29286940e-10,  2.29286940e-10,
             0.00000000e+00,  0.00000000e+00, -2.37473280e-06],
           [-2.04965197e-06,  2.00890275e-14, -4.99999326e-01,
             5.00001731e-01,  2.36539103e-06,  3.68102530e-06,
            -5.00000660e-01, -4.99998283e-01,  4.79362690e-06,
             2.38550029e-06, -1.13192465e-10, -1.13192465e-10,
             0.00000000e+00,  0.00000000e+00,  1.20238230e-06],
           [ 8.18558193e-06, -8.84827294e-07, -3.18104067e-01,
            -9.33160640e-02, -4.11758287e-01, -8.32378051e-01,
             1.02359040e-01,  1.22420881e-01,  4.04651972e-02,
             2.01370990e-02,  9.93383757e-03,  3.73550375e-05,
             0.00000000e+00,  0.00000000e+00, -9.89672810e-03],
           [ 1.53683141e-06, -3.49438172e-07,  8.05487726e-01,
             2.73519139e-01, -1.62610551e-01, -3.28721338e-01,
            -2.69947212e-01, -2.62022893e-01,  1.59835342e-02,
             7.95404528e-03,  3.92309570e-03,  1.47522208e-05,
             0.00000000e+00,  0.00000000e+00, -3.90767999e-03],
           [ 7.66897039e-07, -1.15382430e-06, -6.57673805e-12,
             5.97445842e-01,  3.03342728e-03, -5.48239757e-03,
             5.76085537e-01,  2.13595479e-02,  5.27386992e-02,
            -5.54627905e-01,  5.79347209e-04,  1.24231951e-02,
             0.00000000e+00,  0.00000000e+00,  1.18438603e-02],
           [-1.18551328e-05, -3.61220064e-07, -1.19161527e-10,
             2.34242643e-01,  3.40577916e-01, -1.12559696e-01,
            -3.53808607e-02,  2.69635208e-01,  8.05617067e-01,
             3.05044410e-01,  1.04970107e-02, -6.42639167e-03,
             0.00000000e+00,  0.00000000e+00, -1.70292162e-02],
           [ 3.45306262e-06, -3.58237168e-07, -1.59475484e-11,
            -9.10647559e-02,  8.29463925e-01, -4.31411292e-01,
             1.03130776e-02, -1.01381243e-01, -3.07761927e-01,
            -1.11662997e-01,  1.40481678e-03,  2.63218776e-03,
             0.00000000e+00,  0.00000000e+00,  1.28767446e-03],
           [ 1.31523927e-06, -6.36534027e-05, -7.78861348e-09,
             1.56627740e-01, -2.28303758e-06,  1.65413827e-02,
             5.49524077e-03,  1.51131170e-01, -1.73283435e-01,
             1.51056023e-01,  6.86104125e-01,  3.12113824e-02,
             0.00000000e+00,  0.00000000e+00, -6.54037621e-01],
           [-1.66201262e-08, -1.80577497e-06, -7.26419296e-11,
             4.19132847e-03, -3.81954582e-07, -6.94082009e-10,
            -5.77397036e-01,  5.81588382e-01, -4.18715934e-03,
            -5.72757749e-01,  6.39906926e-03,  1.39503595e-02,
             0.00000000e+00,  0.00000000e+00,  6.74666161e-03],
           [-1.68045491e-06,  1.48435824e-05,  2.16300542e-09,
             4.69400874e-01, -7.54697117e-06,  9.28472331e-09,
             1.10017802e-05,  4.69391545e-01, -4.69082314e-01,
             4.84963195e-01, -1.90540574e-01,  2.32670057e-02,
            -2.22267644e-02,  0.00000000e+00,  2.58390761e-01],
           [ 1.36679692e-06,  9.80158719e-05,  6.10061335e-09,
             3.85561135e-02,  2.34650045e-05,  4.26062198e-08,
            -2.68172505e-04,  3.88228969e-02, -3.88126642e-02,
             1.38537963e-02, -5.37407078e-01, -5.67141962e-01,
             2.68736608e-01,  0.00000000e+00, -5.59135082e-01],
           [ 6.73146689e-07,  1.59166101e-05,  3.00590056e-09,
             4.11430765e-05,  6.46402839e-06,  1.15251966e-08,
             4.03936774e-05,  6.52617330e-08, -1.14052721e-04,
            -4.15196775e-03, -2.64791775e-01,  8.54261678e-02,
            -9.20411038e-01,  0.00000000e+00, -2.74616009e-01],
           [ 7.59212150e-07, -4.01335286e-05,  4.15187586e-09,
             3.87504806e-04, -1.85142357e-06, -2.24523092e-09,
             3.86795007e-04, -6.44981809e-08, -3.55999654e-04,
             1.14268315e-02, -3.65741499e-01,  8.18009582e-01,
             2.83072656e-01,  0.00000000e+00, -3.41807476e-01],
           [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
             0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
             0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
             0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
             0.00000000e+00,  1.00000000e+00,  0.00000000e+00]])
         fun: array([ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, -1.42108547e-14,
            0.00000000e+00,  1.66533454e-15,  0.00000000e+00,  0.00000000e+00,
            0.00000000e+00,  0.00000000e+00, -3.46944695e-18,  0.00000000e+00,
            0.00000000e+00,  0.00000000e+00,  5.20417043e-18])
     message: 'The solution converged.'
        nfev: 2
        njev: 1
         qtf: array([ 4.44088898e-16,  5.68443786e-14,  1.33223055e-14, -7.66075579e-15,
            5.24433267e-15,  1.43121832e-13, -6.23093339e-14,  2.22854435e-14,
           -2.96145643e-14,  1.31382233e-13, -9.60924319e-14, -1.98401192e-15,
            9.47892591e-16, -2.58872935e-15,  0.00000000e+00])
           r: array([-1.00000001e+00, -2.58384736e-06, -3.26897376e-08,  9.75056250e-01,
           -1.00735262e-12,  9.76825658e-01, -5.79221420e-04, -1.51987292e-05,
           -6.90786933e-12, -4.19598445e-08, -4.56665627e-02, -1.08469601e-07,
           -2.86907050e-08, -7.58286072e-04, -2.50535259e-02,  9.99997552e-01,
            9.99995870e-01, -2.36519337e-02,  3.41143460e-07, -2.19580986e-02,
           -5.75279810e-04, -9.60672381e-06,  7.45642413e-06,  2.36144678e-06,
           -4.32853173e-02,  2.34988026e-06,  9.71015195e-06, -7.13573746e-04,
           -2.37377246e-02,  2.00000202e+00, -5.19774698e-01,  4.99999326e-01,
           -1.83803620e-02,  4.99553110e-01, -1.44199053e-05, -3.68102530e-06,
           -5.00000660e-01, -5.36216295e-01,  4.99998197e-01, -4.79362686e-06,
           -6.03849616e-04, -1.98738554e-02, -2.31628590e+00,  3.18104067e-01,
            8.45023283e-02, -1.29515492e+00,  4.11806289e-01,  8.32378050e-01,
            1.02359022e-01,  3.27070968e-01, -1.22420539e-01, -4.04652092e-02,
           -1.77488776e-02,  7.93691193e-02, -8.05487726e-01,  1.81640571e-02,
           -2.01485754e-01,  1.62619535e-01,  3.28721338e-01, -2.69947219e-01,
           -2.11169578e-01,  2.62022957e-01, -1.59835390e-02, -7.50852202e-03,
            1.49014397e-02, -1.72154733e+00,  6.47908982e-01, -3.02909857e-03,
            5.48239722e-03,  5.76079663e-01, -4.66229984e-01, -2.13595133e-02,
           -5.27427158e-02,  5.54860799e-01,  7.43599454e-03,  1.26521674e+00,
           -3.40647696e-01,  1.12559695e-01, -3.53778220e-02,  1.16969861e+00,
           -2.69635710e-01, -8.05614989e-01, -3.08542528e-01, -1.14949854e-01,
           -8.29443673e-01,  4.31411292e-01,  1.03118330e-02, -4.59696147e-01,
            1.01381388e-01,  3.07761076e-01,  1.12676251e-01,  3.34816195e-02,
           -1.65414191e-02,  5.48048250e-03,  1.10973590e-01, -1.51131245e-01,
            1.73273344e-01, -1.51603362e-01,  1.27528358e-02, -5.77403633e-01,
            4.77447407e-03, -5.81588381e-01,  4.18264894e-03,  5.72752553e-01,
           -1.61152229e-04,  4.72072736e-01, -4.69391561e-01,  4.69081978e-01,
           -4.85096922e-01, -1.62940433e-02, -3.88229777e-02,  3.89091440e-02,
           -1.40240424e-02,  1.32527496e-02,  3.84018319e-04,  4.54863040e-03,
            6.52697149e-03, -1.24477234e-02,  7.36148025e-03, -2.30258509e-02])
      status: 1
     success: True
           x: array([-4.41410428, -5.986602  , -7.88445742, -2.00194266, -8.30714557,
           -2.03389701, -4.23809309, -5.21713241, -7.94810423, -3.68749322,
           -2.34133052, -7.36356081, -3.85258587, -3.51907119, -2.        ])

```python
wrapper_py_jac_func(np.full(15,-3.0), 25+273.15, concs)
# wrapper_py_res_func(np.full(15,-3.0), 25+273.15, concs)
wrapper_py_res_func(sol.x, 25+273.15, concs)
```

    array([-5.68434189e-14,  4.44089210e-16,  0.00000000e+00,  2.84217094e-14,
            0.00000000e+00,  7.77156117e-16,  0.00000000e+00,  1.77635684e-15,
            0.00000000e+00, -2.27373675e-13, -1.73472348e-18, -1.73472348e-18,
           -2.60208521e-18,  0.00000000e+00,  0.00000000e+00])

```python
sol.x
```

    array([-4.41410428, -5.986602  , -7.88445742, -2.00194266, -8.30714557,
           -2.03389701, -4.23809309, -5.21713241, -7.94810423, -3.68749322,
           -2.34133052, -7.36356081, -3.85258587, -3.51907119, -2.        ])

```python
cy_calc_cnv_res_equilibrium_NaHCO3_CaCl2(25+273.15, concs, sol.x)
```

    array([-5.68434189e-14,  4.44089210e-16,  0.00000000e+00,  2.84217094e-14,
            0.00000000e+00,  7.77156117e-16,  0.00000000e+00,  1.77635684e-15,
            0.00000000e+00, -2.27373675e-13, -1.73472348e-18, -1.73472348e-18,
           -2.60208521e-18,  0.00000000e+00,  0.00000000e+00])

```python
concs
```

    array([0.01 , 0.005])

```python
10e-3
```

    0.01

```python

```


```javascript
%%javascript
MathJax.Hub.Config({
    TeX: { equationNumbers: { autoNumber: "AMS" } }
});
```

    <IPython.core.display.Javascript object>

# Exercise1 (ODE)

Let $\{ t_k\}$ be a partition of $[a,b]$ such that $a=t_0<t_1<\cdots<t_{N-1}<t_{N}=b$ and $H$ be the constant length of the $k$-th subinterval ($H = t_k - t_{k-1}$). Let us consider the initial value problem

\begin{equation}\label{eul2}
  \begin{cases}
    y' = F(y, t),      & \quad \text{on } [a, b]\\\\
    y(a) = c,
  \end{cases}
\end{equation}
Let $\{ y_k\}$ be the approximate solution i.e.  $y_k\approx g(t_k)=g_k$ where $g$ is the exact solution.
1. Write a python function  EulerMethod  that takes $a,b,c,H,$ and $F$ and return array of all $t_k$ and $y_k$ of \eqref{eul2} using Euler method i.e.
$$ y_{k+1} = y_k + HF(y_k, t_k) $$

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
import warnings
warnings.filterwarnings('ignore')

```

```python
def  EulerMethod(a,b,c,H,F):
    N=int((b-a)/H+1)
    t=np.linspace(a,b,N)
    y=np.zeros((len(t),len(c)))
    y[0]=c
    for k in range (len(t)-1):
        y[k+1]=y[k]+H*F(y[k],t[k])
    return t,y

```

2. Write a python function  EulerMethodModif  that takes $a,b,c,H,$ and $F$ and return array of all $t_k$ and $y_k$ of (1) using the modified implicit euler method i.e.
\begin{equation*}
\begin{cases}
\alpha = HF(y_k, t_k)\\\\
\beta = HF(y_k + \alpha, t_k +H)\\\\
y_{k+1} = y_k + \dfrac{\alpha + \beta}{2}
\end{cases}
\end{equation*}

```python
def EulerMethodModif(a,b,c,H,F):
    N=int((b-a)/H+1)
    t=np.linspace(a,b,N)
    y=np.zeros((len(t),len(c)))
    y[0]=c
    for k in range (len(t)-1):
        alpha=H*F(y[k],t[k])
        beta=H*F(y[k]+alpha,t[k]+H)
        y[k+1]=y[k]+(alpha+beta)/2
    return t,y
```

3. Let us consider the initial value problem

\begin{equation*}
  \begin{cases}
    z'' = 6z' -5z,      & \quad \text{on } [0, 1]\\\\
    z(0) = 1, z'(0) = 3
  \end{cases}\quad \quad (2)
\end{equation*}
Using the characteristic equation, find and type the exact solution of the second order ODE (2) in the Markdown cell below.

$$ z(t)= \cfrac{1}{2}e^{5x}+\cfrac{1}{2}e^{x} $$

4. Define in the Code cell below the function $F$ such that the second order ODE (2) is equivalent to the first order ODE (1)

```python
def F(y,t):
    u,v=y
    dudt=v
    dvdt=6*v-5*u
    return np.array([dudt,dvdt])

```

```python
a=0
b=1
c=np.array([1,3])
H=0.01
N=101
exact_sol_Euler=EulerMethod(a,b,c,H,F)[1][:,0]
```

```python
exact_sol_EulerModi=EulerMethodModif(a,b,c,H,F)[1][:,0]
```

5. Compare graphically the exact and numerical solution of (2) using  odeint

```python
t=np.linspace(a,b,N)
sol=odeint(F,c,t)[:,0]
exact=1/2*(np.exp(5*t)+np.exp(t))
error_odeint=abs(exact-sol)

plt.subplot(2,2,1)
plt.plot(t,exact)
plt.plot(t,sol,'r--')
plt.subplot(2,2,2)
plt.plot(t,error_odeint);
```

```python
plt.plot(t,sol);
```

6. Compare graphically the exact and numerical solution of (2) using  EulerMethod

```python
error_euler=abs(exact-exact_sol_Euler)

plt.subplot(2,2,1)
plt.plot(t,exact)
plt.plot(t,exact_sol_Euler,'r--')
plt.subplot(2,2,2)
plt.plot(t,error_euler);
```

7. Compare graphically the exact and numerical solution of (2) using  EulerMethodModif

```python
error_euler=abs(exact-exact_sol_EulerModi)

plt.subplot(2,2,1)
plt.plot(t,exact)
plt.plot(t,exact_sol_EulerModi,'r--')
plt.subplot(2,2,2)
plt.plot(t,error_euler);
```

8. Which method give a better approximation of the exact solution? Why?

### odeint method is better than others because it's error is closest zero

# Exercise 2 (IPS)

Let $X$ be Binomial r.v. with parameter $(n,p)$ and $Y$ be a Poisson r.v. with parameter $\lambda$.
Under some conditions on $n$, $p$ and $\lambda$, the binomial distribution can be approximated by the Poisson distribution which can be itself approximated by a gaussian distribution.
The goal of the exercise is to experiment these conditions empirically.

```python
from scipy.stats import norm, poisson, binom
#norm,poisson,binom can be used for normal, poisson and binomial distr. (resp)
```

1. Represent the bar plot of $P(X=x)$ for $x=0,1,\ldots,n$  with $n=25$ and $p = 0.1$

```python
rvX=binom(25,0.1)
binomial=np.zeros(26)
for i in range(26):
    binomial[i]=rvX.pmf(i)
binomial

i=np.arange(26)

plt.bar(i,binomial);

```

2. Represent the bar plot of $P(Y=y)$ for $y \in \mathbb{N}$. Choose an appropriate value for $\lambda$ relying on $n$ and $p$ such that $P(Y=y)$ is
an approximation of $P(X=x)$.

```python
rvY=poisson(0.1)
poissonn=np.zeros(26)
for i in range(26):
    poissonn[i]=rvY.pmf(i)
poissonn

i=np.arange(26)

plt.bar(i,poissonn);
```

## the appropriate value of $\lambda$ is $\lambda=np=25\times 0.1=2.5$

```python
rvY=poisson(2.5)
binomial1=np.zeros(26)
for i in range(26):
    binomial1[i]=rvY.pmf(i)
binomial1

i=np.arange(26)

plt.bar(i,binomial1);
```

3. Represent a comparison bar plot of $P(X=x)$ and $P(Y=y)$.

```python
plt.bar(i+0.00,binomial1,color='g')
plt.bar(i+0.5,binomial,color='r');
```

4. Fit a normal distribution on $P(Y=y)$ by drawing a bell curve on $P(Y=y)$. Hint: choose the appropriate expectation and variance of the normal distribtion (Note that $E[Y] = Var[Y] = \lambda$)

```python
plt.bar(i,binomial1)
t=np.linspace(-5,10,100)
norm=1/(2*np.pi)**(1/2)*1/2.5*np.exp(-1/2*(t-2.5)**2/2.5)
plt.plot(t,norm,'r-',linewidth=4);
```

# Pandas part

```python
import numpy as np
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
```

## 1. load titanic dataset from seaborn

```python
data=sns.load_dataset('titanic')
data
```

      survived
      pclass
      sex
      age
      sibsp
      parch
      fare
      embarked
      class
      who
      adult_male
      deck
      embark_town
      alive
      alone

      0
      0
      3
      male
      22.0
      1
      0
      7.2500
      S
      Third
      man
      True
      NaN
      Southampton
      no
      False

      1
      1
      1
      female
      38.0
      1
      0
      71.2833
      C
      First
      woman
      False
      C
      Cherbourg
      yes
      False

      2
      1
      3
      female
      26.0
      0
      0
      7.9250
      S
      Third
      woman
      False
      NaN
      Southampton
      yes
      True

      3
      1
      1
      female
      35.0
      1
      0
      53.1000
      S
      First
      woman
      False
      C
      Southampton
      yes
      False

      4
      0
      3
      male
      35.0
      0
      0
      8.0500
      S
      Third
      man
      True
      NaN
      Southampton
      no
      True

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      886
      0
      2
      male
      27.0
      0
      0
      13.0000
      S
      Second
      man
      True
      NaN
      Southampton
      no
      True

      887
      1
      1
      female
      19.0
      0
      0
      30.0000
      S
      First
      woman
      False
      B
      Southampton
      yes
      True

      888
      0
      3
      female
      NaN
      1
      2
      23.4500
      S
      Third
      woman
      False
      NaN
      Southampton
      no
      False

      889
      1
      1
      male
      26.0
      0
      0
      30.0000
      C
      First
      man
      True
      C
      Cherbourg
      yes
      True

      890
      0
      3
      male
      32.0
      0
      0
      7.7500
      Q
      Third
      man
      True
      NaN
      Queenstown
      no
      True

891 rows × 15 columns

## 2.count the number of missing value in each column

```python
data.isna().sum()
```

    survived         0
    pclass           0
    sex              0
    age            177
    sibsp            0
    parch            0
    fare             0
    embarked         2
    class            0
    who              0
    adult_male       0
    deck           688
    embark_town      2
    alive            0
    alone            0
    dtype: int64

## 3.Remove missing value

```python
data1=data.dropna()
data1
```

      survived
      pclass
      sex
      age
      sibsp
      parch
      fare
      embarked
      class
      who
      adult_male
      deck
      embark_town
      alive
      alone

      1
      1
      1
      female
      38.0
      1
      0
      71.2833
      C
      First
      woman
      False
      C
      Cherbourg
      yes
      False

      3
      1
      1
      female
      35.0
      1
      0
      53.1000
      S
      First
      woman
      False
      C
      Southampton
      yes
      False

      6
      0
      1
      male
      54.0
      0
      0
      51.8625
      S
      First
      man
      True
      E
      Southampton
      no
      True

      10
      1
      3
      female
      4.0
      1
      1
      16.7000
      S
      Third
      child
      False
      G
      Southampton
      yes
      False

      11
      1
      1
      female
      58.0
      0
      0
      26.5500
      S
      First
      woman
      False
      C
      Southampton
      yes
      True

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      871
      1
      1
      female
      47.0
      1
      1
      52.5542
      S
      First
      woman
      False
      D
      Southampton
      yes
      False

      872
      0
      1
      male
      33.0
      0
      0
      5.0000
      S
      First
      man
      True
      B
      Southampton
      no
      True

      879
      1
      1
      female
      56.0
      0
      1
      83.1583
      C
      First
      woman
      False
      C
      Cherbourg
      yes
      False

      887
      1
      1
      female
      19.0
      0
      0
      30.0000
      S
      First
      woman
      False
      B
      Southampton
      yes
      True

      889
      1
      1
      male
      26.0
      0
      0
      30.0000
      C
      First
      man
      True
      C
      Cherbourg
      yes
      True

182 rows × 15 columns

## 4.Size of clean dataset

```python
data1.shape
```

    (182, 15)

## 5.Count the number of survived female

```python
a=data1[data1['sex']=='female']
a['survived'].sum()
```

    82

## 6.Average ages of males who di not survived

```python
a1=data1[data1['sex']=='male']
w=a1[a1['survived']==0]
w['age'].mean()
```

    42.84905660377358

## 7.reproduce the following figure

```python
sns.scatterplot(x='age', y='fare', data=data);
```

```python
sns.boxplot(data=data, x='sex',y='age',hue='who');
```

```python
sns.barplot(data=data, x='pclass', y='age',hue='alone')
```

##  8.building of the logistic regression

```python
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score
import warnings
warnings.filterwarnings('ignore')
```

```python
X=data1[['pclass','age','parch','fare']].values
y=data1[['survived']].values
```

```python
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
```

```python
classifier = LogisticRegression(random_state = 0, solver='lbfgs', multi_class='auto')
classifier.fit(X_train, y_train)
```

    LogisticRegression(random_state=0)

```python
y_pred=classifier.predict(X_test)
y_pred
```

    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
           1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1])

### Confusion matrix

```python
con=confusion_matrix(y_test,y_pred)
con
```

    array([[ 3,  7],
           [ 2, 25]])

```python
sns.heatmap(con,annot=True,annot_kws={"size": 20},cmap="summer_r", xticklabels=np.unique(data.survived),
           yticklabels=np.unique(data.survived));
```

### Accuracy

```python
print("Accuracy:",accuracy_score(y_test, y_pred))
```

    Accuracy: 0.7567567567567568

```python

```


# Frequentist Inference Case Study - Part B

## Learning objectives

Welcome to Part B of the Frequentist inference case study! The purpose of this case study is to help you apply the concepts associated with Frequentist inference in Python. In particular, you'll practice writing Python code to apply the following statistical concepts:
* the _z_-statistic
* the _t_-statistic
* the difference and relationship between the two
* the Central Limit Theorem, including its assumptions and consequences
* how to estimate the population mean and standard deviation from a sample
* the concept of a sampling distribution of a test statistic, particularly for the mean
* how to combine these concepts to calculate a confidence interval

In the previous notebook, we used only data from a known normal distribution. **You'll now tackle real data, rather than simulated data, and answer some relevant real-world business problems using the data.**

## Hospital medical charges

Imagine that a hospital has hired you as their data scientist. An administrator is working on the hospital's business operations plan and needs you to help them answer some business questions.

In this assignment notebook, you're going to use frequentist statistical inference on a data sample to answer the questions:
* has the hospital's revenue stream fallen below a key threshold?
* are patients with insurance really charged different amounts than those without?

Answering that last question with a frequentist approach makes some assumptions, and requires some knowledge, about the two groups.

We are going to use some data on medical charges obtained from [Kaggle](https://www.kaggle.com/easonlai/sample-insurance-claim-prediction-dataset).

For the purposes of this exercise, assume the observations are the result of random sampling from our single hospital. Recall that in the previous assignment, we introduced the Central Limit Theorem (CLT), and its consequence that the distributions of sample statistics approach a normal distribution as $n$ increases. The amazing thing about this is that it applies to the sampling distributions of statistics that have been calculated from even highly non-normal distributions of data! Recall, also, that hypothesis testing is very much based on making inferences about such sample statistics. You're going to rely heavily on the CLT to apply frequentist (parametric) tests to answer the questions in this notebook.

```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import t
from scipy import stats
from numpy.random import seed
medical = pd.read_csv('insurance2.csv')
```

```python
medical.shape
```

    (1338, 8)

```python
medical.head()
```

      age
      sex
      bmi
      children
      smoker
      region
      charges
      insuranceclaim

      0
      19
      0
      27.900
      0
      1
      3
      16884.92400
      1

      1
      18
      1
      33.770
      1
      0
      2
      1725.55230
      1

      2
      28
      1
      33.000
      3
      0
      2
      4449.46200
      0

      3
      33
      1
      22.705
      0
      0
      1
      21984.47061
      0

      4
      32
      1
      28.880
      0
      0
      1
      3866.85520
      1

```python
medical.describe()
```

      age
      sex
      bmi
      children
      smoker
      region
      charges
      insuranceclaim

      count
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000

      mean
      39.207025
      0.505232
      30.663397
      1.094918
      0.204783
      1.515695
      13270.422265
      0.585202

      std
      14.049960
      0.500160
      6.098187
      1.205493
      0.403694
      1.104885
      12110.011237
      0.492871

      min
      18.000000
      0.000000
      15.960000
      0.000000
      0.000000
      0.000000
      1121.873900
      0.000000

      25%
      27.000000
      0.000000
      26.296250
      0.000000
      0.000000
      1.000000
      4740.287150
      0.000000

      50%
      39.000000
      1.000000
      30.400000
      1.000000
      0.000000
      2.000000
      9382.033000
      1.000000

      75%
      51.000000
      1.000000
      34.693750
      2.000000
      0.000000
      2.000000
      16639.912515
      1.000000

      max
      64.000000
      1.000000
      53.130000
      5.000000
      1.000000
      3.000000
      63770.428010
      1.000000

__Q1:__ Plot the histogram of charges and calculate the mean and standard deviation. Comment on the appropriateness of these statistics for the data.

__A:__ Calculate the population mean and standard deviation

```python
_ = plt.hist(medical.charges)
_ = plt.xlabel('Charges ($)')
_ = plt.ylabel('Patients')
_ = plt.title('Avg Patient charges')
```

```python
print("Mean for the data is:{0:f}".format(np.mean(medical.charges)))
print("Standard Deviation is:{0:f}".format(np.std(medical.charges)))
```

    Mean for the data is:13270.422265
    Standard Deviation is:12105.484976

Calculate the sample mean and standard deviation to see what the distribution looks like. Take sample size = 50

```python
def sampler(n):
    return np.random.choice(medical.charges, n)
```

```python
seed(47)
# calculate daily means from the larger sample size here
sample_size = sampler(50)

_ = plt.hist(sample_size, bins=10)
_ = plt.xlabel('Charges($)')
_ = plt.ylabel('Patients')
_ = plt.title('Avg Patient Charges')
```

__Q2:__ The administrator is concerned that the actual average charge has fallen below 12,000, threatening the hospital's operational model. On the assumption that these data represent a random sample of charges, how would you justify that these data allow you to answer that question? And what would be the most appropriate frequentist test, of the ones discussed so far, to apply?

__A:__

Since the sample distribution follows the same distribution pattern as the population distribution, instead of determining confidence interval as a frequentist test, since the distribution is not a normal distribution, apply **bootstraping**. Estimate the uncertainty of staistic .i.e the mean by generating a large family of samples (of size 100) from the one we have. Then characterize the distribution of mean over the family. Note: Draw samples randomy with replacement

For our example, the population is not too large and the distribution is not bi-modal

Let the sample size be 100 (n = 100) and perform bootstrapping over be 1000 replications (range)
https://stats.stackexchange.com/questions/33300/determining-sample-size-necessary-for-bootstrap-method-proposed-method

```python
medical.sample(100, replace = True).describe()
```

      age
      sex
      bmi
      children
      smoker
      region
      charges
      insuranceclaim

      count
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000

      mean
      38.230000
      0.520000
      31.351700
      1.140000
      0.170000
      1.410000
      12112.793376
      0.600000

      std
      14.669321
      0.502117
      6.431836
      1.110283
      0.377525
      1.129002
      10985.970641
      0.492366

      min
      18.000000
      0.000000
      17.385000
      0.000000
      0.000000
      0.000000
      1261.442000
      0.000000

      25%
      24.750000
      0.000000
      26.592500
      0.000000
      0.000000
      0.000000
      3842.122300
      0.000000

      50%
      41.000000
      1.000000
      31.207500
      1.000000
      0.000000
      1.500000
      8959.589500
      1.000000

      75%
      50.250000
      1.000000
      34.516250
      2.000000
      0.000000
      2.000000
      14400.488525
      1.000000

      max
      64.000000
      1.000000
      48.070000
      5.000000
      1.000000
      3.000000
      46200.985100
      1.000000

```python
# Bootstrapped distribution of mean

def bootstrapping(n):
    return pd.DataFrame({'meancharge':[medical.sample(n, replace = True).charges.mean() for i in range(1000)]})
```

```python
s = bootstrapping(n=100)
```

```python
s.meancharge.hist(histtype = 'step')
plt.axvline(medical.charges.mean(), color = 'C1')
```

```python
# Extract the quantile
s.meancharge.quantile(0.0225), s.meancharge.quantile(0.975)
```

    (11133.007826229721, 15568.920332172502)

```python
print("The bootstrapped sample's average patient cost is: ${0:f}".format(s.meancharge.mean()))
```

    The bootstrapped sample's average patient cost is: $13285.091276

**Conclusion** - As you can see, when you draw random samples as a part of bootstrapping, the average medical charge per patient is clearly > 12,000 ich is not a huge concern for the hospital's operating model. As you change the sample size, the margin of error increases, but the sample mean is still > 12K

__Q3:__ Given the nature of the administrator's concern, what is the appropriate confidence interval in this case? A ***one-sided*** or ***two-sided*** interval? (Refresh your understanding of this concept on p. 399 of the *AoS*). Calculate the critical value and the relevant 95% confidence interval for the mean, and comment on whether the administrator should be concerned.

__A:__ Considering the distribution of sample data, it is a one-sided interval

```python
# Since it is a one tailed distribution,
# Reference - https://stackoverflow.com/questions/17559897/python-p-value-from-t-statistic
pop_mean = np.mean(medical.charges)
n = 100

#Find sample mean and std deviation for the bootstrapped sample s (w/ sample size = 100)
sample_mean = np.mean(medical.charges.sample(n, replace = True))
sample_sigma = np.std(medical.charges.sample(n, replace = True))

tt = (sample_mean-pop_mean)/(sample_sigma/np.sqrt(float(n))) # where n = 100 as defined in the bootstrapping function
pval_one_sided = stats.t.sf(np.abs(tt), n-1)

print("t-statistic: {0:f}".format(tt))
print("p-value: {0:f}".format(pval_one_sided))
```

    t-statistic: 0.085879
    p-value: 0.465868

```python
# Calculate confidence interval

import statsmodels.stats.api as sms

a = 1.0 * np.array(medical.charges.sample(100, replace = True))
sms.DescrStatsW(a).tconfint_mean()
```

    (10796.614618772814, 15159.135652027186)

**Conclusion** - With the calcualtion of confidence interval based on bootstrapping, it seems like the average cost of patient would fall below 12,000 in certain samples drawn with replacement, which is a concern for the Hospital administration

The administrator then wants to know whether people with insurance really are charged a different amount to those without.

__Q4:__ State the null and alternative hypothesis here. Use the _t_-test for the difference between means, where the pooled standard deviation of the two groups is given by:
\begin{equation}
s_p = \sqrt{\frac{(n_0 - 1)s^2_0 + (n_1 - 1)s^2_1}{n_0 + n_1 - 2}}
\end{equation}

and the *t*-test statistic is then given by:

\begin{equation}
t = \frac{\bar{x}_0 - \bar{x}_1}{s_p \sqrt{1/n_0 + 1/n_1}}.
\end{equation}

(If you need some reminding of the general definition of ***t-statistic***, check out the definition on p. 404 of *AoS*).

What assumption about the variances of the two groups are we making here?

__A:__
We will calcualte t-test as the inferential statistic to determine if there is a significant difference between the variances of two groups in terms of average cost per patient
- 1st group - patients with insurance
- 2nd group - patients without insurance

**Assumption** we will use here is that there is no difference between the variance of 2 sampling groups drawn from the population

__Q5:__ Perform this hypothesis test both manually, using the above formulae, and then using the appropriate function from [scipy.stats](https://docs.scipy.org/doc/scipy/reference/stats.html#statistical-tests) (hint, you're looking for a function to perform a _t_-test on two independent samples). For the manual approach, calculate the value of the test statistic and then its probability (the p-value). Verify you get the same results from both.

__A:__

**H0** : (Avg cost per patient with an insurance) - (Avg cost per patient who doesnt have insurance) = 0

**H1** : (Avg cost per patient with an insurance) - (Avg cost per patient who doesnt have insurance) <> 0

**This is a two tailed test**

```python
medical.head()
```

      age
      sex
      bmi
      children
      smoker
      region
      charges
      insuranceclaim

      0
      19
      0
      27.900
      0
      1
      3
      16884.92400
      1

      1
      18
      1
      33.770
      1
      0
      2
      1725.55230
      1

      2
      28
      1
      33.000
      3
      0
      2
      4449.46200
      0

      3
      33
      1
      22.705
      0
      0
      1
      21984.47061
      0

      4
      32
      1
      28.880
      0
      0
      1
      3866.85520
      1

```python
insurance_sample = medical.query("insuranceclaim == 1")
no_insurance_sample = medical.query("insuranceclaim == 0")

sample_mean_1 = np.mean(insurance_sample.charges.sample(100, replace=True))
sample_std_1 = np.std(insurance_sample.charges.sample(100, replace = True))
n1 = len(insurance_sample)

sample_mean_2 = np.mean(no_insurance_sample.charges.sample(100, replace = True))
sample_std_2 = np.std(no_insurance_sample.charges.sample(100, replace = True))
n2 = len(no_insurance_sample)
```

```python
# Option 1 to calculate t-statistic and p-value using formula

import math

var1 = (n1 - 1)*(sample_std_1**2)
var2 = (n2 - 1)*(sample_std_2**2)
total_dof = n1 + n2 - 2

pooled_std = np.sqrt((var1 + var2)/total_dof)
#print(pooled_std)
tt_2samples = (sample_mean_1-sample_mean_2)/(pooled_std * (np.sqrt(1/float(n1) + 1/float(n2))))
#= stats.t.cdf(np.abs(tt), n-1)*2

pval_two_sided = 2 * t.cdf(tt_2samples, df=total_dof)

print("t-statistic: {0:f}".format(tt_2samples))
print("p-value: {0:f}".format(pval_two_sided))
```

    t-statistic: 9.851845
    p-value: 2.000000

```python
# Option 2 to calculate t-statistic and p-value using scipy.stats
# Reference : https://stackoverflow.com/questions/22611446/perform-2-sample-t-test
rvs1 = stats.norm.rvs(loc=sample_mean_1,scale=sample_std_1,size=len(insurance_sample))
rvs2 = stats.norm.rvs(loc=sample_mean_2,scale=sample_std_2,size=len(no_insurance_sample))

stats.ttest_ind(rvs1, rvs2)
```

    Ttest_indResult(statistic=10.107240661043102, pvalue=3.414478493510434e-23)

**Question - Why is the p-value so low and not matching????**

Since the p-value is very low (in the far end of spectrum), the null hypothesis can be rejected in favor of alterntaive hypothesis which indicates the variance of price is different between the 2 groups since the base assumption of t statistics test with 2 samples is that there is no difference between means of 2 samples

**Conclusion** - There is a difference in average cost per patient that has insurance and that doesn't have insurance

Congratulations! Hopefully you got the exact same numerical results. This shows that you correctly calculated the numbers by hand. Secondly, you used the correct function and saw that it's much easier to use. All you need to do is pass your data to it.

__Q6:__ Conceptual question: look through the documentation for statistical test functions in scipy.stats. You'll see the above _t_-test for a sample, but can you see an equivalent one for performing a *z*-test from a sample? Comment on your answer.

__A:__
Yes, to compute the z score of each value in the sample, relative to the sample mean and standard deviation.
https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.zscore.html

## Learning outcomes

Having completed this project notebook, you now have good hands-on experience:
* using the central limit theorem to help you apply frequentist techniques to answer questions that pertain to very non-normally distributed data from the real world
* performing inference using such data to answer business questions
* forming a hypothesis and framing the null and alternative hypotheses
* testing this using a _t_-test

Frequentist Case Study/Frequentist Inference Case Study - Part B.ipynb


# Word Embeddings: Intro to CBOW model, activation functions and working with Numpy

In this lecture notebook you will be given an introduction to the continuous bag-of-words model, its activation functions and some considerations when working with Numpy.

Let's dive into it!

```python
import numpy as np
```

# The continuous bag-of-words model

The CBOW model is based on a neural network, the architecture of which looks like the figure below, as you'll recall from the lecture.

## Activation functions

Let's start by implementing the activation functions, ReLU and softmax.

### ReLU

ReLU is used to calculate the values of the hidden layer, in the following formulas:

\begin{align}
 \mathbf{z_1} &= \mathbf{W_1}\mathbf{x} + \mathbf{b_1}  \tag{1} \\
 \mathbf{h} &= \mathrm{ReLU}(\mathbf{z_1})  \tag{2} \\
\end{align}

Let's fix a value for $\mathbf{z_1}$ as a working example.

```python
np.random.seed(10)

z_1 = 10 * np.random.rand(5, 1) - 5

z_1
```

    array([[ 2.71320643],
           [-4.79248051],
           [ 1.33648235],
           [ 2.48803883],
           [-0.01492988]])

Notice that using numpy's `random.rand` function returns a numpy array filled with values taken from a uniform distribution over [0, 1). Numpy allows vectorization so each value is multiplied by 10 and then substracted 5.

To get the ReLU of this vector, you want all the negative values to become zeros.

First create a copy of this vector.

```python
h = z_1.copy()
```

Now determine which of its values are negative.

```python
# Determine which values met the criteria (this is possible because of vectorization)
h < 0
```

    array([[False],
           [ True],
           [False],
           [False],
           [ True]])

You can now simply set all of the values which are negative to 0.

```python
# Slice the array or vector. This is the same as applying ReLU to it
h[h < 0] = 0
```

And that's it: you have the ReLU of $\mathbf{z_1}$!

```python
# Print the vector after ReLU
h
```

    array([[2.71320643],
           [0.        ],
           [1.33648235],
           [2.48803883],
           [0.        ]])

**Now implement ReLU as a function.**

```python
# Define the 'relu' function that will include the steps previously seen
def relu(z):
    result = z.copy()
    result[result < 0] = 0
    return result
```

**And check that it's working.**

```python
# Define a new vector and save it in the 'z' variable
z = np.array([[-1.25459881], [ 4.50714306], [ 2.31993942], [ 0.98658484], [-3.4398136 ]])

# Apply ReLU to it
relu(z)
```

    array([[0.        ],
           [4.50714306],
           [2.31993942],
           [0.98658484],
           [0.        ]])

Expected output:

    array([[0.        ],
           [4.50714306],
           [2.31993942],
           [0.98658484],
           [0.        ]])

### Softmax

The second activation function that you need is softmax. This function is used to calculate the values of the output layer of the neural network, using the following formulas:

\begin{align}
 \mathbf{z_2} &= \mathbf{W_2}\mathbf{h} + \mathbf{b_2}   \tag{3} \\
 \mathbf{\hat y} &= \mathrm{softmax}(\mathbf{z_2})   \tag{4} \\
\end{align}

To calculate softmax of a vector $\mathbf{z}$, the $i$-th component of the resulting vector is given by:

$$ \textrm{softmax}(\textbf{z})_i = \frac{e^{z_i} }{\sum\limits_{j=1}^{V} e^{z_j} }  \tag{5} $$

Let's work through an example.

```python
# Define a new vector and save it in the 'z' variable
z = np.array([9, 8, 11, 10, 8.5])

# Print the vector
z
```

    array([ 9. ,  8. , 11. , 10. ,  8.5])

You'll need to calculate the exponentials of each element, both for the numerator and for the denominator.

```python
# Save exponentials of the values in a new vector
e_z = np.exp(z)
# Print the vector with the exponential values
e_z
```

    array([ 8103.08392758,  2980.95798704, 59874.1417152 , 22026.46579481,
            4914.7688403 ])

The denominator is equal to the sum of these exponentials.

```python
# Save the sum of the exponentials
sum_e_z = np.sum(e_z)

# Print sum of exponentials
sum_e_z
```

    97899.41826492078

And the value of the first element of $\textrm{softmax}(\textbf{z})$ is given by:

```python
# Print softmax value of the first element in the original vector
e_z[0] / sum_e_z
```

    0.08276947985173956

This is for one element. You can use numpy's vectorized operations to calculate the values of all the elements of the $\textrm{softmax}(\textbf{z})$ vector in one go.

**Implement the softmax function.**

```python
# Define the 'softmax' function that will include the steps previously seen
def softmax(z):
    e_z = np.exp(z)
    sum_e_z = np.sum(e_z)
    return e_z / sum_e_z
```

**Now check that it works.**

```python
# Print softmax values for original vector
softmax([9, 8, 11, 10, 8.5])
```

    array([0.08276948, 0.03044919, 0.61158833, 0.22499077, 0.05020223])

Expected output:

    array([0.08276948, 0.03044919, 0.61158833, 0.22499077, 0.05020223])

Notice that the sum of all these values is equal to 1.

```python
# Assert that the sum of the softmax values is equal to 1
sum(softmax([9, 8, 11, 10, 8.5]))
```

    1.0

## Dimensions: 1-D arrays vs 2-D column vectors

Before moving on to implement forward propagation, backpropagation, and gradient descent in the next lecture notebook, let's have a look at the dimensions of the vectors you've been handling until now.

Create a vector of length $V$ filled with zeros.

```python
# Define V. Remember this was the size of the vocabulary in the previous lecture notebook
V = 5

# Define vector of length V filled with zeros
x_array = np.zeros(V)

# Print vector
x_array
```

    array([0., 0., 0., 0., 0.])

This is a 1-dimensional array, as revealed by the `.shape` property of the array.

```python
# Print vector's shape
x_array.shape
```

    (5,)

To perform matrix multiplication in the next steps, you actually need your column vectors to be represented as a matrix with one column. In numpy, this matrix is represented as a 2-dimensional array.

The easiest way to convert a 1D vector to a 2D column matrix is to set its `.shape` property to the number of rows and one column, as shown in the next cell.

```python
# Copy vector
x_column_vector = x_array.copy()

# Reshape copy of vector
x_column_vector.shape = (V, 1)

# Print vector
x_column_vector
```

    array([[0.],
           [0.],
           [0.],
           [0.],
           [0.]])

The shape of the resulting "vector" is:

```python
# Print vector's shape
x_column_vector.shape
```

    (5, 1)

So you now have a 5x1 matrix that you can use to perform standard matrix multiplication.

```python
x_expand_dim_column_vec = np.expand_dims(x_array, axis=1)
print(x_expand_dim_column_vec)
print(x_expand_dim_column_vec.shape)
```

    [[0.]
     [0.]
     [0.]
     [0.]
     [0.]]
    (5, 1)

```python
x_array.shape
```

    (5,)

**Congratulations on finishing this lecture notebook!** Hopefully you now have a better understanding of the activation functions used in the continuous bag-of-words model, as well as a clearer idea of how to leverage Numpy's power for these types of mathematical computations.

In the next lecture notebook you will get a comprehensive dive into:

- Forward propagation.

- Cross-entropy loss.

- Backpropagation.

- Gradient descent.

**See you next time!**


```python
%matplotlib inline

import numpy as np
from math import pi, sqrt
import matplotlib.pyplot as plt
from scipy.optimize import brentq
from scipy.interpolate import Akima1DInterpolator
from ipywidgets import interact

plt.style.use('ggplot')
plt.rcParams['font.size'] = 16.0
```

# Converging Diverging Nozzle

We will assume a calorically perfect, ideal gas.  We will also not include friction or heat transfer.  Thus, we will assume adiabatic flow.

## Getting Started
Let's first set the specific heat ratio
$$\gamma = \frac{c_p}{c_v}$$
We will set a default of 1.4 for air, and define some other variables for convenience.

```python
g = 1.4
gp = g+1
gm = g-1
```

For convenience, we will define the function:
$$f(M) = M \left[ \frac{2}{\gamma + 1}\left( 1 + \frac{\gamma - 1}{2}M^2 \right)\right]^{\frac{-(\gamma+1)}{2(\gamma-1)}}$$
This is used in the area Mach relationship:
$$\frac{A^*}{A} = f(M)$$
Rather than working with the less physically relevant parameter $A^*$, we often replace it with $f(M) A $.  It is important to remember that $f(1) = 1$ as should be clear from its definition.

Our most important expression is conservation of mass.  Using the $f(M)$ function, conservation of mass can be expressed as:
$$\left( \frac{P_0 A f(M)}{\sqrt{T_0}}\right)_1 = \left( \frac{P_0 A f(M)}{\sqrt{T_0}}\right)_2$$
Because we are only concerend with adiabatic flows in this exercise, this is simplified to
$$( P_0 A f(M))_1 = ( P_0 A f(M))_2$$
for regions of isentropic flow this simplifes further to
$$A_1 f(M_1) = A_2 f(M_2)$$
Let's encapsulate $f(M)$ as a function so that we can reuse it for a given Mach number.

```python
def fM(M):
    return M*(2.0/gp*(1 + gm/2.0*M**2))**(-gp/2.0/gm)
```

The equation cannot be analytic inverted.  Thus, if we know the area ratio $A^*/A$ solving for the corresponding Mach number requires a root finding approach:
$$froot(M) = \frac{A^*}{A} - f(M) = 0$$
Note that there are two solutions: a subsonic or a supersonic one and so we will also need to know which branch is sought.  We use brent's method for root finding.

```python
def areaMach(Astar_A, speed):
    """returns Mach number associated with isentropic flow for a given area ratio and flow regime"""

    if speed=='subsonic':
        return brentq(lambda M: Astar_A - fM(M), 1e-6, 1.0)

    elif speed=='supersonic':
        return brentq(lambda M: Astar_A - fM(M), 1.0, 10.0)
```

We will also make heavy use of the isentropic relationships:
$$\frac{T_0}{T} = 1 + \frac{\gamma - 1}{2}M^2 $$
$$\frac{P_0}{P} = \left(\frac{T_0}{T}\right)^{\gamma/(\gamma-1)} $$
$$\frac{\rho_0}{\rho} = \left(\frac{T_0}{T}\right)^{1/(\gamma-1)} $$

```python
def isenT(M):
    return 1 + gm/2.0*M**2

def isenP(M):
    return isenT(M)**(g/gm)

def isenrho(M):
    return isenT(M)**(1.0/gm)
```

These expressions can also be explictly inverted in order to solve for the Mach number given a known pressure, temperature, or density ratio.  We will only need the inverse pressure expression in this exercise.
$$ M = \sqrt{\frac{2}{\gamma -1} \left[\left(\frac{P_0}{P}\right)^{(\gamma-1)/\gamma} -1 \right]}$$

```python
def isenM(P0_P):
    return sqrt(2.0/gm*(P0_P**(gm/g) - 1))
```

## Geometry

This is enough to get us started.  First we need to define the geometry that we want to solve.  We will define the radius at 5 locations along a unit length and fit an Akima spline in between.  The radii can be adjusted, but note that this simulation assumes a converging diverging nozzle so if you setup a different type of geometry the simulation may crash or the results will be nonsensical.

```python
xpts = np.array([0, 0.25, 0.5, 0.75, 1.0])
rpts = np.array([0.35, 0.15, 0.20, 0.27, 0.3])
akima = Akima1DInterpolator(xpts, rpts)

n = 200
x = np.linspace(xpts[0], xpts[-1], n)
r = akima(x)

plt.figure()
plt.plot(x, r)
plt.plot(x, -r)
plt.axis('equal')
plt.text(1.15, 0, '$P_b$')
plt.text(-0.4, 0, '$P_0$ (total)')
plt.show()
```

With the geometry, defined we can now define some important quantities.

```python
# area distribution
A = pi*r**2

# exit area
Ae = A[-1]

# area of throat, and corresponding index
ithroat = np.argmin(A)
Athroat = A[ithroat]
```

## Cases
There are a few discrete cases that each need to be considered separately, and conditions defined for when they occur: fully subsonic flow, standing shock wave, isentropic supersonic flow.  The parameter we will use that dictates the nozzle behavior is the ratio of the back pressure (downstream of the exit) to the total pressure (upstream of the entrance) $P_b/P_0$.  Note that the back pressure is not necessarily equal to the exit pressure (it is only equal if the exit is subsonic).  We will only focus on flow in the nozzle and thus, wont discuss the under- and overexpanded cases. When $P_b/P_0 = 1$ there is no flow.  As we lower the back pressure we will move through the various cases.

## Case I: fully subsonic
If the flow is everywhere subsonic, then under our assumptions it is also isentropic throughout.  As the back pressure is lowered, the flow will accelerate, but remain subsonic, until the limiting where the throat just reaches sonic conditions.  From a mass balance (isentropic) this limiting cases occurs at:
$$\begin{align}
A_{throat} &= A_e f(M_e)\\
\Rightarrow f(M_e) &= A_{throat}/A_e
\end{align}$$
We can now find the critical exit Mach number at which the throat just barely reaches sonic conditions and we still have subsonic flow throughout.  If the exit Mach number was any higher, we would no longer have fully subsonic flow.

```python
# critical Mach number for subsonic flow throughout with sonic flow at throat
Me_crit_sub = areaMach(Athroat/Ae, 'subsonic')
```

If subsonic at the exit, the back pressure matches the exit pressure, and because the flow is isentropic the total pressure is constant throughout the nozzle.
$$\frac{P_b}{P_0} = \frac{P_e}{P_0}$$
We already know the critical exit Mach number and using the isentropic relationship, we can compute the corresponding critical pressure ratio for subsonic flow throughout.  As long as the pressure ratio $P_b/P_0$ is above this critical value, we will have subsonic flow throughout.

```python
# corresponding critical pressure ratio
Pb_P0_crit_sub = 1.0/isenP(Me_crit_sub)
```

So if $$\frac{P_b}{P_0} > \left(\frac{P_b}{P_0}\right)_{crit-sub}$$ we will have subsonic flow through.
We can evaluate the Mach number everywhere from the (isentropic) mass balance
$$\begin{align}
A\ f(M) &= A_e f(M_e)\\
f(M) &= \frac{A_e}{A} f(M_e)
\end{align}$$
where the Mach number at the exit is given by the isentropic relationship

```python
Pb_P0 = 0.99
M = np.zeros(n)

if Pb_P0 > Pb_P0_crit_sub:  # subsonic
    # back pressure equals exit pressure b.c. subsonic
    P0_Pe = 1.0/Pb_P0

    # find exit Mach number from isentropic relationship
    Me = isenM(P0_Pe)

    # precompute f(Me) so we don't have to keep computing in the loop
    fMe = fM(Me)

    # iterate across all areas solving for the subsonic Mach number
    for i in range(n):
        M[i] = areaMach(Ae/A[i]*fMe, 'subsonic')

    # compute pressure distribution
    P_P0 = 1.0/isenP(M)

plt.figure()
plt.plot(x, M)
plt.ylabel('M')

plt.figure()
plt.plot(x, P_P0)
plt.ylabel('$P/P_0$')

# plt.figure()
# plt.plot(x, T_T0)
# plt.ylabel('$T/T_0$')

# plt.figure()
# plt.plot(x, rho_rho0)
# plt.ylabel('$\\rho/\\rho_0$')

plt.show()
```

## Case II: normal shock
As the back pressure decreases, the throat becomes chocked.  However, the back pressure is not yet low enough to permit isentropic supersonic flow through the back end of the nozzle and show a shock wave must be formed.  As the back pressure is decreased the shock wave moves further back along the nozzle until it is right at the exit. As the back pressure is decreased further we have supersonic flow throughout the back end of the nozzle. We must determine the critical pressure ratio that separates the case with a shock wave from the case with isentropic supersonic flow.

Computing this critical case is a little more work.  The limiting case has a shock wave right at the exit.  Thus, we need to find the Mach number behind a normal shock with isentropic supersonic flow all the way up to the exit.  From the normal shock equations we can compute the Mach number behind the shock wave as:
$$M_2 = \sqrt{\frac{2 + (\gamma-1)M_1^2}{2\gamma M_1^2 - (\gamma-1)}}$$

```python
def shockM(M1):
    """computes the mach number behind a normal shock"""
    return sqrt((2 + gm*M1**2)/(2*g*M1**2 - gm))
```

Thus, with a shock wave standing right at the exit the Mach number right behind the shock is:

```python
# Mach number ahead of shock right before exit
Me_up = areaMach(Athroat/Ae, 'supersonic')

# Mach number at exit (downstream of shock)
Me = shockM(Me_up)
```

We now need to determine the total pressure loss across the shock wave, so we can figure out the pressure ratio across the nozzle.  Using a mass balance from the throat to exit:
$$
\begin{align}
( P_0 A f(M))_t &= ( P_0 A f(M))_e\\
{P_0}_t A_t  &= {P_0}_e A_e f(M_e)\\
\Rightarrow \frac{{P_0}_e}{{P_0}_t} f(M_e) &= \frac{A_t}{A_e}\\
\frac{{P_0}_e}{P_e}\frac{P_e}{{P_0}_t} f(M_e)&=\frac{A_t}{A_e}\\
\Rightarrow \frac{{P_0}_t}{P_e} &=\frac{{P_0}_e}{P_e} f(M_e)\frac{A_e}{A_t}\\
\end{align}
$$
Because there is a shock wave, the exit pressure is again equal to the back pressure ($P_e = P_b$).  Also, because there is no shock wave between the throat at the entrance, $P_0 = {P_0}_t$.  Thus, the critcal pressure ratio is:
$$\frac{P_b}{P_0} =\frac{P_e}{{P_0}_e} \frac{1}{f(M_e)}\frac{A_t}{A_e}$$

```python
# corresponding critical pressure ratio
Pb_P0_crit_super = Athroat/(isenP(Me)*fM(Me)*Ae)
```

As long as the pressure is above this critical value, a shock wave will exist in the throat.  This case is a bit more difficult if we want to find the location of the shock.  First we must solve for the exit Mach number using the above mass balance.  However, there is no explicit solution so we must again use a root finding technique:
$$froot(M_e) = f(M_e) \frac{{P_0}_e}{P_e} - \frac{P_0}{P_b}\frac{A_t}{A_e} = 0$$

Once we know the exit Mach number we can find the total pressure loss across the shock using the equation above and repeated here:
$$\frac{{P_0}_e}{{P_0}_t}  = \frac{A_t}{A_e}\frac{1}{f(M_e)}$$

If we know the total pressure loss across the shock, then we know the upstream Mach number from the normal Mach equations (and also the downstream Mach number).
$$\frac{{P_0}_2}{{P_0}_1} = \left(\frac{\gamma +1}{2 \gamma M_1^2 - (\gamma - 1)}\right)^{\frac{1}{\gamma-1}}   \left( \frac{(\gamma + 1) M_1^2}{2 + (\gamma - 1) M_1^2}\right)^{\frac{\gamma}{\gamma-1}}$$
To solve this for $M_1$ requires a root finding method

Using conservation of mass from just behind the shock to the exit (isentropic) allows us to compute the area of where the shock sits:
$$\begin{align}
A_s f(M_s) &= A_e f(M_e) \\
A_s  &= A_e \frac{f(M_e)}{f(M_s)}
\end{align}$$

With a known shock location we can use a mass balance from the entrance to just before the shock (subsonic solution up to throat, supersonic solution after throat).
$$\begin{align}
A f(M) &= A_t \\
f(M) &= \frac{A_t}{A}
\end{align}$$
and after the shock:
$$\begin{align}
A f(M) &= A_e f(M_e) \\
f(M) &= \frac{A_e}{A} f(M_e)
\end{align}$$

```python
Pb_P0 = 0.5

if Pb_P0 > Pb_P0_crit_super:  # shock in nozzle

    # exit Mach number
    Me = brentq(lambda M: fM(M)*isenP(M) - Athroat/Ae/Pb_P0, 1e-6, 1.0)

    # total pressure loss across shock
    P0e_P0t = Athroat/Ae/fM(Me)

    # Mach number ahead of shock
    Mup = brentq(lambda M: P0e_P0t - (gp/(2*g*M**2 - gm))**(1.0/gm)*(gp*M**2/(2 + gm*M**2))**(g/gm), 1.0, 10.0)

    # Mach number after shock
    Mdown = shockM(Mup)

    # area at shock location
    As = Ae*fM(Me)/fM(Mdown)

    # find closest corresponding index of shock (must be aft of throat)
    ishock = np.argwhere(A[ithroat:] >= As)[0][0]
    ishock += ithroat

    # solution up to shock
    for i in range(ithroat):
        M[i] = areaMach(Athroat/A[i], 'subsonic')
    for i in range(ithroat, ishock):
        M[i] = areaMach(Athroat/A[i], 'supersonic')

    # solution after shock
    fMe = fM(Me)
    for i in range(ishock, n):
        M[i] = areaMach(Ae/A[i]*fMe, 'subsonic')

    # isentropic pressure
    P_P0 = 1.0/isenP(M)

    # add additional pressure loss of shock
    P_P0[ishock:] *= P0e_P0t

plt.figure()
plt.plot(x, M)
plt.ylabel('M')

plt.figure()
plt.plot(x, P_P0)
plt.ylabel('$P/P_0$')

plt.show()
```

## Case III: isentropic supersonic flow

The last case is relatively simple.  Choked flow with isentropic flow aft of the nozzle exit.  The exit Mach number is supersonic and thus the exit mach number and the back pressure Mach number need not agree.  When the back pressure is reduced below the previous case, but is still larger than the exit pressure the nozzle is overexpanded and oblique shocks wil exist outside of the nozzle to converge the flow and increase the pressure to match the back pressure.

Conversely, if the back pressure is reduced below the exit pressure then the nozzle is underexpanded and expansion waves will exist outside of the nozzle to expand the flow and decrease the pressure to match the back pressure.

If they back pressure and the exit pressure match perfectly, then the nozzle is ideally expanded.

```python
Pb_P0 = 0.2

for i in range(ithroat):
    M[i] = areaMach(Athroat/A[i], 'subsonic')
for i in range(ithroat, n):
    M[i] = areaMach(Athroat/A[i], 'supersonic')

P_P0 = 1.0/isenP(M)

Pe_P0 = P_P0[-1]

if Pb_P0 > Pe_P0:
    print 'overexpanded'
elif Pb_P0 == Pe_P0:
    print 'ideally expanded'
else:
    print 'underexpanded'

plt.figure()
plt.plot(x, M)
plt.ylabel('M')

plt.figure()
plt.plot(x, P_P0)
plt.ylabel('$P/P_0$')

plt.show()

```

## Interactive

Let's now combine all the cases into one simulation

```python
# an interactive version where you can change the back pressure
# this will not run on nbviewer.  You can view it locally if
# you've installed Jupyter notebook or upload to http://try.jupyter.org

@interact(Pb_P0=(0, 0.995, 0.005))
def nozzle(Pb_P0=0.995):

    M = np.zeros(n)

    if Pb_P0 > Pb_P0_crit_sub:  # subsonic

        # back pressure equals exit pressure b.c. subsonic
        P0_Pe = 1.0/Pb_P0

        # find exit Mach number from isentropic relationship
        Me = isenM(P0_Pe)

        # precompute f(Me) so we don't have to keep computing in the loop
        fMe = fM(Me)

        # iterate across all areas solving for the subsonic Mach number
        for i in range(n):
            M[i] = areaMach(Ae/A[i]*fMe, 'subsonic')

        # compute pressure distribution
        P_P0 = 1.0/isenP(M)

    elif Pb_P0 > Pb_P0_crit_super:  # shock in nozzle

        # exit Mach number
        Me = brentq(lambda M: fM(M)*isenP(M) - Athroat/Ae/Pb_P0, 1e-6, 1.0)

        # total pressure loss across shock
        P0e_P0t = Athroat/Ae/fM(Me)

        # Mach number ahead of shock
        Mup = brentq(lambda M: P0e_P0t - (gp/(2*g*M**2 - gm))**(1.0/gm)*(gp*M**2/(2 + gm*M**2))**(g/gm), 1.0, 10.0)

        # Mach number after shock
        Mdown = shockM(Mup)

        # area at shock location
        As = Ae*fM(Me)/fM(Mdown)

        # find closest corresponding index of shock (must be aft of throat)
        ishock = np.argwhere(A[ithroat:] >= As)[0][0]
        ishock += ithroat

        # solution up to shock
        for i in range(ithroat):
            M[i] = areaMach(Athroat/A[i], 'subsonic')
        for i in range(ithroat, ishock):
            M[i] = areaMach(Athroat/A[i], 'supersonic')

        # solution after shock
        fMe = fM(Me)
        for i in range(ishock, n):
            M[i] = areaMach(Ae/A[i]*fMe, 'subsonic')

        # isentropic pressure
        P_P0 = 1.0/isenP(M)

        # add additional pressure loss of shock
        P_P0[ishock:] *= P0e_P0t

    else:

        for i in range(ithroat):
            M[i] = areaMach(Athroat/A[i], 'subsonic')
        for i in range(ithroat, n):
            M[i] = areaMach(Athroat/A[i], 'supersonic')

        P_P0 = 1.0/isenP(M)

        Pe_P0 = P_P0[-1]

#         if Pb_P0 > Pe_P0:
#             print 'overexpanded'
#         elif Pb_P0 == Pe_P0:
#             print 'ideally expanded'
#         else:
#             print 'underexpanded'

    plt.figure(figsize=(12,4))
    plt.subplot(1,2,1)
    plt.plot(x, M)
    plt.ylim([0, 3])
    plt.ylabel('M')

    plt.subplot(1,2,2)
    plt.plot(x, P_P0)
    plt.plot([0.9, 1.0], [Pb_P0, Pb_P0], '--')
    plt.ylim([0, 1])
    plt.ylabel('$P/P_0$')

    plt.show()

```

```python

```


# Python を使ってネットワーク科学の問題を解決しよう
Chapter 1. 最適化問題とPython プログラミングの初歩

----

明治大学理工学部情報科学科
飯塚 秀明

## ベクトル (ndarray)
Python でベクトル(数の並び) を表すには、`np.array` 関数を用います。

例えば、3 要素のベクトル
\begin{align}
  \begin{pmatrix}1\\ 2\\ 3\end{pmatrix}
\end{align}
を表現するには、
```python
import numpy as np
np.array([1, 2, 3])
```
とします。

ここで、`import numpy as np` という文は、「ベクトルなどを表現するための機能NumPy を`np` という名前で読み込んでください」という意味で、`np.array` 関数などを使用する前に**あらかじめ１回**実行しておく必要があります。
次の行、`np.array([1, 2, 3])` が、実際にベクトルを生成する命令です。
それぞれの注ぎ口の実行結果は最後の行を計算した値が表示されるので、この場合は、
```python
array([1, 2, 3])
```
と表示されれば成功です。
少し変な表記に思われるかもしれませんが、これが先のベクトルを表すPython 上での表現になります。

**【課題】下の注ぎ口に、3要素のベクトル$(1, 2, 3)^\top$ を生成するPython プログラムを書いて実行してみましょう。**

```python

```

## ベクトルの足し算、引き算、スカラー(実数) 倍
同じ要素数のベクトル同士は、足したり引いたりすることができます。
その場合、下記のように成分同士の足し算、引き算となります。
\begin{align}
\begin{pmatrix}a\\ b\end{pmatrix}+\begin{pmatrix}c\\ d\end{pmatrix}
:=\begin{pmatrix}a+c\\ b+d\end{pmatrix},\quad
\begin{pmatrix}a\\ b\end{pmatrix}-\begin{pmatrix}c\\ d\end{pmatrix}
:=\begin{pmatrix}a-c\\ b-d\end{pmatrix}.
\end{align}
足し算や引き算は、普通の数学表記と同様に`+` や`-` 記号を用いて記述できます。
例えば、２要素のベクトル$(5, 3)^\top$ と$(1, 2)^\top$ の足し算は、
```python
import numpy as np
x = np.array([5, 3])
y = np.array([1, 2])
x + y
```
と記述でき、その実行結果は、
```python
array([6, 5])
```
となります。

ベクトルはスカラー(実数) 倍することもでき、その場合、下記のように各要素に実数を乗ずることになります。
\begin{align}
c\begin{pmatrix}a\\ b\end{pmatrix}:=\begin{pmatrix}ca\\ cb\end{pmatrix}
\end{align}
ベクトルの実数倍には、`*` 記号を用います。
例えば、２要素のベクトル$(5, 3)^\top$ を2 倍するには下記のように記述します。
```python
import numpy as np
x = np.array([5, 3])
2 * x
```

**【課題】下の３つの注ぎ口で、ベクトルの足し算、引き算、スカラー(実数) 倍の計算を確認してみましょう。**

```python

```

```python

```

```python

```

## ベクトルの各成分
ベクトル`v` の各成分の値は、`v[番号]` で取得・設定できます。
ここで、`番号` は0 から始まることに注意してください。

**【課題】下の２つの注ぎ口にあるプログラムについて、それぞれ実行結果を予想してみましょう。予想できたら実行して、予想と実際が同じかどうかを確認してください。**

```python
import numpy as np
x = np.array([1, 2, 3])

x[0]
```

```python
import numpy as np
x = np.array([1, 2, 3])
x[0] = 0

x
```

## ベクトル同士の内積
同じ要素数のベクトル同士の内積、
\begin{align}
\left\langle \begin{pmatrix}a\\ b\end{pmatrix}, \begin{pmatrix}c\\ d\end{pmatrix} \right\rangle
:= ac + bd
\end{align}
は、`np.inner` 関数により計算できます。

**【課題】下の注ぎ口にあるプログラムについて、実行結果を予想してみましょう。予想できたら実行して、予想と実際が同じかどうかを確認してください。**

```python
import numpy as np
x = np.array([5, 3])
y = np.array([1, 2])

np.inner(x, y)
```

## 行列
行列(数の２次元的な並び) も、ベクトル同様に`np.array` 関数を用いて生成できます。
次の行列、
\begin{align}
  \begin{pmatrix}
    1 & 2 \\
    3 & 4 \\
  \end{pmatrix}
\end{align}
は、次の注ぎ口にあるプログラムのように記述します。

```python
import numpy as np

np.array([[1, 2], [3, 4]])
```

## 行列の各成分
行列`A` の各成分の値は、`A[行番号, 列番号]` で取得・設定できます。
ベクトルの各成分のときと同様に、`行番号`や`列番号` は0 から始まることに注意してください。

**【課題】下の２つの注ぎ口にあるプログラムについて、それぞれ実行結果を予想してみましょう。予想できたら実行して、予想と実際が同じかどうかを確認してください。**

```python
import numpy as np
A = np.array([[1, 2], [3, 4]])

A[0, 0]
```

```python
import numpy as np
A = np.array([[1, 2], [3, 4]])
A[0, 0] = 5

A
```

## 行列とベクトルの積
行列とベクトルの積、
\begin{align}
  \begin{pmatrix}a & b\\ c & d\end{pmatrix}\begin{pmatrix}e\\ f\end{pmatrix}
  := \begin{pmatrix}ae + bf\\ ce + df\end{pmatrix}
\end{align}
は、`np.dot` 関数を用いて計算します。

**【課題】下の注ぎ口を実行して、計算結果を確認してみましょう。**

```python
import numpy as np
A = np.array([[1, 2], [3, 4]])
x = np.array([5, 6])

np.dot(A, x)
```

## 連立一次方程式
連立一次方程式
\begin{align}
  \begin{cases}
    ax + by = c \\
    dx + ey = f
  \end{cases}
\end{align}
は、行列とベクトルを用いて、
\begin{align}
  \begin{pmatrix}
    a & b \\
    d & e
  \end{pmatrix}
  \begin{pmatrix}x\\ y\end{pmatrix}
  =\begin{pmatrix}c\\ f\end{pmatrix}
\end{align}
とも書けます。

この行列$\begin{pmatrix}a & b\\ d & e\end{pmatrix}$ とベクトル$\begin{pmatrix}c\\ f\end{pmatrix}$ を与えて、解$\begin{pmatrix}x\\ y\end{pmatrix}$ を求めるには、`np.linalg.solve` 関数を用います。

**【課題】下の注ぎ口のプログラムを穴埋めして、はじめの問題を解いてみましょう。**

| 食品＼含まれているもの | ビタミン (mg) | ミネラル (mg) |
| ---- | ---- | ---- |
| A | 0.10 | 4.5 |
| B | 0.15 | 3.0 |

条件: ビタミンは合わせて3mg 以上、ミネラルは合わせて90mg 以上とる。合計量はなるべく少なくしたい。

```python
import numpy as np
A = np.array([[?, ?], [?, ?]])
x = np.array([?, ?])

np.linalg.solve(A, x)
```