diff --git a/firanka/series/__init__.py b/firanka/series/__init__.py index ce1102ae4ee89f9bfb4f99537480d04660c06230..63374d300700521420ba7dca1348c165c7c06385 100644 --- a/firanka/series/__init__.py +++ b/firanka/series/__init__.py @@ -4,14 +4,16 @@ import six import logging from .exceptions import OutOfRangeError, EmptyDomainError -from .range import Range -from .series import DiscreteSeries, FunctionBasedSeries +from .range import Range, REAL_SET +from .series import DiscreteSeries, FunctionBasedSeries, ModuloSeries __all__ = [ + 'REAL_SET', 'OutOfRangeError', 'EmptyDomainError', 'Range', 'FunctionBasedSeries', - 'DiscreteSeries' + 'DiscreteSeries', + 'ModuloSeries', ] diff --git a/firanka/series/range.py b/firanka/series/range.py index 2fdc599bccf80b3d779960dd3ea9306ef72749ee..dc1e4a1c110aa8d4f0db7fa3ffee4d97e1b71fb2 100644 --- a/firanka/series/range.py +++ b/firanka/series/range.py @@ -23,6 +23,9 @@ class Range(object): Range of real numbers. Immutable. """ + def translate(self, x): + return Range(self.start+x, self.stop+x, self.left_inc, self.right_inc) + def __init__(self, *args): if len(args) == 1: rs, = args @@ -76,7 +79,7 @@ class Range(object): def is_empty(self): return (self.start == self.stop) and not (self.left_inc or self.right_inc) - def __len__(self): + def length(self): return self.stop - self.start def __repr__(self): @@ -130,4 +133,5 @@ class Range(object): return hash(self.start) ^ hash(self.stop) -EMPTY_RANGE = Range(0, 0, False, False) \ No newline at end of file +EMPTY_RANGE = Range(0, 0, False, False) +REAL_SET = Range(float('-inf'), float('+inf'), False, False) diff --git a/firanka/series/series.py b/firanka/series/series.py index 142dae2ef0aef55e896ff6fb72b75aaf48c3e680..cad10115e2e5fa261e8983f3347122c97e263da3 100644 --- a/firanka/series/series.py +++ b/firanka/series/series.py @@ -5,7 +5,7 @@ import six import functools import itertools -from .range import Range +from .range import Range, REAL_SET class Series(object): @@ -38,9 +38,18 @@ class Series(object): def apply(self, series, fun): return AppliedSeries(self, series, fun) - def compute(self): - """Simplify self""" - return self + def translate(self, x): + return TranslatedSeries(self, x) + + +class TranslatedSeries(Series): + def __init__(self, series, x): + super(TranslatedSeries, self).__init__(self.domain.translate(x)) + self.series = series + self.x = x + + def _get_for(self, item): + return self.series._get_for(item+self.x) class SlicedSeries(Series): @@ -51,6 +60,7 @@ class SlicedSeries(Series): def _get_for(self, item): return self.parent._get_for(item) + class DiscreteSeries(Series): def __init__(self, data, domain=None): @@ -67,6 +77,75 @@ class DiscreteSeries(Series): raise RuntimeError('should never happen') + def translate(self, x): + return DiscreteSeries([(k+x, v) for k, v in self.data], self.domain.translate(x)) + + def apply(self, series, fun): + new_domain = self.domain.intersection(series.domain) + + if isinstance(series, DiscreteSeries): + a = self.data[::-1] + b = series.data[::-1] + + ptr = self.domain.start + c = [(ptr, fun(self._get_for(ptr), series._get_for(ptr)))] + + def appendif(lst, ptr, v): + if len(lst) > 0: + if lst[-1][0] >= ptr: + return + lst.append((ptr, v)) + + while len(a) > 0 or len(b) > 0: + if len(a) > 0 and len(b) > 0: + if a[-1] < b[-1]: + ptr, v1 = a.pop() + v2 = series._get_for(ptr) + elif a[-1] > b[-1]: + ptr, v1 = b.pop() + v2 = self._get_for(ptr) + else: + ptr, v1 = a.pop() + _, v2 = b.pop() + + assert ptr >= c[-1][0] + + appendif(c, ptr, fun(v1, v2)) + + else: + if len(a) > 0: + rest = a + static_v = series._get_for(ptr) + op = lambda me, const: fun(me, const) + else: + rest = b + static_v = self._get_for(ptr) + op = lambda me, const: fun(const, me) + + for ptr, v in rest: + appendif(c, ptr, op(v, static_v)) + + break + else: + if new_domain.start > self.data[0][0]: + c = [(new_domain.start, fun(self._get_for(new_domain.start), series._get_for(new_domain.start)))] + else: + c = [] + + for k, v in ((k, v) for k, v in self.data if new_domain.start <= k <= new_domain.stop): + newv = fun(v, series._get_for(k)) + + if len(c) > 0: + if c[-1][1] == newv: + continue + + c.append((k, newv)) + + if c[-1][0] != new_domain.stop: + c.append((new_domain.stop, fun(self._get_for(new_domain.stop), series._get_for(new_domain.stop)))) + + return DiscreteSeries(c, new_domain) + def compute(self): """Simplify self""" nd = [self.data[0]] @@ -94,54 +173,20 @@ class AppliedSeries(Series): def _get_for(self, item): return self.op(self.ser1._get_for(item), self.ser2._get_for(item)) - def compute(self): - """ - Attempt to simplify the call tree - """ - if isinstance(self.ser1, DiscreteSeries) and isinstance(self.ser2, - DiscreteSeries): - a = [p for p, q in reversed(self.ser1.data)] - b = [p for p, q in reversed(self.ser2.data)] +class ModuloSeries(Series): + def __init__(self, series): + super(ModuloSeries, self).__init__(REAL_SET) - ptr = self.domain.start - c = [(ptr, self._get_for(ptr))] + self.series = series + self.period = self.series.domain.length() - while len(a) > 0 or len(b) > 0: - if len(a) > 0 and len(b) > 0: - if a[-1] < b[-1]: - ptr = a.pop() - elif a[-1] > b[-1]: - ptr = b.pop() - else: - a.pop() - ptr = b.pop() - - assert ptr >= c[-1][0] - - if ptr > c[-1][0]: - c.append((ptr, self._get_for(ptr))) - - else: - rest = a if len(a) > 0 else b - c.extend((ptr, self._get_for(ptr)) for ptr, v in rest) - break - - return DiscreteSeries(c, self.domain) - elif isinstance(self.ser1, DiscreteSeries) or isinstance(self.ser2, - DiscreteSeries): - dis, nds = (self.ser1, self.ser2) if isinstance(self.ser1, DiscreteSeries) else (self.ser2, self.ser1) - - if dis.data[0][0] != self.domain.start: - p = [(self.domain.start, self._get_for(self.domain.start))] - else: - p = [] - - for ptr, v in dis.data: - p.append((ptr, self._get_for(ptr))) + def _get_for(self, item): + if item < 0: + item = -(item // self.period) * self.period + item + elif item > self.period: + item = item - (item // self.period) * self.period + elif item == self.period: + item = 0 - if dis.data[-1][0] != self.domain.stop: - dis.data.append((self.domain.stop, self._get_for(ptr))) + return self.series._get_for(self.series.domain.start + item) - return DiscreteSeries(p, self.domain) - else: - return self diff --git a/tests/test_series/test_range.py b/tests/test_series/test_range.py index 2bcf86757599255908c535bb87286047e63ff506..0a3c5b1d79270b9920ec2473d120479b3cf03c48 100644 --- a/tests/test_series/test_range.py +++ b/tests/test_series/test_range.py @@ -7,29 +7,26 @@ from firanka.series import Range class TestRange(unittest.TestCase): def do_intersect(self, a, b, val): if type(val) == bool: - if bool(Range(a).intersection(b)) != val: - self.fail('%s ^ %s != %s' % (Range(a), Range(b), val)) - if bool(Range(b).intersection(a)) != val: - self.fail('%s ^ %s != %s' % (Range(b), Range(a), val)) + p = Range(a).intersection(b) + if p.is_empty() != val: + self.fail('%s ^ %s [=> %s] != %s' % (Range(a), Range(b), p, val)) + p = Range(b).intersection(a) + if p.is_empty() != val: + self.fail('%s ^ %s [=> %s] != %s' % (Range(b), Range(a), p, val)) else: self.assertEqual(Range(a).intersection(b), Range(val)) self.assertEqual(Range(b).intersection(a), Range(val)) def test_isempty(self): - def tf(r, p): - s = Range(r) - self.assertEqual(s, r) - self.assertEqual(s.is_empty(), not p) - - tf(Range(-1,-1,False,False), False) - tf(Range(-1,-1,False,True), True) + self.assertTrue(Range(-1,-1,False,False).is_empty()) + self.assertFalse(Range(-1,-1,False,True).is_empty()) self.assertEqual(Range(0,0,False,False), Range(2,2,False,False)) def test_intersection(self): - self.do_intersect(Range(-10, -1, True, True), '<2;3>', False) - self.do_intersect(Range(-10, -1, True, False), '(-1;3>', False) - self.do_intersect('<-10;-1)', '<-1;1>', False) - self.do_intersect(Range(-10, -1, True, False), '(-1;3>', False) + self.do_intersect('<-10;1>', '<2;3>', True) + self.do_intersect('<10;1)', '(-1;3>', True) + self.do_intersect('<-10;-1)', '<-1;1>', True) + self.do_intersect('<-10;-1)', '(-1;3>', True) self.do_intersect('<-10;2)', '<1;5>', '<1;2)') def test_str_and_repr_and_bool(self): diff --git a/tests/test_series/test_series.py b/tests/test_series/test_series.py index 3ba6c1ecc2546783b7d8c2064e95301d101d8f4f..15a1517703857c7e7ad7ea206a26210210519c12 100644 --- a/tests/test_series/test_series.py +++ b/tests/test_series/test_series.py @@ -2,14 +2,12 @@ from __future__ import print_function, absolute_import, division import six import unittest -from firanka.series import DiscreteSeries, FunctionBasedSeries, Range +from firanka.series import DiscreteSeries, FunctionBasedSeries, Range, ModuloSeries class TestDiscreteSeries(unittest.TestCase): - def test_base(self): - s = DiscreteSeries([[0,0], [1,1], [2,2]]) self.assertEqual(s[0], 0) @@ -19,7 +17,6 @@ class TestDiscreteSeries(unittest.TestCase): self.assertRaises(ValueError, lambda: s[-1]) self.assertRaises(ValueError, lambda: s[2.5]) - s = DiscreteSeries([[0,0], [1,1], [2,2]], domain=Range(0,3,True,True)) self.assertEqual(s[0], 0) self.assertEqual(s[0.5], 0) @@ -28,6 +25,12 @@ class TestDiscreteSeries(unittest.TestCase): self.assertRaises(ValueError, lambda: s[-1]) self.assertEqual(s[2.5], 2) + def test_translation(self): + s = DiscreteSeries([[0,0], [1,1], [2,2]]).translate(3) + + self.assertEqual(s[3], 0) + self.assertEqual(s[3.5], 0) + self.assertEqual(s[4], 1) def test_slice(self): series = DiscreteSeries([[0, 0], [1, 1], [2, 2]]) @@ -46,22 +49,31 @@ class TestDiscreteSeries(unittest.TestCase): sb = DiscreteSeries([[0, 1], [1, 2], [2, 3]]) sc = sa.apply(sb, lambda a, b: a+b) - sd = sc.compute() + self.assertIsInstance(sc, DiscreteSeries) self.assertEqual(sc.eval_points([0,1,2]), [1,3,5]) - self.assertEqual(sd.eval_points([0,1,2]), sd.eval_points([0,1,2])) - - self.assertEqual(sd.data, [(0,1),(1,3),(2,5)]) + self.assertEqual(sc.data, [(0,1),(1,3),(2,5)]) def test_eval2(self): sa = DiscreteSeries([[0, 0], [1, 1], [2, 2]]) sb = FunctionBasedSeries(lambda x: x, '<0;2)') sc = sa.apply(sb, lambda a, b: a+b) - sd = sc.compute() self.assertEqual(sc.eval_points([0,1,2]), [0,2,4]) - self.assertEqual(sd.eval_points([0,1,2]), sd.eval_points([0,1,2])) - self.assertEqual(sd.data, [(0,0),(1,2),(2,4)]) + self.assertIsInstance(sc, DiscreteSeries) + self.assertEqual(sc.data, [(0,0),(1,2),(2,4)]) + + def test_eval3(self): + sa = FunctionBasedSeries(lambda x: x**2, '<-10;10)') + sb = FunctionBasedSeries(lambda x: x, '<0;2)') + + sc = sa.apply(sb, lambda a, b: a*b) + + PTS = [0,1,1.9] + EPTS = [x*x**2 for x in PTS] + + self.assertEqual(sc.eval_points(PTS), EPTS) + self.assertTrue(Range('<0;2)') in sc.domain) class TestFunctionBasedSeries(unittest.TestCase): @@ -76,3 +88,19 @@ class TestFunctionBasedSeries(unittest.TestCase): self.assertRaises(ValueError, lambda: sp[2]) self.assertEqual(sp.domain.start, 0.5) self.assertEqual(sp.domain.stop, 1.5) + +class TestModuloSeries(unittest.TestCase): + def test_base(self): + series = ModuloSeries(DiscreteSeries([(0,1),(1,2),(2,3)], '<0;3)')) + + self.assertEquals(series[3], 1) + self.assertEquals(series[4], 2) + self.assertEquals(series[5], 3) + self.assertEquals(series[-1], 3) + + def test_comp_discrete(self): + ser1 = ModuloSeries(FunctionBasedSeries(lambda x: x**2, '<0;3)')) + ser2 = FunctionBasedSeries(lambda x: x, '<0;3)') + + ser3 = ser1.apply(ser2, lambda x, y: x*y) +