diff --git a/README.md b/README.md index c71f28c4f4960f2d978553ea75db83d72c65acf0..f14d95ab6f4e36755c758d87e98bdd6e8f9a7a10 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,17 @@ By definition, _ModuloSeries_ has the domain of all real numbers. Note that someOtherSeries's domain length must be non-zero and finite. Otherwise _ValueError_ will be thrown. +## LinearInterpolationSeries + +These are discretes, but allow you to define an operator that will +take its neighbours into account and let you return a custom value. + +By default, it will assumes that values can be added, subbed, multed and dived, +and will do classical linear interpolation. + +They can either utilize an existing discrete series, or be created just as +any other discrete series would be. + ## Ranges Can be imported from _sai.ranges_. @@ -111,9 +122,7 @@ You can create Ranges as follows: Range(-5, 5, True, False) == Range('<-5;5)') ``` -First boolean argument signifies whether the interval is left-closed, -and second whether it is right-closed. - +For more information [use the source](firanka/ranges.py#L33) Range's are immutable and hashable. They can be sliced: ```python @@ -134,3 +143,8 @@ Or you can check for strict inclusion Range('<-1;1>') in Range('<-2;2>') ``` +## TimeProviders + +**EXPERIMENTAL** + +Can be imported from _sai.timeproviders_. diff --git a/firanka/ranges.py b/firanka/ranges.py index a2b2ae17e7812aab1dc9aa98975cdacbf60a2d0b..3e06c1e886ef5b47446ea54a9f15618c18a93f22 100644 --- a/firanka/ranges.py +++ b/firanka/ranges.py @@ -1,9 +1,11 @@ # coding=UTF-8 from __future__ import print_function, absolute_import, division -import six + import functools import math +import six + __all__ = [ 'Range', 'REAL_SET', @@ -31,6 +33,16 @@ class Range(object): self.right_inc) def __init__(self, *args): + """ + Create like: + + * Range('<a;b>') + * Range(a, b, is_left_closed_, is_right_closed) + * Range(a, b) - will have both sides closed, unless one is inf + * Range(slice(a, b)) - will have both sides closed, unless one is None + + :param args: + """ if len(args) == 1: rs, = args if isinstance(rs, type(self)): @@ -48,6 +60,10 @@ class Range(object): start, stop = rs[1:-1].split(';') args = float(start), float(stop), rs[0] == '<', rs[-1] == '>' + elif len(args) == 2: + args = args[0], args[1], not math.isinf(args[0]), not math.isinf( + args[1]) + q = lambda a, b, args: args[a] and math.isinf(args[b]) if q(2, 0, args) or q(3, 1, args): @@ -65,7 +81,7 @@ class Range(object): if isinstance(x, Range): if ((x.start == self.start) and (x.left_inc ^ self.left_inc)) \ or ((x.stop == self.stop) and ( - x.right_inc ^ self.right_inc)): + x.right_inc ^ self.right_inc)): return False return (x.start >= self.start) and (x.stop <= self.stop) @@ -80,15 +96,15 @@ class Range(object): def is_empty(self): return (self.start == self.stop) and not ( - self.left_inc or self.right_inc) + self.left_inc or self.right_inc) def length(self): return self.stop - self.start def __repr__(self): return 'Range(%s, %s, %s, %s)' % ( - repr(self.start), repr(self.stop), repr(self.left_inc), - repr(self.right_inc)) + repr(self.start), repr(self.stop), repr(self.left_inc), + repr(self.right_inc)) def __getitem__(self, item): if not isinstance(item, slice): diff --git a/firanka/series/__init__.py b/firanka/series/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f73801128dd02f06c00af1719db8c1623be0a171 --- /dev/null +++ b/firanka/series/__init__.py @@ -0,0 +1,16 @@ +# coding=UTF-8 +from __future__ import absolute_import + +from .base import FunctionSeries, DiscreteSeries, Series +from .interpolations import LinearInterpolationSeries, \ + SCALAR_LINEAR_INTERPOLATOR +from .modulo import ModuloSeries + +__all__ = [ + 'FunctionSeries', + 'DiscreteSeries', + 'ModuloSeries', + 'Series', + 'SCALAR_LINEAR_INTERPOLATOR', + 'LinearInterpolationSeries', +] diff --git a/firanka/series.py b/firanka/series/base.py similarity index 88% rename from firanka/series.py rename to firanka/series/base.py index c1bb0cc140d1d7c6f4210b38ce31a9e983c32a5f..6182ce09a2b5197069655a9ef284cd3b957407c4 100644 --- a/firanka/series.py +++ b/firanka/series/base.py @@ -1,19 +1,10 @@ # coding=UTF-8 from __future__ import print_function, absolute_import, division -import math - import six from firanka.exceptions import NotInDomainError -from firanka.ranges import Range, REAL_SET, EMPTY_SET - -__all__ = [ - 'FunctionSeries', - 'DiscreteSeries', - 'ModuloSeries', - 'Series', -] +from firanka.ranges import Range, EMPTY_SET class Series(object): @@ -286,31 +277,3 @@ class JoinedSeries(Series): def _get_for(self, item): return self.op(self.ser1._get_for(item), self.ser2._get_for(item)) - - -class ModuloSeries(Series): - def __init__(self, series, *args, **kwargs): - """ - Construct a modulo series - :param series: base series to use - :raise ValueError: invalid domain length - """ - super(ModuloSeries, self).__init__(REAL_SET, *args, **kwargs) - - self.series = series - self.period = self.series.domain.length() - - if self.period == 0: - raise ValueError('Modulo series cannot have a period of 0') - elif math.isinf(self.period): - raise ValueError('Modulo series cannot have an infinite period') - - 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 - - return self.series._get_for(self.series.domain.start + item) diff --git a/firanka/series/interpolations.py b/firanka/series/interpolations.py new file mode 100644 index 0000000000000000000000000000000000000000..178c0ff901855be5a9240244d8c71d5dbda3ff75 --- /dev/null +++ b/firanka/series/interpolations.py @@ -0,0 +1,49 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import, division + +import six + +from .base import DiscreteSeries, Series + + +def SCALAR_LINEAR_INTERPOLATOR(t0, v0, t1, v1, tt): + """ + Good intepolator if our values can be added, subtracted, multiplied and divided + """ + return v0 + (tt - t0) * (t1 - t0) / (v1 - v0) + + +class LinearInterpolationSeries(DiscreteSeries): + def __init__(self, data, domain=None, + interpolator=SCALAR_LINEAR_INTERPOLATOR, + *args, **kwargs): + """ + :param interpolator: callable(t0: float, v0: any, t1: float, v1: any, tt: float) -> any + This, given intepolation points (t0, v0) and (t1, v1) such that t0 <= tt <= t1, + return a value for index tt + :raise TypeError: a non-discrete series was passed as data + """ + self.interpolator = interpolator + if isinstance(data, DiscreteSeries): + data, domain = data.data, data.domain + elif isinstance(data, Series): + raise TypeError('non-discrete series not supported!') + + super(LinearInterpolationSeries, self).__init__(data, domain, *args, + **kwargs) + + def _get_for(self, item): + if item == self.domain.start: + return self.data[0][1] + + if len(self.data) == 1: + return super(LinearInterpolationSeries, self).__getitem__(item) + + for i in six.moves.range(0, len(self.data) - 1): + cur_i, cur_v = self.data[i] + next_i, next_v = self.data[i + 1] + + if cur_i <= item <= next_i: + return self.interpolator(cur_i, cur_v, next_i, next_v, item) + + return self.data[-1][1] diff --git a/firanka/series/modulo.py b/firanka/series/modulo.py new file mode 100644 index 0000000000000000000000000000000000000000..ed728086214a840ea39286448302e1994d4afd06 --- /dev/null +++ b/firanka/series/modulo.py @@ -0,0 +1,35 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import, division + +import math + +from .base import Series +from ..ranges import REAL_SET + + +class ModuloSeries(Series): + def __init__(self, series, *args, **kwargs): + """ + Construct a modulo series + :param series: base series to use + :raise ValueError: invalid domain length + """ + super(ModuloSeries, self).__init__(REAL_SET, *args, **kwargs) + + self.series = series + self.period = self.series.domain.length() + + if self.period == 0: + raise ValueError('Modulo series cannot have a period of 0') + elif math.isinf(self.period): + raise ValueError('Modulo series cannot have an infinite period') + + 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 + + return self.series._get_for(self.series.domain.start + item) diff --git a/firanka/timeproviders.py b/firanka/timeproviders.py index 2cca5f445727763f15fe355218f8806290311fb8..47dbd0cfe6507a6d0b5cb47e7dc66d2b5441516b 100644 --- a/firanka/timeproviders.py +++ b/firanka/timeproviders.py @@ -1,10 +1,8 @@ # coding=UTF-8 from __future__ import print_function, absolute_import, division -import six -import logging -from .series import Series from .ranges import Range +from .series import Series class BijectionMapping(object): diff --git a/tests/test_range.py b/tests/test_range.py index 5463f9001db84be63bf273a431c9a343b048493a..c8bc9353b508d85f803932cbaf644565ff33a667 100644 --- a/tests/test_range.py +++ b/tests/test_range.py @@ -1,6 +1,8 @@ # coding=UTF-8 from __future__ import print_function, absolute_import, division + import unittest + from firanka.ranges import Range @@ -38,7 +40,7 @@ class TestRange(unittest.TestCase): def test_str_and_repr_and_bool(self): p = Range(-1, 1, True, True) self.assertEqual(eval(repr(p)), p) - self.assertEqual(str(Range(-1, 1, True, True)), '<-1;1>') + self.assertEqual(str(Range(-1, 1)), '<-1;1>') def test_constructor(self): self.assertRaises(ValueError, lambda: Range('#2;3>')) diff --git a/tests/test_series.py b/tests/test_series.py index 8b8a0221fd20a967034bdcc9458c60ad4bd5fe12..94d73bc27346f091801e279cb44aca457b0d6840 100644 --- a/tests/test_series.py +++ b/tests/test_series.py @@ -1,11 +1,13 @@ # coding=UTF-8 from __future__ import print_function, absolute_import, division -import six + import math import unittest -from firanka.series import DiscreteSeries, FunctionSeries, ModuloSeries -from firanka.ranges import Range + from firanka.exceptions import NotInDomainError +from firanka.ranges import Range +from firanka.series import DiscreteSeries, FunctionSeries, ModuloSeries, \ + LinearInterpolationSeries NOOP = lambda x: x @@ -174,3 +176,18 @@ class TestModuloSeries(unittest.TestCase): ser2 = FunctionSeries(NOOP, '<0;3)') ser3 = ser1.join(ser2, lambda x, y: x * y) + + +class TestLinearInterpolation(unittest.TestCase): + def test_lin(self): + series = LinearInterpolationSeries( + DiscreteSeries([(0, 1), (1, 2), (2, 3)], '<0;3)')) + + self.assertEqual(series[0], 1) + self.assertEqual(series[0.5], 1.5) + self.assertEqual(series[1], 2) + self.assertEqual(series[2.3], 3) + + def test_conf(self): + self.assertRaises(TypeError, lambda: LinearInterpolationSeries( + FunctionSeries(NOOP, '<0;3)'))) diff --git a/tests/test_timeproviders.py b/tests/test_timeproviders.py index d6d0d78c56f0aec503e4eab6090eb045d964e10e..87bc595feb1fa122891fa837b6a5095613599536 100644 --- a/tests/test_timeproviders.py +++ b/tests/test_timeproviders.py @@ -1,6 +1,6 @@ # coding=UTF-8 from __future__ import print_function, absolute_import, division -import six + import unittest from firanka.series import DiscreteSeries