From 4a0e987825ef56e241aa6371f99002d30bfca420 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 3 Nov 2017 17:09:57 +0100 Subject: [PATCH 1/7] add a PermissiveMultiples validator --- qcodes/utils/validators.py | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 395cf6555f4..f5d5afad68b 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 @@ -308,6 +312,43 @@ def __repr__(self): return super().__repr__()[:-1] + ', Multiples of {}>'.format(self._divisor) +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. + + Args: + divisor: The number that the validated value should be an integer + multiple of. + precision: The maximally allowed value for + abs(abs((value % divisor)/divisor) - 1) + """ + + def __init__(self, divisor: Union[float, int, np.floating], + precision: float=1e-5) -> 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) + else: + self._mulval = None + + def validate(self, value: Union[float, int, np.floating]) -> None: + self._numval.validate(value) + if self._mulval and isinstance(value, int): + self._mulval(value) + else: + remainder = abs(abs((value % self.divisor)/self.divisor) - 1) + if remainder > self.precision: + raise ValueError('{} is not (close to being) '.format(value) + + 'a multiple of {}.'.format(self.divisor)) + + class MultiType(Validator): """ allow the union of several different validators From 339991efd53f79a580cc1a5ab244c9204bff4084 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Mon, 6 Nov 2017 10:17:40 +0100 Subject: [PATCH 2/7] Add a new validator for non-integer multiples + a test --- qcodes/tests/test_validators.py | 34 ++++++++++++++++++++++++++++++++- qcodes/utils/validators.py | 30 ++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/qcodes/tests/test_validators.py b/qcodes/tests/test_validators.py index 0639a8af3f4..3c3d663214c 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,38 @@ 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): + print(div, mult) + val.validate(mult) + + # finally, a quick test that the precision is indeed setable + + class TestMultiType(TestCase): def test_good(self): diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index f5d5afad68b..1e7a13f6ffd 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -318,15 +318,18 @@ class PermissiveMultiples(Validator): 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 value for - abs(abs((value % divisor)/divisor) - 1) + 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-5) -> None: + precision: float=1e-9) -> None: if divisor == 0: raise ValueError('Can not meaningfully check for multiples of' ' zero.') @@ -334,19 +337,28 @@ def __init__(self, divisor: Union[float, int, np.floating], self.precision = precision self._numval = Numbers() if isinstance(divisor, int): - self._mulval = Multiples(divisor) + self._mulval = Multiples(divisor=abs(divisor)) else: self._mulval = None def validate(self, value: Union[float, int, np.floating]) -> None: self._numval.validate(value) + # if zero, it passes by definition + if value == 0: + return if self._mulval and isinstance(value, int): - self._mulval(value) + self._mulval.validate(abs(value)) else: - remainder = abs(abs((value % self.divisor)/self.divisor) - 1) - if remainder > self.precision: - raise ValueError('{} is not (close to being) '.format(value) + - 'a multiple of {}.'.format(self.divisor)) + # 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)) class MultiType(Validator): From b2a53642e8121551a2683e2fa8ce5e7f97629bda Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Mon, 6 Nov 2017 10:39:58 +0100 Subject: [PATCH 3/7] Match call signatures --- qcodes/utils/validators.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 1e7a13f6ffd..06a15d8cf8a 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -341,7 +341,12 @@ def __init__(self, divisor: Union[float, int, np.floating], else: self._mulval = None - def validate(self, value: Union[float, int, np.floating]) -> 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: From 739623d54715be50505a8154b91945c4d574d0e0 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Mon, 6 Nov 2017 10:46:30 +0100 Subject: [PATCH 4/7] Now actually match them :) --- qcodes/utils/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 06a15d8cf8a..828250ba19c 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -342,7 +342,7 @@ def __init__(self, divisor: Union[float, int, np.floating], self._mulval = None def validate(self, value: Union[float, int, np.floating], - context: str) -> None: + context: str='') -> None: """ Validate the given value. Note that this validator does not use context for anything. From 004387c2894ea7534cc6c6a07676da4ad179cdc4 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Mon, 6 Nov 2017 10:54:42 +0100 Subject: [PATCH 5/7] Make Multiples and PermissiveMultiples numeric --- qcodes/utils/validators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 828250ba19c..b931842b575 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -311,6 +311,8 @@ def validate(self, value, context=''): def __repr__(self): return super().__repr__()[:-1] + ', Multiples of {}>'.format(self._divisor) + is_numeric = True + class PermissiveMultiples(Validator): """ @@ -365,6 +367,8 @@ def validate(self, value: Union[float, int, np.floating], raise ValueError('{} is not a multiple'.format(value) + ' of {}.'.format(self.divisor)) + is_numeric = True + class MultiType(Validator): """ From 3543dcf2a523de5f573f9fd272b05471126abfbe Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Tue, 7 Nov 2017 09:56:59 +0100 Subject: [PATCH 6/7] add the promised precision test --- qcodes/tests/test_validators.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qcodes/tests/test_validators.py b/qcodes/tests/test_validators.py index 3c3d663214c..5f30e8885bf 100644 --- a/qcodes/tests/test_validators.py +++ b/qcodes/tests/test_validators.py @@ -477,10 +477,15 @@ def test_not_passing(self): val = PermissiveMultiples(div) for mult in self.not_multiples[divind]: with self.assertRaises(ValueError): - print(div, mult) 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): From cffd0967aeddebf33c26452d2579a98103aa9b94 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Tue, 7 Nov 2017 10:02:27 +0100 Subject: [PATCH 7/7] add a __repr__ of PermissiveMultiples --- qcodes/utils/validators.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index b931842b575..bd482f877c2 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -367,6 +367,11 @@ def validate(self, value: Union[float, int, np.floating], 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