Skip to content
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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tutorials/flyscanning/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ kernelspec:

# Flyscanning Basics

TODO
TODO
29 changes: 29 additions & 0 deletions docs/tutorials/flyscanning/json_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class JSONWriter:
Copy link
Member

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:

  • You can stream the file into processors, like with a generator, without parsing the whole thing up front.
  • You can parse a partially-written file during a scan.

Copy link
Author

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?

Copy link
Member

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 simple JSONLWriter in bluesky. Perhaps in the docs we can recommend a suitable VSCode plugin?

"""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")
212 changes: 212 additions & 0 deletions docs/tutorials/flyscanning/plotting.md
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))
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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.
Yes, understand that the hook was meant for plotting, but this was the best I could think of after many attempts to connect a simulated motor to a simulated detector. The detector has positional attributes, x and y, that determine the produced signal; as I understood, they are meant to represent the position of the motor, but this connection should be realized outside and independent of the bluesky plan (i.e. to simulated an actual physical sample). Perhaps, there's a better way, but we'll likely need to expand the simulated detector and/or motor classes.

Copy link
Member

Choose a reason for hiding this comment

The 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 ophyd.sim do. For the cookbook in general, we should think through the trade-offs and perhaps document a recommended way to drive simulations external to the Bluesky plan.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 ophyd_async.sim


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
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose in hiding the output?

Copy link
Author

Choose a reason for hiding this comment

The 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

```
45 changes: 45 additions & 0 deletions docs/tutorials/flyscanning/sample_simulator.py
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()
Loading
Loading