diff --git a/qcodes/tests/test_validators.py b/qcodes/tests/test_validators.py index 0639a8af3f4..5f30e8885bf 100644 --- a/qcodes/tests/test_validators.py +++ b/qcodes/tests/test_validators.py @@ -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) @@ -451,6 +451,43 @@ def test_divisors(self): repr(n), '') +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): diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 395cf6555f4..bd482f877c2 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -1,4 +1,6 @@ import math +from typing import Union + import numpy as np BIGSTRING = 1000000000 @@ -217,6 +219,7 @@ def __repr__(self): maxv = self._max_value if self._max_value < BIGINT else None return ''.format(range_str(minv, maxv, 'v')) + class PermissiveInts(Ints): """ requires an integer or a float close to an integer @@ -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 @@ -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 = (''.format(self.divisor, self.precision)) + return repr + + is_numeric = True + class MultiType(Validator): """