Skip to content

Commit a8f1c3d

Browse files
rsbrosti-am-mounceammounce
authored
Continuous measurement (#214)
* core(in-progress): saving a checkmark of work towards implementing a continuous expt feature. Much of the heavy lifting has been done, but changes still need to be verified, validated, and added to our automatic testing if possible. File changes to abstract expt include new continuous preallocate, reallocate, and continuous save point methods; to runinfo include a new continuous_expt boolean attribute, and to experiment.py allow for continuous expt runs. * most minor change to new naming convention. * feat(core): adding ability for continuous measurement expts. Now by setting runinfo.continuous_expt to True, experimentalists can run their experiment designs continuously, and continue to extend their datasets indefinitely. This needs to be tested for how long/what number of continuous experiments can be run before the reallocation resizing bogs down, if ever. Also may want to consider changing the continuous_count var in experiment.py to a runinfo attribute to keep track of the number of repetitions without have to infer it through extraction of dataset size. I will likely find a solution for this where it will only added to continuous expts to not include it unnecessarily in the metadata of other experiments. Furthermore, not all data types/sizes have been tested with our new continuous_save_point function, and so one could still encounter bugs using this feature as we continue to flush it out. If so, please report them along with the circumstance you encountered it with in addition to the error message. * chore(core): fixed minor issues, naming convention, and added runinfo.run_count attribute for continuous experiments. * chore(core): fixed one more layer of continuous_preallocate; however, the lower layers likely still need updating. Furthermore, modifying experiment to be able to test the increasing lag times between experiments. Pushing to test on cint-transport overnight, hopefully this will provide a consistent testing environment since my computer was lagging out and artificially increasing runtimes temporarily. * fix(measurement): updated continuous experiment to account for desired data formatting protocols. * feat(core): implemented continuous measurement ability which works at all scan levels. Still need to ensure that it is the highest level scan; however, despite needing this check the continuous scan works well when set up properly. * fix(core): Now continuous scan must be the highest level scan and not have empty scans below it. --Make sure this is what we want! * chore(core): fix runinfo flake8 issue. * refactor(core): feat continuous measurement scan now implemented with minimal changes to experiment.py. Furthermore, new data points are now not just saved to the hdf5 file, but captured by local arrays. * fix(measurement): updated continuous scan to append values to attributes, write to proper locations in hdf5 of 1 value at a time, and allow for continuous experiments to have a pre-selected stop_at value for testing and/or increased experimental control. * test(measurement)(scans): adding notebook for testing continuous scans timing over long runs (out to 100,000) with different kinds of experiment set ups, as well as plotting their deltas. * chore(test): removing continuous_scan_time_test.ipynb notebook since it is not something we want to pull to the main branch and was simply for us to evaluate the efficacy of continuous scan as a method. * test(refactor): updated tests to reflect expected behavior of saving protocols in abstract expt. * fix(refactor)(measurement)(tests): fixed bug in save_point where average experiments would be treated as generic experiments. * fixed sparse sweep but restoring old working condition of continuous scan saving. * chore(measurement): updating continuous scan to use n_max instead of stop at. Adding runinfo attributes for replacing commonly used continuous scan required functions. Also refactoring check for empty property scan between continuous scans. * chore(core): updated runinfo to match bug fix pr. * refactor(core)(measurement): refactored abstract experiments save point, and runinfo to streamline code calls and partition functionalities into a more organized architecture. * fix(core): fixed errors with abstract and runinfo changes causes test cases to fail. * refactor(measurement): continuing to refactor for continuous scan and measurement changes. Removed continuous_dims and continuous ndims from runinfo for a work around. Still determining if scan.n can be incremented. * feat(core)(measurement): average experiment can now be run continuously by implementing a continuous scan. * fix(measurement): fixing runinfo attribute to no longer fail sparse expt test. * refactor(measurement)(docs): refactored continuous scan iterator to return incrementing integers (0, 1, 2, 3...) instead of endless 0s. Furthermore added a doc string for ContinuousScan and its iterator method. Also updated doc strings to have improved formatting and added one for AbstractScans iterator method as well. * refactor(measurement): updating continuous scan and measurement to use i and n attributes rather than run_count. Both are incrementing infinitely in the iterate function. Updated the docstring for this. * fix(plotting)(docs): added continuous measurement scan demo nb with liveplotting. Updated basic_plots.py to support this. Renamed advanced demo notebooks numbering sequence to begin with an a. * chore: renaming advanced demo notebook to account for advanced demo nb 02 that jasmine already added. * chore: renaming a03 notebook in file title to match file name. --------- Co-authored-by: i-am-mounce <[email protected]> Co-authored-by: [email protected] <[email protected]>
1 parent 786b025 commit a8f1c3d

File tree

12 files changed

+712
-109
lines changed

12 files changed

+712
-109
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ignore=E266,E722,W503,C901
1616
; exclude=
1717

1818
# F401: module imported but unused
19-
#F403 unable to detect undefined names
19+
# F403 unable to detect undefined names
2020
per-file-ignores =
2121
pyscan/__init__.py:F401,F403
2222
pyscan/measurement/__init__.py:F401,F403
File renamed without changes.

demo_notebooks/advanced/a03-example_continuous_scan.ipynb

Lines changed: 276 additions & 0 deletions
Large diffs are not rendered by default.

pyscan/measurement/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .fast_stage_experiment import FastStageSweep, FastStageExperiment
1111
from .fast_galvo_experiment import FastGalvoSweep, FastGalvoExperiment
1212

13-
from .scans import PropertyScan, RepeatScan, FunctionScan, AverageScan
13+
from .scans import PropertyScan, RepeatScan, ContinuousScan, FunctionScan, AverageScan
1414

1515
# Other objects
1616
from .run_info import RunInfo

pyscan/measurement/abstract_experiment.py

Lines changed: 253 additions & 51 deletions
Large diffs are not rendered by default.

pyscan/measurement/experiment.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def generic_experiment(self):
4242
self.runinfo['t{}'.format(i)] = np.zeros(self.runinfo.dims)
4343

4444
t0 = (datetime.now()).timestamp()
45+
4546
# Use for scan, but break if self.runinfo.running=False
4647
for m in self.runinfo.scan3.iterator():
4748
self.runinfo.scan3.i = m
@@ -81,20 +82,12 @@ def generic_experiment(self):
8182
self.runinfo.t3[indicies] = (datetime.now()).timestamp()
8283

8384
if np.all(np.array(self.runinfo.indicies) == 0):
84-
self.runinfo.measured = []
85-
for key, value in data.items():
86-
self.runinfo.measured.append(key)
8785
self.preallocate(data)
8886

89-
for key, value in data.items():
90-
if is_list_type(self[key]):
91-
self[key][self.runinfo.indicies] = value
92-
else:
93-
self[key] = value
9487
if self.runinfo.time:
9588
self.runinfo.t4[indicies] = (datetime.now()).timestamp()
9689

97-
self.save_point()
90+
self.save_point(data)
9891

9992
if self.runinfo.time:
10093
self.runinfo.t5[indicies] = (datetime.now()).timestamp()
@@ -103,21 +96,33 @@ def generic_experiment(self):
10396
self.runinfo.complete = 'stopped'
10497
break
10598

99+
if isinstance(self.runinfo.scan0, ps.ContinuousScan):
100+
self.reallocate()
101+
106102
# Check if complete, stopped early
107103
if self.runinfo.running is False:
108104
self.runinfo.complete = 'stopped'
109105
break
110106

107+
if isinstance(self.runinfo.scan1, ps.ContinuousScan):
108+
self.reallocate()
109+
111110
if self.runinfo.running is False:
112111
self.runinfo.complete = 'stopped'
113112
break
114113

114+
if isinstance(self.runinfo.scan2, ps.ContinuousScan):
115+
self.reallocate()
116+
115117
if self.runinfo.verbose:
116118
print('Scan {}/{} Complete'.format(m + 1, self.runinfo.scan3.n))
117119
if self.runinfo.running is False:
118120
self.runinfo.complete = 'stopped'
119121
break
120122

123+
if isinstance(self.runinfo.scan3, ps.ContinuousScan):
124+
self.reallocate()
125+
121126
self.runinfo.complete = True
122127
self.runinfo.running = False
123128

@@ -148,22 +153,22 @@ def average_experiment(self):
148153
return
149154

150155
# Use for scan, but break if self.runinfo.running=False
151-
for m in range(self.runinfo.scan3.n):
156+
for m in self.runinfo.scan3.iterator():
152157
self.runinfo.scan3.i = m
153158
self.runinfo.scan3.iterate(m, self.devices)
154159
sleep(self.runinfo.scan3.dt)
155160

156-
for k in range(self.runinfo.scan2.n):
161+
for k in self.runinfo.scan2.iterator():
157162
self.runinfo.scan2.i = k
158163
self.runinfo.scan2.iterate(k, self.devices)
159164
sleep(self.runinfo.scan2.dt)
160165

161-
for j in range(self.runinfo.scan1.n):
166+
for j in self.runinfo.scan1.iterator():
162167
self.runinfo.scan1.i = j
163168
self.runinfo.scan1.iterate(j, self.devices)
164169
sleep(self.runinfo.scan1.dt)
165170

166-
for i in range(self.runinfo.scan0.n):
171+
for i in self.runinfo.scan0.iterator():
167172
self.runinfo.scan0.i = i
168173
self.runinfo.scan0.iterate(i, self.devices)
169174
sleep(self.runinfo.scan0.dt)
@@ -172,9 +177,6 @@ def average_experiment(self):
172177

173178
# if on the first row of data, log the data names in self.runinfo.measured
174179
if np.all(np.array(self.runinfo.indicies) == 0):
175-
self.runinfo.measured = []
176-
for key, value in data.items():
177-
self.runinfo.measured.append(key)
178180
self.preallocate(data)
179181

180182
self.rolling_average(data)
@@ -183,24 +185,36 @@ def average_experiment(self):
183185
self.runinfo.complete = 'stopped'
184186
break
185187

186-
self.save_point()
188+
self.save_point(data)
189+
190+
if isinstance(self.runinfo.scan0, ps.ContinuousScan):
191+
self.reallocate()
187192

188-
# self.save_row()
193+
# self.save_row(data)
189194

190195
# Check if complete, stopped early
191196
if self.runinfo.running is False:
192197
self.runinfo.complete = 'stopped'
193198
break
194199

200+
if isinstance(self.runinfo.scan1, ps.ContinuousScan):
201+
self.reallocate()
202+
195203
if self.runinfo.running is False:
196204
self.runinfo.complete = 'stopped'
197205
break
198206

207+
if isinstance(self.runinfo.scan2, ps.ContinuousScan):
208+
self.reallocate()
209+
199210
print('Scan {}/{} Complete'.format(m + 1, self.runinfo.scan3.n))
200211
if self.runinfo.running is False:
201212
self.runinfo.complete = 'stopped'
202213
break
203214

215+
if isinstance(self.runinfo.scan3, ps.ContinuousScan):
216+
self.reallocate()
217+
204218
self.runinfo.complete = True
205219
self.runinfo.running = False
206220

pyscan/measurement/run_info.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pyscan.general.item_attribute import ItemAttribute
33
from pyscan.general.get_pyscan_version import get_pyscan_version
44
from .scans import PropertyScan, AverageScan
5+
import pyscan as ps
56

67

78
class RunInfo(ItemAttribute):
@@ -62,6 +63,9 @@ def __init__(self):
6263
self.initial_pause = 0.1
6364
self.average_d = -1
6465

66+
# Assumed not a continuous expt. If there is a continuous scan this will be set to true in self.check()
67+
self.continuous = False
68+
6569
self.verbose = False
6670
self._pyscan_version = get_pyscan_version()
6771

@@ -102,6 +106,32 @@ def check(self):
102106
(f"Found empty PropertyScan (scan{count_down}) below used scan (scan{used_scan_index}).\n"
103107
+ "Scans must be populated in sequential order.")
104108

109+
# find the scan set to continuous scan (if any) and determine the index
110+
for i, scan in enumerate(self.scans):
111+
if isinstance(scan, ps.ContinuousScan):
112+
self.continuous = True
113+
self.continuous_scan_index = i
114+
115+
# If there is a ContinuousScan, ensure it is the highest level scan
116+
if self.continuous:
117+
for i in range(self.continuous_scan_index + 1, len(self.scans)):
118+
assert isinstance(self.scans[i], PropertyScan) and len(self.scans[i].input_dict) == 0, \
119+
f"ContinuousScan found at scan{self.continuous_scan_index} but is not the highest level scan."
120+
121+
def stop_continuous(self, plus_one=False):
122+
stop = False
123+
if self.continuous:
124+
continuous_scan = self.scans[self.continuous_scan_index]
125+
if hasattr(continuous_scan, 'n_max'):
126+
if plus_one is False:
127+
if continuous_scan.n_max <= continuous_scan.i:
128+
stop = True
129+
elif plus_one is True:
130+
if continuous_scan.n_max <= continuous_scan.i + 1:
131+
stop = True
132+
133+
return stop
134+
105135
@property
106136
def scans(self):
107137
''' Returns array of all scans
@@ -117,6 +147,10 @@ def dims(self):
117147
self.scan2.n,
118148
self.scan3.n)
119149
dims = [n for n in dims if n != 1]
150+
if self.continuous:
151+
if len(dims) - 1 == self.continuous_scan_index:
152+
dims = dims[:-1]
153+
dims.append(1)
120154
self._dims = tuple(dims)
121155
return self._dims
122156

@@ -132,7 +166,7 @@ def average_dims(self):
132166
def ndim(self):
133167
''' Returns number of non 1 sized scans
134168
'''
135-
self._ndim = len(self.dims) # why is this stored as a property? It is never used
169+
self._ndim = len(self.dims)
136170
return self._ndim
137171

138172
@property
@@ -152,6 +186,8 @@ def indicies(self):
152186
self.scan2.i,
153187
self.scan3.i)
154188
self._indicies = self._indicies[:self.ndim]
189+
if self.continuous:
190+
self._indicies = self._indicies[:-1]
155191
return tuple(self._indicies)
156192

157193
@property

pyscan/measurement/scans.py

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@ class AbstractScan(ItemAttribute):
1111

1212
def iterate(self, index, devices):
1313
'''
14-
A function to be implemented by inheriting Scan classes
14+
A function to be implemented by inheriting Scan classes.
1515
'''
1616
pass
1717

1818
def check_same_length(self):
19-
'''A function to be implemented by inheriting Scan classes'''
19+
'''
20+
A function to be implemented by inheriting Scan classes.
21+
'''
2022
pass
2123

2224
# This must be a method and not an attribute as iterators can only be used once
2325
def iterator(self):
26+
'''
27+
Returns an iterator for the scan over its n range.
28+
'''
2429
return iter(range(self.n))
2530

2631

@@ -72,7 +77,8 @@ def iterate(self, index, devices):
7277
continue
7378

7479
def check_same_length(self):
75-
'''Check that the input_dict has values that are arrays of the same length
80+
'''
81+
Check that the input_dict has values that are arrays of the same length.
7682
'''
7783

7884
if len(list(self.scan_dict.keys())) > 0:
@@ -85,7 +91,8 @@ def check_same_length(self):
8591

8692

8793
class FunctionScan(AbstractScan):
88-
'''Class for iterating a function with input values inside an
94+
'''
95+
Class for iterating a function with input values inside an
8996
experimental loop. Inherits from `pyscan.measurement.scans.AbstractScan`.
9097
9198
Parameters
@@ -141,10 +148,11 @@ class RepeatScan(AbstractScan):
141148
'''
142149

143150
def __init__(self, nrepeat, dt=0):
144-
'''Constructor method
151+
'''
152+
Constructor method.
145153
'''
146154
assert nrepeat > 0, "nrepeat must be > 0"
147-
assert nrepeat != np.inf, "nrepeat is np.inf"
155+
assert nrepeat != np.inf, "nrepeat is np.inf, make a continuous scan instead."
148156
self.scan_dict = {}
149157
self.scan_dict['repeat'] = list(range(nrepeat))
150158

@@ -156,7 +164,8 @@ def __init__(self, nrepeat, dt=0):
156164
self.i = 0
157165

158166
def iterate(self, index, devices):
159-
'''Iterates repeat loop
167+
'''
168+
Iterates repeat loop.
160169
'''
161170

162171
# Need a method here to iterate infinitely/continuously.
@@ -170,8 +179,62 @@ def check_same_length(self):
170179
return 1
171180

172181

182+
class ContinuousScan(AbstractScan):
183+
'''
184+
Class for performing a continuous scan, which runs indefinitely or until a specified maximum number of iterations.
185+
Inherits from `pyscan.measurement.scans.AbstractScan`.
186+
187+
Parameters
188+
----------
189+
dt : float, optional
190+
Wait time in seconds after each iteration. Used by experiment classes, defaults to 0.
191+
n_max : int, optional
192+
Maximum number of iterations to run. If not specified, the scan will run indefinitely.
193+
'''
194+
195+
def __init__(self, dt=0, n_max=None):
196+
self.scan_dict = {}
197+
self.scan_dict['continuous'] = []
198+
199+
self.device_names = ['continuous']
200+
self.dt = dt
201+
202+
self.run_count = 0
203+
# essentially run_count
204+
self.n = 1
205+
# current experiment number index
206+
self.i = 0
207+
if n_max is not None:
208+
self.n_max = n_max
209+
210+
def iterate(self, index, devices):
211+
self.run_count += 1
212+
213+
if hasattr(self, "stop_at"):
214+
if not self.n_max <= self.i:
215+
self.scan_dict['continuous'].append(self.i)
216+
else:
217+
self.scan_dict['continuous'].append(self.i)
218+
219+
def iterator(self):
220+
'''
221+
The following iterator increments continuous scan i and n by one each time continuously.
222+
'''
223+
def incrementing_n():
224+
while True:
225+
yield self.i
226+
self.i += 1
227+
self.n += 1
228+
229+
iterator = iter(incrementing_n())
230+
231+
# returns an infinite iterator, overwriting Abstract scans default iterator
232+
return iterator
233+
234+
173235
class AverageScan(AbstractScan):
174-
'''Class for averaging inner loops.
236+
'''
237+
Class for averaging inner loops.
175238
176239
Parameters
177240
----------

pyscan/measurement/sparse_experiment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def run(self):
8484
else:
8585
self[key] = value
8686

87-
self.save_point()
87+
self.save_point(data)
8888

8989
if self.runinfo.running is False:
9090
self.runinfo.complete = 'stopped'

0 commit comments

Comments
 (0)