```python
# This mounts your Google Drive to the Colab VM.
from google.colab import drive
drive.mount('/content/drive')

# TODO: Enter the foldername in your Drive where you have saved the unzipped
# assignment folder, e.g. 'cs231n/assignments/assignment1/'
FOLDERNAME = 'CS231N/assignment/assignment2/'
assert FOLDERNAME is not None, "[!] Enter the foldername."

# Now that we've mounted your Drive, this ensures that
# the Python interpreter of the Colab VM can load
# python files from within it.
import sys
sys.path.append('/content/drive/My Drive/{}'.format(FOLDERNAME))

# This downloads the CIFAR-10 dataset to your Drive
# if it doesn't already exist.
%cd /content/drive/My\ Drive/$FOLDERNAME/cs231n/datasets/
!bash get_datasets.sh
%cd /content/drive/My\ Drive/$FOLDERNAME
```

    Mounted at /content/drive
    /content/drive/My Drive/CS231N/assignment/assignment2/cs231n/datasets
    /content/drive/My Drive/CS231N/assignment/assignment2

# Batch Normalization
One way to make deep networks easier to train is to use more sophisticated optimization procedures such as SGD+momentum, RMSProp, or Adam. Another strategy is to change the architecture of the network to make it easier to train. One idea along these lines is batch normalization, proposed by [1] in 2015.

To understand the goal of batch normalization, it is important to first recognize that machine learning methods tend to perform better with input data consisting of uncorrelated features with zero mean and unit variance. When training a neural network, we can preprocess the data before feeding it to the network to explicitly decorrelate its features. This will ensure that the first layer of the network sees data that follows a nice distribution. However, even if we preprocess the input data, the activations at deeper layers of the network will likely no longer be decorrelated and will no longer have zero mean or unit variance, since they are output from earlier layers in the network. Even worse, during the training process the distribution of features at each layer of the network will shift as the weights of each layer are updated.

The authors of [1] hypothesize that the shifting distribution of features inside deep neural networks may make training deep networks more difficult. To overcome this problem, they propose to insert into the network layers that normalize batches. At training time, such a layer uses a minibatch of data to estimate the mean and standard deviation of each feature. These estimated means and standard deviations are then used to center and normalize the features of the minibatch. A running average of these means and standard deviations is kept during training, and at test time these running averages are used to center and normalize features.

It is possible that this normalization strategy could reduce the representational power of the network, since it may sometimes be optimal for certain layers to have features that are not zero-mean or unit variance. To this end, the batch normalization layer includes learnable shift and scale parameters for each feature dimension.

[1] [Sergey Ioffe and Christian Szegedy, "Batch Normalization: Accelerating Deep Network Training by Reducing
Internal Covariate Shift", ICML 2015.](https://arxiv.org/abs/1502.03167)

```python
# Setup cell.
import time
import numpy as np
import matplotlib.pyplot as plt
from cs231n.classifiers.fc_net import *
from cs231n.data_utils import get_CIFAR10_data
from cs231n.gradient_check import eval_numerical_gradient, eval_numerical_gradient_array
from cs231n.solver import Solver

%matplotlib inline
plt.rcParams["figure.figsize"] = (10.0, 8.0)  # Set default size of plots.
plt.rcParams["image.interpolation"] = "nearest"
plt.rcParams["image.cmap"] = "gray"

%load_ext autoreload
%autoreload 2

def rel_error(x, y):
    """Returns relative error."""
    return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))

def print_mean_std(x,axis=0):
    print(f"  means: {x.mean(axis=axis)}")
    print(f"  stds:  {x.std(axis=axis)}\n")
```

```python
# Load the (preprocessed) CIFAR-10 data.
data = get_CIFAR10_data()
for k, v in list(data.items()):
    print(f"{k}: {v.shape}")
```

    X_train: (49000, 3, 32, 32)
    y_train: (49000,)
    X_val: (1000, 3, 32, 32)
    y_val: (1000,)
    X_test: (1000, 3, 32, 32)
    y_test: (1000,)

# Batch Normalization: Forward Pass
In the file `cs231n/layers.py`, implement the batch normalization forward pass in the function `batchnorm_forward`. Once you have done so, run the following to test your implementation.

Referencing the paper linked to above in [1] may be helpful!

```python
# Check the training-time forward pass by checking means and variances
# of features both before and after batch normalization

# Simulate the forward pass for a two-layer network.
np.random.seed(231)
N, D1, D2, D3 = 200, 50, 60, 3
X = np.random.randn(N, D1)
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)
a = np.maximum(0, X.dot(W1)).dot(W2)

print('Before batch normalization:')
print_mean_std(a,axis=0)

gamma = np.ones((D3,))
beta = np.zeros((D3,))

# Means should be close to zero and stds close to one.
print('After batch normalization (gamma=1, beta=0)')
a_norm, _ = batchnorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=0)

gamma = np.asarray([1.0, 2.0, 3.0])
beta = np.asarray([11.0, 12.0, 13.0])

# Now means should be close to beta and stds close to gamma.
print('After batch normalization (gamma=', gamma, ', beta=', beta, ')')
a_norm, _ = batchnorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=0)
```

    Before batch normalization:
      means: [ -2.3814598  -13.18038246   1.91780462]
      stds:  [27.18502186 34.21455511 37.68611762]

    After batch normalization (gamma=1, beta=0)
      means: [5.32907052e-17 7.04991621e-17 1.85962357e-17]
      stds:  [0.99999999 1.         1.        ]

    After batch normalization (gamma= [1. 2. 3.] , beta= [11. 12. 13.] )
      means: [11. 12. 13.]
      stds:  [0.99999999 1.99999999 2.99999999]

```python
# Check the test-time forward pass by running the training-time
# forward pass many times to warm up the running averages, and then
# checking the means and variances of activations after a test-time
# forward pass.

np.random.seed(231)
N, D1, D2, D3 = 200, 50, 60, 3
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)

bn_param = {'mode': 'train'}
gamma = np.ones(D3)
beta = np.zeros(D3)

for t in range(50):
  X = np.random.randn(N, D1)
  a = np.maximum(0, X.dot(W1)).dot(W2)
  batchnorm_forward(a, gamma, beta, bn_param)

bn_param['mode'] = 'test'
X = np.random.randn(N, D1)
a = np.maximum(0, X.dot(W1)).dot(W2)
a_norm, _ = batchnorm_forward(a, gamma, beta, bn_param)

# Means should be close to zero and stds close to one, but will be
# noisier than training-time forward passes.
print('After batch normalization (test-time):')
print_mean_std(a_norm,axis=0)
```

    After batch normalization (test-time):
      means: [-0.03927354 -0.04349152 -0.10452688]
      stds:  [1.01531428 1.01238373 0.97819988]

# Batch Normalization: Backward Pass
Now implement the backward pass for batch normalization in the function `batchnorm_backward`.

To derive the backward pass you should write out the computation graph for batch normalization and backprop through each of the intermediate nodes. Some intermediates may have multiple outgoing branches; make sure to sum gradients across these branches in the backward pass.

Once you have finished, run the following to numerically check your backward pass.

```python
# Gradient check batchnorm backward pass.
np.random.seed(231)
N, D = 4, 5
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

bn_param = {'mode': 'train'}
fx = lambda x: batchnorm_forward(x, gamma, beta, bn_param)[0]
fg = lambda a: batchnorm_forward(x, a, beta, bn_param)[0]
fb = lambda b: batchnorm_forward(x, gamma, b, bn_param)[0]

dx_num = eval_numerical_gradient_array(fx, x, dout)
da_num = eval_numerical_gradient_array(fg, gamma.copy(), dout)
db_num = eval_numerical_gradient_array(fb, beta.copy(), dout)

_, cache = batchnorm_forward(x, gamma, beta, bn_param)
dx, dgamma, dbeta = batchnorm_backward(dout, cache)

# You should expect to see relative errors between 1e-13 and 1e-8.
print('dx error: ', rel_error(dx_num, dx))
print('dgamma error: ', rel_error(da_num, dgamma))
print('dbeta error: ', rel_error(db_num, dbeta))
```

    dx error:  1.7029261167605239e-09
    dgamma error:  7.420414216247087e-13
    dbeta error:  2.8795057655839487e-12

# Batch Normalization: Alternative Backward Pass
In class we talked about two different implementations for the sigmoid backward pass. One strategy is to write out a computation graph composed of simple operations and backprop through all intermediate values. Another strategy is to work out the derivatives on paper. For example, you can derive a very simple formula for the sigmoid function's backward pass by simplifying gradients on paper.

Surprisingly, it turns out that you can do a similar simplification for the batch normalization backward pass too!

In the forward pass, given a set of inputs $X=\begin{bmatrix}x_1\\x_2\\...\\x_N\end{bmatrix}$,

we first calculate the mean $\mu$ and variance $v$.
With $\mu$ and $v$ calculated, we can calculate the standard deviation $\sigma$  and normalized data $Y$.
The equations and graph illustration below describe the computation ($y_i$ is the i-th element of the vector $Y$).

\begin{align}
& \mu=\frac{1}{N}\sum_{k=1}^N x_k  &  v=\frac{1}{N}\sum_{k=1}^N (x_k-\mu)^2 \\
& \sigma=\sqrt{v+\epsilon}         &  y_i=\frac{x_i-\mu}{\sigma}
\end{align}

The meat of our problem during backpropagation is to compute $\frac{\partial L}{\partial X}$, given the upstream gradient we receive, $\frac{\partial L}{\partial Y}.$ To do this, recall the chain rule in calculus gives us $\frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y} \cdot \frac{\partial Y}{\partial X}$.

The unknown/hard part is $\frac{\partial Y}{\partial X}$. We can find this by first deriving step-by-step our local gradients at
$\frac{\partial v}{\partial X}$, $\frac{\partial \mu}{\partial X}$,
$\frac{\partial \sigma}{\partial v}$,
$\frac{\partial Y}{\partial \sigma}$, and $\frac{\partial Y}{\partial \mu}$,
and then use the chain rule to compose these gradients (which appear in the form of vectors!) appropriately to compute $\frac{\partial Y}{\partial X}$.

If it's challenging to directly reason about the gradients over $X$ and $Y$ which require matrix multiplication, try reasoning about the gradients in terms of individual elements $x_i$ and $y_i$ first: in that case, you will need to come up with the derivations for $\frac{\partial L}{\partial x_i}$, by relying on the Chain Rule to first calculate the intermediate $\frac{\partial \mu}{\partial x_i}, \frac{\partial v}{\partial x_i}, \frac{\partial \sigma}{\partial x_i},$ then assemble these pieces to calculate $\frac{\partial y_i}{\partial x_i}$.

You should make sure each of the intermediary gradient derivations are all as simplified as possible, for ease of implementation.

After doing so, implement the simplified batch normalization backward pass in the function `batchnorm_backward_alt` and compare the two implementations by running the following. Your two implementations should compute nearly identical results, but the alternative implementation should be a bit faster.

```python
np.random.seed(231)
N, D = 100, 500
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

bn_param = {'mode': 'train'}
out, cache = batchnorm_forward(x, gamma, beta, bn_param)

t1 = time.time()
dx1, dgamma1, dbeta1 = batchnorm_backward(dout, cache)
t2 = time.time()
dx2, dgamma2, dbeta2 = batchnorm_backward_alt(dout, cache)
t3 = time.time()

print('dx difference: ', rel_error(dx1, dx2))
print('dgamma difference: ', rel_error(dgamma1, dgamma2))
print('dbeta difference: ', rel_error(dbeta1, dbeta2))
print('speedup: %.2fx' % ((t2 - t1) / (t3 - t2)))
```

    dx difference:  6.284600172572596e-13
    dgamma difference:  0.0
    dbeta difference:  0.0
    speedup: 2.03x

# Fully Connected Networks with Batch Normalization
Now that you have a working implementation for batch normalization, go back to your `FullyConnectedNet` in the file `cs231n/classifiers/fc_net.py`. Modify your implementation to add batch normalization.

Concretely, when the `normalization` flag is set to `"batchnorm"` in the constructor, you should insert a batch normalization layer before each ReLU nonlinearity. The outputs from the last layer of the network should not be normalized. Once you are done, run the following to gradient-check your implementation.

**Hint:** You might find it useful to define an additional helper layer similar to those in the file `cs231n/layer_utils.py`.

```python
np.random.seed(231)
N, D, H1, H2, C = 2, 15, 20, 30, 10
X = np.random.randn(N, D)
y = np.random.randint(C, size=(N,))

# You should expect losses between 1e-4~1e-10 for W,
# losses between 1e-08~1e-10 for b,
# and losses between 1e-08~1e-09 for beta and gammas.
for reg in [0, 3.14]:
  print('Running check with reg = ', reg)
  model = FullyConnectedNet([H1, H2], input_dim=D, num_classes=C,
                            reg=reg, weight_scale=5e-2, dtype=np.float64,
                            normalization='batchnorm')

  loss, grads = model.loss(X, y)
  print('Initial loss: ', loss)

  for name in sorted(grads):
    f = lambda _: model.loss(X, y)[0]
    grad_num = eval_numerical_gradient(f, model.params[name], verbose=False, h=1e-5)
    print('%s relative error: %.2e' % (name, rel_error(grad_num, grads[name])))
  if reg == 0: print()
```

    Running check with reg =  0
    Initial loss:  2.261195509168841
    W1 relative error: 1.10e-04
    W2 relative error: 3.89e-06
    W3 relative error: 8.12e-10
    b1 relative error: 2.22e-03
    b2 relative error: 5.55e-09
    b3 relative error: 5.81e-10
    beta1 relative error: 6.82e-09
    beta2 relative error: 2.40e-09
    gamma1 relative error: 1.83e-08
    gamma2 relative error: 2.82e-09

    Running check with reg =  3.14
    Initial loss:  6.996533219149863
    W1 relative error: 1.98e-06
    W2 relative error: 2.28e-06
    W3 relative error: 2.23e-08
    b1 relative error: 5.55e-09
    b2 relative error: 2.22e-08
    b3 relative error: 7.06e-10
    beta1 relative error: 6.32e-09
    beta2 relative error: 4.34e-09
    gamma1 relative error: 5.61e-09
    gamma2 relative error: 5.28e-09

# Batch Normalization for Deep Networks
Run the following to train a six-layer network on a subset of 1000 training examples both with and without batch normalization.

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

# Try training a very deep net with batchnorm.
hidden_dims = [100, 100, 100, 100, 100]

num_train = 1000
small_data = {
  'X_train': data['X_train'][:num_train],
  'y_train': data['y_train'][:num_train],
  'X_val': data['X_val'],
  'y_val': data['y_val'],
}

weight_scale = 2e-2
bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization='batchnorm')
model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=None)

print('Solver with batch norm:')
bn_solver = Solver(bn_model, small_data,
                num_epochs=10, batch_size=50,
                update_rule='adam',
                optim_config={
                  'learning_rate': 1e-3,
                },
                verbose=True,print_every=20)
bn_solver.train()

print('\nSolver without batch norm:')
solver = Solver(model, small_data,
                num_epochs=10, batch_size=50,
                update_rule='adam',
                optim_config={
                  'learning_rate': 1e-3,
                },
                verbose=True, print_every=20)
solver.train()
```

    Solver with batch norm:
    (Iteration 1 / 200) loss: 2.340974
    (Epoch 0 / 10) train acc: 0.107000; val_acc: 0.115000
    (Epoch 1 / 10) train acc: 0.313000; val_acc: 0.265000
    (Iteration 21 / 200) loss: 2.039345
    (Epoch 2 / 10) train acc: 0.396000; val_acc: 0.280000
    (Iteration 41 / 200) loss: 2.047471
    (Epoch 3 / 10) train acc: 0.484000; val_acc: 0.316000
    (Iteration 61 / 200) loss: 1.739554
    (Epoch 4 / 10) train acc: 0.525000; val_acc: 0.318000
    (Iteration 81 / 200) loss: 1.246973
    (Epoch 5 / 10) train acc: 0.595000; val_acc: 0.335000
    (Iteration 101 / 200) loss: 1.354766
    (Epoch 6 / 10) train acc: 0.638000; val_acc: 0.331000
    (Iteration 121 / 200) loss: 1.014049
    (Epoch 7 / 10) train acc: 0.673000; val_acc: 0.323000
    (Iteration 141 / 200) loss: 1.135644
    (Epoch 8 / 10) train acc: 0.682000; val_acc: 0.305000
    (Iteration 161 / 200) loss: 0.652241
    (Epoch 9 / 10) train acc: 0.781000; val_acc: 0.336000
    (Iteration 181 / 200) loss: 0.782712
    (Epoch 10 / 10) train acc: 0.749000; val_acc: 0.318000

    Solver without batch norm:
    (Iteration 1 / 200) loss: 2.302332
    (Epoch 0 / 10) train acc: 0.129000; val_acc: 0.131000
    (Epoch 1 / 10) train acc: 0.283000; val_acc: 0.250000
    (Iteration 21 / 200) loss: 2.041970
    (Epoch 2 / 10) train acc: 0.316000; val_acc: 0.277000
    (Iteration 41 / 200) loss: 1.900473
    (Epoch 3 / 10) train acc: 0.373000; val_acc: 0.282000
    (Iteration 61 / 200) loss: 1.713156
    (Epoch 4 / 10) train acc: 0.390000; val_acc: 0.310000
    (Iteration 81 / 200) loss: 1.662209
    (Epoch 5 / 10) train acc: 0.434000; val_acc: 0.300000
    (Iteration 101 / 200) loss: 1.696059
    (Epoch 6 / 10) train acc: 0.535000; val_acc: 0.345000
    (Iteration 121 / 200) loss: 1.557987
    (Epoch 7 / 10) train acc: 0.530000; val_acc: 0.304000
    (Iteration 141 / 200) loss: 1.432189
    (Epoch 8 / 10) train acc: 0.628000; val_acc: 0.339000
    (Iteration 161 / 200) loss: 1.034116
    (Epoch 9 / 10) train acc: 0.654000; val_acc: 0.342000
    (Iteration 181 / 200) loss: 0.905794
    (Epoch 10 / 10) train acc: 0.712000; val_acc: 0.328000

Run the following to visualize the results from two networks trained above. You should find that using batch normalization helps the network to converge much faster.

```python
def plot_training_history(title, label, baseline, bn_solvers, plot_fn, bl_marker='.', bn_marker='.', labels=None):
    """utility function for plotting training history"""
    plt.title(title)
    plt.xlabel(label)
    bn_plots = [plot_fn(bn_solver) for bn_solver in bn_solvers]
    bl_plot = plot_fn(baseline)
    num_bn = len(bn_plots)
    for i in range(num_bn):
        label='with_norm'
        if labels is not None:
            label += str(labels[i])
        plt.plot(bn_plots[i], bn_marker, label=label)
    label='baseline'
    if labels is not None:
        label += str(labels[0])
    plt.plot(bl_plot, bl_marker, label=label)
    plt.legend(loc='lower center', ncol=num_bn+1)

plt.subplot(3, 1, 1)
plot_training_history('Training loss','Iteration', solver, [bn_solver], \
                      lambda x: x.loss_history, bl_marker='o', bn_marker='o')
plt.subplot(3, 1, 2)
plot_training_history('Training accuracy','Epoch', solver, [bn_solver], \
                      lambda x: x.train_acc_history, bl_marker='-o', bn_marker='-o')
plt.subplot(3, 1, 3)
plot_training_history('Validation accuracy','Epoch', solver, [bn_solver], \
                      lambda x: x.val_acc_history, bl_marker='-o', bn_marker='-o')

plt.gcf().set_size_inches(15, 15)
plt.show()
```

# Batch Normalization and Initialization
We will now run a small experiment to study the interaction of batch normalization and weight initialization.

The first cell will train eight-layer networks both with and without batch normalization using different scales for weight initialization. The second layer will plot training accuracy, validation set accuracy, and training loss as a function of the weight initialization scale.

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

# Try training a very deep net with batchnorm.
hidden_dims = [50, 50, 50, 50, 50, 50, 50]
num_train = 1000
small_data = {
  'X_train': data['X_train'][:num_train],
  'y_train': data['y_train'][:num_train],
  'X_val': data['X_val'],
  'y_val': data['y_val'],
}

bn_solvers_ws = {}
solvers_ws = {}
weight_scales = np.logspace(-4, 0, num=20)
for i, weight_scale in enumerate(weight_scales):
    print('Running weight scale %d / %d' % (i + 1, len(weight_scales)))
    bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization='batchnorm')
    model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=None)

    bn_solver = Solver(bn_model, small_data,
                  num_epochs=10, batch_size=50,
                  update_rule='adam',
                  optim_config={
                    'learning_rate': 1e-3,
                  },
                  verbose=False, print_every=200)
    bn_solver.train()
    bn_solvers_ws[weight_scale] = bn_solver

    solver = Solver(model, small_data,
                  num_epochs=10, batch_size=50,
                  update_rule='adam',
                  optim_config={
                    'learning_rate': 1e-3,
                  },
                  verbose=False, print_every=200)
    solver.train()
    solvers_ws[weight_scale] = solver
```

    Running weight scale 1 / 20
    Running weight scale 2 / 20
    Running weight scale 3 / 20
    Running weight scale 4 / 20
    Running weight scale 5 / 20
    Running weight scale 6 / 20
    Running weight scale 7 / 20
    Running weight scale 8 / 20
    Running weight scale 9 / 20
    Running weight scale 10 / 20
    Running weight scale 11 / 20
    Running weight scale 12 / 20
    Running weight scale 13 / 20
    Running weight scale 14 / 20
    Running weight scale 15 / 20
    Running weight scale 16 / 20
    Running weight scale 17 / 20
    Running weight scale 18 / 20
    Running weight scale 19 / 20
    Running weight scale 20 / 20

```python
# Plot results of weight scale experiment.
best_train_accs, bn_best_train_accs = [], []
best_val_accs, bn_best_val_accs = [], []
final_train_loss, bn_final_train_loss = [], []

for ws in weight_scales:
  best_train_accs.append(max(solvers_ws[ws].train_acc_history))
  bn_best_train_accs.append(max(bn_solvers_ws[ws].train_acc_history))

  best_val_accs.append(max(solvers_ws[ws].val_acc_history))
  bn_best_val_accs.append(max(bn_solvers_ws[ws].val_acc_history))

  final_train_loss.append(np.mean(solvers_ws[ws].loss_history[-100:]))
  bn_final_train_loss.append(np.mean(bn_solvers_ws[ws].loss_history[-100:]))

plt.subplot(3, 1, 1)
plt.title('Best val accuracy vs. weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Best val accuracy')
plt.semilogx(weight_scales, best_val_accs, '-o', label='baseline')
plt.semilogx(weight_scales, bn_best_val_accs, '-o', label='batchnorm')
plt.legend(ncol=2, loc='lower right')

plt.subplot(3, 1, 2)
plt.title('Best train accuracy vs. weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Best training accuracy')
plt.semilogx(weight_scales, best_train_accs, '-o', label='baseline')
plt.semilogx(weight_scales, bn_best_train_accs, '-o', label='batchnorm')
plt.legend()

plt.subplot(3, 1, 3)
plt.title('Final training loss vs. weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Final training loss')
plt.semilogx(weight_scales, final_train_loss, '-o', label='baseline')
plt.semilogx(weight_scales, bn_final_train_loss, '-o', label='batchnorm')
plt.legend()
plt.gca().set_ylim(1.0, 3.5)

plt.gcf().set_size_inches(15, 15)
plt.show()
```

## Inline Question 1:
Describe the results of this experiment. How does the weight initialization scale affect models with/without batch normalization differently, and why?

## Answer:

Batch normalization is less sensisive to the weight initialization

# Batch Normalization and Batch Size
We will now run a small experiment to study the interaction of batch normalization and batch size.

The first cell will train 6-layer networks both with and without batch normalization using different batch sizes. The second layer will plot training accuracy and validation set accuracy over time.

```python
def run_batchsize_experiments(normalization_mode):
    np.random.seed(231)

    # Try training a very deep net with batchnorm.
    hidden_dims = [100, 100, 100, 100, 100]
    num_train = 1000
    small_data = {
      'X_train': data['X_train'][:num_train],
      'y_train': data['y_train'][:num_train],
      'X_val': data['X_val'],
      'y_val': data['y_val'],
    }
    n_epochs=10
    weight_scale = 2e-2
    batch_sizes = [5,10,50]
    lr = 10**(-3.5)
    solver_bsize = batch_sizes[0]

    print('No normalization: batch size = ',solver_bsize)
    model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=None)
    solver = Solver(model, small_data,
                    num_epochs=n_epochs, batch_size=solver_bsize,
                    update_rule='adam',
                    optim_config={
                      'learning_rate': lr,
                    },
                    verbose=False)
    solver.train()

    bn_solvers = []
    for i in range(len(batch_sizes)):
        b_size=batch_sizes[i]
        print('Normalization: batch size = ',b_size)
        bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=normalization_mode)
        bn_solver = Solver(bn_model, small_data,
                        num_epochs=n_epochs, batch_size=b_size,
                        update_rule='adam',
                        optim_config={
                          'learning_rate': lr,
                        },
                        verbose=False)
        bn_solver.train()
        bn_solvers.append(bn_solver)

    return bn_solvers, solver, batch_sizes

batch_sizes = [5,10,50]
bn_solvers_bsize, solver_bsize, batch_sizes = run_batchsize_experiments('batchnorm')
```

    No normalization: batch size =  5
    Normalization: batch size =  5
    Normalization: batch size =  10
    Normalization: batch size =  50

```python
plt.subplot(2, 1, 1)
plot_training_history('Training accuracy (Batch Normalization)','Epoch', solver_bsize, bn_solvers_bsize, \
                      lambda x: x.train_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)
plt.subplot(2, 1, 2)
plot_training_history('Validation accuracy (Batch Normalization)','Epoch', solver_bsize, bn_solvers_bsize, \
                      lambda x: x.val_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)

plt.gcf().set_size_inches(15, 10)
plt.show()
```

## Inline Question 2:
Describe the results of this experiment. What does this imply about the relationship between batch normalization and batch size? Why is this relationship observed?

## Answer:

If the batch size is too small, the accuracy can goes down because it cannot represent the whole dataset.

# Layer Normalization
Batch normalization has proved to be effective in making networks easier to train, but the dependency on batch size makes it less useful in complex networks which have a cap on the input batch size due to hardware limitations.

Several alternatives to batch normalization have been proposed to mitigate this problem; one such technique is Layer Normalization [2]. Instead of normalizing over the batch, we normalize over the features. In other words, when using Layer Normalization, each feature vector corresponding to a single datapoint is normalized based on the sum of all terms within that feature vector.

[2] [Ba, Jimmy Lei, Jamie Ryan Kiros, and Geoffrey E. Hinton. "Layer Normalization." stat 1050 (2016): 21.](https://arxiv.org/pdf/1607.06450.pdf)

## Inline Question 3:
Which of these data preprocessing steps is analogous to batch normalization, and which is analogous to layer normalization?

1. Scaling each image in the dataset, so that the RGB channels for each row of pixels within an image sums up to 1.
2. Scaling each image in the dataset, so that the RGB channels for all pixels within an image sums up to 1.
3. Subtracting the mean image of the dataset from each image in the dataset.
4. Setting all RGB values to either 0 or 1 depending on a given threshold.

## Answer:

Batch: 1

Layer: 2

# Layer Normalization: Implementation

Now you'll implement layer normalization. This step should be relatively straightforward, as conceptually the implementation is almost identical to that of batch normalization. One significant difference though is that for layer normalization, we do not keep track of the moving moments, and the testing phase is identical to the training phase, where the mean and variance are directly calculated per datapoint.

Here's what you need to do:

* In `cs231n/layers.py`, implement the forward pass for layer normalization in the function `layernorm_forward`.

Run the cell below to check your results.
* In `cs231n/layers.py`, implement the backward pass for layer normalization in the function `layernorm_backward`.

Run the second cell below to check your results.
* Modify `cs231n/classifiers/fc_net.py` to add layer normalization to the `FullyConnectedNet`. When the `normalization` flag is set to `"layernorm"` in the constructor, you should insert a layer normalization layer before each ReLU nonlinearity.

Run the third cell below to run the batch size experiment on layer normalization.

```python
# Check the training-time forward pass by checking means and variances
# of features both before and after layer normalization.

# Simulate the forward pass for a two-layer network.
np.random.seed(231)
N, D1, D2, D3 =4, 50, 60, 3
X = np.random.randn(N, D1)
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)
a = np.maximum(0, X.dot(W1)).dot(W2)

print('Before layer normalization:')
print_mean_std(a,axis=1)

gamma = np.ones(D3)
beta = np.zeros(D3)

# Means should be close to zero and stds close to one.
print('After layer normalization (gamma=1, beta=0)')
a_norm, _ = layernorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=1)

gamma = np.asarray([3.0,3.0,3.0])
beta = np.asarray([5.0,5.0,5.0])

# Now means should be close to beta and stds close to gamma.
print('After layer normalization (gamma=', gamma, ', beta=', beta, ')')
a_norm, _ = layernorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=1)
```

    Before layer normalization:
      means: [-59.06673243 -47.60782686 -43.31137368 -26.40991744]
      stds:  [10.07429373 28.39478981 35.28360729  4.01831507]

    After layer normalization (gamma=1, beta=0)
      means: [ 4.81096644e-16 -7.40148683e-17  2.22044605e-16 -5.92118946e-16]
      stds:  [0.99999995 0.99999999 1.         0.99999969]

    After layer normalization (gamma= [3. 3. 3.] , beta= [5. 5. 5.] )
      means: [5. 5. 5. 5.]
      stds:  [2.99999985 2.99999998 2.99999999 2.99999907]

```python
# Gradient check batchnorm backward pass.
np.random.seed(231)
N, D = 4, 5
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

ln_param = {}
fx = lambda x: layernorm_forward(x, gamma, beta, ln_param)[0]
fg = lambda a: layernorm_forward(x, a, beta, ln_param)[0]
fb = lambda b: layernorm_forward(x, gamma, b, ln_param)[0]

dx_num = eval_numerical_gradient_array(fx, x, dout)
da_num = eval_numerical_gradient_array(fg, gamma.copy(), dout)
db_num = eval_numerical_gradient_array(fb, beta.copy(), dout)

_, cache = layernorm_forward(x, gamma, beta, ln_param)
dx, dgamma, dbeta = layernorm_backward(dout, cache)

# You should expect to see relative errors between 1e-12 and 1e-8.
print('dx error: ', rel_error(dx_num, dx))
print('dgamma error: ', rel_error(da_num, dgamma))
print('dbeta error: ', rel_error(db_num, dbeta))
```

    dx error:  1.4336158494902849e-09
    dgamma error:  4.519489546032799e-12
    dbeta error:  2.276445013433725e-12

# Layer Normalization and Batch Size

We will now run the previous batch size experiment with layer normalization instead of batch normalization. Compared to the previous experiment, you should see a markedly smaller influence of batch size on the training history!

```python
ln_solvers_bsize, solver_bsize, batch_sizes = run_batchsize_experiments('layernorm')

plt.subplot(2, 1, 1)
plot_training_history('Training accuracy (Layer Normalization)','Epoch', solver_bsize, ln_solvers_bsize, \
                      lambda x: x.train_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)
plt.subplot(2, 1, 2)
plot_training_history('Validation accuracy (Layer Normalization)','Epoch', solver_bsize, ln_solvers_bsize, \
                      lambda x: x.val_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)

plt.gcf().set_size_inches(15, 10)
plt.show()
```

## Inline Question 4:
When is layer normalization likely to not work well, and why?

1. Using it in a very deep network
2. Having a very small dimension of features
3. Having a high regularization term

## Answer:

2. If the dimension is very small, then the mean and variance are not representable for the data.


# Lambda School Data Science Module 143

## Introduction to Bayesian Inference

!['Detector! What would the Bayesian statistician say if I asked him whether the--' [roll] 'I AM A NEUTRINO DETECTOR, NOT A LABYRINTH GUARD. SERIOUSLY, DID YOUR BRAIN FALL OUT?' [roll] '... yes.'](https://imgs.xkcd.com/comics/frequentists_vs_bayesians.png)

*[XKCD 1132](https://www.xkcd.com/1132/)*

## Prepare - Bayes' Theorem and the Bayesian mindset

Bayes' theorem possesses a near-mythical quality - a bit of math that somehow magically evaluates a situation. But this mythicalness has more to do with its reputation and advanced applications than the actual core of it - deriving it is actually remarkably straightforward.

### The Law of Total Probability

By definition, the total probability of all outcomes (events) if some variable (event space) $A$ is 1. That is:

$$P(A) = \sum_n P(A_n) = 1$$

The law of total probability takes this further, considering two variables ($A$ and $B$) and relating their marginal probabilities (their likelihoods considered independently, without reference to one another) and their conditional probabilities (their likelihoods considered jointly). A marginal probability is simply notated as e.g. $P(A)$, while a conditional probability is notated $P(A|B)$, which reads "probability of $A$ *given* $B$".

The law of total probability states:

$$P(A) = \sum_n P(A | B_n) P(B_n)$$

In words - the total probability of $A$ is equal to the sum of the conditional probability of $A$ on any given event $B_n$ times the probability of that event $B_n$, and summed over all possible events in $B$.

### The Law of Conditional Probability

What's the probability of something conditioned on something else? To determine this we have to go back to set theory and think about the intersection of sets:

The formula for actual calculation:

$$P(A|B) = \frac{P(A \cap B)}{P(B)}$$

Think of the overall rectangle as the whole probability space, $A$ as the left circle, $B$ as the right circle, and their intersection as the red area. Try to visualize the ratio being described in the above formula, and how it is different from just the $P(A)$ (not conditioned on $B$).

We can see how this relates back to the law of total probability - multiply both sides by $P(B)$ and you get $P(A|B)P(B) = P(A \cap B)$ - replaced back into the law of total probability we get $P(A) = \sum_n P(A \cap B_n)$.

This may not seem like an improvement at first, but try to relate it back to the above picture - if you think of sets as physical objects, we're saying that the total probability of $A$ given $B$ is all the little pieces of it intersected with $B$, added together. The conditional probability is then just that again, but divided by the probability of $B$ itself happening in the first place.

\begin{align}
P(A|B) &= \frac{P(A \cap B)}{P(B)}\\
\Rightarrow P(A|B)P(B) &= P(A \cap B)\\
P(B|A) &= \frac{P(B \cap A)}{P(A)}\\
\Rightarrow P(B|A)P(A) &= P(B \cap A)\\
\Rightarrow P(A|B)P(B) &= P(B|A)P(A) \\
P(A \cap B) &= P(B \cap A)\\
P(A|B) &= \frac{P(B|A) \times P(A)}{P(B)}
\end{align}

### Bayes Theorem

Here is is, the seemingly magic tool:

$$P(A|B) = \frac{P(B|A)P(A)}{P(B)}$$

In words - the probability of $A$ conditioned on $B$ is the probability of $B$ conditioned on $A$, times the probability of $A$ and divided by the probability of $B$. These unconditioned probabilities are referred to as "prior beliefs", and the conditioned probabilities as "updated."

Why is this important? Scroll back up to the XKCD example - the Bayesian statistician draws a less absurd conclusion because their prior belief in the likelihood that the sun will go nova is extremely low. So, even when updated based on evidence from a detector that is $35/36 = 0.972$ accurate, the prior belief doesn't shift enough to change their overall opinion.

There's many examples of Bayes' theorem - one less absurd example is to apply to [breathalyzer tests](https://www.bayestheorem.net/breathalyzer-example/). You may think that a breathalyzer test that is 100% accurate for true positives (detecting somebody who is drunk) is pretty good, but what if it also has 8% false positives (indicating somebody is drunk when they're not)? And furthermore, the rate of drunk driving (and thus our prior belief)  is 1/1000.

What is the likelihood somebody really is drunk if they test positive? Some may guess it's 92% - the difference between the true positives and the false positives. But we have a prior belief of the background/true rate of drunk driving. Sounds like a job for Bayes' theorem!

$$
\begin{aligned}
P(Drunk | Positive) &= \frac{P(Positive | Drunk)P(Drunk)}{P(Positive)} \\
&= \frac{1 \times 0.001}{0.08} \\
&= 0.0125
\end{aligned}
$$

In other words, the likelihood that somebody is drunk given they tested positive with a breathalyzer in this situation is only 1.25% - probably much lower than you'd guess. This is why, in practice, it's important to have a repeated test to confirm (the probability of two false positives in a row is $0.08 * 0.08 = 0.0064$, much lower), and Bayes' theorem has been relevant in court cases where proper consideration of evidence was important.

Source: <https://en.wikipedia.org/wiki/Bayes%27_theorem>

## Live Lecture - Deriving Bayes' Theorem, Calculating Bayesian Confidence

Notice that $P(A|B)$ appears in the above laws - in Bayesian terms, this is the belief in $A$ updated for the evidence $B$. So all we need to do is solve for this term to derive Bayes' theorem. Let's do it together!

```
# Activity 2 - Use SciPy to calculate Bayesian confidence intervals
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.bayes_mvs.html#scipy.stats.bayes_mvs
```

```
from scipy import stats
import numpy as np

np.random.seed(seed=42)

coinflips = np.random.binomial(n=1, p=.5, size=100)
print(coinflips)
```

    [0 1 1 1 0 0 0 1 1 1 0 1 1 0 0 0 0 1 0 0 1 0 0 0 0 1 0 1 1 0 1 0 0 1 1 1 0
     0 1 0 0 0 0 1 0 1 0 1 1 0 1 1 1 1 1 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 1 0 0 1
     1 1 1 0 0 0 1 1 0 0 0 0 1 1 1 0 0 1 1 1 1 0 1 0 0 0]

```
def confidence_interval(data, confidence=.95):
  n = len(data)
  mean = sum(data)/n
  data = np.array(data)
  stderr = stats.sem(data)
  interval = stderr * stats.t.ppf((1 + confidence) / 2.0, n-1)
  return (mean , mean-interval, mean+interval)
```

```
confidence_interval(coinflips, confidence=.95)
```

    (0.47, 0.3704689875017368, 0.5695310124982632)

```
mean_CI, _, _ = stats.bayes_mvs(coinflips, alpha=.95)

mean_CI
```

    Mean(statistic=0.47, minmax=(0.37046898750173674, 0.5695310124982632))

```
??stats.bayes_mvs
```

```
coinflips_mean_dist, _, _ = stats.mvsdist(coinflips)
coinflips_mean_dist
```

    <scipy.stats._distn_infrastructure.rv_frozen at 0x7f38d4c9f390>

```
coinflips_mean_dist.rvs(1000)
```

## Assignment - Code it up!

Most of the above was pure math - now write Python code to reproduce the results! This is purposefully open ended - you'll have to think about how you should represent probabilities and events. You can and should look things up, and as a stretch goal - refactor your code into helpful reusable functions!

Specific goals/targets:

1. Write a function `def prob_drunk_given_positive(prob_drunk_prior, prob_positive, prob_positive_drunk)` that reproduces the example from lecture, and use it to calculate and visualize a range of situations
2. Explore `scipy.stats.bayes_mvs` - read its documentation, and experiment with it on data you've tested in other ways earlier this week
3. Create a visualization comparing the results of a Bayesian approach to a traditional/frequentist approach
4. In your own words, summarize the difference between Bayesian and Frequentist statistics

If you're unsure where to start, check out [this blog post of Bayes theorem with Python](https://dataconomy.com/2015/02/introduction-to-bayes-theorem-with-python/) - you could and should create something similar!

Stretch goals:

- Apply a Bayesian technique to a problem you previously worked (in an assignment or project work) on from a frequentist (standard) perspective
- Check out [PyMC3](https://docs.pymc.io/) (note this goes beyond hypothesis tests into modeling) - read the guides and work through some examples
- Take PyMC3 further - see if you can build something with it!

```
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
```

```
# TODO - code!

def prob_drunk_given_positive(prob_drunk_prior, prob_positive, prob_positive_drunk):
  # reproduces the example from lecture, and use it to calculate and visualize a range of situations
  return prob_positive_drunk * prob_drunk_prior / prob_positive
```

```
i = 1
df = pd.DataFrame(columns=['index','post_prob'])
index = []
post_list = []
```

```
def prob_drunk_positive_recursive(prob_drunk_prior, false_positive, true_positive, n):
#   global result
  post_prob = true_positive * prob_drunk_prior / (false_positive + prob_drunk_prior)

  global i
  index.append(int(i))
  post_list.append(post_prob)
#   print(i, post_prob)
  i += 1

  while i < n:
    prob_drunk_positive_recursive(post_prob, false_positive, true_positive, n)
#   return result

```

```
prob_drunk_positive_recursive(.001, .08, 1, 11)
```

```
index, post_list
```

    ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
     [0.012345679012345678,
      0.1336898395721925,
      0.6256256256256255,
      0.8866254326732111,
      0.9172378490200085,
      0.9197784158727866,
      0.9199822693409904,
      0.9199985815221288,
      0.9199998865216094,
      0.9199999909217278])

```
df['index'] = np.array(index)
df['post_prob'] = np.array(post_list)
```

```
df.head(10)
```

      index
      post_prob

      0
      1
      0.012346

      1
      2
      0.133690

      2
      3
      0.625626

      3
      4
      0.886625

      4
      5
      0.917238

      5
      6
      0.919778

      6
      7
      0.919982

      7
      8
      0.919999

      8
      9
      0.920000

      9
      10
      0.920000

```
df = df[df['index'] <= 10]
df.shape
```

    (0, 2)

```
plt.style.use('fivethirtyeight')
fig, ax = plt.subplots(figsize=(8,6))
plt.ylim(-.05,1)

ax.axhline(y=0, color='black', linewidth=1.5, alpha=1)
ax.axhline(y=.92, color='black', linewidth=1.5, alpha=.5)
ax.axvline(x=0, color='black', linewidth=1.5, alpha=1)
ax.axvline(x=5, color='black', linewidth=1.5, alpha=.5)
plt.plot(df['index'], df['post_prob'], linestyle='--', marker='o', linewidth=2.0)

ax.set_yticks([0, .25, .50, .75, 1])
ax.set_yticklabels(labels=['0','25','50','75','100%'], fontsize=14)
```

After multiple instances of repeating the experiment, the end result % being nearly identical to one calculated using the frequentist approach. So the confidence interval visualizations of a bayesian confidence interval look identical their frequentist counterpart.

## Resources

- [Worked example of Bayes rule calculation](https://en.wikipedia.org/wiki/Bayes'_theorem#Examples) (helpful as it fully breaks out the denominator)
- [Source code for mvsdist in scipy](https://github.com/scipy/scipy/blob/90534919e139d2a81c24bf08341734ff41a3db12/scipy/stats/morestats.py#L139)


Introducción al estudio del Método de los Elementos Finitos

Una aproximación al análisis estático lineal

Sesion 01 - Introducción

 Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license.(c) Carlos Alberto Alvarez Henao

***

***Docente:*** Carlos Alberto Álvarez Henao, I.C. D.Sc.

***e-mail:*** carlosalvarezh@gmail.com

***skype:*** carlos.alberto.alvarez.henao

***Herramienta:*** [Jupyter Notebook](http://jupyter.org/)

***Kernel:*** Python 3.8

***MEDELLÍN - COLOMBIA***

***2021/02***

***

Tabla de Contenidos

1  Introducción

1.1  Mecánica

1.1.1  Modelación y Simulación Computacional

1.1.2  Discretización

1.1.3  Qué es un Elemento Finito?

1.2  Qué es el FEM?

1.2.1  FEM o FEA?

1.2.2  Aplicaciones del FEM

1.2.3  Ecuaciones Diferenciales en Derivadas Parciales

1.2.4  Cómo trabaja el FEM?

1.2.5  Historia del FEM

1.3  Diferentes tipos del FEM

1.3.1  Método de elementos finitos extendido (XFEM)

1.3.2  Método de elementos finitos generalizado (GFEM)

1.3.3  Método mixto de elementos finitos

1.3.4  Método de elementos finitos hp

1.3.5  Método de elementos finitos de Galerkin discontinuo (DG-FEM)

1.4  El proceso de análisis FEM

1.4.1  Modelado

1.4.2  Simulación

1.4.3  Terminología FEM

1.5  Pasos en la solución por el FEM

1.5.1  Discretizar la estructura en elementos.

1.5.2  Determinar la matriz de rigidez local para cada elemento

1.5.3  Ensamblar la matriz de rigidez global

1.5.4  Cálcular el vector de fuerza aplicada

1.5.5  Aplicar condiciones de contorno y resolver los desplazamientos nodales

1.5.6  Resolver las reacciones externas

1.5.7  Resolver las fuerzas nodales

1.5.8  Resolver las tensiones

1.6  Atributos de elementos

1.7  Aspectos computacionales del FEM

## Introducción

### Mecánica

La [Mecánica](https://en.wikipedia.org/wiki/Mechanics) es el área de la física que estudia el movimiento y reposo de los cuerpos, y su evolución con el tiempo, más específicamente la relación entre fuerza, materia y movimiento. Las fuerzas aplicadas a los objetos dan como resultados desplazamientos o cambios de la posición de un objeto en relación con su entorno.

Los problemas a ser tratados en este curso se enmarcan dentro de la [Mecánica computacional](https://en.wikipedia.org/wiki/Computational_mechanics), análisis estático lineal, como se observa en el siguiente esquema:

\begin{equation*}
\text{Mecánica} \left \{
\begin{aligned}
&\text{Teórica} \\
&\text{Computacional} \left \{
\begin{aligned}
&\text{Nano/micro} \\
&\text{Continuo} \left \{
\begin{aligned}
&\text{Fluidos} \\
&\text{Sólidos} \left \{
\begin{aligned}
&\text{Estática} \left \{
\begin{aligned}
&\text{Lineal} \\
&\text{No Lineal}
\end{aligned}
\right.
\\
&\text{Dinámica}
\end{aligned}
\right.
\\
&\text{Multifísica}
\end{aligned}
\right.
\\
&\text{Sistema}
\end{aligned}
\right.
\\
&\text{Aplicada}
\end{aligned}
\right.
\end{equation*}

#### Modelación y Simulación Computacional

La Ciencia Computacional e Ingeniería, es una disciplina que concierne al uso de métodos y dispositivos computacionales para simular eventos físicos y sistemas en ingeniería.

#### Discretización

El proceso de [discretización](https://en.wikipedia.org/wiki/Discretization) consiste en transformar un modelo continuo en un finito número de componentes discretos que pueden ser procesados por una máquina. El [modelo Computacional](https://en.wikipedia.org/wiki/Computational_model) es la versión discretizada de un modelo matemático que se ha desarrollado para ser implementado en una máquina de cómputo. Esta discplina incluye la [modelación matemática](https://en.wikipedia.org/wiki/Mathematical_model), que es un conjunto de ecuaciones algebraicas, diferenciales y/o integrales, que gobiernan un fenómeno físico de un sistema en particular. El modelo se basa en una serie de suposiciones y restricciones impuestas al fenómeno y de las leyes físicas gobernantes.

Algunos métodos empleados en la discretización espacial son:

- ***[Elementos Finitos](https://en.wikipedia.org/wiki/Finite_element_method) (FEM)***

- ***[Elementos de Contorno](https://en.wikipedia.org/wiki/Boundary_element_method) (BEM)***

- ***[Diferencias Finitas](https://en.wikipedia.org/wiki/Finite_difference_method) (FDM)***

- ***[Spectral](https://en.wikipedia.org/wiki/Spectral_method)***

- ***[Libres de Malla](https://en.wikipedia.org/wiki/Meshfree_methods) (Mesfree)***

Cada uno de ellos presenta ventajas y desventajas que deberán ser aprovechadas según el tipo de problema a resolver. En este curso nos centraremos únicamente en el ***Método de los Elementos Finitos (FEM)***.

#### Qué es un *Elemento Finito*?

Para captar la idea de lo que es un elemento finito, veamos un ejemplo en donde se calcula el valor de $\pi$ empleando el método propuesto originalmente por [*Arquimedes*](https://en.wikipedia.org/wiki/Approximations_of_%CF%80). Él sabía que la relación entre la circunferencia de un círculo y su diámetro era $\pi$. Tenía la idea de que podía dibujar un polígono regular inscrito dentro del círculo para aproximarse a $\pi$, y cuantos más lados dibujara en el polígono, mejor aproximación a $\pi$ obtendría. En matemáticas modernas, diríamos que como el número de lados del polígono tiende a infinito, la circunferencia tiende a $2\pi$. Sin embargo, en lugar de dibujar el polígono, Arquímedes calculó la longitud usando un argumento geométrico, como el siguiente:

Dibuja un círculo de radio $1$ centrado en $A$. Inscribe un polígono de $N$ lados dentro de él. Nuestra estimación de $\pi$ es la mitad de la circunferencia del polígono (la circunferencia de un círculo es $2\pi r$, $r=1$, lo que da $2\pi$). A medida que los lados del polígono se hacen cada vez más pequeños, la circunferencia se acerca cada vez más a $2\pi$.

El diagrama muestra un segmento del polígono $ACE$. El lado del polígono $CE$ tiene una longitud $d_n$. Suponiendo que conocemos $d$ para un polígono de $N$ lados, si podemos encontrar una expresión para la longitud $CD=d_{2n}$, la longitud del borde de un polígono con $2N$ lados, solo en términos de $d_n$, entonces podemos mejorar nuestra estimación de $\pi$. Intentemos hacer eso.

Bisecamos el triángulo $CAE$ para hacer que $CAD$ y $DAE$ sean los dos nuevos segmentos iguales del polígono de $2N$ lados. Usando el teorema de Pitágoras en el triángulo rectángulo $ABC$, tenemos

$$AB^2+BC^2=1^2=1$$
$$AB=\sqrt{1+BC^2}$$

dado que

$$BC=\frac{d_n}{2}$$

sustituyendo,

$$AB=\sqrt{1-\left( \frac{d_n}{2}\right)^2}$$
$$BD=1-AB=1-\sqrt{1-\frac{d^2_n}{4}}$$

Usando el teorema de Pitágoras en el triángulo rectángulo $CBD$

$$CD^2=BC^2+BD^2$$

reemplazando

$$CD=d_{2n}=\sqrt{2-2\sqrt{1-\frac{d^2_n}{4}}}$$

$d_{2n}$ es la longitud de un lado del polígono con $2N$ lados.

Esto significa que si conocemos la longitud de los lados de un polígono con $N$ lados, entonces podemos calcular la longitud de los lados de un polígono con $2N$ lados. ¿Qué significa esto? Comencemos con un cuadrado. La inscripción de un cuadrado en un círculo se ve así, con la longitud del lado $\sqrt{2}$. Esto da una estimación de $\pi$ como $2\sqrt{2}$, que es pobre ($\approx2.828$) pero es solo el comienzo del proceso.

Podemos calcular la longitud del lado de un octágono, a partir de la longitud del lado de un cuadrado, y la longitud del lado de un $16$-gono a partir de un octágono, etc.

```python
def piArchimedes(n):
    """
    Calculate n iterations of Archimedes PI recurrence relation
    """
    polygon_edge = 2.0
    polygon_sides = 4
    for i in range(n):
        polygon_edge = 2 - 2 * math.sqrt(1 - polygon_edge / 4)
        polygon_sides *= 2
    return polygon_sides * math.sqrt(polygon_edge) / 2
```

```python
import math

print("n \t pi approx \t Error(%)")
print("1 \t {0:6.8f} \t -".format(0, ))

for n in range(16):
    result = piArchimedes(n)
    error = abs(result - math.pi) / math.pi
    print("{0} \t {1:4.8f} \t {2:4.8f}".format(n, result, error))

```

    n 	 pi approx 	 Error(%)
    1 	 0.00000000 	 -
    0 	 2.82842712 	 0.09968368
    1 	 3.06146746 	 0.02550464
    2 	 3.12144515 	 0.00641315
    3 	 3.13654849 	 0.00160561
    4 	 3.14033116 	 0.00040155
    5 	 3.14127725 	 0.00010040
    6 	 3.14151380 	 0.00002510
    7 	 3.14157294 	 0.00000627
    8 	 3.14158773 	 0.00000157
    9 	 3.14159142 	 0.00000039
    10 	 3.14159235 	 0.00000010
    11 	 3.14159258 	 0.00000002
    12 	 3.14159263 	 0.00000001
    13 	 3.14159265 	 0.00000000
    14 	 3.14159265 	 0.00000000
    15 	 3.14159261 	 0.00000001

 Volver a la Tabla de Contenido

### Qué es el FEM?

El [Método de los Elementos Finitos](https://en.wikipedia.org/wiki/Finite_element_method) (FEM, en inglés) es una técnica numérica empleada principalmente para resolver [ecuaciones diferenciales](https://en.wikipedia.org/wiki/Differential_equation), que surgen en el [modelamiento matemático](https://en.wikipedia.org/wiki/Mathematical_model) en áreas como la ingeniería, ciencias básicas, sociales, entre muchas otras.

#### FEM o FEA?

El método de elementos finitos (FEM) es una técnica numérica utilizada para realizar análisis de elementos finitos (FEA) de cualquier fenómeno físico dado.

Es necesario utilizar las matemáticas para comprender y cuantificar de manera integral cualquier fenómeno físico, como el comportamiento estructural o de los fluidos, el transporte térmico, la propagación de ondas y el crecimiento de células biológicas. La mayoría de estos procesos se describen mediante ecuaciones diferenciales parciales (PDE). Sin embargo, para que una computadora resuelva estas PDE, se han desarrollado técnicas numéricas durante las últimas décadas y una de las más destacadas en la actualidad es el método de elementos finitos.

#### Aplicaciones del FEM

El método de elementos finitos comenzó con una promesa significativa en el modelado de varias aplicaciones mecánicas relacionadas con la ingeniería aeroespacial y civil. Las aplicaciones del método de los elementos finitos recién ahora están comenzando a alcanzar su potencial. Una de las perspectivas más interesantes es su aplicación en [problemas acoplados](https://en.wikipedia.org/wiki/Coupling_(physics)) tales como [interacción fluido-estructura](https://en.wikipedia.org/wiki/Fluid%E2%80%93structure_interaction)(FSI), problemas [termomecánicos](https://en.wikipedia.org/wiki/Thermomechanical_analysis), [termoquímicos](https://en.wikipedia.org/wiki/Thermochemical_cycle), [biomecánica](https://en.wikipedia.org/wiki/Biomechanics), [ingeniería biomédica](https://en.wikipedia.org/wiki/Biomedical_sciences), [piezoeléctrica](https://en.wikipedia.org/wiki/Piezoelectricity), y [electromagnética](https://en.wikipedia.org/wiki/Computational_electromagnetics), entre muchos otros.

#### Ecuaciones Diferenciales en Derivadas Parciales

En matemáticas, una [Ecuación Diferencial en Derivadas Parciales](https://en.wikipedia.org/wiki/Partial_differential_equation) (PDE, de sus siglas en inglés) es una ecuación que impone relaciones entre las diversas derivadas parciales de una función multivariable. Existen diferentes formas de escribir una PDE, pero de las más comunes son:

$$u_x=\frac{\partial u}{\partial x}, \quad u_{xx}=\frac{\partial^2 u}{\partial x^2}, \quad u_{xy}=\frac{\partial^2 u}{\partial x \partial y}=\frac{\partial}{\partial y} \left( \frac{\partial u}{\partial x}\right),$$

Es importante comprender los diferentes géneros de PDE y su idoneidad para su uso con FEM. Comprender esto es particularmente importante para todos, independientemente de la motivación para usar el análisis de elementos finitos. Es fundamental recordar que FEM es una herramienta y cualquier herramienta es tan buena como su usuario.

Aunque, como se ha indicado, el FEM es una herramienta para resolver PDEs, en el curso no se realizará un desarrollo teórico de este tema, por lo que se deja al usuario la responsabilidad de su estudio y adecuado entendimiento. En el libro de [Kreyszig](https://soaneemrana.org/onewebmedia/ADVANCED%20ENGINEERING%20MATHEMATICS%20BY%20ERWIN%20ERESZIG1.pdf), capítulo 12, encontrará información que puede ser una buena referencia para estudio. También el notebook de [Ecuaciones Diferenciales con Python](https://relopezbriega.github.io/blog/2016/01/10/ecuaciones-diferenciales-con-python/) del profesor Raúl Lopez Briega, en español, en donde podrá realizar algunas aplicaciones empleando programación en `python`.

#### Cómo trabaja el FEM?

El [principio de minimización de la energía](https://en.wikipedia.org/wiki/Principle_of_minimum_energy) forma la columna vertebral principal del método de elementos finitos. En otras palabras, cuando se aplica una condición de contorno específica a un cuerpo (como desplazamiento o fuerza), esto puede conducir a varias configuraciones, pero sin embargo, solo una configuración particular es posible o lograda de manera realista. Sólo se elige aquella configuración donde la energía total es mínima. Incluso cuando la simulación se realiza varias veces, prevalecen los mismos resultados.

#### Historia del FEM

Dependiendo de la perspectiva histórica, se puede decir que el FEM tuvo sus orígenes en la obra de Euler, ya en el siglo XVI. Sin embargo, los primeros trabajos matemáticos sobre FEM se pueden encontrar en los trabajos de Schellback(1851) y Courant(1943).

El FEM fue desarrollado de forma independiente por ingenieros para abordar problemas de mecánica estructural relacionados con la ingeniería aeroespacial y civil. Los desarrollos comenzaron a mediados de la década de 1950 con los artículos de [Turner, Clough, Martin y Topp](http://www.ce.memphis.edu/7117/notes/presentations/papers/Turner%20et%20al%20(1956)%20Stiffness%20and%20deflection%20analysis%20of%20comlex%20strucutres.pdf)(1956), [Argyris](https://en.wikipedia.org/wiki/John_Argyris)(1957) y [Babuska y Aziz](https://www.elsevier.com/books/the-mathematical-foundations-of-the-finite-element-method-with-applications-to-partial-differential-equations/aziz/978-0-12-068650-6)(1972). Los libros de [Zienkiewicz](https://en.wikipedia.org/wiki/Olgierd_Zienkiewicz)(1971) sentaron las bases para el desarrollo futuro en FEM.

Una revisión interesante de estos desarrollos históricos se puede encontrar en Oden(1991). Se puede encontrar una revisión del desarrollo de FEM durante los últimos 75 años en este artículo de blog: [75 Years of the Finite Element Method](https://www.simscale.com/blog/2015/11/75-years-of-the-finite-element-method-fem/). En el artículo de [Gupta y Meek](https://people.sc.fsu.edu/~jpeterson/history_fem.pdf)(1996) o en este documento de curso del profesor [Mohite](http://home.iitk.ac.in/~mohite/History_of_FEM.pdf), podrá encontrar más información.

 Volver a la Tabla de Contenido

### Diferentes tipos del FEM

Como se discutió anteriormente, la tecnología FEM tradicional ha demostrado deficiencias en los problemas de modelado relacionados con la mecánica de fluidos y la propagación de ondas. Recientemente se han realizado varias mejoras para optimizar el proceso de solución y ampliar la aplicabilidad del análisis de elementos finitos a una amplia gama de problemas. Algunos de los importantes que aún se utilizan incluyen:

#### Método de elementos finitos extendido (XFEM)

El método de Bubnov-Galerkin requiere continuidad de desplazamiento entre elementos. Aunque problemas como el contacto, la fractura y el daño implican discontinuidades y saltos que no pueden manejarse directamente con el método de elementos finitos. Para superar esta deficiencia, [XFEM](https://en.wikipedia.org/wiki/Extended_finite_element_method) nació en la década de 1990. XFEM funciona mediante la expansión de las funciones de forma con las funciones de pasos de Heaviside. Se asignan grados de libertad adicionales a los nodos alrededor del punto de discontinuidad para que se puedan considerar los saltos.

#### Método de elementos finitos generalizado (GFEM)

[GFEM](https://www.sciencedirect.com/science/article/abs/pii/S0045782501001888) se introdujo casi al mismo tiempo que XFEM en los años 90. Combina las características de los métodos tradicionales FEM y sin malla. Las funciones de forma se definen principalmente por las coordenadas globales y luego se multiplican por partición de unidad para crear funciones de forma elementales locales. Una de las ventajas de GFEM es la prevención de remallado alrededor de singularidades.

#### Método mixto de elementos finitos

El [Método mixto de elementos finitos](https://en.wikipedia.org/wiki/Mixed_finite_element_method) se aplica en varios problemas, como el contacto o la incompresibilidad, en el que las restricciones se imponen utilizando multiplicadores de Lagrange. Estos grados de libertad extra que surgen de los multiplicadores de Lagrange se resuelven de forma independiente. El sistema de ecuaciones se resuelve como un sistema acoplado de ecuaciones.

#### Método de elementos finitos hp

[hp-FEM](https://en.wikipedia.org/wiki/Hp-FEM) es una combinación de refinamiento automático de malla (refinamiento h) y un aumento en el orden de polinomio (refinamiento p). Esto no es lo mismo que hacer refinamientos h y p por separado. Cuando se utiliza el refinamiento hp automático y un elemento se divide en elementos más pequeños (refinamiento h), cada elemento también puede tener diferentes órdenes polinomiales.

#### Método de elementos finitos de Galerkin discontinuo (DG-FEM)

[DG-FEM](https://en.wikipedia.org/wiki/Discontinuous_Galerkin_method) ha mostrado una promesa significativa para utilizar la idea de elementos finitos para resolver ecuaciones hiperbólicas, donde los métodos tradicionales de elementos finitos han sido débiles. Además, también ha mostrado mejoras en la flexión y problemas incompresibles que se observan típicamente en la mayoría de los procesos de materiales. Aquí, se agregan restricciones adicionales a la forma débil que incluye un parámetro de penalización (para evitar la interpenetración) y términos para otro equilibrio de tensiones entre los elementos.

 Volver a la Tabla de Contenido

### El proceso de análisis *FEM*

El comportamiento de un fenómeno en un sistema depende de la geometría o dominio del sistema, la propiedad del material o medio y las condiciones de frontera, iniciales y de carga. Para un sistema de ingeniería, la geometría o el dominio pueden ser muy complejos. Además, la frontera y las condiciones iniciales también pueden ser complicadas. Por lo tanto, en general, es muy difícil resolver la ecuación diferencial gobernante por medios analíticos. En la práctica, la mayoría de los problemas se resuelven mediante métodos numéricos. Entre estos, los métodos de discretización de dominios propugnados por el FEM son los más populares, debido a su practicidad y versatilidad.

#### Modelado

 Fuente: Felippa, C. Introduction to Finite Element Methods.

El procedimiento de modelado computacional que utiliza el FEM consta en general de cuatro pasos:

- Modelado de la geometría.

- [Mallado](https://en.wikipedia.org/wiki/Mesh_generation) (discretización).

- Especificación de propiedad material.

- Especificación de condiciones de contorno, inicial y de carga.

***Ejemplo:*** Elementos unidimensionales

 Fuente: Felippa, C. Introduction to Finite Element Methods.

***Ejemplo:*** Elementos bidimensionales

 Fuente: Springer.

#### Simulación

Las etapas del proceso de simulación empleando la técnica FEM se puede resumir en la siguiente tabla:

 Fuente: Miklos Kuczmann

#### Terminología FEM

- ***Dominio:*** es el conjunto de variables independientes para las que se define una función. En el *FEA*, un dominio es un sistema (región) continuo sobre el cual gobiernan las leyes de la física. En la ingeniería estructural, un dominio podría ser una viga o el marco de un edificio completo. En la ingeniería mecánica, un dominio podría ser una pieza de una máquina o un campo térmico.

- ***Ecuaciones gobernantes:*** son las ecuaciones derivadas de la física del sistema. Muchos sistemas de ingeniería pueden describirse mediante ecuaciones de gobierno, que determinan las características y comportamientos del sistema.

- ***Condiciones de frontera:*** son valores de la función en el borde del rango de algunas de sus variables. Es necesario conocer algunas de las condiciones de contorno para resolver un problema de ingeniería o para encontrar una función desconocida.

- ***Elemento:*** es una parte del dominio del problema y, por lo general, tiene una forma simple como un triángulo o cuadrilátero en 2D, o un tetraedro o un sólido rectangular en 3D.

- ***Nodo:*** es un punto en el dominio y, a menudo, es el vértice de varios elementos. Un nodo también se llama punto nodal.

- ***Malla:*** los elementos y nodos, juntos, forman una malla (mesh o grid, en inglés), que es la estructura de datos central en FEA.

- ***Generación de malla:*** la mayoría del software FEA genera automáticamente una malla refinada para lograr resultados más precisos. Para análisis de elementos finitos complejos o a gran escala, a menudo es imperativo que las computadoras generen una malla de elementos finitos automáticamente. Hay muchos algoritmos diferentes para la generación automática de mallas. En [este enlace](http://www.argusone.com/MeshGeneration.html) podrá ver algunas muestras de malla generadas automáticamente.

- ***Análisis lineal:*** se basa en los siguientes supuestos: (1) estático; (2) Pequeños desplazamientos; (3) El material es linealmente elástico.

- ***Análisis no lineal:*** considera la no linealidad del material y/o la no linealidad geométrica de un sistema de ingeniería. El análisis geométrico no lineal también se denomina análisis de grandes deformaciones.

- ***Grados de libertad:*** Los [grados de libertad](https://en.wikipedia.org/wiki/Degrees_of_freedom_(mechanics)) (DOF) son las principales incógnitas en las ecuaciones que constituyen un modelo de elementos finitos. La resolución de las ecuaciones determina los valores de los grados de libertad para cada nodo del modelo. Estos valores se denominan "datos primarios" en la documentación. Los datos derivados (tensiones, deformaciones, gradientes, flujos, etc.) se calculan a partir de la solución DOF. Los DOF incluyen desplazamientos (UX, UY, UZ), rotaciones (ROTX, ROTY, ROTZ), temperatura (TEMP), entre otros. Los grados de libertad incluidos en cada tipo de elemento reflejan la física del problema subyacente. Debe elegir elementos que ofrezcan solo los grados de libertad que necesita, ya que los grados de libertad adicionales aumentan el tiempo de cálculo y no proporcionan ningún beneficio. Por ejemplo, el análisis estructural (generalmente) no implica transferencia de calor o voltajes. Por lo tanto, los elementos utilizados para modelar estructuras no deben tener TEMP o VOLT DOF.

  A continuación se presenta una tabla resumen con los DOF más comunes para diferentes tipos de elementos estructurales:

|Elemento| DOFs                               |
|:------:|:----------------------------------:|
|Cercha  |$$UX, UY, UZ$$                      |
|Viga    |$$UX, UY, UZ; RotX, RotY, RotZ$$|
|Ladrillo|$$UX, UY, UZ$$                      |
|Placa   |$$UX, UY, UZ, Rot2DOFs-plano^*$$    |

  $^*$El *DOF* rotacional fuera del plano no se considera para elementos de placa.

 Volver a la Tabla de Contenido

### Pasos en la solución por el FEM

El desarrollo de un análisis por elementos finitos involucra una serie de pasos hasta determinar las fuerzas nodales y desplazamientos ocurridos en la estructura. A seguir se enuncian, de forma muy general, cada uno de ellos. A lo largo del curso se entrará más en detalle.

#### Discretizar la estructura en elementos.

Estos elementos están conectados entre sí a través de nodos.

 Fuente: mechanicalc.com

En este primer paso, se compone un modelo matemático de la estructura. Este modelo es una aproximación de la estructura, mientras que la estructura física es continua, el modelo consta de elementos discretos.

En este ejemplo se utiliza elementos tipo viga que se basan en la [teoría de vigas de Euler-Bernoulli](https://en.wikipedia.org/wiki/Euler%E2%80%93Bernoulli_beam_theory). El elemento tiene $2$ nodos, cada uno de los cuales tiene $3$ grados de libertad: traslación en $x$, traslación en $y$ y rotación.

#### Determinar la matriz de rigidez local para cada elemento

Esta matriz representa la rigidez de cada nodo en el elemento en un grado de libertad específico (es decir, determina el desplazamiento de cada nodo en cada grado de libertad bajo una carga dada). Debido a que cada uno de los nodos en el elemento de viga tiene $3$ grados de libertad, una matriz de $6 \times 6$ puede describir completamente la rigidez del elemento.

\begin{equation}
k^{(e)}=
\begin{bmatrix}
\frac{AE}{L} & 0 & 0 & -\frac{AE}{L} & 0 & 0 \\
0 & \frac{12EI}{L^3} & \frac{6EI}{L^2} & 0 & -\frac{12EI}{L^3} & \frac{6EI}{L^2}\\
0 & \frac{6EI}{L^2} & \frac{4EI}{L} & 0 & -\frac{6EI}{L^2} & \frac{2EI}{L}\\
-\frac{AE}{L} & 0 & 0 & \frac{AE}{L} & 0 & 0 \\
0 & -\frac{12EI}{L^3} & -\frac{6EI}{L^2} & 0 & \frac{12EI}{L^3} & -\frac{6EI}{L^2}\\
0 & \frac{6EI}{L^2} & \frac{2EI}{L} & 0 & -\frac{6EI}{L^2} & \frac{4EI}{L}
\end{bmatrix}
\end{equation}

#### Ensamblar la matriz de rigidez global

La matriz de rigidez global para la estructura general se ensambla basándose en la combinación de las matrices de rigidez local. En un nivel alto, la matriz de rigidez global se crea sumando las matrices de rigidez locales:

$$\left[ K\right]=\sum_i \left[ k_i^{(e)}\right]$$

donde la matriz $[k^{(e)}_i]$ es la matriz de rigidez local del elemento $i$.

La matriz de rigidez global será una matriz cuadrada $n \times n$, donde $n$ es $3$ veces el número de nodos en la malla (ya que cada nodo tiene $3$ grados de libertad). Al ensamblar la matriz de rigidez global, los términos de rigidez para cada nodo en la matriz de rigidez elemental se colocan en la ubicación correspondiente en la matriz global. Para cualquier elemento que comparta un nodo, las contribuciones de rigidez de ese nodo se sumarán de cada elemento.

#### Cálcular el vector de fuerza aplicada

El vector de fuerza aplicada será un vector $n \times 1$, donde $n$ es $3$ veces el número de nodos en la malla. El vector de fuerza se ensambla incluyendo las fuerzas aplicadas en cada grado de libertad en cada nodo de la malla:

$$\left\{ F\right\} =\sum_i \left\{ f_i^{(e)} \right\}$$

Una vez que se construyen la matriz de rigidez global y el vector de fuerza aplicada, se pueden resolver los desplazamientos nodales. La siguiente ecuación relaciona las fuerzas y los desplazamientos en la estructura general:

$$\left\{F\right\}+\left\{R\right\}=\left[K\right]\left\{U\right\}$$

donde

- $\left\{F\right\}$ es el vector de fuerza aplicada,

- $\left\{R\right\}$ es el vector de reacción externa,

- $\left[K\right]$ es la matriz de rigidez global, y

- $\left\{U\right\}$ es el vector de desplazamiento nodal.

En la ecuación anterior, $\left\{R\right\}$ y $\left\{U\right\}$ son incógnitas. Para simplificar la solución de esta ecuación, se desea resolver las reacciones y los desplazamientos independientemente unos de otros. Para hacer esto, podemos usar el hecho de que para cada restricción que se aplicó (ya sea una restricción en traslación en $x$, traslación en $y$, o rotación), el desplazamiento asociado con esa restricción será cero. Además, las reacciones externas solo ocurrirán donde se aplicaron restricciones. Por lo tanto, se sabe que para cada grado de libertad en cada nodo:

- Si se aplica una restricción, el desplazamiento en esa restricción será cero y la reacción externa puede ser distinta de cero.

- Si no hay restricción, la reacción externa será cero y el desplazamiento puede ser distinto de cero.

#### Aplicar condiciones de contorno y resolver los desplazamientos nodales

Inicialmente resolveremos la ecuación anterior para los desplazamientos nodales, $\left\{U\right\}$. Con base en el razonamiento anterior, es posible eliminar el vector $\left\{R\right\}$ de la ecuación. Las condiciones de contorno se aplican a la ecuación "poniendo a cero" las filas en las matrices correspondientes a las restricciones aplicadas. Al hacer esto, no hemos perdido ninguna información sobre los desplazamientos nodales, ya que se sabe que los desplazamientos serán cero para cada fila que se puso a cero. Ahora la ecuación se ha reducido a:

$$\left\{F\right\}=\left[K\right]\left\{U\right\}$$

#### Resolver las reacciones externas

La ecuación anterior se puede resolver para $\left\{U\right\}$, después de lo cual se conocen todos los desplazamientos nodales. La única incógnita que queda en la ecuación original es $\left\{R\right\}$, y esto ahora se puede resolver para:

$$\left\{R\right\}=\left[K\right]\left\{U\right\}-\left\{F\right\}$$

Ahora es posible resolver las fuerzas en cada nodo.

#### Resolver las fuerzas nodales

Para cualquier fuerza de reacción encontrada, la fuerza nodal es igual a la fuerza de reacción. Sin embargo, para los nodos donde no hay una reacción externa, las fuerzas en el nodo aún se desconocen. Estos se pueden encontrar extrayendo los desplazamientos apropiados del vector $\left\{U\right\}$ global para construir un vector $\left\{u\right\}$ local para cada elemento. Cada uno de estos vectores locales será un $6 \times 1$. Las fuerzas nodales en cada elemento se pueden resolver usando:

$$\left\{f\right\} = \left[k\right] \left\{u\right\}$$

donde

- $\left\{f\right\}$ es el vector de fuerza del elemento (local),

- $\left[k\right]$ es la matriz de rigidez del elemento, y

- $\left\{u\right\}$ es el vector de desplazamiento del elemento.

#### Resolver las tensiones

Una vez que se conocen las fuerzas en cada nodo de la malla, es posible resolver los esfuerzo en cada nodo:

| Esfuerzo axial |Esfuerzo cortante|Esfuerzo a flexión|Esfuerzos de von Mises|
|:--------------:|:---------------:|:----------------:|:--------------------:|
|$$\sigma_{ax}=\frac{F_{ax}}{A}$$ |$$\tau_{sh}=\frac{F_{sh}}{A}$$|$$\sigma_b=\frac{Mc}{I}$$|$$\sigma_{vm}=\sqrt{(\sigma_{ax}+\sigma_b)^2+3\tau_{sh}^2}$$|

 Volver a la Tabla de Contenido

### Atributos de elementos

Existe una ámplia tecnología de elementos en el FEM. Entre los tipos de elementos más comunes se tienen:

en la siguiente imágen se observa la comparación entre un componente estructural físico y su equivalente a un elemento finito idealizado

 Fuente: Felippa, C. Introduction to Finite Element Methods.

 Volver a la Tabla de Contenido

### Aspectos computacionales del FEM

Coming soon...

 Volver a la Tabla de Contenido

```python
from IPython.core.display import HTML
def css_styling():
    styles = open('./nb_style.css', 'r').read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>

```python

```