Commit c3175ae4 authored by Sebastien Petitdemange's avatar Sebastien Petitdemange
Browse files

Merge branch 'scan_improvements' into 'master'

Scan improvements

See merge request !476
parents c13a54bd 8bf6e816
......@@ -39,6 +39,7 @@ from bliss.common.encoder import Encoder
from bliss.common.hook import MotionHook
import gevent
import re
import math
import types
import functools
import numpy
......@@ -105,6 +106,57 @@ class Motion(object):
return self.__axis
class MotionEstimation(object):
"""
Estimate motion time and displacement based on current axis position
and configuration
"""
def __init__(self, axis, target_pos, initial_pos=None):
self.axis = axis
ipos = axis.position() if initial_pos is None else initial_pos
self.ipos = ipos
fpos = target_pos
delta = fpos - ipos
do_backlash = cmp(delta, 0) != cmp(axis.backlash, 0)
if do_backlash:
delta -= axis.backlash
fpos -= axis.backlash
self.fpos = fpos
self.displacement = displacement = abs(delta)
try:
self.vel = vel = axis.velocity()
self.accel = accel = axis.acceleration()
except NotImplementedError:
self.vel = float('+inf')
self.accel = float('+inf')
self.duration = 0
return
full_accel_time = vel / accel
full_accel_dplmnt = 0.5*accel * full_accel_time**2
full_dplmnt_non_const_vel = 2 * full_accel_dplmnt
reaches_max_velocity = displacement > full_dplmnt_non_const_vel
if reaches_max_velocity:
max_vel = vel
accel_time = full_accel_time
accel_dplmnt = full_accel_dplmnt
dplmnt_non_const_vel = full_dplmnt_non_const_vel
else:
max_vel = math.sqrt(2*displacement)
accel_time = max_vel / accel
accel_dplmnt = displacement / 2.0
dplmnt_non_const_vel = displacement
max_vel_dplmnt = displacement - dplmnt_non_const_vel
max_vel_time = max_vel_dplmnt / vel
self.duration = max_vel_time + 2*accel_time
if do_backlash:
backlash_estimation = MotionEstimation(axis, target_pos, self.fpos)
self.duration += backlash_estimation.duration
@with_custom_members
class Axis(object):
"""
......
......@@ -22,6 +22,8 @@ import numpy
import gevent
from bliss import setup_globals
from bliss.common.axis import MotionEstimation
from bliss.common.temperature import Input, Output, TempControllerCounter
from bliss.common.task_utils import *
from bliss.common.motor_group import Group
from bliss.common.measurement import CounterBase
......@@ -43,6 +45,7 @@ class TimestampPlaceholder:
def __init__(self):
self.name = 'timestamp'
def _get_counters(mg, missing_list):
counters = list()
if mg is not None:
......@@ -87,6 +90,9 @@ def default_chain(chain, scan_pars):
read_cnt_handler = dict()
for cnt in set(counters):
if isinstance(cnt, (Input, Output)):
cnt = TempControllerCounter(cnt.name, cnt)
if isinstance(cnt, CounterBase):
try:
read_all_handler = cnt.read_all_handler()
......@@ -105,6 +111,7 @@ def default_chain(chain, scan_pars):
else:
raise TypeError("`%r' is not a supported counter type" % repr(cnt))
chain.timer = timer
return timer
def step_scan(chain,scan_info,name=None,save=default_writer is not None):
......@@ -134,6 +141,9 @@ def ascan(motor, start, stop, npoints, count_time, *counters, **kwargs):
`(*start*-*stop*)/(*npoints*-1)`. The number of intervals will be
*npoints*-1. Count time is given by *count_time* (seconds).
Use `ascan(..., run=False, return_scan=True)` to create a scan object and
its acquisition chain without executing the actual scan.
Args:
motor (Axis): motor to scan
start (float): motor start position
......@@ -149,6 +159,8 @@ def ascan(motor, start, stop, npoints, count_time, *counters, **kwargs):
title (str): scan title [default: 'ascan <motor> ... <count_time>']
save (bool): save scan data to file [default: True]
sleep_time (float): sleep time between 2 points [default: None]
run (bool): if True (default), run the scan. False means just create
scan object and acquisition chain
return_scan (bool): False by default
"""
scan_info = { 'type': kwargs.get('type', 'ascan'),
......@@ -163,11 +175,22 @@ def ascan(motor, start, stop, npoints, count_time, *counters, **kwargs):
counters = _get_all_counters(counters)
scan_info.update({ 'npoints': npoints, 'total_acq_time': npoints * count_time,
# estimate scan time
step_size = abs(stop - start) / npoints
i_motion_t = MotionEstimation(motor, start).duration
n_motion_t = MotionEstimation(motor, start, start + step_size).duration
total_motion_t = i_motion_t + npoints * n_motion_t
total_count_t = npoints * count_time
estimation = {'total_motion_time': total_motion_t,
'total_count_time': total_count_t,
'total_time': total_motion_t + total_count_t}
scan_info.update({ 'npoints': npoints, 'total_acq_time': total_count_t,
'motors': [TimestampPlaceholder(), motor],
'counters': counters,
'start': [start], 'stop': [stop],
'count_time': count_time })
'count_time': count_time,
'estimation': estimation})
chain = AcquisitionChain(parallel_prepare=True)
timer = default_chain(chain,scan_info)
......@@ -180,7 +203,9 @@ def ascan(motor, start, stop, npoints, count_time, *counters, **kwargs):
scan = step_scan(chain, scan_info,
name=kwargs.setdefault("name","ascan"), save=scan_info['save'])
scan.run()
if kwargs.get('run', True):
scan.run()
if kwargs.get('return_scan',False):
return scan
......@@ -196,6 +221,9 @@ def dscan(motor, start, stop, npoints, count_time, *counters, **kwargs):
At the end of the scan (even in case of error) the motor will return to
its initial position
Use `dscan(..., run=False, return_scan=True)` to create a scan object and
its acquisition chain without executing the actual scan.
Args:
motor (Axis): motor to scan
start (float): motor relative start position
......@@ -211,6 +239,8 @@ def dscan(motor, start, stop, npoints, count_time, *counters, **kwargs):
title (str): scan title [default: 'dscan <motor> ... <count_time>']
save (bool): save scan data to file [default: True]
sleep_time (float): sleep time between 2 points [default: None]
run (bool): if True (default), run the scan. False means just create
scan object and acquisition chain
return_scan (bool): False by default
"""
kwargs['type'] = 'dscan'
......@@ -231,6 +261,10 @@ def mesh(motor1, start1, stop1, npoints1, motor2, start2, stop2, npoints2, count
The scan of motor1 is done at each point scanned by motor2. That is, the
first motor scan is nested within the second motor scan.
Use `mesh(..., run=False, return_scan=True)` to create a scan object and
its acquisition chain without executing the actual scan.
"""
scan_info = { 'type': kwargs.get('type', 'mesh'),
'save': kwargs.get('save', True),
......@@ -244,13 +278,34 @@ def mesh(motor1, start1, stop1, npoints1, motor2, start2, stop2, npoints2, count
scan_info['title'] = template.format(*args)
counters = _get_all_counters(counters)
# estimate scan time
step_size1 = abs(stop1 - start1) / npoints1
i_motion_t1 = MotionEstimation(motor1, start1).duration
n_motion_t1 = MotionEstimation(motor1, start1, start1 + step_size1).duration
total_motion_t1 = npoints1 *npoints2 * n_motion1_t
step_size2 = abs(stop2 - start2) / npoints2
i_motion_t2 = MotionEstimation(motor2, start2).duration
n_motion_t2 = max(MotionEstimation(motor2, start2, start2 + step_size2).duration,
MotionEstimation(motor1, end1, start1).duration)
total_motion_t2 = npoints2 * n_motion2_t
imotion_t = max(i_motion_t1, i_motion_t2)
total_motion_t = imotion_t + total_motion_t1 + total_motion_t2
total_count_t = npoints1 * npoints2 * count_time
estimation = {'total_motion_time': total_motion_t,
'total_count_time': total_count_t,
'total_time': total_motion_t + total_count_t}
scan_info.update({ 'npoints1': npoints1, 'npoints2': npoints2,
'total_acq_time': npoints1 * npoints2 * count_time,
'total_acq_time': total_count_t,
'motors': [TimestampPlaceholder(), motor1, motor2],
'counters': counters,
'start': [start1, start2], 'stop': [stop1, stop2],
'count_time': count_time })
'count_time': count_time,
'estimation': estimation})
chain = AcquisitionChain(parallel_prepare=True)
timer = default_chain(chain,scan_info)
......@@ -265,7 +320,8 @@ def mesh(motor1, start1, stop1, npoints1, motor2, start2, stop2, npoints2, count
scan = step_scan(chain, scan_info,
name=kwargs.setdefault("name","mesh"), save=scan_info['save'])
scan.run()
if kwargs.get('run', True):
scan.run()
if kwargs.get('return_scan', False):
return scan
......@@ -281,6 +337,9 @@ def a2scan(motor1, start1, stop1, motor2, start2, stop2, npoints, count_time,
`(*start*-*stop*)/(*npoints*-1)`. The number of intervals will be
*npoints*-1. Count time is given by *count_time* (seconds).
Use `a2scan(..., run=False, return_scan=True)` to create a scan object and
its acquisition chain without executing the actual scan.
Args:
motor1 (Axis): motor1 to scan
start1 (float): motor1 start position
......@@ -299,6 +358,8 @@ def a2scan(motor1, start1, stop1, motor2, start2, stop2, npoints, count_time,
title (str): scan title [default: 'a2scan <motor1> ... <count_time>']
save (bool): save scan data to file [default: True]
sleep_time (float): sleep time between 2 points [default: None]
run (bool): if True (default), run the scan. False means just create
scan object and acquisition chain
return_scan (bool): False by default
"""
scan_info = { 'type': kwargs.get('type', 'a2scan'),
......@@ -314,11 +375,29 @@ def a2scan(motor1, start1, stop1, motor2, start2, stop2, npoints, count_time,
counters = _get_all_counters(counters)
scan_info.update({ 'npoints': npoints, 'total_acq_time': npoints * count_time,
# estimate scan time
step_size1 = abs(stop1 - start1) / npoints
i_motion1_t = MotionEstimation(motor1, start1).duration
n_motion1_t = MotionEstimation(motor1, start1, start1 + step_size1).duration
step_size2 = abs(stop2 - start2) / npoints
i_motion2_t = MotionEstimation(motor2, start2).duration
n_motion2_t = MotionEstimation(motor2, start2, start2 + step_size2).duration
i_motion_t = max(i_motion1_t, i_motion2_t)
n_motion_t = max(n_motion1_t, n_motion2_t)
total_motion_t = i_motion_t + npoints * nmotion_t
total_count_t = npoints * count_time
estimation = {'total_motion_time': total_motion_t,
'total_count_time': total_count_t,
'total_time': total_motion_t + total_count_t}
scan_info.update({ 'npoints': npoints, 'total_acq_time': total_count_t,
'motors': [TimestampPlaceholder(), motor1, motor2],
'counters': counters,
'start': [start1, start2], 'stop': [stop1, stop2],
'count_time': count_time })
'count_time': count_time,
'estimation': estimation })
chain = AcquisitionChain(parallel_prepare=True)
timer = default_chain(chain,scan_info)
......@@ -334,7 +413,8 @@ def a2scan(motor1, start1, stop1, motor2, start2, stop2, npoints, count_time,
scan = step_scan(chain, scan_info,
name=kwargs.setdefault("name","a2scan"), save=scan_info['save'])
scan.run()
if kwargs.get('run', True):
scan.run()
if kwargs.get('return_scan',False):
return scan
......@@ -355,6 +435,9 @@ def d2scan(motor1, start1, stop1, motor2, start2, stop2, npoints, count_time,
At the end of the scan (even in case of error) the motor will return to
its initial position
Use `d2scan(..., run=False, return_scan=True)` to create a scan object and
its acquisition chain without executing the actual scan.
Args:
motor1 (Axis): motor1 to scan
start1 (float): motor1 relative start position
......@@ -373,6 +456,8 @@ def d2scan(motor1, start1, stop1, motor2, start2, stop2, npoints, count_time,
title (str): scan title [default: 'd2scan <motor1> ... <count_time>']
save (bool): save scan data to file [default: True]
sleep_time (float): sleep time between 2 points [default: None]
run (bool): if True (default), run the scan. False means just create
scan object and acquisition chain
return_scan (bool): False by default
"""
kwargs['type'] = 'd2scan'
......@@ -394,6 +479,9 @@ def timescan(count_time, *counters, **kwargs):
"""
Time scan
Use `timescan(..., run=False, return_scan=True)` to create a scan object and
its acquisition chain without executing the actual scan.
Args:
count_time (float): count time (seconds)
counters (BaseCounter or
......@@ -405,13 +493,19 @@ def timescan(count_time, *counters, **kwargs):
title (str): scan title [default: 'timescan <count_time>']
save (bool): save scan data to file [default: True]
sleep_time (float): sleep time between 2 points [default: None]
run (bool): if True (default), run the scan. False means just create
scan object and acquisition chain
return_scan (bool): False by default
npoints (int): number of points [default: 0, meaning infinite number of points]
output_mode (str): valid are 'tail' (append each line to output) or
'monitor' (refresh output in single line)
[default: 'tail']
"""
scan_info = { 'type': kwargs.get('type', 'timescan'),
'save': kwargs.get('save', True),
'title': kwargs.get('title'),
'sleep_time': kwargs.get('sleep_time') }
'sleep_time': kwargs.get('sleep_time') ,
'output_mode': kwargs.get('output_mode', 'tail') }
if scan_info['title'] is None:
args = scan_info['type'], count_time
......@@ -421,11 +515,19 @@ def timescan(count_time, *counters, **kwargs):
counters = _get_all_counters(counters)
npoints = kwargs.get("npoints", 0)
scan_info.update({ 'npoints': npoints, 'total_acq_time': npoints * count_time,
total_count_t = npoints * count_time
scan_info.update({ 'npoints': npoints, 'total_acq_time': total_count_t,
'motors': [TimestampPlaceholder()],
'counters': counters,
'start': [], 'stop': [], 'count_time': count_time,
'total_acq_time': npoints * count_time })
'start': [], 'stop': [], 'count_time': count_time })
if npoints > 0:
# estimate scan time
estimation = {'total_motion_time': 0,
'total_count_time': total_count_t,
'total_time': total_count_t}
scan_info['estimation'] = estimation
_log.info("Doing %s", scan_info['type'])
......@@ -434,7 +536,10 @@ def timescan(count_time, *counters, **kwargs):
scan = step_scan(chain, scan_info,
name=kwargs.setdefault("name","timescan"), save=scan_info['save'])
scan.run()
if kwargs.get('run', True):
scan.run()
if kwargs.get('return_scan', False):
return scan
......@@ -443,6 +548,9 @@ def loopscan(npoints, count_time, *counters, **kwargs):
"""
Similar to :ref:`timescan` but npoints is mandatory
Use `loopscan(..., run=False, return_scan=True)` to create a scan object and
its acquisition chain without executing the actual scan.
Args:
npoints (int): number of points
count_time (float): count time (seconds)
......@@ -455,7 +563,12 @@ def loopscan(npoints, count_time, *counters, **kwargs):
title (str): scan title [default: 'timescan <count_time>']
save (bool): save scan data to file [default: True]
sleep_time (float): sleep time between 2 points [default: None]
run (bool): if True (default), run the scan. False means just create
scan object and acquisition chain
return_scan (bool): False by default
output_mode (str): valid are 'tail' (append each line to output) or
'monitor' (refresh output in single line)
[default: 'tail']
"""
kwargs.setdefault('npoints', npoints)
kwargs.setdefault('name', 'loopscan')
......@@ -466,6 +579,9 @@ def ct(count_time, *counters, **kwargs):
"""
Count for a specified time
Use `ct(..., run=False, return_scan=True)` to create a count object and
its acquisition chain without executing the actual count.
Note:
This function blocks the current :class:`Greenlet`
......@@ -479,6 +595,8 @@ def ct(count_time, *counters, **kwargs):
name (str): scan name in data nodes tree and directories [default: 'scan']
title (str): scan title [default: 'ct <count_time>']
save (bool): save scan data to file [default: True]
run (bool): if True (default), run the scan. False means just create
scan object and acquisition chain
return_scan (bool): False by default
"""
kwargs['type'] = 'ct'
......
......@@ -59,13 +59,8 @@ class StepScanDataWatch(object):
point_nb = self._last_point_display
for point_nb in range(self._last_point_display,min_nb_points):
motor_channels = [self._channel_name_2_channel.get(channel_name)
for channel_name in self._motors_name]
values = [channel.get(point_nb) for channel in motor_channels]
motor_channels = set(motor_channels)
values.extend((channel.get(point_nb)
for channel in self._channel_name_2_channel.values()
if channel not in motor_channels))
values = dict([(ch_name, ch.get(point_nb))
for ch_name, ch in self._channel_name_2_channel.iteritems()])
send(current_module,"scan_data",
self._scan_info,values)
if min_nb_points is not None:
......
......@@ -16,6 +16,7 @@ import functools
import numpy
from six import print_
from blessings import Terminal
from bliss import setup_globals
from bliss.config import static
......@@ -55,7 +56,7 @@ def initialize(*session_names):
class ScanListener:
'''listen to scan events and compose output'''
HEADER = "Total {npoints} points, {total_acq_time} seconds\n\n" + \
HEADER = "Total {npoints} points{estimation_str}\n\n" + \
"Scan {scan_nb} {start_time_str} {root_path} " + \
"{session_name} user = {user_name}\n" + \
"{title}\n\n" + \
......@@ -70,6 +71,8 @@ class ScanListener:
def __on_scan_new(self, scan_info):
scan_info = dict(scan_info)
self.term = term = Terminal(scan_info.get('stream'))
motors = scan_info['motors']
counters = scan_info['counters']
nb_points = scan_info['npoints']
......@@ -83,8 +86,9 @@ class ScanListener:
unit = 's'
else:
real_motors.append(motor)
dispatcher.connect(self.__on_motor_position_changed,
signal='position', sender=motor)
if term.is_a_tty:
dispatcher.connect(self.__on_motor_position_changed,
signal='position', sender=motor)
unit = motor.config.get('unit', default=None)
motor_label = motor_name
if unit:
......@@ -108,19 +112,33 @@ class ScanListener:
if scan_info['type'] == 'ct':
return
estimation = scan_info.get('estimation')
if estimation:
total = datetime.timedelta(seconds=estimation['total_time'])
motion = datetime.timedelta(seconds=estimation['total_motion_time'])
count = datetime.timedelta(seconds=estimation['total_count_time'])
estimation_str = ', {0} (motion: {1}, count: {2})'.format(total, motion, count)
else:
estimation_str = ''
col_lens = map(lambda x: max(len(x), self.DEFAULT_WIDTH), col_labels)
h_templ = ["{{0:>{width}}}".format(width=col_len)
for col_len in col_lens]
header = " ".join([templ.format(label)
for templ, label in zip(h_templ, col_labels)])
header = self.HEADER.format(column_header=header, **scan_info)
self.col_templ = ["{{0: >{width}}}".format(width=col_len)
header = self.HEADER.format(column_header=header,
estimation_str=estimation_str,
**scan_info)
self.col_templ = ["{{0: >{width}g}}".format(width=col_len)
for col_len in col_lens]
print_(header)
def __on_scan_data(self, scan_info, values):
elapsed_time = time.time() - scan_info['start_time_stamp']
values = [elapsed_time] + values[1:]
elapsed_time = values['timestamp'] - scan_info['start_time_stamp']
motors = scan_info['motors'][1:] # take out timestamp placeholder
motor_values = [values[m.name] for m in motors]
counter_values = [values[c.name] for c in scan_info['counters']]
values = [elapsed_time] + motor_values + counter_values
if scan_info['type'] == 'ct':
# ct is actually a timescan(npoints=1).
norm_values = numpy.array(values) / scan_info['count_time']
......@@ -136,7 +154,11 @@ class ScanListener:
values.insert(0, self._point_nb)
self._point_nb += 1
line = " ".join([self.col_templ[i].format(v) for i, v in enumerate(values)])
print_(line)
if self.term.is_a_tty:
monitor = scan_info.get('output_mode', 'tail') == 'monitor'
print_(line, end=monitor and '\r' or '\n', flush=True)
else:
print_(line)
def __on_scan_end(self, scan_info):
if scan_info['type'] == 'ct':
......@@ -146,6 +168,18 @@ class ScanListener:
dispatcher.disconnect(self.__on_motor_position_changed,
signal='position', sender=motor)
end = datetime.datetime.fromtimestamp(time.time())
start = datetime.datetime.fromtimestamp(scan_info['start_time_stamp'])
dt = end - start
if scan_info.get('output_mode', 'tail') == 'monitor' and self.term.is_a_tty:
print_()
msg = '\nTook {0}'.format(dt)
if 'estimation' in scan_info:
time_estimation = scan_info['estimation']['total_time']
msg += ' (estimation was for {0})'.format(datetime.timedelta(seconds=time_estimation))
print_(msg)
def __on_motor_position_changed(self, position, signal=None, sender=None):
labels = []
for motor in self.real_motors:
......
......@@ -72,18 +72,32 @@ def stdout_redirected(client_uuid, new_stdout):
def init_scans_callbacks(interpreter, output_queue):
def new_scan_callback(scan_info, root_path, scan_actuators, npoints, counters_list):
def new_scan_callback(scan_info):
scan_actuators = scan_info['motors']
if len(scan_actuators) > 1:
scan_actuators = scan_actuators[1:]
output_queue.put((interpreter.get_last_client_uuid(), {"scan_id": scan_info["node_name"], "filename": root_path,
"scan_actuators": scan_actuators, "npoints": npoints,
"counters": counters_list}))
data = (interpreter.get_last_client_uuid(),
{"scan_id": scan_info["node_name"],
"filename": scan_info['root_path'],
"scan_actuators": [actuator.name for actuator in scan_actuators],
"npoints": scan_info['npoints'],
"counters": [ct.name for ct in scan_info['counters']]})
output_queue.put(data)
def update_scan_callback(scan_info, values):
value_list = [values[m.name] for m in scan_info['motors']]
value_list += [values[c.name] for c in scan_info['counters']]
if scan_info["type"] != "timescan":
values = values[1:]
output_queue.put((interpreter.get_last_client_uuid(), {"scan_id": scan_info["node_name"], "values":values}))
value_list = value_list[1:]
data = (interpreter.get_last_client_uuid(),
{"scan_id": scan_info["node_name"],
"values":value_list})
output_queue.put(data)
def scan_end_callback(scan_info):
output_queue.put((interpreter.get_last_client_uuid(), {"scan_id": scan_info["node_name"]}))
data = (interpreter.get_last_client_uuid(),
{"scan_id": scan_info["node_name"]})
output_queue.put(data)
# keep callbacks references
output_queue.callbacks["scans"]["new"] = new_scan_callback
......
......@@ -29,6 +29,7 @@ pyserial == 2.7
ruamel.yaml == 0.11.15
zerorpc
msgpack_numpy
blessings
# --- test requirements ------------------------------------
......
......@@ -54,7 +54,7 @@ def test_scan_callbacks(beacon):
def on_scan_new(scan_info):
res["new"] = True
def on_scan_data(scan_info, values):
res["values"].append(values[1])
res["values"].append(values[counter.name])
def on_scan_end(scan_info):
res["end"] = True
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment