```python
# This cell is mandatory in all Dymos documentation notebooks.
missing_packages = []
try:
    import openmdao.api as om
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !python -m pip install openmdao[notebooks]
    else:
        missing_packages.append('openmdao')
try:
    import dymos as dm
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !python -m pip install dymos
    else:
        missing_packages.append('dymos')
try:
    import pyoptsparse
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !pip install -q condacolab
        import condacolab
        condacolab.install_miniconda()
        !conda install -c conda-forge pyoptsparse
    else:
        missing_packages.append('pyoptsparse')
if missing_packages:
    raise EnvironmentError('This notebook requires the following packages '
                           'please install them and restart this notebook\'s runtime: {",".join(missing_packages)}')
```

# Aircraft Balanced Field Length Calculation

```{admonition} Things you'll learn through this example
- How to perform branching trajectories
- How to constraint the difference between values at the end of two different phases
- Using complex-step differentiation on a monolithic ODE component
```

The United States Federal Aviation Regulations Part 25 defines a
balanced field length for the aircraft as the shortest field which can
accommodate a "balanced takeoff".  In a balanced takeoff the aircraft
accelerates down the runway to some critical speed "V1".

Before achieving V1, the aircraft must be capable of rejecting the takeoff
and coming to a stop before the end of the runway.

After V1, the aircraft must be capable of achieving an altitude of 35 ft
above the end of the runway with a speed of V2 (the minimum safe takeoff
speed or 1.2 x the stall speed) while on a single engine (for two engine aircraft).

At V1, both options must be available. The nominal phase sequence for this trajectory is:

1. Break Release to V1 (br_to_v1)

    Accelerate down the runway under the power of two engines.
    Where V1 is some as-yet-undetermined speed.

2. V1 to Vr (v1_to_vr)

    Accelerate down the runway under the power of a single engine.
    End at "Vr" or the rotation speed.
    The rotation speed here is defined as 1.2 times the stall speed.

3. Rotate (rotate)

    Operating under the power of a single engine, begin pitching the nose
    up (increasing alpha) while rolling down the runway.
    In this example, the rate of change of alpha is linear over some time.

4. Climb (climb)

    Still operating with one engine, begin climbing to a flight path angle
    of no more than 5 degrees.
    This phase ends when the aircraft is 35 feet above the ground with an airspeed
    of 1.25 x the stall speed.

5. Rejected Takeoff (rto)

    Shut down all engines (zero thrust) and apply brakes (increase of
    runway $\mu$ coefficient to 0.3) until the aircraft airspeed is zero.

    This phase is continuous in time and state with the first phase, and so
    forms a branch off of the nominal trajectory.

Since the RTO phase and the climb phase both must end such that they
do not exceed the end of the runway, the final value of range in each
of these two phases must be the same.  We don't know what this value is
until we've solved the problem, so we cannot simply constrain both to
the same fixed value.

Instead, we'll use a trajectory linkage constraint to ensure that `range`
at the end of the RTO phase is equal to `range` at the end of the climb phase.

More information on calculating the balanced field length is available
in section 17.8 of _Aircraft Design: A Conceptual Approach_ by
Daniel Raymer {cite}`raymer2012aircraft`.

## The ODE System

In this problem two sets of ordinary differential equations are used:
one for the aircraft motion on the runway, and one for the aircraft motion once airborne.

For simplification, we're going to assume a constant atmospheric model since the aircraft will never exceed 35 feet of altitude.
Also, since the duration of the trajectory is small, we're going to assume that the vehicle fuel burn is negligible

### The Aerodynamic Model

Both sets of equations of motion have common aerodynamic models.

First, the lift coefficient is computed using a model which assumes linearity in lift wrt the angle of attack.

\begin{align}
    C_L &= C_{L0} + \frac{\alpha}{\alpha_{max}} \left(C_{L-max} - C_{L0}\right)
\end{align}

Next, the drag-due-to-lift factor is computed (Equations 12.48 and 12.61 in Raymer[@raymer2012aircraft]).

\begin{align}
    K_{nom} &= \frac{1}{ \pi  AR  e} \\
    b &= \frac{span}{2} \\
    K &= 33 K_{nom} \frac{ \left(\frac{h + h_w}{b} \right) ^{\frac{3}{2}}}{1.0 + 33 \left( \frac{h + h_w}{b}\right) ^{\frac{3}{2}}}
\end{align}

Note the singularity in the equation for $K$ when $h + h_w$ is negative.
This causes this problem to be difficult to solve using a shooting method.
If the optimizer proposes a combination of initial states and a control history that results in altitude dropping significantly below zero, the propagation will fail.

Finally, the lift and drag are computed after computing the dynamic pressure.

\begin{align}
    q &= 0.5 \rho v^2 \\
    L &= q  S  C_L \\
    D &= q S \left( C_{D0} + K C_{L}^2 \right)
\end{align}

### Stall Speed

This model relies on the ratio of the current true airspeed to stall speed ($\frac{v}{v_{stall}}$).
This constraint is used to trigger the beginning of rotation and is used as a boundary constraint at the end of the initial climb.
Stall speed is given by Equation 5.6 in Raymer[@raymer2012aircraft].

\begin{align}
    W &= m g \\
    v_{stall} &= \sqrt{\frac{2 * W}{\rho S C_{L-max}}}
\end{align}

### Runway Equations of Motion

The runway equations of motion are used to integrate range and speed as the vehicle rolls along the runway.

\begin{align}
  F_r &= mg - L \cos \alpha - T \sin \alpha \\
  \dot{v} &= \frac{T \cos \alpha - D - F_r \mu_r}{m} \\
  \dot{r} &= v
\end{align}

|State  | Description           |Units        |
|-------|-----------------------|-------------|
|r      |range                  |$m$          |
|v      |true airspeed          |$m s^{-1}$   |

### Flight Equations of Motion

The flight equations of motion include two additional state variables: the flight-path angle ($\gamma$) and altitude ($h$).

\begin{align}
  \dot{v} &= \frac{T}{m} \cos \alpha - \frac{D}{m} - g \sin \gamma \\
  \dot{\gamma} &= \frac{T}{m v} \sin \alpha + \frac{L}{m v} - \frac{g \cos \gamma}{v} \\
  \dot{h} &= v \sin \gamma \\
  \dot{r} &= v \cos \gamma
\end{align}

|State    | Description           |Units       |
|---------|-----------------------|------------|
|v        |true airspeed          |$m s^{-1}$  |
|$\gamma$ |flight path angle      |$rad$       |
|r        |range                  |$m$         |
|h        |altitude               |$m$         |

### Treatment of the angle-of-attack ($\alpha$)

In three of the runway phases (break release to $V1$, $V1$ to $V_r$, and rejected takeoff) $\alpha$ is treated as a fixed static parameter.

In the rotation phase, $\alpha$ is treated as a polynomial control of order 1.
$\alpha$ starts at an initial value of zero and increases at a linear rate until the upward forces on the aircraft due to lift and thrust cancel the downward force due to gravity.

In the climb phase, $\alpha$ is treated as a dynamic control to be chosen by the optimizer.

Phase linkage constraints are used to maintain continuity in $\alpha$ throughout the trajectory.

### Parameters

The following parameters define properties of the aircraft and environment for the problem.

|Parameters  | Description               |Units        |Value                    |
|------------|---------------------------|-------------|-------------------------|
|m           |mass                       |$kg$         | 79015.7909              |
|g           |gravitational acceleration |$m s^{-2}$   | 9.80665                 |
|T           |thrust                     |$N$          | 2 x 120101.98 (nominal) |
|$\mu$       |runway friction coefficient|-            | 0.03 (nominal)          |
|h           |altitude                   |$m$          | 0                       |
|$\rho$      |atmospheric density        |$kg\,m^{3}$  | 1.225                   |
|S           |aerodynamic reference area |$m^2$        | 124.7                   |
|CD0         |zero-lift drag coefficient |-            | 0.03                    |
|AR          |wing aspect ratio          |-            | 9.45                    |
|e           |Oswald's wing efficiency   |-            | 801                     |
|span        |wingspan                   |$m$          | 35.7                    |
|h_w         |height of wing above CoG   |$m$          | 1.0                     |
|CL0         |aerodynamic reference area |-            | 0.5                     |
|CL_max      |aerodynamic reference area |-            | 2.0                     |

## The Optimal Control Problem

The following constraints and objective complete the definition of this optimal control problem.

### Objective

|Name   | Phase     | Location | Description | Minimized or Maximized | Ref  |
|-------|-----------|----------|-------------|------------------------|------|
| r     | rto       | final    | range       | Minimized              | 1000 |

### Nonlinear Boundary Constraints

|Name              | Phase    | Description           | Loc   | Units     | Lower  | Upper | Equals | Ref    |
|------------------|----------|-----------------------|-------|-----------|--------|-------|--------|--------|
| v_over_v_stall   | v1_to_vr | $\frac{v}{v_{stall}}$ | final | -         | 1.2    |       |        | 1.2    |
| v                | rto      | airspeed              | final | $ms^{-1}$ |        |       | 0      | 100    |
| F_r              | rotate   | downforce on gear     | final | $N$       |        |       | 0      | 100000 |
| h                | climb    | altitude              | final | $ft$      |        |       | 35     | 35     |
| gam              | climb    | flight path angle     | final | $rad$     |        |       | 5      | 5      |
| v_over_v_stall   | climb    | $\frac{v}{v_{stall}}$ | final | -         | 1.25   |       |        | 1.25   |

### Nonlinear Path Constraints

|Name              | Phase    | Description         | Units     | Lower  | Upper | Equals | Ref    |
|------------------|----------|---------------------|-----------|--------|-------|--------|--------|
| gam              | climb    | flight path angle   | $rad$     | 0      | 5     |        | 5      |

### Phase Continuity Constraints

| First Phase      | Second Phase      | Variables                   |
|------------------|-------------------|-----------------------------|
| br_to_v1[final]  | v1_to_vr[initial] | $time$, $r$, $v$            |
| vr_to_v1[final]  | rotate[initial]   | $time$, $r$, $v$, $\alpha$  |
| rotate[final]    | climb[initial]    | $time$, $r$, $v$, $\alpha$  |
| br_to_v1[final]  | rto[initial]      | $time$, $r$, $v$            |
| climb[final]     | rto[final]        | $r$                         |

## Source Code

Unlike most other Dymos examples, which use analytic derivatives, the ODE in this case is a single component.
All calculations within the ODE are complex-safe and thus we can use complex-step, in conjunction with
[partial derivative coloring](http://openmdao.org/newdocs/versions/latest/features/core_features/working_with_derivatives/simul_derivs.html),
to automatically compute the derivatives using complex-step with reasonable speed.

Since there is significant commonality between the ODEs for the runway roll and the climb, this implementation
uses a single ODE class with an option `mode` that can be set to either `'runway'` or `'climb'`.
Based on the value of `mode`, the component conditionally changes its inputs and outputs.

### BalancedFieldODEComp

```python
class BalancedFieldODEComp(om.ExplicitComponent):
    """
    The ODE System for an aircraft takeoff climb.

    Computes the rates for states v (true airspeed) gam (flight path angle) r (range) and h (altitude).

    References
    ----------
    .. [1] Raymer, Daniel. Aircraft design: a conceptual approach. American Institute of
    Aeronautics and Astronautics, Inc., 2012.
    """
    def initialize(self):
        self.options.declare('num_nodes', types=int)
        self.options.declare('g', types=(float, int), default=9.80665, desc='gravitational acceleration (m/s**2)')
        self.options.declare('mode', values=('runway', 'climb'), desc='mode of operation (ground roll or flight)')

    def setup(self):
        nn = self.options['num_nodes']

        # Scalar (constant) inputs
        self.add_input('rho', val=1.225, desc='atmospheric density at runway', units='kg/m**3')
        self.add_input('S', val=124.7, desc='aerodynamic reference area', units='m**2')
        self.add_input('CD0', val=0.03, desc='zero-lift drag coefficient', units=None)
        self.add_input('CL0', val=0.5, desc='zero-alpha lift coefficient', units=None)
        self.add_input('CL_max', val=2.0, desc='maximum lift coefficient for linear fit', units=None)
        self.add_input('alpha_max', val=np.radians(10), desc='angle of attack at CL_max', units='rad')
        self.add_input('h_w', val=1.0, desc='height of the wing above the CG', units='m')
        self.add_input('AR', val=9.45, desc='wing aspect ratio', units=None)
        self.add_input('e', val=0.801, desc='Oswald span efficiency factor', units=None)
        self.add_input('span', val=35.7, desc='Wingspan', units='m')
        self.add_input('T', val=1.0, desc='thrust', units='N')

        # Dynamic inputs (can assume a different value at every node)
        self.add_input('m', shape=(nn,), desc='aircraft mass', units='kg')
        self.add_input('v', shape=(nn,), desc='aircraft true airspeed', units='m/s')
        self.add_input('h', shape=(nn,), desc='altitude', units='m')
        self.add_input('alpha', shape=(nn,), desc='angle of attack', units='rad')

        # Outputs
        self.add_output('CL', shape=(nn,), desc='lift coefficient', units=None)
        self.add_output('q', shape=(nn,), desc='dynamic pressure', units='Pa')
        self.add_output('L', shape=(nn,), desc='lift force', units='N')
        self.add_output('D', shape=(nn,), desc='drag force', units='N')
        self.add_output('K', val=np.ones(nn), desc='drag-due-to-lift factor', units=None)
        self.add_output('F_r', shape=(nn,), desc='runway normal force', units='N')
        self.add_output('v_dot', shape=(nn,), desc='rate of change of speed', units='m/s**2',
                        tags=['dymos.state_rate_source:v'])
        self.add_output('r_dot', shape=(nn,), desc='rate of change of range', units='m/s',
                        tags=['dymos.state_rate_source:r'])
        self.add_output('W', shape=(nn,), desc='aircraft weight', units='N')
        self.add_output('v_stall', shape=(nn,), desc='stall speed', units='m/s')
        self.add_output('v_over_v_stall', shape=(nn,), desc='stall speed ratio', units=None)

        # Mode-dependent IO
        if self.options['mode'] == 'runway':
            self.add_input('mu_r', val=0.05, desc='runway friction coefficient', units=None)
        else:
            self.add_input('gam', shape=(nn,), desc='flight path angle', units='rad')
            self.add_output('gam_dot', shape=(nn,), desc='rate of change of flight path angle',
                            units='rad/s', tags=['dymos.state_rate_source:gam'])
            self.add_output('h_dot', shape=(nn,), desc='rate of change of altitude', units='m/s',
                            tags=['dymos.state_rate_source:h'])

        self.declare_coloring(wrt='*', method='cs')

    def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
        g = self.options['g']

        # Compute factor k to include ground effect on lift
        rho = inputs['rho']
        v = inputs['v']
        S = inputs['S']
        CD0 = inputs['CD0']
        m = inputs['m']
        T = inputs['T']
        h = inputs['h']
        h_w = inputs['h_w']
        span = inputs['span']
        AR = inputs['AR']
        CL0 = inputs['CL0']
        alpha = inputs['alpha']
        alpha_max = inputs['alpha_max']
        CL_max = inputs['CL_max']
        e = inputs['e']

        outputs['W'] = W = m * g
        outputs['v_stall'] = v_stall = np.sqrt(2 * W / rho / S / CL_max)
        outputs['v_over_v_stall'] = v / v_stall

        outputs['CL'] = CL = CL0 + (alpha / alpha_max) * (CL_max - CL0)
        K_nom = 1.0 / (np.pi * AR * e)
        b = span / 2.0
        fact = ((h + h_w) / b) ** 1.5
        outputs['K'] = K = K_nom * 33 * fact / (1.0 + 33 * fact)

        outputs['q'] = q = 0.5 * rho * v ** 2
        outputs['L'] = L = q * S * CL
        outputs['D'] = D = q * S * (CD0 + K * CL ** 2)

        # Compute the downward force on the landing gear
        calpha = np.cos(alpha)
        salpha = np.sin(alpha)

        # Runway normal force
        outputs['F_r'] = F_r = m * g - L * calpha - T * salpha

        # Compute the dynamics
        if self.options['mode'] == 'climb':
            gam = inputs['gam']
            cgam = np.cos(gam)
            sgam = np.sin(gam)
            outputs['v_dot'] = (T * calpha - D) / m - g * sgam
            outputs['gam_dot'] = (T * salpha + L) / (m * v) - (g / v) * cgam
            outputs['h_dot'] = v * sgam
            outputs['r_dot'] = v * cgam
        else:
            outputs['v_dot'] = (T * calpha - D - F_r * inputs['mu_r']) / m
            outputs['r_dot'] = v
```

## Building and running the problem

In the following code we define and solve the optimal control problem.
Note the use of `add_linkage_constraint` to handle the less common phase
linkage condition, where the range must be equal at the end of the `rto` and `climb` phases.

```python
import matplotlib.pyplot as plt
import numpy as np
import openmdao.api as om
from openmdao.utils.general_utils import set_pyoptsparse_opt
import dymos as dm

p = om.Problem()

_, optimizer = set_pyoptsparse_opt('IPOPT', fallback=True)

p.driver = om.pyOptSparseDriver()
p.driver.declare_coloring()

# Use IPOPT if available, with fallback to SLSQP
p.driver.options['optimizer'] = optimizer
p.driver.options['print_results'] = False
if optimizer == 'IPOPT':
    p.driver.opt_settings['print_level'] = 5
    p.driver.opt_settings['derivative_test'] = 'first-order'

# First Phase: Brake release to V1 - both engines operable
br_to_v1 = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
                    ode_init_kwargs={'mode': 'runway'})
br_to_v1.set_time_options(fix_initial=True, duration_bounds=(1, 1000), duration_ref=10.0)
br_to_v1.add_state('r', fix_initial=True, lower=0, ref=1000.0, defect_ref=1000.0)
br_to_v1.add_state('v', fix_initial=True, lower=0, ref=100.0, defect_ref=100.0)
br_to_v1.add_parameter('alpha', val=0.0, opt=False, units='deg')
br_to_v1.add_timeseries_output('*')

# Second Phase: Rejected takeoff at V1 - no engines operable
rto = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
               ode_init_kwargs={'mode': 'runway'})
rto.set_time_options(fix_initial=False, duration_bounds=(1, 1000), duration_ref=1.0)
rto.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
rto.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
rto.add_parameter('alpha', val=0.0, opt=False, units='deg')
rto.add_timeseries_output('*')

# Third Phase: V1 to Vr - single engine operable
v1_to_vr = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
                    ode_init_kwargs={'mode': 'runway'})
v1_to_vr.set_time_options(fix_initial=False, duration_bounds=(1, 1000), duration_ref=1.0)
v1_to_vr.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
v1_to_vr.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
v1_to_vr.add_parameter('alpha', val=0.0, opt=False, units='deg')
v1_to_vr.add_timeseries_output('*')

# Fourth Phase: Rotate - single engine operable
rotate = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
                  ode_init_kwargs={'mode': 'runway'})
rotate.set_time_options(fix_initial=False, duration_bounds=(1.0, 5), duration_ref=1.0)
rotate.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
rotate.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
rotate.add_polynomial_control('alpha', order=1, opt=True, units='deg', lower=0, upper=10, ref=10, val=[0, 10])
rotate.add_timeseries_output('*')

# Fifth Phase: Climb to target speed and altitude at end of runway.
climb = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=5),
                 ode_init_kwargs={'mode': 'climb'})
climb.set_time_options(fix_initial=False, duration_bounds=(1, 100), duration_ref=1.0)
climb.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
climb.add_state('h', fix_initial=True, lower=0, ref=1.0, defect_ref=1.0)
climb.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
climb.add_state('gam', fix_initial=True, lower=0, ref=0.05, defect_ref=0.05)
climb.add_control('alpha', opt=True, units='deg', lower=-10, upper=15, ref=10)
climb.add_timeseries_output('*')

# Instantiate the trajectory and add phases
traj = dm.Trajectory()
p.model.add_subsystem('traj', traj)
traj.add_phase('br_to_v1', br_to_v1)
traj.add_phase('rto', rto)
traj.add_phase('v1_to_vr', v1_to_vr)
traj.add_phase('rotate', rotate)
traj.add_phase('climb', climb)

# Add parameters common to multiple phases to the trajectory
traj.add_parameter('m', val=174200., opt=False, units='lbm',
                   desc='aircraft mass',
                   targets={'br_to_v1': ['m'], 'v1_to_vr': ['m'], 'rto': ['m'],
                            'rotate': ['m'], 'climb': ['m']})

traj.add_parameter('T_nominal', val=27000 * 2, opt=False, units='lbf', static_target=True,
                   desc='nominal aircraft thrust',
                   targets={'br_to_v1': ['T']})

traj.add_parameter('T_engine_out', val=27000, opt=False, units='lbf', static_target=True,
                   desc='thrust under a single engine',
                   targets={'v1_to_vr': ['T'], 'rotate': ['T'], 'climb': ['T']})

traj.add_parameter('T_shutdown', val=0.0, opt=False, units='lbf', static_target=True,
                   desc='thrust when engines are shut down for rejected takeoff',
                   targets={'rto': ['T']})

traj.add_parameter('mu_r_nominal', val=0.03, opt=False, units=None, static_target=True,
                   desc='nominal runway friction coefficient',
                   targets={'br_to_v1': ['mu_r'], 'v1_to_vr': ['mu_r'],  'rotate': ['mu_r']})

traj.add_parameter('mu_r_braking', val=0.3, opt=False, units=None, static_target=True,
                   desc='runway friction coefficient under braking',
                   targets={'rto': ['mu_r']})

traj.add_parameter('h_runway', val=0., opt=False, units='ft',
                   desc='runway altitude',
                   targets={'br_to_v1': ['h'], 'v1_to_vr': ['h'], 'rto': ['h'],
                            'rotate': ['h']})

traj.add_parameter('rho', val=1.225, opt=False, units='kg/m**3', static_target=True,
                   desc='atmospheric density',
                   targets={'br_to_v1': ['rho'], 'v1_to_vr': ['rho'], 'rto': ['rho'],
                            'rotate': ['rho']})

traj.add_parameter('S', val=124.7, opt=False, units='m**2', static_target=True,
                   desc='aerodynamic reference area',
                   targets={'br_to_v1': ['S'], 'v1_to_vr': ['S'], 'rto': ['S'],
                            'rotate': ['S'], 'climb': ['S']})

traj.add_parameter('CD0', val=0.03, opt=False, units=None, static_target=True,
                   desc='zero-lift drag coefficient',
                   targets={f'{phase}': ['CD0'] for phase in ['br_to_v1', 'v1_to_vr',
                                                              'rto', 'rotate' 'climb']})

traj.add_parameter('AR', val=9.45, opt=False, units=None, static_target=True,
                   desc='wing aspect ratio',
                   targets={f'{phase}': ['AR'] for phase in ['br_to_v1', 'v1_to_vr',
                                                             'rto', 'rotate' 'climb']})

traj.add_parameter('e', val=801, opt=False, units=None, static_target=True,
                   desc='Oswald span efficiency factor',
                   targets={f'{phase}': ['e'] for phase in ['br_to_v1', 'v1_to_vr',
                                                            'rto', 'rotate' 'climb']})

traj.add_parameter('span', val=35.7, opt=False, units='m', static_target=True,
                   desc='wingspan',
                   targets={f'{phase}': ['span'] for phase in ['br_to_v1', 'v1_to_vr',
                                                               'rto', 'rotate' 'climb']})

traj.add_parameter('h_w', val=1.0, opt=False, units='m', static_target=True,
                   desc='height of wing above CG',
                   targets={f'{phase}': ['h_w'] for phase in ['br_to_v1', 'v1_to_vr',
                                                              'rto', 'rotate' 'climb']})

traj.add_parameter('CL0', val=0.5, opt=False, units=None, static_target=True,
                   desc='zero-alpha lift coefficient',
                   targets={f'{phase}': ['CL0'] for phase in ['br_to_v1', 'v1_to_vr',
                                                              'rto', 'rotate' 'climb']})

traj.add_parameter('CL_max', val=2.0, opt=False, units=None, static_target=True,
                   desc='maximum lift coefficient for linear fit',
                   targets={f'{phase}': ['CL_max'] for phase in ['br_to_v1', 'v1_to_vr',
                                                                 'rto', 'rotate' 'climb']})

traj.add_parameter('alpha_max', val=10.0, opt=False, units='deg', static_target=True,
                   desc='angle of attack at maximum lift',
                   targets={f'{phase}': ['alpha_max'] for phase in ['br_to_v1', 'v1_to_vr',
                                                                    'rto', 'rotate' 'climb']})

# Standard "end of first phase to beginning of second phase" linkages
# Alpha changes from being a parameter in v1_to_vr to a polynomial control
# in rotate, to a dynamic control in `climb`.
traj.link_phases(['br_to_v1', 'v1_to_vr'], vars=['time', 'r', 'v'])
traj.link_phases(['v1_to_vr', 'rotate'], vars=['time', 'r', 'v', 'alpha'])
traj.link_phases(['rotate', 'climb'], vars=['time', 'r', 'v', 'alpha'])
traj.link_phases(['br_to_v1', 'rto'], vars=['time', 'r', 'v'])

# Less common "final value of r must match at ends of two phases".
traj.add_linkage_constraint(phase_a='rto', var_a='r', loc_a='final',
                            phase_b='climb', var_b='r', loc_b='final',
                            ref=1000)

# Define the constraints and objective for the optimal control problem
v1_to_vr.add_boundary_constraint('v_over_v_stall', loc='final', lower=1.2, ref=100)

rto.add_boundary_constraint('v', loc='final', equals=0., ref=100, linear=True)

rotate.add_boundary_constraint('F_r', loc='final', equals=0, ref=100000)

climb.add_boundary_constraint('h', loc='final', equals=35, ref=35, units='ft', linear=True)
climb.add_boundary_constraint('gam', loc='final', equals=5, ref=5, units='deg', linear=True)
climb.add_path_constraint('gam', lower=0, upper=5, ref=5, units='deg')
climb.add_boundary_constraint('v_over_v_stall', loc='final', lower=1.25, ref=1.25)

rto.add_objective('r', loc='final', ref=1000.0)

#
# Setup the problem and set the initial guess
#
p.setup(check=True)

p.set_val('traj.br_to_v1.t_initial', 0)
p.set_val('traj.br_to_v1.t_duration', 35)
p.set_val('traj.br_to_v1.states:r', br_to_v1.interp('r', [0, 2500.0]))
p.set_val('traj.br_to_v1.states:v', br_to_v1.interp('v', [0, 100.0]))
p.set_val('traj.br_to_v1.parameters:alpha', 0, units='deg')

p.set_val('traj.v1_to_vr.t_initial', 35)
p.set_val('traj.v1_to_vr.t_duration', 35)
p.set_val('traj.v1_to_vr.states:r', v1_to_vr.interp('r', [2500, 300.0]))
p.set_val('traj.v1_to_vr.states:v', v1_to_vr.interp('v', [100, 110.0]))
p.set_val('traj.v1_to_vr.parameters:alpha', 0.0, units='deg')

p.set_val('traj.rto.t_initial', 35)
p.set_val('traj.rto.t_duration', 35)
p.set_val('traj.rto.states:r', rto.interp('r', [2500, 5000.0]))
p.set_val('traj.rto.states:v', rto.interp('v', [110, 0]))
p.set_val('traj.rto.parameters:alpha', 0.0, units='deg')

p.set_val('traj.rotate.t_initial', 70)
p.set_val('traj.rotate.t_duration', 5)
p.set_val('traj.rotate.states:r', rotate.interp('r', [1750, 1800.0]))
p.set_val('traj.rotate.states:v', rotate.interp('v', [80, 85.0]))
p.set_val('traj.rotate.polynomial_controls:alpha', 0.0, units='deg')

p.set_val('traj.climb.t_initial', 75)
p.set_val('traj.climb.t_duration', 15)
p.set_val('traj.climb.states:r', climb.interp('r', [5000, 5500.0]), units='ft')
p.set_val('traj.climb.states:v', climb.interp('v', [160, 170.0]), units='kn')
p.set_val('traj.climb.states:h', climb.interp('h', [0, 35.0]), units='ft')
p.set_val('traj.climb.states:gam', climb.interp('gam', [0, 5.0]), units='deg')
p.set_val('traj.climb.controls:alpha', 5.0, units='deg')

dm.run_problem(p, run_driver=True, simulate=True)

print(p.get_val('traj.rto.states:r')[-1])

sim_case = om.CaseReader('dymos_solution.db').get_case('final')

fig, axes = plt.subplots(2, 1, sharex=True, gridspec_kw={'top': 0.92})
for phase in ['br_to_v1', 'rto', 'v1_to_vr', 'rotate', 'climb']:
    r = sim_case.get_val(f'traj.{phase}.timeseries.states:r', units='ft')
    v = sim_case.get_val(f'traj.{phase}.timeseries.states:v', units='kn')
    t = sim_case.get_val(f'traj.{phase}.timeseries.time', units='s')
    axes[0].plot(t, r, '-', label=phase)
    axes[1].plot(t, v, '-', label=phase)
fig.suptitle('Balanced Field Length')
axes[1].set_xlabel('time (s)')
axes[0].set_ylabel('range (ft)')
axes[1].set_ylabel('airspeed (kts)')
axes[0].grid(True)
axes[1].grid(True)

tv1 = sim_case.get_val('traj.br_to_v1.timeseries.time', units='s')[-1, 0]
v1 = sim_case.get_val('traj.br_to_v1.timeseries.states:v', units='kn')[-1, 0]

tf_rto = sim_case.get_val('traj.rto.timeseries.time', units='s')[-1, 0]
rf_rto = sim_case.get_val('traj.rto.timeseries.states:r', units='ft')[-1, 0]

axes[0].annotate(f'field length = {r[-1, 0]:5.1f} ft', xy=(t[-1, 0], r[-1, 0]),
                 xycoords='data', xytext=(0.7, 0.5),
                 textcoords='axes fraction', arrowprops=dict(arrowstyle='->'),
                 horizontalalignment='center', verticalalignment='top')

axes[0].annotate(f'', xy=(tf_rto, rf_rto),
                 xycoords='data', xytext=(0.7, 0.5),
                 textcoords='axes fraction', arrowprops=dict(arrowstyle='->'),
                 horizontalalignment='center', verticalalignment='top')

axes[1].annotate(f'$v1$ = {v1:5.1f} kts', xy=(tv1, v1), xycoords='data', xytext=(0.5, 0.5),
                 textcoords='axes fraction', arrowprops=dict(arrowstyle='->'),
                 horizontalalignment='center', verticalalignment='top')

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

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p.get_val('traj.rto.states:r')[-1], 2188.2, tolerance=0.01)
```

## References

```{bibliography}
:filter: docname in docnames
```


```
!pip install cirq
```

    Collecting cirq
      Downloading https://files.pythonhosted.org/packages/53/29/a66c4c28306dae359745e37c4c10120e477da44cb050d06d8ceb1117a22a/cirq-0.7.0-py3-none-any.whl (1.2MB)
         |████████████████████████████████| 1.2MB 4.9MB/s
    Collecting protobuf==3.8.0
      Downloading https://files.pythonhosted.org/packages/d2/fb/29de8d08967f0cce1bb10b39846d836b0f3bf6776ddc36aed7c73498ca7e/protobuf-3.8.0-cp36-cp36m-manylinux1_x86_64.whl (1.2MB)
         |████████████████████████████████| 1.2MB 59.0MB/s
    Requirement already satisfied: dataclasses; python_version < "3.7" in /usr/local/lib/python3.6/dist-packages (from cirq) (0.7)
    Requirement already satisfied: matplotlib~=3.0 in /usr/local/lib/python3.6/dist-packages (from cirq) (3.1.3)
    Requirement already satisfied: scipy in /usr/local/lib/python3.6/dist-packages (from cirq) (1.4.1)
    Requirement already satisfied: requests~=2.18 in /usr/local/lib/python3.6/dist-packages (from cirq) (2.21.0)
    Requirement already satisfied: google-api-python-client~=1.6 in /usr/local/lib/python3.6/dist-packages (from cirq) (1.7.11)
    Collecting networkx==2.3
      Downloading https://files.pythonhosted.org/packages/85/08/f20aef11d4c343b557e5de6b9548761811eb16e438cee3d32b1c66c8566b/networkx-2.3.zip (1.7MB)
         |████████████████████████████████| 1.8MB 54.5MB/s
    Collecting sympy==1.4
      Downloading https://files.pythonhosted.org/packages/21/21/f4105795ca7f35c541d82c5b06be684dd2f5cb4f508fb487cd7aea4de776/sympy-1.4-py2.py3-none-any.whl (5.3MB)
         |████████████████████████████████| 5.3MB 50.1MB/s
    Requirement already satisfied: sortedcontainers~=2.0 in /usr/local/lib/python3.6/dist-packages (from cirq) (2.1.0)
    Requirement already satisfied: pandas in /usr/local/lib/python3.6/dist-packages (from cirq) (0.25.3)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.6/dist-packages (from cirq) (3.6.6)
    Requirement already satisfied: numpy~=1.16 in /usr/local/lib/python3.6/dist-packages (from cirq) (1.17.5)
    Requirement already satisfied: setuptools in /usr/local/lib/python3.6/dist-packages (from protobuf==3.8.0->cirq) (45.2.0)
    Requirement already satisfied: six>=1.9 in /usr/local/lib/python3.6/dist-packages (from protobuf==3.8.0->cirq) (1.12.0)
    Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (2.6.1)
    Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (1.1.0)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (2.4.6)
    Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (0.10.0)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (2019.11.28)
    Requirement already satisfied: urllib3<1.25,>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (1.24.3)
    Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (3.0.4)
    Requirement already satisfied: idna<2.9,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (2.8)
    Requirement already satisfied: httplib2<1dev,>=0.9.2 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (0.11.3)
    Requirement already satisfied: google-auth>=1.4.1 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (1.7.2)
    Requirement already satisfied: google-auth-httplib2>=0.0.3 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (0.0.3)
    Requirement already satisfied: uritemplate<4dev,>=3.0.0 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (3.0.1)
    Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.6/dist-packages (from networkx==2.3->cirq) (4.4.1)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.6/dist-packages (from sympy==1.4->cirq) (1.1.0)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas->cirq) (2018.9)
    Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (0.2.8)
    Requirement already satisfied: cachetools<3.2,>=2.0.0 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (3.1.1)
    Requirement already satisfied: rsa<4.1,>=3.1.4 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (4.0)
    Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in /usr/local/lib/python3.6/dist-packages (from pyasn1-modules>=0.2.1->google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (0.4.8)
    Building wheels for collected packages: networkx
      Building wheel for networkx (setup.py) ... done
      Created wheel for networkx: filename=networkx-2.3-py2.py3-none-any.whl size=1556408 sha256=d3ee3c13ca251200746c137efef35eece519c8d2c69901baac916d558d4f4142
      Stored in directory: /root/.cache/pip/wheels/de/63/64/3699be2a9d0ccdb37c7f16329acf3863fd76eda58c39c737af
    Successfully built networkx
    ERROR: albumentations 0.1.12 has requirement imgaug<0.2.7,>=0.2.5, but you'll have imgaug 0.2.9 which is incompatible.
    Installing collected packages: protobuf, networkx, sympy, cirq
      Found existing installation: protobuf 3.10.0
        Uninstalling protobuf-3.10.0:
          Successfully uninstalled protobuf-3.10.0
      Found existing installation: networkx 2.4
        Uninstalling networkx-2.4:
          Successfully uninstalled networkx-2.4
      Found existing installation: sympy 1.1.1
        Uninstalling sympy-1.1.1:
          Successfully uninstalled sympy-1.1.1
    Successfully installed cirq-0.7.0 networkx-2.3 protobuf-3.8.0 sympy-1.4

```
import cirq
```

```
# ref: https://colab.research.google.com/drive/1LHTDuIsb0mwVlEqbyrQ8K1rKdK6fFV7v
# Here I'm defining a utility function to print circuits nicely using an svg
from cirq.contrib.svg import circuit_to_svg
from IPython.display import SVG, display
import os

q0, q1 = cirq.GridQubit.rect(1, 2)
my_circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1))

def circuit_print(circuit):
  """Convert the cirq.Circuit to a temp svg and display + garbage collect."""
  with open("temp.svg", 'w+') as f:
    f.write(circuit_to_svg(circuit))
  display(SVG('temp.svg'))
  os.remove('temp.svg')

# Now we can use the above-defined function anywhere in the notebook
circuit_print(my_circuit)
```

```
from cirq.contrib import qcircuit
# Colab doesn't support inline LaTeX compilation, but you can copy and paste
# the printed LaTeX code to run on your own machine. Follow these instructions:
# http://physics.unm.edu/CQuIC/Qcircuit/Qtutorial.pdf
print(qcircuit.circuit_to_latex_using_qcircuit(my_circuit))
```

    \Qcircuit @R=1em @C=0.75em {
     \\
     &\lstick{\text{(0, 0)}}& \qw&\gate{\text{H}} \qw&\control \qw    &\qw\\
     &\lstick{\text{(0, 1)}}& \qw&                \qw&\targ    \qw\qwx&\qw\\
     \\
    }

```

```


#  Pawnee Fire analysis

The Pawnee Fire was a large wildfire that burned in Lake County, California. The fire started on June 23, 2018 and burned a total of 15,185 acres (61 km2) before it was fully contained on July 8, 2018.

## Remote Sensing using Sentinel-2 layer

```python
from arcgis import GIS
gis = GIS(profile='plenary_deldev_profile')
```

For this analysis, we will be using Sentinel-2 imagery from the Living Atlas.

Sentinel-2 is an Earth observation mission developed by ESA as part of the Copernicus Programme to perform terrestrial observations in support of services such as forest monitoring, land cover changes detection, and natural disaster management.

```python
sentinel_item = gis.content.search('Sentinel-2 Views', outside_org=True)[0]
sentinel_item
```

                        Sentinel-2 Views

Sentinel-2, 10m Multispectral, Multitemporal, 13-band images with visual renderings and indices.  This Imagery Layer is sourced from the Sentinel-2 on AWS collections and is updated daily with new imagery.  This layer is in beta release.Imagery Layer by esri

Last Modified: December 19, 2018

8 comments, 93,761 views

### Select before and after rasters

```python
import arcgis

sentinel = sentinel_item.layers[0]
aoi = {'spatialReference': {'latestWkid': 3857, 'wkid': 102100},
 'xmax': -13643017.100720055,
 'xmin': -13652113.10708598,
 'ymax': 4739654.477447927,
 'ymin': 4731284.622850712}
arcgis.env.analysis_extent = aoi
sentinel.extent = aoi
```

```python
import pandas as pd
from datetime import datetime

selected = sentinel.filter_by(where="acquisitiondate BETWEEN timestamp '2018-06-15 00:00:00' AND timestamp '2018-06-24 19:59:59'",
                              geometry=arcgis.geometry.filters.intersects(aoi))

df = selected.query(out_fields="AcquisitionDate, Tile_ID, CloudCover", order_by_fields="AcquisitionDate").df
df['acquisitiondate'] = pd.to_datetime(df['acquisitiondate'], unit='ms')
df.tail(40)
```

      acquisitiondate
      cloudcover
      objectid
      shape_Area
      shape_Length
      tile_id
      SHAPE

      0
      2018-06-17 19:02:28
      0.1070
      2686387
      1.214547e+10
      446919.588256
      20180617T224132_10SEJ_0
      {'rings': [[[-13599954.1142, 4730228.964000002...

      1
      2018-06-17 19:13:35
      0.0398
      2686843
      4.138418e+09
      271266.855576
      20180618T001031_10SEJ_1
      {'rings': [[[-13634891.7136, 4731760.155000001...

      2
      2018-06-19 19:03:34
      0.0273
      2701464
      1.089364e+10
      438058.126931
      20180619T190334_10SEJ_0
      {'rings': [[[-13667279.6085, 4773934.774300002...

      3
      2018-06-22 19:04:33
      0.0000
      2750251
      1.402370e+10
      486152.331987
      20180622T190432_10SEJ_0
      {'rings': [[[-13596402.5766, 4745349.801899999...

      4
      2018-06-24 19:01:08
      0.0000
      2800097
      1.991829e+10
      560634.860343
      20180624T190108_10SEJ_0
      {'rings': [[[-13549971.4055, 4828395.097300000...

```python
prefire = sentinel.filter_by('OBJECTID=2750251') # 2017-07-01
midfire = sentinel.filter_by('OBJECTID=2800097') # 2017-07-24
```

## Visual Assessment

```python
from arcgis.raster.functions import *

truecolor = apply(midfire, 'Natural Color with DRA')
truecolor
```

### Visualize Burn Scars

Extract the [12, 11, 4] bands to improve visibility of fire and burn scars. This band combination pushes further into the SWIR range of the electromagnetic spectrum, where there is less susceptibility to smoke and haze generated by a burning fire.

```python
extract_band(midfire, [12,11,4])
```

For comparison, the same area before the fire started shows no burn scar.

```python
extract_band(prefire, [12,11,4])
```

## Quantitative Assessment

The **Normalized Burn Ratio (NBR)** can be used to delineate the burnt areas and identify the severity of the fire.

The formula for the NBR is very similar to that of NDVI except that it uses near-infrared band 9 and the short-wave infrared band 13:
\begin{align}
{\mathbf{NBR}} = \frac{\mathbf{B9} - \mathbf{B13}}{\mathbf{B9} + \mathbf{B13} + \mathbf{WS}} \\
\end{align}

The NBR equation was designed to be calcualted from reflectance, but it can be calculated from radiance and digital_number_(dn) with changes to the burn severity table below. The WS parameter is used for water suppression, and is typically 2000.

For a given area, NBR is calculated from an image just prior to the burn and a second NBR is calculated for an image immediately following the burn. Burn extent and severity is judged by taking the difference between these two index layers:

\begin{align}
{\Delta \mathbf{NBR}} = \mathbf{NBR_{prefire}} - \mathbf{NBR_{postfire}} \\
\end{align}

The meaning of the ∆NBR values can vary by scene, and interpretation in specific instances should always be based on some field assessment. However, the following table from the USGS FireMon program can be useful as a first approximation for interpreting the NBR difference:

| \begin{align}{\Delta \mathbf{NBR}}  \end{align}      | Burn Severity |
| ------------- |:-------------:|
| 0.1 to 0.27   | Low severity burn |
| 0.27 to 0.44  | Medium severity burn |
| 0.44 to 0.66 | Moderate severity burn |
| > 0.66 | High severity burn |

[Source: http://wiki.landscapetoolbox.org/doku.php/remote_sensing_methods:normalized_burn_ratio]

### Use Band Arithmetic and Map Algebra

```python
nbr_prefire  = band_arithmetic(prefire, "(b9 - b13) / (b9 + b13 + 2000)")
nbr_postfire = band_arithmetic(midfire, "(b9 - b13) / (b9 + b13 + 2000)")

nbr_diff = nbr_prefire - nbr_postfire
```

```python
burnt_areas = colormap(remap(nbr_diff,
                             input_ranges=[0.1,  0.27,  # low severity
                                           0.27, 0.44,  # medium severity
                                           0.44, 0.66,  # moderate severity
                                           0.66, 1.00], # high severity burn
                             output_values=[1, 2, 3, 4],
                             no_data_ranges=[-1, 0.1], astype='u8'),
                       colormap=[[4, 0xFF, 0xC3, 0], [3, 0xFA, 0x8E, 0], [2, 0xF2, 0x55, 0],
                                 [1, 0xE6, 0,    0]])
```

```python
burnt_areas.draw_graph()
```

### Area calculation

```python
pixx = (aoi['xmax'] - aoi['xmin']) / 1200.0
pixy = (aoi['ymax'] - aoi['ymin']) / 450.0

res = burnt_areas.compute_histograms(aoi, pixel_size={'x':pixx, 'y':pixy})

numpix = 0
histogram = res['histograms'][0]['counts'][1:]
for i in histogram:
    numpix += i
```

### Report burnt area

```python
from IPython.display import HTML
sqmarea = numpix * pixx * pixy # in sq. m
acres = 0.00024711 * sqmarea   # in acres

HTML('<h3>Fire has consumed <font color="red">{:,} acres</font>  till {}</h3>.' \
     .format(int(acres), df.iloc[-1]['acquisitiondate'].date()))
```

Fire has consumed 3,569 acres  till 2018-06-24
.

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

plt.title('Distribution by severity', y=-0.1)
plt.pie(histogram, labels=['Low Severity', 'Medium Severity',
                           'Moderate Severity', 'High Severity'])
plt.axis('equal')
```

### Visualize burnt areas

```python
firemap = gis.map()
firemap.extent = aoi
firemap
```

```python
firemap.add_layer([truecolor, burnt_areas])
```

## Raster to Feature layer conversion

Use Raster Analytics and Geoanalytics to convert the burnt area raster to a feature layer. The `to_features()` method converts the raster to a feature layer and `create_buffers()` fills holes in the features and dissolves them to output one feature that covers the extent of the Thomas Fire.

#### Access Portal

```python
#gis = GIS("https://datasciencedev.esri.com/portal", "rjackson", "admin123", verify_cert=False)
portal = GIS("https://python.playground.esri.com/portal", "arcgis_python", "amazing_arcgis_123")

fire = portal.content.search('PawneeFireArea', 'Feature Layer')[0]
fire
```

                        PawneeFireArea

PawneeFireAreaFeature Layer Collection by arcgis_python

Last Modified: October 16, 2018

0 comments, 17 views

### Convert and Buffer Data

```python
from arcgis.geoanalytics.use_proximity import create_buffers

fire_item = burnt_areas.to_features(output_name='Pawnee_Fire_boundary_output_tst')

fire_layer = fire_item.layers[0]
```

```python
fire = create_buffers(fire_layer, 100, 'Meters', dissolve_option='All',
                      multipart=True, output_name='PawneeFireAreaTst')
```

## Visualize feature layer

```python
vectormap = gis.map()
vectormap.basemap = 'dark-gray'
vectormap.extent  = aoi
vectormap.add_layer(fire)
vectormap
```

## Impact Assessment

### Assess human impact

```python
from arcgis.geoenrichment import enrich
from arcgis.geometry import filters
from arcgis.features import GeoAccessor, GeoSeriesAccessor
import pandas as pd
```

```python
sdf = pd.DataFrame.spatial.from_layer(fire.layers[0])

gis = GIS(profile='deldev')

fire_geometry = sdf.iloc[0].SHAPE
sa_filter = filters.intersects(geometry=fire_geometry,
                               sr=fire_geometry['spatialReference'])
```

```python
def age_pyramid(df):
    import warnings
    import seaborn as sns
    import matplotlib.pyplot as plt

    %matplotlib inline
    warnings.simplefilter(action='ignore', category=FutureWarning)
    pd.options.mode.chained_assignment = None
    plt.style.use('ggplot')

    df = df[[x for x in impacted_people.columns if 'MALE' in x or 'FEM' in x]]
    sf = pd.DataFrame(df.sum())
    age = sf.index.str.extract('(\d+)').astype('int64')
    f = sf[sf.index.str.startswith('FEM')]
    m = sf[sf.index.str.startswith('MALE')]
    sf = sf.reset_index(drop = True)
    f = f.reset_index(drop = True)
    m = m.reset_index(drop = True)
    sf['age'] = age
    f["age"] = age
    m["age"] = age
    f = f.sort_values(by='age', ascending=False).set_index('age')
    m = m.sort_values(by='age', ascending=False).set_index('age')

    popdf = pd.concat([f, m], axis=1)
    popdf.columns = ['F', 'M']
    popdf['agelabel'] = popdf.index.map(str) + ' - ' + (popdf.index+4).map(str)
    popdf.M = -popdf.M

    sns.barplot(x="F", y="agelabel", color="#CC6699", label="Female", data=popdf, edgecolor='none')
    sns.barplot(x="M",  y="agelabel", color="#008AB8", label="Male",   data=popdf,  edgecolor='none')
    plt.ylabel('Age group')
    plt.xlabel('Number of people');
    return plt;
```

### Age Pyramid of affected population

```python
from arcgis.geoenrichment import enrich

impacted_people = enrich(sdf, 'Age')
age_pyramid(impacted_people)
```

talks/FederalDevSummit2019/PlenaryDemos/demo 02 - RA_ML/02 - Wildfire analysis using Sentinel-2 imagery.ipynb


# Optimization for Medical Image Segmentation with 2D U-Net on Intel® Xeon Scalable Platform

#### Agenda

1. Background information introduction
2. Intel's optimization technologies on Intel® Xeon Scalable Processors
3. Let's do coding!

### 1. Background Information Introduction
#### 1.1 Brain MRI scan
Magnetic resonance imaging (MRI) of the brain is a safe and painless test that uses a magnetic field and radio waves to produce detailed images of the brain and the brain stem. An MRI differs from a CAT scan (also called a CT scan or a computed axial tomography scan) because it does not use radiation.

An MRI scanner consists of a large doughnut-shaped magnet that often has a tunnel in the center. Patients are placed on a table that slides into the tunnel. Some centers have open MRI machines that have larger openings and are helpful for patients with claustrophobia. MRI machines are located in hospitals and radiology centers.

During the exam, radio waves manipulate the magnetic position of the atoms of the body, which are picked up by a powerful antenna and sent to a computer. The computer performs millions of calculations, resulting in clear, cross-sectional black and white images of the body. These images can be converted into three-dimensional (3-D) pictures of the scanned area. This helps pinpoint problems in the brain and the brain stem when the scan focuses on those areas.

**Reference:** https://kidshealth.org/en/parents/mri-brain.html

**Reference:** https://github.com/IntelAI/unet

### 1.2 U-Net for brain images segmentation

U-Net implementation in TensorFlow for FLAIR abnormality segmentation in brain MRI based on a deep learning segmentation algorithm used in [Association of genomic subtypes of lower-grade gliomas with shape features automatically extracted by a deep learning algorithm](https://doi.org/10.1016/j.compbiomed.2019.05.002).

```latex
@article{buda2019association,
  title={Association of genomic subtypes of lower-grade gliomas with shape features automatically extracted by a deep learning algorithm},
  author={Buda, Mateusz and Saha, Ashirbani and Mazurowski, Maciej A},
  journal={Computers in Biology and Medicine},
  volume={109},
  year={2019},
  publisher={Elsevier},
  doi={10.1016/j.compbiomed.2019.05.002}
}
```

Topology structured as the following:

**Reference:** https://github.com/mateuszbuda/brain-segmentation-pytorch

### 2. Intel's optimization technologies on Intel® Xeon Scalable Processors

In order to take full advantage of Intel® architecture and to extract maximum performance, the TensorFlow framework has been optimized with Intel® Math Kernel Library for Deep Neural Networks (Intel® oneDNN) primitives, a popular performance library for deep learning applications.

For more information about the optimizations as well as performance data, see the blog post:[TensorFlow Optimizations on Modern Intel® Architecture](https://software.intel.com/en-us/articles/tensorflow-optimizations-on-modern-intel-architecture).

Installation guide of Intel Optimization for TensorFlow can be found at [Intel® Optimization for TensorFlow Installation Guide](https://software.intel.com/en-us/articles/intel-optimization-for-tensorflow-installation-guide).

#### 2.1 Optimization with TensorFlow switches
**intra_op_parallelism_threads**
- Number of threads in each threadpool for an operation (like matrix multiplication or reduction).
- Recommend: #physical cores, found in Linux with ‘lscpu’ command.

**inter_op_parallelism_threads**
- Number of thread pools for independent operations.
- Recommend: #cpu sockets,  found in Linux with ‘lscpu’ command.

Note, need to test with the model & platform to find the best parameters.

#### 2.2 Optimization with Intel® oneDNN switches
Intel oneDNN utilizes OpenMP to leverage Intel architecture.
Following environment variables for vectorization and multi-threading.

**KMP_AFFINITY**
- Restricts execution of certain threads to a subset of the physical processing units in a multiprocessor computer.
- Recommend: ```export KMP_AFFINITY=granularity=fine,compact,1,0```

**KMP_BLOCKTIME**
- Set the time (milliseconds), that a thread wait for, after completing the execution of a parallel region, before sleeping.
- Recommend: ```export KMP_BLOCKTIME=0 (or 1)```

**OMP_NUM_THREADS**
- Set maximum number of threads to use for OpenMP parallel regions
- Recommend: ```export OMP_NUM_THREADS=num physical cores```

Note, recommend users tuning these values for their specific neural network model and platform.

#### 2.3 Optimization with miscellaneous configurations/tools
**Numactl**
- Running on a NUMA-enabled machine brings with it special considerations. NUMA or non-uniform memory access is a memory layout design used in data center machines meant to take advantage of locality of memory in multi-socket machines with multiple memory controllers and blocks. In most cases, inference runs best when confining both the execution and memory usage to a single NUMA node.
- Recommend: ```numactl --cpunodebind=N --membind=N python <pytorch_script>```

**Batch size**
- Can increase usage and efficiency of hardware resources.
- Optional according to your requirements.

A more detailed introduction of maximizing performance with Intel Optimization for TensorFlow can be found [here](https://software.intel.com/en-us/articles/maximize-tensorflow-performance-on-cpu-considerations-and-recommendations-for-inference).

https://software.intel.com/en-us/articles/maximize-tensorflow-performance-on-cpu-considerations-and-recommendations-for-inference

### 3. Let's do coding!

#### 3.0 Dataset
We use [brain tumor segmentation (BraTS) subset](https://drive.google.com/file/d/1A2IU8Sgea1h3fYLpYtFb2v7NYdMjvEhU/view?usp=sharing) of the [Medical Segmentation Decathlon](http://medicaldecathlon.com/) dataset. The dataset has the [Creative Commons Attribution-ShareAlike 4.0 International license](https://creativecommons.org/licenses/by-sa/4.0/).

Please follow instructions [here](https://github.com/IntelAI/unet/blob/master/2D/00_Prepare-Data.ipynb) to prepare the dataset.

#### 3.1 Import required packages

```python
%matplotlib inline
import os
import numpy as np
import tensorflow as tf
import keras as K
import h5py
import math
import pandas as pd
import time
import matplotlib.pyplot as plt
import sys; sys.argv=['']; del sys

from argparser import args
from data import load_data
from model import unet
```

#### 3.2 Check TensorFlow version, and do sanity check

```python
print ("We are using TensorFlow version", tf.__version__,\
       "with Intel(R) oneDNN", "enabled" if tf.pywrap_tensorflow.IsMklEnabled() else "disabled",)
```

#### 3.3 Define the DICE coefficient and loss function
The Sørensen–Dice coefficient is a statistic used for comparing the similarity of two samples. Given two sets, X and Y, it is defined as

\begin{equation}
Dice = \frac{2|X\cap Y|}{|X|+|Y|}
\end{equation}

```python
def calc_dice(target, prediction, smooth=0.01):
    """
    Sorensen Dice coefficient
    """
    prediction = np.round(prediction)

    numerator = 2.0 * np.sum(target * prediction) + smooth
    denominator = np.sum(target) + np.sum(prediction) + smooth
    coef = numerator / denominator

    return coef

def calc_soft_dice(target, prediction, smooth=0.01):
    """
    Sorensen (Soft) Dice coefficient - Don't round preictions
    """
    numerator = 2.0 * np.sum(target * prediction) + smooth
    denominator = np.sum(target) + np.sum(prediction) + smooth
    coef = numerator / denominator

    return coef
```

#### 3.4 Load images

```python
data_path = os.path.join("../../data/decathlon/144x144/")
data_filename = "Task01_BrainTumour.h5"
hdf5_filename = os.path.join(data_path, data_filename)
imgs_train, msks_train, imgs_validation, msks_validation, imgs_testing, msks_testing = load_data(hdf5_filename)
imgs_warmup=imgs_testing[:500]
imgs_infere=imgs_testing[500:2500]
print("Number of imgs_warmup: {}".format(imgs_warmup.shape[0]))
print("Number of imgs_infere: {}".format(imgs_infere.shape[0]))
```

#### 3.5 Load model

```python
unet_model = unet()
model = unet_model.load_model(os.path.join("./output/unet_model_for_decathlon.hdf5"))
```

#### 3.6 Define function to inference on input images and plot results out

```python
def plot_results(model, imgs_validation, msks_validation, img_no):
    img = imgs_validation[idx:idx+1]
    msk = msks_validation[idx:idx+1]

    pred_mask = model.predict(img, verbose=1, steps=None)

    plt.figure(figsize=(15, 15))
    plt.subplot(1, 3, 1)
    plt.imshow(img[0, :, :, 0], cmap="bone", origin="lower")
    plt.axis("off")
    plt.title("MRI Input", fontsize=20)
    plt.subplot(1, 3, 2)
    plt.imshow(msk[0, :, :, 0], origin="lower")
    plt.axis("off")
    plt.title("Ground truth", fontsize=20)
    plt.subplot(1, 3, 3)
    plt.imshow(pred_mask[0, :, :, 0], origin="lower")
    plt.axis("off")
    plt.title("Prediction\nDice = {:.4f}".format(calc_dice(pred_mask, msk)), fontsize=20)
    plt.tight_layout()
```

#### 3.7 Run inference and plot

```python
indicies_validation = [40, 63, 43, 55, 99]
for idx in indicies_validation:
    plot_results(model, imgs_validation, msks_validation, idx)
```

#### 3.8 Demo of 2D U-Net inference optimization

See demo in console.

*Performance results:*
- Latency
  - Time spent for processing 1 image.
  - Unit: millisecond per frame (ms/f)
- Throughput
  - Number of processed images in 1 second.
  - Unit: frames per second (f/s)

*Runs:*
- Single stream
  - Batch size 1
    - Without numactl
      - Default configuration
      - Configuration with optimization
    - With numactl
      - Default configuration
      - Configuration with optimization
  - Batch size 128
    - With numactl
      - Default configuration
      - Configuration with optimization
- Multiple streams
  - Batch size 128
    - Configuration with optimization
      - 2 streams
      - 4 streams
      - 8 streams

##### 3.8.1 Configuration with optimization & numactl

```python
def plot_perf(x_labels, latencys, throughputs, xlabel, batch_size):
    x = np.arange(len(x_labels))
    plt.figure(figsize=(16,6))
    plt.subplot(121)
    ax1 = plt.gca()
    ax1.bar(x, latencys, width=0.3)
    ax1.set_title('Latency (batch size: {}, the less the better)'.format(batch_size))
    ax1.set_xlabel(xlabel)
    ax1.set_ylabel('Latency (ms)')
    ax1.set_xticks(x)
    ax1.set_xticklabels(x_labels)
    for rect, label in zip(ax1.patches, latencys):
        height = rect.get_height()
        ax1.text(rect.get_x() + rect.get_width() / 2, height, label, ha='center', va='bottom')

    plt.subplot(122)
    ax2 = plt.gca()
    ax2.bar(x, throughputs, color='tab:green', width=0.3)
    ax2.set_title('Throughput (batch size: {}, the larger the better)'.format(batch_size))
    ax2.set_xlabel(xlabel)
    ax2.set_ylabel('Through (fps)')
    ax2.set_xticks(x)
    ax2.set_xticklabels(x_labels)
    for rect, label in zip(ax2.patches, throughputs):
        height = rect.get_height()
        ax2.text(rect.get_x() + rect.get_width() / 2, height, label, ha='center', va='bottom')

    plt.show()
```

```python
# load result performance data
rsts = []
with open('04_rst_opt_numa.csv', 'r') as f:
    for line in f.readlines():
        rst = line.strip().split(',')
        rsts.append(rst)
```

```python
# batch size: 1
x_labels = ['default conf', 'conf with optmization', 'conf with opt + numactl']
latencys_conf_1 = [float(rsts[1][2]), float(rsts[2][2]), float(rsts[3][2])]
throughputs_conf_1 = [float(rsts[1][3]), float(rsts[2][3]), float(rsts[3][3])]
plot_perf(x_labels, latencys_conf_1, throughputs_conf_1, 'Configuration', 1)
```

```python
# batch size: 128
x_labels = ['default conf', 'conf with optmization', 'conf with opt + numactl']
latencys_conf_128 = [float(rsts[4][2]), float(rsts[5][2]), float(rsts[6][2])]
throughputs_conf_128 = [float(rsts[4][3]), float(rsts[5][3]), float(rsts[6][3])]
plot_perf(x_labels, latencys_conf_128, throughputs_conf_128, 'Configuration', 128)
```

##### 3.8.2 number of streams

```python
# load result performance data
rsts = []
with open('04_rst_instances.csv', 'r') as f:
    for line in f.readlines():
        rst = line.strip().split(',')
        rsts.append(rst)
```

```python
# batch size: 1
x_labels = [1, 2, 4, 8]
latencys_stream_1 = [float(rsts[1][2]), float(rsts[3][2]), float(rsts[4][2]), float(rsts[5][2])]
throughputs_stream_1 = [float(rsts[1][3]), float(rsts[3][3]), float(rsts[4][3]), float(rsts[5][3])]
plot_perf(x_labels, latencys_stream_1, throughputs_stream_1, 'Number of streams', 1)
```

```python
# batch size: 128
x_labels = [1, 2, 4, 8]
latencys_stream_128 = [float(rsts[2][2]), float(rsts[6][2]), float(rsts[7][2]), float(rsts[8][2])]
throughputs_stream_128 = [float(rsts[2][3]), float(rsts[6][3]), float(rsts[7][3]), float(rsts[8][3])]
plot_perf(x_labels, latencys_stream_128, throughputs_stream_128, 'Number of streams', 128)
```

### 4. Summary

```python
def plot_summary(x_labels, y_labels, xlabel, ylabel, title, color):
    x = np.arange(len(x_labels))
    plt.figure(figsize=(16,6))
    ax = plt.gca()
    ax.bar(x, y_labels, color=color, width=0.3)
    ax.set_title(title)
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_xticks(x)
    ax.set_xticklabels(x_labels)
    for rect, label in zip(ax.patches, y_labels):
        height = rect.get_height()
        ax.text(rect.get_x() + rect.get_width() / 2, height, label, ha='center', va='bottom')
    plt.show()
```

#### 4.0 Scenarios:
1. You need to get the inference result as soon as possible. (Online processing)
2. You need to process a large bunch of data in a certain period. (Offline processing)

#### 4.1 Online processing

From **latency** perspective point of view.

\* Run 1 stream processing 1 image/frame on 1 socket with configuration with optimization

```python
x_labels = ['default conf,\nbs=1, stream=1', 'conf with optmization,\nbs=1, stream=1', 'conf with opt + numactl,\nbs=1, stream=1', 'conf with opt + numactl,\nbs=1, stream=2', 'conf with opt + numactl,\nbs=1, stream=4']
x = np.arange(len(x_labels))
latencys_summary = [latencys_conf_1[0], latencys_conf_1[1], latencys_conf_1[2], latencys_stream_1[1], latencys_stream_1[2]]
plot_summary(x_labels, latencys_summary, 'Configuration', 'Latency (ms)', 'Latency with different configurations (the less the better)', 'tab:blue')

print('Number of sockets: 2')
```

#### 4.2 Offline processing

From **throughput** perspective point of view.

\* Run multiple streams processing a batch of images/frames on all sockets with configuration with optimization

```python
x_labels = ['conf with opt + numactl,\nbs=1, stream=1', 'conf with opt + numactl,\nbs=128, stream=1', 'conf with opt + numactl,\nbs=128, stream=4', 'conf with opt + numactl,\nbs=128, stream=8']
x = np.arange(len(x_labels))
throughputs_summary = [throughputs_conf_1[2], throughputs_conf_128[2], throughputs_stream_128[1], throughputs_stream_128[2]]
plot_summary(x_labels, throughputs_summary, 'Configuration', 'Throughput (fps)', 'Throughput with different configurations (the larger the better)', 'tab:green')

print('Number of sockets: 2')
```

**The optimization methods in this course are not the best.**

**All optimization methods should be adjusted based on test result. It depends on the detailed model, dataset, HardWare & user case.**

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
   http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

SPDX-License-Identifier: EPL-2.0


```python
%run src/init_notebooks.py
check_notebook()
hide_toggle()
```

# Umbrella sampling

## Questions this tutorial will address
* How can I get insight faster than with plain molecular dynamics?
* What problems can umbrella sampling help with?
* How should I design and set up umbrella sampling simulations?

## Metadata

Version: 1.0
Contributions: Dr Mark Abraham, Dr Christian Blau
Supported by: ENCCS (https://enccs.se/), BioExcel (https://bioexcel.eu/)

## Target audience

The content of this tutorial suits experienced users of GROMACS.
Such users should be familiar with basic theory behind MD simulations, along with setting them up and running them in GROMACS.

## Prerequisites

* GROMACS 2020.x (other versions will likely work, but might look a bit different)

### Optional

* a molecular viewer, e.g. VMD
* a plotting tool, e.g. xmgrace

## What is umbrella sampling?

Many modelling problems require sampling transitions between coarse states.
In principle, molecular dynamics simulations will eventually undertake those transitions often enough that insight into paths, barrier sizes, and rates can be gained.
But that is extremely inefficient.
We would often like to be able to guide or *pull* the sampling to where we need it.

### Example: membrane permeation

*The permeability of skin to drugs can be estimated by pulling a drug (in the center) across the bilayers that remain after skin cells dry.*

### Example: host-guest binding

*Host-guest systems model ligands binding to biomacromolecules.
The free-energy of binding can be estimated by pulling the blue guest away from the wireframe host.*

### Pathways for pulling

Given an initial guess at a pathway, it is much more efficient to bias the sampling to places that lie on that pathway.
The system is allowed to explore the equilibrium state described by the normal molecular potential plus the bias.
The observed frequencies of configurations can then be re-weighted to remove the effect of the bias.
The resulting unbiased samples can be used to get insight into e.g. changes in free energy along the pathway.

In the above examples, simple paths for pulling would lie in the horizontal direction, between centers of mass of different groups.
Sometimes simple paths do not exist and need to be composed piece-wise.

## Model system for this tutorial

In this tutorial, we will use a simple system that needs only moderate resources to sample.
Nevertheless, it captures a lot of the essential complexity that characterizes real problems.
We will use umbrella sampling, also known as pulling, to examine the free-energy profile of bringing two pyrimidine molecules together in an aqueous solution.

*A single molecule of pyrimidine*

We want to estimate the free energy of binding of two such molecules, and make any observations that we can about the pathway of binding.
For the path, or *reaction coordinate* we will choose the vector that joins the center of mass of two pyrimidine molecules.
We will run simulations along that pathway to understand the conformations that are adopted there.
To locate the free-energy minimum of any bound conformation we will need to bring them quite close together (ie. < 1 nm) and also quite far apart.

## Quiz

How far apart do we need to take the molecules in order to estimate the free energy of binding?

* Infinitely far
* 1 nm
* 5 nm
* It depends

## Sampling a reaction coordinate

Ideally, we would run just one simulation in the bound state and one in the free state.
They would both happen to visit similar conformations, and we could compute the free energy of the transition between bound and free states directly.

This is possible to do, but rarely efficient.
Instead we run many simulations, each at different positions along the reaction coordinate.
We try to choose the points close enough together that some of the sampling at each point will be of configurations that are also close to adjacent points along the path.
If successful, this means that we can estimate the free-energy difference of going from one point to another.
Doing that pairwise estimation along the whole pathway means we can estimate the free-energy difference of the whole pathway.
Remember that free energy differences are independent of the path taken between the states!

*A series of displacements (shown in dark red) along a reaction coordinate are chosen to create a series of simulations whose configuration ensembles overlap those of adjacent simulations.*

## Quiz

What would happen if the displacements are chosen too far apart?

* The overall computation would be more efficient
* The overall computation would be less efficient
* The result would be more likely to be correct
* The result would be less likely to be correct

## The biasing potential

Once the reaction coordinate and sampling locations are chosen, a very common choice of biasing potential is harmonic in the displacement along that coordinate.
The shape of an harmonic potential is like an upside-down umbrella, which is how one name for this enhanced-sampling method came about!

One simulation is run at each single location along the reaction coordinate, and during the simulation if the system diffuses away from that location it experiences a force that tends to return it to that location.
In this tutorial, we will measure the displacement between the center of mass of each molecule.
During the simulation, any force on the center of mass due to the bias is distributed to the particles according to their mass.

*Schematic illustration of four simulations of the same system used as part of an umbrella sampling study.
Each simulation has a different harmonic "umbrella" potential (shown in green) that keeps it near its chosen displacement along the reaction coordinate (shown in dark red).
The umbrella displacements and widths are carefully chosen so that the resulting configurational ensemble (blue) for each simulation overlaps those of adjacent simulations.*

*Two Gaussian distributions that overlap are shown.
How much the distributions sampled by your biased simulations should overlap depends.
Too much and you will be running more simulations than you need to.
Too little and your free-energy profile will be inaccurate.*

## Quiz

What would happen if the harmonic potentials are too "stiff" ie. narrow in shape?

* The overall computation would be more efficient
* The overall computation would be less efficient
* The result would be more likely to be correct
* The result would be less likely to be correct

## Planning for an umbrella sampling simulation

In order to do umbrella sampling, a simulation must be equilibrated at each location on the reaction coordinate, with the umbrella potential active.
This can be tricky, because it is often not a simple case of "click and drag!"

Sometimes it is enough to start from either a bound or free configuration near one of the desired reaction coordinate points, and equilibrate to it.
The endpoint of that equilibration might be used to start the next equilibration at an adjacent location.
If the locations have been chosen well, this will work fine, because the adjacent location will sample nearby configurations that can be reached easily.
However, it means that the last simulation in the series won't start until all the other equilibrations have completed.

An alternative is to run "steered molecular dynamics" to do a non-equilbrium trajectory that is forced to move along the reaction coordinate.
From that trajectory we can pick suitable configurations to start the equilibration at each location along the reaction coordinate.
In GROMACS, this is called "constant-force pulling."

Another alternative is "interactive molecular dynamics" where the user can link visualization software to a running simulation and choose where to apply forces to effect transitions.
This can help to provide insight into possible reaction coordinates, as well as where to sample along them.

### Discussion

All these approaches can introduce a *path dependence* into the configurations chosen to begin the equilibration. This might be a significant risk in a real scientific investigation. How might this be mitigated?

## System preparation for this tutorial

For this tutorial, 24 systems have been prepared in advance using the first method described above.
This will help us focus on the mechanics of umbrella sampling, rather than details of solvating and choosing box sizes that are common to all simulations.

However, we should now look at those mechanics!

## GROMACS .mdp options for pulling

Using functionality in GROMACS requires setting appropriate options in the ``.mdp`` file.
The ones we'll use will let the simulation know that pulling will take place, as well as describe the reaction coordinate(s) and bias(es).
GROMACS typically uses *pull groups* and *pull geometries* to define *pull coordinates* (ie reaction coordinate).
In the example in this tutorial, we will use one pull group for each pyrimidine molecule.
The distances betwen the centers of mass of those groups will define the single pull coordinate.

Together, the ``.mdp`` lines for the above pulling setup are:

```
pull                     = yes
pull-ngroups             = 2
pull-group1-name         = pyrimidine_1
pull-group2-name         = pyrimidine_2
pull-ncoords             = 1
pull-coord1-groups       = 1 2
pull-coord1-geometry     = distance
```

As with other kinds of groups in GROMACS (e.g. temperature-coupling groups), the pull groups are described by *index groups* in the *index file* also supplied to ``gmx grompp`` (typically named ``index.ndx``).

To use umbrella sampling, we describe that as the ``pull-coord-type`` and choose a width for the harmonic potential ``pull-coord-k``.
In this tutorial, the umbrella will not move so has a zero ``pull-coord-rate``.
Finally, each system we prepare will have a fixed location chosen for the umbrella along the reaction coordinate, here 0.834 nm.

```
pull-coord1-type         = umbrella
pull-coord1-k            = 5000.0
pull-coord1-rate         = 0.0
pull-coord1-init         = 0.834
```

These and many other ``.mdp`` options for pulling are described more full in [the documentation](http://manual.gromacs.org/2020.4/user-guide/mdp-options.html#com-pulling).

Let us look at one of the ``grompp.mdp`` run input files:

```python
%less sampling/run-08/grompp.mdp
```

Here we can see a very normal production simulation of 5 ns, using the above-mentioned pull-code options.

What range of initial reaction coordinate locations was chosen over the set of simulations?
(Remember the default unit for distance is nm.)

```python
! grep pull-coord1-init sampling/run-*/grompp.mdp
```

### Quiz

Does that range seem suitable?
* Yes
* No, too many short distances
* No, not enough short distances
* No, too many long distances
* No, not enough long distances

What differences exist between these run input files? Let's look at two adjacent ones:

```python
! diff sampling/run-08/grompp.mdp sampling/run-09/grompp.mdp
```

What is in the simulation topology?

```python
%less sampling/run-08/topol.top
```

How are the index groups set up for the pull groups? Remember, the order of atoms in the coordinate file ``conf.gro`` matches the order implied by the ``[ molecules ]`` section above.

```python
! head -n 10 sampling/run-08/index.ndx
```

Finally, let's visualize one of the systems:

```python
nv.show_structure_file('sampling/run-08/conf.gro')

```

## Running ``gmx grompp``

If you have GROMACS installed, then you can go ahead and run ``gmx grompp -n`` in each of the ``sampling/run-*`` directories.
We'll need the ``.tpr`` files later for the analysis, so we'll go ahead and use a bash loop to do that now:

```bash
%%bash
for run_directory in sampling/run-*
do
  (cd $run_directory; gmx grompp -n)
done
```

## Running ``gmx mdrun``

These are quite small systems, but we have a lot of them, so we won't try to run them together now. Even if you have a GPU, the set of them will take 10-20 hours to run. If you did run them, then an efficient approach on a cluster would be to use `mpiexec -np 24 gmx_mpi mdrun -multidir sampling/run*`. Alternatively, you could run them one by one with:

```
(for run_directory in sampling/run-*; \
 do \
   (cd $run_directory; gmx mdrun) \
 done)
```

In each directory, this command produces the usual ``.log``, ``.xtc``, ``.edr``, and ``.cpt`` files together with two other output files, namely ``pullx.xvg`` and ``pullf.xvg``. Respectively, these contain the time series for that simulation of the distance along the reaction coordinate, and the resulting scalar force that biases the simulation back towards the ``pull-coord-init`` distance value.

## Analysing pulling simulations with WHAM

GROMACS provides a tool called ``gmx wham`` that executes the *Weighted Histogram Analysis Method* (WHAM).
We will use it to analyse the biased sampling along the reaction coordinate $\xi$, reweight those samples, and combine the sampling along the reaction coordinate to return a free-energy profile along $\xi$.
That profile is also known as a *potential of mean force* for historical reasons.

The free energy profile we seek, $A(\xi)$ is related to the probability $P(\xi)$ of finding configurations at different points on the reaction coordinate $\xi$:
\begin{equation}
A(\xi) = −k_\beta T \mathrm{ln} P(\xi)
\end{equation}
$T$ is the temperature of our simulation, and $k_\beta$ is Boltzmann'\s constant.

However we don't have time to wait for enough sampling to compute $P(\xi)$ directly.
Instead, we have run our series of biased simulations (indexed with $i$ from 1 to $N_w$), with added umbrella potentials $V_i(x)$.
These produced a set of biased probability distributions $P_i^\mathrm{bias}(\xi)$.
We'd like to use those to estimate the unbiased free energy $A_i(\xi)$ based on simulation $i$ by removing the umbrella potential $V_i(\xi)$ and the free-energy contribution from that bias, $F_i$, after the fact:
\begin{equation}
A_i(\xi) = −k_\beta T \mathrm{ln} P_i^\mathrm{bias}(\xi) - V_i(\xi) + F_i
\end{equation}
Sadly, the values of $F_i$ *depend* on the bias, so there is no easy solution.

To get the most reliable estimate for $P(\xi)$ we want to combine the information from all the biased simulations.
\begin{equation}
P(\xi) = \Sigma_{i=1}^{N_w} w_i P_i^\mathrm{bias}(\xi)
\end{equation}
There exists a statistically optimal way to determine the weights $w_i$, but going through the derivation is out of scope for this course.
Please check out https://doi.org/10.1002/jcc.540130812 to see the details!

The resulting set of $N_w + 1$ WHAM equations are
\begin{equation}
P(\xi) = \frac{ \Sigma_{i=1}^{N_w} h_i(\xi) } { \Sigma_{j=1}^{N_w} n_j \exp ( -\beta V_j(\xi) + F_j )}
\end{equation}
\begin{equation}
\exp (F_i) = \int d\xi P(\xi) \exp(-\beta V_i(\xi) )
\end{equation}
where $h_i(\xi)$ is the histogram count from simulation $i$ observed at reaction coordinate $\xi$, $n_i$ is the count of total observations from simulation $i$, and $\beta=\frac{1}{k_\beta T}$.
In practice, this requires choosing a range of $\xi$ over which to plot a histogram, and a number of bins into which to divide the histogram.

There are multiple unknowns, namely $P(\xi)$ and $F_i$.
In such cases, one often resorts to an iterative solution.
We guess part of the solution and updating the guess until we find values that are *self-consistent* with all the available information.
In general, that would be difficult for WHAM.
However, if the simulations overlap sufficiently and have adequate sampling, we know that they should mutually agree on the value of $P(\xi)$ in the region where they overlap.
This means the iteration to find self-consistency has a decent chance to succeed.

It often works well to make a  first guess of a flat free-energy profile, ie. $F_i = 0$ for all $i$.
Thereafter, one can compute $P(\xi)$, then update $F_i$, etc. until the values stop changing.
Then they are said to be self consistent.

Note that the above equations are simplified in several ways.
Properly, one should compute the autocorrelation time of the observations and reweight them accordingly.
However, in practice these times are normally the same across all $i$ simulations.
(A notable exception is phase transitions.)
We have assumed that the bias potential depends only on $\xi$, that all simulations have a common temperature, and that it is necessary to construct a histogram.
Much research has gone into generalizations and improvements to umbrella simulation and WHAM protocols, and some links and further reading are found below.

## Analysing our pulling simulations

The sampling along the reaction coordinate is recorded in the ``pullx.xvg`` files created by ``gmx mdrun``.
The bias force experienced there is computed by ``gmx wham`` based on the information in the corresponding ``topol.tpr`` file.
We provide will ``gmx wham`` with two lists of files, respectively of ``pullx.xvg`` and ``topol.tpr`` files.
The order of the simulations within the lists does not matter, but the pair of files for each simulation must be in matching places in the two lists.
That's how ``gmx wham`` is able to match the observed distances with the expected bias.

For this tutorial, those files have been provided for you, so let's construct the lists of those files:

```python
! ls -1 analysis/run-*/pullx.xvg > pullx.dat; cat pullx.dat; ls -1 sampling/run-*/topol.tpr > tpr.dat; cat tpr.dat
```

Let's go ahead and run ``gmx wham`` with those inputs!
In additional to the terminal output, it produces two output files that we can plot.

```python
run_command('gmx wham -it tpr.dat -ix pullx')
metadata, data = parse_xvg("profile.xvg")
plot_data(data, metadata)
```

By default, ``gmx wham`` will observe the range of sampled reaction coordinate values and decide the range over which it will put samples into histograms to compute the free energy.
It reports that it ``Determined boundaries to 0.289226 and 2.399760``

### Quiz

Is that reasonable, given the range of ``pull-coord-init`` we prepared and checked above?

* We can't know
* No
* Yes

Earlier in the output, it reports

```
Warning, poor sampling bin 198 (z=2.38393). Check your histograms!
Warning, poor sampling bin 199 (z=2.39448). Check your histograms!
```

so it looks like the automatic range detection might be too ambitious. Scroll down in the output above to see the plot of the histogram, and let's see how it looks.

The free energy required to be at each location on the reaction coordinate is estimated.
It doesn't look too bad, e.g. no sharp spike at large distances.
But from the above warnings, the sampling at large distances is suspect, so let's be more conservative.
We'll set the minimum and maximum extent of the histograms manually.
It's also convenient to set the zero of the free energy to be at large distances in this case, as that is intended to approximately model a pair of infinitely separated pyrimidine modules.

```python
run_command('gmx wham -it tpr.dat -ix pullx -min 0.288 -max 2.35 -zprof0 2.35')
metadata, data = parse_xvg("profile.xvg")
plot_data(data, metadata)
```

No warning this time!

Now we can see easily that we probably haven't chosen simulations for which the reaction coordinate far enough away to properly reach the asymptote we expect at large distances.
The free energy grows slowly to a peak around 0.7 nm, reaches a trough as the pyrimidines get closer, and then spikes hard as the centers of mass get so close that van der Waals' repulsions dominate.

### Discussion - think for a minute, then write in the hackmd document

What do you think is happening around 0.5 nm?
What do you think is happening when the solutes are 0.7 nm apart?

Let's investigate by taking a look at the initial configuration taken from 0.5 nm.
In a real situation, you would consider the whole trajectory, but the initial configuration is enough for this tutorial.

```python
view = nv.show_structure_file('sampling/run-05/conf.gro')
view

```

Modify the line above to inspect the initial conditional also for 0.7 nm (in run-07 folder).
Can you see anything to confirm your hypotheses?

## Sampling overlap

A successful umbrella sampling simulation requires that the simulations subject to the umbrellas produce configurations that overlap with those sampled by adjacent simulations.
The ``gmx wham`` tool also prints out a convenient plot (named ``histo.xvg`` by default) that provides clues whether that has happened.
The extent of the overlap suggests whether sufficient overlap exists. Let's see that in action:

```python
metadata, data = parse_xvg("histo.xvg")
plot_data(data, metadata)
```

It is clear that the distributions described by the simulations do overlap.
If they did not overlap enough, then adding another simulation at intermediate point(s) in reaction-coordinate space would improve the results.

## Was there enough sampling?

This perennial question in MD often doesn't have a good answer.
If the converged simulation never found a relevant part of configuration space, then just running more simulation is unlikely to help you.
If we hadn't sampled this system enough, we'd run into problems making a histogram.
Let's use the functionality of ``gmx wham`` to look only at the first two nanoseconds of data, to see the kind of problems we might run into:

```python
run_command('gmx wham -it tpr.dat -ix pullx.dat -b 0 -e 2 -bins 50')
metadata, data = parse_xvg("profile.xvg")
plot_data(data, metadata)
metadata, data = parse_xvg("histo.xvg")
plot_data(data, metadata)
```

Here we see that despite reasonable overlap of the limited sampling, the PMF produced has spiky artefacts.
That suggests the total sampling is too low, and particularly so in the region of the artefacts (in this case, that means everywhere).
If you would see this in a real simulation study, now you know what to do.

## What happens if the reaction coordinate is too widely spaced?

Sometimes an initial guess for reaction-coordinate spacing is too conservative.
In such cases, the overlap between simulations is too low.
The iterative process that solves the WHAM equations will definitely fail for lack of data in such regions.
This is sometimes clearly seen in the profile it produces.
Here, we will use the ``bash`` tool called ``seq`` to help us omit every second simulation from the sampling to observe the kind of effect that can be seen:

```bash
%%bash
rm -f pullx.dat tpr.dat
for index in $(seq -w 0 2 23)
do
  echo "analysis/run-$index/pullx.xvg" >> pullx.dat
  echo "sampling/run-$index/topol.tpr" >> tpr.dat
done
```

```python
run_command('gmx wham -it tpr.dat -ix pullx.dat')
metadata, data = parse_xvg("profile.xvg")
plot_data(data, metadata)
metadata, data = parse_xvg("histo.xvg")
plot_data(data, metadata)
```

Note again that warnings were seen, and the artifacts had a suggestive regularity, because the missing regions were regularly spaced.
That regularity is itself an artifact of our choice to space the simulations evenly in reaction-coordinate space.
If the spacing between simulations had been refined already, then you will need to consider carefully where artefacts from lack of sampling overlap might exist.
Overlaying the locations of the target locations in configuration space can be helpful here.

## Other possible artefacts

As with all methods that require choosing a reaction coordinate, if the choice does not capture essential features of the landscape, then the conclusions are unlikely to be correct.
Exploratory studies are often needed to provide the insight to choose these reaction coordinates.
Often in real work with biomolecular systems, a process including multiple coordinate stages is needed.
For example, one reaction coordinate might open a pocket, then another might guide a ligand into the pocket.

## Further reading about WHAM, ``gmx wham``, and alternatives

There is a lot more functionality in ``gmx wham`` than we have covered, including bootstrapped error estimates. See https://doi.org/10.1021/ct100494z for details.

The WHAM technique is widely used across the field of molecular simulations, not just for umbrella sampling.
It has its own page at [the alchemistry.org wiki](http://www.alchemistry.org/wiki/Weighted_Histogram_Analysis_Method)
Many different implementations exist, and if you have any doubt about the numerical quality of any of them, you should definitely check against another implementation.
For example, this [open-source implementation](http://membrane.urmc.rochester.edu/?page_id=126) produces good results.

WHAM is part of a family of related methods that include BAR and MBAR.
In particular, MBAR can be seen as a generalization of WHAM.
It also frees the user from having to choose a histogram range, which is both convenient and statistically sound.
The [alchemistry.org wiki](http://www.alchemistry.org/wiki/Multistate_Bennett_Acceptance_Ratio) contains more information for you.
An [open-source implementation of MBAR](https://github.com/choderalab/pymbar) is recommended.

## Closing thoughts

There are many alternative kinds of pulling available in GROMACS.
These include alternative coordinate types, e.g. angles, as well as pulling ``geometries``.
Do check out [the documentation](http://manual.gromacs.org/2020.4/user-guide/mdp-options.html#com-pulling) and consider if you need those capabilities for your simulation problems!

## Other resources

* Prof Justin Lemkul's excellent [MD tutorial on umbrella sampling](http://www.mdtutorials.com/gmx/umbrella/index.html)
* The [alchemistry.org wiki]()
* Some example publications using umbrella sampling [here](https://doi.org/10.1021/jp9110794), [here](https://doi.org/10.1021/ja211929h), and [here](https://doi.org/10.1007/s10822-016-9970-8).