-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
make truncate() static, so it can be easily accessed, e.g, by Checkpo…
…int(); fully test UniformSequence; move number of digits of precision of truncated values in a UniformSequence to cfg files; move UniformSequence to separate module to avoid circular import
- Loading branch information
1 parent
0660c0c
commit 49102a3
Showing
6 changed files
with
135 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
""" Test uniform sequence | ||
:Author: Arthur Goldberg <Arthur.Goldberg@mssm.edu> | ||
:Date: 2019-12-11 | ||
:Copyright: 2019, Karr Lab | ||
:License: MIT | ||
""" | ||
|
||
from math import pi | ||
import unittest | ||
import sys | ||
|
||
from wc_utils.util.uniform_seq import UniformSequence | ||
|
||
|
||
class TestUniformSequence(unittest.TestCase): | ||
|
||
def test_uniform_sequence(self): | ||
initial_values = [((0, 1), (0, 1, 2, 3)), | ||
((2, 1), (2, 3)), | ||
((0, -1), (0, -1, -2, -3)), | ||
((0, .1), (0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1.)), | ||
((0, .3), (0, .3, .6, .9, 1.2)), | ||
# an example from Guido van Rossum: http://code.activestate.com/recipes/577068/ | ||
((0, .7), (0, .7, 1.4, 2.1)), | ||
] | ||
for args, expected_seq in initial_values: | ||
start, period = args | ||
us = UniformSequence(start, period) | ||
for expected in expected_seq: | ||
next = us.__next__() | ||
self.assertEqual(next, expected) | ||
self.assertEqual(float(us.truncate(next)), next) | ||
|
||
us = UniformSequence(0, 1) | ||
self.assertEqual(us.__iter__(), us) | ||
|
||
with self.assertRaisesRegex(ValueError, "UniformSequence: step .* can't be a fraction"): | ||
UniformSequence(0, pi) | ||
|
||
us = UniformSequence(sys.float_info.max, 2) | ||
with self.assertRaisesRegex(StopIteration, "UniformSequence: floating-point rounding error:"): | ||
us.__next__() | ||
us.__next__() | ||
|
||
us = UniformSequence(pi, 1) | ||
with self.assertRaisesRegex(StopIteration, "UniformSequence: truncation error"): | ||
us.truncate(us.__next__()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,5 @@ | |
seed = 139 | ||
[[github]] | ||
github_api_token = | ||
[[misc]] | ||
uniform_seq_precision = 6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
""" Generate an infinite sequence of evenly spaced values | ||
:Author: Arthur Goldberg <Arthur.Goldberg@mssm.edu> | ||
:Date: 2019-12-11 | ||
:Copyright: 2019, Karr Lab | ||
:License: MIT | ||
""" | ||
|
||
from fractions import Fraction | ||
import collections | ||
|
||
from wc_utils.config.core import get_config | ||
uniform_seq_precision = get_config()['wc_utils']['misc']['uniform_seq_precision'] | ||
|
||
|
||
class UniformSequence(collections.abc.Iterator): | ||
""" Generate an infinite sequence of evenly spaced values, especially for non-integral step sizes | ||
Avoids floating-point roundoff errors by using a :obj:`Fraction` to represent the step size as | ||
a ratio of integers, and raising exceptions when errors occur. | ||
Attributes: | ||
_start (:obj:`float`): starting point of the sequence | ||
_fraction_step (:obj:`Fraction`): step size for the sequence | ||
_num_steps (:obj:`int`): number of steps taken in the sequence | ||
""" | ||
MAX_DENOMINATOR = 1_000_000 | ||
|
||
def __init__(self, start, step): | ||
""" Initialize a :obj:`UniformSequence` | ||
Args: | ||
start (:obj:`float`): starting point of the sequence | ||
step (:obj:`float`): step size for the sequence | ||
""" | ||
self._start = start | ||
self._fraction_step = Fraction(step).limit_denominator(max_denominator=self.MAX_DENOMINATOR) | ||
if step * self._fraction_step.denominator != self._fraction_step.numerator: | ||
raise ValueError(f"UniformSequence: step {step} can't be a fraction of integers " | ||
f"denominator <= {self.MAX_DENOMINATOR}") | ||
self._num_steps = 0 | ||
|
||
def __iter__(self): | ||
return self | ||
|
||
def __next__(self): | ||
""" Get next value in the sequence | ||
Raises: | ||
:obj:`StopIteration`: if the next value encounters a floating-point rounding error | ||
""" | ||
next_value = self._start + \ | ||
(self._num_steps * self._fraction_step.numerator) / self._fraction_step.denominator | ||
if (next_value - self._start) * self._fraction_step.denominator != \ | ||
self._num_steps * self._fraction_step.numerator: | ||
raise StopIteration(f'UniformSequence: floating-point rounding error:\n' | ||
f'_start: {self._start}; ' | ||
f'_num_steps: {self._num_steps}; ' | ||
f'_fraction_step.numerator: {self._fraction_step.numerator}; ' | ||
f'_fraction_step.denominator: {self._fraction_step.denominator}') | ||
# ensure that next_value can be safely truncated | ||
self.truncate(next_value) | ||
self._num_steps += 1 | ||
return next_value | ||
|
||
@staticmethod | ||
def truncate(value): | ||
""" Truncate a uniform sequence value into fixed-point notation for output | ||
Raise an exception if truncation loses precision. | ||
Raises: | ||
:obj:`StopIteration`: if the truncated value does not equal `value` | ||
""" | ||
truncated_value = f'{value:.{uniform_seq_precision}f}' | ||
if float(truncated_value) != value: | ||
raise StopIteration(f'UniformSequence: truncation error:\n' | ||
f'value: {value}; truncated_value: {truncated_value} ' | ||
f'num digits precision: {uniform_seq_precision}; ') | ||
return truncated_value |