Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validators/permissive multiples #852

Merged
merged 10 commits into from Nov 7, 2017
34 changes: 33 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,38 @@ 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):
print(div, mult)
val.validate(mult)

# finally, a quick test that the precision is indeed setable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops... nice catch.



class TestMultiType(TestCase):

def test_good(self):
Expand Down
70 changes: 66 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,64 @@ 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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a relative tolerance too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mimicking np.allclose? I am a bit undecided. I think for most cases it will suffice to say "be precise to within 0.5 nV".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes as long as people are not using "strange" units its fine, we can always add later if needed

raise ValueError('{} is not a multiple'.format(value) +
' of {}.'.format(self.divisor))

is_numeric = True


class MultiType(Validator):
"""
Expand Down