Skip to content

Commit 411be25

Browse files
committed
Add module needed for last commit.
1 parent 6f41434 commit 411be25

1 file changed

Lines changed: 300 additions & 0 deletions

File tree

accumulator.py

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
#!/usr/bin/env python3
2+
3+
from functools import reduce
4+
import unittest
5+
6+
import numpy as np
7+
8+
class Reduction:
9+
10+
"""Operation that reduces an array to a scalar.
11+
12+
Methods:
13+
__init__
14+
__call__
15+
has_ltr
16+
"""
17+
18+
def __init__(self, array_op=None, ltr_op=None, ltr_start=None,
19+
ltr_finalize=None):
20+
"""Initialize a reduction from input functions.
21+
22+
Arguments:
23+
array_op - A function that does a reduction given an array. If this
24+
function is specified, it defines the reduction completely.
25+
Otherwise, ltr_op must be specified.
26+
ltr_op - A function that takes two arguments, and produces an output
27+
representing their combination. This function is used to
28+
reduce the array if array_op is not present.
29+
ltr_start - If present, ltr_start is the first argument of ltr_op the
30+
first time it is called, while the first element of the
31+
array is the second argument. Otherwise, the first two
32+
elements of the array are used as the arguments.
33+
ltr_finalize - If present, this function is called on the result of the
34+
final ltr_op call. Otherwise, the result is returned
35+
unchanged.
36+
"""
37+
self.ltr_op = ltr_op
38+
self.ltr_start = ltr_start
39+
self.ltr_finalize = ltr_finalize
40+
if array_op is None:
41+
assert ltr_op is not None, \
42+
"Cannot create a Reduction with no input function."
43+
if ltr_finalize is None:
44+
ltr_finalize = lambda x: x
45+
if ltr_start is None:
46+
self.reduce = lambda arr: ltr_finalize(reduce(ltr_op, arr))
47+
else:
48+
self.reduce = lambda arr: ltr_finalize(reduce(ltr_op, arr, ltr_start))
49+
else:
50+
self.reduce = array_op
51+
52+
def __call__(self, array):
53+
"""Reduce an array."""
54+
return self.reduce(array)
55+
56+
def has_ltr(self):
57+
"""Whether or not this reduction uses an ltr_op."""
58+
return self.ltr_op is not None
59+
60+
# Save builtin sum for testing purposes.
61+
_sum = sum
62+
63+
sum = Reduction(ltr_op=lambda x,y: x+y,
64+
ltr_start=0.)
65+
66+
mean = Reduction(ltr_op=lambda x,y:(x[0]+y,x[1]+1),
67+
ltr_start=(0.,0),
68+
ltr_finalize=lambda x: x[0]/x[1])
69+
"""Reduction that takes the mean of an array."""
70+
71+
def _median_func(array):
72+
length = len(array)
73+
sorted_array = sorted(array)
74+
if length % 2 == 0:
75+
return 0.5 * (sorted_array[length//2 - 1] + sorted_array[length//2])
76+
else:
77+
return sorted_array[length//2]
78+
79+
median = Reduction(array_op=_median_func)
80+
"""Reduction that takes the median of an array."""
81+
82+
max = Reduction(ltr_op=max)
83+
"""Reduction that takes the max of an array."""
84+
85+
min = Reduction(ltr_op=min)
86+
"""Reduction that takes the min of an array."""
87+
88+
def percentile(percent):
89+
"""Function that produces a Reduction finding a given percentile.
90+
91+
Note that percentile(0.) is equivalent to min, percentile(100.) is
92+
equivalent to max, and percentile(50.) is equivalent to median, up to
93+
rounding error.
94+
"""
95+
if percent == 100.:
96+
return max
97+
frac = percent*0.01
98+
def array_op(array):
99+
ngaps = len(array)-1
100+
weight, index = np.modf(frac*ngaps)
101+
index = int(index)
102+
sort_array = sorted(array)
103+
return sort_array[index] + weight*(sort_array[index+1]-sort_array[index])
104+
return Reduction(array_op=array_op)
105+
106+
107+
class Accumulator:
108+
109+
"""Object that accumulates a set of values being reduced.
110+
111+
Methods:
112+
__init__
113+
push
114+
output
115+
merge
116+
merge_series
117+
"""
118+
119+
def __init__(self, reductions):
120+
"""Create an Accumulator from a set of reductions."""
121+
self._reductions = reductions
122+
self._series = []
123+
124+
def push(self, value):
125+
"""Add a new value to the series being accumulated."""
126+
self._series.append(value)
127+
128+
def output(self):
129+
"""Output the accumulated value."""
130+
return [red(self._series) for red in self._reductions]
131+
132+
def merge(self, other):
133+
"""Merge another accumulator into this one."""
134+
self._series += other._series
135+
136+
def merge_series(self, series):
137+
"""Add a whole to this accumulator."""
138+
self._series += series
139+
140+
141+
class TestReductions(unittest.TestCase):
142+
143+
@staticmethod
144+
def closest_to_mean(array):
145+
mean = _sum(array) / len(array)
146+
closest = array[0]
147+
for x in array[1:]:
148+
if abs(x - mean) < abs(closest - mean):
149+
closest = x
150+
return closest
151+
152+
def test_array_op(self):
153+
close_reduce = Reduction(array_op=self.closest_to_mean)
154+
self.assertEqual(close_reduce.reduce([0,1,2.1,3,4]), 2.1)
155+
156+
def test_call_interface(self):
157+
close_reduce = Reduction(array_op=self.closest_to_mean)
158+
self.assertEqual(close_reduce([0,4,2.1,3,1]), 2.1)
159+
160+
def test_ltr(self):
161+
sum_reduce = Reduction(ltr_op=lambda x,y: x+y)
162+
self.assertEqual(sum_reduce.ltr_op(1.1, 2.5), 3.6)
163+
164+
def test_ltr_reduce(self):
165+
sum_reduce = Reduction(ltr_op=lambda x,y: x+y)
166+
self.assertEqual(sum_reduce([1.1, 2.5, 6.2]), 9.8)
167+
168+
def test_no_argument_error(self):
169+
with self.assertRaises(AssertionError):
170+
Reduction()
171+
172+
def test_ltr_start(self):
173+
sum_len_reduce = Reduction(ltr_op=lambda x,y:(x[0]+y,x[1]+1),
174+
ltr_start=(0,0))
175+
total, length = sum_len_reduce.ltr_op(sum_len_reduce.ltr_start, 4.)
176+
self.assertEqual(total, 4.)
177+
self.assertEqual(length, 1)
178+
179+
def test_ltr_start_reduce(self):
180+
sum_len_reduce = Reduction(ltr_op=lambda x,y:(x[0]+y,x[1]+1),
181+
ltr_start=(0.,0))
182+
self.assertEqual(sum_len_reduce([1.1, 2.5, 6.2]), (9.8, 3))
183+
184+
def test_ltr_finalize(self):
185+
sum_squared_reduce = Reduction(ltr_op=lambda x,y: x+y,
186+
ltr_finalize=lambda x: x*x)
187+
x = sum_squared_reduce.ltr_finalize(sum_squared_reduce.ltr_op(2., 3.))
188+
self.assertEqual(x, 25.)
189+
190+
def test_ltr_finalize_reduce(self):
191+
sum_squared_reduce = Reduction(ltr_op=lambda x,y: x+y,
192+
ltr_finalize=lambda x: x*x)
193+
x = sum_squared_reduce([2., 3., 4.])
194+
self.assertEqual(x, 81.)
195+
196+
def test_ltr_start_finalize_reduce(self):
197+
mean_reduce = Reduction(ltr_op=lambda x,y:(x[0]+y,x[1]+1),
198+
ltr_start=(0.,0),
199+
ltr_finalize=lambda x: x[0]/x[1])
200+
x = mean_reduce([2., 3., 4., 6., 12., 0., 1.])
201+
self.assertEqual(x, 4.)
202+
203+
def test_has_ltr(self):
204+
close_reduce = Reduction(array_op=self.closest_to_mean)
205+
sum_reduce = Reduction(ltr_op=lambda x,y: x+y)
206+
self.assertFalse(close_reduce.has_ltr())
207+
self.assertTrue(sum_reduce.has_ltr())
208+
209+
def test_mean(self):
210+
x = mean([2., 3., 4., 6., 12., 0., 1.])
211+
self.assertTrue(mean.has_ltr())
212+
self.assertEqual(x, 4.)
213+
214+
def test_median(self):
215+
array = [2., 3., 4., 6., 12., 0., 1.]
216+
x = median(array)
217+
x2 = median(array[1:])
218+
self.assertEqual(x, 3.)
219+
self.assertEqual(x2, 3.5)
220+
221+
def test_max(self):
222+
array = [2., 3., 4., 6., 12., 0., 1.]
223+
self.assertTrue(max.has_ltr())
224+
x = max(array)
225+
self.assertEqual(x, 12.)
226+
227+
def test_min(self):
228+
array = [2., 3., 4., 6., 12., 0., 1.]
229+
self.assertTrue(min.has_ltr())
230+
x = min(array)
231+
self.assertEqual(x, 0.)
232+
233+
def test_sum(self):
234+
array = [2., 3., 4., 6., 12., 0., 1.]
235+
self.assertTrue(min.has_ltr())
236+
x = sum(array)
237+
self.assertEqual(x, 28.)
238+
239+
def test_sum_empty(self):
240+
x = sum([])
241+
self.assertEqual(x, 0.)
242+
243+
def test_percentile(self):
244+
array = [20., 3., 4., 6., 12., 40., 25.]
245+
test_percent = 80.
246+
self.assertAlmostEqual(percentile(test_percent)(array), 24.)
247+
248+
def test_percentile_0(self):
249+
array = [20., 3., 4., 6., 12., 40., 25.]
250+
test_percent = 0.
251+
self.assertEqual(percentile(test_percent)(array), 3.)
252+
253+
def test_percentile_100(self):
254+
array = [20., 3., 4., 6., 12., 40., 25.]
255+
test_percent = 100.
256+
self.assertEqual(percentile(test_percent)(array), 40.)
257+
258+
259+
class TestAccumulator(unittest.TestCase):
260+
261+
def test_accumulator_array(self):
262+
median_acc = Accumulator([median])
263+
array = [2., 3., 4., 6., 12., 0., 1.]
264+
for num in array:
265+
median_acc.push(num)
266+
self.assertEqual(median_acc.output()[0], 3.)
267+
268+
def test_accumulator_multi_array(self):
269+
median_acc = Accumulator([median, percentile(100./3.)])
270+
array = [2., 3., 4., 6., 12., 0., 1.]
271+
for num in array:
272+
median_acc.push(num)
273+
self.assertEqual(median_acc.output()[0], 3.)
274+
self.assertAlmostEqual(median_acc.output()[1], 2.)
275+
276+
def test_accumulator_merge(self):
277+
median_acc = Accumulator([median, percentile(100./3.)])
278+
median2_acc = Accumulator([median, percentile(100./3.)])
279+
array = [2., 3., 4., 6., 12., 0., 1.]
280+
for i in range(len(array)):
281+
if i < 3:
282+
median_acc.push(array[i])
283+
else:
284+
median2_acc.push(array[i])
285+
median_acc.merge(median2_acc)
286+
self.assertEqual(median_acc.output()[0], 3.)
287+
self.assertAlmostEqual(median_acc.output()[1], 2.)
288+
289+
def test_accumulator_merge_series(self):
290+
median_acc = Accumulator([median, percentile(100./3.)])
291+
series = []
292+
array = [2., 3., 4., 6., 12., 0., 1.]
293+
for i in range(len(array)):
294+
if i < 3:
295+
median_acc.push(array[i])
296+
else:
297+
series.append(array[i])
298+
median_acc.merge_series(series)
299+
self.assertEqual(median_acc.output()[0], 3.)
300+
self.assertAlmostEqual(median_acc.output()[1], 2.)

0 commit comments

Comments
 (0)