Skip to content

Commit

Permalink
Merge branch 'feat/program_builder' into issues/781_linspace_program_…
Browse files Browse the repository at this point in the history
…builder
  • Loading branch information
terrorfisch committed Oct 19, 2023
2 parents 6670bbf + 14455a5 commit 63c2745
Show file tree
Hide file tree
Showing 18 changed files with 355 additions and 75 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/pythontest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ jobs:


steps:
- name: Prepare gmpy2 build dependencies
- name: Add gmpy extra feature
if: ${{ matrix.time-type }} == 'gmpy2'
run: |
sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev
echo "INSTALL_EXTRAS=${{ env.INSTALL_EXTRAS }},Faster-fractions" >> $GITHUB_ENV
- name: Checkout repository
Expand Down
1 change: 1 addition & 0 deletions changes.d/692.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `__pow__` as a repetition shortcut. This means you can do `my_pulse_template ** 5` or `my_pulse_template ** 'my_repetition_count'`.
2 changes: 2 additions & 0 deletions changes.d/801.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``PulseTemplate.pad_to`` method to help padding to minimal lengths or multiples of given durations.

49 changes: 30 additions & 19 deletions qupulse/_program/tabor.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
import sys
from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast,\
Hashable
Expand Down Expand Up @@ -429,7 +430,7 @@ def __init__(self,
else:
self.setup_advanced_sequence_mode()

self._sampled_segments = self._calc_sampled_segments()
self._sampled_segments, self._waveform_to_segment = self._calc_sampled_segments()

@property
def markers(self) -> Tuple[Optional[ChannelID], Optional[ChannelID]]:
Expand Down Expand Up @@ -463,19 +464,22 @@ def _marker_data(self, waveform: Waveform, time: np.ndarray, marker_idx: int):
marker = self._markers[marker_idx]
return waveform.get_sampled(channel=marker, sample_times=time) != 0

def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]]:
def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence[int]], Sequence[int]]:
"""
Returns:
(segments, segment_lengths)
((segments, segment_lengths), waveform_to_segment)
"""
time_array, segment_lengths = get_sample_times(self._parsed_program.waveforms, self._sample_rate)
time_array, waveform_samples = get_sample_times(self._parsed_program.waveforms, self._sample_rate)

if np.any(segment_lengths % 16 > 0) or np.any(segment_lengths < 192):
if np.any(waveform_samples % 16 > 0) or np.any(waveform_samples < 192):
raise TaborException('At least one waveform has a length that is smaller 192 or not a multiple of 16')

segments = []
for i, waveform in enumerate(self._parsed_program.waveforms):
t = time_array[:segment_lengths[i]]
segments: Dict[TaborSegment, int] = {}
segment_lengths = []

waveform_to_segment = []
for waveform, n_samples in zip(self._parsed_program.waveforms, waveform_samples):
t = time_array[:n_samples]
marker_time = t[::2]
segment_a = self._channel_data(waveform, t, 0)
segment_b = self._channel_data(waveform, t, 1)
Expand All @@ -487,8 +491,12 @@ def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]]
ch_b=segment_b,
marker_a=marker_a,
marker_b=marker_b)
segments.append(segment)
return segments, segment_lengths
previous_segment_count = len(segments)
segment_idx = segments.setdefault(segment, previous_segment_count)
waveform_to_segment.append(segment_idx)
if segment_idx == previous_segment_count:
segment_lengths.append(n_samples)
return (list(segments.keys()), np.array(segment_lengths, dtype=np.uint64)), waveform_to_segment

def setup_single_sequence_mode(self) -> None:
assert self.program.depth() == 1
Expand Down Expand Up @@ -555,10 +563,13 @@ def update_volatile_parameters(self, parameters: Mapping[str, numbers.Number]) -

return modifications

def get_sequencer_tables(self): # -> List[List[TableDescription, Optional[MappedParameter]]]:
return self._parsed_program.sequencer_tables
def get_sequencer_tables(self) -> Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]:
wf_to_seq = self._waveform_to_segment
return [[(TableDescription(rep_count, wf_to_seq[elem_idx], jump), volatile)
for (rep_count, elem_idx, jump), volatile in sequencer_table]
for sequencer_table in self._parsed_program.sequencer_tables]

def get_advanced_sequencer_table(self) -> List[TableEntry]:
def get_advanced_sequencer_table(self) -> Sequence[TableEntry]:
"""Advanced sequencer table that can be used via the download_adv_seq_table pytabor command"""
return self._parsed_program.advanced_sequencer_table

Expand Down Expand Up @@ -645,12 +656,12 @@ def prepare_program_for_advanced_sequence_mode(program: Loop, min_seq_len: int,
i += 1


ParsedProgram = NamedTuple('ParsedProgram', [('advanced_sequencer_table', Sequence[TableEntry]),
('sequencer_tables', Sequence[Sequence[
Tuple[TableDescription, Optional[VolatileProperty]]]]),
('waveforms', Tuple[Waveform, ...]),
('volatile_parameter_positions', Dict[Union[int, Tuple[int, int]],
VolatileRepetitionCount])])
@dataclasses.dataclass
class ParsedProgram:
advanced_sequencer_table: Sequence[TableEntry]
sequencer_tables: Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]
waveforms: Tuple[Waveform, ...]
volatile_parameter_positions: Dict[Union[int, Tuple[int, int]], VolatileRepetitionCount]


def parse_aseq_program(program: Loop, used_channels: FrozenSet[ChannelID]) -> ParsedProgram:
Expand Down
6 changes: 6 additions & 0 deletions qupulse/expressions/sympy.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,12 @@ def __truediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> '
def __rtruediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar':
return self.make(self._sympified_expression.__rtruediv__(self._extract_sympified(other)))

def __floordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar':
return self.make(self._sympified_expression.__floordiv__(self._extract_sympified(other)))

def __rfloordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar':
return self.make(self._sympified_expression.__rfloordiv__(self._extract_sympified(other)))

def __neg__(self) -> 'ExpressionScalar':
return self.make(self._sympified_expression.__neg__())

Expand Down
96 changes: 87 additions & 9 deletions qupulse/hardware/awgs/zihdawg.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import hashlib
import argparse
import re
from abc import abstractmethod
from abc import abstractmethod, ABC

try:
# zhinst fires a DeprecationWarning from its own code in some versions...
Expand Down Expand Up @@ -394,7 +394,7 @@ def _initialize_awg_module(self):
self._awg_module.set('awgModule/device', self.master_device.serial)
self._awg_module.set('awgModule/index', self.awg_group_index)
self._awg_module.execute()
self._elf_manager = ELFManager(self._awg_module)
self._elf_manager = ELFManager.DEFAULT_CLS(self._awg_module)
self._upload_generator = ()

@property
Expand Down Expand Up @@ -501,6 +501,7 @@ def upload(self, name: str,
def _start_compile_and_upload(self):
self._uploaded_seqc_source = None
self._upload_generator = self._elf_manager.compile_and_upload(self._required_seqc_source)
logger.debug(f"_start_compile_and_upload: %r", next(self._upload_generator, "Finished"))

def _wait_for_compile_and_upload(self):
for state in self._upload_generator:
Expand Down Expand Up @@ -560,6 +561,8 @@ def arm(self, name: Optional[str]) -> None:

if self._required_seqc_source != self._uploaded_seqc_source:
self._wait_for_compile_and_upload()
assert self._required_seqc_source == self._uploaded_seqc_source, "_wait_for_compile_and_upload did not work " \
"as expected."

self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0)

Expand All @@ -581,10 +584,8 @@ def arm(self, name: Optional[str]) -> None:
self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER,
self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2))

# this was a workaround for problems in the past and I totally forgot why it was here
# for ch_pair in self.master.channel_tuples:
# ch_pair._wait_for_compile_and_upload()
self.enable(True)
if name is not None:
self.enable(True)

def run_current_program(self) -> None:
"""Run armed program."""
Expand Down Expand Up @@ -802,7 +803,9 @@ def offsets(self) -> Tuple[float, ...]:
return tuple(map(self.master_device.offset, self._channels()))


class ELFManager:
class ELFManager(ABC):
DEFAULT_CLS = None

class AWGModule:
def __init__(self, awg_module: zhinst_core.AwgModule):
"""Provide an easily mockable interface to the zhinst AwgModule object"""
Expand Down Expand Up @@ -838,6 +841,14 @@ def compiler_source_file(self) -> str:
def compiler_source_file(self, source_file: str):
self._module.set('compiler/sourcefile', source_file)

@property
def compiler_source_string(self) -> str:
return self._module.getString('compiler/sourcestring')

@compiler_source_string.setter
def compiler_source_string(self, source_string: str):
self._module.set('compiler/sourcestring', source_string)

@property
def compiler_upload(self) -> bool:
"""auto upload after compiling"""
Expand Down Expand Up @@ -912,6 +923,74 @@ def _source_hash(source_string: str) -> str:
# use utf-16 because str is UTF16 on most relevant machines (Windows)
return hashlib.sha512(bytes(source_string, 'utf-16')).hexdigest()

@abstractmethod
def compile_and_upload(self, source_string: str) -> Generator[str, str, None]:
"""The function returns a generator that yields the current state of the progress. The generator is empty iff
the upload is complete. An exception is raised if there is an error.
To abort send 'abort' to the generator. (not implemented :P)
Example:
>>> my_source = 'playWave("my_wave");'
>>> for state in elf_manager.compile_and_upload(my_source):
... print('Current state:', state)
... time.sleep(1)
Args:
source_string: Source code to compile
Returns:
Generator object that needs to be consumed
"""


class SimpleELFManager(ELFManager):
def __init__(self, awg_module: zhinst.ziPython.AwgModule):
"""This implementation does not attempt to do something clever like caching."""
super().__init__(awg_module)

def compile_and_upload(self, source_string: str) -> Generator[str, str, None]:
self.awg_module.compiler_upload = True
self.awg_module.compiler_source_string = source_string

while True:
status, msg = self.awg_module.compiler_status
if status == - 1:
yield 'compiling'
elif status == 0:
break
elif status == 1:
raise HDAWGCompilationException(msg)
elif status == 2:
logger.warning("Compiler warings: %s", msg)
break
else:
raise RuntimeError("Unexpected status", status, msg)

while True:
status_int, progress = self.awg_module.elf_status
if progress == 1.0:
break
elif status_int == 1:
HDAWGUploadException(self.awg_module.compiler_status)
else:
yield 'uploading @ %d%%' % (100*progress)


ELFManager.DEFAULT_CLS = SimpleELFManager


class CachingELFManager(ELFManager):
def __init__(self, awg_module: zhinst.ziPython.AwgModule):
"""FAILS TO UPLOAD THE CORRECT ELF FOR SOME REASON"""
super().__init__(awg_module)

# automatically upload after successful compilation
self.awg_module.compiler_upload = True

self._compile_job = None # type: Optional[Union[str, Tuple[str, int, str]]]
self._upload_job = None # type: Optional[Union[Tuple[str, float], Tuple[str, int]]]

def _update_compile_job_status(self):
"""Store current compile status in self._compile_job."""
compiler_start = self.awg_module.compiler_start
Expand All @@ -920,8 +999,7 @@ def _update_compile_job_status(self):

elif isinstance(self._compile_job, str):
if compiler_start:
# compilation is running
pass
logger.debug("Compiler is running.")

else:
compiler_status, status_string = self.awg_module.compiler_status
Expand Down
8 changes: 4 additions & 4 deletions qupulse/program/waveforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,8 @@ def unsafe_sample(self,
entries = self._table

for entry1, entry2 in pairwise(entries):
indices = slice(np.searchsorted(sample_times, entry1.t, 'left'),
np.searchsorted(sample_times, entry2.t, 'right'))
indices = slice(sample_times.searchsorted(entry1.t, 'left'),
sample_times.searchsorted(entry2.t, 'right'))
output_array[indices] = \
entry2.interp((float(entry1.t), entry1.v),
(float(entry2.t), entry2.v),
Expand Down Expand Up @@ -626,7 +626,7 @@ def unsafe_sample(self,
# indexing in numpy and their copy/reference behaviour
end = time + subwaveform.duration

indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left'))
indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left'))
subwaveform.unsafe_sample(channel=channel,
sample_times=sample_times[indices]-np.float64(time),
output_array=output_array[indices])
Expand Down Expand Up @@ -843,7 +843,7 @@ def unsafe_sample(self,
time = 0
for _ in range(self._repetition_count):
end = time + body_duration
indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left'))
indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left'))
self._body.unsafe_sample(channel=channel,
sample_times=sample_times[indices] - float(time),
output_array=output_array[indices])
Expand Down
Loading

0 comments on commit 63c2745

Please sign in to comment.