# Thorpe Scales

This section is available as a jupyter notebook in the `docs/` directory or at https://github.com/modscripps/mixsea/tree/main/docs.

Here we'll examine some of the different parameter options used for estimating the turbulent dissipation rate, $\epsilon$, from Thorpe scales. The method was first published by Stephen Thorpe– see Thorpe (1977).

**Basic idea**

Thorpe's method for estimating dissipation relies on the Ozmidov scale

\begin{equation}
L_{O}=\alpha \epsilon^{1/2} N^{-3/2},
\end{equation}

defined by dimensional analysis as the scale at which stratification begins to affect turbulence, where $\alpha=\mathcal{O}(1)$ is a proportionality constant and $N$ is a measure of the background stratification (we will see later the importance of its definition).

Thorpe's insight was to identify the Ozmidov scale with the scale $L_{T}$ (now named after him) of the largest vertical overturns (gravitational instability) observed in a density profile. He proposed sorting the unstable profile to produce a stable one, keeping track of individual water parcels vertical displacements to use their root mean square displacement $d(z)$ as a proxy for the overturn scale
\begin{equation}
L_{T} = \sqrt{\overline{d^{2}}}.
\end{equation}

Setting $L_{O} = L_{T}$ and inverting for $\epsilon$ gives
\begin{equation}
\boxed{\epsilon = C_{0} L_{T}^{2} N^{3},}
\end{equation}

where we identify $C_{0} = \alpha^{-2}$ as an $\mathcal{O}(1)$ free parameter, perhaps tuned based on microstructure measurements of $\epsilon$.

***Warning:** The Thorpe scale method is an **event-based** turbulence parameterization, so one must be wary of spatial and temporal intermittency when interpreting results.*

Thus, the Thorpe scale method in practice can be separated three steps: 1) finding the overturns, 2) determining their Thorpe scales by sorting, and 3) determining the background stratification.

## Example

First, we load modules and example data.

```python
import mixsea as mx
import numpy as np
import matplotlib.pyplot as plt

ctd = mx.helpers.read_ctd_testfile()
```

Our example data contain some NaN values, as is common with observational datasets. Below we isolate the good data by removing NaNs.

```python
notnan = np.isfinite(ctd["depth"]) & np.isfinite(ctd["t"]) & np.isfinite(ctd["SP"])

depth = ctd["depth"][notnan]
t = ctd["t"][notnan]
SP = ctd["SP"][notnan]
lon = ctd["lon"][0]
lat = ctd["lat"][0]
```

### A detailed look at the output and diagnostics

Now we run the Thorpe scale function on the good test data, selecting input parameters that are reasonable for the deep ocean.

```python
dnoise = 5e-4  # Noise parameter
alpha = 0.95  # Coefficient relating the Thorpe and Ozmidov scales.
# Background value of epsilon applied where no overturns are detected.
background_eps = np.nan
# Do not use the intermediate profile method
use_ip = False
# Critical value of the overturn ratio
Roc = 0.3

# Calculate Thorpe scales and diagnostics.
eps, N2, diag = mx.overturn.eps_overturn(
    depth,
    t,
    SP,
    lon,
    lat,
    dnoise=dnoise,
    alpha=alpha,
    Roc=Roc,
    background_eps=background_eps,
    use_ip=use_ip,
    return_diagnostics=True,
)
```

The standard output `eps` and `N2` (buoyancy frequency squared) have been quality controlled. The type and strictness of quality control can be set using arguments to `eps_overturn` (see the function docstring for details) In addition, numerous diagnostic variables are output in the dictionary `diag`. The diagnostic dictionary contains output from pre-quality control, as well as, flags which indicate the quality control state and miscellanous other variables that might be useful.

```python
for key in diag:
    print(key)
```

Some of the diagnostics are as follows:

* `eps`: the dissipation rate estimated before applying quality control flags.
* `N2`: the buoyancy frequency squared estimated before applying quality control flags.
* `Lt`: the Thorpe scale.
* `thorpe_disp`: the Thorpe displacements.
* `dens`: potential density (this may appear 'steppy' due to the use of different reference pressures at for different pressure ranges).
* `dens_sorted`: sorted potential density.
* `dens_ip`: density from the intermediate profile method. (Does not appear in the list above because the intermediate profile method was not used)
* `t_ip`: temperature from the intermediate profile method. (Does not appear in the list above because intermediate profile method was not used)
* `Ro`: the overturn ratio.

Some of the quality control flags:

* `noise_flag`: if density/CT difference from the top to bottom of the overturn is less than the noise parameter, this flag will be True.
* `N2_flag`: if buoyancy frequency is negative, this flag will be True.
* `ends_flag`: if an overturn contains the first or last point in the data, this flag will be True.
* `Ro_flag`: if the overturn ratio is less than the critical value, this flag will be True.

If we zoom into a segment, it is easier to understand what these diagnostics mean.

```python
# Plot only in the depth range:
cut = (depth > 4260) & (depth < 4500)
zc = depth[cut]

# finding unsorting indices for intuitive Thorpe displacements
#idx_unsorted = np.argsort(idx_sorted)

#axs[1].plot(thorpe_displacement[idx_unsorted][inoverturn_unsorted], z[inoverturn_unsorted])

fig, axs = plt.subplots(1, 4, sharey=True, figsize=(9, 6))
axs[0].plot(diag["dens"][cut]-1000, zc, label="density", lw=1.)
axs[0].plot(diag["dens_sorted"][cut]-1000, zc, label="sorted density", lw=1.)
axs[1].plot(diag["eps"][cut], zc, label="rejected epsilon", color="r")
axs[1].plot(eps[cut], zc, label="accepted epsilon", color="g")
axs[2].plot(diag["N2"][cut], zc, label="rejected N2", color="r")
axs[2].plot(N2[cut], zc, label="accepted N2", color="g")
axs[3].plot(diag["thorpe_disp"][cut], zc, label="Thorpe displacement")
axs[3].plot(diag["Lt"][cut], zc, label="±Thorpe scale")
axs[3].plot(-diag["Lt"][cut], zc, "C1--")
axs[0].invert_yaxis()
axs[0].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[1].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[2].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[3].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[0].set_ylabel("Depth [m]")
axs[0].set_xlabel("Potential density [kg m$^{-3}$]")
axs[1].set_xlabel(r"$\epsilon$ [W kg$^{-1}$]")
axs[2].set_xlabel(r"$N^2$ [rad$^2$ s$^{-2}$]")
axs[3].set_xlabel("Thorpe displacement [m]")

fig.align_labels()
```

Starting from the left, we have potential density and sorted potential density. Overturning patches should correspond to regions where the potential density is different from the sorted potential density. Next is the dissipation rate, $\epsilon$, plotted in green are the values that have passed quality control and in red those that were rejected by some criteria. The plot looks a bit odd because there are jumps in $\epsilon$. This is caused by small overturns adjacent to large overturns. Ultimately, a lot of small overturns will be rejected by quality control. Third from the left is buoyancy frequency, which can be calculated in a number of different ways. We will explore this in more detail later. Last is the Thorpe displacements and Thorpe length scale. Large negative values atop large positive values represent large overturns; whether this leads to large dissipation depends on the stratification.

The state (true/false) of some of the quality control flags is plotted below for the same segment of data.

```python
fig, axs = plt.subplots(
    1, 5, sharey=True, figsize=(9, 6), gridspec_kw={"width_ratios": [2, 1, 1, 1, 2]}
)
axs[0].plot(diag["thorpe_disp"][cut], zc, label="Thorpe displacement")
axs[0].plot(diag["Lt"][cut], zc, label="Thorpe scale")
axs[0].plot(-diag["Lt"][cut], zc, "C1--")
axs[1].plot(diag["noise_flag"][cut], zc, color="r")
axs[2].plot(diag["N2_flag"][cut], zc, color="r")
axs[3].plot(diag["ends_flag"][cut], zc, color="r")
axs[4].plot(diag["Ro"][cut], zc, label="Ro value")
axs[4].plot(diag["Ro_flag"][cut], zc, color="r", label="Ro flag")
axs[4].axvline(Roc, color="k", ls=":", label="Ro critical")
axs[0].invert_yaxis()
axs[0].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[4].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[0].set_ylabel("Depth [m]")
axs[0].set_xlabel("Thorpe displacement [m]")
axs[1].set_xlabel("Noise flag")
axs[2].set_xlabel("N2 flag")
axs[3].set_xlabel("Ends flag")
axs[4].set_xlabel("Ro flag and value")

fig.align_labels()
```

The noise flag is raised (true) where the density difference (top to bottom) of the overturning region is smaller than the noise parameter. The N2 flag has not been raised anywhere, indicating there are no regions of negative buoyancy frequency. The ends flag is raised for the last overturn since it contains the end point of the data. However, it is just a warning flag and not used to quality control the output. The Ro flag is raised where the overturn ratio is less than the critical value Gargett and Garner (2008). Note that for this demonstration we have set the critical overturn ratio a bit higher than would be normal.

### The intermediate profile method

The intermediate profile method attempts to reduce the influence of random noise when estimating overturns. For technical details, see Ferron et al. (1998) and Gargett and Garner (2008).

What does the method do? Here we construct three intermediate profiles from a segment of temperature data.

```python
acc = 1e-3
hinge = 0

# Restrict ourselves to:
cut = (depth < 4300) & (depth > 4250)

tup = mx.overturn.intermediate_profile(t[cut], acc, hinge, 'up')
tdo = mx.overturn.intermediate_profile(t[cut], acc, hinge, 'down')
tav = mx.overturn.intermediate_profile(t[cut], acc, hinge, 'ave')

fig, ax = plt.subplots(1, 1)
ax.invert_yaxis()
ax.plot(t[cut], depth[cut], label='data')
ax.plot(tup, depth[cut], label='up')
ax.plot(tdo, depth[cut], label='down')
ax.plot(tav, depth[cut], label='ave')
ax.legend()
ax.set_xlabel('Temperature [$^\circ$C]')
ax.set_ylabel('Depth [m]');
```

The method creates a new profile that moves in increments of the argument `acc`, which is a measure of the accuracy of the observations. A region that looks unstable in the data (such as the cold water over light in the region around 4270 m) might be smoothed out in the intermediate profile because the fluctuations are less than the accuracy. The method can be applied top down (`down`), bottom up (`up`) or the average of up and down (`ave`).

Now we investigate what the method does to estimates of $\epsilon$.

```python
dnoise = 5e-4
alpha = 0.95
# Use the intermediate profile method
use_ip = True

eps, N2, diag = mx.overturn.eps_overturn(
    depth,
    t,
    SP,
    lon,
    lat,
    dnoise=dnoise,
    alpha=alpha,
    use_ip=use_ip,
    return_diagnostics=True,
)
```

Plot the same depth range as the case that did not use the method.

```python
cut = (depth > 4260) & (depth < 4500)
zc = depth[cut]

fig, axs = plt.subplots(1, 4, sharey=True, figsize=(9, 6))
axs[0].plot(diag["dens"][cut]-1000, zc, label="density")
axs[0].plot(diag["dens_sorted"][cut]-1000, zc, label="sorted density")
axs[0].plot(diag["dens_ip"][cut]-1000, zc, label="intermediate density")
axs[1].plot(diag["eps"][cut], zc, label="rejected epsilon", color="r")
axs[1].plot(eps[cut], zc, label="accepted epsilon", color="g")
axs[2].plot(diag["N2"][cut], zc, label="rejected N2", color="r")
axs[2].plot(N2[cut], zc, label="accepted N2", color="g")
axs[3].plot(diag["thorpe_disp"][cut], zc, label="Thorpe displacement")
axs[3].plot(diag["Lt"][cut], zc, label="±Thorpe scale")
axs[3].plot(-diag["Lt"][cut], zc, "C1--")
axs[0].invert_yaxis()
axs[0].legend(loc="lower left", bbox_to_anchor=(-0.1, 1))
axs[1].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[2].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[3].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[0].set_ylabel("Depth [m]")
axs[0].set_xlabel("Potential density [kg m$^{-3}$]")
axs[1].set_xlabel(r"$\epsilon$ [W kg$^{-1}$]")
axs[2].set_xlabel(r"$N^2$ [rad$^2$ s$^{-2}$]")
axs[3].set_xlabel("Thorpe displacement [m]")

fig.align_labels()
```

Now we're using the intermediate profile method, the results have changed dramatically. Overturns are diagnosed from the intermediate profile of density, which is different to the actual density. The overturning patches may be of different sizes to those estimated without the intermediate profile. Sorting the intermediate profile results in strange looking Thorpe displacements, because the sorting algorithm struggles when many data points have an identical value. There is an argument to be made the method should not be applied to data that has already been bin averaged (as ours has) and the method may work better on raw CTD data that is more noisy.

Because the intermediate profile method fundamentally changes many aspects of the Thorpe scale method, including the size of detected overturns and the Thorpe displacements, it is not straightforward or necessarily meaningful to raise a flag for rejected overturns. Consequently, most flags are disabled when using the method and so are not shown.

### Compare methods of estimating $N^2$

There are a few different options for calculating buoyancy frequency:

```python
use_ip = False

# Choose buoyancy frequency method
N2_method = "teosp1"
eps_teosp1, N2_teosp1 = mx.overturn.eps_overturn(
    depth, t, SP, lon, lat, use_ip=use_ip, N2_method=N2_method,
)

N2_method = "teos"
eps_teos, N2_teos = mx.overturn.eps_overturn(
    depth, t, SP, lon, lat, dnoise, use_ip=use_ip, N2_method=N2_method,
)

N2_method = "bulk"
eps_bulk, N2_bulk = mx.overturn.eps_overturn(
    depth, t, SP, lon, lat, use_ip=use_ip, N2_method=N2_method,
)

N2_method = "endpt"
eps_endpt, N2_endpt = mx.overturn.eps_overturn(
    depth, t, SP, lon, lat, use_ip=use_ip, N2_method=N2_method,
)
```

A brief explanation of the methods:

* `teos`: This method estimates $N^2$ from the thermodynamic equation of state 2010 (TEOS), using the tempurature and salinity at the first and last point in the overturn.
* `toesp1`: This method is the same as above except it uses the temperature and salinity from the points immediately above and below the overturn (p1 meaning plus 1).
* `bulk`: This method comes from Smyth et al. (2001) and has the advantage of being insensitive to errors in determining the patch size. Buoyancy frequency is proportional to the root mean square density anomaly of an overturn, divided by the Thorpe scale.
* `endpt`: This simple method estimates buoyancy frequency from the potential density gradient across each overturn.

More methods may be added in future versions of `mixsea`.

```python
# Plot only in the depth range:
cut = (depth > 4225) & (depth < 4350)
zc = depth[cut]

fig, axs = plt.subplots(1, 3, sharey=True, figsize=(9, 6))
axs[0].plot(diag["dens"][cut]-1000, zc, label="density")
axs[0].plot(diag["dens_sorted"][cut]-1000, zc, label="sorted density")
axs[1].plot(eps_teosp1[cut], zc, label="teosp1")
axs[1].plot(eps_teos[cut], zc, label="teos")
axs[1].plot(eps_bulk[cut], zc, label="bulk")
axs[1].plot(eps_endpt[cut], zc, label="endpt")
axs[2].plot(N2_teosp1[cut], zc, label="teosp1")
axs[2].plot(N2_teos[cut], zc, label="teos")
axs[2].plot(N2_bulk[cut], zc, label="bulk")
axs[2].plot(N2_endpt[cut], zc, label="endpt")
axs[0].invert_yaxis()
axs[0].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[1].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[2].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[0].set_ylabel("Depth [m]")
axs[0].set_xlabel("Potential density [kg m$^{-3}$]")
axs[1].set_xlabel(r"$\epsilon$ [W kg$^{-1}$]")
axs[2].set_xlabel(r"$N^2$ [rad$^2$ s$^{-2}$]")

fig.align_labels()
```

The different methods can alter the dissipation rate estimate by almost an order of magnitude. The `endpt` and `teos` methods produce such similar results that they are indistinguishable. The `teosp1` method tends to produce higher $N^2$ estimates, because there is often a jump in stratification adjacent to an overturning patch. The `bulk` method tends to give the smallest $N^2$ estimates.

### Thorpe scales from temperature alone

In some cases it may be desirable to use temperature, rather than density, to detect unstable patches. There are two different ways of doing this:

* Provide both temperature and salinity profiles but set `overturns_from_t = True`. In this case, overturns will be diagnosed from the conservative temperature profile or temperature profile, depending on the equation of state chosen. Buoyancy frequency will be estimated from the equation of state or potential density as specified by `N2_method`.

* Provide as arguments to `eps_overturn` a profile of temperature and a constant salinity. Pseudo potential density will be calculated assuming a constant salinity profile. Overturns will be diagnosed from the pseudo potential density profile. Depending on the `N2_method` chosen, buoyancy frequency may be estimated from pseudo potential density or the equation of state at constant salinity. As such, buoyancy frequency values will be incorrect in the ocean but perhaps useful for estimating $\epsilon$. It is probably a good idea to choose a plausible salinity value, such as 0 psu for a fresh water environment or 35 psu for an ocean environment.

Note that `dnoise` should describe the temperature noise level if `overturns_from_t = True` and the density noise level at constant salinity if salinity is constant.

```python
use_ip = False
N2_method = "teos"

eps_t, N2_t = mx.overturn.eps_overturn(
    depth, t, 35, lon, lat, use_ip=use_ip, N2_method=N2_method,
)

eps_ts, N2_ts = mx.overturn.eps_overturn(
    depth,
    t,
    SP,
    lon,
    lat,
    dnoise=2e-4,
    use_ip=use_ip,
    N2_method=N2_method,
    overturns_from_t=True,
)

eps, N2 = mx.overturn.eps_overturn(
    depth, t, SP, lon, lat, dnoise, use_ip=use_ip, N2_method=N2_method,
)

fig, axs = plt.subplots(1, 2, sharey=True, figsize=(6, 6))
axs[0].plot(eps_ts[cut], zc, label="CT")
axs[0].plot(eps_t[cut], zc, label="pseudo potential density")
axs[0].plot(eps[cut], zc, label="potential density (normal case)")
axs[1].plot(N2_ts[cut], zc)
axs[1].plot(N2_t[cut], zc)
axs[1].plot(N2[cut], zc)
axs[0].invert_yaxis()
axs[0].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[0].set_ylabel("Depth [m]")
axs[0].set_xlabel(r"$\epsilon$ [W kg$^{-1}$]")
axs[1].set_xlabel(r"$N^2$ [rad$^2$ s$^{-2}$]")

fig.align_labels()
```

The smaller noise parameter in the `overturns_from_t` case means that more overturns are found, however, $\epsilon$ tends to be a bit lower than the normal case.

### Linear equation of state

By default, Thorpe scales are estimated from density using the nonlinear [TEOS-10](https://www.teos-10.org/) equation of state. It is also possible to use a linear equation of state, e.g.

\begin{equation}
\rho = \rho_0 ( 1 - \alpha (T - T_0) + \beta (S - S_0)),
\end{equation}

which may be useful for numerical modelling or theoretical applications.

You may tune the linear parameters as shown below.

```python
linear_EOS_params = dict(rho0=1045.4, a=2e-4, b=7e-4, SP0=35, t0=15)
eps_linear, N2_linear, diag_linear = mx.overturn.eps_overturn(
    depth, t, SP, lon, lat, use_ip=False, EOS="linear", linear_EOS_params=linear_EOS_params, N2_method="bulk", return_diagnostics=True
)

eps_gsw, N2_gsw, diag_gsw = mx.overturn.eps_overturn(
    depth, t, SP, lon, lat, use_ip=False, EOS="gsw", N2_method="bulk", return_diagnostics=True
)
```

```python
# Plot only in the depth range:
cut = (depth > 4225) & (depth < 4800)
zc = depth[cut]

fig, axs = plt.subplots(1, 3, sharey=True, figsize=(9, 6))
axs[0].plot(diag_linear["dens"][cut]-1000, zc, label="linear")
# axs[0].plot(diag_linear["dens_sorted"][cut]-1000, zc, label="sorted density")
axs[0].plot(diag_gsw["dens"][cut]-1000, zc, label="gsw")
# axs[0].plot(diag_gsw["dens_sorted"][cut]-1000, zc, label="sorted density")
axs[1].plot(eps_linear[cut], zc, label="linear")
axs[1].plot(eps_gsw[cut], zc, label="gsw")
axs[2].plot(N2_linear[cut], zc, label="linear")
axs[2].plot(N2_gsw[cut], zc, label="gsw")
axs[0].invert_yaxis()
axs[0].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[1].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[2].legend(loc="lower left", bbox_to_anchor=(0, 1))
axs[0].set_ylabel("Depth [m]")
axs[0].set_xlabel("Potential density [kg m$^{-3}$]")
axs[1].set_xlabel(r"$\epsilon$ [W kg$^{-1}$]")
axs[2].set_xlabel(r"$N^2$ [rad$^2$ s$^{-2}$]")

fig.align_labels()
```

Notice how density appears to decrease with depth and the deepest overturn is larger in the linear case. Consider using potential temperature rather than _in situ_ temperature for deep observations when using the linear equation of state, otherwise there is a risk of finding spurious overturns.


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Class 11: Introduction to Dynamic Optimization: A Two-Period Cake-Eating Problem

Dynamic optimization, the optimal choice over elements in a time series, is at the heart of macroeconomic theory. People and firms routinely make decisions that affect their future opportunities. For example, a household chooses how much of its income to save and therefore allocates some of its current consumption to the future. In this Notebook, we consider a simple two-period dynamic optimization model. A person has some initial quantity of cake and chooses the optimal way to eat it over the next two periods. We solve for the optimal consumption path.

## Preferences

Suppose that a person has only two periods to eat cake. Let $C_0$ denote the quantity of cake consumed in period 0 and let $C_1$ denote the quantity consumed in period 1. The person's utility in period 0 from consuming $C_0$ and $C_1$ is given by:

\begin{align}
U(C_0,C_1) & = \log C_0 + \beta \log C_1,
\end{align}

where $0\leqslant\beta\leqslant 1$ is the person's *subjective discount factor*. $U$ is an *intertemporal utility function* and $\log C$ is the *flow* of utility generated each period. The presence of $\beta$ indicates the the person places lower weight on consumption in the future. The utility function implies indifference curves of the form:

\begin{align}
C_1 & =  e^{ \frac{\bar{U} - \log C_0}{\beta}},
\end{align}

where $\bar{U}$ is some given level of utility.

### Example: Plot Indifference Curves for Different Utility Levels

For $C_0$ between 0.01 and 10 and $\beta=0.95$, plot indifference curves for $\bar{U}=1, 2, 3, 4$. Set x- and y-axis limits to [0,10].

```python
# Define a function for computing c1 given a beta value and an array of c0 values
def indifference_curve(u,beta,c0):

    return np.exp((u-np.log(c0))/beta)

# Create a variable called 'c0' that stores c0 values from 0.01 to 10
c0 = np.arange(0.01,10,0.001)

# Create a variable called 'beta' that stores the value of beta
beta = 0.95

# Plot indifference curves
plt.plot(c0,indifference_curve(u=1,beta=beta,c0=c0),lw=3,alpha=0.75,label='u=1')
plt.plot(c0,indifference_curve(u=2,beta=beta,c0=c0),lw=3,alpha=0.75,label='u=2')
plt.plot(c0,indifference_curve(u=3,beta=beta,c0=c0),lw=3,alpha=0.75,label='u=3')
plt.plot(c0,indifference_curve(u=4,beta=beta,c0=c0),lw=3,alpha=0.75,label='u=4')

plt.xlim([0,10])
plt.ylim([0,10])
plt.xlabel('$C_0$',fontsize=14)
plt.ylabel('$C_1$',fontsize=14)
plt.title('Indifference curves of $\log C_0 + 0.95 \log C_1$')

# Place legend to right of figure. PROVIDED
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))

plt.grid()
plt.tight_layout()
```

### Example: Plot Indifference Curves for Different $\beta$ Values

For $C_0$ between 0.01 and 30 and $\bar{U} = 3$, plot indifference curves for $\beta=1, 0.95,0.5,0.25$. Set x-axis limits to [0,30] and y-axis limits to [0,10].

```python
# Create a variable called u that stores the value of utility for the indifference curves
u = 3

# Create variable called 'c0' that stores c0 values from 0.01 to 30
c0 = np.arange(0.01,30,0.001)

# Plot indifference curves
plt.plot(c0,indifference_curve(u=u,beta=1,c0=c0),lw=3,alpha=0.75,label='$\\beta=1$')
plt.plot(c0,indifference_curve(u=u,beta=0.95,c0=c0),lw=3,alpha=0.75,label='$\\beta=0.95$')
plt.plot(c0,indifference_curve(u=u,beta=0.5,c0=c0),lw=3,alpha=0.75,label='$\\beta=0.5$')
plt.plot(c0,indifference_curve(u=u,beta=0.25,c0=c0),lw=3,alpha=0.75,label='$\\beta=0.25$')

plt.xlim([0,30])
plt.ylim([0,10])
plt.xlabel('$C_0$',fontsize=14)
plt.ylabel('$C_1$',fontsize=14)
plt.title('Indifference curves of $\log C_0 + \\beta \log C_1$')

# Place legend to right of figure. PROVIDED
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))

plt.grid()
plt.tight_layout()
```

## Budget Constraints and the Boundary Condition

Let $K_t$ denote the quantity of cake available in period $t$ and let $K_0$ denote the initial quantity of cake. The cake neither grows nor depreciates over time so the person faces the following budget constraints each period:

\begin{align}
C_0 & =  K_0 - K_1\\
C_1 & =  K_1 - K_2.
\end{align}

Finally, the person's problem is subject to a *boundary condition* that imposes a limit on the endpoint of the problem. Since the person only consumes cake in periods 0 and 1, there is no value in leaving leftover cake at the end of period 1. This reasoning implies the boundary condition:

\begin{align}
K_2 & = 0.
\end{align}

The boundary condition allows us to eliminate $K_2$ from the period 1 budget constraint. Combining the two budget constraints to eliminate $K_1$ implies an *intertemporal budget constraint* that links consumption in period 0 with consumption in period 1:

\begin{align}
C_0 & =  K_0 - C_1.
\end{align}

Notice that the implied price of $C_0$ in terms of $C_1$ is 1.

### Example: Plot the Intertemporal Budget Constraint

For $C_0$ between 0.01 and 20 and plot indifference curves for $K_0=5, 10, 15, 20$. Set x-axis limits to [0,10] and y-axis limits to [0,30].

```python
# Define a function for computing c1 given a k0 value and an array of c0 values
def bc(k0,c0):
    '''Function for computing the intertemporal budget constraint for the two-period cake-eating problem

            Args:
            k0 (float):         initial quantity of cake
            c0 (numpy array):   array of initial consumption values

        Returns:
            numpy array
    '''

    c1 = k0-c0
    c1[c1<0]=0

    return c1

# Create variable called 'c0' that stores c0 values from 0.01 to 10
c0 = np.arange(0.01,10,0.001)

# Plot budget constraints
plt.plot(c0,bc(5,c0),lw=3,alpha=0.75,label='$K_0=5$')
plt.plot(c0,bc(10,c0),lw=3,alpha=0.75,label='$K_0=10$')
plt.plot(c0,bc(15,c0),lw=3,alpha=0.75,label='$K_0=15$')
plt.plot(c0,bc(20,c0),lw=3,alpha=0.75,label='$K_0=20$')

plt.xlabel('$C_0$',fontsize=14)
plt.ylabel('$C_1$',fontsize=14)
plt.title('Budget Constraints: $C_1 = K_0 - C_1$')

# Place legend to right of figure. PROVIDED
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))

plt.grid()
plt.tight_layout()
```

## Optimization

The person's problem is to choose $C_0$, $C_1$, $K_1$, and $K_2$ to maximize his utility subject to the two budget constraints and the boundary condition. However, the problem can be simplified considerably. Use the boundary condition to eliminate $K_2$ from the budget constraints. Then use the budget constraints to substitute $C_0$ and $C_1$ out of the utility function. The result is an unconstrained optimization problem in $K_1$.

\begin{align}
\max_{K_1} \; \log (K_0 - K_1) + \beta \log K_1,
\end{align}

Take the derivative with respect to $K_1$ and set the derivative equal to zero to obtain the *first-order* or *optimality* condition:

\begin{align}
-\frac{1}{K_0 - K_1} + \frac{\beta}{K_1} & = 0.
\end{align}

Solve for $K_1$ to find the optimal cake holding for period 1:

\begin{align}
\boxed{K_1  = \frac{\beta}{1+\beta}K_0}
\end{align}

Then use the budget constraints to infer optimal cake consumption in period 0:
\begin{align}
\boxed{C_0  = \frac{1}{1+\beta}K_0}
\end{align}

and period 1:

\begin{align}
\boxed{C_1  = \frac{\beta}{1+\beta}K_0}.
\end{align}

Notice that $C_0 + C_1 = K_0$ so all of the cake is eaten by the end.

### Example: Solve for the Optimal Consumption Path

Compute $K_1, C_0, C_1$ for $\beta = 0.9$ and $K_0 = 20$.

```python
# Define a function that returns the solution to the two-period cake-eating problem
def two_period_cake(beta,cake0):

    '''Function for computing solution to two-period cake-eating problem

            Args:
            beta (float):   subjective discount factor
            cake0 (float):  initial quantity of cake

        Returns:
            numpy array, numpy array
    '''

    # Initialize arrays for cake and consumption
    cake = np.zeros(3)
    consumption = np.zeros(2)

    # Assign values to cake array elements
    cake[0] = cake0
    cake[1] = beta/(1+beta)*cake[0]

    # Assign values to consumption array elements as implied by the budget constraints
    consumption[0] = cake[0]-cake[1]
    consumption[1] = cake[1]

    return cake,consumption

# Create a cariable called 'cake0' that stores initial cake value
cake0 = 20

# Create a cariable called 'beta' that stores beta value
beta = 0.9

# Compute the optimal path
cake,consumption = two_period_cake(beta=beta,cake0=cake0)
print('k[1]:',round(cake[1],5))
print('c[0]:',round(consumption[0],5))
print('c[1]:',round(consumption[1],5))
```

    k[1]: 9.47368
    c[0]: 10.52632
    c[1]: 9.47368

$\beta <1$ means that the person places greater weight on consumption in period 0 and therefore the person chooses to consume a greater share of the cake in period 0.