 Tutorial 2

 Physics Informed Neural Networks Part 1

 Manual 1D Heat Equation Solver

# Overview

This notebook is based on two papers: *[Physics-Informed Neural Networks:  A Deep LearningFramework for Solving Forward and Inverse ProblemsInvolving Nonlinear Partial Differential Equations](https://www.sciencedirect.com/science/article/pii/S0021999118307125)* and *[Hidden Physics Models:  Machine Learning of NonlinearPartial Differential Equations](https://www.sciencedirect.com/science/article/pii/S0021999117309014)* with the help of  Fergus Shone and Michael Macraild.

These tutorials will go through solving Partial Differential Equations using Physics Informed Neural Networks focusing on the Burgers Equation and a more complex example using the Navier Stokes Equation

**This introduction section is replicated in all PINN tutorial notebooks (please skip if you've already been through)**

If you have not already then in your repositoy directory please run the following code.

```bash
git submodule init
git submodule update --init --recursive
```

**If this does not work please clone the [PINNs](https://github.com/maziarraissi/PINNs) repository into your Physics_Informed_Neural_Networks folder**

Physics Informed Neural Networks

For a typical Neural Network using algorithms like gradient descent to look for a hypothesis, data is the only guide, however if the data is noisy or sparse and we already have governing physical models we can use the knowledge we already know to optimize and inform the algorithms. This can be done via [feature engineering]() or by adding a physical inconsistency term to the loss function.

## The very basics

If you know nothing about neural networks there is a [toy neural network python code example](https://github.com/cemac/LIFD_ENV_ML_NOTEBOOKS/tree/main/ToyNeuralNetwork) included in the [LIFD ENV ML Notebooks Repository]( https://github.com/cemac/LIFD_ENV_ML_NOTEBOOKS). Creating a 2 layer neural network to illustrate the fundamentals of how Neural Networks work and the equivalent code using the python machine learning library [tensorflow](https://keras.io/).

## Recommended reading

The in-depth theory behind neural networks will not be covered here as this tutorial is focusing on application of machine learning methods. If you wish to learn more here are some great starting points.

* [All you need to know on Neural networks](https://towardsdatascience.com/nns-aynk-c34efe37f15a)
* [Introduction to Neural Networks](https://victorzhou.com/blog/intro-to-neural-networks/)
* [Physics Guided Neural Networks](https://towardsdatascience.com/physics-guided-neural-networks-pgnns-8fe9dbad9414)
* [Maziar Rassi's Physics informed GitHub web Page](https://maziarraissi.github.io/PINNs/)

 Machine Learning Theory

## Physics informed Neural Networks

Neural networks work by using lots of data to calculate weights and biases from data alone to minimise the loss function enabling them to act as universal function approximators. However these lose their robustness when data is limited. However by using known physical laws or empirical validated relationships the solutions from neural networks can be sufficiently constrained by disregarding no realistic solutions.

A Physics Informed Neural Network considers a parameterized and nonlinear partial differential equation in the general form;

\begin{align}
     u_t + \mathcal{N}[u; \lambda] &= 0, && x \in \Omega, t \in [0,T],\\
\end{align}

where $\mathcal{u(t,x)}$ denores the hidden solution, $\mathcal{N}$ is a nonlinear differential operator acting on $u$, $\mathcal{\lambda}$ and $\Omega$ is a subset of $\mathbb{R}^D$ (the prescribed data). This set up an encapsulation of a wide range of problems such as diffusion processes, conservation laws,  advection-diffusion-reaction  systems,  and  kinetic  equations and conservation laws.

Here we will go though this for the 1 Heat equation and Navier stokes equations

 Python

## Tensorflow

There are many machine learning python libraries available, [TensorFlow](https://www.tensorflow.org/) a is one such library. If you have GPUs on the machine you are using TensorFlow will automatically use them and run the code even faster!

## Further Reading

* [Running Jupyter Notebooks](https://jupyter.readthedocs.io/en/latest/running.html#running)
* [Tensorflow optimizers](https://www.tutorialspoint.com/tensorflow/tensorflow_optimizers.htm)

 Requirements

These notebooks should run with the following requirements satisfied

 Python Packages:

* Python 3
* tensorflow > 2
* numpy
* matplotlib
* scipy

 Data Requirements

This notebook referes to some data included in the git hub repositroy imported via the git submodules command mentioned in the installation instructions

**Contents:**

1. **[1D Heat Equation Non ML Example](PINNs_1DHeatEquations_nonML.ipynb)**
2. [1D Heat Equation PINN Example](PINNs_1DHeatEquationExample.ipynb)
3. [Navier-Stokes PINNs discovery of PDE’s](PINNs_NavierStokes_example.ipynb)
4. [Navier-Stokes PINNs Hidden Fluid Mechanics](PINNs_NavierStokes_HFM.ipynb)

Load in all required modules (including some auxiliary code) and turn off warnings.

```python
# For readability: disable warnings
import warnings
warnings.filterwarnings('ignore')
```

```python
import sys
sys.path.insert(0, 'PINNs/Utilities/')
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import scipy.io
from scipy.interpolate import griddata
import time
from itertools import product, combinations
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.gridspec as gridspec
from time import time
import scipy.sparse as sp
import scipy.sparse.linalg as la
```

 1D Heat Equation (forwards)

Given
- the initial temperature profile $u(x,0) = m(x)$,
- the thermal diffusivity $k$,
- a prescribed temperature $u(0,t) = u(L,t) = 0$ at the extremities of the rod;

solve the heat equation

\begin{equation}
\begin{array}{ll}
\frac{\partial u}{\partial t} - k \frac{\partial^2}{\partial x^2} u = 0 & \forall x\,\in\,(0, L)\; \forall t \in (0,T)\\
u(x, 0) = m(x) & \forall x \in [0,L] \\
u(0,t) = u(L,t) = 0 & \forall t \in (0, T],
\end{array}
\end{equation}

and observe the temperature at the final time $T$:

\begin{equation} \mathcal{F}(m) = u(x, T). \end{equation}

#### Analytical solution to the forward problem.

If

\begin{equation} m(x) = \sin\left(n\, \frac{\pi}{L} x \right), \quad n = 1,2,3, \ldots ,\end{equation}

then

\begin{equation} u(x,t) = e^{ -k\left(n\, \frac{\pi}{L} \right)^2 t} \sin\left(n\,\frac{\pi}{L} x \right) \end{equation}

is the unique solution to the heat equation.

 1D Heat Equation (inverse)

Given the forward model F and a noisy measurament d of the temperature profile at time T, find the initial temperature profile m

such that
\begin{equation}
F(m)=d.
\end{equation}

### Discretization

To discretize the problem, we use finite differences in space and Implicit Euler in time.

#### Semidiscretization in space
We divide the $[0, L]$ interval in $n_x$ subintervals of the same lenght $h = \frac{L}{n_x}$, and we denote with $u_j(t) := u( jh, t)$ the value of the temperature at point $x_j = jh$ and time $t$.

We then use a centered finite difference approximation of the second derivative in space and write

\begin{equation} \frac{\partial u_j(t)}{\partial t} - k \frac{u_{j-1}(t) - 2u_j(t) + u_{j+1}(t)}{h^2} \quad \text{for } j=1,2,\ldots,n_x-1, \end{equation}

with the boundary condition $u_0(t) = u_{n_x}(t) = 0$.

By letting

\begin{equation} \mathbf{u}(t) = \begin{bmatrix}u_1(t)\\u_2(t)\\ \ldots\\ u_{n_x-1}(t) \end{bmatrix}\end{equation}

be the vector collecting the values of the temperature $u$ at the points $x_j = j\,h$, we then write the system of ordinary differential equations (ODEs):
$$ \frac{\partial}{\partial t} \mathbf{u}(t) + K \mathbf{u}(t) = 0,$$
where $K \in \mathbb{R}^{(n_x-1) \times (n_x-1)}$ is the tridiagonal matrix given by

\begin{equation} K = \frac{k}{h^2}\begin{bmatrix}  2 & -1 &       &        &        &    \\
                                    -1 &  2 & -1    &        &        &    \\
                                       & -1 &  2    & -1     &        &    \\
                                       &    &\ldots & \ldots & \ldots &    \\
                                       &    &       & -1     &     2  & -1 \\
                                       &    &       &        &     -1 & 2  \\
                     \end{bmatrix}.\end{equation}

#### Time discretization
We subdivide the time interval $(0, T]$ in $n_t$ time step of size $\Delta t = \frac{T}{n_t}$.
By letting $\mathbf{u}^{(i)} = \mathbf{u}(i\,\Delta t)$ denote the discretized temperature profile at time $t_i = i\,\Delta t$, the Implicit Euler scheme reads

\begin{equation} \frac{\mathbf{u}^{(i+1)} - \mathbf{u}^{(i)}}{\Delta t} + K\mathbf{u}^{(i+1)} = 0, \quad \text{for } i=0,1,\ldots, n_t-1.\end{equation}

After simple algebraic manipulations and exploiting the initial condition $u(x,0) = m(x)$, we then obtain

\begin{equation}
\mathbf{u}^{(0)} = \mathbf{m} \\
\mathbf{u}^{(i+1)} = \left( I + \Delta t\, K\right)^{-1} \mathbf{u}^{(i)},
\end{equation}

or equivalently

\begin{equation} \mathbf{u}^{(i)} = \left( I + \Delta t\, K\right)^{-i} \mathbf{m}.\end{equation}

# Define some helper functions to Solve 1D heat equation forwards

In the code below, the function `assembleMatrix` generates the finite difference matrix $\left( I + \Delta t\, K \right)$ and the function `solveFwd` evaluates the forward model

\begin{equation} F\, \mathbf{m} = \left( I + \Delta t\, K\right)^{-n_t}\, \mathbf{m}. \end{equation}

```python
def plot(f, style, **kwargs):
    x = np.linspace(0., L, nx+1)
    f_plot = np.zeros_like(x)
    f_plot[1:-1] = f
    plt.plot(x,f_plot, style, **kwargs)

def assembleMatrix(n):
    diagonals = np.zeros((3, n))   # 3 diagonals
    diagonals[0,:] = -1.0/h**2
    diagonals[1,:] =  2.0/h**2
    diagonals[2,:] = -1.0/h**2
    K = k*sp.spdiags(diagonals, [-1,0,1], n,n)
    M = sp.spdiags(np.ones(n), 0, n,n)

    return M + dt*K

def solveFwd(m):
    A = assembleMatrix(m.shape[0])
    u_old = m.copy()
    for i in np.arange(nt):
        u = la.spsolve(A, u_old)
        u_old[:] = u

    return u
```

### A naive solution to the inverse problem

If $\mathcal{F}$ is invertible a naive solution to the inverse problem $\mathcal{F} m = d$ is simply to set

\begin{equation} m = \mathcal{F}^{-1} d. \end{equation}

The function `naiveSolveInv` computes the solution of the discretized inverse problem $\mathbf{m} = F^{-1} \mathbf{d}$ as

\begin{equation} \mathbf{m} = \left( I + \Delta t\,K\right)^{n_t} \mathbf{d}. \end{equation}

```python
def naiveSolveInv(d):
    A = assembleMatrix(d.shape[0])

    p_i = d.copy()
    for i in np.arange(nt):
        p = A*p_i
        p_i[:] = p

    return p
```

```python
# Edit nx or noise_std_dev to see the impact on the naive solver

nx = 20 # default 20
noise_std_dev = 1e-4 # default

T = 1.0
L = 1.0
k = 0.005

nt = 100

h = L/float(nx)
dt = T/float(nt)

x = np.linspace(0.+h, L-h, nx-1) #place nx-1 equispace point in the interior of [0,L] interval
m_true = np.power(.5,-36)*np.power(x,20)*np.power(1. - x, 16)  #smooth true initial condition
#m_true = 0.5 - np.abs(x-0.5)                                    #initial condition with a corner
u_true = solveFwd(m_true)

d = u_true + noise_std_dev*np.random.randn(u_true.shape[0])

m = naiveSolveInv(d)

plot(u_true, "-b", label = 'u(T)')
plot(d, "og", label = 'd')
plt.legend()
plt.title('sample data to be used in Niave solver ')
plt.show()

plot(m_true, "-r", label = 'm_true')
plot(m, "-b", label = 'm')
plt.legend()
plt.title('Naive Solution coarse mesh')
plt.show()

```

If you have played around with the code above you will see that:
- for a very coarse mesh (`nx = 20`) and no measurement noise (`noise_std_dev = 0.0`) the naive solution is quite good
- for a finer mesh (`nx = 100`) and/or even small measurement noise (`noise_std_dev = 0.0001`) the naive solution is very poor

### Why does the naive solution fail?

Let \begin{equation}v_n = \sqrt{\frac{2}{L}} \sin\left( n \, \frac{\pi}{L} x \right)\end{equation}

with n=1,2,3, then we have that

\begin{equation} \mathcal{F} v_n = \lambda_n v_n, \quad \text{where the eigenvalues } \lambda_n = e^{-kT\left(\frac{\pi}{L} n \right)^2}. \end{equation}

**Note 1**:
- Large eigenvalues $\lambda_n$ corresponds to smooth eigenfunctions $v_n$;
- Small eigenvalues $\lambda_n$ corresponds to oscillatory eigenfuctions $v_n$.

The figure below shows that the eigenvalues $\lambda_n$ decays extremely fast, that is the matrix $F$ (discretization of the forward model $\mathcal{F}$) is extremely ill conditioned.

```python
T = 1.0
L = 1.0
k = 0.005

i = np.arange(1,50)
lambdas = np.exp(-k*T*np.power(np.pi/L*i,2))

plt.semilogy(i, lambdas, 'ob')
plt.xlabel('i')
plt.ylabel('lambda_i')
plt.title('Eigen Value decay')
plt.show()
```

**Note 2**: The functions $v_n$ ($n=1,2,3, \ldots$) form an orthonormal basis of $L^2([0,1]$).

That is, every function $f \in L^2([0,1])$ can be written as

\begin{equation} f = \sum_{n=1}^\infty \alpha_n v_n, \text{ where } \alpha_n = \int_0^1 f v_n dx.\end{equation}

Consider now the noisy problem

\begin{equation} d = \mathcal{F}m_{\rm true} + \eta, \end{equation}

where
- $d$ is the data (noisy measurements)
- $\eta$ is the noise: $\eta(x) = \sum_{n=1}^\infty \eta_n v_n(x)$
- $m_{\rm true}$ is the true value of the parameter that generated the data
- $\mathcal{F}$ is the forward heat equation

Then, the naive solution to the inverse problem $\mathcal{F}m = d$ is

\begin{equation} m = \mathcal{F}^{-1}d = \mathcal{F}^{-1}\left( \mathcal{F}m_{\rm true} + \eta \right) = m_{\rm true} + \mathcal{F}^{-1} \eta = m_{\rm true} + \mathcal{F}^{-1} \sum_{n=1}^{\infty} \eta_n v_n = m_{\rm true} +  \sum_{n=1}^{\infty} \frac{\eta_n}{\lambda_n} v_n. \end{equation}

If the coefficients $\eta_n = \int_0^1 \eta(x) \, v_n(x) \, dx$ do not decay sufficiently fast with respect to the eigenvalues $\lambda_n$, then the naive solution is unstable.

This implies that oscillatory components can not reliably be reconstructed from noisy data since they correspond to small eigenvalues.

This means we must take a different approach even to a relatively simple looking problem which may require filtering or computationally expensive calculations. This is with an even sample of data to use. So we might consider looking to PINNs

## Next steps

Now we've gone through a Naive manual approach to solving a simple 1D Heat equation we look at the benefits of using neural networks to solve more complex equations starting with the next notebook linked below:

[1D Heat Equation PINN Example](PINNs_1DHeatEquationExample.ipynb)

```python

```

```python

```


# [ATM 623: Climate Modeling](../index.ipynb)

[Brian E. J. Rose](http://www.atmos.albany.edu/facstaff/brose/index.html), University at Albany

# Lecture 7: Grey radiation modeling with climlab

### About these notes:

This document uses the interactive [`Jupyter notebook`](https://jupyter.org) format. The notes can be accessed in several different ways:

- The interactive notebooks are hosted on `github` at https://github.com/brian-rose/ClimateModeling_courseware
- The latest versions can be viewed as static web pages [rendered on nbviewer](http://nbviewer.ipython.org/github/brian-rose/ClimateModeling_courseware/blob/master/index.ipynb)
- A complete snapshot of the notes as of May 2017 (end of spring semester) are [available on Brian's website](http://www.atmos.albany.edu/facstaff/brose/classes/ATM623_Spring2017/Notes/index.html).

[Also here is a legacy version from 2015](http://www.atmos.albany.edu/facstaff/brose/classes/ATM623_Spring2015/Notes/index.html).

Many of these notes make use of the `climlab` package, available at https://github.com/brian-rose/climlab

```python
#  Ensure compatibility with Python 2 and 3
from __future__ import print_function, division
```

## Contents

1. [Introducing `climlab`](#section1)
2. [Using `climlab` to implement the two-layer leaky greenhouse model](#section2)
3. [The observed annual, global mean temperature profile](#section3)
4. [A 30-layer model using the observed temperatures](#section4)
5. [Radiative forcing in the 30-layer model](#section5)
6. [Radiative equilibrium in the 30-layer model](#section6)
7. [Radiative-Convective Equilibrium in the 30-layer model](#section7)
8. [Putting stratospheric ozone in the grey-gas model](#section8)

____________

## 1. Introducing `climlab`
____________

``climlab`` is a flexible engine for process-oriented climate modeling.
It is based on a very general concept of a model as a collection of individual,
interacting processes. ``climlab`` defines a base class called ``Process``, which
can contain an arbitrarily complex tree of sub-processes (each also some
sub-class of ``Process``). Every climate process (radiative, dynamical,
physical, turbulent, convective, chemical, etc.) can be simulated as a stand-alone
process model given appropriate input, or as a sub-process of a more complex model.
New classes of model can easily be defined and run interactively by putting together an
appropriate collection of sub-processes.

``climlab`` is a work-in-progress, and the code base will evolve substantially over the course of this semester.
The latest code can always be found on ``github``:

https://github.com/brian-rose/climlab

You are strongly encouraged to clone the ``climlab`` repository and use ``git`` to keep your local copy up-to-date.

Running this notebook requires that ``climlab`` is already installed on your system.

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
from xarray.ufuncs import cos, deg2rad, log
import climlab
```

____________

## 2. Using `climlab` to implement the two-layer leaky greenhouse model
____________

One of the things that ``climlab`` is set up to do is the grey-radiation modeling we have already been discussing.

Since we already derived a [complete analytical solution to the two-layer leaky greenhouse model](Lecture06 -- Elementary greenhouse models.ipynb), we will use this to validate the `climlab` code.

### Validation

We want to verify that the model reproduces the observed OLR given observed temperatures, and the absorptivity that we tuned in the analytical model. The target numbers are:

\begin{align}
T_s &= 288 \text{ K} \\
T_0 &= 275 \text{ K} \\
T_1 &= 230 \text{ K} \\
\end{align}

$$ \epsilon = 0.586  $$

$$ OLR = 238.5 \text{ W m}^{-2} $$

### Initialize a model in `climlab`
The first thing we do is create a new model.

The following example code is sparsely commented but will hopefully orient you on the basics of defining and working with a `climlab Process` object.

```python
#  Test in a 2-layer atmosphere
col = climlab.GreyRadiationModel(num_lev=2)
print( col)
```

    climlab Process of type <class 'climlab.model.column.GreyRadiationModel'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (2,)
    The subprocess tree:
    Untitled: <class 'climlab.model.column.GreyRadiationModel'>
       LW: <class 'climlab.radiation.greygas.GreyGas'>
       SW: <class 'climlab.radiation.greygas.GreyGasSW'>
       insolation: <class 'climlab.radiation.insolation.FixedInsolation'>

```python
col.subprocess
```

    {'LW': <climlab.radiation.greygas.GreyGas at 0x30d5d0a58>,
     'SW': <climlab.radiation.greygas.GreyGasSW at 0x30d5d0a90>,
     'insolation': <climlab.radiation.insolation.FixedInsolation at 0x109f7b4a8>}

Every item in the above dictionary is itself an instance of the `climlab.Process` object:

```python
print( col.subprocess['LW'])
```

    climlab Process of type <class 'climlab.radiation.greygas.GreyGas'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (2,)
    The subprocess tree:
    Untitled: <class 'climlab.radiation.greygas.GreyGas'>

The `state` dictionary holds the state variables of the model. In this case, temperatures:

```python
col.state
```

    {'Tatm': Field([200., 278.]), 'Ts': Field([288.])}

Access these either through dictionary methods or as attributes of the model object:

```python
print( col.state['Ts'])
print( col.Ts)
col.Ts is col.state['Ts']
```

    [288.]
    [288.]

    True

Now we are assigning the "observed" temperatures to our model state:

```python
col.Ts[:] = 288.
col.Tatm[:] = np.array([230., 275.])
col.state
```

    {'Tatm': Field([230., 275.]), 'Ts': Field([288.])}

```python
LW = col.subprocess['LW']
print( LW)
```

    climlab Process of type <class 'climlab.radiation.greygas.GreyGas'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (2,)
    The subprocess tree:
    Untitled: <class 'climlab.radiation.greygas.GreyGas'>

```python
LW.absorptivity
```

    Field([0.47737425, 0.47737425])

```python
#  copying the tuned value of epsilon from Lecture 6 notes
LW.absorptivity = 0.586
LW.absorptivity
```

    Field([0.586, 0.586])

```python
#  This does all the calculations that would be performed at each time step,
#  but doesn't actually update the temperatures
col.compute_diagnostics()
#  Print out the dictionary
col.diagnostics
```

    {'ASR': Field([239.2513]),
     'LW_absorbed_atm': array([ 20.02990881, -96.98387764]),
     'LW_absorbed_sfc': Field([-161.57076227]),
     'LW_down_sfc': array([228.53426769]),
     'LW_emission': Field([ 92.98664086, 190.03779837]),
     'LW_up_sfc': Field([390.10502995]),
     'OLR': array([238.5247311]),
     'SW_absorbed_atm': array([0., 0.]),
     'SW_absorbed_sfc': Field([239.2513]),
     'SW_down_TOA': Field([341.3]),
     'SW_up_TOA': array([102.0487]),
     'SW_up_sfc': Field([102.0487]),
     'absorbed': array([0., 0.]),
     'absorbed_total': 0.0,
     'coszen': Field([1.]),
     'emission': Field([0., 0.]),
     'emission_sfc': Field([0.]),
     'flux_from_sfc': Field([102.0487]),
     'flux_reflected_up': array([  0.    ,   0.    , 102.0487]),
     'flux_to_sfc': array([341.3]),
     'flux_to_space': array([102.0487]),
     'insolation': Field([341.3]),
     'planetary_albedo': Field([0.299])}

```python
#  Check OLR against our analytical solution
col.OLR
```

    array([238.5247311])

```python
# Like the state variables, the diagnostics can also be accessed in two different ways
col.diagnostics['OLR']
```

    array([238.5247311])

```python
col.state
```

    {'Tatm': Field([230., 275.]), 'Ts': Field([288.])}

```python
# perform a single time step
col.step_forward()
```

```python
col.state
```

    {'Tatm': Field([230.33784312, 273.3641795 ]), 'Ts': Field([289.60514636])}

We just stepped forward one discreet unit in time. Because we didn't specify a timestep when we created the model, it is set to a default value:

```python
col.timestep
```

    86400.0

which is 1 day (expressed in seconds).

Now we will integrate the model out to equilibrium.

We could easily write a loop to call the `step_forward()` method many times.

Or use a handy shortcut that allows us to specify the integration length in physical time units:

```python
# integrate out to radiative equilibrium
col.integrate_years(2.)
```

    Integrating for 730 steps, 730.4844 days, or 2.0 years.
    Total elapsed time is 2.0014116660123062 years.

```python
# Check for equilibrium
col.ASR - col.OLR
```

    Field([-2.34514516e-07])

```python
#  The temperatures at radiative equilibrium
col.state
```

    {'Tatm': Field([233.72131685, 262.28540231]), 'Ts': Field([296.38447748])}

Compare these to the analytical solutions for radiative equilibrium with $\epsilon = 0.58$:

\begin{align}
T_s &= 296.4 \text{ K} \\
T_0 &= 262.3 \text{ K} \\
T_1 &= 233.8 \text{ K} \\
\end{align}

So it looks like `climlab` agrees with our analytical results to within 0.1 K. That's good.

____________

## 3. The observed annual, global mean temperature profile
____________

We want to model the OLR in a column whose temperatures match observations. As we've done before, we'll calculate the global, annual mean air temperature from the NCEP Reanalysis data.

```python
## The NOAA ESRL server is shutdown! January 2019
## This will try to read the data over the internet.
#ncep_filename = 'air.mon.1981-2010.ltm.nc'
##  to read over internet
#ncep_url = "http://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/ncep.reanalysis.derived/pressure/"
#path = ncep_url
##  Open handle to data
#ncep_air = xr.open_dataset( path + ncep_filename, decode_times=False )
url = 'http://apdrc.soest.hawaii.edu:80/dods/public_data/Reanalysis_Data/NCEP/NCEP/clima/pressure/air'
air = xr.open_dataset(url)
# The name of the vertical axis is different than the NOAA ESRL version..
ncep_air = air.rename({'lev': 'level'})
print( ncep_air)
```

    <xarray.Dataset>
    Dimensions:         (lat: 73, level: 17, lon: 144, time: 12)
    Coordinates:
      * time            (time) datetime64[ns] 2001-01-01 2001-02-01 ... 2001-12-01
      * level           (level) float64 1e+03 925.0 850.0 700.0 ... 30.0 20.0 10.0
      * lat             (lat) float64 -90.0 -87.5 -85.0 -82.5 ... 85.0 87.5 90.0
      * lon             (lon) float64 0.0 2.5 5.0 7.5 ... 350.0 352.5 355.0 357.5
    Data variables:
        air             (time, level, lat, lon) float32 ...
        valid_yr_count  (time, level, lat, lon) float32 ...
    Attributes:
        title:          NMC reanalysis atlas
        Conventions:    COARDS\nGrADS
        dataType:       Grid
        documentation:  http://apdrc.soest.hawaii.edu/datadoc/ncep_clima.php
        history:        Mon Dec 31 09:16:43 HST 2018 : imported by GrADS Data Ser...

```python
#  Take global, annual average and convert to Kelvin
weight = cos(deg2rad(ncep_air.lat)) / cos(deg2rad(ncep_air.lat)).mean(dim='lat')
Tglobal = (ncep_air.air * weight).mean(dim=('lat','lon','time'))
print( Tglobal)
```

    <xarray.DataArray (level: 17)>
    array([ 15.179084,  11.207003,   7.838328,   0.219942,  -6.448343, -14.888846,
           -25.570469, -39.369689, -46.797913, -53.65224 , -60.563557, -67.006054,
           -65.532933, -61.486643, -55.853586, -51.59395 , -43.219986])
    Coordinates:
      * level    (level) float64 1e+03 925.0 850.0 700.0 ... 50.0 30.0 20.0 10.0

We're going to convert this to degrees Kelvin, using a handy list of pre-defined constants in `climlab.constants`

```python
climlab.constants.tempCtoK
```

    273.15

```python
Tglobal += climlab.constants.tempCtoK
print( Tglobal)
```

    <xarray.DataArray (level: 17)>
    array([288.329084, 284.357003, 280.988328, 273.369942, 266.701657, 258.261154,
           247.579531, 233.780311, 226.352087, 219.49776 , 212.586443, 206.143946,
           207.617067, 211.663357, 217.296414, 221.55605 , 229.930014])
    Coordinates:
      * level    (level) float64 1e+03 925.0 850.0 700.0 ... 50.0 30.0 20.0 10.0

```python
#  A handy re-usable routine for making a plot of the temperature profiles
#  We will plot temperatures with respect to log(pressure) to get a height-like coordinate

def zstar(lev):
    return -np.log(lev / climlab.constants.ps)

def plot_soundings(result_list, name_list, plot_obs=True, fixed_range=True):
    color_cycle=['r', 'g', 'b', 'y']
    # col is either a column model object or a list of column model objects
    #if isinstance(state_list, climlab.Process):
    #    # make a list with a single item
    #    collist = [collist]
    fig, ax = plt.subplots(figsize=(9,9))
    if plot_obs:
        ax.plot(Tglobal, zstar(Tglobal.level), color='k', label='Observed')
    for i, state in enumerate(result_list):
        Tatm = state['Tatm']
        lev = Tatm.domain.axes['lev'].points
        Ts = state['Ts']
        ax.plot(Tatm, zstar(lev), color=color_cycle[i], label=name_list[i])
        ax.plot(Ts, 0, 'o', markersize=12, color=color_cycle[i])
    #ax.invert_yaxis()
    yticks = np.array([1000., 750., 500., 250., 100., 50., 20., 10., 5.])
    ax.set_yticks(-np.log(yticks/1000.))
    ax.set_yticklabels(yticks)
    ax.set_xlabel('Temperature (K)', fontsize=14)
    ax.set_ylabel('Pressure (hPa)', fontsize=14)
    ax.grid()
    ax.legend()
    if fixed_range:
        ax.set_xlim([200, 300])
        ax.set_ylim(zstar(np.array([1000., 5.])))
    #ax2 = ax.twinx()

    return ax
```

```python
plot_soundings([],[] );
```

____________

## 4. A 30-layer model using the observed temperatures
____________

```python
#  initialize a grey radiation model with 30 levels
col = climlab.GreyRadiationModel()
print( col)
```

    climlab Process of type <class 'climlab.model.column.GreyRadiationModel'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (30,)
    The subprocess tree:
    Untitled: <class 'climlab.model.column.GreyRadiationModel'>
       LW: <class 'climlab.radiation.greygas.GreyGas'>
       SW: <class 'climlab.radiation.greygas.GreyGasSW'>
       insolation: <class 'climlab.radiation.insolation.FixedInsolation'>

```python
col.lev
```

    array([ 16.66666667,  50.        ,  83.33333333, 116.66666667,
           150.        , 183.33333333, 216.66666667, 250.        ,
           283.33333333, 316.66666667, 350.        , 383.33333333,
           416.66666667, 450.        , 483.33333333, 516.66666667,
           550.        , 583.33333333, 616.66666667, 650.        ,
           683.33333333, 716.66666667, 750.        , 783.33333333,
           816.66666667, 850.        , 883.33333333, 916.66666667,
           950.        , 983.33333333])

```python
 col.lev_bounds
```

    array([   0.        ,   33.33333333,   66.66666667,  100.        ,
            133.33333333,  166.66666667,  200.        ,  233.33333333,
            266.66666667,  300.        ,  333.33333333,  366.66666667,
            400.        ,  433.33333333,  466.66666667,  500.        ,
            533.33333333,  566.66666667,  600.        ,  633.33333333,
            666.66666667,  700.        ,  733.33333333,  766.66666667,
            800.        ,  833.33333333,  866.66666667,  900.        ,
            933.33333333,  966.66666667, 1000.        ])

```python
# interpolate to 30 evenly spaced pressure levels
lev = col.lev
Tinterp = np.interp(lev, np.flipud(Tglobal.level), np.flipud(Tglobal))
Tinterp
#  Need to 'flipud' because the interpolation routine
#  needs the pressure data to be in increasing order
```

    array([224.34737153, 211.66335696, 206.96234647, 208.29144464,
           212.5864428 , 217.19398737, 221.78253551, 226.35208723,
           231.30423641, 236.08018094, 240.6799208 , 245.27966066,
           249.35980124, 252.92034254, 256.48088384, 259.66790491,
           262.48140575, 265.29490658, 267.81303779, 270.03579936,
           272.25856092, 274.21642907, 275.90940379, 277.60237852,
           279.29535324, 280.98832797, 282.48551703, 283.98270609,
           285.6810303 , 287.44639955])

```python
# Initialize model with observed temperatures
col.Ts[:] = Tglobal[0]
col.Tatm[:] = Tinterp
```

```python
# This should look just like the observations
result_list = [col.state]
name_list = ['Observed, interpolated']
plot_soundings(result_list, name_list);
```

### Tune absorptivity to get observed OLR

```python
col.compute_diagnostics()
col.OLR
```

    array([263.15004222])

```python
# Need to tune absorptivity to get OLR = 238.5
epsarray = np.linspace(0.01, 0.1, 100)
OLRarray = np.zeros_like(epsarray)
```

```python
for i in range(epsarray.size):
    col.subprocess['LW'].absorptivity = epsarray[i]
    col.compute_diagnostics()
    OLRarray[i] = col.OLR

plt.plot(epsarray, OLRarray)
plt.grid()
plt.xlabel('epsilon')
plt.ylabel('OLR')
```

The necessary value seems to lie near 0.055 or so.

We can be more precise with a numerical root-finder.

```python
def OLRanom(eps):
    col.subprocess['LW'].absorptivity = eps
    col.compute_diagnostics()
    return col.OLR - 238.5
```

```python
# Use numerical root-finding to get the equilibria
from scipy.optimize import brentq
# brentq is a root-finding function
#  Need to give it a function and two end-points
#  It will look for a zero of the function between those end-points
eps = brentq(OLRanom, 0.01, 0.1)
print( eps)
```

    0.053690752586678686

```python
col.subprocess.LW.absorptivity = eps
col.subprocess.LW.absorptivity
```

    Field([0.05369075, 0.05369075, 0.05369075, 0.05369075, 0.05369075,
           0.05369075, 0.05369075, 0.05369075, 0.05369075, 0.05369075,
           0.05369075, 0.05369075, 0.05369075, 0.05369075, 0.05369075,
           0.05369075, 0.05369075, 0.05369075, 0.05369075, 0.05369075,
           0.05369075, 0.05369075, 0.05369075, 0.05369075, 0.05369075,
           0.05369075, 0.05369075, 0.05369075, 0.05369075, 0.05369075])

```python
col.compute_diagnostics()
col.OLR
```

    array([238.5])

____________

## 5. Radiative forcing in the 30-layer model
____________

Let's compute radiative forcing for a **2% increase in absorptivity**.

```python
#  clone our model using a built-in climlab function
col2 = climlab.process_like(col)
print( col2)
```

    climlab Process of type <class 'climlab.model.column.GreyRadiationModel'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (30,)
    The subprocess tree:
    Untitled: <class 'climlab.model.column.GreyRadiationModel'>
       LW: <class 'climlab.radiation.greygas.GreyGas'>
       SW: <class 'climlab.radiation.greygas.GreyGasSW'>
       insolation: <class 'climlab.radiation.insolation.FixedInsolation'>

```python
col2.subprocess['LW'].absorptivity *= 1.02
col2.subprocess['LW'].absorptivity
```

    Field([0.05476457, 0.05476457, 0.05476457, 0.05476457, 0.05476457,
           0.05476457, 0.05476457, 0.05476457, 0.05476457, 0.05476457,
           0.05476457, 0.05476457, 0.05476457, 0.05476457, 0.05476457,
           0.05476457, 0.05476457, 0.05476457, 0.05476457, 0.05476457,
           0.05476457, 0.05476457, 0.05476457, 0.05476457, 0.05476457,
           0.05476457, 0.05476457, 0.05476457, 0.05476457, 0.05476457])

```python
#  Radiative forcing by definition is the change in TOA radiative flux,
# HOLDING THE TEMPERATURES FIXED.
col2.Ts - col.Ts
```

    Field([0.])

```python
col2.Tatm - col.Tatm
```

    Field([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

```python
col2.compute_diagnostics()
col2.OLR
```

    array([236.65384095])

The OLR decreased after we added the extra absorbers, as we expect. Now we can calculate the Radiative Forcing:

```python
RF = -(col2.OLR - col.OLR)
print( 'The radiative forcing is %.2f W/m2.' %RF)
```

    The radiative forcing is 1.85 W/m2.

____________

## 6. Radiative equilibrium in the 30-layer model
____________

```python
re = climlab.process_like(col)
```

```python
#  To get to equilibrium, we just time-step the model forward long enough
re.integrate_years(1.)
```

    Integrating for 365 steps, 365.2422 days, or 1.0 years.
    Total elapsed time is 0.9993368783782377 years.

```python
#  Check for energy balance
print( 'The net downward radiative flux at TOA is %.4f W/m2.' %(re.ASR - re.OLR))
```

    The net downward radiative flux at TOA is -0.0015 W/m2.

```python
result_list.append(re.state)
name_list.append('Radiative equilibrium (grey gas)')
plot_soundings(result_list, name_list)
```

Some properties of the **radiative equilibrium** temperature profile:

- The surface is warmer than observed.
- The lower troposphere is colder than observed.
- Very cold air is sitting immediately above the warm surface.
- There is no tropopause, no stratosphere.

____________

## 7. Radiative-Convective Equilibrium in the 30-layer model
____________

We recognize that the large drop in temperature just above the surface is unphysical. Parcels of air in direct contact with the ground will be warmed by mechansisms other than radiative transfer.

These warm air parcels will then become buoyant, and will convect upward, mixing their heat content with the environment.

We **parameterize** the statistical effects of this mixing through a **convective adjustment**.

At each timestep, our model checks for any locations at which the **lapse rate** exceeds some threshold. Unstable layers are removed through an energy-conserving mixing formula.

This process is assumed to be fast relative to radiative heating. In the model, it is instantaneous.

### Add the convective adjustment as an additional subprocess

```python
#  Here is the existing model
print( re)
```

    climlab Process of type <class 'climlab.model.column.GreyRadiationModel'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (30,)
    The subprocess tree:
    Untitled: <class 'climlab.model.column.GreyRadiationModel'>
       LW: <class 'climlab.radiation.greygas.GreyGas'>
       SW: <class 'climlab.radiation.greygas.GreyGasSW'>
       insolation: <class 'climlab.radiation.insolation.FixedInsolation'>

```python
#  First we make a new clone
rce = climlab.process_like(re)
#  Then create a new ConvectiveAdjustment process
conv = climlab.convection.ConvectiveAdjustment(state=rce.state,
                                               adj_lapse_rate=6.)
#  And add it to our model
rce.add_subprocess('Convective Adjustment', conv)
print( rce)
```

    climlab Process of type <class 'climlab.model.column.GreyRadiationModel'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (30,)
    The subprocess tree:
    Untitled: <class 'climlab.model.column.GreyRadiationModel'>
       LW: <class 'climlab.radiation.greygas.GreyGas'>
       SW: <class 'climlab.radiation.greygas.GreyGasSW'>
       insolation: <class 'climlab.radiation.insolation.FixedInsolation'>
       Convective Adjustment: <class 'climlab.convection.convadj.ConvectiveAdjustment'>

This model is exactly like our previous models, except for one additional subprocess called ``Convective Adjustment``.

We passed a parameter ``adj_lapse_rate`` (in K / km) that sets the neutrally stable lapse rate -- in this case, 6 K / km.

This number is chosed to very loosely represent the net effect of **moist convection**.

```python
#  Run out to equilibrium
rce.integrate_years(1.)
```

    Integrating for 365 steps, 365.2422 days, or 1.0 years.
    Total elapsed time is 1.9986737567564754 years.

```python
#  Check for energy balance
rce.ASR - rce.OLR
```

    Field([0.0007796])

```python
result_list.append(rce.state)
name_list.append('Radiatve-Convective equilibrium (grey gas)')
```

```python
plot_soundings(result_list, name_list)
```

Introducing convective adjustment into the model cools the surface quite a bit (compared to Radiative Equilibrium, in green here) -- and warms the lower troposphere. It gives us a MUCH better fit to observations.

But of course we still have no stratosphere.

____________

## 8. Putting stratospheric ozone in the grey-gas model
____________

Our model has no equivalent of the stratosphere, where temperature increases with height. That's because our model has been completely transparent to shortwave radiation up until now.

We can load the observed ozone climatology from the input files for the CESM model:

```python
datapath = "http://ramadda.atmos.albany.edu:8080/repository/opendap/Top/Users/BrianRose/CESM_runs/"
endstr = "/entry.das"

ozone = xr.open_dataset( datapath + 'som_input/ozone_1.9x2.5_L26_2000clim_c091112.nc' + endstr )
```

```python
print( ozone)
```

    <xarray.Dataset>
    Dimensions:  (ilev: 27, lat: 96, lev: 26, lon: 144, slat: 95, slon: 144, time: 12)
    Coordinates:
      * ilev     (ilev) float64 2.194 4.895 9.882 18.05 ... 903.3 956.0 985.1 1e+03
      * lat      (lat) float64 -90.0 -88.11 -86.21 -84.32 ... 84.32 86.21 88.11 90.0
      * lev      (lev) float64 3.545 7.389 13.97 23.94 ... 867.2 929.6 970.6 992.6
      * lon      (lon) float64 0.0 2.5 5.0 7.5 10.0 ... 350.0 352.5 355.0 357.5
      * slat     (slat) float64 -89.05 -87.16 -85.26 -83.37 ... 85.26 87.16 89.05
      * slon     (slon) float64 -1.25 1.25 3.75 6.25 ... 348.8 351.2 353.8 356.2
      * time     (time) object 2000-01-15 00:00:00 ... 2000-12-15 00:00:00
    Data variables:
        P0       float64 ...
        date     (time) int32 ...
        datesec  (time) int32 ...
        gw       (lat) float64 ...
        hyai     (ilev) float64 ...
        hyam     (lev) float64 ...
        hybi     (ilev) float64 ...
        hybm     (lev) float64 ...
        w_stag   (slat) float64 ...
        O3       (time, lev, lat, lon) float32 ...
        PS       (time, lat, lon) float32 ...
    Attributes:
        Conventions:                     CF-1.0
        source:                          CAM
        case:                            ar5_cam_1850-2000_03
        title:
        logname:                         lamar
        host:                            be0207en.ucar.ed
        Version:                         $Name$
        revision_Id:                     $Id$
        initial_file:                    /ptmp/lamar/cam-inputs/ar5_cam_1850_06.c...
        topography_file:                 /fs/cgd/csm/inputdata/atm/cam/topo/USGS-...
        history:                         Wed Jan  5 14:56:28 2011: ncks -d time,1...
        nco_openmp_thread_number:        1
        NCO:                             4.0.5
        DODS_EXTRA.Unlimited_Dimension:  time

The pressure levels in this dataset are:

```python
print( ozone.lev)
```

    <xarray.DataArray 'lev' (lev: 26)>
    array([  3.544638,   7.388814,  13.967214,  23.944625,  37.23029 ,  53.114605,
            70.05915 ,  85.439115, 100.514695, 118.250335, 139.115395, 163.66207 ,
           192.539935, 226.513265, 266.481155, 313.501265, 368.81798 , 433.895225,
           510.455255, 600.5242  , 696.79629 , 787.70206 , 867.16076 , 929.648875,
           970.55483 , 992.5561  ])
    Coordinates:
      * lev      (lev) float64 3.545 7.389 13.97 23.94 ... 867.2 929.6 970.6 992.6
    Attributes:
        long_name:      hybrid level at midpoints (1000*(A+B))
        units:          level
        positive:       down
        standard_name:  atmosphere_hybrid_sigma_pressure_coordinate
        formula_terms:  a: hyam b: hybm p0: P0 ps: PS

### Take the global average of the ozone climatology, and plot it as a function of pressure (or height)

```python
#  Take global, annual average and convert to Kelvin
weight_ozone = cos(deg2rad(ozone.lat)) / cos(deg2rad(ozone.lat)).mean(dim='lat')
O3_global = (ozone.O3 * weight_ozone).mean(dim=('lat','lon','time'))
print( O3_global)
```

    <xarray.DataArray (lev: 26)>
    array([7.827929e-06, 8.641505e-06, 7.589400e-06, 5.245671e-06, 3.177616e-06,
           1.823200e-06, 9.807570e-07, 6.228705e-07, 4.476205e-07, 3.344812e-07,
           2.625703e-07, 2.078981e-07, 1.570746e-07, 1.124255e-07, 8.060050e-08,
           6.278265e-08, 5.429906e-08, 4.995061e-08, 4.600757e-08, 4.229778e-08,
           3.805591e-08, 3.387686e-08, 3.121716e-08, 2.978071e-08, 2.879810e-08,
           2.754299e-08])
    Coordinates:
      * lev      (lev) float64 3.545 7.389 13.97 23.94 ... 867.2 929.6 970.6 992.6

```python
ax = plt.figure(figsize=(10,8)).add_subplot(111)
ax.plot( O3_global * 1.E6, -np.log(ozone.lev/climlab.constants.ps) )
ax.set_xlabel('Ozone (ppm)', fontsize=16)
ax.set_ylabel('Pressure (hPa)', fontsize=16 )
yticks = np.array([1000., 750., 500., 250., 100., 50., 20., 10., 5.])
ax.set_yticks(-np.log(yticks/1000.))
ax.set_yticklabels(yticks)
ax.grid()
ax.set_title('Global, annual mean ozone concentration', fontsize = 24);
```

This shows that most of the ozone is indeed in the stratosphere, and peaks near the top of the stratosphere.

Now create a new column model object **on the same pressure levels as the ozone data**.  We are also going set an adjusted lapse rate of 6 K / km.

```python
# the RadiativeConvectiveModel is pre-defined in climlab
#  It contains the same components are our previous model
#   But here we are specifying a different set of vertical levels.
oz_col = climlab.RadiativeConvectiveModel(lev = ozone.lev, adj_lapse_rate=6)
print( oz_col)
```

    climlab Process of type <class 'climlab.model.column.RadiativeConvectiveModel'>.
    State variables and domain shapes:
      Ts: (1,)
      Tatm: (26,)
    The subprocess tree:
    Untitled: <class 'climlab.model.column.RadiativeConvectiveModel'>
       LW: <class 'climlab.radiation.greygas.GreyGas'>
       SW: <class 'climlab.radiation.greygas.GreyGasSW'>
       insolation: <class 'climlab.radiation.insolation.FixedInsolation'>
       convective adjustment: <class 'climlab.convection.convadj.ConvectiveAdjustment'>

Now we will do something new: let the column absorb some shortwave radiation. We will assume that the shortwave absorptivity is proportional to the ozone concentration we plotted above.

Now we need to weight the absorptivity by the pressure (mass) of each layer.

```python
# This number is an arbitrary parameter that scales how absorptive we are making the ozone
# in our grey gas model
ozonefactor = 75
dp = oz_col.Tatm.domain.lev.delta
epsSW = O3_global.values * dp * ozonefactor
```

We want to use the field `epsSW` as the absorptivity for our SW radiation model.

Let's see what the absorptivity is current set to:

```python
print( oz_col.subprocess['SW'].absorptivity)
```

    [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
     0. 0.]

It defaults to zero.

Before changing this (putting in the ozone), let's take a look at the shortwave absorption in the column:

```python
oz_col.compute_diagnostics()
```

```python
oz_col.diagnostics['SW_absorbed_atm']
```

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

Let's now put in the ozone:

```python
oz_col.subprocess['SW'].absorptivity = epsSW
print( oz_col.subprocess['SW'].absorptivity)
```

    [3.20948549e-03 3.37750296e-03 4.71182551e-03 4.57614201e-03
     3.47591203e-03 2.24450924e-03 1.18884331e-03 7.11369788e-04
     5.50761612e-04 4.84170273e-04 4.47141487e-04 4.16507314e-04
     3.70212131e-04 3.11733073e-04 2.62922861e-04 2.40936639e-04
     2.45147939e-04 2.65307555e-04 2.87482272e-04 2.95567946e-04
     2.67120872e-04 2.16427978e-04 1.66169127e-04 1.15468088e-04
     6.79353134e-05 3.81013280e-05]

Let's check how this changes the SW absorption:

```python
oz_col.compute_diagnostics()
oz_col.SW_absorbed_atm
```

    array([1.40571621, 1.47671285, 2.05685574, 1.99236565, 1.50916587,
           0.97239284, 0.51429089, 0.30750441, 0.23797743, 0.20913838,
           0.19309134, 0.17981756, 0.1597929 , 0.13452308, 0.11343944,
           0.10393806, 0.10574109, 0.11442201, 0.12396838, 0.12743553,
           0.11515129, 0.09328389, 0.07161233, 0.04975714, 0.02927232,
           0.01641659])

It is now non-zero, and largest near the top of the column (also top of the array) where the ozone concentration is highest.

Now it's time to run the model out to radiative-convective equilibrium

```python
oz_col.integrate_years(1.)
```

    Integrating for 365 steps, 365.2422 days, or 1.0 years.
    Total elapsed time is 0.9993368783782377 years.

```python
print( oz_col.ASR - oz_col.OLR)
```

    [-0.00396053]

And let's now see what we got!

```python
result_list.append(oz_col.state)
name_list.append('Radiative-Convective equilibrium with O3')
```

```python
#  Make a plot to compare observations, Radiative Equilibrium, Radiative-Convective Equilibrium, and RCE with ozone!
plot_soundings(result_list, name_list)
```

And we finally have something that looks looks like the tropopause, with temperature increasing above at approximately the correct rate.

There are still plenty of discrepancies between this model solution and the observations, including:

- Tropopause temperature is too warm, by about 15 degrees.
- Surface temperature is too cold

There are a number of parameters we might adjust if we wanted to improve the fit, including:

- Longwave absorptivity
- Surface albedo

Feel free to experiment! (That's what models are for, after all).

### The take home message

The dominant effect of stratospheric ozone is to vastly increase the radiative equilibrium temperature in the ozone layer. The temperature needs to be higher so that the longwave emission can balance the shortwave absorption.

Without ozone to absorb incoming solar radiation, the **temperature does not increase with height**.

This simple grey-gas model illustrates this principle very clearly.

[Back to ATM 623 notebook home](../index.ipynb)

____________
## Version information
____________

```python
%load_ext version_information
%version_information numpy, scipy, matplotlib, xarray, climlab
```

Software	Version

Python	3.6.2 64bit [GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]

IPython	6.1.0

OS	Darwin 17.7.0 x86_64 i386 64bit

numpy	1.14.2

scipy	1.0.0

matplotlib	2.0.2

xarray	0.11.2

climlab	0.7.1.dev5

Tue Jan 15 13:25:17 2019 EST

____________

## Credits

The author of this notebook is [Brian E. J. Rose](http://www.atmos.albany.edu/facstaff/brose/index.html), University at Albany.

It was developed in support of [ATM 623: Climate Modeling](http://www.atmos.albany.edu/facstaff/brose/classes/ATM623_Spring2015/), a graduate-level course in the [Department of Atmospheric and Envionmental Sciences](http://www.albany.edu/atmos/index.php)

Development of these notes and the [climlab software](https://github.com/brian-rose/climlab) is partially supported by the National Science Foundation under award AGS-1455071 to Brian Rose. Any opinions, findings, conclusions or recommendations expressed here are mine and do not necessarily reflect the views of the National Science Foundation.
____________

```python

```

Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb


# FFM232, Klassisk fysik och vektorfält - Föreläsningsanteckningar

 **[Christian Forssén](http://fy.chalmers.se/subatom/tsp/), Institutionen för fysik, Chalmers, Göteborg, Sverige**

Date: **Oct 3, 2016**

# 9. Lösningar av Poissons ekvation

Vi vet att Poissons ekvation
$$
\Delta \phi(\vec{r}) = - \rho(\vec{r}),
$$
har entydiga lösningar om

$$
\begin{equation}
\phi|_{\partial V} = f(\vec{r}) \quad  \mathrm{Dirichlets~randvillkor} \nonumber
\end{equation}
$$

$$
\begin{equation}
(\nabla\phi)|_{\partial V} \cdot \vec{n} = g(\vec{r}) \quad  \mathrm{Neumans~randvillkor} \nonumber
\end{equation}
$$

där $f$ och $g$ är funktioner på randen $\partial V$.

## Lösning av Poissons ekvation

Vi kommer att betrakta fyra olika lösningsmetoder:

### 1. Greensfunktionsmetoden

### 2. Spegling

### 3. Variabelseparation

### 4. Numeriska metoder

* De tre förstnämnda är analytiska metoder som vi introducerar för att ge en fysikalisk förståelse av lösningarna.

* De numeriska metoderna är förstås viktigast för praktiska tillämpningar. Se datoruppgift.

## 1. Greensfunktionsmetoden

Vi skall här titta på lösningar till Poissons ekvation,
$$
\Delta\phi(\vec{r})=-\rho(\vec{r}).
$$
Vi begränsar oss till homogena randvillkor, dvs. $f,g=0$ på randen.

Lös först (med randvillkor!)
$$
\Delta G(\vec{r},\vec{r}{\;}') = -\delta(\vec{r} - \vec{r}{\;}'),
$$
dvs för en punktkälla i $\vec{r} = \vec{r}{\;}'$ med $q=1$.

[Comment 1: Notera att Laplaceoperatorn verkar på variabeln $\vec{r}$ (inte $\vec{r}{\;}'$). Dvs., $\Delta = \Delta_{\vec{r}} = \partial_x^2 + \partial_y^2 + \partial_z^2$.]

Källfördelningen $\rho$ kan skrivas som en superposition
$$
\rho(\vec{r})=\int_{V'}\mbox{d}V'\,\delta^3(\vec{r}-\vec{r}{\;}')\rho(\vec{r}{\;}').
$$
Lösningen blir en superposition av Greensfunktioner
$$
\phi(\vec{r})=\int_{V'}\mbox{d}V'\,G(\vec{r},\vec{r}{\;}')\rho(\vec{r}{\;}').
$$
Detta visas genom insättning:

$$
\begin{equation}
\Delta\phi(\vec{r}) = \Delta\int_{V'}\mbox{d}V'\,G(\vec{r},\vec{r}{\;}')\rho(\vec{r}{\;}')
=\int_{V'}\mbox{d}V'\,\Delta G(\vec{r},\vec{r}{\;}')\rho(\vec{r}{\;}') \nonumber
\end{equation}
$$

$$
\begin{equation}
=-\int_{V'}\mbox{d}V'\,\delta^3(\vec{r}-\vec{r}{\;}')\rho(\vec{r}{\;}')
=-\rho(\vec{r}).
\end{equation}
$$

* Notera att Greensfunktionen $G$ på ett område $V$ bestäms av formen på området och av randvillkoren på $\partial V$.

* På $\mathbf{R}^3$ är Greensfunktionen

$$
G(\vec{r},\vec{r}{\;}')=\frac{1}{4\pi|\vec{r}-\vec{r}{\;}'|},
$$
och lösningen till Poissons ekvation med källa $\rho$ blir
$$
\phi(\vec{r})=\int_{\mathbf{R}^3}\mbox{d}V'\,\frac{\rho(\vec{r}{\;}')}{4\pi|\vec{r}-\vec{r}{\;}'|}.
$$
[Comment 2: Detta är förstås samma uttryck som vi härledde genom superposition i kapitel 6.]

---------------------------------------------------------
### Exempel: linjekälla

Betrakta en linjekälla, $\rho(\vec{r})=k\delta^2(\vec{\rho})$, i $\mathbf{R}^3$.

Vi skall integrera över linjekällan och introducerar koordinaten
$$
\vec{r}{\;}' = \vec{\rho}{\;}' + z' \hat{z}' = \rho' \hat{\rho}' + z' \hat{z},
$$
där vi noterar att det inte behövs något "prim" på $z$-riktningen.

Vi sätter in i
$$
\phi(\vec{r}) = \int_{\mathbf{R}^3}\rho(\vec{r}{\;}')G(\vec{r},\vec{r}{\;}')\mbox{d}V'
=\int_{-\infty}^{\infty}dz'\int_{\mathbf{R}^2}dS'
       \frac{k\delta^2(\vec{\rho}{\;}')}{4\pi|\vec{r}-(\rho'\hat\rho{}'+z'\hat z)|} .
$$

Integralen $\int dS'$ över $x'$ och $y'$ kan enkelt utföras tack vare deltafunktionen. Resultatet:
$$
\phi(\vec{r})=\int_{-\infty}^{\infty}dz' \frac{k}{4\pi|\vec{r}-z'\hat z|}
$$
som är identiskt med den direkta konstruktionen från kap. 6.

---------------------------------------------------------
### Exempel: virveltråd

Poissons ekvation för ett vektorfält,
$$
\Delta\vec A = -\vec\jmath,
$$
Divergensfritt fält $\vec B$ som därmed kan uttryckas som rotationen av en
vektorpotential, $\vec B=\nabla\times\vec A$.

Lösningen blir
$$
\vec A(\vec{r})=\int_{\mathbf{R}^3} \mbox{d}V'\, \frac{\vec\jmath(\vec{r}{\;}')}{4\pi|\vec{r}-\vec{r}{\;}'|}.
$$
Av speciellt intresse är en virveltråd med konstant styrka $J$ längs en oändlig eller sluten kurva $C$. Virveltätheten blir $\vec{jmath}(\vec{r}{\;}') = J \delta^2(\vec{\rho}{\;}') \mbox{d}\vec{r}{\;}' / \mbox{d}r'$, dvs den är riktad längs kurvan $C$, där vektorn $\vec{\rho}{\;}'$ är vinkelrät mot kurvan. Vektorpotentialen blir
$$
\vec A(\vec{r})=\int_C \frac{J \mbox{d}\vec{r}{\;}'}{4\pi|\vec{r}-\vec{r}{\;}'|},
$$
där vi kan utföra volymsintegralen över $\mbox{d}r'\mbox{d}\vec{\rho}{\;}'$, dvs en riktning längs kurvan $C$ och en yta vinkelrät mot densamma.

Resultatet motsvarar t.ex. den EM vektorpotentialen från en tunn elektrisk ledare med strömmen $J$.

---------------------------------------------------------

## 2. Spegling

* Vi såg att lösningen $\phi(\vec{r})$ erhålls enkelt om man har tillgång till Greensfunktionen. Men denna är ofta svår att finna.

* För vissa geometrier erbjuder *speglingsmetoden* ett väldigt elegant sätt att konstruera Greensfunktionen.

Betrakta halvrymden $\{\vec{r}:\,z>0\}$ med ett homogent randvillkor på planet
$z=0$:
* Dirichlets randvillkor: $\phi=0$, eller

* Neumanns, $\frac{\partial \phi}{\partial z} = 0$.

[Comment 3: Detta är ett bra tillfälle att repetera begreppen ekvipotentialytor och fältlinjer (till vektorfältet $-\nabla\phi$). Se till att förstå att ett randvillkor $\phi=0$ (Dirichlet) betyder att randen är en ekvipotentialyta, och att $\vec n \cdot \nabla\phi=0$ betyder att fältlinjerna är parallella med randen.]

* Spegelladdningen finns i ett område som vi inte är intresserade av ($z<0$), men hjälper till att uppfylla randvillkoren.

Med $\vec{r}_0 = (x_0,y_0,z_0)$ och $\vec{r}_1 = (x_0,y_0,-z_0)$ och:
* $q_1 = q$ uppfylls Neumanns randvillor

* $q_1 = -q$ uppfylls Dirichlets randvillor

dvs potentialen från den två punktladdningarna
$$
\phi(\vec{r}) = \frac{q}{4 \pi |\vec{r} - \vec{r}_0|} \pm \frac{q}{4 \pi |\vec{r} - \vec{r}_1|}
$$
I det förra fallet är fältlinjerna parallella med $z=0$ planet, i det senare fallet ligger ekvipotentialytan $\phi=0$ i $z=0$ planet.

Greensfunktionen med Dirichlets homogena randvillkor blir här alltså
$$
G (\vec{r},\vec{r}_0)=\frac{1}{4\pi|\vec{r}-\vec{r}_0|} - \frac{1} {4\pi|\vec{r}-\vec{r}_1|}.
$$

[Comment 4: Notera att $G(\vec{r},\vec{r}_0)$ uppfyller $\Delta G(\vec{r},\vec{r}_0) = - \delta(\vec{r}-\vec{r}_0)$ i det övre halvrummet.]

Intressant nog fungerar speglingsmetoden även för cirklar i två dimensioner och sfärer i tre dimensioner (i det senare fallet dock endast för Dirichlets randvillkor). Se demonstrationsuppgift.

## 3. Variabelseparation

* Bygger på att man löser ekvationerna stegvis i en variabel i taget.

* Problemet skall *passa bra* ihop med ett visst koordinatsystem.

---------------------------------------------------------

### Exempel: Laplaces ekvation på en cirkelskiva

* $\Delta\phi=0$, på $\varrho=\sqrt{x^2+y^2}<a$.

* Betrakta fallet där randvillkoret enbart innehåller ett vinkelberoende

$$
\left. \phi(\vec{r}) \right|_{\partial V} = h(\varphi)
$$

Laplaceoperatorn är
$$
\Delta = \frac{1}{\varrho} \frac{\partial}{\partial \varrho} \varrho \frac{\partial}{\partial \varrho} +
 \frac{1}{\varrho^2} \frac{\partial^2}{\partial \varphi^2}.
$$
Vi kan lösa ekvationen genom att separera beroendet på variabeln $\varphi$ och
$\varrho$.

### Specialfall

Antag att randvillkoret är
$$
\phi(a,\varphi)=\phi_0\cos m\varphi,
$$
där $m$ är ett heltal.

Vi ansätter att hela lösningen har just detta beroende av $\varphi$, så att
$$
\phi(\varrho)=f(\varrho)\cos m\varphi
$$
Funktionen $\cos m\varphi$ uppfyller
$$
\frac{\partial ^2}{\partial \varphi^2} \cos m\varphi = -m^2 \cos m\varphi,
$$
dvs. den är en egenfunktion till $\frac{\partial^2}{\partial \varphi^2}$ med egenvärdet $-m^2$.

Insättning:
$$
\frac{1}{\varrho} \frac{\mbox{d}}{\mbox{d}\varrho} \left( \varrho \frac{\mbox{d}f(\varrho)}{\mbox{d}\varrho} \right) \cos m \varphi - \frac{m^2}{\varrho^2}f(\varrho)\cos m\varphi=0,
$$
och om detta skall gälla överallt på cirkelskivan måste man ha
$$
\varrho \frac{\mbox{d}}{\mbox{d}\varrho} \left(\varrho \frac{\mbox{d}f(\varrho)}{\mbox{d}\varrho} \right)
-m^2f(\varrho)=0.
$$
Den partiella differentialekvationen har nu reducerats till en ordinär differentialekvation.

* Ansatz: $f(\varrho)=A\varrho^p$

* Löser ekvationen med $p^2-m^2=0$, dvs. $p=\pm m$, där minustecknet väljs bort på grund av singulariteten i origo.

* Slutsats:

$$
\phi(\varrho,\varphi)=\phi_0\left( \frac{\varrho}{a}\right)^m \cos m\varphi,
$$
är en lösning till Laplaces ekvation på cirkelskivan med randvillkoret $\phi(a,\varphi)=\phi_0\cos m\varphi$.

### Mer allmänt randvillkor

Med randvillkoret
$$
\phi(a,\varphi)=h(\varphi),
$$
ansätter vi lösningen $\phi(\varrho,\varphi) = f(\varrho) g(\varphi)$.

Laplacianen blir
$$
\Delta \phi = \Delta \left( f(\varrho) g(\varphi) \right) = g(\varphi) \frac{1}{\varrho} \frac{\partial}{\partial\varrho} \left( \varrho \frac{\partial f(\varrho)}{\partial\varrho} \right) + \frac{f(\varrho)}{\rho^2} \frac{\partial^2 g}{\partial\varphi^2} = 0.
$$
Detta ger den *separerade* ekvationen
$$
\frac{f(\varrho) g(\varphi)}{\varrho^2} \left[ \frac{\varrho^2}{f(\varrho)} \frac{1}{\varrho} \frac{\partial}{\partial\varrho} \left( \varrho \frac{\partial f(\varrho)}{\partial\varrho} \right) + \frac{1}{g(\varphi)} \frac{\partial^2 g}{\partial\varphi^2} \right] = 0,
$$
där den första termen i hakparantesen enbart beror på $\varrho$ och den andra bara på $\varphi$. Därmed måste bägge vara konstanta (för att gälla för alla $\varrho,\varphi$). Vi sätter den första till $-\lambda$ och den andra till $+\lambda$.

Studera vinkelekvationen först
$$
\frac{\partial^2 g}{\partial\varphi^2} = \lambda g(\varphi),
$$
dvs vi kan tolka $g$ som en egenfunktion till $\partial^2 / \partial\varphi^2$. Lösningen är
$$
g(\varphi) = A \cos (m \varphi) + B \sin (m \varphi),
$$
med *egenvärdet* $\lambda = -m^2$. Funktionen måste uppfylla randvillkoret $g(0) = g(2\pi)$ vilket ger att $m = 0,1,2,\ldots$ (notera att $m=0$ är meningslös för sinus-termen).

Den kvarvarande, radiella ekvationen blir nu
$$
\frac{1}{\varrho} \frac{\partial}{\partial\varrho} \left( \varrho \frac{\partial f(\varrho)}{\partial\varrho} \right) - \frac{m^2}{\varrho^2} f(\varrho) = 0.
$$

* $m=0$, vilket innebär att $g(\varphi) = A$

$$
\frac{\partial}{\partial\varrho} \left( \varrho \frac{\partial f(\varrho)}{\partial\varrho} \right) = 0 \quad \Rightarrow \quad
\varrho \frac{\partial f(\varrho)}{\partial\varrho} = B \quad \Rightarrow \quad
\frac{\partial f(\varrho)}{\partial\varrho} = B \varrho.
$$
Med lösningen $f(\varrho) = A + B \ln(\varrho)$, där den andra termen motsvarar en punktkälla i två dimensioner (vi skippar denna).

Alltså är $\phi(\vec{r}) = A$ (konstant) en lösning om randvillkoret är $h(\varphi) = A$ (konstant).

* $m > 0$, ansätt lösning $f(\varrho) = C \varrho^p$

$$
\frac{1}{\varrho} \frac{\partial}{\partial\varrho} \left( \varrho \frac{\partial}{\partial\varrho} \varrho^p \right) - \frac{m^2}{\varrho^2} \varrho^p = 0 \quad \Rightarrow \quad p^2 \varrho^{p-2} - m^2 \varrho^{p-2} = 0 \quad \Rightarrow \quad
p = \pm m
$$
Med lösningen $f(\varrho) = A \varrho^m + \frac{B}{\varrho^m}$, där den andra termen är singulär i origo (vi skippar denna).

Med randvillkoret från ovan $h(\varphi) = \cos m \varphi$, $f(a) = \phi_0$ får vi lösningen
$$
\phi(\vec{r}) = \phi_0 \left( \frac{\varrho}{a} \right)^m \cos m\varphi,
$$
som ovan.

För ett mer allmänt randvillkor kan man (Fourier)-utveckla
$$
h(\varphi) = \sum_{m=0}^\infty a_m \cos(m\varphi) + b_m \sin(m\varphi),
$$
vilket ger lösningen
$$
\phi(\vec{r}) = \sum_{m=0}^\infty a_m \left( \frac{\varrho}{a} \right)^m \cos(m\varphi) + b_m \left( \frac{\varrho}{a} \right)^m \sin(m\varphi).
$$
OBS: ingår ej i denna kurs att kunna göra en sådan Fourierutveckling.

---------------------------------------------------------

[Comment 5: Separationsmetoden kan förstås användas med fler än två variabler. Vill man t.ex. använda den i sfäriska koordinater, hittar man egenfunktioner i tur och ordning i $\varphi$, $\theta$ och $r$. Se veckans tal. Eller så hittar man direkt egenfunktioner på $S^2$, s.k. klotytefunktioner.]