|
6 | 6 | ~addDeviceDataAsStream |
7 | 7 | ~execute_command_list |
8 | 8 | ~get_command_list |
| 9 | + ~lineup |
9 | 10 | ~nscan |
10 | 11 | ~parse_Excel_command_file |
11 | 12 | ~parse_text_command_file |
@@ -251,6 +252,127 @@ def _internal(blocking_function, *args, **kwargs): |
251 | 252 | return status |
252 | 253 |
|
253 | 254 |
|
| 255 | +def lineup( |
| 256 | + counter, axis, minus, plus, npts, |
| 257 | + time_s=0.1, peak_factor=4, width_factor=0.8, |
| 258 | + _md={}): |
| 259 | + """ |
| 260 | + lineup and center a given axis, relative to current position |
| 261 | +
|
| 262 | + PARAMETERS |
| 263 | + |
| 264 | + counter : object |
| 265 | + instance of ophyd.Signal (or subclass such as ophyd.scaler.ScalerChannel) |
| 266 | + dependent measurement to be maximized |
| 267 | + |
| 268 | + axis : movable object |
| 269 | + instance of ophyd.Signal (or subclass such as EpicsMotor) |
| 270 | + independent axis to use for alignment |
| 271 | + |
| 272 | + minus : float |
| 273 | + first point of scan at this offset from starting position |
| 274 | + |
| 275 | + plus : float |
| 276 | + last point of scan at this offset from starting position |
| 277 | + |
| 278 | + npts : int |
| 279 | + number of data points in the scan |
| 280 | + |
| 281 | + time_s : float (default: 0.1) |
| 282 | + count time per step (if counter is ScalerChannel object) |
| 283 | + |
| 284 | + peak_factor : float (default: 4) |
| 285 | + maximum must be greater than 'peak_factor'*minimum |
| 286 | + |
| 287 | + width_factor : float (default: 0.8) |
| 288 | + fwhm must be less than 'width_factor'*plot_range |
| 289 | +
|
| 290 | + EXAMPLE: |
| 291 | +
|
| 292 | + RE(lineup(diode, foemirror.theta, -30, 30, 30, 1.0)) |
| 293 | + """ |
| 294 | + # first, determine if counter is part of a ScalerCH device |
| 295 | + scaler = None |
| 296 | + obj = counter.parent |
| 297 | + if isinstance(counter.parent, ScalerChannel): |
| 298 | + if hasattr(obj, "parent") and obj.parent is not None: |
| 299 | + obj = obj.parent |
| 300 | + if hasattr(obj, "parent") and isinstance(obj.parent, ScalerCH): |
| 301 | + scaler = obj.parent |
| 302 | + |
| 303 | + if scaler is not None: |
| 304 | + old_sigs = scaler.stage_sigs |
| 305 | + scaler.stage_sigs["preset_time"] = time_s |
| 306 | + scaler.select_channels([counter.name]) |
| 307 | + |
| 308 | + if hasattr(axis, "position"): |
| 309 | + old_position = axis.position |
| 310 | + else: |
| 311 | + old_position = axis.value |
| 312 | + |
| 313 | + def peak_analysis(): |
| 314 | + aligned = False |
| 315 | + if counter.name in bec.peaks["cen"]: |
| 316 | + table = pyRestTable.Table() |
| 317 | + table.labels = ("key", "value") |
| 318 | + table.addRow(("axis", axis.name)) |
| 319 | + table.addRow(("detector", counter.name)) |
| 320 | + table.addRow(("starting position", old_position)) |
| 321 | + for key in bec.peaks.ATTRS: |
| 322 | + table.addRow((key, bec.peaks[key][counter.name])) |
| 323 | + logger.info(f"alignment scan results:\n{table}") |
| 324 | + |
| 325 | + lo = bec.peaks["min"][counter.name][-1] # [-1] means detector |
| 326 | + hi = bec.peaks["max"][counter.name][-1] # [0] means axis |
| 327 | + fwhm = bec.peaks["fwhm"][counter.name] |
| 328 | + final = bec.peaks["cen"][counter.name] |
| 329 | + |
| 330 | + ps = list(bec._peak_stats.values())[0][counter.name] # PeakStats object |
| 331 | + # get the X data range as received by PeakStats |
| 332 | + x_range = abs(max(ps.x_data) - min(ps.x_data)) |
| 333 | + |
| 334 | + if final is None: |
| 335 | + logger.error(f"centroid is None") |
| 336 | + final = old_position |
| 337 | + elif fwhm is None: |
| 338 | + logger.error(f"FWHM is None") |
| 339 | + final = old_position |
| 340 | + elif hi < peak_factor*lo: |
| 341 | + logger.error(f"no clear peak: {hi} < {peak_factor}*{lo}") |
| 342 | + final = old_position |
| 343 | + elif fwhm > width_factor*x_range: |
| 344 | + logger.error(f"FWHM too large: {fwhm} > {width_factor}*{x_range}") |
| 345 | + final = old_position |
| 346 | + else: |
| 347 | + aligned = True |
| 348 | + |
| 349 | + logger.info(f"moving {axis.name} to {final} (aligned: {aligned})") |
| 350 | + yield from bps.mv(axis, final) |
| 351 | + else: |
| 352 | + logger.error("no statistical analysis of scan peak!") |
| 353 | + yield from bps.null() |
| 354 | + |
| 355 | + # too sneaky? We're modifying this structure locally |
| 356 | + bec.peaks.aligned = aligned |
| 357 | + bec.peaks.ATTRS = ('com', 'cen', 'max', 'min', 'fwhm') |
| 358 | + |
| 359 | + md = dict(_md) |
| 360 | + md["purpose"] = "alignment" |
| 361 | + yield from bp.rel_scan([counter], axis, minus, plus, npts, md=md) |
| 362 | + yield from peak_analysis() |
| 363 | + |
| 364 | + if bec.peaks.aligned: |
| 365 | + # again, tweak axis to maximize |
| 366 | + md["purpose"] = "alignment - fine" |
| 367 | + fwhm = bec.peaks["fwhm"][counter.name] |
| 368 | + yield from bp.rel_scan([counter], axis, -fwhm, fwhm, npts, md=md) |
| 369 | + yield from peak_analysis() |
| 370 | + |
| 371 | + if scaler is not None: |
| 372 | + scaler.select_channels() |
| 373 | + scaler.stage_sigs = old_sigs |
| 374 | + |
| 375 | + |
254 | 376 | def nscan(detectors, *motor_sets, num=11, per_step=None, md=None): |
255 | 377 | """ |
256 | 378 | Scan over ``n`` variables moved together, each in equally spaced steps. |
|
0 commit comments