# Initialize Otter
import otter
grader = otter.Notebook("lab10-fitting.ipynb")
π»π§ͺ Lab 10: 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.
Entering Your Information for Credit#
To receive credit for assignments it is important we can identify your work from others. To do this we will ask you to enter your information in the following code block.
Before you begin#
Run the block of code at the top of the notebook that imports and sets up the autograder. This will allow you to check your work.
# Please provide your first name, last name, Drexel ID, and Drexel email. Make sure these are provided as strings. "STRINGS ARE TEXT ENCLOSED IN QUOTATION MARKS."
# In the assignments you will see sections of code that you need to fill in that are marked with ... (three dots). Replace the ... with your code.
first_name = ...
last_name = ...
drexel_id = ...
drexel_email = ...
grader.check("q0-Checking-Your-Name")
2. Importing Functions#
Here we will import some packages that we need for this assignment
2.1 import numpy
as np
2.2 import the submodule matplotlib.pyplot
and assign it to plt
2.3 import from scipy.optimize
the curve_fit
function
# 2.1 you need to import numpy as np
...
# 2.2 You need to import the submodule matplotlib.pyplot and assign it to plt
...
# 2.3 You need to import from scipy.optimize the curve_fit function
...
grader.check("q2-Importing Functions")
3. 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:
3.1 Define a class
MathExpression
.
3.2 Build an initialization function.
The function should take a variable
func
which is a mathematical functionThe function should accept
**kwargs
The
kwargs
will be the parameters of the fitting functionWe want to save the
kwargs
, each key-value pair ofkwargs
where the key is the variable name, andfunc
as an attribute of the object
3.3 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 inputx
, and**kwargs
3.4 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 inputx
, and the**kwargs
values set at initialization.
# 3.1 define a class MathExpression
...
# 3.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.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
...
# 3.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("q3-Implement MathExpression")
4. Define a Linear Function#
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.
4.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.
# 4.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("q4 - Linear Function")
5. 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.
5.1 Instantiate an object Linear_
using the MathExpression
Class
.
The input
func
should be theLinearFunction
you wroteThe variables needed for kwargs must be provided as key value pairs.
In this case, those key value pairs are
m = 1
, andb = -3
.
5.2 Derive a linearly-spaced vector for the independent variable.
Use the
np.linspace
function to make a linearly-spaced array from0
to10
with100
stepsAssign the array to
x
5.3. call the built-in evaluate
method of the LinearFunction
object over the range defined by x
.
assign the output to a variable
y
5.4 Use the function plt.plot
to plot x
vs y
.
assign the plot to a variable called
linear_plot
# 5.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)
...
# 5.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
...
# 5.3 call the built-in `evaluate` method of the LinearFunction object over the range defined by x
# assign the output to a variable y
...
# 5.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("q5 - Plotting")
6. A NoisyFunction#
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.
6.1 It inherits information from an object of type MathExpression
6.2 It can generate noisy data, which is to simulate real experimental data.
6.3 It can fit the data to an known function.
6.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)
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)
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
def generate_noisy_data(self, x_range, noise_amplitude):
# use np.linspace to generate x data along the range
# generate 100 points
x = np.linspace(x_range[0], x_range[1], 100)
# call the evaluate method of the base function object to return the true values
y_true = self.evaluate(x)
# 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
noise = noise_amplitude * np.random.randn(len(x))
# add the noise to the y_true value
# save it in a new variable y
y = y_true + noise
# return x, y, and y_true
return x, y, y_true
# build a function fit_data that takes 2 inputs x and y
def fit_data(self, x, 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.
# popt is the fit results
popt, pcov = curve_fit(self.func, x, y, p0=list(self.kwargs.values()))
# Return popt and pcov
return popt, 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
def plot_fit_results(self, x_range, noise_amplitude):
# 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
fig, axs = plt.subplots(1, 2, figsize=(10, 5))
# make a list of strings called labels
# The 0th index should be "True Fit"
# The 1st index should be "Noisy Fit"
labels = ['True Fit', 'Noisy Fit']
# call the generate_noisy_data function from this class
# save the returned variables to x, y, and y_true
x, y, y_true = self.generate_noisy_data(x_range, noise_amplitude)
# 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
for i, ax in enumerate(axs):
# 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)
ax.scatter(x, y, s=5, label='Raw Data')
# 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 labels[i] == 'True Fit':
# 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
ax.plot(x, y_true, 'r', label=labels[i])
# 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.
elif labels[i] == 'Noisy Fit':
# call the method in the class fit_data
# This should fit the x and y data
# have it return the variables popt and pcov
popt, pcov = self.fit_data(x, y)
# 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
y_noisy_fit = self.func(x, *popt)
# 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
ax.plot(x, y_noisy_fit, 'b', label=labels[i])
# else passes to the next loop
else:
pass
# 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'
ax.set_xlabel('x')
ax.set_ylabel('y')
# call the legend method of the axis object to show the legend
ax.legend()
return fig
7. 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.
7.1 Create a NoisyFunction
instance for a linear function
Assign this object to the variable
linear_func
The input
func
should be theLinearFunction
you wroteThe slope should be
3
and the intercept should be4
7.2 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
, andy_true
7.3 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
))
7.4 Use the built-in method to plot the fit result
set the range equal to
0
to16
set the noise amplitude equal to
0.3
Save this to the variable plot
seed = np.random.seed(42)
# 7.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
linear_func = ...
# 7.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
x, y, y_true = ...
# 7.3 Fit the data
# save the fit results to popt, pcov
popt, pcov = ...
# 7.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
plot = ...
grader.check("q7 - plotting and fitting")
8. 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
.
8.1 SineFunction
where \(A\) represents amplitude, \(f\) represents the frequency, and \(\phi\) represents the phase.
8.2 ExponentialFunction
# 8.1 Write the SineFunction here
# the inputs should be x, amplitude, frequency, phase
...
# 8.1 Write the ExponentialFunction here
# the inputs should be x, a, b
...
grader.check("q8-new functions")
9 Using our class with the sine function#
Now we can use nearly the same code from Task 7 to test our SineFunction
.
9.1 Create a NoisyFunction
instance for the sine function
Assign this object to the variable
sine_func
The input
func
should be theSineFunction
you wroteThe amplitude should be 2, the frequency should be .2, and the phase should be .5
9.2 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
, andy_true
9.3 Fit the x
and y
data using the proper method of sine_func
save the fit results to
popt
,pcov
9.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
seed = np.random.seed(42)
# 9.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
sine_func = ...
# 9.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
x, y, y_true = ...
# 9.3 Fit the data
# save the fit results to popt, pcov
popt, pcov = ...
# 9.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
plot = ...
grader.check("q9-using the class with new Sine functions")
10 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!
10.1 Create a NoisyFunction
instance for a exponential function
Assign this object to the variable
exp_func
The input
func
should be theExponentialFunction
you wrotea
should be 2, andb
should be .2
10.2 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
, andy_true
10.3 Fit the x
and y
data using the proper method of exp_func
save the fit results to
popt
,pcov
10.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
seed = np.random.seed(42)
# 10.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
exp_func = ...
# 10.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
x, y, y_true = ...
# 10.3 Fit the data
# save the fit results to popt, pcov
popt, pcov = ...
# 10.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
plot = ...
grader.check("q10-using the class with new exponential functions")
Submitting Your Assignment#
To submit your assignment, please show the TA in your lab section your completed work before you leave the lab.