# Simulating Predator and Prey Relationship

Without a predator, rabbits will reproduce until they reach the carrying capacity of the land. When coyotes show up, they will eat the rabbits and reproduce until they can't find enough rabbits. We will explore the fluctuations in the two populations over time.

# Using Lotka-Volterra Model

## Part 1: Rabbits without predators

According to [Mother Earth News](https://www.motherearthnews.com/homesteading-and-livestock/rabbits-on-pasture-intensive-grazing-with-bunnies-zbcz1504), a rabbit eats six square feet of pasture per day. Let's assume that our rabbits live in a five acre clearing in a forest: 217,800 square feet/6 square feet = 36,300 rabbit-days worth of food. For simplicity, let's assume the grass grows back in two months. Thus, the carrying capacity of five acres is 36,300/60 = 605 rabbits.

Female rabbits reproduce about six to seven times per year. They have six to ten children in a litter.  According to [Wikipedia](https://en.wikipedia.org/wiki/Rabbit), a wild rabbit reaches sexual maturity when it is about six months old and typically lives one to two years. For simplicity, let's assume that in the presence of unlimited food, a rabbit lives forever, is immediately sexually mature, and has 1.5 children every month.

For our purposes, then, let $x_t$ be the number of rabbits in our five acre clearing on month $t$.
$$
\begin{equation*}
  R_t = R_{t-1} + 1.5\frac{605 - R_{t-1}}{605} R_{t-1}
\end{equation*}
$$

The formula could be put into general form
$$
\begin{equation*}
  R_t = R_{t-1} + growth_{R} \times \big( \frac{capacity_{R} - R_{t-1}}{capacity_{R}} \big) R_{t-1}
\end{equation*}
$$

By doing this, we allow users to interact with growth rate and the capacity value visualize different interaction

```python
from __future__ import print_function
from ipywidgets import interact, interactive, fixed, interact_manual
from IPython.display import display, clear_output
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
```

```python
%matplotlib inline
style = {'description_width': 'initial'}
capacity_R = widgets.FloatText(description="Capacity", value=605)
growth_rate_R = widgets.FloatText(description="Growth rate", value=1.5)
initial_R = widgets.FloatText(description="Initial population",style=style, value=1)
button_R = widgets.Button(description="Plot Graph")
display(initial_R, capacity_R, growth_rate_R, button_R)

def plot_graph_r(b):
    clear_output()
    display(initial_R, capacity_R, growth_rate_R, button_R)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    t = np.arange(0, 20, 1)
    s = np.zeros(t.shape)
    R = initial_R.value
    for i in range(t.shape[0]):
        s[i] = R
        #print(s[i])
        nextvalue = growth_rate_R.value * (capacity_R.value - R)/(capacity_R.value) * R
        #print('nextvalue: ', nextvalue)
        R = R + nextvalue

    ax.plot(t, s)
    ax.set(xlabel='time (months)', ylabel='rabbits',
       title='Rabbits Without Predators')
    ax.grid()

button_R.on_click(plot_graph_r)
```

## Part 2: Coyotes without preys
According to [Huntwise](https://www.besthuntingtimes.com/blog/2020/2/3/why-you-should-coyote-hunt-how-to-get-started), coyotes need to consume about 2-3 pounds of food per day. Their diet is 90 percent mammalian. The perfect adult cottontail rabbits weigh 2.6 pounds on average. Thus, we assume the coyote eats one rabbit per day.

For coyotes, the breeding season is in February and March. According to [Wikipedia](https://en.wikipedia.org/wiki/Coyote#Social_and_reproductive_behaviors), females have a gestation period of 63 days, with an average litter size of 6, though the number fluctuates depending on coyote population density and the abundance of food. By fall, the pups are old enough to hunt for themselves.

In the absence of rabbits, the number of coyotes will drop, as their food supply is scarce.
The formula could be put into general form:

$$
\begin{align*}
  C_t & \sim (1 - death_{C}) \times C_{t-1}\\
  &= C_{t-1} - death_{C} \times C_{t-1}
\end{align*}
$$

```python
%matplotlib inline
style = {'description_width': 'initial'}
initial_C=widgets.FloatText(description="Initial Population",style=style,value=200.0)
declining_rate_C=widgets.FloatText(description="Death rate",value=0.5)
button_C=widgets.Button(description="Plot Graph")
display(initial_C, declining_rate_C, button_C)

def plot_graph_c(b):
    clear_output()
    display(initial_C, declining_rate_C, button_C)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    t1 = np.arange(0, 20, 1)
    s1 = np.zeros(t1.shape)
    C = initial_C.value
    for i in range(t1.shape[0]):
        s1[i] = C
        C = (1 - declining_rate_C.value)*C

    ax.plot(t1, s1)
    ax.set(xlabel='time (months)', ylabel='coyotes',
       title='Coyotes Without Predators')
    ax.grid()

button_C.on_click(plot_graph_c)

```

## Part 3: Interaction Between Coyotes and Rabbit
With the simple interaction from the first two parts, now we can combine both interaction and come out with simple interaction.
$$
\begin{align*}
  R_t &= R_{t-1} + growth_{R} \times \big( \frac{capacity_{R} - R_{t-1}}{capacity_{R}} \big) R_{t-1} - death_{R}(C_{t-1})\times R_{t-1}\\\\
  C_t &= C_{t-1} - death_{C} \times C_{t-1} + growth_{C}(R_{t-1}) \times C_{t-1}
\end{align*}
$$

In equations above, death rate of rabbit is a function parameterized by the amount of coyote. Similarly, the growth rate of coyotes is a function parameterized by the amount of the rabbit.

The death rate of the rabbit should be $0$ if there are no coyotes, while it should approach $1$ if there are many coyotes. One of the formula fulfilling this characteristics is hyperbolic function.

$$
\begin{equation}
death_R(C) = 1 - \frac{1}{xC + 1}
\end{equation}
$$

where $x$ determines how quickly $death_R$ increases as the number of coyotes ($C$) increases. Similarly, the growth rate of the coyotes should be $0$ if there are no rabbits, while it should approach infinity if there are many rabbits. One of the formula fulfilling this characteristics is a linear function.

$$
\begin{equation}
growth_C(R) = yC
\end{equation}
$$

where $y$ determines how quickly $growth_C$ increases as number of rabbit ($R$) increases.

Putting all together, the final equtions are

$$
\begin{align*}
  R_t &= R_{t-1} + growth_{R} \times \big( \frac{capacity_{R} - R_{t-1}}{capacity_{R}} \big) R_{t-1} - \big( 1 - \frac{1}{xC_{t-1} + 1} \big)\times R_{t-1}\\\\
  C_t &= C_{t-1} - death_{C} \times C_{t-1} + yR_{t-1}C_{t-1}
\end{align*}
$$

```python
#### %matplotlib inline
initial_rabbit = widgets.FloatText(description="Initial Rabbit", value=1)
initial_coyote = widgets.FloatText(description="Initial Coyote", value=1)
capacity = widgets.FloatText(description="capacity_R", value=5)
growth_rate = widgets.FloatText(description="growth_R", value=1)
death_rate = widgets.FloatText(description="death_C", value=1)
x = widgets.FloatText(description="x", value=1)
y = widgets.FloatText(description="y", value=1)
button = widgets.Button(description="Plot Graph")
display(initial_rabbit, initial_coyote, capacity, growth_rate, death_rate, x, y, button)
def plot_graph(b):
    clear_output()
    display(initial_rabbit, initial_coyote, capacity, growth_rate, death_rate, x, y, button)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    t = np.arange(0, 20, 0.5)
    s = np.zeros(t.shape)
    p = np.zeros(t.shape)
    R = initial_rabbit.value
    C = initial_coyote.value
    for i in range(t.shape[0]):
        s[i] = R
        p[i] = C
        R = R + growth_rate.value * (capacity.value - R)/(capacity.value) * R - (1 - 1/(x.value*C + 1))*R
        C = C - death_rate.value * C  + y.value*s[i]*C

    ax.plot(t, s, label="rabit")
    ax.plot(t, p, label="coyote")
    ax.set(xlabel='time (months)', ylabel='rabbits',
       title='Coyotes Rabbit Predator Prey Relationship')
    ax.grid()
    ax.legend()

button.on_click(plot_graph)
```

## Using ABM simulation
In ABMs of ecological and evolutionary dynamics, prey naturally grow but get eaten by predators, while the predators grow if they get prey, but naturally die off if they can't find any food.
In our ABM simulation model, we will randomly choosing an agent to update the system's state in an asynchronous manner.
### Design the data structure to store the attributes of the the prey and predators
The information about about agent type must be represented in the data structure, also in order to simulate the interactions in a spacel, the information about the agents spatial location is also needed. In the code, we use r_init and c_init to represent the initial population of rabbits and coyotes. The for loop iterates r_init + c_init times, and in the first r_init iteration, the prey agents are generated, while the predator agents are generated for the rest.

### The rules for how prey and predators behave on their own:
If a prey agent meets a predator agent, it dies with some probability because of predation. We will implement death as the removal of the prey from the preys agents list.
If a predator agent can't find any prey agents nearby, it dies with some probability because of the lack of food. Otherwise, it will reproduce at a certain reproduction rate.
According to [Purely Facts](http://purelyfacts.com/question/12/which-is-faster-a-coyote-or-a-rabbit?DDA=28&DDB=98),  the top speed is roughly 64 kph for coyote, and 4 kph for rabbit, so let's assume coyotes are 15 times faster than rabbit. Furthermore, to ensure our simulation model can naturally handle situations where the size of the agent population changes rapidly, and guarantees that each agent is updated once, on average, in each unit time length, we defined a update_one_unit_time() function to address the issue. We make the unit length of time passes by in each asynchronous updating proportional to the size of the agent populationat the time of updating. This way, the progress of time will be steady in the simulation, even if the number of agents changes over time.

### Static Single Step

```python
from __future__ import print_function
from ipywidgets import interact, interactive, fixed, interact_manual
from IPython.display import display, clear_output
import ipywidgets as widgets
%matplotlib inline
# matplotlib.use('TkAgg')
from pylab import *
import copy as cp

r_init = 0 # Initial rabbit population
c_init = 0 #Initial coyotes population
nr = 0 # Capacity of rabbits
mr = 20.0 # Magnitude of movement of rabbits in feet per day
rr = 0 # Reproduction rate of rabbits (per day)
dc = 0 # Death rate of coyotes when there is no rabbit (per day)
rc = 0 # Reproduction rate of coyotes (per day)
cd = 20.0 # Radius for collision detection in feet
mc = mr * 2 # Magnitude of movement of coyotes in feet per day
cc = 3 # How many unburned rabbits a coyote can contain
ic = 0.04 # Likelihood that an immigrant coyote will show up on any day
ir = 0.1 #Likelihood that an immigrant rabbit will show up on any day
cdsq = cd**2

width = 500 # Width of pasture in feet
height = 500 # Height of pasture in feet

class Rabbit:
    pass

class Coyote:
    pass

def initialize(a):
    global r_init, c_init, nr, mr, rr, dc, rc, mc, cc, ic, ir, cdsq

    # Give default values
    r_init = initial_rabbit_count.value # Initial rabbit population
    c_init = initial_coyote_count.value #Initial coyotes population
    nr = rabbit_capacity.value # Capacity of rabbits
    mr = 20.0 # Magnitude of movement of rabbits in feet per day
    rr = rabbit_reproduction.value # Reproduction rate of rabbits (per day)
    dc = coyote_death_rate.value # Death rate of coyotes when there is no rabbit (per day)
    rc = coyote_birth_rate.value # Reproduction rate of coyotes (per day)
    cd = 20.0 # Radius for collision detection in feet
    mc = mr * 2 # Magnitude of movement of coyotes in feet per day
    cc = 3 # How many unburned rabbits a coyote can contain
    ic = 0.04 # Likelihood that an immigrant coyote will show up on any day
    ir = 0.1 #Likelihood that an immigrant rabbit will show up on any day
    cdsq = cd**2

    '''
    Created for CSE/ECE 6730 final project
    '''
    global rabbits, coyotes, rdata, cdata
    rabbits = []
    coyotes = []
    rdata = []
    cdata = []
    for i in range(r_init):
        ag = Rabbit()
        ag.birthday = 0
        ag.x = width * random()
        ag.y = height * random()
        rabbits.append(ag)

    for i in range(c_init):
        ag = Coyote()
        ag.unburned_rabbits = 0 #Born hungry
        ag.birthday = 0
        ag.x = width * random()
        ag.y = height * random()
        coyotes.append(ag)

    observe()

def update_one_unit_time(b):
    global rabbits, coyotes

    new_coyotes = []
    new_rabbits = []

    # Find the weighted center of mass for coyotes
    # Coyotes who see no rabbits will head in this direction
    center_x = width / 2.0
    center_y = height / 2.0
    for coyote in coyotes:
        sx = 0.0
        sy = 0.0
        n = 0
        sx += coyote.x * coyote.unburned_rabbits
        sy += coyote.y * coyote.unburned_rabbits
        n += coyote.unburned_rabbits
        if n > 0:
            center_x = sx / n
            center_y = sy / n

    # Immigrant coyote?
    if random() < ic:
            ag = Coyote()
            ag.unburned_rabbits = 1
            ag.birthday = 0 # FIXME
            ag.x = width * random()
            ag.y = height * random()
            new_coyotes.append(ag)

    # Immigrant rabbit?
    if random() < ir:
            ag = Rabbit()
            ag.birthday = 0 # FIXME
            ag.x = width * random()
            ag.y = height * random()
            new_rabbits.append(ag)

    # Move rabbits randomly
    for ag in rabbits:
        ag.x += uniform(-mr, mr)
        ag.y += uniform(-mr, mr)
        ag.x = width if ag.x > width else 0 if ag.x < 0 else ag.x
        ag.y = height if ag.y > height else 0 if ag.y < 0 else ag.y

    # Move coyotes to a nearby rabbit
    for coyote in coyotes:
        reachable_rabbits = [nb for nb in rabbits if (coyote.x - nb.x)**2 +(coyote.y - nb.y)**2 < mc**2]
        reachable_count = len(reachable_rabbits)

        # No reachable rabbits? Move toward fat coyotes
        if reachable_count == 0:
            coyote.x = 0.9 * coyote.x + 0.1 * center_x
            coyote.y = 0.9 * coyote.y + 0.1 * center_y
        else:
            if reachable_count ==  1:
                target = reachable_rabbits[0]
            if reachable_count > 1:
                target = reachable_rabbits[randint(0, reachable_count - 1)]

            coyote.x = target.x
            coyote.y = target.y

    # Feed the coyotes
    for coyote in coyotes:

        # Burn one rabbit
        coyote.unburned_rabbits = coyote.unburned_rabbits - 1

        # Find the rabbits that are nearby
        near_rabbits = [nb for nb in rabbits if (coyote.x - nb.x)**2 +(coyote.y - nb.y)**2 < cdsq]

        # How many rabbits will the coyote eat?
        to_eat = min(cc - coyote.unburned_rabbits, len(near_rabbits))

        # Eat them
        while to_eat > 0:
            rabbit_to_eat = near_rabbits[0]
            rabbits.remove(rabbit_to_eat)
            del near_rabbits[0]
            coyote.unburned_rabbits += 1
            to_eat = to_eat - 1

    # Kill hungry coyotes
    i = 0
    while i < len(coyotes):
        coyote = coyotes[i]

        # Empty belly for eight days? Coyote dies
        if coyote.unburned_rabbits < -8:
            del coyotes[i]
            continue

        # Less? Coyote has a chance
        if coyote.unburned_rabbits <= 0:
            if random() < dc:
                del coyotes[i]
            else:
                i += 1
        else:
            i += 1

    # Reproduce fed coyotes
    for coyote in coyotes:

        if coyote.unburned_rabbits > 0 and random() < rc:
            ag = Coyote()
            ag.unburned_rabbits = 0 #Born hungry
            ag.birthday = 0 # FIXME
            ag.x = coyote.x + 10.0 * random()
            ag.y = coyote.y + 10.0 * random()
            new_coyotes.append(ag)

    # Reproduce uneated rabbits
    growth_rate = 1 - len(rabbits)/nr;
    for rabbit in rabbits:
          if random() < rr * growth_rate:
            ag = Rabbit()
            ag.birthday = 0 # FIXME
            ag.x = rabbit.x + 10.0 * random()
            ag.y = rabbit.y + 10.0 * random()
            new_rabbits.append(ag)

    # Update arrays
    coyotes.extend(new_coyotes)
    rabbits.extend(new_rabbits)
    observe()

def observe():
    global agents, rdata, cdata, width, height
    #cla()
    clear_output()
    display(initial_rabbit_count, initial_coyote_count, rabbit_capacity, rabbit_reproduction, coyote_death_rate, coyote_birth_rate,button_init, button_step)

    fig = plt.figure(figsize=(20,10))
    ax = fig.add_subplot(121)
    #cla()

    rdata.append(len(rabbits))
    if len(rabbits) > 0:
        x = [ag.x for ag in rabbits]
        y = [ag.y for ag in rabbits]
        ax.plot(x,y,'k.')

    cdata.append(len(coyotes))
    if len(coyotes) > 0:
        x = [ag.x for ag in coyotes]
        y = [ag.y for ag in coyotes]
        ax.plot(x,y,'ro')
    ax.axis('image')
    ax.axis([0,width,0,height])
    summary = "Rabbits: {}  Coyotes: {}".format(len(rabbits), len(coyotes))

    ax = fig.add_subplot(122)
    #cla()
    ax.plot(rdata, label ='prey')
    ax.plot(cdata, label = 'predator')
    ax.set_title(summary)
    #print("hello")
    legend()

style = {'description_width': 'initial'}
initial_rabbit_count = widgets.IntText(description="Initial_rabbit_count", value=400, style=style)
initial_coyote_count = widgets.IntText(description="Initial_coyote_count", value=12, style=style)
rabbit_capacity = widgets.IntText(description="Rabbit_capacity",style=style, value=605)
rabbit_reproduction = widgets.FloatText(description="Rabbit_reproduction",style=style, value=0.1)
coyote_death_rate = widgets.FloatText(description="coyote_death_rate",style=style, value=0.07)
coyote_birth_rate = widgets.FloatText(description="coyote_birth_rate",style=style, value=0.03)
button_step = widgets.Button(description="Step")
button_init = widgets.Button(description="Init")
display(initial_rabbit_count, initial_coyote_count, rabbit_capacity, rabbit_reproduction, coyote_death_rate, coyote_birth_rate,button_init, button_step)

button_init.on_click(initialize)
button_step.on_click(update_one_unit_time)

```

### GUI Interaction

```python
from __future__ import print_function
from ipywidgets import interact, interactive, fixed, interact_manual
from IPython.display import display, clear_output
import ipywidgets as widgets
import matplotlib
matplotlib.use('TkAgg')
from pylab import *
import copy as cp
import pycxsimulator

# Supress warnings
# import sys
# oldstderr = sys.stderr
# sys.stderr = open('/dev/null', 'w')

# Give default values
r_init = 400 # Initial rabbit population
c_init = 12 #Initial coyotes population
nr = 605 # Capacity of rabbits
mr = 20.0 # Magnitude of movement of rabbits in feet per day
rr = 0.1 # Reproduction rate of rabbits (per day)
dc = 0.07 # Death rate of coyotes when there is no rabbit (per day)
rc = 0.03 # Reproduction rate of coyotes (per day)
cd = 20.0 # Radius for collision detection in feet
mc = mr * 2 # Magnitude of movement of coyotes in feet per day
cc = 3 # How many unburned rabbits a coyote can contain
ic = 0.04 # Likelihood that an immigrant coyote will show up on any day
ir = 0.1 #Likelihood that an immigrant rabbit will show up on any day
cdsq = cd**2

width = 500 # Width of pasture in feet
height = 500 # Height of pasture in feet

class Rabbit:
    pass

class Coyote:
    pass

def initialize():
    '''
    Created for CSE/ECE 6730 final project
    '''
    global rabbits, coyotes, rdata, cdata
    rabbits = []
    coyotes = []
    rdata = []
    cdata = []
    for i in range(r_init):
        ag = Rabbit()
        ag.birthday = 0
        ag.x = width * random()
        ag.y = height * random()
        rabbits.append(ag)

    for i in range(c_init):
        ag = Coyote()
        ag.unburned_rabbits = 0 #Born hungry
        ag.birthday = 0
        ag.x = width * random()
        ag.y = height * random()
        coyotes.append(ag)

def update_one_unit_time():
    global rabbits, coyotes

    new_coyotes = []
    new_rabbits = []

    # Find the weighted center of mass for coyotes
    # Coyotes who see no rabbits will head in this direction
    center_x = width / 2.0
    center_y = height / 2.0
    for coyote in coyotes:
        sx = 0.0
        sy = 0.0
        n = 0
        sx += coyote.x * coyote.unburned_rabbits
        sy += coyote.y * coyote.unburned_rabbits
        n += coyote.unburned_rabbits
        if n > 0:
            center_x = sx / n
            center_y = sy / n

    # Immigrant coyote?
    if random() < ic:
            ag = Coyote()
            ag.unburned_rabbits = 1
            ag.birthday = 0 # FIXME
            ag.x = width * random()
            ag.y = height * random()
            new_coyotes.append(ag)

    # Immigrant rabbit?
    if random() < ir:
            ag = Rabbit()
            ag.birthday = 0 # FIXME
            ag.x = width * random()
            ag.y = height * random()
            new_rabbits.append(ag)

    # Move rabbits randomly
    for ag in rabbits:
        ag.x += uniform(-mr, mr)
        ag.y += uniform(-mr, mr)
        ag.x = width if ag.x > width else 0 if ag.x < 0 else ag.x
        ag.y = height if ag.y > height else 0 if ag.y < 0 else ag.y

    # Move coyotes to a nearby rabbit
    for coyote in coyotes:
        reachable_rabbits = [nb for nb in rabbits if (coyote.x - nb.x)**2 +(coyote.y - nb.y)**2 < mc**2]
        reachable_count = len(reachable_rabbits)

        # No reachable rabbits? Move toward fat coyotes
        if reachable_count == 0:
            coyote.x = 0.9 * coyote.x + 0.1 * center_x
            coyote.y = 0.9 * coyote.y + 0.1 * center_y
        else:
            if reachable_count ==  1:
                target = reachable_rabbits[0]
            if reachable_count > 1:
                target = reachable_rabbits[randint(0, reachable_count - 1)]

            coyote.x = target.x
            coyote.y = target.y

    # Feed the coyotes
    for coyote in coyotes:

        # Burn one rabbit
        coyote.unburned_rabbits = coyote.unburned_rabbits - 1

        # Find the rabbits that are nearby
        near_rabbits = [nb for nb in rabbits if (coyote.x - nb.x)**2 +(coyote.y - nb.y)**2 < cdsq]

        # How many rabbits will the coyote eat?
        to_eat = min(cc - coyote.unburned_rabbits, len(near_rabbits))

        # Eat them
        while to_eat > 0:
            rabbit_to_eat = near_rabbits[0]
            rabbits.remove(rabbit_to_eat)
            del near_rabbits[0]
            coyote.unburned_rabbits += 1
            to_eat = to_eat - 1

    # Kill hungry coyotes
    i = 0
    while i < len(coyotes):
        coyote = coyotes[i]

        # Empty belly for eight days? Coyote dies
        if coyote.unburned_rabbits < -8:
            del coyotes[i]
            continue

        # Less? Coyote has a chance
        if coyote.unburned_rabbits <= 0:
            if random() < dc:
                del coyotes[i]
            else:
                i += 1
        else:
            i += 1

    # Reproduce fed coyotes
    for coyote in coyotes:

        if coyote.unburned_rabbits > 0 and random() < rc:
            ag = Coyote()
            ag.unburned_rabbits = 0 #Born hungry
            ag.birthday = 0 # FIXME
            ag.x = coyote.x + 10.0 * random()
            ag.y = coyote.y + 10.0 * random()
            new_coyotes.append(ag)

    # Reproduce uneated rabbits
    growth_rate = 1 - len(rabbits)/nr;
    for rabbit in rabbits:
          if random() < rr * growth_rate:
            ag = Rabbit()
            ag.birthday = 0 # FIXME
            ag.x = rabbit.x + 10.0 * random()
            ag.y = rabbit.y + 10.0 * random()
            new_rabbits.append(ag)

    # Update arrays
    coyotes.extend(new_coyotes)
    rabbits.extend(new_rabbits)

def observe():
    global agents, rdata, cdata, width, height
    cla()

    subplot(1, 2 , 1)
    cla()

    rdata.append(len(rabbits))
    if len(rabbits) > 0:
        x = [ag.x for ag in rabbits]
        y = [ag.y for ag in rabbits]
        plot(x,y,'k.')

    cdata.append(len(coyotes))
    if len(coyotes) > 0:
        x = [ag.x for ag in coyotes]
        y = [ag.y for ag in coyotes]
        plot(x,y,'ro')
    axis('image')
    axis([0,width,0,height])
    summary = "Rabbits: {}  Coyotes: {}".format(len(rabbits), len(coyotes))

    p = subplot(1, 2, 2)
    cla()
    plot(rdata, label ='prey')
    plot(cdata, label = 'predator')
    p.set_title(summary)

    legend()

# Parameter setters
def initial_rabbit_count (val = r_init):
    '''
    Initial rabbit population
    '''
    global r_init
    r_init = int(val) # or int(val), str(val), etc.
    return val

def initial_coyote_count (val = c_init):
    '''
    Initial coyote population
    '''
    global c_init
    c_init = int(val) # or int(val), str(val), etc.
    return val

def rabbit_capacity (val = nr):
    '''
    Max rabbit population
    '''
    global nr
    nr = int(val) # or int(val), str(val), etc.
    return val

def rabbit_reproduction (val = rr ):
    '''
    Probability that a rabbit will give birth any particular day
    '''
    global r
    rr = float(val) # or int(val), str(val), etc.
    return val

def coyote_death_rate (val = dc ):
    '''
    Probability that a coyote will die on a day it is not near any rabbits
    '''
    global dc
    dc = float(val) # or int(val), str(val), etc.
    return val

def coyote_birth_rate (val = rc):
    '''
    Probability that a coyote will give birth on any particular day
    '''
    global rc
    rc = float(val) # or int(val), str(val), etc.
    return val

pycxsimulator.GUI(parameterSetters = [initial_rabbit_count, initial_coyote_count, rabbit_capacity, rabbit_reproduction, coyote_death_rate, coyote_birth_rate]).start(func = [initialize, observe, update_one_unit_time])

```

```python

```

```python

```

```python

```


Content under Creative Commons Attribution license CC-BY 4.0, code under BSD 3-Clause License © 2016 L.A. Barba, N.C. Clementi, G.F. Forsyth.  Based on https://github.com/numerical-mooc/numerical-mooc/blob/master/lessons/05_relax/05_01_2D.Laplace.Equation.ipynb also under CC-BY and MIT licenses, 2015.
# Relax and hold steady

Many problems in physics have no time dependence, yet are rich with physical meaning: the gravitational field produced by a massive object, the electrostatic potential of a charge distribution, the displacement of a stretched membrane and the steady flow of fluid through a porous medium ... all these can be modeled by **Poisson's equation**:

\begin{equation}
\nabla^2 u = f
\end{equation}

where the unknown $u$ and the known $f$ are functions of space, in a domain $\Omega$. To find the solution, we require boundary conditions. These could be  Dirichlet boundary conditions, specifying the value of the solution on the boundary,

\begin{equation}
u = b_1 \text{ on } \partial\Omega,
\end{equation}

or Neumann boundary conditions, specifying the normal derivative of the solution on the boundary,

\begin{equation}
\frac{\partial u}{\partial n} = b_2 \text{ on } \partial\Omega.
\end{equation}

A boundary-value problem consists of finding $u$, given the above information. Numerically, we can do this using *relaxation methods*, which start with an initial guess for $u$ and then iterate towards the solution. Let's find out how!

## Laplace's equation

The particular case of $f=0$ (homogeneous case) results in Laplace's equation:

\begin{equation}
\nabla^2 u = 0
\end{equation}

For example, the equation for steady, two-dimensional heat conduction is:

\begin{equation}
\frac{\partial ^2 T}{\partial x^2} + \frac{\partial ^2 T}{\partial y^2} = 0
\end{equation}

where $T$ is a temperature that has reached steady state. The Laplace equation models the equilibrium state of a system under the supplied boundary conditions.

The study of solutions to Laplace's equation is called *potential theory*, and the solutions themselves are often potential fields. Let's use $p$ from now on to represent our generic dependent variable, and write Laplace's equation again (in two dimensions):

\begin{equation}
\frac{\partial ^2 p}{\partial x^2} + \frac{\partial ^2 p}{\partial y^2} = 0
\end{equation}

Like in the diffusion equation, we discretize the second-order derivatives with *central differences*. If you need to refresh your mind, check out this [lesson](http://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_03_1DDiffusion.ipynb) and try to discretize the equation by yourself. On a two-dimensional Cartesian grid, it gives:

\begin{equation}
\frac{p_{i+1, j}  - 2p_{i,j}  + p_{i-1,j} }{\Delta x^2} + \frac{p_{i,j+1}  - 2p_{i,j}  + p_{i, j-1} }{\Delta y^2} = 0
\end{equation}

When $\Delta x = \Delta y$, we end up with the following equation:

\begin{equation}
p_{i+1, j}   + p_{i-1,j} + p_{i,j+1}  + p_{i, j-1}- 4 p_{i,j} = 0
\end{equation}

This tells us that the Laplacian differential operator at grid point $(i,j)$ can be evaluated discretely using the value of $p$ at that point (with a factor $-4$) and the four neighboring points to the left and right, above and below grid point $(i,j)$.

The stencil of the discrete Laplacian operator is shown in Figure 1. It is typically called the *five-point stencil*, for obvious reasons.

#### Figure 1: Laplace five-point stencil.

The discrete equation above is valid for every interior point in the domain. If we write the equations for *all* interior points, we have a linear system of algebraic equations. We *could* solve the linear system directly (e.g., with Gaussian elimination), but we can be more clever than that!

Notice that the coefficient matrix of such a linear system has mostly zeroes. For a uniform spatial grid, the matrix is *block diagonal*: it has diagonal blocks that are tridiagonal with $-4$ on the main diagonal and $1$ on two off-center diagonals, and two more diagonals with $1$. All of the other elements are zero. Iterative methods are particularly suited for a system with this structure, and save us from storing all those zeroes.

We will start with an initial guess for the solution, $p_{i,j}^{0}$, and use the discrete Laplacian to get an update, $p_{i,j}^{1}$, then continue on computing $p_{i,j}^{k}$ until we're happy.  Note that $k$ is _not_ a time index here, but an index corresponding to the number of iterations we perform in the *relaxation scheme*.

At each iteration, we compute updated values $p_{i,j}^{k+1}$ in a (hopefully) clever way so that they converge to a set of values satisfying Laplace's equation. The system will reach equilibrium only as the number of iterations tends to $\infty$, but we can approximate the equilibrium state by iterating until the change between one iteration and the next is *very* small.

The most intuitive method of iterative solution is known as the [**Jacobi method**](https://en.wikipedia.org/wiki/Jacobi_method), in which the values at the grid points are replaced by the corresponding weighted averages:

\begin{equation}
p^{k+1}_{i,j} = \frac{1}{4} \left(p^{k}_{i,j-1} + p^k_{i,j+1} + p^{k}_{i-1,j} + p^k_{i+1,j} \right)
\end{equation}

This method does indeed converge to the solution of Laplace's equation. Thank you Professor Jacobi!

##### Challenge task

Grab a piece of paper and write out the coefficient matrix for a discretization with 7 grid points in the $x$ direction (5 interior points) and 5 points in the $y$ direction (3 interior). The system should have 15 unknowns, and the coefficient matrix three diagonal blocks. Assume prescribed Dirichlet boundary conditions on all sides (not necessarily zero).

### Boundary conditions and relaxation

Suppose we want to model steady-state heat transfer on (say) a computer chip with one side insulated (zero Neumann BC), two sides held at a fixed temperature (Dirichlet condition) and one side touching a component that has a sinusoidal distribution of temperature.
We would need to solve Laplace's equation with boundary conditions like

\begin{equation}
  \begin{gathered}
p=0 \text{ at } x=0\\
\frac{\partial p}{\partial x} = 0 \text{ at } x = L\\
p = 0 \text{ at }y = 0 \\
p = \sin \left(  \frac{\frac{3}{2}\pi x}{L} \right) \text{ at } y = H.
  \end{gathered}
\end{equation}

We'll take $L=1$ and $H=1$ for the sizes of the domain in the $x$ and $y$ directions.

One of the defining features of elliptic PDEs is that they are "driven" by the boundary conditions.  In the iterative solution of Laplace's equation, boundary conditions are set and **the solution relaxes** from an initial guess to join the boundaries together smoothly, given those conditions.  Our initial guess will be $p=0$ everywhere. Now, let's relax!

First, we import our usual smattering of libraries (plus a few new ones!)

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

To visualize 2D data, we can use [`pyplot.imshow()`](http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.imshow), but a 3D plot can sometimes show a more intuitive view the solution. Or it's just prettier!

Be sure to enjoy the many examples of 3D plots in the `mplot3d` section of the [Matplotlib Gallery](http://matplotlib.org/gallery.html#mplot3d).

We'll import the `Axes3D` library from Matplotlib and also grab the `cm` package, which provides different colormaps for visualizing plots.

```python
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
```

Let's define a function for setting up our plotting environment, to avoid repeating this set-up over and over again. It will save us some typing.

```python
def plot_3D(x, y, p):
    '''Creates 3D plot with appropriate limits and viewing angle

    Parameters:
    ----------
    x: array of float
        nodal coordinates in x
    y: array of float
        nodal coordinates in y
    p: 2D array of float
        calculated potential field

    '''
    fig = pyplot.figure(figsize=(11,7), dpi=100)
    ax = fig.gca(projection='3d')
    X,Y = numpy.meshgrid(x,y)
    surf = ax.plot_surface(X,Y,p[:], rstride=1, cstride=1, cmap=cm.viridis,
            linewidth=0, antialiased=False)

    ax.set_xlim(0,1)
    ax.set_ylim(0,1)
    ax.set_xlabel('$x$')
    ax.set_ylabel('$y$')
    ax.set_zlabel('$z$')
    ax.view_init(30,45)

```

##### Note
This plotting function uses *Viridis*, a new (and _awesome_) colormap available in Matplotlib versions 1.5 and greater.  If you see an error when you try to plot using <tt>cm.viridis</tt>, just update Matplotlib using <tt>conda</tt> or <tt>pip</tt>.

### Analytical solution

The Laplace equation with the boundary conditions listed above has an analytical solution, given by

\begin{equation}
p(x,y) = \frac{\sinh \left( \frac{\frac{3}{2} \pi y}{L}\right)}{\sinh \left(  \frac{\frac{3}{2} \pi H}{L}\right)} \sin \left( \frac{\frac{3}{2} \pi x}{L} \right)
\end{equation}

where $L$ and $H$ are the length of the domain in the $x$ and $y$ directions, respectively.

We will use [`numpy.meshgrid`](http://docs.scipy.org/doc/numpy/reference/generated/numpy.meshgrid.html) to plot our 2D solutions. This is a function that takes two vectors (`x` and `y`, say) and returns two 2D arrays of $x$ and $y$ coordinates that we then use to create the contour plot. Always useful, [`linspace`](https://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.linspace.html) creates 1-row arrays of equally spaced numbers: it helps for defining $x$ and $y$ axes in line plots, but now we want the analytical solution plotted for every point in our domain.  To do this, we'll use in the analytical solution the 2D arrays generated by `numpy.meshgrid`.

```python
def p_analytical(x, y):
    X, Y = numpy.meshgrid(x,y)

    p_an = numpy.sinh(1.5*numpy.pi*Y / x[-1]) /\
    (numpy.sinh(1.5*numpy.pi*y[-1]/x[-1]))*numpy.sin(1.5*numpy.pi*X/x[-1])

    return p_an
```

Ok, let's try out the analytical solution and use it to test the `plot_3D` function we wrote above.

```python
nx = 41
ny = 41

x = numpy.linspace(0,1,nx)
y = numpy.linspace(0,1,ny)

p_an = p_analytical(x,y)
```

```python
plot_3D(x,y,p_an)
```

It worked!  This is what the solution *should* look like when we're 'done' relaxing. (And isn't viridis a cool colormap?)

### How long do we iterate?

We noted above that there is no time dependence in the Laplace equation.  So it doesn't make a lot of sense to use a `for` loop with `nt` iterations.

Instead, we can use a `while` loop that continues to iteratively apply the relaxation scheme until the difference between two successive iterations is small enough.

But how small is small enough?  That's a good question.  We'll try to work that out as we go along.

To compare two successive potential fields, a good option is to use the [L2 norm][1] of the difference.  It's defined as

\begin{equation}
|\textbf{x}| = \sqrt{\sum_{i=0, j=0}^k \left|p^{k+1}_{i,j} - p^k_{i,j}\right|^2}
\end{equation}

But there's one flaw with this formula.  We are summing the difference between successive iterations at each point on the grid. So what happens when the grid grows? (For example, if we're refining the grid, for whatever reason.) There will be more grid points to compare and so more contributions to the sum. The norm will be a larger number just because of the grid size!

That doesn't seem right.  We'll fix it by normalizing the norm, dividing the above formula by the norm of the potential field at iteration $k$.

For two successive iterations, the relative L2 norm is then calculated as

\begin{equation}
|\textbf{x}| = \frac{\sqrt{\sum_{i=0, j=0}^k \left|p^{k+1}_{i,j} - p^k_{i,j}\right|^2}}{\sqrt{\sum_{i=0, j=0}^k \left|p^k_{i,j}\right|^2}}
\end{equation}

Our Python code for this calculation is a one-line function:

[1]:http://en.wikipedia.org/wiki/Norm_%28mathematics%29#Euclidean_norm

```python
def L2_error(p, pn):
    return numpy.sqrt(numpy.sum((p - pn)**2)/numpy.sum(pn**2))
```

Now, let's define a function that will apply Jacobi's method for Laplace's equation.  Three of the boundaries are Dirichlet boundaries and so we can simply leave them alone.  Only the Neumann boundary needs to be explicitly calculated at each iteration, and we'll do it by discretizing the derivative in its first order approximation:

```python
def laplace2d(p, l2_target):
    '''Iteratively solves the Laplace equation using the Jacobi method

    Parameters:
    ----------
    p: 2D array of float
        Initial potential distribution
    l2_target: float
        target for the difference between consecutive solutions

    Returns:
    -------
    p: 2D array of float
        Potential distribution after relaxation
    '''

    l2norm = 1
    pn = numpy.empty_like(p)
    while l2norm > l2_target:
        pn = p.copy()
        p[1:-1,1:-1] = .25 * (pn[1:-1,2:] + pn[1:-1, :-2] \
                              + pn[2:, 1:-1] + pn[:-2, 1:-1])

        ##Neumann B.C. along x = L
        p[1:-1, -1] = p[1:-1, -2]     # 1st order approx of a derivative
        l2norm = L2_error(p, pn)

    return p
```

##### Rows and columns, and index order
The physical problem has two dimensions, so we also store the temperatures in two dimensions: in a 2D array.

We chose to store it with the $y$ coordinates corresponding to the rows of the array and $x$ coordinates varying with the columns (this is just a code design decision!). If we are consistent with the stencil formula (with $x$ corresponding to index $i$ and $y$ to index $j$), then $p_{i,j}$ will be stored in array format as `p[j,i]`.

This might be a little confusing as most of us are used to writing coordinates in the format $(x,y)$, but our preference is to have the data stored so that it matches the physical orientation of the problem. Then, when we make a plot of the solution, the visualization will make sense to us, with respect to the geometry of our set-up. That's just nicer than to have the plot rotated!

#### Figure 2: Row-column data storage

As you can see on Figure 2 above, if we want to access the value $18$ we would write those coordinates as $(x_2, y_3)$.  You can also see that its location is the 3rd row, 2nd column, so its array address would be `p[3,2]`.

Again, this is a design decision.  However you can choose to manipulate and store your data however you like; just remember to be consistent!

### Let's relax!

The initial values of the potential field are zero everywhere (initial guess), except at the boundary:

$$p = \sin \left(  \frac{\frac{3}{2}\pi x}{L} \right) \text{ at } y=H$$

To initialize the domain, `numpy.zeros` will handle everything except that one Dirichlet condition. Let's do it!

```python
##variable declarations
nx = 41
ny = 41

##initial conditions
p = numpy.zeros((ny,nx)) ##create a XxY vector of 0's

##plotting aids
x = numpy.linspace(0,1,nx)
y = numpy.linspace(0,1,ny)

##Dirichlet boundary conditions
p[-1,:] = numpy.sin(1.5*numpy.pi*x/x[-1])

```

Now let's visualize the initial conditions using the `plot_3D` function, just to check we've got it right.

```python
plot_3D(x, y, p)
```

The `p` array is equal to zero everywhere, except along the boundary $y = 1$.  Hopefully you can see how the relaxed solution and this initial condition are related.

Now, run the iterative solver with a target L2-norm difference between successive iterations of $10^{-8}$.

```python
p = laplace2d(p.copy(), 1e-8)
```

Let's make a gorgeous plot of the final field using the newly minted `plot_3D` function.

```python
plot_3D(x,y,p)
```

Awesome!  That looks pretty good.  But we'll need more than a simple visual check, though. The "eyeball metric" is very forgiving!

## Convergence analysis

### Convergence, Take 1

We want to make sure that our Jacobi function is working properly.  Since we have an analytical solution, what better way than to do a grid-convergence analysis?  We will run our solver for several grid sizes and look at how fast the L2 norm of the difference between consecutive iterations decreases.

Let's make our lives easier by writing a function to "reset" the initial guess for each grid so we don't have to keep copying and pasting them.

```python
def laplace_IG(nx):
    '''Generates initial guess for Laplace 2D problem for a
    given number of grid points (nx) within the domain [0,1]x[0,1]

    Parameters:
    ----------
    nx: int
        number of grid points in x (and implicitly y) direction

    Returns:
    -------
    p: 2D array of float
        Pressure distribution after relaxation
    x: array of float
        linspace coordinates in x
    y: array of float
        linspace coordinates in y
    '''

    ##initial conditions
    p = numpy.zeros((nx,nx)) ##create a XxY vector of 0's

    ##plotting aids
    x = numpy.linspace(0,1,nx)
    y = x

    ##Dirichlet boundary conditions
    p[:,0] = 0
    p[0,:] = 0
    p[-1,:] = numpy.sin(1.5*numpy.pi*x/x[-1])

    return p, x, y
```

Now run Jacobi's method on the Laplace equation using four different grids, with the same exit criterion of $10^{-8}$ each time. Then, we look at the error versus the grid size in a log-log plot. What do we get?

```python
nx_values = [11, 21, 41, 81]
l2_target = 1e-8

error = numpy.empty_like(nx_values, dtype=numpy.float)

for i, nx in enumerate(nx_values):
    p, x, y = laplace_IG(nx)

    p = laplace2d(p.copy(), l2_target)

    p_an = p_analytical(x, y)

    error[i] = L2_error(p, p_an)

```

```python
pyplot.figure(figsize=(6,6))
pyplot.grid(True)
pyplot.xlabel(r'$n_x$', fontsize=18)
pyplot.ylabel(r'$L_2$-norm of the error', fontsize=18)

pyplot.loglog(nx_values, error, color='k', ls='--', lw=2, marker='o')
pyplot.axis('equal');
```

Hmm. That doesn't look like 2nd-order convergence, but we're using second-order finite differences.  *What's going on?*  The culprit is the boundary conditions. Dirichlet conditions are order-agnostic (a  set value is a set value), but the scheme we used for the Neumann boundary condition is 1st-order.

Remember when we said that the boundaries drive the problem?  One boundary that's 1st-order completely tanked our spatial convergence.  Let's fix it!

### 2nd-order Neumann BCs

Up to this point, we have used the first-order approximation of a derivative to satisfy Neumann B.C.'s. For a boundary located at $x=0$ this reads,

\begin{equation}
\frac{p^{k+1}_{1,j} - p^{k+1}_{0,j}}{\Delta x} = 0
\end{equation}

which, solving for $p^{k+1}_{0,j}$ gives us

\begin{equation}
p^{k+1}_{0,j} = p^{k+1}_{1,j}
\end{equation}

Using that Neumann condition will limit us to 1st-order convergence.  Instead, we can start with a 2nd-order approximation (the central-difference approximation):

\begin{equation}
\frac{p^{k+1}_{1,j} - p^{k+1}_{-1,j}}{2 \Delta x} = 0
\end{equation}

That seems problematic, since there is no grid point $p^{k}_{-1,j}$.  But no matter … let's carry on. According to the 2nd-order approximation,

\begin{equation}
p^{k+1}_{-1,j} = p^{k+1}_{1,j}
\end{equation}

Recall the finite-difference Jacobi equation with $i=0$:

\begin{equation}
p^{k+1}_{0,j} = \frac{1}{4} \left(p^{k}_{0,j-1} + p^k_{0,j+1} + p^{k}_{-1,j} + p^k_{1,j} \right)
\end{equation}

Notice that the equation relies on the troublesome (nonexistent) point $p^k_{-1,j}$, but according to the equality just above, we have a value we can substitute, namely $p^k_{1,j}$. Ah! We've completed the 2nd-order Neumann condition:

\begin{equation}
p^{k+1}_{0,j} = \frac{1}{4} \left(p^{k}_{0,j-1} + p^k_{0,j+1} + 2p^{k}_{1,j} \right)
\end{equation}

That's a bit more complicated than the first-order version, but it's relatively straightforward to code.

##### Note

Do not confuse $p^{k+1}_{-1,j}$ with <tt>p[-1]</tt>:
<tt>p[-1]</tt> is a piece of Python code used to refer to the last element of a list or array named <tt>p</tt>.  $p^{k+1}_{-1,j}$ is a 'ghost' point that describes a position that lies outside the actual domain.

### Convergence, Take 2

We can copy the previous Jacobi function and replace only the line implementing the Neumann boundary condition.

##### Careful!
Remember that our problem has the Neumann boundary located at $x = L$ and not $x = 0$ as we assumed in the derivation above.

```python
def laplace2d_neumann(p, l2_target):
    '''Iteratively solves the Laplace equation using the Jacobi method
    with second-order Neumann boundary conditions

    Parameters:
    ----------
    p: 2D array of float
        Initial potential distribution
    l2_target: float
        target for the difference between consecutive solutions

    Returns:
    -------
    p: 2D array of float
        Potential distribution after relaxation
    '''

    l2norm = 1
    pn = numpy.empty_like(p)
    while l2norm > l2_target:
        pn = p.copy()
        p[1:-1,1:-1] = .25 * (pn[1:-1,2:] + pn[1:-1, :-2] \
                              + pn[2:, 1:-1] + pn[:-2, 1:-1])

        ##2nd-order Neumann B.C. along x = L
        p[1:-1,-1] = .25 * (2*pn[1:-1,-2] + pn[2:, -1] + pn[:-2, -1])

        l2norm = L2_error(p, pn)

    return p
```

Again, this is the exact same code as before, but now we're running the Jacobi solver with a 2nd-order Neumann boundary condition.  Let's do a grid-refinement analysis, and plot the error versus the grid spacing.

```python
nx_values = [11, 21, 41, 81]
l2_target = 1e-8

error = numpy.empty_like(nx_values, dtype=numpy.float)

for i, nx in enumerate(nx_values):
    p, x, y = laplace_IG(nx)

    p = laplace2d_neumann(p.copy(), l2_target)

    p_an = p_analytical(x, y)

    error[i] = L2_error(p, p_an)
```

```python
pyplot.figure(figsize=(6,6))
pyplot.grid(True)
pyplot.xlabel(r'$n_x$', fontsize=18)
pyplot.ylabel(r'$L_2$-norm of the error', fontsize=18)

pyplot.loglog(nx_values, error, color='k', ls='--', lw=2, marker='o')
pyplot.axis('equal');
```

Nice!  That's much better.  It might not be *exactly* 2nd-order, but it's awfully close. (What is ["close enough"](http://ianhawke.github.io/blog/close-enough.html) in regards to observed convergence rates is a thorny question.)

Now, notice from this plot that the error on the finest grid is around $0.0002$. Given this, perhaps we didn't need to continue iterating until a target difference between two solutions of $10^{-8}$. The spatial accuracy of the finite difference approximation is much worse than that! But we didn't know it ahead of time, did we? That's the "catch 22" of iterative solution of systems arising from discretization of PDEs.

## Final word

The Jacobi method is the simplest relaxation scheme to explain and to apply. It is also the *worst* iterative solver! In practice, it is seldom used on its own as a solver, although it is useful as a smoother with multi-grid methods. There are much better iterative methods! If you are curious you can find them in this [lesson](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/05_relax/05_03_Iterate.This.ipynb).

## Solve using linear system (*)

(*) Based on Claudio Torres' implementation for Scientific Computing course. Only for Dirichlet's border conditions.

```python
import numpy as np
from numpy.linalg import solve
```

```python
# Define global indexing
def index(i, j, nCols=(Ny+1)):
    return j + i*nCols

def elliptic_PDE(Nx, Ny, f, g1, g2, g3, g4, x_dom=(0, 1), y_dom=(0, 1)):

    # Discretize x and y
    x = np.linspace(x_dom[0], x_dom[1], Nx+1)
    y = np.linspace(y_dom[0], y_dom[1], Ny+1)

    # Define the discretization parameters
    dx = x[1]-x[0]
    dy = y[1]-y[0]

    # Create the matrix and the right hand size vector
    A = np.zeros([(Nx+1)*(Ny+1), (Nx+1)*(Ny+1)])
    b = np.zeros([(Nx+1)*(Ny+1), 1])

    for i in range(Nx+1):
        for j in range(Ny+1):

            k = index(i,j)

            if j==0: # y=ymin
                A[k,k] = 1.
                b[k] = g1(x[i])

            elif i==Nx: # x=xmax
                A[k,k] = 1.
                b[k] = g2(y[j])

            elif j==Ny: # y=ymax
                A[k,k] = 1.
                b[k] = g3(x[i])

            elif i==0: # x=xmin
                A[k,k] = 1.
                b[k] = g4(y[j])
            else:
                A[k, k] = -2./dx**2 - 2./dy**2
                A[k,index(i+1,j)] = 1./dx**2
                A[k,index(i-1,j)] = 1./dx**2
                A[k,index(i,j-1)] = 1./dy**2
                A[k,index(i,j+1)] = 1./dy**2

                b[k] = f(x[i], y[j])

    # Solve the linear system
    w = solve(A, b)

    return x, y, w.reshape(Nx+1, Ny+1).T
```

```python
L = 1

# Define Boundary Conditions
f = lambda x,y : np.sin(x)*np.exp(-y**2)
g1 = lambda x : np.cos(x) # u(x,0)
g2 = lambda x : np.exp(x**2) # u(x_N, y)
g3 = lambda x : np.sin(1.5*np.pi*x/L) #u(Y_n, x)
g4 = lambda x: -1/np.cos(x) # u(0, y)

# Define grid discretization
Nx, Ny = 20, 10

# Solve equation
x, y, u = elliptic_PDE(Nx, Ny, f, g1, g2, g3, g4, x_dom=(0, 1), y_dom=(0, 1))
```

```python
plot_3D(x, y, u)
```

```python
#Ignore this cell, It simply loads a style for the notebook.

from IPython.core.display import HTML
def css_styling():
    try:
        styles = open("../styles/custom.css", "r").read()
        return HTML(styles)
    except:
        pass
css_styling()
```


```python
def symbolize(s):
    """
    Converts a a string (equation) to a SymPy symbol object
    """
    from sympy import sympify
    s1=s.replace('.','*')
    s2=s1.replace('^','**')
    s3=sympify(s2)

    return(s3)
```

```python
def eval_multinomial(s,vals=None,symbolic_eval=False):
    """
    Evaluates polynomial at vals.
    vals can be simple list, dictionary, or tuple of values.
    vals can also contain symbols instead of real values provided those symbols have been declared before using SymPy
    """
    from sympy import Symbol
    sym_s=symbolize(s)
    sym_set=sym_s.atoms(Symbol)
    sym_lst=[]
    for s in sym_set:
        sym_lst.append(str(s))
    sym_lst.sort()
    if symbolic_eval==False and len(sym_set)!=len(vals):
        print("Length of the input values did not match number of variables and symbolic evaluation is not selected")
        return None
    else:
        if type(vals)==list:
            sub=list(zip(sym_lst,vals))
        elif type(vals)==dict:
            l=list(vals.keys())
            l.sort()
            lst=[]
            for i in l:
                lst.append(vals[i])
            sub=list(zip(sym_lst,lst))
        elif type(vals)==tuple:
            sub=list(zip(sym_lst,list(vals)))
        result=sym_s.subs(sub)

    return result
```

```python
def flip(y,p):
    import numpy as np
    lst=[]
    for i in range(len(y)):
        f=np.random.choice([1,0],p=[p,1-p])
        lst.append(f)
    lst=np.array(lst)
    return np.array(np.logical_xor(y,lst),dtype=int)
```

```python
def gen_classification_symbolic(m=None,n_samples=100,n_features=2,flip_y=0.0):
    """
    Generates classification sample based on a symbolic expression.
    Calculates the output of the symbolic expression at randomly generated (Gaussian distribution) points and
    assigns binary classification based on sign.
    m: The symbolic expression. Needs x1, x2, etc as variables and regular python arithmatic symbols to be used.
    n_samples: Number of samples to be generated
    n_features: Number of variables. This is automatically inferred from the symbolic expression. So this is ignored
                in case a symbolic expression is supplied. However if no symbolic expression is supplied then a
                default simple polynomial can be invoked to generate classification samples with n_features.
    flip_y: Probability of flipping the classification labels randomly. A higher value introduces more noise and make
            the classification problem harder.
    Returns a numpy ndarray with dimension (n_samples,n_features+1). Last column is the response vector.
    """

    import numpy as np
    from sympy import Symbol,sympify

    if m==None:
        m=''
        for i in range(1,n_features+1):
            c='x'+str(i)
            c+=np.random.choice(['+','-'],p=[0.5,0.5])
            m+=c
        m=m[:-1]
    sym_m=sympify(m)
    n_features=len(sym_m.atoms(Symbol))
    evals=[]
    lst_features=[]
    for i in range(n_features):
        lst_features.append(np.random.normal(scale=5,size=n_samples))
    lst_features=np.array(lst_features)
    lst_features=lst_features.T
    for i in range(n_samples):
        evals.append(eval_multinomial(m,vals=list(lst_features[i])))

    evals=np.array(evals)
    evals_binary=evals>0
    evals_binary=evals_binary.flatten()
    evals_binary=np.array(evals_binary,dtype=int)
    evals_binary=flip(evals_binary,p=flip_y)
    evals_binary=evals_binary.reshape(n_samples,1)

    lst_features=lst_features.reshape(n_samples,n_features)
    x=np.hstack((lst_features,evals_binary))

    return (x)
```

```python
x=gen_classification_symbolic(m='2*x1+3*x2+5*x3',n_samples=10,flip_y=0.0)
```

```python
import pandas as pd
df=pd.DataFrame(x)
```

```python
df
```

      0
      1
      2
      3

      0
      1.993945
      1.116260
      2.316286
      1.0

      1
      1.230979
      4.672876
      9.164157
      1.0

      2
      -6.900166
      -4.564144
      0.344308
      0.0

      3
      1.556408
      3.551907
      7.105238
      1.0

      4
      -3.528923
      7.970909
      -3.765006
      0.0

      5
      2.237070
      3.920372
      2.548449
      1.0

      6
      -4.423419
      -0.609899
      -8.466901
      0.0

      7
      5.475675
      -7.311347
      3.444522
      1.0

      8
      7.752732
      -0.346992
      1.162572
      1.0

      9
      3.104909
      4.527720
      -4.397039
      0.0

```python
x=gen_classification_symbolic(m='12*x1/(x2+5*x3)',n_samples=10,flip_y=0.2)
df=pd.DataFrame(x)
df
```

      0
      1
      2
      3

      0
      1.192160
      -0.633254
      -0.530373
      0.0

      1
      1.838186
      -1.355400
      5.590450
      0.0

      2
      -7.882258
      3.829910
      -5.454632
      1.0

      3
      -0.475023
      0.827497
      3.000924
      0.0

      4
      1.386086
      3.123963
      -12.426151
      1.0

      5
      -11.121159
      -3.388428
      -5.406741
      1.0

      6
      2.322904
      -4.708977
      3.414145
      1.0

      7
      -4.734882
      -3.354211
      -2.332581
      1.0

      8
      0.586311
      -2.557310
      7.387032
      1.0

      9
      -0.531481
      10.494158
      -2.378411
      0.0

Symbolic regression classification generator.ipynb


# sympy!

Logo is from the [SymPy webpage](https://www.sympy.org/en/index.html)

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

%matplotlib inline

import sympy
```

```python
#seems optional; can make some of the output look better
sympy.init_printing(use_unicode=True)
```

From the [SymPy webpage](https://www.sympy.org/en/index.html):

> SymPy is a Python library for symbolic mathematics. It aims to become a full-featured computer algebra system (CAS) while keeping the code as simple as possible in order to be comprehensible and easily extensible. SymPy is written entirely in Python.

From the TAMU [Math page](https://www.math.tamu.edu/courses/math151/):
>### MATH 151 - Engineering Mathematics I - Fall 2021
>##### Credits 4. 3 Lecture Hours. 2 Lab Hours.
>
>(MATH 2413) Engineering Mathematics I. Rectangular coordinates, vectors, analytic geometry, functions, limits, derivatives of functions, applications, integration, computer algebra. MATH 171 designed to be a more demanding version of this course. Only one of the following will satisfy the requirements for a degree: MATH 131, MATH 142, MATH 147, MATH 151 and MATH 171.
>**Prerequisite**: MATH 150 or equivalent or acceptable score on TAMU Math Placement Exam; also taught at Galveston and Qatar campuses.

There are some helpful documents on the pages for that course. Take a look at these documents to familiarize yourself with what is going to covered and **how** they do it.

Here is a lab example from the course: https://www.math.tamu.edu/courses/math151/Python/Math151Lab1b.pdf

```python
s0 = 400
v0 = -16
t = 4
"st = (g/2)t^2 + v0t + s0"
g = -32

st = (g/2) * (t**2) + v0*t + s0

```

```python
print(st)
```

    80.0

### Make it a function
We're going to create a function that will save us from having to type in the formula each time.

*Note: if our equation/formula changes, we will have to redo the function too*

```python
def question1(s0, v0, t, g=-32):
    """Calculate the height of the ball"""
    st = (g/2) * (t**2) + v0*t + s0
    return st
```

Now we can test out our function by calling it with different values

```python
question1(400, -16, 4)
```

```python
question1(10, 400, 25)
```

### Importing the entire package
The sympy documentation and the MATH 151 Lab worksheet both demonstrate importing the entire package into the current namespace. If this is one of the only packages you'll be using, it will work just fine. However, if you are using multiple packages and import them all into here like below, you *could* end up clobbering, or overwriting, a different function with the same name.

That sounds scary but nothing is permanent. If that happens, just restart the Python kernel.

```python
from sympy import *
```

The above method of importing is generally frowned upon by the Python community. While it is a valid line, it can overwrite functions/classes if two packages use the same name for a function.

### Creating symbols
sympy uses symbols (hence the name....)

Before we can use these symbols we have to create them first. We can call our symbols anything we want but it's usually best to stick with simple, obvious names.

```python
x = symbols('x')
```

So let's call ours what the assignment calls them

```python
s0 = symbols('s0')
v0 = symbols('v0')
g = symbols('g')
t = symbols('t')
```

We can also accomplish the above line in one single line:

```python
s0, v0, g, t = symbols('s0 v0 g t')
```

Now let's see what that did

```python
s0 + v0 + 10  # 'fake' equation just to see the symbols
```

Notice how the output above looks a little different than the one below here

```python
print(s0 + v0+10)
```

    s0 + v0 + 10

Using the print statement removes any formatting that sympy has applied to the symbol objects we created.

### Creating equations/formulas
Let's use those symbols to create our equation from above. Sympy calls these expressions but I'll call them equations/formulas here.

```python
height = ((g/2) * t**2) + v0*t + s0
```

```python
height
```

This looks a little different from the equation on our sheet right? Is this one wrong?

No, it turns out that sympy took it upon itself to 'simplify' the equation and move *t2* to be with *g*

### Using the equation
Now we want to input values for the different variables in that equation and obtain a result.

To do this, we use the subs method

```python
height.subs([(s0, 400), [v0, -16], [t,4], [g, -32]])
```

Important to note that our original equation is unmodified. The substitution that we did was temporary.

```python
height # remains unmodified
```

```python
# now with different values
height.subs([(s0, 10), [v0, 400], [t,25], [g, -32]])
```

We can also use loops to input values so that we don't need to manually type everything in, every single time we want to change the values.

```python
# create a list of lists with values for each variable
values = [
    [400, -16, 4, -32],
    [10, 400, 25, -32]
]
```

```python
for inputs in values:
    answer = height.subs([(s0, inputs[0]), [v0, inputs[1]], [t,inputs[2]], [g, inputs[3]]])
    print(answer)
```

    80
    10

We can use list comprehensions to complete the same process. List comprehensions can sometimes be faster and clearer (and sometimes NOT!)

```python
[height.subs([(s0, inputs[0]), [v0, inputs[1]], [t,inputs[2]], [g, inputs[3]]])
for inputs in values]
```

While the above line works and displays the results, it does **not** save the results and so we cannot do anything with them later. To *save* the results, we need to catch the returned results.

```python
answers = [height.subs([(s0, inputs[0]), [v0, inputs[1]], [t,inputs[2]], [g, inputs[3]]])
for inputs in values]
```

```python
answers
```

```python
# get the first answer
answers[0]
```

What about the next function?

sin(ex)

Let's try a different way to 'encode' our equation without typing it out explicitly using symbols.

To do this we'll use the sympify function (get it?)

 sympy.sympify('my_equation')

```python
function = sympy.sympify('x+1')
```

```python
function
```

Great! Now let's code in our next one

```python
function = sympy.sympify('sin(e**x)')
```

```python
function
```

Looks right. Let's try it:

```python
a = (0, function.subs(x, 0).evalf())
```

```python
a
```

That worked. Let's try another number for x

```python
# this way may sometimes not work with multiple variables
b = (1, function.subs(x,1).evalf())
print(b)
```

    (1, sin(e))

What happened here? Why didn't it work?

It turns out that when we typed `e` into our equation, it was interpreted as being a variable (symbol) called e. The code has no idea that `e` has a special meaning.

How do fix this? Use the exponential function instead

```python
function = sympy.sympify('sin(exp(x))')
function
```

Though it looks the same, it's 'different' under the hood.

Let's try our substitution from earlier

```python
# this way may sometimes not work with multiple variables
b = (1, function.subs(x,1).evalf())
print(b)
```

    (1, 0.410781290502909)

What's that `.evalf()` bit?

Sometimes you will need to tell sympy to run the calculation.

It doesn't 'complete' the calculation of sin(x) because it will output a floating point (decimal) number that is cutoff, and therefore not *exactly* equal sin(x). To avoid this, sympy maintains it as sin(x) unless we tell it to evaluate it.

If we just substituted it as we did earlier:

```python
function.subs(x,1)
```

We still have e in our result. To get a number we have to evaluate the function:

```python
# this way may sometimes not work with multiple variables
b = (1, function.subs(x,1).evalf())
print(b)
```

    (1, 0.410781290502909)

```python
# sympy documentation says this is the way to structure it
b = (1, function.evalf(subs={'x':1}))
print(b)
```

    (1, 0.410781290502909)

What if you have multiple variables? That curly bracket in the `subs=` part is a dictionary. We can continue to add values for different variables.

```python
big_eq = sympy.sympify('sin(a)+b+c')
big_eq
```

```python
big_eq.subs([(a,1),(b,2),(c,3)])
```

What happened here? Turns out we never defined a, b, and c to be symbols. I honestly thought the sympify function did that for use behind the scenes...

We can just use strings instead:

```python
big_eq.subs([('a',1),('b',2),('c',3)])
```

But notice how it maintains the result as sin(1) instead of a decimal number? That's where evalf comes in.

```python
big_eq.evalf(subs={'a':1,'b':2,'c':3})
```

Plus it looks a little cleaner. We could even make it easier on our eyes by doing this equivalent thing:

```python
big_eq.evalf(subs={'a':1,
                   'b':2,
                   'c':3 })
```

More examples:

```python
b = (1, function.evalf(subs={x:sympy.log(sympy.pi/2.)}))
print(b)
```

    (1, 1.00000000000000)

```python
sympy.log(100,10) #note this function uses the natural log by default
```

### Plotting
We can also plot functions using sympy and it ends up being easier to do and nicer looking without much effort.

```python
sympy.plot(sympy.sympify('x**3-25*x**2+3'), (x, -10, 10), ylim=(-5,10))
```

```python
made_up = x**3-25*x**2+3
```

```python
made_up
```

```python
made_up.subs(x, 0)
```

Let's go through a list of x values to obtain the y values and then we can plot those.
The `range()` function below will create our list of values for us.

```python
output = []
for value in range(-10, 11):
    output.append(made_up.subs(x, value))
```

```python
print(output
    )
```

    [-3497, -2751, -2109, -1565, -1113, -747, -461, -249, -105, -23, 3, -21, -89, -195, -333, -497, -681, -879, -1085, -1293, -1497]

```python
# same thing but in a list comprehension instead
output = [made_up.subs(x, value) for value in range(-10, 11)]
```

```python
print(output) #output should be the same
```

    [-3497, -2751, -2109, -1565, -1113, -747, -461, -249, -105, -23, 3, -21, -89, -195, -333, -497, -681, -879, -1085, -1293, -1497]

Now we can plot it

```python
plt.plot(output)
```

Why doesn't it look the same?

1. We didn't provide any x values so it added its own
2. We didn't give it any kind of boundaries so it plotted what it needed to

We can make it look similar.

```python
plt.plot(range(-10, 11), output)
plt.xlim(-10, 10)
plt.ylim(-5,10)
plt.grid()
```

But it's not exactly the same. We could go through the process of trying to recreate it OR we can simply use what sympy has provided.

Another reason to use the sympy version, it evaluates at more x values. What do I mean?

Look closely:

```python
# sympy
sympy.plot(sympy.sympify('x**3-25*x**2+3'), (x, -10, 10), ylim=(-5,10))

#ours
plt.figure(figsize=(7,5))
plt.plot(range(-10, 11), output)
plt.xlim(-11, 11)
plt.xticks(np.arange(-10, 11, 2.5))
plt.ylim(-5,10)
plt.grid()
```

Now zoom in near 3

```python
# sympy
sympy.plot(sympy.sympify('x**3-25*x**2+3'), (x, -1, 1), ylim=(1,4))

#ours
plt.figure(figsize=(7,5))
plt.plot(range(-10, 11), output)
plt.xlim(-1, 1)
plt.ylim(1,4)
plt.grid()
```

Notice how our data are 'pointed' and not a nice smooth curve? It's because we only evaluated our function at integer values.

### Expanding and factoring

```python
sympy.factor(made_up)
```

```python
test2 = (x+3)*(x**2-4)
```

```python
test2
```

```python
test2_expanded = sympy.expand(test2)
test2_expanded
```

```python
sympy.factor(test2_expanded)
```

### Points and Lines
Create a couple of instances of points

```python
p = sympy.Point(4,-2)
q = sympy.Point(-1,3)
```

Now let's create a line from those two points

```python
l = sympy.Line(p, q)
```

And now we can get things from this line:

```python
l.slope
```

How about angles between lines?

```python
p = sympy.Point(1,0)
q = sympy.Point(0,1)

origin = sympy.Point(0,0)

```

```python
plt.scatter([1,0,0], [0, 1, 0], marker='o')
plt.quiver([0,0], [0,0], [1,0], [0, 1], scale=2)
plt.annotate('First', (0.1, .01))
```

```python
first = sympy.Line(origin, p)
second = sympy.Line(origin, q)
```

```python
first.slope
```

```python
second.slope
```

Angle between our two lines

```python
first.angle_between(second)
```

Output is in radians, let's convert to degrees

```python
float(first.angle_between(second))
```

```python
np.rad2deg(float(first.angle_between(second)))
```

```python
second.intersection(l)
```

```python
l
```

```python
l.intersect(first)
```

```python
sympy.solve(sympy.sympify(['-1*x+y-3', '-1*x+y+2']).subs('x', (4,-1)), 'y')
```

```python
sympy.plot('-1*x + 2', (x, -5, 5))
```

```python
sympy.sympify(['-1*x+y-3', '-1*x+y+2'])
```

```python

```