# Notebook from koreantiger/AESM_1440-binder
Path: Segregated_flow_student.ipynb

<code>
import numpy as np
import matplotlib.pyplot as plt
import math
from mpl_toolkits.mplot3d import Axes3D
# Interface Model - for Rock Fluid Interaction II (AES 1320)
"""
## output parameters that you may need
# x   [1  x nx] vector with spatial co-ordinates of gridblock center
# h,  [1  x nx] vector with interface height at the endtime
# t_A [1  x nn] vector with intermediate times at which interface is stored
# h_A [nn x nx] matrix with the interface as function of space for the
#               times specified in t_A

"""

# Play around parameters
visco_o = 5e-3             # oil viscosity
visco_w = 1e-3             # water viscosity
uD = 1e-8                  # injection Darcy velocity
# to lesser extent

nx = 100                   # number of gridblocks
L = 400                    # domain size

#  domain properties
H = 50
theta = math.pi/4

# rock properties

phi = 0.38
kx = 1e-12

# Fluid properties

rho_o = 800               # oil desnity
rho_w = 1000              # water density
drho_wo = rho_w-rho_o     # density difference
g = 9.81                  # gravitational accelaration

# rock fluid properties

sor = 0.1
swc = 0.2
kroe = 0.95
krwe = 0.5

# end point mobilities
mob_we =  krwe*kx/visco_w
mob_oe =  kroe*kx/visco_o

# help variables
drho_wo_g_sin   = drho_wo * g * math.sin(theta)
drho_wo_g_cos   = drho_wo * g * math.cos(theta)

# numerical parameters
x = np.zeros(nx)
xv = np.zeros(nx+1)

dx = L/nx             # cell size

x = dx*(np.arange(1,nx+1) - 0.5)  # cell centroids
xv = np.arange(nx+1) * dx       # cell interface position
# Mobiltiy ratio and Gravity number

M = mob_we/mob_oe                           # mobility ratio
G = drho_wo_g_sin/(uD*visco_w/krwe/kx)      # Gravity number

"""
## CFL criteria (you may want to ignore this)
# Here the program determines the maximum time step the program may use
# such that the program remains numerically stable
# For explicit numerical scheme fluid is not allowed to travel
# more than one gridblock in one time-step.
# For convection : dt < dx / v_max ( v_max is maximum velocity)
# For diffusion  : dt < 0.5 * dx^2 / D_max ( D_max maximum diffusivity)
"""
DS = phi*(1-sor-swc)

# # Determine maximum convective velocity
Qt = H*uD                    # specified injected flux
Qmax = Qt/DS*max( (1+G)/(M*H) , M-G/H )

#  Determine maximum diffusion coefficient
hTest = np.linspace(0,H,101)
FWt = M*hTest/(M*hTest+(H-hTest))
Dmax = max( Qt* FWt*(H-hTest)/H* G/M / math.tan(theta) ) / DS

#  Determine maximum allowable time step
dt = min(0.5*dx**2/Dmax, dx/Qmax)

#  Simulation time
max_time = 5e+10                                                                          # user specified max time  (not the endtime)
endtime  = min(max_time,0.6*L*(DS*H/Qt))                                                  # endtime of simulation
nn = 21                                                                                # number of times that the interface will approximately be stored
dt = min(dt,endtime/(nn-1))                                                               # implications for time step
plotTime = np.arange(endtime/(nn-1), endtime + endtime/(nn-1) , endtime/(nn-1))           # time interval between to storage times
nt = math.ceil(endtime/dt)                                                                # number of time steps

# initial condition

h_init = np.zeros(nx)                                                                     # initial interface height distribution
h = h_init                                                                                # vector with interface heights used in simulation
"""
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%        1D simulation part
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
"""
#  Constant coefficients in computation
CFL = dt/dx/DS

# Initialize of main variables
t = 0
pcount = 0
t_A = []
h_A = []
t_A.append(t)
h_A.append(h_A)

# Range vector
xri = np.arange(nx - 1)
xr = np.arange(nx)

# mobilities initialize
mob_o = np.zeros(nx)
mob_w = np.zeros(nx)
MW = np.zeros(nx)
MOW = np.zeros(nx)
FLUX_conv = np.zeros(nx)

# init arithmetic avg of mobilities for diffusive fluxes
MOW_A = np.zeros(nx+1)
FLUX_dif = np.zeros(nx - 1)
pos = np.zeros(nx - 1)
QW = np.zeros(nx+1)
h - np.zeros(nx)

fig = plt.figure(1)
ax = fig.add_subplot(111)

# Computational loop

while t < endtime:
    # increase timestep
    t += dt
    # set old height equal to present heaight
    h_old = h

    # oil and water mobility within each cell
    mob_o[xr] = mob_oe * (H - h[xr])
    mob_w[xr] = mob_we * h[xr]

    # mobility products (see fractional flow function)
    MW[xr] = mob_w[xr] / (mob_w[xr]+mob_o[xr])
    MOW[xr] = mob_o[xr] * MW[xr]

    # total convective flux (in cell center, consists of viscous + gravity term)
    FLUX_conv[xr] = Qt * MW[xr] - drho_wo_g_sin * MOW[xr]

    #     % diffusive flux (already computed at cell interface)
    #     % Arithmetic avg. mobilities required for diffusive fluxes
    MOW_A[xri+1] = (MOW[xri]+MOW[xri+1])/2
    MOW_A[0] = 2*MOW_A[2]-MOW_A[3]
    MOW_A[nx] = 2*MOW_A[nx-1]-MOW_A[nx-2]
    FLUX_dif[xri] = - drho_wo_g_cos * MOW_A[xri+1] * ( h[xri+1] - h[xri] ) / dx

    # upwind weighted flux (assuming water flow from cell (i) to cell (i+1) )
    # interface flux (convective: gravity + viscous)
    pos[xri] = (FLUX_conv[xri]>= FLUX_conv[xri+1])
    QW[xri+1] = pos[xri]*(FLUX_conv[xri]) + (1-pos[xri]) * FLUX_conv[xri+1] + FLUX_dif[xri]
    QW[0] = Qt

    # flux in last cell is extrapolated
    a = 1
    QW[nx] = (1+a)*QW[nx-1]-a*QW[nx-2]

    # saturation update
    h[xr] = h_old[xr] - CFL * (QW[xr + 1] - QW[xr])

    # Fudge-work to keep between proper bounds
    h = np.maximum(np.minimum(h, H), 0)

    # plotting of h(x,t) profile in space

    if t >= plotTime[pcount]:
        # plot various stuff
        pcount += 1
        t_A.append(t)
        h_A.append(h)
        # plot interface height (h(x,t) as function of position
        ax.plot(x,h,'.-')
        ax.set_xlabel('z [m]')
        ax.set_ylabel('h [m]')
        ax.set_title("interface height h at time t= {}".format(t))
        ax.set_ylim(-0.05*H, 1.05*H)
        ax.set_aspect('equal')
        ax.autoscale(enable=True, tight=True)
        ax.autoscale_view(-360 / 2 / math.pi * theta, 90)
        #plt.pause(0.01)
        #ax.clear()

"""
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%        Figures
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
"""
"""
%% plot of the final interface height (at time t=endtime) as function of
%% the dip-co-ordinate
"""

fig = plt.figure(102)
ax = fig.add_subplot(111)
ax.plot(x,h)
ax.set_xlabel('z [m]')
ax.set_ylabel('h [m]')
ax.set_title("interface height h at time t= {}".format(t))
ax.set_ylim(-0.05 * H, 1.05 * H)
ax.set_aspect('equal')
ax.autoscale(enable=True, tight=True)
ax.set_title("Situation at time t = {}, M={} , G+1={}".format(t, M, G+1),  fontsize=9)

"""
%% plot of the interface height as function of the dip-co-ordinate at
%% all times specified in the time vector t_A
"""
fig = plt.figure(103)
ax = fig.add_subplot(111)
h_Ar = np.zeros([len(h_A), nx])
t_Ar = np.zeros(len(h_A))
for i in range(1, np.shape(h_Ar)[0]):
    h_Ar[i,:] = h_A[i]
    t_Ar[i] = t_A[i]

rr = np.arange(0,pcount,5)
for i in range(0,pcount+1,5):
    ax.plot(x,h_Ar[i], label = "t ={}".format(t_Ar[i]))

ax.set_xlabel('z [m]')
ax.set_ylabel('h [m]')
ax.set_ylim(-0.05 * H, 1.05 * H)
ax.set_aspect('equal')
ax.legend(loc='upper left', bbox_to_anchor=(0.55, 4.05))
ax.set_title("Evolution  M={} , G+1={}".format(M, G+1),  fontsize=9)
ax.autoscale(enable=True, tight=True)

plt.show()
_____no_output_____
</code>


# Notebook from MarcusFra/workshop
Path: 02_usecases/personalize_recommendations/99_05_Interacting_with_Campaigns_and_Filters.ipynb

# Interacting with Campaigns

In this notebook, you will deploy and interact with campaigns in Amazon Personalize.

1. [Introduction](#intro)
1. [Create campaigns](#create)
1. [Interact with campaigns](#interact)
1. [Batch recommendations](#batch)
1. [Wrap up](#wrapup)

## Introduction
[Back to top](#top)

At this point, you should have several solutions and at least one solution version for each. Once a solution version is created, it is possible to get recommendations from them, and to get a feel for their overall behavior.

This notebook starts off by deploying each of the solution versions from the previous notebook into individual campaigns. Once they are active, there are resources for querying the recommendations, and helper functions to digest the output into something more human-readable.

As you with your customer on Amazon Personalize, you can modify the helper functions to fit the structure of their data input files to keep the additional rendering working.

To get started, once again, we need to import libraries, load values from previous notebooks, and load the SDK._____no_output_____![](img/05.png)_____no_output_____

import time
from time import sleep
import json
from datetime import datetime
import uuid
import random

import boto3
import pandas as pd_____no_output_____%store -r_____no_output_____personalize = boto3.client('personalize')
personalize_runtime = boto3.client('personalize-runtime')

# Establish a connection to Personalize's event streaming
personalize_events = boto3.client(service_name='personalize-events')_____no_output_____

## Interact with campaigns
[Back to top](#top)

Now that all campaigns are deployed and active, we can start to get recommendations via an API call. Each of the campaigns is based on a different recipe, which behave in slightly different ways because they serve different use cases. We will cover each campaign in a different order than used in previous notebooks, in order to deal with the possible complexities in ascending order (i.e. simplest first).

First, let's create a supporting function to help make sense of the results returned by a Personalize campaign. Personalize returns only an `item_id`. This is great for keeping data compact, but it means you need to query a database or lookup table to get a human-readable result for the notebooks. We will create a helper function to return a human-readable result from the LastFM dataset.

Start by loading in the dataset which we can use for our lookup table._____no_output_____

# Create a dataframe for the items by reading in the correct source CSV
items_df = pd.read_csv(dataset_dir + '/movies.csv', sep=',', usecols=[0,1], encoding='latin-1', dtype={'movieId': "object", 'title': "str"},index_col=0)

# Render some sample data
items_df.head(5)_____no_output_____

By defining the ID column as the index column it is trivial to return an artist by just querying the ID._____no_output_____

movie_id_example = 589
title = items_df.loc[movie_id_example]['title']
print(title)Terminator 2: Judgment Day (1991)

That isn't terrible, but it would get messy to repeat this everywhere in our code, so the function below will clean that up._____no_output_____

def get_movie_by_id(movie_id, movie_df=items_df):
    """
    This takes in an artist_id from Personalize so it will be a string,
    converts it to an int, and then does a lookup in a default or specified
    dataframe.

    A really broad try/except clause was added in case anything goes wrong.

    Feel free to add more debugging or filtering here to improve results if
    you hit an error.
    """
    try:
        return movie_df.loc[int(movie_id)]['title']
    except:
        return "Error obtaining title"_____no_output_____

Now let's test a few simple values to check our error catching._____no_output_____

# A known good id (The Princess Bride)
print(get_movie_by_id(movie_id="1197"))Princess Bride, The (1987)
# A bad type of value
print(get_movie_by_id(movie_id="987.9393939"))Error obtaining title

# _The ^^ above ^^ cell should show an error______no_output_____

# Really bad values
print(get_movie_by_id(movie_id="Steve"))Error obtaining title

# _The ^^ above ^^ cell should show an error______no_output_____Great! Now we have a way of rendering results. _____no_output_____### SIMS

SIMS requires just an item as input, and it will return items which users interact with in similar ways to their interaction with the input item. In this particular case the item is a movie. _____no_output_____The cells below will handle getting recommendations from SIMS and rendering the results. Let's see what the recommendations are for the first item we looked at earlier in this notebook (Terminator 2: Judgment Day)._____no_output_____

get_recommendations_response = personalize_runtime.get_recommendations(
    campaignArn = sims_campaign_arn,
    itemId = str(589),
)_____no_output_____item_list = get_recommendations_response['itemList']
for item in item_list:
    print(get_movie_by_id(movie_id=item['itemId']))Jurassic Park (1993)
Braveheart (1995)
Terminator, The (1984)
Fugitive, The (1993)
Speed (1994)
Crimson Tide (1995)
GoldenEye (1995)
Batman (1989)
Clear and Present Danger (1994)
True Lies (1994)
Mask, The (1994)
Die Hard: With a Vengeance (1995)
In the Line of Fire (1993)
Lion King, The (1994)
Ghost (1990)
Forrest Gump (1994)
Apollo 13 (1995)
Cliffhanger (1993)
Star Trek: Generations (1994)
Firm, The (1993)
Die Hard (1988)
Seven (a.k.a. Se7en) (1995)
Indiana Jones and the Last Crusade (1989)
Mission: Impossible (1996)
Mrs. Doubtfire (1993)

Congrats, this is your first list of recommendations! This list is fine, but it would be better to see the recommendations for our sample collection of artists render in a nice dataframe. Again, let's create a helper function to achieve this._____no_output_____

# Update DF rendering
pd.set_option('display.max_rows', 30)

def get_new_recommendations_df(recommendations_df, movie_ID):
    # Get the movie name
    movie_name = get_movie_by_id(movie_ID)
    # Get the recommendations
    get_recommendations_response = personalize_runtime.get_recommendations(
        campaignArn = sims_campaign_arn,
        itemId = str(movie_ID),
    )
    # Build a new dataframe of recommendations
    item_list = get_recommendations_response['itemList']
    recommendation_list = []
    for item in item_list:
        movie = get_movie_by_id(item['itemId'])
        recommendation_list.append(movie)
    new_rec_DF = pd.DataFrame(recommendation_list, columns = [movie_name])
    # Add this dataframe to the old one
    recommendations_df = pd.concat([recommendations_df, new_rec_DF], axis=1)
    return recommendations_df_____no_output_____

Now, let's test the helper function with several different movies. Let's sample some data from our dataset to test our SIMS campaign. Grab 5 random movies from our dataframe.

Note: We are going to show similar titles, so you may want to re-run the sample until you recognize some of the movies listed_____no_output_____

samples = items_df.sample(5)
samples_____no_output_____sims_recommendations_df = pd.DataFrame()
movies = samples.index.tolist()

for movie in movies:
    sims_recommendations_df = get_new_recommendations_df(sims_recommendations_df, movie)

sims_recommendations_df_____no_output_____

You may notice that a lot of the items look the same, hopefully not all of them do (this is more likely with a smaller # of interactions). This shows that the evaluation metrics should not be the only thing you rely on when evaluating your solution version. So when this happens, what can you do to improve the results?

This is a good time to think about the hyperparameters of the Personalize recipes. The SIMS recipe has a `popularity_discount_factor` hyperparameter (see [documentation](https://docs.aws.amazon.com/personalize/latest/dg/native-recipe-sims.html)). Leveraging this hyperparameter allows you to control the nuance you see in the results. This parameter and its behavior will be unique to every dataset you encounter, and depends on the goals of the business. You can iterate on the value of this hyperparameter until you are satisfied with the results, or you can start by leveraging Personalize's hyperparameter optimization (HPO) feature. For more information on hyperparameters and HPO tuning, see the [documentation](https://docs.aws.amazon.com/personalize/latest/dg/customizing-solution-config-hpo.html)._____no_output_____### User Personalization

HRNN is one of the more advanced algorithms provided by Amazon Personalize. It supports personalization of the items for a specific user based on their past behavior and can intake real time events in order to alter recommendations for a user without retraining.

Since HRNN relies on having a sampling of users, let's load the data we need for that and select 3 random users. Since Movielens does not include user data, we will select 3 random numbers from the range of user id's in the dataset._____no_output_____

if not USE_FULL_MOVIELENS:
    users = random.sample(range(1, 600), 3)
else:
    users = random.sample(range(1, 162000), 3)
users_____no_output_____

Now we render the recommendations for our 3 random users from above. After that, we will explore real-time interactions before moving on to Personalized Ranking.

Again, we create a helper function to render the results in a nice dataframe.

#### API call results_____no_output_____

# Update DF rendering
pd.set_option('display.max_rows', 30)

def get_new_recommendations_df_users(recommendations_df, user_id):
    # Get the movie name
    #movie_name = get_movie_by_id(artist_ID)
    # Get the recommendations
    get_recommendations_response = personalize_runtime.get_recommendations(
        campaignArn = userpersonalization_campaign_arn,
        userId = str(user_id),
    )
    # Build a new dataframe of recommendations
    item_list = get_recommendations_response['itemList']
    recommendation_list = []
    for item in item_list:
        movie = get_movie_by_id(item['itemId'])
        recommendation_list.append(movie)
    #print(recommendation_list)
    new_rec_DF = pd.DataFrame(recommendation_list, columns = [user_id])
    # Add this dataframe to the old one
    recommendations_df = pd.concat([recommendations_df, new_rec_DF], axis=1)
    return recommendations_df_____no_output_____recommendations_df_users = pd.DataFrame()
#users = users_df.sample(3).index.tolist()

for user in users:
    recommendations_df_users = get_new_recommendations_df_users(recommendations_df_users, user)

recommendations_df_users_____no_output_____

Here we clearly see that the recommendations for each user are different. If you were to need a cache for these results, you could start by running the API calls through all your users and store the results, or you could use a batch export, which will be covered later in this notebook._____no_output_____Now lets apply item filters to see recommendations for one of these users within a genre
_____no_output_____

def get_new_recommendations_df_by_filter(recommendations_df, user_id, filter_arn):
    # Get the movie name
    #movie_name = get_movie_by_id(artist_ID)
    # Get the recommendations
    get_recommendations_response = personalize_runtime.get_recommendations(
        campaignArn = userpersonalization_campaign_arn,
        userId = str(user_id),
        filterArn = filter_arn
    )
    # Build a new dataframe of recommendations
    item_list = get_recommendations_response['itemList']
    recommendation_list = []
    for item in item_list:
        movie = get_movie_by_id(item['itemId'])
        recommendation_list.append(movie)
    #print(recommendation_list)
    filter_name = filter_arn.split('/')[1]
    new_rec_DF = pd.DataFrame(recommendation_list, columns = [filter_name])
    # Add this dataframe to the old one
    recommendations_df = pd.concat([recommendations_df, new_rec_DF], axis=1)
    return recommendations_df_____no_output_____user_____no_output_____

You can see the recommendations for movies within a given genre. Within a VOD application you could create Shelves (also known as rails or carosels) easily by using these filters. Depending on the information you have about your items, You could also filter on additional information such as keyword, year/decade etc._____no_output_____

recommendations_df_shelves = pd.DataFrame()
for filter_arn in meta_filter_arns:
    recommendations_df_shelves = get_new_recommendations_df_by_filter(recommendations_df_shelves, user, filter_arn)
for filter_arn in decade_filter_arns:
    recommendations_df_shelves = get_new_recommendations_df_by_filter(recommendations_df_shelves, user, filter_arn)

recommendations_df_shelves_____no_output_____

The next topic is real-time events. Personalize has the ability to listen to events from your application in order to update the recommendations shown to the user. This is especially useful in media workloads, like video-on-demand, where a customer's intent may differ based on if they are watching with their children or on their own.

Additionally the events that are recorded via this system are stored until a delete call from you is issued, and they are used as historical data alongside the other interaction data you provided when you train your next models.

#### Real time events

Start by creating an event tracker that is attached to the campaign._____no_output_____

response = personalize.create_event_tracker(
    name='MovieTracker',
    datasetGroupArn=dataset_group_arn
)
print(response['eventTrackerArn'])
print(response['trackingId'])
TRACKING_ID = response['trackingId']
event_tracker_arn = response['eventTrackerArn']arn:aws:personalize:us-east-1:835319576252:event-tracker/1e2b508b
b5b715b6-6d9d-4166-a64b-e320f968c84d

We will create some code that simulates a user interacting with a particular item. After running this code, you will get recommendations that differ from the results above.

We start by creating some methods for the simulation of real time events._____no_output_____

session_dict = {}

def send_movie_click(USER_ID, ITEM_ID, EVENT_TYPE):
    """
    Simulates a click as an envent
    to send an event to Amazon Personalize's Event Tracker
    """
    # Configure Session
    try:
        session_ID = session_dict[str(USER_ID)]
    except:
        session_dict[str(USER_ID)] = str(uuid.uuid1())
        session_ID = session_dict[str(USER_ID)]

    # Configure Properties:
    event = {
    "itemId": str(ITEM_ID),
    }
    event_json = json.dumps(event)

    # Make Call

    personalize_events.put_events(
    trackingId = TRACKING_ID,
    userId= str(USER_ID),
    sessionId = session_ID,
    eventList = [{
        'sentAt': int(time.time()),
        'eventType': str(EVENT_TYPE),
        'properties': event_json
        }]
    )

def get_new_recommendations_df_users_real_time(recommendations_df, user_id, item_id, event_type):
    # Get the artist name (header of column)
    movie_name = get_movie_by_id(item_id)
    # Interact with different movies
    print('sending event ' + event_type + ' for ' + get_movie_by_id(item_id))
    send_movie_click(USER_ID=user_id, ITEM_ID=item_id, EVENT_TYPE=event_type)
    # Get the recommendations (note you should have a base recommendation DF created before)
    get_recommendations_response = personalize_runtime.get_recommendations(
        campaignArn = userpersonalization_campaign_arn,
        userId = str(user_id),
    )
    # Build a new dataframe of recommendations
    item_list = get_recommendations_response['itemList']
    recommendation_list = []
    for item in item_list:
        artist = get_movie_by_id(item['itemId'])
        recommendation_list.append(artist)
    new_rec_DF = pd.DataFrame(recommendation_list, columns = [movie_name])
    # Add this dataframe to the old one
    #recommendations_df = recommendations_df.join(new_rec_DF)
    recommendations_df = pd.concat([recommendations_df, new_rec_DF], axis=1)
    return recommendations_df_____no_output_____

At this point, we haven't generated any real-time events yet; we have only set up the code. To compare the recommendations before and after the real-time events, let's pick one user and generate the original recommendations for them._____no_output_____

# First pick a user
user_id = user

# Get recommendations for the user
get_recommendations_response = personalize_runtime.get_recommendations(
        campaignArn = userpersonalization_campaign_arn,
        userId = str(user_id),
    )

# Build a new dataframe for the recommendations
item_list = get_recommendations_response['itemList']
recommendation_list = []
for item in item_list:
    artist = get_movie_by_id(item['itemId'])
    recommendation_list.append(artist)
user_recommendations_df = pd.DataFrame(recommendation_list, columns = [user_id])
user_recommendations_df_____no_output_____

Ok, so now we have a list of recommendations for this user before we have applied any real-time events. Now let's pick 3 random artists which we will simulate our user interacting with, and then see how this changes the recommendations._____no_output_____

# Next generate 3 random movies
movies = items_df.sample(3).index.tolist()_____no_output_____# Note this will take about 15 seconds to complete due to the sleeps
for movie in movies:
    time.sleep(5)
    user_recommendations_df = get_new_recommendations_df_users_real_time(user_recommendations_df, user_id, movie,'click')
user_recommendations_dfsending event click for Super Size Me (2004)
sending event click for Transporter 3 (2008)
sending event click for Stoning of Soraya M., The (2008)

In the cell above, the first column after the index is the user's default recommendations from HRNN, and each column after that has a header of the artist that they interacted with via a real time event, and the recommendations after this event occurred.

The behavior may not shift very much; this is due to the relatively limited nature of this dataset and effect of a few random clicks. If you wanted to better understand this, try simulating clicking more movies, and you should see a more pronounced impact._____no_output_____Now lets look at the event filters, which allow you to filter items based on the interaction data. For this dataset, it could be click or watch based on the data we imported, but could be based on whatever interaction schema you design (click, rate, like, watch, purchase etc.) For VOD shelves you could move a title from "Top picks for you" to a "Watch again", the watch again recommendations will be based on the users current interactions, but only recommend titles that have already been watched.
_____no_output_____

recommendations_df_events = pd.DataFrame()
for filter_arn in interaction_filter_arns:
    recommendations_df_events = get_new_recommendations_df_by_filter(recommendations_df_events, user, filter_arn)

recommendations_df_events_____no_output_____

now lets send a watch event in for 4 unwatched recommendations, which would simulate watching 4 movies. In a VOD application, you may choose to send in an event after they have watched a significant amount (over 75%) of a piece of content. Sending at 100% complete could miss people that stop short of the credits._____no_output_____

 # Get the recommendations
top_unwatched_recommendations_response = personalize_runtime.get_recommendations(
    campaignArn = userpersonalization_campaign_arn,
    userId = str(user_id),
    filterArn = filter_arn,
    numResults=4)
item_list = top_unwatched_recommendations_response['itemList']
for item in item_list:
    print('sending event watch for ' + get_movie_by_id(item['itemId']))
    send_movie_click(USER_ID=user_id, ITEM_ID=item['itemId'], EVENT_TYPE='watch')
    time.sleep(10)
sending event watch for Shrek 2 (2004)
sending event watch for Casino Royale (2006)
sending event watch for Little Miss Sunshine (2006)
sending event watch for Avatar (2009)

Now we can look at the event filters to see the updated watched and unwatched recommendations _____no_output_____

recommendations_df_events = pd.DataFrame()
for filter_arn in interaction_filter_arns:
    recommendations_df_events = get_new_recommendations_df_by_filter(recommendations_df_events, user, filter_arn)

recommendations_df_events_____no_output_____

### Personalized Ranking

The core use case for personalized ranking is to take a collection of items and to render them in priority or probable order of interest for a user. For a VOD application you want dynamically render a personalized shelf/rail/carousel based on some information (director, location, superhero franchise, movie time period etc). This may not be information that you have in your metadata, so a item metadata filter will not work, howeverr you may have this information within you system to generate the item list.

To demonstrate this, we will use the same user from before and a random collection of items._____no_output_____

rerank_user = user
rerank_items = items_df.sample(25).index.tolist()_____no_output_____

Now build a nice dataframe that shows the input data._____no_output_____

rerank_list = []
for item in rerank_items:
    movie = get_movie_by_id(item)
    rerank_list.append(movie)
rerank_df = pd.DataFrame(rerank_list, columns = ['Un-Ranked'])
rerank_df_____no_output_____

Then make the personalized ranking API call._____no_output_____

# Convert user to string:
user_id = str(rerank_user)
rerank_item_list = []
for item in rerank_items:
    rerank_item_list.append(str(item))

# Get recommended reranking
get_recommendations_response_rerank = personalize_runtime.get_personalized_ranking(
        campaignArn = rerank_campaign_arn,
        userId = user_id,
        inputList = rerank_item_list
)

get_recommendations_response_rerank_____no_output_____

Now add the reranked items as a second column to the original dataframe, for a side-by-side comparison._____no_output_____

ranked_list = []
item_list = get_recommendations_response_rerank['personalizedRanking']
for item in item_list:
    movie = get_movie_by_id(item['itemId'])
    ranked_list.append(movie)
ranked_df = pd.DataFrame(ranked_list, columns = ['Re-Ranked'])
rerank_df = pd.concat([rerank_df, ranked_df], axis=1)
rerank_df_____no_output_____

You can see above how each entry was re-ordered based on the model's understanding of the user. This is a popular task when you have a collection of items to surface a user, a list of promotions for example._____no_output_____## Batch recommendations
[Back to top](#top)

There are many cases where you may want to have a larger dataset of exported recommendations. Recently, Amazon Personalize launched batch recommendations as a way to export a collection of recommendations to S3. In this example, we will walk through how to do this for the HRNN solution. For more information about batch recommendations, please see the [documentation](https://docs.aws.amazon.com/personalize/latest/dg/getting-recommendations.html#recommendations-batch). This feature applies to all recipes, but the output format will vary.

A simple implementation looks like this:

```python
import boto3

personalize_rec = boto3.client(service_name='personalize')

personalize_rec.create_batch_inference_job (
    solutionVersionArn = "Solution version ARN",
    jobName = "Batch job name",
    roleArn = "IAM role ARN",
    jobInput =
       {"s3DataSource": {"path": S3 input path}},
    jobOutput =
       {"s3DataDestination": {"path":S3 output path"}}
)
```

The SDK import, the solution version arn, and role arns have all been determined. This just leaves an input, an output, and a job name to be defined.

Starting with the input for HRNN, it looks like:

```JSON
{"userId": "4638"}
{"userId": "663"}
{"userId": "3384"}
```

This should yield an output that looks like this:

```JSON
{"input":{"userId":"4638"}, "output": {"recommendedItems": ["296", "1", "260", "318"]}}
{"input":{"userId":"663"}, "output": {"recommendedItems": ["1393", "3793", "2701", "3826"]}}
{"input":{"userId":"3384"}, "output": {"recommendedItems": ["8368", "5989", "40815", "48780"]}}
```

The output is a JSON Lines file. It consists of individual JSON objects, one per line. So we will need to put in more work later to digest the results in this format._____no_output_____### Building the input file

When you are using the batch feature, you specify the users that you'd like to receive recommendations for when the job has completed. The cell below will again select a few random users and will then build the file and save it to disk. From there, you will upload it to S3 to use in the API call later._____no_output_____

# We will use the same users from before
users
# Write the file to disk
json_input_filename = "json_input.json"
with open(data_dir + "/" + json_input_filename, 'w') as json_input:
    for user_id in users:
        json_input.write('{"userId": "' + str(user_id) + '"}\n')_____no_output_____# Showcase the input file:
!cat $data_dir"/"$json_input_filename{"userId": "346"}
{"userId": "75"}
{"userId": "247"}

Upload the file to S3 and save the path as a variable for later._____no_output_____

# Upload files to S3
boto3.Session().resource('s3').Bucket(bucket_name).Object(json_input_filename).upload_file(data_dir+"/"+json_input_filename)
s3_input_path = "s3://" + bucket_name + "/" + json_input_filename
print(s3_input_path)s3://835319576252personalizepocvod/json_input.json

Batch recommendations read the input from the file we've uploaded to S3. Similarly, batch recommendations will save the output to file in S3. So we define the output path where the results should be saved._____no_output_____

# Define the output path
s3_output_path = "s3://" + bucket_name + "/"
print(s3_output_path)s3://835319576252personalizepocvod/

Now just make the call to kick off the batch export process._____no_output_____

batchInferenceJobArn = personalize.create_batch_inference_job (
    solutionVersionArn = userpersonalization_solution_version_arn,
    jobName = "VOD-POC-Batch-Inference-Job-UserPersonalization_" + str(round(time.time()*1000)),
    roleArn = role_arn,
    jobInput =
     {"s3DataSource": {"path": s3_input_path}},
    jobOutput =
     {"s3DataDestination":{"path": s3_output_path}}
)
batchInferenceJobArn = batchInferenceJobArn['batchInferenceJobArn']_____no_output_____

Run the while loop below to track the status of the batch recommendation call. This can take around 30 minutes to complete, because Personalize needs to stand up the infrastructure to perform the task. We are testing the feature with a dataset of only 3 users, which is not an efficient use of this mechanism. Normally, you would only use this feature for bulk processing, in which case the efficiencies will become clear._____no_output_____

%%time

current_time = datetime.now()
print("Import Started on: ", current_time.strftime("%I:%M:%S %p"))

max_time = time.time() + 6*60*60 # 6 hours
while time.time() < max_time:
    describe_dataset_inference_job_response = personalize.describe_batch_inference_job(
        batchInferenceJobArn = batchInferenceJobArn
    )
    status = describe_dataset_inference_job_response["batchInferenceJob"]['status']
    print("DatasetInferenceJob: {}".format(status))

    if status == "ACTIVE" or status == "CREATE FAILED":
        break

    time.sleep(60)

current_time = datetime.now()
print("Import Completed on: ", current_time.strftime("%I:%M:%S %p"))Import Started on:  04:01:17 AM
DatasetInferenceJob: ACTIVE
Import Completed on:  04:01:17 AM
CPU times: user 4.37 ms, sys: 114 µs, total: 4.48 ms
Wall time: 29.9 ms
s3 = boto3.client('s3')
export_name = json_input_filename + ".out"
s3.download_file(bucket_name, export_name, data_dir+"/"+export_name)

# Update DF rendering
pd.set_option('display.max_rows', 30)
with open(data_dir+"/"+export_name) as json_file:
    # Get the first line and parse it
    line = json.loads(json_file.readline())
    # Do the same for the other lines
    while line:
        # extract the user ID
        col_header = "User: " + line['input']['userId']
        # Create a list for all the artists
        recommendation_list = []
        # Add all the entries
        for item in line['output']['recommendedItems']:
            movie = get_movie_by_id(item)
            recommendation_list.append(movie)
        if 'bulk_recommendations_df' in locals():
            new_rec_DF = pd.DataFrame(recommendation_list, columns = [col_header])
            bulk_recommendations_df = bulk_recommendations_df.join(new_rec_DF)
        else:
            bulk_recommendations_df = pd.DataFrame(recommendation_list, columns=[col_header])
        try:
            line = json.loads(json_file.readline())
        except:
            line = None
bulk_recommendations_df_____no_output_____

## Wrap up
[Back to top](#top)

With that you now have a fully working collection of models to tackle various recommendation and personalization scenarios, as well as the skills to manipulate customer data to better integrate with the service, and a knowledge of how to do all this over APIs and by leveraging open source data science tools.

Use these notebooks as a guide to getting started with your customers for POCs. As you find missing components, or discover new approaches, cut a pull request and provide any additional helpful components that may be missing from this collection.

You'll want to make sure that you clean up all of the resources deployed during this POC. We have provided a separate notebook which shows you how to identify and delete the resources in `04_Clean_Up_Resources.ipynb`._____no_output_____

%store event_tracker_arn
%store batchInferenceJobArn_____no_output_____


# Notebook from Dzeiberg/processBioDBs
Path: 14_Trust_The_Reprocess.ipynb

from IPython.core.display import display, HTML
display(HTML(""))
_____no_output_____import numpy as np
import pandas as pd
from tqdm.notebook import tqdm_____no_output_____aaTable = {"Ala":"A",
           "Arg": "R",
           "Asn": "N",
           "Asp": "D",
           "Cys": "C",
           "Gln": "Q",
           "Glu": "E",
           "Gly": "G",
           "His": "H",
           "Ile": "I",
           "Leu": "L",
           "Lys": "K",
           "Met": "M",
           "Phe": "F",
           "Pro": "P",
           "Ser": "S",
           "Thr": "T",
           "Trp": "W",
           "Tyr": "Y",
           "Val": "V"}_____no_output_____import pickle_____no_output_____npid2seq = pickle.load(open("/data/projects/processBio/ncbi/npid2seq.pkl","rb"))
# npid2seq = pickle.load(open("/ssdata/projects"))_____no_output_____

# gnomAD_____no_output_____

df = pd.read_csv("/data/projects/processBio/gnomad/gnomad.exomes.r2.1.1.sites.vcf.vep",
                 header=93,delimiter="\t")_____no_output_____df_____no_output_____

Originally 103,321,415 variants_____no_output_____

df = df[df.Consequence.str.contains("missense")]_____no_output_____df_____no_output_____

1,652,025 missense variants_____no_output_____

df = df.assign(info=df.Extra.apply(lambda s: dict([l.split("=") for l in s.split(";")])))_____no_output_____df.Feature.unique().shape_____no_output_____from ensembl_rest import sequence_id, lookup, symbol_lookup, HTTPError
def getSequenceFromTranscript(transcriptID):
    return sequence_id(lookup(transcriptID,
                              params={"expand":True})["Translation"]["id"])["seq"]_____no_output_____transcripts = df.Feature.unique()_____no_output_____gnomadTranscriptToSeq = {}
for t in tqdm(transcripts):
    gnomadTranscriptToSeq[t] = getSequenceFromTranscript(t)_____no_output_____import pickle_____no_output_____pickle.dump(gnomadTranscriptToSeq, open("/data/projects/processBio/gnomad/transcriptToSeq.pkl","wb"))_____no_output_____gnomadTranscriptToSeq = pickle.load(open("/data/projects/processBio/gnomad/transcriptToSeq.pkl","rb"))_____no_output_____def makeGnomADSeq(row):
    og,var = row.Amino_acids[0], row.Amino_acids[-1]
    loc = int(row["Protein_position"]) - 1
    refSeq= gnomadTranscriptToSeq[row["Feature"]]
    if len(refSeq) <= loc:
        print("invalid seq len")
        return ""
    if refSeq[loc] == og:
        return refSeq[:loc] + var + refSeq[loc+1:]
    print("invalid reference AA")
    return ""_____no_output_____df = df.assign(seq=df.apply(lambda row: makeGnomADSeq(row),axis=1))_____no_output_____df[df.seq != ""]_____no_output_____df.to_pickle("/data/projects/processBio/gnomad/df.reprocessed.pd.pkl")_____no_output_____df_____no_output_____

# HGMD_____no_output_____

hgmd = pd.read_csv("/data/projects/processBio/hgmd/HGMD_PRO_2019_1_hg19.vcf",delimiter="\t",header=14)_____no_output_____hgmd = hgmd.assign(INFO = hgmd.INFO.apply(lambda s: dict([l.split("=") for l in s.split(";")])))_____no_output_____hgmd_____no_output_____

Originally 229,161 HGMD variants_____no_output_____

hgmd = hgmd[hgmd.INFO.apply(lambda i: i["CLASS"] == "DM")]_____no_output_____hgmd_____no_output_____

180,092 variants with class DM_____no_output_____

from Bio import Entrez
Entrez.email = 'zeiberg.d2@northeastern.edu'

def getSequenceFromNPID(npid):
    "Return the protein sequence from "
    handle = Entrez.efetch(db="protein",id=npid, rettype="fasta", retmode="text")
    lines = handle.readlines()
    lines = [l.strip() for l in lines]
    return "".join(lines[1:])_____no_output_____def getProt(i):
    if "PROT" in i:
        return i["PROT"][:i["PROT"].find(":")]
    return ""

def hasProt(i):
    if "PROT" in i:
        return 1
    return 0_____no_output_____def getHGMDVariantInfo(i):
    if "PROT" not in i:
        return (np.nan, np.nan)
    npid, variant = i["PROT"].split(":")
    variant = variant[2:]
    og,loc,var = variant[0], variant[1:-1], variant[-1]
    if og not in aaTable.values() or var not in aaTable.values():
        return (np.nan, np.nan)
    try:
        loc = int(loc) - 1
    except ValueError:
        return (np.nan, np.nan)
    return (npid, (og,loc,var))_____no_output_____hgmdVariantInfo = hgmd.INFO.apply(lambda i: getHGMDVariantInfo(i))_____no_output_____hgmd = hgmd.assign(variantInfo = hgmdVariantInfo[hgmdVariantInfo.apply(lambda t: not pd.isna(t[0]))])_____no_output_____hgmd_____no_output_____hgmd[~hgmd.variantInfo.isna()]_____no_output_____def getHGMDNPIDs(info):
    if not pd.isna(info):
        npid,_ = info
        if npid not in npid2seq:
            npid2seq[npid] = getSequenceFromNPID(npid)
    _____no_output_____for info in tqdm(hgmd.variantInfo):
    getHGMDNPIDs(info)_____no_output_____def makeHGMDSeq(info):
    if pd.isna(info):
        return ""
    npid, (og,loc,var) = info
    refSeq = npid2seq[npid]
    if len(refSeq) < loc:
        return ""
    if refSeq[loc] != og:
        return ""
    return refSeq[:loc] + var + refSeq[loc+1:]_____no_output_____hgmdSeqs = []
for info in tqdm(hgmd.variantInfo):
    hgmdSeqs.append(makeHGMDSeq(info))_____no_output_____hgmd = hgmd.assign(seq=hgmdSeqs)_____no_output_____hgmd[hgmd.seq != ""]_____no_output_____

# Clinvar_____no_output_____

summary = pd.read_csv("/data/projects/processBio/clinvar/clinvar/variant_summary.txt",delimiter="\t")
_____no_output_____summary = summary[(summary.Type == "single nucleotide variant") & (summary.ReviewStatus.isin(["criteria provided, single submitter",
              "criteria provided, multiple submitters, no conflicts",
              "reviewed by expert panel",
              "practice guideline",
              ])) & (summary.Assembly == "GRCh38")]_____no_output_____summary = summary.assign(nmid=summary.Name.apply(lambda n: n[:n.find("(")]))_____no_output_____summary_____no_output_____

767,700 single nucleotide variants with reference genome GRCh38 with one-star review status_____no_output_____

g2rs = pd.read_csv("/data/projects/processBio/ncbi/gene2refseq",delimiter="\t")_____no_output_____g2rs = g2rs[(g2rs.status == "REVIEWED")
            & (g2rs.assembly == "Reference GRCh38.p13 Primary Assembly")
            & (g2rs["protein_accession.version"] != "-")]_____no_output_____nmid2npid = {}
for nmid in tqdm(summary.nmid.unique()):
    nmid2npid[nmid] = g2rs[g2rs["RNA_nucleotide_accession.version"] == nmid]["protein_accession.version"].unique()_____no_output_____summary = summary.assign(npid=summary.nmid.apply(lambda n: nmid2npid[n]))_____no_output_____
for npid in tqdm(set([s[0] for s in summary.npid.values if len(s)])):
    if npid not in npid2seq:
        try:
            npid2seq[npid] = getSequenceFromNPID(npid)
        except Entrez.HTTPError:
            raise_____no_output_____pickle.dump(npid2seq,open("/data/projects/processBio/ncbi/npid2seq.pkl","wb"))_____no_output_____summary_____no_output_____summary = summary.assign(npid=summary[summary.npid.str.len() != 0].npid.apply(lambda n: n[0] if len(n) else np.nan))_____no_output_____summary[~summary.npid.isna()]_____no_output_____

623,532 variants found a matching protein id_____no_output_____

summary[~summary.npid.isna()].ClinicalSignificance.value_counts()_____no_output_____clinvar = summary[(~summary.npid.isna()) & (summary.ClinicalSignificance.isin(["Likely benign",
                                                                     "Benign",
                                                                     "Pathogenic",
                                                                     "Likely pathogenic",
                                                                     "Benign/Likely benign",
                                                                     "Pathogenic/Likely pathogenic"]))]_____no_output_____clinvar.loc[33]_____no_output_____

328,243 variants have matching protein and valid status_____no_output_____

def extractVariantFromClinvar(n):
    if n.rfind("(p.") != -1:
        variant = n[n.rfind("(p.") + 3 : -1]
#         print(variant)
        og,loc,var = variant[:3], variant[3:-3], variant[-3:]
        try:
            loc = int(loc) -1
        except ValueError:
            return np.nan, np.nan,np.nan
        if variant[-1] == "=":
#             print("variant is =")
            return np.nan, np.nan,np.nan
        return og,loc,var
    return np.nan, np.nan, np.nan_____no_output_____clinvar = clinvar.assign(variantInfo=clinvar.Name.apply(lambda n: extractVariantFromClinvar(n)))_____no_output_____# clinvar = clinvar[clinvar.variantInfo.apply(lambda t: not pd.isna(t[0]))]
clinvar[clinvar.variantInfo.apply(lambda t: pd.isna(t[0]))].RCVaccession_____no_output_____clinvar_____no_output_____

73,371 variants have matching protein, valid status, and have alternate residue != "="_____no_output_____

clinvar.ClinicalSignificance.value_counts()_____no_output_____clinvar_____no_output_____def makeClinvarSeq(row):
    npid = row["npid"]
    refSeq = npid2seq[npid]
    og,loc,var = row["variantInfo"]
    if og not in aaTable or var not in aaTable:
        if var != "Ter":
            print('invalid parsing of variant... ', og, loc, var)
        return ""
    og = aaTable[og]
    var = aaTable[var]
    if len(refSeq) <= loc:
        print('invalid seq len')
        return ""
    if refSeq[loc] != og:
        print('invalid ref AA')
        return ""
    return refSeq[:loc] + var + refSeq[loc+1:]_____no_output_____clinvar = clinvar.assign(seq = clinvar.apply(lambda row: makeClinvarSeq(row),axis=1))_____no_output_____clinvar[clinvar.seq != ""]_____no_output_____

Successfully retreived 55,087 variants_____no_output_____# Combine Labeled Data_____no_output_____

hgmd = hgmd.assign(ClinSigSimple=1)
hgmd = hgmd.assign(db="hgmd")_____no_output_____clinvar = clinvar.assign(db="clinvar")_____no_output_____hgmd = hgmd.assign(npid = hgmd.variantInfo.apply(lambda i: i[0] if not pd.isna(i) else np.nan))_____no_output_____labeledDF = pd.concat((hgmd[hgmd.seq != ""][["ClinSigSimple", "seq", "npid", "db","variantInfo"]],
  clinvar[clinvar.seq != ""][["npid", "seq", "ClinSigSimple", "db","variantInfo"]]))_____no_output_____labeledDF_____no_output_____labeledDF.npid.unique().shape_____no_output_____labeledDF.ClinSigSimple.value_counts()_____no_output_____labeledDF.groupby("npid").count().seq.mean()_____no_output_____(labeledDF.groupby("npid").count() > 100).sum()_____no_output_____import torch
import esm
import os
import torch.nn as nn
os.environ['CUDA_DEVICE_ORDER']='PCI_BUS_ID'
os.environ['CUDA_VISIBLE_DEVICES']='1,2,3'

model, alphabet = esm.pretrained.esm1b_t33_650M_UR50S()
batch_converter = alphabet.get_batch_converter()_____no_output_____model = model.to("cuda:2")_____no_output_____from tqdm.notebook import trange_____no_output_____def embedSeq(Data):
    BATCHSIZE=1
    representations = []
    for start in trange(0,len(Data),BATCHSIZE):
        batch_labels, batch_strs, batch_tokens = batch_converter(Data[start : start + BATCHSIZE])
        # Extract per-residue representations (on CPU)
        with torch.no_grad():
            results = model(batch_tokens.to("cuda:2"), repr_layers=[33], return_contacts=True)
        token_representations = results["representations"][33].cpu()
        del results, batch_labels, batch_strs, batch_tokens
        representations.append(token_representations[0,1:-1].cpu().numpy())
        del token_representations
    return representations_____no_output_____def prepSeq(row):
    if type(row["variantInfo"][1]) == tuple:
        loc= row["variantInfo"][1][1]
    else:
        loc = row["variantInfo"][1]
    seq = row["seq"]
    return seq[max(0,loc-510) : min(len(seq), loc + 510)]_____no_output_____labeledDF = labeledDF.assign(preppedSeq = labeledDF.apply(lambda row: prepSeq(row),axis=1))_____no_output_____df.head()_____no_output_____def prepGnomad(row):
    loc = int(row["Protein_position"])-1
    seq = row["seq"]
    return seq[max(0,loc-510) : min(len(seq), loc + 510)]_____no_output_____df= df.assign(preppedSeq=df.apply(lambda row: prepGnomad(row),axis=1))_____no_output_____from tqdm.notebook import trange_____no_output_____GnomadData = list(df.preppedSeq.items())_____no_output_____gnomadEmbeddings = embedSeq(GnomadData)_____no_output_____df = df.assign(embedding=gnomadEmbeddings)_____no_output_____df.to_pickle('/data/projects/processBio/gnomadReprocessed.pkl')
_____no_output_____Data = list(labeledDF.preppedSeq.items())_____no_output_____embeddings = embedSeq(Data)_____no_output_____embeddings_____no_output_____labeledDF = labeledDF.assign(embedding=embeddings)_____no_output_____labeledDF_____no_output_____import pickle_____no_output_____labeledDF = labeledDF.assign(vec=labeledDF.embedding.apply(lambda e: e.mean(0)))_____no_output_____labeledDF.to_pickle("/data/projects/processBio/labeledDFReprocessed.pkl")_____no_output_____labeledDF.ClinSigSimple.value_counts()_____no_output_____labeledDF.groupby("npid").ClinSigSimple.value_counts().describe()_____no_output_____ls /data/projects/processBio/_____no_output_____


# Notebook from plusk01/nonlinearquad
Path: utils.ipynb

import numpy as np_____no_output_____

# Utilities

This notebook contains utility code that is useful to multiple notebooks._____no_output_____## Derivative Filter

In many control schemes that follow trajectories, higher order derivatives are often needed. This section discusses an implementation of a digital derivative based in Laplace system theory.

Recall from system theory that the Laplace transform is a useful way of analyzing linear time-invariant (LTI) systems. It takes time-domain systems and expresses them in the Laplace domain (a superset of the Fourier frequency domain). The unilateral Laplace transform is defined by
$$
\begin{equation}
F(s) = \int_0^\infty f(t) e^{-st} dt,
\end{equation}
$$
where $t$ is a time variable and $s$ is the Laplace variable. This transform can be thought as projecting a time-domain function onto exponentials of varying phase.

This transform is particularly useful when analyzing LTI systems of differential equations. In particular, the Laplace transform of the time-derivative of a continuous system $f(t)$ can be found to be
$$
\begin{equation}
\frac{d}{dt} f(t) \stackrel{\mathcal{L}}{\rightleftharpoons} sF(s).
\end{equation}
$$

Therefore, differentiation in the time-domain corresponds to multiplication by $s$ in the Laplace domain. Although a pure derivative in the Laplace domain is causal, it is not realizable (i.e., can you build an RLC circuit that realizes $F(s)=s$?). Therefore, a band-limited derivative (i.e., a *dirty derivative*) is used:
$$
\begin{equation}\label{eq:dirty-derivative}
G(s) = \frac{s}{\tau s + 1}.
\end{equation}
$$

From analog filter design, remember that a first-order low-pass filter with cutoff frequency $\omega_c$ is defined in the Laplace domain as
$$
\begin{equation}\label{eq:lpf}
H_{LPF}(s) = \frac{\omega_c}{s + \omega_c} = \frac{1}{s/\omega_c + 1},
\end{equation}
$$
and a first-order high-pass filter is
$$
\begin{equation}\label{eq:hpf}
H_{HPF}(s) = \frac{s}{s + \omega_c} = \frac{s/\omega_c}{s/\omega_c + 1}.
\end{equation}
$$

Note that the cutoff frequency is related to the filter time-constant (and RC circuits) by
$$
\begin{equation}
\tau = RC = \frac{1}{2\pi f_c} = \frac{1}{\omega_c}.
\end{equation}
$$

From this discussion, we can see that the dirty derivative \eqref{eq:dirty-derivative} can be thought of as a filtered version of a pure derivative with bandwidth $1/\tau$. This is useful as the LPF will prevent the derivative operator from picking up high-frequency components of the input signal and amplifying noise.

A common value of $\tau$ is $0.5$ ($20$ Hz). A higher $\tau$ corresponds to less bandwidth and more rejection of high-frequency components, which results in a smoother output.

### Digital Implementation

In order to implement a derivative filter in a computer, it of course must be discretized. The technique used here is to map $G(s)$ to the $z$-domain via the bilinear transform (AKA, Tustin approximation)
$$
\begin{equation}
s \mapsto \frac{2}{T}\frac{1-z^{-1}}{1+z^{-1}} ,
\end{equation}
$$
where $T$ is the sample period. Once we have an expression in the $z$-domain, the inverse $z$-transform can be used to give a discrete-time implementation of the dirty derivative.

#### Resources

- [SE.DSP: First-Derivative Analog Filter](https://dsp.stackexchange.com/questions/41109/first-derivative-analog-filter)
- [Blog: Causal, but not Realizable](http://blog.jafma.net/2015/10/04/differentiation-derivative-is-causal-but-not-exactly-realizable/)_____no_output_____

class DirtyDerivative:
    """Dirty Derivative

    Provides a first-order derivative of a signal.

    This class creates a filtered derivative based on a
    band-limited low-pass filter with transfer function:

        G(s) = s/(tau*s + 1)

    This is done because a pure differentiator (D(s) = s)
    is not realizable.
    """
    def __init__(self, order=1, tau=0.05):
        # time constant of dirty-derivative filter.
        # Higher leads to increased smoothing.
        self.tau = tau

        # Although this class only provides a first-order
        # derivative, we use this parameter to know how
        # many measurements to ignore so that the incoming
        # data is smooth and stable. Otherwise, the filter
        # would be hit with a step function, causing
        # downstream dirty derivatives to be hit with very
        # large step functions.
        self.order = order

        # internal memory for lagged signal value
        self.x_d1 = None

        # Current value of derivative
        self.dxdt = None

    def update(self, x, Ts):
        # Make sure to store the first `order` measurements,
        # but don't use them until we have seen enough
        # measurements to produce a stable output
        if self.order > 0:
            self.order -= 1
            self.x_d1 = x
            return np.zeros(x.shape)

        # Calculate digital derivative constants
        a1 = (2*self.tau - Ts)/(2*self.tau + Ts)
        a2 = 2/(2*self.tau + Ts)

        if self.dxdt is None:
            self.dxdt = np.zeros(x.shape)

        # calculate derivative
        self.dxdt = a1*self.dxdt + a2*(x - self.x_d1)

        # store value for next time
        self.x_d1 = x

        return self.dxdt_____no_output_____

---_____no_output_____## Rotation Matrices

Throughout this notebook, we use *right-handed*, *passive* rotations to express the same geometrical vector in various coordinate frames. When Euler angles are used, the *intrinsic* 3-2-1 (Z-Y-X) sequence is used. This means that a yaw ($\psi$) rotation is first performed in $\mathcal{F}_A$ around the $\mathbf{k}^A$ axis to get to $\mathcal{F}_B$. Then, a pitch ($\theta$) rotation is performed about the $\mathbf{j}^B$ axis of frame B to get to $\mathcal{F}_C$. Lastly, a roll ($\phi$) rotation is performed about the $\mathbf{i}^C$ axis to get to $\mathcal{F}_D$. The order of rotation is important, and if data from $\mathcal{F}_A$ is meant to be expressed in $\mathcal{F}_D$, the rotations are composed as
$$
\begin{equation}
x^D = R_C^D(\phi) R_B^C(\theta) R_A^B(\psi)x^A = R_A^D(\phi,\theta,\psi)x^A = R_A^D x^A.
\end{equation}
$$

Python implementations of these passive rotations are defined below._____no_output_____

# input angle in radians
def rotx(ph): return np.array([[1,0,0],[0,np.cos(ph),np.sin(ph)],[0,-np.sin(ph),np.cos(ph)]])
def roty(th): return np.array([[np.cos(th),0,-np.sin(th)],[0,1,0],[np.sin(th),0,np.cos(th)]])
def rotz(ps): return np.array([[np.cos(ps),np.sin(ps),0],[-np.sin(ps),np.cos(ps),0],[0,0,1]])
def rot3(ph,th,ps): return rotx(ph).dot(roty(th).dot(rotz(ps)))
# input angle in degrees
def rotxd(ph): return rotx(np.radians(ph))
def rotyd(th): return roty(np.radians(th))
def rotzd(ps): return rotz(np.radians(ps))
def rot3d(ph,th,ps): return rot3(np.radians(ph),np.radians(th),np.radians(ps))_____no_output_____

---_____no_output_____## PID Controller

One of the simplest forms of feedback control is the proportional-integral-derivative (PID) controller. It is perhaps the most widely used form of control because it is an intuitive technique for minimizing error. However, stability can only be guaranteed for second-order systems._____no_output_____### Digital Implementation

In order to implement a PID controller on a computer, we need to discretize our continuous expression for the integral and derivative terms of the PID controller._____no_output_____

class SimplePID:
    def __init__(self, kp, ki, kd, min, max, tau=0.05):
        self.kp = kp
        self.ki = ki
        self.kd = kd
        self.min = min
        self.max = max
        self.tau = tau

        self.derivative = 0.0
        self.integral = 0.0

        self.last_error = 0.0

    @staticmethod
    def _clamp(v, limit):
        return v if np.abs(v) < limit else limit*np.sign(v)

    def run(self, error, dt, derivative=None, pclamp=None):

        # P term
        if self.kp:
            # Proportional error clamp, it specified
            e = error if pclamp is None else self._clamp(error, pclamp)
            p_term = self.kp * e
        else:
            p_term = 0.0

        # D term
        if self.kd:
            if derivative:
                self.derivative = derivative
            elif dt > 0.0001:
                self.derivative = (2.0*self.tau - dt)/(2.0*self.tau + dt)*self.derivative + 2.0/(2.0*self.tau + dt)*(error - self.last_error)
            else:
                self.derivative = 0.0
            d_term = self.kd * self.derivative
        else:
            d_term = 0.0

        # I term
        if self.ki:
            self.integral += (dt/2.0) * (error + self.last_error)
            i_term = self.ki * self.integral
        else:
            i_term = 0.0

        # combine
        u = p_term + d_term + i_term

        # saturate
        if u < self.min:
            u_sat = self.min
        elif u > self.max:
            u_sat = self.max
        else:
            u_sat = u

        # integrator anti-windup
        if self.ki:
            if abs(p_term + d_term) > abs(u_sat):
                # PD is already saturating, so set integrator to 0 but don't let it run backwards
                self.integral = 0
            else:
                # otherwise only let integral term at most take us just up to saturation
                self.integral = (u_sat - p_term - d_term) / self.ki

        # bookkeeping
        self.last_error = error

        return u_sat_____no_output_____

---_____no_output_____## Numerical Integration

Before our discussion of the dynamical equations of motion that describe the time evolution of the quadrotor in $\text{SE}(3)$, we should discuss how to properly integrate these differential equations numerically for use in a simulation. The two most common types of numerical integrators are Euler's method and the fourth order Runge-Kutta method, or RK4.

### The Euler Method

The Euler method is an explicit algorithm that uses the limit definition of the derivative:
$$
\frac{df}{dt} = \lim_{h\to0} \frac{f(t + h) - f(t)}{h}.
$$
Let $g(t) \triangleq \frac{df}{dt}(t)$. Disregarding the limit and rearranging the difference quotient yields
$$
f(t+h) = f(t) + g(t)h.
$$
Using the step size $h$ as the sample period and using discrete notation, we have
$$
f[k+1] = f[k] + g[k] T_s.
$$

### RK4

Of course, the Euler Method is a first-order, rough approximation of differential equations. Given a differential equation $\dot{y} = f(t,y)$, the RK4 method is given by
$$
\begin{align}
y_{n+1}	&=	y_{n}+\frac{h}{6}\left(k_{1}+2k_{2}+2k_{3}+k4\right) \\
t_{n+1}	&=	t_{n}+h,
 \end{align}
$$

where
$$
\begin{align}
k_{1} &= f\left(t_{n},y_{n}\right) \\
k_{2} &= f\left(t_{n}+\frac{h}{2},y_{n}+\frac{h}{2}k_{1}\right) \\
k_{3} &= f\left(t_{n}+\frac{h}{2},y_{n}+\frac{h}{2}k_{2}\right) \\
k_{4} &= f\left(t_{n}+h,y_{n}+hk_{3}\right).
\end{align}
$$

For an expository derivation of RK4, see [2]._____no_output_____

def rk4(f, y, dt):
    """Runge-Kutta 4th Order

    Solves an autonomous (time-invariant) differential equation of the form dy/dt = f(y).
    """
    k1 = f(y)
    k2 = f(y + dt/2*k1)
    k3 = f(y + dt/2*k2)
    k4 = f(y + dt  *k3)
    return y + (dt/6)*(k1 + 2*k2 + 2*k3 + k4)_____no_output_____

#### RK4 Example

Consider the autonomous evolution of position $x \in \mathbb{R}$ as
$$
\dot{x} = \cos(\omega(t))
$$

where
$$
\omega(t) =
\begin{cases}
    2 \pi t, & 0 < t < 2.5 \\
    4 \pi t, & 2.5 \leq t < 5 \\
\end{cases}.
$$

Note that this equation is not smooth (there is a jump at $t = 2.5$), but we are still able to numerically integrate it._____no_output_____

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

# simulation timing
Tf = 5
Ts = 0.01
N = int(Tf/Ts)

# initital condition
x = 0

# simulation history
x_hist = np.zeros((N,1))

for i in range(N-1):
    # dynamics
    freq = 1 if i < N//2 else 2
    v = np.cos(2*np.pi*freq*(Ts*i))
    f = lambda x: v

    # propagate dynamics by solving the differential equation
    x = rk4(f, x, Ts)

    # add to history
    x_hist[i+1] = x

plt.plot(np.arange(0, Tf, Ts), x_hist)
plt.grid(); plt.xlabel('Time (s)'); plt.ylabel('Position (m)')
plt.show()_____no_output_____

### Lie Group Integration

Note that the above formulations we assume Euclidean spaces instead of matrix Lie groups._____no_output_____---_____no_output_____