{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# How to Search for Bluesky Data?\n", "\n", "Show [how to search](https://github.com/BCDA-APS/apstools/issues/674) for\n", "Bluesky data from a [databroker](https://blueskyproject.io/databroker) catalog.\n", "\n", "The databroker [search and\n", "lookup](https://blueskyproject.io/databroker/tutorials/search-and-lookup.html)\n", "tutorial is a great way to learn how to search for Bluesky data.\n", "This How-To guide continues from the tutorial, using additional support from\n", "[apstools](https://BCDA-APS.github.io/apstools/latest/).\n", "Custom queries are expressed using [MongoDB\n", "Query](https://www.mongodb.com/docs/manual/reference/operator/query/).\n", "Additional help with the MongoDB query language, operators, and syntax may be\n", "found online.\n", "The content from\n", "[w3schools](https://www.w3schools.com/python/python_mongodb_query.asp) is both\n", "informative and compact." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What is a catalog?\n", "\n", "A *catalog* is a group of Bluesky measurements from the [databroker](https://blueskyproject.io/databroker).\n", "\n", "Here, we create a catalog, a group of Bluesky *runs* from the `class_2021_03` database." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "catalogs available: list(databroker.catalog)=['bdp2022', 'class_2021_03', '6idb_export', 'apstools_test', 'class_data_examples', 'usaxs_test', 'korts202106', 'training']\n" ] }, { "data": { "application/yaml": "class_2021_03:\n args:\n asset_registry_db: mongodb://localhost:27017/class_2021_03-bluesky\n metadatastore_db: mongodb://localhost:27017/class_2021_03-bluesky\n name: class_2021_03\n description: ''\n driver: databroker._drivers.mongo_normalized.BlueskyMongoCatalog\n metadata:\n catalog_dir: /home/prjemian/.local/share/intake/\n", "text/plain": [ "class_2021_03:\n", " args:\n", " asset_registry_db: mongodb://localhost:27017/class_2021_03-bluesky\n", " metadatastore_db: mongodb://localhost:27017/class_2021_03-bluesky\n", " name: class_2021_03\n", " description: ''\n", " driver: databroker._drivers.mongo_normalized.BlueskyMongoCatalog\n", " metadata:\n", " catalog_dir: /home/prjemian/.local/share/intake/\n" ] }, "metadata": { "application/json": { "root": "class_2021_03" } }, "output_type": "display_data" } ], "source": [ "import databroker\n", "\n", "print(f\"catalogs available: {list(databroker.catalog)=}\")\n", "\n", "cat = databroker.catalog[\"class_2021_03\"].v2\n", "cat" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What does the `cat` object describe?" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "cat.name='class_2021_03'\n", "len(cat)=40 measurements (runs) in the catalog\n", "cat.metadata={'catalog_dir': '/home/prjemian/.local/share/intake/'}\n" ] } ], "source": [ "print(f\"{cat.name=}\")\n", "print(f\"{len(cat)=} measurements (runs) in the catalog\")\n", "print(f\"{cat.metadata=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NOTE: A search on a catalog object returns a new catalog as filtered by the search parameters." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What is a run?\n", "\n", "Bluesky uses the term *run* to describe a single measurement in a catalog. A run contains data and metadata from a single measurement, scan, or other data acquisition, such as [count](https://blueskyproject.io/bluesky/generated/bluesky.plans.count.html#bluesky.plans.count) or [scan](https://blueskyproject.io/bluesky/generated/bluesky.plans.scan.html#bluesky.plans.scan).\n", "\n", "A *run* consists of several parts. Each part has its own type of document, as follows:\n", "\n", "document | description\n", "--- | ---\n", "start | Initial information about the measurement, including metadata.\n", "descriptor | A description of the data to be collected.\n", "event | The measurement data.\n", "stop | A final summary of the measurement." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## How to retrieve a run?\n", "\n", "Retrieve any run from the catalog using one of three references:\n", "\n", "reference | example | type | description\n", "--- | --- | --- | ---\n", "`scan_id` | `cat[192]` | *positive* integer | not necessarily unique, returns most recent\n", "relative | `cat[-1]` | *negative* integer | `-1` is most recent run, `-2` is the run before, ...\n", "`uid` | `cat[\"abc1234\"]` | [UUID](https://docs.python.org/3/library/uuid.html#uuid.uuid4) string | unique, matches the first characters of the start document `uid`\n", "\n", "A `uid` is created and returned by the Bluesky `RunEngine` after it executes a\n", "plan (that [starts a new run](https://blueskyproject.io/bluesky/msg.html#open-run)).\n", "\n", "While a full `uid` provides a unique reference to a run, it appears to humans to\n", "be a random sequence of hexadecimal characters with hyphens at irregular\n", "intervals. Partial representation of the `uid` is allowed, matching from the\n", "start of the full `uid`. The given *short* `uid` must include enough characters\n", "to make a unique match in the catalog **and** must include up to the first\n", "non-numeric character to avoid mis-interpretation as an integer. You will be\n", "advised if the *short* `uid` is not a unique match.\n", "\n", "The first seven characters (`uid7`) are often sufficient as a short `uid`. There is a 1 in $16^7$ (268 million) chance of this not being unique." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Each of these references retrieve the same run:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "scan_id: cat[192]=\n", "scan_id: cat['192']=\n", "relative: cat[-1]=\n", "relative: cat['-1']=\n", "short uid: cat['e3']=\n", "short uid: cat['e3862991-688d']=\n" ] } ], "source": [ "print(f\"scan_id: {cat[192]=}\")\n", "print(f\"scan_id: {cat['192']=}\") # treated as an integer by databroker\n", "print(f\"relative: {cat[-1]=}\")\n", "print(f\"relative: {cat['-1']=}\") # treated as an integer by databroker\n", "print(f\"short uid: {cat['e3']=}\") # shortest version that works\n", "print(f\"short uid: {cat['e3862991-688d']=}\") # must include the hyphen" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Show the run has the expected `scan_id`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "scan_id: cat[192].metadata['start']['scan_id']=192\n" ] }, { "data": { "text/plain": [ "BlueskyRun\n", " uid='e3862991-688d-43dc-8442-d85ccbb3d6c8'\n", " exit_status='success'\n", " 2021-05-19 15:22:03.070 -- 2021-05-19 15:22:07.087\n", " Streams:\n", " * baseline\n", " * primary\n" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "print(f\"scan_id: {cat[192].metadata['start']['scan_id']=}\")\n", "cat[-1]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There is one more way to retrieve runs from the catalog, iterate over the catalog to get the full `uid` of each run, then use `cat[uid]` to get the run object. This technique might be more useful on smaller catalogs.\n", "\n", "Here, we break after the first one (to limit output):" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "uid='e3862991-688d-43dc-8442-d85ccbb3d6c8' cat[uid]=\n" ] } ], "source": [ "for uid in cat:\n", " print(f\"{uid=} {cat[uid]=}\")\n", " break" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Search with `listruns()`\n", "\n", "**Q**: What are the most recent runs?\n", "\n", "The `apstools.utils.listruns()` function from\n", "[apstools](https://bcda-aps.github.io/apstools/latest/) provides a listing of the most\n", "recent runs. Taking all the default settings, `listruns()` shows (up to) the 20\n", "most recent runs in the catalog.\n", "\n", "Here, the first column is an index number (and can be ignored), the remaining\n", "columns are labeled. Note the `time` column includes both the `yyyy-mm-dd` date\n", "and the `HH:MM:SS` time of day (24-hour time)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: class_2021_03\n", " scan_id time plan_name detectors\n", "0 192 2021-05-19 15:22:03 rel_scan [scaler1, adsimdet, zaxis]\n", "1 191 2021-05-19 15:19:27 rel_scan [scaler1, adsimdet]\n", "2 190 2021-05-19 15:18:59 rel_scan [scaler1]\n", "3 189 2021-05-19 15:18:28 rel_scan [scaler1, temperature]\n", "4 188 2021-05-19 15:14:56 rel_scan [scaler1]\n", "5 187 2021-05-19 15:13:47 rel_scan [scaler1]\n", "6 33 2021-04-07 15:55:09 scan [noisy]\n", "7 33 2021-03-17 00:32:55 count [adsimdet]\n", "8 32 2021-03-17 00:31:44 count [temperature]\n", "9 31 2021-03-17 00:31:24 count [temperature]\n", "10 30 2021-03-17 00:31:23 count [temperature]\n", "11 29 2021-03-17 00:31:23 count [temperature]\n", "12 28 2021-03-17 00:30:33 rel_scan [noisy]\n", "13 27 2021-03-17 00:30:27 rel_scan [noisy]\n", "14 26 2021-03-17 00:30:21 rel_scan [noisy]\n", "15 25 2021-03-17 00:30:04 rel_scan [noisy]\n", "16 24 2021-03-17 00:29:57 rel_scan [noisy]\n", "17 23 2021-03-17 00:29:40 rel_scan [noisy]\n", "18 22 2021-03-17 00:27:55 count [scaler1, noisy]\n", "19 21 2021-03-17 00:27:23 count [scaler1]\n" ] } ], "source": [ "from apstools.utils import listruns\n", "\n", "listruns()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are many options for `listruns()`, show them using the `listruns?` syntax:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[0;31mSignature:\u001b[0m\n", "\u001b[0mlistruns\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mcat\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mkeys\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mmissing\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m''\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mnum\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m20\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mprinting\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'smart'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mreverse\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0msince\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0msortby\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'time'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mtablefmt\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'dataframe'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mtimefmt\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'%Y-%m-%d %H:%M:%S'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0muntil\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mids\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mquery\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mDocstring:\u001b[0m\n", "List runs from catalog.\n", "\n", "This function provides a thin interface to the highly-reconfigurable\n", "``ListRuns()`` class in this package.\n", "\n", "PARAMETERS\n", "\n", "cat\n", " *object* :\n", " Instance of databroker v1 or v2 catalog.\n", "keys\n", " *str* or *[str]* or None:\n", " Include these additional keys from the start document.\n", " (default: ``None`` means ``\"scan_id time plan_name detectors\"``)\n", "missing\n", " *str*:\n", " Test to report when a value is not available.\n", " (default: ``\"\"``)\n", "ids\n", " *[int]* or *[str]*:\n", " List of ``uid`` or ``scan_id`` value(s).\n", " Can mix different kinds in the same list.\n", " Also can specify offsets (e.g., ``-1``).\n", " According to the rules for ``databroker`` catalogs,\n", " a string is a ``uid`` (partial representations allowed),\n", " an int is ``scan_id`` if positive or an offset if negative.\n", " (default: ``None``)\n", "num\n", " *int* :\n", " Make the table include the ``num`` most recent runs.\n", " (default: ``20``)\n", "printing\n", " *bool* or ``\"smart\"``:\n", " If ``True``, print the table to stdout.\n", " If ``\"smart\"``, then act as shown below.\n", " (default: ``True``)\n", "\n", " ================ ===================\n", " session action(s)\n", " ================ ===================\n", " python session print and return ``None``\n", " Ipython console return ``DataFrame`` object\n", " Jupyter notebook return ``DataFrame`` object\n", " ================ ===================\n", "\n", "reverse\n", " *bool* :\n", " If ``True``, sort in descending order by ``sortby``.\n", " (default: ``True``)\n", "since\n", " *str* :\n", " include runs that started on or after this ISO8601 time\n", " (default: ``\"1995-01-01\"``)\n", "sortby\n", " *str* :\n", " Sort columns by this key, found by exact match in either\n", " the ``start`` or ``stop`` document.\n", " (default: ``\"time\"``)\n", "tablefmt\n", " *str* :\n", " When returning an object, specify which type\n", " of object to return.\n", " (default: ``\"dataframe\",``)\n", "\n", " ========== ==============\n", " value object\n", " ========== ==============\n", " dataframe ``pandas.DataFrame``\n", " table ``str(pyRestTable.Table)``\n", " ========== ==============\n", "\n", "timefmt\n", " *str* :\n", " The ``time`` key (also includes keys ``\"start.time\"`` and ``\"stop.time\"``)\n", " will be formatted by the ``self.timefmt`` value.\n", " See https://strftime.org/ for examples. The special ``timefmt=\"raw\"``\n", " is used to report time as the raw value (floating point time as used in\n", " python's ``time.time()``).\n", " (default: ``\"%Y-%m-%d %H:%M:%S\",``)\n", "until\n", " *str* :\n", " include runs that started before this ISO8601 time\n", " (default: ``2100-12-31``)\n", "``**query``\n", " *dict* :\n", " Any additional keyword arguments will be passed to\n", " the databroker to refine the search for matching runs\n", " using the ``mongoquery`` package.\n", "\n", "RETURNS\n", "\n", "object:\n", " ``None`` or ``str`` or ``pd.DataFrame()`` object\n", "\n", "EXAMPLE::\n", "\n", " TODO\n", "\n", "(new in release 1.5.0)\n", "\u001b[0;31mFile:\u001b[0m ~/Documents/projects/BCDA-APS/apstools/apstools/utils/list_runs.py\n", "\u001b[0;31mType:\u001b[0m function\n" ] } ], "source": [ "listruns?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Search within a range of dates\n", "\n", "**Q**: What are the runs between certain dates?\n", "\n", "To find runs that started since a particular date, use `listruns(since=\"yyyy-mm-dd hh:mm\")` where `yyyy-mm-dd hh:mm` is a suggestion. You only need to supply the parts that matter, so \"2:00\" would find all runs that started after 2 AM today.\n", "\n", "Here, we look for runs **since** the beginning of April 2021. (Because the date is\n", "incomplete, it is implied that the full specification is `2021-04-01\n", "00:00:00.0000000`.)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: class_2021_03\n", " scan_id time plan_name detectors\n", "0 192 2021-05-19 15:22:03 rel_scan [scaler1, adsimdet, zaxis]\n", "1 191 2021-05-19 15:19:27 rel_scan [scaler1, adsimdet]\n", "2 190 2021-05-19 15:18:59 rel_scan [scaler1]\n", "3 189 2021-05-19 15:18:28 rel_scan [scaler1, temperature]\n", "4 188 2021-05-19 15:14:56 rel_scan [scaler1]\n", "5 187 2021-05-19 15:13:47 rel_scan [scaler1]\n", "6 33 2021-04-07 15:55:09 scan [noisy]\n" ] } ], "source": [ "listruns(since=\"2021-04\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Likewise, find runs that started before a particular date and time using `listruns(until=\"yyyy-mm-dd hh:mm\")`\n", "\n", "Here, the specification will include any date **until** (before) the beginning of May 2021." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: class_2021_03\n", " scan_id time plan_name detectors\n", "0 33 2021-04-07 15:55:09 scan [noisy]\n", "1 33 2021-03-17 00:32:55 count [adsimdet]\n", "2 32 2021-03-17 00:31:44 count [temperature]\n", "3 31 2021-03-17 00:31:24 count [temperature]\n", "4 30 2021-03-17 00:31:23 count [temperature]\n", "5 29 2021-03-17 00:31:23 count [temperature]\n", "6 28 2021-03-17 00:30:33 rel_scan [noisy]\n", "7 27 2021-03-17 00:30:27 rel_scan [noisy]\n", "8 26 2021-03-17 00:30:21 rel_scan [noisy]\n", "9 25 2021-03-17 00:30:04 rel_scan [noisy]\n", "10 24 2021-03-17 00:29:57 rel_scan [noisy]\n", "11 23 2021-03-17 00:29:40 rel_scan [noisy]\n", "12 22 2021-03-17 00:27:55 count [scaler1, noisy]\n", "13 21 2021-03-17 00:27:23 count [scaler1]\n", "14 20 2021-03-17 00:27:22 count [scaler1]\n", "15 19 2021-03-16 17:06:01 scan [scaler1, temperature]\n", "16 18 2021-03-16 17:05:51 scan [scaler1, temperature]\n", "17 17 2021-03-16 17:05:42 scan [scaler1, temperature]\n", "18 16 2021-03-16 17:05:32 scan [scaler1, temperature]\n", "19 15 2021-03-16 17:05:03 scan [scaler1, temperature]\n" ] } ], "source": [ "listruns(until=\"2021-05\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can combine them:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: class_2021_03\n", " scan_id time plan_name detectors\n", "0 33 2021-04-07 15:55:09 scan [noisy]\n" ] } ], "source": [ "listruns(since=\"2021-04\", until=\"2021-05\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Search metadata keys\n", "\n", "**Q**: How to search for runs matching certain metadata?\n", "\n", "Bluesky stores a run's metadata in the `start` document. Any of the terms may\n", "be searched by adding keyword arguments to `listruns()`, the value is the\n", "content to match.\n", "\n", "Since only a few metadata keys are *standard* (such as `time` and `uid`), you should be prepared for the possibility that any particular key may not be found." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's take a look at the metadata of the most recent run in the catalog:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'start': Start({'beamline_id': 'APS_Python_training_2021',\n", " 'detectors': ['scaler1', 'adsimdet', 'zaxis'],\n", " 'hints': {'dimensions': [[['zaxis_h'], 'primary']]},\n", " 'instrument_name': 'class_2021_03',\n", " 'login_id': 'prjemian@zap',\n", " 'motors': ['zaxis_h'],\n", " 'notebook': 'UB_autosave',\n", " 'num_intervals': 7,\n", " 'num_points': 8,\n", " 'objective': 'Demonstrate UB matrix save & restore',\n", " 'pid': 3712584,\n", " 'plan_args': {'args': [\"PseudoSingle(prefix='', name='zaxis_h', \"\n", " \"parent='zaxis', settle_time=0.0, timeout=None, \"\n", " \"egu='', limits=(0, 0), source='computed', \"\n", " \"read_attrs=['readback', 'setpoint'], \"\n", " 'configuration_attrs=[], idx=0)',\n", " -0.1,\n", " 0.1],\n", " 'detectors': [\"ScalerCH(prefix='gp:scaler1', name='scaler1', \"\n", " \"read_attrs=['channels', 'channels.chan01', \"\n", " \"'channels.chan01.s', 'channels.chan02', \"\n", " \"'channels.chan02.s', 'channels.chan03', \"\n", " \"'channels.chan03.s', 'channels.chan04', \"\n", " \"'channels.chan04.s', 'time'], \"\n", " \"configuration_attrs=['channels', \"\n", " \"'channels.chan01', 'channels.chan01.chname', \"\n", " \"'channels.chan01.preset', \"\n", " \"'channels.chan01.gate', 'channels.chan02', \"\n", " \"'channels.chan02.chname', \"\n", " \"'channels.chan02.preset', \"\n", " \"'channels.chan02.gate', 'channels.chan03', \"\n", " \"'channels.chan03.chname', \"\n", " \"'channels.chan03.preset', \"\n", " \"'channels.chan03.gate', 'channels.chan04', \"\n", " \"'channels.chan04.chname', \"\n", " \"'channels.chan04.preset', \"\n", " \"'channels.chan04.gate', 'count_mode', 'delay', \"\n", " \"'auto_count_delay', 'freq', 'preset_time', \"\n", " \"'auto_count_time', 'egu'])\",\n", " \"MySimDetector(prefix='ad:', name='adsimdet', \"\n", " \"read_attrs=['hdf1'], configuration_attrs=['cam', \"\n", " \"'cam.acquire_period', 'cam.acquire_time', \"\n", " \"'cam.image_mode', 'cam.manufacturer', \"\n", " \"'cam.model', 'cam.num_exposures', \"\n", " \"'cam.num_images', 'cam.trigger_mode', 'hdf1'])\",\n", " \"MyZaxis(prefix='', name='zaxis', \"\n", " \"settle_time=0.0, timeout=None, egu='', \"\n", " \"limits=(0, 0), source='computed', \"\n", " \"read_attrs=['h', 'h.readback', 'h.setpoint', \"\n", " \"'k', 'k.readback', 'k.setpoint', 'l', \"\n", " \"'l.readback', 'l.setpoint', 'mu', 'omega', \"\n", " \"'delta', 'gamma'], \"\n", " \"configuration_attrs=['energy', 'geometry_name', \"\n", " \"'class_name', 'UB', 'reflections_details', 'h', \"\n", " \"'k', 'l'], concurrent=True)\"],\n", " 'num': 8,\n", " 'per_step': 'None'},\n", " 'plan_name': 'rel_scan',\n", " 'plan_pattern': 'inner_product',\n", " 'plan_pattern_args': {'args': [\"PseudoSingle(prefix='', name='zaxis_h', \"\n", " \"parent='zaxis', settle_time=0.0, \"\n", " \"timeout=None, egu='', limits=(0, 0), \"\n", " \"source='computed', read_attrs=['readback', \"\n", " \"'setpoint'], configuration_attrs=[], idx=0)\",\n", " -0.1,\n", " 0.1],\n", " 'num': 8},\n", " 'plan_pattern_module': 'bluesky.plan_patterns',\n", " 'plan_type': 'generator',\n", " 'proposal_id': 'training',\n", " 'scan_id': 192,\n", " 'time': 1621455723.0701044,\n", " 'uid': 'e3862991-688d-43dc-8442-d85ccbb3d6c8',\n", " 'versions': {'apstools': '1.5.0rc1',\n", " 'bluesky': '1.6.7',\n", " 'databroker': '1.2.2',\n", " 'epics': '3.4.3',\n", " 'h5py': '3.2.1',\n", " 'intake': '0.6.2',\n", " 'matplotlib': '3.3.4',\n", " 'numpy': '1.20.1',\n", " 'ophyd': '1.6.1',\n", " 'pyRestTable': '2020.0.3',\n", " 'spec2nexus': '2021.1.8'}}),\n", " 'stop': Stop({'exit_status': 'success',\n", " 'num_events': {'baseline': 2, 'primary': 8},\n", " 'reason': '',\n", " 'run_start': 'e3862991-688d-43dc-8442-d85ccbb3d6c8',\n", " 'time': 1621455727.087438,\n", " 'uid': '70b9a95f-59aa-408d-8819-1463f3eebac5'}),\n", " 'catalog_dir': None}" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "run = cat[-1]\n", "run.metadata" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A plan's name is stored in the run's metadata as the `plan_name` key. (There is\n", "no guarantee that a run will have this metadata key.) To list runs measured\n", "with the `count` plan, use `listruns(plan_name=\"count\")`." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: class_2021_03\n", " scan_id time plan_name detectors\n", "0 33 2021-03-17 00:32:55 count [adsimdet]\n", "1 32 2021-03-17 00:31:44 count [temperature]\n", "2 31 2021-03-17 00:31:24 count [temperature]\n", "3 30 2021-03-17 00:31:23 count [temperature]\n", "4 29 2021-03-17 00:31:23 count [temperature]\n", "5 22 2021-03-17 00:27:55 count [scaler1, noisy]\n", "6 21 2021-03-17 00:27:23 count [scaler1]\n", "7 20 2021-03-17 00:27:22 count [scaler1]\n", "8 5 2021-03-15 11:54:56 count [temperature]\n", "9 4 2021-03-15 11:49:42 count [temperature]\n", "10 3 2021-03-15 11:46:21 count [temperature]\n", "11 2 2021-03-15 11:44:21 count [temperature]\n", "12 1 2021-03-15 00:52:29 count [temperature]\n" ] } ], "source": [ "listruns(plan_name=\"count\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can combine a search with more than one metadata key, such as *any `count` run `#20`*." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: class_2021_03\n", " scan_id time plan_name detectors\n", "0 20 2021-03-17 00:27:22 count [scaler1]\n" ] } ], "source": [ "listruns(plan_name=\"count\", scan_id=20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Search metadata keys using MongoDB Query\n", "\n", "**Q**: How to search for a range of some metadata key?\n", "\n", "Searching for a specific `scan_id` is awkward. Since we know that `scan_id` is\n", "stored as a number, we can apply range limits (such as `30 <= scan_id < 100`).\n", "That type of search requires the syntax from [MongoDB\n", "Query](https://www.mongodb.com/docs/manual/reference/operator/query/).\n", "\n", "A Query is built as a Python dictionary where the comparison operators are the\n", "keys and the comparison values are the corresponding values. These are the\n", "terms we need for this query:\n", "\n", "operator | MongoDB Query\n", "--- | ---\n", "`>=` | `\"$gte\"`\n", "`<` | `\"$lt\"`\n", "\n", "The full Query is `scan_id={\"$gte\": 30, \"$lt\": 100}`.\n", "\n", "Here, search for `scan_id` matching that Query and a `count` plan:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: class_2021_03\n", " scan_id time plan_name detectors\n", "0 33 2021-03-17 00:32:55 count [adsimdet]\n", "1 32 2021-03-17 00:31:44 count [temperature]\n", "2 31 2021-03-17 00:31:24 count [temperature]\n", "3 30 2021-03-17 00:31:23 count [temperature]\n" ] } ], "source": [ "listruns(scan_id={\"$gte\": 30, \"$lt\": 100}, plan_name=\"count\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the `plan_name=\"count\"` expression here is equivalent to the MongoDB Query expression `plan_name={\"$eq\": \"count\"}`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Search with a Filtered catalog\n", "\n", "As an alternative to `listruns()`, you can create a *filtered* catalog by\n", "applying [MongoDB Query](https://www.mongodb.com/docs/manual/reference/operator/query/)\n", "searches to an existing catalog.\n", "\n", "Let's start with a larger initial catalog to demonstrate." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "There are 8116 runs in the 'training' catalog.\n" ] } ], "source": [ "cat = databroker.catalog[\"training\"].v2\n", "print(f\"There are {len(cat)} runs in the '{cat.name}' catalog.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Get runs from the `count()` plan\n", "\n", "**Q**: How many runs use the `count` plan?\n", "\n", "To answer this, we must know how to apply the MongoDB Query to the catalog. The catalog has a `.search(query)` method, where the `query` term is the dictionary, similar to how it was used above. Instead of a keyword, though, the metadata key is ***in*** the dictionary." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "query={'plan_name': {'$eq': 'count'}}\n", "There are 295 runs collected by the 'count()' plan.\n" ] } ], "source": [ "query = {\"plan_name\":{\"$eq\": \"count\"}}\n", "print(f\"{query=}\")\n", "filtered_cat = cat.search(query)\n", "print(f\"There are {len(filtered_cat)} runs collected by the 'count()' plan.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Searches can get more detailed. Find all `count` runs with `scan_id` between 70\n", "and 80. (This catalog has duplicates in this range.) Display the filtered\n", "catalog using `listruns()`" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "len(filtered_cat)=18\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "catalog: search results\n", " scan_id time plan_name detectors\n", "0 79 2022-05-28 13:09:24 count [simdet]\n", "1 78 2022-05-28 13:06:23 count [adsimdet]\n", "2 77 2022-05-27 12:37:54 count [adsimdet]\n", "3 76 2022-05-27 12:28:50 count [adsimdet]\n", "4 75 2022-05-27 12:27:19 count [simdet]\n", "5 74 2022-05-26 17:29:10 count [simdet]\n", "6 73 2022-05-26 17:25:14 count [simdet]\n", "7 72 2022-05-26 17:24:33 count [simdet]\n", "8 71 2022-05-26 17:23:51 count [simdet]\n", "9 79 2021-04-15 09:00:24 count [scaler1, count_difference]\n", "10 78 2021-04-15 09:00:22 count [scaler1, count_difference]\n", "11 77 2021-04-15 08:59:20 count [scaler1, count_difference]\n", "12 76 2021-04-15 08:59:15 count [scaler1, count_difference]\n", "13 75 2021-04-15 08:59:10 count [scaler1, count_difference]\n", "14 74 2021-04-15 08:59:04 count [scaler1, count_difference]\n", "15 73 2021-04-15 08:48:09 count [scaler1]\n", "16 72 2021-04-15 08:48:05 count [scaler1]\n", "17 71 2021-04-15 08:48:00 count [scaler1]\n" ] } ], "source": [ "filtered_cat = cat.search({\"plan_name\":{\"$eq\": \"count\"}, \"scan_id\": {\"$gt\": 70, \"$lt\": 80}})\n", "print(f\"{len(filtered_cat)=}\")\n", "listruns(filtered_cat)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Custom report\n", "\n", "**Q**: How to make a custom report?\n", "\n", "List of recent runs using `scan` (or `rel_scan`) including these keys:\n", "\n", "- plan\n", "- detectors\n", "- start position\n", "- end position\n", "- number of points\n", "- metadata\n", "\n", "All of this information is available from the run but some of it is not\n", "presented in the metadata with individually-named keys. Because some of the\n", "information to be reported (specifically, arguments to the plan, including start\n", "and end position) is not easily extracted with MongoDB, custom code is needed;\n", "the `listruns()` function cannot be used.\n", "\n", "Make a function that matches a pre-determined set of search terms and prints a report." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "since='2022-05'\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/.conda/envs/bluesky_2022_3/lib/python3.9/site-packages/databroker/queries.py:89: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n", " timezone = lz.zone\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "len(filtered_cat)=129\n", "======= ======== ========= ===== ======== ======= ======== ========================== ======= ================ =========================== ============ ===========\n", "scan_id plan detectors motor start end n_points date uid7 beamline_id instrument_name login_id proposal_id\n", "======= ======== ========= ===== ======== ======= ======== ========================== ======= ================ =========================== ============ ===========\n", "299 scan noisy m1 -1.2 1.2 21 2022-10-25 14:10:06.464000 3703202 Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "2 rel_scan noisy m1 -0.27632 0.27632 23 2022-10-12 15:23:32.336000 4704d7f Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "1 rel_scan noisy m1 -2 2 23 2022-10-12 15:23:26.220000 a5ada81 Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "271 rel_scan noisy m1 -0.56269 0.56269 21 2022-09-07 14:40:31.996000 c53365c Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "270 rel_scan noisy m1 -5 5 21 2022-09-07 14:40:03.057000 166dc23 Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "269 rel_scan noisy m1 -1.2 1.2 21 2022-09-07 14:39:39.521000 11542a9 Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "268 rel_scan noisy m1 -0.28913 0.28913 21 2022-09-07 14:38:30.682000 c51776e Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "267 rel_scan noisy m1 -1.2 1.2 21 2022-09-07 14:38:18.649000 4bb967f Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "266 rel_scan noisy m1 -1.2 1.2 21 2022-09-07 14:37:51.675000 7fe7229 Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "265 rel_scan noisy m1 -1.2 1.2 21 2022-09-07 14:37:32.813000 7969b6e Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "263 scan noisy m1 -1.2 1.2 21 2022-09-07 11:27:13.747000 c7b10f1 Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "260 scan noisy m1 -1.2 1.2 21 2022-06-29 09:42:54.977000 0783d90 Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "259 scan noisy m1 -1.2 1.2 21 2022-06-29 09:40:13.666000 2a487dd Bluesky_training BCDA EPICS Bluesky training prjemian@zap training \n", "10 rel_scan noisy m1 -0.22238 0.22238 23 2022-05-24 15:05:32.791000 72faef4 \n", "9 rel_scan noisy m1 -2 2 23 2022-05-24 15:05:16.337000 ad2bac4 \n", "8 rel_scan noisy m1 -0.06626 0.06626 23 2022-05-24 15:05:11.307000 9102fe6 \n", "7 rel_scan noisy m1 -0.08168 0.08168 23 2022-05-24 15:05:08.414000 dcafde6 \n", "6 rel_scan noisy m1 -0.25194 0.25194 23 2022-05-24 15:05:02.797000 d57a79a \n", "5 rel_scan noisy m1 -2.1 2.1 23 2022-05-24 15:04:45.942000 c82643b \n", "4 rel_scan noisy m1 -0.17233 0.17233 23 2022-05-24 15:04:38.514000 318ae81 \n", "======= ======== ========= ===== ======== ======= ======== ========================== ======= ================ =========================== ============ ===========\n", "\n" ] } ], "source": [ "import datetime\n", "import pyRestTable\n", "import time\n", "\n", "def custom_list(cat, n_runs=20, start_md_keys=None):\n", " \"\"\"\n", " List n_runs most recent, successful (rel_)scan runs.\n", "\n", " PARAMETERS\n", "\n", " cat *object*:\n", " Databroker catalog to be searched.\n", " n_runs *int*:\n", " Maximum number of runs to list in table. (default: 20)\n", " start_md_keys *[str]*:\n", " List of additional (start document) metadata keys to report.\n", " (default: 'beamline_id, instrument_name, login_id, proposal_id')\n", " \"\"\"\n", " start_md_keys = start_md_keys or \"\"\"\n", " beamline_id instrument_name login_id proposal_id\n", " \"\"\".split()\n", "\n", " ts = time.time() - 60*60*24*7*(52/2) # ~6 months ago\n", " dt = datetime.datetime.fromtimestamp(ts)\n", " since = f\"{dt.year}-{dt.month:02d}\" # start of that month\n", " print(f\"{since=}\")\n", "\n", " query = dict(\n", " plan_name={\"$in\": [\"scan\", \"rel_scan\"]},\n", " detectors=[\"noisy\"], # only this detector\n", " )\n", " query.update(databroker.queries.TimeRange(since=since))\n", " filtered_cat = cat.v2.search(query)\n", " print(f\"{len(filtered_cat)=}\")\n", "\n", " table = pyRestTable.Table()\n", " table.labels = \"\"\"\n", " scan_id plan detectors motor start end n_points date uid7\n", " \"\"\".split()\n", " table.labels += start_md_keys\n", "\n", " def get_name_from_device_repr(text):\n", " \"\"\"\n", " Dig out the name of the motor object from the 'plan_args'.\n", "\n", " \"MyEpicsMotor(prefix='gp:m1', name='m1', ...)\"\n", " From this string, only the `name='m1'` part is interesting here.\n", " \"\"\"\n", " s = text.find(\"name='\") + 6\n", " f = text[s:].find(\"'\") + s\n", " return text[s:f]\n", "\n", " for uid in filtered_cat:\n", " run = filtered_cat[uid]\n", "\n", " success = run.metadata['stop']['exit_status'] == \"success\"\n", " if not success:\n", " continue\n", "\n", " uid7 = uid[:7]\n", " date = datetime.datetime.fromtimestamp(round(run.metadata['start']['time'], 3))\n", " scan_id = run.metadata['start']['scan_id']\n", " detectors = run.metadata['start']['detectors']\n", " # motors = run.metadata['start']['motors']\n", " plan = run.metadata['start']['plan_name']\n", " # n_points = run.metadata['start']['num_points'] # requested\n", " plan_args = run.metadata['start']['plan_args']['args']\n", " n_points = run.metadata['stop']['num_events']['primary'] # recorded\n", "\n", " # This data can't be extracted using only MongoDB Query\n", " p_start, p_end = plan_args[1:3] # as defined in the bp.scan() plan\n", " # TODO: this assumes only 1 motor is scanned!\n", " motors = [get_name_from_device_repr(plan_args[0])]\n", " # Multi-axis scans are: motor, start, finish triples for each axis, in order\n", "\n", " # build the table row\n", " row = [\n", " scan_id,\n", " plan, \", \".join(detectors), \", \".join(motors), round(p_start, 5), round(p_end, 5), n_points,\n", " date, uid7,\n", " ]\n", " row += [run.metadata['start'].get(k, \"\") for k in start_md_keys]\n", "\n", " table.rows.append(row)\n", " if len(table.rows) >= n_runs:\n", " break\n", "\n", " print(table)\n", "\n", "custom_list(cat, 20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "-----------\n", "\n", "## Other searches: \n", "\n", "
\n", "TODO:\n", "\n", "Some of these seaches may need additional Python code to complete. Others may\n", "be expedited by additional MongoDB Query constructs.\n", "\n", "* What version of `bluesky` was used 6 months ago?\n", "* When was `apstools` version 1.2 - 1.4 used?\n", "* Find all runs with sample \"xyz\" measured with detector `adsimdet`. List the most recent ones.\n", " * Do we have any APS catalogs with sample name as metadata?\n", " * similar: `listruns(detectors={\"$in\": [\"adsimdet\"]}, plan_name=\"count\")`\n", "* Restrict that list to runs in APS cycle `2021-3` (if no APS cycle info, the last 4 months of 2021).\n", "* Find runs where \"Joe User\" appears somewhere in the metadata.\n", "* Find runs where \"silver behenate\" appears somewhere in the metadata.\n", "* Find runs that failed for some reason. Is there any indication why? (`run.metadata['stop']['reason']`)\n", "\n", " ```py\n", " listruns(\n", " cat,\n", " keys=\"uid scan_id stop.exit_status stop.reason\",\n", " **{\n", " \"stop.exit_status\": {\"$ne\": \"success\"}, # FIXME: not working\n", " }\n", " )\n", " ```\n", "\n", "* Plot centroid and width of all successful scans of specific y vs. x with more than one data point for the last week.\n", "* What about these?\n", " - using start or stop document metadata\n", " - user\n", " - sample\n", " - Proposal or ESAF ID\n", " - fuzzy or misspelled terms\n", " - combination searches\n", "\n", "
" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3.9.13 ('bluesky_2022_3')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.13 | packaged by conda-forge | (main, May 27 2022, 16:56:21) \n[GCC 10.3.0]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "b8b33b6f973508780ebf5a25daa70491e33966a11f9c922d918d5dafd5e1ee6b" } } }, "nbformat": 4, "nbformat_minor": 2 }