Skip to content

Commit

Permalink
Validators/permissive multiples (#852)
Browse files Browse the repository at this point in the history
* add a PermissiveMultiples validator

* Add a new validator for non-integer multiples + a test

* Match call signatures

* Now actually match them :)

* Make Multiples and PermissiveMultiples numeric

* add the promised precision test

* add a __repr__ of PermissiveMultiples
  • Loading branch information
WilliamHPNielsen committed Nov 7, 2017
1 parent 86b0322 commit 8b258ad
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 5 deletions.
39 changes: 38 additions & 1 deletion qcodes/tests/test_validators.py
Expand Up @@ -4,7 +4,7 @@

from qcodes.utils.validators import (Validator, Anything, Bool, Strings,
Numbers, Ints, PermissiveInts,
Enum, MultiType,
Enum, MultiType, PermissiveMultiples,
Arrays, Multiples, Lists, Callable, Dict)


Expand Down Expand Up @@ -451,6 +451,43 @@ def test_divisors(self):
repr(n), '<Ints 1<=v<=10, Multiples of 3>')


class TestPermissiveMultiples(TestCase):
divisors = [40e-9, -1, 0.2225, 1/3, np.pi/2]

multiples = [[800e-9, -40e-9, 0, 1],
[3, -4, 0, -1, 1],
[1.5575, -167.9875, 0],
[2/3, 3, 1, 0, -5/3, 1e4],
[np.pi, 5*np.pi/2, 0, -np.pi/2]]

not_multiples = [[801e-9, 4.002e-5],
[1.5, 0.9999999],
[0.2226],
[0.6667, 28/9],
[3*np.pi/4]]

def test_passing(self):
for divind, div in enumerate(self.divisors):
val = PermissiveMultiples(div)
for mult in self.multiples[divind]:
val.validate(mult)

def test_not_passing(self):
for divind, div in enumerate(self.divisors):
val = PermissiveMultiples(div)
for mult in self.not_multiples[divind]:
with self.assertRaises(ValueError):
val.validate(mult)

# finally, a quick test that the precision is indeed setable
def test_precision(self):
pm_lax = PermissiveMultiples(35e-9, precision=3e-9)
pm_lax.validate(72e-9)
pm_strict = PermissiveMultiples(35e-9, precision=1e-10)
with self.assertRaises(ValueError):
pm_strict.validate(70.2e-9)


class TestMultiType(TestCase):

def test_good(self):
Expand Down
75 changes: 71 additions & 4 deletions qcodes/utils/validators.py
@@ -1,4 +1,6 @@
import math
from typing import Union

import numpy as np

BIGSTRING = 1000000000
Expand Down Expand Up @@ -217,6 +219,7 @@ def __repr__(self):
maxv = self._max_value if self._max_value < BIGINT else None
return '<Ints{}>'.format(range_str(minv, maxv, 'v'))


class PermissiveInts(Ints):
"""
requires an integer or a float close to an integer
Expand Down Expand Up @@ -280,10 +283,11 @@ def validate(self, value, context=''):

class Multiples(Ints):
"""
A validator that checks if a value is an integer multiple of a fixed devisor
This class extends validators.Ints such that the value is also checked for
being integer between an optional min_value and max_value. Furthermore this
validator checks that the value is an integer multiple of an fixed, integer
A validator that checks if a value is an integer multiple of a
fixed divisor. This class extends validators.Ints such that the
value is also checked for being integer between an optional
min_value and max_value. Furthermore this validator checks that
the value is an integer multiple of an fixed, integer
divisor. (i.e. value % divisor == 0)
Args:
divisor (integer), the value need the be a multiple of this divisor
Expand All @@ -307,6 +311,69 @@ def validate(self, value, context=''):
def __repr__(self):
return super().__repr__()[:-1] + ', Multiples of {}>'.format(self._divisor)

is_numeric = True


class PermissiveMultiples(Validator):
"""
A validator that checks whether a value is an integer multiple
of a fixed divisor (to within some precision). If both value and
divisor are integers, the (exact) Multiples validator is used.
We also allow negative values, meaning that zero by construction is
always a valid value.
Args:
divisor: The number that the validated value should be an integer
multiple of.
precision: The maximally allowed absolute error between the value and
the nearest true multiple
"""

def __init__(self, divisor: Union[float, int, np.floating],
precision: float=1e-9) -> None:
if divisor == 0:
raise ValueError('Can not meaningfully check for multiples of'
' zero.')
self.divisor = divisor
self.precision = precision
self._numval = Numbers()
if isinstance(divisor, int):
self._mulval = Multiples(divisor=abs(divisor))
else:
self._mulval = None

def validate(self, value: Union[float, int, np.floating],
context: str='') -> None:
"""
Validate the given value. Note that this validator does not use
context for anything.
"""
self._numval.validate(value)
# if zero, it passes by definition
if value == 0:
return
if self._mulval and isinstance(value, int):
self._mulval.validate(abs(value))
else:
# floating-point division cannot be trusted, so we try to
# multiply our way out of the problem by constructing true
# multiples in the relevant range and see if `value` is one
# of them (within rounding errors)
divs = int(divmod(value, self.divisor)[0])
true_vals = np.array([n*self.divisor for n in range(divs, divs+2)])
abs_errs = [abs(tv-value) for tv in true_vals]
if min(abs_errs) > self.precision:
raise ValueError('{} is not a multiple'.format(value) +
' of {}.'.format(self.divisor))

def __repr__(self):
repr = ('<PermissiveMultiples, Multiples of '
'{} to within {}>'.format(self.divisor, self.precision))
return repr

is_numeric = True


class MultiType(Validator):
"""
Expand Down

0 comments on commit 8b258ad

Please sign in to comment.