use-case_count_summer_days_cmip6.ipynb 18.9 KB
Newer Older
1
2
3
4
5
6
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
7
    "# Calculate a climate index in a server hosting all the climate model data \n",
Marco Kulüke's avatar
Marco Kulüke committed
8
    "\n",
9
    "We will show here how to count the annual summer days for a particular geolocation of your choice using the results of a climate model, in particular, we can chose one of the historical or one of the shared socioeconomic pathway (ssp) experiments of the Coupled Model Intercomparison Project [CMIP6](https://pcmdi.llnl.gov/CMIP6/).\n",
10
    "\n",
11
    "This Jupyter notebook is meant to run in the Jupyterhub server of the German Climate Computing Center [DKRZ](https://www.dkrz.de/) which is an [ESGF](https://esgf.llnl.gov/) repository that hosts 4 petabytes of CMIP6 data. Please, choose the Python 3 unstable kernel on the Kernel tab above, it contains all the common geoscience packages. See more information on how to run Jupyter notebooks at DKRZ [here](https://www.dkrz.de/up/systems/mistral/programming/jupyter-notebook). Find there how to run this Jupyter notebook in the DKRZ server out of the Jupyterhub, which will entail that you create the environment accounting for the required package dependencies. Running this Jupyter notebook in your premise, which is also known as \"client-side computing\", will also require that you install the necessary packages on you own but it will anyway fail because you will not have direct access to the data pool, which one of the main benefits of the \"server-side data-near computing\" we demonstrate in this use case. \n",
12
13
    "\n",
    "Thanks to the data and computer scientists Marco Kulüke, Fabian Wachsmann, Regina Kwee-Hinzmann, Caroline Arnold, Felix Stiehler, Maria Moreno, and Stephan Kindermann at DKRZ for their contribution to this notebook."
14
15
16
17
18
19
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
20
    "In this use case you will learn the following:\n",
21
22
    "- How to access a dataset from the DKRZ CMIP6 model data archive\n",
    "- How to count the annual number of summer days for a particular geolocation using this model dataset\n",
23
24
    "- How to visualize the results\n",
    "\n",
25
    "\n",
26
    "You will use:\n",
27
28
    "- [Intake](https://github.com/intake/intake) for finding the data in the catalog of the DKRZ archive\n",
    "- [Xarray](http://xarray.pydata.org/en/stable/) for loading and processing the data\n",
29
    "- [hvPlot](https://hvplot.holoviz.org/index.html) for visualizing the data in the Jupyter notebook and save the plots in your local computer"
30
31
   ]
  },
32
33
34
35
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
36
    "## 0. Load Packages"
37
38
   ]
  },
39
40
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
41
   "execution_count": null,
42
   "metadata": {},
Marco Kulüke's avatar
Marco Kulüke committed
43
   "outputs": [],
44
   "source": [
45
46
47
48
49
50
51
52
    "import numpy as np                    # fundamental package for scientific computing\n",
    "import pandas as pd                   # data analysis and manipulation tool\n",
    "import xarray as xr                   # handling labelled multi-dimensional arrays\n",
    "import intake                         # to find data in a catalog, this notebook explains how it works\n",
    "from ipywidgets import widgets        # to use widgets in the Jupyer Notebook\n",
    "from geopy.geocoders import Nominatim # Python client for several popular geocoding web services\n",
    "import folium                         # visualization tool for maps\n",
    "import hvplot.pandas                  # visualization tool for interactive plots"
53
54
55
56
57
58
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
59
    "## 1. Which dataset do we need? -> Choose Shared Socioeconomic Pathway, Place, and Year\n",
Marco Kulüke's avatar
Marco Kulüke committed
60
61
    "\n",
    "<a id='selection'></a>"
62
63
64
65
   ]
  },
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
66
   "execution_count": null,
67
   "metadata": {},
Marco Kulüke's avatar
Marco Kulüke committed
68
   "outputs": [],
Marco Kulüke's avatar
Marco Kulüke committed
69
   "source": [
70
    "# Produce the widget where we can select what experiment we are interested on \n",
Marco Kulüke's avatar
Marco Kulüke committed
71
    "\n",
72
73
74
    "experiments = {'historical':range(1850, 2015), 'ssp585':range(2015, 2101), 'ssp126':range(2015, 2101), \n",
    "               'ssp245':range(2015, 2101), 'ssp119':range(2015, 2101), 'ssp434':range(2015, 2101), \n",
    "               'ssp460':range(2015, 2101)}\n",
Marco Kulüke's avatar
Marco Kulüke committed
75
76
    "experiment_box = widgets.Dropdown(options=experiments, description=\"Select  experiment: \", disabled=False,)\n",
    "display(experiment_box)"
77
78
79
80
81
82
83
84
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
85
86
    "# Produce the widget where we can select what geolocation and year are interested on \n",
    "\n",
Marco Kulüke's avatar
Marco Kulüke committed
87
88
89
    "place_box = widgets.Text(description=\"Enter place:\")\n",
    "display(place_box)\n",
    "\n",
Marco Kulüke's avatar
Marco Kulüke committed
90
    "x = experiment_box.value\n",
Marco Kulüke's avatar
Marco Kulüke committed
91
92
    "year_box = widgets.Dropdown(options=x, description=\"Select year: \", disabled=False,)\n",
    "display(year_box)"
93
94
95
96
97
98
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
99
    "### 1.1 Find Coordinates of chosen Place\n",
100
    "If ambiguous, the most likely coordinates will be chosen, e.g. \"Hamburg\" results in \"Hamburg, 20095, Deutschland\", (53.55 North, 10.00 East)"
101
102
103
104
   ]
  },
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
105
   "execution_count": null,
106
   "metadata": {},
Marco Kulüke's avatar
Marco Kulüke committed
107
   "outputs": [],
108
   "source": [
109
110
    "# We use the module Nominatim gives us the geographical coordinates of the place we selected above\n",
    "\n",
111
    "geolocator = Nominatim(user_agent=\"any_agent\")\n",
Marco Kulüke's avatar
Marco Kulüke committed
112
    "location = geolocator.geocode(place_box.value)\n",
Marco Kulüke's avatar
Marco Kulüke committed
113
    "\n",
Marco Kulüke's avatar
Marco Kulüke committed
114
115
116
117
118
119
120
121
    "print(location.address)\n",
    "print((location.latitude, location.longitude))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
122
    "### 1.2 Show Place on a Map"
Marco Kulüke's avatar
Marco Kulüke committed
123
124
125
126
127
128
129
130
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
131
132
    "# We use the folium package to plot our selected geolocation in a map\n",
    "\n",
133
134
135
    "m = folium.Map(location=[location.latitude, location.longitude])\n",
    "tooltip = location.latitude, location.longitude\n",
    "folium.Marker([location.latitude, location.longitude], tooltip=tooltip).add_to(m)\n",
Marco Kulüke's avatar
Marco Kulüke committed
136
137
138
139
140
141
142
    "display(m)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
143
    "We have defined the place and time. Now, we can search for the climate model dataset."
144
145
146
147
148
149
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
150
    "## 2. Intake Catalog\n",
151
    "Similar to the shopping catalog at your favorite online bookstore, the intake catalog contains information (e.g. model, variables, and time range) about each dataset (the title, author, and number of pages of the book, for instance) that you can access before loading the data. It means that thanks to the catalog, you can find where is the book just by using some keywords and you do not need to hold it in your hand to know the number of pages, for instance.\n",
152
153
    "\n",
    "### 2.1 Load the Intake Catalog\n",
154
    "We load the catalog descriptor with the intake package. The catalog is updated daily. The catalog descriptor is created by the DKRZ developers that manage the catalog, you do not need to care so much about it, knowing where it is and loading it is enough:"
155
156
   ]
  },
157
158
159
160
161
162
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
163
    "# Path to catalog descriptor on the DKRZ server\n",
164
165
    "col_url = \"/work/ik1017/Catalogs/mistral-cmip6.json\"\n",
    "\n",
166
    "# Open the catalog with the intake package and name it \"col\" as short for \"collection\"\n",
167
    "col = intake.open_esm_datastore(col_url)"
168
169
170
171
172
173
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
174
    "Let's see what is inside the intake catalog. The underlying data base is given as a pandas dataframe which we can access with \"col.df\". Then, \"col.df.head()\" shows us the first rows of the table of the catalog."
175
176
177
178
179
180
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
181
    "This catalog contains all datasets of the CMIP6 archive at DKRZ. In the next step we narrow the results down by chosing a model and variable."
Marco Kulüke's avatar
Marco Kulüke committed
182
183
184
185
186
187
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
188
    "### 2.2 Browse the Intake Catalog\n",
189
    "In this example we chose the Max-Planck Earth System Model in High Resolution Mode (\"MPI-ESM1-2-HR\") and the maximum temperature near surface (\"tasmax\") as variable. We also choose an experiment. CMIP6 comprises several kind of experiments. Each experiment has various simulation members. you can find more information in the [CMIP6 Model and Experiment Documentation](https://pcmdi.llnl.gov/CMIP6/Guide/dataUsers.html#5-model-and-experiment-documentation)."
190
191
192
193
   ]
  },
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
194
   "execution_count": null,
195
196
197
   "metadata": {},
   "outputs": [],
   "source": [
198
199
    "# Store the name of the model we chose in a variable named \"climate_model\"\n",
    "\n",
200
    "climate_model = \"MPI-ESM1-2-LR\" # here we choose Max-Plack Institute's Earth Sytem Model in high resolution\n",
201
    "\n",
202
203
    "# This is how we tell intake what data we want\n",
    "\n",
204
    "query = dict(\n",
205
206
207
208
209
    "    source_id      = climate_model, # the model \n",
    "    variable_id    = \"tasmax\", # temperature at surface, maximum\n",
    "    table_id       = \"day\", # daily maximum\n",
    "    experiment_id  = experiment_box.label, # what we selected in the drop down menu,e.g. SSP2.4-5 2015-2100\n",
    "    member_id      = \"r10i1p1f1\", # \"r\" realization, \"i\" initialization, \"p\" physics, \"f\" forcing\n",
210
    ")\n",
211
    "\n",
212
    "# Intake looks for the query we just defined in the catalog of the CMIP6 data pool at DKRZ\n",
213
214
    "cat = col.search(**query)\n",
    "\n",
215
    "# Show query results\n",
216
    "cat.df"
217
218
   ]
  },
Marco Kulüke's avatar
Marco Kulüke committed
219
220
221
222
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
223
224
225
226
    "The result of the query are like the list of results you get when you search for articles in the internet by writing  keywords in your search engine (Duck duck go, Ecosia, Google,...). Thanks to the intake package, we did not need to know the path of each dataset, just selecting some keywords (the model name, the variable,...) was enough to obtain the results. If advance users are still interested in the location of the data inside the DKRZ archive, intake also provides the path and the OpenDAP URL (see the last columns above). \n",
    "\n",
    "\n",
    "Now we will find which file in the dataset contains our selected year so in the next section we can just load that specific file and not the whole dataset."
Marco Kulüke's avatar
Marco Kulüke committed
227
228
229
230
231
232
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
233
    "### 2.3 Find the Dataset That Contains the Year You Selected in Drop Down Menu Above"
Marco Kulüke's avatar
Marco Kulüke committed
234
235
   ]
  },
236
237
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
238
   "execution_count": null,
239
240
241
   "metadata": {},
   "outputs": [],
   "source": [
242
    "# Create a copy of cat.df, thus further modifications do not affect it \n",
243
    "query_result_df = cat.df.copy() # new dataframe to play with\n",
Marco Kulüke's avatar
Marco Kulüke committed
244
    "\n",
245
    "# Each dataset contains many files, extract the initial and final year of each file \n",
Marco Kulüke's avatar
Marco Kulüke committed
246
247
248
    "query_result_df[\"start_year\"] = query_result_df[\"time_range\"].str[0:4].astype(int) # add column with start year\n",
    "query_result_df[\"end_year\"] = query_result_df[\"time_range\"].str[9:13].astype(int) # add column with end year\n",
    "\n",
249
250
251
    "# Delete the time range column\n",
    "query_result_df.drop(columns=[\"time_range\"], inplace = True) # \"inplace = False\" will drop the column in the view but not in the actual dataframe\n",
    "query_result_df.iloc[0:3]"
252
253
254
255
256
257
258
259
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
260
261
262
    "# Select the file that contains the year we selected in the drop down menu above, e.g. 2015\n",
    "selected_file = query_result_df_m[(year_box.value >= query_result_df[\"start_year\"]) & (\n",
    "                   year_box.value <= query_result_df[\"end_year\"])]\n",
263
    "\n",
264
265
    "# Path of the file that contains the selected year    \n",
    "selected_path = selected_file[\"path\"].values[0] \n",
266
    "\n",
267
    "# Show the path of the file that contains the selected year\n",
Marco Kulüke's avatar
Marco Kulüke committed
268
269
270
271
272
273
274
    "selected_path"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
275
    "## 3. Load the model data"
Marco Kulüke's avatar
Marco Kulüke committed
276
277
278
279
280
281
282
283
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
284
    "# Load Data with the open_dataset() xarray method\n",
Marco Kulüke's avatar
Marco Kulüke committed
285
    "ds_tasmax = xr.open_dataset(selected_path)\n",
Marco Kulüke's avatar
Marco Kulüke committed
286
287
    "\n",
    "# Open variable \"tasmax\" over the whole time range\n",
Marco Kulüke's avatar
Marco Kulüke committed
288
289
    "tasmax_xr = ds_tasmax[\"tasmax\"]\n",
    "\n",
Marco Kulüke's avatar
Marco Kulüke committed
290
291
292
    "# Define start and end time string\n",
    "time_start = str(year_box.value) + \"-01-01T12:00:00.000000000\"\n",
    "time_end = str(year_box.value) + \"-12-31T12:00:00.000000000\"\n",
293
    "\n",
Marco Kulüke's avatar
Marco Kulüke committed
294
295
    "# Slice selected year\n",
    "tasmax_year_xr = tasmax_xr.loc[time_start:time_end, :, :]"
296
297
   ]
  },
298
299
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
300
   "execution_count": null,
301
   "metadata": {},
Marco Kulüke's avatar
Marco Kulüke committed
302
   "outputs": [],
303
   "source": [
Marco Kulüke's avatar
Marco Kulüke committed
304
    "# Let's have a look at the xarray data array\n",
305
    "\n",
306
    "tasmax_year_xr"
307
308
   ]
  },
Marco Kulüke's avatar
Marco Kulüke committed
309
310
311
312
313
314
315
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see not only the numbers, but also information about it, such as long name, units, and the data history. This information is called metadata."
   ]
  },
316
  {
317
   "cell_type": "markdown",
318
   "metadata": {},
319
   "source": [
320
    "## 4. Compare Model Grid Cell with chosen Location"
321
   ]
322
323
324
  },
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
325
   "execution_count": null,
326
   "metadata": {},
Marco Kulüke's avatar
Marco Kulüke committed
327
   "outputs": [],
328
   "source": [
329
    "# Find nearest model coordinate by finding the index of the nearest grid point\n",
Marco Kulüke's avatar
Marco Kulüke committed
330
331
332
333
334
    "\n",
    "abslat = np.abs(tasmax_year_xr[\"lat\"] - location.latitude)\n",
    "abslon = np.abs(tasmax_year_xr[\"lon\"] - location.longitude)\n",
    "c = np.maximum(abslon, abslat)\n",
    "\n",
335
    "([xloc], [yloc]) = np.where(c == np.min(c)) # xloc and yloc are the indices of the neares model grid point"
336
337
338
339
340
341
342
343
344
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Draw map again\n",
345
    "\n",
346
    "m = folium.Map(location=[location.latitude, location.longitude], zoom_start=8)\n",
347
348
349
    "\n",
    "\n",
    "tooltip = location.latitude, location.longitude\n",
350
351
352
    "folium.Marker(\n",
    "    [location.latitude, location.longitude],\n",
    "    tooltip=tooltip,\n",
Marco Kulüke's avatar
Marco Kulüke committed
353
    "    popup=\"Location selected by You\",\n",
354
    ").add_to(m)\n",
355
    "\n",
356
357
358
359
360
361
362
    "#\n",
    "tooltip = float(tasmax_year_xr[\"lat\"][yloc].values), float(tasmax_year_xr[\"lon\"][xloc].values)\n",
    "folium.Marker(\n",
    "    [tasmax_year_xr[\"lat\"][yloc], tasmax_year_xr[\"lon\"][xloc]],\n",
    "    tooltip=tooltip,\n",
    "    popup=\"Model Grid Cell Center\",\n",
    ").add_to(m)\n",
363
    "\n",
364
    "# Define coordinates of model grid cell (just for visualization)\n",
365
366
367
368
    "rect_lat1_model = (tasmax_year_xr[\"lat\"][yloc - 1] + tasmax_year_xr[\"lat\"][yloc]) / 2\n",
    "rect_lon1_model = (tasmax_year_xr[\"lon\"][xloc - 1] + tasmax_year_xr[\"lon\"][xloc]) / 2\n",
    "rect_lat2_model = (tasmax_year_xr[\"lat\"][yloc + 1] + tasmax_year_xr[\"lat\"][yloc]) / 2\n",
    "rect_lon2_model = (tasmax_year_xr[\"lon\"][xloc + 1] + tasmax_year_xr[\"lon\"][xloc]) / 2\n",
369
    "\n",
370
    "# Draw model grid cell\n",
371
    "folium.Rectangle(\n",
372
    "    bounds=[[rect_lat1_model, rect_lon1_model], [rect_lat2_model, rect_lon2_model]],\n",
373
374
375
376
377
    "    color=\"#ff7800\",\n",
    "    fill=True,\n",
    "    fill_color=\"#ffff00\",\n",
    "    fill_opacity=0.2,\n",
    ").add_to(m)\n",
378
    "\n",
379
    "m"
380
381
   ]
  },
382
383
384
385
386
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Climate models have a finite resolution. Hence, models do not provide the data of a particular point, but the mean over a model grid cell. Take this in mind when comparing model data with observed data (e.g. weather stations).\n",
387
388
    "\n",
    "\n",
389
390
391
    "Now, we will visualize the daily maximum temperature time series of the model grid cell."
   ]
  },
Marco Kulüke's avatar
Marco Kulüke committed
392
393
394
395
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
396
    "## 5. Draw Temperature Time Series and Count Summer days"
397
398
   ]
  },
399
400
401
402
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
403
    "The definition of a summer day varies from region to region. According to the [German Weather Service](https://www.dwd.de/EN/ourservices/germanclimateatlas/explanations/elements/_functions/faqkarussel/sommertage.html), \"a summer day is a day on which the maximum air temperature is at least 25.0°C\". Depending on the place you selected, you might want to apply a different threshold to calculate the summer days index. "
404
405
   ]
  },
Marco Kulüke's avatar
Marco Kulüke committed
406
407
408
409
410
411
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
412
    "tasmax_year_place_xr = tasmax_year_xr[:, yloc, xloc] - 273.15 # Convert Kelvin to °C\n",
413
414
    "tasmax_year_place_df = pd.DataFrame(index = tasmax_year_place_xr['time'].values, \n",
    "                                    columns = ['Temperature', 'Summer Day Threshold']) # create the dataframe\n",
415
    "\n",
416
417
    "tasmax_year_place_df.loc[:, 'Model Temperature'] = tasmax_year_place_xr.values # insert model data into the dataframe\n",
    "tasmax_year_place_df.loc[:, 'Summer Day Threshold'] = 25 # insert the threshold into the dataframe\n",
Marco Kulüke's avatar
Marco Kulüke committed
418
    "\n",
419
    "# Plot data and define title and legend\n",
420
    "tasmax_year_place_df.hvplot.line(y=['Model Temperature', 'Summer Day Threshold'], \n",
421
422
423
    "                                 value_label='Temperature in °C', legend='bottom', \n",
    "                                 title='Daily maximum Temperature near Surface for '+place_box.value, \n",
    "                                 height=500, width=620)"
424
425
426
427
428
429
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
430
    "As we can see, the maximum daily temperature is highly variable over the year. As we are using the mean temperature in a model grid cell, the amount of summer days might we different that what you would expect at a single location."
Marco Kulüke's avatar
Marco Kulüke committed
431
432
   ]
  },
433
434
  {
   "cell_type": "code",
Marco Kulüke's avatar
Marco Kulüke committed
435
   "execution_count": null,
436
   "metadata": {},
Marco Kulüke's avatar
Marco Kulüke committed
437
   "outputs": [],
438
   "source": [
439
    "# Summer days index calculation\n",
440
441
442
    "no_summer_days_model = tasmax_year_place_xr[tasmax_year_place_xr > 25].size # count the number of summer days\n",
    "\n",
    "# Print results in a sentence\n",
443
444
445
    "print(\"According to the German Weather Service definition, in the \" +experiment_box.label +\" experiment the \" \n",
    "      +climate_model +\" model shows \" +str(no_summer_days_model) +\" summer days for \" +str(place_box.value) \n",
    "      + \" in \" + str(year_box.value) +\".\")"
446
447
448
   ]
  },
  {
Marco Kulüke's avatar
Marco Kulüke committed
449
   "cell_type": "markdown",
450
451
   "metadata": {},
   "source": [
Marco Kulüke's avatar
Marco Kulüke committed
452
    "[Try another location and year](#selection)"
453
454
455
456
457
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
458
   "display_name": "Python 3 unstable (using the module python3/unstable)",
459
   "language": "python",
460
   "name": "python3_unstable"
461
462
463
464
465
466
467
468
469
470
471
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
472
   "version": "3.7.8"
473
474
475
476
477
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}