-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flyscanning Tutorial #22
base: main
Are you sure you want to change the base?
Changes from all commits
1fcc11d
b5627ef
d7da5a2
d99f22c
3df3463
3df35f3
44c51c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,4 +13,4 @@ kernelspec: | |
|
||
# Flyscanning Basics | ||
|
||
TODO | ||
TODO |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
class JSONWriter: | ||
"""Callback to write a Bluesky stream into a JSON file. Useful for debugging. | ||
|
||
Parameters | ||
---------- | ||
filepath : str | ||
A desired path to a .json file used to save the data. | ||
|
||
""" | ||
|
||
def __init__(self, filepath: str): | ||
if not filepath.endswith(".json"): | ||
filepath = filepath + ".json" | ||
self.filepath = filepath | ||
|
||
def __call__(self, name, doc): | ||
import json | ||
|
||
if name == "start": | ||
self.file = open(self.filepath, "w") | ||
self.file.write("[\n") | ||
|
||
json.dump({"name": name, "doc": doc}, self.file) | ||
|
||
if name == "stop": | ||
self.file.write("\n]") | ||
self.file.close() | ||
else: | ||
self.file.write(",\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
--- | ||
jupytext: | ||
text_representation: | ||
extension: .md | ||
format_name: myst | ||
format_version: 0.13 | ||
jupytext_version: 1.16.4 | ||
kernelspec: | ||
display_name: Python 3 (ipykernel) | ||
language: python | ||
name: python3 | ||
--- | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
# Live Plotting of Streamed Data | ||
|
||
A distictive characteristic of fly scanning plans is their reliance on continuous data streams rather than acquisition of data points invidually. In Bluesky, data streaming is realized via StreamResource and StreamDatum documents (as aooposed to Resource and Datum in step scanning). The eventual number of data points in the stream (e.g. image frames acquired during a flyscanning experiment) is typically unknown at the begininng of the aquisition; furtehrmore, the frames may arrive at non-uniformly spaced time intervals. Eventually, the frames become parts of the same resource and are often appended to the same file (in case of hdf5) by the detector. | ||
|
||
|
||
We introduce the notion of Consolidators to facilitate random access by data access services, e.g. Tiled. Consolidators are analogous to Handlers that have existed in Bluesky for a long time, but they enable working with streamed data. A Consolidator is defined for a particular stream of data and is tied to the underlying resource; it knows how to read the data from disk in the most efficient manner. | ||
|
||
|
||
In this notebook, we will explore the use of Consolidators as an option for live plotting of streamed data. | ||
|
||
```{code-cell} ipython3 | ||
%load_ext autoreload | ||
%autoreload 2 | ||
|
||
import os | ||
import asyncio | ||
import ophyd_async | ||
import bluesky | ||
from ophyd_async.sim.demo import PatternDetector, SimMotor | ||
import bluesky.plans as bp | ||
import bluesky.plan_stubs as bps | ||
import bluesky.preprocessors as bpp | ||
from bluesky.callbacks.core import CollectLiveStream | ||
from bluesky.callbacks.mpl_plotting import LiveStreamPlot | ||
from bluesky.run_engine import RunEngine | ||
from pathlib import Path | ||
from sample_simulator import SampleSimulator | ||
from json_writer import JSONWriter | ||
|
||
|
||
print(f"{ophyd_async.__version__=}\n{bluesky.__version__ =}") | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
import matplotlib | ||
import matplotlib.pyplot as plt | ||
|
||
%matplotlib widget | ||
plt.ion() | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
fig, ax = plt.subplots(1, 2, figsize=(13, 4)) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
det = PatternDetector(name="PATTERN1", path=Path('.')) | ||
motx = SimMotor(name="Motor_X", instant=False) | ||
moty = SimMotor(name="Motor_Y", instant=False) | ||
await motx.velocity.set(2.0) | ||
await moty.velocity.set(1.0) | ||
|
||
RE = RunEngine({}, during_task = SampleSimulator(det, motx, moty)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting! This hook is meant to run things like plotting event loops. I'm not sure how I feel about using it to run a simulator like this. Maybe? Is there precedent elsewhere? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I don't think there's a precedent, or at least I haven't seen one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an important general question. Sometimes we have run simulations like this as separate processes, sometimes as IOCs. Sometimes we have used threads, as a couple objects in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As part of the ophyd-async 1.0 release I'll make something that looks very much like this in |
||
|
||
cl = CollectLiveStream() | ||
pl_1d = LiveStreamPlot(cl, data_key="PATTERN1-sum", ax=ax[0]) | ||
pl_2d = LiveStreamPlot(cl, data_key="PATTERN1", ax=ax[1], clim=(0, 16)) | ||
wr = JSONWriter('./documents_test.json') | ||
|
||
RE.subscribe(cl) | ||
RE.subscribe(pl_1d) | ||
RE.subscribe(pl_2d) | ||
RE.subscribe(wr) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
### Simple count plan without moving motors | ||
RE(bp.count([det], num=5, delay=0.2)) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
### One-dimensional scan | ||
plan = bp.scan([det], motx, 0, 2.5, 50) | ||
RE(plan) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
### Spiral trajectory | ||
spiral_plan = bp.spiral([det], motx, moty, x_start=0.0, y_start=0.0, | ||
x_range=3.0, y_range=3.0, dr=0.3, nth=10) | ||
RE(spiral_plan) | ||
|
||
# from bluesky.simulators import plot_raster_path | ||
# plot_raster_path(spiral_plan, 'motor1', 'motor2', probe_size=.01) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: There are many empty cells in this file. But of course we can fix this up right before merging. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure! will clean it up, when it's ready. |
||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
### A custom plan | ||
def plan(): | ||
yield from scan([det], motx, 0, 2.5, 50) | ||
|
||
yield from bps.unstage_all(det, motx) | ||
|
||
RE(plan()) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
RE.stop() | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
await det.prepare(0) | ||
await det.unstage() | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
--- | ||
jupyter: | ||
outputs_hidden: true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose in hiding the output? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm, it's not me... |
||
--- | ||
gen = bp.fly([det]) | ||
for msg in gen: | ||
print(msg) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
--- | ||
jupyter: | ||
outputs_hidden: true | ||
--- | ||
gen = bp.count([det], num=5, delay=0.5) | ||
for msg in gen: | ||
print(msg) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
from ophyd_async.sim.demo._pattern_detector._pattern_generator import generate_gaussian_blob, generate_interesting_pattern | ||
import matplotlib.colors as mcolors | ||
from mpl_toolkits.axes_grid1 import make_axes_locatable | ||
import numpy as np | ||
|
||
fig, ax = plt.subplots(1, 1, figsize=(7, 7)) | ||
|
||
x_arr = np.linspace(0, 6, 100) | ||
y_arr = np.linspace(-3.14, 3.14, 100) | ||
xx, yy = np.meshgrid(x_arr, y_arr) | ||
|
||
im1 = generate_gaussian_blob(width=350, height=200) | ||
im2 = generate_interesting_pattern(xx, yy) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
im = ax.imshow(im2) | ||
divider = make_axes_locatable(ax) | ||
cax = divider.append_axes("right", size="5%", pad=0.05) | ||
plt.colorbar(im, cax=cax) | ||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` | ||
|
||
```{code-cell} ipython3 | ||
|
||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import asyncio | ||
import threading | ||
from bluesky.utils import DefaultDuringTask | ||
from ophyd_async.sim.demo import PatternDetector, SimMotor | ||
from typing import Optional | ||
|
||
class SampleSimulator(DefaultDuringTask): | ||
|
||
"""A helper class that connects simulated PatternDetector to SimMotors to mimic an actual physical sample. | ||
|
||
Changes the signal produced by the pattern detector depending on the motors' positions. | ||
|
||
""" | ||
|
||
def __init__(self, det : PatternDetector, motx : Optional[SimMotor] = None, moty : Optional[SimMotor] = None): | ||
self._loop = asyncio.new_event_loop() | ||
self.det = det | ||
self.motx = motx | ||
self.moty = moty | ||
super().__init__() | ||
|
||
@property | ||
def motx_pos(self): | ||
if self.motx is not None: | ||
return self._loop.run_until_complete(self.motx.user_readback.get_value()) | ||
else: return 0.0 | ||
|
||
@property | ||
def moty_pos(self): | ||
if self.moty is not None: | ||
return self._loop.run_until_complete(self.moty.user_readback.get_value()) | ||
else: return 0.0 | ||
|
||
def block(self, blocking_event): | ||
def target(event): | ||
while not event.wait(0.1): | ||
self.det.writer.pattern_generator.x = self.motx_pos | ||
self.det.writer.pattern_generator.y = self.moty_pos | ||
|
||
ev = threading.Event() | ||
th = threading.Thread(target = target, args=(ev, ), daemon=True) | ||
th.start() | ||
super().block(blocking_event) | ||
|
||
ev.set() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this purpose we sometimes use jsonlines because it is stream-able. Consider whether that would be a good fit here.
The main selling points are:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thans, Dan! I think jsonlines is actually what was there initially (I am using the piece of code you have written during one of my first days here when you were explaining Bluesky to me). As far as I remember, I had some difficulties with opening jsonl with VS code or maybe it didn't format it correctly (probably I just needed to install another extension), but it worked with json. I like the selling points though!
On this note, do you think it would make sense to add these callbacks to Bluesky directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely! A
bluesky.callbacks.jsonl
module would be great to have.We actually ship this functionality as a "suitcase",
suitcase-jsonl
but the amount of framework here is overkill for this use case. Better to ship a simpleJSONLWriter
in bluesky. Perhaps in the docs we can recommend a suitable VSCode plugin?