Tutorial¶
The databroker is a tool to access data from many sources through a unified interface. It emphasizes rich searching capabilities and handling multiple concurrent “streams” of data in an organized way.
Basic Walkthrough¶
Get a Broker¶
List the names of available configurations.
In [1]: from databroker import list_configs
In [2]: list_configs()
Out[2]: ['example']
If this list is empty, no one has created any configuration files yet. See the section on Configuration.
Make a databroker using one of the configurations.
In [3]: from databroker import Broker
In [4]: db = Broker.named('example')
Load Data as a Table¶
Load the most recently saved run.
In [5]: header = db[-1]
The result, a Header
, encapsulates the metadata from this run. Loading
the data itself can be a longer process, so it’s a separate step. For scalar
data, the most convenient method is:
In [6]: header.table()
Out[6]:
time det motor motor_setpoint
seq_num
1 2020-11-04 22:11:59.017866850 0.606531 1.0 1.0
2 2020-11-04 22:11:59.022908449 0.135335 2.0 2.0
3 2020-11-04 22:11:59.026233912 0.011109 3.0 3.0
4 2020-11-04 22:11:59.029233932 0.000335 4.0 4.0
5 2020-11-04 22:11:59.032141685 0.000004 5.0 5.0
This object is DataFrame, a spreadsheet-like object provided by the library pandas.
Note
For Python novices we point out that header
above is an arbitrary
variable name. It could have been:
h = db[-1]
h.table()
or even in one line:
db[-1].table()
Do Analysis or Export¶
DataFrames can be used to perform fast computations on labeled data, such as
In [7]: t = header.table()
In [8]: t.mean(numeric_only=True)
Out[8]:
det 0.150663
motor 3.000000
motor_setpoint 3.000000
dtype: float64
In [9]: t['det'] / t['motor']
Out[9]:
seq_num
1 6.065307e-01
2 6.766764e-02
3 3.702999e-03
4 8.386566e-05
5 7.453306e-07
dtype: float64
or export to a file.
In [10]: t.to_csv('data.csv')
Load Data Lazily (Good for Image Data)¶
The Header.table
method is just one way to load the data. Another is
Header.data
, which loads data for one specific field (i.e., one column
of the table) in a “lazy”, streaming fashion.
In [11]: data = header.data('det')
In [12]: data # This a 'generator' that will load data when we loop through it.
Out[12]: <generator object Header.data at 0x7fe8bcff8de0>
In [13]: for point in data:
....: print(point)
....:
0.6065306597126334
0.1353352832366127
0.011108996538242306
0.00033546262790251185
3.726653172078671e-06
The Header.data
method is suitable for loading image data. See
the API Documentation for more methods.
Explore Metadata¶
Everything recorded at the start of the run is in header.start
.
In [14]: header.start
Out[14]:
{'uid': 'bb805845-5757-4642-b3ef-7ab14e9fd31e',
'time': 1604527919.0073605,
'versions': {'ophyd': '1.5.4', 'bluesky': '1.6.7'},
'scan_id': 5,
'plan_type': 'generator',
'plan_name': 'scan',
'detectors': ['det'],
'motors': ['motor'],
'num_points': 5,
'num_intervals': 4,
'plan_args': {'detectors': ["SynGauss(prefix='', name='det', read_attrs=['val'], configuration_attrs=['Imax', 'center', 'sigma', 'noise', 'noise_multiplier'])"],
'num': 5,
'args': ["SynAxis(prefix='', name='motor', read_attrs=['readback', 'setpoint'], configuration_attrs=['velocity', 'acceleration'])",
1,
5],
'per_step': 'None'},
'hints': {'dimensions': [[['motor'], 'primary']]},
'plan_pattern': 'inner_product',
'plan_pattern_module': 'bluesky.plan_patterns',
'plan_pattern_args': {'num': 5,
'args': ["SynAxis(prefix='', name='motor', read_attrs=['readback', 'setpoint'], configuration_attrs=['velocity', 'acceleration'])",
1,
5]}}
Information only knowable at the end, like the exit status (success, abort,
fail) is stored in header.stop
.
In [15]: header.stop
Out[15]:
{'run_start': 'bb805845-5757-4642-b3ef-7ab14e9fd31e',
'time': 1604527919.0332673,
'uid': 'fe00f574-61a1-4ddc-9f9d-2560a41f01ee',
'exit_status': 'success',
'reason': '',
'num_events': {'primary': 5}}
Metadata about the devices involved and their configuration is stored in
header.descriptors
, but that is quite a lot to dig through, so it’s useful
to start with some convenience methods that extract the list of devices or the
fields that they reported:
In [16]: header.devices()
Out[16]: {'det', 'motor'}
In [17]: header.fields()
Out[17]: {'det', 'motor', 'motor_setpoint'}
To extract configuration data recorded by a device:
In [18]: header.config_data('motor')
Out[18]: {'primary': [{'motor_velocity': 1, 'motor_acceleration': 1}]}
(A realistic example might report, for example, exposure_time or zero point.)
Searching¶
The “slicing” (square bracket) syntax is a quick way to search based on relative indexing, unique ID, or counting number scan_id. Examples:
# Get the most recent run.
header = db[-1]
# Get the fifth most recent run.
header = db[-5]
# Get a list of all five most recent runs, using Python slicing syntax.
headers = db[-5:]
# Get a run whose unique ID ("RunStart uid") begins with 'x39do5'.
header = db['x39do5']
# Get a run whose integer scan_id is 42. Note that this might not be
# unique. In the event of duplicates, the most recent match is returned.
header = db[42]
Calling a Broker like a function (with parentheses) accesses richer searches.
Common search parameters include plan_name
, motor
, and detectors
.
Any user-provided metadata can be used in a search. Examples:
# Search by plan name.
headers = db(plan_name='scan')
# Search for runs involving a motor with the name 'eta'.
headers = db(motor='eta')
# Search for runs operated by a given user---assuming this metadata was
# recorded in the first place!
headers = db(operator='Dan')
# Search by time range. (These keywords have a special meaning.)
headers = db(since='2015-03-05', until='2015-03-10')
Full-text search is also supported, for MongoDB-backed deployments. (Other
deployments will raise NotImplementedError
if you try this.)
# Perform text search on all values in the Run Start document.
headers = db('keyword')
Note that partial words are not matched, but partial phrases are. For example, ‘good’ will match to ‘good sample’ but ‘goo’ will not.
Unlike the “slicing” (square bracket) queries, rich searches can return an unbounded number of results. To avoid slowness, the results are loaded “lazily,” only as needed. Here’s an example of what works and what doesn’t.
In [19]: headers = db(plan_name='scan')
In [20]: headers
Out[20]: <databroker.v1.Results at 0x7fe8bd067a20>
In [21]: headers[2] # Fails! The results are not a list.
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-21-6a220047f4ab> in <module>
----> 1 headers[2] # Fails! The results are not a list.
TypeError: 'Results' object does not support indexing
In [22]: list(headers)[2] # This works, but might be slow if the results are large.
Out[22]: <databroker.v1.Header at 0x7fe8bc8c04e0>
Looping through them loads one at a time, conserving memory.
In [23]: for header in headers:
....: print(header.table()['det'].mean())
....:
0.1506628257537126
0.1506628257537126
0.1506628257537126
0.1506628257537126
0.1506628257537126