# You must make sure to run all cells in sequence using shift + enter or you might encounter errors
from pykubegrader.initialize import initialize_assignment

responses = initialize_assignment("1_homework_q", "week_8", "homework", assignment_points = 75.0, assignment_tag = 'week8-homework')

# Initialize Otter
import otter
grader = otter.Notebook("1_homework_q.ipynb")

๐Ÿ  Homework Week 8: Combining Concepts to Build Interoperable Code for Plotting ๐Ÿ“Š๐Ÿ”ง#

This lab uses all of the concepts we have learned in python programming to make reusable building blocks for generating, plotting, and fitting data.

As an engineer you will regularly be presented with noisy data and want to fit that data to a function. In this assignment, we will build some machinery and a coding schema to generate noisy data (since we do not have real data), plot data, and fit the results. We will build tools to simplify the visualization of the results. The code will be designed to be interoperable. If we were to have a new type of data or mathematical expression, we could reuse all the code we have written.

Question 1 (Points: 3.0): Importing Functions#

Here we will import some packages that we need for this assignment

  1. import numpy as np

  2. import the submodule matplotlib.pyplot and assign it to plt

  3. import from scipy.optimize the curve_fit function

# 1. you need to import numpy as np
...
# 2. You need to import the submodule matplotlib.pyplot and assign it to plt
...
# 3. You need to import from scipy.optimize the curve_fit function
...
grader.check("q1-Importing Functions")

Question 2 (Points: 13.0): 2. Implementing a Class for MathExpressions#

In Python and programming in general, it is common to have a base class that adds functionality to an object. For example, if you have a car that you are building, you might want to add blind spot detection. You can create the blind spot detection software and hardware and install it on multiple cars types. We will do the same with a math function. We will build a class that adds a methods to fit and evaluate a math expression.

When we build this class we need it to be flexible to accept math functions with a flexible number of input parameters. We implement a fit function where we can input any parameters into the function (this is important for optimization required to fit data), and an evaluate method which uses the parameters set during initialization.

Follow these steps:

  1. Define a class MathExpression.

  2. Build an initialization function.

  • The function should take a variable func which is a mathematical function

  • The function should accept **kwargs

    • The kwargs will be the parameters of the fitting function

    • We want to save the kwargs, each key-value pair of kwargs where the key is the variable name, and func as an attribute of the object

  1. Define a method fit.

  • That accepts a required input x, and **kwargs

  • Return the result when you call the objects method func that accepts a required input x, and **kwargs

  1. Define an method for the class evaluate.

  • Takes a required input x

  • Returns the result when you call the objects method func that accepts a required input x, and the **kwargs values set at initialization.

# 1. define a class MathExpression
...
    # 2. Use the built in initialization function
    # The function should take a variable func which is a mathematical function for generation and fitting (we will define this later)
    # The function should accept **kwargs
    # The kwargs will be the parameters of the fitting function 
    ...
        
        # use the .items() method to extract from kwargs a list of tuples with the key value pairs
        # Create a for loop that extracts the key value pairs from the kwargs
        #   make sure to unpack the tuple into the variables key and value
        # Inside the for loop, use the built in python method setattr to add an attribute to the MathExpression Object 
        #   for the key, use the variable key, 
        #   for the value, use the variable value
        ...
        
        # save the kwargs as an attribute of the object named kwargs
        ...
        
        # save the function as an attribute of the object named func
        ...
        
    # 3. for the class, define a method fit that accepts a required input x, and **kwargs
    #   x is the points to sample along the x axis
    #   **kwargs are new parameters to test
    #   Note: this is a requirement of the fitting function
    ...
        
        # return the result when you call the object's method func that accepts a required input x and **kwargs
        ...
        
    # 4. for the class, define a method evaluate that takes a required input x
    ...
       
        # return the result when you call the object's method func that accepts a required input x and the kwargs values set at initialization.
        ...
grader.check("q2-Implement MathExpression")

Question 3 (Points: 3.0): 3. Define a Linear Function#

\[ Y = slope * x + intercept \]

We want to define a simple linear function. A linear model is one of the simplest fitting functions used in science and engineering. It says that an independent variable directly influences a dependent variable.

  1. Build a function LinearFunction

  • LinearFunction should take three inputs: x for the independent variable, m for the slope, b for the intercept.

  • Return the calculation of y, where y = slope * x + intercept.

  • Make sure to replace slope and intercept with the correct variables.

# 1. build a function LinearFunction
# linear function should take two inputs m - for the slope, and b for the intercept
# return the calculation of y, where y = slope * x + intercept. 
# Make sure to replace slope and intercept with the correct variables.
...
grader.check("q3 - Linear Function")

Question 4 (Points: 6.0): 4. Plotting#

One of the best ways to understand your code is doing what you expect is to visualize the results. We can do this by plotting the data.

  1. Instantiate an object Linear_ using the MathExpression Class.

  • The input func should be the LinearFunction you wrote

  • The variables needed for kwargs must be provided as key value pairs.

  • In this case, those key value pairs are m = 1, and b = -3.

  1. Derive a linearly-spaced vector for the independent variable.

  • Use the np.linspace function to make a linearly-spaced array from 0 to 10 with 100 steps

  1. call the built-in evaluate method of the LinearFunction object over the range defined by x.

  • assign the output to a variable y

  1. Use the function plt.plot to plot x vs y.

  • assign the plot to a variable called linear_plot

# 1. Instantiate an object Linear_ using the MathExpression Class
# The variables needed for the Kwargs m = 1, and b = -3 need to be provided as key value pairs. 
# example my_function(x=1), this would provide a **kwargs key (x) value (1)

...

# 2. use the np.linspace function to make a linearly-spaced array from 0 to 10, with 100 steps
# save this to the variable x

...

# 3. call the built-in `evaluate` method of the LinearFunction object over the range defined by x
# assign the output to a variable y

...

# 4. use the function plt.plot to plot x vs y

...

# we do not need to add more labels to this plot. It was just for testing.
grader.check("q4 - Plotting")

5. A NoisyFunction#

You are responsible for making a 3-4 minute VoiceThread submission that briefly explains each function of this class. You are not responsible for writing any code or comments in this Task.

We have written a function with several useful methods that you might want to use as an engineer.

1 It inherits information from an object of type MathExpression

2 It can generate noisy data, which is to simulate real experimental data.

3 It can fit the data to an known function.

4 It can fit, plot and format the results for quick visualization.

# build a class Noisy Function that Inherits MathExpression
# This is simply done writing class NoisyFunction(MathExpression)
...
    # This class does not require any specific initialization variables, but it will require that we pass the inherited variable through.
    # we have done this for you 
    # you should not touch this code. If you do, here it is
    # def __init__(self, *args, **kwargs):
    #    super().__init__(*args, **kwargs)
    ...
    # build a function generate noisy data with two required inputs
    #   x_range - a list of the minimum and max ranges for to generate the data
    #   noise_amplitude - a float from which sets the magnitude of the noise
    ...
        # use np.linspace to generate x data along the range
        # generate 100 points
        ...
        # call the evaluate method of the base function object to return the true values
        ...
        # generate the noise vector by multiplying the noise amplitude by an random noise vector of length x  
        # the random noise vector should be generated with np.random.randn
        ...
        # add the noise to the y_true value
        # save it in a new variable y
        ...
        # return x, y, and y_true
        ...
    # build a function fit_data that takes 2 inputs x and y
    ...
        # call the curvefit function
        # provide the inherited pointed to the function
        # For the initial guess provide the true values they are contained within the saved variable kwargs
        # You can get the values of a dictionary using the built-in method .values()
        # P0 takes a list. You need to convert the values to a list.
        # Curvefit should return two values assign them to popt and pcov. 
        # 
        # is the fit results
        ...
        # Return popt and pcov
        ...
    # build a function plot_fit_results that plots the raw and fit results
    # This function should take two input parameters:
    #   x_range - a tuple defining the x range to plot
    #   noise_amplitude - a float used to control the magnitude of the noise
    ...

            # use the subplots function in pyplot to create a multiplot figure with 2 columns and 1 row.
            # set the figure size = 10, 5 - figsize takes a tuple
            # assign the output to two variables fig and axs
            ...

            # make a list of strings called labels 
            # The 0th index should be "True Fit"
            # The 1st index should be "Noisy Fit"
            ...

            # call the generate_noisy_data function from this class
            # save the returned variables to x, y, and y_true
            ...

            # make a look that loops over the axs with enumerate.
            # set the iterator equal to the variable i
            # set the value equal to the variable ax
            ...
    
                # create a scatterplot on the axis object 
                # plot x vs y
                # make the marker size = 5 (with the `s` tag)
                # add a label 'Raw Data' (with the `label` tag)
                ...
    
                # write an if statement that is true if the label of the graph is "True Fit". This is ordered by the list labels you created.
                ...
        
                    # if the statement was true on the axis object plot
                    # x vs y true
                    # make the line red using the 'r' flag
                    # add a label as indexed from the list
                    ...
    
                # add an elif statement that is true if the label of the graph is "Noisy Fit" this is ordered by the list labels you created.
                ...
        
                    # call the method in the class fit_data
                    # This should fit the x and y data
                    # have it return the variables popt and pcov
                    ...
        
                    # use the predicted popt and the inherited function to generate the y values for the fit results
                    # make sure to unpack the popt list with the * operator
                    ...
        
                    # on the axis object plot
                    # x vs y_noisy_fit
                    # make the line blue using the 'b' flag
                    # add a label as indexed from the list
                    ...
    
                # else passes to the next loop
                ...
    
                # use the set_xlabel method to set the x-label of the graph to 'x'
                # use the set_ylabel method to set the y-label of the graph to 'y'
                ...
    
                # call the legend method of the axis object to show the legend
                ...
            # return the fig object
            ...
grader.check("q5 - Noisy Function")

Question 6 (Points: 11.0): 6. Testing our Class#

In coding it is always a good idea to run all parts of your code to check it is performing as expected.

  1. Create a NoisyFunction instance for a linear function

  • Assign this object to the variable linear_func

  • The input func should be the LinearFunction you wrote

  • The slope should be 3 and the intercept should be 4

  1. Call the proper method of linear_func to generate noisy data over the range from 0- \(2\pi\)

  • the noise amplitude is 1

  • save the results to x, y, and y_true

  1. Fit the x and y data using the proper method of linear_func

  • save the fit results to popt, pcov

  • (these names represent the optimal parameters (popt) and the parametersโ€™ covariance (pcov))

  1. Use the built-in method to plot the fit result

  • set the range equal to 0 to 16

  • set the noise amplitude equal to 0.3

  • Save this to the variable plot

seed = np.random.seed(42)
# 1. Create a NoisyFunction instance for a linear function
# Assign this object to the variable linear_func
# The slope should be 3 and the intercept should be 4
...
# 2. call the generate_noisy_data function of the object the linear_func to produce noisy data over the range from 0-2pi
#   use a tuple to indicate the range from 0-2pi
#   the noise amplitude is 1
#   save the results to x, y, and y_true
...
# 3. Fit the data
# save the fit results to popt, pcov
...

# 4. Use the built-in method to plot the fit result
# set the range equal to 0 to 16
# set the noise amplitude equal to 0.3
# Save this to the variable plot
...
grader.check("q6 - plotting and fitting")

Question 7 (Points: 6.0): 7. Defining new math functions#

Now we can view the flexibility (interoperability) of our code.

Define two new functions. Follow the logic of the way you created LinearFunction.

  1. SineFunction

\[g(x) = A (sin(2\pi f x+\phi))\]

where \(A\) represents amplitude, \(f\) represents the frequency, and \(\phi\) represents the phase.

  1. ExponentialFunction

\[h(x) = ae^{bx}\]
# 1. Write the SineFunction here
# the inputs should be x, amplitude, frequency, phase
...

# 2. Write the ExponentialFunction here
# the inputs should be x, a, b
...
grader.check("q7-new functions")

8 Using our class with the sine function#

Now we can use nearly the same code from Task 7 to test our SineFunction.

  1. Create a NoisyFunction instance for the sine function

  • Assign this object to the variable sine_func

  • The input func should be the SineFunction you wrote

  • The amplitude should be 2, the frequency should be .2, and the phase should be .5

  1. Apply the generate_noisy_data method of sine_func to produce noisy data over the range from 0-6 \(\pi\)

  • use a tuple to indicate the range from 0-6 \(\pi\)

  • the noise amplitude is 1.5

  • save the results to x, y, and y_true

  1. Fit the x and y data using the proper method of sine_func

  • save the fit results to popt, pcov

  1. Use the built-in method to plot the fit results

  • set the range equal to 0 to 6 \(\pi\)

  • set the noise amplitude equal to 1.5

  • Save this to the variable plot

seed = np.random.seed(42)
# 1. Create a NoisyFunction instance for a sine function
# Assign this object to the variable sine_func
# The amplitude should be 2, the frequency should be .2, and the phase should be .5
...
# 2. evaluate the sine_func to produce noisy data over the range from 0-6pi
# the noise amplitude is 1.5
# save the results to x, y, and y_true
...
# 3. Fit the data
# save the fit results to popt, pcov
...
# 4. Use the built-in method to plot the fit results
# set the range equal to 0 to 6 pi
# set the noise amplitude equal to 1.5
# Save this to the variable plot
...
grader.check("q8-using the class with new Sine functions")

9 Using our class with the Exponential function#

Now we can use nearly the exact same code to test our Exponential function. We wrote one code that we can use over and over again in different ways!

  1. Create a NoisyFunction instance for a exponential function

  • Assign this object to the variable exp_func

  • The input func should be the ExponentialFunction you wrote

  • a should be 2, and b should be .2

  1. Call the proper method of exp_func to generate noisy data over the range from 0-6 \(\pi\)

  • use a tuple to indicate the range from 0-6 \(\pi\)

  • the noise amplitude is 1.5

  • save the results to x, y, and y_true

  1. Fit the x and y data using the proper method of exp_func

  • save the fit results to popt, pcov

  1. Use the built-in method to plot the fit results

  • set the range equal to 0 to 6 \(\pi\)

  • set the noise amplitude equal to 1.5

  • save this to the variable plot

seed = np.random.seed(42)
# 1. Create a NoisyFunction instance for a exponential function
# Assign this object to the variable exp_func
# The a should be 2, and b should be .2
...
# 2. evaluate the exp_func to produce noisy data over the range from 0-6pi
# the noise amplitude is 1.5
# save the results to x, y, and y_true
...
# 3. Fit the data
# save the fit results to popt, pcov
...
# 4. Use the built-in method to plot the fit results
# set the range equal to 0 to 6 pi
# set the noise amplitude equal to 1.5
# Save this to the variable plot
...
grader.check("q9-using the class with new exponential functions")

Submitting Assignment#

Please run the following block of code using shift + enter to submit your assignment, you should see your score.

from pykubegrader.submit.submit_assignment import submit_assignment

submit_assignment("week8-homework", "1_homework_q")