Coverage for /Users/Newville/Codes/xraylarch/larch/epics/larchscan.py: 33%

82 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-09 10:08 -0600

1#!/usr/bin/env python 

2from __future__ import print_function 

3 

4MODDOC = """ 

5=== Epics Scanning Functions for Larch === 

6 

7 

8This does not used the Epics SScan Record, and the scan is intended to run 

9as a python application, but many concepts from the Epics SScan Record are 

10borrowed. Where appropriate, the difference will be noted here. 

11 

12A Step Scan consists of the following objects: 

13 a list of Positioners 

14 a list of Triggers 

15 a list of Counters 

16 

17Each Positioner will have a list (or numpy array) of position values 

18corresponding to the steps in the scan. As there is a fixed number of 

19steps in the scan, the position list for each positioners must have the 

20same length -- the number of points in the scan. Note that, unlike the 

21SScan Record, the list of points (not start, stop, step, npts) must be 

22given. Also note that the number of positioners or number of points is not 

23limited. 

24 

25A Trigger is simply an Epics PV that will start a particular detector, 

26usually by having 1 written to its field. It is assumed that when the 

27Epics ca.put() to the trigger completes, the Counters associated with the 

28triggered detector will be ready to read. 

29 

30A Counter is simple a PV whose value should be recorded at every step in 

31the scan. Any PV can be a Counter, including waveform records. For many 

32detector types, it is possible to build a specialized class that creates 

33many counters. 

34 

35Because Triggers and Counters are closely associated with detectors, a 

36Detector is also defined, which simply contains a single Trigger and a list 

37of Counters, and will cover most real use cases. 

38 

39In addition to the core components (Positioners, Triggers, Counters, Detectors), 

40a Step Scan contains the following objects: 

41 

42 breakpoints a list of scan indices at which to pause and write data 

43 collected so far to disk. 

44 extra_pvs a list of (description, PV) tuples that are recorded at 

45 the beginning of scan, and at each breakpoint, to be 

46 recorded to disk file as metadata. 

47 pre_scan() method to run prior to scan. 

48 post_scan() method to run after scan. 

49 at_break() method to run at each breakpoint. 

50 

51Note that Postioners and Detectors may add their own pieces into extra_pvs, 

52pre_scan(), post_scan(), and at_break(). 

53 

54With these concepts, a Step Scan ends up being a fairly simple loop, going 

55roughly (that is, skipping error checking) as: 

56 

57 pos = <DEFINE POSITIONER LIST> 

58 det = <DEFINE DETECTOR LIST> 

59 run_pre_scan(pos, det) 

60 [p.move_to_start() for p in pos] 

61 record_extra_pvs(pos, det) 

62 for i in range(len(pos[0].array)): 

63 [p.move_to_pos(i) for p in pos] 

64 while not all([p.done for p in pos]): 

65 time.sleep(0.001) 

66 [trig.start() for trig in det.triggers] 

67 while not all([trig.done for trig in det.triggers]): 

68 time.sleep(0.001) 

69 [det.read() for det in det.counters] 

70 

71 if i in breakpoints: 

72 write_data(pos, det) 

73 record_exrta_pvs(pos, det) 

74 run_at_break(pos, det) 

75 write_data(pos, det) 

76 run_post_scan(pos, det) 

77 

78Note that multi-dimensional mesh scans over a rectangular grid is not 

79explicitly supported, but these can be easily emulated with the more 

80flexible mechanism of unlimited list of positions and breakpoints. 

81Non-mesh scans are also possible. 

82 

83A step scan can have an Epics SScan Record or StepScan database associated 

84with it. It will use these for PVs to post data at each point of the scan. 

85""" 

86import os, shutil 

87import time 

88from threading import Thread 

89import json 

90import numpy as np 

91import random 

92 

93from datetime import timedelta 

94 

95from larch import Group 

96from larch.utils import debugtime, fix_varname 

97 

98try: 

99 from epics import PV, caget, caput, get_pv, poll 

100 HAS_EPICS = True 

101except ImportError: 

102 HAS_EPICS = False 

103 

104try: 

105 import epicsscan 

106 from epicsscan import (Counter, Trigger, AreaDetector, get_detector, 

107 ASCIIScanFile, Positioner) 

108 from epicsscan.scandb import ScanDB, InstrumentDB, ScanDBException, ScanDBAbort 

109 from epics.devices.struck import Struck 

110 

111 HAS_EPICSSCAN = True 

112except ImportError: 

113 HAS_EPICSSCAN = False 

114 

115 

116SCANDB_NAME = '_scan._scandb' 

117INSTDB_NAME = '_scan._instdb' 

118_scandb = None 

119_instdb = None 

120 

121def scan_from_db(scanname, filename='scan.001', _larch=None): 

122 """ 

123 get scan definition from ScanDB by name 

124 """ 

125 global _scandb 

126 if _scandb is None: 

127 print('scan_from_db: need to connect to scandb!') 

128 return 

129 try: 

130 scan = _scandb.make_scan(scanname, larch=_larch) 

131 scan.filename = filename 

132 except ScanDBException: 

133 raise ScanDBException("no scan definition '%s' found" % scanname) 

134 return scan 

135 

136def do_scan(scanname, filename='scan.001', nscans=1, comments='', _larch=None): 

137 """do_scan(scanname, filename='scan.001', nscans=1, comments='') 

138 

139 execute a step scan as defined in Scan database 

140 

141 Parameters 

142 ---------- 

143 scanname: string, name of scan 

144 filename: string, name of output data file 

145 comments: string, user comments for file 

146 nscans: integer (default 1) number of repeats to make. 

147 

148 Examples 

149 -------- 

150 do_scan('cu_xafs', 'cu_sample1.001', nscans=3) 

151 

152 Notes 

153 ------ 

154 1. The filename will be incremented so that each scan uses a new filename. 

155 """ 

156 global _scandb 

157 if _scandb is None: 

158 print('do_scan: need to connect to scandb!') 

159 return 

160 if nscans is not None: 

161 _scandb.set_info('nscans', nscans) 

162 

163 scan = scan_from_db(scanname, filename=filename, _larch=_larch) 

164 scan.comments = comments 

165 if scan.scantype == 'slew': 

166 return scan.run(filename=filename, comments=comments) 

167 else: 

168 scans_completed = 0 

169 nscans = int(_scandb.get_info('nscans')) 

170 abort = _scandb.get_info('request_abort', as_bool=True) 

171 while (scans_completed < nscans) and not abort: 

172 scan.run() 

173 scans_completed += 1 

174 nscans = int(_scandb.get_info('nscans')) 

175 abort = _scandb.get_info('request_abort', as_bool=True) 

176 return scan 

177 

178def get_dbinfo(key, default=None, as_int=False, as_bool=False, 

179 full_row=False, _larch=None, **kws): 

180 """get a value for a keyword in the scan info table, 

181 where most status information is kept. 

182 

183 Arguments 

184 --------- 

185 key name of data to look up 

186 default (default None) value to return if key is not found 

187 as_int (default False) convert to integer 

188 as_bool (default False) convert to bool 

189 full_row (default False) return full row, not just value 

190 

191 Notes 

192 ----- 

193 1. if this key doesn't exist, it will be added with the default 

194 value and the default value will be returned. 

195 2. the full row will include notes, create_time, modify_time 

196 

197 """ 

198 global _scandb 

199 if _scandb is None: 

200 print('get_dbinfo: need to connect to scandb!') 

201 return 

202 return _scandb.get_info(key, default=default, full_row=full_row, 

203 as_int=as_int, as_bool=as_bool, **kws) 

204 

205def set_dbinfo(key, value, notes=None, _larch=None, **kws): 

206 """ 

207 set a value for a keyword in the scan info table. 

208 """ 

209 global _scandb 

210 if _scandb is None: 

211 print('set_dbinfo: need to connect to scandb!') 

212 return 

213 return _scandb.set_info(key, value, notes=notes) 

214 

215def connect_scandb(scandb=None, dbname=None, _larch=None, **kwargs): 

216 from epicsscan.scandb import ScanDB, InstrumentDB 

217 global _scandb, _instdb 

218 if _scandb is not None: 

219 return _scandb 

220 if scandb is None: 

221 scandb = ScanDB(dbname=dbname, **kwargs) 

222 

223 if scandb is not None: 

224 _scandb = scandb 

225 

226 if _larch is not None: 

227 _larch.symtable.set_symbol(SCANDB_NAME, _scandb) 

228 

229 if _instdb is None: 

230 _instdb = InstrumentDB(_scandb) 

231 

232 if _larch is not None: 

233 _larch.symtable.set_symbol(INSTDB_NAME, _instdb) 

234 return _scandb