From 5f27fa614e857e5ee78fcec54f293cb68dff45b2 Mon Sep 17 00:00:00 2001
From: Sander de Snoo <59472150+sldesnoo-Delft@users.noreply.github.com>
Date: Mon, 21 Nov 2022 10:51:03 +0100
Subject: [PATCH] First version of new Scan class

---
 CHANGELOG.md                        |   5 +
 core_tools/sweeps/scans.py          | 319 ++++++++++++++++++++++++++++
 examples/demo_station/scans_demo.py |  82 +++++++
 3 files changed, 406 insertions(+)
 create mode 100644 core_tools/sweeps/scans.py
 create mode 100644 examples/demo_station/scans_demo.py

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70f11210..8a52d384 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
 # Changelog
 All notable changes to core_tools will be documented in this file.
 
+## \[1.4.8] - 2022-11-20
+
+- Improved performance of measurement for measurements > 30 s.
+- Added first version of new Scan class.
+
 ## \[1.4.7] - 2022-11-18
 
 - Fixed import of NumpyJSONEncoder for new qcodes versions
diff --git a/core_tools/sweeps/scans.py b/core_tools/sweeps/scans.py
new file mode 100644
index 00000000..0a554e32
--- /dev/null
+++ b/core_tools/sweeps/scans.py
@@ -0,0 +1,319 @@
+import logging
+import time
+import numpy as np
+
+from core_tools.data.measurement import Measurement
+from core_tools.sweeps.progressbar import progress_bar
+from pulse_lib.sequencer import sequencer
+#from core_tools.sweeps.sweep_utility import KILL_EXP
+
+class Break(Exception):
+    # TODO @@@ allow loop parameter to break to
+    def __init__(self, msg, loops=None):
+        super().__init__(msg)
+        self._loops = loops
+
+    def exit_loop(self):
+        if self._loops is None:
+            return True
+        self._loops -= 1
+        return self._loops > 0
+
+class Action:
+    def __init__(self, name, delay=0.0):
+        self._delay = delay
+        self.name = name
+
+    @property
+    def delay(self):
+        return self._delay
+
+class Setter(Action):
+    def __init__(self, param, n_points, delay=0.0, resetable=True):
+        super().__init__(f'set {param.name}', delay)
+        self._param = param
+        self._n_points = n_points
+        self._resetable = resetable
+
+    @property
+    def param(self):
+        return self._param
+
+    @property
+    def n_points(self):
+        return self._n_points
+
+    @property
+    def resetable(self):
+        return self._resetable
+
+    def __iter__(self):
+        raise NotImplementedError()
+
+class Getter(Action):
+    def __init__(self, param, delay=0.0):
+        super().__init__(f'get {param.name}', delay)
+        self._param = param
+
+    @property
+    def param(self):
+        return self._param
+
+
+class Function(Action):
+    def __init__(self, func, *args, delay=0.0, add_dataset=False,
+                 add_last_values=False, **kwargs):
+        super().__init__(f'do {func.__name__}', delay)
+        self._func = func
+        self._add_dataset = add_dataset
+        self._add_last_values = add_last_values
+        self._args = args
+        self._kwargs = kwargs
+
+    @property
+    def add_dataset(self):
+        return self._add_dataset
+
+    def __call__(self, dataset, last_values):
+        if self._add_dataset or self._add_last_values:
+            kwargs = self._kwargs.copy()
+        else:
+            kwargs = self._kwargs
+        if self._add_dataset:
+            kwargs['dataset'] = dataset
+        if self._add_last_values:
+            kwargs['last_values'] = {
+                    param.name:value
+                    for param,value in last_values
+                    if param is not None
+                    }
+        self._func(*self._args, **kwargs)
+
+
+def _start_sequence(sequence):
+    sequence.upload()
+    sequence.play()
+
+
+class ArraySetter(Setter):
+    def __init__(self, param, data, delay=0.0, resetable=True):
+        super().__init__(param, len(data), delay, resetable)
+        self._data = data
+
+    def __iter__(self):
+        for value in self._data:
+            yield value
+
+
+def sweep(parameter, data, stop=None, n_points=None, delay=0.0, resetable=True):
+    if stop is not None:
+        start = data
+        data = np.linspace(start, stop, n_points)
+    return ArraySetter(parameter, data, delay, resetable)
+
+
+class Scan:
+    def __init__(self, *args, name='', reset_param=False, silent=False):
+        self.name = name
+        self.reset_param = reset_param
+        self.silent = silent
+
+        self.actions = []
+        self.meas = Measurement(self.name, silent=silent)
+
+        self.setters = []
+        self.m_instr = []
+
+        for arg in args:
+            if isinstance(arg, Setter):
+                self.setters.append(arg)
+                self.actions.append(arg)
+            elif isinstance(arg, sequencer):
+                # TODO @@@@ check order of parameters
+                seq_params = arg.params
+                for var in seq_params:
+                    setter = ArraySetter(var, var.values, resetable=False)
+                    self.setters.append(setter)
+                    self.actions.append(setter)
+                self.actions.append(Function(_start_sequence, arg))
+                self.meas.add_snapshot('sequence', arg.metadata)
+                if hasattr(arg, 'starting_lambda'):
+                    print('WARNING: sequencer starting_lambda is not supported anymore')
+            elif isinstance(arg, Getter):
+                self.actions.append(arg)
+                self.m_instr.append(arg.param)
+            elif isinstance(arg, Function):
+                self.actions.append(arg)
+            else:
+                self.actions.append(Getter(arg))
+                self.m_instr.append(arg)
+
+        set_params = []
+        self.n_tot = 1
+        for setter in self.setters:
+            self.meas.register_set_parameter(setter.param, setter.n_points)
+            set_params.append(setter.param)
+            self.n_tot *= setter.n_points
+
+        for instr in self.m_instr:
+            self.meas.register_get_parameter(instr, *set_params)
+
+        if name == '':
+            if len(self.setters) == 0:
+                self.name = '0D_' + self.m_instr[0].name[:10]
+            else:
+                self.name += '{}D_'.format(len(self.setters))
+
+        self.meas.name = self.name
+
+    def run(self):
+        try:
+            n_params = len(self.setters) + len(self.m_instr)
+            start = time.perf_counter()
+            with self.meas as m:
+                runner = Runner(m, self.actions, n_params, self.n_tot)
+                runner.run(self.reset_param, self.silent)
+            duration = time.perf_counter() - start
+            logging.info(f'Total duration: {duration:5.2f} s ({duration/self.n_tot*1000:5.1f} ms/pt)')
+        except Break as b:
+            logging.warning(f'Measurement break: {b}')
+#        except KILL_EXP: # TODO @@@ check job_manager
+#            logging.warning('Measurement aborted')
+        except KeyboardInterrupt:
+            logging.warning('Measurement interrupted')
+            raise KeyboardInterrupt('Measurement interrupted') from None
+        except Exception as ex:
+            print(f'\n*** ERROR in measurement: {ex}')
+            logging.error('Exception in measurement', exc_info=True)
+            raise
+
+        return self.meas.dataset
+
+    def put(self):
+        TODO()
+
+
+class Runner:
+    def __init__(self, measurement, actions, n_param, n_tot):
+        self._measurement = measurement
+        self._actions = actions
+        self._n_tot = n_tot
+        self._n = 0
+        self._data = [[None,None]]*n_param
+        self._action_duration = [0.0]*len(actions)
+        self._store_duration = 0.0
+
+    def run(self, reset_param=False, silent=False):
+        if reset_param:
+            start_values = self._get_start_values()
+        self._n_data = 0
+        self.pbar = progress_bar(self._n_tot) if not silent else None
+        try:
+            self._loop()
+        except:
+            last_index = {
+                param.name:data
+                for action,(param,data) in zip(self._actions, self._data)
+                if isinstance(action, Setter)
+                }
+            msg = f'Measurement stopped at {last_index}'
+            if not silent:
+                print('\n'+msg, flush=True)
+            logging.info(msg)
+            raise
+        finally:
+            if self.pbar is not None:
+                self.pbar.close()
+            if reset_param:
+                self._reset_params(start_values)
+
+    def _get_start_values(self):
+        return [
+                (action.param, action.param())
+                if isinstance(action, Setter) and action.resetable else (None,None)
+                for action in self._actions
+                ]
+
+    def _reset_params(self, start_values):
+        for param,value in start_values:
+            if param is not None:
+                try:
+                    param(value)
+                except:
+                    logging.error(f'Failed to reset parameter {param.name}')
+
+    def _loop(self, iaction=0, iparam=0):
+        if iaction == len(self._actions):
+            # end of action list: store results
+            self._push_results()
+            self._inc_count()
+#            if self.KILL:
+#                raise KILL_EXP
+            return
+
+        action = self._actions[iaction]
+        if isinstance(action, Setter):
+            self._loop_setter(action, iaction, iparam)
+            return
+
+        try:
+            t_start = time.perf_counter()
+            next_param = iparam
+            if isinstance(action, Getter):
+                next_param += 1
+                value = action.param()
+                self._data[iparam] = [action.param, value]
+
+            elif isinstance(action, Function):
+                if action.add_dataset:
+                    self._push_results(iparam)
+                action(self._measurement.dataset, self._data)
+
+            if action._delay:
+                time.sleep(action._delay)
+            self._action_duration[iaction] += time.perf_counter()-t_start
+            self._loop(iaction+1, next_param)
+        except Break:
+            for i in range(iparam, len(self._data)):
+                self._data[i][1] = None
+            self._push_results()
+            raise
+
+    def _loop_setter(self, action, iaction, iparam):
+        for value in action:
+            try:
+                t_start = time.perf_counter()
+                action.param(value)
+                value = action.param()
+                self._data[iparam] = [action.param, value]
+                if action._delay:
+                    time.sleep(action._delay)
+                self._action_duration[iaction] += time.perf_counter()-t_start
+                self._loop(iaction+1, iparam+1)
+            except Break as b:
+                if b.exit_loop():
+                    raise
+                # TODO @@@ fill missing data?? dataset must be rectangular/box
+                # what should be the values for the setters when they are not actually set?
+
+    def _inc_count(self):
+        self._n += 1
+        if self.pbar is not None:
+            self.pbar += 1
+        n = self._n
+        if n % 10 == 0:
+            t_actions = {action.name:f'{self._action_duration[i]*1000/n:4.1f}'
+                         for i,action in enumerate(self._actions)}
+            t_store = self._store_duration*1000/n
+            logging.debug(f'npt:{n} actions: {t_actions} store:{t_store:5.1f} ms')
+
+    def _push_results(self, iparam=None):
+        t_start = time.perf_counter()
+        if iparam is not None:
+            data = self._data[self._n_data:iparam]
+            self._n_data = iparam
+        else:
+            data = self._data[self._n_data:]
+            self._n_data = 0
+        self._measurement.add_result(*data)
+        self._store_duration += time.perf_counter()-t_start
diff --git a/examples/demo_station/scans_demo.py b/examples/demo_station/scans_demo.py
new file mode 100644
index 00000000..0b9fffa2
--- /dev/null
+++ b/examples/demo_station/scans_demo.py
@@ -0,0 +1,82 @@
+import core_tools as ct
+from core_tools.sweeps.scans import Scan, sweep, Function, Break
+import qcodes as qc
+from qcodes import ManualParameter
+from qcodes.parameters.specialized_parameters import ElapsedTimeParameter
+
+ct.configure('./setup_config/ct_config_measurement.yaml')
+
+ct.launch_databrowser()
+
+station = qc.Station()
+
+x = ManualParameter('x', initial_value=0)
+y = ManualParameter('y', initial_value=9)
+
+t = ElapsedTimeParameter('t')
+
+#%%
+
+ds1 = Scan(
+        sweep(x, -20, 20, 11, delay=0.01),
+        t,
+        name='test_scan',
+        silent=True,
+        ).run()
+
+
+#%%
+t.reset_clock()
+
+ds_inner = []
+def inner_scan():
+    ds = Scan(
+        sweep(x, -20, 20, 11, delay=0.01),
+        t,
+        name='test_inner_scan',
+        silent=True,
+        ).run()
+    ds_inner.append(ds)
+
+ds2 = Scan(
+        sweep(y, -1, 1, 3, delay=0.2),
+        Function(inner_scan),
+        t,
+        reset_param=True).run()
+
+
+#%%
+def check_x(last_values, dataset):
+    max_x = max(dataset.m1.x())
+    if max_x > 4:
+        raise Break(f'max x = {max_x}. Last {last_values}')
+
+ds3 = Scan(
+        sweep(x, -20, 20, 11, delay=0.1),
+        t,
+        Function(check_x, add_dataset=True, add_last_values=True),
+        name='test_break').run()
+
+#%%
+def check_t(last_values):
+    # abort after 0.5 s
+    t = last_values['t']
+    if t > 0.5:
+        raise Break(f't={t:5.2f} s')
+
+t.reset_clock()
+
+ds4 = Scan(
+        sweep(x, -20, 20, 11, delay=0.1),
+        sweep(y, -1, 1, 3),
+        t,
+        Function(check_t, add_last_values=True),
+        name='test_break_2D').run()
+
+
+#%%
+ds5 = Scan(
+        sweep(x, -20, 20, 21),
+        sweep(y, -10, 10, 41, delay=0.001),
+        t,
+        name='test_2D').run()
-- 
GitLab