# *In vivo* parameter inference from single-molecule mRNA FISH data.

(c) 2017 Manuel Razo. This work is licensed under a [Creative Commons Attribution License CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/). All code contained herein is licensed under an [MIT license](https://opensource.org/licenses/MIT).

```python
import glob
# Our numerical workhorses
import numpy as np
import scipy.optimize
import scipy.special
import scipy.integrate
import mpmath
import pandas as pd
# Import libraries to parallelize processes
from joblib import Parallel, delayed

# Import matplotlib stuff for plotting
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib as mpl
from matplotlib.backends.backend_pdf import PdfPages

# Seaborn, useful for graphics
import seaborn as sns

# Import the utils for this project
import chann_cap_utils as chann_cap

# Set PBoC plotting style
chann_cap.set_plotting_style()

# Magic function to make matplotlib inline; other style specs must come AFTER
%matplotlib inline

# This enables SVG graphics inline (only use with static plots (non-Bokeh))
%config InlineBackend.figure_format = 'svg'

figdir = '../../fig/chemical_master_mRNA_FISH/'
tmpdir = '../../tmp/'
```

## $\LaTeX$ macros
$\newcommand{kon}{k_{\text{on}}}$
$\newcommand{koff}{k_{\text{off}}}$
$\newcommand{rm}{r _m}$
$\newcommand{gm}{\gamma _m}$
$\newcommand{mm}{\left\langle m \right\rangle}$

## Exploring the single-molecule mRNA FISH data from Jones et al. 2014.

In this notebook we will explore the use of the raw data from [Jones et al. 2014](http://science.sciencemag.org/content/346/6216/1533) on single molecule mRNA FISH data to make progress in determining parameters for the model.

First of all let's load the data.

```python
df = pd.read_csv('../../data/mRNA_FISH/Jones_Brewster_2014.csv', index_col=0)
df = df[df.experiment != 'control']
df = df[df.experiment != 'Control']
df = df[df.experiment != 'Ctrl']
df.head()
```

      area_cells
      date
      experiment
      spots_totals

      0
      321
      20110920
      5DL10
      0

      1
      407
      20110920
      5DL10
      4

      2
      337
      20110920
      5DL10
      3

      3
      343
      20110920
      5DL10
      2

      4
      320
      20110920
      5DL10
      0

Now let's plot the raw data for the *lacUV5* promoter used in the project.

```python
# Extract the lacUV5 data
dfUV5 = df[df.experiment == 'UV5']
# Plot the histogram of the data with bins of width 1
_ = plt.hist(dfUV5.spots_totals, bins=np.arange(0, dfUV5.spots_totals.max()),
             normed=1, histtype='stepfilled', align='left')

# Label the plot
plt.xlabel('mRNA / cell')
plt.ylabel('probability')
plt.savefig(figdir + 'lacUV5_raw_mRNA_fish.png', bbox_inches='tight')
```

# Two-state unregulated promoter model

Given the lack of agreement between the theory and the data at the protein level we will explore the possibility of using a two-state model for the unregulated promter where it transitions from an empty promoter to occupied by RNAP.

[Peccoud & Ycart](http://www.sciencedirect.com/science/article/pii/S0040580985710271) first derived the mRNA distribution for a two-state promoter from the chemical master equation. This distribution is given by
$$
\small
P(m) = {\Gamma\left( {\kon \over \gm} + m \right) \over
        \Gamma (m + 1) \Gamma\left( {\kon + \koff \over \gm} + m \right)}
        {\Gamma\left( {\kon + \koff \over \gm} \right) \over
         \Gamma\left( {\kon \over \gm} \right) }
         \left( {\rm \over \gm} \right)^m
         {}_1F_1 \left( {\kon \over \gm} + m, {\koff + \kon \over \gm} + m,
                       {-\rm \over \gm} \right),
$$
where ${}_1F_1$ is the confluent hypergeometric function.

[Sanchez & Kondev](http://linkinghub.elsevier.com/retrieve/pii/S1046202313000959) indicate that in the limit where $\koff \gg \kon$, i.e. an alternative form of the weak promoter approximaiton, and also for $\koff > \gm$ the equation simplifies to a Gamma distribution of the form
$$
P(m) = {\left( {\koff \over \rm} \right)^{\kon \over \gm} \over
        \Gamma \left( {\kon \over \gm} \right)}
        m^{{\kon \over \gm} - 1}
        e^{- \left( {\koff \over \rm} m \right)}.
$$

This convenient form of the distribution has a mean mRNA copy number $\mm$ of the form
$$
\mm = {\kon \over \gm} \cdot {\rm \over \koff}.
$$
The variance takes the form
$$
\sigma_m^2 = {\kon \over \gm} \left( {\rm \over \koff} \right)^2.
$$

Therefore the noise $\eta_m$ satisfies
$$
\eta_m^2 = {\sigma_m^2 \over \mm^2} = {\gm \over \kon}.
$$

### Testing the Gamma distribution with the mRNA data

If we assume the Gamma distribution for the mRNA copy number we can estimate the parameters given the data. [Wikipedia](https://en.wikipedia.org/wiki/Gamma_distribution) says that the two parameters of the distribution are
$$
k \equiv {\kon \over \gm}, \;\; \theta \equiv {\rm \over \koff}.
$$

We will use the `scipy.stats.gamma.fit` function to fit the parameters.

```python
gamma_shape, gamma_loc, gamma_scale = \
scipy.stats.gamma.fit(dfUV5.spots_totals.values)
```

Let's now plot the distribution along with the histogram to compare them side to side.

```python
# Extract the lacUV5 data
dfUV5 = df[df.experiment == 'UV5']
# Define bins
bins = np.arange(0, dfUV5.spots_totals.max())
# Plot the histogram of the data with bins of width 1
_ = plt.hist(dfUV5.spots_totals, bins=bins, normed=1, histtype='stepfilled',
             alpha=0.7, label='experimental data', align='left')

pdf = scipy.stats.gamma.pdf(x=bins, a=gamma_shape,
                            loc=gamma_loc, scale=gamma_scale)
plt.plot(bins, pdf, color='r', ls='--', lw=3, label='Gamma fit')

# Label the plot
plt.xlabel('mRNA / cell')
plt.ylabel('probability')
plt.legend()
```

    <matplotlib.legend.Legend at 0x111b2eeb8>

Let's now plot the cumulative distribution function.

```python
# Compute the empirical CDF
x, y = chann_cap.ecdf(dfUV5.spots_totals.values)
# Compute the theoretical CDF
cdf = scipy.stats.gamma.cdf(x=x, a=gamma_shape,
                            loc=gamma_loc, scale=gamma_scale)

plt.plot(x, y, label='experimental data')
plt.plot(x, cdf, color='r', ls='--', label='Gamma fit')

plt.xlabel('mRNA / cell')
plt.ylabel('cumulative distribution')
plt.legend()
```

    <matplotlib.legend.Legend at 0x15158aab38>

Now we compute the moments of the distribution.

```python
moments = scipy.stats.gamma.stats(a=gamma_shape,
                            loc=gamma_loc, scale=gamma_scale, moments='mvsk')
m_mean, m_var, m_skew, m_kurt = [float(x) for x in moments]
print(
'''
mean mRNA : {0:.1f}
var mRNA : {1:.1f}
skewness mRNA : {2:.1f}
kurtosis mRNA : {3:.1f}
'''.format(m_mean, m_var, m_skew, m_kurt)
)
```

    mean mRNA : 13.9
    var mRNA : 43.1
    skewness mRNA : 0.7
    kurtosis mRNA : 0.8

### Inferring back parameters.

Having fit the distribution moments we can compute some parameter ratios. As discussed before the noise $\eta$ would give
$$
\eta_m^2 = {\sigma_m^2 \over \mm^2} = {\gm \over \kon}.
$$

If instead we compute the fano-factor (variance / mean) we can obtain
$$
\text{fano-factor} = {\rm \over \koff}.
$$

Let's compute these paremter ratios.

```python
gamma_m_koff = m_var / m_mean**2
rm_koff = m_var / m_mean
print(
'''
gamma_m / kon = {0:.2f}
r_m / koff = {1:.2f}
'''.format(gamma_m_koff, rm_koff)
)
```

    gamma_m / kon = 0.22
    r_m / koff = 3.09

# Issues with this approximation.

This approximation has a several caveat, and that is that the distribution derived by Peccoud & Ycart is **discrete** by nature, while the Gamma distribution is a **continuous** distribution. This obviously comes with its own caveats that need to be extensively studied if one is to make progress with this.

# Parameter inference as defined by Peccoud & Ycart.

In the original paper Peccoud and Ycart define a way to compute three of the parameters given some estimators. The derivation will be left for later, but for now it suffices to say that the parameters can be inferred from experimental data as follows:

The mean mRNA copy number for the full distribution is given by
$$
\mm = {\kon \over \koff + \kon} {\rm \over \gm}.
$$
The variance is given by
$$
\sigma_m^2 = {\rm \over \gm}{\kon \over \kon + \koff} +
             {\kon \koff \over (\kon + \koff)^2}
             {\rm^2 \over \gm (\kon + \koff + \gm)}.
$$

The parameters can be inferred as
\begin{align}
\kon &= {2 r_1 (r_3 - r_2) \over r_1 r_2 - 2 r_1 r_3 + r_2 r_3},\\
\koff &= {2 (r_2 - r_1)(r_1 - r_3)(r_3 - r_2) \over
          (r_1 r_2 - 2 r_1 r_3 + r_2 r_3)(r_1 - 2 r_2 + r_3)},\\
\rm &= {-r_1 r_2 + 2 r_1 r_3 - r_2 r_3 \over r_1 - 2 r_2 + r_3},
\end{align}
where $r_i$ are the successive ratios of the exponential moments, i.e.
\begin{align}
r_1 &= \mm \\
r_2 &= {\left\langle m (m - 1) \right\rangle \over \mm} , \\
r_2 &= {\left\langle m (m - 1) (m - 2) \right\rangle \over \left\langle m (m - 1) \right\rangle}.
\end{align}

The best estimators for these moments are given by
\begin{align}
\mm &= {1 \over N} \sum_{i=1}^N x_i,\\
{\left\langle m (m - 1) \right\rangle} &= {1 \over N} \sum_{i = 1}^N
                                             x_i (x_i - 1)\\
{\left\langle m (m - 2) (m - 3) \right\rangle} &= {1 \over N} \sum_{i = 1}^N
                                             x_i (x_i - 1) (x_i - 2).
\end{align}

Let's use all these equations to compute the parameter values.

```python
# Extract parameters to compute moments
mRNA = dfUV5.spots_totals.values
N = len(mRNA)

# 1. Compute the moments estimators
m_first = 1 / N * np.sum(mRNA)
m_second = 1 / N * np.sum(mRNA * (mRNA - 1))
m_third = 1 / N * np.sum(mRNA * (mRNA - 1) * (mRNA - 2))

# 2. Compute moment ratios
r1 = m_first
r2 = m_second / m_first
r3 = m_third / m_second

# 3. Compute parameters
kon_UV5 = 2 * r1 * (r3 - r2) / (r1 * r2 - 2 * r1 * r3 + r2 * r3)
koff_UV5 = 2 * (r2 - r1) * (r1 - r3) * (r3 - r2) / \
           ((r1 * r2 - 2 * r1 * r3 + r2 * r3) * (r1 - 2 * r2 + r3))
rm_UV5 = (- r1 * r2 + 2 * r1 * r3 - r2 * r3) / \
         (r1 - 2 * r2 + r3)

# 4. Compute the values knowing the gamma_m value
gamma_m = 1 # 0.00284
kon = kon_UV5 * gamma_m
koff = koff_UV5 * gamma_m
rm = rm_UV5 * gamma_m

print(
'''
Parameter values
----------------
kon  = {0:.3f} s^-1
koff = {1:.3f} s^-1
rm   = {2:.3f} s^-1
'''.format(kon, koff, rm)
)
```

    Parameter values
    ----------------
    kon  = 5.858 s^-1
    koff = 46.554 s^-1
    rm   = 124.706 s^-1

### Testing the parameters by computing the complete two-state distribution of mRNA.

Having obtained what looks like reasonable parameter values let's now define a function that computes the probability mass function for our discrete distribution. We will compute the log probability since this is numerically more stable. The only caveat is that `scipy`'s implementation of the hypergeometric function is not completely stable, so we will use the `mpmath` implementation instead.

```python
# define a np.frompyfunc that allows us to evaluate the sympy.mp.math.hyp1f1
np_log_hyp= np.frompyfunc(lambda x, y, z: \
mpmath.ln(mpmath.hyp1f1(x, y, z, zeroprec=1000)), 3, 1)

def log_p_m_unreg(mRNA, kp_on, kp_off, gm, rm):
    '''
    Computes the log probability lnP(m) for an unregulated promoter,
    i.e. the probability of having m mRNA.

    Parameters
    ----------
    mRNA : float.
        mRNA copy number at which evaluate the probability.
    kp_on : float.
        rate of activation of the promoter in the chemical master equation
    kp_off : float.
        rate of deactivation of the promoter in the chemical master equation
    gm : float.
        1 / half-life time for the mRNA.
    rm : float.
        production rate of the mRNA

    Returns
    -------
    log probability lnP(m)
    '''
    # Convert the mRNA copy number to a  numpy array
    mRNA = np.array(mRNA)

    # Compute the probability
    lnp = scipy.special.gammaln(kp_on / gm + mRNA) \
    - scipy.special.gammaln(mRNA + 1) \
    - scipy.special.gammaln((kp_off + kp_on) / gm + mRNA) \
    + scipy.special.gammaln((kp_off + kp_on) / gm) \
    - scipy.special.gammaln(kp_on / gm) \
    + mRNA * np.log(rm / gm) \
    + np_log_hyp(kp_on / gm + mRNA,
            (kp_off + kp_on) / gm + mRNA, -rm / gm)

    return lnp.astype(float)
```

Having defined this function let's compare the theoretical prediction with the experimental data!

```python
# Define bins
bins = np.arange(0, dfUV5.spots_totals.max())

logp_mRNA = log_p_m_unreg(bins, kon, koff, gamma_m, rm)

# Plot the histogram of the data with bins of width 1
_ = plt.hist(dfUV5.spots_totals, bins=bins, normed=1, histtype='stepfilled',
             alpha=0.7, label='experimental data', align='left')

plt.plot(bins, np.exp(logp_mRNA), color='r', ls='--', lw=3,# drawstyle='steps',
         label='two-state promoter')

# Label the plot
plt.xlabel('mRNA / cell')
plt.ylabel('probability')
plt.legend()
plt.savefig(figdir + 'lacUV5_two_state_PMF_fit.png')
```

Let's now look at the CDF.

```python
# Compute the empirical CDF
x, y = chann_cap.ecdf(dfUV5.spots_totals.values)
# Compute the theoretical CDF
cdf = np.cumsum(np.exp(logp_mRNA))

plt.plot(x, y, label='experimental data')
plt.plot(bins, cdf, color='r', ls='-', label='two-state promoter fit')
#          drawstyle='steps')

plt.xlabel('mRNA / cell')
plt.ylabel('cumulative distribution')
plt.legend()
plt.savefig(figdir + 'lacUV5_two_state_CDF_fit.png')
```

The agreement between theory and data seems to be very promising!
That means that the parameters inferred could be potentially meaningful.

# Extending fitting procedure to all promoters.

Let's extend this procedure to all of the promoters in the data-set.

First let's look at all the promoters that do not contain the string `ngmL` in their name since these are regulated promoters.

```python
# Define data frame without the experiments with induction
df_unreg = df[['ngmL' not in x for x in df.experiment]]

# Generate list of promoters by increasing mean mRNA copy number
promoters = df_unreg.groupby('experiment').mean().spots_totals.sort_values().index

# Generate a dictionary with the colors to be used in the plot
colors = sns.color_palette('Blues', n_colors=len(promoters))
color_dict = dict(zip(promoters, colors))

# Loop through different promoters
for prom in promoters:
    # Extract the promoter data
    data = df_unreg[df_unreg.experiment == prom]
    # Compute the empirical CDF
    x, y = chann_cap.ecdf(data.spots_totals.values)
    # Plot the ECDF
    plt.plot(x, y, label=prom, color=color_dict[prom])

# Label plot
plt.xlabel('mRNA / cell')
plt.ylabel('cumulative distribution')
plt.legend(fontsize=8, ncol=2)
plt.savefig(figdir + 'unreg_promoters_ecdf.png', bbox_inches='tight')
```

Let's now perform the parameter estimate for this data. We will store all of the parameters into a tidy `DataFrame`.

```python
# Group data by experiment
df_group = df_unreg.groupby('experiment')

# List the variabes to be saved in the DataFrame.
var = ['experiment', 'mean_mRNA', 'n_cells',
       'kp_on', 'kp_off', 'rm', 'gm']
# Initialize Data Frame to save rates
df_param = pd.DataFrame(columns=var)

# Loop through promotres
for group, data in df_group:
    # Extract data to compute moments
    mRNA = data.spots_totals.values
    N = len(mRNA)

    # 1. Compute the moments estimators
    m_first = 1 / N * np.sum(mRNA)
    m_second = 1 / N * np.sum(mRNA * (mRNA - 1))
    m_third = 1 / N * np.sum(mRNA * (mRNA - 1) * (mRNA - 2))

    # 2. Compute moment ratios
    r1 = m_first
    r2 = m_second / m_first
    r3 = m_third / m_second

    # 3. Compute parameters
    kon = 2 * r1 * (r3 - r2) / (r1 * r2 - 2 * r1 * r3 + r2 * r3)
    koff = 2 * (r2 - r1) * (r1 - r3) * (r3 - r2) / \
               ((r1 * r2 - 2 * r1 * r3 + r2 * r3) * (r1 - 2 * r2 + r3))
    rm = (- r1 * r2 + 2 * r1 * r3 - r2 * r3) / \
             (r1 - 2 * r2 + r3)

    # 4. Compute the values knowing the gamma_m value
    gamma_m = 0.00284
    kon = kon * gamma_m
    koff = koff * gamma_m
    rm = rm * gamma_m

    # 5. Append results into data frame
    param = pd.Series([group, data.spots_totals.mean(), len(data),
                       kon, koff, rm, gamma_m], index=var)
    df_param = df_param.append(param, ignore_index=True)
```

```python
df_param.sort_values('mean_mRNA')
```

      experiment
      mean_mRNA
      n_cells
      kp_on
      kp_off
      rm
      gm

      17
      WTDL30
      0.067071
      5442
      0.001032
      -0.011513
      -0.001935
      0.00284

      15
      WTDL20
      0.082745
      1559
      0.000566
      0.039490
      0.016637
      0.00284

      16
      WTDL20v2
      0.089799
      3029
      0.000633
      0.013811
      0.005820
      0.00284

      19
      WTDR30
      0.119952
      2476
      0.001032
      0.012169
      0.004358
      0.00284

      8
      5DR20
      0.123272
      2677
      0.000866
      0.004802
      0.002292
      0.00284

      18
      WTDR20
      0.124294
      1416
      -0.000022
      -0.000056
      0.001265
      0.00284

      9
      5DR30
      0.185266
      2742
      0.002181
      0.019925
      0.005333
      0.00284

      10
      5DR5
      0.263112
      2269
      0.003406
      -0.018653
      -0.003345
      0.00284

      6
      5DR10
      0.330279
      2619
      0.004394
      -0.020372
      -0.003411
      0.00284

      14
      WTDL10
      0.347604
      2546
      0.001859
      -0.012627
      -0.005716
      0.00284

      3
      5DL30
      0.439483
      7279
      0.001670
      -0.021580
      -0.014878
      0.00284

      2
      5DL20
      0.882541
      4061
      0.002567
      -0.028530
      -0.025345
      0.00284

      13
      WT
      0.984549
      1618
      0.006200
      -0.040695
      -0.015557
      0.00284

      1
      5DL10
      2.708247
      2886
      0.009972
      -0.050167
      -0.031003
      0.00284

      4
      5DL5
      4.051852
      1890
      0.012162
      -0.225111
      -0.201480
      0.00284

      0
      5DL1
      8.980287
      2232
      0.012829
      0.073102
      0.170835
      0.00284

      11
      HG105
      12.954908
      2506
      0.011673
      -0.041811
      -0.094997
      0.00284

      5
      5DR1
      13.683183
      1332
      0.019220
      1.397479
      2.864410
      0.00284

      12
      UV5
      13.937689
      2648
      0.016636
      0.132214
      0.354166
      0.00284

      7
      5DR1v2
      15.159964
      2213
      0.015223
      0.061928
      0.218198
      0.00284

Let's plot the promoters that do not have negative parameter values and save them into a multi-page PDF document

```python
with PdfPages(figdir + 'unreg_promoter_fits_PMF.pdf') as pdf:
    for prom in promoters:
        # Extract parameters from this specific promoter
        par = df_param[df_param.experiment == prom][['kp_on', 'kp_off',
                                                         'rm', 'gm']]
        # Check if all parameters have positive values
        if np.any(par < 0):
            continue

        # Convert parameters into dictionary
        par = par.to_dict(orient='records')[0]
        # Extract data from promoter
        df_plot = df[df.experiment == prom]

        # Define bins
        bins = np.arange(0, df_plot.spots_totals.max())

        # Comput theoretical PMF
        logp_mRNA = log_p_m_unreg(bins, **par)

        # Plot the histogram of the data with bins of width 1
        _ = plt.hist(df_plot.spots_totals, bins=bins, normed=1,
                     histtype='stepfilled', alpha=0.7,
                     label='experimental data', align='left')

        # Plot theoretical pdf
        plt.plot(bins, np.exp(logp_mRNA), color='r', ls='--', lw=3,
                 label='two-state promoter')

        # Label the plot
        plt.xlabel('mRNA / cell')
        plt.ylabel('probability')
        title = r'{0:s} $\left\langle m \right\rangle$: {1:.1f}'.format(prom,
                df_param[df_param.experiment == prom].mean_mRNA.values[0])
        plt.title(title)
        plt.legend()
        plt.tight_layout()
        pdf.savefig()
        plt.close()
```

```python
with PdfPages(figdir + 'unreg_promoter_fits_CDF.pdf') as pdf:
    for prom in promoters:
        # Extract parameters from this specific promoter
        par = df_param[df_param.experiment == prom][['kp_on', 'kp_off',
                                                         'rm', 'gm']]
        # Check if all parameters have positive values
        if np.any(par < 0):
            continue

        # Convert parameters into dictionary
        par = par.to_dict(orient='records')[0]
        # Extract data from promoter
        df_plot = df[df.experiment == prom]

        # Define bins
        bins = np.arange(0, df_plot.spots_totals.max())

        # Comput theoretical PMF
        logp_mRNA = log_p_m_unreg(bins, **par)
        # Compute the theoretical CDF
        cdf = np.cumsum(np.exp(logp_mRNA))

        # Compute the empirical CDF
        x, y = chann_cap.ecdf(df_plot.spots_totals.values)

        # Plot both CDFs
        plt.plot(x, y, label='experimental data')
        plt.plot(bins, cdf, color='r', ls='-', label='two-state promoter fit')

        # Label the plot
        plt.xlabel('mRNA / cell')
        plt.ylabel('probability')
        title = r'{0:s} $\left\langle m \right\rangle$: {1:.1f}'.format(prom,
                df_param[df_param.experiment == prom].mean_mRNA.values[0])
        plt.title(title)
        plt.legend()
        plt.tight_layout()
        pdf.savefig()
        plt.close()
```


# Optimización de funciones escalares diferenciables con `SymPy`

> - Mediante optimización se obtienen soluciones elegantes tanto en teoría como en ciertas aplicaciones.
> - La teoría de optimización usa elementos comenzando con cálculo elemental y álgebra lineal básica, y luego se extiende con análisis funcional y convexo.
> - Las aplicaciones en optimización involucran ciencia, ingeniería, economía, finanzas e industria.
> - El amplio y creciente uso de la optimización lo hace escencial para estudiantes y profesionales de cualquier rama de la ciencia y la tecnología.

**Referencia:**
- http://www.math.uwaterloo.ca/~hwolkowi//henry/reports/talks.d/t06talks.d/06msribirs.d/optimportance.shtml

Algunas aplicaciones son:

1. Ingeniería
 - Encontrar la composición de equilibrio de una mezcla de diferentes átomos.
 - Planeación de ruta para un robot (o vehículo aéreo no tripulado).
 - Planeación de la mano de obra óptima en una construcción o planta de producción.
2. Distribución óptima de recursos.
 - Distribución de rutas de vuelo.
 - Encontrar una dieta óptima.
 - Planeación de ruta óptima.
3. Optimización financiera
 - Administración de riesgos.
 - Portafolios de inversión.

En esta clase veremos aspectos básicos de optimización. En específico, veremos cómo obtener máximos y mínimos de una función escalar de una variable (como en cálculo diferencial).

___
## 0. Librerías que usaremos

Como ya dijimos en la primer clase `python` es el lenguaje de programación (el cual es de alto nivel). Sin embargo, `python` solo tiene unos pocos comandos primitivos y para hacer más fácil su uso en nuestras actividades de simulación en ingeniería, otras personas ya han escrito ciertas librerías por nosotros.

### 0.1 `NumPy`

`NumPy` (Numerical Python) es la librería fundamental para computación científica (numérica) con `Python`. Contiene, entre otras cosas:
- un objeto tipo arreglo N-dimensional muy poderoso
- funciones sofisticadas
- funciones de álgebra lineal, transformada de Fourier y números aleatorios.

Por lo anterior, `NumPy` es de amplio uso entre la comunidad científica e ingenieril (por su manejo de cantidades vectoriales). De la misma manera, se usa para guardar datos. Para nuestros propósitos, se puede usar libremente.

**Referencia:**
- http://www.numpy.org/

`NumPy` ya viene incluido en la instalación estándar de Anaconda por defecto. Para comenzar a usarlo, solo debemos de importarlo:

```python
# importar la librería numpy
import numpy as np
```

```python
# Creamos un vector
x = np.array([0, 7, 8.4, -3.5, 2])
x
```

    array([ 0. ,  7. ,  8.4, -3.5,  2. ])

```python
# Función seno sobre los elementos de vector x
np.sin(x)
```

    array([0.        , 0.6569866 , 0.85459891, 0.35078323, 0.90929743])

$$
\int e^x dx = e^x +C
$$

$$
\int_{1}^{2} e^{x^2} dx
$$

### 0.2 `SymPy`

`SymPy` (Symbolic Python) es una librería de `Python` para matemáticas simbólicas. Su objetivo es convertirse en un sistema de álgebra computacional con las mejores características, manteniendo el código lo más simple posible para que sea comprensible.

**Referencia:**
- http://www.sympy.org/en/index.html

`SymPy` ya viene incluido en la instalación estándar de Anaconda por defecto. Para comenzar a usarlo, solo debemos de importarlo:

```python
# importar la librería sympy
import sympy as sym
```

```python
# Ayuda en la función init_printing
help(sym.init_printing)
```

    Help on function init_printing in module sympy.interactive.printing:

    init_printing(pretty_print=True, order=None, use_unicode=None, use_latex=None, wrap_line=None, num_columns=None, no_global=False, ip=None, euler=False, forecolor=None, backcolor='Transparent', fontsize='10pt', latex_mode='plain', print_builtin=True, str_printer=None, pretty_printer=None, latex_printer=None, scale=1.0, **settings)
        Initializes pretty-printer depending on the environment.

        Parameters
        ==========

        pretty_print : boolean, default=True
            If True, use pretty_print to stringify or the provided pretty
            printer; if False, use sstrrepr to stringify or the provided string
            printer.
        order : string or None, default='lex'
            There are a few different settings for this parameter:
            lex (default), which is lexographic order;
            grlex, which is graded lexographic order;
            grevlex, which is reversed graded lexographic order;
            old, which is used for compatibility reasons and for long expressions;
            None, which sets it to lex.
        use_unicode : boolean or None, default=None
            If True, use unicode characters;
            if False, do not use unicode characters;
            if None, make a guess based on the environment.
        use_latex : string, boolean, or None, default=None
            If True, use default LaTeX rendering in GUI interfaces (png and
            mathjax);
            if False, do not use LaTeX rendering;
            if None, make a guess based on the environment;
            if 'png', enable latex rendering with an external latex compiler,
            falling back to matplotlib if external compilation fails;
            if 'matplotlib', enable LaTeX rendering with matplotlib;
            if 'mathjax', enable LaTeX text generation, for example MathJax
            rendering in IPython notebook or text rendering in LaTeX documents;
            if 'svg', enable LaTeX rendering with an external latex compiler,
            no fallback
        wrap_line : boolean
            If True, lines will wrap at the end; if False, they will not wrap
            but continue as one line. This is only relevant if ``pretty_print`` is
            True.
        num_columns : int or None, default=None
            If int, number of columns before wrapping is set to num_columns; if
            None, number of columns before wrapping is set to terminal width.
            This is only relevant if ``pretty_print`` is True.
        no_global : boolean, default=False
            If True, the settings become system wide;
            if False, use just for this console/session.
        ip : An interactive console
            This can either be an instance of IPython,
            or a class that derives from code.InteractiveConsole.
        euler : boolean, optional, default=False
            Loads the euler package in the LaTeX preamble for handwritten style
            fonts (http://www.ctan.org/pkg/euler).
        forecolor : string or None, optional, default=None
            DVI setting for foreground color. None means that either 'Black',
            'White', or 'Gray' will be selected based on a guess of the IPython
            terminal color setting. See notes.
        backcolor : string, optional, default='Transparent'
            DVI setting for background color. See notes.
        fontsize : string, optional, default='10pt'
            A font size to pass to the LaTeX documentclass function in the
            preamble. Note that the options are limited by the documentclass.
            Consider using scale instead.
        latex_mode : string, optional, default='plain'
            The mode used in the LaTeX printer. Can be one of:
            {'inline'|'plain'|'equation'|'equation*'}.
        print_builtin : boolean, optional, default=True
            If ``True`` then floats and integers will be printed. If ``False`` the
            printer will only print SymPy types.
        str_printer : function, optional, default=None
            A custom string printer function. This should mimic
            sympy.printing.sstrrepr().
        pretty_printer : function, optional, default=None
            A custom pretty printer. This should mimic sympy.printing.pretty().
        latex_printer : function, optional, default=None
            A custom LaTeX printer. This should mimic sympy.printing.latex().
        scale : float, optional, default=1.0
            Scale the LaTeX output when using the ``png`` or ``svg`` backends.
            Useful for high dpi screens.
        settings :
            Any additional settings for the ``latex`` and ``pretty`` commands can
            be used to fine-tune the output.

        Examples
        ========

        >>> from sympy.interactive import init_printing
        >>> from sympy import Symbol, sqrt
        >>> from sympy.abc import x, y
        >>> sqrt(5)
        sqrt(5)
        >>> init_printing(pretty_print=True) # doctest: +SKIP
        >>> sqrt(5) # doctest: +SKIP
          ___
        \/ 5
        >>> theta = Symbol('theta') # doctest: +SKIP
        >>> init_printing(use_unicode=True) # doctest: +SKIP
        >>> theta # doctest: +SKIP
        \u03b8
        >>> init_printing(use_unicode=False) # doctest: +SKIP
        >>> theta # doctest: +SKIP
        theta
        >>> init_printing(order='lex') # doctest: +SKIP
        >>> str(y + x + y**2 + x**2) # doctest: +SKIP
        x**2 + x + y**2 + y
        >>> init_printing(order='grlex') # doctest: +SKIP
        >>> str(y + x + y**2 + x**2) # doctest: +SKIP
        x**2 + x + y**2 + y
        >>> init_printing(order='grevlex') # doctest: +SKIP
        >>> str(y * x**2 + x * y**2) # doctest: +SKIP
        x**2*y + x*y**2
        >>> init_printing(order='old') # doctest: +SKIP
        >>> str(x**2 + y**2 + x + y) # doctest: +SKIP
        x**2 + x + y**2 + y
        >>> init_printing(num_columns=10) # doctest: +SKIP
        >>> x**2 + x + y**2 + y # doctest: +SKIP
        x + y +
        x**2 + y**2

        Notes
        =====

        The foreground and background colors can be selected when using 'png' or
        'svg' LaTeX rendering. Note that before the ``init_printing`` command is
        executed, the LaTeX rendering is handled by the IPython console and not SymPy.

        The colors can be selected among the 68 standard colors known to ``dvips``,
        for a list see [1]_. In addition, the background color can be
        set to  'Transparent' (which is the default value).

        When using the 'Auto' foreground color, the guess is based on the
        ``colors`` variable in the IPython console, see [2]_. Hence, if
        that variable is set correctly in your IPython console, there is a high
        chance that the output will be readable, although manual settings may be
        needed.

        References
        ==========

        .. [1] https://en.wikibooks.org/wiki/LaTeX/Colors#The_68_standard_colors_known_to_dvips

        .. [2] https://ipython.readthedocs.io/en/stable/config/details.html#terminal-colors

        See Also
        ========

        sympy.printing.latex
        sympy.printing.pretty

```python
# imprimir en formato latex
sym.init_printing(use_latex='mathjax')
```

La funcionalidad de imprimir en formato LaTex que nos da `SymPy` mediante el proyecto `mathjax` hace de `SymPy` una herramienta muy atractiva...

Notar que en `SymPy` y en `NumPy` existen funciones con el mismo nombre, pero reciben tipos de datos diferentes...

```python
# Definimos la variable simbólica x
sym.var('x')
```

$\displaystyle x$

```python
sym.sin(x)
```

$\displaystyle \sin{\left(x \right)}$

```python
np.sin(x)
```

```python
y = np.array([0, 1, 3.14])
np.sin(y)
```

    array([0.        , 0.84147098, 0.00159265])

```python
sym.sin(y)
```

```python
# from sympy import *
# from numpy import *
# Esta práctica no es para nada recomendable.
```

```python
sin(y)
```

    array([0.        , 0.84147098, 0.00159265])

```python
log(y)
```

    /home/esteban/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in log
      """Entry point for launching an IPython kernel.

    array([     -inf, 0.       , 1.1442228])

Explicar el uso de la sintaxis `from numpy import *` y sus peligros (no recomendable).

### 0.3 `PyPlot` de `matplotlib`

El módulo `PyPlot` de la librería `matplotlib` contiene funciones que nos permite generar una gran cantidad de gráficas rápidamente. Las funciones de este módulo están escritas con el mismo nombre que las funciones para graficar en `Matlab`.

**Referencia:**
- https://matplotlib.org/api/pyplot_summary.html

```python
# importar matplotlib.pyplot
# import matplotlib.pyplot as plt
from matplotlib import pyplot as plt
# comando para que las gráficas salgan en la misma ventana
%matplotlib inline
```

Ya que revisamos todas las librerías que usaremos, empecemos con la clase como tal...

___
Basamos todos los resultados en los siguientes teoremas:

## 1. Teorema de Fermat (análisis)

Si una función $f(x)$ alcanza un máximo o mínimo local en $x=c$, y si la derivada $f'(c)$ existe en el punto $c$, entonces $f'(c) = 0$.

### Ejemplo

Sabemos que la función $f(x)=x^2$ tiene un mínimo global en $x=0$, pues

$$f(x)=x^2\geq0,\qquad\text{y}\qquad f(x)=x^2=0 \qquad\text{si y solo si}\qquad x=0.$$

```python
# declarar la variable real x
sym.var('x')
```

$\displaystyle x$

```python
# declarar ahora f=x^2 y mostrar
f = x**2  # En python x^2 es otra cosa
```

Cuidado

```python
4**2
```

$\displaystyle 16$

```python
4^2 #  4: 1 0 0
    #  2: 0 1 0
    #     1 1 0: 6
```

$\displaystyle 6$

```python
# derivar f respecto a x y mostrar
df = sym.diff(f, x)  # df / dx
df
```

$\displaystyle 2 x$

```python
# resolver f'(x)=0 y mostrar soluciones
help(sym.solve)
```

    Help on function solve in module sympy.solvers.solvers:

    solve(f, *symbols, **flags)
        Algebraically solves equations and systems of equations.

        Currently supported are:
            - polynomial,
            - transcendental
            - piecewise combinations of the above
            - systems of linear and polynomial equations
            - systems containing relational expressions.

        Input is formed as:

        * f
            - a single Expr or Poly that must be zero,
            - an Equality
            - a Relational expression
            - a Boolean
            - iterable of one or more of the above

        * symbols (object(s) to solve for) specified as
            - none given (other non-numeric objects will be used)
            - single symbol
            - denested list of symbols
              e.g. solve(f, x, y)
            - ordered iterable of symbols
              e.g. solve(f, [x, y])

        * flags
            'dict'=True (default is False)
                return list (perhaps empty) of solution mappings
            'set'=True (default is False)
                return list of symbols and set of tuple(s) of solution(s)
            'exclude=[] (default)'
                don't try to solve for any of the free symbols in exclude;
                if expressions are given, the free symbols in them will
                be extracted automatically.
            'check=True (default)'
                If False, don't do any testing of solutions. This can be
                useful if one wants to include solutions that make any
                denominator zero.
            'numerical=True (default)'
                do a fast numerical check if ``f`` has only one symbol.
            'minimal=True (default is False)'
                a very fast, minimal testing.
            'warn=True (default is False)'
                show a warning if checksol() could not conclude.
            'simplify=True (default)'
                simplify all but polynomials of order 3 or greater before
                returning them and (if check is not False) use the
                general simplify function on the solutions and the
                expression obtained when they are substituted into the
                function which should be zero
            'force=True (default is False)'
                make positive all symbols without assumptions regarding sign.
            'rational=True (default)'
                recast Floats as Rational; if this option is not used, the
                system containing floats may fail to solve because of issues
                with polys. If rational=None, Floats will be recast as
                rationals but the answer will be recast as Floats. If the
                flag is False then nothing will be done to the Floats.
            'manual=True (default is False)'
                do not use the polys/matrix method to solve a system of
                equations, solve them one at a time as you might "manually"
            'implicit=True (default is False)'
                allows solve to return a solution for a pattern in terms of
                other functions that contain that pattern; this is only
                needed if the pattern is inside of some invertible function
                like cos, exp, ....
            'particular=True (default is False)'
                instructs solve to try to find a particular solution to a linear
                system with as many zeros as possible; this is very expensive
            'quick=True (default is False)'
                when using particular=True, use a fast heuristic instead to find a
                solution with many zeros (instead of using the very slow method
                guaranteed to find the largest number of zeros possible)
            'cubics=True (default)'
                return explicit solutions when cubic expressions are encountered
            'quartics=True (default)'
                return explicit solutions when quartic expressions are encountered
            'quintics=True (default)'
                return explicit solutions (if possible) when quintic expressions
                are encountered

        Examples
        ========

        The output varies according to the input and can be seen by example::

            >>> from sympy import solve, Poly, Eq, Function, exp
            >>> from sympy.abc import x, y, z, a, b
            >>> f = Function('f')

        * boolean or univariate Relational

            >>> solve(x < 3)
            (-oo < x) & (x < 3)

        * to always get a list of solution mappings, use flag dict=True

            >>> solve(x - 3, dict=True)
            [{x: 3}]
            >>> sol = solve([x - 3, y - 1], dict=True)
            >>> sol
            [{x: 3, y: 1}]
            >>> sol[0][x]
            3
            >>> sol[0][y]
            1

        * to get a list of symbols and set of solution(s) use flag set=True

            >>> solve([x**2 - 3, y - 1], set=True)
            ([x, y], {(-sqrt(3), 1), (sqrt(3), 1)})

        * single expression and single symbol that is in the expression

            >>> solve(x - y, x)
            [y]
            >>> solve(x - 3, x)
            [3]
            >>> solve(Eq(x, 3), x)
            [3]
            >>> solve(Poly(x - 3), x)
            [3]
            >>> solve(x**2 - y**2, x, set=True)
            ([x], {(-y,), (y,)})
            >>> solve(x**4 - 1, x, set=True)
            ([x], {(-1,), (1,), (-I,), (I,)})

        * single expression with no symbol that is in the expression

            >>> solve(3, x)
            []
            >>> solve(x - 3, y)
            []

        * single expression with no symbol given

              In this case, all free symbols will be selected as potential
              symbols to solve for. If the equation is univariate then a list
              of solutions is returned; otherwise -- as is the case when symbols are
              given as an iterable of length > 1 -- a list of mappings will be returned.

                >>> solve(x - 3)
                [3]
                >>> solve(x**2 - y**2)
                [{x: -y}, {x: y}]
                >>> solve(z**2*x**2 - z**2*y**2)
                [{x: -y}, {x: y}, {z: 0}]
                >>> solve(z**2*x - z**2*y**2)
                [{x: y**2}, {z: 0}]

        * when an object other than a Symbol is given as a symbol, it is
          isolated algebraically and an implicit solution may be obtained.
          This is mostly provided as a convenience to save one from replacing
          the object with a Symbol and solving for that Symbol. It will only
          work if the specified object can be replaced with a Symbol using the
          subs method.

              >>> solve(f(x) - x, f(x))
              [x]
              >>> solve(f(x).diff(x) - f(x) - x, f(x).diff(x))
              [x + f(x)]
              >>> solve(f(x).diff(x) - f(x) - x, f(x))
              [-x + Derivative(f(x), x)]
              >>> solve(x + exp(x)**2, exp(x), set=True)
              ([exp(x)], {(-sqrt(-x),), (sqrt(-x),)})

              >>> from sympy import Indexed, IndexedBase, Tuple, sqrt
              >>> A = IndexedBase('A')
              >>> eqs = Tuple(A[1] + A[2] - 3, A[1] - A[2] + 1)
              >>> solve(eqs, eqs.atoms(Indexed))
              {A[1]: 1, A[2]: 2}

            * To solve for a *symbol* implicitly, use 'implicit=True':

                >>> solve(x + exp(x), x)
                [-LambertW(1)]
                >>> solve(x + exp(x), x, implicit=True)
                [-exp(x)]

            * It is possible to solve for anything that can be targeted with
              subs:

                >>> solve(x + 2 + sqrt(3), x + 2)
                [-sqrt(3)]
                >>> solve((x + 2 + sqrt(3), x + 4 + y), y, x + 2)
                {y: -2 + sqrt(3), x + 2: -sqrt(3)}

            * Nothing heroic is done in this implicit solving so you may end up
              with a symbol still in the solution:

                >>> eqs = (x*y + 3*y + sqrt(3), x + 4 + y)
                >>> solve(eqs, y, x + 2)
                {y: -sqrt(3)/(x + 3), x + 2: (-2*x - 6 + sqrt(3))/(x + 3)}
                >>> solve(eqs, y*x, x)
                {x: -y - 4, x*y: -3*y - sqrt(3)}

            * if you attempt to solve for a number remember that the number
              you have obtained does not necessarily mean that the value is
              equivalent to the expression obtained:

                >>> solve(sqrt(2) - 1, 1)
                [sqrt(2)]
                >>> solve(x - y + 1, 1)  # /!\ -1 is targeted, too
                [x/(y - 1)]
                >>> [_.subs(z, -1) for _ in solve((x - y + 1).subs(-1, z), 1)]
                [-x + y]

            * To solve for a function within a derivative, use dsolve.

        * single expression and more than 1 symbol

            * when there is a linear solution

                >>> solve(x - y**2, x, y)
                [(y**2, y)]
                >>> solve(x**2 - y, x, y)
                [(x, x**2)]
                >>> solve(x**2 - y, x, y, dict=True)
                [{y: x**2}]

            * when undetermined coefficients are identified

                * that are linear

                    >>> solve((a + b)*x - b + 2, a, b)
                    {a: -2, b: 2}

                * that are nonlinear

                    >>> solve((a + b)*x - b**2 + 2, a, b, set=True)
                    ([a, b], {(-sqrt(2), sqrt(2)), (sqrt(2), -sqrt(2))})

            * if there is no linear solution then the first successful
              attempt for a nonlinear solution will be returned

                >>> solve(x**2 - y**2, x, y, dict=True)
                [{x: -y}, {x: y}]
                >>> solve(x**2 - y**2/exp(x), x, y, dict=True)
                [{x: 2*LambertW(-y/2)}, {x: 2*LambertW(y/2)}]
                >>> solve(x**2 - y**2/exp(x), y, x)
                [(-x*sqrt(exp(x)), x), (x*sqrt(exp(x)), x)]

        * iterable of one or more of the above

            * involving relationals or bools

                >>> solve([x < 3, x - 2])
                Eq(x, 2)
                >>> solve([x > 3, x - 2])
                False

            * when the system is linear

                * with a solution

                    >>> solve([x - 3], x)
                    {x: 3}
                    >>> solve((x + 5*y - 2, -3*x + 6*y - 15), x, y)
                    {x: -3, y: 1}
                    >>> solve((x + 5*y - 2, -3*x + 6*y - 15), x, y, z)
                    {x: -3, y: 1}
                    >>> solve((x + 5*y - 2, -3*x + 6*y - z), z, x, y)
                    {x: 2 - 5*y, z: 21*y - 6}

                * without a solution

                    >>> solve([x + 3, x - 3])
                    []

            * when the system is not linear

                >>> solve([x**2 + y -2, y**2 - 4], x, y, set=True)
                ([x, y], {(-2, -2), (0, 2), (2, -2)})

            * if no symbols are given, all free symbols will be selected and a list
              of mappings returned

                >>> solve([x - 2, x**2 + y])
                [{x: 2, y: -4}]
                >>> solve([x - 2, x**2 + f(x)], {f(x), x})
                [{x: 2, f(x): -4}]

            * if any equation doesn't depend on the symbol(s) given it will be
              eliminated from the equation set and an answer may be given
              implicitly in terms of variables that were not of interest

                >>> solve([x - y, y - 3], x)
                {x: y}

        Notes
        =====

        solve() with check=True (default) will run through the symbol tags to
        elimate unwanted solutions.  If no assumptions are included all possible
        solutions will be returned.

            >>> from sympy import Symbol, solve
            >>> x = Symbol("x")
            >>> solve(x**2 - 1)
            [-1, 1]

        By using the positive tag only one solution will be returned:

            >>> pos = Symbol("pos", positive=True)
            >>> solve(pos**2 - 1)
            [1]

        Assumptions aren't checked when `solve()` input involves
        relationals or bools.

        When the solutions are checked, those that make any denominator zero
        are automatically excluded. If you do not want to exclude such solutions
        then use the check=False option:

            >>> from sympy import sin, limit
            >>> solve(sin(x)/x)  # 0 is excluded
            [pi]

        If check=False then a solution to the numerator being zero is found: x = 0.
        In this case, this is a spurious solution since sin(x)/x has the well known
        limit (without dicontinuity) of 1 at x = 0:

            >>> solve(sin(x)/x, check=False)
            [0, pi]

        In the following case, however, the limit exists and is equal to the
        value of x = 0 that is excluded when check=True:

            >>> eq = x**2*(1/x - z**2/x)
            >>> solve(eq, x)
            []
            >>> solve(eq, x, check=False)
            [0]
            >>> limit(eq, x, 0, '-')
            0
            >>> limit(eq, x, 0, '+')
            0

        Disabling high-order, explicit solutions
        ----------------------------------------

        When solving polynomial expressions, one might not want explicit solutions
        (which can be quite long). If the expression is univariate, CRootOf
        instances will be returned instead:

            >>> solve(x**3 - x + 1)
            [-1/((-1/2 - sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)) - (-1/2 -
            sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)/3, -(-1/2 +
            sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)/3 - 1/((-1/2 +
            sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)), -(3*sqrt(69)/2 +
            27/2)**(1/3)/3 - 1/(3*sqrt(69)/2 + 27/2)**(1/3)]
            >>> solve(x**3 - x + 1, cubics=False)
            [CRootOf(x**3 - x + 1, 0),
             CRootOf(x**3 - x + 1, 1),
             CRootOf(x**3 - x + 1, 2)]

            If the expression is multivariate, no solution might be returned:

            >>> solve(x**3 - x + a, x, cubics=False)
            []

        Sometimes solutions will be obtained even when a flag is False because the
        expression could be factored. In the following example, the equation can
        be factored as the product of a linear and a quadratic factor so explicit
        solutions (which did not require solving a cubic expression) are obtained:

            >>> eq = x**3 + 3*x**2 + x - 1
            >>> solve(eq, cubics=False)
            [-1, -1 + sqrt(2), -sqrt(2) - 1]

        Solving equations involving radicals
        ------------------------------------

        Because of SymPy's use of the principle root (issue #8789), some solutions
        to radical equations will be missed unless check=False:

            >>> from sympy import root
            >>> eq = root(x**3 - 3*x**2, 3) + 1 - x
            >>> solve(eq)
            []
            >>> solve(eq, check=False)
            [1/3]

        In the above example there is only a single solution to the
        equation. Other expressions will yield spurious roots which
        must be checked manually; roots which give a negative argument
        to odd-powered radicals will also need special checking:

            >>> from sympy import real_root, S
            >>> eq = root(x, 3) - root(x, 5) + S(1)/7
            >>> solve(eq)  # this gives 2 solutions but misses a 3rd
            [CRootOf(7*_p**5 - 7*_p**3 + 1, 1)**15,
            CRootOf(7*_p**5 - 7*_p**3 + 1, 2)**15]
            >>> sol = solve(eq, check=False)
            >>> [abs(eq.subs(x,i).n(2)) for i in sol]
            [0.48, 0.e-110, 0.e-110, 0.052, 0.052]

            The first solution is negative so real_root must be used to see
            that it satisfies the expression:

            >>> abs(real_root(eq.subs(x, sol[0])).n(2))
            0.e-110

        If the roots of the equation are not real then more care will be
        necessary to find the roots, especially for higher order equations.
        Consider the following expression:

            >>> expr = root(x, 3) - root(x, 5)

        We will construct a known value for this expression at x = 3 by selecting
        the 1-th root for each radical:

            >>> expr1 = root(x, 3, 1) - root(x, 5, 1)
            >>> v = expr1.subs(x, -3)

        The solve function is unable to find any exact roots to this equation:

            >>> eq = Eq(expr, v); eq1 = Eq(expr1, v)
            >>> solve(eq, check=False), solve(eq1, check=False)
            ([], [])

        The function unrad, however, can be used to get a form of the equation for
        which numerical roots can be found:

            >>> from sympy.solvers.solvers import unrad
            >>> from sympy import nroots
            >>> e, (p, cov) = unrad(eq)
            >>> pvals = nroots(e)
            >>> inversion = solve(cov, x)[0]
            >>> xvals = [inversion.subs(p, i) for i in pvals]

        Although eq or eq1 could have been used to find xvals, the solution can
        only be verified with expr1:

            >>> z = expr - v
            >>> [xi.n(chop=1e-9) for xi in xvals if abs(z.subs(x, xi).n()) < 1e-9]
            []
            >>> z1 = expr1 - v
            >>> [xi.n(chop=1e-9) for xi in xvals if abs(z1.subs(x, xi).n()) < 1e-9]
            [-3.0]

        See Also
        ========

            - rsolve() for solving recurrence relationships
            - dsolve() for solving differential equations

```python
sol = sym.solve(df, x)
sol
```

$\displaystyle \left[ 0\right]$

```python
# Encuentra también soluciones complejas
sym.solve(x**2 + 1, x)
```

$\displaystyle \left[ - i, \  i\right]$

```python
# Puedo resolver ecuaciones que involucren más de una variable
sym.solve(x**2 * y + 3 * y**3 * x + 1, x)
```

$\displaystyle \left[ \right]$

Veamos la gráfica...

```python
# convertir f e una función que se pueda evaluar numéricamente (función lambdify de la librería sympy)
f_num = sym.lambdify(x, f, 'numpy')
```

```python
help(sym.lambdify)
```

    Help on function lambdify in module sympy.utilities.lambdify:

    lambdify(args, expr, modules=None, printer=None, use_imps=True, dummify=False)
        Translates a SymPy expression into an equivalent numeric function

        For example, to convert the SymPy expression ``sin(x) + cos(x)`` to an
        equivalent NumPy function that numerically evaluates it:

        >>> from sympy import sin, cos, symbols, lambdify
        >>> import numpy as np
        >>> x = symbols('x')
        >>> expr = sin(x) + cos(x)
        >>> expr
        sin(x) + cos(x)
        >>> f = lambdify(x, expr, 'numpy')
        >>> a = np.array([1, 2])
        >>> f(a)
        [1.38177329 0.49315059]

        The primary purpose of this function is to provide a bridge from SymPy
        expressions to numerical libraries such as NumPy, SciPy, NumExpr, mpmath,
        and tensorflow. In general, SymPy functions do not work with objects from
        other libraries, such as NumPy arrays, and functions from numeric
        libraries like NumPy or mpmath do not work on SymPy expressions.
        ``lambdify`` bridges the two by converting a SymPy expression to an
        equivalent numeric function.

        The basic workflow with ``lambdify`` is to first create a SymPy expression
        representing whatever mathematical function you wish to evaluate. This
        should be done using only SymPy functions and expressions. Then, use
        ``lambdify`` to convert this to an equivalent function for numerical
        evaluation. For instance, above we created ``expr`` using the SymPy symbol
        ``x`` and SymPy functions ``sin`` and ``cos``, then converted it to an
        equivalent NumPy function ``f``, and called it on a NumPy array ``a``.

        .. warning::
           This function uses ``exec``, and thus shouldn't be used on unsanitized
           input.

        Arguments
        =========

        The first argument of ``lambdify`` is a variable or list of variables in
        the expression. Variable lists may be nested. Variables can be Symbols,
        undefined functions, or matrix symbols. The order and nesting of the
        variables corresponds to the order and nesting of the parameters passed to
        the lambdified function. For instance,

        >>> from sympy.abc import x, y, z
        >>> f = lambdify([x, (y, z)], x + y + z)
        >>> f(1, (2, 3))
        6

        The second argument of ``lambdify`` is the expression, list of
        expressions, or matrix to be evaluated. Lists may be nested. If the
        expression is a list, the output will also be a list.

        >>> f = lambdify(x, [x, [x + 1, x + 2]])
        >>> f(1)
        [1, [2, 3]]

        If it is a matrix, an array will be returned (for the NumPy module).

        >>> from sympy import Matrix
        >>> f = lambdify(x, Matrix([x, x + 1]))
        >>> f(1)
        [[1]
         [2]]

        Note that the argument order here, variables then expression, is used to
        emulate the Python ``lambda`` keyword. ``lambdify(x, expr)`` works
        (roughly) like ``lambda x: expr`` (see :ref:`lambdify-how-it-works` below).

        The third argument, ``modules`` is optional. If not specified, ``modules``
        defaults to ``["scipy", "numpy"]`` if SciPy is installed, ``["numpy"]`` if
        only NumPy is installed, and ``["math", "mpmath", "sympy"]`` if neither is
        installed. That is, SymPy functions are replaced as far as possible by
        either ``scipy`` or ``numpy`` functions if available, and Python's
        standard library ``math``, or ``mpmath`` functions otherwise.

        ``modules`` can be one of the following types

         - the strings ``"math"``, ``"mpmath"``, ``"numpy"``, ``"numexpr"``,
           ``"scipy"``, ``"sympy"``, or ``"tensorflow"``. This uses the
           corresponding printer and namespace mapping for that module.
         - a module (e.g., ``math``). This uses the global namespace of the
           module. If the module is one of the above known modules, it will also
           use the corresponding printer and namespace mapping (i.e.,
           ``modules=numpy`` is equivalent to ``modules="numpy"``).
         - a dictionary that maps names of SymPy functions to arbitrary functions
           (e.g., ``{'sin': custom_sin}``).
         - a list that contains a mix of the arguments above, with higher priority
           given to entries appearing first (e.g., to use the NumPy module but
           override the ``sin`` function with a custom version, you can use
           ``[{'sin': custom_sin}, 'numpy']``).

        The ``dummify`` keyword argument controls whether or not the variables in
        the provided expression that are not valid Python identifiers are
        substituted with dummy symbols. This allows for undefined functions like
        ``Function('f')(t)`` to be supplied as arguments. By default, the
        variables are only dummified if they are not valid Python identifiers. Set
        ``dummify=True`` to replace all arguments with dummy symbols (if ``args``
        is not a string) - for example, to ensure that the arguments do not
        redefine any built-in names.

        .. _lambdify-how-it-works:

        How it works
        ============

        When using this function, it helps a great deal to have an idea of what it
        is doing. At its core, lambdify is nothing more than a namespace
        translation, on top of a special printer that makes some corner cases work
        properly.

        To understand lambdify, first we must properly understand how Python
        namespaces work. Say we had two files. One called ``sin_cos_sympy.py``,
        with

        .. code:: python

            # sin_cos_sympy.py

            from sympy import sin, cos

            def sin_cos(x):
                return sin(x) + cos(x)

        and one called ``sin_cos_numpy.py`` with

        .. code:: python

            # sin_cos_numpy.py

            from numpy import sin, cos

            def sin_cos(x):
                return sin(x) + cos(x)

        The two files define an identical function ``sin_cos``. However, in the
        first file, ``sin`` and ``cos`` are defined as the SymPy ``sin`` and
        ``cos``. In the second, they are defined as the NumPy versions.

        If we were to import the first file and use the ``sin_cos`` function, we
        would get something like

        >>> from sin_cos_sympy import sin_cos # doctest: +SKIP
        >>> sin_cos(1) # doctest: +SKIP
        cos(1) + sin(1)

        On the other hand, if we imported ``sin_cos`` from the second file, we
        would get

        >>> from sin_cos_numpy import sin_cos # doctest: +SKIP
        >>> sin_cos(1) # doctest: +SKIP
        1.38177329068

        In the first case we got a symbolic output, because it used the symbolic
        ``sin`` and ``cos`` functions from SymPy. In the second, we got a numeric
        result, because ``sin_cos`` used the numeric ``sin`` and ``cos`` functions
        from NumPy. But notice that the versions of ``sin`` and ``cos`` that were
        used was not inherent to the ``sin_cos`` function definition. Both
        ``sin_cos`` definitions are exactly the same. Rather, it was based on the
        names defined at the module where the ``sin_cos`` function was defined.

        The key point here is that when function in Python references a name that
        is not defined in the function, that name is looked up in the "global"
        namespace of the module where that function is defined.

        Now, in Python, we can emulate this behavior without actually writing a
        file to disk using the ``exec`` function. ``exec`` takes a string
        containing a block of Python code, and a dictionary that should contain
        the global variables of the module. It then executes the code "in" that
        dictionary, as if it were the module globals. The following is equivalent
        to the ``sin_cos`` defined in ``sin_cos_sympy.py``:

        >>> import sympy
        >>> module_dictionary = {'sin': sympy.sin, 'cos': sympy.cos}
        >>> exec('''
        ... def sin_cos(x):
        ...     return sin(x) + cos(x)
        ... ''', module_dictionary)
        >>> sin_cos = module_dictionary['sin_cos']
        >>> sin_cos(1)
        cos(1) + sin(1)

        and similarly with ``sin_cos_numpy``:

        >>> import numpy
        >>> module_dictionary = {'sin': numpy.sin, 'cos': numpy.cos}
        >>> exec('''
        ... def sin_cos(x):
        ...     return sin(x) + cos(x)
        ... ''', module_dictionary)
        >>> sin_cos = module_dictionary['sin_cos']
        >>> sin_cos(1)
        1.38177329068

        So now we can get an idea of how ``lambdify`` works. The name "lambdify"
        comes from the fact that we can think of something like ``lambdify(x,
        sin(x) + cos(x), 'numpy')`` as ``lambda x: sin(x) + cos(x)``, where
        ``sin`` and ``cos`` come from the ``numpy`` namespace. This is also why
        the symbols argument is first in ``lambdify``, as opposed to most SymPy
        functions where it comes after the expression: to better mimic the
        ``lambda`` keyword.

        ``lambdify`` takes the input expression (like ``sin(x) + cos(x)``) and

        1. Converts it to a string
        2. Creates a module globals dictionary based on the modules that are
           passed in (by default, it uses the NumPy module)
        3. Creates the string ``"def func({vars}): return {expr}"``, where ``{vars}`` is the
           list of variables separated by commas, and ``{expr}`` is the string
           created in step 1., then ``exec``s that string with the module globals
           namespace and returns ``func``.

        In fact, functions returned by ``lambdify`` support inspection. So you can
        see exactly how they are defined by using ``inspect.getsource``, or ``??`` if you
        are using IPython or the Jupyter notebook.

        >>> f = lambdify(x, sin(x) + cos(x))
        >>> import inspect
        >>> print(inspect.getsource(f))
        def _lambdifygenerated(x):
            return (sin(x) + cos(x))

        This shows us the source code of the function, but not the namespace it
        was defined in. We can inspect that by looking at the ``__globals__``
        attribute of ``f``:

        >>> f.__globals__['sin']
        <ufunc 'sin'>
        >>> f.__globals__['cos']
        <ufunc 'cos'>
        >>> f.__globals__['sin'] is numpy.sin
        True

        This shows us that ``sin`` and ``cos`` in the namespace of ``f`` will be
        ``numpy.sin`` and ``numpy.cos``.

        Note that there are some convenience layers in each of these steps, but at
        the core, this is how ``lambdify`` works. Step 1 is done using the
        ``LambdaPrinter`` printers defined in the printing module (see
        :mod:`sympy.printing.lambdarepr`). This allows different SymPy expressions
        to define how they should be converted to a string for different modules.
        You can change which printer ``lambdify`` uses by passing a custom printer
        in to the ``printer`` argument.

        Step 2 is augmented by certain translations. There are default
        translations for each module, but you can provide your own by passing a
        list to the ``modules`` argument. For instance,

        >>> def mysin(x):
        ...     print('taking the sin of', x)
        ...     return numpy.sin(x)
        ...
        >>> f = lambdify(x, sin(x), [{'sin': mysin}, 'numpy'])
        >>> f(1)
        taking the sin of 1
        0.8414709848078965

        The globals dictionary is generated from the list by merging the
        dictionary ``{'sin': mysin}`` and the module dictionary for NumPy. The
        merging is done so that earlier items take precedence, which is why
        ``mysin`` is used above instead of ``numpy.sin``.

        If you want to modify the way ``lambdify`` works for a given function, it
        is usually easiest to do so by modifying the globals dictionary as such.
        In more complicated cases, it may be necessary to create and pass in a
        custom printer.

        Finally, step 3 is augmented with certain convenience operations, such as
        the addition of a docstring.

        Understanding how ``lambdify`` works can make it easier to avoid certain
        gotchas when using it. For instance, a common mistake is to create a
        lambdified function for one module (say, NumPy), and pass it objects from
        another (say, a SymPy expression).

        For instance, say we create

        >>> from sympy.abc import x
        >>> f = lambdify(x, x + 1, 'numpy')

        Now if we pass in a NumPy array, we get that array plus 1

        >>> import numpy
        >>> a = numpy.array([1, 2])
        >>> f(a)
        [2 3]

        But what happens if you make the mistake of passing in a SymPy expression
        instead of a NumPy array:

        >>> f(x + 1)
        x + 2

        This worked, but it was only by accident. Now take a different lambdified
        function:

        >>> from sympy import sin
        >>> g = lambdify(x, x + sin(x), 'numpy')

        This works as expected on NumPy arrays:

        >>> g(a)
        [1.84147098 2.90929743]

        But if we try to pass in a SymPy expression, it fails

        >>> try:
        ...     g(x + 1)
        ... # NumPy release after 1.17 raises TypeError instead of
        ... # AttributeError
        ... except (AttributeError, TypeError):
        ...     raise AttributeError() # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        AttributeError:

        Now, let's look at what happened. The reason this fails is that ``g``
        calls ``numpy.sin`` on the input expression, and ``numpy.sin`` does not
        know how to operate on a SymPy object. **As a general rule, NumPy
        functions do not know how to operate on SymPy expressions, and SymPy
        functions do not know how to operate on NumPy arrays. This is why lambdify
        exists: to provide a bridge between SymPy and NumPy.**

        However, why is it that ``f`` did work? That's because ``f`` doesn't call
        any functions, it only adds 1. So the resulting function that is created,
        ``def _lambdifygenerated(x): return x + 1`` does not depend on the globals
        namespace it is defined in. Thus it works, but only by accident. A future
        version of ``lambdify`` may remove this behavior.

        Be aware that certain implementation details described here may change in
        future versions of SymPy. The API of passing in custom modules and
        printers will not change, but the details of how a lambda function is
        created may change. However, the basic idea will remain the same, and
        understanding it will be helpful to understanding the behavior of
        lambdify.

        **In general: you should create lambdified functions for one module (say,
        NumPy), and only pass it input types that are compatible with that module
        (say, NumPy arrays).** Remember that by default, if the ``module``
        argument is not provided, ``lambdify`` creates functions using the NumPy
        and SciPy namespaces.

        Examples
        ========

        >>> from sympy.utilities.lambdify import implemented_function
        >>> from sympy import sqrt, sin, Matrix
        >>> from sympy import Function
        >>> from sympy.abc import w, x, y, z

        >>> f = lambdify(x, x**2)
        >>> f(2)
        4
        >>> f = lambdify((x, y, z), [z, y, x])
        >>> f(1,2,3)
        [3, 2, 1]
        >>> f = lambdify(x, sqrt(x))
        >>> f(4)
        2.0
        >>> f = lambdify((x, y), sin(x*y)**2)
        >>> f(0, 5)
        0.0
        >>> row = lambdify((x, y), Matrix((x, x + y)).T, modules='sympy')
        >>> row(1, 2)
        Matrix([[1, 3]])

        ``lambdify`` can be used to translate SymPy expressions into mpmath
        functions. This may be preferable to using ``evalf`` (which uses mpmath on
        the backend) in some cases.

        >>> import mpmath
        >>> f = lambdify(x, sin(x), 'mpmath')
        >>> f(1)
        0.8414709848078965

        Tuple arguments are handled and the lambdified function should
        be called with the same type of arguments as were used to create
        the function:

        >>> f = lambdify((x, (y, z)), x + y)
        >>> f(1, (2, 4))
        3

        The ``flatten`` function can be used to always work with flattened
        arguments:

        >>> from sympy.utilities.iterables import flatten
        >>> args = w, (x, (y, z))
        >>> vals = 1, (2, (3, 4))
        >>> f = lambdify(flatten(args), w + x + y + z)
        >>> f(*flatten(vals))
        10

        Functions present in ``expr`` can also carry their own numerical
        implementations, in a callable attached to the ``_imp_`` attribute. This
        can be used with undefined functions using the ``implemented_function``
        factory:

        >>> f = implemented_function(Function('f'), lambda x: x+1)
        >>> func = lambdify(x, f(x))
        >>> func(4)
        5

        ``lambdify`` always prefers ``_imp_`` implementations to implementations
        in other namespaces, unless the ``use_imps`` input parameter is False.

        Usage with Tensorflow:

        >>> import tensorflow as tf
        >>> from sympy import Max, sin, lambdify
        >>> from sympy.abc import x

        >>> f = Max(x, sin(x))
        >>> func = lambdify(x, f, 'tensorflow')

        After tensorflow v2, eager execution is enabled by default.
        If you want to get the compatible result across tensorflow v1 and v2
        as same as this tutorial, run this line.

        >>> tf.compat.v1.enable_eager_execution()

        If you have eager execution enabled, you can get the result out
        immediately as you can use numpy.

        If you pass tensorflow objects, you may get an ``EagerTensor``
        object instead of value.

        >>> result = func(tf.constant(1.0))
        >>> print(result)
        tf.Tensor(1.0, shape=(), dtype=float32)
        >>> print(result.__class__)
        <class 'tensorflow.python.framework.ops.EagerTensor'>

        You can use ``.numpy()`` to get the numpy value of the tensor.

        >>> result.numpy()
        1.0

        >>> var = tf.Variable(2.0)
        >>> result = func(var) # also works for tf.Variable and tf.Placeholder
        >>> result.numpy()
        2.0

        And it works with any shape array.

        >>> tensor = tf.constant([[1.0, 2.0], [3.0, 4.0]])
        >>> result = func(tensor)
        >>> result.numpy()
        [[1. 2.]
         [3. 4.]]

        Notes
        =====

        - For functions involving large array calculations, numexpr can provide a
          significant speedup over numpy. Please note that the available functions
          for numexpr are more limited than numpy but can be expanded with
          ``implemented_function`` and user defined subclasses of Function. If
          specified, numexpr may be the only option in modules. The official list
          of numexpr functions can be found at:
          https://numexpr.readthedocs.io/en/latest/user_guide.html#supported-functions

        - In previous versions of SymPy, ``lambdify`` replaced ``Matrix`` with
          ``numpy.matrix`` by default. As of SymPy 1.0 ``numpy.array`` is the
          default. To get the old default behavior you must pass in
          ``[{'ImmutableDenseMatrix':  numpy.matrix}, 'numpy']`` to the
          ``modules`` kwarg.

          >>> from sympy import lambdify, Matrix
          >>> from sympy.abc import x, y
          >>> import numpy
          >>> array2mat = [{'ImmutableDenseMatrix': numpy.matrix}, 'numpy']
          >>> f = lambdify((x, y), Matrix([x, y]), modules=array2mat)
          >>> f(1, 2)
          [[1]
           [2]]

        - In the above examples, the generated functions can accept scalar
          values or numpy arrays as arguments.  However, in some cases
          the generated function relies on the input being a numpy array:

          >>> from sympy import Piecewise
          >>> from sympy.utilities.pytest import ignore_warnings
          >>> f = lambdify(x, Piecewise((x, x <= 1), (1/x, x > 1)), "numpy")

          >>> with ignore_warnings(RuntimeWarning):
          ...     f(numpy.array([-1, 0, 1, 2]))
          [-1.   0.   1.   0.5]

          >>> f(0)
          Traceback (most recent call last):
              ...
          ZeroDivisionError: division by zero

          In such cases, the input should be wrapped in a numpy array:

          >>> with ignore_warnings(RuntimeWarning):
          ...     float(f(numpy.array([0])))
          0.0

          Or if numpy functionality is not required another module can be used:

          >>> f = lambdify(x, Piecewise((x, x <= 1), (1/x, x > 1)), "math")
          >>> f(0)
          0

```python
f
```

$\displaystyle x^{2}$

```python
# f es una expresión simbólica y no se puede evaluar
f(10)
```

```python
# f_num sí se puede evaluar
f_num(10)
```

$\displaystyle 100$

```python
help(np.linspace)
```

    Help on function linspace in module numpy:

    linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0)
        Return evenly spaced numbers over a specified interval.

        Returns `num` evenly spaced samples, calculated over the
        interval [`start`, `stop`].

        The endpoint of the interval can optionally be excluded.

        .. versionchanged:: 1.16.0
            Non-scalar `start` and `stop` are now supported.

        Parameters
        ----------
        start : array_like
            The starting value of the sequence.
        stop : array_like
            The end value of the sequence, unless `endpoint` is set to False.
            In that case, the sequence consists of all but the last of ``num + 1``
            evenly spaced samples, so that `stop` is excluded.  Note that the step
            size changes when `endpoint` is False.
        num : int, optional
            Number of samples to generate. Default is 50. Must be non-negative.
        endpoint : bool, optional
            If True, `stop` is the last sample. Otherwise, it is not included.
            Default is True.
        retstep : bool, optional
            If True, return (`samples`, `step`), where `step` is the spacing
            between samples.
        dtype : dtype, optional
            The type of the output array.  If `dtype` is not given, infer the data
            type from the other input arguments.

            .. versionadded:: 1.9.0

        axis : int, optional
            The axis in the result to store the samples.  Relevant only if start
            or stop are array-like.  By default (0), the samples will be along a
            new axis inserted at the beginning. Use -1 to get an axis at the end.

            .. versionadded:: 1.16.0

        Returns
        -------
        samples : ndarray
            There are `num` equally spaced samples in the closed interval
            ``[start, stop]`` or the half-open interval ``[start, stop)``
            (depending on whether `endpoint` is True or False).
        step : float, optional
            Only returned if `retstep` is True

            Size of spacing between samples.

        See Also
        --------
        arange : Similar to `linspace`, but uses a step size (instead of the
                 number of samples).
        geomspace : Similar to `linspace`, but with numbers spaced evenly on a log
                    scale (a geometric progression).
        logspace : Similar to `geomspace`, but with the end points specified as
                   logarithms.

        Examples
        --------
        >>> np.linspace(2.0, 3.0, num=5)
        array([2.  , 2.25, 2.5 , 2.75, 3.  ])
        >>> np.linspace(2.0, 3.0, num=5, endpoint=False)
        array([2. ,  2.2,  2.4,  2.6,  2.8])
        >>> np.linspace(2.0, 3.0, num=5, retstep=True)
        (array([2.  ,  2.25,  2.5 ,  2.75,  3.  ]), 0.25)

        Graphical illustration:

        >>> import matplotlib.pyplot as plt
        >>> N = 8
        >>> y = np.zeros(N)
        >>> x1 = np.linspace(0, 10, N, endpoint=True)
        >>> x2 = np.linspace(0, 10, N, endpoint=False)
        >>> plt.plot(x1, y, 'o')
        [<matplotlib.lines.Line2D object at 0x...>]
        >>> plt.plot(x2, y + 0.5, 'o')
        [<matplotlib.lines.Line2D object at 0x...>]
        >>> plt.ylim([-0.5, 1])
        (-0.5, 1)
        >>> plt.show()

```python
# Coordenadas x (abscisas)
x_num = np.linspace(-10, 10, 100)
x_num
```

    array([-10.        ,  -9.7979798 ,  -9.5959596 ,  -9.39393939,
            -9.19191919,  -8.98989899,  -8.78787879,  -8.58585859,
            -8.38383838,  -8.18181818,  -7.97979798,  -7.77777778,
            -7.57575758,  -7.37373737,  -7.17171717,  -6.96969697,
            -6.76767677,  -6.56565657,  -6.36363636,  -6.16161616,
            -5.95959596,  -5.75757576,  -5.55555556,  -5.35353535,
            -5.15151515,  -4.94949495,  -4.74747475,  -4.54545455,
            -4.34343434,  -4.14141414,  -3.93939394,  -3.73737374,
            -3.53535354,  -3.33333333,  -3.13131313,  -2.92929293,
            -2.72727273,  -2.52525253,  -2.32323232,  -2.12121212,
            -1.91919192,  -1.71717172,  -1.51515152,  -1.31313131,
            -1.11111111,  -0.90909091,  -0.70707071,  -0.50505051,
            -0.3030303 ,  -0.1010101 ,   0.1010101 ,   0.3030303 ,
             0.50505051,   0.70707071,   0.90909091,   1.11111111,
             1.31313131,   1.51515152,   1.71717172,   1.91919192,
             2.12121212,   2.32323232,   2.52525253,   2.72727273,
             2.92929293,   3.13131313,   3.33333333,   3.53535354,
             3.73737374,   3.93939394,   4.14141414,   4.34343434,
             4.54545455,   4.74747475,   4.94949495,   5.15151515,
             5.35353535,   5.55555556,   5.75757576,   5.95959596,
             6.16161616,   6.36363636,   6.56565657,   6.76767677,
             6.96969697,   7.17171717,   7.37373737,   7.57575758,
             7.77777778,   7.97979798,   8.18181818,   8.38383838,
             8.58585859,   8.78787879,   8.98989899,   9.19191919,
             9.39393939,   9.5959596 ,   9.7979798 ,  10.        ])

```python
y_num = f_num(x_num)
y_num
```

    array([1.00000000e+02, 9.60004081e+01, 9.20824406e+01, 8.82460973e+01,
           8.44913784e+01, 8.08182838e+01, 7.72268136e+01, 7.37169677e+01,
           7.02887460e+01, 6.69421488e+01, 6.36771758e+01, 6.04938272e+01,
           5.73921028e+01, 5.43720029e+01, 5.14335272e+01, 4.85766758e+01,
           4.58014488e+01, 4.31078461e+01, 4.04958678e+01, 3.79655137e+01,
           3.55167840e+01, 3.31496786e+01, 3.08641975e+01, 2.86603408e+01,
           2.65381084e+01, 2.44975003e+01, 2.25385165e+01, 2.06611570e+01,
           1.88654219e+01, 1.71513111e+01, 1.55188246e+01, 1.39679625e+01,
           1.24987246e+01, 1.11111111e+01, 9.80512193e+00, 8.58075707e+00,
           7.43801653e+00, 6.37690032e+00, 5.39740843e+00, 4.49954086e+00,
           3.68329762e+00, 2.94867871e+00, 2.29568411e+00, 1.72431385e+00,
           1.23456790e+00, 8.26446281e-01, 4.99948985e-01, 2.55076013e-01,
           9.18273646e-02, 1.02030405e-02, 1.02030405e-02, 9.18273646e-02,
           2.55076013e-01, 4.99948985e-01, 8.26446281e-01, 1.23456790e+00,
           1.72431385e+00, 2.29568411e+00, 2.94867871e+00, 3.68329762e+00,
           4.49954086e+00, 5.39740843e+00, 6.37690032e+00, 7.43801653e+00,
           8.58075707e+00, 9.80512193e+00, 1.11111111e+01, 1.24987246e+01,
           1.39679625e+01, 1.55188246e+01, 1.71513111e+01, 1.88654219e+01,
           2.06611570e+01, 2.25385165e+01, 2.44975003e+01, 2.65381084e+01,
           2.86603408e+01, 3.08641975e+01, 3.31496786e+01, 3.55167840e+01,
           3.79655137e+01, 4.04958678e+01, 4.31078461e+01, 4.58014488e+01,
           4.85766758e+01, 5.14335272e+01, 5.43720029e+01, 5.73921028e+01,
           6.04938272e+01, 6.36771758e+01, 6.69421488e+01, 7.02887460e+01,
           7.37169677e+01, 7.72268136e+01, 8.08182838e+01, 8.44913784e+01,
           8.82460973e+01, 9.20824406e+01, 9.60004081e+01, 1.00000000e+02])

```python
# graficar
# Crear ventana de graficos y damos medidas de la ventana
plt.figure(figsize=(6, 4))
# Sirve para hacer el grafico y determinar sus caracteristicas
plt.plot(x_num, y_num, label='$f(x)=x^2$')
# Punto de mínimo
plt.plot(0, f_num(0), 'o', label='Mínimo de $f(x)$')
# Los signos de $ son para escribir expresiones matemáticas
# Nombre del eje x de la grafica
plt.xlabel('$x$')
# Nombre del eje y
plt.ylabel('$y$')
# Sirve para poner las etiquetas de las graficas
plt.legend()
# Sirve para poner la cuadricula
plt.grid()
```

**Otra manera de hacer lo anterior**

Concepto de función...

```python
# Concepto de función
# def nombre_de_la_funcion(argumento[s]):
#     --> Operación 1
#     --> Operación 2
#     --> ...
#     --> Operación N
#     return [lo que vaya a devolver]
def cuadrado(x):
    return x**2
```

```python
cuadrado(x)
```

$\displaystyle x^{2}$

```python
cuadrado(x_num)
```

    array([1.00000000e+02, 9.60004081e+01, 9.20824406e+01, 8.82460973e+01,
           8.44913784e+01, 8.08182838e+01, 7.72268136e+01, 7.37169677e+01,
           7.02887460e+01, 6.69421488e+01, 6.36771758e+01, 6.04938272e+01,
           5.73921028e+01, 5.43720029e+01, 5.14335272e+01, 4.85766758e+01,
           4.58014488e+01, 4.31078461e+01, 4.04958678e+01, 3.79655137e+01,
           3.55167840e+01, 3.31496786e+01, 3.08641975e+01, 2.86603408e+01,
           2.65381084e+01, 2.44975003e+01, 2.25385165e+01, 2.06611570e+01,
           1.88654219e+01, 1.71513111e+01, 1.55188246e+01, 1.39679625e+01,
           1.24987246e+01, 1.11111111e+01, 9.80512193e+00, 8.58075707e+00,
           7.43801653e+00, 6.37690032e+00, 5.39740843e+00, 4.49954086e+00,
           3.68329762e+00, 2.94867871e+00, 2.29568411e+00, 1.72431385e+00,
           1.23456790e+00, 8.26446281e-01, 4.99948985e-01, 2.55076013e-01,
           9.18273646e-02, 1.02030405e-02, 1.02030405e-02, 9.18273646e-02,
           2.55076013e-01, 4.99948985e-01, 8.26446281e-01, 1.23456790e+00,
           1.72431385e+00, 2.29568411e+00, 2.94867871e+00, 3.68329762e+00,
           4.49954086e+00, 5.39740843e+00, 6.37690032e+00, 7.43801653e+00,
           8.58075707e+00, 9.80512193e+00, 1.11111111e+01, 1.24987246e+01,
           1.39679625e+01, 1.55188246e+01, 1.71513111e+01, 1.88654219e+01,
           2.06611570e+01, 2.25385165e+01, 2.44975003e+01, 2.65381084e+01,
           2.86603408e+01, 3.08641975e+01, 3.31496786e+01, 3.55167840e+01,
           3.79655137e+01, 4.04958678e+01, 4.31078461e+01, 4.58014488e+01,
           4.85766758e+01, 5.14335272e+01, 5.43720029e+01, 5.73921028e+01,
           6.04938272e+01, 6.36771758e+01, 6.69421488e+01, 7.02887460e+01,
           7.37169677e+01, 7.72268136e+01, 8.08182838e+01, 8.44913784e+01,
           8.82460973e+01, 9.20824406e+01, 9.60004081e+01, 1.00000000e+02])

```python
dcuadrado = sym.diff(cuadrado(x), x)
dcuadrado
```

$\displaystyle 2 x$

```python
sol = sym.solve(dcuadrado, x)
sol
```

$\displaystyle \left[ 0\right]$

```python
# graficar
# Crear ventana de graficos y damos medidas de la ventana
plt.figure(figsize=(6, 4))
# Sirve para hacer el grafico y determinar sus caracteristicas
plt.plot(x_num, cuadrado(x_num), label='$f(x)=x^2$')
# Punto de mínimo
plt.plot(0, cuadrado(0), 'o', label='Mínimo de $f(x)$')
# Los signos de $ son para escribir expresiones matemáticas
# Nombre del eje x de la grafica
plt.xlabel('$x$')
# Nombre del eje y
plt.ylabel('$y$')
# Sirve para poner las etiquetas de las graficas
plt.legend()
# Sirve para poner la cuadricula
plt.grid()
```

El converso del teorema anterior no es cierto.

### Actividad
Considere $g(x)=x^3$.
- Usando `sympy`, muestre que $g'(0)=0$.
- Sin embargo, descartar que $x=0$ es un extremo de $g(x)$ viendo su **gráfica**.

```python
# Declarar la variable simbolica x
sym.var('x')
```

$\displaystyle x$

```python
# Definimos funcion g(x)
def g(x):
    return x**3
```

```python
# Derivamos g(x)
dg = sym.diff(g(x), x)
dg
```

$\displaystyle 3 x^{2}$

```python
# Puntos criticos
sol = sym.solve(dg, x)
sol
```

$\displaystyle \left[ 0\right]$

```python
# graficar
x_num = np.linspace(-1, 1, 100)
y_num = g(x_num)
# Crear ventana de graficos y damos medidas de la ventana
plt.figure(figsize=(6, 4))
plt.axvline(x=0, color='k')
plt.axhline(y=0, color='k')
# Sirve para hacer el grafico y determinar sus caracteristicas
plt.plot(x_num, y_num, linewidth=3, label='$g(x)=x^3$')
plt.plot(0, 0, 'o', markersize=20, label='$(0, 0)$')
# Nombre del eje x de la grafica
plt.xlabel('$x$')
# Nombre del eje y
plt.ylabel('$y$')
# Sirve para poner las etiquetas de las graficas
plt.legend()
# Sirve para poner la cuadricula
plt.grid()
```

## 2. Criterio de la segunda derivada

Sea $f(x)$ una función tal que $f’(c)=0$ y cuya segunda derivada existe en un intervalo abierto que contiene a $c$.
- Si $f’’(c)>0$, entonces $f(c)$ es un mínimo relativo.
- Si $f’’(c)<0$, entonces $f(c)$ es un máximo relativo.
- Si $f’’(c)=0$, entonces el criterio no decide.

### Ejemplo

Mostrar, usando `sympy`, que la función $f(x)=x^2$ tiene un mínimo relativo en $x=0$.

Ya vimos que $f'(0)=0$. Notemos que:

```python
# Método subs

```

```python

```

```python

```

```python

```

```python
# Sacamos la segunda derivada

```

```python

```

Por tanto, por el criterio de la segunda derivada, $f(0)=0$ es un mínimo relativo (en efecto, el mínimo global).

### Ejemplo
¿Qué pasa con $g(x)=x^3$ al intentar utilizar el criterio de la segunda derivada? (usar `sympy`).

```python
# Definimos la función g(x)=x^3

```

```python
# Sacamos la derivada de la función g respecto a x g'(x)=3x^2

```

```python
# Observamos que g'(0)=0 (es un punto crítico)

```

```python
# Obtenemos la segunda derivada de la función g respecto a x g''(x)=6x

```

```python
# Evaluamos g''(0)

```

Como $g''(0)=0$ entonces el criterio de la segunda derivada no concluye.

### Actividad

¿Qué pasa con $h(x)=x^4$ al intentar utilizar el criterio de la segunda derivada?.

```python

```

```python

```

```python

```

```python

```

```python

```

## 3. Método para determinar extremos absolutos de una función continua y=f(x) en [a,b]

- Determinar todos los valores críticos $c_1, c_2, c_3, \dots, c_n$ en $(a,b)$.
- Evaluar $f$ en todos los valores críticos y en los extremos $x=a$ y $x=b$.
- El más grande y el más pequeño de los valores de la lista $f(a), f(b), f(c_1), f(c_2), \dots, f(c_n)$ son el máximo absoluto y el mínimo absoluto, respectivamente, de f en el intervalo [a,b].

### Ejemplo

Determinar los extremos absolutos de $f(x)=x^2-6x$ en $\left[0,5\right]$.

Obtenemos los puntos críticos de $f$ en $\left[0,5\right]$:

```python
# Definimos f

```

```python
# Derivamos f

```

```python
# Resolver la ecuación f'(x)=0 para encontrar puntos críticos

```

Evaluamos $f$ en los extremos y en los puntos críticos:

```python

```

Concluimos que el máximo absoluto de $f$ en $\left[0,5\right]$ es $0$ y se alcanza en $x=0$, y que el mínimo absoluto es $-9$ y se alcanza en $x=3$.

```python
# graficar
# Crear ventana de graficos y damos medidas de la ventana

# Sirve para hacer el grafico y determinar sus caracteristicas

# Nombre del eje x de la grafica

# Nombre del eje y

# Sirve para poner las etiquetas de las graficas

# Sirve para poner la cuadricula

```

### Actividad
Determinar los valores extremos absolutos de $h(x)=x^3-3x$ en $\left[-2.2,1.8\right]$, usando `sympy`. Mostrar en una gráfica.

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

### En varias variables...

El procedimiento es análogo.

Si una función $f:\mathbb{R}^n\to\mathbb{R}$ alcanza un máximo o mínimo local en $\boldsymbol{x}=\boldsymbol{c}\in\mathbb{R}^n$, y $f$ es diferenciable en el punto $\boldsymbol{x}=\boldsymbol{c}$, entonces $\left.\frac{\partial f}{\partial \boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{c}}=\boldsymbol{0}$ (todas las derivadas parciales en el punto $\boldsymbol{x}=\boldsymbol{c}$ son cero).

**Criterio de la segunda derivada:** para ver si es máximo o mínimo, se toma la segunda derivada (matriz jacobiana) y se verifica definición negativa o positiva, respectivamente.

Si se restringe a cierta región, hay ciertas técnicas. La más general, pero también la más compleja es la de **multiplicadores de Lagrange**.

**Ejemplo:** hacer a mano a la vez para corroborar...

```python
sym.var('x y')
x, y
```

```python
def f(x, y):
    return x**2 + y**2
```

```python
dfx = sym.diff(f(x,y), x)
dfy = sym.diff(f(x,y), y)
dfx, dfy
```

```python
xy_c = sym.solve([dfx, dfy], [x, y])
xy_c
```

```python
x_c, y_c = xy_c[x], xy_c[y]
x_c, y_c
```

```python
d2fx = sym.diff(f(x,y), x, 2)
d2fy = sym.diff(f(x,y), y, 2)
dfxy = sym.diff(f(x,y), x, y)

Jf = sym.Matrix([[d2fx, dfxy], [dfxy, d2fy]])
Jf.eigenvals()
```

```python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

x = np.linspace(-2, 2, 100)
y = x
X, Y = np.meshgrid(x, y)

ax.plot_surface(X, Y, f(X, Y))
ax.plot([x_c], [y_c], [f(x_c,y_c)], '*r')
```

# Anuncios parroquiales

## 1. [Curso gratis sugerido](https://www.kaggle.com/learn/python)

## 2. La próxima clase hay quiz (clases 1 y 2 - lo que hemos visto).

<footer id="attribution" style="float:right; color:#808080; background:#fff;">
Created with Jupyter by Esteban Jiménez Rodríguez.
</footer>