# N body simulation

```python
from IPython.core.display import HTML
css_file = 'https://raw.githubusercontent.com/ngcm/training-public/master/ipython_notebook_styles/ngcmstyle.css'
HTML(url=css_file)
```

<link href='http://fonts.googleapis.com/css?family=Open+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

In [Greg's original post](http://software-carpentry.org/blog/2014/10/why-we-dont-teach-testing.html) he specifically references the [Python 3 version](http://benchmarksgame.alioth.debian.org/u32/program.php?test=nbody&lang=python3&id=1) of the [n-body benchmark](http://benchmarksgame.alioth.debian.org/u32/performance.php?test=nbody). In particular, he asks how to test the `advance` function.

There's a number of useful and important comments underneath the original post, in particular [this thread on details of the algorithm and the problems with the code](http://software-carpentry.org/blog/2014/10/why-we-dont-teach-testing.html#comment-1662471640), but here we'll start from scratch.

Let's reproduce that code here (please note the [OSI BSD license](http://benchmarksgame.alioth.debian.org/license.html)).

```python
# The Computer Language Benchmarks Game
# http://benchmarksgame.alioth.debian.org/
#
# originally by Kevin Carson
# modified by Tupteq, Fredrik Johansson, and Daniel Nanz
# modified by Maciej Fijalkowski
# 2to3

import sys

def combinations(l):
    result = []
    for x in range(len(l) - 1):
        ls = l[x+1:]
        for y in ls:
            result.append((l[x],y))
    return result

PI = 3.14159265358979323
SOLAR_MASS = 4 * PI * PI
DAYS_PER_YEAR = 365.24

BODIES = {
    'sun': ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], SOLAR_MASS),

    'jupiter': ([4.84143144246472090e+00,
                 -1.16032004402742839e+00,
                 -1.03622044471123109e-01],
                [1.66007664274403694e-03 * DAYS_PER_YEAR,
                 7.69901118419740425e-03 * DAYS_PER_YEAR,
                 -6.90460016972063023e-05 * DAYS_PER_YEAR],
                9.54791938424326609e-04 * SOLAR_MASS),

    'saturn': ([8.34336671824457987e+00,
                4.12479856412430479e+00,
                -4.03523417114321381e-01],
               [-2.76742510726862411e-03 * DAYS_PER_YEAR,
                4.99852801234917238e-03 * DAYS_PER_YEAR,
                2.30417297573763929e-05 * DAYS_PER_YEAR],
               2.85885980666130812e-04 * SOLAR_MASS),

    'uranus': ([1.28943695621391310e+01,
                -1.51111514016986312e+01,
                -2.23307578892655734e-01],
               [2.96460137564761618e-03 * DAYS_PER_YEAR,
                2.37847173959480950e-03 * DAYS_PER_YEAR,
                -2.96589568540237556e-05 * DAYS_PER_YEAR],
               4.36624404335156298e-05 * SOLAR_MASS),

    'neptune': ([1.53796971148509165e+01,
                 -2.59193146099879641e+01,
                 1.79258772950371181e-01],
                [2.68067772490389322e-03 * DAYS_PER_YEAR,
                 1.62824170038242295e-03 * DAYS_PER_YEAR,
                 -9.51592254519715870e-05 * DAYS_PER_YEAR],
                5.15138902046611451e-05 * SOLAR_MASS) }

SYSTEM = list(BODIES.values())
PAIRS = combinations(SYSTEM)

def advance(dt, n, bodies=SYSTEM, pairs=PAIRS):

    for i in range(n):
        for (([x1, y1, z1], v1, m1),
             ([x2, y2, z2], v2, m2)) in pairs:
            dx = x1 - x2
            dy = y1 - y2
            dz = z1 - z2
            mag = dt * ((dx * dx + dy * dy + dz * dz) ** (-1.5))
            b1m = m1 * mag
            b2m = m2 * mag
            v1[0] -= dx * b2m
            v1[1] -= dy * b2m
            v1[2] -= dz * b2m
            v2[0] += dx * b1m
            v2[1] += dy * b1m
            v2[2] += dz * b1m
        for (r, [vx, vy, vz], m) in bodies:
            r[0] += dt * vx
            r[1] += dt * vy
            r[2] += dt * vz
```

In previous notebooks we compared the results of the algorithm with the expected behaviour within the context of a *model*. The model only considered the behaviour of the numerical algorithm: we took it for granted that the correct physics had been implemented. In this case, the `advance` function is doing two things, neither of which are immediately obvious:

1. it evaluates the forces due to gravitational interaction between all of the particles, and
2. it evolves the system of particles forwards in time.

In testing whether the results are *close enough*, we need to test within the context of *two* models: we need to check that the correct physics is implemented, and we need to check that the numerical evolution behaves as expected.

## The models

The physics that we're modelling is the [classical n body problem](http://en.wikipedia.org/wiki/N-body_problem) of massive particles interacting only through gravity. The [general formulation](http://en.wikipedia.org/wiki/N-body_problem#General_formulation) gives the equations of motion

$$
\begin{equation}
  m_i \frac{d^2 \vec{r}_i}{d t^2} = \sum_{j \ne i} \frac{ G m_i m_j (\vec{r}_j - \vec{r}_i) }{|\vec{r}_j - \vec{r}_i|^3}
\end{equation}
$$

where $m_i, \vec{r}_i$ are the masses and positions of the $i^{\text{th}}$ particle. This model is Newton's laws (which are equivalent to the conservation of energy and momentum) plus Newton's law of gravity.

The numerical method we need to model uses the [*semi-implicit Euler method*](http://en.wikipedia.org/wiki/Semi-implicit_Euler_method)1 to update in time, and a [direct particle-particle method](http://en.wikipedia.org/wiki/N-body_problem#Few_bodies) to compute the spatial interactions. The semi-implicit Euler method is first order accurate (with respect to the timestep $\Delta t$), and the particle-particle interactions are exact.

That is, we can think of the algorithm as solving the ODE

$$
\begin{equation}
  \frac{d y}{dt} = F(t, y)
\end{equation}
$$

where the vector $y$ contains the positions $\vec{r}$ and velocities $\vec{v}$ of each body. Due to the physics of the problem (ie, there's a total energy that we want to conserve) there's an advantage in considering the positions and velocities separately, but when it comes to measure the error we don't need to do that: we just treat the `advance` function as an algorithm that takes the full vector $y_n$ as input and returns $y_{n+1}$.

What this means in practical terms is that we expect the error in time to have similar convergence rate behaviour to the standard Euler method discussed before: the convergence rate should be $1$, which can be checked both by the self-convergence being within $0.585$ and $1.585$, and also that the error in the convergence rate goes down by at least $1/3$ as the base resolution of $\Delta t$ is reduced by $2$. Checking the local truncation error would require more detailed analysis, but in principle should follow the same steps as before.

What's more interesting here is the additional checks from the complexities of the particle-particle interaction. We can use our previous techniques to check that the time update algorithm is behaving as expected. However, the particle-particle interaction itself contains many steps, and those also need testing.

Let's check the output on (a copy of) the default input, taking a *single* step.

```python
import copy
bodies = copy.deepcopy(SYSTEM)
advance(dt=0.01, n=1, bodies=bodies, pairs=combinations(bodies))
print(bodies)
print(SYSTEM)
```

    [([12.905190971986693, -15.102456648865521, -0.22341579268383283], [1.0821409847561863, 0.8694752833109706, -0.010821379117708814], 0.0017237240570597112), ([8.333218184594687, 4.1430349688596575, -0.40343728476075313], [-1.0148533649892928, 1.8236404735352374, 0.00861323535682711], 0.011286326131968767), ([4.847339930856543, -1.1321630550460413, -0.10387091638393478], [0.5908488391821772, 2.8156988981387063, -0.02488719128116714], 0.03769367487038949), ([15.389485801827046, -25.913363875177772, 0.17891118741867673], [0.9788686976130451, 0.5950734810190818, -0.034758553169444574], 0.0020336868699246304), ([1.5983797301314366e-07, -3.0187796864956455e-08, -3.730115986360403e-09], [1.5983797301314366e-05, -3.0187796864956456e-06, -3.7301159863604027e-07], 39.47841760435743)]
    [([12.894369562139131, -15.111151401698631, -0.22330757889265573], [1.0827910064415354, 0.8687130181696082, -0.010832637401363636], 0.0017237240570597112), ([8.34336671824458, 4.124798564124305, -0.4035234171143214], [-1.0107743461787924, 1.8256623712304119, 0.008415761376584154], 0.011286326131968767), ([4.841431442464721, -1.1603200440274284, -0.10362204447112311], [0.606326392995832, 2.81198684491626, -0.02521836165988763], 0.03769367487038949), ([15.379697114850917, -25.919314609987964, 0.17925877295037118], [0.979090732243898, 0.5946989986476762, -0.034755955504078104], 0.0020336868699246304), ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 39.47841760435743)]

Comparing these two is going to be a real pain. So, let's create a utility function that will give the difference between two configurations of bodies. It will return the differences in the locations and the velocities as arrays, but will also include the "total" error as a single number - the sum of the absolute values of all errors. If this norm is zero then the two configurations are identical.

```python
def differences(bodies1, bodies2):
    """
    Compare two configurations.
    """
    assert len(bodies1) == len(bodies2), "Configurations must have same number of bodies! {}, {}".format(len(bodies1), len(bodies2))
    N = len(bodies1)
    d_positions = numpy.zeros((N, 3))
    d_velocities = numpy.zeros((N, 3))
    norm_difference = 0.0
    for n in range(N):
        d_positions[n, :] = numpy.array(bodies1[n][0]) - numpy.array(bodies2[n][0])
        d_velocities[n, :] = numpy.array(bodies1[n][1]) - numpy.array(bodies2[n][1])
        norm_difference += numpy.sum(numpy.abs(d_positions[n, :])) + numpy.sum(numpy.abs(d_velocities[n, :]))
    return norm_difference, d_positions, d_velocities
```

```python
norm1, d_x, d_v = differences(bodies, SYSTEM)
```

```python
d_v
```

    array([[ -6.50021685e-04,   7.62265141e-04,   1.12582837e-05],
           [ -4.07901881e-03,  -2.02189770e-03,   1.97473980e-04],
           [ -1.54775538e-02,   3.71205322e-03,   3.31170379e-04],
           [ -2.22034631e-04,   3.74482371e-04,  -2.59766537e-06],
           [  1.59837973e-05,  -3.01877969e-06,  -3.73011599e-07]])

Now we have to think about how to test the particle-particle interaction. The parameter we can change in order to do the test is the configuration `bodies` (and the associated list of `pairs`, which we'll always take to be `combinations(bodies)`).

So we can either change the positions, or the velocities, of the bodies, or their masses (or some combination of the three). What changes make meaningful tests?

Well, the coordinates chosen should not matter, *if* we change them in a self-consistent way, *and* interpret the results appropriately. For example, if we *ran time backwards* by changing the direction of time (`dt` $\to$ `-dt`) and changing the sign of the velocities, then the results should be identical (after re-flipping the direction of time).

```python
def flip_time(bodies):
    """
    Flip the time by flipping the velocity.
    """

    for i in range(len(bodies)):
        for j in range(3):
            bodies[i][1][j] *= -1.0
```

```python
bodies_flip_time = copy.deepcopy(SYSTEM)
flip_time(bodies_flip_time)
advance(dt=-0.01, n=1, bodies=bodies_flip_time, pairs=combinations(bodies_flip_time))
flip_time(bodies_flip_time)
norm_flip_time, d_x_flip_time, d_v_flip_time = differences(bodies, bodies_flip_time)
print("Norm of differences is {}.".format(norm_flip_time))
```

    Norm of differences is 0.0.

As expected, the positions are identical. Note that the velocities are significantly different, because they still have the opposite sign, but if we flip the velocities back as well, then they will be identical.

Why did we do this? Because one of the first steps in debugging, and hence in testing, should be to [check the conservation laws](http://software-carpentry.org/v5/novice/python/05-defensive.html#know-what-its-supposed-to-do). This is just an example of a conservation law resulting from a symmetry: the consequence of [Emmy Noether's famous theorem](http://en.wikipedia.org/wiki/Noether's_theorem). There is a [detailed list][1] of symmetries that a physical theory could have, each of which has an associated conservation law. Not all physical models obey all symmetries, and not every numerical  simulation obeys all the symmetries of the physical model (maybe due to boundary conditions, or numerical approximations). However, in the n-body case we expect a large number of these symmetries to hold, which we can check.

These include:

* discrete parity symmetry (sending $\vec{r} \to -\vec{r}$)
* discrete time symmetry (sending $t \to -t$)
* discrete charge symmetry (not relevant here - no charges)
* continuous space translation ($\vec{r} \to \vec{r} + \vec{a}$)
* continuous space rotation
* continuous time translation (this is obeyed by the continuum model, but not by the numerical model)
* continuous conformal (like) symmetry ($t \to b t$, $\vec{r} \to b \vec{r}$, and $m \to b m$ all together).

[1]: http://en.wikipedia.org/wiki/Symmetry_(physics)#Conservation_laws_and_symmetry

In the first test where we "ran time in reverse" we were checking discrete time symmetry. We can now check the other symmetries systematically.

#### Discrete space symmetry

We should be able to flip individual coordinates: if $y \to -y$ and $v_y \to -v_y$, for example, then  the evolution of the other coordinates and velocities should be unaffected (and if this flip is reversed after evolution, the position should be identical).

```python
def flip_coordinate(bodies, coord):
    """
    Flip a single coordinate direction
    """

    for i in range(len(bodies)):
        bodies[i][0][coord] *= -1.0
        bodies[i][1][coord] *= -1.0
```

```python
for coord in range(3):
    bodies_flip_coord = copy.deepcopy(SYSTEM)
    flip_coordinate(bodies_flip_coord, coord)
    advance(dt=0.01, n=1, bodies=bodies_flip_coord, pairs=combinations(bodies_flip_coord))
    flip_coordinate(bodies_flip_coord, coord)
    norm_flip_coord, d_x_flip_coord, d_v_flip_coord = differences(bodies, bodies_flip_coord)
    print("Norm of differences is {} (flipped coordinate {}).".format(norm_flip_coord, coord))
```

    Norm of differences is 0.0 (flipped coordinate 0).
    Norm of differences is 0.0 (flipped coordinate 1).
    Norm of differences is 0.0 (flipped coordinate 2).

#### Continuous space translation

Next, note that only the *separation* between bodies is meant to matter. So if we arbitrarily translate all coordinates by some amount whilst leaving the velocities untouched, and reverse this after evolution, the result should be identical.

```python
def translate_coordinate(bodies, shift):
    """
    Translate the coordinates of all bodies
    """

    for i in range(len(bodies)):
        for n in range(3):
            bodies[i][0][n] += shift[n]
```

```python
shift = 10.0*(-0.5+numpy.random.rand(3)) # Random coordinate shift in [-5, 5]
bodies_shift = copy.deepcopy(SYSTEM)
translate_coordinate(bodies_shift, shift)
advance(dt=0.01, n=1, bodies=bodies_shift, pairs=combinations(bodies_shift))
translate_coordinate(bodies_shift, -shift)
norm_shift, d_x_shift, d_v_shift = differences(bodies, bodies_shift)
print("Norm of differences is {}.".format(norm_shift))
```

    Norm of differences is 3.090701803516006e-15.

In this case the repeated operations introduce some floating point round-off error. This should be related to the round off introduced by the shift, and bounded by the number of bodies ($5$) multiplied by the total round-off introduced:

```python
numpy.sum(numpy.abs(numpy.spacing(shift)))*5
```

    5.5511151231257827e-15

The precise differences are:

```python
print(d_x_shift)
print(d_v_shift)
```

    [[ -1.77635684e-15   0.00000000e+00  -1.11022302e-16]
     [  0.00000000e+00   0.00000000e+00  -1.11022302e-16]
     [  8.88178420e-16   0.00000000e+00   8.32667268e-17]
     [  0.00000000e+00   0.00000000e+00   0.00000000e+00]
     [ -6.62471880e-17  -4.71076778e-17   7.50034688e-18]]
    [[ 0.  0.  0.]
     [ 0.  0.  0.]
     [ 0.  0.  0.]
     [ 0.  0.  0.]
     [ 0.  0.  0.]]

We see that the some of the bodies have errors around the expected floating point limit, and these dominate the resulting error (which is well within the expected bound).

#### Continuous space rotation

Yet another coordinate transformation can be tried without affecting the results: a rotation. We'll pick three random Euler angles and [set up the rotation matrix](http://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions). In general, we should do a translation first to ensure no body is at the origin (here, the Sun will be), to avoid specializing the problem, but given the generality of the code that's a very minor worry.

```python
def rotate_bodies(bodies, angles, invert=False):
    """
    Rotate the coordinates of all bodies.
    """

    Rx = numpy.array([[1.0, 0.0, 0.0],
                      [0.0, numpy.cos(angles[0]), -numpy.sin(angles[0])],
                      [0.0, numpy.sin(angles[0]), numpy.cos(angles[0])]])
    Ry = numpy.array([[numpy.cos(angles[1]), 0.0, numpy.sin(angles[1])],
                      [0.0, 1.0, 0.0],
                      [-numpy.sin(angles[1]), 0.0, numpy.cos(angles[1])]])
    Rz = numpy.array([[numpy.cos(angles[2]), -numpy.sin(angles[2]), 0.0],
                      [numpy.sin(angles[2]), numpy.cos(angles[2]), 0.0],
                      [0.0, 0.0, 1.0]])
    if invert:
        R = numpy.dot(numpy.dot(Rx, Ry), Rz)
    else:
        R = numpy.dot(Rz, numpy.dot(Ry, Rx))
    for i in range(len(bodies)):
        x = numpy.array(bodies[i][0])
        v = numpy.array(bodies[i][1])
        xp = numpy.dot(R, x)
        vp = numpy.dot(R, v)
        for n in range(3):
            bodies[i][0][n] = xp[n]
            bodies[i][1][n] = vp[n]
```

```python
angles = numpy.pi/4.0*numpy.random.rand(3) # Random Euler angles in [0, pi/4]
bodies_rotate = copy.deepcopy(SYSTEM)
rotate_bodies(bodies_rotate, angles)
advance(dt=0.01, n=1, bodies=bodies_rotate, pairs=combinations(bodies_rotate))
rotate_bodies(bodies_rotate, -angles, invert=True)
norm_rotate, d_x_rotate, d_v_rotate = differences(bodies, bodies_rotate)
print("Norm of differences is {}.".format(norm_rotate))
```

    Norm of differences is 2.7932524155836443e-14.

The size of the differences is considerably larger, thanks to the number of operations performed. With three rotation matrices combined, each containing nine entries, applied to five bodies, we would expect a total error of the order of:

```python
3*9*5*numpy.sum(numpy.abs(numpy.spacing(angles)))
```

    4.496403249731884e-14

#### Continuous conformal (like) symmetry

We could also try scaling the coordinates and the masses. As the [force goes as](http://en.wikipedia.org/wiki/N-body_problem#General_formulation) $M^2/L^2$, if we scale time, position and mass by the same amount then the results should not change. Note that because we scale *both* length and time, the velocity does not change.

```python
def scale_bodies(bodies, scale):
    """
    Scale coordinates and masses.
    """

    bodies_scale = []
    for (x, v, m) in bodies:
        new_x = copy.deepcopy(x)
        new_v = copy.deepcopy(v)
        new_m = m * scale
        for i in range(3):
            new_x[i] *= scale
        bodies_scale.append((new_x, new_v, new_m))

    return bodies_scale
```

```python
scale = 2.0
bodies_scale = scale_bodies(SYSTEM, scale)
advance(dt=0.02, n=1, bodies=bodies_scale, pairs=combinations(bodies_scale))
bodies_rescale = scale_bodies(bodies_scale, 1.0/scale)
norm_scale, d_x_scale, d_v_scale = differences(bodies, bodies_rescale)
print("Norm of differences is {}.".format(norm_scale))
```

    Norm of differences is 0.0.

This scaling does not *prove* that the force goes as $M^2/L^2$; it only shows that the force contains $M$ and $L$ to the same power. To show that it's got the appropriate form we should either [compare to an oracle or test simpler cases](http://software-carpentry.org/v5/novice/python/05-defensive.html#know-what-its-supposed-to-do). The [Java n-body code](http://benchmarksgame.alioth.debian.org/u32/benchmark.php?test=nbody&lang=java) is (likely) the oracle it was tested against; testing against a simpler case will be done later.

## Convergence - continuous time translation

Above, we noted that continuous time translation (which corresponds to conservation of energy) may not be perfectly retained by our *numerical method*, even though it is perfectly preserved by the theory. However, as we're solving an ODE in time, we can use the techniques applied to the phugoid model to ensure the numerical method is converging at the expected rate. As we're expecting the method to converge at first order, we need the error in the measured self-convergence rate to reduce by at least $1/3$ when the resolution (in time) is reduced by $2$.

```python
T = 10.0 # The base resolution will take 1000 steps
dt_values = numpy.array([0.01*2**(-i) for i in range(4)])
bodies_list = []
for i, dt in enumerate(dt_values):
    bodies_loop = copy.deepcopy(SYSTEM)
    advance(dt=dt, n=int(T/dt), bodies=bodies_loop, pairs=combinations(bodies_loop))
    bodies_list.append(bodies_loop)

```

```python
convergence_rate = numpy.zeros((2,))
for i in range(len(convergence_rate)):
    numerator, diff_x, diff_v= differences(bodies_list[i], bodies_list[i+1])
    denominator, diff_x, diff_v = differences(bodies_list[i+1], bodies_list[i+2])
    convergence_rate[i] = numpy.log(numerator/denominator)/numpy.log(2.0)
    print("Convergence rate (base dt={}) is {} (error {}).".format(
            dt_values[i], convergence_rate[i], numpy.abs(convergence_rate[i]-1.0)))
print("Is the convergence rate close enough to 1 for the answers to match? {}".format(
        numpy.log(1.0+0.5)/numpy.log(2.0) < convergence_rate[1] < numpy.log(2.0**2-1.0)/numpy.log(2.0)))
print("Does the convergence of the convergence rate show it's close enough to 1? {}".format(
        numpy.abs(convergence_rate[1]-1.0) < 2.0/3.0*numpy.abs(convergence_rate[0]-1.0)))
```

    Convergence rate (base dt=0.01) is 0.9964571407422125 (error 0.003542859257787523).
    Convergence rate (base dt=0.005) is 0.9982227199049917 (error 0.0017772800950083267).
    Is the convergence rate close enough to 1 for the answers to match? True
    Does the convergence of the convergence rate show it's close enough to 1? True

This shows that the time evolution is close enought to first order as expected. We haven't explicitly shown that it's the semi-implicit Euler method, as explicitly calculating the local truncation error would be a lot of work, and in this case we're really not interested in the specific time integration scheme - just that it converges.

## Checking the specific force law

In order to check that we've really implemented Newton's law of gravity, we need to test a very simplified situation where we can predict the precise answer. Let's set up a two body problem starting from rest.

Once we've done that, we can vary one parameter and check that the acceleration behaves as expected. The two cases we have are varying a mass, so that if $m_1 \to b m_1$ then

$$
\begin{equation}
  \vec{a}_2(b) = b \vec{a_2}(1)
\end{equation}
$$

and varying separation $|\vec{r}_1 - \vec{r}_2|$ so that $|\vec{r}_1 - \vec{r}_2| \to c |\vec{r}_1 - \vec{r}_2|$

$$
\begin{equation}
  \vec{a}_2(c) =  c^{-2} \vec{a}(1).
\end{equation}
$$

Over a single timestep $\Delta t$ we can approximate the acceleration as

$$
\begin{equation}
  \vec{a}_2(t=0) \simeq \frac{\vec{v}_2(t=\Delta t) - \vec{v}_2(t=0)}{\Delta t}.
\end{equation}
$$

For *this specific algorithm* we have that

$$
\begin{equation}
   \vec{v}_2(t=\Delta t) = \vec{v}_2(t=0) + \Delta t \, \vec{a}_2(t=0),
\end{equation}
$$

which means that the approximation for the acceleration is, in fact, exact:

$$
\begin{equation}
  \vec{a}_2(t=0) = \frac{\vec{v}_2(t=\Delta t) - \vec{v}_2(t=0)}{\Delta t}.
\end{equation}
$$

For a general algorithm we would have a first order in $\Delta t$ error in our approximation to the acceleration, so would have to use Richardson extrapolation methods as before in order to calculate the acceleration, and hence check the force.

```python
body_1 = ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.1*SOLAR_MASS)
body_2 = ([-1.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.1*SOLAR_MASS)
body_1_2_separation = ([3.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.1*SOLAR_MASS)
body_1_2_mass = ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.2*SOLAR_MASS)
two_bodies = [copy.deepcopy(body_1), copy.deepcopy(body_2)]
two_bodies_2_separation = [copy.deepcopy(body_1_2_separation), copy.deepcopy(body_2)]
two_bodies_2_mass = [copy.deepcopy(body_1_2_mass), copy.deepcopy(body_2)]
advance(dt=0.01, n=1, bodies=two_bodies, pairs=combinations(two_bodies))
advance(dt=0.01, n=1, bodies=two_bodies_2_separation, pairs=combinations(two_bodies_2_separation))
advance(dt=0.01, n=1, bodies=two_bodies_2_mass, pairs=combinations(two_bodies_2_mass))
print("The acceleration (hence velocity) should decrease as separation^2")
print("So these two numbers should match: {} {} (difference: {})".format(
        two_bodies[1][1][0],
        two_bodies_2_separation[1][1][0]*4.0,
        numpy.abs(two_bodies[1][1][0]-two_bodies_2_separation[1][1][0]*4.0)))
print("The acceleration (hence velocity) should increase as mass^1")
print("So these two numbers should match: {} {} (difference: {})".format(
        two_bodies[1][1][0],
        two_bodies_2_mass[1][1][0]/2.0,
        numpy.abs(two_bodies[1][1][0]-two_bodies_2_mass[1][1][0]/2.0)))
```

    The acceleration (hence velocity) should decrease as separation^2
    So these two numbers should match: 0.009869604401089358 0.009869604401089358 (difference: 0.0)
    The acceleration (hence velocity) should increase as mass^1
    So these two numbers should match: 0.009869604401089358 0.009869604401089358 (difference: 0.0)

Of course, we could have just got (un)lucky in comparing single data points when we vary either $b$ or $c$. Instead we can choose a set of points at random, and fit to a more general model; for example, fit to

$$
\begin{equation}
  \vec{a}_2 (b) = \sum_{i=0}^4 p_i b^i
\end{equation}
$$

or

$$
\begin{equation}
  \vec{a}_2 (c) = \sum_{i=0}^4 p_i c^{-i}:
\end{equation}
$$

```python
bodies_list_separation = []
separation_scale = numpy.random.rand(5)
separations = separation_scale + 1.0
separation_v = numpy.zeros_like(separations)
for i, scale in enumerate(separation_scale):
    body_1_separation = ([scale, 0.0, 0.0], [0.0, 0.0, 0.0], 0.1*SOLAR_MASS)
    two_bodies_separation = [copy.deepcopy(body_1_separation), copy.deepcopy(body_2)]
    advance(dt=0.01, n=1, bodies=two_bodies_separation, pairs=combinations(two_bodies_separation))
    bodies_list_separation.append(two_bodies_separation)
    separation_v[i] = two_bodies_separation[1][1][0]

bodies_list_mass = []
mass_scale = numpy.random.rand(5)
masses = 0.1 * mass_scale
mass_v = numpy.zeros_like(masses)
for i, scale in enumerate(mass_scale):
    body_1_mass = ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.1*scale*SOLAR_MASS)
    two_bodies_mass = [copy.deepcopy(body_1_mass), copy.deepcopy(body_2)]
    advance(dt=0.01, n=1, bodies=two_bodies_mass, pairs=combinations(two_bodies_mass))
    bodies_list_mass.append(two_bodies_mass)
    mass_v[i] = two_bodies_mass[1][1][0]
```

```python
p_separation = numpy.polyfit(1./separations, separation_v, len(separations)-1)
p_mass = numpy.polyfit(masses, mass_v, len(masses)-1)
print("We expect the third-to-last (separation^{-2}) coefficient to dominate:")
print("Coefficients from separation: {}".format(p_separation))
print("Coefficient of separation^{{-2}}: {:.5g}".format(p_separation[-3]))
print("Largest other coefficient: {:.5g}".format(numpy.max(numpy.abs(numpy.delete(p_separation,-3)))))
print("We expect the second-to-last (mass^{1}) coefficient to dominate:")
print("Coefficients from mass: {}".format(p_mass))
print("Coefficient of mass^{{1}}: {:.5g}".format(p_mass[-2]))
print("Largest other coefficient: {:.5g}".format(numpy.max(numpy.abs(numpy.delete(p_mass,-2)))))
```

    We expect the third-to-last (separation^{-2}) coefficient to dominate:
    Coefficients from separation: [  2.80087953e-13  -8.47221417e-13   3.94784176e-02  -4.76829993e-13
       8.87847308e-14]
    Coefficient of separation^{-2}: 0.039478
    Largest other coefficient: 8.4722e-13
    We expect the second-to-last (mass^{1}) coefficient to dominate:
    Coefficients from mass: [  1.30669862e-11  -2.85143237e-12   2.14919187e-13   9.86960440e-02
       6.96273251e-17]
    Coefficient of mass^{1}: 0.098696
    Largest other coefficient: 1.3067e-11

We we see that the expected coefficient dominates, but we're not getting a fit that's absolutely perfect. This is likely due to the limitations of the fitting algorithm.

So, we have an algorithm that

* obeys all the expected symmetries;
* depends on the size of the separation and the mass only;
* scales as expected with mass and separation.

This strongly suggests that it implements Newton's laws, and the force term that's implemented has the form

$$
  \vec{F} = G \frac{m_1 m_2 (\vec{r}_2 - \vec{r}_1)}{|\vec{r}_2 - \vec{r}_1|^3}.
$$

The only remaining question: is the value of $G$ correct?

Our previous calculations give us the value of $G$ internal to the algorithm; we just take our value for the acceleration $\vec{a}_2 = \vec{F}/m_2$ and take out the masses and separations:

```python
dt = 0.01
separation = 2.0
mass = 0.1
G = two_bodies[1][1][0]/mass*separation**2/dt
print("Algorithm G, in units of AU^3 SolarMass^{{-1}} years^{{-2}}: {}".format(G))
```

    Algorithm G, in units of AU^3 SolarMass^{-1} years^{-2}: 39.47841760435743

```python
from scipy import constants
print("Accepted value of G, in units of {}: {}".format(
        constants.unit('Newtonian constant of gravitation'),
        constants.G))
```

    Accepted value of G, in units of m^3 kg^-1 s^-2: 6.67384e-11

```python
solar_mass_in_kg = 1.9891e30
year_in_seconds = 3.15569e7
AU_in_metres = 149597870700.0
print("Algorithm G, in units of m^3 kg^-1 s^-2: {}".format(
        G*(AU_in_metres**3/(solar_mass_in_kg)/year_in_seconds**2)))
```

    Algorithm G, in units of m^3 kg^-1 s^-2: 6.67253235346714e-11

So the constants agree to four significant figures; without more in-depth knowledge of the original implementation, this might just reflect experimental uncertainties in the knowledge of $G$ at the time. Given the [experimental uncertainty](http://en.wikipedia.org/wiki/Gravitational_constant#Laws_and_constants) in the value of the gravitational constant, I would say this check is close enough.

##### Footnotes

The method that got me to this conclusion is a classic *just Google it* style, as follows. Looking at the original n-body example there's no useful documentation, but the [about section](http://benchmarksgame.alioth.debian.org/u32/performance.php?test=nbody#about) says that is uses a simple [symplectic integrator](http://en.wikipedia.org/wiki/Symplectic_integrator). The [first order example](http://en.wikipedia.org/wiki/Symplectic_integrator#A_first-order_example) is the [semi-implicit Euler method](http://en.wikipedia.org/wiki/Semi-implicit_Euler_method), which appears to match the code.  ↩


# Aim of this notebook

* To construct the singular curve of universal type to finalize the solution of the optimal control problem

# Preamble

```python
from sympy import *
init_printing(use_latex='mathjax')

# Plotting
%matplotlib inline
## Make inline plots raster graphics
from IPython.display import set_matplotlib_formats
## Import modules for plotting and data analysis
import matplotlib.pyplot as plt
from matplotlib import gridspec,rc,colors
import matplotlib.ticker as plticker
## Parameters for seaborn plots
import seaborn as sns
sns.set(style='white',font_scale=1.25,
        rc={"xtick.major.size": 6, "ytick.major.size": 6,
            'text.usetex': False, 'font.family': 'serif', 'font.serif': ['Times']})

import pandas as pd
pd.set_option('mode.chained_assignment',None)

import numpy as np
from scipy.optimize import fsolve, root
from scipy.integrate import ode
backend = 'dopri5'
import warnings

# Timer
import time

from copy import deepcopy

from itertools import cycle
palette_size = 10;
clrs = sns.color_palette("Reds",palette_size)
iclrs = cycle(clrs) # iterated colors

# Suppress warnings
import warnings
warnings.filterwarnings("ignore")
```

# Parameter values

* Birth rate and const of downregulation are defined below in order to fit some experim. data

```python
d = .13 # death rate
c = .04 # cost of resistance
α = .3 # low equilibrium point at expression of the main pathway (high equilibrium is at one)
θ = .45 # threshold value for the expression of the main pathway
κ = 40 # robustness parameter
L = .2 # parameter used to model the effect of treatment (see the line below)
```

* Symbolic variables - the list insludes μ & μbar, because they will be varied later

```python
σ, φ0, φ, x, μ, μbar = symbols('sigma, phi0, phi, x, mu, mubar')
```

* Main functions

```python
A = 1-σ*(1-θ)*(1-L)
Θ = θ+σ*(1-θ)*L
Eminus = (α*A-Θ)**2/2
ΔE = A*(1-α)*((1+α)*A/2-Θ)
ΔEf = lambdify(σ,ΔE)
```

* Birth rate and cost of downregulation

```python
b = (0.1*(exp(κ*(ΔEf(1)))+1)-0.14*(exp(κ*ΔEf(0))+1))/(exp(κ*ΔEf(1))-exp(κ*ΔEf(0))) # birth rate
χ = 1-(0.14*(exp(κ*ΔEf(0))+1)-b*exp(κ*ΔEf(0)))/b
b, χ
```

$$\left ( 0.140154784911281, \quad 0.299759386875464\right )$$

* Hamiltonian *H* and a part of it ρ that includes the control variable σ

```python
h = b*(χ/(exp(κ*ΔE)+1)*(1-x)+c*x)
H = -φ0 + φ*(b*(χ/(exp(κ*ΔE)+1)-c)*x*(1-x)+μ*(1-x)/(exp(κ*ΔE)+1)-μbar*exp(-κ*Eminus)*x) + h
ρ = (φ*(b*χ*x+μ)+b*χ)/(exp(κ*ΔE)+1)*(1-x)-φ*μbar*exp(-κ*Eminus)*x
H, ρ
```

$$\left ( \phi \left(\frac{\mu \left(- x + 1\right)}{e^{40 \left(- 0.396 \sigma + 0.2\right) \left(- 0.308 \sigma + 0.7\right)} + 1} - \bar{\mu} x e^{- 20 \left(- 0.242 \sigma - 0.15\right)^{2}} + x \left(-0.00560619139645122 + \frac{0.042012712392668}{e^{40 \left(- 0.396 \sigma + 0.2\right) \left(- 0.308 \sigma + 0.7\right)} + 1}\right) \left(- x + 1\right)\right) - \phi_{0} + 0.00560619139645122 x + \frac{0.042012712392668 \left(- x + 1\right)}{e^{40 \left(- 0.396 \sigma + 0.2\right) \left(- 0.308 \sigma + 0.7\right)} + 1}, \quad - \bar{\mu} \phi x e^{- 20 \left(- 0.242 \sigma - 0.15\right)^{2}} + \frac{\left(- x + 1\right) \left(\phi \left(\mu + 0.042012712392668 x\right) + 0.042012712392668\right)}{e^{40 \left(- 0.396 \sigma + 0.2\right) \left(- 0.308 \sigma + 0.7\right)} + 1}\right )$$

* Same but for no treatment (σ = 0)

```python
h0 = h.subs(σ,0)
H0 = H.subs(σ,0)
ρ0 = ρ.subs(σ,0)
H0, ρ0
```

$$\left ( \phi \left(0.00368423989943599 \mu \left(- x + 1\right) - 0.637628151621773 \bar{\mu} x - 0.00545140648517063 x \left(- x + 1\right)\right) - \phi_{0} + 0.00545140648517063 x + 0.000154784911280596, \quad - 0.637628151621773 \bar{\mu} \phi x + 0.00368423989943599 \left(- x + 1\right) \left(\phi \left(\mu + 0.042012712392668 x\right) + 0.042012712392668\right)\right )$$

* Machinery: definition of the Poisson brackets

```python
PoissonBrackets = lambda H1, H2: diff(H1,x)*diff(H2,φ)-diff(H1,φ)*diff(H2,x)
```

* Necessary functions and defining the right hand side of dynamical equations

```python
ρf = lambdify((x,φ,σ,μ,μbar),ρ)
ρ0f = lambdify((x,φ,μ,μbar),ρ0)
dxdτ = lambdify((x,φ,σ,μ,μbar),-diff(H,φ))
dφdτ = lambdify((x,φ,σ,μ,μbar),diff(H,x))
dVdτ = lambdify((x,σ),h)
dρdσ = lambdify((σ,x,φ,μ,μbar),diff(ρ,σ))
dδρdτ = lambdify((x,φ,σ,μ,μbar),-PoissonBrackets(ρ0-ρ,H))
def ode_rhs(t,state,μ,μbar):
    x, φ, V, δρ = state
    σs = [0,1]
    if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
        σstar = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
    else:
        σstar = 1.;
    if ρf(x,φ,σstar,μ,μbar) < ρ0f(x,φ,μ,μbar):
        sgm = 0
    else:
        sgm = σstar
    return [dxdτ(x,φ,sgm,μ,μbar),dφdτ(x,φ,sgm,μ,μbar),dVdτ(x,sgm),dδρdτ(x,φ,σstar,μ,μbar)]
def σstarf(x,φ,μ,μbar):
    if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
        σstar = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
    else:
        σstar = 1.;
    if ρf(x,φ,σstar,μ,μbar) < ρ0f(x,φ,μ,μbar):
        sgm = 0
    else:
        sgm = σstar
    return sgm
```

```python
def get_primary_field(name, experiment,μ,μbar):
    solutions = {}
    solver = ode(ode_rhs).set_integrator(backend)
    τ0 = experiment['τ0']
    tms = np.linspace(τ0,experiment['T_end'],1e3+1)
    for x0 in experiment['x0']:
        δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
        solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
        sol = []; k = 0;
        while (solver.t < experiment['T_end']) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
            solver.integrate(tms[k])
            sol.append([solver.t]+list(solver.y))
            k += 1
        solutions[x0] = {'solution': sol}
    for x0, entry in solutions.items():
        entry['τ'] = [entry['solution'][j][0] for j in range(len(entry['solution']))]
        entry['x'] = [entry['solution'][j][1] for j in range(len(entry['solution']))]
        entry['φ'] = [entry['solution'][j][2] for j in range(len(entry['solution']))]
        entry['V'] = [entry['solution'][j][3] for j in range(len(entry['solution']))]
        entry['δρ'] = [entry['solution'][j][4] for j in range(len(entry['solution']))]
    return solutions
def get_δρ_value(tme,x0,μ,μbar):
    solver = ode(ode_rhs).set_integrator(backend)
    δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
    solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
    while (solver.t < tme) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tme)
        sol = [solver.t]+list(solver.y)
    return solver.y[3]
def get_δρ_ending(params,μ,μbar):
    tme, x0 = params
    solver = ode(ode_rhs).set_integrator(backend)
    δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
    solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
    δτ = 1.0e-8; tms = [tme,tme+δτ]
    _k = 0; sol = []
    while (_k<len(tms)):# and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append(solver.y)
        _k += 1
    #print(sol)
    return(sol[0][3],(sol[1][3]-sol[0][3])/δτ)
def get_state(tme,x0,μ,μbar):
    solver = ode(ode_rhs).set_integrator(backend)
    δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
    solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
    δτ = 1.0e-8; tms = [tme,tme+δτ]
    _k = 0; sol = []
    while (solver.t < tms[-1]) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append(solver.y)
        _k += 1
    return(list(sol[0])+[(sol[1][3]-sol[0][3])/δτ])
```

# Machinery for the universal line

* To find the universal singular curve we need to define two parameters

```python
γ0 = PoissonBrackets(PoissonBrackets(H,H0),H)
γ1 = PoissonBrackets(PoissonBrackets(H0,H),H0)
```

* The dynamics

```python
dxdτSingExpr = -(γ0*diff(H0,φ)+γ1*diff(H,φ))/(γ0+γ1)
dφdτSingExpr = (γ0*diff(H0,x)+γ1*diff(H,x))/(γ0+γ1)
dVdτSingExpr = (γ0*h0+γ1*h)/(γ0+γ1)
σSingExpr = γ1*σ/(γ0+γ1)
```

* Machinery for Python: lambdify the functions above

```python
dxdτSing = lambdify((x,φ,σ,μ,μbar),dxdτSingExpr)
dφdτSing = lambdify((x,φ,σ,μ,μbar),dφdτSingExpr)
dVdτSing = lambdify((x,φ,σ,μ,μbar),dVdτSingExpr)
σSing = lambdify((x,φ,σ,μ,μbar),σSingExpr)
```

```python
def ode_rhs_Sing(t,state,μ,μbar):
    x, φ, V = state
    if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
        σstar = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
    else:
        σstar = 1.;
    #print([σstar,σSing(x,φ,σstar,μ,μbar)])
    return [dxdτSing(x,φ,σstar,μ,μbar),dφdτSing(x,φ,σstar,μ,μbar),dVdτSing(x,φ,σstar,μ,μbar)]
# def ode_rhs_Sing(t,state,μ,μbar):
#     x, φ, V = state
#     if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
#         σstar = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
#     else:
#         σstar = 1.;
#     σTrav = fsolve(lambda σ: dxdτ(x,φ,σ,μ,μbar)-dxdτSing(x,φ,σstar,μ,μbar),.6)[0]
#     print([σstar,σTrav])
#     return [dxdτSing(x,φ,σstar,μ,μbar),dφdτSing(x,φ,σstar,μ,μbar),dVdτ(x,σTrav)]
def get_universal_curve(end_point,tmax,Nsteps,μ,μbar):
    tms =  np.linspace(end_point[0],tmax,Nsteps);
    solver = ode(ode_rhs_Sing).set_integrator(backend)
    solver.set_initial_value(end_point[1:4],end_point[0]).set_f_params(μ,μbar)
    _k = 0; sol = []
    while (solver.t < tms[-1]):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    return sol
def get_σ_universal(tme,end_point,μ,μbar):
    δτ = 1.0e-8; tms = [tme,tme+δτ]
    solver = ode(ode_rhs_Sing).set_integrator(backend)
    solver.set_initial_value(end_point[1:4],end_point[0]).set_f_params(μ,μbar)
    _k = 0; sol = []
    while (solver.t < tme+δτ):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    x, φ = sol[0][:2]
    sgm = fsolve(lambda σ: dxdτ(x,φ,σ,μ,μbar)-(sol[1][0]-sol[0][0])/δτ,θ/2)[0]
    return sgm
def get_state_universal(tme,end_point,μ,μbar):
    solver = ode(ode_rhs_Sing).set_integrator(backend)
    solver.set_initial_value(end_point[1:4],end_point[0]).set_f_params(μ,μbar)
    solver.integrate(tme)
    return [solver.t]+list(solver.y)
```

```python
def ode_rhs_with_σstar(t,state,μ,μbar):
    x, φ, V = state
    if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
        σ = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
    else:
        σ = 1.;
    return [dxdτ(x,φ,σ,μ,μbar),dφdτ(x,φ,σ,μ,μbar),dVdτ(x,σ)]
def ode_rhs_with_given_σ(t,state,σ,μ,μbar):
    x, φ, V = state
    return [dxdτ(x,φ,σ,μ,μbar),dφdτ(x,φ,σ,μ,μbar),dVdτ(x,σ)]
def get_trajectory_with_σstar(starting_point,tmax,Nsteps,μ,μbar):
    tms = np.linspace(starting_point[0],tmax,Nsteps)
    solver = ode(ode_rhs_with_σstar).set_integrator(backend)
    solver.set_initial_value(starting_point[1:],starting_point[0]).set_f_params(μ,μbar)
    sol = []; _k = 0;
    while solver.t < max(tms) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    return sol
def get_trajectory_with_given_σ(starting_point,tmax,Nsteps,σ,μ,μbar):
    tms = np.linspace(starting_point[0],tmax,100)
    solver = ode(ode_rhs_with_given_σ).set_integrator(backend)
    solver.set_initial_value(starting_point[1:],starting_point[0]).set_f_params(σ,μ,μbar)
    sol = []; _k = 0;
    while solver.t < max(tms) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    return sol
def get_state_with_σstar(tme,starting_point,μ,μbar):
    solver = ode(ode_rhs_with_σstar).set_integrator(backend)
    solver.set_initial_value(starting_point[1:4],starting_point[0]).set_f_params(μ,μbar)
    solver.integrate(tme)
    return [solver.t]+list(solver.y)
def get_finalizing_point_from_universal_curve(tme,tmx,end_point,μ,μbar):
    unv_point = get_state_universal(tme,end_point,μ,μbar)
    return get_state_with_σstar(tmx,unv_point,μ,μbar)[1]
```

# Field of optimal trajectories as the solution of the Bellman equation

* μ & μbar are varied by *T* and *T*bar ($\mu=1/T$ and $\bar\mu=1/\bar{T}$)

```python
tmx = 720.
end_switching_curve = {'t': 24., 'x': .9/.8}
# for Τ, Τbar in zip([28]*5,[14,21,28,35,60]):
for Τ, Τbar in zip([28],[60]):
    μ = 1./Τ; μbar = 1./Τbar
    print("Parameters: μ = %.5f, μbar = %.5f"%(μ,μbar))
    end_switching_curve['t'], end_switching_curve['x'] = fsolve(get_δρ_ending,(end_switching_curve['t'],.8*end_switching_curve['x']),args=(μ,μbar),xtol=1.0e-12)
    end_point = [end_switching_curve['t']]+get_state(end_switching_curve['t'],end_switching_curve['x'],μ,μbar)
    print("Ending point for the switching line: τ = %.1f days, x = %.1f%%" % (end_point[0], end_point[1]*100))
    print("Checking the solution - should give zero values: ")
    print(get_δρ_ending([end_switching_curve['t'],end_switching_curve['x']],μ,μbar))
    print("* Constructing the primary field")
    experiments = {
        'sol1': { 'T_end': tmx, 'τ0': 0., 'x0': list(np.linspace(0,end_switching_curve['x']-(1e-3),10))+
                 list(np.linspace(end_switching_curve['x']+(1e-6),1.,10)) } }
    primary_field = []
    for name, values in experiments.items():
        primary_field.append(get_primary_field(name,values,μ,μbar))
    print("* Constructing the switching curve")
    switching_curve = []
    x0s = np.linspace(end_switching_curve['x'],1,21); _y = end_switching_curve['t']

    for x0 in x0s:
        tme = fsolve(get_δρ_value,_y,args=(x0,μ,μbar))[0]
        if (tme>0):
            switching_curve = switching_curve+[[tme,get_state(tme,x0,μ,μbar)[0]]]
            _y = tme
    print("* Constructing the universal curve")
    universal_curve = get_universal_curve(end_point,tmx,25,μ,μbar)
    print("* Finding the last characteristic")
    #time0 = time.time()
    tuniv = fsolve(get_finalizing_point_from_universal_curve,tmx-40.,args=(tmx,end_point,μ,μbar,))[0]
    #print("The proccess to find the last characteristic took %0.1f minutes" % ((time.time()-time0)/60.))
    univ_point = get_state_universal(tuniv,end_point,μ,μbar)
    print("The last point on the universal line:")
    print(univ_point)
    last_trajectory = get_trajectory_with_σstar(univ_point,tmx,50,μ,μbar)
    print("Final state:")
    final_state = get_state_with_σstar(tmx,univ_point,μ,μbar)
    print(final_state)
    print("Fold-change in tumor size: %.2f"%(exp((b-d)*tmx-final_state[-1])))
    # Plotting
    plt.rcParams['figure.figsize'] = (6.75, 4)
    _k = 0
    for solutions in primary_field:
        for x0, entry in solutions.items():
            plt.plot(entry['τ'], entry['x'], 'k-', linewidth=.9, color=clrs[_k%palette_size])
            _k += 1
    plt.plot([x[0] for x in switching_curve],[x[1] for x in switching_curve],linewidth=2,color="red")
    plt.plot([end_point[0]],[end_point[1]],marker='o',color="red")
    plt.plot([x[0] for x in universal_curve],[x[1] for x in universal_curve],linewidth=2,color="red")
    plt.plot([x[0] for x in last_trajectory],[x[1] for x in last_trajectory],linewidth=.9,color="black")
    plt.xlim([0,tmx]); plt.ylim([0,1]);
    plt.xlabel("time, days"); plt.ylabel("fraction of resistant cells")
    plt.show()
    print()
```

```python
import csv
from numpy.linalg import norm
File = open("../figures/draft/sensitivity_mu-example.csv", 'w')
File.write("T,Tbar,mu,mubar,sw_end_t,sw_end_x,univ_point_t,univ_point_x,outcome,err_sw_t,err_sw_x\n")
writer = csv.writer(File,lineterminator='\n')

tmx = 720.
end_switching_curve0 = {'t': 23.36, 'x': .9592}
end_switching_curve_prev_t = end_switching_curve['t']
tuniv = tmx-30.
Τbars = np.arange(120,110,-2) #need to change here if more
for Τ in Τbars:
    μ = 1./Τ
    end_switching_curve = deepcopy(end_switching_curve0)
    for Τbar in Τbars:
        μbar = 1./Τbar
        print("* Parameters: T = %.1f, Tbar = %.1f (μ = %.5f, μbar = %.5f)"%(Τ,Τbar,μ,μbar))
        success = False; err = 1.
        while (not success)|(norm(err)>1e-6):
            end_switching_curve = {'t': 2*end_switching_curve['t']-end_switching_curve_prev_t-.001,
                                   'x': end_switching_curve['x']-0.002}
            sol = root(get_δρ_ending,(end_switching_curve['t'],end_switching_curve['x']),args=(μ,μbar))
            end_switching_curve_prev_t = end_switching_curve['t']
            end_switching_curve_prev_x = end_switching_curve['x']
            end_switching_curve['t'], end_switching_curve['x'] = sol.x
            success = sol.success
            err = get_δρ_ending([end_switching_curve['t'],end_switching_curve['x']],μ,μbar)
            if (not success):
                print("! Trying again...", sol.message)
            elif (norm(err)>1e-6):
                print("! Trying again... Convergence is not sufficient")
        else:
            end_point = [end_switching_curve['t']]+get_state(end_switching_curve['t'],end_switching_curve['x'],μ,μbar)
            print("Ending point: t = %.2f, x = %.2f%%"%(end_switching_curve['t'],100*end_switching_curve['x'])," Checking the solution:",err)
            universal_curve = get_universal_curve(end_point,tmx,25,μ,μbar)
            tuniv = root(get_finalizing_point_from_universal_curve,tuniv,args=(tmx,end_point,μ,μbar)).x
            err_tuniv = get_finalizing_point_from_universal_curve(tuniv,tmx,end_point,μ,μbar)
            univ_point = get_state_universal(tuniv,end_point,μ,μbar)
            print("tuniv = %.2f"%tuniv," Checking the solution: ",err_tuniv)
            final_state = get_state_with_σstar(tmx,univ_point,μ,μbar)
            outcome = exp((b-d)*tmx-final_state[-1])
            print("Fold-change in tumor size: %.2f"%(outcome))
            output = [Τ,Τbar,μ,μbar,end_switching_curve['t'],end_switching_curve['x']]+list(univ_point[0:2])+[outcome]+list(err)+[err_tuniv]
            writer.writerow(output)
        if (Τbar==Τ):
            end_switching_curve0 = deepcopy(end_switching_curve)

File.close()
```

    * Parameters: T = 120.0, Tbar = 120.0 (μ = 0.00833, μbar = 0.00833)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    Ending point: t = 43.94, x = 92.84%  Checking the solution: (-2.7006279317595854e-08, -1.1914446499779019e-08)
    tuniv = 656.53  Checking the solution:  -8.0798839841e-12
    Fold-change in tumor size: 1.11
    * Parameters: T = 120.0, Tbar = 118.0 (μ = 0.00833, μbar = 0.00847)
    Ending point: t = 43.89, x = 92.73%  Checking the solution: (-1.7344666138311858e-08, -4.8119089737485873e-08)
    tuniv = 656.97  Checking the solution:  3.00085650773e-12
    Fold-change in tumor size: 1.09
    * Parameters: T = 120.0, Tbar = 116.0 (μ = 0.00833, μbar = 0.00862)
    Ending point: t = 43.79, x = 92.62%  Checking the solution: (-1.2685041635657623e-08, 3.6918493488895216e-09)
    tuniv = 657.05  Checking the solution:  1.7980637812e-11
    Fold-change in tumor size: 1.06
    * Parameters: T = 120.0, Tbar = 114.0 (μ = 0.00833, μbar = 0.00877)
    Ending point: t = 43.72, x = 92.51%  Checking the solution: (-1.9878475069761174e-13, 1.1444109912769432e-14)
    tuniv = 657.36  Checking the solution:  1.11022302463e-16
    Fold-change in tumor size: 1.04
    * Parameters: T = 120.0, Tbar = 112.0 (μ = 0.00833, μbar = 0.00893)
    Ending point: t = 43.65, x = 92.39%  Checking the solution: (2.0457370335496411e-16, -1.4128422391608128e-17)
    tuniv = 657.67  Checking the solution:  1.11022302463e-16
    Fold-change in tumor size: 1.01
    * Parameters: T = 118.0, Tbar = 120.0 (μ = 0.00847, μbar = 0.00833)
    Ending point: t = 43.69, x = 92.87%  Checking the solution: (-2.3692779680055269e-08, -3.269167732111009e-08)
    tuniv = 656.90  Checking the solution:  -3.69231034636e-12
    Fold-change in tumor size: 1.13
    * Parameters: T = 118.0, Tbar = 118.0 (μ = 0.00847, μbar = 0.00847)
    Ending point: t = 43.58, x = 92.76%  Checking the solution: (-1.3395095041897369e-08, 4.4019561776721631e-08)
    tuniv = 656.86  Checking the solution:  2.84964760122e-11
    Fold-change in tumor size: 1.11
    * Parameters: T = 118.0, Tbar = 116.0 (μ = 0.00847, μbar = 0.00862)
    Ending point: t = 43.60, x = 92.65%  Checking the solution: (-1.1289966761475234e-08, -1.4969356321439766e-07)
    tuniv = 657.96  Checking the solution:  1.28284952106e-11
    Fold-change in tumor size: 1.08
    * Parameters: T = 118.0, Tbar = 114.0 (μ = 0.00847, μbar = 0.00877)
    Ending point: t = 43.46, x = 92.54%  Checking the solution: (9.6789370680576414e-14, 6.6004724002762103e-15)
    tuniv = 657.64  Checking the solution:  3.87082033093e-12
    Fold-change in tumor size: 1.06
    * Parameters: T = 118.0, Tbar = 112.0 (μ = 0.00847, μbar = 0.00893)
    Ending point: t = 43.39, x = 92.43%  Checking the solution: (1.804022460116881e-13, -1.5390442505598505e-13)
    tuniv = 657.94  Checking the solution:  -2.15487350186e-13
    Fold-change in tumor size: 1.03
    * Parameters: T = 116.0, Tbar = 120.0 (μ = 0.00862, μbar = 0.00833)
    Ending point: t = 43.42, x = 92.90%  Checking the solution: (-1.9451303168186168e-08, -3.6554528725415964e-08)
    tuniv = 657.20  Checking the solution:  -6.83112594269e-12
    Fold-change in tumor size: 1.16
    * Parameters: T = 116.0, Tbar = 118.0 (μ = 0.00862, μbar = 0.00847)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 43.41, x = 92.80%  Checking the solution: (-1.3661690581785762e-08, -1.4806224740659346e-07)
    tuniv = 657.95  Checking the solution:  1.63146579579e-12
    Fold-change in tumor size: 1.13
    * Parameters: T = 116.0, Tbar = 116.0 (μ = 0.00862, μbar = 0.00862)
    Ending point: t = 43.27, x = 92.69%  Checking the solution: (-1.6010962866663033e-13, -8.4632834931553141e-15)
    tuniv = 657.63  Checking the solution:  3.88245685601e-12
    Fold-change in tumor size: 1.11
    * Parameters: T = 116.0, Tbar = 114.0 (μ = 0.00862, μbar = 0.00877)
    Ending point: t = 43.19, x = 92.58%  Checking the solution: (1.3055056786388298e-11, 3.3093864565241406e-13)
    tuniv = 657.93  Checking the solution:  3.94630161882e-12
    Fold-change in tumor size: 1.08
    * Parameters: T = 116.0, Tbar = 112.0 (μ = 0.00862, μbar = 0.00893)
    Ending point: t = 43.12, x = 92.46%  Checking the solution: (1.6018858399577569e-14, 1.0324374869261036e-15)
    tuniv = 658.23  Checking the solution:  -1.99767979936e-12
    Fold-change in tumor size: 1.05
    * Parameters: T = 114.0, Tbar = 120.0 (μ = 0.00877, μbar = 0.00833)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 43.14, x = 92.94%  Checking the solution: (-1.5108305319068232e-08, -1.479771191064693e-09)
    tuniv = 657.34  Checking the solution:  -5.51789169911e-12
    Fold-change in tumor size: 1.18
    * Parameters: T = 114.0, Tbar = 118.0 (μ = 0.00877, μbar = 0.00847)
    Ending point: t = 43.12, x = 92.84%  Checking the solution: (-1.3127479394635852e-08, -1.0791913141667277e-07)
    tuniv = 658.08  Checking the solution:  4.07815448078e-12
    Fold-change in tumor size: 1.15
    * Parameters: T = 114.0, Tbar = 116.0 (μ = 0.00877, μbar = 0.00862)
    Ending point: t = 43.00, x = 92.73%  Checking the solution: (1.2633460282342689e-12, 1.7033445749256512e-12)
    tuniv = 657.92  Checking the solution:  1.09303399665e-11
    Fold-change in tumor size: 1.13
    * Parameters: T = 114.0, Tbar = 114.0 (μ = 0.00877, μbar = 0.00877)
    Ending point: t = 42.92, x = 92.62%  Checking the solution: (-7.4922629000407151e-14, -1.9960153685443375e-14)
    tuniv = 658.22  Checking the solution:  9.53604556431e-12
    Fold-change in tumor size: 1.10
    * Parameters: T = 114.0, Tbar = 112.0 (μ = 0.00877, μbar = 0.00893)
    Ending point: t = 42.85, x = 92.50%  Checking the solution: (-1.6939842744138033e-13, -2.3807277111347937e-13)
    tuniv = 658.52  Checking the solution:  1.40221584344e-11
    Fold-change in tumor size: 1.07
    * Parameters: T = 112.0, Tbar = 120.0 (μ = 0.00893, μbar = 0.00833)
    Ending point: t = 42.88, x = 92.98%  Checking the solution: (-1.1758343450175372e-08, -3.7262725858862016e-08)
    tuniv = 657.80  Checking the solution:  -5.73836117512e-12
    Fold-change in tumor size: 1.21
    * Parameters: T = 112.0, Tbar = 118.0 (μ = 0.00893, μbar = 0.00847)
    Ending point: t = 42.79, x = 92.87%  Checking the solution: (-7.889332548659039e-13, 1.7695212857518809e-14)
    tuniv = 657.92  Checking the solution:  3.99958885455e-11
    Fold-change in tumor size: 1.18
    * Parameters: T = 112.0, Tbar = 116.0 (μ = 0.00893, μbar = 0.00862)
    Ending point: t = 42.72, x = 92.77%  Checking the solution: (1.333639450230243e-13, 3.1306165904749315e-15)
    tuniv = 658.22  Checking the solution:  -2.09600670154e-11
    Fold-change in tumor size: 1.15
    * Parameters: T = 112.0, Tbar = 114.0 (μ = 0.00893, μbar = 0.00877)
    Ending point: t = 42.65, x = 92.65%  Checking the solution: (3.425363461026943e-14, 2.4115812349630145e-15)
    tuniv = 658.51  Checking the solution:  -2.86443577191e-11
    Fold-change in tumor size: 1.12
    * Parameters: T = 112.0, Tbar = 112.0 (μ = 0.00893, μbar = 0.00893)
    Ending point: t = 42.58, x = 92.54%  Checking the solution: (4.510545630780086e-14, 1.2877680961189885e-15)
    tuniv = 658.81  Checking the solution:  1.10841613665e-11
    Fold-change in tumor size: 1.10

* Here I investigate the dependence of $\mathrm{FoldChange}(T,\bar T)$. I fix $T$ at 15,30,45,60 days, and then I vary $\bar T$ between zero and $4T$. The example below is just a simulation for only one given value of $T$.

```python
import csv
from numpy.linalg import norm
File = open("../results/sensitivity1.csv", 'w')
File.write("T,Tbar,mu,mubar,sw_end_t,sw_end_x,univ_point_t,univ_point_x,outcome,err_sw_t,err_sw_x\n")
writer = csv.writer(File,lineterminator='\n')

tmx = 720.
end_switching_curve = {'t': 23.36, 'x': .9592}
end_switching_curve_prev_t = end_switching_curve['t']
tuniv = tmx-30.
Τ = 15
Τbars_step = .5; Tbars = np.arange(Τ*4,0,-Τbars_step)
for Τbar in Tbars:
    μ = 1./Τ; μbar = 1./Τbar
    print("* Parameters: T = %.1f, Tbar = %.1f (μ = %.5f, μbar = %.5f)"%(Τ,Τbar,μ,μbar))
    success = False; err = 1.
    while (not success)|(norm(err)>1e-6):
        end_switching_curve = {'t': 2*end_switching_curve['t']-end_switching_curve_prev_t-.001,
                               'x': end_switching_curve['x']-0.002}
        sol = root(get_δρ_ending,(end_switching_curve['t'],end_switching_curve['x']),args=(μ,μbar))
        end_switching_curve_prev_t = end_switching_curve['t']
        end_switching_curve_prev_x = end_switching_curve['x']
        end_switching_curve['t'], end_switching_curve['x'] = sol.x
        success = sol.success
        err = get_δρ_ending([end_switching_curve['t'],end_switching_curve['x']],μ,μbar)
        if (not success):
            print("! Trying again...", sol.message)
        elif (norm(err)>1e-6):
            print("! Trying again... Convergence is not sufficient")
    else:
        end_point = [end_switching_curve['t']]+get_state(end_switching_curve['t'],end_switching_curve['x'],μ,μbar)
        print("Ending point: t = %.2f, x = %.2f%%"%(end_switching_curve['t'],100*end_switching_curve['x'])," Checking the solution:",err)
        universal_curve = get_universal_curve(end_point,tmx,25,μ,μbar)
        tuniv = root(get_finalizing_point_from_universal_curve,tuniv,args=(tmx,end_point,μ,μbar)).x
        err_tuniv = get_finalizing_point_from_universal_curve(tuniv,tmx,end_point,μ,μbar)
        univ_point = get_state_universal(tuniv,end_point,μ,μbar)
        print("tuniv = %.2f"%tuniv," Checking the solution: ",err_tuniv)
        final_state = get_state_with_σstar(tmx,univ_point,μ,μbar)
        outcome = exp((b-d)*tmx-final_state[-1])
        print("Fold-change in tumor size: %.2f"%(outcome))
        output = [Τ,Τbar,μ,μbar,end_switching_curve['t'],end_switching_curve['x']]+list(univ_point[0:2])+[outcome]+list(err)+[err_tuniv]
        writer.writerow(output)

File.close()
```

* The results are aggregated in a file **sensitivity1_agg.csv**.

```python
df = pd.DataFrame.from_csv("../figures/draft/sensitivity1_agg.csv").reset_index().drop(['err_sw_t','err_sw_x','err_tuniv'],1)
df['Tratio'] = df['Tbar']/df['T']
df.head()
```

<div>
<style>
    .dataframe thead tr:only-child th {
        text-align: right;
    }

    .dataframe thead th {
        text-align: left;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>T</th>
      <th>Tbar</th>
      <th>mu</th>
      <th>mubar</th>
      <th>sw_end_t</th>
      <th>sw_end_x</th>
      <th>univ_point_t</th>
      <th>univ_point_x</th>
      <th>outcome</th>
      <th>Tratio</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>45</td>
      <td>180.0</td>
      <td>0.022222</td>
      <td>0.005556</td>
      <td>29.936471</td>
      <td>0.965414</td>
      <td>667.300425</td>
      <td>0.859250</td>
      <td>4.737060</td>
      <td>4.000000</td>
    </tr>
    <tr>
      <th>1</th>
      <td>45</td>
      <td>179.0</td>
      <td>0.022222</td>
      <td>0.005587</td>
      <td>29.925299</td>
      <td>0.965235</td>
      <td>667.383412</td>
      <td>0.858656</td>
      <td>4.718207</td>
      <td>3.977778</td>
    </tr>
    <tr>
      <th>2</th>
      <td>45</td>
      <td>178.0</td>
      <td>0.022222</td>
      <td>0.005618</td>
      <td>29.914016</td>
      <td>0.965055</td>
      <td>667.466799</td>
      <td>0.858057</td>
      <td>4.699231</td>
      <td>3.955556</td>
    </tr>
    <tr>
      <th>3</th>
      <td>45</td>
      <td>177.0</td>
      <td>0.022222</td>
      <td>0.005650</td>
      <td>29.902622</td>
      <td>0.964873</td>
      <td>667.550603</td>
      <td>0.857452</td>
      <td>4.680132</td>
      <td>3.933333</td>
    </tr>
    <tr>
      <th>4</th>
      <td>45</td>
      <td>176.0</td>
      <td>0.022222</td>
      <td>0.005682</td>
      <td>29.891112</td>
      <td>0.964689</td>
      <td>667.634783</td>
      <td>0.856842</td>
      <td>4.660908</td>
      <td>3.911111</td>
    </tr>
  </tbody>
</table>
</div>

```python

```

scripts/.ipynb_checkpoints/B. Solution of the control problem including the universal line + Sensitivity analysis for FigS10-checkpoint.ipynb


# Module Circuit

This document describes the **circuit** module that can be used to solve electronica circuits.

The main element in the module is the **circuit class**

The module relies on [sympy](http://www.sympy.org) to perform the calculations.

# Importing the module

In order to use the module,you need to **load** and **import** it.

The following code loads and imports the **circuit** module in the **Colaboratory** environment. Outside of Colaboratory just use the usual import method.

The code clears the output text after import showing only the loaded linear module version.

We will also load the **linear** module because we need later to draw bode plots.

```python
# Set True inside colaboratory, False elsewere
colaboratory = True

if colaboratory:
    # Load the circuit module
    !rm circuit.*
    !wget https://raw.githubusercontent.com/R6500/Python-bits/master/Modules/circuit.py

    # Load the linear module
    !rm linear.*
    !wget https://raw.githubusercontent.com/R6500/Python-bits/master/Modules/linear.py

# Import the modules
import circuit
import linear as lin

# Set colaboratory if needed on linear
lin.setColaboratory(colaboratory)

# Erase output after import
from IPython.display import clear_output
clear_output()

# Check loaded module
try:
    print('circuit version: ',circuit.version)
    print('linear version:  ',lin.version)
except:
    print('Error loading module')
    raise
```

    circuit version:  28/03/2018-B
    linear version:   22/03/2018

## The Circuit Class

The circuit class defines a circuit

To create an instance of the class, assuming you use the normal import, just use:

>`object = circuit.circuit()`

The circuits are defined by a set of **nodes** you can name the nodes as you want using strings or numbers. Although the usual way is to use numbers.

Every circuit, however, must have a node with named **0**. Not having this node in a circuit generates an exception.

## Adding components to a circuit

Components can be added using the **add\*** member functions

`object.addR(self,name,node1,node2,value=None)`

>Add a resistor to the circuit between **node1** and **node2**.
>You can optionally provide a numeric value, in ohm, for the resistor.

`object.addC(self,name,node1,node2,value=None)`

>Add a capacitor to the circuit between **node1** and **node2**.
>You can optionally provide a numeric value, in Farad, for the capacitor.

`object.addL(self,name,node1,node2,value=None)`

>Add an inductor to the circuit between **node1** and **node2**.
>You can optionally provide a numeric value, in Henry, for the inductor.

`object.addV(self,name,node1,node2,value=None)`

>Add a voltage source to the circuit between **node1** (+) and **node2** (-).
>You can optionally provide a numeric value, in Volt, for the source.

`object.addI(self,name,node1,node2,value=None)`

>Add a current source to the circuit between **node1** (+) and **node2** (-).
>The current is defined to go from **node2** (-) to **node1** (+)
>You can optionally provide a numeric value, in Volt, for the source.

`object.addVM(self,name,node1,node2)`

>Add a voltage measurement component between **node1** (+) and **node2** (-)
>This component just measures a voltage and doesn't change the circuit operation.
>It can be used to give its voltage in the circuit solution.
>This component can also be used to control a controlled source.

`object.addIM(self,name,node1,node2)`

>Add a current measurement component between **node1** (+) and **node2** (-)
>This component just measures a current and doesn't change the circuit operation.
>The two nodes are short circuited by this component.
>The component can be used to give its currents in the circuit solution.
>This component can also be used to control a controlled source.

`object.addCVS(self,name,node1,node2,cont,value=None)`

>Add a controlled voltage source between **node1** (+) and **node2** (-)
>Voltage is proportional to the a voltage or current measurement
>The proportionality constant is defined by **name**
>**cont** is the name of a previously defined voltage or current measurement component

`object.addCIS(self,name,node1,node2,cont,value=None)`

>Add a controlled current source between **node1** (+) and **node2** (-)
>The current is defined to go from **node2** (-) to **node1** (+)
>Current is proportional to the a voltage or current measurement
>The proportionality constant is defined by **name**
>**cont** is the name of a previously defined voltage or current measurement component

All the above methods return a **sympy** symbol associated to the added component that is generated from the name provided.

In the case of the voltage sources, independent (addV) or controlled (addCVS), the current going out of the source is also associated to a symbol, but this one is not returned by the functions.

## Circuit symbol names

Every component you include in a circuit adds a **symbol name**

For instance, adding a resistor with:

>`object.addR('R1',1,0,1000)`

Adds the name **'R1'** to the listo of symbols of the circuit. As explained before, those symbols are returned by the **add\*** methods.

Aside from the names or each component you add, the module adds other additional symbols:

* Every voltage source gets a symbol associated to its current going from terminal (-) to (+)

>The simbol name is the same name of the source with a **i** prefix. For instance:

>`object.addV('Vs',1,0,1)`

>Adds the **Vs** and the **iVs** symbols

>This also applies to controlled voltage sources

* Every node that is not ground **0** gets a symbol for its voltage

>The simbol name is the same name of the node with a **v** prefix. For instance:

>`object.addR('R1',1,2,1000)`

>Adds the **R1**, **v1** and **v2** symbols

The circuit solution always includes the names of the components associated to the voltage and current **measurement** components.

Other symbols can also be found in the circuit solutions as you will always get as many solution symbols as independent variables are in the circuit.

All symbols for the circuit can be obtained from the **symbols** member of the class as will be explained later.

## Solving a circuit

A circuit can be solved using the **solve** method

>`object.solve()`

This function retuns the circuit solution as a **dictionary** that uses the **symbol names** as keys. This dictionary is also available as the object member **solution**

>`object.solution`

Once you have solved a circuit, the object associated to it also holds the particular solution in the **particular** member. This particular solution is obtained by subtituting all component **symbols** by their defined **values**.

>`object.particular`

## Additional circuit elements

The **circuit** object has some additional member elements a part from **solution** and **particular** that can come handy in some calculations

`object.sSolution`

**Dictionary** of circuit solutions using sympy **symbols** for keys instead of **names**

`object.symbol`

**Dictionary** of all **sympy** circuit symbols using **names** for keys

`object.equations`

**List** of all **equations** used to solve the circuit

`object.unknowns`

**List** of all **unknowns** sympy symbols for which we solve the circuit

There are other member elements that are not normaly useful to access from outside the module internal operations

`object.components`

List of components in the circuit. Each component is defined as a **dictionary**

`object.meas`

**Dictionary** of voltage and current measurement components using **names** for keys

`object.subsDic`

Substitution **dictionary** for component **values** using **symbols** for keys

`object.name`

Symbol **dictionary** with all **names** that can become unknowns using **symbols** for keys

## Evaluation helper functions

The **circuit** module includes three helper functions to evaluate a **sympy** expression for several values of one of its symbols.

The **expr2func** function generates a python function from a **sympy** expression

>`expr2func(expr,sym1,sym2,...)`

Parameters:

>**expre** is the expression to convert

>**sym1,sym2,...** is a list of symbols to convert to function parameters

**Example** of usage:

```python
import sympy
x,y = sympy.symbols('x,y')
expr = 2*x*y
f = circuit.expr2func(expr,x,y)
print('f(2,4) =',f(2,4))
```

    f(2,4) = 16

The **evalList** functions evaluates a **sympy** expression in a set of values

>`evalList(expr,var,set)`

Parameters:

>**expr** is the expression to evaluate

>**var** is the sympy symbol to substituye for a set of values

>**set** is an iterable set of values

The function returns a **numpy** array

One of the uses of the evaluation is to calculate the frequency response of a circuit, the **evalFrequs** enables us to evaluate one **sympy** expression for several frequency **f** values.

>`evalFreqs(expr,set)`

Parameters:

>**expr** is the expression to evaluate

>**set** is an iterable set of frequencies (in Hz)

The function returns a **numpy** array of complex numbers that evaluates the using the following assignment for the **s** symbol

$\qquad s = j \: 2\pi f$

for each frequency of the **set** iterable.

The function is specially usefull for bode plots using the **linear** module:

> `f_vector = linear.frange(f_start,f_end)`
> `res = evalFreqs(expr,f_vector)`
> `linear.drawBodeFromComplex(f_vector,res)`

## $\LaTeX$ support

The module supports **latex** for the equations generated by the code when working inside the **Jupyter** environment.

It doesn't work, however, in colaboratory.

Latex is disabled by default, to enable latex suppor execute the module **setLatex** function:

>`setLatex(flag=True)`

Where **flag** indicates if we want to enable Latex (defaults to True)

Once enabled, you can use the show command to show equations in latex:

>`show(expr)`

If **latex** is not enabled, **show** just uses a normal print

## The verbose option

By default the module does not write anything on the standard output.

You can, however, set the module to be verbose on its operation.

The **setVerbose()** function controls the information given on standard output

>`setVerbose(flag)`

Where **flag** activates output text when **True** and deactivates it when **False**

By default, flag is **True**, so **setVerbose()** without arguments activate text output.

# Examples

The following section shows some circuit examples

## First simple DC circuit

The following circuit includes one voltage source, one current source and two resistors.

The output voltage **Vo** will be defined with a voltage measurement component.

The following code describes the circuit.

We will use the **verbose** option that gives information about the module actions.

```python
# Set verbose option
circuit.setVerbose()

# Circuit 1 definiton
c1 = circuit.circuit()
c1.addV('Vs',1,0,5)
c1.addR('R1',1,2,1000)
c1.addR('R2',2,0,1000)
c1.addI('Is',2,0,0.01)
c1.addVM('Vo',2,0)
```

    Starting a new circuit
    Voltage supply Vs added between nodes 1 and 0 with value 5
    Resistor R1 added between nodes 1 and 2 with value 1000
    Resistor R2 added between nodes 2 and 0 with value 1000
    Current supply Is added between nodes 2 and 0 with value 0.01
    Voltage measurement Vo added between nodes 2 and 0

    Vo

Now we can solve the circuit:

```python
c1.solve()
```

    Solving the circuit
    There are 3 nodes :
         0
         1
         2
    Creating node variables
         v1
         v2
    Creating KCL equations
         iVs - (v1 - v2)/R1
         Is - v2/R2 - (-v1 + v2)/R1
    Adding V source equations
    Adding I measurement equations
    Adding V measurement equations
    Processing controlled elements
    Circuit equations:
         iVs - (-Vo + v1)/R1
         Is - Vo/R2 - (Vo - v1)/R1
         Eq(Vs, v1)
    Unknowns: {iVs, Vo, v1}
    Circuit solution:
         {'iVs': (-Is*R2 + Vs)/(R1 + R2), 'Vo': R2*(Is*R1 + Vs)/(R1 + R2), 'v1': Vs}
    Circuit solution with substitutions:
         {'iVs': -0.00250000000000000, 'Vo': 7.50000000000000, 'v1': 5}

    {'Vo': R2*(Is*R1 + Vs)/(R1 + R2), 'iVs': (-Is*R2 + Vs)/(R1 + R2), 'v1': Vs}

All the text, except the last line that is the output of `c1.solve()` is generated due to the **verbose** setting.

You can get the variables in the circuit solutions using the code:

```python
# Show the list of solution symbols
print(list(c1.solution))
```

    ['iVs', 'Vo', 'v1']

As you can see, the **Vo** measurement element is included in the solution. In this circuit, the current in the voltage source **iVs** and node 1 voltage **v1** are also one of the solutions.

You can get one of the circuit solutions by its name, both in the **symbolic** and the **numerical** cases:

```python
# Symbolic solution
print('Vo =',c1.solution['Vo'],'  (Symbolic)')

# Numeric solution
print('Vo =',c1.particular['Vo'],'V   (Particular)')
```

    Vo = R2*(Is*R1 + Vs)/(R1 + R2)   (Symbolic)
    Vo = 7.50000000000000 V   (Particular)

## Second simple DC circuit

The following circuit includes one voltage source, one current source and two resistors.

The output voltage **Vo** will be defined with a voltage measurement component.

From now on, we will disable the **verbose** option.

The following code describes the circuit and solves it.

```python
# Disable the verbose option
circuit.setVerbose(False)

# Circuit 2 definition
c2 = circuit.circuit()
c2.addV('Vs',1,0,6)
c2.addR('R1',1,2,1000)
c2.addR('R2',2,3,1000)
c2.addR('R3',3,0,1000)
c2.addVM('Vo',2,3)
c2.solve()
```

    {'Vo': R2*Vs/(R1 + R2 + R3),
     'iVs': Vs/(R1 + R2 + R3),
     'v1': Vs,
     'v2': Vs*(R2 + R3)/(R1 + R2 + R3),
     'v3': R3*Vs/(R1 + R2 + R3)}

The last command returns the shown circuit solution. As in the previous circuit you can get the **Vo** value:

```python
# Symbolic solution
print('Vo =',c2.solution['Vo'],'  (Symbolic)')

# Numeric solution
print('Vo =',c2.particular['Vo'],'V   (Particular)')
```

    Vo = R2*Vs/(R1 + R2 + R3)   (Symbolic)
    Vo = 2 V   (Particular)

If we want, we can see the circuit equations. If **Eq** is present, it identifies the two sides of the equation.
One side equations, **without Eq**, shall equal zero.

We can also see the unknowns for which we solve the circuit.

```python
# Equations
print('Circuit equations')
for eq in c2.equations:
    print('   ',eq)
print()

# Unknowns
print('Circuit unknowns for wich we solve')
print('   ',c2.unknowns)
```

    Circuit equations
        iVs - (v1 - v2)/R1
        -(v2 - v3)/R2 - (-v1 + v2)/R1
        -v3/R3 - (-v2 + v3)/R2
        Eq(Vs, v1)
        Eq(Vo, v2 - v3)

    Circuit unknowns for wich we solve
        {v1, iVs, v3, v2, Vo}

## Current measurement

The following circuit includes current measurement elements

Note that, although, nodes 2, 3 and 4 are really at the same voltage, we need to use three different numbers to define the currents.

The following code defines and solves the circuit.

```python
# Circuit 3 definition

c3 = circuit.circuit()
c3.addV('Vs',1,0,5)
c3.addR('R1',1,2,1000)
c3.addR('R2',3,0,2000)
c3.addR('R3',4,0,2000)
c3.addIM('I1',3,2)
c3.addIM('I2',4,2)
c3.solve()
```

    {'I1': R3*Vs/(R1*R2 + R1*R3 + R2*R3),
     'I2': R2*Vs/(R1*R2 + R1*R3 + R2*R3),
     'iVs': Vs*(R2 + R3)/(R1*R2 + R1*R3 + R2*R3),
     'v1': Vs,
     'v2': R2*R3*Vs/(R1*R2 + R1*R3 + R2*R3)}

We can show the two desired solutions:

```python
print('I1 =',c3.solution['I1'])
print('I2 =',c3.solution['I2'])
```

    I1 = R3*Vs/(R1*R2 + R1*R3 + R2*R3)
    I2 = R2*Vs/(R1*R2 + R1*R3 + R2*R3)

We can ask for the unknowns in the circuit:

```python
print('Unknowns:',c3.unknowns)
```

    Unknowns: {v1, iVs, I1, v2, I2}

You can see that although we defined nodes 1, 2, 3 and 4, only node voltages **v1** and **v2** are present in the list.

Nodes 3 and 4 have been substituted by node 2 voltage.

This is seen more clearly in the list of equations:

```python
# Equations
print('Circuit equations')
for eq in c3.equations:
    print('   ',eq)
print()
```

    Circuit equations
        iVs - (v1 - v2)/R1
        -I1 - I2 - (-v1 + v2)/R1
        I1 - v2/R2
        I2 - v2/R3
        Eq(Vs, v1)

## Controlled voltage source

The following circuit includes voltage controlled source

The following code defines and solves the circuit.

```python
# Circuit 4 definition
c = circuit.circuit()
c.addV('Vs',1,0,5)
c.addR('R1',1,2,1000)
c.addR('R2',2,0,1000)
c.addVM('Vm',2,0)
c.addCVS('k',3,0,'Vm',10)
c.addR('R3',3,0)
c.addVM('Vo',3,0)
c.solve()
```

    {'Vm': R2*Vs/(R1 + R2),
     'Vo': R2*Vs*k/(R1 + R2),
     'iVs': Vs/(R1 + R2),
     'ik': R2*Vs*k/(R3*(R1 + R2)),
     'v1': Vs}

## Controlled current source

The following circuit includes a current controlled source

The following code defines and solves the circuit.

```python
# Circuit 5 definition
c5 = circuit.circuit()
c5.addV('Vs',1,0,5)
c5.addR('R1',1,2,1000)
c5.addIM('Im',0,2)
c5.addCIS('beta',3,0,'Im',10)
c5.addR('R3',3,0)
c5.addVM('Vo',3,0)
c5.solve()
```

    {'Im': Vs/R1, 'Vo': R3*Vs*beta/R1, 'iVs': Vs/R1, 'v1': Vs}

## AC Circuit

Now we can try an AC circuit

The following code describes and solves the circuit

```python
# Circuit 6 definition
c6 = circuit.circuit()
c6.addV('Vs',1,0,1)
c6.addR('R1',1,2,1000)
c6.addR('R3',2,3,100)
c6.addC('C1',2,0,1e-6)
c6.addC('C2',3,0,100e-9)
c6.addVM('Vo',3,0)
c6.solve()
```

    {'Vo': Vs/(C1*R1*s + C2*s*(C1*R1*R3*s + R1 + R3) + 1),
     'iVs': Vs*s*(C1*C2*R3*s + C1 + C2)/(C1*C2*R1*R3*s**2 + C1*R1*s + C2*R1*s + C2*R3*s + 1),
     'v1': Vs,
     'v2': Vs*(C2*R3*s + 1)/(C1*R1*s + C2*s*(C1*R1*R3*s + R1 + R3) + 1)}

The **Vo** solution both in symbolic and particular form is:

```python
print('Vo =',c6.solution['Vo'])
print('Vo =',c6.particular['Vo'])
```

    Vo = Vs/(C1*R1*s + C2*s*(C1*R1*R3*s + R1 + R3) + 1)
    Vo = 1/(1.0e-7*s*(0.1*s + 1100) + 0.001*s + 1)

Now we can also get the **poles** and **zeros** of the circuit using the **sympy** functions

```python
import sympy

numer,denom =c6.particular['Vo'].as_numer_denom()
print('Num =',numer)
print('Den =',denom)
print()

zeros = sympy.roots(numer)
poles = sympy.roots(denom)
print('Zeros =',zeros)
print('Poles =',poles)
print()
```

    Num = 1
    Den = 1.0e-7*s*(0.1*s + 1100) + 0.001*s + 1

    Zeros = {}
    Poles = {-110091.666030631: 1, -908.333969368548: 1}

Now we can use the **linear** module, imported as **lin** to show a bode plot

```python
# Get the circuit output Vo
Vo  = c6.particular['Vo']

# Define a frequency vector between 1Hz and 1MHz
fv  = lin.frange(1,1e6)

# Evaluate Vo at all frequencies
res = circuit.evalFreqs(Vo,fv)

# Draw the bode plot
lin.drawBodeFromComplex(fv,res)
```

## AC Resonant Circuit

We can also solve an AC resonant circuit with a capacitor and an inductor

The following code describes and solves the circuit

```python
# Circuit 7 definition
c7 = circuit.circuit()
c7.addI('Is',1,0,1)
c7.addC('C',1,0,1e-6)
c7.addL('L',1,0,1e-3)
c7.addVM('Vo',1,0)
c7.solve()
```

    {'Vo': Is*L*s/(C*L*s**2 + 1)}

We can also find the **poles** and **zeros**

The circuit is band pass as it has a zero at 0 and two complex poles

Q is infinite as it has no resistive elements

```python
import sympy

numer,denom =c7.particular['Vo'].as_numer_denom()
print('Num =',numer)
print('Den =',denom)
print()

zeros = sympy.roots(numer)
poles = sympy.roots(denom)
print('Zeros =',zeros)
print('Poles =',poles)
```

    Num = 0.001*s
    Den = 1.0e-9*s**2 + 1

    Zeros = {0: 1}
    Poles = {-31622.7766016838*I: 1, 31622.7766016838*I: 1}

A more realistic circuit will have also a resistance

```python
c7.addR('R',1,0,1e6)
c7.solve()

numer,denom =c7.particular['Vo'].as_numer_denom()
print('Num =',numer)
print('Den =',denom)
print()

zeros = sympy.roots(numer)
poles = sympy.roots(denom)
print('Zeros =',zeros)
print('Poles =',poles)
```

    Num = 1000.0*s
    Den = 0.001*s**2 + 0.001*s + 1000000.0

    Zeros = {0: 1}
    Poles = {-0.5 - 31622.7765977309*I: 1, -0.5 + 31622.7765977309*I: 1}

We can also draw the bode plot for this circuit

```python
Vo  = c7.particular['Vo']
fv  = lin.frange(1000,10000,ppd=500)
res = circuit.evalFreqs(Vo,fv)
lin.drawBodeFromComplex(fv,res)
```

## Thevenin equivalent

We can use the module also to obtain a thevenin equivalent

As we know, the open circuit **Vo** is the thevenin voltage **Vth**

```python
# Circuit definition
c8 = circuit.circuit()
c8.addV('Vs',1,0,5)
c8.addR('R',1,2,1000)
c8.addIM('I1',3,2)
c8.addCIS('beta',3,0,'I1',100)
c8.addVM('Vo',3,0)
c8.solve()

# Thevenin voltage
Vth = c8.solution['Vo']
print('Vth =',Vth)
```

    Vth = Vs

The thevenin resistance **Rth** can be calculating adding a **Vx** voltage on the output and obtaining the resistance as:

$\qquad R_{th} = \left. \frac{Vx}{Ix} \right|_{V_s = 0}$

Where $Ix$ is the current output in the $Vx$ source.

The following **code** calculates **Rth**

```python
# Thevenin resistance calculation

# Add Vx output source
# Beware: If you modify this cell
# Don't execute this cell two times before executing the previous cell
# or you will get several Vx voltage sources in the circuit
c8.addV('Vx',3,0)

# Solve the circuit
c8.solve()

# Show the Ix solution that is named iVx
Ix = c8.solution['iVx']
print('Ix =',Ix)

# We get the 'Vx' symbol form the nDic dictionary
Vx = c8.symbol['Vx']

# We susbtitute Vs for 0 in the Ix solution
Ix = Ix.subs('Vs',0)

# Calculate and show Rth value
Rth = c8.symbol['Vx'] / c8.solution['iVx'].subs('Vs',0)
print('Rth =',Rth)
```

    Ix = (-Vs + Vx)*(beta + 1)/R
    Rth = R/(beta + 1)

## Opamp circuit

We can also solve operational amplifier circuits

One way is to define the amplifier as a controlled voltage source:

$\qquad V_d = V_{(+)}-V_{(-)} \qquad V_O = A \cdot V_d$

Once we solve the circuit we can obtain three posible solutions:

**Finite gain Opamp**
In this case we just use the default solution with the **A** value for the opamp.

**Ideal Opamp**
To obtain the ideal opamp solution we obtain the circuit solution when A tends to infinity.

$\qquad V_O = \left. V_O \right|_{A \rightarrow \infty}$

**Dominant pole Opamp**
In this case we substitute **A** for **A(s)** using the dominant pole model

$\qquad A = A(s) = \frac{A_O}{1+s/p1}$

The following code solves the above circuit considering the finite gain model.

```python
# Circuit definition
c9 = circuit.circuit()
c9.addV('Vs',1,0,1)       # Vs value is 1 so that Vo gives gain
c9.addR('Ri',1,2,1000)
c9.addR('Rf',2,3,10000)
c9.addVM('Vo',3,0)
# Opamp base model
c9.addVM('Vd',0,2)
c9.addCVS('A',3,0,'Vd')
c9.solve()

# Base solution with finite gain A
Vo_A = c9.solution['Vo']
print('Finite gain opamp solution')
print('  Vo =',Vo_A)
```

    Finite gain opamp solution
      Vo = -A*Rf*Vs/(A*Ri + Rf + Ri)

If use the **limit** function of the **sympy** module, we can obtain the ideal opamp response

```python
# Import the limit function and the infinite symbol
from sympy import oo, limit

# Get the circuit 'A' symbol
A = c9.symbol['A']

# Calculate the limit of the solution when A->oo
Vo_i = limit(Vo_A,A,oo)
print('Ideal opamp solution')
print('  Vo =',Vo_i)
```

    Ideal opamp solution
      Vo = -Rf*Vs/Ri

The solution for an opamp with a **dominant** pole model can be obtained using the **subs** method to substituye the gain **A** for a a single pole model

```python
import sympy

# New symbols for s, Ao and p1
s,Ao,p1 = sympy.symbols('s,Ao,p1')

# Define A(s) and substitute A for A(s)
# simplify(), expand() and collect() give a better expression
A_s = Ao*p1/(s+p1)
Vo_d = (Vo_A.subs(A,A_s)).simplify().expand()
Vo_d = sympy.collect(Vo_d,s)

print('Dominant pole solution')
print('  Vo =',Vo_d)
```

    Dominant pole solution
      Vo = -Ao*Rf*Vs*p1/(Ao*Ri*p1 + Rf*p1 + Ri*p1 + s*(Rf + Ri))

We can use the values of the components to draw the bode plot of the circuit

```python
# Get the Vo solution with component values
Vo_d=c9.particular['Vo']

# Define values for the dominant pole model
p1 = 16 # rad/s
Ao = 100000

# Substitute A for the dominant pole model
Vo_d = (Vo_d.subs(A,Ao*p1/(s+p1))).simplify()

print('Numerical solution in s domain')
print('   ',Vo_d)
print()

# Bode plot
fv  = lin.frange(10,100000)
res = circuit.evalFreqs(Vo_d,fv)
lin.drawBodeFromComplex(fv,res)
```

The bode plot shows the signal gain because the numeric **value** of the input **Vs** was 1.

We cab compute the signal gain in the symbolic solution dividing **Vo** by **Vs**

```python
# Gain calculation
Gain = Vo_A/c9.symbol['Vs']

print('Signal gain')
print('  ',Gain)
print()

print('Signal gain for A -> oo')
print('  ',limit(Gain,c9.symbol['A'],oo))
```

    Signal gain
       -A*Rf/(A*Ri + Rf + Ri)

    Signal gain for A -> oo
       -Rf/Ri

We can also compute the **noise gain $N_G$**

This gain is the one associated to the **GBW** product conservation in dominant pole opamp circuits.

The noise gain can be calculated as the gain for a **$V_n$** source in series with the **(+)** terminal when the input **$V_s$** is zero.

$\qquad N_G = \left. \frac{V_O}{V_n} \right|_{Vs = 0}$

We will rewrite the circut to measure this noise gain.

```python
# Noise gain Circuit definition
c9b = circuit.circuit()
c9b.addR('Ri',0,2,1000)
c9b.addR('Rf',2,3,10000)
c9b.addV('Vn',4,0)
c9b.addVM('Vo',3,0)
# Opamp base model
c9b.addVM('Vd',4,2)
c9b.addCVS('A',3,0,'Vd')
c9b.solve()

# The noise gain is calculated
Ng = c9b.solution['Vo']/c9b.symbol['Vn']

print('Noise gain')
print('  ',Ng)
print()

print('Noise gain for A -> oo')
print('  ',limit(Ng,c9b.symbol['A'],oo))
```

    Noise gain
       A*(Rf + Ri)/(A*Ri + Rf + Ri)

    Noise gain for A -> oo
       (Rf + Ri)/Ri

## Document information

Copyright © Vicente Jiménez (2018)

Last update: 28/3/2018

This work is licensed under a [Creative Common Attribution-ShareAlike 4.0 International license](http://creativecommons.org/licenses/by-sa/4.0/).

The **circuit.py** code is licensed under the [MIT License](https://opensource.org/licenses/MIT).

You can find the module [here](https://github.com/R6500/Python-bits/tree/master/Modules)

See my blogs [AIM65](http://aim65.blogspot.com.es/) (in spanish) and [R6500](http://r6500.blogspot.com.es/) (in english)