Skip to content

Commit

Permalink
Merge pull request #476 from qutech/issues/475_time_type_precision
Browse files Browse the repository at this point in the history
Convert duration with full precision
  • Loading branch information
terrorfisch committed Oct 22, 2019
2 parents e9e8fcf + 3e3cb08 commit 4dc1092
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 84 deletions.
3 changes: 3 additions & 0 deletions ReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## pending/current ##

- General:
- Improve `TimeType` consistency by leveraging str(float) for rounding by default.

- Hardware:
- Add a `measure_program` method to the DAC interface. This method is used by the QCoDeS integration.
- Add a `set_measurement_mask` to DAC interface. This method is used by the QCoDeS integration.
Expand Down
4 changes: 2 additions & 2 deletions qupulse/_program/waveforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def compare_key(self) -> Any:

@property
def duration(self) -> TimeType:
return time_from_float(self._table[-1].t)
return TimeType.from_float(self._table[-1].t)

def unsafe_sample(self,
channel: ChannelID,
Expand Down Expand Up @@ -245,7 +245,7 @@ def __init__(self, expression: ExpressionScalar,
raise ValueError('FunctionWaveforms may not depend on anything but "t"')

self._expression = expression
self._duration = time_from_float(duration)
self._duration = TimeType.from_float(duration)
self._channel_id = channel

@property
Expand Down
5 changes: 3 additions & 2 deletions qupulse/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from qupulse.serialization import AnonymousSerializable
from qupulse.utils.sympy import sympify, to_numpy, recursive_substitution, evaluate_lambdified,\
get_most_simple_representation, get_variables
from qupulse.utils.types import TimeType

__all__ = ["Expression", "ExpressionVariableMissingException", "ExpressionScalar", "ExpressionVector"]

Expand Down Expand Up @@ -45,7 +46,7 @@ def _parse_evaluate_numeric_arguments(self, eval_args: Dict[str, Number]) -> Dic
def _parse_evaluate_numeric_result(self,
result: Union[Number, numpy.ndarray],
call_arguments: Any) -> Union[Number, numpy.ndarray]:
allowed_types = (float, numpy.number, int, complex, bool, numpy.bool_)
allowed_types = (float, numpy.number, int, complex, bool, numpy.bool_, TimeType)
if isinstance(result, tuple):
result = numpy.array(result)
if isinstance(result, numpy.ndarray):
Expand All @@ -56,7 +57,7 @@ def _parse_evaluate_numeric_result(self,
if obj_types == {sympy.Float} or obj_types == {sympy.Float, sympy.Integer}:
return result.astype(float)
elif obj_types == {sympy.Integer}:
return result.astype(np.int64)
return result.astype(numpy.int64)
else:
raise NonNumericEvaluation(self, result, call_arguments)
elif isinstance(result, allowed_types):
Expand Down
5 changes: 3 additions & 2 deletions qupulse/hardware/dacs/alazar.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def register_measurement_windows(self,
program_name: str,
windows: Dict[str, Tuple[np.ndarray, np.ndarray]]) -> None:
program = self._registered_programs[program_name]
sample_factor = TimeType(int(self.config.captureClockConfiguration.numeric_sample_rate(self.card.model)),
sample_factor = TimeType.from_fraction(int(self.config.captureClockConfiguration.numeric_sample_rate(self.card.model)),
10 ** 9)
program.clear_masks()

Expand All @@ -133,7 +133,8 @@ def arm_program(self, program_name: str) -> None:
config.masks, config.operations, total_record_size = self._registered_programs[program_name].iter(
self._make_mask)

sample_factor = TimeType(self.config.captureClockConfiguration.numeric_sample_rate(self.card.model), 10 ** 9)
sample_factor = TimeType.from_fraction(self.config.captureClockConfiguration.numeric_sample_rate(self.card.model),
10 ** 9)

if not config.operations:
raise RuntimeError("No operations: Arming program without operations is an error as there will "
Expand Down
238 changes: 222 additions & 16 deletions qupulse/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import inspect
import numbers
import fractions
import collections
import itertools
from collections.abc import Mapping as ABCMapping
import functools
import warnings

import numpy
Expand All @@ -16,28 +14,236 @@
MeasurementWindow = typing.Tuple[str, numbers.Real, numbers.Real]
ChannelID = typing.Union[str, int]


try:
import gmpy2
TimeType = gmpy2.mpq
except ImportError:
gmpy2 = None

warnings.warn('gmpy2 not found. Using fractions.Fraction as fallback. Install gmpy2 for better performance.'
'time_from_float might produce slightly different results')


def _with_other_as_time_type(fn):
"""This is decorator to convert the other argument and the result into a :class:`TimeType`"""
@functools.wraps(fn)
def wrapper(self, other) -> 'TimeType':
converted = _converter.get(type(other), TimeType)(other)
result = fn(self, converted)
if result is NotImplemented:
return result
elif type(result) is TimeType._InternalType:
return TimeType(result)
else:
return result
return wrapper


class TimeType:
"""This type represents a rational number with arbitrary precision.
Internally it uses :func:`gmpy2.mpq` (if available) or :class:`fractions.Fraction`
"""
__slots__ = ('_value',)

_InternalType = fractions.Fraction if gmpy2 is None else type(gmpy2.mpq())
_to_internal = fractions.Fraction if gmpy2 is None else gmpy2.mpq

def __init__(self, value: numbers.Rational = 0.):
if type(value) == type(self):
self._value = value._value
else:
self._value = self._to_internal(value)

@property
def numerator(self):
return self._value.numerator

@property
def denominator(self):
return self._value.denominator

def __round__(self, *args, **kwargs):
return self._value.__round__(*args, **kwargs)

def __abs__(self):
return TimeType(self._value.__abs__())

def __hash__(self):
return self._value.__hash__()

def __ceil__(self):
return int(self._value.__ceil__())

def __floor__(self):
return int(self._value.__floor__())

def __int__(self):
return int(self._value)

@_with_other_as_time_type
def __mod__(self, other: 'TimeType'):
return self._value.__mod__(other._value)

@_with_other_as_time_type
def __rmod__(self, other: 'TimeType'):
return self._value.__rmod__(other._value)

def __neg__(self):
return TimeType(self._value.__neg__())

def __pos__(self):
return self

@_with_other_as_time_type
def __pow__(self, other: 'TimeType'):
return self._value.__pow__(other._value)

@_with_other_as_time_type
def __rpow__(self, other: 'TimeType'):
return self._value.__rpow__(other._value)

def __trunc__(self):
return int(self._value.__trunc__())

@_with_other_as_time_type
def __mul__(self, other: 'TimeType'):
return self._value.__mul__(other._value)

def time_from_float(time: float, absolute_error: float=1e-12) -> TimeType:
@_with_other_as_time_type
def __rmul__(self, other: 'TimeType'):
return self._value.__mul__(other._value)

@_with_other_as_time_type
def __add__(self, other: 'TimeType'):
return self._value.__add__(other._value)

@_with_other_as_time_type
def __radd__(self, other: 'TimeType'):
return self._value.__radd__(other._value)

@_with_other_as_time_type
def __sub__(self, other: 'TimeType'):
return self._value.__sub__(other._value)

@_with_other_as_time_type
def __rsub__(self, other: 'TimeType'):
return self._value.__rsub__(other._value)

@_with_other_as_time_type
def __truediv__(self, other: 'TimeType'):
return self._value.__truediv__(other._value)

@_with_other_as_time_type
def __rtruediv__(self, other: 'TimeType'):
return self._value.__rtruediv__(other._value)

@_with_other_as_time_type
def __floordiv__(self, other: 'TimeType'):
return self._value.__floordiv__(other._value)

@_with_other_as_time_type
def __rfloordiv__(self, other: 'TimeType'):
return self._value.__rfloordiv__(other._value)

@_with_other_as_time_type
def __le__(self, other: 'TimeType'):
return self._value.__le__(other._value)

@_with_other_as_time_type
def __ge__(self, other: 'TimeType'):
return self._value.__ge__(other._value)

@_with_other_as_time_type
def __lt__(self, other: 'TimeType'):
return self._value.__lt__(other._value)

@_with_other_as_time_type
def __gt__(self, other: 'TimeType'):
return self._value.__gt__(other._value)

def __eq__(self, other):
if type(other) == type(self):
return self._value.__eq__(other._value)
else:
return self._value == other

@classmethod
def from_float(cls, value: float, absolute_error: typing.Optional[float] = None) -> 'TimeType':
"""Convert a floating point number to a TimeType using one of three modes depending on `absolute_error`.
The default str(value) guarantees that all floats have a different result with sensible rounding.
This was chosen as default because it is the expected behaviour most of the time if the user defined the float
from a literal in code.
Args:
value: Floating point value to convert to arbitrary precision TimeType
absolute_error:
- :obj:`None`: Use `str(value)` as a proxy to get consistent precision
- 0: Return the exact value of the float i.e. float(0.8) == 3602879701896397 / 4503599627370496
- 0 < `absolute_error` <= 1: Use `absolute_error` to limit the denominator
Raises:
ValueError: If `absolute_error` is not None and not 0 <= `absolute_error` <= 1
"""
# gmpy2 is at least an order of magnitude faster than fractions.Fraction
return gmpy2.mpq(gmpy2.f2q(time, absolute_error))
if absolute_error is None:
# this method utilizes the 'print as many digits as necessary to destinguish between all floats'
# functionality of str
if type(value) in (cls, cls._InternalType, fractions.Fraction):
return cls(value)
else:
return cls(cls._to_internal(str(value).replace('e', 'E')))

def time_from_fraction(numerator: int, denominator: int = 1) -> TimeType:
return gmpy2.mpq(numerator, denominator)
elif absolute_error == 0:
return cls(cls._to_internal(value))
elif absolute_error < 0:
raise ValueError('absolute_error needs to be at least 0')
elif absolute_error > 1:
raise ValueError('absolute_error needs to be smaller 1')
else:
if cls._InternalType is fractions.Fraction:
return fractions.Fraction(value).limit_denominator(int(1 / absolute_error))
else:
return cls(gmpy2.f2q(value, absolute_error))

@classmethod
def from_fraction(cls, numerator: int, denominator: int) -> 'TimeType':
"""Convert a fraction to a TimeType.
Args:
numerator: Numerator of the time fraction
denominator: Denominator of the time fraction
"""
return cls(cls._to_internal(numerator, denominator))

def __repr__(self):
return 'TimeType(%s)' % self.__str__()

def __str__(self):
return '%d/%d' % (self._value.numerator, self._value.denominator)

def __float__(self):
return int(self._value.numerator) / int(self._value.denominator)


# this asserts isinstance(TimeType, Rational) is True
numbers.Rational.register(TimeType)


_converter = {
float: TimeType.from_float,
TimeType: lambda x: x
}

except ImportError:
warnings.warn('gmpy2 not found. Using fractions.Fraction as fallback. Install gmpy2 for better performance.')

TimeType = fractions.Fraction
def time_from_float(value: float, absolute_error: typing.Optional[float] = None) -> TimeType:
"""See :func:`TimeType.from_float`."""
return TimeType.from_float(value, absolute_error)

def time_from_float(time: float, absolute_error: float = 1e-12) -> TimeType:
return fractions.Fraction(time).limit_denominator(int(1/absolute_error))

def time_from_fraction(numerator: int, denominator: int = 1) -> TimeType:
return fractions.Fraction(numerator=numerator, denominator=denominator)
def time_from_fraction(numerator: int, denominator: int) -> TimeType:
"""See :func:`TimeType.from_float`."""
return TimeType.from_fraction(numerator, denominator)


class DocStringABCMeta(abc.ABCMeta):
Expand Down
Loading

0 comments on commit 4dc1092

Please sign in to comment.