Skip to content

Commit

Permalink
Merge branch 'master' into fix/eval_sympy_zero_in_array
Browse files Browse the repository at this point in the history
  • Loading branch information
terrorfisch committed Jan 14, 2022
2 parents c9512bf + 9e4b28f commit b8487da
Show file tree
Hide file tree
Showing 12 changed files with 479 additions and 33 deletions.
36 changes: 27 additions & 9 deletions .github/workflows/pythontest.yaml
Expand Up @@ -2,25 +2,25 @@ name: Pytest and coveralls

on:
workflow_dispatch: ~
push:
branches: [ $default-branch ]
pull_request:
branches: [ $default-branch ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: true
fail-fast: false
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9"]
install-extras: ["tests,plotting,zurich-instruments,tektronix,tabor-instruments", "tests,plotting,zurich-instruments,tektronix,tabor-instruments,Faster-fractions"]
time-type: ["fractions", "gmpy2"]

steps:
- name: Install gmpy2 build dependencies if required
- name: Prepare gmpy2 build dependencies if required
run: |
if [[ ${{ matrix.install-extras }} == *Faster-fractions* ]]; then
export INSTALL_EXTRAS="tests,plotting,zurich-instruments,tektronix,tabor-instruments"
if [[ ${{ matrix.time-type }} == *gmpy2* ]]; then
sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev
export INSTALL_EXTRAS=$INSTALL_EXTRAS",Faster-fractions"
fi
- name: Checkout repository
Expand All @@ -41,18 +41,26 @@ jobs:
- name: Install package
run: |
python -m pip install .[${{ matrix.install-extras }}]
python -m pip install .[$INSTALL_EXTRAS]
- name: Test with pytest
run: |
coverage run -m pytest
coverage run -m pytest --junit-xml pytest.xml
- name: Upload coverage data to coveralls.io
run: coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: python-${{ matrix.python-version }}-${{ matrix.install-extras }}
COVERALLS_FLAG_NAME: python-${{ matrix.python-version }}-${{ matrix.time-type }}
COVERALLS_PARALLEL: true

- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v2
with:
name: Unit Test Results ( ${{ matrix.python-version }}-${{ matrix.time-type }} )
path: |
pytest.xml
coveralls:
name: Indicate completion to coveralls.io
Expand All @@ -66,3 +74,13 @@ jobs:
coveralls --service=github --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

event_file:
name: "Event File"
runs-on: ubuntu-latest
steps:
- name: Upload
uses: actions/upload-artifact@v2
with:
name: Event File
path: ${{ github.event_path }}
37 changes: 37 additions & 0 deletions .github/workflows/unittest_publish.yaml
@@ -0,0 +1,37 @@
name: Unit Test Results

on:
workflow_run:
workflows: ["Pytest and coveralls"]
types:
- completed

jobs:
unit-test-results:
name: Unit Test Results
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion != 'skipped'

steps:
- name: Download and Extract Artifacts
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: |
mkdir -p artifacts && cd artifacts
artifacts_url=${{ github.event.workflow_run.artifacts_url }}
gh api "$artifacts_url" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact
do
IFS=$'\t' read name url <<< "$artifact"
gh api $url > "$name.zip"
unzip -d "$name" "$name.zip"
done
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
with:
commit: ${{ github.event.workflow_run.head_sha }}
event_file: artifacts/Event File/event.json
event_name: ${{ github.event.workflow_run.event }}
files: "artifacts/**/*.xml"
2 changes: 2 additions & 0 deletions changes.d/622.feature
@@ -0,0 +1,2 @@
Add option to automatically reduce the sample rate of HDAWG playback for piecewise constant pulses.
Use `qupulse._program.seqc.WaveformPlayback.ENABLE_DYNAMIC_RATE_REDUCTION` to enable it.
58 changes: 56 additions & 2 deletions qupulse/_program/_loop.py
Expand Up @@ -2,16 +2,18 @@
from collections import defaultdict
from enum import Enum
import warnings
import bisect

import numpy as np
import sympy.ntheory


from qupulse._program.waveforms import Waveform
from qupulse._program.waveforms import Waveform, ConstantWaveform
from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty

from qupulse.utils import is_integer
from qupulse.utils.types import TimeType, MeasurementWindow
from qupulse.utils.tree import Node, is_tree_circular
from qupulse.utils.numeric import smallest_factor_ge

from qupulse._program.waveforms import SequenceWaveform, RepetitionWaveform

Expand Down Expand Up @@ -578,6 +580,58 @@ def make_compatible(program: Loop, minimal_waveform_length: int, waveform_quantu
assert comp_level == _CompatibilityLevel.compatible


def roll_constant_waveforms(program: Loop, minimal_waveform_quanta: int, waveform_quantum: int, sample_rate: TimeType):
"""This function finds waveforms in program that can be replaced with repetitions of shorter waveforms and replaces
them. Complexity O(N_waveforms)
This is possible if:
- The waveform is constant on all channels
- waveform.duration * sample_rate / waveform_quantum has a factor that is bigger than minimal_waveform_quanta
Args:
program:
minimal_waveform_quanta:
waveform_quantum:
sample_rate:
"""
waveform = program.waveform

if waveform is None:
for child in program:
roll_constant_waveforms(child, minimal_waveform_quanta, waveform_quantum, sample_rate)
else:
waveform_quanta = (waveform.duration * sample_rate) // waveform_quantum

# example
# waveform_quanta = 15
# minimal_waveform_quanta = 2
# => repetition_count = 5, new_waveform_quanta = 3
if waveform_quanta < minimal_waveform_quanta * 2:
# there is no way to roll this waveform because it is too short
return

const_values = waveform.constant_value_dict()
if const_values is None:
# The waveform is not constant
return

new_waveform_quanta = smallest_factor_ge(waveform_quanta, min_factor=minimal_waveform_quanta)
if new_waveform_quanta == waveform_quanta:
# the waveform duration in samples has no suitable factor
# TODO: Option to insert multiple Loop objects
return

additional_repetition_count = waveform_quanta // new_waveform_quanta

new_waveform = ConstantWaveform.from_mapping(
duration=waveform_quantum * new_waveform_quanta / sample_rate,
constant_values=const_values)

# use the private properties to avoid invalidating the duration cache of the parent loop
program._repetition_definition = program.repetition_definition * additional_repetition_count
program._waveform = new_waveform


class MakeCompatibleWarning(ResourceWarning):
pass

Expand Down
64 changes: 49 additions & 15 deletions qupulse/_program/seqc.py
Expand Up @@ -64,13 +64,16 @@ class BinaryWaveform:
"""
__slots__ = ('data',)

PLAYBACK_QUANTUM = 16
PLAYBACK_MIN_QUANTA = 2

def __init__(self, data: np.ndarray):
""" always use both channels?
""" TODO: always use both channels?
Args:
data: data as returned from zhinst.utils.convert_awg_waveform
"""
n_quantum, remainder = divmod(data.size, 3 * 16)
n_quantum, remainder = divmod(data.size, 3 * self.PLAYBACK_QUANTUM)
assert n_quantum > 1, "Waveform too short (min len is 32)"
assert remainder == 0, "Waveform has not a valid length"
assert data.dtype is np.dtype('uint16')
Expand Down Expand Up @@ -152,6 +155,8 @@ def to_csv_compatible_table(self):
>>> np.savetxt(waveform_dir, binary_waveform.to_csv_compatible_table(), fmt='%u')
"""
assert self.data.size % self.PLAYBACK_QUANTUM == 0, "conversion to csv requires a valid length"

table = np.zeros((len(self), 2), dtype=np.uint32)
table[:, 0] = self.ch1
table[:, 1] = self.ch2
Expand All @@ -161,6 +166,21 @@ def to_csv_compatible_table(self):

return table

def playback_possible(self) -> bool:
"""Returns if the waveform can be played without padding"""
return self.data.size % self.PLAYBACK_QUANTUM == 0

def dynamic_rate(self, max_rate: int = 12) -> int:
min_pre_division_quanta = 2 * self.PLAYBACK_QUANTUM

reduced = self.data.reshape(-1, 3)
for n in range(max_rate):
n_quantum, remainder = divmod(reduced.shape[0], min_pre_division_quanta)
if remainder != 0 or n_quantum < self.PLAYBACK_MIN_QUANTA or np.any(reduced[::2, :] != reduced[1::2, :]):
return n
reduced = reduced[::2, :]
return max_rate


class ConcatenatedWaveform:
def __init__(self):
Expand Down Expand Up @@ -1356,23 +1376,38 @@ def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_gen

class WaveformPlayback(SEQCNode):
ADVANCE_DISABLED_COMMENT = ' // advance disabled do to parent repetition'
ENABLE_DYNAMIC_RATE_REDUCTION = False

__slots__ = ('waveform', 'shared')
__slots__ = ('waveform', 'shared', 'rate')

def __init__(self, waveform: Tuple[BinaryWaveform, ...], shared: bool = False):
def __init__(self, waveform: Tuple[BinaryWaveform, ...], shared: bool = False, rate: int = None):
assert isinstance(waveform, tuple)
if self.ENABLE_DYNAMIC_RATE_REDUCTION and rate is None:
for wf in waveform:
rate = wf.dynamic_rate(12 if rate is None else rate)
self.waveform = waveform
self.shared = shared
self.rate = rate

def samples(self) -> int:
"""Samples consumed in the big concatenated waveform"""
if self.shared:
return 0
else:
wf_lens = set(map(len, self.waveform))
assert len(wf_lens) == 1
wf_len, = wf_lens
if self.rate is not None:
wf_len //= (1 << self.rate)
return wf_len

def rate_reduced_waveform(self) -> Tuple[BinaryWaveform]:
if self.rate is None:
return self.waveform
else:
return tuple(BinaryWaveform(wf.data.reshape((-1, 3))[::(1 << self.rate), :].ravel())
for wf in self.waveform)

def stepping_hash(self) -> int:
if self.shared:
return hash((type(self), self.waveform))
Expand All @@ -1382,7 +1417,7 @@ def stepping_hash(self) -> int:
def same_stepping(self, other: 'WaveformPlayback') -> bool:
same_type = type(self) is type(other) and self.shared == other.shared
if self.shared:
return same_type and self.waveform == other.waveform
return same_type and self.rate == other.rate and self.waveform == other.waveform
else:
return same_type and self.samples() == other.samples()

Expand All @@ -1391,24 +1426,23 @@ def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']:

def _visit_nodes(self, waveform_manager: ProgramWaveformManager):
if not self.shared:
waveform_manager.request_concatenated(self.waveform)
waveform_manager.request_concatenated(self.rate_reduced_waveform())

def to_source_code(self, waveform_manager: ProgramWaveformManager,
node_name_generator: Iterator[str], line_prefix: str, pos_var_name: str,
advance_pos_var: bool = True):
rate_adjustment = "" if self.rate is None else f", {self.rate}"
if self.shared:
yield '{line_prefix}playWave({waveform});'.format(waveform=waveform_manager.request_shared(self.waveform),
line_prefix=line_prefix)
yield f'{line_prefix}playWave(' \
f'{waveform_manager.request_shared(self.rate_reduced_waveform())}' \
f'{rate_adjustment});'
else:
wf_name = waveform_manager.request_concatenated(self.waveform)
wf_name = waveform_manager.request_concatenated(self.rate_reduced_waveform())
wf_len = self.samples()
play_cmd = '{line_prefix}playWaveIndexed({wf_name}, {pos_var_name}, {wf_len});'
play_cmd = f'{line_prefix}playWaveIndexed({wf_name}, {pos_var_name}, {wf_len}{rate_adjustment});'

if advance_pos_var:
advance_cmd = ' {pos_var_name} = {pos_var_name} + {wf_len};'
advance_cmd = f' {pos_var_name} = {pos_var_name} + {wf_len};'
else:
advance_cmd = self.ADVANCE_DISABLED_COMMENT
yield (play_cmd + advance_cmd).format(wf_name=wf_name,
wf_len=wf_len,
pos_var_name=pos_var_name,
line_prefix=line_prefix)
yield play_cmd + advance_cmd
6 changes: 6 additions & 0 deletions qupulse/_program/volatile.py
Expand Up @@ -62,3 +62,9 @@ def __int__(self):
def update_volatile_dependencies(self, new_constants: Mapping[str, numbers.Number]) -> int:
self._scope = self._scope.change_constants(new_constants)
return int(self)

def __eq__(self, other):
if type(self) == type(other):
return self._scope is other._scope and self._expression == other._expression
else:
return NotImplemented
2 changes: 1 addition & 1 deletion qupulse/_program/waveforms.py
Expand Up @@ -779,7 +779,7 @@ def unsafe_sample(self,
end = time + body_duration
indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left'))
self._body.unsafe_sample(channel=channel,
sample_times=sample_times[indices] - time,
sample_times=sample_times[indices] - float(time),
output_array=output_array[indices])
time = end
return output_array
Expand Down
26 changes: 26 additions & 0 deletions qupulse/utils/numeric.py
@@ -1,13 +1,39 @@
from typing import Tuple, Type
from numbers import Rational
from math import gcd
from operator import le
from functools import partial

import sympy


def lcm(a: int, b: int):
"""least common multiple"""
return a * b // gcd(a, b)


def smallest_factor_ge(n: int, min_factor: int, brute_force: int = 5):
"""Find the smallest factor of n that is greater or equal min_factor
Args:
n: number to factorize
min_factor: factor must be larger this
brute_force: range(min_factor, min(min_factor + brute_force)) is probed by brute force
Returns:
Smallest factor of n that is greater or equal min_factor
"""
assert min_factor <= n

# this shortcut force shortcut costs 1us max
for factor in range(min_factor, min(min_factor + brute_force, n)):
if n % factor == 0:
return factor
else:
return min(filter(partial(le, min_factor),
sympy.ntheory.divisors(n, generator=True)))


def _approximate_int(alpha_num: int, d_num: int, den: int) -> Tuple[int, int]:
"""Find the best fraction approximation of alpha_num / den with an error smaller d_num / den. Best means the
fraction with the smallest denominator.
Expand Down

0 comments on commit b8487da

Please sign in to comment.