# Pricing a Vanilla European Call Option

```python
#Import Libraries
import numpy as np
import pandas as pd
from scipy import stats
from scipy.stats import norm
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.ticker as ticker
from matplotlib.axis import Axis
from scipy.stats import ncx2
```

Abstract

In this paper, we price a vanilla European call option under the Heston model and then simulate the monthly share price over a year using the Constant Elasticity of Variance (CEV) model, with the assumption of constant volatility each month. Monte Carlo simulations with varying sample sizes are run and the results are plotted against the closed form value for com- parison.

#### Keyboard:
European Options, Heston model, Constant Elasticity of Variance (CEV) model and Stochastic Volatility.

##  Introduction

In this paper we go beyond the constant volatility assumption in the Black-Scholes model. Most of code implemented in this submission is derived from Module 5 [7] of the course.
We initialize most variables as given by the question.

- Maturity = 1 year
- Strike price as 100\$
- $S_0=100$ share price is \$100
- $r=8%$  (compound interest rate risk free)
- $\sigma=30%$ as volatility

```python
#Share Specific Information
r = 0.08 #risk-free continuously compounded interest rate
S0 = 100 #current share price
sigma = 0.3

#Call Specific Information
K = 100
T = 1
k_log = np.log(K)
```

## Part 1: Fourier pricing

```python

```

Black-Scholes option pricing models assumed volatility of the underlying assets to be constant or a predetermined function of time, we will now implement a model which features instantaneous variance of asset price using volatility that evolves stochastically in time.  Although there are several models incorporating stochastic volatility including introduced by Hull and White [1], Stein and Stein [2] and  Heston [3] [4],  we will implement Heston’s constant interest rate model.

With the assumption that the underlying share follows the Heston model dynamics, the additional parameters required are specified as follows:

```python
#Share Specific Information
v0 = 0.06
kappa = 9
theta = 0.06
sigma = 0.3
rho = -0.4

#Approximation Information
t_max = 30
N = 100
```

The Characteristic function is implemented using a function presented by Albrecher et al [5].  The function is written as:
$$\phi_{S_T} = exp(C(\tau;u)+D(\tau;u)v_t + iu \log(S_t) )$$

Where,

$$C(\tau;u) = ri\tau u + \theta \kappa [\tau x\_ - \frac{1}{a}\log(\frac{1-ge^{d\tau}}{1-g})],$$

$$D(\tau;u) = (\frac{1-e^{d\tau}}{1-ge^{d\tau}})x\_,$$

\begin{equation}
  \begin{split}
    \tau = T -t \\
    g = \frac{x\_}{x_+}\\
    x_\pm = \frac{b\pm d}{2a}
  \end{split}
\quad\quad
  \begin{split}
    d = \sqrt{b^2 - 4ac}\\
    c = - \frac{u^2 + Ui}{2} \\
    b = \kappa - \rho \sigma iu \\
    a = \frac {\sigma^2}{2} \\
  \end{split}
\end{equation}

```python
#Characteristic function code
a = sigma**2/2

def c(u):
    return -(u**2+1j*u)/2

def d(u):
    return np.sqrt(b(u)**2-4*a*c(u))

def b(u):
    return kappa - rho*sigma*1j*u

def g(u):
    return xminus(u)/xplus(u)

def C(u):
    val1 = T*xminus(u)-np.log((1-g(u)*np.exp(-T*d(u)))/(1-g(u)))/a
    return r*T*1j*u + theta*kappa*val1

def D(u):
    val1 = 1-np.exp(-T*d(u))
    val2 = 1-g(u)*np.exp(-T*d(u))
    return (val1/val2)*xminus(u)

def log_char(u):
    return np.exp(C(u) + D(u)*v0 + 1j*u*np.log(S0))

def xminus(u):
    return (b(u)-d(u))/(2*a)

def xplus(u):
    return (b(u)+d(u))/(2*a)

def adj_char(u):
    return log_char(u-1j)/log_char(-1j)
```

Now we vectorize the code, calculate an estimate for integrals and calculate the Fourier estimate of our call price.

```python
delta_t = t_max/N
from_1_to_N = np.linspace(1,N,N)
t_n = (from_1_to_N-1/2)*delta_t

first_integral = sum((((np.exp(-1j*t_n*k_log)*adj_char(t_n)).imag)/t_n)*delta_t)
second_integral = sum((((np.exp(-1j*t_n*k_log)*log_char(t_n)).imag)/t_n)*delta_t)

fourier_call_val = S0*(1/2 + first_integral/np.pi)-np.exp(-r*T)*K*(1/2 + second_integral/np.pi)
fourier_call_val
```

    13.734895692109049

To see the effectiveness of the pricing option under Heston dynamics we will also price the call option under Black-Scholes assumption.

```python
d_1_stock = (np.log(S0/K)+(r + sigma**2/2)*(T))/(sigma*np.sqrt(T))
d_2_stock = d_1_stock - sigma*np.sqrt(T)

analytic_callprice = S0*norm.cdf(d_1_stock)-K*np.exp(-r*(T))*norm.cdf(d_2_stock)
analytic_callprice
```

    15.711312547892973

## Part 2: CEV Simulations

We use the following formula to simulate share price walk :
$$S_{t_{i+1}} = S_{t_i}e^{(r - \frac{\sigma^2 (t_i,t_{i+1})}{2})(t_{i+1}-t_i)+\sigma(t_i, t_{i+1})\sqrt{t_{i+1}-t_i}Z}$$
where $S_{ti}$ is the share price at time $t_i$, $\sigma(t_i, t_{i+1})$ is the volatility for the period $[t_i,t_{i+1}]$, r is the risk-free interest rate, and $Z \sim N(0,1)$

For estimations we will use sample size from 1000 upto 50,000 for monthly simulation over 1 year. More over
we define $\sigma(t_i,t_{i+1}) = \sigma(S_{ti})^{\gamma-1}$,
with $\sigma = 0.3$ and $\gamma = 0.75$.

We are attempting to run simulations using the CEV model. However, while the CEV model assumes that volatility is a continuous function of time and share price, we are making a simplifying assumption that volatility is constant over each simulation period.

Cox [6] developed the constant elasticity of variance (CEV) option pricing model, it attempts to capture stochastic volatility and is given by:

$dS_t = \mu S_t dt + \sigma {S_t}^{\gamma} dW_t$

If $\gamma = 1$ this model return the same value as Black-Scholes model, however if the value of $\gamma < 1$ we experience an effect called leverage effect where the volatility increases as the price decreases over subsequent time periods.

```python
#from question
sigma_const = 0.30
gamma = 0.75

#assumption
r = 0.08
S0 = 100
T = 1
sample_sizes = range(1000, 50001, 1000)

timesteps = 12
```

First, we define our helper functions. The next_share_price function is used to calculate the evolution of the share price at $t+1$, from the share price at t. We generate the random variable $Z$ from within this function. The effective sigma is also written as a function of the share price at $t$.

Just for exploration purposes, we also added a varying_vol flag, to allow us to switch between a constant volatility and varying volatility. We use the initial stock price instead of the previous price when assuming a contant volatility.

The other function is the generate_share_price_path function. We first create an empty numpy array of shape (sample_size x timesteps+1). Note the addition of one element to the timestep, as the first element is equal to the initial stock price. We then iterate through each path, and each timestep, applying the next_share_price function against the previous share price. We then convert the result into a pandas DataFrame and return the results.

```python
def next_share_price(prev_price, r, dT, sigma_const, gamma, sample_size, varying_vol = True):
    Z = stats.norm.rvs(size=sample_size)
    if varying_vol:
        sigma = sigma_const*(prev_price)**(gamma-1)
    else:
        sigma = sigma_const*(S0)**(gamma-1)

    return prev_price*np.exp((r-(sigma**2)/2)*(dT)+(sigma)*(np.sqrt(dT))*Z)

def generate_share_price_path(S0, r, T, sigma_const, gamma, sample_size, timesteps, varying_vol = True):
    df = pd.DataFrame([S0]*sample_size)
    for t in range(1, timesteps+1):
        df[t] = next_share_price(df[t-1], r, 1/timesteps, sigma_const, gamma, sample_size, varying_vol)
    return df.T
```

```python
T = 10
sample_size = 20

share_price_path_cev = generate_share_price_path(S0, r, T, sigma_const, gamma, sample_size, timesteps)
share_price_path_black_scholes = \
  generate_share_price_path(S0, r, T, sigma_const, 1.0, sample_size, timesteps, varying_vol=False)

red_patch = mpatches.Patch(color='red', label='CEV')
blue_patch = mpatches.Patch(color='blue', label='BSM')

fig, ax = plt.subplots(figsize=(12,4))
ax.plot(share_price_path_cev, color='red')
ax.plot(share_price_path_black_scholes, color='blue')
plt.xlabel("Timestep")
plt.ylabel("Share price")
plt.legend(handles=[red_patch, blue_patch], loc='upper left')
plt.title("CEV vs BSM Stock Price Paths")
plt.show()
```

Next, we create a python dictionary called share_price_paths, to hold our results for part 2. The key of this dictionary would be the number of sample price paths, while the values would be the dataframes containing the price paths. We also track the rough processing time required at each step, by printing the time when each iteration completes.

```python
import time

T = 1
sample_sizes = range(1000, 50001, 1000)

share_price_paths = {}

print("Start generating share price path")
start = time.time()
for sample_size in sample_sizes:
    share_val = generate_share_price_path(S0, r, T, sigma_const, gamma, sample_size, timesteps, varying_vol=False)

    share_price_paths[sample_size] = share_val
    #print("Updated for sample size {} at {}".format(sample_size, datetime.datetime.now().strftime('%H:%M')))
end = time.time()
print(f"Generating all samples paths takes {(end - start):.2f}s")
```

    Start generating share price path
    Generating all samples paths takes 1.59s

To display our output, we show the first 10 price paths generated by our iteration with 1000 samples:

```python
share_price_paths[1000].iloc[:, 0:10]
```

      0
      1
      2
      3
      4
      5
      6
      7
      8
      9

      0
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000
      100.000000

      1
      100.857562
      100.106398
      103.296220
      103.237244
      102.477151
      100.141983
      99.860355
      99.265029
      100.465829
      96.664721

      2
      102.299627
      102.170988
      103.124669
      106.038341
      102.533543
      101.845595
      100.491371
      99.395467
      105.083317
      92.029417

      3
      101.867161
      93.865529
      101.634586
      104.976242
      108.473032
      103.778385
      101.742397
      94.643315
      102.962963
      94.429574

      4
      102.813675
      91.982826
      105.212492
      102.155602
      106.666274
      101.342648
      102.277561
      97.341252
      100.677285
      100.997206

      5
      100.933753
      96.627653
      106.951672
      104.840510
      107.463153
      101.973944
      98.958936
      98.323329
      101.680525
      102.179669

      6
      101.957430
      95.208273
      104.464336
      106.214328
      107.054801
      106.243952
      100.637030
      103.183938
      104.453154
      107.263225

      7
      104.571916
      92.148076
      102.967291
      106.638492
      104.328094
      110.008954
      97.368919
      107.018006
      102.202276
      102.605055

      8
      106.576893
      90.127079
      104.435163
      106.043822
      103.826505
      111.997720
      100.257464
      106.680848
      98.383468
      103.449951

      9
      107.267472
      95.636057
      108.110705
      108.198575
      104.420906
      117.704747
      99.341943
      108.905238
      102.333938
      103.678585

      10
      115.118830
      97.666874
      111.960859
      110.175120
      103.001066
      114.141261
      105.439317
      110.180501
      102.920079
      104.825189

      11
      117.368677
      97.603712
      115.867144
      106.104115
      105.544988
      112.539562
      107.405076
      107.156807
      100.694101
      102.665990

      12
      121.017598
      97.368054
      113.049343
      103.781586
      100.745486
      112.449897
      109.702714
      109.048143
      105.944799
      100.248490

Next, we plot the price paths for the iteration with 1000 samples,

```python
fig, ax = plt.subplots(figsize=(12,4))

ax.plot(share_price_paths[1000])
plt.xlabel('Timestep')
plt.ylabel('Share price')
plt.title('Sample of 1000 Share Price Paths')
plt.show()
```

## Part 3: Monte-Carlo pricing

Using Monte Carlo, we calculate the price of the vanilla call option as follows:

```python
price_estimate = []
price_std = []

for size in sample_sizes:
    S_Ts = share_price_paths[size].iloc[12, :]
    payoff = np.maximum(S_Ts - K, 0)
    discounted_price = np.exp(-r*T)*payoff
    price_estimate.append(discounted_price.mean())
    price_std.append(discounted_price.std()/np.sqrt(size))
```

```python
print("The price estimated by Monte Carlo when using sample size of 50,000 is : {:.3f}".format(price_estimate[-1]))
```

    The price estimated by Monte Carlo when using sample size of 50,000 is : 8.713

### Comparison with calculation of CEV model using noncentral chi-squared distribution

```python
S0 = 100
sigma = 0.3
gamma = 0.75
r = 0.08
T = 1
```

```python
z = 2 + 1/(1-gamma)
def C(t,K):
    kappa = 2*r/(sigma**2*(1-gamma)*(np.exp(2*r*(1-gamma)*t)-1))
    x = kappa*S0**(2*(1-gamma))*np.exp(2*r*(1-gamma)*t)
    y = kappa*K**(2*(1-gamma))
    return S0*(1-ncx2.cdf(y,z,x))-K*np.exp(-r*t)*ncx2.cdf(x,z-2,y)
```

```python
cev_call_price = C(T, 100)
```

```python
print("The price calculated via CEV model using noncentral chi-squared distribution is : {:.3f}".format(cev_call_price))
```

    The price calculated via CEV model using noncentral chi-squared distribution is : 8.702

## Part 4:  Monte Carlo estimates

We plot the prices of our Monte Carlo estimates against the CEV noncentral chi-squared distribution prices calculated from Part 1 above:

```python
fig, ax = plt.subplots(figsize=(12,4))
ticks_x = ticker.FuncFormatter(lambda x, pos: '{0:g}'.format(x * 1000))

ax.plot([cev_call_price]*50, label='CEV Call Price')
ax.plot(price_estimate, '.', label='Monte Carlo Estimates')
ax.plot(cev_call_price + 3*np.array(price_std), 'r')
ax.plot(cev_call_price - 3*np.array(price_std), 'r')
ax.xaxis.set_major_formatter(ticks_x)
plt.legend()
plt.show()
```

We also plot the prices of our Monte Carlo estimates against the Fourier Call prices calculated from Part 1 above:

```python
fig, ax = plt.subplots(figsize=(12,4))

ax.plot([fourier_call_val]*50, label='Fourier Call Price')
ax.plot(price_estimate, '.', label='Monte Carlo Estimates')
ax.plot(fourier_call_val + 3*np.array(price_std), 'r')
ax.plot(fourier_call_val - 3*np.array(price_std), 'r')
ax.xaxis.set_major_formatter(ticks_x)

plt.legend()
plt.show()
```

We notice that there is a discrepency. Upon further investigation, for the Heston model, ```v_0 = 0.06``` ```(v_0 = stock volatility ^ 0.5)```, so ```stock volatility = .06**0.5= 0.2449```. Note that sigma under Heston model refers to volatility of stock volatility

Under our stock price Monte Carlo calculation, the default ```stock volatility is 0.0948``` (given by  ```0.3*(100)*-.25``` ). Thus its much less than Heston. To have the same initial stock volatility, we find a new value for $\sigma$ by equating $\sigma(S_{ti})^{\gamma-1} = \sqrt{0.06}$, giving us $\sigma = 0.775$.

We then find that our newly calculated Monte Carlo calculated call prices are aligned with the Fourier prices.

```python
sigma_const = 0.775

T = 1
sample_sizes = range(1000, 50001, 1000)

share_price_paths = {}

print("Start generating share price path")
start = time.time()
for sample_size in sample_sizes:
    share_val = generate_share_price_path(S0, r, T, sigma_const, gamma, sample_size, timesteps, varying_vol=False)

    share_price_paths[sample_size] = share_val
    #print("Updated for sample size {} at {}".format(sample_size, datetime.datetime.now().strftime('%H:%M')))
end = time.time()
print(f"Generating all samples paths takes {(end - start):.2f}s")
```

    Start generating share price path
    Generating all samples paths takes 1.63s

```python
price_estimate = []
price_std = []

for size in sample_sizes:
    S_Ts = share_price_paths[size].iloc[12, :]
    payoff = np.maximum(S_Ts - K, 0)
    discounted_price = np.exp(-r*T)*payoff
    price_estimate.append(discounted_price.mean())
    price_std.append(discounted_price.std()/np.sqrt(size))
```

```python
fig, ax = plt.subplots(figsize=(12,4))

ax.plot([fourier_call_val]*50, label='Fourier Call Price')
ax.plot(price_estimate, '.', label='Monte Carlo Estimates')
ax.plot(fourier_call_val + 3*np.array(price_std), 'r')
ax.plot(fourier_call_val - 3*np.array(price_std), 'r')
ax.xaxis.set_major_formatter(ticks_x)
plt.legend()
plt.show()
```

#### Conclusion
A vanilla European call option was priced for a fluctuating volatility condition, using the Heston model. The call price was evaluated as USD 13.73 which was verified to be similar to - but lower than - a constant volatility estimate using the Black-Scholes model at USD 15.71.

Initially the values calculated for the underlying share price did not agree as expected, with the estimates well outside the expected error range. This was because the volatility term was not equivalent in the two different calculation methods. After resolving this, the CEV model calcu- lated a price of USD 8.70 for the underlying share, which agreed closely with the Monte Carlo estimate of USD 8.69. This represents an absolute error of 1c and a relative discrepancy of approximately 0.1

The agreement between the results from various methods, although expected, provides confidence to choose the most suitable method for a situation with the knowledge that the results are accurate within a small margin of error.

#### References

[1] Hull, J. and White, A. (1987). The pricing of options on assets with stochastic volatilities. The journal of finance, 42(2):281–300.

[2] Stein, E. M. and Stein, J. C. (1991). Stock price distributions with stochastic volatility: an analytic approach. Review of financial Studies, 4(4):727–752.

[3] Heston, S. L. (1993). A closed-form solution for options with stochastic volatility with applications to bond and currency options. Review of financial studies, 6(2):327–343.

[4] Heston, S. L. (1997). A simple new formula for options with stochastic volatility.

[5] Albrecher, H., Mayer, P., Schoutens, W. and Tistaert, J. (2007). “The Little Heston Trap”, Wilmott (1): 83–92.

Pricing a Vanilla European Call Option.ipynb


# Shooting Method

Boundary-value problems are also ordinary differential equations—the difference is that our two constraints are at boundaries of the domain, rather than both being at the starting point.

For example, consider the ODE
\begin{equation}
y^{\prime\prime} + xy^{\prime} - xy = 2x
\end{equation}
with the boundary conditions $y(0)=1$ and $y(2)=8$.

The numerical methods we have already discussed (e.g., Forward Euler, Runge-Kutta) require values of $y$ and $y^{\prime}$ at the starting point, $x=0$. So we can't use these directly because we are missing $y^{\prime}(0)$.

But, what if we could *guess* a value for the missing initial condition, then integrate towards the second boundary condition using one of our familiar numerical methods, and then adjust our guess if necessary and repeat? This concept is the **shooting method**.

The shooting method algorithm is:

1. Guess a value of the missing initial condition; in this case, that is $y'(0)$.
2. Integrate the ODE like an initial-value problem, using our existing numerical methods, to get the given boundary condition(s); in this case, that is $y(L)$.
3. Assuming your trial solution for $y(L)$ does not match the given boundary condition, adjust your guess for $y'(0)$ and repeat.

Now, this algorithm will not work particularly well if all your guesses are random/uninformed. Fortunately, we can use linear interpolation to inform a third guess based on two initial attempts:
\begin{align}
\text{guess 3} &= \text{guess 2} + m \left( \text{target} - \text{solution 2} \right) \\
m &= \frac{\text{guess 1} - \text{guess 2}}{\text{solution 1} - \text{solution 2}}
\end{align}
where "target" is the target boundary condition—in this case, $y(L)$.

## Example: linear ODE

Let's try solving the given ODE using the shooting method:
\begin{equation}
y^{\prime\prime} + xy^{\prime} - xy = 2x
\end{equation}
with the boundary conditions $y(0)=1$ and $y(2)=8$.

First, we need to convert this 2nd-order ODE into a system of two 1st-order ODEs, where we can define $u = y'$:
\begin{align}
y' &= u \\
u' &= 2x + xy - xu
\end{align}

```matlab
%%file shooting_rhs.m
function dydx = shooting_rhs(x, y)

dydx = zeros(2,1);
dydx(1) = y(2);
dydx(2) = 2*x - x*y(2) + x*y(1);
```

    Created file '/Users/niemeyek/projects/ME373-book/content/bvps/shooting_rhs.m'.

```matlab
clear all; clc

% target boundary condition
target = 8;

% Pick a guess for y'(0) of 1
guess1 = 1;
[X, Y] = ode45('shooting_rhs', [0 2], [1 guess1]);
solution1= Y(end,1);
fprintf('Solution 1: %5.2f\n', solution1);

% Pick a second guess for y'(0) of 4
guess2 = 4;
[X, Y] = ode45('shooting_rhs', [0 2], [1 guess2]);
solution2 = Y(end,1);
fprintf('Solution 2: %5.2f\n', solution2);

% now use linear interpolation to find a new guess
m = (guess1 - guess2)/(solution1 - solution2);
guess3 = guess2 + m*(target-solution2);
fprintf('Guess 3:    %5.2f\n', guess3);

[X, Y] = ode45('shooting_rhs', [0 2], [1 guess3]);
solution3 = Y(end,1);
fprintf('Solution 3: %5.2f\n', solution3);
fprintf('Target:     %5.2f\n', target);

plot(X, Y(:,1)); axis([0 2 0 9])
```

As you can see, using linear interpolation, we are able to find the correct guess for the missing initial condition $y'(0)$ with in just three steps. This works so well because this is a *linear* ODE. If we had a nonlinear ODE, it would take more tries, as we'll see shortly.

## Example: nonlinear ODE

We can use the shooting method to solve a famous fluids problem: the [Blasius boundary layer](https://en.wikipedia.org/wiki/Blasius_boundary_layer).

:::{figure-md} fig-boundary-layer

Laminar boundary layer, taken from <https://commons.wikimedia.org/wiki/File:Laminar_boundary_layer_scheme.svg>
:::

To get to a solveable ODE, we start with the conservation of momentum equation (i.e., Navier–Stokes equation) in the $x$-direction:
\begin{equation}
u \frac{\partial u}{\partial x} + v \frac{\partial u}{\partial y} = \nu \frac{\partial^2 u}{\partial y^2}
\end{equation}
and the conservation of mass equation:
\begin{equation}
\frac{\partial u}{\partial x} + \frac{\partial v}{\partial y} = 0 \;,
\end{equation}
where $u$ is the velocity component in the $x$-direction, $v$ is the velocity component in the $y$-direction, and $\nu$ is the fluid's kinematic viscosity. The boundary conditions are that $u = v = 0$ at $y=0$, and that $u = U_{\infty}$ as $y \rightarrow \infty$, where $U_{\infty}$ is the free-stream velocity.

Blasius solved this problem by converting the PDE into an ODE, by recognizing that the boundary layer thickness is given by $\delta(x) \sim \sqrt{\frac{x \nu}{U_{\infty}}}$, and then nondimensionalizing the position coordinates using a similarity variable
\begin{equation}
\eta = y \sqrt{\frac{U_{\infty}}{2 \nu x}}
\end{equation}

By introducing the stream function, $\psi (x,y)$, we can ensure the continuity equation is satisfied:
\begin{equation}
u = \frac{\partial \psi}{\partial y} \;, \quad v = -\frac{\partial \psi}{\partial x}
\end{equation}

Let's check this, using SymPy:

```matlab
%%python
import sympy as sym
sym.init_printing()
x, y, u, v = sym.symbols('x y u v')

# Streamfunction
psi = sym.Function(r'psi')(x,y)

# Define u and v based on the streamfunction
u = psi.diff(y)
v = -psi.diff(x)

# Check the continuity equation:
print(u.diff(x) + v.diff(y) == 0)
```

    True

Using the boundary layer thickness and free-stream velocity, we can define the dimensionlesss stream function $f(\eta)$:
\begin{equation}
f(\eta) = \frac{\psi}{U_{\infty}} \sqrt{\frac{U_{\infty}}{2 \nu x}}
\end{equation}
which relates directly to the velocity components:
\begin{align}
u &= \frac{\partial \psi}{\partial y} = \frac{\partial \psi}{\partial f} \frac{\partial f}{\partial \eta} \frac{\partial \eta}{\partial y} \\
 &= U_{\infty} \sqrt{\frac{2 \nu x}{U_{\infty}}} \cdot f^{\prime}(\eta) \cdot \sqrt{\frac{U_{\infty}}{2 \nu x}} \\
u &= U_{\infty} f^{\prime} (\eta) \\
v &= -\frac{\partial \psi}{\partial x} = -\left( \frac{\partial \psi}{\partial x} + \frac{\partial \psi}{\partial \eta} \frac{\partial \eta}{\partial x} \right) \\
 &= \sqrt{\frac{\nu U_{\infty}}{2x}} \left( \eta f^{\prime} - f \right)
\end{align}

We can insert these into the $x$-momentum equation, which leads to an ODE for the dimensionless stream function $f(\eta)$:
\begin{equation}
f^{\prime\prime\prime} + f f^{\prime\prime} = 0 \;,
\end{equation}
with the boundary conditions $f = f^{\prime} = 0$ at $\eta = 0$, and $f^{\prime} = 1$ as $\eta \rightarrow \infty$.

This is a 3rd-order ODE, which we can solve by converting it into three 1st-order ODEs:
\begin{align}
y_1 &= f \quad y_1^{\prime} = y_2 \\
y_2 &= f^{\prime} \quad y_2^{\prime} = y_3 \\
y_3 &= f^{\prime\prime} \quad y_3^{\prime} = -y_1 y_3
\end{align}
and we can use the shooting method to solve by recognizing that we have two initial conditions, $y_1(0) = y_2(0) = 0$, and are missing $y_3(0)$. We also have a target boundary condition: $y_2(\infty) = 1$.

(Note: obviously we cannot truly integrate over $0 \leq \eta < \infty$. Instead, we just need to choose a large enough number. In this case, using 10 is sufficient.)

Let's create a function to evaluate the derivatives:

```matlab
%%file blasius_rhs.m
function dydx = blasius_rhs(eta, y)

dydx = zeros(3,1);

dydx(1) = y(2);
dydx(2) = y(3);
dydx(3) = -y(1) * y(3);
```

    Created file '/Users/niemeyek/projects/ME373-book/content/bvps/blasius_rhs.m'.

First, let's try the same three-step approach we used for the simpler example, taking two guesses and then using linear interpolation to find a third guess:

```matlab
clear all; clc

target = 1.0;

guesses = zeros(3,1);
solutions = zeros(3,1);

guesses(1) = 1;
[eta, F] = ode45('blasius_rhs', [0 10], [0 0 guesses(1)]);
solutions(1) = F(end, 2);

guesses(2) = 0.1;
[eta, F] = ode45('blasius_rhs', [0 10], [0 0 guesses(2)]);
solutions(2) = F(end, 2);

m = (guesses(1) - guesses(2))/(solutions(1) - solutions(2));
guesses(3) = guesses(2) + m*(target - solutions(2));

[eta, F] = ode45('blasius_rhs', [0 10], [0 0 guesses(3)]);
solutions(3) = F(end, 2);

tries = [1; 2; 3];
table(tries, guesses, solutions)
fprintf('Target: %5.2f\n', target);
```

    ans =

      3x3 table

        tries    guesses    solutions
        _____    _______    _________

          1            1     1.6553
          2          0.1     0.3566
          3      0.54587     1.1056

    Target:  1.00

So, for this problem, using linear interpolation did *not* get us the correct solution on the third try. This is because the ODE is nonlinear. But, you can see that we are converging towards the correct solution—it will just take more tries.

Rather than manually take an unknown (and potentially large) number of guesses, let's automate this with a `while` loop:

```matlab
clear all; clc

target = 1.0;

% get these arrays of stored values started.
% note: I'm only doing this to make it easier to show a table of values
% at the end; otherwise, there's no need to store these values.
tries = [1; 2; 3];
guesses = zeros(3,1);
solutions = zeros(3,1);

guesses(1) = 1;
[eta, F] = ode45('blasius_rhs', [0 10], [0 0 guesses(1)]);
solutions(1) = F(end, 2);

guesses(2) = 0.1;
[eta, F] = ode45('blasius_rhs', [0 10], [0 0 guesses(2)]);
solutions(2) = F(end, 2);

num = 2;
solutions(3) = -1000.; % doing this to kick off the while loop
while abs(target - solutions(num)) > 1.e-9
    num = num + 1;
    m = (guesses(num-2) - guesses(num-1))/(solutions(num-2) - solutions(num-1));
    guesses(num) = guesses(num-1) + m*(target - solutions(num-1));
    [eta, F] = ode45('blasius_rhs', [0 1e3], [0 0 guesses(num)]);
    solutions(num) = F(end, 2);
    tries(num) = num;

    % we should probably set a maximum number of iterations, just to prevent
    % an infinite while loop in case something goes wrong
    if num >= 1e4
        break
    end
end

table(tries, guesses, solutions)
fprintf('Number of iterations required: %d', num)
```

    ans =

      7x3 table

        tries    guesses    solutions
        _____    _______    _________

          1            1      1.6553
          2          0.1      0.3566
          3      0.54587      1.1056
          4      0.48301       1.019
          5      0.46922     0.99951
          6      0.46957           1
          7      0.46957           1

    Number of iterations required: 7

```matlab
%plot -r 200
plot(F(:, 2), eta); ylim([0 5])
xlabel("f^{\prime}(\eta) = u/U_{\infty}")
ylabel('\eta')
```

We can see that this plot of $\eta$, the $y$ position normalized by the boundary-layer thickness, vs. nondimensional velocity matches the original figure.


Session 1

Variables and data types. Arrays. Structure

**Course creator and speaker:**  Manuel David Soto. MSc in Geology, University of Texas at Austin, USA.

**Organizer:** Ulises Berman. Student of Geophysical Eng, USB, Caracas, Venezuela. President of the AAPG USB Student Chapter.

Table of contents

* [1 Variables and  data types](#variables)
   * [1.1 Introduction](#intro)
   * [1.2 Variables in Python](#variables)
   * [1.3 Data types](#data_types)
       * [1.3.1 Numeric data type](#numeric_data_type)
       * [1.3.2 Boolean data type](#boolean_data_type)
       * [1.3.3 Dictionary](#dictionary)
       * [1.3.4 Sequences](#sequences)
           * [1.3.4.1 Strings](#strings)
           * [1.3.4.2 List](#lists)
           * [1.3.4.3 List of lists](#list_list)
           * [1.3.4.4 Tuples](#tuples)
* [2 Arrays](#arrays)
   * [2.1 Introduction](#numpy_basics)
   * [2.2 Generating NumPy arrays](#creation1)
   * [2.3 Accessing the array elements](#numpy_accesing)
   * [2.4 NumPy array attributes](#numpy_attributes)
   * [2.5. Linear algebra](#lineal_algebra)

* [3 Python structure](#struc)
   * [3.1 Built-in functions](#bif)
   * [3.2 Standard libraries](#sl)
   * [3.3 External libraries](#el)
   * [3.4 User defined functions](#udf)

 1 Variables and  data types

 1.1 Introduction

My first Python program

```python
# My life is simple, I'm just a comment

print('Hello World!')
```

 Python as a calculator

```python
11 + 666
```

```python
10000 * 5
```

Print is a function that allows concatenation of elements of different type, separated by commas

```python
print('This number', 10000 * 123456789, 'is big')
```

 Python Arithmetic Operators

The arithmetic operators available in Python are:

|Operator|Description|Example|
| --- | --- | --- |
| + Addition| Add values on either side of the operator| 5 + 4|
| - Subtraction| Subtract right operand from left operand| 5 - 3 |
| * Multiplication| Multiplies values on either side of the operator| 5 * 3 |
| / Division | Divides left hand operand by right hand operand| 6 / 2|
| % Modulus | Divides left hand operand by right hand operand and returns remainder| 6%3|
| ** Exponent | Performs exponential (power) calculation on operators | 6 ** 2|
| // floor division | Performs the floor division of two operators |  9 //4 |

```python
#The normal division

9/4
```

```python
# Integer part of the division

9//4
```

```python
# Remainder of the division

9 % 4
```

 1.2 Variables in Python

Variables are reserved memory locations to store values. This means that when you create a variable you reserve some space in memory.

Based on the data type of a variable, the interpreter allocates memory and decides what can be stored in the reserved memory. Therefore, by assigning different data types to variables, you can store integers, decimals or characters in these variables.

 Rules for variables

* A variable name **must** start with a letter or the underscore character.
* A variable name **cannot** start with a number.
* A variable name **can only** contain alpha-numeric characters and underscores (A-z, 0-9, and _ ).
* Variable names **are case-sensitive** (python, Python and PYTHON are three different variables).

 Assigning values to variables

The declaration happens automatically when you assign a value to a variable. The equal sign (=) is used to assign values to variables. The operand to the left of the = operator is the name of the variable and the operand to the right of the = operator is the value stored in the variable.

More about variables at: https://realpython.com/python-variables/

```python
# Simple calculation

apples = 20
cost = 0.15
currency = 'Euros'
total = cost * apples

print('Total cost of apples:', total, currency)
```

```python
# Example Pythagoras calculation

side1 = 20
side2 = 30
unit = 'cm'
side3 = (side1**2 + side2**2)**0.5

print("Side 3 :", side3, unit)
```

 🏋
    Exercise 1:

If the Average velocity to the target is 3000 m/sec and the target depth is 5000 m, calculate the two way time to the target according to the following formula:

\begin{align}
TWT=2*( \frac{Depth}{Velocity})
\end{align}

Define the variables and use the formula to obtain the TWT.

 Keywords

In Python there are some reserved words that cannot be used as a variable names:

|Keyword|Description|
| --- | ---|
and|A logical operator|
as|To create an alias|
assert|For debugging|
|break|To break out of a loop|
|class|To define a class|
|continue|To continue to the next iteration of a loop|
|def|To define a function|
|del|To delete an object|
|elif|Used in conditional statements, same as else if|
|else|Used in conditional statements|
|except|Used with exceptions, what to do when an exception occurs|
|False|Boolean value, result of comparison operations|
|finally|Used with exceptions, a block of code that will be executed no matter if there is an exception or not|
|for|To create a for loop|
|from|To import specific parts of a module|
|global|To declare a global variable|
|if|To make a conditional statement|
|import|To import a module|
|in|To check if a value is present in a list, tuple, etc.|
|is|To test if two variables are equal|
|lambda|To create an anonymous function|
|None|Represents a null value|
|nonlocal|To declare a non-local variable|
|not|A logical operator|
|or|A logical operator|
|pass|A null statement, a statement that will do nothing|
|raise|To raise an exception|
|return|To exit a function and return a value|
|True|Boolean value, result of comparison operations|
|try|To make a try...except statement|
|while|To create a while loop|
|with|Used to simplify exception handling|
|yield|To end a function, returns a generator|

More information on data types at: https://docs.python.org/3/library/datatypes.html

```python

```

 1.3 Data types

In Python data types refers to the classification or categorization of data items. It represents the kind of value that tells what operation can be performed on a particular data. The following image shows the main data types available in Python, as well as the more relevant for log analysis (in red):

The function `type()` tells you the type of data of a variable

```python
# What kind of data is the variable the unit in the Pythagoras calculation?

type(unit)
```

 1.3.1 Numeric data type

In Python, numeric data types represents the data which has a numerical value

* **Integer**: The number could be positive or negative, and there is not limit to how long an integer value can be.
* **Float**: It is a real number with floating point representation. It is specified by a decimal point.
* **Complex**: It is a complex number. It is specified as (real part) + (imaginary part)i.
* **None**: It is another special type to denote null object or variable

```python
# Assignning variables

a = 10
b = 10.0
c = 10.5 + 20.5j

print("Integer:", a)
print("Real or float:", b)
print("Real + complex:", c)
```

```python
# Printing the data types

print(type(a))
print(type(b))
print(type(c))
```

```python
# The value of e is changing

e = a + b

print(e)
```

```python
d = a + c

print(d)
```

```python
# None is null object or variable, different of zero

e = None
type(e)
```

 🏋
    Exercise 2:

The relation between velocity and depth is related according to the following formula:

\begin{align}
Velocity (m/sec)=2800 + 0.7 * Depth (m)
\end{align}

* What is the Velocity (m/sec) at 3000 meters?
* What is the Velocity (m/sec) at 6000 feet?

The conversion factor from ft to mt is 0.3048

1.3.2 Boolean data type and operators

In Python the Boolean variables can have **True** or **False** value.

```python
a = True
b = False

print(a)
print(b, '\n')

print("a type :", type(a))
print("b type :", type(b))
```

 Boolean operators

The Boolean operators available in Python are:

Assume that a = 8 and b = 10

|Operator | Description| Example|
| --- | --- | --- |
|==|If the values of two operands are equal, then the condition becomes true.|(a == b) is not true.|
|!=|If values of two operands are not equal, then condition becomes true.|(a != b) is true.|
|<>|If values of two operands are not equal, then condition becomes true.|(a <> b) is true. This is similar to != operator.|
|>|If the value of left operand is greater than the value of right operand, then condition becomes true.|(a > b) is not true.|
|<|If the value of left operand is less than the value of right operand, then condition becomes true.|(a =|If the value of left operand is greater than or equal to the value of right operand, then condition becomes true.|(a >= b) is not true.|
|<=|If the value of left operand is less than or equal to the value of right operand, then condition becomes true.|(a <= b) is true.|

```python
a = 10 == 20
print(a)
```

 🏋
    Exercise 3:

* How much time takes a wave to travel through water in 2220 m, V=1500 m/s?
* How much time takes a wave to travel through air  in 500 m, V=340 m/s?
* Write a small piece of code in Python to calculate the travel times and also find which is longer.

Python Logical Operators

The Logical operators available in Python are:

|Operator | Description| Example|
| --- | --- | --- |
|and Logical AND|If both the operands are true then condition becomes true.|(a and b) is true.|
|or Logical OR|If any of the two operands are non-zero then condition becomes true.|(a or b) is true.|
|not Logical NOT|Used to reverse the logical state of its operand.|Not(a and b) is false.|

```python
b = True and True
print(b)

c = False or False
print(c)

d = not(False)
print(d)
```

1.3.3 Dictionary

A dictionary is a collection of unordered, changeable and non indexable values. It has a general structure that can be summarized as keys-values pairs, in which both can be strings or numbers, but keys have to be unique. In Python, dictionaries are written with curly brackets, with colon (:) as separator between each individual key and value, and comma (,) as separator between pairs. Dictionaries are good option for formations tops, mineral compositions, etc.

```python
# Example of a dictionary example (key1: value1, key2: value2, ... , keyn: valuen,)

grocery = {2: "fajitas", 1: "chicken", 4: "fish", 3:"coffe", 7:"bread", 0:'milk'}

print(grocery)
```

```python
# Values associated with the keys

print(grocery[0])
print(grocery[1])
print(grocery[3])
print(grocery[7])
```

Some useful Python functions and methods of the list data type

|Description| Python function and methods|
| --- | --- |
|Dictionary length|  `len(dictionary_variable)`|
|Adding elements to dictionary|  `dictionary_variable[key]=value`|
|Deleting elements from the dictionary| `del dictionary_variable[key]`|
|Clear a dictionary| `dictionary_variable.clear()`|

```python
# Check these functions

print(len(grocery))

grocery[5] = "olives"

print(grocery)

del grocery[3]

print(grocery)
```

```python
grocery.clear()

print(grocery)
```

```python
# Example of a dictionary with strings as keys

shale = {'clay': 60, "calcite": 20, "quartz": 15, "micas": 5}
unit = 'weight %'
print(shale)
print('Shale: ', shale['quartz'], unit)
```

 🏋
    Exercise 4:

Write a dictionary with the following composition (weight %) of a granite:

    Quartz       25
    Orthoclase   50
    Plagioclase  10
    Micas        10
    Anphibole     5

Using Python, present the values for orthoclase and micas

1.3.4 Sequences

Sequences in Python is a succession of values bound together by a container that has a same type. There are 7 types of sequences in Python but the more important three are String, Tuples and Lists. Using an index we can access to the elements of the sequence. In Python the index start in zero, and a certain element of the sequence can be access with square brackets. Indexes are numbered from 0 to n-1 where n is the number of items (or characters), and they are positioned between the items:

    Sequence:  H  e  l  l  o  ,     w  o  r  l  d  !

    Index:     0  1  2  3  4  5  6  7  8  9 10 11 12

 The function `len()` gives you the length of the sequence

```python
sequence="Hello, World!"

print(sequence[0])
print(sequence[1])
print(sequence[2])
print(sequence[3])
print(sequence[4])
print(sequence[5])
print(sequence[6])
print(sequence[7])
print(sequence[8])
print(sequence[9])
print(sequence[10])
print(sequence[11])
print(sequence[12])

print(sequence[6]) # What character is this?

print("The length of the sequence is:",len(sequence))

print("The type of the sequence is:",type(sequence))
```

1.3.4.1 Strings

A string can be created simply by enclosing characters in quotes. Python treats single quotes the same as double quotes.

```python
# Examples

variable1 = "Salmorejo"
print(variable1)

variable2 = 'Salmón'
print(variable2)

variable3 = 'Sandía'
print(variable3)
```

```python
# Everything count in the strings

variable2 == 'Salmon'
```

 Some useful Python function and methods

|Description| Python fuction or method|
| --- | --- |
|Concatenating strings|  `string1 + string2`|
|Replacing characters in the string|    `string1.replace(char1, char2)`|
|Verify if some characters are in string| `char1 in string1`|
|Find the possition of a character in a string| `string1.find(char1)`|
|Length of a string| `len(string1)`|

```python
lunch = variable1 + variable2 + variable3
print(lunch,'\n')

lunch = variable1+", "+variable2+", "+variable3
print(lunch)
```

```python
lunch = lunch.replace("Sandía", "Café")

print(lunch)
```

```python
filename = "program.txt"

filename = filename.replace("txt", "py")

print(filename)
```

```python
filename = filename.replace(".", "-v2.")

print(filename)
```

```python
# The in commands give you a Boolean variable

country = 'Malaysia'

'lay' in country
```

```python
# The find commands give you the index of the string

country.find('lay')
```

 🏋
    Exercise 5:

Write in Python a variable with the following string

"A subsurface body of rock having sufficient porosity and permeability to store and transmit fluids. Metamorphic rocks are the most common reservoir rocks because they have more porosity than most igneous and sedimentary rocks and form under temperature conditions at which hydrocarbons can be preserved. A reservoir is a critical component of a complete petroleum system."

* Print the variable in the screen
* Correct the mistakes in the variable using the replace method
* Verify if temperature is in the string
* Verify if kritical is in the string
* What is the string length?

1.3.4.2 Lists

A list is sequence of different data types [strings, floats. integers, boolean, ...] inside square brackets and separated by commas.

```python
# Example

list1 = ["orange", "banana", 1, 2, True, None]

print(list1)
```

You can access one element of the list using square brackets, but remember start counting in 0

```python
# Example 2nd element

list1[1]
```

```python
# 3th and 5th elements

print(list1[2])
print(list1[4])
```

A list could be initialized in different ways

* Create a empty list: `list1=[]`
* Create a list with n equal values: `list1=[value]*n`

```python
# Empty list

list1 = []
print(list1)

# List of 10 elements

list2 = [30]*10
print(list2)

print(len(list2))
```

To change an element of the list (this method cannot be used with Tuples or strings)

```python
# Changing 4th & 6th elements

list2[3] = 10
list2[5] = 100

print(list2)
```

 Some useful methods and functions of lists data type

|Function or method|Python Expression|
| --- | --- |
| add an element to a list | `list.append(obj)`|
| Maximum of a list | `max(list)`|
| Minimum of a list | `min(list)`|
| Returns count of how many times obj occurs in list | `list.count(obj)`|
| Sort the list | `list.sort()`|
| Reverse the list | `list.reverse()`|

```python
# Append a value

list2.append(1000)
print(list2)

# Print maximum, minimum and how many times ocurrs an object

print("Maximum value:",max(list2))
print("Minimum value:",min(list2))
print("How many times occurs 30:",list2.count(30))

#Sorting a list

list2.sort()
print(list2)

#Reversing a list

list2.reverse()
print(list2)
```

```python
list2
```

 🏋
    Exercise 7:

These are the horizon tops and its depths (m):

    Surface       0
    Cretaceous 1500
    Jurassic   2000
    Triassic   3000
    Paleozoic  4500

* Store the list of horizons and its corresponding depths in a list variable
* Print the lists
* Print the name and the depth of the third horizon in the list
* What is the maximum depth?
* What is the minimum depth?
* What is the average depth?
* How many horizons are in the list
* Verify that the number of horizons names and number of horizons tops are the same

1.3.4.3 Lists of lists

A list of list could be interpreted as a matrix in algebra notation, for example

[list row 1, list row 2, list row 3, list row 4]

[ [ E11, E12, E13, E14, E15],
  [ E21, E22, E23, E24, E25],
  [ E31, E32, E33, E34, E35],
  [ E41, E42, E43, E44, E45],
  [ E51, E52, E53, E54, E55],
  [ E61, E62, E63, E64, E65],
  [ E71, E72, E73, E74, E75] ]

```python
# Example

a = [[2, 3, 4], [5, 6, 7], [8, 9, 10]]

print(a)
```

Using the following Python expressions allow to access to the list of list elements:

|Description|Python expression|
| --- | --- |
|A row| `variable[row]`|
|A particular location| `variable[row][column]`|

```python
row = 2
column = 1

print(a[row])

print(a[row][column])
```

The differences between lists and tuples are subtle but important. Ones created lists can be modified, tuples instead are immutable.

1.3.4.4 Tuples

A tuple is sequence of different Python objects (strings, floats. integers, boolean, ...) inside parenthesis and separated by commas.

```python
# Example

tuple1 = ("Oranges", "Onion", "Lettuce")

tuple2 = ("TT1", 34567, 76543, 23)

print(tuple1)

print(tuple1[0])

print(tuple1[2])

print(tuple2)
```

 🏋
    Exercise 6:

These are the horizon tops and its depths (m):

    Surface       0
    Cretaceous 1500
    Jurassic   2000
    Triassic   3000
    Paleozoic  4500

Write one tuple with the horizons name and the corresponding depths. Print the name and the depth of the third horizon.

```python
# Demostration

one_list = [1,2,3,4]
one_tuple = (1,2,3,4)

print(one_list)
print(one_tuple)
```

```python
# Lets change 3rd in the list

one_list[2] = 5
print(one_list)
```

```python
# Lets change 3rd in the tuple

one_tuple[2] = 5
print(one_tuple)
```

End of part

[Return to Table of Content](#toc)

2 Arrays

https://numpy.org

2.1 Introduction

**NumPy (Numerical Python) is an external package that provides the most important data type for us (geoscientists, engineers, petrophysicists), the array (or matrix).** Most of the data we are going to use have this structure which holds some benefits over the Python lists, such as:

* Being more compact
* Faster access in reading and writing items
* Algebra operations
* Can be parallelized for GPU processing

To use NumPy, first, you need to install the module using the pip command in the Command Prompt (Python introduction)

`pip install numpy`

Inside the Jupyter notebook or script, import the module with:

`import numpy as np`

Been np an alias to facilitate the writing of the numpy functions

```python
import numpy as np
```

2.2 Generating NumPy arrays

There are different ways to create arrays (vectors and matrices), some of the most important are:

* Converting a list or list of lists to an array with the function `np.array()`
* Creating **1D array (vector) or 2D array (matrix)** of zeros with the function `np.zeros()`
* Creating 1D array or 2D array of fix value with the function `np.full()`
* Creating 1D array or 2D array of random values (floats) with the function `np.random.rand()`
* Creating 1D array or 2D array of random values (integer) with the function `np.random.randint()`

Let's review them

```python
# Converting list to vector or 1d array

list0 = [10, 1, 20, 3]    # lists have commas, arrays not

vector = np.array(list0)

print("List:", list0)
print(type(list0), '\n')

print("Vector:", vector)
print(type(vector))
```

```python
# Converting lists of list to matrix or 2d array

list_list = [[2.0, 3, 4], [5, 6, 7], [8, 9, 10]]

matrix = np.array(list_list)

print("List of lists:", list_list,'\n')
print("Matrix:\n", '\n', matrix)
print(type(matrix))
```

```python
# Creating a vector of zeros

vector = np.zeros(10)

print("Vector of zeros:", vector)
```

```python
# Creating a matriz of zeros

matrix = np.zeros((5, 5))

print("Array of zeros:\n", '\n', matrix)
```

```python
# Creating a vector with a constant value

vector8 = np.full((5), 8)

print("Vector with constant value:\n", '\n',vector8)
```

```python
# Creating a matrix with a constant value

matrix9 = np.full((5, 8), 9)

print("Array with constant value:\n", '\n',matrix9)
```

```python
# Generating an array with float random values (0 to 1)

array_random = np.random.rand(1,5)

# You can use also np.random.rand(5) but there is a small difference

print("Array with random values:", array_random)

array_random.shape
```

```python
# Generating a matrix with float random values (0 to 1)

matrix_random = np.random.rand(7,5)

print("Matrix with random values:\n", '\n', matrix_random)
```

```python
# Generating a vector with random integers (0 to 100)

print("Vector with random integers: \n", '\n', np.random.randint(100, size=(5)))
```

```python
# Generating a vector with random integers (20 to 80)

print("Vector with random integers: \n", '\n', np.random.randint(20, 80, size=(5,5)))
```

```python
# Generating a matrix with inteder random values (0 to 100)

print("Matrix with random integers: \n", '\n', np.random.randint(100, size=(5,5)))
```

 🏋
    Exercise 8:

Create a matrix (list of lists) with the following values:

    | 6   8  12  24  33|
    |11   4  12   6  67|
    |23   9  67  10  43|
    | 1  12  34  18   2|
    | 7  11   2  23  78|
    | 8  24   1   5  23|

* Print the matrix
* Print the row 3
* Calculate the dimensions of the matrix
* Print the element [3,4]
* What is the minimum of the line 3
* What is the maximum of the line 2

2.3 Accessing the array elements

The i-th value (counting from zero) can be accessed by specifying the desired index in square brackets, just as with Python lists:

```python
# Printing the 3rd element of a vector

wavelet=np.array([10,1,20,3])

print(wavelet[2])
```

Using the following Python expressions allow to access to the elements of a a 2D array  :

|Description|Python expression|
| --- | --- |
|A row| `variable[row]`|
|A column| `variable[:,column]`|
|A particular location| `variable[row, column]`|

```python
matrix = np.random.rand(6,6)

print(matrix)
```

```python
# Printing 3th row

print(matrix[2])
```

```python
# Printing 5th column

print(matrix[:,4])
```

```python
# Printing a specific element

print(matrix[2, 4])
```

2.4 Numpy array attributes

These are the main attributes of a NumPy array:

|Description | Attribute |
| --- | --- |
|dimensions of the array (1 or 2D)| `variable.ndim`|
|Size of each dimension | `variable.shape`|
|Array Size|`variable.size`|
|Array type| `variable.dtype`|
|Array total size in bytes| `variable.nbytes`|

```python
print("Number of dimensions :",matrix.ndim)
print("Shape of the array :",matrix.shape)
print("Array size:",matrix.size)
print("Array type:",matrix.dtype)
print("Element size in bytes:",matrix.itemsize)
print("Array total size in bytes:",matrix.nbytes)
```

 🏋
    Exercise 9:

* Generate a 5x5 matrix of constant value equal to 10
* Generate a random 6x6 matrix
* Print the values of the third column of both arrays
* Print the attributes of both matrices
* Generate a 3 million elements array and calculate all its attributes

2.5 Linear algebra

One of disadvantages of a Python list is that we need to iterate on each element of the list to perform mathematical operations, while you can do operation between arrays, even linear algebra, with a very simple notation:

Basic concepts on linear algebra at:

https://towardsdatascience.com/linear-algebra-for-deep-learning-f21d7e7d7f23

https://www.khanacademy.org/math/linear-algebra

```python
# Define a matrix

matrix = np.array([[2.0, 7, 4], [5, 6, 9], [1, 9, 10]])

print("Matrix:\n", '\n', matrix)
```

```python
# Multiply by a constant

matrix10 = matrix*10

print("\nResult of the multiplication by an integer:\n", '\n', matrix10)
```

```python
# Sum of matrixes

matrix2 = matrix + matrix10

print("\nSum of matrices:\n", matrix2)
```

```python
# Multiply a matrix by a vector

vector = np.array([10, 23, 45])

result1 = matrix10 * vector

print("Multiply a matrix by a vector\n", '\n', result1)
```

```python
# Dot product of two Nump arrays

a = np.array([[1, 2], [3, 4]])
b = np.array([[11, 12], [13, 14]])

print('a:\n',a,'\n')
print('b:\n',b,'\n')

print("Dot product:\n", np.dot(a, b))
```

```python
# Power of a matrix

print("Matrix result1 raised to power 5:\n", '\n', np.linalg.matrix_power(matrix, 5))

# This is not powering each element of the matrix
```

```python
# Determinant of a matrix

print("Determinant of matrix\n", '\n', np.linalg.det(matrix))
```

```python
# Inverse of a matrix

print("\nInverse of matrix:\n", np.linalg.inv(matrix))
```

 🏋
    Exercise 10:

Generate two 5x5 matrices, one with constant value equal to 10, and the other with random values

* Determine if the matrices are singular (Determinant=0)
* Calculate the inverse of the matrices
* Sum both matrices
* Convert the constant matrix to zero matrix

 🏋
    Exercise 11:

Solve the following Linear equations system:

\begin{align}
1x+3y-2z = 5  \\
3x+5y+6z = 7  \\
2x+4y+3z = 8
\end{align}

The linear system can be represented in linear algebra as:

\begin{align}
A \cdot x=b
\end{align}

The solution will be:

\begin{align}
x=A^{-1} \cdot b
\end{align}

End of part

[Return to Table of Content](#toc)

3 Python structure

Functions are the building block of Python. They are executed when they are call, receiving parameters and then produce a result or perform a tasks. As a concept it is the same as a mathematic function:

\begin{align}
\ {P}=f(x,y)
\end{align}

Been x,y the **arguments** (if there is any) and P the **result** (if there is any). The functions in Python can be integrated or built in the core of the program, group in standard libraries (included in Python but inactive) or in public packages (built outside of Python), or even defined by the user:

Let's see examples of these four components of Python:

 3.1 Built-in functions

The Python interpreter has a number of **built-in functions (BIF, the skull) and types that are always available.** They are listed here in alphabetical order.

|Function|Description|
| --- | --- |
|abs()|Returns the absolute value of a number|
|all()|Returns True if all items in an iterable object are true|
|any()|Returns True if any item in an iterable object is true|
|ascii()|Returns a readable version of an object. Replaces none-ascii characters with escape character|
|bin()|Returns the binary version of a number|
|bool()|Returns the boolean value of the specified object|
|bytearray()|Returns an array of bytes|
|bytes()|Returns a bytes object|
|callable()|"Returns True if the specified object is callable, otherwise False"|
|chr()|Returns a character from the specified Unicode code.|
|classmethod()|Converts a method into a class method|
|compile()|"Returns the specified source as an object, ready to be executed"|
|complex()|Returns a complex number|
|delattr()|Deletes the specified attribute (property or method) from the specified object|
|dict()|Returns a dictionary (Array)|
|dir()|Returns a list of the specified object's properties and methods|
|divmod()|Returns the quotient and the remainder when argument1 is divided by argument2|
|enumerate()|Takes a collection (e.g. a tuple) and returns it as an enumerate object|
|eval()|Evaluates and executes an expression|
|exec()|Executes the specified code (or object)|
|filter()|Use a filter function to exclude items in an iterable object|
|float()|Returns a floating point number|
|format()|Formats a specified value|
|frozenset()|Returns a frozenset object|
|getattr()|Returns the value of the specified attribute (property or method)|
|globals()|Returns the current global symbol table as a dictionary|
|hasattr()|Returns True if the specified object has the specified attribute (property/method)|
|hash()|Returns the hash value of a specified object|
|help()|Executes the built-in help system|
|hex()|Converts a number into a hexadecimal value|
|id()|Returns the id of an object|
|input()|Allowing user input|
|int()|Returns an integer number|
|isinstance()|Returns True if a specified object is an instance of a specified object|
|issubclass()|Returns True if a specified class is a subclass of a specified object|
|iter()|Returns an iterator object|
|len()|Returns the length of an object|
|list()|Returns a list|
|locals()|Returns an updated dictionary of the current local symbol table|
|map()|Returns the specified iterator with the specified function applied to each item|
|max()|Returns the largest item in an iterable|
|memoryview()|Returns a memory view object|
|min()|Returns the smallest item in an iterable|
|next()|Returns the next item in an iterable|
|object()|Returns a new object|
|oct()|Converts a number into an octal|
|open()|Opens a file and returns a file object|
|ord()|Convert an integer representing the Unicode of the specified character|
|pow()|Returns the value of x to the power of y|
|print()|Prints to the standard output device|
|property()|"Gets, sets, deletes a property"|
|range()|"Returns a sequence of numbers, starting from 0 and increments by 1 (by default)"|
|repr()|Returns a readable version of an object|
|reversed()|Returns a reversed iterator|
|round()|Rounds a numbers|
|set()|Returns a new set object|
|setattr()|Sets an attribute (property/method) of an object|
|slice()|Returns a slice object|
|sorted()|Returns a sorted list|
|@staticmethod()|Converts a method into a static method|
|str()|Returns a string object|
|sum()|Sums the items of an iterator|
|super()|Returns an object that represents the parent class|
|tuple()|Returns a tuple|
|type()|Returns the type of an object|
|vars()|Returns the _dict_ property of an object|
|zip()|"Returns an iterator, from two or more iterators"|

A complete description of the built-in functions can be found in the following link: https://docs.python.org/3/library/functions.html

Some mathematical BIF

```python
# Absolute value, minimum, maximum, ...

list_numbers = [10, 20, 15, 2, 70, 12, 22]

print("Absolute value:", abs(-20))
print("Minimum value :", min(list_numbers))
print("Maximum value :", max(list_numbers))
print("Sorted list   :", sorted(list_numbers))
print("Round a number to 4th decimals :", round(23.456789, 4))
print("Round a number to 2nd decimals :", round(23.456789, 2))
print("Round a number with no decimals :", round(23.456789, 0))
print("Sum of a list of numbers :", sum(list_numbers))
```

 🏋
    Exercise 12:

* Create a NumPy array with some random values
* Calculate the maximum and the minimum of the array
* Calculate the Average of the terms
* Print the numbers rounded to 6 decimals

Conversion BIF

The most useful functions to convert data types between float, integer and string

```python
# Conversion from string to float

print(float("10.4"))
print(float(10))

# Conversion from string to integer

print(int("10"))
print(int(10.2))
# print(int("10.2"))
print(int(float("10.2")))
```

```python
# Conversion of number to string, very important for file writing

p = 12.567890
q = str(p)

print('q =', q)
print(type(q),'\n')

r = str(round(p, 2))
print('r =', r)
print(type(r),'\n')
```

 🏋
    Exercise 13:

Practice with a couple of examples on how to convert between float, int and str

Input BIF

The `input()` function allows interactive user input from the keyboard and convert it into a string.

```python
# input function

# datetime is a Python standard library so is included in the installation but needs to be imported

from datetime import datetime

temp = input("Please enter the temperature in ºC of your city: ")    # Remember, temp contains an string

dt = datetime.now()

print()
print('The temperature in Madrid is', temp, 'ºC')
print('The local time: ', str(dt.hour)+':'+str(dt.minute))
print('Date: ', str(dt.day)+'/'+str(dt.month)+'/'+str(dt.year))
```

```python
# Try to do execute this

new_temp = temp + 5
```

 🏋
    Exercise 14:

In a cell write a small program for temperature conversion between Celsius to Fahrenheit with user interactive input

Range BIF

The `range()` function returns a sequence of numbers, starting from 0 by default, and increments by 1 (by default), and ends at a specified number. The syntax is the following:

`range(start, stop, step)`

|Parameter|Description|
| --- | --- |
|start|Optional. An integer number specifying at which position to start. Default is 0|
|stop|Required. An integer number specifying at which position to end.|
|step|Optional. An integer number specifying the incrementation. Default is 1|

Some examples:

```python
x = range(13)

print("Example 1:", x)
print("Example 1:", list(x))
type(x)
```

```python
x = range(0, 13)

print("Example 2:", list(x))
```

```python
x = range(1, 13)

print("Example 3:", list(x))
```

```python
x = range(1, 13, 2)

print("Example 4:", list(x))
```

 🏋
    Exercise 15:  Generate a range between 1 and 3000000

* Calculate the average
* Calculate the minimum
* Calculate the maximum

 3.2 Standard libraries functions

**There are hundreds of Standard Libraries (SL, the bones) with their respective functions.** They are developed by the Python Foundation, and are already available in Python (included in the installation), however they need to be imported before use them:

https://docs.python.org/3/library/

Some important Standard libraries for us are:

* random
* math (see below)
* statistics

#### Math SL

In addition to the few Math BIF there is a Math SL that contains more complex mathematical functions:

|Function                                   |Description|
| --- | --- |
|ceil(x)                                    |Returns the smallest integer greater than or equal|
|copysign(x, y)                             |Returns x with the sign of y|
|fabs(x)                                    |Returns the absolute value of x|
|factorial(x)                               |Returns the factorial of x|
|floor(x)                                   |Returns the largest integer less than or equal to x|
|fmod(x, y)                                 |Returns the remainder when x is divided by y|
|frexp(x)                                   |Returns the mantissa and exponent of x as the pair|
|fsum(iterable)                             |Returns an accurate floating point sum of values in|
|isfinite(x)                                |Returns True if x is neither an infinity nor a NaN|
|isinf(x)                                   |Returns True if x is a positive or negative infinit|
|isnan(x)                                   |Returns True if x is a NaN|
|ldexp(x, i)                                |Returns x * (2**i)|
|modf(x)                                    |Returns the fractional and integer parts of x|
|trunc(x)                                   |Returns the truncated integer value of x|
|exp(x)                                     |Returns e**x|
|expm1(x)                                   |Returns e**x - 1|
|log(x[, base])                             |Returns the logarithm of x to the base (defaults to|
|log1p(x)                                   |Returns the natural logarithm of 1+x|
|log2(x)                                    |Returns the base-2 logarithm of x|
|log10(x)                                   |Returns the base-10 logarithm of x|
|pow(x, y)                                  |Returns x raised to the power y|
|sqrt(x)                                    |Returns the square root of x|
|acos(x)                                    |Returns the arc cosine of x|
|asin(x)                                    |Returns the arc sine of x|
|atan(x)                                    |Returns the arc tangent of x|
|atan2(y, x)                                |Returns atan(y / x)|
|cos(x)                                     |Returns the cosine of x|
|hypot(x, y)                                |Returns the Euclidean norm, sqrt(x*x + y*y)|
|sin(x)                                     |Returns the sine of x|
|tan(x)                                     |Returns the tangent of x|
|degrees(x)                                 |Converts angle x from radians to degrees|
|radians(x)                                 |Converts angle x from degrees to radians|
|acosh(x)                                   |Returns the inverse hyperbolic cosine of x|
|asinh(x)                                   |Returns the inverse hyperbolic sine of x|
|atanh(x)                                   |Returns the inverse hyperbolic tangent of x|
|cosh(x)                                    |Returns the hyperbolic cosine of x|
|sinh(x)                                    |Returns the hyperbolic cosine of x|
|tanh(x)                                    |Returns the hyperbolic tangent of x|
|erf(x)                                     |Returns the error function at x|
|erfc(x)                                    |Returns the complementary error function at x|
|gamma(x)                                   |Returns the Gamma function at x|
|lgamma(x)                                  |Returns the natural logarithm of the absolute value|
|pi                                         |Mathematical constant, the ratio of circumference o|
|e                                          |mathematical constant e (2.71828...)|

More information on Math go to: https://docs.python.org/3/library/math.html

```python
# Math is alredy included in the Python instalation so the pip isntall is not necessary, just the import

import math
```

```python
print(math.pi)

angle_degree = 45

print(math.sin(angle_degree/180*math.pi))    # The argument are radians

side1 = 10
side2 = 20

side3 = math.sqrt(side1**2+side2**2)
print("Side 3:", side3)
```

 🏋
    Exercise 12:

Generate a list of angles from 0 to 359 degrees with step of 2 degrees. Using a for loop calculate cosine of each angle. Try to do a plot.

 3.3 External libraries

There are hundred of thousands of **External Libraries (EL, the muscles) or packages** with their respective function. They are developed by different individuals, companies, or institutions so they are not included in the Python installation, they need to be installed (via pip) and then imported before use them:

https://pypi.org/

Some important EL (all with their oun website) for us are:

* Numpy (numerical operation, see below)
* Matplotlib (plotting)
* Pandas (spreadsheet operations)
* Scipy (mathematics, science, and engineering)
* Pillow (image manipulation)
* Lasio (Read and write LAS)
* Welly (load and present LAS)

Examples of both groups are shown below:

Numpy EL

```python
# As EL, Numpy need to be installed and then imported before been used

import numpy as np

print(np.pi)

round(np.pi, 5)
```

The Numpy `np.arange()` function returns a sequence of numbers, starting from 0 by default, and increments by 1 (by default), and ends at a specified number. The syntax is the following:

`NumPy.arange(start, stop, step)`

|Parameter|Description|
| --- | --- |
|start|Optional. A number specifying at which position to start. Default is 0|
|stop|Required. A number specifying at which position to end.|
|step|Optional. A number specifying the incrementation. Default is 1|

Contrary to the `range()`, here the step can be a fraction. Some examples:

```python
x = np.arange(13)

print("Example 1:", x)

type(x)
```

```python
x = np.arange(1, 13)

print("Example 2:", x)
```

```python
x = np.arange(1, 13, 2)

print("Example 3:", x)
```

```python
x = np.arange(0, 1, 0.1)

print("Example 4:", x)
```

 3.4 User defined function

A **User Defined Function (UDF)** is a piece of code defined by the user. This type of functions are important for the following reasons:

* User-defined functions help to decompose a large program into small segments which makes program easy to understand, maintain and debug.
* If repeated code occurs in a program. Function can be used to include those codes and execute when needed by calling that function.
* Programmers working on large project can divide the workload by making different functions.

The syntax of the user defined function is:

    def function_name(arguments):
        code
        code
        ....
        return values

In the UDF the colon (:) and indentation (a tab or four spaces) are critical because define what is defined inside the function. Only the return value or variable are know by the rest of the program. Here is an example of UDF:

```python
# Example of a function returnning one value

def conversion_C_F(temp_c):
    temp_f = temp_c * 9/5 + 32
    return temp_f
```

```python
f = conversion_C_F(-2)

print('Temperature: ', f, 'ºF')
```

 🏋
    Exercise 16:  Create a function to convert from Fahrenheit to Celsius

```python
# Example of a function with multiple arguments and returnning one value

def line_equation(a, b, x):
    y = a * x + b
    return y
```

```python
# Example of a function with multiple arguments and returnning one multiple values

def equations(a, b, x):
    y1 = a * x + b
    y2 = a * x
    return y1, y2
```

```python
y = line_equation(10, 34, 10)

print(y)
print(line_equation(10, 34, 100))
```

```python
# The name of the ouput variables are not related with the internal variables of the UDF

yy1, yy2 = equations(10, 34, 100)

print(yy1, yy2)
print(equations(100, 340, 1000))
```

 🏋
    Exercise 17:  Create a function to calculate and return the following values of a list:

* Average
* Minimum
* Maximum

End of part

[Return to Table of Content](#toc)


#  Taylor problem 3.23

last revised: 04-Jan-2020 by Dick Furnstahl [furnstahl.1@osu.edu]

**This notebook is almost ready to go, except that the initial conditions and $\Delta v$ are different from the problem statement and there is no statement to print the figure.  Fix these and you're done!**

This is a conservation of momentum problem, which in the end lets us determine the trajectories of the two masses before and after the explosion.  How should we visualize that the center-of-mass of the pieces continues to follow the original parabolic path?

Plan:
1. Plot the original trajectory, also continued past the explosion time.
2. Plot the two trajectories after the explosion.
3. For some specified times of the latter two trajectories, connect the points and indicate the center of mass.

The implementation here could certainly be improved!  Please make suggestions (and develop improved versions).

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

First define some functions we think we will need.  The formulas are based on our paper-and-pencil work.

The trajectory starting from $t=0$ is:

$
\begin{align}
  x(t) &= x_0 + v_{x0} t  \\
  y(t) &= y_0 + v_{y0} t - \frac{1}{2} g t^2
\end{align}
$

```python
def trajectory(x0, y0, vx0, vy0, t_pts, g=9.8):
    """Calculate the x(t) and y(t) trajectories for an array of times,
        which must start with t=0.
    """
    return x0 + vx0*t_pts, y0 + vy0*t_pts - g*t_pts**2/2.
```

The velocity at the final time $t_f$ is:

$
\begin{align}
  v_{x}(t) &= v_{x0}  \\
  v_{y}(t) &= v_{y0}  - g t_f
\end{align}
$

```python
def final_velocity(vx0, vy0, t_pts, g=9.8):
    """Calculate the vx(t) and vy(t) at the end of an array of times t_pts"""
    return vx0, vy0 - g*t_pts[-1]  # -1 gives the last element
```

The center of mass of two particles at $(x_1, y_1)$ and $(x_2, y_2)$ is:

$
\begin{align}
 x_{cm} &= \frac{1}{2}(x_1 + x_2)  \\
 y_{cm} &= \frac{1}{2}(y_1 + y_2)
\end{align}
$

```python
def com_position(x1, y1, x2, y2):
    """Find the center-of-mass (com) position given two positions (x,y)."""
    return (x1 + x2)/2., (y1 + y2)/2.
```

**1. Calculate and plot the original trajectory up to the explosion.**

```python
# initial conditions
x0_before, y0_before = [0., 0.]    # put the origin at the starting point
vx0_before, vy0_before = [6., 3.]  # given in the problem statement
g = 1.  # as recommended

# Array of times to calculate the trajectory up to the explosion at t=4
t_pts_before = np.array([5., 6., 7., 8., 9.])
x_before, y_before = trajectory(x0_before, y0_before,
                                vx0_before, vy0_before,
                                t_pts_before, g)
```

```python
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(x_before, y_before, 'ro-')

ax.set_xlabel('x')
ax.set_ylabel('y')
```

Does it make sense so far?  Note that we could use more intermediate points to make a more correct curve (rather than the piecewise straight lines) but this is fine at least for a first pass.

**2. Calculate and plot the two trajectories after the explosion.**

For the second part of the trajectory, we reset our clock to $t=0$ because that is how our trajectory function is constructed.  We'll need initial positions and velocities of the pieces just after the explosion.  These are the final position of the combined piece before the explosion and the final velocity plus and minus $\Delta \mathbf{v}$.  We are told $\Delta \mathbf{v}$.   We have to figure out the final velocity before the explosion.

```python
delta_v = np.array([1., 3.])   # change in velociy of one piece

# reset time to 0 for calculating trajectories
t_pts_after = np.array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
# Also could have used np.arange(0.,6.,1.)

x0_after = x_before[-1]   # -1 here means the last element of the array
y0_after = y_before[-1]
vxcm0_after, vycm0_after = final_velocity(vx0_before, vy0_before,
                                          t_pts_before, g)
# The _1 and _2 refer to the two pieces after the explosinon
vx0_after_1 = vxcm0_after + delta_v[0]
vy0_after_1 = vycm0_after + delta_v[1]
vx0_after_2 = vxcm0_after - delta_v[0]
vy0_after_2 = vycm0_after - delta_v[1]

# Given the initial conditions after the explosion, we calculate trajectories
x_after_1, y_after_1 = trajectory(x0_after, y0_after,
                                  vx0_after_1, vy0_after_1,
                                  t_pts_after, g)
x_after_2, y_after_2 = trajectory(x0_after, y0_after,
                                  vx0_after_2, vy0_after_2,
                                  t_pts_after, g)
# This is the center-of-mass trajectory
xcm_after, ycm_after = trajectory(x0_after, y0_after,
                                  vxcm0_after, vycm0_after,
                                  t_pts_after, g)

# These are calculated points of the center-of-mass
xcm_pts, ycm_pts = com_position(x_after_1, y_after_1, x_after_2, y_after_2)
```

```python
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(x_before, y_before, 'ro-', label='before explosion')
ax.plot(x_after_1, y_after_1, 'go-', label='piece 1 after')
ax.plot(x_after_2, y_after_2, 'bo-', label='piece 2 after')
ax.plot(xcm_after, ycm_after, 'r--', label='original trajectory')
ax.plot(xcm_pts, ycm_pts, 'o', color='black', label='center-of-mass of 1 and 2')
for i in range(len(t_pts_after)):
    ax.plot([x_after_1[i], x_after_2[i]],
            [y_after_1[i], y_after_2[i]],
            'k--'
           )
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend();
```

```python

```

```python

```


## First Assignment

#### 1) Apply the appropriate string methods to the **x** variable (as '.upper') to change it exactly to: "$Dichlorodiphenyltrichloroethane$".

```python
x = "DiClOrod  IFeNi  lTRicLOr oETaNo  DiChlorod iPHeny lTrichL oroEThaNe"
```

```python
x = "DiClOrod  IFeNi  lTRicLOr oETaNo  DiChlorod iPHeny lTrichL oroEThaNe"
x[32:68].lower().replace(" ", "").capitalize()

```

    'Dichlorodiphenyltrichloroethane'

#### 2) Assign respectively the values: 'word', 15, 3.14 and 'list' to variables A, B, C and D in a single line of code. Then, print them in that same order on a single line separated by a space, using only one print statement.

```python
A, B, C, D = "word", 15, 3.14, "list"

print(A, B, C, D)
```

    word 15 3.14 list

#### 3) Use the **input()** function to receive an input in the form **'68.4 1.71'**, that is, two floating point numbers in a line separated by space. Then, assign these numbers to the variables **w** and **h** respectively, which represent an individual's weight and height (hint: take a look at the '.split()' method). With this data, calculate the individual's Body Mass Index (BMI) from the following relationship:

\begin{equation}
BMI = \dfrac{weight}{height^2}
\end{equation}

```python
w, h = map(float, input("Enter your weight and height: ").split())
bmi = w / (h * h)
print("Your BMI is " + str(round(bmi, 1)))
```

    Enter your weight and height: 55 1.66
    Your BMI is 20.0

#### This value can also be classified according to ranges of values, following to the table below. Use conditional structures to classify and print the classification assigned to the individual.

<\center>

(source: https://healthtravelguide.com/bmi-calculator/)

```python
w, h = map(float, input("Enter your weight and height: ").split())
bmi = w / (h * h)

if (bmi < 18.5):
    print("Underweight")
elif(bmi >= 25.0 and bmi <= 29.9):
    print("Pre-obesity")
elif(bmi >= 30.0 and bmi <= 34.9):
    print("Obesity class I")
elif(bmi >= 35.0 and bmi <= 39.9):
    print("Obesity class II")
elif(bmi > 40):
    print("Obesity class III")
else:
    print("Normal weight")

```

    Enter your weight and height: 55 1.66
    Normal weight

#### 4) Receive an integer as an input and, using a loop, calculate the factorial of this number, that is, the product of all the integers from one to the number provided.

```python
num = int(input("Enter a number:"))

f = 1

for i in range(1, num + 1):
    f = f * i

print(f)
```

    Enter a number:6
    720

#### 5) Using a while loop and the input function, read an indefinite number of integers until the number read is -1. Present the sum of all these numbers in the form of a print, excluding the -1 read at the end.

```python
inputs = input("Enter Number: ")
list = []

while int(inputs) != -1:
    list.append(inputs)
    inputs = input("Enter Num: ")
print(list)
```

    Enter Number: 6
    Enter Num: 6
    Enter Num: -1
    ['6', '6']

#### 6) Read the **first name** of an employee, his **amount of hours worked** and the **salary per hour** in a single line separated by commas. Next, calculate the **total salary** for this employee and show it to two decimal places.

```python
employee = {
    "first_name": "Joe",
    "hours_worked": 304,
    "hourly_wage": 24
}

print(employee)

total_salary = employee["hours_worked"] * employee["hourly_wage"]

print(round(total_salary, 2))
```

    {'first_name': 'Joe', 'hours_worked': 304, 'hourly_wage': 24}
    7296

#### 7) Read three floating point values **A**, **B** and **C** respectively. Then calculate itens a, b, c, d and e:

```python
A, B, C = map(float, input("Enter decimal numbers: ").split())
```

    Enter decimal numbers: 6.6 5.5 3.3

    a) the area of the triangle rectangle with A as the base and C as the height.

```python
A, C = map(float, input("Enter base and height: ").split())

area = (A * C) / 2

print("The area of this triangle is: " + str(area))
```

    Enter base and height: 4 5
    The area of this triangle is: 10.0

    b) the area of the circle of radius C. (pi = 3.14159)

```python
C = float(input("Enter radius: "))

import math
area = (C * C) * math.pi

print("The area of this circle is: " + str(area))
```

    Enter radius: 5
    The area of this circle is: 78.53981633974483

    c) the area of the trapezoid that has A and B for bases and C for height.

```python
A, B, C = map(float, input("Enter bases and height: ").split())

area = ((A + B) / 2) * C

print("The area of this trapezoid is: " + str(area))
```

    Enter bases and height: 6 5 4
    The area of this trapezoid is: 22.0

    d) the area of the square that has side B.

```python
B = float(input("Enter side: "))

area = B * B

print("The area of this square is: " + str(area))
```

    Enter side: 4
    The area of this square is: 16.0

    e) the area of the rectangle that has sides A and B.

```python
A, B = map(float, input("Enter base and height: ").split())

area = A * B

print("The area of this rectangle is: " + str(area))
```

    Enter base and height: 3 5
    The area of this rectangle is: 15.0

#### 8) Read **the values a, b and c** and calculate the **roots of the second degree equation** $ax^{2}+bx+c=0$ using [this formula](https://en.wikipedia.org/wiki/Quadratic_equation). If it is not possible to calculate the roots, display the message **“There are no real roots”**.

```python
import cmath

a, b, c = map(float, input("Enter numbers: ").split())

d = (b**2) - (4 *a * c)

r1 = (-b - cmath.sqrt(d)) / (2 * a)
r2 = (-b + cmath.sqrt(d)) / (2 * a)

if(d < 0):
    print("There are no real roots")
else:
    print(r1, r2)
```

    Enter numbers: 2 1 1
    There are no real roots

#### 9) Read four floating point numerical values corresponding to the coordinates of two geographical coordinates in the cartesian plane. Each point will come in a line with its coordinates separated by space. Then calculate and show the distance between these two points.

(obs: $d=\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$)

```python
import math

p1 = []
p2 = []

x1, y1 = map(float, input("Enter first point coordinates: ").split())
x2, y2 = map(float, input("Enter second point coordinates: ").split())

p1.append(x1)
p1.append(y1)
p2.append(x2)
p2.append(y2)

distance = math.sqrt(((p1[0] - p2[0])**2) + ((p1[1] - p2[1])**2))

print(distance)
```

    Enter first point coordinates: 5 6
    Enter second point coordinates: 3 5
    2.23606797749979

#### 10) Read **two floating point numbers** on a line that represent **coordinates of a cartesian point**. With this, use **conditional structures** to determine if you are at the origin, printing the message **'origin'**; in one of the axes, printing **'x axis'** or **'y axis'**; or in one of the four quadrants, printing **'q1'**, **'q2**', **'q3'** or **'q4'**.

```python
x, y  = map(float, input("Enter x-value and y-value: ").split())

if x > 0 and y > 0:
    print("q1")
elif x < 0 and y > 0:
    print("q2")
elif x < 0 and y < 0:
    print("q3")
elif x > 0 and y < 0:
    print("q4")
else:
    print("origin")
```

    Enter x-value and y-value: 1 -4
    q4

#### 11) Read an integer that represents a phone code for international dialing.
#### Then, inform to which country the code belongs to, considering the generated table below:
(You just need to consider the first 10 entries)

```python
import pandas as pd
df = pd.read_html('https://en.wikipedia.org/wiki/Telephone_numbers_in_Europe')[1]
df = df.iloc[:,:2]
df.head(20)
```

      Country
      Country calling code

      0
      Austria
      43

      1
      Belgium
      32

      2
      Bulgaria
      359

      3
      Croatia
      385

      4
      Cyprus
      357

      5
      Czech Republic
      420

      6
      Denmark
      45

      7
      Estonia
      372

      8
      Finland
      358

      9
      France
      33

      10
      Germany
      49

      11
      Greece
      30

      12
      Hungary
      36

      13
      Iceland
      354

      14
      Ireland
      353

      15
      Italy
      39

      16
      Latvia
      371

      17
      Liechtenstein
      423

      18
      Lithuania
      370

      19
      Luxembourg
      352

```python
n = int(input("Enter phone code: "))

if(n == 43):
    print("Austria")
elif(n == 32):
    print("Belgium")
elif(n == 359):
    print("Bulgaria")
elif(n == 385):
    print("Croatia")
elif(n == 357):
    print("Cyprus")
elif(n == 420):
    print("Czech Republic")
elif(n == 45):
    print("Denmark")
elif(n == 372):
    print("Estonia")
elif(n == 358):
    print("Finland")
elif(n == 33):
    print("France")
```

    Enter phone code: 33
    France

#### 12) Write a piece of code that reads 6 numbers in a row. Next, show the number of positive values entered. On the next line, print the average of the values to one decimal place.

```python
inputs = float(input("Enter Number: "))
list = []

while len(list) < 5:
    list.append(inputs)
    inputs = float(input("Enter Num: "))

pos_count = 1

for num in list:
    if num >= 0:
        pos_count += 1

print("number of positive numbers", pos_count)
print("average of values", round(sum(list) / len(list), 1))
```

#### 13) Read an integer **N**. Then print the **square of each of the even values**, from 1 to N, including N, if applicable, arranged one per line.

```python
n = int(input("Enter Number: "))

for n in range(1, n + 1):
    if n % 2 == 0:
        print(n * n)
```

#### 14) Using **input()**, read an integer and print its classification as **'even / odd'** and **'positive / negative'** . The two classes for the number must be printed on the same line separated by a space. In the case of zero, print only **'null'**.

```python
n = int(input("Enter a number: "))

if(n < 0 and n%2 != 0):
    print("negative odd")
elif(n > 0 and n%2 == 0):
    print("positive even")
elif(n < 0 and n%2 == 0):
    print("negative even")
elif(n > 0 and n%2 != 0):
    print("positive odd")
elif(n == 0):
    print("null")
```

## Challenge
#### 15) Ordering problems are recurrent in the history of programming. Over time, several algorithms have been developed to fulfill this function. The simplest of these algorithms is the [**Bubble Sort**](https://en.wikipedia.org/wiki/Bubble_sort), which is based on comparisons of elements two by two in a loop of passes through the elements. Your mission, if you decide to accept it, will be to input six whole numbers ramdonly ordered. Then implement the **Bubble Sort** principle to order these six numbers **using only loops and conditionals**.
#### At the end, print the six numbers in ascending order on a single line separated by spaces.

```python
inputs = int(input("Enter Number: "))
list = []

while len(list) < 6:
    list.append(inputs)
    inputs = int(input("Enter Number: "))

count = len(list)

for outer in range(count - 1):
    for i in range(count - outer - 1):
        if list[i] > list[i + 1]:
            list[i], list[i + 1] = list[i + 1], list[i]

print(*list)
```