Skip to content

Commit 05e3746

Browse files
author
maxbeer99
committed
Merge remote-tracking branch 'origin/feat/linspace_measurements' into feat/linspace_measurements/atssimple
2 parents 08f8e5f + 4922d71 commit 05e3746

18 files changed

+975
-61
lines changed

qupulse/examples/expectation_checker.py

Lines changed: 481 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# -*- coding: utf-8 -*-
2+
import numpy as np
3+
4+
from qupulse.pulses import PointPT, ConstantPT, RepetitionPT, ForLoopPT, TablePT,\
5+
FunctionPT, AtomicMultiChannelPT, SequencePT, MappingPT, ParallelConstantChannelPT
6+
from qupulse.program.linspace import LinSpaceBuilder
7+
8+
from qupulse.plotting import plot
9+
from qupulse.utils import to_next_multiple
10+
11+
from expectation_checker import ExpectationChecker, HDAWGAlazar
12+
13+
#%% Get Devices
14+
15+
# Get the Alazar and the HDAWG in hardcoded configuration (2V p-p range)
16+
# Connect any Marker from the HDAWG to Trig in of the Alazar
17+
# Connect one dummy channel from HDAWG to the external clock of the Alazar,
18+
# then set 0.5V-10MHz-oscillator on this channel (e.g. in HDAWG-Webinterface)
19+
20+
ha = HDAWGAlazar("DEVXXXX","USB",)
21+
22+
#%% Example pulse definitions
23+
24+
# PulseTemplates must be defined on channels named as 'ZI0_X', X in A to H
25+
# (ensure correct mapping to Alazar)
26+
# Markers will be overwritten to play a marker on each channel to trigger the Alazar
27+
# identifiers of the final PT will be the names of the plotted object
28+
29+
30+
class ShortSingleRampTest():
31+
def __init__(self, base_time=1e3):
32+
hold = ConstantPT(base_time, {'a': '-1. + idx * 0.01'})
33+
pt = hold.with_iteration('idx', 200)
34+
self.pulse_template = MappingPT(pt,
35+
channel_mapping={'a':'ZI0_A',},
36+
identifier=self.__class__.__name__
37+
)
38+
39+
class ShortSingleRampTestWithPlay():
40+
def __init__(self,base_time=1e3+8):
41+
# init = PointPT([(1.0,1e4)],channel_names=('ZI0_MARKER_FRONT',))
42+
init = FunctionPT('1.0+1e-9*t',base_time,channel='ZI0_A_MARKER_FRONT')#.pad_to(to_next_multiple(1.0,16,4),)
43+
44+
hold = ConstantPT(base_time, {'a': '-1. + idx * 0.01'})#.pad_to(to_next_multiple(1.0,16,4))
45+
pt = ParallelConstantChannelPT(init,dict(ZI0_A=0.))@(ParallelConstantChannelPT(hold,dict(ZI0_A_MARKER_FRONT=0.)).with_iteration('idx', 200))
46+
self.pulse_template = MappingPT(pt,
47+
channel_mapping={'a':'ZI0_A',},
48+
identifier=self.__class__.__name__
49+
)
50+
51+
class SequencedRepetitionTest():
52+
def __init__(self,base_time=1e2,rep_factor=2):
53+
wait = AtomicMultiChannelPT(
54+
ConstantPT(f'64*{base_time}', {'a': '-1. + idx_a * 0.01 + y_gain', }),
55+
ConstantPT(f'64*{base_time}', {'b': '-0.5 + idx_b * 0.02'})
56+
)
57+
58+
dependent_constant = AtomicMultiChannelPT(
59+
ConstantPT(64*base_time, {'a': '-1.0 + y_gain'}),
60+
ConstantPT(64*base_time, {'b': '-0.5 + idx_b*0.02',}),
61+
)
62+
63+
dependent_constant2 = AtomicMultiChannelPT(
64+
ConstantPT(64*base_time, {'a': '-0.5 + y_gain'}),
65+
ConstantPT(64*base_time, {'b': '-0.3 + idx_b*0.02',}),
66+
)
67+
68+
69+
pt = (dependent_constant @ dependent_constant2.with_repetition(rep_factor) @ (wait.with_iteration('idx_a', rep_factor))).with_iteration('idx_b', rep_factor)\
70+
71+
self.pulse_template = MappingPT(pt,parameter_mapping=dict(y_gain=0.3,),
72+
channel_mapping={'a':'ZI0_A','b':'ZI0_C'},
73+
identifier=self.__class__.__name__
74+
)
75+
76+
77+
class SteppedRepetitionTest():
78+
def __init__(self,base_time=1e2,rep_factor=2):
79+
80+
wait = ConstantPT(f'64*{base_time}*(1+idx_t)', {'a': '-0.5 + idx_a * 0.15', 'b': '-.5 + idx_a * 0.3'})
81+
normal_pt = ParallelConstantChannelPT(FunctionPT("sin(t/1000)","t_sin",channel='a'),{'b':-0.2})
82+
amp_pt = ParallelConstantChannelPT("amp*1/8"*FunctionPT("sin(t/2000)","t_sin",channel='a'),{'b':-0.5})
83+
# amp_pt2 = ParallelConstantChannelPT("amp2*1/8"*FunctionPT("sin(t/1000)","t_sin",channel='a'),{'b':-0.5})
84+
amp_inner = ParallelConstantChannelPT(FunctionPT(f"(1+amp)*1/(2*{rep_factor})*sin(4*pi*t/t_sin)","t_sin",channel='a'),{'b':-0.5})
85+
amp_inner2 = ParallelConstantChannelPT(FunctionPT(f"(1+amp2)*1/(2*{rep_factor})*sin((1*freq)*4*pi*t/t_sin)+off/(2*{rep_factor})","t_sin",channel='a'),{'b':-0.3})
86+
87+
pt = ((((normal_pt@amp_inner2).with_iteration('off', rep_factor)@normal_pt@wait)\
88+
.with_repetition(rep_factor))@amp_inner.with_iteration('amp', rep_factor))\
89+
.with_iteration('amp2', rep_factor).with_iteration('freq', rep_factor).with_iteration('idx_a',rep_factor)
90+
91+
self.pulse_template = MappingPT(pt,parameter_mapping=dict(t_sin=64*base_time,idx_t=1,
92+
#idx_a=1,#freq=1,#amp2=1
93+
),
94+
channel_mapping={'a':'ZI0_A','b':'ZI0_C'},
95+
identifier=self.__class__.__name__)
96+
97+
class TimeSweepTest():
98+
def __init__(self,base_time=1e2,rep_factor=3):
99+
wait = ConstantPT(f'64*{base_time}*(1+idx_t)',
100+
{'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'})
101+
102+
random_constant = ConstantPT(64*base_time, {'a': -.4, 'b': -.3})
103+
meas = ConstantPT(64*base_time, {'a': 0.05, 'b': 0.06})
104+
105+
singlet_scan = (SequencePT(random_constant,wait,meas,identifier='s')).with_iteration('idx_a', rep_factor)\
106+
.with_iteration('idx_b', rep_factor)\
107+
.with_iteration('idx_t', rep_factor)
108+
109+
self.pulse_template = MappingPT(singlet_scan,channel_mapping={'a':'ZI0_A','b':'ZI0_C'},
110+
identifier=self.__class__.__name__)
111+
112+
113+
#%% Instantiate Checker
114+
115+
# select exemplary pulse
116+
pulse = ShortSingleRampTest(1e3+8)
117+
# pulse = ShortSingleRampTestWithPlay()
118+
# pulse = SequencedRepetitionTest(1e3,4)
119+
# pulse = SteppedRepetitionTest(1e2,3)
120+
# pulse = TimeSweepTest(1e2,3)
121+
122+
# Define a program builder to test program with:
123+
program_builder = LinSpaceBuilder(
124+
#set to True to ensure triggering at Program start if program starts with constant pulse
125+
play_marker_when_constant=True,
126+
#in case stepped repetitions are needed, insert variables here:
127+
to_stepping_repeat={'example',},
128+
)
129+
130+
# Data will be saved as xr.Dataset in save_path
131+
# data_offsets corrects for offsets in Alazar (not in saved data, only in plotting)
132+
checker = ExpectationChecker(ha, pulse.pulse_template,
133+
program_builder=program_builder,
134+
save_path=SAVE_HERE,
135+
data_offsets={'t_offset':-100.,'v_offset':0.008,'v_scale':0.9975}
136+
)
137+
138+
assert float(pulse.pulse_template.duration) < 1e7, "Ensure you know what you're doing when recording long data"
139+
140+
#%% Run the checker
141+
142+
checker.run()

qupulse/hardware/awgs/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def __init__(self, program: AllowedProgramTypes,
226226
if program_type == _ProgramType.Linspace:
227227
#!!! the voltage resolution may not be adequately represented if voltage transformations are not None?
228228
self._transformed_commands = self._transform_linspace_commands(to_increment_commands(program,))
229-
229+
230230
if waveforms is None:
231231
if program_type is _ProgramType.Loop:
232232
waveforms = OrderedDict((node.waveform, None)

qupulse/hardware/dacs/alazar.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,13 +390,16 @@ def register_mask_for_channel(self, mask_id: str, hw_channel: int, mask_type='au
390390
raise NotImplementedError('Currently only can do cross buffer mask')
391391
self._mask_prototypes[mask_id] = (hw_channel, mask_type)
392392

393-
def measure_program(self, channels: Iterable[str]) -> Dict[str, np.ndarray]:
393+
def measure_program(self, channels: Iterable[str] = None) -> Dict[str, np.ndarray]:
394394
"""
395395
Get all measurements at once and write them in a dictionary.
396396
"""
397397

398398
scanline_data = self.__card.extractNextScanline()
399-
399+
400+
if channels is None:
401+
channels = scanline_data.operationResults.keys()
402+
400403
scanline_definition = scanline_data.definition
401404
operation_definitions = {operation.identifier: operation
402405
for operation in scanline_definition.operations}

qupulse/hardware/setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ def register_program(self, name: str,
111111
raise TypeError('The provided run_callback is not callable')
112112

113113
if channels is None:
114-
channels = next(program.get_depth_first_iterator()).waveform.defined_channels
114+
# channels = next(program.get_depth_first_iterator()).waveform.defined_channels
115+
channels = program.get_defined_channels()
115116
if channels - set(self._channel_map.keys()):
116117
raise KeyError('The following channels are unknown to the HardwareSetup: {}'.format(
117118
channels - set(self._channel_map.keys())))

qupulse/plotting.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from qupulse.pulses.pulse_template import PulseTemplate
2929
from qupulse.program.waveforms import Waveform
3030
from qupulse.program.loop import Loop, to_waveform
31-
31+
from qupulse.program.linspace import LinSpaceTopLevel
3232

3333
__all__ = ["render", "plot", "PlottingNotPossibleException"]
3434

@@ -37,7 +37,9 @@ def render(program: Union[Loop],
3737
sample_rate: Real = 10.0,
3838
render_measurements: bool = False,
3939
time_slice: Tuple[Real, Real] = None,
40-
plot_channels: Optional[Set[ChannelID]] = None) -> Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
40+
plot_channels: Optional[Set[ChannelID]] = None,
41+
individualize_times: bool = False,
42+
) -> Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
4143
List[MeasurementWindow]]:
4244
"""'Renders' a pulse program.
4345
@@ -50,7 +52,7 @@ def render(program: Union[Loop],
5052
render_measurements: If True, the third return value is a list of measurement windows.
5153
time_slice: The time slice to be rendered. If None, the entire pulse will be shown.
5254
plot_channels: Only channels in this set are rendered. If None, all will.
53-
55+
individualize_times: return individual time-voltage array pairs for constant value cleanup
5456
Returns:
5557
A tuple (times, values, measurements). times is a numpy.ndarray of dimensions sample_count where
5658
containing the time values. voltages is a dictionary of one numpy.ndarray of dimensions sample_count per
@@ -100,10 +102,36 @@ def render(program: Union[Loop],
100102
for ch in channels}
101103
for ch, ch_voltage in voltages.items():
102104
waveform.get_sampled(channel=ch, sample_times=times, output_array=ch_voltage)
103-
105+
106+
107+
if individualize_times:
108+
# new_dict = {ch: (np.copy(times),volts) for ch,volts in voltages.items()}
109+
new_dict = {}
110+
for ch in channels:
111+
new_dict[ch] = deduplicate_with_aux(voltages[ch],np.copy(times),)
112+
return times, new_dict, measurements
113+
104114
return times, voltages, measurements
105115

106116

117+
def deduplicate_with_aux(arr, aux, threshold=1e-4):
118+
# Calculate the absolute differences between consecutive elements
119+
diffs = np.abs(np.diff(arr, prepend=arr[0]))
120+
121+
# Use cumsum to track the cumulative differences
122+
cumulative_diffs = np.cumsum(diffs)
123+
124+
# Find indices where cumulative differences exceed the threshold
125+
mask = np.concatenate(([0],np.where(np.diff(np.floor_divide(cumulative_diffs,threshold),prepend=cumulative_diffs[0])>0)[0],[-1]))
126+
127+
# Apply the mask to both the main and auxiliary arrays
128+
dedup_arr = arr[mask]
129+
dedup_aux = aux[mask]
130+
131+
return dedup_aux, dedup_arr
132+
133+
134+
107135
def _render_loop(loop: Loop,
108136
render_measurements: bool,) -> Tuple[Waveform, List[MeasurementWindow]]:
109137
"""Transform program into single waveform and measurement windows.
@@ -132,6 +160,7 @@ def plot(pulse: Union[PulseTemplate, Loop],
132160
stepped: bool=True,
133161
maximum_points: int=10**6,
134162
time_slice: Tuple[Real, Real]=None,
163+
individualize_times: bool = False,
135164
**kwargs) -> Any: # pragma: no cover
136165
"""Plots a pulse using matplotlib.
137166
@@ -182,7 +211,8 @@ def plot(pulse: Union[PulseTemplate, Loop],
182211
times, voltages, measurements = render(program,
183212
sample_rate,
184213
render_measurements=bool(plot_measurements),
185-
time_slice=time_slice)
214+
time_slice=time_slice,
215+
individualize_times=individualize_times)
186216
else:
187217
times, voltages, measurements = np.array([]), dict(), []
188218

@@ -214,9 +244,15 @@ def plot(pulse: Union[PulseTemplate, Loop],
214244
for ch_name, voltage in voltages.items():
215245
label = 'channel {}'.format(ch_name)
216246
if stepped:
217-
line, = axes.step(times, voltage, **{**dict(where='post', label=label), **kwargs})
247+
if individualize_times:
248+
line, = axes.step(voltage[0], voltage[1], **{**dict(where='post', label=label), **kwargs})
249+
else:
250+
line, = axes.step(times, voltage, **{**dict(where='post', label=label), **kwargs})
218251
else:
219-
line, = axes.plot(times, voltage, **{**dict(label=label), **kwargs})
252+
if individualize_times:
253+
axes.plot(voltage[0], voltage[1], **{**dict(label=label), **kwargs})
254+
else:
255+
line, = axes.plot(times, voltage, **{**dict(label=label), **kwargs})
220256
legend_handles.append(line)
221257

222258
if plot_measurements:
@@ -235,8 +271,8 @@ def plot(pulse: Union[PulseTemplate, Loop],
235271

236272
axes.legend(handles=legend_handles)
237273

238-
max_voltage = max((max(channel, default=0) for channel in voltages.values()), default=0)
239-
min_voltage = min((min(channel, default=0) for channel in voltages.values()), default=0)
274+
max_voltage = max((max(channel if not individualize_times else channel[1], default=0) for channel in voltages.values()), default=0)
275+
min_voltage = min((min(channel if not individualize_times else channel[1], default=0) for channel in voltages.values()), default=0)
240276

241277
# add some margins in the presentation
242278
axes.set_xlim(-0.5+time_slice[0], time_slice[1] + 0.5)

qupulse/program/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from typing import Optional, Union, Sequence, ContextManager, Mapping, Tuple, Generic, TypeVar, Iterable, Dict
2-
from typing import Protocol, runtime_checkable
2+
from typing import Protocol, runtime_checkable, Set
33

44
from qupulse._program.waveforms import Waveform
55
from qupulse.utils.types import MeasurementWindow, TimeType
66
from qupulse._program.volatile import VolatileRepetitionCount
77
from qupulse.parameter_scope import Scope
88
from qupulse.expressions import sympy as sym_expr
99
from qupulse.expressions.simple import SimpleExpression
10-
10+
from qupulse import ChannelID
1111

1212
RepetitionCount = Union[int, VolatileRepetitionCount, SimpleExpression[int]]
1313
HardwareTime = Union[TimeType, SimpleExpression[TimeType]]
@@ -74,7 +74,7 @@ def with_iteration(self, index_name: str, rng: range,
7474
def evaluate_nested_stepping(self, scope: Scope, parameter_names: set[str]) -> bool:
7575
return False
7676

77-
def to_program(self) -> Optional[Program]:
77+
def to_program(self, defined_channels: Set[ChannelID]) -> Optional[Program]:
7878
"""Further addition of new elements might fail after finalizing the program."""
7979

8080

0 commit comments

Comments
 (0)