{ "cells": [ { "cell_type": "markdown", "id": "23831a0e-7b09-4548-9179-dff7f85111b6", "metadata": {}, "source": [ "
\n", "
\n", "
\n", "\n", "
\n", "

\n", " earthaccess and the cloud, the force awakens\n", "

\n", "

\n", " Luis A. López
\n", " NSIDC, NASA Openscapes\n", "

\n", "

\n", " NASA ESDS Tech Spotlight, Feb 2024\n", "

\n", "
\n", "
\n", "\n", "**Art**: **Allison Horts**" ] }, { "cell_type": "markdown", "id": "a0fe0d34-c41d-4de3-babc-a018900fa0d6", "metadata": {}, "source": [ "Notes:\n", "\n", "> * Disclaimer: Opinions expressed here are solely my own and do not express the views or opinions of my employer.\n", ">\n", "> * This notebook runs out of a local experimental environment that has not been pushed to the main earthaccess repository and is not expected to run as is until these changes are there.\n", ">\n", "> * This notebook focuses on the Pangeo ecosystem, as Python is the dominant language is safe to assume that we have plenty of researchers using this technologies.\n", "> \n", "> * This will be a pop-culture driven presentation, expect memes and millennial sense of humor." ] }, { "cell_type": "markdown", "id": "9cda10db-bbc3-409b-bc7c-5fda9ed1571b", "metadata": {}, "source": [ "## 1. In the cloud era, what can earthaccess do for us?\n", "\n", "---\n", "
\n", "\n", "* ### Access remote files, automatically handling authentication and serialization.\n", " * #### NASA Openscapes [cookbook examples](https://nasa-openscapes.github.io/earthdata-cloud-cookbook/tutorials/Observing_Seasonal_Ag_Changes.html)\n", "* ### Generate an on-the-fly Zarr compatible cache with Kerchunk!\n", " * #### Presented at the [Dask demo day for August/2023](https://youtu.be/MepdrbDjYPc?t=984)\n", "* ### Smart Access ◀\n", " * ### Sneak peak today, *more details at SciPy 2024!* \n", "* ### Scale out workflows with Dask ◀\n", " * ### [Processing Terabyte-Scale NASA Cloud Datasets with Coiled](https://blog.coiled.io/blog/processing-terabyte-scale-nasa-cloud-datasets-with-coiled.html)\n", "\n", "--- \n", "
\n", "\n", "
\n", "\n", "
\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "1350657b-7c0f-423b-8805-59e063d99a5c", "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "(function(root) {\n", " function now() {\n", " return new Date();\n", " }\n", "\n", " var force = true;\n", " var py_version = '3.3.4'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", " var reloading = false;\n", " var Bokeh = root.Bokeh;\n", "\n", " if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n", " root._bokeh_timeout = Date.now() + 5000;\n", " root._bokeh_failed_load = false;\n", " }\n", "\n", " function run_callbacks() {\n", " try {\n", " root._bokeh_onload_callbacks.forEach(function(callback) {\n", " if (callback != null)\n", " callback();\n", " });\n", " } finally {\n", " delete root._bokeh_onload_callbacks;\n", " }\n", " console.debug(\"Bokeh: all callbacks have finished\");\n", " }\n", "\n", " function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n", " if (css_urls == null) css_urls = [];\n", " if (js_urls == null) js_urls = [];\n", " if (js_modules == null) js_modules = [];\n", " if (js_exports == null) js_exports = {};\n", "\n", " root._bokeh_onload_callbacks.push(callback);\n", "\n", " if (root._bokeh_is_loading > 0) {\n", " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", " return null;\n", " }\n", " if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", " run_callbacks();\n", " return null;\n", " }\n", " if (!reloading) {\n", " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", " }\n", "\n", " function on_load() {\n", " root._bokeh_is_loading--;\n", " if (root._bokeh_is_loading === 0) {\n", " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", " run_callbacks()\n", " }\n", " }\n", " window._bokeh_on_load = on_load\n", "\n", " function on_error() {\n", " console.error(\"failed to load \" + url);\n", " }\n", "\n", " var skip = [];\n", " if (window.requirejs) {\n", " window.requirejs.config({'packages': {}, 'paths': {'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n", " require([\"jspanel\"], function(jsPanel) {\n", "\twindow.jsPanel = jsPanel\n", "\ton_load()\n", " })\n", " require([\"jspanel-modal\"], function() {\n", "\ton_load()\n", " })\n", " require([\"jspanel-tooltip\"], function() {\n", "\ton_load()\n", " })\n", " require([\"jspanel-hint\"], function() {\n", "\ton_load()\n", " })\n", " require([\"jspanel-layout\"], function() {\n", "\ton_load()\n", " })\n", " require([\"jspanel-contextmenu\"], function() {\n", "\ton_load()\n", " })\n", " require([\"jspanel-dock\"], function() {\n", "\ton_load()\n", " })\n", " require([\"gridstack\"], function(GridStack) {\n", "\twindow.GridStack = GridStack\n", "\ton_load()\n", " })\n", " require([\"notyf\"], function() {\n", "\ton_load()\n", " })\n", " root._bokeh_is_loading = css_urls.length + 9;\n", " } else {\n", " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", " }\n", "\n", " var existing_stylesheets = []\n", " var links = document.getElementsByTagName('link')\n", " for (var i = 0; i < links.length; i++) {\n", " var link = links[i]\n", " if (link.href != null) {\n", "\texisting_stylesheets.push(link.href)\n", " }\n", " }\n", " for (var i = 0; i < css_urls.length; i++) {\n", " var url = css_urls[i];\n", " if (existing_stylesheets.indexOf(url) !== -1) {\n", "\ton_load()\n", "\tcontinue;\n", " }\n", " const element = document.createElement(\"link\");\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.rel = \"stylesheet\";\n", " element.type = \"text/css\";\n", " element.href = url;\n", " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", " document.body.appendChild(element);\n", " } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n", " var urls = ['https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n", " for (var i = 0; i < urls.length; i++) {\n", " skip.push(urls[i])\n", " }\n", " } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n", " var urls = ['https://cdn.holoviz.org/panel/1.3.8/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n", " for (var i = 0; i < urls.length; i++) {\n", " skip.push(urls[i])\n", " }\n", " } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n", " var urls = ['https://cdn.holoviz.org/panel/1.3.8/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n", " for (var i = 0; i < urls.length; i++) {\n", " skip.push(urls[i])\n", " }\n", " } var existing_scripts = []\n", " var scripts = document.getElementsByTagName('script')\n", " for (var i = 0; i < scripts.length; i++) {\n", " var script = scripts[i]\n", " if (script.src != null) {\n", "\texisting_scripts.push(script.src)\n", " }\n", " }\n", " for (var i = 0; i < js_urls.length; i++) {\n", " var url = js_urls[i];\n", " if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n", "\tif (!window.requirejs) {\n", "\t on_load();\n", "\t}\n", "\tcontinue;\n", " }\n", " var element = document.createElement('script');\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.async = false;\n", " element.src = url;\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " document.head.appendChild(element);\n", " }\n", " for (var i = 0; i < js_modules.length; i++) {\n", " var url = js_modules[i];\n", " if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n", "\tif (!window.requirejs) {\n", "\t on_load();\n", "\t}\n", "\tcontinue;\n", " }\n", " var element = document.createElement('script');\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.async = false;\n", " element.src = url;\n", " element.type = \"module\";\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " document.head.appendChild(element);\n", " }\n", " for (const name in js_exports) {\n", " var url = js_exports[name];\n", " if (skip.indexOf(url) >= 0 || root[name] != null) {\n", "\tif (!window.requirejs) {\n", "\t on_load();\n", "\t}\n", "\tcontinue;\n", " }\n", " var element = document.createElement('script');\n", " element.onerror = on_error;\n", " element.async = false;\n", " element.type = \"module\";\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " element.textContent = `\n", " import ${name} from \"${url}\"\n", " window.${name} = ${name}\n", " window._bokeh_on_load()\n", " `\n", " document.head.appendChild(element);\n", " }\n", " if (!js_urls.length && !js_modules.length) {\n", " on_load()\n", " }\n", " };\n", "\n", " function inject_raw_css(css) {\n", " const element = document.createElement(\"style\");\n", " element.appendChild(document.createTextNode(css));\n", " document.body.appendChild(element);\n", " }\n", "\n", " var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.3.4.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.4.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.4.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.4.min.js\", \"https://cdn.holoviz.org/panel/1.3.8/dist/panel.min.js\"];\n", " var js_modules = [];\n", " var js_exports = {};\n", " var css_urls = [];\n", " var inline_js = [ function(Bokeh) {\n", " Bokeh.set_log_level(\"info\");\n", " },\n", "function(Bokeh) {} // ensure no trailing comma for IE\n", " ];\n", "\n", " function run_inline_js() {\n", " if ((root.Bokeh !== undefined) || (force === true)) {\n", " for (var i = 0; i < inline_js.length; i++) {\n", "\ttry {\n", " inline_js[i].call(root, root.Bokeh);\n", "\t} catch(e) {\n", "\t if (!reloading) {\n", "\t throw e;\n", "\t }\n", "\t}\n", " }\n", " // Cache old bokeh versions\n", " if (Bokeh != undefined && !reloading) {\n", "\tvar NewBokeh = root.Bokeh;\n", "\tif (Bokeh.versions === undefined) {\n", "\t Bokeh.versions = new Map();\n", "\t}\n", "\tif (NewBokeh.version !== Bokeh.version) {\n", "\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n", "\t}\n", "\troot.Bokeh = Bokeh;\n", " }} else if (Date.now() < root._bokeh_timeout) {\n", " setTimeout(run_inline_js, 100);\n", " } else if (!root._bokeh_failed_load) {\n", " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", " root._bokeh_failed_load = true;\n", " }\n", " root._bokeh_is_initializing = false\n", " }\n", "\n", " function load_or_wait() {\n", " // Implement a backoff loop that tries to ensure we do not load multiple\n", " // versions of Bokeh and its dependencies at the same time.\n", " // In recent versions we use the root._bokeh_is_initializing flag\n", " // to determine whether there is an ongoing attempt to initialize\n", " // bokeh, however for backward compatibility we also try to ensure\n", " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", " // before older versions are fully initialized.\n", " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", " root._bokeh_is_initializing = false;\n", " root._bokeh_onload_callbacks = undefined;\n", " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", " load_or_wait();\n", " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", " setTimeout(load_or_wait, 100);\n", " } else {\n", " root._bokeh_is_initializing = true\n", " root._bokeh_onload_callbacks = []\n", " var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n", " if (!reloading && !bokeh_loaded) {\n", "\troot.Bokeh = undefined;\n", " }\n", " load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n", "\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", "\trun_inline_js();\n", " });\n", " }\n", " }\n", " // Give older versions of the autoload script a head-start to ensure\n", " // they initialize before we start loading newer version.\n", " setTimeout(load_or_wait, 100)\n", "}(window));" ], "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.3.4'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = false;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n require([\"jspanel\"], function(jsPanel) {\n\twindow.jsPanel = jsPanel\n\ton_load()\n })\n require([\"jspanel-modal\"], function() {\n\ton_load()\n })\n require([\"jspanel-tooltip\"], function() {\n\ton_load()\n })\n require([\"jspanel-hint\"], function() {\n\ton_load()\n })\n require([\"jspanel-layout\"], function() {\n\ton_load()\n })\n require([\"jspanel-contextmenu\"], function() {\n\ton_load()\n })\n require([\"jspanel-dock\"], function() {\n\ton_load()\n })\n require([\"gridstack\"], function(GridStack) {\n\twindow.GridStack = GridStack\n\ton_load()\n })\n require([\"notyf\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 9;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.8/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.3.8/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.3.8/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.3.4.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.4.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.4.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.4.min.js\", \"https://cdn.holoviz.org/panel/1.3.8/dist/panel.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "\n", "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", "}\n", "\n", "\n", " function JupyterCommManager() {\n", " }\n", "\n", " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", " comm_manager.register_target(comm_id, function(comm) {\n", " comm.on_msg(msg_handler);\n", " });\n", " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", " comm.onMsg = msg_handler;\n", " });\n", " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", " var messages = comm.messages[Symbol.asyncIterator]();\n", " function processIteratorResult(result) {\n", " var message = result.value;\n", " console.log(message)\n", " var content = {data: message.data, comm_id};\n", " var buffers = []\n", " for (var buffer of message.buffers || []) {\n", " buffers.push(new DataView(buffer))\n", " }\n", " var metadata = message.metadata || {};\n", " var msg = {content, buffers, metadata}\n", " msg_handler(msg);\n", " return messages.next().then(processIteratorResult);\n", " }\n", " return messages.next().then(processIteratorResult);\n", " })\n", " }\n", " }\n", "\n", " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", " if (comm_id in window.PyViz.comms) {\n", " return window.PyViz.comms[comm_id];\n", " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", " if (msg_handler) {\n", " comm.on_msg(msg_handler);\n", " }\n", " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", " comm.open();\n", " if (msg_handler) {\n", " comm.onMsg = msg_handler;\n", " }\n", " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", " comm_promise.then((comm) => {\n", " window.PyViz.comms[comm_id] = comm;\n", " if (msg_handler) {\n", " var messages = comm.messages[Symbol.asyncIterator]();\n", " function processIteratorResult(result) {\n", " var message = result.value;\n", " var content = {data: message.data};\n", " var metadata = message.metadata || {comm_id};\n", " var msg = {content, metadata}\n", " msg_handler(msg);\n", " return messages.next().then(processIteratorResult);\n", " }\n", " return messages.next().then(processIteratorResult);\n", " }\n", " }) \n", " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", " return comm_promise.then((comm) => {\n", " comm.send(data, metadata, buffers, disposeOnDone);\n", " });\n", " };\n", " var comm = {\n", " send: sendClosure\n", " };\n", " }\n", " window.PyViz.comms[comm_id] = comm;\n", " return comm;\n", " }\n", " window.PyViz.comm_manager = new JupyterCommManager();\n", " \n", "\n", "\n", "var JS_MIME_TYPE = 'application/javascript';\n", "var HTML_MIME_TYPE = 'text/html';\n", "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", "var CLASS_NAME = 'output';\n", "\n", "/**\n", " * Render data to the DOM node\n", " */\n", "function render(props, node) {\n", " var div = document.createElement(\"div\");\n", " var script = document.createElement(\"script\");\n", " node.appendChild(div);\n", " node.appendChild(script);\n", "}\n", "\n", "/**\n", " * Handle when a new output is added\n", " */\n", "function handle_add_output(event, handle) {\n", " var output_area = handle.output_area;\n", " var output = handle.output;\n", " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", " return\n", " }\n", " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", " if (id !== undefined) {\n", " var nchildren = toinsert.length;\n", " var html_node = toinsert[nchildren-1].children[0];\n", " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", " var scripts = [];\n", " var nodelist = html_node.querySelectorAll(\"script\");\n", " for (var i in nodelist) {\n", " if (nodelist.hasOwnProperty(i)) {\n", " scripts.push(nodelist[i])\n", " }\n", " }\n", "\n", " scripts.forEach( function (oldScript) {\n", " var newScript = document.createElement(\"script\");\n", " var attrs = [];\n", " var nodemap = oldScript.attributes;\n", " for (var j in nodemap) {\n", " if (nodemap.hasOwnProperty(j)) {\n", " attrs.push(nodemap[j])\n", " }\n", " }\n", " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", " oldScript.parentNode.replaceChild(newScript, oldScript);\n", " });\n", " if (JS_MIME_TYPE in output.data) {\n", " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", " }\n", " output_area._hv_plot_id = id;\n", " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", " window.PyViz.plot_index[id] = Bokeh.index[id];\n", " } else {\n", " window.PyViz.plot_index[id] = null;\n", " }\n", " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", " var bk_div = document.createElement(\"div\");\n", " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", " var script_attrs = bk_div.children[0].attributes;\n", " for (var i = 0; i < script_attrs.length; i++) {\n", " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", " }\n", " // store reference to server id on output_area\n", " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", " }\n", "}\n", "\n", "/**\n", " * Handle when an output is cleared or removed\n", " */\n", "function handle_clear_output(event, handle) {\n", " var id = handle.cell.output_area._hv_plot_id;\n", " var server_id = handle.cell.output_area._bokeh_server_id;\n", " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", " if (server_id !== null) {\n", " comm.send({event_type: 'server_delete', 'id': server_id});\n", " return;\n", " } else if (comm !== null) {\n", " comm.send({event_type: 'delete', 'id': id});\n", " }\n", " delete PyViz.plot_index[id];\n", " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", " var doc = window.Bokeh.index[id].model.document\n", " doc.clear();\n", " const i = window.Bokeh.documents.indexOf(doc);\n", " if (i > -1) {\n", " window.Bokeh.documents.splice(i, 1);\n", " }\n", " }\n", "}\n", "\n", "/**\n", " * Handle kernel restart event\n", " */\n", "function handle_kernel_cleanup(event, handle) {\n", " delete PyViz.comms[\"hv-extension-comm\"];\n", " window.PyViz.plot_index = {}\n", "}\n", "\n", "/**\n", " * Handle update_display_data messages\n", " */\n", "function handle_update_output(event, handle) {\n", " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", " handle_add_output(event, handle)\n", "}\n", "\n", "function register_renderer(events, OutputArea) {\n", " function append_mime(data, metadata, element) {\n", " // create a DOM node to render to\n", " var toinsert = this.create_output_subarea(\n", " metadata,\n", " CLASS_NAME,\n", " EXEC_MIME_TYPE\n", " );\n", " this.keyboard_manager.register_events(toinsert);\n", " // Render to node\n", " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", " render(props, toinsert[0]);\n", " element.append(toinsert);\n", " return toinsert\n", " }\n", "\n", " events.on('output_added.OutputArea', handle_add_output);\n", " events.on('output_updated.OutputArea', handle_update_output);\n", " events.on('clear_output.CodeCell', handle_clear_output);\n", " events.on('delete.Cell', handle_clear_output);\n", " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", "\n", " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", " safe: true,\n", " index: 0\n", " });\n", "}\n", "\n", "if (window.Jupyter !== undefined) {\n", " try {\n", " var events = require('base/js/events');\n", " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", " register_renderer(events, OutputArea);\n", " }\n", " } catch(err) {\n", " }\n", "}\n" ], "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ "
\n", "
\n", "
\n", "" ] }, "metadata": { "application/vnd.holoviews_exec.v0+json": { "id": "p1002" } }, "output_type": "display_data" } ], "source": [ "import earthaccess\n", "\n", "import hvplot.xarray\n", "import xarray as xr\n", "import time\n", "\n", "auth = earthaccess.login()" ] }, { "cell_type": "code", "execution_count": 94, "id": "716dae68-36e6-47d9-bbe5-33b1c6612bb2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Granules found: 20\n" ] } ], "source": [ "datasets = [\"ATL03\"]\n", "\n", "results = earthaccess.search_data(\n", " short_name=datasets,\n", " cloud_hosted=True,\n", " temporal=(\"2022-01\", \"2023-10\"),\n", " bounding_box=(34.44,31.52,34.48,31.54),\n", " count=10\n", ")" ] }, { "cell_type": "code", "execution_count": 95, "id": "4dee6684-8c5f-4c39-b251-724734a20f24", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " \n", "
\n", "
\n", "
\n", "
\n", "
\n", "

Data: ATL03_20220808183613_07311602_006_01.h5

\n", "

Size: 663.88 MB

\n", "

Cloud Hosted: True

\n", "
\n", "
\n", " \"Data\"Data\n", "
\n", "
\n", "
\n", "
\n", " " ], "text/plain": [ "Collection: {'EntryTitle': 'ATLAS/ICESat-2 L2A Global Geolocated Photon Data V006'}\n", "Spatial coverage: {'HorizontalSpatialDomain': {'Geometry': {'GPolygons': [{'Boundary': {'Points': [{'Longitude': 30.52795, 'Latitude': 59.5424}, {'Longitude': 30.30746, 'Latitude': 59.5308}, {'Longitude': 30.56149, 'Latitude': 58.25762}, {'Longitude': 31.13017, 'Latitude': 55.16358}, {'Longitude': 31.55882, 'Latitude': 52.61709}, {'Longitude': 31.94924, 'Latitude': 50.13897}, {'Longitude': 32.58842, 'Latitude': 45.72077}, {'Longitude': 33.14276, 'Latitude': 41.56439}, {'Longitude': 33.63922, 'Latitude': 37.5987}, {'Longitude': 34.04775, 'Latitude': 34.18401}, {'Longitude': 34.78772, 'Latitude': 27.64814}, {'Longitude': 34.86486, 'Latitude': 26.94847}, {'Longitude': 34.99077, 'Latitude': 26.95949}, {'Longitude': 34.91437, 'Latitude': 27.65923}, {'Longitude': 34.18329, 'Latitude': 34.19532}, {'Longitude': 33.78073, 'Latitude': 37.60982}, {'Longitude': 33.29258, 'Latitude': 41.57551}, {'Longitude': 32.74893, 'Latitude': 45.73193}, {'Longitude': 32.12403, 'Latitude': 50.15024}, {'Longitude': 31.74331, 'Latitude': 52.62832}, {'Longitude': 31.32622, 'Latitude': 55.17493}, {'Longitude': 30.77427, 'Latitude': 58.26925}, {'Longitude': 30.52795, 'Latitude': 59.5424}]}}]}}}\n", "Temporal coverage: {'RangeDateTime': {'BeginningDateTime': '2022-08-08T18:36:11.782Z', 'EndingDateTime': '2022-08-08T18:44:42.495Z'}}\n", "Size(MB): 663.8765316009521\n", "Data: ['https://data.nsidc.earthdatacloud.nasa.gov/nsidc-cumulus-prod-protected/ATLAS/ATL03/006/2022/08/08/ATL03_20220808183613_07311602_006_01.h5']" ] }, "execution_count": 95, "metadata": {}, "output_type": "execute_result" } ], "source": [ "results[3]" ] }, { "cell_type": "code", "execution_count": 96, "id": "eb60263d-16e3-4b77-9f6f-afdc16ce478e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('349.38 MB', 'ATL03_20220208031618_07311402_006_01.h5'),\n", " ('663.88 MB', 'ATL03_20220808183613_07311602_006_01.h5'),\n", " ('665.11 MB', 'ATL03_20221107141555_07311702_006_01.h5'),\n", " ('779.02 MB', 'ATL03_20220509225606_07311502_006_01.h5'),\n", " ('800.61 MB', 'ATL03_20230807011436_07312002_006_01.h5'),\n", " ('1115.44 MB', 'ATL03_20220926042045_00831706_006_01.h5'),\n", " ('1584.02 MB', 'ATL03_20230625151938_00832006_006_01.h5'),\n", " ('2042.96 MB', 'ATL03_20220828054448_10281606_006_01.h5'),\n", " ('2634.99 MB', 'ATL03_20230508053526_07311902_006_01.h5'),\n", " ('3586.49 MB', 'ATL03_20220529100448_10281506_006_01.h5')]" ] }, "execution_count": 96, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ordered = sorted(results, key=lambda d: d.size())\n", "[(f\"{round(g.size(),2)} MB\", g[\"meta\"][\"native-id\"]) for g in ordered]" ] }, { "cell_type": "markdown", "id": "7fbb57d5-1a11-47d5-b274-c1ed5f0e3bbe", "metadata": {}, "source": [ "## 2. What \"cloud native workflows\" means?\n", "
\n", "\n", "* ### When we run cloud native workflows, we don't replicate the archives. e.g. COGs or Zarr. Instead, this is what usually happens:\n", "\n", " * ```python\n", " import xarray as xr\n", " \n", " # We open our data and we live happily ever after\n", " ds = xr.open_dataset(some_zarr_store, engine=\"zarr\")\n", " \n", " ```\n", "
\n", "\n", "* #### Most mission data is not in COGs or Zarr but fear not, **earthaccess** FTW!.\n", "\n", "\n", "> [Closed Platforms vs. Open Architectures for Cloud-Native Earth System Analytics](https://medium.com/pangeo/closed-platforms-vs-open-architectures-for-cloud-native-earth-system-analytics-1ad88708ebb6)\n", "> \n", "> [HDF in the Cloud](https://matthewrocklin.com/blog/work/2018/02/06/hdf-in-the-cloud)\n", "\n", "\n", "\n", "
\n", "Credit: Open Architecture for scalable cloud-based data analytics. From Abernathey, Ryan (2020): Data Access Modes in Science.
Figure. https://doi.org/10.6084/m9.figshare.11987466.v1\n", "
" ] }, { "cell_type": "code", "execution_count": 5, "id": "1fe4beef-c0b9-49c5-9578-9b45ebe22287", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Opening 1 granules, approx size: 0.34 GB\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e542e7e4c06d4cfa98d48a6ba0d2020d", "version_major": 2, "version_minor": 0 }, "text/plain": [ "QUEUEING TASKS | : 0%| | 0/1 [00:00\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:         (delta_time: 3376706, ds_surf_type: 5)\n",
       "Coordinates:\n",
       "  * delta_time      (delta_time) datetime64[ns] 2022-02-08T03:16:18.010370560...\n",
       "    lat_ph          (delta_time) float64 ...\n",
       "    lon_ph          (delta_time) float64 ...\n",
       "Dimensions without coordinates: ds_surf_type\n",
       "Data variables:\n",
       "    dist_ph_across  (delta_time) float32 ...\n",
       "    dist_ph_along   (delta_time) float32 ...\n",
       "    h_ph            (delta_time) float32 ...\n",
       "    pce_mframe_cnt  (delta_time) uint32 ...\n",
       "    ph_id_channel   (delta_time) uint8 ...\n",
       "    ph_id_count     (delta_time) uint8 ...\n",
       "    ph_id_pulse     (delta_time) uint8 ...\n",
       "    quality_ph      (delta_time) int8 ...\n",
       "    signal_conf_ph  (delta_time, ds_surf_type) int8 ...\n",
       "    weight_ph       (delta_time) uint8 ...\n",
       "Attributes:\n",
       "    Description:  Contains arrays of the parameters for each received photon.\n",
       "    data_rate:    Data are stored at the photon detection rate.
" ], "text/plain": [ "\n", "Dimensions: (delta_time: 3376706, ds_surf_type: 5)\n", "Coordinates:\n", " * delta_time (delta_time) datetime64[ns] 2022-02-08T03:16:18.010370560...\n", " lat_ph (delta_time) float64 ...\n", " lon_ph (delta_time) float64 ...\n", "Dimensions without coordinates: ds_surf_type\n", "Data variables:\n", " dist_ph_across (delta_time) float32 ...\n", " dist_ph_along (delta_time) float32 ...\n", " h_ph (delta_time) float32 ...\n", " pce_mframe_cnt (delta_time) uint32 ...\n", " ph_id_channel (delta_time) uint8 ...\n", " ph_id_count (delta_time) uint8 ...\n", " ph_id_pulse (delta_time) uint8 ...\n", " quality_ph (delta_time) int8 ...\n", " signal_conf_ph (delta_time, ds_surf_type) int8 ...\n", " weight_ph (delta_time) uint8 ...\n", "Attributes:\n", " Description: Contains arrays of the parameters for each received photon.\n", " data_rate: Data are stored at the photon detection rate." ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "\n", "start_time = time.time()\n", "ds_normal = xr.open_dataset(\n", " fo_normal,\n", " group=\"/gt1l/heights\",\n", " engine=\"h5netcdf\")\n", "t_normal = round(time.time() - start_time, 2)\n", "\n", "print(f\"Opening the data (out-of-region) with xarray took {t_normal} seconds\")\n", "ds_normal" ] }, { "cell_type": "markdown", "id": "4ea96ea7-5dcb-4122-9de9-01e4b69c4208", "metadata": {}, "source": [ "## 3. Let's use the force.\n", "\n", "
\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 10, "id": "122f83fa-ad3a-4c2c-afc2-79bdb9c1a734", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Opening 1 granules, approx size: 0.34 GB\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "5e51b827e71042e59b923dc0315eda2c", "version_major": 2, "version_minor": 0 }, "text/plain": [ "QUEUEING TASKS | : 0%| | 0/1 [00:00\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:         (delta_time: 3376706, ds_surf_type: 5)\n",
       "Coordinates:\n",
       "  * delta_time      (delta_time) datetime64[ns] 2022-02-08T03:16:18.010370560...\n",
       "    lat_ph          (delta_time) float64 ...\n",
       "    lon_ph          (delta_time) float64 ...\n",
       "Dimensions without coordinates: ds_surf_type\n",
       "Data variables:\n",
       "    dist_ph_across  (delta_time) float32 ...\n",
       "    dist_ph_along   (delta_time) float32 ...\n",
       "    h_ph            (delta_time) float32 ...\n",
       "    pce_mframe_cnt  (delta_time) uint32 ...\n",
       "    ph_id_channel   (delta_time) uint8 ...\n",
       "    ph_id_count     (delta_time) uint8 ...\n",
       "    ph_id_pulse     (delta_time) uint8 ...\n",
       "    quality_ph      (delta_time) int8 ...\n",
       "    signal_conf_ph  (delta_time, ds_surf_type) int8 ...\n",
       "    weight_ph       (delta_time) uint8 ...\n",
       "Attributes:\n",
       "    Description:  Contains arrays of the parameters for each received photon.\n",
       "    data_rate:    Data are stored at the photon detection rate.
" ], "text/plain": [ "\n", "Dimensions: (delta_time: 3376706, ds_surf_type: 5)\n", "Coordinates:\n", " * delta_time (delta_time) datetime64[ns] 2022-02-08T03:16:18.010370560...\n", " lat_ph (delta_time) float64 ...\n", " lon_ph (delta_time) float64 ...\n", "Dimensions without coordinates: ds_surf_type\n", "Data variables:\n", " dist_ph_across (delta_time) float32 ...\n", " dist_ph_along (delta_time) float32 ...\n", " h_ph (delta_time) float32 ...\n", " pce_mframe_cnt (delta_time) uint32 ...\n", " ph_id_channel (delta_time) uint8 ...\n", " ph_id_count (delta_time) uint8 ...\n", " ph_id_pulse (delta_time) uint8 ...\n", " quality_ph (delta_time) int8 ...\n", " signal_conf_ph (delta_time, ds_surf_type) int8 ...\n", " weight_ph (delta_time) uint8 ...\n", "Attributes:\n", " Description: Contains arrays of the parameters for each received photon.\n", " data_rate: Data are stored at the photon detection rate." ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "start_time = time.time()\n", "\n", "ds_smart = xr.open_dataset(\n", " fo_smart,\n", " group=\"/gt1l/heights\",\n", " engine=\"h5netcdf\")\n", "\n", "t_smart = round(time.time() - start_time, 2)\n", "print(f\"Opening the data (out-of-region) with xarray took {t_smart} seconds with earthacess smart open\")\n", "\n", "ds_smart" ] }, { "cell_type": "code", "execution_count": 12, "id": "8d8073f2-1bae-4bb8-a774-f936f31e3ff2", "metadata": {}, "outputs": [], "source": [ "mean_normal = ds_normal.h_ph.mean()\n", "mean_smart = ds_smart.h_ph.mean()\n", "\n", "assert mean_normal == mean_smart" ] }, { "cell_type": "markdown", "id": "6002132a-bef5-4eb9-9b88-4f87c0ff7c86", "metadata": {}, "source": [ "## 4. Recap and the importance of optimized access patterns.\n", "\n", "* ### We accessed one granule of mission data with earthaccess and xarray.\n", " * #### The first time we opened it as usual, it takes forever...\n", " * #### The second time we used experimental earthaccess' \"smart open\" capabilities.\n", " \n", "* ### Now let's see some I/O stats" ] }, { "cell_type": "code", "execution_count": 92, "id": "c57de1e9-6f5b-4dc5-8992-8b62c3b8684d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "1 data granule\n", "Size: 349.38 MB\n", "\n", "Egress:\n", " without earthaccess: 3199.29 MB \n", " with earthaccess : 112.0 MB\n", "\n", "Time to science:\n", " without earthaccess: 15.9 minutes \n", " with earthaccess : 0.52 minutes\n", "\n" ] } ], "source": [ "print(f\"\"\"\n", "1 data granule\n", "Size: {round(ordered[0].size(), 2)} MB\n", "\n", "Egress:\n", " without earthaccess: {round(fo_normal.cache.total_requested_bytes / 1024**2, 2)} MB \n", " with earthaccess : {round(fo_smart.cache.total_requested_bytes / 1024**2, 2)} MB\n", "\n", "Time to science:\n", " without earthaccess: {round(t_normal/60, 2)} minutes \n", " with earthaccess : {round(t_smart/60, 2)} minutes\n", "\"\"\")" ] }, { "cell_type": "markdown", "id": "59f4f92f-618d-470b-929a-86cb9bd4bf76", "metadata": {}, "source": [ "> Bibliography:\n", "> * [Patrick Quinn's Cloud-Optimized Format Study](https://ntrs.nasa.gov/api/citations/20200001178/downloads/20200001178.pdf)\n", "> * [ Aleksandar Jelenak: Toward Cloud-Optimized HDF5 Files](https://ntrs.nasa.gov/citations/20220019334)" ] }, { "cell_type": "markdown", "id": "e0ce0528-f6b2-4801-9764-3e84084e6c0f", "metadata": {}, "source": [ "## 5. Why is this important?\n", "\n", "
\n", "\n", "
\n", "\n", "Source: [2023 NASA ESDS Program Highlights](https://www.earthdata.nasa.gov/esds/esds-highlights/2023-esds-highlights)\n", "\n", "
\n", "\n", "\n", " \n", "* #### As data in the cloud grows, we need to provide researchers with the means to do cloud native workflows, **the simple way!**\n", "* #### Cloud native workflows could get expensive, **earthaccess can help!**\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "ed192214-fcb8-413c-8b4b-7160c4eac8c4", "metadata": {}, "source": [ "# 6. Cloud compute with *earthaccess*, from [terabyte](https://blog.coiled.io/blog/processing-terabyte-scale-nasa-cloud-datasets-with-coiled.html) to petabyte scale analysis.\n", "\n", "### We are going to answer a scientific question: \"How cold do the Great Lakes get in December?\"\n", "\n", "* #### Local execution: 10 years, 2 days around Santa's visit. ~ 10GB of HDF\n", "* #### Cloud execution: 20 years, all December. ~ 0.25 TB of HDF\n", "\n", "* #### We'll use NASA's [GHRSST Level 4 MUR Global Foundation Sea Surface Temperature Analysis (v4.1)](https://podaac.jpl.nasa.gov/dataset/MUR-JPL-L4-GLOB-v4.1)\n", "\n", "
\n", "
\n", "\n", "#### Now you're probably asking, what AWS integration is he going to use? Lambda? Step Functions? Perhaps a novel SQS-powered pipeline...\n", "\n", "
\n", "\n", "
\n", "\n", "
\n" ] }, { "cell_type": "markdown", "id": "1620b56c-0daf-4691-a600-a3f7ec499126", "metadata": { "jupyter": { "source_hidden": true } }, "source": [ "\n", "> A Little background story\n", "\n", "
\n", "
\n", "\n", "

\n", " Me and others talking to Nobel Prize winner Paul Romer, very approchable guy who happens to be a Pythonista. Paris, 2023.\n", "

\n", "
\n", "\n", "
\n", "\n", "* After talking to Paul Romer, I met some Dask core maintainers and started to chat about our projects. They left NVIDIA to start a company called Coiled.\n", "* They(Coiled) were looking for a simple package to handle NASA APIs for a climate startup, I was looking for ways to unlock the real potential of earthaccess.\n", "* A colaboration from academia and industry led to improvements in *earthaccess* \n" ] }, { "cell_type": "code", "execution_count": 24, "id": "cd14bad9-142a-4e15-8c6b-ef4d0b1ac481", "metadata": {}, "outputs": [], "source": [ "%%capture\n", "import coiled\n", "import dask.bag as db\n", "import numpy as np\n", "\n", "december_ts = []\n", "total_size = 0\n", "total_granules = 0\n", "\n", "for year in range(2013, 2023):\n", " print(f\"Searching for year {year}: \")\n", " results = earthaccess.search_data(\n", " short_name=\"MUR-JPL-L4-GLOB-v4.1\",\n", " # temporal=(f\"{year}-12-01\", f\"{year}-12-31\"),\n", " temporal=(f\"{year}-12-24\", f\"{year}-12-25\"),\n", " )\n", " total_size += round(sum([g.size() for g in results]), 2)\n", " total_granules += len(results)\n", " december_ts.append({f\"{year}\": results})" ] }, { "cell_type": "code", "execution_count": 25, "id": "0052b978-1832-421a-b432-0fb204108567", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "granules: 20, total size: 9.45GB\n" ] } ], "source": [ "print(f\"granules: {total_granules}, total size: {round(total_size/1024, 2)}GB\")" ] }, { "cell_type": "markdown", "id": "cdbbd87d-d943-4dd3-aebf-68bf46f79dc2", "metadata": {}, "source": [ "
\n", "

\n", " “Unfortunately, no one can be told what cloud compute at scale is.
\n", " You have to see it for yourself.”
\n", " — Luis López\n", "

\n", "\n", "
\n" ] }, { "cell_type": "code", "execution_count": 80, "id": "240a8f5e-82ac-42a2-9c15-4654bcca7265", "metadata": { "jupyter": { "source_hidden": true } }, "outputs": [], "source": [ "@coiled.function(\n", " name=\"clusty\",\n", " region=\"us-west-2\", # Run in the same region as data\n", " environ=earthaccess.auth_environ(), # Forward Earthdata auth to cloud VMs\n", " arm=True, # Use ARM-based instances \n", " vm_type=\"x2gd.medium\",\n", " local=True\n", ")\n", "def process(data):\n", " granules, params= data\n", " results = []\n", " io_stats = {\n", " \"total\": 0,\n", " \"hits\": 0,\n", " \"miss\": 0\n", " }\n", " bbox = params[\"bbox\"]\n", " variables = params[\"variables\"]\n", " agg = params[\"agg\"]\n", "\n", " def aggr_func(dataset, op):\n", " \"\"\"Apply different aggregations to an xarray Dataset.\"\"\"\n", " supported_ops = {\n", " 'mean': lambda ds: ds.mean(\"time\"), 'median': lambda ds: ds.median(\"time\"), 'min': lambda ds: ds.min(\"time\"),\n", " 'max': lambda ds: ds.max(\"time\"), 'std': lambda ds: ds.std(\"time\")}\n", " try:\n", " func = supported_ops[op]\n", " return func(dataset)\n", " except KeyError:\n", " raise ValueError(f'\"{op}\" aggregation operation not supported')\n", " \n", " earthaccess.login()\n", "\n", " fileset = earthaccess.open(granules, smart=True)\n", " min_lon, min_lat, max_lon, max_lat = bbox\n", " for file in fileset:\n", " ds = xr.open_dataset(file)\n", " ds = ds.sel(lon=slice(min_lon, max_lon), lat=slice(min_lat, max_lat))\n", " # this is particular to this dataset but can be parametrized\n", " cond = (ds.sea_ice_fraction < 0.15) | np.isnan(ds.sea_ice_fraction)\n", " result = ds.where(cond)\n", " results.append(result[variables])\n", " # This is only to keep track of IO\n", " io_stats[\"total\"] += file.cache.total_requested_bytes\n", " io_stats[\"hits\"] += file.cache.hit_count\n", " io_stats[\"miss\"] += file.cache.miss_count\n", " cube = xr.concat(results, dim=\"time\")\n", " if agg:\n", " cube_redux = aggr_func(cube, agg)\n", " cube_redux[\"time\"] = cube.time.min().values\n", " return (cube_redux, io_stats)\n", " return (cube[variables], io_stats)" ] }, { "cell_type": "markdown", "id": "beb051d9-a256-4c3f-8e98-925906eb049e", "metadata": {}, "source": [ "### Remote code execution, or local code execution... our choice." ] }, { "cell_type": "code", "execution_count": 82, "id": "3330d9b1-e5d1-484f-8b68-3552f345f883", "metadata": {}, "outputs": [ { "data": { "image/png": "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uCSpfv36Hk+CYLFjxw6NGjVK//rXv1S7dm2vxwFQiXz00Uc6d+6c7rjjjgsOnwEnjB8/XuHh4YX/rQ9cTMOGDUu9TqNGjQIwCYJVwVu0dujQocj3Cr62YsWKQI5UrlS6A+h9+/ZJkq655poi37v22msvuA4ABNqXX36pHTt26I477lCVKlW8HgdBxO/366uvvtKf/vQnLVmyRImJiWrRooXXYyEI+P1+PfHEE7r11ls1ZMgQr8dBkNq9e7datWqliIgINW7cWPfddx8fqosy2bJliyQpMTFR06ZN049+9CNFRkaqRYsWeuaZZ3T69GmPJ0Sw+vzzz7Vx40b1799f9erV83oclHM9e/ZU27Zt9c4772jixIk6ffq00tPTNXPmTL322muKjY3VgAEDvB4T5VjdunUlqdgPHCz42t69ewM6U3lS6d6Co+D9MIt7dU/B186cORPQmQBAkrKysvTjH/9YNWrUuOAtgoCS7N69W1dffXXh/69SpYpGjBihV1991cOpEEz++9//aufOndq1a5fXoyCIpaamKjU1VdK3H6o7ffp0ffjhh5o2bZruvvtuj6dDeXbixAlJ37693TvvvFP49eTkZI0ZM0Yff/yx1q9fr5iYGK9GRJAaP368JPH2GyiT0NBQLV26VM8++6weffRR+f3+wu/deeedeu211xQVFeXhhCjvEhMTNX/+fP3sZz9TWFiYevToofT0dL355puaM2eOJFXqJ1Ur3SugjTGX9T0AcFNWVpYGDhyo3bt3a/bs2YqLi/N6JASp8+fPa/369dq+fbvXoyAIfPXVV/rtb3+rv/71r2ratKnX4yBI9erVS/PmzdPXX3+t9PR0bdq0SYMGDVJubq6GDx+ujIwMr0dEOVZwyPPuu+/qF7/4hVJSUpSZmaklS5aodevW2rlzp0aPHu3xlAg258+f15QpU9SoUSP169fP63EQJD799FNt3br1gsNnSdq+fbs2btzo0VQIFsOHD1fnzp115MgR9e/fXzExMWrSpIn+8Ic/aOjQoZK+/SD5yqrS/clr1KghSUpLSyvyvVOnTl1wHQAIhFOnTqlPnz7asGGDFixYoO7du3s9EoLIVVddJWOM8vLy9NVXX2n8+PHav3+/evfurYMHD3o9Hsq5n/3sZ2rXrp1++tOfej0KgticOXPUv39/NWzYUDExMercubPef/999ezZ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zJkzzeOPP27q169vJJnrrrvOjB492pw8edKzuVBUfn6+Wbp0qRk2bJiJiYkx4eHh5vbbbzevv/66SU5O9myuvLw8s27dOvO73/3OdOjQwUgyDRo0MD//+c/Ntm3bPJsLxdu7d6956aWXTMuWLY0k0759e/P73//ebNiwweTn53s214EDB8zYsWPN7bffbiIjI01kZKS54447zKxZs0xubq5nc6EoegZb9AxOoGewRc9gi57BCfQMtuiZ6/KC4gB648aN5oEHHjBRUVEmMjLSDBo0yMyZM8fk5OR4PVoReXl5ZsmSJebxxx831atXN5GRkeb+++83n3zyidejVWrHjh0zL774omnWrJmRZDp37mz+9a9/mRMnTng9WrG+/PJL88c//tG0adPGSDI33nijmTx5MpHyUE5Ojpk4caJJSEgwPp/PNGrUyDz77LPl9gfQU6dOmXfffdf07dvXhISEmMaNG5vf/e535vjx416PVqnRM9iiZ7BFz+AEegZb9Ay26BmcQM8CpnwfQK9YscL07dvXSDKdOnUyb7zxhklLS/N6rDLLzMw0b731lunYsaORZBITE82qVau8HqtSOXz4sBk5cqSJiooyDRo0ML/5zW/Mrl27vB6rzPx+v1m+fLm5//77TVhYmGnZsqV58803zfnz570erdI4f/68ef31102LFi1MeHi4ue+++0xSUpLJy8vzerQyO3DggHnxxRdN/fr1TXR0tHn66afNkSNHvB6rUqFnsEXPYIuewQn0DLboGWzRMziBngVc+TyAXr16tenVq5eRZBISEszcuXO9Hsna6tWrTf/+/S/4M/n9fq/HqrAOHDhgRo4caapUqWIaNmxo/vrXv5qsrCyvx7KSnJxsRo4caapWrWoaNGhQIf5M5VlmZqYZM2aMadq0qYmIiDBDhw41e/bs8XosK+fPnzdvvvmmadasWeGf6csvv/R6rAqNnsEWPYMtegYn0DPYomewRc/gBHrmmfJ1AL1t2zbTrVs34/P5zG233WbWrFnj9UiOW7VqlUlMTDSSTPfu3c3nn3/u9UgVyunTp83IkSNNWFiYadWqlZkwYYLJzs72eixHHTlyxDzzzDMmOjraNGrUyEyZMqU8PrgELb/fbyZOnGgaNGhgqlWrZp599llz9OhRr8dyVHZ2thk3bpxp2bKlCQsLM88884yn72tVEdEz2KJn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Fy9ebCSZU6dOuT5YwR0syTz88MNm7969JjMz06xatcq0a9fOSDIvvvhisdd/9NFHzZdffmny8vKMMcaMGjXKSDKxsbFm3rx55syZM+bQoUPm//7v/0xISIipUqWK+frrr40xxkyYMMFIMnXq1DFvvPGGOXTokMnMzDSbN282w4cPNytWrDDGGDNp0iQjybRr184sWLDApKammszMTLNy5UrTvn17I8msW7fOGGNM69atTXR0tJk5c6Y5ffq0ycrKMp999pl57rnnzDvvvFP4Zyjr9dx04sQJI8ksX77ctTVq1qxpxo0b59rtF2APebOHjDHm5ptvNiNGjHDt9h9//HHTt29f127/+9hH3uyjsWPHmnr16rl2+/Ss4u8hesYecgI9Yx/ZomfsIVv0jD3kBHrGPrJFz9hDtkroWdED6Pnz5xtJJisry/XBCu7gG264wfj9/gu+t3//fhMeHm6uuuqqItfv0qVLket36dLFSDLr168vss4TTzxhJJk333zTGGPMTTfddMEzDxfTs2dPExoaao4ePVrkezt37jSSzPPPP1943datW5vc3NxSb7Ms13PTmTNnjCSzcOFC19aIiooy7777rmu3X4A95J1bb73VPPbYY67d/rBhw0z//v1du/3vYx95Y/z48aZ69equ3T49+05F3UP0jD3kBHrGPrJFz9hDtugZe8gJ9Ix9ZIuesYdsldCzvCLvAZ2TkyNJioiI+OG3XNO3b1/5fL4LvtayZUu1adNG+/fvL3L93r17F7n+vn37VKdOHXXp0qXI9fv37194HUnavXu3atWqpd69e5c4186dO5Wfn69mzZopLCxMoaGhCgkJUUhIiNq2bStJOnTokCTptddek9/vV6tWrfSTn/xE//3vf7V169Yit1nW67mp4EMDsrOzXVsjOzubPaSKu4ekbx8jKtIekthHXjwWubmH6Nl3KvIekugZe8gOPWMf2aJn7CFb9Iw95AR6xj6yRc/YQ7ZK6lmRA+hgcLE3vv/hJrHl9/slfftG3vn5+fL7/TLGXPBm4AV/gdq3b6/du3dr0qRJatGihVavXq3ExES1bdtWO3bsKLx+Wa/npoL5nf739X1u3rYT2EPOcHsPff/fU3nEPrJjjCn3jxVuYw/ZoWfsIafQM/aRDXrGHrJFz9hDTqFn7CMb9Iw9ZKvEnv3wNdHB8JL41157rcht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DQZvrF27NiAPKtK3eyglJUVHjhwJyHoIjLVr16phw4aufzquJLVp00b16tUjThXMgQMH9PXXXwf0sYieVTz0DLboGWzRMziBnsEWPYMtegYnlNazix5Ad+/eXZ988onS09NdGQyBd+rUKX322Wfq3r17QNa78cYbFRkZqWXLlgVkPQTG8uXL1aNHj4Cs5fP51K1bN/ZQBbNs2TJFRUWpY8eOAVmPnlU89AxOoGewRc9gi57BCfQMtugZbJWlZxc9gO7bt6/8fr+WLl3qynAIvMWLF8vn86lXr14BWS8qKko33XSTFi5cGJD14L5z585p1apVSkxMDNiaiYmJWrZsmbKzswO2JtyVlJSknj17qkqVKgFZj55VPPQMtugZnEDPYIuewRY9gxPoGWyVpWcXPYCuXbu2OnfurAULFrgyHAIvKSlJXbt2df1TTb+vX79+Wrx4sfLz8wO2JtyzbNkynT9/XrfeemvA1kxMTFRWVpZWrVoVsDXhntzcXC1dujSgPyTTs4qHnsEWPYMtegYn0DPYomewRc/ghLL07KIH0JI0YMAAzZ07lzhVAOfPn9ecOXM0cODAgK7bv39/paamauXKlQFdF+6YPn26brjhBjVq1Chga8bGxqp9+/b68MMPA7Ym3LNw4UKlp6drwIABAV2XnlUc9AxOoGewRc9gi57BCfQMtugZbJW1ZyUeQA8ZMkTHjx/XihUrnJwNHpg3b57S09N13333BXTdNm3aqEOHDpoxY0ZA14XzsrKyNGvWLA0dOjTgaw8ePFgzZ85UXl5ewNeGsyZPnqwePXqoefPmAV2XnlUc9Ay26BmcQM9gi57BFj2DE+gZbJW1ZyUeQF9xxRW67rrrNH36dEeHQ+BNmTJFvXv3VpMmTQK+9r333quZM2cqNzc34GvDObNmzVJOTo6GDBkS8LXvvfdenThxgjgFudOnT2vevHl68MEHA742Pas46Bls0TPYomdwAj2DLXoGW/QMTihrz0o8gJakoUOHavr06crKynJsOATWyZMnlZSUpIceesiT9X/84x8rNTVV8+bN82R9OGPSpEnq16+f6tatG/C1W7durS5duujtt98O+NpwzowZM+Tz+TRo0CBP1qdnwY+ewQn0DLboGWzRMziBnsEWPYOtS+lZqQfQDz30kHJycnhWIohNnTpVERERAX9/sQLNmzdX3759NWHC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", "text/plain": [ "" ] }, "execution_count": 82, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Step 3: Run function on each file in parallel\n", "params = {\n", " \"bbox\" : (-94.93, 40.26, -73.83, 48.95),\n", " \"variables\": [\"analysed_sst\"],\n", " \"agg\": \"mean\"\n", "}\n", "\n", "data = list(map(lambda e: (list(e.values())[0], params), december_ts))\n", "\n", "dask_bag = db.from_sequence(data)\n", "c = dask_bag.map(process)\n", "c.visualize()" ] }, { "cell_type": "code", "execution_count": 83, "id": "445a6982-b83e-435e-a6ac-cca640f363f4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 28.9 s, sys: 5.62 s, total: 34.6 s\n", "Wall time: 2min 37s\n" ] }, { "data": { "text/plain": [ "{'miss': 252, 'hits': 1968, 'total': 1008.0}" ] }, "execution_count": 83, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "computation = process.map(data)\n", "dds, iostats = zip(*computation)\n", "\n", "# This is only to consolidate the i/o stats\n", "agg = {key:sum(i[key] for i in iostats if key in i) \n", " for key in set(a for l in iostats for a in l.keys())}\n", "\n", "agg[\"total\"] = agg[\"total\"] / (1024*1024) # MB\n", "agg" ] }, { "cell_type": "code", "execution_count": 84, "id": "b3325042-c8e9-407a-98a6-7368dcad735c", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:       (time: 10, lat: 870, lon: 2111)\n",
       "Coordinates:\n",
       "  * lat           (lat) float32 40.26 40.27 40.28 40.29 ... 48.93 48.94 48.95\n",
       "  * lon           (lon) float32 -94.93 -94.92 -94.91 ... -73.85 -73.84 -73.83\n",
       "  * time          (time) datetime64[ns] 2013-12-24T09:00:00 ... 2022-12-24T09...\n",
       "Data variables:\n",
       "    analysed_sst  (time, lat, lon) float32 nan nan nan nan ... nan nan nan nan
" ], "text/plain": [ "\n", "Dimensions: (time: 10, lat: 870, lon: 2111)\n", "Coordinates:\n", " * lat (lat) float32 40.26 40.27 40.28 40.29 ... 48.93 48.94 48.95\n", " * lon (lon) float32 -94.93 -94.92 -94.91 ... -73.85 -73.84 -73.83\n", " * time (time) datetime64[ns] 2013-12-24T09:00:00 ... 2022-12-24T09...\n", "Data variables:\n", " analysed_sst (time, lat, lon) float32 nan nan nan nan ... nan nan nan nan" ] }, "execution_count": 84, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ds = xr.concat(dds, dim=\"time\")\n", "\n", "# transform to celcius\n", "ds[\"analysed_sst\"] = ds[\"analysed_sst\"] - 273.15\n", "ds.analysed_sst.attrs['units'] = 'degC'\n", "\n", "ds" ] }, { "cell_type": "code", "execution_count": 85, "id": "bc82c37a-4183-48ff-bd45-f53e9c0be2c5", "metadata": {}, "outputs": [ { "data": {}, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ "
\n", "
\n", "
\n", "" ], "text/plain": [ ":DynamicMap [time]\n", " :Overlay\n", " .Polygons.I :Polygons [lon,lat] (analysed_sst)\n", " .Coastline.I :Feature [Longitude,Latitude]" ] }, "execution_count": 85, "metadata": { "application/vnd.holoviews_exec.v0+json": { "id": "p118215" } }, "output_type": "execute_result" } ], "source": [ "hvplot.output(widget_location=\"bottom\")\n", "ds.analysed_sst.hvplot.contourf(\n", " groupby=\"time\",\n", " cmap=\"viridis\",\n", " x=\"lon\", y=\"lat\",\n", " levels=10,\n", " coastline=True)" ] }, { "cell_type": "code", "execution_count": 86, "id": "cab212fc-72d1-4896-b5d3-b183da01908c", "metadata": {}, "outputs": [ { "data": {}, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ "
\n", "
\n", "
\n", "" ], "text/plain": [ ":NdOverlay [lat]\n", " :Distribution [analysed_sst] (Density)" ] }, "execution_count": 86, "metadata": { "application/vnd.holoviews_exec.v0+json": { "id": "p121801" } }, "output_type": "execute_result" } ], "source": [ "ds.isel(time=0).sel(lat=[42,44,46,48]).hvplot.kde(\"analysed_sst\", by='lat', alpha=0.5)" ] }, { "cell_type": "code", "execution_count": 89, "id": "8db40265-5a51-417e-9c57-e2b4c8b3dcf5", "metadata": {}, "outputs": [ { "data": {}, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ "
\n", "
\n", "
\n", "" ], "text/plain": [ ":NdOverlay [lat]\n", " :Distribution [analysed_sst] (Density)" ] }, "execution_count": 89, "metadata": { "application/vnd.holoviews_exec.v0+json": { "id": "p122233" } }, "output_type": "execute_result" } ], "source": [ "ds.isel(time=3).sel(lat=[42,44,46,48]).hvplot.kde(\"analysed_sst\", by='lat', alpha=0.5)" ] }, { "cell_type": "code", "execution_count": 103, "id": "ee44669f-76fc-4e41-8a54-a5fb3914f247", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "total egress: 0.98gb\n" ] } ], "source": [ "print(f\"total egress: {round(agg['total']/1024, 2)}gb\")" ] }, { "cell_type": "markdown", "id": "9701f5d8-10e7-4ef0-9e87-a600b51fbc53", "metadata": { "jp-MarkdownHeadingCollapsed": true }, "source": [ "
\n", "

\n", " Cloud computing with earthaccess\n", "

\n", "\n", "\n", "
\n", "
\n", "\n", "

\n", " Thanks to the earthaccess community and the projects that have supported this work!\n", "

\n", "\n", "\n", "\n", "\n", "
\n", "\n", "\n", "\n", "
\n", "
\n", "
\n", "\n", "\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.13" } }, "nbformat": 4, "nbformat_minor": 5 }