Skip to content

Commit

Permalink
make truncate() static, so it can be easily accessed, e.g, by Checkpo…
Browse files Browse the repository at this point in the history
…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
artgoldberg committed Dec 11, 2019
1 parent 0660c0c commit 49102a3
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 106 deletions.
38 changes: 1 addition & 37 deletions tests/util/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
:License: MIT
"""

from math import pi
import six
import unittest

from wc_utils.util.misc import (most_qual_cls_name, round_direct, OrderableNone, quote, isclass,
isclass_by_name, obj_to_str, as_dict, internet_connected,
DFSMAcceptor, UniformSequence)
isclass_by_name, obj_to_str, as_dict, internet_connected, DFSMAcceptor)
from wc_utils.util.stats import ExponentialMovingAverage


Expand Down Expand Up @@ -205,37 +203,3 @@ def test_dfsm_acceptor(self):

with self.assertRaisesRegex(ValueError, 'no transitions available from start state'):
DFSMAcceptor('s', 'e', [('f', 'm1', 0), ('e', 'm1', 'f')])


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)),
((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(pi, 1)
with self.assertRaisesRegex(StopIteration, "UniformSequence: truncation error"):
for i in range(100):
next_value = us.__next__()

us = UniformSequence(pi, 1)
with self.assertRaisesRegex(StopIteration, "UniformSequence: truncation error"):
us.truncate(us.__next__())
48 changes: 48 additions & 0 deletions tests/util/test_uniform_seq.py
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__())
2 changes: 2 additions & 0 deletions wc_utils/config/core.default.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
seed = 139
[[github]]
github_api_token =
[[misc]]
uniform_seq_precision = 6
4 changes: 4 additions & 0 deletions wc_utils/config/core.schema.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@
[[github]]
github_api_token = string(default=None)
# authentication token for GitHub

[[misc]]
uniform_seq_precision = integer(default=6)
# number of digits of precision of truncated values in a UniformSequence
69 changes: 0 additions & 69 deletions wc_utils/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,72 +290,3 @@ def run(self, transition_messages):
if self.state == self.accepting_state:
return DFSMAcceptor.ACCEPT
return DFSMAcceptor.FAIL


class UniformSequence(collections.abc.Iterator):
""" Generate an infinite uniform sequence, especially for non-integral step sizes
Uses a :obj:`Fraction` to represent the step size as a ratio of integers.
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
_digits_precision (:obj:`int`): number of digits of precision; the sequence terminates if a step
cannot be represented with `_digits_precision` digits
"""

def __init__(self, start, step, digits_precision=6):
""" Initialize a :obj:`UniformSequence`
Args:
start (:obj:`float`): starting point of the sequence
step (:obj:`float`): step size for the sequence
digits_precision (:obj:`int`): number of digits of precision; the sequence terminates
if a step cannot be represented with `_digits_precision` digits
"""
self._start = start
MAX_DENOMINATOR = 1000000
self._fraction_step = Fraction(step).limit_denominator(max_denominator=MAX_DENOMINATOR)
if step * self._fraction_step.denominator != self._fraction_step.numerator:
raise ValueError(f"UniformSequence: step {step} can't be a fraction with "
f"denominator <= {MAX_DENOMINATOR}")
self._digits_precision = digits_precision
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'_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 truncated
self.truncate(next_value)
self._num_steps += 1
return next_value

def truncate(self, value):
""" Truncate a 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:.{self._digits_precision}f}'
if float(truncated_value) != value:
raise StopIteration(f'UniformSequence: truncation error:\n'
f'value: {value}; truncated_value: {truncated_value} '
f'_fraction_step.denominator: {self._fraction_step.denominator}')
return truncated_value
80 changes: 80 additions & 0 deletions wc_utils/util/uniform_seq.py
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

0 comments on commit 49102a3

Please sign in to comment.