![Callysto.ca Banner](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-top.jpg?raw=true)

(Click **Cell** > **Run All** before proceeding.)

%matplotlib inline

#----------
#Import modules and packages
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
import random

#----------

#import ipywidgets as widgets
#import random

#This function produces a multiple choice form with four options
def multiple_choice(option_1, option_2, option_3, option_4):
    option_list = [option_1, option_2, option_3, option_4]
    answer = option_list[0]
    letters = ["(A) ", "(B) ", "(C) ", "(D) "]

    #Boldface letters at the beginning of each option
    start_bold = "\033[1m"; end_bold = "\033[0;0m"

    #Randomly shuffle the options
    random.shuffle(option_list)

    #Prints the letters (A) to (D) in sequence with randomly chosen options
    for i in range(4):
        option_text = option_list.pop()
        print(start_bold + letters[i] + end_bold + option_text)

        #Stores the correct answer
        if option_text == answer:
            letter_answer = letters[i]

    button1 = widgets.Button(description="(A)"); button2 = widgets.Button(description="(B)")
    button3 = widgets.Button(description="(C)"); button4 = widgets.Button(description="(D)")

    button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
    button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'

    container = widgets.HBox(children=[button1,button2,button3,button4])
    display(container)
    print(" ", end='\r')

    def on_button1_clicked(b):
        if "(A) " == letter_answer:
            print("Correct!    ", end='\r')
            button1.style.button_color = 'Moccasin'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'
        else:
            print("Try again.", end='\r')
            button1.style.button_color = 'Lightgray'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'

    def on_button2_clicked(b):
        if "(B) " == letter_answer:
            print("Correct!    ", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Moccasin'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'
        else:
            print("Try again.", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Lightgray'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'

    def on_button3_clicked(b):
        if "(C) " == letter_answer:
            print("Correct!    ", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Moccasin'; button4.style.button_color = 'Whitesmoke'
        else:
            print("Try again.", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Lightgray'; button4.style.button_color = 'Whitesmoke'

    def on_button4_clicked(b):
        if "(D) " == letter_answer:
            print("Correct!    ", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Moccasin'
        else:
            print("Try again.", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Lightgray'

    button1.on_click(on_button1_clicked); button2.on_click(on_button2_clicked)
    button3.on_click(on_button3_clicked); button4.on_click(on_button4_clicked)

# Static and Kinetic Friction

## Introduction

**Friction** is the name given to the force that resists the relative motion of one object as it slides against another. Most surfaces are rough and uneven. Even smooth surfaces, such as a polished mirror, can appear bumpy under a microscope. The bumps on these surfaces interlock as they rub against one another, which prevents them from sliding freely.

The more an object presses into a surface, the more the surface pushes back against the object. The force pushing back against the object is called the **normal force**, **$F_{n}$** and it is always perpendicular to the surface. From the image above, we can imagine that the more two objects are pressed together, the more difficult it is for the interlocking points to lift up and slide passed one another. As a consequence, the friction force increases. A relationship therefore exists between the magnitude of the friction force and that of the normal force. This relationship is expressed by the following equation:

$$F_{f}=\mu F_{n}$$

where $F_{f}$ and $F_{n}$ are the magnitudes of the friction and normal forces respectively, and $\mu$ is the **coefficient of friction**. Here $\mu$ is the Greek letter $mu$.  The direction of the force of friction is parallel to the surface and opposite to the other forces acting on the object.

Use the slider below to observe the relationship between the normal force and the friction force. Pick any value for the coefficient of friction. Move the slider for the normal force back and forth to calculate the corresponding force of friction.

#import ipywidgets as widgets

coeff = widgets.FloatSlider(description="Coefficient",min=0.1,max=0.9)
normal_force = widgets.IntSlider(description="Normal force",min=5,max=50)

#Boldface letters at the beginning of each option
start_bold = "\033[1m"; end_bold = "\033[0;0m"

def f(coeff, normal_force):
    friction_force = coeff * normal_force
    print(start_bold + "Friction force = (coefficient of friction) X (normal force)" + end_bold)
    print("Friction force = {} X {} = {} N".format(coeff, normal_force, round(friction_force,1)))

out = widgets.interactive_output(f,{'coeff': coeff, 'normal_force': normal_force})

widgets.HBox([widgets.VBox([coeff, normal_force]), out])

**Question:** *What happens to the friction force when the normal force increases?*

#import ipywidgets as widgets

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = "The friction force increases."
option_2 = "The friction force decreases."
option_3 = "The friction force remains constant."
option_4 = "The friction force equals zero."

multiple_choice(option_1, option_2, option_3, option_4)

The force of friction felt by an object depends on whether the objects are in motion with respect to each other.  In general the force of friction felt by an object which is not moving with respect to another is stronger than the force of friction felt once it begins moving.

**Static friction** describes the friction force that acts between an object and a surface to prevent it from sliding when a force is applied to it. Static friction applies to objects that are stationary with respect to one another.

**Kinetic friction** describes the friction force that acts between an object and the surface it slides upon when a force is applied to it. Kinetic friction applies to objects that are in motion with respect to one another.

### Coefficients of Friction

Values for the coefficients of friction have been derived experimentally for various materials as they interact with one another. When we describe the friction acting on a stationary object, we must use the **coefficient of static friction, $\mu_{s}$**. When we describe the friction of a sliding object, we must use the **coefficient of kinetic friction, $\mu_{k}$**. Some values for the coefficients of static and kinetic friction are shown in the table below:

 Materials          | Coefficients of static friction ($\mu_{s}$)| Coefficients of kinetic friction ($\mu_{k}$)
 ---                | ---                                       | ---
 Steel on steel     | 0.7                                       | 0.6
 Glass on glass     | 0.9                                       | 0.4
 Wood on wood       | 0.4                                       | 0.2
 Rubber on concrete | 1.0                                       | 0.8

Use the slider again to observe the relationship between the coefficient of friction and the friction force. Pick any value for the normal force. Move the slider for the coefficient of friction back and forth to calculate the corresponding force of friction.

#import ipywidgets as widgets

coeff = widgets.FloatSlider(description="Coefficient",min=0.1,max=0.9)
normal_force = widgets.IntSlider(description="Normal force",min=5,max=50)

#Boldface letters at the beginning of each option
start_bold = "\033[1m"; end_bold = "\033[0;0m"

def f(coeff, normal_force):
    friction_force = coeff * normal_force
    print(start_bold + "Friction force = (coefficient of friction) X (normal force)" + end_bold)
    print("Friction force = {} X {} = {} N".format(coeff, normal_force, round(friction_force,1)))

out = widgets.interactive_output(f,{'coeff': coeff, 'normal_force': normal_force})

widgets.HBox([widgets.VBox([coeff, normal_force]), out])

**Question:** *What happens to the friction force when the coefficient of friction increases?*

#import ipywidgets as widgets

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = "The friction force increases."
option_2 = "The friction force decreases."
option_3 = "The friction force remains constant."
option_4 = "The friction force equals zero."

multiple_choice(option_1, option_2, option_3, option_4)

It is to be noted that the amount $\mu_s F_{n}$ is actually the **maximal value** for the magnitude of **static** friction $F_{f}$.  Assuming the forces acting on an object are insufficient to overcome this maximal value, the object will be stationary.  In this case, the actual magnitude of the force of static friction, $F_{f}$, will typically be less than maximal; it is exactly that amount required to have a net force of zero so that the object does not move.

### Solving Friction Problems

When solving a friction problem, it is useful to construct a free-body diagram. A **free-body diagram** is a simple graphical representation of an object and all the relevant forces acting upon it. An example of a free-body diagram is shown below:

* $F_{a}$ represents the **applied force**: the force acting in the direction of motion of the object.
* $F_{f}$ represents the **friction force**: the force acting in the direction opposite to the motion of the object.
* $mg$ represents the weight: the force of gravity acting on the object. Here, $m$ is the mass of the object, and $g$ is the gravitational acceleration directed downwards and of magnitude $g=9.8\:m/s^2$.
* $F_{n}$ represents the normal force: the force perpendicular to the surface pressing up against the object.

### Example
A 10 kg object is pushed across a flat horizontal surface with an applied force of 25 N. The same object is later pushed again with an applied force of 50 N. The coefficients of static and kinetic frictions are 0.40 and 0.30, respectively. Calculate the friction force ($F_{f}$) at each of the applied forces. Use the formula: $F_{f}=\mu F_{n}$.

**Step 1:** *Construct a free-body diagram*. The free-body diagram shown above may be used for this example.

**Step 2:** *Calculate the known forces*: **Recall:** The weight of the object, $mg$, is equal to the mass of the object multiplied by the acceleration due to gravity.

$$mg = (10 \times 9.8)\:N = 98\:N$$

Since there is no vertical acceleration on the object, the total vertical force must vanish so the magnitude of the normal force must be equal to that of the weight of the object ($F_{n} = mg$). Therefore,

$$F_{n} = mg = (10 \times 9.8)\:N = 98\:N$$

**Step 3:** *Determine the maximum static friction force*: Now that the normal force is known, we can calculate the maximum static friction force using the following equation:

$$F_{s}=\mu_{s} F_{n} = (0.40 \times 98)\:N = 39\:N$$

This means that the object will resist up to 39 N of applied force before it begins to move.

**Step 4:** *Determine the friction force at 25 N*: Since the applied force of 25 N is less than the maximum static friction force, the object will remain stationary. The friction will oppose the applied force up to 25 N. Therefore,

$$F_{f} = 25\:N$$

The direction of $F_{f}$ will be opposite to the direction of the applied force.

**Step 5:** *Determine the friction force at 50 N*: Since the applied force of 50 N is greater than the maximum static friction force, the object will begin to move. When in motion, the object is no longer being opposed by the static friction force. Instead it is being opposed by the kinetic friction force. We can calculate the kinetic friction force using the following equation:

$$F_{k}=\mu_{k} F_{n} = (0.30 \times 98)\:N = 29\:N$$

Therefore, the magnitude of the friction force will be:

$$F_{f} = 29\:N$$

Once again, the direction of $F_{f}$ will be opposite to the direction of the applied force.

**Answer:** At an applied force of 25 N, the object is stationary and the magnitude of the friction force is 25 N. At an applied force of 50 N, the object is in motion and the magnitude of the friction force is 29 N. In both cases, the force of friction is directed parallel to the surface and opposite to the direction of the applied force.

## Practice Problems
(Click **Cell** > **Run Cells** to generate new random values for each question. Refer to the previous table to get the coefficients of friction for specific materials.)

#import random
#import ipywidgets as widgets

#Randomize mass and friction coefficient
mass = random.randint(20,50)
coeff = (random.randint(10,100))/100

#Print question
question = "A " + str(mass) +" kg object is pushed across a flat horizontal surface. The coefficient of kinetic friction between the moving object and the surface is " + str(coeff) +". What is the magnitude of the friction force?"
print(question)

#Answer calculation
#Friction force = (friction coefficient) X (normal force)
answer = coeff*(mass*9.8)
answer = round(answer)

#Define range of values for random multiple choices
min = int(coeff*((mass-15)*9.8))
max = int(coeff*((mass+15)*9.8))

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(min,max),3)
while choice_list.count(answer) >= 1:
    choice_list = random.sample(range(min,max),3)

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = str(answer) + " N"
option_2 = str(choice_list[0]) + " N"
option_3 = str(choice_list[1]) + " N"
option_4 = str(choice_list[2]) + " N"

multiple_choice(option_1, option_2, option_3, option_4)

#import random
#import ipywidgets as widgets

#Randomize mass and applied force
mass = random.randint(20,50)
applied_force = random.randint(150,300)

#Randomize material
material_options = ['steel', 'glass', 'wood']
material =random.choice(material_options)

#Define friction coefficients based on selected material
if material == 'steel':
    us = 0.7; uk = 0.6
elif material == 'glass':
    us = 0.9; uk = 0.4
elif material == 'wood':
    us = 0.4; uk = 0.2

#Print question
question = "If a %d kg %s object is pushed across a flat horizontal %s surface with an applied force of %d N, what is the magnitude of the friction force?" %(mass, material, material, applied_force)
print(question)

#Answer calculation
#Friction force = (friction coefficient) X (normal force)
static_friction = us*(mass*9.8)
kinetic_friction = uk*(mass*9.8)

#If the applied force is less than or equal to the maximum static friction force, the object will remain stationary
#The friction force equals the applied force
if applied_force <= static_friction:
    answer = applied_force
else:
    answer = kinetic_friction

answer = round(answer)

#Define range of values for random multiple choices
if applied_force <= static_friction:
    min = applied_force-15
    max = applied_force+15
else:
    min = int(uk*((mass-15)*9.8))
    max = int(uk*((mass+15)*9.8))

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(min,max),3)
while choice_list.count(answer) >= 1:
    choice_list = random.sample(range(min,max),3)

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = str(answer) + " N"
option_2 = str(choice_list[0]) + " N"
option_3 = str(choice_list[1]) + " N"
option_4 = str(choice_list[2]) + " N"

multiple_choice(option_1, option_2, option_3, option_4)

#import random
#import ipywidgets as widgets

#Randomize applied and friction forces
applied_force = random.randint(51,75)
friction_force = random.randint(25,50)

#Randomize material
material_options = ['steel', 'glass', 'wood']
material =random.choice(material_options)

#Define friction coefficients based on selected material
if material == 'steel':
    uk = 0.6; us = 0.7
elif material == 'glass':
    uk = 0.4; us = 0.9
elif material == 'wood':
    uk = 0.2; us = 0.4

#Print question
question = "If a %s object is pushed across a flat horizontal %s surface with an applied force of %d N, and a friction force of %d N occurs, what is the magnitude of the normal force?" %(material, material, applied_force, friction_force)
print(question)

#Answer calculation
#weight = normal force = (friction force)/(kinetic friction coefficient)
answer = friction_force/uk
answer = round(answer)

#Define range of values for random multiple choices
min = int((friction_force-15)/uk); max = int((friction_force+15)/uk)

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(min,max),3)
while choice_list.count(answer) >= 1:
    choice_list = random.sample(range(min,max),3)

#Assign each option to these four variables
#Option1 contains the answer
option_1 = str(answer) + " N"
option_2 = str(random.randint(min,max)) + " N"
option_3 = str(random.randint(min,max)) + " N"
option_4 = str(random.randint(min,max)) + " N"

multiple_choice(option_1, option_2, option_3, option_4)

#import random
#import ipywidgets as widgets
#import numpy as np

#Randomize mass and applied force
mass = random.randint(5,10)
applied_force = random.randint(46,80)
friction_force = random.randint(10,45)

#Print question
question = "A %d kg object is pulled across a flat horizontal surface by a force of %d N. The friction force is %d N. What is the coefficient of kinetic friction?" %(mass, applied_force, friction_force)
print(question)

#Answer calculation
#friction coefficient = (friction force) / (normal force)
answer = friction_force/(mass*9.8)
answer = round(answer,2)

#Define range of values for random multiple choices
min = 0.1
max = 0.9
unique = 0

#Create three choices that are unique (and not equal to the answer)
while unique < 4:
    choice_list = np.random.uniform(min, max, size=(3,))
    for i in range(3):
        choice_list[i] = round(choice_list[i],2)

    choice_list = np.append(choice_list, answer)
    list_unique = np.unique(choice_list)
    unique = list_unique.size

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = str(answer)
option_2 = str(choice_list[0])
option_3 = str(choice_list[1])
option_4 = str(choice_list[2])

multiple_choice(option_1, option_2, option_3, option_4)

Use the diagram below to answer the following question:

#import random
#import ipywidgets as widgets

#Randomize mass and friction coefficient
mass = random.randint(20,50)
coeff = (random.randint(10,100))/100

#Print question
question = "A " + str(mass) +" kg object is suspended by a pulley and connected to an object resting on a flat surface. The friction coefficient between the object and the surface is " + str(coeff) +". What is the minimum mass required to keep this suspended object stationary?"
print(question)

#Answer calculation
#Friction force = applied force = (mass) X (9.8)
friction_force = mass*9.8

normal_force = friction_force/coeff
answer = normal_force/9.8
answer = round(answer)

#Define range of values for random multiple choices
min = int((normal_force-50)/9.8)
max = int((normal_force+50)/9.8)

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(min,max),3)
while choice_list.count(answer) >= 1:
    choice_list = random.sample(range(min,max),3)

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = str(answer) + " N"
option_2 = str(choice_list[0]) + " N"
option_3 = str(choice_list[1]) + " N"
option_4 = str(choice_list[2]) + " N"

multiple_choice(option_1, option_2, option_3, option_4)

## Experiment

As mentioned above, the coefficients of static and kinetic friction are determined experimentally for different materials.

**Determination of Static Friction**

Try the following experiment to determine the coefficient of static friction.

**Step 1:** Find an object and a flat horizontal surface. Record the material type for the object and surface.

**Step 2:** Measure the mass of the object using a mass balance (kg).

**Step 3:** Calculate the magnitude of the normal force using the following formula: $F_{n} = mg$

**Step 4:** Using a spring scale, gradually apply a force to the object. Record the value on the scale the moment the object begins to move. At this moment, the applied force is equal to the maximum static friction force.

**Step 5:** Calculate the coefficient of static friction using the following formula: $\mu_{s} = F_{f} \div F_{n}$

**Determination of Kinetic Friction**

Continue the experiment to determine the coefficient of kinetic friction.

**Step 6:** Using a spring scale, apply enough force to drag the object across the surface at a constant velocity (acceleration = 0). Record the value on the scale while the object is moving. So long as the object moves at a constant velocity, the applied force is equal to the kinetic friction force.

**Step 7:** Calculate the coefficient of kinetic friction using the following formula: $\mu_{k} = F_{f} \div F_{n}$

 (**Double click** this cell and update the table with your own data. Click **Cell** > **Run Cells** when done.)

Materials | Object mass (g) | Normal force (N) | Applied force (N) | Static friction coefficient | Kinetic friction coefficient
 ---      | ---             | ---              | ---               | ---                         | ----
 ?        | ?               | ?                | ?                 | ?                           | ?
 ?        | ?               | ?                | ?                 | ?                           | ?
 ?        | ?               | ?                | ?                 | ?                           | ?
 ?        | ?               | ?                | ?                 | ?                           | ?
 ?        | ?               | ?                | ?                 | ?                           | ?

## Force Diagrams

A useful way to visualize the friction forces is to construct a force diagram, as shown below. This diagram depicts the magnitudes of the forces acting on an object as it is pushed across a flat horizontal surface with increasing force. The y-axis depicts the friction force ($F_{f}$), and the x-axis depicts the applied force ($F_{a}$). The sliders can be manipulated to change the normal force ($F_{n}$), the coefficient of static friction ($\mu_{s}$), and the coefficient of kinetic friction ($\mu_{k}$).

#import ipywidgets as widgets
#import matplotlib.pyplot as plt

def f(Fn=50,μs=0.75,μk=0.25):
    #Fn = normal force
    #μs = coefficient of static friction
    #μk = coefficient of kinetic friction

    plt.figure()
    xs = Fn*μs
    xk = Fn*μk
    plt.plot([0,xs,xs,100],[0,xs,xk,xk])
    plt.plot([xs,xs],[xk,0], linestyle="dotted")
    plt.ylim(0, 100)
    plt.xlim(0,100)
    plt.ylabel('Friction force (N)')
    plt.xlabel('Applied force (N)')
    plt.annotate(xy=[xs-15,xs+5],s="Static friction (max)")
    plt.annotate(xy=[(xs+(100-xs)/2)-10,xk+5],s="Kinetic friction")
    plt.show()

interactive_plot = widgets.interactive(f,Fn=(35,75,5),μs=(0.5, 1.0),μk=(0.1, 0.5))
output = interactive_plot.children[-1]
output.layout.height = '280px'
interactive_plot

**Interpreting the graph**

Read the graph from left to right. As the applied force gradually increases along the x-axis, the friction force also increases. Notice that the magnitude of the friction force is equal to the applied force ($F_{f} = F_{a}$) until it reaches the point of maximum static friction. This point is shown on the graph as a **peak**. The point of maximum static friction is determined by the following equation: $F_{f}=\mu_{s} F_{n}$. As the applied force continues to increase beyond the peak of maximum static friction, the object begins to move. The friction force is now described by the kinetic friction equation: $F_{f}=\mu_{k} F_{n}$.

Move the slider for the normal force back and forth. As the normal force increases, what happens to the static and kinetic friction forces?

#import ipywidgets as widgets

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = "The static and kinetic friction forces both increase."
option_2 = "The static and kinetic friction forces both decrease."
option_3 = "The static friction force increases and the kinetic friction force decreases."
option_4 = "The static friction force decreases and the kinetic friction force increases."

multiple_choice(option_1, option_2, option_3, option_4)

Move the slider for the static friction coefficient back and forth. As the static friction coefficient increases, what happens to the static and kinetic friction forces?

#import ipywidgets as widgets

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = "The static friction force increases and the kinetic friction force remains constant."
option_2 = "The static friction force decreases and the kinetic friction force remains constant."
option_3 = "The static friction force increases and the kinetic friction force decreases."
option_4 = "The static friction force decreases and the kinetic friction force increases."

multiple_choice(option_1, option_2, option_3, option_4)

Move the slider for the kinetic friction coefficient back and forth. As the kinetic friction coefficient increases, what happens to the static and kinetic friction forces?

#import ipywidgets as widgets

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = "The kinetic friction force increases and the static friction force remains constant."
option_2 = "The kinetic friction force decreases and the static friction force remains constant."
option_3 = "The kinetic friction force increases and the static friction force decreases."
option_4 = "The kinetic friction force decreases and the static friction force increases."

multiple_choice(option_1, option_2, option_3, option_4)

## Conclusions

In this notebook, the concepts of static and kinetic friction were examined. In summary:

* **Friction** describes the force that resists the relative motion of an object as it slides across a surface. Friction forces are proportional to the normal force:

$$F_{f} = \mu F_{n}$$

* **Static friction** describes the friction force that acts between an object and the surface to prevent it from sliding. Static friction must be overcome for an object to move.

* **Kinetic friction** describes the friction force that acts between an object and the surface it slides upon. Kinetic friction is used when describing objects in motion.

* **Coefficients of static and kinetic friction** are determined experimentally for different materials. Once determined, these values can be tabulated and used to solve friction problems. An experimental method for determining the coefficients of static and kinetic friction was presented.

* **Friction problems** can be solved using free-body diagrams, the friction formula, and the tabulated values for the coefficients of friction.

* **Force diagrams** can be used to visualize the relationship between the friction force, the applied force, the normal force, and the coefficients of static and kinetic friction.

Images in this notebook represent original artwork.

%%html

<form action="javascript:code_toggle()"><input type="submit" id="toggleButton" value="Show Code"></form>

[![Callysto.ca License](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-bottom.jpg?raw=true)](https://github.com/callysto/curriculum-notebooks/blob/master/LICENSE.md)


#!/usr/bin/env python
# coding: utf-8

# # 14 - Advanced topics - Cement Pavers albedo example
#
# This journal creates a paver underneath the single-axis trackers, and evaluates the improvement for one day -- June 17th with and without the pavers for a location in Davis, CA.
#
# ![Paver](../images_wiki/AdvancedJournals/Pavers.PNG)
#
# Measurements:
# ![Paver](../images_wiki/AdvancedJournals/Pavers_Geometry.PNG)

# In[1]:

import os
from pathlib import Path
import pandas as pd

testfolder = str(Path().resolve().parent.parent / 'bifacial_radiance' / 'TEMP' / 'Tutorial_14')
if not os.path.exists(testfolder):
    os.makedirs(testfolder)

print ("Your simulation will be stored in %s" % testfolder)

# In[2]:

from bifacial_radiance import *
import numpy as np

# In[3]:

simulationname = 'tutorial_14'

#Location:
lat = 38.5449 # Davis, CA
lon = -121.7405 # Davis, CA
# MakeModule Parameters
moduletype='test-module'
numpanels = 1  # AgriPV site has 3 modules along the y direction (N-S since we are facing it to the south) .
x = 0.95
y = 1.838
xgap = 0.02# Leaving 2 centimeters between modules on x direction
ygap = 0.0 # 1 - up
zgap = 0.06 # gap between modules and torquetube.

# Other default values:

# TorqueTube Parameters
axisofrotationTorqueTube=True
torqueTube = False
cellLevelModule = True

numcellsx = 6
numcellsy = 10
xcell = 0.156
ycell = 0.158
xcellgap = 0.015
ycellgap = 0.015

sensorsy = numcellsy   # one sensor per cell

cellLevelModuleParams = {'numcellsx': numcellsx, 'numcellsy':numcellsy,
                         'xcell': xcell, 'ycell': ycell, 'xcellgap': xcellgap, 'ycellgap': ycellgap}

# SceneDict Parameters
gcr = 0.33 # m
albedo = 0.2  #'grass'     # ground albedo
hub_height = 1.237 # m
nMods = 20 # six modules per row.
nRows = 3  # 3 row

azimuth_ang = 90 # Facing east

demo = RadianceObj(simulationname,path = testfolder)  # Create a RadianceObj 'object'
demo.setGround(albedo) #
epwfile = demo.getEPW(lat, lon)
metdata = demo.readWeatherFile(epwfile, coerce_year=2021) # read in the EPW weather data from above
mymodule=demo.makeModule(name=moduletype,x=x,y=y,numpanels = numpanels, xgap=xgap, ygap=ygap)
mymodule.addCellModule(numcellsx=numcellsx, numcellsy=numcellsy,
                       xcell=xcell, ycell=ycell, xcellgap=xcellgap, ycellgap=ycellgap)

# In[4]:

description = 'Sherman Williams "Chantilly White" acrylic paint'
materialpav = 'sw_chantillywhite'
Rrefl = 0.5
Grefl = 0.5
Brefl = 0.5
demo.addMaterial(material=materialpav, Rrefl=Rrefl, Grefl=Grefl, Brefl=Brefl, comment=description)

# ### Simulation without Pavers

# In[5]:

timeindex = metdata.datetime.index(pd.to_datetime('2021-06-17 12:0:0 -8'))  # Davis, CA is TZ -8
demo.gendaylit(timeindex)

tilt = demo.getSingleTimestampTrackerAngle(metdata, timeindex=timeindex, gcr=gcr,
                                   azimuth=180, axis_tilt=0,
                                   limit_angle=60, backtrack=True)
# create a scene with all the variables
sceneDict = {'tilt':tilt,'gcr': gcr,'hub_height':hub_height,'azimuth':azimuth_ang, 'module_type':moduletype, 'nMods': nMods, 'nRows': nRows}
scene = demo.makeScene(module=mymodule, sceneDict=sceneDict) #makeScene creates a .rad file with 20 modules per row, 7 rows.
octfile = demo.makeOct(demo.getfilelist())  # makeOct combines all of the ground, sky and object fil|es into a .oct file.

# In[6]:

analysis = AnalysisObj(octfile, demo.name)  # return an analysis object including the scan dimensions for back irradiance
frontscan, backscan = analysis.moduleAnalysis(scene, sensorsy=sensorsy)
analysis.analysis(octfile, simulationname+"_noPavers", frontscan, backscan)  # compare the back vs front irradiance
print("Simulation without Pavers Finished")

# ## Looping on the day

# In[7]:

j=0
starttimeindex = metdata.datetime.index(pd.to_datetime('2021-06-17 7:0:0 -8'))
endtimeindex = metdata.datetime.index(pd.to_datetime('2021-06-17 19:0:0 -8'))
for timess in range (starttimeindex, endtimeindex):
    j+=1
    demo.gendaylit(timess)
    tilt = demo.getSingleTimestampTrackerAngle(metdata, timeindex=timess, gcr=gcr,
                                       azimuth=180, axis_tilt=0,
                                       limit_angle=60, backtrack=True)
    # create a scene with all the variables
    sceneDict = {'tilt':tilt,'gcr': gcr,'hub_height':hub_height,'azimuth':azimuth_ang, 'module_type':moduletype, 'nMods': nMods, 'nRows': nRows}
    scene = demo.makeScene(module=mymodule, sceneDict=sceneDict) #makeScene creates a .rad file with 20 modules per row, 7 rows.
    octfile = demo.makeOct(demo.getfilelist())  # makeOct combines all of the ground, sky and object fil|es into a .oct file
    frontscan, backscan = analysis.moduleAnalysis(scene, sensorsy=sensorsy)
    analysis.analysis(octfile, simulationname+"_noPavers_"+str(j), frontscan, backscan)  # compare the back vs front irradiance

# ### Simulation With Pavers

# In[8]:

demo.gendaylit(timeindex)
tilt = demo.getSingleTimestampTrackerAngle(metdata, timeindex=timeindex, gcr=gcr,
                                   azimuth=180, axis_tilt=0,
                                   limit_angle=60, backtrack=True)
# create a scene with all the variables
sceneDict = {'tilt':tilt,'gcr': gcr,'hub_height':hub_height,'azimuth':azimuth_ang, 'module_type':moduletype, 'nMods': nMods, 'nRows': nRows}
scene = demo.makeScene(module=mymodule, sceneDict=sceneDict) #makeScene creates a .rad file with 20 modules per row, 7 rows.

# In[9]:

torquetubelength = demo.module.scenex*(nMods)
pitch = demo.module.sceney/gcr
startpitch = -pitch * (nRows-1)/2
p_w = 0.947 # m
p_h = 0.092 # m
p_w2 = 0.187 # m
p_h2 = 0.184 # m
offset_w1y = -(p_w/2)+(p_w2/2)
offset_w2y = (p_w/2)-(p_w2/2)

customObjects = []
for i in range (0, nRows):
    name='PAVER'+str(i)
    text='! genbox {} paver{} {} {} {} | xform -t {} {} 0 | xform -t {} 0 0'.format(materialpav, i,
                                    p_w, torquetubelength, p_h,
                                    -p_w/2, (-torquetubelength+demo.module.sceney)/2.0,
                                    startpitch+pitch*i)
    text += '\r\n! genbox {} paverS1{} {} {} {} | xform -t {} {} 0 | xform -t {} 0 0'.format(materialpav, i,
                                    p_w2, torquetubelength, p_h2,
                                    -p_w2/2+offset_w1y, (-torquetubelength+demo.module.sceney)/2.0,
                                    startpitch+pitch*i)
    text += '\r\n! genbox {} paverS2{} {} {} {} | xform -t {} {} 0 | xform -t {} 0 0'.format(materialpav, i,
                                    p_w2, torquetubelength, p_h2,
                                    -p_w2/2+offset_w2y, (-torquetubelength+demo.module.sceney)/2.0,
                                    startpitch+pitch*i)

    customObject = demo.makeCustomObject(name,text)
    customObjects.append(customObject)
    demo.appendtoScene(radfile=scene.radfiles, customObject=customObject, text="!xform -rz 0")

# In[10]:

demo.makeOct()

# You can view the geometry generated in the terminal with:
#
# ### rvu -vf views\front.vp -e .01 -pe 0.01 -vp -5 -14 1 -vd 0 0.9946 -0.1040 tutorial_14.oct

# In[11]:

## Comment the ! line below to run rvu from the Jupyter notebook instead of your terminal.
## Simulation will stop until you close the rvu window

#!rvu -vf views\front.vp -e .01 -pe 0.01 -vp -5 -14 1 -vd 0 0.9946 -0.1040 tutorial_14.oct

# In[12]:

analysis = AnalysisObj(octfile, demo.name)  # return an analysis object including the scan dimensions for back irradiance
frontscan, backscan = analysis.moduleAnalysis(scene, sensorsy=sensorsy)
analysis.analysis(octfile, simulationname+"_WITHPavers", frontscan, backscan)  # compare the back vs front irradiance
print("Simulation WITH Pavers Finished")

# ## LOOP WITH PAVERS

# In[13]:

j=0
for timess in range (starttimeindex, endtimeindex):
    j+=1
    demo.gendaylit(timess)
    tilt = demo.getSingleTimestampTrackerAngle(metdata, timeindex=timess, gcr=gcr,
                                       azimuth=180, axis_tilt=0,
                                       limit_angle=60, backtrack=True)
    # create a scene with all the variables
    sceneDict = {'tilt':tilt,'gcr': gcr,'hub_height':hub_height,'azimuth':azimuth_ang, 'module_type':moduletype, 'nMods': nMods, 'nRows': nRows}
    scene = demo.makeScene(mymodule, sceneDict=sceneDict) #makeScene creates a .rad file with 20 modules per row, 7 rows.
    # Appending Pavers here
    demo.appendtoScene(radfile=scene.radfiles, customObject=customObjects[0], text="!xform -rz 0")
    demo.appendtoScene(radfile=scene.radfiles, customObject=customObjects[1], text="!xform -rz 0")
    demo.appendtoScene(radfile=scene.radfiles, customObject=customObjects[2], text="!xform -rz 0")
    octfile = demo.makeOct(demo.getfilelist())  # makeOct combines all of the ground, sky and object fil|es into a .oct file
    frontscan, backscan = analysis.moduleAnalysis(scene, sensorsy=sensorsy)
    analysis.analysis(octfile, simulationname+"_WITHPavers_"+str(j), frontscan, backscan)  # compare the back vs front irradiance

# # RESULTS ANALYSIS NOON

# In[14]:

df_0 = load.read1Result(os.path.join(testfolder, 'results', 'irr_tutorial_14_noPavers.csv'))
df_w = load.read1Result(os.path.join(testfolder, 'results', 'irr_tutorial_14_WITHPavers.csv'))

# In[15]:

df_0

# In[16]:

df_w

# ## Improvement in Rear Irradiance

# In[17]:

round((df_w['Wm2Back'].mean()-df_0['Wm2Back'].mean())*100/df_0['Wm2Back'].mean(),1)

# # RESULT ANALYSIS DAY

# In[18]:

df_0 = load.read1Result(os.path.join(testfolder, 'results', 'irr_tutorial_14_noPavers_1.csv'))
df_w = load.read1Result(os.path.join(testfolder, 'results', 'irr_tutorial_14_WITHPavers_1.csv'))

# In[19]:

df_w

# In[20]:

df_0

# In[21]:

round((df_w['Wm2Back'].mean()-df_0['Wm2Back'].mean())*100/df_0['Wm2Back'].mean(),1)

# In[22]:

average_back_d0=[]
average_back_dw=[]
average_front = []
hourly_rearirradiance_comparison = []

timessimulated = endtimeindex-starttimeindex

for i in range (1, timessimulated+1):
    df_0 = load.read1Result(os.path.join(testfolder, 'results', 'irr_tutorial_14_noPavers_'+str(i)+'.csv'))
    df_w = load.read1Result(os.path.join(testfolder, 'results', 'irr_tutorial_14_WITHPavers_'+str(i)+'.csv'))
    print(round((df_w['Wm2Back'].mean()-df_0['Wm2Back'].mean())*100/df_0['Wm2Back'].mean(),1))
    hourly_rearirradiance_comparison.append(round((df_w['Wm2Back'].mean()-df_0['Wm2Back'].mean())*100/df_0['Wm2Back'].mean(),1))
    average_back_d0.append(df_0['Wm2Back'].mean())
    average_back_dw.append(df_w['Wm2Back'].mean())
    average_front.append(df_0['Wm2Front'].mean())

# In[23]:

print("Increase in rear irradiance: ", round((sum(average_back_dw)-sum(average_back_d0))*100/sum(average_back_d0),1))

# In[24]:

print("BG no Pavers: ", round(sum(average_back_d0)*100/sum(average_front),1))
print("BG with Pavers: ", round(sum(average_back_dw)*100/sum(average_front),1))

# In[27]:

import matplotlib.pyplot as plt

#metdata.datetime[starttime].hour # 7
#metdata.datetime[endtimeindex].hour # 17
xax= [7, 8, 9, 10, 11, 12,13,14,15,16,17,18]  # Lazy way to get the x axis...

# In[28]:

plt.plot(xax,hourly_rearirradiance_comparison)
plt.ylabel('$\Delta$ in G$_{rear}$ [%] \n(G$_{rear-with}$ - G$_{rear-without}$ / G$_{rear-without}$)')
plt.xlabel('Hour')

docs/tutorials/14 - Advanced topics - Cement Pavers albedo example.py


#!/usr/bin/env python
# coding: utf-8

# ---
# title: ERP Core N400 in MNE-Python: part III
# date: 2021-04-23
# image:
#   preview_only: true
# tags:
# - Python
# - EEG
# - Preprocessing
# categories:
# - Python
# - EEG
# - English
# summary: "Adapting ICA artifact rejection."
# copyright_license:
#   enable: true
#
# ---

# In the [last post](https://msilvestrin.me/post/n400_2/) on adapting ERP CORE's N400 pipeline to MNE-Python, we did the ICA-related steps to correct blinking and horizontal eye-movement artfacts from the signal. Here we do some final steps in cleaning the data: interpolation of bad channels, epoching and rejection of epochs with possible artifacts. These steps are decipted in the  _Elist_Bin_Epoch_ script of ERP CORE resources (script 5).
#
# Contrary to previous posts, I won't make the whole process in a single participant and show the loop for all participants at the end. That is because the result for some af the steps are saved as intermediate stages of the preprocessing pipeline. I should also tell you up-front that I wasn't able to find implemented equivalents for some of the procedures ERP CORE uses to define artifacts. That forced me to implement a python version of what they do. Therefore, this post is a little longer the the other ones, so buckle up! On the bright side, at the end we will have the data ready to start looking for those N400 ERPs.
#

# ## Interpolation
# Our first step here is to interpolate bad channels. Bad channels for each participant are listed in the file "Interpolate_Channels_N400.xlsx" provided by ERP CORE. Below, I load the information in that file, make a list of bad channels for each participant and add that list to their `raw.info['bads']`.
#
# Load the information:

# In[3]:

import os
os.chdir('D:\\EEGdata\\Erp CORE\\N400')

import mne
# import some methods directly so we can call them by name
from mne.io import read_raw
from mne.epochs import read_epochs

import numpy as np
import pandas as pd

get_ipython().run_line_magic('matplotlib', 'qt')
get_ipython().run_line_magic('gui', 'qt')

# In[8]:

# load as DataFrame
interpolate_path = "D:\\EEGdata\\Erp CORE\\N400\\EEG_ERP_Processing\\Interpolate_Channels_N400.xlsx"
chans_interpolate = pd.read_excel(interpolate_path)
# Turn strings to a list of strings for each participant
chans_interpolate = chans_interpolate.loc[:,"Name of Interpolated Channel"].str.split(" ")

# Now I'll deal with some nuisences in the file: there is an asterisk at the beginning of the channel list for each participant, and Fp channels are in upper-case. We remove the asterisk and correct the captalization.

# In[11]:

# make function to remove asterisk and correct
def remove_star(item):
    # if item is a list
    if isinstance(item, list):
        # if list is longer than 1
        if len(item) > 1:
            # remove asterisk
            clean_names = [item[0].lstrip("*")]
            # add channel to a list clean_names
            clean_names.extend(item[1:])
            # look for Fp channels and correct captalization
            for n, elec in enumerate(clean_names):
                if elec =='FP1' or elec =='FP2':
                    clean_names[n] = elec.capitalize()
        else:
            clean_names = [item[0].lstrip("*")]
            if clean_names[0] =='FP1' or clean_names[0] =='FP2':
                clean_names[0] = clean_names[0].capitalize()
    else:
        clean_names = np.nan
    return clean_names

# apply function
chans_interpolate = chans_interpolate.apply(remove_star)
chans_interpolate.loc[chans_interpolate.notnull()].head()

# We have already indentified the channels to be interpolated, however if we try to do the interpolation now MNE will throw us an error because we have more EEG channels than mapped on the 3D digitation. The channels HEOG_left, HEOG_right and VEOG_lower are currently identified as EEG channels, but they are actually extra channels we used to make the proper HEOG and VEOG channels. We will drop them, since they already served their purpose.
#
# Let's do it for all participants and save an intermediate ("interpol") raw file:

# In[ ]:

for subj in range(1,41):
    raw_correct_path = "D:\\EEGdata\\Erp CORE\\N400\\mne\\raw_corrected\\N400_ERP_CORE_{}_correct-raw.fif".format(subj)
    raw_correct = read_raw(raw_correct_path, preload = True)
    #only do interpolation if there is at least one channel to interpolate
    if  not chans_interpolate.isna()[subj-1]:
        raw_correct.info['bads'].extend(chans_interpolate[subj-1])
        # drop extra channels
        raw_correct.drop_channels(['HEOG_left', 'HEOG_right', 'VEOG_lower'])
        raw_correct.interpolate_bads()
    else:
        raw_correct.drop_channels(['HEOG_left', 'HEOG_right', 'VEOG_lower'])

    interpol_path = "D:\\EEGdata\\Erp CORE\\N400\\mne\\raw_interpol\\N400_ERP_CORE_{}_interpol-raw.fif".format(subj)
    raw_correct.save(interpol_path, overwrite=True)

# ## Event list for epochs
# Now let's make our event list and event_ids for epoching.
#
# As we did at the back at beginning of preprocessing [(in this post)](https://msilvestrin.me/post/n400_1/), we will get the events from the `annotations` in the data. Until now, we have two types of annotation descriptions in the data: "BAD_seg" and numbers corresponding to triggers used in the experiment. The ERP CORE file "N400_Event_Code_Scheme.xlsx" has the meaning for each code. For instance, the code 121 refers to a Priming stimuli which is unrelated to the target stimulus and comes from the second list of words; while the code 201 means that the participant gave a correct answer.
#
# We are going to change these codes to a descriptive account of each event. A nice feature of MNE is that we can make _hierarchical ids_. We will follow the hierarchy shown in the N400_Event_Code_Scheme file: Word Type > Relatedness > Word List.
#
# First, we will create a dictionary with the correspondence between codes and events to properly identify them later. We will be able to use these ids to select epochs afterwards.

# In[ ]:

# load data
subj = 1
interpol_path = "D:\\EEGdata\\Erp CORE\\N400\\mne\\raw_interpol\\N400_ERP_CORE_{}_interpol-raw.fif".format(subj)
raw_interpol = read_raw(interpol_path)
# make dict with correspondences between codes and hierarchical ids
description_dict = {'111' : 'Prime/Related/L1',
             '112' : 'Prime/Related/L2',
             '121' : 'Prime/Unrelated/L1',
             '122' : 'Prime/Unrelated/L2',
             '211' : 'Target/Related/L1',
             '212' : 'Target/Related/L2',
             '221' : 'Target/Unrelated/L1',
             '222' : 'Target/Unrelated/L2',
             '201' : 'Hit',
             '202' : 'Miss',
             'BAD_seg': 'BAD_seg'
                   }

# We will change the description list with some [Pandas](https://pandas.pydata.org/) functionality. It is beyond this tutorial to teach Pandas' skills, but it is nice for you to know that, although most of the backbone of MNE is in [Numpy](https://numpy.org/), it also allows exportation to Pandas' data structures.
#
# In summary, what we do in the one-liner below is make a copy of the annotations descriptions while transforming it to a Pandas series, then we make the substituitions according to our `description dictionary` and turn it back to a numpy array.

# In[ ]:

raw_interpol.annotations.description = pd.Series(raw_interpol.annotations.description).map(description_dict).to_numpy()
# substitute annotations description
set(raw_interpol.annotations.description)

# Now we are almost ready to make the `events` numpy array we will need to epoch the data. We will use the `events_from_annotations` function. One last step before that: create a dictionary with correspondences between hierarchical ids and trigger numbers.
#
# You may be wondering why we would do that when we just made the reverse dictionary on our last step. The thing is that MNE's event list is a _numeric array_ composed of columns _instant_, _blank column_ and _event code_, and the default behavior of `events_from_annotations` is to assign the event codes as \[0,1,2,3...\]. However, I want to keep the original codes from ERP CORE, so I need to use the `events_id` input with a dict showing the correspondences. I should say that for the rest of our MNE operations the numeric codes themselves are actually rather irrelevant, what really matters are the correspondences in `event_ids`. I am just being picky to keep as close as possible to the original ERP CORE material.

# In[ ]:

event_ids = {'Prime/Related/L1': 111,
             'Prime/Related/L2': 112,
             'Prime/Unrelated/L1' : 121,
             'Prime/Unrelated/L2' : 122,
             'Target/Related/L1' : 211,
             'Target/Related/L2' : 212,
             'Target/Unrelated/L1' : 221,
             'Target/Unrelated/L2' : 222,
             'Hit' : 201,
             'Miss' : 202}
events, event_ids = mne.events_from_annotations(raw_interpol, event_ids)

# So, our events look like this (_instant_, _blank column_, _event code_):

# In[7]:

events

# ## Epoching and artfact rejection
# The ERP CORE pipeline is __very throrough__ in its epoch dropping due to ocular artifact detection. There are different thresholds for epoch dropping for: (1) EEG channels, (2) the VEOG channel and (3 and 4) for both variations of the HEOG channel (with and without ICA applied). In MNE, we can set epoch rejection thresholds for each channel type by setting the `reject` input when we create the epochs.
#
# However, as far as I understand from the documentation, MNE uses a simple peak-to-peak algorithm to do its automatic rejection. ERP CORE pipeline uses such an algorithm only for rejection on the scalp EEG channels. For VEOG and uncorrected HEOG channels it uses a windowed peak-to-peak algorithm and for ICA corrected HEOG it uses a step algorithm (both as implemented in the ERP LAB toolbox).
#
# Since ERP CORE gives us the parameters, and the algorithms themselves are quite straightforward, I have implemented them below. I make use of the nifty feature of MNE that allows us to loop through epochs. On each epoch I run the algorithms on the corresponding channels and, if some amplitude is above the threshold, it is marked as bad with a `MW_Reject` annotation. The value compared to the treshold on each algorithm are the following:
#
# * Step algorithm: $\bmod (mean(window1)-mean(window2))$
# * Peak-to-peak algorithm: $\bmod (max(window1)-min(window2))$
#
# Below, we (1) load the the information we need for the artfact rejection procedure, (2) create the dictionaries we need for marking the events in the experiment (as shown above); then, in the loop, we (3) add the annotations for epoching, (4) do the epoching with `mne.Epochs()`, (5) run the artfact rejection algorithm and (6) save the epochs.

# In[ ]:

#get eog channels numbers
subj = 1
interpol_path = "D:\\EEGdata\\Erp CORE\\N400\\mne\\raw_interpol\\N400_ERP_CORE_{}_interpol-raw.fif".format(subj)
raw_interpol = read_raw(interpol_path)
eog_nums = [np.where(np.array(raw_interpol.ch_names) == 'VEOG')[0].item(0),
            np.where(np.array(raw_interpol.ch_names) == 'HEOG')[0].item(0),
            np.where(np.array(raw_interpol.ch_names) == 'HEOG_ICA')[0].item(0)]

#get veog moving windows parameters from files
veog_path = "D:\\EEGdata\\Erp CORE\\N400\\EEG_ERP_Processing\\AR_Parameters_for_MW_CRAP_N400.xlsx"
veog_mw_params = pd.read_excel(veog_path)
#convert time columns to seconds
veog_mw_params.iloc[:,3:] = veog_mw_params.iloc[:,3:].apply(lambda x: x*1e-3)
#convert threshold to volts
veog_mw_params.Threshold = veog_mw_params.Threshold*1e-4

# the same for non ICA corrected heog moving windows
heog_path = "D:\\EEGdata\\Erp CORE\\N400\\EEG_ERP_Processing\\AR_Parameters_for_MW_Blinks_N400.xlsx"
heog_mw_params = pd.read_excel(heog_path)
heog_mw_params.iloc[:,3:] = heog_mw_params.iloc[:,3:].apply(lambda x: x*1e-3)
heog_mw_params.Threshold = heog_mw_params.Threshold*1e-4

#... and for ICA corrected HEOG moving windows
heog_ica_path = "D:\\EEGdata\\Erp CORE\\N400\\EEG_ERP_Processing\\AR_Parameters_for_SL_HEOG_N400.xlsx"
heog_ica_mw_params = pd.read_excel(heog_ica_path)
heog_ica_mw_params.iloc[:,3:] = heog_ica_mw_params.iloc[:,3:].apply(lambda x: x*1e-3)
heog_ica_mw_params.Threshold = heog_ica_mw_params.Threshold*1e-4

#put moving windows dfs in a list
mw_params = [veog_mw_params, heog_mw_params, heog_ica_mw_params]

# make dict with correspondences between codes and hierarchical ids
description_dict = {'111' : 'Prime/Related/L1',
                    '112' : 'Prime/Related/L2',
                    '121' : 'Prime/Unrelated/L1',
                    '122' : 'Prime/Unrelated/L2',
                    '211' : 'Target/Related/L1',
                    '212' : 'Target/Related/L2',
                    '221' : 'Target/Unrelated/L1',
                    '222' : 'Target/Unrelated/L2',
                    '201' : 'Hit',
                    '202' : 'Miss',
                    'BAD_seg': 'BAD_seg'
               }

 #create correspondence between description and trigger number
event_ids = {'Prime/Related/L1': 111,
             'Prime/Related/L2': 112,
             'Prime/Unrelated/L1' : 121,
             'Prime/Unrelated/L2' : 122,
             'Target/Related/L1' : 211,
             'Target/Related/L2' : 212,
             'Target/Unrelated/L1' : 221,
             'Target/Unrelated/L2' : 222,
             'Hit' : 201,
             'Miss' : 202}

# loop
for subj in range(1,41):
    interpol_path = "D:\\EEGdata\\Erp CORE\\N400\\mne\\raw_interpol\\N400_ERP_CORE_{}_interpol-raw.fif".format(subj)
    raw_interpol = read_raw(interpol_path)

    #add event descriptions to annotations
    raw_interpol.annotations.description = pd.Series(raw_interpol.annotations.description).map(description_dict).to_numpy()
    set(raw_interpol.annotations.description)

    #create events and event ids
    events, event_ids = mne.events_from_annotations(raw_interpol, event_ids)

    #change heog channel types
    raw_interpol.set_channel_types({'HEOG_ICA':'emg', 'HEOG':'misc'})
    epochs = mne.Epochs(raw_interpol,
                        events,
                        event_ids,
                        baseline = (-.2,0),
                        tmax = .8,
                        reject = dict(eeg  = 200e-6)
                       )

    epos_to_drop = []
    # run through every epoch
    for i in range(len(epochs.events)):
        # for each channel of interest
        for eog_chan_num, mw_df in zip(eog_nums, mw_params): #
            # get moving windows parameters
                w_size = mw_df['Window Size'].iloc[subj-1]
                w_start = mw_df['Time Window Minimum'].iloc[subj-1].round(2)
                w_start_last = np.round(mw_df['Time Window Maximum'].iloc[subj-1] - mw_df['Window Size'].iloc[subj-1],2)
                w_stop = mw_df['Time Window Maximum'].iloc[subj-1].round(2)
                w_stop_first = np.round(w_start + w_size,2)
               # create an array with starting and stopping times of every window
                mw_starts = np.arange(w_start,
                                      w_start_last,
                                      w_size)
                mw_stops = np.arange(w_stop_first,
                                     w_stop,
                                     w_size)
        # only run if channel is not empty, e.g. already marked as bad
            if epochs[i].get_data()[:,eog_chan_num,:].shape[0] != 0:
                #get channel data
                chan_data = epochs[i].get_data()[:,eog_chan_num,:].flatten()
                #make list with data for each window
                mws = [ chan_data[epochs.time_as_index(start).item(0) : epochs.time_as_index(stop).item(0)] for start, stop in zip(mw_starts, mw_stops)]

               # if channel is veog or uncorrected heog do peak-to-peak artfact detection
                if eog_chan_num < 32:
                    mws = pd.DataFrame(mws).T
                    peak2peak = mws.apply(lambda x: np.abs(x.max() - x.min()))
                    if any(peak2peak > mw_df['Threshold'].iloc[subj-1]):
                        epos_to_drop.append(i)
                # if channel is heog_ica do step artfact detection
                else:
                    steps = [True for i in range(len(mws)-1) if np.abs(mws[i].mean()-mws[i+1].mean()) > mw_df['Threshold'].iloc[subj-1]]
                    if steps:
                        epos_to_drop.append(i)
    # if any epoch was set as bad add it to drop list with label MW Reject
    if epos_to_drop:
        epochs.drop(epos_to_drop,'MW_Reject')
    #Save epochs
    epochs_path = "D:\\EEGdata\\Erp CORE\\N400\\mne\\epochs\\N400_ERP_CORE_{}-epo.fif".format(subj)
    print('Subject {}'.format(subj)) # line to keep up with analysis as it runs
    epochs.save(epochs_path, overwrite=True)

# Lastly, let's check the percentage of dropped epochs for each participant.

# In[ ]:

percent_dropped = pd.DataFrame(data = {'Subj': np.repeat(np.nan, 40),
                                       'Dropped': np.repeat(np.nan, 40)})
for subj in range(1,41):
    epochs_path = "D:\\EEGdata\\Erp CORE\\N400\\mne\\epochs\\N400_ERP_CORE_{}-epo.fif".format(subj)
    epochs = read_epochs(epochs_path)

    percent_dropped['Subj'].iloc[subj-1] = subj
    percent_dropped['Dropped'].iloc[subj-1] = np.round(epochs.drop_log_stats(),2)

# In[6]:

percent_dropped['Dropped'].describe().round(2)

# Dropped epochs per participant range from roughly 4% to 85%. Here, we will set consider participants with more than 30% of epochs dropped as participants with too many artfacts to be used. The ones that do not pass this filter are below:

# In[7]:

bad_subjs = percent_dropped.loc[percent_dropped.Dropped >30].Subj
bad_subjs.to_csv('bad_subjs.csv')
bad_subjs.count()

# We have 5 unsuable participants. This is different from ERP CORE's original preprocessing results, where they lose a single participant. However, I wasn't able to exactly reproduce the ICA step in the pipeline, so that is probably where we are falling a little behind. Anyway, 35 participants are more than enough for us to grasp the N400 effects in the following analyses. So, enough preprocesing, let's get to those ERPs in the next post!
#
# Let's finish contemplating some hard earned epochs:

# In[12]:

epochs.plot(n_epochs = 4)


# To add a new cell, type '#%%'
# To add a new markdown cell, type '#%% [markdown]'
#%% Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataScience.changeDirOnImportExport setting
# ms-python.python added
import os
try:
	os.chdir(os.path.join(os.getcwd(), 'notebooks'))
	print(os.getcwd())
except:
	pass
#%%
from IPython import get_ipython

#%% [markdown]
#
#%% [markdown]
# Lambda School Data Science, Unit 2: Predictive Modeling
#
# # Applied Modeling, Module 2
#
# You will use your portfolio project dataset for all assignments this sprint.
#
# ## Assignment
#
# Complete these tasks for your project, and document your work.
#
# - [ ] Plot the distribution of your target.
#     - Classification problem: Are your classes imbalanced? Then, don't use just accuracy.
#     - Regression problem: Is your target skewed? If so, let's discuss in Slack.
# - [ ] Continue to clean and explore your data. Make exploratory visualizations.
# - [ ] Fit a model. Does it beat your baseline?
# - [ ] Try xgboost.
# - [ ] Get your model's permutation importances.
#
# You should try to complete an initial model today, because the rest of the week, we're making model interpretation visualizations.
#
#
# ## Reading
#
# Top recommendations in _**bold italic:**_
#
# #### Permutation Importances
# - _**[Kaggle / Dan Becker: Machine Learning Explainability](https://www.kaggle.com/dansbecker/permutation-importance)**_
# - [Christoph Molnar: Interpretable Machine Learning](https://christophm.github.io/interpretable-ml-book/feature-importance.html)
#
# #### (Default) Feature Importances
#   - [Ando Saabas: Selecting good features, Part 3, Random Forests](https://blog.datadive.net/selecting-good-features-part-iii-random-forests/)
#   - [Terence Parr, et al: Beware Default Random Forest Importances](https://explained.ai/rf-importance/index.html)
#
# #### Gradient Boosting
#   - [A Gentle Introduction to the Gradient Boosting Algorithm for Machine Learning](https://machinelearningmastery.com/gentle-introduction-gradient-boosting-algorithm-machine-learning/)
#   - _**[A Kaggle Master Explains Gradient Boosting](http://blog.kaggle.com/2017/01/23/a-kaggle-master-explains-gradient-boosting/)**_
#   - [_An Introduction to Statistical Learning_](http://www-bcf.usc.edu/~gareth/ISL/ISLR%20Seventh%20Printing.pdf) Chapter 8
#   - [Gradient Boosting Explained](http://arogozhnikov.github.io/2016/06/24/gradient_boosting_explained.html)
#   - _**[Boosting](https://www.youtube.com/watch?v=GM3CDQfQ4sw) (2.5 minute video)**_

#%%
import os, sys
in_colab = 'google.colab' in sys.modules

# If you're in Colab...
if in_colab:
    # Pull files from Github repo
    os.chdir('/content')
    get_ipython().system('git init .')
    get_ipython().system('git remote add origin https://github.com/LambdaSchool/DS-Unit-2-Applied-Modeling.git')
    get_ipython().system('git pull origin master')
    # Install packages in Colab
    get_ipython().system('pip install category_encoders==2.0.0')
    get_ipython().system('pip install eli5==0.10.1')
    get_ipython().system('pip install pandas-profiling==2.3.0')
    get_ipython().system('pip install pdpbox==0.2.0')
    get_ipython().system('pip install plotly==4.1.1')
    get_ipython().system('pip install shap==0.30.0')

    # Install required python packages
    get_ipython().system('pip install -r requirements.txt')

    # Change into directory for module
    os.chdir('module2')

#%%
# import important libraries
import numpy as np
import pandas as pd
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import category_encoders as ce
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import confusion_matrix
from xgboost import XGBClassifier
from sklearn.metrics import roc_auc_score
import eli5
from eli5.sklearn import PermutationImportance

#%%
# import the dataset
df = pd.read_csv('https://raw.githubusercontent.com/oganm/dndstats/master/docs/uniqueTable.tsv',
                 sep='\t')

#%%
# double check the import worked correctly
df.head()

#%%
# create a has spells feature

# create a function that returns true if there is a spell or false if there is not
def check_spells(item):
  # check if the value is a float. This works because np.nan is a float value and the value will be a string if there are spells
  if isinstance(item, float):
    # if nan is found return false (no spells)
    return False
  else:
    # else return true (spell found)
    return True

# apply the function to the dataframe
df['has_spells'] = df['processedSpells'].apply(check_spells)

#%%
df.head()

#%%
# create a has feat feature

# create a function that returns true if it has a feat and false if there is not
def check_feats(item):
  if isinstance(item, float):
    return False
  else:
    return True

# apply the function to the dataframe
df['has_feats'] = df['feats'].apply(check_feats)

#%%
# create a HP per level feature
df['HP_per_level'] = df['HP'] / df['level']

#%%
df.head()

#%%
# drop uneeded columns due to high randomness or high variance
drop_columns_variance = ['name', 'date', 'day']
df = df.drop(columns = drop_columns_variance)

# drop columns due to leakage
drop_columns_leak = ['subclass', 'class']
df = df.drop(columns = drop_columns_leak)

# drop columns due to duplication
drop_columns_dup = ['race']
df = df.drop(columns = drop_columns_dup)

# check the head
df.head()

#%%
df['justClass'].value_counts()

#%%
# get our target
target = 'justClass'
targetAllowed = ['Fighter', 'Rogue', 'Cleric', 'Barbarian', 'Paladin', 'Ranger', 'Sorcerer', 'Wizard', 'Monk', 'Druid', 'Bard', 'Warlock']

#%%
# df = df[df[target] targetAllowed]
df = df.loc[df[target].isin(targetAllowed)]

#%%
df['justClass'].value_counts()

#%%
# get our features
train_features = df.drop(columns=target)
numeric_features = train_features.select_dtypes(include='number').columns.tolist()
cardinality = train_features.select_dtypes(exclude='number').nunique()
categorical_features = cardinality[cardinality <= 75].index.tolist()
features = numeric_features + categorical_features

#%%
features

#%%
# majority class check
majority_class = df[target].mode()
print('Majority class is', majority_class)
y_pred = [majority_class] * len(df)
accuracy_score(df[target], y_pred)

#%%
# train test split the data (80/20)
train, val = train_test_split(df, train_size=0.80, test_size=.20,
                               stratify=df[target], random_state=42)

#%%
X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]

#%%
# fit a pipeline (Decision Tree)
pipelineTree = make_pipeline(
    ce.OneHotEncoder(use_cat_names=True),
    SimpleImputer(strategy='mean'),
    StandardScaler(),
    DecisionTreeClassifier(max_depth=3)
)

pipelineTree.fit(X_train, y_train)

#%%
# validation accuracy (Decision Tree)
y_pred_tree = pipelineTree.predict(X_val)
print('Validation Accuracy', accuracy_score(y_val, y_pred_tree))

#%%
y_pred_tree

#%%
# fit a pipeline (Random Forest)
pipelineForest = make_pipeline(
    ce.OneHotEncoder(use_cat_names=True),
    SimpleImputer(strategy='mean'),
    StandardScaler(),
    RandomForestClassifier(n_estimators=50, random_state=42, n_jobs=-1)
)

pipelineForest.fit(X_train, y_train)

#%%
y_pred_forest = pipelineForest.predict(X_val)
print('Validation Accuracy', accuracy_score(y_val, y_pred_forest))

#%%
y_pred_forest

#%%
transformers = make_pipeline(
    ce.OneHotEncoder(use_cat_names=True),
    SimpleImputer(strategy='mean'),
    StandardScaler()
)

# tranform the data
X_train_transformed = transformers.fit_transform(X_train)
X_val_transformed = transformers.transform(X_val)

eval_set = [(X_train_transformed, y_train),
            (X_val_transformed, y_val)]

#%%
# model = RandomForestClassifier(n_estimators=50, random_state=42, n_jobs=-1)
# model.fit(X_train_transformed, y_train)

#%%

#%%
model = XGBClassifier(n_estimators=1000, n_jobs=-1)
model.fit(X_train_transformed, y_train, eval_set=eval_set, early_stopping_rounds=10)

#%%
# Validation accuracy
y_pred = model.predict(X_val_transformed)
print('Validation Accuracy', accuracy_score(y_val, y_pred))

#%%
y_pred

#%%
# permuter = PermutationImportance(
#     model,
#     scoring='accuracy',
#     n_iter=3,
#     random_state=42
# )

# permuter.fit(X_val_transformed, y_val)
# feature_names = X_val.columns.tolist()

# eli5.show_weights(
#     permuter,
#     top=None, # show importance of all features
#     feature_names=feature_names
# )

#%%
# the row to anaylze in the shaply plot
# change the number in row to find the shaply value
row = 25
row = X_train.iloc[[row]]

#%%
# import shap

# explainer = shap.TreeExplainer(model)
# row_processed = transformers.transform(row)
# shap_values = explainer.shap_values(row_processed)

# shap.initjs()
# shap.force_plot(
#     base_value=explainer.expected_value,
#     shap_values=shap_values,
#     features=row
# )

#%%

#%%


# -*- coding: utf-8 -*-
"""
Various small and named graphs, together with some compact generators.

"""
__author__ ="""Aric Hagberg (hagberg@lanl.gov)\nPieter Swart (swart@lanl.gov)"""
#    Copyright (C) 2004-2008 by
#    Aric Hagberg <hagberg@lanl.gov>
#    Dan Schult <dschult@colgate.edu>
#    Pieter Swart <swart@lanl.gov>
#    All rights reserved.
#    BSD license.

__all__ = ['make_small_graph',
           'LCF_graph',
           'bull_graph',
           'chvatal_graph',
           'cubical_graph',
           'desargues_graph',
           'diamond_graph',
           'dodecahedral_graph',
           'frucht_graph',
           'heawood_graph',
           'house_graph',
           'house_x_graph',
           'icosahedral_graph',
           'krackhardt_kite_graph',
           'moebius_kantor_graph',
           'octahedral_graph',
           'pappus_graph',
           'petersen_graph',
           'sedgewick_maze_graph',
           'tetrahedral_graph',
           'truncated_cube_graph',
           'truncated_tetrahedron_graph',
           'tutte_graph']

import networkx as nx
from networkx.generators.classic import empty_graph, cycle_graph, path_graph, complete_graph
from networkx.exception import NetworkXError

#------------------------------------------------------------------------------
#   Tools for creating small graphs
#------------------------------------------------------------------------------
def make_small_undirected_graph(graph_description, create_using=None):
    """
    Return a small undirected graph described by graph_description.

    See make_small_graph.
    """
    if create_using is not None and create_using.is_directed():
        raise NetworkXError("Directed Graph not supported")
    return make_small_graph(graph_description, create_using)

def make_small_graph(graph_description, create_using=None):
    """
    Return the small graph described by graph_description.

    graph_description is a list of the form [ltype,name,n,xlist]

    Here ltype is one of "adjacencylist" or "edgelist",
    name is the name of the graph and n the number of nodes.
    This constructs a graph of n nodes with integer labels 0,..,n-1.

    If ltype="adjacencylist"  then xlist is an adjacency list
    with exactly n entries, in with the j'th entry (which can be empty)
    specifies the nodes connected to vertex j.
    e.g. the "square" graph C_4 can be obtained by

    >>> G=nx.make_small_graph(["adjacencylist","C_4",4,[[2,4],[1,3],[2,4],[1,3]]])

    or, since we do not need to add edges twice,

    >>> G=nx.make_small_graph(["adjacencylist","C_4",4,[[2,4],[3],[4],[]]])

    If ltype="edgelist" then xlist is an edge list
    written as [[v1,w2],[v2,w2],...,[vk,wk]],
    where vj and wj integers in the range 1,..,n
    e.g. the "square" graph C_4 can be obtained by

    >>> G=nx.make_small_graph(["edgelist","C_4",4,[[1,2],[3,4],[2,3],[4,1]]])

    Use the create_using argument to choose the graph class/type.
    """
    ltype=graph_description[0]
    name=graph_description[1]
    n=graph_description[2]

    G=empty_graph(n, create_using)
    nodes=G.nodes()

    if ltype=="adjacencylist":
        adjlist=graph_description[3]
        if len(adjlist) != n:
            raise NetworkXError("invalid graph_description")
        G.add_edges_from([(u-1,v) for v in nodes for u in adjlist[v]])
    elif ltype=="edgelist":
        edgelist=graph_description[3]
        for e in edgelist:
            v1=e[0]-1
            v2=e[1]-1
            if v1<0 or v1>n-1 or v2<0 or v2>n-1:
                raise NetworkXError("invalid graph_description")
            else:
                G.add_edge(v1,v2)
    G.name=name
    return G

def LCF_graph(n,shift_list,repeats,create_using=None):
    """
    Return the cubic graph specified in LCF notation.

    LCF notation (LCF=Lederberg-Coxeter-Fruchte) is a compressed
    notation used in the generation of various cubic Hamiltonian
    graphs of high symmetry. See, for example, dodecahedral_graph,
    desargues_graph, heawood_graph and pappus_graph below.

    n (number of nodes)
      The starting graph is the n-cycle with nodes 0,...,n-1.
      (The null graph is returned if n < 0.)

    shift_list = [s1,s2,..,sk], a list of integer shifts mod n,

    repeats
      integer specifying the number of times that shifts in shift_list
      are successively applied to each v_current in the n-cycle
      to generate an edge between v_current and v_current+shift mod n.

    For v1 cycling through the n-cycle a total of k*repeats
    with shift cycling through shiftlist repeats times connect
    v1 with v1+shift mod n

    The utility graph K_{3,3}

    >>> G=nx.LCF_graph(6,[3,-3],3)

    The Heawood graph

    >>> G=nx.LCF_graph(14,[5,-5],7)

    See http://mathworld.wolfram.com/LCFNotation.html for a description
    and references.

    """
    if create_using is not None and create_using.is_directed():
        raise NetworkXError("Directed Graph not supported")

    if n <= 0:
        return empty_graph(0, create_using)

    # start with the n-cycle
    G=cycle_graph(n, create_using)
    G.name="LCF_graph"
    nodes=G.nodes()

    n_extra_edges=repeats*len(shift_list)
    # edges are added n_extra_edges times
    # (not all of these need be new)
    if n_extra_edges < 1:
        return G

    for i in range(n_extra_edges):
        shift=shift_list[i%len(shift_list)] #cycle through shift_list
        v1=nodes[i%n]                    # cycle repeatedly through nodes
        v2=nodes[(i + shift)%n]
        G.add_edge(v1, v2)
    return G

#-------------------------------------------------------------------------------
#   Various small and named graphs
#-------------------------------------------------------------------------------

def bull_graph(create_using=None):
    """Return the Bull graph. """
    description=[
        "adjacencylist",
        "Bull Graph",
        5,
        [[2,3],[1,3,4],[1,2,5],[2],[3]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def chvatal_graph(create_using=None):
    """Return the Chvátal graph."""
    description=[
        "adjacencylist",
        "Chvatal Graph",
        12,
        [[2,5,7,10],[3,6,8],[4,7,9],[5,8,10],
         [6,9],[11,12],[11,12],[9,12],
         [11],[11,12],[],[]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def cubical_graph(create_using=None):
    """Return the 3-regular Platonic Cubical graph."""
    description=[
        "adjacencylist",
        "Platonic Cubical Graph",
        8,
        [[2,4,5],[1,3,8],[2,4,7],[1,3,6],
         [1,6,8],[4,5,7],[3,6,8],[2,5,7]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def desargues_graph(create_using=None):
    """ Return the Desargues graph."""
    G=LCF_graph(20, [5,-5,9,-9], 5, create_using)
    G.name="Desargues Graph"
    return G

def diamond_graph(create_using=None):
    """Return the Diamond graph. """
    description=[
        "adjacencylist",
        "Diamond Graph",
        4,
        [[2,3],[1,3,4],[1,2,4],[2,3]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def dodecahedral_graph(create_using=None):
    """ Return the Platonic Dodecahedral graph. """
    G=LCF_graph(20, [10,7,4,-4,-7,10,-4,7,-7,4], 2, create_using)
    G.name="Dodecahedral Graph"
    return G

def frucht_graph(create_using=None):
    """Return the Frucht Graph.

    The Frucht Graph is the smallest cubical graph whose
    automorphism group consists only of the identity element.

    """
    G=cycle_graph(7, create_using)
    G.add_edges_from([[0,7],[1,7],[2,8],[3,9],[4,9],[5,10],[6,10],
                [7,11],[8,11],[8,9],[10,11]])

    G.name="Frucht Graph"
    return G

def heawood_graph(create_using=None):
    """ Return the Heawood graph, a (3,6) cage. """
    G=LCF_graph(14, [5,-5], 7, create_using)
    G.name="Heawood Graph"
    return G

def house_graph(create_using=None):
    """Return the House graph (square with triangle on top)."""
    description=[
        "adjacencylist",
        "House Graph",
        5,
        [[2,3],[1,4],[1,4,5],[2,3,5],[3,4]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def house_x_graph(create_using=None):
    """Return the House graph with a cross inside the house square."""
    description=[
        "adjacencylist",
        "House-with-X-inside Graph",
        5,
        [[2,3,4],[1,3,4],[1,2,4,5],[1,2,3,5],[3,4]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def icosahedral_graph(create_using=None):
    """Return the Platonic Icosahedral graph."""
    description=[
        "adjacencylist",
        "Platonic Icosahedral Graph",
        12,
        [[2,6,8,9,12],[3,6,7,9],[4,7,9,10],[5,7,10,11],
         [6,7,11,12],[7,12],[],[9,10,11,12],
         [10],[11],[12],[]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def krackhardt_kite_graph(create_using=None):
    """
    Return the Krackhardt Kite Social Network.

    A 10 actor social network introduced by David Krackhardt
    to illustrate: degree, betweenness, centrality, closeness, etc.
    The traditional labeling is:
    Andre=1, Beverley=2, Carol=3, Diane=4,
    Ed=5, Fernando=6, Garth=7, Heather=8, Ike=9, Jane=10.

    """
    description=[
        "adjacencylist",
        "Krackhardt Kite Social Network",
        10,
        [[2,3,4,6],[1,4,5,7],[1,4,6],[1,2,3,5,6,7],[2,4,7],
         [1,3,4,7,8],[2,4,5,6,8],[6,7,9],[8,10],[9]]
         ]
    G=make_small_undirected_graph(description, create_using)
    return G

def moebius_kantor_graph(create_using=None):
    """Return the Moebius-Kantor graph."""
    G=LCF_graph(16, [5,-5], 8, create_using)
    G.name="Moebius-Kantor Graph"
    return G

def octahedral_graph(create_using=None):
    """Return the Platonic Octahedral graph."""
    description=[
        "adjacencylist",
        "Platonic Octahedral Graph",
        6,
        [[2,3,4,5],[3,4,6],[5,6],[5,6],[6],[]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def pappus_graph():
    """ Return the Pappus graph."""
    G=LCF_graph(18,[5,7,-7,7,-7,-5],3)
    G.name="Pappus Graph"
    return G

def petersen_graph(create_using=None):
    """Return the Petersen graph."""
    description=[
        "adjacencylist",
        "Petersen Graph",
        10,
        [[2,5,6],[1,3,7],[2,4,8],[3,5,9],[4,1,10],[1,8,9],[2,9,10],
         [3,6,10],[4,6,7],[5,7,8]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def sedgewick_maze_graph(create_using=None):
    """
    Return a small maze with a cycle.

    This is the maze used in Sedgewick,3rd Edition, Part 5, Graph
    Algorithms, Chapter 18, e.g. Figure 18.2 and following.
    Nodes are numbered 0,..,7
    """
    G=empty_graph(0, create_using)
    G.add_nodes_from(range(8))
    G.add_edges_from([[0,2],[0,7],[0,5]])
    G.add_edges_from([[1,7],[2,6]])
    G.add_edges_from([[3,4],[3,5]])
    G.add_edges_from([[4,5],[4,7],[4,6]])
    G.name="Sedgewick Maze"
    return G

def tetrahedral_graph(create_using=None):
    """ Return the 3-regular Platonic Tetrahedral graph."""
    G=complete_graph(4, create_using)
    G.name="Platonic Tetrahedral graph"
    return G

def truncated_cube_graph(create_using=None):
    """Return the skeleton of the truncated cube."""
    description=[
        "adjacencylist",
        "Truncated Cube Graph",
        24,
        [[2,3,5],[12,15],[4,5],[7,9],
         [6],[17,19],[8,9],[11,13],
         [10],[18,21],[12,13],[15],
         [14],[22,23],[16],[20,24],
         [18,19],[21],[20],[24],
         [22],[23],[24],[]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G

def truncated_tetrahedron_graph(create_using=None):
    """Return the skeleton of the truncated Platonic tetrahedron."""
    G=path_graph(12, create_using)
#    G.add_edges_from([(1,3),(1,10),(2,7),(4,12),(5,12),(6,8),(9,11)])
    G.add_edges_from([(0,2),(0,9),(1,6),(3,11),(4,11),(5,7),(8,10)])
    G.name="Truncated Tetrahedron Graph"
    return G

def tutte_graph(create_using=None):
    """Return the Tutte graph."""
    description=[
        "adjacencylist",
        "Tutte's Graph",
        46,
        [[2,3,4],[5,27],[11,12],[19,20],[6,34],
         [7,30],[8,28],[9,15],[10,39],[11,38],
         [40],[13,40],[14,36],[15,16],[35],
         [17,23],[18,45],[19,44],[46],[21,46],
         [22,42],[23,24],[41],[25,28],[26,33],
         [27,32],[34],[29],[30,33],[31],
         [32,34],[33],[],[],[36,39],
         [37],[38,40],[39],[],[],
         [42,45],[43],[44,46],[45],[],[]]
        ]
    G=make_small_undirected_graph(description, create_using)
    return G