{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Antenna reader example" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from radiocalibrationtoolkit import *\n", "from healpy.newvisufunc import projview\n", "import plotly.graph_objects as go\n", "\n", "# This ensures Plotly output works in multiple places:\n", "# plotly_mimetype: VS Code notebook UI\n", "# notebook: \"Jupyter: Export to HTML\" command in VS Code\n", "# See https://plotly.com/python/renderers/#multiple-renderers\n", "import plotly.io as pio\n", "\n", "pio.renderers.default = \"plotly_mimetype+notebook\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "layout_settings = dict(\n", " xaxis=dict(title=\"azimuth [°]\",\n", " tickprefix=\"\",\n", " ticksuffix=\"\",\n", " dtick=30),\n", " yaxis=dict(\n", " title=\"zenith angle [°]\",\n", " tickprefix=\"\",\n", " ticksuffix=\"\",\n", " dtick=10,\n", " ),\n", " coloraxis=dict(colorbar=dict(\n", " title=dict(\n", " text=\"VEL\",\n", " side=\"right\",\n", " ),\n", " tickprefix=\"\",\n", " ticksuffix=\"\",\n", " ), ),\n", " font=dict(\n", " size=15,\n", " color=\"black\",\n", " ),\n", ")\n", "\n", "colormap = 'jet'\n", "\n", "\n", "def get_plotly_cbar_label(quantity):\n", " if quantity == 'absolute':\n", " return \"<|H|>\"\n", " elif \"Phi_amp\" in quantity:\n", " return \"|HΦ|\"\n", " elif \"Theta_amp\" in quantity:\n", " return \"|Hϴ|\"\n", "\n", "\n", "shift_phi = 0" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "# create antenna instance\n", "antenna_inst = AntennaPattern(\"./antenna_setup_files/SALLA_EW.xml\")\n", "\n", "# the NS antenna pattern is shift by 90 degrees clockwise in azimuth, to turn in back, we use shift_phi=-90\n", "# shift_phi = -90\n", "# new_antenna_conventions = {\n", "# \"shift_phi\": shift_phi,\n", "# }\n", "# antenna_inst = AntennaPattern(\"./antenna_setup_files/SALLA_NS.xml\",\n", "# new_antenna_conventions=new_antenna_conventions)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# XML values in dictionary\n", "# antenna_inst.get_raw()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# get antenna gain for unpolarized emission\n", "df = antenna_inst.get(frequency=45, quantity=\"absolute\")\n", "\n", "# plot\n", "fig = px.imshow(df.T,\n", " width=600,\n", " aspect=\"cube\",\n", " origin='lower',\n", " color_continuous_scale=colormap)\n", "fig.update_layout(**layout_settings)\n", "fig.update_layout(coloraxis=dict(colorbar=dict(title=dict(\n", " text=get_plotly_cbar_label(\"absolute\")))))\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# interpolate\n", "df = antenna_inst.get(frequency=45,\n", " quantity='absolute',\n", " interp_phi=np.linspace(0, 360, 200),\n", " interp_theta=np.linspace(0, 90, 100))\n", "\n", "# plot\n", "fig = px.imshow(df.T,\n", " width=600,\n", " aspect=\"cube\",\n", " origin='lower',\n", " color_continuous_scale=colormap)\n", "fig.update_layout(**layout_settings)\n", "fig.update_layout(coloraxis=dict(colorbar=dict(title=dict(\n", " text=get_plotly_cbar_label(\"absolute\")))))\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# antenna gain as a function of frequency, azimuth and zenith in the required polarization as volumetric dataset\n", "volumetric_dataset_df = antenna_inst.get_volumetric_dataset(\n", " quantity=\"EAHTheta_amp\", frequencies=np.arange(30, 80, 1))\n", "volumetric_dataset_df" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# zenith slice\n", "df = volumetric_dataset_df.unstack().stack(level=0).loc[:, 66, :]\n", "# plot\n", "fig = px.imshow(df,\n", " width=600,\n", " aspect=\"cube\",\n", " origin='lower',\n", " color_continuous_scale=colormap)\n", "fig.update_layout(**layout_settings)\n", "fig.update_layout(\n", " title=\"Slice at 66° zenith angle\",\n", " xaxis=dict(title=\"azimuth [°]\"),\n", " yaxis=dict(title=\"frequency [MHz]\"),\n", " coloraxis=dict(colorbar=dict(title=dict(\n", " text=get_plotly_cbar_label(\"EAHTheta_amp\")))),\n", ")\n", "\n", "fig.show()\n", "\n", "# azimuth slice\n", "df = volumetric_dataset_df.loc[:, 180, :]\n", "# plot\n", "fig = px.imshow(df,\n", " width=600,\n", " aspect=\"cube\",\n", " origin='lower',\n", " color_continuous_scale=colormap)\n", "fig.update_layout(**layout_settings)\n", "fig.update_layout(\n", " title=\"Slice at 180° azimuth\",\n", " xaxis=dict(title=\"zenith angle [°]\", dtick=10),\n", " yaxis=dict(title=\"frequency [MHz]\"),\n", " coloraxis=dict(colorbar=dict(title=dict(\n", " text=get_plotly_cbar_label(\"EAHTheta_amp\")))),\n", ")\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "# some settings\n", "font = \"Arial Black\"\n", "colorscale = \"jet\"\n", "\n", "scene = dict(\n", " xaxis=dict(title=\"Zenith angle [°]\", color=\"black\", dtick=\"30\"),\n", " yaxis=dict(title=\"Azimuth [°]\", color=\"black\", dtick=\"90\"),\n", " zaxis=dict(\n", " title=\"frequency [MHz]\",\n", " tickvals=[40, 50, 60, 70, 80],\n", " ),\n", " aspectratio=dict(x=0.75, y=0.75,\n", " z=0.75), # Adjust the aspect ratio as needed\n", ")\n", "\n", "# define some functions\n", "\n", "\n", "def get_volume_data4plots(antenna_inst, quantity):\n", " \"\"\"\n", " Get volumetric data and grid for creating 3D plots.\n", "\n", " Parameters\n", " ----------\n", " antenna_inst : AntennaInstance\n", " An instance of the antenna used for data retrieval.\n", " quantity : str\n", " The quantity of interest (e.g., 'absolute', 'EAHTheta_amp').\n", "\n", " Returns\n", " -------\n", " tuple\n", " A tuple containing:\n", " 1. volumetric_dataset_df : pandas.DataFrame\n", " The volumetric dataset as a pandas DataFrame.\n", " 2. volume_data : numpy.ndarray\n", " The reshaped volumetric dataset.\n", " 3. PHI : numpy.ndarray\n", " The meshgrid of PHI values.\n", " 4. FREQ : numpy.ndarray\n", " The meshgrid of frequency values.\n", " 5. THETA : numpy.ndarray\n", " The meshgrid of THETA values.\n", " \"\"\"\n", "\n", " volumetric_dataset_df = antenna_inst.get_volumetric_dataset(\n", " quantity=quantity, frequencies=np.arange(30, 80, 3))\n", "\n", " volume_data = volumetric_dataset_df.to_numpy().reshape(\n", " volumetric_dataset_df.index.get_level_values(\"freq\").nunique(),\n", " -1,\n", " volumetric_dataset_df.shape[1],\n", " )\n", " PHI, FREQ, THETA = np.meshgrid(\n", " volumetric_dataset_df.index.get_level_values(\"phi\").unique().values,\n", " volumetric_dataset_df.index.get_level_values(\"freq\").unique().values,\n", " volumetric_dataset_df.columns.values,\n", " )\n", " return volumetric_dataset_df, volume_data, PHI, FREQ, THETA\n", "\n", "\n", "def quantity_label2colorbar_dict(quantity_label):\n", " \"\"\"\n", " Create a colorbar dictionary for a given quantity label.\n", "\n", " Parameters\n", " ----------\n", " quantity_label : str\n", " The label for the quantity to be displayed on the colorbar.\n", "\n", " Returns\n", " -------\n", " dict\n", " A dictionary containing colorbar configuration settings.\n", " \"\"\"\n", " return dict(len=0.75,\n", " title=dict(\n", " text=\"\" + quantity_label + \" [m]\",\n", " side=\"right\",\n", " ),\n", " tickprefix=\"\",\n", " ticksuffix=\"\",\n", " tickfont=dict(size=30))\n", "\n", "\n", "def create_volume_trace(\n", " volume_data,\n", " PHI,\n", " FREQ,\n", " THETA,\n", " quantity,\n", " opacity=1,\n", " surface_fill=0.001,\n", " surface_count=1,\n", " opacityscale=\"uniform\",\n", " caps=dict(x_show=False, y_show=False, z_show=False),\n", "):\n", " \"\"\"\n", " Create a go.Volume trace based on the given parameters.\n", "\n", " Parameters\n", " ----------\n", " volume_data : numpy.ndarray\n", " The reshaped volumetric data.\n", " PHI : numpy.ndarray\n", " The meshgrid of PHI values.\n", " FREQ : numpy.ndarray\n", " The meshgrid of frequency values.\n", " THETA : numpy.ndarray\n", " The meshgrid of THETA values.\n", " quantity : str\n", " The quantity of interest.\n", " opacity : float, optional\n", " The opacity of the volume. Default is 1.\n", " surface_fill : float, optional\n", " The surface fill value. Default is 0.001.\n", " surface_count : int, optional\n", " The surface count value. Default is 1.\n", " opacityscale : str, optional\n", " The opacity scale. Default is \"uniform\".\n", " caps : dict, optional\n", " The caps configuration. Default is dict(x_show=False, y_show=False, z_show=False).\n", "\n", " Returns\n", " -------\n", " go.Volume\n", " A go.Volume trace object.\n", " \"\"\"\n", " return go.Volume(\n", " y=PHI.flatten(),\n", " x=THETA.flatten(),\n", " z=FREQ.flatten(),\n", " value=np.around(volume_data.flatten(), 3),\n", " cmin=0,\n", " cmax=2,\n", " opacity=opacity,\n", " # isomax=1.3,\n", " surface_fill=surface_fill,\n", " surface_count=surface_count,\n", " colorscale=\"jet\",\n", " opacityscale=opacityscale,\n", " caps=caps,\n", " colorbar=quantity_label2colorbar_dict(get_plotly_cbar_label(quantity)),\n", " )\n", "\n", "\n", "def built_volume_and_slice_plots(\n", " antenna_inst,\n", " quantity='absolute',\n", " slice_types={\n", " \"no_slices\": [],\n", " \"slices_z\": [45, 60],\n", " \"slices_x\": [30, 65],\n", " \"slices_y\": [0, 270],\n", " }):\n", " \"\"\"\n", " Build 3D volume and slice plots for a given antenna instance.\n", "\n", " Parameters\n", " ----------\n", " antenna_inst : AntennaInstance\n", " An instance of the antenna used for data retrieval.\n", " quantity : str, optional\n", " The quantity of interest (default is 'absolute').\n", " slice_types : dict, optional\n", " Dictionary specifying slice types and their locations (default is None).\n", " \"\"\"\n", "\n", " volumetric_dataset_df, volume_data, PHI, FREQ, THETA = get_volume_data4plots(\n", " antenna_inst, quantity)\n", "\n", " # make plots\n", " for slice_type, locations in slice_types.items():\n", " if slice_type == \"no_slices\":\n", " fig = go.Figure()\n", " fig.add_trace(\n", " create_volume_trace(\n", " volume_data,\n", " PHI,\n", " FREQ,\n", " THETA,\n", " quantity,\n", " opacity=0.8,\n", " surface_fill=1,\n", " opacityscale=\"max\",\n", " surface_count=40,\n", " caps=dict(x_show=True, y_show=True, z_show=True),\n", " ))\n", " else:\n", " fig = go.Figure()\n", " slice_trace = create_volume_trace(\n", " volume_data,\n", " PHI,\n", " FREQ,\n", " THETA,\n", " quantity,\n", " )\n", " slice_trace.update(\n", " **{slice_type: dict(show=True, locations=locations)})\n", " fig.add_trace(slice_trace)\n", "\n", " fig.update_layout(\n", " scene=scene,\n", " margin=dict(r=50, b=10, l=10, t=10),\n", " height=600,\n", " autosize=False,\n", " font=dict(family=font, size=18, color=\"black\"),\n", " )\n", " fig.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "## show antenna pattern as volumetric data with slices in all axis\n", "built_volume_and_slice_plots(antenna_inst, quantity=\"absolute\")\n", "# built_volume_and_slice_plots(antenna_inst, quantity=\"EAHTheta_amp\")\n", "# built_volume_and_slice_plots(antenna_inst, quantity=\"EAHPhi_amp\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "# show antenna pattern as volumetric data with slices in all axis\n", "# version of plots with sliders\n", "# first some functions...\n", "\n", "\n", "def show_slices(x, y, z, volume_data, quantity, slice_type=\"x\"):\n", " \"\"\"\n", " Show slices of volumetric data using the given parameters.\n", "\n", " Parameters\n", " ----------\n", " x : np.ndarray\n", " The x-coordinate values.\n", " y : np.ndarray\n", " The y-coordinate values.\n", " z : np.ndarray\n", " The z-coordinate values.\n", " volume_data : np.ndarray\n", " The volumetric data.\n", " slice_type : str, optional\n", " The type of slice. Valid values are 'x', 'y', or 'z'. Default is 'x'.\n", "\n", " Returns\n", " -------\n", " None\n", " This function does not return anything. It displays the plot.\n", "\n", " \"\"\"\n", " quantity_label = get_plotly_cbar_label(quantity)\n", "\n", " x_delta = np.diff(x)[0]\n", " y_delta = np.diff(y)[0]\n", " z_delta = np.diff(z)[0]\n", "\n", " cmin = 0\n", " cmax = 2\n", " colorscale = \"jet\"\n", "\n", " if slice_type == \"x\":\n", " slider_label = x\n", " fig = go.Figure(frames=[\n", " go.Frame(\n", " data=go.Surface(\n", " x=np.ones(x.size) * x[k],\n", " y=y,\n", " z=(np.ones((z.size, y.size)) * z[:, np.newaxis]).T,\n", " surfacecolor=(volume_data[:, :, k]).T,\n", " cmin=cmin,\n", " cmax=cmax,\n", " ),\n", " name=str(k),\n", " ) for k in range(x.size)\n", " ])\n", "\n", " # default fig\n", " fig.add_trace(\n", " go.Surface(\n", " x=np.ones(x.size) * x[0],\n", " y=y,\n", " z=(np.ones((z.size, y.size)) * z[:, np.newaxis]).T,\n", " surfacecolor=(volume_data[:, :, 0]).T,\n", " colorscale=colorscale,\n", " cmin=cmin,\n", " cmax=cmax,\n", " colorbar=quantity_label2colorbar_dict(quantity_label),\n", " ))\n", "\n", " elif slice_type == \"y\":\n", " slider_label = y\n", " fig = go.Figure(frames=[\n", " go.Frame(\n", " data=go.Surface(\n", " x=x,\n", " y=np.ones(y.size) * y[k],\n", " z=np.ones((z.size, x.size)) * z[:, np.newaxis],\n", " surfacecolor=(volume_data[:, y.size - 1 - k, :]),\n", " cmin=cmin,\n", " cmax=cmax,\n", " ),\n", " name=str(k),\n", " ) for k in range(y.size)\n", " ])\n", "\n", " # default fig\n", " fig.add_trace(\n", " go.Surface(\n", " x=x,\n", " y=np.ones(y.size) * y[0],\n", " z=np.ones((z.size, x.size)) * z[:, np.newaxis],\n", " surfacecolor=(volume_data[:, 0, :]),\n", " colorscale=colorscale,\n", " cmin=cmin,\n", " cmax=cmax,\n", " colorbar=quantity_label2colorbar_dict(quantity_label),\n", " ))\n", " elif slice_type == \"z\":\n", " slider_label = z\n", "\n", " fig = go.Figure(frames=[\n", " go.Frame(\n", " data=go.Surface(\n", " x=x,\n", " y=y,\n", " z=z[k] * np.ones((y.size, x.size)),\n", " surfacecolor=np.flipud(volume_data[z.size - 1 - k]),\n", " cmin=cmin,\n", " cmax=cmax,\n", " ),\n", " name=str(k),\n", " ) for k in range(z.size)\n", " ])\n", "\n", " # default fig\n", " fig.add_trace(\n", " go.Surface(\n", " x=x,\n", " y=y,\n", " z=z[0] * np.ones((y.size, x.size)),\n", " surfacecolor=np.flipud(volume_data[z.size - 1]),\n", " colorscale=colorscale,\n", " cmin=cmin,\n", " cmax=cmax,\n", " colorbar=quantity_label2colorbar_dict(quantity_label),\n", " ))\n", "\n", " def frame_args(duration):\n", " return {\n", " \"frame\": {\n", " \"duration\": duration\n", " },\n", " \"mode\": \"immediate\",\n", " \"fromcurrent\": True,\n", " \"transition\": {\n", " \"duration\": duration,\n", " \"easing\": \"linear\"\n", " },\n", " }\n", "\n", " sliders = [{\n", " \"pad\": {\n", " \"b\": 10,\n", " \"t\": 60\n", " },\n", " \"len\":\n", " 0.9,\n", " \"x\":\n", " 0.1,\n", " \"y\":\n", " 0,\n", " \"steps\": [{\n", " \"args\": [[f.name], frame_args(0)],\n", " \"label\": str(slider_label[k]),\n", " \"method\": \"animate\",\n", " } for k, f in enumerate(fig.frames)],\n", " }]\n", "\n", " # Layout\n", " fig.update_layout(\n", " title=\"
Slices in volumetric data\",\n", " width=800,\n", " height=800,\n", " scene=dict(\n", " yaxis=dict(range=[y[0] - y_delta, y[-1] + y_delta],\n", " autorange=False), # Set the y-axis range from -5 to +5\n", " zaxis=dict(range=[z[0] - z_delta, z[-1] + z_delta],\n", " autorange=False),\n", " xaxis=dict(range=[x[0] - x_delta, x[-1] + x_delta],\n", " autorange=False),\n", " aspectratio=dict(x=1, y=1, z=1),\n", " ),\n", " updatemenus=[{\n", " \"buttons\": [\n", " {\n", " \"args\": [None, frame_args(50)],\n", " \"label\": \"▶\", # play symbol\n", " \"method\": \"animate\",\n", " },\n", " {\n", " \"args\": [[None], frame_args(0)],\n", " \"label\": \"◼\", # pause symbol\n", " \"method\": \"animate\",\n", " },\n", " ],\n", " \"direction\":\n", " \"left\",\n", " \"pad\": {\n", " \"r\": 10,\n", " \"t\": 70\n", " },\n", " \"type\":\n", " \"buttons\",\n", " \"x\":\n", " 0.1,\n", " \"y\":\n", " 0,\n", " }],\n", " sliders=sliders,\n", " )\n", "\n", " fig.update_layout(\n", " scene=scene,\n", " margin=dict(r=50, b=10, l=10, t=10),\n", " font=dict(family=font, size=18, color=\"black\"),\n", " )\n", "\n", " fig.show()\n", "\n", "\n", "def get_adjustable_plot_slices(antenna_inst, quantity=\"absolute\"):\n", " \"\"\"\n", " Generate adjustable 3D slices for a given antenna instance and quantity.\n", "\n", " Parameters\n", " ----------\n", " antenna_inst : AntennaInstance\n", " An instance of the antenna used for data retrieval.\n", " quantity : str, optional\n", " The quantity of interest (default is \"absolute\").\n", "\n", " Returns\n", " -------\n", " None\n", " This function generates adjustable 3D slices and does not return any values.\n", " \"\"\"\n", " volumetric_dataset_df = antenna_inst.get_volumetric_dataset(\n", " quantity=quantity, frequencies=np.arange(30, 80, 3))\n", "\n", " # create arrays\n", " volume_data = volumetric_dataset_df.to_numpy().reshape(\n", " volumetric_dataset_df.index.get_level_values(\"freq\").nunique(),\n", " -1,\n", " volumetric_dataset_df.shape[1],\n", " )\n", " x = volumetric_dataset_df.columns.values\n", " y = volumetric_dataset_df.index.get_level_values(\"phi\").unique().values\n", " z = volumetric_dataset_df.index.get_level_values(\"freq\").unique().values\n", "\n", " # create slices with sliders\n", " show_slices(x, y, z, volume_data, quantity, slice_type=\"x\")\n", " show_slices(x, y, z, volume_data, quantity, slice_type=\"y\")\n", " show_slices(x, y, z, volume_data, quantity, slice_type=\"z\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "get_adjustable_plot_slices(antenna_inst, quantity=\"absolute\")\n", "# get_adjustable_plot_slices(antenna_inst, quantity=\"EAHTheta_amp\")\n", "# get_adjustable_plot_slices(antenna_inst, quantity=\"EAHPhi_amp\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# convert to healpy format\n", "# additional shift if NS antenna\n", "update_antenna_conventions = {\n", " 'shift_phi': -90 + shift_phi,\n", " 'flip_theta': True,\n", " 'flip_phi': False,\n", " 'in_degrees': True,\n", " 'add_invisible_sky': True\n", "}\n", "\n", "antenna_hpmap_inst = antenna_inst.convert2hp(frequency=45,\n", " **update_antenna_conventions)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# hp maps are by default in galactic coordinates, so we need to specify the LST and latitude of the local observer\n", "lst = 18\n", "LATITUDE = -35.206667\n", "rotation_parameters = create_rotation_parameters(lst, LATITUDE)\n", "rotator = create_rotator(lst, LATITUDE, coord=[\"G\", \"C\"])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "antenna_hpmap = antenna_hpmap_inst.get_map(rotator=rotator)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# galaxy\n", "projview(\n", " antenna_hpmap,\n", " cmap='jet',\n", " return_only_data=False,\n", " graticule=True,\n", " graticule_labels=True,\n", " title='Galactic coordinates',\n", " xtick_label_color='w',\n", " # projection_type='cart'\n", ")\n", "\n", "# from galaxy to local\n", "projview(\n", " antenna_hpmap,\n", " cmap='jet',\n", " return_only_data=False,\n", " coord=['G', 'C'],\n", " rot=rotation_parameters,\n", " graticule=True,\n", " graticule_labels=True,\n", " title='Local coordinates',\n", " xtick_label_color='w',\n", " # projection_type='cart'\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.10.12" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false }, "vscode": { "interpreter": { "hash": "e7370f93d1d0cde622a1f8e1c04877d8463912d04d973331ad4851f04de6915a" } } }, "nbformat": 4, "nbformat_minor": 2 }