Repository: microsoft/Qcodes

Issue:
Multiples validator only works for Ints
The `Multiples(Ints)` validator checks if a give value is a multiple of some other value (the `divisor`). 

This validator only works for `Ints` and there is no reason for this. 

A perfectly valid example would be requiring for values to be multiples of 20ns. 

The current value would then be valid `40e-9` as this would return 0.0. Currently this will raise an error as 40e-9 (which is a multiple of 20e-9) raises an error as it is not an Int. I propose dropping the constraint of it having to be an Int. 


Discussion:
Thats sensible enough but we will need to be careful about the implementation 
as floating point math cannot be assumed to be distributive. For example 

```python
In [1]: divisor = (1/3)*1e-10
In [2]: print(((1/3)*1e-9)%divisor)
3.333333333333331e-11
```

Resolving patch:
diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py
--- 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 '<Ints{}>'.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 = ('<PermissiveMultiples, Multiples of '
+                '{} to within {}>'.format(self.divisor, self.precision))
+        return repr
+
+    is_numeric = True
+
 
 class MultiType(Validator):
     """


Test patch:
diff --git a/qcodes/tests/test_validators.py b/qcodes/tests/test_validators.py
--- 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), '<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):