Skip to content

Conversation

@prjemian
Copy link
Contributor

@prjemian prjemian added the bug label May 30, 2022
@prjemian prjemian added this to the 1.6.2 milestone May 30, 2022
@prjemian prjemian self-assigned this May 30, 2022
@prjemian
Copy link
Contributor Author

Test this locally before merging

@prjemian
Copy link
Contributor Author

prjemian commented May 31, 2022

CI fails with this error:

E           event_model.UndefinedAssetSpecification: "Resource document with uid b57d7481-d402-4e83-af3d-14b0414c59fa refers to spec 'AD_HDF5' which is not defined in the Filler's handler registry."

That is because the area-detector-handlers package is not installed.

@prjemian
Copy link
Contributor Author

Another CI failure now:

E           event_model.EventModelError: Error instantiating handler class <class 'area_detector_handlers.handlers.AreaDetectorHDF5Handler'> with Resource document {'spec': 'AD_HDF5', 'root': '/', 'resource_path': 'tmp/images/test_image_0000.h5', 'resource_kwargs': {'frame_per_point': 1}, 'path_semantics': 'posix', 'uid': '571cfd77-0ff1-42e8-a694-2f18b851865c', 'run_start': '0ee9f843-16ae-4851-a4ff-dbba41ae3d2f'}. Its 'root' field / was *not* modified by root_map.

@prjemian
Copy link
Contributor Author

E               event_model.UnresolvableForeignKeyError: ('8eba8e30-6f72-4907-b1e1-1bac34a3db78/0', 'Event with uid 9123f415-941d-4bef-846c-abaca85af0e8 refers to unknown Datum datum_id 8eba8e30-6f72-4907-b1e1-1bac34a3db78/0')

@gfabbris
Copy link
Collaborator

Every time I've seen these last two error was because it could not find the image because the name or path was wrong (here /tmp/images/test_image_0000.h5).

@prjemian
Copy link
Contributor Author

Every time I've seen these last two error was because it could not find the image because the name or path was wrong (here /tmp/images/test_image_0000.h5).

@gfabbris : Thanks! The test passes locally so I'm thinking this must be associated with conda environment, where the dockers are mounted in the GitHub CI, or in how the path names are constructed. The trick here is to get the CI to show the names in the various parts of the process.

@prjemian
Copy link
Contributor Author

@danielballan: I'm missing a detail here and the area detector workflow is failing Lots of diagnostics have been added to the workflow to track down this pesky varmint.

E           event_model.EventModelError: Error instantiating handler class <class 'area_detector_handlers.handlers.AreaDetectorHDF5Handler'> with Resource document {'spec': 'AD_HDF5', 'root': '/', 'resource_path': 'tmp/images/test_image_0000.h5', 'resource_kwargs': {'frame_per_point': 1}, 'path_semantics': 'posix', 'uid': '0615d2ee-6258-41a3-983c-b006de6bd8ed', 'run_start': 'fc5a3756-b98a-435a-beaf-57fd27c93801'}. Its 'root' field / was *not* modified by root_map.

We're unit testing custom (EPICS PV-directed) area detector file names with bluesky's bp.count() plan. It passes these tests at home but not on GitHub Actions. Here's the relevant section (so I believe) of the conda environment requirements:

- area-detector-handlers
- bluesky >=1.6.7
- databroker =1
- databroker-pack

@prjemian
Copy link
Contributor Author

The custom detector classes are:

class AD_EpicsHdf5FileName(FileStorePluginBase): # lgtm [py/missing-call-to-init]
"""
custom class to define image file name from EPICS
.. index:: Ophyd Device Support; AD_EpicsHdf5FileName
.. caution:: *Caveat emptor* applies here. You assume expertise!
Replace standard Bluesky algorithm where file names
are defined as UUID strings, virtually guaranteeing that
no existing images files will ever be overwritten.
Also, this method decouples the data files from the databroker,
which needs the files to be named by UUID.
.. autosummary::
~make_filename
~generate_datum
~get_frames_per_point
~stage
To allow users to control the file **name**,
we override the ``make_filename()`` method here
and we need to override some intervening classes.
To allow users to control the file **number**,
we override the ``stage()`` method here
and triple-comment out that line, and bring in
sections from the methods we are replacing here.
The image file name is set in `FileStoreBase.make_filename()`
from `ophyd.areadetector.filestore_mixins`. This is called
(during device staging) from `FileStoreBase.stage()`
EXAMPLE:
To use this custom class, we need to connect it to some
intervening structure. Here are the steps:
#. override default file naming
#. use to make your custom iterative writer
#. use to make your custom HDF5 plugin
#. use to make your custom AD support
imports::
from bluesky import RunEngine, plans as bp
from ophyd.areadetector import SimDetector, SingleTrigger
from ophyd.areadetector import ADComponent, ImagePlugin, SimDetectorCam
from ophyd.areadetector import HDF5Plugin
from ophyd.areadetector.filestore_mixins import FileStoreIterativeWrite
override default file naming::
from apstools.devices import AD_EpicsHdf5FileName
make a custom iterative writer::
class myHdf5EpicsIterativeWriter(AD_EpicsHdf5FileName, FileStoreIterativeWrite): pass
make a custom HDF5 plugin::
class myHDF5FileNames(HDF5Plugin, myHdf5EpicsIterativeWriter): pass
define support for the detector (simulated detector here)::
class MySimDetector(SingleTrigger, SimDetector):
'''SimDetector with HDF5 file names specified by EPICS'''
cam = ADComponent(SimDetectorCam, "cam1:")
image = ADComponent(ImagePlugin, "image1:")
hdf1 = ADComponent(
myHDF5FileNames,
suffix = "HDF1:",
root = "/",
write_path_template = "/",
)
create an instance of the detector::
simdet = MySimDetector("13SIM1:", name="simdet")
if hasattr(simdet.hdf1.stage_sigs, "array_counter"):
# remove this so array counter is not set to zero each staging
del simdet.hdf1.stage_sigs["array_counter"]
simdet.hdf1.stage_sigs["file_template"] = '%s%s_%3.3d.h5'
setup the file names using the EPICS HDF5 plugin::
simdet.hdf1.file_path.put("/tmp/simdet_demo/") # ! ALWAYS end with a "/" !
simdet.hdf1.file_name.put("test")
simdet.hdf1.array_counter.put(0)
If you have not already, create a bluesky RunEngine::
RE = RunEngine({})
take an image::
RE(bp.count([simdet]))
INTERNAL METHODS
"""
def __init__(self, *args, **kwargs):
FileStorePluginBase.__init__(self, *args, **kwargs)
self.filestore_spec = "AD_HDF5" # spec name stored in resource doc
self.stage_sigs.update(
[
("file_template", "%s%s_%4.4d.h5"),
("file_write_mode", "Stream"),
("capture", 1),
]
)
def make_filename(self):
"""
overrides default behavior: Get info from EPICS HDF5 plugin.
"""
# start of the file name, file number will be appended per template
filename = self.file_name.get()
# this is where the HDF5 plugin will write the image,
# relative to the IOC's filesystem
write_path = self.file_path.get()
# this is where the DataBroker will find the image,
# on a filesystem accessible to Bluesky
read_path = write_path
return filename, read_path, write_path
# def generate_datum(self, key, timestamp, datum_kwargs):
# """Generate a uid and cache it with its key for later insertion."""
# template = self.file_template.get()
# filename, read_path, write_path = self.make_filename()
# file_number = self.file_number.get()
# hdf5_file_name = template % (read_path, filename, file_number)
# # inject the actual name of the HDF5 file here into datum_kwargs
# datum_kwargs["HDF5_file_name"] = hdf5_file_name
# logger.debug("make_filename: %s", hdf5_file_name)
# logger.debug("write_path: %s", write_path)
# return super().generate_datum(key, timestamp, datum_kwargs)
def get_frames_per_point(self):
"""overrides default behavior"""
return self.num_capture.get()
def stage(self):
"""
overrides default behavior
Set EPICS items before device is staged, then copy EPICS
naming template (and other items) to ophyd after staging.
"""
# Make a filename.
filename, read_path, write_path = self.make_filename()
# Ensure we do not have an old file open.
set_and_wait(self.capture, 0)
# These must be set before parent is staged (specifically
# before capture mode is turned on. They will not be reset
# on 'unstage' anyway.
set_and_wait(self.file_path, write_path)
set_and_wait(self.file_name, filename)
# set_and_wait(self.file_number, 0)
# get file number now since it is incremented during stage()
file_number = self.file_number.get()
# Must avoid parent's stage() since it sets file_number to 0
# Want to call grandparent's stage()
# super().stage() # avoid this - sets `file_number` to zero
# call grandparent.stage()
FileStoreBase.stage(self)
# AD does the file name templating in C
# We can't access that result until after acquisition
# so we apply the same template here in Python.
template = self.file_template.get()
self._fn = template % (read_path, filename, file_number)
self._fp = read_path
if not self.file_path_exists.get():
raise IOError(f"Path {self.file_path.get()} does not exist on IOC.")
self._point_counter = itertools.count()
# from FileStoreHDF5.stage()
res_kwargs = {"frame_per_point": self.get_frames_per_point()}
self._generate_resource(res_kwargs)

and

class myHdf5EpicsIterativeWriter(AD_EpicsHdf5FileName, FileStoreIterativeWrite):
"""Build a custom iterative writer using the override."""
pass
class myHDF5FileNames(HDF5Plugin, myHdf5EpicsIterativeWriter):
"""Make a custom HDF5 plugin using the override."""
pass
# define support for the detector (ADSimDetector here):
class MySimDetector(SingleTrigger, SimDetector):
"""SimDetector with HDF5 file names specified by EPICS."""
_default_read_attrs = ["hdf1"]
cam = ADComponent(SimDetectorCam, "cam1:")
image = ADComponent(ImagePlugin, "image1:")
hdf1 = ADComponent(
myHDF5FileNames,
suffix="HDF1:",
write_path_template=WRITE_PATH_TEMPLATE,
read_path_template=READ_PATH_TEMPLATE,
)

and implementation here:

camera = MySimDetector(IOC, name="camera")
assert isinstance(camera, MySimDetector)
if hasattr(camera.hdf1.stage_sigs, "array_counter"):
# remove this so array counter is not set to zero each staging
del camera.hdf1.stage_sigs["array_counter"]
camera.cam.stage_sigs["num_images"] = 1
camera.hdf1.stage_sigs["num_capture"] = 1
camera.hdf1.stage_sigs["file_template"] = "%s%s_%3.3d.h5"
camera.hdf1.stage_sigs["compression"] = "zlib"
# ensure the capture is always staged last
camera.hdf1.stage_sigs["capture"] = camera.hdf1.stage_sigs.pop("capture")
camera.wait_for_connection()
assert camera.connected
camera.hdf1.warmup()
file_name = "test_image"
file_number = camera.hdf1.file_number.get()
file_path = AD_IOC_MOUNT_PATH / IMAGE_DIR
file_template = "%s%s_%4.4d.h5"
camera.hdf1.create_directory.put(-5)
camera.hdf1.file_name.put(file_name)
camera.hdf1.file_path.put(str(file_path))
camera.hdf1.stage_sigs["file_template"] = file_template

@prjemian
Copy link
Contributor Author

This is the initial exception (as shown in the logs):

2022-05-31T16:55:50.2489721Z             # Look up the cached Datum doc.
2022-05-31T16:55:50.2490037Z             try:
2022-05-31T16:55:50.2490261Z >               datum_doc = self._datum_cache[datum_id]
2022-05-31T16:55:50.2490599Z E               KeyError: '7677d5cb-8e93-4285-9117-87045e916754/0'

@prjemian
Copy link
Contributor Author

Passes locally, fails here.

@prjemian
Copy link
Contributor Author

prjemian commented Jun 1, 2022

tests

image file exists as acquired

# Can bluesky see the new image file?
ioc_file_name = (file_template % (file_path, "/" + file_name, file_number))
assert camera.hdf1.full_file_name.get() == ioc_file_name
ioc_tmp = pathlib.Path(BLUESKY_MOUNT_PATH).parent
assert ioc_tmp.exists(), ioc_tmp
# get the full file name in terms of the bluesky file directory
_n = len(str(AD_IOC_MOUNT_PATH.parent))
resource_file_name = ioc_file_name[_n:]
image_file = ioc_tmp / resource_file_name.lstrip("/")
assert image_file.exists(), (
f"ioc_tmp={ioc_tmp}"
f" resource_file_name={resource_file_name}"
f" image_file={image_file}"
)

document content in databroker v1

# verify that the problem key in datum_kwargs is not found
for uid in uids:
uid_start = None
uid_resource = None
id_datum = None
for doc_type, doc in cat.v1[uid].documents():
if doc_type == "start":
uid_start = doc["uid"]
elif doc_type == "datum":
id_datum = f"{uid_resource}/0"
assert doc["_name"] == "Datum", doc
assert doc["datum_id"] == id_datum, doc
# "HDF5_file_name" is an unexpected key in datum_kwargs
assert doc["datum_kwargs"] == {"point_number": 0}, doc
assert doc["resource"] == uid_resource, doc
elif doc_type == "resource":
uid_resource = doc["uid"]
assert doc["path_semantics"] == "posix", doc
assert doc["resource_kwargs"] == {"frame_per_point": 1}, doc
assert doc["resource_path"] == ioc_file_name.lstrip("/"), doc
assert doc["root"] == "/", doc
assert doc["run_start"] == uid_start, doc
assert doc["spec"] == "AD_HDF5", doc

databroker v2 view of the resource

run = cat.v2[uid]
assert run is not None
primary = run.primary
rsrc = primary._resources
assert isinstance(rsrc, list), rsrc
assert len(rsrc) == 1
rsrc = rsrc[0].to_dict()
assert isinstance(rsrc, dict), rsrc
assert rsrc["path_semantics"] == "posix", rsrc
assert rsrc["resource_kwargs"] == {"frame_per_point": 1}, rsrc
assert rsrc["resource_path"] == ioc_file_name.lstrip("/"), rsrc
assert rsrc["root"] == "/", rsrc
assert rsrc["run_start"] == uid_start, rsrc
assert rsrc["spec"] == "AD_HDF5", rsrc
assert rsrc["uid"] == uid_resource, rsrc

image file exists

# Could databroker find this file?
_n = len(str(AD_IOC_MOUNT_PATH.parent))
resource_file_name = ioc_file_name[_n:]
image_file = ioc_tmp / resource_file_name.lstrip("/")
assert image_file.exists()

image content found in file

# Could databroker read this image?
with h5py.File(str(image_file), "r") as h5:
key = "/entry/data/data"
assert key in h5
data = h5[key]
assert data is not None
assert data.shape == (1, 1024, 1024)

failing test

# If this errors, one cause is issue 651:

@prjemian
Copy link
Contributor Author

prjemian commented Jun 1, 2022

Consider these lines in make_filename() from the super class: https://github.com/bluesky/ophyd/blob/aa17c1e896aa3874ff722d72c8f6d9aa59c3ebaa/ophyd/areadetector/filestore_mixins.py#L417-L421

        filename = new_short_uid()
        formatter = datetime.now().strftime
        write_path = formatter(self.write_path_template)
        read_path = formatter(self.read_path_template)
        return filename, read_path, write_path

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unexpected key in datum_kwargs

3 participants