Skip to content

Commit

Permalink
Add unit tests (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaslink authored Nov 12, 2023
2 parents d268801 + 56091f8 commit 7fb23dd
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 72 deletions.
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[run]
branch = True

omit =
frequenpy/version.py
frequenpy/constants.py
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
__pycache__
test*.py
dist
build
*egg-info*

.venv
workdir
.coverage
37 changes: 21 additions & 16 deletions frequenpy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import argparse

from frequenpy.loaded_string.animation import LoadedStringAnimation, DEFAULT_SPEED
from frequenpy.loaded_string.animation import LoadedStringAnimation

from frequenpy.constants import APP_DESCRIPTION, APP_NAME

Expand Down Expand Up @@ -31,6 +31,8 @@
EXAMPLE_LS = "frequenpy loaded_string --masses 3 --modes 1 2 3 --speed 0.1 --boundary 0"
EPILOG_LS = "Example: {}".format(EXAMPLE_LS)

OUTPUT_FOLDER = 'workdir'


def setup_logger(verbose=False):
logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO, format=LOG_FORMAT)
Expand All @@ -50,44 +52,47 @@ def add_loaded_string_parser(subparsers):
o = p.add_argument_group('optional arguments')
o.add_argument('--modes', type=int, default=[1], metavar='', nargs='+', help=HELP_LS_MODES)
o.add_argument('--boundary', type=int, default=0, help=HELP_LS_BOUNDARY)
o.add_argument('--speed', type=float, default=DEFAULT_SPEED, help=HELP_LS_SPEED)
o.add_argument('--speed', type=float, default=1, help=HELP_LS_SPEED)
o.add_argument('--save', action='store_true', help=HELP_LS_SAVE)


def parse_args(parser, subparsers):
def parse_args(args):
help_arg = '--help'
if len(sys.argv) < 2:
if len(args) == 0:
return [help_arg]

if len(sys.argv) == 2:
system = sys.argv[1]
if len(args) == 1:
system = args[0]
if system not in AVAILABLE_SYSTEMS:
raise ValueError(f'System {system} is not a valid option.')

return [system, help_arg]

return sys.argv[1:]

return args

def main():
parser = argparse.ArgumentParser(prog=APP_NAME, description=GREETING, epilog=APP_EPILOG)
subparsers = parser.add_subparsers(dest='system', help='Choose a system to simulate')
add_loaded_string_parser(subparsers)

def run(args, folder=OUTPUT_FOLDER):
setup_logger()

try:
args = parser.parse_args(args=parse_args(parser, subparsers))
parser = argparse.ArgumentParser(prog=APP_NAME, description=GREETING, epilog=APP_EPILOG)
subparsers = parser.add_subparsers(dest='system', help='Choose a system to simulate')
add_loaded_string_parser(subparsers)

args = parser.parse_args(args=parse_args(args))

if args.system == BEADED_STRING:
animation = LoadedStringAnimation.build(
args.masses, args.modes, args.boundary, args.speed)
args.masses, args.modes, args.boundary, speed=args.speed, folder=folder)

animation.animate(save=args.save)
animation.start(save=args.save)

except ValueError as e:
logger.error(e)


def main():
run(sys.argv[1:])


if __name__ == '__main__':
main()
110 changes: 57 additions & 53 deletions frequenpy/loaded_string/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,73 @@
MARGIN_RIGHT = 0.95
MARGIN_TOP = 0.95

ANIMATIONS_FOLDER = 'workdir'
OUTPUT_FOLDER = 'workdir'
FILENAME_TPL = "{}masses_{}modes.mp4"

NUMBER_OF_FRAMES = 2000
DEFAULT_SPEED = 1
N_FRAMES = 2000
SPEED = 1


class LoadedStringAnimation(object):
def __init__(self, loaded_string, number_of_frames, speed=DEFAULT_SPEED):
class LoadedStringAnimation:
def __init__(self, loaded_string, n_frames=N_FRAMES, speed=SPEED, folder=OUTPUT_FOLDER):
self._loaded_string = loaded_string
self._number_of_frames = number_of_frames
self._n_frames = n_frames
self._speed = speed
self._folder = folder

self._line = self._build_line()
self._figure = self._build_figure()
self._frames = self._build_frames()

def animate(self, save=False):
def start(self, save=False):
anim = self._build_animation()

if save:
self._save(anim)
return self._save(anim)

plt.show()

def build(N, modes, boundary, speed):
@classmethod
def build(cls, N, modes, boundary, **kwargs):
loaded_string = loaded_string_factory.create(N, modes, boundary)
return LoadedStringAnimation(loaded_string, NUMBER_OF_FRAMES, speed)
return cls(loaded_string, **kwargs)

def _build_line(self):
if self._loaded_string.N == self._loaded_string.CONTINUOUS_LIMIT:
return self._build_line_without_markers()
else:
return self._build_line_with_markers()

def _build_line_with_markers(self):
X, Y = self._loaded_string.rest_position

return plt.Line2D(
X, Y,
marker=LINE_MARKERTYPE,
lw=LINE_WIDTH,
markersize=LINE_MARKERSIZE,
markerfacecolor=LINE_MARKERFACECOLOR,
color=LINE_COLOR,
markevery=slice(1, len(X) - 1, 1)
)

def _build_line_without_markers(self):
X, Y = self._loaded_string.rest_position

return plt.Line2D(
X, Y,
lw=LINE_WIDTH,
color=LINE_COLOR
)

def _build_frames(self):
self._loaded_string.apply_speed(self._speed)
frames = range(0, self._n_frames)

return [
self._loaded_string.position_at_time_t(t)
for t in frames
]

def _build_figure(self):
x_rest_position, _ = self._loaded_string.rest_position
Expand Down Expand Up @@ -99,43 +138,6 @@ def _left_support(self, x_distance_from_origin):
def _right_support(self, x_distance_from_origin):
return self._support(x_distance_from_origin)

def _build_line(self):
if self._loaded_string.N == self._loaded_string.CONTINUOUS_LIMIT:
return self._build_line_without_markers()
else:
return self._build_line_with_markers()

def _build_line_with_markers(self):
X, Y = self._loaded_string.rest_position

return plt.Line2D(
X, Y,
marker=LINE_MARKERTYPE,
lw=LINE_WIDTH,
markersize=LINE_MARKERSIZE,
markerfacecolor=LINE_MARKERFACECOLOR,
color=LINE_COLOR,
markevery=slice(1, len(X) - 1, 1)
)

def _build_line_without_markers(self):
X, Y = self._loaded_string.rest_position

return plt.Line2D(
X, Y,
lw=LINE_WIDTH,
color=LINE_COLOR
)

def _build_frames(self):
self._loaded_string.apply_speed(self._speed)
frames = range(0, self._number_of_frames)

return [
self._loaded_string.position_at_time_t(t)
for t in frames
]

def _update(self, frame_number):
self._line.set_data(self._frames[frame_number])

Expand All @@ -145,20 +147,22 @@ def _build_animation(self):
return animation.FuncAnimation(
self._figure,
self._update,
frames=self._number_of_frames,
frames=self._n_frames,
interval=5,
blit=True,
repeat=True)

def _save(self, animation):
logger.info('Saving animation...this could take a while...')

self._create_directory(ANIMATIONS_FOLDER)
self._create_folder(self._folder)
filename = FILENAME_TPL.format(self._loaded_string.N, self._loaded_string.modes)
filepath = path.join(ANIMATIONS_FOLDER, filename)
filepath = path.join(self._folder, filename)

animation.save(filepath, savefig_kwargs={'facecolor': BACKGROUND_COLOR})

def _create_directory(self, directory):
if not path.exists(directory):
makedirs(directory)
return filepath

def _create_folder(self, folder):
if not path.exists(folder):
makedirs(folder)
5 changes: 4 additions & 1 deletion frequenpy/loaded_string/loaded_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def __init__(self, N, modes):

self.rest_position = self._get_rest_position()

def __len__(self):
return self.N

def position_at_time_t(self, t):
X, _ = self.rest_position
Y = self._y_position_at_time_t(t)
Expand Down Expand Up @@ -70,7 +73,7 @@ def _standing_wave_equation(self, A, k, n, a, phi, omega, t, theta):

@abstractmethod
def _wavenumber(self, p):
pass
pass # pragma: nocover


class LoadedStringFixed(LoadedString):
Expand Down
2 changes: 1 addition & 1 deletion frequenpy/loaded_string/loaded_string_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def create(N, modes, boundary):
if boundary == BOUNDARY_FREE:
return LoadedStringFree(N, modes)
else:
raise NotImplementedError(
raise ValueError(
"{} is not a valid boundary condition".format(boundary)
)

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
flake8<7
pytest-cov<5

-e .
18 changes: 18 additions & 0 deletions tests/loaded_string/test_animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os

from matplotlib import pyplot

from frequenpy.loaded_string.animation import LoadedStringAnimation


def test_animation(monkeypatch, tmp_path):
anim = LoadedStringAnimation.build(N=5, modes=[1, 2], boundary=0, n_frames=1, folder=tmp_path)
monkeypatch.setattr(pyplot, "show", lambda *x, **y: 0)
anim.start()

filepath = anim.start(save=True)
assert os.path.exists(filepath)

folder = os.path.join(tmp_path, 'new')
anim = LoadedStringAnimation.build(N=30, modes=[1], boundary=0, n_frames=1, folder=folder)
anim.start(save=True)
40 changes: 40 additions & 0 deletions tests/loaded_string/test_loaded_string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
import numpy as np

from frequenpy.loaded_string import loaded_string_factory


def test_loaded_string_factory():

loaded_string_factory.create(N=5, modes=[1, 2], boundary=0)
loaded_string_factory.create(N=5, modes=[1, 2], boundary=1)
loaded_string_factory.create(N=5, modes=[1, 2], boundary=2)

with pytest.raises(ValueError):
loaded_string_factory.create(N=5, modes=[1, 2], boundary=3)

with pytest.raises(ValueError):
loaded_string_factory.create(N=7, modes=[1, 2], boundary=0)

with pytest.raises(ValueError):
loaded_string_factory.create(N=2, modes=[1, 2, 3], boundary=0)

with pytest.raises(ValueError):
loaded_string_factory.create(N=2, modes=[1, 3], boundary=0)

with pytest.raises(ValueError):
loaded_string_factory.create(N=2, modes=[0, 2], boundary=0)


def test_loaded_string():
ls = loaded_string_factory.create(N=5, modes=[1, 2], boundary=0)

assert len(ls) == 5
assert ls.modes == '1-2'

ls.position_at_time_t(1)

ls.apply_speed(2)

_, Y = ls.rest_position
assert np.all(Y == 0)
26 changes: 26 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pytest

from frequenpy import cli
from frequenpy.loaded_string.animation import LoadedStringAnimation


class AnimationMock:
def start(self, *args, **kwargs):
pass


def test_cli(monkeypatch):
monkeypatch.setattr(LoadedStringAnimation, 'build', lambda *args, **kwargs: AnimationMock())

cli.run([
'loaded_string', '--masses', '5', '--modes', '1', '2', '--speed', '0.1', '--boundary', '0'
])

with pytest.raises(SystemExit):
cli.run([])

with pytest.raises(ValueError):
cli.parse_args(['invalid'])

with pytest.raises(AssertionError):
cli.run(['loaded_string'])

0 comments on commit 7fb23dd

Please sign in to comment.