Skip to content

Commit

Permalink
write solutions for challenges; close #4
Browse files Browse the repository at this point in the history
  • Loading branch information
edbennett committed Jun 12, 2020
1 parent 399d1a8 commit 7a249cd
Show file tree
Hide file tree
Showing 3 changed files with 300 additions and 0 deletions.
156 changes: 156 additions & 0 deletions _episodes/02-writing-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,84 @@ Line width of blue plotter is 5
> variables attached to the object, and functions into methods. Some
> of these you won't be able to set in the class definition, but will
> need to be set before the functions will work.
>
>> ## Solution
>>
>> ~~~
>> class FitterPlotter:
>> x_data = None
>> y_data = None
>> x_err = None
>> y_err = None
>>
>> fit_result = None
>> fit_form = None
>> num_fit_params = None
>>
>> xmin = None
>> xmax = None
>>
>> def odr_fit(self, p0=None):
>> if None in (self.x_data, self.y_data, self.fit_form):
>> raise ValueError("x_data, y_data, and fit_form must be specified")
>> if not p0 and not self.num_fit_params:
>> raise ValueError("p0 or num_fit_params must be specified")
>> if p0 and (self.num_fit_params is not None):
>> assert len(p0) == self.num_fit_params
>>
>> data_to_fit = RealData(self.x_data, self.y_data, self.x_err, self.y_err)
>> model_to_fit_with = Model(self.fit_form)
>> if not p0:
>> p0 = tuple(1 for _ in range(self.num_fit_params))
>>
>> odr_analysis = ODR(data_to_fit, model_to_fit_with, p0)
>> odr_analysis.set_job(fit_type=0)
>> self.fit_result = odr_analysis.run()
>> return self.fit_result
>>
>> def plot_results(self, filename=None):
>> if None in (self.x_data, self.y_data):
>> raise ValueError("x_data and y_data must be specified")
>> fig, ax = subplots()
>> xmin, xmax = self.xmin, self.xmax
>> if xmin is None:
>> xmin = min(self.x_data)
>> if xmax is None:
>> xmax = max(self.x_data)
>>
>> if self.fit_result is not None:
>> x_range = linspace(xmin, xmax, 1000)
>> ax.plot(x_range, self.fit_form(self.fit_result.beta, x_range),
>> label='Fit')
>> fig.suptitle(f'Data: $A={self.fit_result.beta[0]:.02}'
>> f'\\pm{self.fit_result.cov_beta[0][0]**0.5:.02}, '
>> f'B={self.fit_result.beta[1]:.02}'
>> f'\\pm{self.fit_result.cov_beta[1][1]**0.5:.02}$')
>>
>> ax.errorbar(self.x_data, self.y_data, xerr=self.x_err, yerr=self.y_err,
>> fmt='.', label='Data')
>> ax.set_xlabel(r'$x$')
>> ax.set_ylabel(r'$y$')
>> ax.legend(loc=0, frameon=False)
>>
>> if filename is not None:
>> fig.savefig(filename)
>>
>>
>> fitterplotter = FitterPlotter()
>> fitterplotter.x_data = [0, 1, 2, 3, 4, 5]
>> fitterplotter.y_data = [1, 3, 2, 4, 5, 5]
>> fitterplotter.x_err = [0.2, 0.1, 0.3, 0.2, 0.5, 0.3]
>> fitterplotter.y_err = [0.4, 0.4, 0.1, 0.2, 0.1, 0.4]
>> fitterplotter.fit_form = linear
>> fitterplotter.num_fit_params = 2
>>
>> fitterplotter.odr_fit()
>> fitterplotter.plot_results()
>> show()
>> ~~~
>> {: .language-python}
> {: .solution}
{: .challenge}
Expand Down Expand Up @@ -343,6 +421,84 @@ usable, rather than deferring these errors to a long way down the line.
> Adjust your solution to the _Plots of fits_ challenge above so that
> it has an initialiser which checks that the needed parameters are
> given before initialising the object.
>
>> ## Solution
>>
>> ~~~
>> class FitterPlotter:
>> fit_result = None
>>
>> def __init__(self, x_data, y_data, x_err=None, y_err=None,
>> fit_form=None, num_fit_params=None, xmin=None, xmax=None):
>> self.x_data = x_data
>> self.y_data = y_data
>> self.x_err = x_err
>> self.y_err = y_err
>> self.fit_form = fit_form
>> self.num_fit_params = num_fit_params
>> self.xmin = xmin
>> self.xmax = xmax
>>
>> def odr_fit(self, p0=None):
>> if self.fit_form is None:
>> raise ValueError("fit_form must be specified")
>> if not p0 and not self.num_fit_params:
>> raise ValueError("p0 or num_fit_params must be specified")
>> if p0 and (self.num_fit_params is not None):
>> assert len(p0) == self.num_fit_params
>>
>> data_to_fit = RealData(self.x_data, self.y_data, self.x_err, self.y_err)
>> model_to_fit_with = Model(self.fit_form)
>> if not p0:
>> p0 = tuple(1 for _ in range(self.num_fit_params))
>>
>> odr_analysis = ODR(data_to_fit, model_to_fit_with, p0)
>> odr_analysis.set_job(fit_type=0)
>> self.fit_result = odr_analysis.run()
>> return self.fit_result
>>
>> def plot_results(self, filename=None):
>> fig, ax = subplots()
>> xmin, xmax = self.xmin, self.xmax
>> if xmin is None:
>> xmin = min(self.x_data)
>> if xmax is None:
>> xmax = max(self.x_data)
>>
>> if self.fit_result is not None:
>> x_range = linspace(xmin, xmax, 1000)
>> ax.plot(x_range, self.fit_form(self.fit_result.beta, x_range),
>> label='Fit')
>> fig.suptitle(f'Data: $A={self.fit_result.beta[0]:.02}'
>> f'\\pm{self.fit_result.cov_beta[0][0]**0.5:.02}, '
>> f'B={self.fit_result.beta[1]:.02}'
>> f'\\pm{self.fit_result.cov_beta[1][1]**0.5:.02}$')
>>
>> ax.errorbar(self.x_data, self.y_data, xerr=self.x_err, yerr=self.y_err,
>> fmt='.', label='Data')
>> ax.set_xlabel(r'$x$')
>> ax.set_ylabel(r'$y$')
>> ax.legend(loc=0, frameon=False)
>>
>> if filename is not None:
>> fig.savefig(filename)
>>
>>
>> fitterplotter = FitterPlotter(
>> x_data=[0, 1, 2, 3, 4, 5],
>> y_data=[1, 3, 2, 4, 5, 5],
>> x_err=[0.2, 0.1, 0.3, 0.2, 0.5, 0.3],
>> y_err=[0.4, 0.4, 0.1, 0.2, 0.1, 0.4],
>> fit_form=linear,
>> num_fit_params=2
>> )
>>
>> fitterplotter.odr_fit()
>> fitterplotter.plot_results()
>> show()
>> ~~~
>> {: .language-python}
> {: .solution}
{: .challenge}
{% include links.md %}
Expand Down
38 changes: 38 additions & 0 deletions _episodes/05-dunder.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,44 @@ class Triangle(Polygon):
> Add a class method that generates a triangle with three random edge
> lengths (for example, using `random.random()`. Use this to construct
> and sort a list of 10 random triangles.
>
>> ## Solution
>>
>> Add an import at the top of the file:
>>
>> ~~~
>> from random import random
>> ~~~
>> {: .language-python}
>>
>> Also add new class method:
>>
>> ~~~
>> @classmethod
>> def random(cls):
>> '''Returns a triangle with three random length sides in the
>> range [0, 1).
>> If the sum of the two short sides isn't longer than the
>> long side (and so the triangle doesn't close), then try
>> again. There is an infinitesimal probability that this
>> method will never return, as randomness keeps delivering
>> invalid triangles.'''
>>
>> random_triangle = cls([random(), random(), random()])
>> while isinstance(random_triangle.area(), complex):
>> random_triangle = cls([random(), random(), random()])
>> return random_triangle
>> ~~~
>> {: .language-python}
>>
>> Testing this:
>>
>> ~~~
>> random_triangles = [Triangle.random() for _ in range(10)]
>> [triangle.area() for triangle in sorted(random_triangles)]
>> ~~~
>> {: .language-python}
> {: .solution}
{: .challenge}
> ## Arithmetic
Expand Down
106 changes: 106 additions & 0 deletions _episodes/06-duck.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ for number in FibonacciIterator(100):
> Look back at the solutions for the `QuadraticPlotter`,
> `PolynomialPlotter`, and `FunctionPlotter`. What problems do you see
> with the `plot` method of these classes?
>
>> ## Solution
>>
>> The arguments to `FunctionPlotter.plot()`, `PolynomialPlotter.plot()`,
>> and `QuadraticPlotter.plot()` are all different—one expects a
>> callable, one expects a list of coefficients as one argument, and
>> one expects three coefficients as separate arguments. In general,
>> specialistations of a class should keep the same interface to its
>> functions, and the parent class should be interchangeable with its
>> specialisations.
> {: .solution}
{: .challenge}
> ## Over to you
Expand Down Expand Up @@ -314,6 +325,29 @@ inherit it.)
> The hashable protocol allows classes to be used as dictionary keys
> and as members of sets. Look up [the hashable protocol][hashable]
> and adjust the `Polygon` class so that it follows this.
>
> Test this by using a `Triangle` instance as a dict key:
>
> ~~~
> triangle_descriptions = {
> Triangle([3, 4, 5]): "The basic Pythagorean triangle"
> }
> ~~~
> {: .language-python}
>
>> ## Solution
>>
>> The hashable protocol requires implementing one method,
>> `__hash__()`, which should return a hash of the aspects of the
>> instance that make it unique. Lists can't be hashed, so we also
>> need to turn the list of `side_lengths` into a tuple.
>>
>> ~~~
>> def __hash__(self):
>> return hash(tuple(self.side_lengths))
>> ~~~
>> {: .language-python}
> {: .solution}
{: .challenge}
Expand Down Expand Up @@ -383,6 +417,78 @@ make it more complicated to get a view of the big picture.
> How could the `FunctionPlotter`, `PolynomialPlotter`, and
> `QuadraticPlotter` be refactored to make use of composition instead
> of inheritance?
>
>> ## Solution
>>
>> One way of doing this is to define a "plottable function"
>> interface. An object respecting this interface would:
>>
>> * be callable
>> * accept one argument
>> * return \\(f(x)\\)
>>
>> Then, with the `FunctionPlotter` as defined previously, there is no
>> need to subclass to create `QuadraticPlotter`s and
>> `PolynomialPlotter`s; instead, we can define a `QuadraticFunction`
>> class as:
>>
>> ~~~
>> class Quadratic:
>> def __init__(self, a, b, c):
>> self.a = a
>> self.b = b
>> self.c = c
>>
>> def __call__(self, x):
>> return self.a * x ** 2 + self.b * x + self.c
>> ~~~
>> {: .language-python}
>>
>> This can then be passed to a `FunctionPlotter`:
>>
>> ~~~
>> plotter = FunctionPlotter()
>> plotter.plot(Quadratic(1, -1, 1))
>> ~~~
>> {: .language-python}
>>
>> Alternatively, we can _encapsulate_ the function to be plotted as
>> part of the class.
>>
>> ~~~
>> from matplotlib.colors import is_color_like
>>
>> class FunctionPlotter:
>> def __init__(self, function, color='red', linewidth=1, x_min=-10, x_max=10):
>> assert is_color_like(color)
>> self.color = color
>> self.linewidth = linewidth
>> self.x_min = x_min
>> self.x_max = x_max
>> self.function = function
>>
>> def plot(self):
>> '''Plot a function of a single argument.
>> The line is plotted in the colour specified by color, and with width
>> linewidth.'''
>> fig, ax = subplots()
>> x = linspace(self.x_min, self.x_max, 1000)
>> ax.plot(x, self.function(x), color=self.color, linewidth=self.linewidth)
>> fig.show()
>> ~~~
>> {: .language-python}
>>
>> This could then be used as:
>>
>> ~~~
>> from numpy import sin
>> sin_plotter = FunctionPlotter(sin)
>> quadratic_plotter = FunctionPlotter(Quadratic(1, -1, 1), color='blue')
>> sin_plotter.plot()
>> quadratic_plotter.plot()
>> ~~~
>> {: .language-python}
> {: .solution}
{: .challenge}
[hashable]: https://docs.python.org/library/collections.abc.html#collections.abc.Hashable

0 comments on commit 7a249cd

Please sign in to comment.