{ "cells": [ { "cell_type": "markdown", "id": "a13b8653", "metadata": {}, "source": [ "# Observations of Messier 51 from many telescope archives" ] }, { "cell_type": "markdown", "id": "2453f437", "metadata": {}, "source": [ "This case study searches for X-ray observations of the Messier 51 galaxy, taken by a variety of X-ray telescopes. We chose this galaxy because it is active (and thus likely X-ray bright), and nearby - this makes it a priority target for many X-ray observatories. This process could equally be applied to any named object (or object at a specific set of coordinates)." ] }, { "cell_type": "markdown", "id": "5e2a28b1", "metadata": {}, "source": [ "## Import Statements" ] }, { "cell_type": "code", "execution_count": 1, "id": "aa9f16e8", "metadata": {}, "outputs": [], "source": [ "from warnings import warn\n", "\n", "from astropy.coordinates import SkyCoord\n", "\n", "from daxa.archive import Archive\n", "from daxa.archive.assemble import assemble_archive_from_positions\n", "from daxa.exceptions import NoObsAfterFilterError\n", "from daxa.mission import XMMPointed, Chandra, ASCA, Suzaku, ROSATPointed, ROSATAllSky, \\\n", " MISS_INDEX\n", "from daxa.mission.tools import multi_mission_filter_on_positions" ] }, { "cell_type": "markdown", "id": "fa824356", "metadata": {}, "source": [ "## Other Tutorials" ] }, { "cell_type": "markdown", "id": "cc8b77fc", "metadata": {}, "source": [ "These case studies are meant to be highly specific examples of how you might acquire data for a particular science case, they do not provide general instruction on how to use DAXA missions or archives. We instead direct you to:\n", "\n", "* [Using DAXA missions](../missions.html) - Here we explain what DAXA mission classes are and how to use them to select only the data you need.\n", "* [Creating a DAXA archive](../archives.html) - This explains how to create an archive, load an existing archive, and the various properties and features of DAXA archives.\n", "* [Processing telescope data](../../../tutorials.process.html) - The processing tutorials for different missions are presented here, though there may not yet be processing support for all missions.\n", "\n", "Reading through these should give you a good understanding of how DAXA can be used to acquire, organise, and process multi-mission X-ray datasets for your specific use case." ] }, { "cell_type": "markdown", "id": "c39e054d", "metadata": {}, "source": [ "## One-line solution\n", "Though we provide individual functions that wrap the various steps required to collect all data at a given position for various telescope archives, we also include a one-line solution which executes these steps; the [assemble_archive_from_positions() function](../../../daxa.archive.html#daxa.archive.assemble.assemble_archive_from_positions). This function will by default initialise all available missions within DAXA, and then calls each mission's `filter_on_positions()` method. The return is a list of mission objects that have had the positional filtering applied, note that if no observations are returned after filtering then this mission is excluded.\n", "\n", "In this tutorial we assume knowledge of the `BaseMission.filter_on_positions()` method, if you are unfamiliar please consult the [relevant tutorial](../missions.html#Filtering-on-position).\n", "\n", "In this case study, the goal is to collect all observations of M51 from XMM, Chandra, ASCA, Suzaku, ROSAT Pointed, and ROSAT All-Sky missions (note that other missions are supported by DAXA). We present here how to achieve this with the one line solution.\n", "\n", "Firstly, in order to see how to format the input to the `missions` argument of `assemble_archive_from_positions()`, we can look at the `MISS_INDEX` dictionary:" ] }, { "cell_type": "code", "execution_count": 6, "id": "6e9bc673", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'xmm_pointed': daxa.mission.xmm.XMMPointed,\n", " 'nustar_pointed': daxa.mission.nustar.NuSTARPointed,\n", " 'erosita_calpv': daxa.mission.erosita.eROSITACalPV,\n", " 'erosita_all_sky_de_dr1': daxa.mission.erosita.eRASS1DE,\n", " 'chandra': daxa.mission.chandra.Chandra,\n", " 'rosat_all_sky': daxa.mission.rosat.ROSATAllSky,\n", " 'rosat_pointed': daxa.mission.rosat.ROSATPointed,\n", " 'swift': daxa.mission.swift.Swift,\n", " 'suzaku': daxa.mission.suzaku.Suzaku,\n", " 'asca': daxa.mission.asca.ASCA,\n", " 'integral_pointed': daxa.mission.integral.INTEGRALPointed}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "MISS_INDEX" ] }, { "cell_type": "markdown", "id": "974dbdf7", "metadata": {}, "source": [ "We can therefore use the following list to input into the `missions` argument of the `assemble_archive_from_positions()` function:" ] }, { "cell_type": "code", "execution_count": 2, "id": "b3e63b1a", "metadata": {}, "outputs": [], "source": [ "missions = ['xmm_pointed', 'chandra', 'asca', 'suzaku', 'rosat_pointed', 'rosat_all_sky']" ] }, { "cell_type": "markdown", "id": "c8584da9", "metadata": {}, "source": [ "We then input the position of M51 in the SkyCoord format:" ] }, { "cell_type": "code", "execution_count": 3, "id": "8c0bdee5", "metadata": {}, "outputs": [], "source": [ "position = SkyCoord(202.47,\t47.2, unit='deg')" ] }, { "cell_type": "markdown", "id": "cb9e06e9", "metadata": {}, "source": [ "So we can finally run the `assemble_archive_from_positions()` function with our inputs (note that the `missions` argument is optional, and by default all missions within DAXA would be searched):" ] }, { "cell_type": "code", "execution_count": 6, "id": "cee83d71", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/xmm.py:83: UserWarning: 140 of the 18123 observations located for this mission have been removed due to NaN RA or Dec values\n", " self._fetch_obs_info()\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/base.py:1373: UserWarning: Chandra FoV are difficult to define, as they can be strongly dependant on observation mode; as such take these as very approximate.\n", " fov = self.fov\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/asca.py:125: UserWarning: 5 of the 3079 observations located for ASCA have been removed due to all instrument exposures being zero.\n", " self._fetch_obs_info()\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/base.py:98: UserWarning: There are multiple chosen instruments SIS0, SIS1, GIS2, GIS3 for asca with different FoVs, but they observe simultaneously. As such the search distance has been set to the largest FoV of the chosen instruments.\n", " any_ret = change_func(*args, **kwargs)\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/suzaku.py:109: UserWarning: 14 of the 3055 observations located for Suzaku have been removed due to all instrument exposures being zero.\n", " self._fetch_obs_info()\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/suzaku.py:297: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise an error in a future version of pandas. Value '['EGS' 'GS' 'EGS' ... 'GCL' 'GCL' 'EGS']' has dtype incompatible with int16, please explicitly cast to a compatible dtype first.\n", " rel_suzaku.loc[type_recog, 'target_category'] = rel_suzaku.loc[type_recog, 'target_category'].apply(\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/archive/assemble.py:61: UserWarning: No observations found after the filter for suzaku, will not be included in the output dictionary.\n", " miss_list = multi_mission_filter_on_positions(positions, search_distance, missions)\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/archive/base.py:196: UserWarning: Proprietary data have been selected, but no credentials provided; as such the proprietary data have been excluded from download and further processing.\n", " mission.download(download_products=download_products[mission.name])\n", "Downloading XMM-Newton Pointed data: 100%|██████████| 16/16 [1:30:41<00:00, 340.11s/it]\n", "Downloading Chandra data: 100%|██████████| 28/28 [59:09<00:00, 126.78s/it] \n", "Downloading ASCA data: 100%|██████████| 2/2 [09:06<00:00, 273.26s/it]\n", "Downloading ROSAT Pointed data: 100%|██████████| 6/6 [00:59<00:00, 9.90s/it]\n", "Downloading RASS data: 100%|██████████| 2/2 [00:52<00:00, 26.14s/it]\n" ] } ], "source": [ "# M51 is the name of the archive\n", "arch = assemble_archive_from_positions('M51', positions=position, missions=missions, \n", " download_products=False)" ] }, { "cell_type": "code", "execution_count": 8, "id": "9f6ff6ea", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "-----------------------------------------------------\n", "Version - 0.0.0\n", "Number of missions - 5\n", "Total number of observations - 54\n", "Beginning of earliest observation - 1990-07-11 00:00:00\n", "End of latest observation - 2022-01-08 17:51:21\n", "\n", "-- XMM-Newton Pointed --\n", " Internal DAXA name - xmm_pointed\n", " Chosen instruments - M1, M2, PN\n", " Number of observations - 16\n", " Fully Processed - False\n", "\n", "-- Chandra --\n", " Internal DAXA name - chandra\n", " Chosen instruments - ACIS, HRC\n", " Number of observations - 28\n", " Fully Processed - False\n", "\n", "-- ASCA --\n", " Internal DAXA name - asca\n", " Chosen instruments - SIS0, SIS1, GIS2, GIS3\n", " Number of observations - 2\n", " Fully Processed - False\n", "\n", "-- ROSAT Pointed --\n", " Internal DAXA name - rosat_pointed\n", " Chosen instruments - PSPCB, PSPCC, HRI\n", " Number of observations - 6\n", " Fully Processed - False\n", "\n", "-- RASS --\n", " Internal DAXA name - rosat_all_sky\n", " Chosen instruments - PSPC\n", " Number of observations - 2\n", " Fully Processed - False\n", "-----------------------------------------------------\n", "\n" ] } ], "source": [ "arch.info()" ] }, { "cell_type": "markdown", "id": "0f7b18f4", "metadata": {}, "source": "To perform the search without creating an Archive object, we can use the [multi_mission_filter_on_positions() function](../../../daxa.mission.html#daxa.mission.tools.multi_mission_filter_on_positions):" }, { "cell_type": "code", "execution_count": 4, "id": "96ef7008", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/xmm.py:83: UserWarning: 140 of the 18126 observations located for this mission have been removed due to NaN RA or Dec values\n", " self._fetch_obs_info()\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/base.py:1373: UserWarning: Chandra FoV are difficult to define, as they can be strongly dependant on observation mode; as such take these as very approximate.\n", " fov = self.fov\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/asca.py:125: UserWarning: 5 of the 3079 observations located for ASCA have been removed due to all instrument exposures being zero.\n", " self._fetch_obs_info()\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/base.py:108: UserWarning: There are multiple chosen instruments SIS0, SIS1, GIS2, GIS3 for asca with different FoVs, but they observe simultaneously. As such the search distance has been set to the largest FoV of the chosen instruments.\n", " any_ret = change_func(*args, **kwargs)\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/suzaku.py:109: UserWarning: 14 of the 3055 observations located for Suzaku have been removed due to all instrument exposures being zero.\n", " self._fetch_obs_info()\n", "/Users/jp735/Desktop/DAXA_dev/DAXA/daxa/mission/suzaku.py:297: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise an error in a future version of pandas. Value '['EGS' 'GS' 'EGS' ... 'GCL' 'GCL' 'EGS']' has dtype incompatible with int16, please explicitly cast to a compatible dtype first.\n", " rel_suzaku.loc[type_recog, 'target_category'] = rel_suzaku.loc[type_recog, 'target_category'].apply(\n", "/var/folders/86/x534hjnd60nfqyng9m_xwb740000gr/T/ipykernel_46523/339423454.py:1: UserWarning: No observations found after the filter for suzaku, will not be included in the output dictionary.\n", " multi_mission_filter_on_positions(position, missions=missions)\n" ] }, { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "multi_mission_filter_on_positions(position, missions=missions)" ] }, { "cell_type": "markdown", "id": "cda7ed3b", "metadata": {}, "source": [ "## Breaking Down the Steps\n", "\n", "To better understand what the `assemble_archive_from_positions()` function is doing, we show the individual steps that have taken place." ] }, { "cell_type": "markdown", "id": "1db78091", "metadata": {}, "source": [ "## Defining Missions\n", "\n", "We create instances of the XMM, Chandra, ASCA, Suzaku, ROSAT Pointed, and ROSAT All-Sky missions in order to search their archives - other missions are supported by DAXA (and can be found in the missions tutorial), but these are a subset likely to have observations of M51:" ] }, { "cell_type": "code", "execution_count": 2, "id": "032436b4", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/dt237/code/DAXA/daxa/mission/xmm.py:83: UserWarning: 140 of the 17697 observations located for this mission have been removed due to NaN RA or Dec values\n", " self._fetch_obs_info()\n", "/Users/dt237/code/DAXA/daxa/mission/asca.py:91: UserWarning: 5 of the 3079 observations located for ASCA have been removed due to all instrument exposures being zero.\n", " self._fetch_obs_info()\n", "/Users/dt237/code/DAXA/daxa/mission/suzaku.py:96: UserWarning: 14 of the 3055 observations located for Suzaku have been removed due to all instrument exposures being zero.\n", " self._fetch_obs_info()\n" ] } ], "source": [ "xm = XMMPointed()\n", "ch = Chandra()\n", "asc = ASCA()\n", "su = Suzaku()\n", "rp = ROSATPointed()\n", "ra = ROSATAllSky()" ] }, { "cell_type": "markdown", "id": "c7dda794", "metadata": {}, "source": [ "## Searching for observations" ] }, { "cell_type": "markdown", "id": "df78c594", "metadata": {}, "source": [ "In this instance we use the `filter_on_name` method to search for observations of M51 - this will use the Sesame name resolver to look-up the coordinates for the object. Alternatively, we could use the `filter_on_positions` method and supply the coordinate ourselves:" ] }, { "cell_type": "code", "execution_count": 3, "id": "2e622e5a", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/dt237/code/DAXA/daxa/mission/base.py:1075: UserWarning: Chandra FoV are difficult to define, as they can be strongly dependant on observation mode; as such take these as very approximate.\n", " fov = self.fov\n", "/Users/dt237/code/DAXA/daxa/mission/base.py:97: UserWarning: There are multiple chosen instruments SIS0, SIS1, GIS2, GIS3 for asca with different FoVs, but they observe simultaneously. As such the search distance has been set to the largest FoV of the chosen instruments.\n", " any_ret = change_func(*args, **kwargs)\n" ] } ], "source": [ "xm.filter_on_name(\"M51\")\n", "ch.filter_on_name(\"M51\")\n", "asc.filter_on_name(\"M51\")\n", "rp.filter_on_name(\"M51\")\n", "ra.filter_on_name(\"M51\")" ] }, { "cell_type": "markdown", "id": "4ba6b610", "metadata": {}, "source": [ "We have deliberately separated the Suzaku search, as we are aware that it will not find any matching data - as such we'll use this to highlight that the standard Python exception-catching statements can be used to stop a failure to find data derailing your script (for instance you might wish to iterate through a list of missions and have a try-except statement like this:" ] }, { "cell_type": "code", "execution_count": 4, "id": "fb181a5a", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/opt/anaconda3/envs/daxa_dev/lib/python3.9/site-packages/IPython/core/interactiveshell.py:3433: UserWarning: The positional search has returned no Suzaku observations.\n", " exec(code_obj, self.user_global_ns, self.user_ns)\n" ] } ], "source": [ "try:\n", " su.filter_on_name(\"M51\")\n", "except NoObsAfterFilterError as err:\n", " warn(err.message, stacklevel=2)" ] }, { "cell_type": "markdown", "id": "a055080c", "metadata": {}, "source": [ "## Example of observations identified from filtering" ] }, { "cell_type": "markdown", "id": "a51d1fac", "metadata": {}, "source": [ "We can use the `filtered_obs_info` property to retrieve the information table describing the accepted observations:" ] }, { "cell_type": "code", "execution_count": 5, "id": "7ff1fc78", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
radecObsIDscience_usablestartenddurationinstrumentwith_filtertarget_categorytarget_nameproc_revfits_type
368202.4747.2RH600601N00True1994-06-18 13:11:33.0000001994-06-24 08:04:24.0000010 days 10:05:23HRINNGSM 512RDF 3_4
917202.4747.2RP600158N00True1991-11-28 16:07:59.9999981991-12-13 17:50:20.9999990 days 06:39:16PSPCBNNGSN5194/N51952RDF 3_4
2904202.4747.2RH600062A03True1994-05-22 05:24:01.0000021994-05-23 07:16:07.0000000 days 02:36:32HRINNGSM512RDF 3_4
3096202.4747.2RH600062A01True1992-05-22 23:44:46.0000001992-06-05 21:50:42.9999990 days 02:27:11HRINNGSM512RFITS V3.
3174202.4747.2RH600062A00True1991-12-07 09:46:57.0000031992-01-10 05:10:12.9999960 days 02:22:21HRINNGSM512RFITS V3.
3303202.4747.2RH601115N00True1997-12-26 23:37:19.0000011997-12-30 07:46:24.0000010 days 02:15:04HRINNGS2RDF 4_2
\n", "
" ], "text/plain": [ " ra dec ObsID science_usable start \\\n", "368 202.47 47.2 RH600601N00 True 1994-06-18 13:11:33.000000 \n", "917 202.47 47.2 RP600158N00 True 1991-11-28 16:07:59.999998 \n", "2904 202.47 47.2 RH600062A03 True 1994-05-22 05:24:01.000002 \n", "3096 202.47 47.2 RH600062A01 True 1992-05-22 23:44:46.000000 \n", "3174 202.47 47.2 RH600062A00 True 1991-12-07 09:46:57.000003 \n", "3303 202.47 47.2 RH601115N00 True 1997-12-26 23:37:19.000001 \n", "\n", " end duration instrument with_filter \\\n", "368 1994-06-24 08:04:24.000001 0 days 10:05:23 HRI N \n", "917 1991-12-13 17:50:20.999999 0 days 06:39:16 PSPCB N \n", "2904 1994-05-23 07:16:07.000000 0 days 02:36:32 HRI N \n", "3096 1992-06-05 21:50:42.999999 0 days 02:27:11 HRI N \n", "3174 1992-01-10 05:10:12.999996 0 days 02:22:21 HRI N \n", "3303 1997-12-30 07:46:24.000001 0 days 02:15:04 HRI N \n", "\n", " target_category target_name proc_rev fits_type \n", "368 NGS M 51 2 RDF 3_4 \n", "917 NGS N5194/N5195 2 RDF 3_4 \n", "2904 NGS M51 2 RDF 3_4 \n", "3096 NGS M51 2 RFITS V3. \n", "3174 NGS M51 2 RFITS V3. \n", "3303 NGS 2 RDF 4_2 " ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "rp.filtered_obs_info" ] }, { "cell_type": "markdown", "id": "f905d480", "metadata": {}, "source": [ "## Defining an Archive" ] }, { "cell_type": "markdown", "id": "750c635b", "metadata": {}, "source": [ "The filtered missions can then be used to define an archive containing the selected data:" ] }, { "cell_type": "code", "execution_count": 6, "id": "efd11a02", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Downloading XMM-Newton Pointed data: 100%|████████████████████████████████████| 16/16 [07:37<00:00, 28.60s/it]\n", "Downloading Chandra data: 100%|███████████████████████████████████████████████| 28/28 [02:57<00:00, 6.32s/it]\n", "Downloading ASCA data: 100%|████████████████████████████████████████████████████| 2/2 [01:00<00:00, 30.23s/it]\n", "Downloading ROSAT Pointed data: 100%|███████████████████████████████████████████| 6/6 [00:11<00:00, 1.93s/it]\n", "Downloading RASS data: 100%|████████████████████████████████████████████████████| 2/2 [00:09<00:00, 4.55s/it]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "-----------------------------------------------------\n", "Number of missions - 5\n", "Total number of observations - 54\n", "Beginning of earliest observation - 1990-07-11 00:00:00\n", "End of latest observation - 2022-01-08 17:51:21\n", "\n", "-- XMM-Newton Pointed --\n", " Internal DAXA name - xmm_pointed\n", " Chosen instruments - M1, M2, PN\n", " Number of observations - 16\n", " Fully Processed - False\n", "\n", "-- Chandra --\n", " Internal DAXA name - chandra\n", " Chosen instruments - ACIS-I, ACIS-S, HRC-I, HRC-S\n", " Number of observations - 28\n", " Fully Processed - False\n", "\n", "-- ASCA --\n", " Internal DAXA name - asca\n", " Chosen instruments - SIS0, SIS1, GIS2, GIS3\n", " Number of observations - 2\n", " Fully Processed - False\n", "\n", "-- ROSAT Pointed --\n", " Internal DAXA name - rosat_pointed\n", " Chosen instruments - PSPCB, PSPCC, HRI\n", " Number of observations - 6\n", " Fully Processed - False\n", "\n", "-- RASS --\n", " Internal DAXA name - rosat_all_sky\n", " Chosen instruments - PSPC\n", " Number of observations - 2\n", " Fully Processed - False\n", "-----------------------------------------------------\n", "\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "arch = Archive('M51', [xm, ch, asc, rp, ra])\n", "arch.info()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3.13.1 ('daxa_dev': venv)", "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.13.1" }, "vscode": { "interpreter": { "hash": "b745ee2e03c74046425ba0075c6a485bd3a0b3209d382eeafefb40aa57902723" } } }, "nbformat": 4, "nbformat_minor": 5 }