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
« 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
4MODDOC = """
5=== Epics Scanning Functions for Larch ===
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.
12A Step Scan consists of the following objects:
13 a list of Positioners
14 a list of Triggers
15 a list of Counters
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.
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.
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.
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.
39In addition to the core components (Positioners, Triggers, Counters, Detectors),
40a Step Scan contains the following objects:
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.
51Note that Postioners and Detectors may add their own pieces into extra_pvs,
52pre_scan(), post_scan(), and at_break().
54With these concepts, a Step Scan ends up being a fairly simple loop, going
55roughly (that is, skipping error checking) as:
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]
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)
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.
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
93from datetime import timedelta
95from larch import Group
96from larch.utils import debugtime, fix_varname
98try:
99 from epics import PV, caget, caput, get_pv, poll
100 HAS_EPICS = True
101except ImportError:
102 HAS_EPICS = False
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
111 HAS_EPICSSCAN = True
112except ImportError:
113 HAS_EPICSSCAN = False
116SCANDB_NAME = '_scan._scandb'
117INSTDB_NAME = '_scan._instdb'
118_scandb = None
119_instdb = None
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
136def do_scan(scanname, filename='scan.001', nscans=1, comments='', _larch=None):
137 """do_scan(scanname, filename='scan.001', nscans=1, comments='')
139 execute a step scan as defined in Scan database
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.
148 Examples
149 --------
150 do_scan('cu_xafs', 'cu_sample1.001', nscans=3)
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)
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
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.
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
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
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)
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)
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)
223 if scandb is not None:
224 _scandb = scandb
226 if _larch is not None:
227 _larch.symtable.set_symbol(SCANDB_NAME, _scandb)
229 if _instdb is None:
230 _instdb = InstrumentDB(_scandb)
232 if _larch is not None:
233 _larch.symtable.set_symbol(INSTDB_NAME, _instdb)
234 return _scandb