Skip to content

Commit b13cfa0

Browse files
Merge pull request #166 from chris-greening/add-noise-feature
Implementing add noise method
2 parents c75bb1f + 5881c6b commit b13cfa0

4 files changed

Lines changed: 85 additions & 21 deletions

File tree

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setuptools.setup(
99
name="spyrograph",
10-
version="0.28.1",
10+
version="0.29.0",
1111
author="Chris Greening",
1212
author_email="[email protected]",
1313
description="Library for drawing spirographs in Python",

spyrograph/core/_cycloid.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ def __init__(
1717
self, R: Number, r: Number, thetas: List[Number] = None,
1818
theta_start: Number = None, theta_stop: Number = None,
1919
theta_step: Number = None, origin: Tuple[Number, Number] = (0, 0),
20-
orientation: Number = 0
20+
orientation: Number = 0, noise: Number = None
2121
) -> None:
22-
super().__init__(R, r, r, thetas, theta_start, theta_stop, theta_step, origin, orientation)
22+
super().__init__(R, r, r, thetas, theta_start, theta_stop, theta_step, origin, orientation, noise)
2323
# pylint: disable=pointless-string-statement
2424
"""Instantiate a cycloid curve from given input parameters. A
2525
hypocycloid is a curve drawn by tracing a point from a circle as it

spyrograph/core/_trochoid.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(
3434
self, R: Number, r: Number, d: Number, thetas: List[Number] = None,
3535
theta_start: Number = None, theta_stop: Number = None,
3636
theta_step: Number = None, origin: Tuple[Number, Number] = (0, 0),
37-
orientation: Number = 0
37+
orientation: Number = 0, noise: List["np.array"] = None
3838
) -> None:
3939
"""Model of a trochoid curve from given input parameters. A trochoid is
4040
a curve drawn by tracing a point from a circle as it rolls around the
@@ -76,6 +76,7 @@ def __init__(
7676
self.thetas = _validate_theta(thetas, theta_start, theta_stop, theta_step)
7777
self.origin = origin
7878
self.orientation = orientation
79+
self.noise = noise
7980

8081
self._validate_inputs()
8182
self._calculate_path()
@@ -109,15 +110,17 @@ def translate(self, x: Number = 0, y: Number = 0) -> "_Trochoid":
109110
d=self.d,
110111
thetas=self.thetas,
111112
origin=(self.origin[0]+x, self.origin[1]+y),
112-
orientation=self.orientation
113+
orientation=self.orientation,
114+
noise=self.noise
113115
)
114116
except TypeError:
115117
translated_shape = self.__class__(
116118
R=self.R,
117119
r=self.r,
118120
thetas=self.thetas,
119121
origin=(self.origin[0]+x, self.origin[1]+y),
120-
orientation=self.orientation
122+
orientation=self.orientation,
123+
noise=self.noise
121124
)
122125
return translated_shape
123126

@@ -157,15 +160,18 @@ def scale(self, factor: Number) -> Union["_Trochoid", "_Cycloid"]:
157160
d=self.d*factor,
158161
thetas=self.thetas,
159162
origin=self.origin,
160-
orientation=self.orientation
163+
orientation=self.orientation,
164+
noise=[self.noise[0]*factor, self.noise[1]*factor]
161165
)
162166
except TypeError:
163167
scaled_shape = self.__class__(
164168
R=self.R*factor,
165169
r=self.r*factor,
166170
thetas=self.thetas,
167171
origin=self.origin,
168-
orientation=self.orientation
172+
orientation=self.orientation,
173+
noise=[self.noise[0]*factor, self.noise[1]*factor]
174+
169175
)
170176
return scaled_shape
171177

@@ -205,18 +211,45 @@ def rotate(self, angle: float, degrees: bool = False):
205211
d=self.d,
206212
thetas=self.thetas,
207213
origin=self.origin,
208-
orientation=self.orientation + angle
214+
orientation=self.orientation + angle,
215+
noise=self.noise
209216
)
210217
except TypeError:
211218
rotated_shape = self.__class__(
212219
R=self.R,
213220
r=self.r,
214221
thetas=self.thetas,
215222
origin=self.origin,
216-
orientation=self.orientation + angle
223+
orientation=self.orientation + angle,
224+
noise=self.noise
217225
)
218226
return rotated_shape
219227

228+
def add_noise(self, x_scale: Number = 0, y_scale: Number = 0) -> Union["_Trochoid", "_Cycloid"]:
229+
x_noise = np.random.normal(0, x_scale, size=len(self.x))
230+
y_noise = np.random.normal(0, y_scale, size=len(self.y))
231+
noise = [x_noise, y_noise]
232+
try:
233+
noisy_shape = self.__class__(
234+
R=self.R,
235+
r=self.r,
236+
d=self.d,
237+
thetas=self.thetas,
238+
origin=self.origin,
239+
orientation=self.orientation,
240+
noise=noise
241+
)
242+
except TypeError:
243+
noisy_shape = self.__class__(
244+
R=self.R,
245+
r=self.r,
246+
thetas=self.thetas,
247+
origin=self.origin,
248+
orientation=self.orientation,
249+
noise=noise
250+
)
251+
return noisy_shape
252+
220253
def plot(self, **kwargs) -> Tuple["matplotlib.matplotlib.Figure", "matplotlib.axes._axes.Axes"]:
221254
"""
222255
Plot the shape and return the associated matplotlib Figure and Axes objects.
@@ -642,12 +675,19 @@ def _calculate_path(self) -> None:
642675
self.x, self.y = _apply_rotation(self.x, self.y, self.orientation)
643676
self.x += self.origin[0]
644677
self.y += self.origin[1]
678+
if self.noise is None:
679+
self.noise = [
680+
np.zeros(len(self.x)),
681+
np.zeros(len(self.y))
682+
]
683+
# self.noise = _apply_rotation(self.noise[0], self.noise[1], self.orientation)
684+
self.x += self.noise[0]
685+
self.y += self.noise[1]
645686
self.min_x = min(self.x)
646687
self.max_x = max(self.x)
647688
self.min_y = min(self.y)
648689
self.max_y = max(self.y)
649-
650-
self.coords = list(zip(self.x, self.y, self.thetas))
690+
self.coords = list(zip(self.x + self.noise[0], self.y+self.noise[1], self.thetas))
651691

652692
def _validate_inputs(self) -> None:
653693
"""Validate input parameters"""
@@ -796,7 +836,7 @@ def _draw_circle(
796836
y: The y-coordinate of the center of the circle.
797837
radius: The radius of the circle to be drawn.
798838
799-
The circle drawn, uses steps = 200 that defines the smoothness of the circle.
839+
The circle drawn, uses steps = 200 that defines the smoothness of the circle.
800840
"""
801841
t.up()
802842
t.seth(0)

tests/_trochoid.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ def test_dunder_repr_values(self, instance):
7474
assert f"r={instance.r}" in repr_val
7575
assert f"d={instance.d}" in repr_val
7676

77+
def test_add_noise_return_instance_is_same_class(self, instance):
78+
"""Test that the return instance is from the same class"""
79+
noisy_instance = instance.add_noise(x_scale=2, y_scale=2)
80+
assert noisy_instance.__class__ is instance.__class__
81+
7782
def test_scale_return_instance_is_same_class(self, instance):
7883
"""Test that the return instance is from the same class"""
7984
scaled_instance = instance.scale(factor=2)
@@ -101,43 +106,62 @@ def test_scale_factor_parameters_smaller(self, instance):
101106
assert smaller_scaled_instance.d == instance.d*.5
102107
assert (smaller_scaled_instance.thetas == instance.thetas).all()
103108

109+
def test_add_noise_origin_is_preserved(self, thetas):
110+
if issubclass(self.class_name, _Cycloid):
111+
shape = self.class_name(
112+
R=100,
113+
r=200,
114+
thetas=thetas,
115+
origin=(100,100)
116+
)
117+
elif issubclass(self.class_name, _Trochoid):
118+
shape = self.class_name(
119+
R=100,
120+
r=200,
121+
d=300,
122+
thetas=thetas,
123+
origin=(100, 100)
124+
)
125+
noisy_shape = shape.add_noise(x_scale=10, y_scale=10)
126+
assert noisy_shape.origin == shape.origin
127+
104128
def test_scale_origin_is_preserved(self, thetas):
105129
if issubclass(self.class_name, _Cycloid):
106-
translated_shape = self.class_name(
130+
shape = self.class_name(
107131
R=100,
108132
r=200,
109133
thetas=thetas,
110134
origin=(100,100)
111135
)
112136
elif issubclass(self.class_name, _Trochoid):
113-
translated_shape = self.class_name(
137+
shape = self.class_name(
114138
R=100,
115139
r=200,
116140
d=300,
117141
thetas=thetas,
118142
origin=(100, 100)
119143
)
120-
scaled_shape = translated_shape.scale(2)
121-
assert scaled_shape.origin == translated_shape.origin
144+
scaled_shape = shape.scale(2)
145+
assert scaled_shape.origin == shape.origin
122146

123147
def test_rotate_origin_is_preserved(self, thetas):
124148
if issubclass(self.class_name, _Cycloid):
125-
translated_shape = self.class_name(
149+
shape = self.class_name(
126150
R=100,
127151
r=200,
128152
thetas=thetas,
129153
origin=(100, 100)
130154
)
131155
elif issubclass(self.class_name, _Trochoid):
132-
translated_shape = self.class_name(
156+
shape = self.class_name(
133157
R=100,
134158
r=200,
135159
d=300,
136160
thetas=thetas,
137161
origin=(100, 100)
138162
)
139-
rotated_shape = translated_shape.rotate(2)
140-
assert rotated_shape.origin == rotated_shape.origin
163+
rotated_shape = shape.rotate(2)
164+
assert rotated_shape.origin == shape.origin
141165

142166
def test_create_range_theta_inputs(self, thetas):
143167
R = 5

0 commit comments

Comments
 (0)