Testing Class Material
This commit is contained in:
commit
05b1f6cdd5
70
.gitignore
vendored
Normal file
70
.gitignore
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
.pytest_cache
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*,cover
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
#macos stuff
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Jupyter
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# general
|
||||||
|
_archive/
|
6
LICENSE.txt
Normal file
6
LICENSE.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
The material in this repository is released under the
|
||||||
|
CC Attribution-Share Alike 4.0 International
|
||||||
|
license.
|
||||||
|
|
||||||
|
Full license text available at
|
||||||
|
https://creativecommons.org/licenses/by-sa/4.0/
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# testing_debugging_profiling
|
||||||
|
Material for the class "Testing, debugging, profiling -- Python tools for building software"
|
BIN
continuous_integration.pdf
Normal file
BIN
continuous_integration.pdf
Normal file
Binary file not shown.
BIN
continuous_integration.pptx
Normal file
BIN
continuous_integration.pptx
Normal file
Binary file not shown.
BIN
debugging.pdf
Normal file
BIN
debugging.pdf
Normal file
Binary file not shown.
BIN
debugging.pptx
Normal file
BIN
debugging.pptx
Normal file
Binary file not shown.
BIN
extra_slides/code_organization_slides.pptx
Normal file
BIN
extra_slides/code_organization_slides.pptx
Normal file
Binary file not shown.
BIN
extra_slides/mocking.pptx
Normal file
BIN
extra_slides/mocking.pptx
Normal file
Binary file not shown.
30
extra_slides/mocking/code.py
Normal file
30
extra_slides/mocking/code.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
>>> from smtplib import SMTP
|
||||||
|
>>> mock_smtp = Mock(spec=SMTP)
|
||||||
|
|
||||||
|
>>> isinstance(mock_smtp, SMTP)
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> mock_smtp.<TAB>
|
||||||
|
mock_smtp.assert_any_call mock_smtp.attach_mock mock_smtp.call_args
|
||||||
|
mock_smtp.assert_called_once_with mock_smtp.auth mock_smtp.call_args_list
|
||||||
|
mock_smtp.assert_called_with mock_smtp.auth_cram_md5 mock_smtp.call_count >
|
||||||
|
mock_smtp.assert_has_calls mock_smtp.auth_login mock_smtp.called
|
||||||
|
mock_smtp.assert_not_called mock_smtp.auth_plain mock_smtp.close
|
||||||
|
|
||||||
|
>>> mock_smtp.bogus
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
AttributeError Traceback (most recent call last)
|
||||||
|
<ipython-input-17-4856e93b6e10> in <module>()
|
||||||
|
----> 1 mock_smtp.bogus
|
||||||
|
|
||||||
|
/Users/pberkes/miniconda3/envs/gnode/lib/python3.5/unittest/mock.py in __getattr__(self, name)
|
||||||
|
576 elif self._mock_methods is not None:
|
||||||
|
577 if name not in self._mock_methods or name in _all_magics:
|
||||||
|
--> 578 raise AttributeError("Mock object has no attribute %r" % name)
|
||||||
|
579 elif _is_magic(name):
|
||||||
|
580 raise AttributeError(name)
|
||||||
|
|
||||||
|
AttributeError: Mock object has no attribute 'bogus'
|
||||||
|
|
||||||
|
|
64
extra_slides/mocking/demo_Mock.py
Normal file
64
extra_slides/mocking/demo_Mock.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
##### Mock basic
|
||||||
|
m = Mock()
|
||||||
|
m.x = 3
|
||||||
|
m.x
|
||||||
|
m.f(1,2,3)
|
||||||
|
m.whatever(3, key=2)
|
||||||
|
m
|
||||||
|
m.f
|
||||||
|
m.g
|
||||||
|
|
||||||
|
##### special attributes and assert methods
|
||||||
|
mock=Mock()
|
||||||
|
mock.f(2,3)
|
||||||
|
mock.f('a')
|
||||||
|
|
||||||
|
mock.f.called
|
||||||
|
mock.add.called
|
||||||
|
mock.f.called
|
||||||
|
mock.f.call_args
|
||||||
|
mock.f.call_count
|
||||||
|
mock.f.call_args_list
|
||||||
|
mock.f.assert_called_with('a')
|
||||||
|
mock.f.assert_called_once_with('a')
|
||||||
|
mock.f.assert_called_with(2, 3)
|
||||||
|
mock.f.assert_any_call(2, 3)
|
||||||
|
mock.f.assert_has_calls(['a', (2,3)])
|
||||||
|
|
||||||
|
#### return_value and side_effect
|
||||||
|
mock.g.return_value = 7
|
||||||
|
mock.g(32)
|
||||||
|
mock.g('r')
|
||||||
|
|
||||||
|
# useful to simulate file errors or server errors
|
||||||
|
mock.g.side_effect = Exception('Noooo')
|
||||||
|
mock.g(2)
|
||||||
|
mock.g.side_effect = lambda x: x.append(2)
|
||||||
|
a=[1]
|
||||||
|
mock.g(a)
|
||||||
|
a
|
||||||
|
|
||||||
|
mock.g.side_effect = [1, 4, 5]
|
||||||
|
mock.g()
|
||||||
|
mock.g()
|
||||||
|
mock.g()
|
||||||
|
mock.g()
|
||||||
|
|
||||||
|
#####
|
||||||
|
mock = Mock()
|
||||||
|
mock.f(3,4)
|
||||||
|
mock.g('a')
|
||||||
|
mock.f.a()
|
||||||
|
mock.method_calls
|
||||||
|
|
||||||
|
result = m.h(32)
|
||||||
|
result(1)
|
||||||
|
m.mock_calls
|
||||||
|
|
||||||
|
##### spec
|
||||||
|
from chaco.api import Plot
|
||||||
|
m2 = Mock(spec=Plot)
|
||||||
|
isinstance(m2, Plot)
|
||||||
|
m2.add
|
||||||
|
m2.add(12,'asdfasd')
|
||||||
|
m2.aaa
|
41
extra_slides/mocking/report.py
Normal file
41
extra_slides/mocking/report.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
report_template = """
|
||||||
|
Report
|
||||||
|
======
|
||||||
|
|
||||||
|
The experiment was a {judgment}!
|
||||||
|
Let's do this again, with a bigger budget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def send_report(result, smtp):
|
||||||
|
if result > 0.5:
|
||||||
|
judgment = 'big success'
|
||||||
|
else:
|
||||||
|
judgment = 'total failure'
|
||||||
|
report = report_template.format(judgment=judgment)
|
||||||
|
smtp.send_message(
|
||||||
|
report,
|
||||||
|
from_addr='pony@magicpony.com',
|
||||||
|
to_addrs=['ferenc@magicpony.com'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
def test_send_report_success():
|
||||||
|
smtp = Mock()
|
||||||
|
|
||||||
|
send_report(0.6, smtp)
|
||||||
|
assert smtp.send_message.call_count == 1
|
||||||
|
pos_args, kw_args = smtp.send_message.call_args
|
||||||
|
message = pos_args[0]
|
||||||
|
assert 'success' in message
|
||||||
|
|
||||||
|
smtp.reset_mock()
|
||||||
|
|
||||||
|
send_report(0.4, smtp)
|
||||||
|
assert smtp.send_message.call_count == 1
|
||||||
|
args, kwargs = smtp.send_message.call_args
|
||||||
|
message = args[0]
|
||||||
|
assert 'failure' in message
|
||||||
|
|
13
extra_slides/mocking/telescope/telescope_driver.py
Normal file
13
extra_slides/mocking/telescope/telescope_driver.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
def connect(address):
|
||||||
|
import time
|
||||||
|
time.sleep(5)
|
||||||
|
return '1'
|
||||||
|
|
||||||
|
def get_angle(address):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def set_angle(address, angle):
|
||||||
|
if angle < 0 or angle > 1.40:
|
||||||
|
raise IOError('Telescope jammed -- please call technical support')
|
||||||
|
return True
|
30
extra_slides/mocking/telescope/telescope_model.py
Normal file
30
extra_slides/mocking/telescope/telescope_model.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import telescope_driver
|
||||||
|
|
||||||
|
|
||||||
|
class TelescopeModel(object):
|
||||||
|
|
||||||
|
# Minimum safe elevation angle (see handbook).
|
||||||
|
MIN_ANGLE = 0.0
|
||||||
|
|
||||||
|
# Maximum safe elevation angle (see handbook).
|
||||||
|
MAX_ANGLE = 80.0
|
||||||
|
|
||||||
|
def __init__(self, address):
|
||||||
|
self.address = address
|
||||||
|
# Connect to telescope
|
||||||
|
self.connection = telescope_driver.connect(address)
|
||||||
|
# Get initial state of telescope.
|
||||||
|
self.current_angle = telescope_driver.get_angle(self.connection)
|
||||||
|
|
||||||
|
def set_elevation_angle(self, angle):
|
||||||
|
""" Set the elevation angle of the telescope (in rad).
|
||||||
|
|
||||||
|
If the angle is outside the range allowed by the manufacturer,
|
||||||
|
raise a ValueError.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if angle < self.MIN_ANGLE or angle > self.MAX_ANGLE:
|
||||||
|
raise ValueError('Unsafe elevation angle: {}'.format(angle))
|
||||||
|
|
||||||
|
telescope_driver.set_angle(self.connection, angle)
|
||||||
|
self.current_angle = angle
|
12
extra_slides/mocking/telescope/test_telescope_model.py
Normal file
12
extra_slides/mocking/telescope/test_telescope_model.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import numpy as np
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
from telescope_model import TelescopeModel
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsafe_elevation_angle():
|
||||||
|
telescope = TelescopeModel(address='10.2.1.1')
|
||||||
|
elevation_angle = np.pi / 2.0
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
telescope.set_elevation_angle(elevation_angle)
|
|
@ -0,0 +1,29 @@
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
from telescope_model import TelescopeModel
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsafe_elevation_angle():
|
||||||
|
with mock.patch('telescope_model.telescope_driver'):
|
||||||
|
telescope = TelescopeModel(address='10.2.1.1')
|
||||||
|
elevation_angle = np.pi / 2.0
|
||||||
|
with raises(ValueError):
|
||||||
|
telescope.set_elevation_angle(elevation_angle)
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_initialization():
|
||||||
|
connection_id = 'bogus_connection'
|
||||||
|
initial_angle = 1.23
|
||||||
|
|
||||||
|
with mock.patch('telescope_model.telescope_driver') as driver:
|
||||||
|
driver.connect.return_value = connection_id
|
||||||
|
driver.get_angle.return_value = initial_angle
|
||||||
|
|
||||||
|
telescope = TelescopeModel(address='10.2.1.1')
|
||||||
|
assert telescope.connection == connection_id
|
||||||
|
assert driver.connect.call_count == 1
|
||||||
|
assert telescope.current_angle == initial_angle
|
||||||
|
|
BIN
extra_slides/packaging.pptx
Normal file
BIN
extra_slides/packaging.pptx
Normal file
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 415 KiB |
22
extra_slides/packaging/noiser_project_final/noiser/main.py
Normal file
22
extra_slides/packaging/noiser_project_final/noiser/main.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from scipy.ndimage import imread
|
||||||
|
from pkg_resources import resource_filename
|
||||||
|
|
||||||
|
from noiser.noise import white_noise
|
||||||
|
from noiser.utils import copy_image
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
path = resource_filename('noiser', os.path.join('images', 'baboon_kandinsky.png'))
|
||||||
|
print(path)
|
||||||
|
img = imread(path)
|
||||||
|
noisy = copy_image(white_noise(img, 20))
|
||||||
|
plt.imshow(noisy)
|
||||||
|
plt.draw()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,7 @@
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def white_noise(image, std):
|
||||||
|
noise = np.random.normal(scale=std, size=image.shape).astype(image.dtype)
|
||||||
|
noisy = image + noise
|
||||||
|
return noisy
|
|
@ -0,0 +1,8 @@
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Noiser',
|
||||||
|
version='1.0',
|
||||||
|
packages=find_packages(),
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import numpy as np
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
|
from noiser.noise import white_noise
|
||||||
|
|
||||||
|
|
||||||
|
def test_white_noise():
|
||||||
|
n_images, height, width = 201, 101, 102
|
||||||
|
dtype = np.float32
|
||||||
|
|
||||||
|
# Create ``n_images`` identical image.
|
||||||
|
base_image = np.random.rand(1, height, width, 3).astype(dtype) - 0.5
|
||||||
|
images = np.repeat(base_image, n_images, axis=0)
|
||||||
|
|
||||||
|
std = 0.13
|
||||||
|
noisy = white_noise(images, std=std)
|
||||||
|
|
||||||
|
# dtype and shape are preserved.
|
||||||
|
assert noisy.dtype == dtype
|
||||||
|
assert noisy.shape == images.shape
|
||||||
|
|
||||||
|
# Mean and std of noisy image match expectations.
|
||||||
|
assert_allclose(images.mean(0), base_image[0], atol=1e-4)
|
||||||
|
assert np.isclose((noisy - images).std(), std, atol=1e-4)
|
|
@ -0,0 +1,14 @@
|
||||||
|
import numpy as np
|
||||||
|
from numpy.testing import assert_array_equal
|
||||||
|
|
||||||
|
from noiser.utils import copy_image
|
||||||
|
|
||||||
|
|
||||||
|
def test_copy_image():
|
||||||
|
height, width = 101, 102
|
||||||
|
dtype = np.float32
|
||||||
|
|
||||||
|
image = np.random.rand(height, width, 3).astype(dtype)
|
||||||
|
copy = copy_image(image)
|
||||||
|
assert_array_equal(copy, image)
|
||||||
|
|
15
extra_slides/packaging/noiser_project_final/noiser/utils.pyx
Normal file
15
extra_slides/packaging/noiser_project_final/noiser/utils.pyx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import numpy as np
|
||||||
|
cimport numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def copy_image(np.ndarray img):
|
||||||
|
cdef int h = img.shape[0]
|
||||||
|
cdef int w = img.shape[1]
|
||||||
|
cdef int c = img.shape[2]
|
||||||
|
cdef np.ndarray copy = np.empty([h, w, c], dtype=img.dtype)
|
||||||
|
|
||||||
|
for i in range(h):
|
||||||
|
for j in range(w):
|
||||||
|
for k in range(c):
|
||||||
|
copy[i, j, k] = img[i, j, k]
|
||||||
|
return copy
|
29
extra_slides/packaging/noiser_project_final/setup.py
Normal file
29
extra_slides/packaging/noiser_project_final/setup.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
import numpy
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
from setuptools.extension import Extension
|
||||||
|
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
Extension(
|
||||||
|
'noiser.utils',
|
||||||
|
["noiser/utils.pyx"],
|
||||||
|
include_dirs=[numpy.get_include()],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Noiser',
|
||||||
|
version='1.0',
|
||||||
|
packages=find_packages(),
|
||||||
|
ext_modules=cythonize(extensions),
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'baboon=noiser.main:main',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
package_data={
|
||||||
|
'noiser': ['images/*.png'],
|
||||||
|
}
|
||||||
|
)
|
31
extra_slides/profiling/residuals.py
Normal file
31
extra_slides/profiling/residuals.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import numpy as np
|
||||||
|
import theano
|
||||||
|
from theano import tensor as T
|
||||||
|
|
||||||
|
|
||||||
|
SLOPE = 3.1
|
||||||
|
INTERCEPT = -1.2
|
||||||
|
|
||||||
|
|
||||||
|
def residual_stats_theano(x, y):
|
||||||
|
expected = SLOPE * x + INTERCEPT
|
||||||
|
residuals = y - expected
|
||||||
|
return residuals.mean(), residuals.std()
|
||||||
|
|
||||||
|
|
||||||
|
x_var = T.vector()
|
||||||
|
y_var = T.vector()
|
||||||
|
|
||||||
|
residual_stats = theano.function(
|
||||||
|
inputs=[x_var, y_var],
|
||||||
|
outputs=residual_stats_theano(x_var, y_var),
|
||||||
|
allow_input_downcast=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
x = np.linspace(-10, 10, 1000)
|
||||||
|
y = SLOPE * x + INTERCEPT
|
||||||
|
y += np.random.normal(loc=0.1, scale=0.5, size=x.shape)
|
||||||
|
mn, std = residual_stats(x, y)
|
||||||
|
print('Residual mean=', mn, ', std=', std)
|
35
extra_slides/profiling/residuals_profile.py
Normal file
35
extra_slides/profiling/residuals_profile.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import numpy as np
|
||||||
|
import theano
|
||||||
|
from theano import tensor as T
|
||||||
|
|
||||||
|
theano.config.profile_memory = True
|
||||||
|
theano.config.profile = True
|
||||||
|
|
||||||
|
|
||||||
|
SLOPE = 3.1
|
||||||
|
INTERCEPT = -1.2
|
||||||
|
|
||||||
|
|
||||||
|
def residual_stats_theano(x, y):
|
||||||
|
expected = SLOPE * x + INTERCEPT
|
||||||
|
residuals = y - expected
|
||||||
|
return residuals.mean(), residuals.std()
|
||||||
|
|
||||||
|
|
||||||
|
x_var = T.vector()
|
||||||
|
y_var = T.vector()
|
||||||
|
|
||||||
|
residual_stats = theano.function(
|
||||||
|
inputs=[x_var, y_var],
|
||||||
|
outputs=residual_stats_theano(x_var, y_var),
|
||||||
|
allow_input_downcast=True,
|
||||||
|
profile=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
x = np.linspace(-10, 10, 1000)
|
||||||
|
y = SLOPE * x + INTERCEPT
|
||||||
|
y += np.random.normal(loc=0.1, scale=0.5, size=x.shape)
|
||||||
|
mn, std = residual_stats(x, y)
|
||||||
|
print('Residual mean=', mn, ', std=', std)
|
1
hands_on/bug_hunt/datastore/README
Normal file
1
hands_on/bug_hunt/datastore/README
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is the directory used as a datastore by `file_datastore.py`.
|
113
hands_on/bug_hunt/file_datastore.py
Normal file
113
hands_on/bug_hunt/file_datastore.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class FileDatastore:
|
||||||
|
"""Datastore based on a regular file system.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
base_path: str
|
||||||
|
Filesystem path at which the data store is based.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_path):
|
||||||
|
if not os.path.exists(base_path):
|
||||||
|
raise FileNotFoundError(f'Base path {base_path} does not exist')
|
||||||
|
if not os.path.isdir(base_path):
|
||||||
|
raise NotADirectoryError(f'Base path {base_path} exists but is not a directory')
|
||||||
|
|
||||||
|
self.base_path = base_path
|
||||||
|
|
||||||
|
def open(self, path, mode):
|
||||||
|
""" Open a file-like object
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path relative to the root of the data store.
|
||||||
|
mode: str
|
||||||
|
Specifies the mode in which the file is opened.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
IO[Any]
|
||||||
|
An open file-like object.
|
||||||
|
"""
|
||||||
|
path = os.path.join(self.base_path, path)
|
||||||
|
return open(path, mode)
|
||||||
|
|
||||||
|
def read(self, path):
|
||||||
|
""" Read a sequence of bytes from the data store.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path relative to the root of the data store.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The sequence of bytes read from `path`.
|
||||||
|
"""
|
||||||
|
with self.open(path, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def write(self, path, data) -> None:
|
||||||
|
""" Write a sequence of bytes to the data store.
|
||||||
|
|
||||||
|
`path` contains the path relative to the root of the data store, including the name
|
||||||
|
of the file to be created. If a file already exists at `path`, it is overwritten.
|
||||||
|
|
||||||
|
Intermediate directories that do not exist will be created.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path relative to the root of the data store.
|
||||||
|
data: bytes
|
||||||
|
Sequence of bytes to write at `path`.
|
||||||
|
"""
|
||||||
|
path = os.path.join(self.base_path, path)
|
||||||
|
self.makedirs(os.path.dirname(path))
|
||||||
|
with self.open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
def exists(self, path):
|
||||||
|
""" Returns True if the file exists.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path relative to the root of the data store.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
True if the file exists, false otherwise
|
||||||
|
"""
|
||||||
|
complete_path = os.path.join(self.base_path, path)
|
||||||
|
return os.path.exists(complete_path)
|
||||||
|
|
||||||
|
def makedirs(self, path):
|
||||||
|
""" Creates the specified directory if needed.
|
||||||
|
|
||||||
|
If the directories already exist, the method does not do anything.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path relative to the root of the data store.
|
||||||
|
"""
|
||||||
|
complete_path = os.path.join(self.base_path, path)
|
||||||
|
os.makedirs(complete_path, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
data = b'A test! 012'
|
||||||
|
datastore = FileDatastore(base_path='./datastore')
|
||||||
|
datastore.write('a/mydata.bin', data)
|
||||||
|
|
||||||
|
# This should pass!
|
||||||
|
# The code works correctly if `base_path` is an absolute path :-(
|
||||||
|
assert os.path.exists('./datastore/a/mydata.bin')
|
65
hands_on/debugger/analyze_sums_and_differences.ipynb
Normal file
65
hands_on/debugger/analyze_sums_and_differences.ipynb
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "30f3f04f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from debugger_example import sum_over_difference"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "3632cacb",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"result = sum_over_difference(7, 5)\n",
|
||||||
|
"print(result)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "02577028",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"result = sum_over_difference(12, 12)\n",
|
||||||
|
"print(result)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "58fc58c0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.11.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
23
hands_on/debugger/debugger_example.py
Executable file
23
hands_on/debugger/debugger_example.py
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
def add(arg1, arg2):
|
||||||
|
return arg1 + arg2
|
||||||
|
|
||||||
|
|
||||||
|
def sub(arg1, arg2):
|
||||||
|
return arg1 - arg2
|
||||||
|
|
||||||
|
|
||||||
|
def div(arg1, arg2):
|
||||||
|
return arg1 / arg2
|
||||||
|
|
||||||
|
|
||||||
|
def sum_over_difference(arg1, arg2):
|
||||||
|
"""Compute sum of arguments over difference of arguments."""
|
||||||
|
arg_sum = add(arg1, arg2)
|
||||||
|
arg_diff = sub(arg1, arg2)
|
||||||
|
sum_over_diff = div(arg_sum, arg_diff)
|
||||||
|
return sum_over_diff
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
result = sum_over_difference(7, 5)
|
||||||
|
print(result)
|
33
hands_on/factorial/factorial.py
Normal file
33
hands_on/factorial/factorial.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
""" Compute the factorial of a set of numbers stored in a file. """
|
||||||
|
|
||||||
|
def factorial(n):
|
||||||
|
if n == 0:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return factorial(n-1) * n
|
||||||
|
|
||||||
|
|
||||||
|
def read_data(filename):
|
||||||
|
numbers = []
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
number = int(line)
|
||||||
|
numbers.append(number)
|
||||||
|
return numbers
|
||||||
|
|
||||||
|
|
||||||
|
def compute_factorials_for_list(numbers):
|
||||||
|
factorials = []
|
||||||
|
for number in numbers:
|
||||||
|
result = factorial(number)
|
||||||
|
factorials.append(result)
|
||||||
|
return factorials
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
numbers = read_data('numbers.txt')
|
||||||
|
factorials = compute_factorials_for_list(numbers)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
100000
hands_on/factorial/numbers.txt
Normal file
100000
hands_on/factorial/numbers.txt
Normal file
File diff suppressed because it is too large
Load diff
13
hands_on/factorial/test_factorial.py
Normal file
13
hands_on/factorial/test_factorial.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
""" Tests for the factorial function. """
|
||||||
|
|
||||||
|
from factorial import factorial
|
||||||
|
|
||||||
|
|
||||||
|
def test_factorial():
|
||||||
|
factorial_cases = [(1, 1),
|
||||||
|
(0, 1),
|
||||||
|
(5, 2*3*4*5),
|
||||||
|
(30, 265252859812191058636308480000000)]
|
||||||
|
|
||||||
|
for n, fact_n in factorial_cases:
|
||||||
|
assert factorial(n) == fact_n
|
8
hands_on/first/first.py
Normal file
8
hands_on/first/first.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
def times_3(x):
|
||||||
|
""" Multiply x by 3.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
x : The item to multiply by 3.
|
||||||
|
"""
|
||||||
|
return x * 3
|
19
hands_on/first/test_first.py
Normal file
19
hands_on/first/test_first.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from first import times_3
|
||||||
|
|
||||||
|
|
||||||
|
def test_times_3_integer():
|
||||||
|
value = 7
|
||||||
|
expected = 21
|
||||||
|
|
||||||
|
result = times_3(value)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_times_3_string():
|
||||||
|
value = 'wow'
|
||||||
|
expected = 'wowwowwow'
|
||||||
|
|
||||||
|
result = times_3(value)
|
||||||
|
|
||||||
|
assert result == expected
|
8
hands_on/first_teacher/first.py
Normal file
8
hands_on/first_teacher/first.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
def times_3(x):
|
||||||
|
""" Multiply x by 3.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
x : The item to multiply by 3.
|
||||||
|
"""
|
||||||
|
return x * 3
|
10
hands_on/local_maxima/local_maxima.py
Normal file
10
hands_on/local_maxima/local_maxima.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
def find_maxima(x):
|
||||||
|
"""Find local maxima of x.
|
||||||
|
|
||||||
|
Input arguments:
|
||||||
|
x -- 1D list of real numbers
|
||||||
|
|
||||||
|
Output:
|
||||||
|
idx -- list of indices of the local maxima in x
|
||||||
|
"""
|
||||||
|
return []
|
10
hands_on/local_maxima_part2/local_maxima.py
Normal file
10
hands_on/local_maxima_part2/local_maxima.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
def find_maxima(x):
|
||||||
|
"""Find local maxima of x.
|
||||||
|
|
||||||
|
Input arguments:
|
||||||
|
x -- 1D list of real numbers
|
||||||
|
|
||||||
|
Output:
|
||||||
|
idx -- list of indices of the local maxima in x
|
||||||
|
"""
|
||||||
|
return []
|
30
hands_on/local_maxima_part2/test_local_maxima.py
Normal file
30
hands_on/local_maxima_part2/test_local_maxima.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from local_maxima import find_maxima
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima():
|
||||||
|
values = [1, 3, -2, 0, 2, 1]
|
||||||
|
expected = [1, 4]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_edges():
|
||||||
|
values = [4, 2, 1, 3, 1, 5]
|
||||||
|
expected = [0, 3, 5]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_empty():
|
||||||
|
values = []
|
||||||
|
expected = []
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau():
|
||||||
|
raise Exception('not yet implemented')
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_not_a_plateau():
|
||||||
|
raise Exception('not yet implemented')
|
49
hands_on/local_maxima_part3_debug/local_maxima.py
Normal file
49
hands_on/local_maxima_part3_debug/local_maxima.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
def find_maxima(x):
|
||||||
|
"""Find local maxima of x.
|
||||||
|
|
||||||
|
Input arguments:
|
||||||
|
x -- 1D list of real numbers
|
||||||
|
|
||||||
|
Output:
|
||||||
|
idx -- list of indices of the local maxima in x
|
||||||
|
"""
|
||||||
|
|
||||||
|
maxima = []
|
||||||
|
|
||||||
|
len_x = len(x)
|
||||||
|
if len_x == 0:
|
||||||
|
return maxima
|
||||||
|
|
||||||
|
maxima = check_first_element(x, maxima)
|
||||||
|
maxima = check_last_element(x, maxima)
|
||||||
|
|
||||||
|
# Check numbers in between
|
||||||
|
i = 1
|
||||||
|
while i < len_x - 1:
|
||||||
|
if x[i] > x[i - 1]:
|
||||||
|
# We have found a potential maximum or start of a plateau
|
||||||
|
plateau_start = i
|
||||||
|
while i < len_x - 1 and x[i] == x[i + 1]:
|
||||||
|
i += 1
|
||||||
|
plateau_end = i
|
||||||
|
if x[plateau_end] > x[plateau_end + 1]:
|
||||||
|
maxima.append(plateau_start)
|
||||||
|
i += 1
|
||||||
|
return maxima
|
||||||
|
|
||||||
|
|
||||||
|
def check_first_element(x, maxima):
|
||||||
|
if x[0] > x[1]:
|
||||||
|
maxima.append(0)
|
||||||
|
return maxima
|
||||||
|
|
||||||
|
|
||||||
|
def check_last_element(x, maxima):
|
||||||
|
if x[-1] > x[-2]:
|
||||||
|
maxima.append(len(x) - 1)
|
||||||
|
return maxima
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = find_maxima([1, 2, 2, 1])
|
||||||
|
print(result)
|
107
hands_on/local_maxima_part3_debug/test_local_maxima.py
Normal file
107
hands_on/local_maxima_part3_debug/test_local_maxima.py
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from local_maxima import find_maxima
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima():
|
||||||
|
values = [1, 3, -2, 0, 2, 1]
|
||||||
|
expected = [1, 4]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_edges():
|
||||||
|
values = [4, 2, 1, 0, 1, 5]
|
||||||
|
expected = [0, 5]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_empty():
|
||||||
|
values = []
|
||||||
|
expected = []
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau():
|
||||||
|
values = [1, 2, 2, 1]
|
||||||
|
expected = [1]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_not_a_plateau():
|
||||||
|
values = [1, 2, 2, 3, 1]
|
||||||
|
expected = [3]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
# the tests below here fail, can you get them to pass?
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_correct_order():
|
||||||
|
# TASK: get this test to pass
|
||||||
|
values = [2, 1, 5, 1, 9]
|
||||||
|
expected = [0, 2, 4]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_one_value():
|
||||||
|
# TASK: get this test to pass
|
||||||
|
values = [1]
|
||||||
|
expected = [0]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_long_plateau():
|
||||||
|
# TASK: Change the implementation for when there is a plateau
|
||||||
|
# for uneven plateau length, return the middle index, e.g. [1, 2, *2*, 2, 1] --> [2]
|
||||||
|
# for even plateau length, return the index left of the middle e.g. [1, 2, *2*, 2, 2, 1] --> [2]
|
||||||
|
values = [1, 2, 2, 2, 2, 2, 1, 8, 0]
|
||||||
|
expected = [3, 7]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau_at_end():
|
||||||
|
# TASK: make sure plateaus at the end are handled properly (see test above)
|
||||||
|
values = [1, 2, 2]
|
||||||
|
expected = [1]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau_at_start():
|
||||||
|
# TASK: make sure plateaus at the start are handled properly (see test above)
|
||||||
|
values = [1, 1, 0, 0]
|
||||||
|
expected = [0]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_all_same_values():
|
||||||
|
# TASK: implement a check for lists where there is no local maximum
|
||||||
|
values = [1, 1]
|
||||||
|
expected = [0]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_letters():
|
||||||
|
# stings can be evaluated with > and <, who knew!
|
||||||
|
# Find an easy solution so that both "t"s are recognised as local maxima
|
||||||
|
values = ["T", "e", "s", "t", "s", "!"]
|
||||||
|
expected = [0, 3]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_new_inputs_to_make_current_function_fail():
|
||||||
|
# should you actually be done with all tests, then you can think of other cases where the current function fails
|
||||||
|
# and write tests for them and fix them
|
||||||
|
assert False
|
1
hands_on/logistic_fun/readme.md
Normal file
1
hands_on/logistic_fun/readme.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This exercise is in a separate repo!
|
22
hands_on/numpy_equality/test_numpy_equality.py
Normal file
22
hands_on/numpy_equality/test_numpy_equality.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def test_equality():
|
||||||
|
x = np.array([1, 1])
|
||||||
|
y = np.array([2, 2])
|
||||||
|
z = np.array([3, 3])
|
||||||
|
assert x + y == z
|
||||||
|
|
||||||
|
|
||||||
|
def test_equality_with_nan():
|
||||||
|
x = np.array([1, np.nan])
|
||||||
|
y = np.array([2, np.nan])
|
||||||
|
z = np.array([3, np.nan])
|
||||||
|
assert x + y == z
|
||||||
|
|
||||||
|
|
||||||
|
def test_allclose_with_nan():
|
||||||
|
x = np.array([1.1, np.nan])
|
||||||
|
y = np.array([2.2, np.nan])
|
||||||
|
z = np.array([3.3, np.nan])
|
||||||
|
assert x + y == z
|
8
hands_on_solutions/first/first.py
Normal file
8
hands_on_solutions/first/first.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
def times_3(x):
|
||||||
|
""" Multiply x by 3.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
x : The item to multiply by 3.
|
||||||
|
"""
|
||||||
|
return x * 3
|
28
hands_on_solutions/first/test_first.py
Normal file
28
hands_on_solutions/first/test_first.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from first import times_3
|
||||||
|
|
||||||
|
|
||||||
|
def test_times_3_integer():
|
||||||
|
value = 7
|
||||||
|
expected = 21
|
||||||
|
|
||||||
|
result = times_3(value)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_times_3_string():
|
||||||
|
value = 'wow'
|
||||||
|
expected = 'wowwowwow'
|
||||||
|
|
||||||
|
result = times_3(value)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_times_3_list():
|
||||||
|
value = [1]
|
||||||
|
expected = [1, 1, 1]
|
||||||
|
|
||||||
|
result = times_3(value)
|
||||||
|
|
||||||
|
assert result == expected
|
44
hands_on_solutions/local_maxima/local_maxima.py
Normal file
44
hands_on_solutions/local_maxima/local_maxima.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
def find_maxima(x):
|
||||||
|
"""Find local maxima of x.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> x = [1, 3, -2, 0, 2, 1]
|
||||||
|
>>> find_maxima(x)
|
||||||
|
[1, 4]
|
||||||
|
|
||||||
|
If in a local maximum several elements have the same value,
|
||||||
|
return the left-most index.
|
||||||
|
Example:
|
||||||
|
>>> x = [1, 2, 2, 1]
|
||||||
|
>>> find_maxima(x)
|
||||||
|
[1]
|
||||||
|
|
||||||
|
Input arguments:
|
||||||
|
x -- 1D list of real numbers
|
||||||
|
|
||||||
|
Output:
|
||||||
|
idx -- list of indices of the local maxima in x
|
||||||
|
"""
|
||||||
|
|
||||||
|
idx = []
|
||||||
|
up = False
|
||||||
|
down = False
|
||||||
|
for i in range(len(x)):
|
||||||
|
if i == 0 or x[i-1] < x[i]:
|
||||||
|
up = True
|
||||||
|
up_idx = i
|
||||||
|
elif x[i-1] > x[i]:
|
||||||
|
up = False
|
||||||
|
|
||||||
|
# if x[i-1] == x[i], no change
|
||||||
|
|
||||||
|
if i+1 == len(x) or x[i+1] < x[i]:
|
||||||
|
down = True
|
||||||
|
elif x[i+1] > x[i]:
|
||||||
|
down = False
|
||||||
|
|
||||||
|
if up and down:
|
||||||
|
idx.append(up_idx)
|
||||||
|
|
||||||
|
return idx
|
||||||
|
|
36
hands_on_solutions/local_maxima/test_local_maxima.py
Normal file
36
hands_on_solutions/local_maxima/test_local_maxima.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from local_maxima import find_maxima
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima():
|
||||||
|
values = [1, 3, -2, 0, 2, 1]
|
||||||
|
expected = [1, 4]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_edges():
|
||||||
|
values = [4, 2, 1, 3, 1, 5]
|
||||||
|
expected = [0, 3, 5]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_empty():
|
||||||
|
values = []
|
||||||
|
expected = []
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau():
|
||||||
|
values = [1, 2, 2, 1]
|
||||||
|
expected = [1]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_not_a_plateau():
|
||||||
|
values = [1, 2, 2, 3, 1]
|
||||||
|
expected = [3]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
|
@ -0,0 +1,67 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def find_maxima(x):
|
||||||
|
"""Find local maxima of x.
|
||||||
|
|
||||||
|
Input arguments:
|
||||||
|
x -- 1D list of real numbers
|
||||||
|
|
||||||
|
Output:
|
||||||
|
idx -- list of indices of the local maxima in x
|
||||||
|
"""
|
||||||
|
maxima = []
|
||||||
|
len_x = len(x)
|
||||||
|
if len_x == 0:
|
||||||
|
return maxima
|
||||||
|
elif len_x == 1:
|
||||||
|
return [0]
|
||||||
|
|
||||||
|
# additional checks
|
||||||
|
if np.all([isinstance(item, str) for item in x]):
|
||||||
|
x = [item.lower() for item in x]
|
||||||
|
if np.all([item == x[0] for item in x]):
|
||||||
|
return [0]
|
||||||
|
|
||||||
|
maxima = check_first_element(x, maxima)
|
||||||
|
# Check numbers in between
|
||||||
|
i = 1
|
||||||
|
while i < len_x - 1:
|
||||||
|
if x[i] >= x[i - 1]:
|
||||||
|
# We have found a potential maximum or start of a plateau
|
||||||
|
# breakpoint()
|
||||||
|
if i == 1 and x[i] == x[i - 1]:
|
||||||
|
plateau_start = i - 1
|
||||||
|
else:
|
||||||
|
plateau_start = i
|
||||||
|
while i < len_x - 1 and x[i] == x[i + 1]:
|
||||||
|
i += 1
|
||||||
|
plateau_end = i
|
||||||
|
if plateau_end == len_x - 1:
|
||||||
|
maxima.append((plateau_end + plateau_start) // 2)
|
||||||
|
elif x[plateau_end] > x[plateau_end + 1]:
|
||||||
|
maxima.append((plateau_end + plateau_start) // 2)
|
||||||
|
i += 1
|
||||||
|
maxima = check_last_element(x, maxima)
|
||||||
|
|
||||||
|
return maxima
|
||||||
|
|
||||||
|
|
||||||
|
def check_first_element(x, maxima):
|
||||||
|
if x[0] > x[1]:
|
||||||
|
maxima.append(0)
|
||||||
|
return maxima
|
||||||
|
|
||||||
|
|
||||||
|
def check_last_element(x, maxima):
|
||||||
|
if x[-1] > x[-2]:
|
||||||
|
maxima.append(len(x) - 1)
|
||||||
|
return maxima
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# result = find_maxima([1, 3, -2, 0, 2, 1])
|
||||||
|
result = find_maxima([1, 2, 2, 1])
|
||||||
|
print(result)
|
|
@ -0,0 +1,97 @@
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from local_maxima_solution import find_maxima
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima():
|
||||||
|
values = [1, 3, -2, 0, 2, 1]
|
||||||
|
expected = [1, 4]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_edges():
|
||||||
|
values = [4, 2, 1, 0, 1, 5]
|
||||||
|
expected = [0, 5]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_empty():
|
||||||
|
values = []
|
||||||
|
expected = []
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau():
|
||||||
|
values = [1, 2, 2, 1]
|
||||||
|
expected = [1]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_not_a_plateau():
|
||||||
|
values = [1, 2, 2, 3, 1]
|
||||||
|
expected = [3]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
# the tests below here fail, can you get them to pass?
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_correct_order():
|
||||||
|
values = [2, 1, 5, 1, 9]
|
||||||
|
expected = [0, 2, 4]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_one_value():
|
||||||
|
values = [1]
|
||||||
|
expected = [0]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_long_plateau():
|
||||||
|
values = [1, 2, 2, 2, 2, 2, 1, 8, 0]
|
||||||
|
expected = [3, 7]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau_at_end():
|
||||||
|
values = [1, 2, 2]
|
||||||
|
expected = [1]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_plateau_at_start():
|
||||||
|
values = [1, 1, 0, 0]
|
||||||
|
expected = [0]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_all_same_values():
|
||||||
|
values = [1, 1]
|
||||||
|
expected = [0]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_letters():
|
||||||
|
values = ["T", "e", "s", "t", "s", "!"]
|
||||||
|
expected = [0, 3]
|
||||||
|
maxima = find_maxima(values)
|
||||||
|
assert maxima == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_maxima_new_inputs_to_make_current_function_fail():
|
||||||
|
# should you actually be done with all tests, then you can think of other cases where the current function fails
|
||||||
|
# and write tests for them and fix them
|
||||||
|
assert True
|
BIN
hands_on_solutions/logistic_fun/bifurcation.png
Normal file
BIN
hands_on_solutions/logistic_fun/bifurcation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 567 KiB |
28
hands_on_solutions/logistic_fun/conftest.py
Normal file
28
hands_on_solutions/logistic_fun/conftest.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
# add a commandline option to pytest
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
"""Add random seed option to py.test.
|
||||||
|
"""
|
||||||
|
parser.addoption('--seed', dest='seed', type=int, action='store',
|
||||||
|
help='set random seed')
|
||||||
|
|
||||||
|
|
||||||
|
# configure pytest to automatically set the rnd seed if not passed on CLI
|
||||||
|
def pytest_configure(config):
|
||||||
|
seed = config.getvalue("seed")
|
||||||
|
# if seed was not set by the user, we set one now
|
||||||
|
if seed is None or seed == ('NO', 'DEFAULT'):
|
||||||
|
config.option.seed = int(np.random.randint(2**31-1))
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config):
|
||||||
|
return f'Using random seed: {config.option.seed}'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def random_state(request):
|
||||||
|
random_state = np.random.RandomState(request.config.option.seed)
|
||||||
|
return random_state
|
10
hands_on_solutions/logistic_fun/conftest2.py
Normal file
10
hands_on_solutions/logistic_fun/conftest2.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
SEED = 42
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def random_state():
|
||||||
|
print(f'Seed: {SEED}')
|
||||||
|
random_state = np.random.RandomState(SEED)
|
||||||
|
return random_state
|
25
hands_on_solutions/logistic_fun/logistic.py
Normal file
25
hands_on_solutions/logistic_fun/logistic.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def f(x, r):
|
||||||
|
"""
|
||||||
|
takes r and x as input and returns r*x*(1-x)
|
||||||
|
"""
|
||||||
|
return r * x * (1 - x)
|
||||||
|
|
||||||
|
|
||||||
|
def iterate_f(it, xi, r):
|
||||||
|
"""
|
||||||
|
takes a number of iterations `it`, a starting value,
|
||||||
|
and a parameter value for r. It should execute f repeatedly (it times),
|
||||||
|
each time using the last result of f as the new input to f. Append each
|
||||||
|
iteration's result to a list l. Finally, convert the list into a numpy
|
||||||
|
array and return it.
|
||||||
|
"""
|
||||||
|
x = xi
|
||||||
|
xs = []
|
||||||
|
for _ in range(it):
|
||||||
|
x = f(x, r)
|
||||||
|
xs.append(x)
|
||||||
|
|
||||||
|
return np.array(xs)
|
70
hands_on_solutions/logistic_fun/plot_logfun.py
Normal file
70
hands_on_solutions/logistic_fun/plot_logfun.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""Usage:
|
||||||
|
```
|
||||||
|
plot_trajectory(100, 3.6, 0.1)
|
||||||
|
plot_bifurcation(2.5, 4.2, 0.001)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from logistic import iterate_f
|
||||||
|
|
||||||
|
|
||||||
|
def plot_trajectory(n, r, x0, fname="single_trajectory.png"):
|
||||||
|
"""
|
||||||
|
Saves a plot of a single trajectory of the logistic function
|
||||||
|
|
||||||
|
inputs
|
||||||
|
n: int (number of iterations)
|
||||||
|
r: float (r value for the logistic function)
|
||||||
|
x0: float (between 0 and 1, starting point for the iteration)
|
||||||
|
fname: str (filename to which to save the image)
|
||||||
|
|
||||||
|
returns
|
||||||
|
fig, ax (matplotlib objects)
|
||||||
|
"""
|
||||||
|
l = iterate_f(n, x0, r)
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
|
ax.plot(list(range(n)), l)
|
||||||
|
fig.suptitle('Logistic Function')
|
||||||
|
|
||||||
|
fig.savefig(fname)
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_bifurcation(start, end, step, fname="bifurcation.png", it=100000,
|
||||||
|
last=300):
|
||||||
|
"""
|
||||||
|
Saves a plot of the bifurcation diagram of the logistic function. The
|
||||||
|
`start`, `end`, and `step` parameters define for which r values to
|
||||||
|
calculate the logistic function. If you space them too closely, it might
|
||||||
|
take a very long time, if you dont plot enough, your bifurcation diagram
|
||||||
|
won't be informative. Choose wisely!
|
||||||
|
|
||||||
|
inputs
|
||||||
|
start, end, step: float (which r values to calculate the logistic
|
||||||
|
function for)
|
||||||
|
fname: str (filename to which to save the image)
|
||||||
|
it: int (how many iterations to run for each r value)
|
||||||
|
last: int (how many of the last iterates to plot)
|
||||||
|
|
||||||
|
|
||||||
|
returns
|
||||||
|
fig, ax (matplotlib objects)
|
||||||
|
"""
|
||||||
|
r_range = np.arange(start, end, step)
|
||||||
|
x = []
|
||||||
|
y = []
|
||||||
|
|
||||||
|
for r in r_range:
|
||||||
|
l = iterate_f(it, 0.1, r)
|
||||||
|
ll = l[len(l) - last::].copy()
|
||||||
|
lll = np.unique(ll)
|
||||||
|
y.extend(lll)
|
||||||
|
x.extend(np.ones(len(lll)) * r)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(20, 10))
|
||||||
|
ax.scatter(x, y, s=0.1, color='k')
|
||||||
|
ax.set_xlabel("r")
|
||||||
|
fig.savefig(fname)
|
||||||
|
return fig, ax
|
3
hands_on_solutions/logistic_fun/plot_script.py
Normal file
3
hands_on_solutions/logistic_fun/plot_script.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from plot_logfun import plot_trajectory
|
||||||
|
|
||||||
|
plot_trajectory(100, 3.4, 0.1)
|
81
hands_on_solutions/logistic_fun/readme.md
Normal file
81
hands_on_solutions/logistic_fun/readme.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# Testing Project for ASPP 2023 Mexico
|
||||||
|
|
||||||
|
## Exercise 1 -- @parametrize and the logistic map
|
||||||
|
|
||||||
|
Make a file `logistic.py` and `test_logistic.py` in the same folder as this
|
||||||
|
readme and the `plot_logfun.py` file. Implement the code for the logistic map
|
||||||
|
in the `logistic.py` file:
|
||||||
|
|
||||||
|
a) Implement the logistic map f(𝑥)=𝑟∗𝑥∗(1−𝑥) . Use `@parametrize`
|
||||||
|
to test the function for the following cases:
|
||||||
|
```
|
||||||
|
x=0.1, r=2.2 => f(x, r)=0.198
|
||||||
|
x=0.2, r=3.4 => f(x, r)=0.544
|
||||||
|
x=0.75, r=1.7 => f(x, r)=0.31875
|
||||||
|
```
|
||||||
|
|
||||||
|
b) Implement the function `iterate_f` that runs `f` for `it`
|
||||||
|
iterations, each time passing the result back into f.
|
||||||
|
Use `@parametrize` to test the function for the following cases:
|
||||||
|
```
|
||||||
|
x=0.1, r=2.2, it=1 => iterate_f(it, x, r)=[0.198]
|
||||||
|
x=0.2, r=3.4, it=4 => f(x, r)=[0.544, 0.843418, 0.449019, 0.841163]
|
||||||
|
x=0.75, r=1.7, it=2 => f(x, r)=[0.31875, 0.369152]
|
||||||
|
```
|
||||||
|
|
||||||
|
c) Import and call the `plot_trajectory` function from the `plot_logfun`
|
||||||
|
module to look at the trajectories generated by your code. The `plot_logfun`
|
||||||
|
imports and uses your `logistic.py` code. Import the module
|
||||||
|
and call the function in a new `plot_script.py` file.
|
||||||
|
|
||||||
|
Try with values `r<3`, `r>4` and `3<r<4` to get a feeling for how the function
|
||||||
|
behaves differently with different parameters. Note that your input x0 should
|
||||||
|
be between 0 and 1.
|
||||||
|
|
||||||
|
## Exercise 2 -- Check the convergence of an attractor using fuzzing
|
||||||
|
a) Write a numerical fuzzing test that checks that, for `r=1.5`, all
|
||||||
|
starting points converge to the attractor `f(x, r) = 1/3`.
|
||||||
|
|
||||||
|
b) Use `pytest.mark` to mark the tests from the previous exercise with one mark
|
||||||
|
(they relate to the correct implementation of the logistic map) and the
|
||||||
|
test from this exercise with another (relates to the behavior of the logistic
|
||||||
|
map). Try executing first the first set of tests and then the second set of
|
||||||
|
tests separately.
|
||||||
|
|
||||||
|
## Exercise 3 -- Chaotic behavior
|
||||||
|
Some r values for `3<r<4` have some interesting properties. A chaotic
|
||||||
|
trajectory doesn't diverge but also doesn't converge.
|
||||||
|
|
||||||
|
## Visualize the bifurcation diagram
|
||||||
|
a) Use the `plot_trajectory` function from the `plot_logfun` module using your
|
||||||
|
implementation of `f` and `iterate_f` to look at the bifurcation diagram.
|
||||||
|
|
||||||
|
The script generates an output image, `bifurcation_diagram.png`.
|
||||||
|
|
||||||
|
b) Write a test that checks for chaotic behavior when r=3.8. Run the
|
||||||
|
logistic map for 100000 iterations and verify the conditions for
|
||||||
|
chaotic behavior:
|
||||||
|
|
||||||
|
1) The function is deterministic: this does not need to be tested in
|
||||||
|
this case
|
||||||
|
2) Orbits must be bounded: check that all values are between 0 and 1
|
||||||
|
3) Orbits must be aperiodic: check that the last 1000 values are all
|
||||||
|
different
|
||||||
|
4) Sensitive dependence on initial conditions: this is the bonus
|
||||||
|
exercise below
|
||||||
|
|
||||||
|
The test should check conditions 2) and 3)!
|
||||||
|
|
||||||
|
|
||||||
|
## Bonus Exercise 4 -- The Butterfly Effect
|
||||||
|
For the same value of `r`, test the sensitive dependence on initial
|
||||||
|
conditions, a.k.a. the butterfly effect. Use the following definition of SDIC.
|
||||||
|
|
||||||
|
>`f` is a function and `x0` and `y0` are two possible seeds.
|
||||||
|
>If `f` has SDIC then:
|
||||||
|
>there is a number `delta` such that for any `x0` there is a `y0` that is not
|
||||||
|
>more than `init_error` away from `x0`, where the initial condition `y0` has
|
||||||
|
>the property that there is some integer n such that after n iterations, the
|
||||||
|
>orbit is more than `delta` away from the orbit of `x0`. That is
|
||||||
|
>|xn-yn| > delta
|
||||||
|
|
BIN
hands_on_solutions/logistic_fun/single_trajectory.png
Normal file
BIN
hands_on_solutions/logistic_fun/single_trajectory.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
108
hands_on_solutions/logistic_fun/test_logistic.py
Normal file
108
hands_on_solutions/logistic_fun/test_logistic.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import numpy as np
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from logistic import f, iterate_f
|
||||||
|
|
||||||
|
# set the random seed for once here
|
||||||
|
SEED = np.random.randint(0, 2**31)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def random_state():
|
||||||
|
print(f'Using seed {SEED}')
|
||||||
|
random_state = np.random.RandomState(SEED)
|
||||||
|
return random_state
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('a', [1, 2, 3])
|
||||||
|
@pytest.mark.parametrize('b', [5, 6, 7])
|
||||||
|
def test_addition_increases(a, b):
|
||||||
|
print(a, b)
|
||||||
|
assert b + a > a
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'x, r, expected',
|
||||||
|
[
|
||||||
|
(0.1, 2.2, 0.198),
|
||||||
|
(0.2, 3.4, 0.544),
|
||||||
|
(0.75, 1.7, 0.31875),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_f(x, r, expected):
|
||||||
|
result = f(x, r)
|
||||||
|
assert_allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'x, r, it, expected',
|
||||||
|
[
|
||||||
|
(0.1, 2.2, 1, [0.198]),
|
||||||
|
(0.2, 3.4, 4, [0.544, 0.843418, 0.449019, 0.841163]),
|
||||||
|
(0.75, 1.7, 2, [0.31875, 0.369152]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_iterate_f(x, r, it, expected):
|
||||||
|
result = iterate_f(it, x, r)
|
||||||
|
assert_allclose(result, expected, rtol=1e-5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_attractor_converges():
|
||||||
|
SEED = 42
|
||||||
|
random_state = np.random.RandomState(SEED)
|
||||||
|
|
||||||
|
for _ in range(100):
|
||||||
|
x = random_state.uniform(0, 1)
|
||||||
|
result = iterate_f(100, x, 1.5)
|
||||||
|
assert_allclose(result[-1], 1 / 3)
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# These only work after adding the fixture
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
|
def test_attractor_converges2(random_state):
|
||||||
|
for _ in range(100):
|
||||||
|
x = random_state.uniform(0, 1)
|
||||||
|
result = iterate_f(100, x, 1.5)
|
||||||
|
assert_allclose(result[-1], 1 / 3)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
|
def test_chaotic_behavior(random_state):
|
||||||
|
r = 3.8
|
||||||
|
for _ in range(10):
|
||||||
|
x = random_state.uniform(0, 1)
|
||||||
|
result = iterate_f(100000, x, r)
|
||||||
|
assert np.all(result >= 0.0)
|
||||||
|
assert np.all(result <= 1.0)
|
||||||
|
assert min(np.abs(np.diff(result[-1000:]))) > 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
|
def test_sensitivity_to_initial_conditions(random_state):
|
||||||
|
"""
|
||||||
|
`f` is a function and `x0` and `y0` are two possible seeds.
|
||||||
|
If `f` has SDIC then:
|
||||||
|
there is a number `delta` such that for any `x0` there is a `y0` that is
|
||||||
|
not more than `init_error` away from `x0`, where the initial condition `y0`
|
||||||
|
has the property that there is some integer n such that after n iterations,
|
||||||
|
the orbit is more than `delta` away from the orbit of `x0`. That is
|
||||||
|
|xn-yn| > delta
|
||||||
|
"""
|
||||||
|
delta = 0.1
|
||||||
|
n = 10000
|
||||||
|
x0 = random_state.rand()
|
||||||
|
x0_diffs = random_state.rand(100) * 0.001 - 0.0005
|
||||||
|
|
||||||
|
result_list = []
|
||||||
|
for x0_diff in x0_diffs:
|
||||||
|
x1 = x0 + x0_diff
|
||||||
|
l_x = iterate_f(n, x0, 3.8)
|
||||||
|
l_y = iterate_f(n, x1, 3.8)
|
||||||
|
result_list.append(any(abs(l_x - l_y) > delta))
|
||||||
|
assert any(result_list)
|
408
hands_on_solutions/massmail_solution/massmail
Normal file
408
hands_on_solutions/massmail_solution/massmail
Normal file
|
@ -0,0 +1,408 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# Script to send mass email
|
||||||
|
#
|
||||||
|
# Copyright (C) 2003-2017 Tiziano Zito <opossumnano@gmail.com>, Jakob Jordan <jakobjordan@posteo.de>
|
||||||
|
#
|
||||||
|
# This script is free software and comes without any warranty, to
|
||||||
|
# the extent permitted by applicable law. You can redistribute it
|
||||||
|
# and/or modify it under the terms of the Do What The Fuck You Want
|
||||||
|
# To Public License, Version 2, as published by Sam Hocevar.
|
||||||
|
# http://www.wtfpl.net
|
||||||
|
#
|
||||||
|
# Full license text:
|
||||||
|
#
|
||||||
|
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
# Version 2, December 2004.
|
||||||
|
#
|
||||||
|
# Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
# copies of this license document, and changing it is allowed as long
|
||||||
|
# as the name is changed.
|
||||||
|
#
|
||||||
|
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
#
|
||||||
|
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
|
|
||||||
|
import smtplib, getopt, sys, os, email, getpass
|
||||||
|
import email.header
|
||||||
|
import email.mime.text
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
|
||||||
|
PROGNAME = os.path.basename(sys.argv[0])
|
||||||
|
USAGE = """Send mass mail
|
||||||
|
Usage:
|
||||||
|
%s [...] PARAMETER_FILE < BODY
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-F FROM set the From: header for all messages.
|
||||||
|
Must be ASCII. This argument is required
|
||||||
|
|
||||||
|
-S SUBJECT set the Subject: header for all messages
|
||||||
|
|
||||||
|
-B BCC set the Bcc: header for all messages.
|
||||||
|
Must be ASCII
|
||||||
|
|
||||||
|
-s SEPARATOR set field separator in parameter file,
|
||||||
|
default: ";"
|
||||||
|
|
||||||
|
-e ENCODING set PARAMETER_FILE *and* BODY character set
|
||||||
|
encoding, default: "UTF-8". Note that if you fuck
|
||||||
|
up this one, your email will be full of rubbish:
|
||||||
|
You have been warned!
|
||||||
|
|
||||||
|
-f fake run: don't really send emails, just print to
|
||||||
|
standard output what would be done. Don't be scared
|
||||||
|
if you can not read the body: it is base64 encoded
|
||||||
|
UTF8 text
|
||||||
|
|
||||||
|
-z SERVER the SMTP server to use. This argument is required
|
||||||
|
|
||||||
|
-P PORT the SMTP port to use.
|
||||||
|
|
||||||
|
-u SMTP user name. If not set, use anonymous SMTP
|
||||||
|
connection
|
||||||
|
|
||||||
|
-p SMTP password. If not set you will be prompted for one
|
||||||
|
|
||||||
|
-h show this usage message
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The message body is read from standard input or
|
||||||
|
typed in interactively (exit with ^D) and keywords are subsituted with values
|
||||||
|
read from a parameter file. The first line of the parameter file defines
|
||||||
|
the keywords. The keyword $EMAIL$ must always be present and contains a comma
|
||||||
|
separated list of email addresses.
|
||||||
|
Keep in mind shell escaping when setting headers with white spaces or special
|
||||||
|
characters.
|
||||||
|
Character set encodings are those supported by python.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* Example of a parameter file:
|
||||||
|
|
||||||
|
$NAME$; $SURNAME$; $EMAIL$
|
||||||
|
John; Smith; j@guys.com
|
||||||
|
Anne; Joyce; a@girls.com
|
||||||
|
|
||||||
|
* Example of body:
|
||||||
|
|
||||||
|
Dear $NAME$ $SURNAME$,
|
||||||
|
|
||||||
|
I think you are a great guy/girl!
|
||||||
|
|
||||||
|
Cheers,
|
||||||
|
|
||||||
|
My self.
|
||||||
|
|
||||||
|
* Example usage:
|
||||||
|
|
||||||
|
%s -F "Great Guy <gg@guys.com>" -S "You are a great guy" -B "Not so great Guy <ngg@guys.com>" parameter-file < body
|
||||||
|
|
||||||
|
"""%(PROGNAME, PROGNAME)
|
||||||
|
|
||||||
|
def error(s):
|
||||||
|
sys.stderr.write(PROGNAME+': ')
|
||||||
|
sys.stderr.write(s+'\n')
|
||||||
|
sys.stderr.flush()
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
def parse_command_line_options(arguments):
|
||||||
|
# parse options
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(arguments, "hfs:F:S:B:R:e:u:p:P:z:")
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
error(str(err)+USAGE)
|
||||||
|
|
||||||
|
# set default options
|
||||||
|
options = {
|
||||||
|
'sep': u';',
|
||||||
|
'fake': False,
|
||||||
|
'from': '',
|
||||||
|
'subject': '',
|
||||||
|
'bcc': '',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
'smtpuser': None,
|
||||||
|
'smtppassword': None,
|
||||||
|
'server': None,
|
||||||
|
'port': 0,
|
||||||
|
'in_reply_to': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
for option, value in opts:
|
||||||
|
if option == "-e":
|
||||||
|
options['encoding'] = value
|
||||||
|
if option == "-s":
|
||||||
|
options['sep'] = value
|
||||||
|
elif option == "-F":
|
||||||
|
options['from'] = value
|
||||||
|
elif option == "-S":
|
||||||
|
options['subject'] = value
|
||||||
|
elif option == "-B":
|
||||||
|
options['bcc'] = value
|
||||||
|
elif option == "-R":
|
||||||
|
options['in_reply_to'] = value
|
||||||
|
elif option == "-h":
|
||||||
|
error(USAGE)
|
||||||
|
elif option == "-f":
|
||||||
|
options['fake'] = True
|
||||||
|
elif option == "-u":
|
||||||
|
options['smtpuser'] = value
|
||||||
|
elif option == "-p":
|
||||||
|
options['smtppassword'] = value
|
||||||
|
elif option == "-P":
|
||||||
|
options['port'] = int(value)
|
||||||
|
elif option == "-z":
|
||||||
|
options['server'] = value
|
||||||
|
|
||||||
|
if len(args) == 0:
|
||||||
|
error('You must specify a parameter file')
|
||||||
|
|
||||||
|
if len(options['from']) == 0:
|
||||||
|
error('You must set a from address with option -F')
|
||||||
|
|
||||||
|
if options['server'] is None:
|
||||||
|
error('You must set a SMTP server with option -z')
|
||||||
|
|
||||||
|
if options['sep'] == ",":
|
||||||
|
error('Separator can not be a comma')
|
||||||
|
|
||||||
|
# get password if needed
|
||||||
|
if options['smtpuser'] is not None and options['smtppassword'] is None:
|
||||||
|
options['smtppassword'] = getpass.getpass('Enter password for %s: '%options['smtpuser'])
|
||||||
|
|
||||||
|
# set filenames of parameter and mail body
|
||||||
|
options['fn_parameters'] = args[0]
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
def parse_parameter_file(options):
|
||||||
|
pars_fh = open(options['fn_parameters'], 'rbU')
|
||||||
|
pars = pars_fh.read()
|
||||||
|
pars_fh.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
pars = unicode(pars, encoding=options['encoding'])
|
||||||
|
except UnicodeDecodeError, e:
|
||||||
|
error('Error decoding "'+options['fn_parameters']+'": '+str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
options['subject'] = unicode(options['subject'], encoding=options['encoding'])
|
||||||
|
except UnicodeDecodeError, e:
|
||||||
|
error('Error decoding SUBJECT: '+str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
options['from'] = unicode(options['from'], encoding=options['encoding'])
|
||||||
|
except UnicodeDecodeError, e:
|
||||||
|
error('Error decoding FROM: '+str(e))
|
||||||
|
|
||||||
|
if options['in_reply_to'] and not options['in_reply_to'].startswith('<'):
|
||||||
|
options['in_reply_to'] = '<{}>'.format(options['in_reply_to'])
|
||||||
|
|
||||||
|
# split lines
|
||||||
|
pars = pars.splitlines()
|
||||||
|
|
||||||
|
# get keywords from first line
|
||||||
|
keywords_list = [key.strip() for key in pars[0].split(options['sep'])]
|
||||||
|
|
||||||
|
# fail immediately if no EMAIL keyword is found
|
||||||
|
if '$EMAIL$' not in keywords_list:
|
||||||
|
error('No $EMAIL$ keyword found in %s'%options['fn_parameters'])
|
||||||
|
|
||||||
|
# check that all keywords start and finish with a '$' character
|
||||||
|
for key in keywords_list:
|
||||||
|
if not key.startswith('$') or not key.endswith('$'):
|
||||||
|
error(('Keyword "%s" malformed: should be $KEYWORD$'%key).encode(options['encoding']))
|
||||||
|
|
||||||
|
# gather all values
|
||||||
|
email_count = 0
|
||||||
|
keywords = dict([(key, []) for key in keywords_list])
|
||||||
|
for count, line in enumerate(pars[1:]):
|
||||||
|
# ignore empty lines
|
||||||
|
if len(line) == 0:
|
||||||
|
continue
|
||||||
|
values = [key.strip() for key in line.split(options['sep'])]
|
||||||
|
if len(values) != len(keywords_list):
|
||||||
|
error(('Line %d in "%s" malformed: %d values found instead of'
|
||||||
|
' %d: %s'%(count+1,options['fn_parameters'],len(values),len(keywords_list),line)).encode(options['encoding']))
|
||||||
|
for i, key in enumerate(keywords_list):
|
||||||
|
keywords[key].append(values[i])
|
||||||
|
email_count += 1
|
||||||
|
|
||||||
|
return keywords, email_count
|
||||||
|
|
||||||
|
def create_email_bodies(options, keywords, email_count, body):
|
||||||
|
try:
|
||||||
|
body = unicode(body, encoding=options['encoding'])
|
||||||
|
except UnicodeDecodeError, e:
|
||||||
|
error('Error decoding email body: '+str(e))
|
||||||
|
|
||||||
|
# find keywords and substitute with values
|
||||||
|
# we need to create email_count bodies
|
||||||
|
msgs = {}
|
||||||
|
|
||||||
|
for i in range(email_count):
|
||||||
|
lbody = body
|
||||||
|
for key in keywords:
|
||||||
|
lbody = lbody.replace(key, keywords[key][i])
|
||||||
|
|
||||||
|
# Any single dollar left? That means that the body was malformed
|
||||||
|
single_dollar_exists = lbody.count('$') != 2 * lbody.count('$$')
|
||||||
|
if single_dollar_exists:
|
||||||
|
raise ValueError('Malformed email body: unclosed placeholder or non-escaped dollar sign.')
|
||||||
|
|
||||||
|
# Replace double dollars with single dollars
|
||||||
|
lbody = lbody.replace('$$', '$')
|
||||||
|
|
||||||
|
# encode body
|
||||||
|
lbody = email.mime.text.MIMEText(lbody.encode(options['encoding']), 'plain', options['encoding'])
|
||||||
|
msgs[keywords['$EMAIL$'][i]] = lbody
|
||||||
|
|
||||||
|
return msgs
|
||||||
|
|
||||||
|
def add_email_headers(options, msgs):
|
||||||
|
# msgs is now a dictionary with {emailaddr:body}
|
||||||
|
# we need to add the headers
|
||||||
|
|
||||||
|
for emailaddr in msgs:
|
||||||
|
msg = msgs[emailaddr]
|
||||||
|
msg['To'] = str(emailaddr)
|
||||||
|
msg['From'] = email.header.Header(options['from'])
|
||||||
|
if options['subject']:
|
||||||
|
msg['Subject'] = email.header.Header(options['subject'].encode(options['encoding']), options['encoding'])
|
||||||
|
if options['in_reply_to']:
|
||||||
|
msg['In-Reply-To'] = email.header.Header(options['in_reply_to'])
|
||||||
|
msgs[emailaddr] = msg
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_messages(options, msgs):
|
||||||
|
server = smtplib.SMTP(options['server'], port=options['port'])
|
||||||
|
|
||||||
|
if options['smtpuser'] is not None:
|
||||||
|
server.starttls()
|
||||||
|
server.login(options['smtpuser'], options['smtppassword'])
|
||||||
|
|
||||||
|
for emailaddr in msgs:
|
||||||
|
print 'Sending email to:', emailaddr
|
||||||
|
emails = [e.strip() for e in emailaddr.split(',')]
|
||||||
|
if len(options['bcc']) > 0:
|
||||||
|
emails.append(options['bcc'])
|
||||||
|
if options['fake']:
|
||||||
|
print msgs[emailaddr].as_string()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
out = server.sendmail(options['from'], emails, msgs[emailaddr].as_string())
|
||||||
|
except Exception, err:
|
||||||
|
error(str(err))
|
||||||
|
|
||||||
|
if len(out) != 0:
|
||||||
|
error(str(out))
|
||||||
|
|
||||||
|
server.close()
|
||||||
|
|
||||||
|
def test_dummy():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_parse_parameter_file():
|
||||||
|
expected_keywords = {u'$VALUE$': [u'this is a test'], u'$EMAIL$': [u'testrecv@test']}
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
f.write('$EMAIL$;$VALUE$\ntestrecv@test;this is a test')
|
||||||
|
f.flush()
|
||||||
|
cmd_options = [
|
||||||
|
'-F', 'testfrom@test',
|
||||||
|
'-z', 'localhost',
|
||||||
|
f.name,
|
||||||
|
]
|
||||||
|
options = parse_command_line_options(cmd_options)
|
||||||
|
keywords, email_count = parse_parameter_file(options)
|
||||||
|
assert keywords == expected_keywords
|
||||||
|
|
||||||
|
def test_local_sending():
|
||||||
|
parameter_string = '$EMAIL$;$NAME$;$VALUE$\ntestrecv@test.org;TestName;531'
|
||||||
|
email_body = 'Dear $NAME$,\nthis is a test: $VALUE$\nBest regards'
|
||||||
|
email_to = 'testrecv@test.org'
|
||||||
|
email_from = 'testfrom@test.org'
|
||||||
|
email_subject = 'Test Subject'
|
||||||
|
email_encoding = 'utf-8'
|
||||||
|
|
||||||
|
expected_email = email.mime.text.MIMEText('Dear TestName,\nthis is a test: 531\nBest regards'.encode(email_encoding), 'plain', email_encoding)
|
||||||
|
expected_email['To'] = email_to
|
||||||
|
expected_email['From'] = email_from
|
||||||
|
expected_email['Subject'] = email.header.Header(email_subject.encode(email_encoding), email_encoding)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
f.write(parameter_string)
|
||||||
|
f.flush()
|
||||||
|
cmd_options = [
|
||||||
|
'-F', email_from,
|
||||||
|
'-S', email_subject,
|
||||||
|
'-z', 'localhost',
|
||||||
|
'-e', email_encoding,
|
||||||
|
f.name
|
||||||
|
]
|
||||||
|
options = parse_command_line_options(cmd_options)
|
||||||
|
keywords, email_count = parse_parameter_file(options)
|
||||||
|
msgs = create_email_bodies(options, keywords, email_count, email_body)
|
||||||
|
add_email_headers(options, msgs)
|
||||||
|
assert msgs['testrecv@test.org'].as_string() == expected_email.as_string()
|
||||||
|
|
||||||
|
def test_malformed_body():
|
||||||
|
parameter_string = '$EMAIL$;$NAME$;$VALUE$\ntestrecv@test.org;TestName;531'
|
||||||
|
email_body = '$NAME$VALUE$'
|
||||||
|
email_from = 'testfrom@test.org'
|
||||||
|
email_subject = 'Test Subject'
|
||||||
|
email_encoding = 'utf-8'
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
f.write(parameter_string)
|
||||||
|
f.flush()
|
||||||
|
cmd_options = [
|
||||||
|
'-F', email_from,
|
||||||
|
'-S', email_subject,
|
||||||
|
'-z', 'localhost',
|
||||||
|
'-e', email_encoding,
|
||||||
|
f.name
|
||||||
|
]
|
||||||
|
options = parse_command_line_options(cmd_options)
|
||||||
|
keywords, email_count = parse_parameter_file(options)
|
||||||
|
with raises(ValueError):
|
||||||
|
msgs = create_email_bodies(options, keywords, email_count, email_body)
|
||||||
|
|
||||||
|
def test_double_dollar():
|
||||||
|
parameter_string = '$EMAIL$;$NAME$;$VALUE$\ntestrecv@test.org;TestName;531'
|
||||||
|
email_body = 'Dear $NAME$,\nyou owe us 254$$'
|
||||||
|
email_to = 'testrecv@test.org'
|
||||||
|
email_from = 'testfrom@test.org'
|
||||||
|
email_subject = 'Test Subject'
|
||||||
|
email_encoding = 'utf-8'
|
||||||
|
|
||||||
|
expected_email = email.mime.text.MIMEText('Dear TestName,\nyou owe us 254$'.encode(email_encoding), 'plain', email_encoding)
|
||||||
|
expected_email['To'] = email_to
|
||||||
|
expected_email['From'] = email_from
|
||||||
|
expected_email['Subject'] = email.header.Header(email_subject.encode(email_encoding), email_encoding)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
f.write(parameter_string)
|
||||||
|
f.flush()
|
||||||
|
cmd_options = [
|
||||||
|
'-F', email_from,
|
||||||
|
'-S', email_subject,
|
||||||
|
'-z', 'localhost',
|
||||||
|
'-e', email_encoding,
|
||||||
|
f.name
|
||||||
|
]
|
||||||
|
options = parse_command_line_options(cmd_options)
|
||||||
|
keywords, email_count = parse_parameter_file(options)
|
||||||
|
msgs = create_email_bodies(options, keywords, email_count, email_body)
|
||||||
|
assert msgs['testrecv@test.org'].get_payload(decode=email_encoding) == expected_email.get_payload(decode=email_encoding)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
options = parse_command_line_options(sys.argv[1:])
|
||||||
|
keywords, email_count = parse_parameter_file(options)
|
||||||
|
msgs = create_email_bodies(options, keywords, email_count, sys.stdin.read())
|
||||||
|
add_email_headers(options, msgs)
|
||||||
|
send_messages(options, msgs)
|
22
hands_on_solutions/numerical_fuzzing/test_var.py
Normal file
22
hands_on_solutions/numerical_fuzzing/test_var.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from math import isclose
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
def test_var_deterministic():
|
||||||
|
x = numpy.array([-2.0, 2.0])
|
||||||
|
expected = 4.0
|
||||||
|
assert isclose(numpy.var(x), expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_var_fuzzing():
|
||||||
|
rand_state = numpy.random.RandomState(8393)
|
||||||
|
|
||||||
|
N, D = 100000, 5
|
||||||
|
# Goal variances: [0.1 , 0.45, 0.8 , 1.15, 1.5]
|
||||||
|
expected = numpy.linspace(0.1, 1.5, D)
|
||||||
|
|
||||||
|
# Generate random, D-dimensional data
|
||||||
|
x = rand_state.randn(N, D) * numpy.sqrt(expected)
|
||||||
|
variance = numpy.var(x, axis=0)
|
||||||
|
numpy.testing.assert_allclose(variance, expected, rtol=1e-2)
|
23
hands_on_solutions/numpy_equality/test_numpy_equality.py
Normal file
23
hands_on_solutions/numpy_equality/test_numpy_equality.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import numpy as np
|
||||||
|
from numpy.testing import assert_allclose, assert_equal
|
||||||
|
|
||||||
|
|
||||||
|
def test_equality():
|
||||||
|
x = np.array([1, 1])
|
||||||
|
y = np.array([2, 2])
|
||||||
|
z = np.array([3, 3])
|
||||||
|
assert_equal(x + y, z)
|
||||||
|
|
||||||
|
|
||||||
|
def test_equality_with_nan():
|
||||||
|
x = np.array([1, np.nan])
|
||||||
|
y = np.array([2, np.nan])
|
||||||
|
z = np.array([3, np.nan])
|
||||||
|
assert_equal(x + y, z)
|
||||||
|
|
||||||
|
|
||||||
|
def test_allclose_with_nan():
|
||||||
|
x = np.array([1.1, np.nan])
|
||||||
|
y = np.array([2.2, np.nan])
|
||||||
|
z = np.array([3.3, np.nan])
|
||||||
|
assert_allclose(x + y, z)
|
30
how_I_compiled_qcachegrind_on_osx.txt
Normal file
30
how_I_compiled_qcachegrind_on_osx.txt
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
1) Install dependencies with MacPorts:
|
||||||
|
sudo port install qt4-mac graphviz
|
||||||
|
|
||||||
|
Get source code for qcachegrind:
|
||||||
|
|
||||||
|
2) Get kcachegrind-0.7.4.tar.gz from http://kcachegrind.sourceforge.net/html/Download.html
|
||||||
|
|
||||||
|
3) Unzip it:
|
||||||
|
tar xfz ~/Downloads/kcachegrind-0.7.4.tar.gz
|
||||||
|
|
||||||
|
We need "qmake" to find the right libraries
|
||||||
|
|
||||||
|
4) Set PATH to find the right "qmake":
|
||||||
|
export PATH=/opt/local/libexec/qt4/bin:/opt/local/bin:/opt/local/sbin:$PATH
|
||||||
|
|
||||||
|
5) Set library path:
|
||||||
|
export DYLD_LIBRARY_PATH=/opt/local/libexec/qt4/Library/Frameworks/
|
||||||
|
|
||||||
|
Compile:
|
||||||
|
|
||||||
|
6) Compile:
|
||||||
|
cd kcachegrind-0.7.4/
|
||||||
|
cd qcachegrind/
|
||||||
|
qmake -nocache
|
||||||
|
make
|
||||||
|
mv qcachegrind.app /Applications/
|
||||||
|
|
||||||
|
Done!
|
||||||
|
|
||||||
|
See also http://blogs.perl.org/users/rurban/2013/04/install-kachegrind-on-macosx-with-ports.html
|
106
material/code.py
Normal file
106
material/code.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"""
|
||||||
|
This is code that is copy-and-pasted in the slides.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_arithmetic():
|
||||||
|
assert 1 == 1
|
||||||
|
assert 2 * 3 == 6
|
||||||
|
|
||||||
|
def test_len_list():
|
||||||
|
lst = ['a', 'b', 'c']
|
||||||
|
assert len(lst) == 3
|
||||||
|
|
||||||
|
def test_various():
|
||||||
|
assert 'Hi'.islower()
|
||||||
|
assert 2 + 1 == 3
|
||||||
|
assert [2] + [1] == [2, 1]
|
||||||
|
assert 'a' + 'b' != 'ab'
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_and_2():
|
||||||
|
assert 1 + 2 == 3
|
||||||
|
|
||||||
|
def test_1_and_2_float():
|
||||||
|
assert 1.1 + 2.2 == 3.3
|
||||||
|
|
||||||
|
|
||||||
|
from math import isclose
|
||||||
|
|
||||||
|
def test_floating_point_math():
|
||||||
|
assert isclose(1.1 + 2.2, 3.3)
|
||||||
|
|
||||||
|
def test_floating_point_math2():
|
||||||
|
assert isclose(1.121, 1.2, abs_tol=1e-1)
|
||||||
|
assert isclose(1.121, 1.2, abs_tol=1e-2)
|
||||||
|
|
||||||
|
def test_floating_point_math3():
|
||||||
|
assert isclose(120.1, 121.4, rel_tol=1e-1)
|
||||||
|
assert isclose(120.4, 121.4, rel_tol=1e-2)
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
def test_numpy_equality():
|
||||||
|
x = numpy.array([1, 1])
|
||||||
|
y = numpy.array([2, 2])
|
||||||
|
z = numpy.array([3, 3])
|
||||||
|
assert x + y == z
|
||||||
|
|
||||||
|
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
class SomeException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
do_something = do_something_else = lambda : 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_raises():
|
||||||
|
with raises(SomeException):
|
||||||
|
do_something()
|
||||||
|
do_something_else()
|
||||||
|
|
||||||
|
|
||||||
|
def test_raises2():
|
||||||
|
with raises(ValueError):
|
||||||
|
int('XYZ')
|
||||||
|
|
||||||
|
|
||||||
|
def test_lower():
|
||||||
|
# Given
|
||||||
|
string = 'HeLlO wOrld'
|
||||||
|
expected = 'hello world'
|
||||||
|
|
||||||
|
# When
|
||||||
|
output = string.lower()
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert output == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_lower_empty_string():
|
||||||
|
# Given
|
||||||
|
string = ''
|
||||||
|
expected = ''
|
||||||
|
|
||||||
|
# When
|
||||||
|
output = string.lower()
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert output == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_lower1():
|
||||||
|
# Given
|
||||||
|
# Each test case is a tuple of (input, expected_result)
|
||||||
|
test_cases = [('HeLlO wOrld', 'hello world'),
|
||||||
|
('hi', 'hi'),
|
||||||
|
('123 ([?', '123 ([?'),
|
||||||
|
('', '')]
|
||||||
|
|
||||||
|
for string, expected in test_cases:
|
||||||
|
# When
|
||||||
|
output = string.lower()
|
||||||
|
# Then
|
||||||
|
assert output == expected
|
217
material/logistic_map_plots.ipynb
Normal file
217
material/logistic_map_plots.ipynb
Normal file
File diff suppressed because one or more lines are too long
BIN
profiling.pptx
Normal file
BIN
profiling.pptx
Normal file
Binary file not shown.
BIN
testing_part1.pptx
Normal file
BIN
testing_part1.pptx
Normal file
Binary file not shown.
BIN
testing_part2.pptx
Normal file
BIN
testing_part2.pptx
Normal file
Binary file not shown.
6
testing_project/LICENSE.txt
Normal file
6
testing_project/LICENSE.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
The material in this repository is released under the
|
||||||
|
CC Attribution-Share Alike 4.0 International
|
||||||
|
license.
|
||||||
|
|
||||||
|
Full license text available at
|
||||||
|
https://creativecommons.org/licenses/by-sa/4.0/
|
1
testing_project/README.md
Normal file
1
testing_project/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Testing Project
|
28
testing_project/demos/conftest_example.py
Normal file
28
testing_project/demos/conftest_example.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
# add a commandline option to pytest
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
"""Add random seed option to py.test.
|
||||||
|
"""
|
||||||
|
parser.addoption('--seed', dest='seed', type=int, action='store',
|
||||||
|
help='set random seed')
|
||||||
|
|
||||||
|
|
||||||
|
# configure pytest to automatically set the rnd seed if not passed on CLI
|
||||||
|
def pytest_configure(config):
|
||||||
|
seed = config.getvalue("seed")
|
||||||
|
# if seed was not set by the user, we set one now
|
||||||
|
if seed is None or seed == ('NO', 'DEFAULT'):
|
||||||
|
config.option.seed = int(np.random.randint(2 ** 31 - 1))
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config):
|
||||||
|
return f'Using random seed: {config.option.seed}'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def random_state(request):
|
||||||
|
random_state = np.random.RandomState(request.config.option.seed)
|
||||||
|
return random_state
|
28
testing_project/demos/test_parametrize.py
Normal file
28
testing_project/demos/test_parametrize.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_for_loop_simple():
|
||||||
|
cases = [1, 2, 3]
|
||||||
|
for a in cases:
|
||||||
|
assert a > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('a', [1, 2, 3])
|
||||||
|
def test_parametrize_simple(a):
|
||||||
|
# This test will be run 3 times, with a=1, a=2, and a=3
|
||||||
|
assert a > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_for_loop_multiple():
|
||||||
|
cases = [(1, 'hi', 'hi'), (2, 'no', 'nono')]
|
||||||
|
for a, b, expected in cases:
|
||||||
|
result = b * a
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('a, b, expected', [(1, 'hi', 'hi'), (2, 'no', 'nono')])
|
||||||
|
def test_parametrize_multiple(a, b, expected):
|
||||||
|
# This test will be run 2 times, with a=1, b='hi', expected='hi'
|
||||||
|
# and a=2, b='no', expected='nono'
|
||||||
|
result = b * a
|
||||||
|
assert result == expected
|
1
testing_project/logistic.py
Normal file
1
testing_project/logistic.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Your code goes here
|
32
testing_project/logistic_fit.py
Normal file
32
testing_project/logistic_fit.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from logistic import iterate_f
|
||||||
|
|
||||||
|
|
||||||
|
def fit_r(xs):
|
||||||
|
""" Takes a population trajectory and returns the value of r that generated it.
|
||||||
|
|
||||||
|
By far not the most efficient method, but it always finds the optimal value of r with 1/1000
|
||||||
|
precision.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
xs : list of float
|
||||||
|
A population trajectory.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
r: float
|
||||||
|
The value of r that generated the population trajectory.
|
||||||
|
"""
|
||||||
|
xs = np.asarray(xs)
|
||||||
|
x0 = xs[0]
|
||||||
|
it = len(xs) - 1
|
||||||
|
|
||||||
|
def error(r):
|
||||||
|
return np.linalg.norm(xs - iterate_f(it, x0, r))
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for r in np.linspace(0, 4, 4001):
|
||||||
|
errors.append((r, error(r)))
|
||||||
|
return min(errors, key=lambda x: x[1])[0]
|
70
testing_project/plot_logistic.py
Normal file
70
testing_project/plot_logistic.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""Usage:
|
||||||
|
```
|
||||||
|
plot_trajectory(100, 3.6, 0.1)
|
||||||
|
plot_bifurcation(2.5, 4.2, 0.001)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from logistic import iterate_f
|
||||||
|
|
||||||
|
|
||||||
|
def plot_trajectory(n, r, x0, fname="single_trajectory.png"):
|
||||||
|
"""
|
||||||
|
Saves a plot of a single trajectory of the logistic function
|
||||||
|
|
||||||
|
inputs
|
||||||
|
n: int (number of iterations)
|
||||||
|
r: float (r value for the logistic function)
|
||||||
|
x0: float (between 0 and 1, starting point for the iteration)
|
||||||
|
fname: str (filename to which to save the image)
|
||||||
|
|
||||||
|
returns
|
||||||
|
fig, ax (matplotlib objects)
|
||||||
|
"""
|
||||||
|
xs = iterate_f(n, x0, r)
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
|
ax.plot(list(range(n)), xs)
|
||||||
|
fig.suptitle('Logistic Function')
|
||||||
|
|
||||||
|
fig.savefig(fname)
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_bifurcation(start, end, step, fname="bifurcation.png", it=100000,
|
||||||
|
last=300):
|
||||||
|
"""
|
||||||
|
Saves a plot of the bifurcation diagram of the logistic function. The
|
||||||
|
`start`, `end`, and `step` parameters define for which r values to
|
||||||
|
calculate the logistic function. If you space them too closely, it might
|
||||||
|
take a very long time, if you dont plot enough, your bifurcation diagram
|
||||||
|
won't be informative. Choose wisely!
|
||||||
|
|
||||||
|
inputs
|
||||||
|
start, end, step: float (which r values to calculate the logistic
|
||||||
|
function for)
|
||||||
|
fname: str (filename to which to save the image)
|
||||||
|
it: int (how many iterations to run for each r value)
|
||||||
|
last: int (how many of the last iterates to plot)
|
||||||
|
|
||||||
|
|
||||||
|
returns
|
||||||
|
fig, ax (matplotlib objects)
|
||||||
|
"""
|
||||||
|
r_range = np.arange(start, end, step)
|
||||||
|
x = []
|
||||||
|
y = []
|
||||||
|
|
||||||
|
for r in r_range:
|
||||||
|
xs = iterate_f(it, 0.1, r)
|
||||||
|
all_xs = xs[len(xs) - last::].copy()
|
||||||
|
unique_xs = np.unique(all_xs)
|
||||||
|
y.extend(unique_xs)
|
||||||
|
x.extend(np.ones(len(unique_xs)) * r)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(20, 10))
|
||||||
|
ax.scatter(x, y, s=0.1, color='k')
|
||||||
|
ax.set_xlabel("r")
|
||||||
|
fig.savefig(fname)
|
||||||
|
return fig, ax
|
4
testing_project/pytest.ini
Normal file
4
testing_project/pytest.ini
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# pytest.ini
|
||||||
|
[pytest]
|
||||||
|
norecursedirs = *
|
||||||
|
|
28
testing_project/solution/conftest.py
Normal file
28
testing_project/solution/conftest.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
# add a commandline option to pytest
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
"""Add random seed option to py.test.
|
||||||
|
"""
|
||||||
|
parser.addoption('--seed', dest='seed', type=int, action='store',
|
||||||
|
help='set random seed')
|
||||||
|
|
||||||
|
|
||||||
|
# configure pytest to automatically set the rnd seed if not passed on CLI
|
||||||
|
def pytest_configure(config):
|
||||||
|
seed = config.getvalue("seed")
|
||||||
|
# if seed was not set by the user, we set one now
|
||||||
|
if seed is None or seed == ('NO', 'DEFAULT'):
|
||||||
|
config.option.seed = int(np.random.randint(2 ** 31 - 1))
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config):
|
||||||
|
return f'Using random seed: {config.option.seed}'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def random_state(request):
|
||||||
|
random_state = np.random.RandomState(request.config.option.seed)
|
||||||
|
return random_state
|
24
testing_project/solution/logistic.py
Normal file
24
testing_project/solution/logistic.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def f(x, r):
|
||||||
|
""" Compute the logistic map for a given value of x and r. """
|
||||||
|
return r * x * (1 - x)
|
||||||
|
|
||||||
|
|
||||||
|
def iterate_f(it, x0, r):
|
||||||
|
""" Generate a population trajectory.
|
||||||
|
|
||||||
|
Takes a number of iterations `it`, a starting value, x0,
|
||||||
|
and a parameter value for r. It executes f repeatedly (it times),
|
||||||
|
each time using the last result of f as the new input to f. Append each
|
||||||
|
iteration's result to a list l. Finally, convert the list into a numpy
|
||||||
|
array and return it.
|
||||||
|
"""
|
||||||
|
x = x0
|
||||||
|
xs = [x0]
|
||||||
|
for _ in range(it):
|
||||||
|
x = f(x, r)
|
||||||
|
xs.append(x)
|
||||||
|
|
||||||
|
return np.array(xs)
|
32
testing_project/solution/logistic_fit.py
Normal file
32
testing_project/solution/logistic_fit.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from logistic import iterate_f
|
||||||
|
|
||||||
|
|
||||||
|
def fit_r(xs):
|
||||||
|
""" Takes a population trajectory and returns the value of r that generated it.
|
||||||
|
|
||||||
|
By far not the most efficient method, but it always finds the optimal value of r with 1/1000
|
||||||
|
precision.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
xs : list of float
|
||||||
|
A population trajectory.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
r: float
|
||||||
|
The value of r that generated the population trajectory.
|
||||||
|
"""
|
||||||
|
xs = np.asarray(xs)
|
||||||
|
x0 = xs[0]
|
||||||
|
it = len(xs) - 1
|
||||||
|
|
||||||
|
def error(r):
|
||||||
|
return np.linalg.norm(xs - iterate_f(it, x0, r))
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for r in np.linspace(0, 4, 4001):
|
||||||
|
errors.append((r, error(r)))
|
||||||
|
return min(errors, key=lambda x: x[1])[0]
|
54
testing_project/solution/test_logistic.py
Normal file
54
testing_project/solution/test_logistic.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import numpy as np
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from logistic import f, iterate_f
|
||||||
|
|
||||||
|
|
||||||
|
def test_f():
|
||||||
|
# Test cases are (x, r, expected)
|
||||||
|
cases = [
|
||||||
|
(0.1, 2.2, 0.198),
|
||||||
|
(0.2, 3.4, 0.544),
|
||||||
|
(0.5, 2, 0.5),
|
||||||
|
]
|
||||||
|
for x, r, expected in cases:
|
||||||
|
result = f(x, r)
|
||||||
|
assert_allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_f_corner_cases():
|
||||||
|
# Test cases are (x, r, expected)
|
||||||
|
cases = [
|
||||||
|
(0, 1.1, 0),
|
||||||
|
(1, 3.7, 0),
|
||||||
|
]
|
||||||
|
for x, r, expected in cases:
|
||||||
|
result = f(x, r)
|
||||||
|
assert_allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_convergence():
|
||||||
|
SEED = 42
|
||||||
|
random_state = np.random.RandomState(SEED)
|
||||||
|
r = 1.5
|
||||||
|
for _ in range(100):
|
||||||
|
x0 = random_state.uniform(0.0000001, 0.9999999)
|
||||||
|
xs = iterate_f(it=100, x0=x0, r=r)
|
||||||
|
assert np.isclose(xs[-1], 1 / 3)
|
||||||
|
|
||||||
|
# SEED = 42
|
||||||
|
# @pytest.fixture
|
||||||
|
# def random_state():
|
||||||
|
# print(f"Using seed {SEED}")
|
||||||
|
# random_state = np.random.RandomState(SEED)
|
||||||
|
# return random_state
|
||||||
|
|
||||||
|
|
||||||
|
#@pytest.mark.xfail
|
||||||
|
def test_random_convergence_decorator(random_state):
|
||||||
|
r = 1.5
|
||||||
|
for _ in range(100):
|
||||||
|
x0 = random_state.uniform(0.0000001, 0.9999999)
|
||||||
|
xs = iterate_f(it=100, x0=x0, r=r)
|
||||||
|
assert np.isclose(xs[-1], 1 / 3)
|
29
testing_project/solution/test_logistic_fit.py
Normal file
29
testing_project/solution/test_logistic_fit.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import numpy as np
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
|
from logistic import iterate_f
|
||||||
|
from logistic_fit import fit_r
|
||||||
|
|
||||||
|
|
||||||
|
SEED = 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_logistic_fit():
|
||||||
|
r = 3.123
|
||||||
|
x0 = 0.322
|
||||||
|
xs = iterate_f(it=27, x0=x0, r=r)
|
||||||
|
|
||||||
|
assert_allclose(r, fit_r(xs), atol=1e-3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_logistic_fit_randomized():
|
||||||
|
random_state = np.random.RandomState(SEED)
|
||||||
|
# We test for 100 random values of x0 and r, to make sure that the function works in general.
|
||||||
|
for _ in range(100):
|
||||||
|
x0 = random_state.uniform(0.0001, 0.9999)
|
||||||
|
# Round `r` to 1/1000 to make sure that it matches the precision of the fit_r function,
|
||||||
|
# so that r can be exactly recovered.
|
||||||
|
r = round(random_state.uniform(0.001, 3.999), 3)
|
||||||
|
xs = iterate_f(it=17, x0=x0, r=r)
|
||||||
|
|
||||||
|
assert_allclose(r, fit_r(xs), atol=1e-3)
|
38
testing_project/solution/test_logistic_parametrize.py
Normal file
38
testing_project/solution/test_logistic_parametrize.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from logistic import f, iterate_f
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('x, r, expected', [
|
||||||
|
(0.1, 2.2, 0.198),
|
||||||
|
(0.2, 3.4, 0.544),
|
||||||
|
(0.5, 2, 0.5),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_f(x, r, expected):
|
||||||
|
result = f(x, r)
|
||||||
|
assert_allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('x, r, expected', [
|
||||||
|
(0, 1.1, 0),
|
||||||
|
(1, 3.7, 0),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_f_special_x_values(x, r, expected):
|
||||||
|
result = f(x, r)
|
||||||
|
assert_allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'x, r, it, expected',
|
||||||
|
[
|
||||||
|
(0.1, 2.2, 1, [0.1, 0.198]),
|
||||||
|
(0.2, 3.4, 4, [0.2, 0.544, 0.843418, 0.449019, 0.841163]),
|
||||||
|
(0.5, 2, 2, [0.5, 0.5, 0.5]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_iterate_f(x, r, it, expected):
|
||||||
|
result = iterate_f(it, x, r)
|
||||||
|
assert_allclose(result, expected, rtol=1e-5)
|
16
testing_project/test_logistic.py
Normal file
16
testing_project/test_logistic.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
|
from logistic import f
|
||||||
|
|
||||||
|
# Add here your test for the logistic map
|
||||||
|
|
||||||
|
|
||||||
|
def test_f_corner_cases():
|
||||||
|
# Test cases are (x, r, expected)
|
||||||
|
cases = [
|
||||||
|
(0, 1.1, 0),
|
||||||
|
(1, 3.7, 0),
|
||||||
|
]
|
||||||
|
for x, r, expected in cases:
|
||||||
|
result = f(x, r)
|
||||||
|
assert_allclose(result, expected)
|
Loading…
Reference in a new issue