From f7499fef5c81a28897ce5ef9e75b5be1b934bf80 Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Tue, 24 Mar 2026 15:49:51 -0600 Subject: [PATCH 1/8] replaced relative path to modules in CCSS collect SWE notebook --- .../ccss_swe_collect_observations.ipynb | 209 +++++++++++++++++- 1 file changed, 205 insertions(+), 4 deletions(-) diff --git a/examples/collect_observations/ccss_swe_collect_observations.ipynb b/examples/collect_observations/ccss_swe_collect_observations.ipynb index 947d402..c2f1525 100644 --- a/examples/collect_observations/ccss_swe_collect_observations.ipynb +++ b/examples/collect_observations/ccss_swe_collect_observations.ipynb @@ -34,21 +34,222 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "tags": [] }, - "outputs": [], + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.1/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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "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" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.jsdelivr.net/npm/@holoviz/geoviews@1.11.0/dist/geoviews.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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import os\n", "import time\n", "import sys\n", + "from pathlib import Path\n", "\n", "prefix = os.environ['CONDA_PREFIX']\n", "os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", "\n", "# add the src directory to the path so we can import evaluation modules\n", - "sys.path.append('../../src/')\n", + "sys.path.append(str((Path.cwd().absolute() / \"../../src\").resolve()))\n", "\n", "import pyproj\n", "import pandas as pd\n", @@ -366,7 +567,7 @@ ], "metadata": { "kernelspec": { - "display_name": "cssi_evaluation", + "display_name": "nwm_env", "language": "python", "name": "python3" }, From 0d34523c386f1acf6712bcc43da0b2e1ac04eeec Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Wed, 25 Mar 2026 15:13:20 -0600 Subject: [PATCH 2/8] commit to merge in new plotting function --- .../parflow_swe_point_scale_evaluation.ipynb | 526 ++- ...le_evaluation_copyAMY_Hydrodata_code.ipynb | 2993 +++++++++++++++++ src/cssi_evaluation/utils/plot_utils.py | 271 +- 3 files changed, 3693 insertions(+), 97 deletions(-) create mode 100644 examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb diff --git a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb index 88d04c8..52c8fef 100644 --- a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb +++ b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb @@ -44,29 +44,244 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "tags": [] }, - "outputs": [], + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.1/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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "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": "p1010" + } + }, + "output_type": "display_data" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], "source": [ "import os\n", "import sys\n", - "import glob\n", "from pathlib import Path\n", - "\n", "import holoviews as hv\n", "import hvplot.pandas\n", - "import geopandas as gpd\n", + "import hvplot.xarray\n", + "import pyproj\n", "import pandas as pd\n", + "import numpy as np\n", + "import xarray as xr\n", + "import geopandas as gpd\n", + "from dask.distributed import Client\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.dates as mdates\n", + "import hf_hydrodata as hf\n", + "import subsettools\n", + "\n", "\n", "# Import the Evaluation library from the project root.\n", - "sys.path.append(str((Path.cwd().absolute() / \"../../../src\").resolve()))\n", - "from cssi_evaluation.snow import utils\n", - "from cssi_evaluation import evaluation_metrics\n", + "sys.path.append(str((Path.cwd().absolute() / \"../../src\").resolve()))\n", "\n", - "# import notebook-specific utilities\n", - "from utils import nwm_utils\n", + "from cssi_evaluation.variables import snow_utils\n", + "from cssi_evaluation.utils import metric_utils\n", + "from cssi_evaluation.utils import evaluation_utils\n", "\n", "hv.extension('bokeh')\n", "\n", @@ -119,39 +334,39 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "0dfc3fb3", "metadata": {}, "outputs": [], "source": [ - "import os\n", - "import sys\n", + "# import os\n", + "# import sys\n", "\n", - "prefix = os.environ['CONDA_PREFIX']\n", - "os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", + "# prefix = os.environ['CONDA_PREFIX']\n", + "# os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", "\n", - "# add the src directory to the path so we can import evaluation modules\n", - "sys.path.append('../../src/')\n", + "# # add the src directory to the path so we can import evaluation modules\n", + "# sys.path.append('../../src/')\n", "\n", - "import sys\n", - "import pyproj\n", - "import pandas as pd\n", - "import numpy as np\n", - "import xarray as xr\n", - "import geopandas as gpd\n", - "from dask.distributed import Client\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib.dates as mdates\n", - "import hf_hydrodata as hf\n", - "import subsettools\n", - "import hvplot.xarray\n", + "# import sys\n", + "# import pyproj\n", + "# import pandas as pd\n", + "# import numpy as np\n", + "# import xarray as xr\n", + "# import geopandas as gpd\n", + "# from dask.distributed import Client\n", + "# import matplotlib.pyplot as plt\n", + "# import matplotlib.dates as mdates\n", + "# import hf_hydrodata as hf\n", + "# import subsettools\n", + "# import hvplot.xarray\n", "\n", "\n", - "from cssi_evaluation.utils import plot_utils\n", + "# from cssi_evaluation.utils import plot_utils\n", "\n", "\n", - "%load_ext autoreload\n", - "%autoreload 2\n" + "# %load_ext autoreload\n", + "# %autoreload 2\n" ] }, { @@ -166,7 +381,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "a365996d", "metadata": {}, "outputs": [], @@ -191,10 +406,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "6f2b08d0", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dashboard link: http://127.0.0.1:8787/status\n", + "\n" + ] + } + ], "source": [ "# use a try accept loop so we only instantiate the client\n", "# if it doesn't already exist.\n", @@ -217,7 +441,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "e50dc99d", "metadata": {}, "outputs": [], @@ -255,10 +479,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "c8355563", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HUC-8 ID: 14020001\n", + "HUC-8 name: East-Taylor\n" + ] + } + ], "source": [ "# ✏️ Specify HUC8 ID and Name for watershed of interest\n", "huc_8_code = '14020001' # East-Taylor HUC-8\n", @@ -278,10 +511,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "965bd6ea", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: 'examples/parflow/domain_data/domainMask_East-Taylor_conus1.npy'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[11], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m ij_bounds, mask \u001b[38;5;241m=\u001b[39m subsettools\u001b[38;5;241m.\u001b[39mdefine_huc_domain([huc_8_code], \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mconus1\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m----> 3\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mdomain_data_path\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43mdomainMask_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_name\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_conus1.npy\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmask\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 5\u001b[0m plt\u001b[38;5;241m.\u001b[39mimshow(mask, origin\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlower\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28mprint\u001b[39m(ij_bounds)\n", + "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36msave\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/numpy/lib/npyio.py:518\u001b[0m, in \u001b[0;36msave\u001b[0;34m(file, arr, allow_pickle, fix_imports)\u001b[0m\n\u001b[1;32m 516\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m file\u001b[38;5;241m.\u001b[39mendswith(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m):\n\u001b[1;32m 517\u001b[0m file \u001b[38;5;241m=\u001b[39m file \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m--> 518\u001b[0m file_ctx \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mwb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 520\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m file_ctx \u001b[38;5;28;01mas\u001b[39;00m fid:\n\u001b[1;32m 521\u001b[0m arr \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39masanyarray(arr)\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'examples/parflow/domain_data/domainMask_East-Taylor_conus1.npy'" + ] + } + ], "source": [ "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", "\n", @@ -302,7 +549,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "1ff5c1d4", "metadata": {}, "outputs": [], @@ -329,7 +576,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "13fbc81f", "metadata": {}, "outputs": [], @@ -351,10 +598,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "aa20e59f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: 'examples/parflow/domain_data/East-Taylor_14020001_lat_2d.npy'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[14], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# Save 2D arrays of Lat & Lon\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mdomain_data_path\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_name\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_code\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_lat_2d.npy\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlat\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m np\u001b[38;5;241m.\u001b[39msave(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdomain_data_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;132;01m{\u001b[39;00mhuc_8_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mhuc_8_code\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_lon_2d.npy\u001b[39m\u001b[38;5;124m\"\u001b[39m, lon)\n\u001b[1;32m 5\u001b[0m \u001b[38;5;66;03m# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \u001b[39;00m\n", + "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36msave\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/numpy/lib/npyio.py:518\u001b[0m, in \u001b[0;36msave\u001b[0;34m(file, arr, allow_pickle, fix_imports)\u001b[0m\n\u001b[1;32m 516\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m file\u001b[38;5;241m.\u001b[39mendswith(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m):\n\u001b[1;32m 517\u001b[0m file \u001b[38;5;241m=\u001b[39m file \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m--> 518\u001b[0m file_ctx \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mwb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 520\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m file_ctx \u001b[38;5;28;01mas\u001b[39;00m fid:\n\u001b[1;32m 521\u001b[0m arr \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39masanyarray(arr)\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'examples/parflow/domain_data/East-Taylor_14020001_lat_2d.npy'" + ] + } + ], "source": [ "# Save 2D arrays of Lat & Lon\n", "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", @@ -381,10 +642,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "41e8d63a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'grid_df' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[15], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mgrid_df\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'grid_df' is not defined" + ] + } + ], "source": [ "grid_df" ] @@ -418,10 +691,137 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "3aa8210e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
site_idsite_namesite_typeagencystatevariable_nameunitsdatasetvariabletemporal_resolution...latitudelongitudesite_query_urldate_metadata_last_updatedtz_cddoiconus1_iconus1_jconus2_iconus2_j
0380:CO:SNTLButteSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.89435-106.95327https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone942.0650.01372.01601.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.81982-106.58962https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone972.0638.01402.01589.0
\n", + "

2 rows × 24 columns

\n", + "
" + ], + "text/plain": [ + " site_id site_name site_type agency state \\\n", + "0 380:CO:SNTL Butte SNOTEL station NRCS CO \n", + "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO \n", + "\n", + " variable_name units dataset variable \\\n", + "0 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", + "1 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", + "\n", + " temporal_resolution ... latitude longitude \\\n", + "0 daily ... 38.89435 -106.95327 \n", + "1 daily ... 38.81982 -106.58962 \n", + "\n", + " site_query_url \\\n", + "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "\n", + " date_metadata_last_updated tz_cd doi conus1_i conus1_j conus2_i conus2_j \n", + "0 2023-03-07 PST None 942.0 650.0 1372.0 1601.0 \n", + "1 2023-03-07 PST None 972.0 638.0 1402.0 1589.0 \n", + "\n", + "[2 rows x 24 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "avail_df = hf.get_site_variables(variable = \"swe\",\n", " huc_id = [huc_8_code], grid = 'conus1',\n", @@ -442,10 +842,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "f5c95f67", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "DriverError", + "evalue": "examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mCPLE_OpenFailedError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32mfiona/ogrext.pyx:136\u001b[0m, in \u001b[0;36mfiona.ogrext.gdal_open_vector\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mfiona/_err.pyx:291\u001b[0m, in \u001b[0;36mfiona._err.exc_wrap_pointer\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mCPLE_OpenFailedError\u001b[0m: examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory", + "\nDuring handling of the above exception, another exception occurred:\n", + "\u001b[0;31mDriverError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[17], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m### Select station locations that fall within the HUC8 watershed\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \n\u001b[1;32m 3\u001b[0m \u001b[38;5;66;03m# Path to the watershed shapefile that was just created\u001b[39;00m\n\u001b[1;32m 4\u001b[0m watershed \u001b[38;5;241m=\u001b[39m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdomain_data_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124mEast-Taylor_14020001.shp\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m----> 5\u001b[0m watershed_gdf \u001b[38;5;241m=\u001b[39m \u001b[43mgpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_file\u001b[49m\u001b[43m(\u001b[49m\u001b[43mwatershed\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mto_crs(epsg\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m4326\u001b[39m)\n\u001b[1;32m 7\u001b[0m \u001b[38;5;66;03m# Create GeoDataFrame of all available stations\u001b[39;00m\n\u001b[1;32m 8\u001b[0m filtered_all_stations_gdf \u001b[38;5;241m=\u001b[39m gpd\u001b[38;5;241m.\u001b[39mGeoDataFrame(\n\u001b[1;32m 9\u001b[0m avail_df,\n\u001b[1;32m 10\u001b[0m geometry\u001b[38;5;241m=\u001b[39mgpd\u001b[38;5;241m.\u001b[39mpoints_from_xy(\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 14\u001b[0m crs\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEPSG:4326\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 15\u001b[0m )\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/geopandas/io/file.py:259\u001b[0m, in \u001b[0;36m_read_file\u001b[0;34m(filename, bbox, mask, rows, engine, **kwargs)\u001b[0m\n\u001b[1;32m 256\u001b[0m path_or_bytes \u001b[38;5;241m=\u001b[39m filename\n\u001b[1;32m 258\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m engine \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfiona\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m--> 259\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_read_file_fiona\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 260\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath_or_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfrom_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbbox\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbbox\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmask\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmask\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrows\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrows\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 261\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 262\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m engine \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpyogrio\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 263\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _read_file_pyogrio(\n\u001b[1;32m 264\u001b[0m path_or_bytes, bbox\u001b[38;5;241m=\u001b[39mbbox, mask\u001b[38;5;241m=\u001b[39mmask, rows\u001b[38;5;241m=\u001b[39mrows, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 265\u001b[0m )\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/geopandas/io/file.py:303\u001b[0m, in \u001b[0;36m_read_file_fiona\u001b[0;34m(path_or_bytes, from_bytes, bbox, mask, rows, where, **kwargs)\u001b[0m\n\u001b[1;32m 300\u001b[0m reader \u001b[38;5;241m=\u001b[39m fiona\u001b[38;5;241m.\u001b[39mopen\n\u001b[1;32m 302\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m fiona_env():\n\u001b[0;32m--> 303\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[43mreader\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath_or_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m features:\n\u001b[1;32m 304\u001b[0m crs \u001b[38;5;241m=\u001b[39m features\u001b[38;5;241m.\u001b[39mcrs_wkt\n\u001b[1;32m 305\u001b[0m \u001b[38;5;66;03m# attempt to get EPSG code\u001b[39;00m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/env.py:457\u001b[0m, in \u001b[0;36mensure_env_with_credentials..wrapper\u001b[0;34m(*args, **kwds)\u001b[0m\n\u001b[1;32m 454\u001b[0m session \u001b[38;5;241m=\u001b[39m DummySession()\n\u001b[1;32m 456\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m env_ctor(session\u001b[38;5;241m=\u001b[39msession):\n\u001b[0;32m--> 457\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/__init__.py:336\u001b[0m, in \u001b[0;36mopen\u001b[0;34m(fp, mode, driver, schema, crs, encoding, layer, vfs, enabled_drivers, crs_wkt, allow_unsupported_drivers, **kwargs)\u001b[0m\n\u001b[1;32m 333\u001b[0m path \u001b[38;5;241m=\u001b[39m parse_path(fp)\n\u001b[1;32m 335\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m mode \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m--> 336\u001b[0m colxn \u001b[38;5;241m=\u001b[39m \u001b[43mCollection\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 337\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 338\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 339\u001b[0m \u001b[43m \u001b[49m\u001b[43mdriver\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdriver\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 340\u001b[0m \u001b[43m \u001b[49m\u001b[43mencoding\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mencoding\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 341\u001b[0m \u001b[43m \u001b[49m\u001b[43mlayer\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlayer\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 342\u001b[0m \u001b[43m \u001b[49m\u001b[43menabled_drivers\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43menabled_drivers\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 343\u001b[0m \u001b[43m \u001b[49m\u001b[43mallow_unsupported_drivers\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mallow_unsupported_drivers\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 344\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 345\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 346\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m mode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 347\u001b[0m colxn \u001b[38;5;241m=\u001b[39m Collection(\n\u001b[1;32m 348\u001b[0m path,\n\u001b[1;32m 349\u001b[0m mode,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 358\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 359\u001b[0m )\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/collection.py:243\u001b[0m, in \u001b[0;36mCollection.__init__\u001b[0;34m(self, path, mode, driver, schema, crs, encoding, layer, vsi, archive, enabled_drivers, crs_wkt, ignore_fields, ignore_geometry, include_fields, wkt_version, allow_unsupported_drivers, **kwargs)\u001b[0m\n\u001b[1;32m 241\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 242\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m Session()\n\u001b[0;32m--> 243\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msession\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 244\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[1;32m 245\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m WritingSession()\n", + "File \u001b[0;32mfiona/ogrext.pyx:588\u001b[0m, in \u001b[0;36mfiona.ogrext.Session.start\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mfiona/ogrext.pyx:143\u001b[0m, in \u001b[0;36mfiona.ogrext.gdal_open_vector\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mDriverError\u001b[0m: examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory" + ] + } + ], "source": [ "### Select station locations that fall within the HUC8 watershed\n", "\n", @@ -1585,7 +2009,7 @@ ], "metadata": { "kernelspec": { - "display_name": "cssi_evaluation", + "display_name": "nwm_env", "language": "python", "name": "python3" }, diff --git a/examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb new file mode 100644 index 0000000..20d26fc --- /dev/null +++ b/examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb @@ -0,0 +1,2993 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![NWM](../img/NWM.png)\n", + "\n", + "# Use HydroData to Retrieve Modeled and Observed Snow Data for a Watershed of Interest with ParFlow-CONUS Outputs vs Observed Snow Water Equivalent (SWE) - Full Evaluation Workflow\n", + "Authors: Irene Garousi-Nejad (igarousi@cuahsi.org), Danielle Tijerina-Kreuzer (dtijerina@cuahsi.org) \n", + "Last updated: Feb 2026" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Introduction: \n", + "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites. We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Prepare the Python Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import the libraries needed to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ce97d33e", + "metadata": { + "tags": [] + }, + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.1/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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "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" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.jsdelivr.net/npm/@holoviz/geoviews@1.11.0/dist/geoviews.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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.jsdelivr.net/npm/@holoviz/geoviews@1.11.0/dist/geoviews.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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import os\n", + "import sys\n", + "from pathlib import Path\n", + "import holoviews as hv\n", + "import hvplot.pandas\n", + "import hvplot.xarray\n", + "import pyproj\n", + "import pandas as pd\n", + "import numpy as np\n", + "import xarray as xr\n", + "import geopandas as gpd\n", + "from dask.distributed import Client\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.dates as mdates\n", + "import hf_hydrodata as hf\n", + "import subsettools\n", + "\n", + "\n", + "# Import the Evaluation library from the project root.\n", + "sys.path.append(str((Path.cwd().absolute() / \"../../src\").resolve()))\n", + "\n", + "from cssi_evaluation.variables import snow_utils\n", + "from cssi_evaluation.utils import metric_utils\n", + "from cssi_evaluation.utils import evaluation_utils\n", + "from cssi_evaluation.utils import plot_utils\n", + "\n", + "hv.extension('bokeh')\n", + "\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "id": "e86aae63", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "markdown", + "id": "e88ed1ef", + "metadata": {}, + "source": [ + "### 1a. Python Environment \n", + "\n", + "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." + ] + }, + { + "cell_type": "markdown", + "id": "c0f30927", + "metadata": {}, + "source": [ + "Import the libraries needed to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0dfc3fb3", + "metadata": {}, + "outputs": [], + "source": [ + "# import os\n", + "# import sys\n", + "\n", + "# prefix = os.environ['CONDA_PREFIX']\n", + "# os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", + "\n", + "# # add the src directory to the path so we can import evaluation modules\n", + "# sys.path.append('../../src/')\n", + "\n", + "# import sys\n", + "# import pyproj\n", + "# import pandas as pd\n", + "# import numpy as np\n", + "# import xarray as xr\n", + "# import geopandas as gpd\n", + "# from dask.distributed import Client\n", + "# import matplotlib.pyplot as plt\n", + "# import matplotlib.dates as mdates\n", + "# import hf_hydrodata as hf\n", + "# import subsettools\n", + "# import hvplot.xarray\n", + "\n", + "\n", + "# from cssi_evaluation.utils import plot_utils\n", + "\n", + "\n", + "# %load_ext autoreload\n", + "# %autoreload 2\n" + ] + }, + { + "cell_type": "markdown", + "id": "8e058228", + "metadata": {}, + "source": [ + "### 1b. Register Pin and Access HydroData\n", + "\n", + "To access the HydroData catalog you will need to sign up for a [HydroFrame account](https://hydrogen.princeton.edu/signup) (do this only once), [create a 4-digit PIN](https://hydrogen.princeton.edu/pin), and register your pin in order to have access to the HydroData datasets (you will do this in the next code cell below). To note, you PIN will expire after 7 days and will need to recreate it after that time. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a365996d", + "metadata": {}, + "outputs": [], + "source": [ + "# You need to register on https://hydrogen.princeton.edu/pin \n", + "# and run the following with your registered information\n", + "# before you can use the hydrodata utilities\n", + "hf.register_api_pin(\"dtt2@princeton.edu\", \"7837\")" + ] + }, + { + "cell_type": "markdown", + "id": "825c288d", + "metadata": {}, + "source": [ + "### 1c. Dask \n", + "\n", + "We'll use dask to parallelize our code. To manage parallel computation and visualize progress of long-running tasks, we initialize a Dask “cluster,” which defines how many workers are used and how much computing power each worker has. \n", + "\n", + "In this setup, we create a Dask client with `Client(n_workers=6, threads_per_worker=1, memory_limit='2GB')`, which launches a cluster with 6 workers. Each worker uses a single thread, typically mapped to one CPU core, allowing for efficient parallel processing across 6 cores. Each worker also has a memory limit of 2 GB, for a total of up to 12 GB across the cluster.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6f2b08d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dashboard link: http://127.0.0.1:8787/status\n", + "\n" + ] + } + ], + "source": [ + "# use a try accept loop so we only instantiate the client\n", + "# if it doesn't already exist.\n", + "try:\n", + " print('Dashboard link:', client.dashboard_link)\n", + "except: \n", + " # The client should be customized to your workstation resources.\n", + " client = Client(n_workers=6, threads_per_worker=1, memory_limit='2GB') \n", + " print('Dashboard link:', client.dashboard_link)\n", + "print(client)" + ] + }, + { + "cell_type": "markdown", + "id": "b8620cfc", + "metadata": {}, + "source": [ + "## 2. Set Paths" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e50dc99d", + "metadata": {}, + "outputs": [], + "source": [ + "# Start and end times of a water year (to note, these dates were chosen to align with the PFCONUS1 early 2000s runs)\n", + "StartDate = '2003-10-01'\n", + "EndDate = '2005-09-30'\n", + "\n", + "domain_data_path = './domain_data/' # path to the model domain data\n", + "\n", + "# Path to save results (obs and mod stands for observation and modeled, respectively)\n", + "OBS_OutputFolder = './obs_outputs' \n", + "MOD_OutputFolder = './mod_outputs'" + ] + }, + { + "cell_type": "markdown", + "id": "feb58871", + "metadata": {}, + "source": [ + "## 3. Retrieve Observed Snow Data " + ] + }, + { + "cell_type": "markdown", + "id": "45ca2832", + "metadata": {}, + "source": [ + "### 3a. Define the watershed of interest\n", + "\n", + "One of the simplest ways to gather data and model output from HydroData is by specifying a [Hydrologic Unit Code](https://www.usgs.gov/national-hydrography/watershed-boundary-dataset). Before we retrieve any hydrologic information, we need to indicate a HUC8 code and use it to gather snow water equivalent (SWE) observations from SNOTEL sites \n", + "\n", + "✏️ If you have a specific HUC8 in mind, you can change the variable `huc_8_code` below." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c8355563", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HUC-8 ID: 14020001\n", + "HUC-8 name: East-Taylor\n" + ] + } + ], + "source": [ + "# ✏️ Specify HUC8 ID and Name for watershed of interest\n", + "huc_8_code = '14020001' # East-Taylor HUC-8\n", + "print(f'HUC-8 ID: {huc_8_code}')\n", + "\n", + "huc_8_name = 'East-Taylor'\n", + "print(f'HUC-8 name: {huc_8_name}')" + ] + }, + { + "cell_type": "markdown", + "id": "4fafdcad", + "metadata": {}, + "source": [ + "# SUBSET TOOLS - prob not keeping section" + ] + }, + { + "cell_type": "markdown", + "id": "5de02c3b", + "metadata": {}, + "source": [ + "Use the Subsettools function `define_huc_domain()` to get the actual CONUS1 indices associated with the East-Taylor HUC-O8. It returns a tuple `(imin, jmin, imax, jmax)` of grid indices that define a bounding box containing our region (or point) of interest (Note: (imin, jmin, imax, jmax) are the west, south, east and north boundaries of the box respectively) and a mask for that domain." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "965bd6ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(928, 617, 996, 666)\n", + "(49, 68)\n" + ] + }, + { + "data": { + "image/png": "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zJs3L5544omxLmnMPP7443HNNddEfX19FAqF+PWvf93vfEop1qxZE/X19TFlypRYsmRJ7N27d2yKHQOtra1x+eWXR1VVVZx//vnxkY98JPbv39/vmoneo40bN8acOXOiuro6qqurY+HChfHQQw8Vz0/0/pxMa2trFAqFaG5uLo5N9D6tWbMmCoVCv6Ourq54fqL357i//e1v8alPfSrOO++8mDp1alx66aWxc+fO4vmJ3KeSDh+//OUvo7m5OVavXh27du2K97znPbFs2bL461//OtaljYnDhw/H3LlzY8OGDSc9v3bt2li3bl1s2LAhduzYEXV1dbF06dLo7e3NXOnYaG9vj5UrV8aTTz4ZbW1tcfTo0WhqaorDhw8Xr5noPZoxY0bcfvvt8dRTT8VTTz0V73//+2P58uXFX3gTvT+vtmPHjrjzzjtjzpw5/cb1KeId73hHPP/888Vjz549xXP6E3Ho0KG44oorYtKkSfHQQw/Fvn374v/+7//iDW94Q/GaCd2nYX97XAbvete70g033NBv7KKLLkrf+MY3xqii0hER6f777y8+fvnll1NdXV26/fbbi2P/+c9/Uk1NTfrRj340BhWOva6urhQRqb29PaWkR6dyzjnnpJ/85Cf68yq9vb2psbExtbW1pcWLF6ebb745peR9lFJKt912W5o7d+5Jz+nPK77+9a+nK6+88pTnJ3qfSnbm48iRI7Fz585oamrqN97U1BTbt28fo6pK14EDB6Kzs7NfvyorK2Px4sUTtl/d3d0REXHuuedGhB692rFjx2LLli1x+PDhWLhwof68ysqVK+Pqq6+OD3zgA/3G9ekVzzzzTNTX10dDQ0N8/OMfj2effTYi9Oe4Bx98MObPnx8f+9jH4vzzz4/LLrss7rrrruL5id6nkg0f//jHP+LYsWNRW1vbb7y2tjY6OzvHqKrSdbwn+vWKlFLccsstceWVV8bs2bMjQo+O27NnT5x99tlRWVkZN9xwQ9x///1x8cUX688JtmzZEn/4wx+itbV1wDl9iliwYEH87Gc/i0ceeSTuuuuu6OzsjEWLFsU///lP/fmvZ599NjZu3BiNjY3xyCOPxA033BBf/epX42c/+1lEeB9VjHUBgykUCv0ep5QGjPE/+vWKm266KZ5++un43e9+N+DcRO/R2972tti9e3f861//il/96ldx/fXXR3t7e/H8RO9PR0dH3HzzzbF169Y466yzTnndRO7TsmXLiv99ySWXxMKFC+Otb31rbNq0Kd797ndHxMTuT0TEyy+/HPPnz4+WlpaIiLjsssti7969sXHjxvjMZz5TvG6i9qlkZz7e+MY3xutf//oBCbCrq2tAUiSKd5rrV8RXvvKVePDBB+Oxxx6LGTNmFMf16BWTJ0+OCy+8MObPnx+tra0xd+7c+P73v68//7Vz587o6uqKefPmRUVFRVRUVER7e3v84Ac/iIqKimIvJnqfTjRt2rS45JJL4plnnvE++q/p06fHxRdf3G/s7W9/e3HBxETvU8mGj8mTJ8e8efOira2t33hbW1ssWrRojKoqXQ0NDVFXV9evX0eOHIn29vYJ06+UUtx0001x3333xaOPPhoNDQ39zuvRyaWUoq+vT3/+66qrroo9e/bE7t27i8f8+fPjk5/8ZOzevTve8pa36NOr9PX1xR//+MeYPn2699F/XXHFFQOW+v/pT3+KCy64ICL8Pirp1S5btmxJkyZNSj/96U/Tvn37UnNzc5o2bVr6y1/+MtaljYne3t60a9eutGvXrhQRad26dWnXrl3p4MGDKaWUbr/99lRTU5Puu+++tGfPnvSJT3wiTZ8+PfX09Ixx5Xl8+ctfTjU1NWnbtm3p+eefLx4vvPBC8ZqJ3qNVq1alxx9/PB04cCA9/fTT6Zvf/GZ63etel7Zu3ZpS0p9TOXG1S0r69LWvfS1t27YtPfvss+nJJ59MH/7wh1NVVVXxd/NE709KKf3+979PFRUV6bvf/W565pln0s9//vM0derUdO+99xavmch9KunwkVJKP/zhD9MFF1yQJk+enN75zncWl01ORI899liKiAHH9ddfn1J6ZenWbbfdlurq6lJlZWV673vfm/bs2TO2RWd0st5ERLr77ruL10z0Hn3+858v/n1605velK666qpi8EhJf07l1eFjovfp2muvTdOnT0+TJk1K9fX1acWKFWnv3r3F8xO9P8f95je/SbNnz06VlZXpoosuSnfeeWe/8xO5T4WUUhqbORcAYCIq2Xs+AIDxSfgAALISPgCArIQPACAr4QMAyEr4AACyEj4AgKyEDwAgK+EDAMhK+AAAshI+AICshA8AIKv/B/Uxx4GPSUf5AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", + "\n", + "np.save(f'{domain_data_path}domainMask_{huc_8_name}_conus1.npy', mask)\n", + "\n", + "plt.imshow(mask, origin='lower')\n", + "print(ij_bounds)\n", + "print(mask.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "61745fb2", + "metadata": {}, + "source": [ + "Using the domain mask and the i,j PF-CONUS1 indices, we use a hf_hydrodata function to find and save the associated grid cell center lat/lon pair for each grid cell in the domain. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1ff5c1d4", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract bounds\n", + "i_min, j_min, i_max, j_max = ij_bounds\n", + "mask_shape = mask.shape #shape of the subset rectangular domain\n", + "\n", + "# Create i/j index ranges\n", + "i_vals = np.arange(i_min, i_max)\n", + "j_vals = np.arange(j_min, j_max)\n", + "\n", + "# Create full 2D grid (note indexing order carefully)\n", + "jj, ii = np.meshgrid(j_vals, i_vals, indexing=\"ij\")" + ] + }, + { + "cell_type": "markdown", + "id": "c6ae90bf", + "metadata": {}, + "source": [ + "Because the function `hf.to_latlon()` finds the coordinates at the lower left corner of a grid cell, we add 0.5 to each i,j index pair to find the **lat/lon at the grid cell center**." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "13fbc81f", + "metadata": {}, + "outputs": [], + "source": [ + "# Compute grid cell centers\n", + "ii_center = ii + 0.5\n", + "jj_center = jj + 0.5\n", + "\n", + "# Convert to lat/lon (vectorized loop)\n", + "lat = np.zeros(mask_shape)\n", + "lon = np.zeros(mask_shape)\n", + "\n", + "for r in range(mask_shape[0]):\n", + " for c in range(mask_shape[1]):\n", + " lat[r, c], lon[r, c] = hf.to_latlon(\"conus1\",\n", + " ii_center[r, c],\n", + " jj_center[r, c])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "aa20e59f", + "metadata": {}, + "outputs": [], + "source": [ + "# Save 2D arrays of Lat & Lon\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lon_2d.npy\", lon)\n", + "\n", + "# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \n", + "grid_df = pd.DataFrame({\n", + " \"i\": ii.ravel(),\n", + " \"j\": jj.ravel(),\n", + " \"lat\": lat.ravel(),\n", + " \"lon\": lon.ravel(),\n", + "})\n", + "grid_df.to_csv(f\"{domain_data_path}df_{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.csv\", index=False)\n", + "\n", + "# Save a shapefile of the watershed Lat & Lon\n", + "grid_gdf = gpd.GeoDataFrame(\n", + " grid_df,\n", + " geometry=gpd.points_from_xy(grid_df.lon, grid_df.lat),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "# Save the grid points / GeoDataFrame to a shapefile for later use\n", + "grid_gdf.to_file(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.shp\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41e8d63a", + "metadata": {}, + "outputs": [], + "source": [ + "grid_df" + ] + }, + { + "cell_type": "markdown", + "id": "0cecfac1", + "metadata": {}, + "source": [ + "# Get SWE from hydrodata" + ] + }, + { + "cell_type": "markdown", + "id": "84549c32", + "metadata": {}, + "source": [ + "### 3b. Explore the available SWE data in a watershed " + ] + }, + { + "cell_type": "markdown", + "id": "e088705e", + "metadata": {}, + "source": [ + "
\n", + "

📖 Did you know?

\n", + "

The Snow Telemetry (SNOTEL) network, managed by the USDA Natural Resources Conservation Service (NRCS), monitors snowpack conditions across key watersheds in the western United States to support water supply forecasting and climate monitoring. SNOTEL sites are fully automated stations that continuously measure snow water equivalent (SWE), snow depth, precipitation, temperature, and other meteorological variables throughout the year. Unlike manual snow survey programs, SNOTEL provides high-temporal-resolution observations that enable near–real-time assessment of snowpack evolution and interannual variability. These data are widely used for operational forecasting, drought assessment, and long-term climate analysis.

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "de83d6b6", + "metadata": {}, + "source": [ + "Explore what SWE data is available at sites within the HUC ID you specified that operated during WY2004 and WY2005. If you want to check other variables besides SWE, you can change the `variable` argument name. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3aa8210e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
site_idsite_namesite_typeagencystatevariable_nameunitsdatasetvariabletemporal_resolution...latitudelongitudesite_query_urldate_metadata_last_updatedtz_cddoiconus1_iconus1_jconus2_iconus2_j
0380:CO:SNTLButteSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.89435-106.95327https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone942.0650.01372.01601.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.81982-106.58962https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone972.0638.01402.01589.0
\n", + "

2 rows × 24 columns

\n", + "
" + ], + "text/plain": [ + " site_id site_name site_type agency state \\\n", + "0 380:CO:SNTL Butte SNOTEL station NRCS CO \n", + "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO \n", + "\n", + " variable_name units dataset variable \\\n", + "0 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", + "1 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", + "\n", + " temporal_resolution ... latitude longitude \\\n", + "0 daily ... 38.89435 -106.95327 \n", + "1 daily ... 38.81982 -106.58962 \n", + "\n", + " site_query_url \\\n", + "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "\n", + " date_metadata_last_updated tz_cd doi conus1_i conus1_j conus2_i conus2_j \n", + "0 2023-03-07 PST None 942.0 650.0 1372.0 1601.0 \n", + "1 2023-03-07 PST None 972.0 638.0 1402.0 1589.0 \n", + "\n", + "[2 rows x 24 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "avail_df = hf.get_site_variables(variable = \"swe\",\n", + " huc_id = [huc_8_code], grid = 'conus1',\n", + " date_start = StartDate, date_end = EndDate)\n", + "\n", + "# View first five records\n", + "avail_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "268915dc", + "metadata": {}, + "source": [ + "### 3c. Map the SNOTEL stations inside the HUC-08 watershed that have available data in the selected time range \n", + "To note here, we are using pre-loaded shape files for the East-Taylor HUC8, which are located in the `/domain_data/` directory." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "f5c95f67", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sites CRS: EPSG:4326\n", + "Total sites in watershed: 2\n" + ] + } + ], + "source": [ + "### Select station locations that fall within the HUC8 watershed\n", + "\n", + "# Path to the watershed shapefile that was just created\n", + "watershed = f'{domain_data_path}East-Taylor_14020001.shp'\n", + "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", + "\n", + "# Create GeoDataFrame of all available stations\n", + "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", + " avail_df,\n", + " geometry=gpd.points_from_xy(\n", + " avail_df.longitude,\n", + " avail_df.latitude\n", + " ),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", + "\n", + "# Combine watershed polygons into one geometry\n", + "watershed_union = watershed_gdf.geometry.unary_union\n", + "\n", + "# Filter stations that fall within the watershed\n", + "sites_in_watershed = filtered_all_stations_gdf[\n", + " filtered_all_stations_gdf.geometry.within(watershed_union)\n", + "].copy()\n", + "\n", + "sites_in_watershed.reset_index(drop=True, inplace=True)\n", + "\n", + "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "06a6b39b", + "metadata": {}, + "source": [ + "Plot these sites on a map. Then, hover over the pins to see the site names." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "a1e3bc39", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# this may take a moment to load, but it should pop up in a new window\n", + "m = plot_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "354fc021", + "metadata": {}, + "source": [ + "## 4. Retrieve SNOTEL point observations and metadata from HydroData \n", + "Use the `hf.get_point_data()` function to retrieve daily, start-of-day SWE from SNOTEL sites:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d74eeccb", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a folder to save observations\n", + "isExist = os.path.exists(OBS_OutputFolder)\n", + "if isExist == True:\n", + " exit\n", + "else:\n", + " os.mkdir(OBS_OutputFolder)" + ] + }, + { + "cell_type": "markdown", + "id": "b1805ac2", + "metadata": {}, + "source": [ + "### 4a. Get HydroData Observed SWE\n", + "Gather the SNOTEL data for all stations within the watershed and save CSV:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f0f2beb6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
date380:CO:SNTL680:CO:SNTL
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", + "
" + ], + "text/plain": [ + " date 380:CO:SNTL 680:CO:SNTL\n", + "0 2003-10-01 0.0 0.0\n", + "1 2003-10-02 0.0 0.0\n", + "2 2003-10-03 0.0 0.0\n", + "3 2003-10-04 0.0 0.0\n", + "4 2003-10-05 0.0 0.0" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Request point observations data\n", + "data_df = hf.get_point_data(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", + " date_start=StartDate, date_end=EndDate,\n", + " huc_id=[huc_8_code], grid='conus1')\n", + " #polygon=watershed_bbox, polygon_crs=watershed_crs)\n", + "\n", + "# save\n", + "data_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL.csv', index=False)\n", + "\n", + "# Ensure date column is datetime\n", + "data_df[\"date\"] = pd.to_datetime(data_df[\"date\"])\n", + "\n", + "# View first five records\n", + "data_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "fb365fbf", + "metadata": {}, + "source": [ + "### 4b. Get Metadata for HydroData Observed SWE\n", + "Also, retrieve the metadata for the same stations we retrieved SWE observations for:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1cb40a7c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
site_idsite_namesite_typeagencystatelatitudelongitudefirst_date_data_availablelast_date_data_availablerecord_countsite_query_urldate_metadata_last_updatedtz_cddoihuc8conus1_iconus1_jconus2_iconus2_jusda_elevation
0380:CO:SNTLButteSNOTEL stationNRCSCO38.89435-106.953271981-10-012026-03-2116243https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone14020001942.0650.01372.01601.010200.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCO38.81982-106.589621980-08-042026-03-2116666https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone14020001972.0638.01402.01589.09621.0
\n", + "
" + ], + "text/plain": [ + " site_id site_name site_type agency state latitude longitude \\\n", + "0 380:CO:SNTL Butte SNOTEL station NRCS CO 38.89435 -106.95327 \n", + "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO 38.81982 -106.58962 \n", + "\n", + " first_date_data_available last_date_data_available record_count \\\n", + "0 1981-10-01 2026-03-21 16243 \n", + "1 1980-08-04 2026-03-21 16666 \n", + "\n", + " site_query_url \\\n", + "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "\n", + " date_metadata_last_updated tz_cd doi huc8 conus1_i conus1_j \\\n", + "0 2023-03-07 PST None 14020001 942.0 650.0 \n", + "1 2023-03-07 PST None 14020001 972.0 638.0 \n", + "\n", + " conus2_i conus2_j usda_elevation \n", + "0 1372.0 1601.0 10200.0 \n", + "1 1402.0 1589.0 9621.0 " + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Request site-level attributes for these sites\n", + "metadata_df = hf.get_point_metadata(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", + " date_start=StartDate, date_end=EndDate,\n", + " huc_id=['14020001'], grid='conus1')\n", + "\n", + "# save\n", + "metadata_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL_metadata.csv', index=False)\n", + "\n", + "# View first five records\n", + "metadata_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "d17b371a", + "metadata": {}, + "source": [ + "The metadata file is an important addition to the observations and it is recommended to always gather and save this for the observations you are using (particularly to support reproducibility within an open-science workflow). The saved file has useful attributes like site names, first and last date of available data, lat/lon, and the query URL. \n", + "\n", + "Additionally, the metadata contains **ParFlow-CONUS1 and ParFlow-CONUS2 `i,j` indices, which indicate the exact model domain grid cell the observation aligns with**. This is a useful HydroData feature that removes the need for users to manually match station latitude/longitude coordinates to the appropriate model grid cell, as this spatial mapping is handled directly within HydroData. We will use these indices below to extract PF-CONUS1 modeled SWE for each SNOTEL station in the section below. " + ] + }, + { + "cell_type": "markdown", + "id": "0e50455e", + "metadata": {}, + "source": [ + "## 5. Retrieve ParFlow-CONUS1 Modeled Snow Data" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "545a9d22", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a folder to save results\n", + "isExist = os.path.exists(MOD_OutputFolder)\n", + "if isExist == True:\n", + " exit\n", + "else:\n", + " os.mkdir(MOD_OutputFolder)" + ] + }, + { + "cell_type": "markdown", + "id": "56eb4bb4", + "metadata": {}, + "source": [ + "The following section retrieves ParFlow-CONUS1 data for each SNOTEL site within our HUC-08 watershed. The code identifies the CONUS1 `i,j` indices associated with each SNOTEL site, indicated in the `metadata_df`. It then extracts the CONUS1 modeled SWE output for the site and the period of interest, returning the result as a DataFrame. To fairly compare with SNOTEL, which reports SWE once daily at the start of the local day, model output is aggregated by day, using the argment `\"temporal_resolution\": \"daily\"`. Finally, the processed data is saved as a CSV file for each site. \n", + "\n", + "### 5a. ParFlow CONUS1 Model Dataset Information\n", + "We can print some information about the model output dataset by using the `hf.get_catalog_entry()` to get the CONUS1 model dataset metadata. " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "10647da1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '12', 'dataset': 'conus1_baseline_mod', 'dataset_version': '', 'file_type': 'pfb', 'variable': 'swe', 'dataset_var': 'swe', 'temporal_resolution': 'daily', 'units': 'mm', 'aggregation': 'eod', 'grid': 'conus1', 'path': 'swe.daily.eod.{wy_daynum:03d}.pfb', 'file_grouping': 'wy_daynum', 'entry_start_date': None, 'entry_end_date': None, 'documentation_notes': '', 'site_type': '', 'variable_type': 'surface_water', 'has_z': '', 'dataset_type': 'parflow', 'datasource': 'hydroframe', 'paper_dois': '10.5194/gmd-14-7223-2021', 'dataset_dois': '', 'dataset_start_date': '2002-10-01', 'dataset_end_date': '2006-09-30', 'structure_type': 'gridded', 'has_ensemble': '', 'unit_type': 'length', 'period': 'daily'}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conus1_options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\"\n", + "}\n", + "hf.get_catalog_entry(conus1_options)" + ] + }, + { + "cell_type": "markdown", + "id": "c6fd1306", + "metadata": {}, + "source": [ + "Before we gather model outputs at the specific SNOTEL sites, we can visualize SWE across our HUC-08. This is plotted for one day at 1km lateral resolution." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "ba48a33a", + "metadata": {}, + "outputs": [], + "source": [ + "# retrieve gridded PF-CONUS1 SWE for the entire HUC8 watershed\n", + "grid_swe_options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\",\n", + " \"temporal_resolution\": \"daily\",\n", + " \"start_time\": '2004-04-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", + " \"end_time\": '2004-04-02',\n", + " \"huc_id\": huc_8_code\n", + " }\n", + " \n", + " # Get gridded data\n", + "grid_swe = hf.get_gridded_data(grid_swe_options)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "b82b9574", + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Image [x,y] (SWE)" + ] + }, + "execution_count": 20, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1011" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "grid_swe_map = xr.DataArray(grid_swe[0], dims=(\"y\", \"x\"), name=\"SWE\")\n", + "grid_swe_map.hvplot.image(cmap=\"YlGnBu\", colorbar=True, aspect=\"equal\", title=f\"{huc_8_name} Gridded SWE on 2004-04-01\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "73a13787", + "metadata": {}, + "source": [ + "Now, retrieve the PF-CONUS1 modeled SWE from the SNOTEL site locations. Here we use the CONUS1 i and j indices from the `metadata_df` and grab the SWE from those grid cells. \n", + "\n", + "First, create a copy of the model dataframe (`model_df`) so we have the same data structure:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "17143151", + "metadata": {}, + "outputs": [], + "source": [ + "# Copy data_df to model_df so we have the same timestamps and site_id structure\n", + "model_df = data_df.copy()\n", + "\n", + "# Set all non-date columns to NaN to prepare for filling in model data\n", + "non_date_cols = model_df.columns.difference([\"date\"])\n", + "model_df[non_date_cols] = np.nan\n", + "\n", + "# Rename site_id columns for PF outputs \n", + "model_df.columns = [\n", + " col if col == \"date\" else col.replace(\":SNTL\", \"\") + \":PFCONUS1\"\n", + " for col in model_df.columns\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "523bd35c", + "metadata": {}, + "source": [ + "Use the function `hf.get_gridded_data()` and PF-CONUS1 `i,j` indices to select the SWE output for the correct location and time period: " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "a814204c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
date380:CO:PFCONUS1680:CO:PFCONUS1
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", + "
" + ], + "text/plain": [ + " date 380:CO:PFCONUS1 680:CO:PFCONUS1\n", + "0 2003-10-01 0.0 0.0\n", + "1 2003-10-02 0.0 0.0\n", + "2 2003-10-03 0.0 0.0\n", + "3 2003-10-04 0.0 0.0\n", + "4 2003-10-05 0.0 0.0" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Loop over each station in metadata_df\n", + "for idx, row in metadata_df.iterrows():\n", + " site_id = row[\"site_id\"] # original SNTL site_id\n", + " col_name = site_id.replace(\":SNTL\", \"\") + \":PFCONUS1\" # corresponding column in model_df\n", + " conus_i = int(row[\"conus1_i\"])\n", + " conus_j = int(row[\"conus1_j\"])\n", + " \n", + " # Build options dict for this station\n", + " options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\",\n", + " \"temporal_resolution\": \"daily\",\n", + " \"start_time\": '2003-10-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", + " \"end_time\": '2005-10-01',\n", + " \"grid_point\": [conus_i, conus_j]\n", + " }\n", + " \n", + " # Get gridded data\n", + " data = hf.get_gridded_data(options)\n", + " \n", + " # Fill column in model_df\n", + " # Convert to numeric in case hf returns lists or other types\n", + " model_df[col_name] = np.squeeze(np.array(data))\n", + "\n", + "# Ensure date column is datetime\n", + "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", + "\n", + "# Save\n", + "model_df.to_csv(f'./{MOD_OutputFolder}/df_{huc_8_name}_{huc_8_code}_PFCONUS1.csv', index=False)\n", + " \n", + "model_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "7464828b", + "metadata": {}, + "source": [ + "## 6. Quick plot sanity check \n", + "Plot a simple timeseries of modeled and observed SWE to make sure our data retrieval was successful. " + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "fbe43f6a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAoFxJREFUeJzs3Xd8XXXh//HXuTf3Zu/ZtEn3pC100ZbVAoWyRRBQEESqomxBQeSnwleWoIAWAUWgyCqyVASBMlosZbSF0r1X2ibN3uMm957fHye5oxlN0pvcm+T9fDz66Oesez/JSW7u+36WYZqmiYiIiIiIiIgEnS3UFRARERERERHprxS6RURERERERHqIQreIiIiIiIhID1HoFhEREREREekhCt0iIiIiIiIiPUShW0RERERERKSHKHSLiIiIiIiI9BCFbhEREREREZEeEhHqCoQDj8fDgQMHiI+PxzCMUFdHREREREREwpxpmlRVVZGdnY3N1n57tkI3cODAAXJyckJdDREREREREelj8vLyGDJkSLvHFbqB+Ph4wPpmJSQkBBzzeDwUFRWRnp7e4acXEhq6P+FL9yb86R6FP92j8Kb7E550X8Kf7lF40/3pvMrKSnJycrx5sj0K3eDtUp6QkNBm6K6vrychIUE/dGFI9yd86d6EP92j8Kd7FN50f8KT7kv40z0Kb7o/XXe4Icr6LoqIiIiIiIj0EIVuERERERERkR6i0C0iIiIiIiLSQzSmuwvcbjeNjY2hrob48Xg8NDY2Ul9f36kxJw6HA7vd3gs1ExERERERUejuFNM0KSgooKKiItRVkUOYponH46GqqqrTa6wnJSWRlZWlNdlFRERERKTHKXR3QnV1NU1NTWRkZBATE6OwFkZM06SpqYmIiIjD3hfTNKmtraWwsBCAQYMG9UYVRURERERkAFPoPgy32019fT2DBg0iNTU11NWRQ3QldANER0cDUFhYSEZGhrqai4iIiIhIj9JEaofR2NiIYRjExMSEuioSJC33UuPzRURERESkpyl0d5K6lPcfupciIiIiItJbFLpFREREREREeohCt3Tb0qVLMQyD8vLyTl8zbNgwHnnkkSN63jvvvJNjjjnmiB5DREREJOw01sNnT8Cuj0NdExEJIoXufuzKK6/EMAx+/OMftzp2zTXXYBgGV155Ze9XTERERERaW3Y/vHMb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RMTdgyAiIiLXWrx4MWpra7F169Z28xYuXIijR4/i2LFj7eb985//xOOPP46amhoAV7/T/dRTT6G2tta+TGlpKe644w6cPXsWVqvVPn3atGm466678Nprr/X6/SEiItIKL3cPgIiIiNxLRKDT6QAAOTk5eO2113Ds2DHU19ejtbUVV65cwc8//wyz2dzh7fPy8iAiGDZsmMP0pqYmDBw40OXjJyIiUjM23URERH3c8ePHERUVhfLyctx3331ITU3Fn//8ZwQGBmL//v1ISUlBS0uL09srigKDwYDc3FwYDAaHef7+/q4ePhERkaqx6SYiIurDdu/ejcLCQjz99NM4cuQIWltb8fbbb0Ovv3ral08//dRheaPRCJvN5jBt7NixsNlsqK6uxsSJE2/Z2ImIiLSATTcREVEf0dTUhKqqKthsNpw/fx5fffUV0tPTMXPmTCxatAiFhYVobW3FunXrMGvWLBw4cADr1693WMeQIUPQ2NiIb775BnFxcfDz88OwYcPw29/+FosWLcLbb7+NsWPHoqamBrt378bo0aNx3333uekeExERuR/PXk5ERNRHfPXVVwgLC8OQIUOQnJyMnJwcrF27Ftu2bYPBYMCYMWOwatUqvPHGGxg1ahQ2bdqE9PR0h3VMmDABqampWLBgAYKDg/Hmm28CADZu3IhFixbh2WefRUxMDGbPno3vvvsOERER7rirREREqsGzlxMRERERERG5CN/pJiIiIiIiInIRNt1ERERERERELsKmm4iIiIiIiMhF2HQTERERERERuQibbiIiIiIiIiIXYdNNRERERERE5CJsuomIiIiIiIhchE03ERERERERkYuw6SYiIiIiIiJyETbdRERERERERC7CppuIiIiIiIjIRdh0ExEREREREbnI/wOi5/NV6KggEAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "\n", + "ax.plot(data_df[\"date\"], model_df[\"380:CO:PFCONUS1\"], label=\"Modeled\", linewidth=2)\n", + "\n", + "ax.plot(data_df[\"date\"], data_df[\"380:CO:SNTL\"], label=\"Observed\", linewidth=2)\n", + "\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"SWE (mm)\")\n", + "\n", + "# Date formatting for x-axis\n", + "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", + "ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))\n", + "\n", + "ax.legend(loc='upper left')\n", + "ax.grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "da3df109", + "metadata": {}, + "source": [ + "# Start of comparison from old notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Spatial Mapping of the SNOTEL sites \n", + "Before evaluating model performance, we plot the GIS data associated with the records in the combined DataFrame. The map below shows the SNOTEL stations included in the evaluation dataset, along with the watershed boundary used for the model simulations. Hover over the pins to see the site names. \n", + "\n", + "We also print a table of the SNOTEL site metadata to help with the single site selection." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sites CRS: EPSG:4326\n", + "Total sites in watershed: 2\n" + ] + }, + { + "data": { + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Path to the watershed shapefile\n", + "watershed = f\"{domain_data_path}/East-Taylor_14020001.shp\"\n", + "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", + "\n", + "# Create GeoDataFrame of all available stations\n", + "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", + " metadata_df,\n", + " geometry=gpd.points_from_xy(\n", + " metadata_df.longitude,\n", + " metadata_df.latitude\n", + " ),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", + "\n", + "# Combine watershed polygons into one geometry\n", + "watershed_union = watershed_gdf.geometry.unary_union\n", + "\n", + "# Filter stations that fall within the watershed\n", + "sites_in_watershed = filtered_all_stations_gdf[\n", + " filtered_all_stations_gdf.geometry.within(watershed_union)\n", + "].copy()\n", + "\n", + "sites_in_watershed.reset_index(drop=True, inplace=True)\n", + "\n", + "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", + "\n", + "m = plot_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sites_in_watershed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Compare Modeled and Observed SWE Timeseries at a Single Site\n", + "\n", + "Once we have both observation data and modeling outpus, it's important to evaluate how well the model reproduces observed data. The following plots are simple timeseries comparisons of **modeled vs. observed** SWE. These types of plots provide a straight-forward visual of how well the observations and simulations agree and are a great start for assessing general model performance. \n", + "\n", + "📊 We include two figures:\n", + "\n", + "1. **Time Series Overlay:** Plots the observed and modeled values together over time. This helps identify:\n", + " - Periods of systematic bias\n", + " - Timing differences in peaks and lows\n", + " - General agreement in trends\n", + "\n", + "2. **Scatter Plot with 1:1 Line:** Plots each modeled value against its corresponding observed value. This highlights:\n", + " - Accuracy across the full range of values\n", + " - Over- or under-prediction patterns\n", + " - Outliers or extreme events" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Review the sites within the watershed from the interactive map above and click on the markers to view the site name and code. Recall, we also printed out the site metadata for all sites within the watershed, which contains the 3-letter site codes.\n", + "\n", + "✏️ Once you’ve identified the site of interest, **enter its site code in the next code cell for `my_site_code`**: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# choose a site of interest within the watershed\n", + "my_site_code = '380:CO:'\n", + "\n", + "# make sure date columns are datetime and set as index for easier plotting and metric calculations\n", + "data_df[\"date\"] = pd.to_datetime(data_df[\"date\"])\n", + "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", + "\n", + "data_df = data_df.set_index(\"date\")\n", + "model_df = model_df.set_index(\"date\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c1f5648", + "metadata": {}, + "outputs": [], + "source": [ + "plot_utils.comparison_plots(data_df, model_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1', site_label=None)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To move beyond an overall summary of daily performance, we replot the modeled vs. observed SWE scatter while highlighting specific months with a distinct color. This gives us more information about the **seasonal model performance**. \n", + "\n", + "Let's customize the scatter plot by allowing you to highlight specific months with a distinct color. The selected months will appear in one color, while all other months will appear in a different color. This customization reveals whether there are **seasonal patterns** in the relationship between observed and modeled SWE, allowing us to distinguish model behavior during the key snowpack phases of accumulation and ablation (melt). Identifying these patterns is important for diagnosing the model’s strengths and limitations during different parts of the snow season.\n", + "\n", + "You can change the list of highlighted months (for example, October–December for early accumulation or March–May for spring melt) to explore in the scatter plot how model performance varies across different parts of the snow season. This seasonal perspective motivates the _peak SWE analysis_ that follows." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 For this example, let's highlight the _early snow accumulation period_ of October - January:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_df['month'] = data_df.index.month\n", + "model_df['month'] = model_df.index.month\n", + "\n", + "plot = plot_utils.plot_custom_scatter_SWE(\n", + " data_df,\n", + " model_df,\n", + " f\"{my_site_code}SNTL\",\n", + " f\"{my_site_code}PFCONUS1\",\n", + " site_label=my_site_code,\n", + " highlight_months=[10, 11, 12, 1],\n", + ")\n", + "\n", + "plot" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

What does this plot tell us about how well the model performs during the early snow accumulation period at this site?
\n", + "HINT: How close are the green points to the 1:1 line?

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Peak SWE Evaluation at the Watershed Scale \n", + "As we saw in the previous section, how well a model matches observations can differ greatly throughout the year. The following section focuses on **peak SWE** (or maximum SWE) analysis. \n", + "\n", + "**Peak SWE is a key diagnostic for snow-dominated hydrologic systems** because it represents the maximum amount of liquid water stored in the snowpack before the spring melt. Evaluating both the magnitude (quantity) and timing (date) of peak SWE provides insight into whether the model is accurately representing snow accumulation and seasonal energy balance. \n", + "\n", + "Errors in peak SWE can have important hydrologic consequences, as peak accumulation strongly influences:\n", + "- The volume of water available for spring runoff\n", + "- The timing of streamflow peaks\n", + "- Soil moisture recharge and groundwater contributions\n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "_Example daily SWE at a single site, showing two important periods in snow processes: accumulation (before peak) and ablation (after peak). The vertical line marks peak SWE._" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.1 Comparing Modeled and Observed Peak SWE at All Sites in the Watershed\n", + "In this section, we evaluate observed and modeled peak SWE for all stations within our watershed and for all years selected in the `StartDate` and `EndDate` above. \n", + "\n", + "#### 📋 Modeled SWE on the Date of Observed Peak SWE (magnitude) \n", + "This comparison evaluates the modeled SWE on the **specific day when observed SWE reaches its maximum.** By fixing the timing to the observed peak, this comparison isolates errors in SWE magnitude. \n", + "It answers the question: *How much SWE does the model simulate on the day the observed snowpack reaches its maximum?*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# isolate the columns associated with observations and model predictions.\n", + "# these will be inputs to our same-day comparison function.\n", + "obs_cols = sorted([col for col in combined_df.columns if col.endswith('SNTL')])\n", + "mod_cols = sorted([col for col in combined_df.columns if col.endswith('PFCONUS1')])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# compute the same-day SWE comparison during the observed peak SWE for each of the observation and modeled sites.\n", + "df_observed_peak = utils.modeled_swe_at_observed_peak(combined_df, obs_cols, mod_cols)\n", + "df_observed_peak" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 Visualize the amount of SWE on **the day of observed peak SWE occurs** for both the model and observations at each station" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Rearrange the dataframe to long format for easier plotting\n", + "df_long = (\n", + " df_observed_peak\n", + " .reset_index() \n", + " .melt(\n", + " id_vars=['Station', 'Water_Year', 'date'],\n", + " value_vars=['Observed', 'Modeled'],\n", + " var_name='Source',\n", + " value_name='SWE'\n", + " )\n", + ")\n", + "# Create scatter plot of observed and modeled SWE on the day of observed peak SWE\n", + "scatter_obs_peak = df_long.hvplot.scatter(\n", + " x='Station',\n", + " y='SWE',\n", + " by='Source', # Observed vs Modeled\n", + " ylabel='SWE on Observed Peak Day (mm)',\n", + " title='Observed and Modeled SWE on the Day of Observed Peak SWE',\n", + " size=70,\n", + " width=700,\n", + " height=450,\n", + " alpha=0.8,\n", + " hover_cols=['Water_Year'],\n", + " rot=45\n", + ")\n", + "\n", + "# Customize the scatter plot appearance\n", + "scatter_by_station = (\n", + " scatter_obs_peak \n", + " .opts(legend_position='top_right')\n", + ")\n", + "\n", + "scatter_by_station" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 📋 Modeled vs Observed Peak SWE Comparison (timing & magnitude) \n", + "This comparison evaluates the modeled and observed peak SWE values and their corresponding dates independently. Unlike the previous comparison that fixed the timing to the observed peak swe, this analysis shows the actual days of modeled and observed peak SWE, which may occur on different dates. As a result, it captures errors in both **peak SWE magnitude** and **peak timing**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# compute the different-day SWE comparison for each of the observed and modeled sites.\n", + "df_both_peak = utils.modeled_vs_observed_peak_swe(combined_df, obs_cols, mod_cols)\n", + "df_both_peak" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 Visualize the quantity of peak SWE for both the model and observations at each station" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "### NEED TO DECIDE HOW TO FORMAT THIS PLOT AND IF WE WANT TO HAVE THE \"SAME_DAY\" PLOT\n", + "\n", + "# Create the scatter plot\n", + "scatter_plot_both_peak = df_both_peak.hvplot.scatter(\n", + " x='Observed',\n", + " y='Modeled',\n", + " xlabel='Observed SWE (mm)',\n", + " ylabel='Modeled SWE (mm)',\n", + " title='Modeled vs. Observed Peak SWE',\n", + " size=35,\n", + " width=500,\n", + " height=400,\n", + " color='#E69F00',\n", + " hover_cols=['Station', 'Water_Year']\n", + ")#.relabel('Peak SWE')\n", + "\n", + "# Add 1:1 line (perfect match line)\n", + "swe_max = df_both_peak[['Observed', 'Modeled']].max().max()\n", + "\n", + "one_to_one_line = hv.Curve(([0, swe_max], [0, swe_max])).opts(\n", + " color='gray',\n", + " line_dash='dashed',\n", + " line_width=1,\n", + ").relabel('1:1 Line')\n", + "\n", + "# Combine scatter plot and 1:1 line into an Overlay\n", + "scatter_with_line = (scatter_plot_both_peak * one_to_one_line).opts( #scatter_plot_obs_peak * \n", + " legend_position='bottom_right'\n", + ")\n", + "\n", + "scatter_with_line" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 Visualizing Model Error for Peak SWE\n", + "\n", + "The previous scatter plots indicate that the modeled and observed peak SWE magnitude and timing don't always align. Next, we plot the degree to which \n", + "\n", + "The previous scatter plots highlight differences between modeled and observed peak SWE timing and magnitude, but interpreting these variations can be challenging when comparing modeled and observed values directly. To make these differences more explicit, we compute errors in both peak timing and peak SWE magnitude and visualize them directly. This approach clarifies both the direction and magnitude of model bias and facilitates comparison across stations and water years.\n", + "\n", + "First, add columns `Peak_Date_Diff_Days` and `Peak_SWE_Diff` to the DataFrame `df_both_peak` for computed difference in peak SWE date difference and peak SWE quantity between modeled and observed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute the difference in peak SWE days and peak SWE amounts between modeled and observed\n", + "df_both_peak['Peak_Date_Diff_Days'] = (df_both_peak['Modeled_Date'] - \n", + " df_both_peak['Observed_Date']).dt.days\n", + "df_both_peak['Peak_SWE_Diff'] = (df_both_peak['Modeled'] - \n", + " df_both_peak['Observed'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df02795e", + "metadata": {}, + "outputs": [], + "source": [ + "df_both_peak" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 Visualize the error between the modeled and observed peak SWE " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter to separate each water year\n", + "year1 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].min()]\n", + "year2 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].max()]\n", + "\n", + "bar1 = year1.hvplot.bar(\n", + " x='Station',\n", + " y='Peak_Date_Diff_Days',\n", + " rot=45,\n", + " ylabel='Date Difference (days)',\n", + " title=f'Peak SWE Date Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", + " width=400,\n", + " height=400,\n", + " color='Peak_Date_Diff_Days',\n", + " hover_cols=['Modeled', 'Observed']\n", + ")\n", + "bar2 = year1.hvplot.bar(\n", + " x='Station',\n", + " y='Peak_SWE_Diff',\n", + " rot=45,\n", + " ylabel='SWE Difference (m)',\n", + " title=f'Peak SWE Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", + " width=400,\n", + " height=400,\n", + " color='Peak_SWE_Diff',\n", + " hover_cols=['Modeled', 'Observed']\n", + ")\n", + "\n", + "# Combine side by side\n", + "layout = (bar1 + bar2)\n", + "layout.opts(shared_axes=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The left panel shows the timing error (date difference) and the right panel the magnitude error (SWE difference). When we computed the difference in date and SWE quantity above, we took `modeled - observed` so: \n", + "\n", + "| | DATE OF PEAK SWE | PEAK SWE QUANTITY |\n", + "|---|---|---|\n", + "| + Positive Values | modeled AFTER observed | modeled GREATER THAN observed |\n", + "| - Negative Values | modeled BEFORE observed | modeled LESS THAN observed | " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE both earlier and less than the observed peak SWE? Perhaps try changing the my_site_code from earlier in the notebook to rerun nwm_utils.comparison_plots() to see the timeseries for a different station to look at the peak magnitude and timing. \n", + "\n", + "
What happens if you change the year that is plotted?
✏️ Try modifying the bar plot code from bar1 = year1.hvplot.bar to bar1 = year2.hvplot.bar. Don't forget to change the title!

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 📊 Next, we combine the timing and magnitude errors and plot them together for each station." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "scatter = df_both_peak.hvplot.scatter(\n", + " x='Peak_Date_Diff_Days',\n", + " y='Peak_SWE_Diff',\n", + " by='Station', # Water_Year\n", + " xlabel='Peak SWE Timing Error (days)',\n", + " ylabel='Peak SWE Magnitude Error (mm)',\n", + " title='Peak SWE Timing vs Magnitude Error',\n", + " size=80,\n", + " width=600,\n", + " height=400,\n", + " hover_cols=['Water_Year']\n", + ")\n", + "\n", + "# Add reference lines\n", + "vline = hv.VLine(0).opts(color='gray', line_dash='dashed')\n", + "hline = hv.HLine(0).opts(color='gray', line_dash='dashed')\n", + "\n", + "(scatter * vline * hline).opts(legend_position='top_left', show_grid=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "✏️ **Try changing how we view this plot.** \n", + "We can modify a line in the section of code from `by='Station'` to `by='Water_Year'` to better visualize the errors in the different Water Years. \n", + "Are there any patterns that jump out? Which year was modeled peak SWE consistently less than observed peak SWE? " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Compute and Statistics and Error Metrics \n", + "The previous section visualized when and where modeled SWE differs from observations, both in terms of peak SWE timing and magnitude. However, visual inspection alone makes it difficult to compare performance across sites or to summarize model behavior in a consistent or quantifiable way. In this section, we compute commonly used statistical error metrics to quantify model performance, allowing us to objectively assess bias, error magnitude, and variability for sites within the watershed. \n", + "\n", + "Proposed outline (DTK, Jan 2026):\n", + "- Summary metrics at a station\n", + "- Summary metrics at all stations within the watershed\n", + "- Combined timing and magnitude for all stations within the watershed (Condon metric)\n", + "- Focus on timing: summary statistics for single station for accumulation & ablation periods (using the new wrapper: `nwm_utils.compute_stats_period()`)\n", + "- Melt period statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "nwm_utils.compute_stats(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pearson and Spearman correlations are both close to 1, suggesting a strong relationship between observed and modeled SWE. As shown on the timeseries plot, this strong correlation alone does not indicate a \"good\" model. For example, it does not guarantee accurate timing of key events, such as peak SWE or melt onset. Let's compare these as well. The following code uses `report_max_dates_and_values` function to identify the peak SWE value and the date it occurs for both the observed (CCSS) and modeled (NWM) datasets. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why? As you review the results, compare the peak flow amounts and the timing of snowmelt onset. Do you see any significant differences? Which dataset indicates an earlier melt?

\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "summary_table = nwm_utils.report_max_dates_and_values(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n", + "summary_table" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Summary Metrics at Multiple Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "site_codes = ['DAN', 'HRS', 'KIB', 'PDS', 'SLI', 'TUM', 'WHW']\n", + "\n", + "rows = []\n", + "\n", + "for site in site_codes:\n", + " obs_col = f'CCSS_{site}_swe_m'\n", + " mod_col = f'NWM_{site}_swe_m'\n", + "\n", + " stats_table = nwm_utils.compute_stats(combined_df, obs_col, mod_col)\n", + "\n", + " rows.append({\n", + " 'Station': site,\n", + " 'Mean_Obs': stats_table.loc['observed', 'Mean'],\n", + " 'Mean_Mod': stats_table.loc['modeled', 'Mean'],\n", + " 'Bias_m': stats_table.loc['Bias (Modeled - Observed)', 'Mean'],\n", + " 'Pearson_r': stats_table.loc['Pearson Correlation', 'Mean'],\n", + " 'Spearman_r': stats_table.loc['Spearman Correlation', 'Mean'],\n", + " 'NSE': stats_table.loc['Nash-Sutcliffe Efficiency (NSE)', 'Mean'],\n", + " 'KGE': stats_table.loc['Kling-Gupta Efficiency (KGE)', 'Mean']\n", + " })\n", + "\n", + "stats_AllStations = pd.DataFrame(rows)\n", + "\n", + "print('All Stations Statistics Summary:')\n", + "stats_AllStations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats_AllStations.hvplot.bar(\n", + " x='Station',\n", + " y='NSE',\n", + " rot=45,\n", + " ylabel='Nash–Sutcliffe Efficiency',\n", + " title='NSE by Station',\n", + " height=400,\n", + " width=600,\n", + " bar_width=0.5\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats_summary.hvplot.scatter(\n", + " x='Station',\n", + " y='Bias_m',\n", + " size=100,\n", + " rot=45,\n", + " ylabel='Bias (m)',\n", + " title='Mean SWE Bias by Station'\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Combine Magnitude (absolute relative bias) and Timing (Spearman's rho) metrics using the Condon metric (and with all stations, a Condon diagram)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bias1 = evaluation_metrics.bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", + "bias1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "abs_bias = evaluation_metrics.absolute_relative_bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", + "abs_bias" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "srho = evaluation_metrics.spearman_rank(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", + "srho" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "evaluation_metrics.condon(abs_bias, srho)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

\n", + " What is the modeled SWE on the date when the observed SWE reaches its peak?
\n", + " ✏️ Use the code snippet below to find the answer.\n", + "

\n", + "
\n",
+    "  \n",
+    "    # Find date of the peak SWE from observed data\n",
+    "    date_obs_max = combined_df['CCSS_HRS_swe_m'].idxmax()\n",
+    "\n",
+    "    # Get corresponding value of modeled data on that date\n",
+    "    value_mod_at_max_obs = combined_df.loc[date_obs_max, 'NWM_HRS_swe_m']\n",
+    "  
\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Focus on Timing: Melt Period Metrics\n", + "Compare the average melt rate over the full melt period. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following function computes the melt period length by identifying the first date after the peak SWE when SWE drops to zero and remains at zero for at least (`min_zero_days`) consecutive days. This is used to define the end of the melt period. Finally, the function calculates the average melt rate, which represents the rate at which snow disappeared, expressed in meters per day, over the full melt period." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "melt_stats_df = utils.compute_melt_period_statistics(combined_df)\n", + "melt_stats_df.head()\n", + "melt_stats_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "observed_melt_period = nwm_utils.compute_melt_period(combined_df[f'CCSS_{my_site_code}_swe_m'])\n", + "observed_melt_period" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "modeled_melt_period = nwm_utils.compute_melt_period(combined_df[f'NWM_{my_site_code}_swe_m'])\n", + "modeled_melt_period" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "accum_months = [10, 11, 12, 1, 2, 3]\n", + "ablation_months = [4, 5, 6]\n", + "\n", + "accum_stats = nwm_utils.compute_stats_period(\n", + " combined_df,\n", + " f'CCSS_{my_site_code}_swe_m',\n", + " f'NWM_{my_site_code}_swe_m',\n", + " accum_months\n", + ")\n", + "\n", + "accum_stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "ablation_stats = nwm_utils.compute_stats_period(\n", + " combined_df,\n", + " f'CCSS_{my_site_code}_swe_m',\n", + " f'NWM_{my_site_code}_swe_m',\n", + " ablation_months\n", + ")\n", + "\n", + "ablation_stats" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

\n", + " If you recall from earlier, we plotted the timeseries of out selected station. Replot it below. Do the metrics make sense given the visual comparison between modeled and observed? For example, when you look at the timeseries, is the model consistently predicting SWE to be higher or lower than observations? Does this align with the Bias sign (+ or -)?\n", + "

\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nwm_utils.comparison_plots(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "nwm_env", + "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 +} diff --git a/src/cssi_evaluation/utils/plot_utils.py b/src/cssi_evaluation/utils/plot_utils.py index c4fd9b0..6e6b483 100644 --- a/src/cssi_evaluation/utils/plot_utils.py +++ b/src/cssi_evaluation/utils/plot_utils.py @@ -589,24 +589,120 @@ def find_column(columns, candidates): return m -def comparison_plots(df, ts_obs, ts_mod): +# def comparison_plots(df, ts_obs, ts_mod): +# """ +# Create a set of comparison plots (timeseries overlay and scatter plot with 1:1 line) + +# Parameters: +# df: dataframe with combined observed and modeled timeseries for each site +# ts_obs (str): column heading for observed timeseries in df +# ts_mod (str): column heading for modeled timeseries in df +# """ + +# df = df.copy() +# df.index.name = ( +# "date" # change the index name to "Date" for better hover tooltip display +# ) + +# # Timeseries plot (Overlay) +# observed_plot = df.hvplot.line( +# y=ts_obs, +# ylabel="Snow Water Equivalent (mm)", +# xlabel="", +# label="Observed SWE", +# color="blue", +# line_width=2, +# width=500, +# height=400, +# ) + +# modeled_plot = df.hvplot.line( +# y=ts_mod, +# ylabel="Snow Water Equivalent (mm)", +# xlabel="", +# label="Modeled SWE", +# color="orchid", +# line_width=2, +# width=500, +# height=400, +# ) + +# # Overlay (combines both lines into a single visual object) +# timeseries_plot = (observed_plot * modeled_plot).opts( +# title="Observed vs Modeled SWE\nDaily Time Series", +# legend_position="top_right", +# ) + +# # Scatter plot +# scatter_plot = df.hvplot.scatter( +# x=ts_obs, +# y=ts_mod, +# xlabel="Observed SWE (mm)", +# ylabel="Modeled SWE (mm)", +# color="black", +# width=500, +# height=400, +# size=15, +# hover_cols=["date"], # This will add the date (index) to hover tooltip +# ) + +# # Add 1:1 line (perfect match line) +# swe_max = max(df[ts_obs].max(), df[ts_mod].max()) +# one_to_one_line = ( +# hv.Curve(([0, swe_max], [0, swe_max])) +# .opts( +# color="gray", +# line_dash="solid", +# line_width=1, +# ) +# .relabel("1:1 Line") +# ) # This is the correct way to set a label for a Curve + +# # Combine scatter plot and 1:1 line into an Overlay +# scatter_with_line = (scatter_plot * one_to_one_line).opts( +# title="Observed vs Modeled SWE\nScatter with 1:1 Line", +# legend_position="bottom_right", +# ) + +# # Combine both into a 1-row, 2-column layout +# layout = (timeseries_plot + scatter_with_line).opts(shared_axes=False) + +# return layout + +def comparison_plots(df_obs, df_mod, obs_col, mod_col, site_label=None): """ - Create a set of comparison plots (timeseries overlay and scatter plot with 1:1 line) + Create comparison plots (timeseries + scatter) for a given site + using separate observed and modeled dataframes with different column names. Parameters: - df: dataframe with combined observed and modeled timeseries for each site - ts_obs (str): column heading for observed timeseries in df - ts_mod (str): column heading for modeled timeseries in df + df_obs : DataFrame + Observations dataframe + df_mod : DataFrame + Modeled dataframe + obs_col : str + Column name in df_obs (e.g., '360:CO:SNTL') + mod_col : str + Column name in df_mod (e.g., '360:CO:PFCONUS1') + site_label : str, optional + Clean name for titles (e.g., '360:CO') """ - df = df.copy() - df.index.name = ( - "date" # change the index name to "Date" for better hover tooltip display + # --- Combine + align --- + df = ( + df_obs[[obs_col]] + .rename(columns={obs_col: "observed"}) + .join(df_mod[[mod_col]].rename(columns={mod_col: "modeled"}), how="inner") + .dropna() ) - # Timeseries plot (Overlay) + df.index.name = "date" + + # Clean label for plotting + label = site_label if site_label else obs_col + + # --- Timeseries plot --- observed_plot = df.hvplot.line( - y=ts_obs, + y="observed", ylabel="Snow Water Equivalent (mm)", xlabel="", label="Observed SWE", @@ -617,7 +713,7 @@ def comparison_plots(df, ts_obs, ts_mod): ) modeled_plot = df.hvplot.line( - y=ts_mod, + y="modeled", ylabel="Snow Water Equivalent (mm)", xlabel="", label="Modeled SWE", @@ -627,54 +723,117 @@ def comparison_plots(df, ts_obs, ts_mod): height=400, ) - # Overlay (combines both lines into a single visual object) timeseries_plot = (observed_plot * modeled_plot).opts( - title="Observed vs Modeled SWE\nDaily Time Series", + title=f"{label}: Observed vs Modeled SWE\nDaily Time Series", legend_position="top_right", ) - # Scatter plot + # --- Scatter plot --- scatter_plot = df.hvplot.scatter( - x=ts_obs, - y=ts_mod, + x="observed", + y="modeled", xlabel="Observed SWE (mm)", ylabel="Modeled SWE (mm)", color="black", width=500, height=400, size=15, - hover_cols=["date"], # This will add the date (index) to hover tooltip + hover_cols=["date"], ) - # Add 1:1 line (perfect match line) - swe_max = max(df[ts_obs].max(), df[ts_mod].max()) + # --- 1:1 line --- + swe_max = max(df["observed"].max(), df["modeled"].max()) + one_to_one_line = ( hv.Curve(([0, swe_max], [0, swe_max])) - .opts( - color="gray", - line_dash="solid", - line_width=1, - ) + .opts(color="gray", line_width=1) .relabel("1:1 Line") - ) # This is the correct way to set a label for a Curve + ) - # Combine scatter plot and 1:1 line into an Overlay scatter_with_line = (scatter_plot * one_to_one_line).opts( - title="Observed vs Modeled SWE\nScatter with 1:1 Line", + title=f"{label}: Observed vs Modeled SWE\nScatter with 1:1 Line", legend_position="bottom_right", ) - # Combine both into a 1-row, 2-column layout + # --- Layout --- layout = (timeseries_plot + scatter_with_line).opts(shared_axes=False) return layout +# def plot_custom_scatter_SWE( +# df, +# obs_col, +# mod_col, +# *, +# highlight_months=None, +# month_col="month", +# size=15, +# width=500, +# height=400, +# ): +# """ +# Flexible scatter plot with optional month highlighting and 1:1 line. + +# Parameters +# ---------- +# df : pandas.DataFrame +# Input dataframe +# obs_col : str +# Column name for observed SWE +# mod_col : str +# Column name for modeled SWE +# highlight_months : list[int], optional +# Months to highlight (e.g., [10, 11]) +# month_col : str +# Column containing month values +# """ + +# df = df.copy() + +# # Handle highlighting +# if highlight_months is not None and month_col in df.columns: +# df["color"] = df[month_col].apply( +# lambda m: "teal" if m in highlight_months else "tomato" +# ) +# color = "color" +# else: +# color = "black" + +# scatter = df.hvplot.scatter( +# x=obs_col, +# y=mod_col, +# xlabel="Observed SWE (mm)", +# ylabel="Modeled SWE (mm)", +# title="Observed vs. Modeled SWE at " + obs_col, +# size=15, +# width=500, +# height=400, +# hover_cols=["index", "month"], +# color="color", +# ) + +# # 1:1 line +# swe_max = max(df[obs_col].max(), df[mod_col].max()) +# one_to_one = ( +# hv.Curve(([0, swe_max], [0, swe_max])) +# .opts( +# color="gray", +# line_dash="dashed", +# line_width=1, +# ) +# .relabel("1:1 Line") +# ) + +# return (scatter * one_to_one).opts(legend_position="bottom_right") + def plot_custom_scatter_SWE( - df, + df_obs, + df_mod, obs_col, mod_col, *, + site_label=None, highlight_months=None, month_col="month", size=15, @@ -682,26 +841,42 @@ def plot_custom_scatter_SWE( height=400, ): """ - Flexible scatter plot with optional month highlighting and 1:1 line. + Flexible scatter plot with optional month highlighting and 1:1 line, + using separate observed and modeled dataframes. Parameters ---------- - df : pandas.DataFrame - Input dataframe + df_obs : pandas.DataFrame + Observations dataframe + df_mod : pandas.DataFrame + Modeled dataframe obs_col : str Column name for observed SWE mod_col : str Column name for modeled SWE + site_label : str, optional + Clean name for plot title highlight_months : list[int], optional Months to highlight (e.g., [10, 11]) month_col : str - Column containing month values + Column containing month values (must exist or be derivable) """ - df = df.copy() + # --- Combine + align --- + df = ( + df_obs[[obs_col]] + .rename(columns={obs_col: "observed"}) + .join(df_mod[[mod_col]].rename(columns={mod_col: "modeled"}), how="inner") + .dropna() + ) + + df.index.name = "date" + + # --- Add month column if needed --- + if highlight_months is not None: + if month_col not in df.columns: + df[month_col] = df.index.month - # Handle highlighting - if highlight_months is not None and month_col in df.columns: df["color"] = df[month_col].apply( lambda m: "teal" if m in highlight_months else "tomato" ) @@ -709,21 +884,25 @@ def plot_custom_scatter_SWE( else: color = "black" + label = site_label if site_label else obs_col + + # --- Scatter plot --- scatter = df.hvplot.scatter( - x=obs_col, - y=mod_col, + x="observed", + y="modeled", xlabel="Observed SWE (mm)", ylabel="Modeled SWE (mm)", - title="Observed vs. Modeled SWE at " + obs_col, - size=15, - width=500, - height=400, - hover_cols=["index", "month"], - color="color", + title=f"{label}: Observed vs. Modeled SWE", + size=size, + width=width, + height=height, + hover_cols=["date", month_col] if highlight_months else ["date"], + color=color, ) - # 1:1 line - swe_max = max(df[obs_col].max(), df[mod_col].max()) + # --- 1:1 line --- + swe_max = max(df["observed"].max(), df["modeled"].max()) + one_to_one = ( hv.Curve(([0, swe_max], [0, swe_max])) .opts( From b8d0f3f3bad4b5d30686ffeab648842cacc01820 Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Wed, 25 Mar 2026 17:21:32 -0600 Subject: [PATCH 3/8] parflow swe notebook working up to the stats section --- .../ccss_swe_collect_observations.ipynb | 285 + .../parflow_swe_point_scale_evaluation.ipynb | 6298 +++++++++++++++-- ...rflow_swe_point_scale_evaluation_OLD.ipynb | 2160 ++++++ ...le_evaluation_copyAMY_Hydrodata_code.ipynb | 2993 -------- src/cssi_evaluation/utils/plot_utils.py | 11 +- src/cssi_evaluation/variables/snow_utils.py | 46 + 6 files changed, 8096 insertions(+), 3697 deletions(-) create mode 100644 examples/collect_observations/ccss_swe_collect_observations.ipynb create mode 100644 examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb delete mode 100644 examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb diff --git a/examples/collect_observations/ccss_swe_collect_observations.ipynb b/examples/collect_observations/ccss_swe_collect_observations.ipynb new file mode 100644 index 0000000..e257e0a --- /dev/null +++ b/examples/collect_observations/ccss_swe_collect_observations.ipynb @@ -0,0 +1,285 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "

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

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

\n", + " \n", + " Source: California Department of Water Resources. California Department of Water Resources staff members Manon von Kaenel (left), Jordan Thoennes, and Andy Reising conduct the first media snow survey of the 2025 season at Phillips Station in the Sierra Nevada, about 90 miles east of Sacramento off Highway 50 in El Dorado County. Photo taken January 2, 2025. Image obtained from\n", + " this link.\n", + " \n", + "

\n", + "\n", + "# Retrieve California Cooperative Snow Surveys (CCSS) Data for a Watershed of Interest\n", + "\n", + " **Author:** Irene Garousi-Nejad ([igarousi@cuahsi.org](mailto:igarousi@cuahsi.org))\n", + " **Last updated:** March 24, 2026" + ] + }, + { + "cell_type": "markdown", + "id": "622da967", + "metadata": {}, + "source": [ + "#### Introduction: \n", + "This notebook demonstrates how to access the California Cooperative Snow Surveys (CCSS) data, which is program led by the California Department of Water Resources (DWR) to support water supply forecasting and flood management missions. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Prepare the Python Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ensure that the `cssi_evaluation` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `GettingStarted.md` file." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import the libraries needed to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "prefix = os.environ['CONDA_PREFIX']\n", + "os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", + "\n", + "# add the src directory to the path so we can import evaluation modules\n", + "#sys.path.append('../../src/')\n", + "repo_root = Path.cwd().resolve().parents[1]\n", + "sys.path.insert(0, str(repo_root / \"src\"))\n", + "\n", + "import pyproj\n", + "import pandas as pd\n", + "import xarray as xr\n", + "import geopandas as gpd\n", + "from dask.distributed import Client\n", + "\n", + "from cssi_evaluation.utils import plot_utils, dataPrep_utils\n", + "from cssi_evaluation.external_data_access import observation_utils\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Set Inputs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# repository path\n", + "repo_root = Path.cwd().resolve().parents[1]\n", + "\n", + "# path to the model domain data\n", + "domain_data_path = f\"{repo_root}/examples/nwm/domain_data/\" \n", + "\n", + "# Path to the watershed shapefile\n", + "watershed_path = f\"{domain_data_path}TolumneRiver_18040009.shp\"\n", + "\n", + "# Start and end times of a water year (note that this code currently works for one water year)\n", + "StartDate = '2018-10-01'\n", + "EndDate = '2020-09-30'\n", + "\n", + "# Path to save results (obs and mod stands for observation and modeled, respectively)\n", + "OBS_OutputFolder = './cssi_outputs' " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### 3. Retrieve Observed Snow Data " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This code reads a GeoJSON file of all snow observation stations and filters them to include only stations in California with available CSV data. It also loads the watershed boundary shapefile (`TolumneRiver_18040009.shp`), converts it to the appropriate coordinate system, and merges all watershed polygons into a single MultiPolygon. Finally, it counts how many observation sites fall within this combined watershed boundary, providing the number of sites located inside the study area. \n", + "\n", + "**Heads up**: You might need to run the next cell twice. Sometimes, the spatial filtering does not return any results the first time due to how geometries are loaded or processed in the background." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create geodataframe of all stations\n", + "all_stations_gdf = gpd.read_file('https://raw.githubusercontent.com/egagli/snotel_ccss_stations/main/all_stations.geojson').set_index('code')\n", + "all_stations_gdf = all_stations_gdf[all_stations_gdf['csvData']==True]\n", + "filtered_all_stations_gdf = all_stations_gdf[all_stations_gdf.state.str.contains('California')] \n", + "\n", + "# Read the watershed shapefile and standardize to WGS84\n", + "watershed_gdf = gpd.read_file(watershed_path).to_crs(epsg=4326)\n", + "\n", + "# Combine all polygons into a single MultiPolygon\n", + "watershed_union = watershed_gdf.geometry.unary_union\n", + "\n", + "# Use the polygon geometry to select snotel sites that are within the domain\n", + "gdf_in_bbox = filtered_all_stations_gdf[filtered_all_stations_gdf.geometry.within(watershed_union)]\n", + "gdf_in_bbox.reset_index(inplace=True)\n", + "print(f'There are {len(gdf_in_bbox)} sites from', ', '.join(set(gdf_in_bbox.network)), 'network(s) within the watershed.')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Plot these sites on a map. Then, hover over the pins to see the site names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c535a38c", + "metadata": {}, + "outputs": [], + "source": [ + "gdf_in_bbox" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Plot the sites within the watershed boundary using the plot_utils function\n", + "m = plot_utils.map_sites_within_watershed(gdf_in_bbox, watershed_gdf, zoom_start=9) \n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check the start and end date of available data for these sites." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for i in gdf_in_bbox.index:\n", + " print('Site ', gdf_in_bbox.iloc[i].code, \":\", gdf_in_bbox.iloc[i].beginDate, \"-\", gdf_in_bbox.iloc[i].endDate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following uses the `observation_utils.py` script to download observed data for the sites within the domain. Since all the sites are from the (California Cooperative Snow Survey) CCSS network, we use the `getCCSSData` function from the module to get data. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

📖 Did you know?

\n", + "

The California Cooperative Snow Surveys (CCSS) program, managed primarily by the California Department of Water Resources (DWR), monitors snowpack across key California watersheds to estimate annual water supply. Most CCSS sites are manual snow courses, where surveyors physically measure snow depth and snow water equivalent (SWE) several times a year, though some sites have been upgraded with automated sensors. In contrast, SNOTEL sites, managed by the USDA Natural Resources Conservation Service (NRCS), are fully automated and collect data continuously across the western United States, including California. While CCSS primarily supports water supply forecasting, SNOTEL supports both water supply forecasting and climate monitoring.

\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create a folder to save results\n", + "isExist = os.path.exists(OBS_OutputFolder)\n", + "if isExist == True:\n", + " exit\n", + "else:\n", + " os.mkdir(OBS_OutputFolder)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# download CCSS data for each site within the watershed bounding box, save to /obs_outputs folder\n", + "for i in gdf_in_bbox.index:\n", + " observation_utils.getCCSSData(gdf_in_bbox.name[i], gdf_in_bbox.code[i], 'Ca', StartDate, EndDate, OBS_OutputFolder)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "nwm_env", + "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 +} diff --git a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb index 52c8fef..be0d1a2 100644 --- a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb +++ b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb @@ -8,7 +8,7 @@ "\n", "# Use HydroData to Retrieve Modeled and Observed Snow Data for a Watershed of Interest with ParFlow-CONUS Outputs vs Observed Snow Water Equivalent (SWE) - Full Evaluation Workflow\n", "Authors: Irene Garousi-Nejad (igarousi@cuahsi.org), Danielle Tijerina-Kreuzer (dtijerina@cuahsi.org) \n", - "Last updated: Feb 2026" + "Last updated: March 2026" ] }, { @@ -16,27 +16,30 @@ "metadata": {}, "source": [ "#### Introduction: \n", - "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites. We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. \n", - "\n", - "# FIX THIS: This notebook requires ParFlow-CONUS output, SNOTEL data, and metadata CSVs that are created in the `01_HydroData_collection.ipynb` notebook." + "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites. We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. " ] }, { "cell_type": "markdown", + "id": "e86aae63", "metadata": {}, "source": [ - "## 1. Prepare the Python Environment" + "## 1. Setup" ] }, { "cell_type": "markdown", + "id": "e88ed1ef", "metadata": {}, "source": [ + "### 1a. Python Environment \n", + "\n", "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." ] }, { "cell_type": "markdown", + "id": "c0f30927", "metadata": {}, "source": [ "Import the libraries needed to run this notebook:" @@ -44,7 +47,8 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 1, + "id": "ce97d33e", "metadata": { "tags": [] }, @@ -93,12 +97,12 @@ "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ - "
\n", - "
\n", + "
\n", + "
\n", "
\n", "\n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_sites_on_map(metadata_df, color_by_site_type=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a1e3bc39", + "metadata": {}, + "outputs": [], + "source": [ + "# # this may take a moment to load, but it should pop up in a new window\n", + "# m = plot_utils.map_sites_within_watershed(sites_in_watershed, watershed_gdf, zoom_start=9)\n", + "# m" + ] + }, + { + "cell_type": "markdown", + "id": "b1805ac2", + "metadata": {}, + "source": [ + "\n", + "### 2c. Gather the SNOTEL data for all stations within the watershed using the `hf.get_point_data()` function to retrieve daily, start-of-day SWE from SNOTEL sites:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f0f2beb6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
date380:CO:SNTL680:CO:SNTL
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", + "
" + ], + "text/plain": [ + " date 380:CO:SNTL 680:CO:SNTL\n", + "0 2003-10-01 0.0 0.0\n", + "1 2003-10-02 0.0 0.0\n", + "2 2003-10-03 0.0 0.0\n", + "3 2003-10-04 0.0 0.0\n", + "4 2003-10-05 0.0 0.0" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Request point observations data\n", + "obs_df = hf.get_point_data(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", + " date_start=StartDate, date_end=EndDate,\n", + " huc_id=[huc_8_code], grid='conus1')\n", + " #polygon=watershed_bbox, polygon_crs=watershed_crs)\n", + "\n", + "# save\n", + "obs_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL.csv', index=False)\n", + "\n", "# Ensure date column is datetime\n", - "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", + "obs_df[\"date\"] = pd.to_datetime(obs_df[\"date\"])\n", "\n", - "# Save\n", - "model_df.to_csv(f'./{MOD_OutputFolder}/df_{huc_8_name}_{huc_8_code}_PFCONUS1.csv', index=False)\n", + "# View first five records\n", + "obs_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "0e50455e", + "metadata": {}, + "source": [ + "## 3. Retrieve ParFlow-CONUS1 Modeled Snow Data" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "545a9d22", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a folder to save results\n", + "isExist = os.path.exists(MOD_OutputFolder)\n", + "if isExist == True:\n", + " exit\n", + "else:\n", + " os.mkdir(MOD_OutputFolder)" + ] + }, + { + "cell_type": "markdown", + "id": "56eb4bb4", + "metadata": {}, + "source": [ + "The following section retrieves ParFlow-CONUS1 data for each SNOTEL site within our HUC-08 watershed. The code identifies the CONUS1 `i,j` indices associated with each SNOTEL site, indicated in the `metadata_df`. It then extracts the CONUS1 modeled SWE output for the site and the period of interest, returning the result as a DataFrame. To fairly compare with SNOTEL, which reports SWE once daily at the start of the local day, model output is aggregated by day, using the argment `\"temporal_resolution\": \"daily\"`. Finally, the processed data is saved as a CSV file for each site. \n", + "\n", + "### 3a. ParFlow CONUS1 Model Dataset Information\n", + "We can print some information about the model output dataset by using the `hf.get_catalog_entry()` to get the CONUS1 model dataset metadata. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "10647da1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '12', 'dataset': 'conus1_baseline_mod', 'dataset_version': '', 'file_type': 'pfb', 'variable': 'swe', 'dataset_var': 'swe', 'temporal_resolution': 'daily', 'units': 'mm', 'aggregation': 'eod', 'grid': 'conus1', 'path': 'swe.daily.eod.{wy_daynum:03d}.pfb', 'file_grouping': 'wy_daynum', 'entry_start_date': None, 'entry_end_date': None, 'documentation_notes': '', 'site_type': '', 'variable_type': 'surface_water', 'has_z': '', 'dataset_type': 'parflow', 'datasource': 'hydroframe', 'paper_dois': '10.5194/gmd-14-7223-2021', 'dataset_dois': '', 'dataset_start_date': '2002-10-01', 'dataset_end_date': '2006-09-30', 'structure_type': 'gridded', 'has_ensemble': '', 'unit_type': 'length', 'period': 'daily'}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conus1_options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\"\n", + "}\n", + "hf.get_catalog_entry(conus1_options)" + ] + }, + { + "cell_type": "markdown", + "id": "c6fd1306", + "metadata": {}, + "source": [ + "Before we gather model outputs at the specific SNOTEL sites, we can visualize SWE across our HUC-08. This is plotted for one day at 1km lateral resolution." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ba48a33a", + "metadata": {}, + "outputs": [], + "source": [ + "# retrieve gridded PF-CONUS1 SWE for the entire HUC8 watershed\n", + "grid_swe_options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\",\n", + " \"temporal_resolution\": \"daily\",\n", + " \"start_time\": '2004-04-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", + " \"end_time\": '2004-04-02',\n", + " \"huc_id\": huc_8_code\n", + " }\n", " \n", - "model_df.head(5)" + " # Get gridded data\n", + "grid_swe = hf.get_gridded_data(grid_swe_options)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "b82b9574", + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Image [x,y] (SWE)" + ] + }, + "execution_count": 17, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1011" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "grid_swe_map = xr.DataArray(grid_swe[0], dims=(\"y\", \"x\"), name=\"SWE\")\n", + "grid_swe_map.hvplot.image(cmap=\"YlGnBu\", colorbar=True, aspect=\"equal\", title=f\"{huc_8_name} Gridded SWE on 2004-04-01\")\n" ] }, { "cell_type": "markdown", - "id": "7464828b", + "id": "73a13787", "metadata": {}, "source": [ - "## 6. Quick plot sanity check \n", - "Plot a simple timeseries of modeled and observed SWE to make sure our data retrieval was successful. " + "Create a copy of the model dataframe (`model_df`) so we have the same data structure:" ] }, { "cell_type": "code", - "execution_count": null, - "id": "fbe43f6a", + "execution_count": 18, + "id": "17143151", "metadata": {}, "outputs": [], "source": [ - "fig, ax = plt.subplots(figsize=(10, 4))\n", - "\n", - "ax.plot(data_df[\"date\"], model_df[\"380:CO:PFCONUS1\"], label=\"Modeled\", linewidth=2)\n", - "\n", - "ax.plot(data_df[\"date\"], data_df[\"380:CO:SNTL\"], label=\"Observed\", linewidth=2)\n", + "# Copy obs_df to model_df so we have the same timestamps and site_id structure\n", + "model_df = obs_df.copy()\n", "\n", - "ax.set_xlabel(\"Date\")\n", - "ax.set_ylabel(\"SWE (mm)\")\n", - "\n", - "# Date formatting for x-axis\n", - "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", - "ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))\n", - "\n", - "ax.legend(loc='upper left')\n", - "ax.grid(True, alpha=0.3)\n", + "# Set all non-date columns to NaN to prepare for filling in model data\n", + "non_date_cols = model_df.columns.difference([\"date\"])\n", + "model_df[non_date_cols] = np.nan\n", "\n", - "plt.tight_layout()" + "# Rename site_id columns for PF outputs \n", + "model_df.columns = [\n", + " col if col == \"date\" else col.replace(\":SNTL\", \"\") + \":PFCONUS1\"\n", + " for col in model_df.columns\n", + "]" ] }, { "cell_type": "markdown", - "id": "da3df109", + "id": "523bd35c", "metadata": {}, "source": [ - "# Start of comparison from old notebook" + "Now, retrieve the PF-CONUS1 modeled SWE from the the corresponding SNOTEL locations. Here we use the CONUS1 `i,j` indices from the `metadata_df` and grab the SWE from those grid cells using the function `hf.get_gridded_data()`:" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 19, + "id": "a814204c", "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
date380:CO:PFCONUS1680:CO:PFCONUS1
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", + "
" + ], + "text/plain": [ + " date 380:CO:PFCONUS1 680:CO:PFCONUS1\n", + "0 2003-10-01 0.0 0.0\n", + "1 2003-10-02 0.0 0.0\n", + "2 2003-10-03 0.0 0.0\n", + "3 2003-10-04 0.0 0.0\n", + "4 2003-10-05 0.0 0.0" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "## 3. Spatial Mapping of the SNOTEL sites \n", - "Before evaluating model performance, we plot the GIS data associated with the records in the combined DataFrame. The map below shows the SNOTEL stations included in the evaluation dataset, along with the watershed boundary used for the model simulations. Hover over the pins to see the site names. \n", + "# Loop over each station in metadata_df\n", + "for idx, row in metadata_df.iterrows():\n", + " site_id = row[\"site_id\"] # original SNTL site_id\n", + " col_name = site_id.replace(\":SNTL\", \"\") + \":PFCONUS1\" # corresponding column in model_df\n", + " conus_i = int(row[\"conus1_i\"])\n", + " conus_j = int(row[\"conus1_j\"])\n", + " \n", + " # Build options dict for this station\n", + " options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\",\n", + " \"temporal_resolution\": \"daily\",\n", + " \"start_time\": '2003-10-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", + " \"end_time\": '2005-10-01',\n", + " \"grid_point\": [conus_i, conus_j]\n", + " }\n", + " \n", + " # Get gridded data\n", + " data = hf.get_gridded_data(options)\n", + " \n", + " # Fill column in model_df\n", + " # Convert to numeric in case hf returns lists or other types\n", + " model_df[col_name] = np.squeeze(np.array(data))\n", + "\n", + "# Ensure date column is datetime\n", + "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", "\n", - "We also print a table of the SNOTEL site metadata to help with the single site selection." + "# Save\n", + "model_df.to_csv(f'./{MOD_OutputFolder}/df_{huc_8_name}_{huc_8_code}_PFCONUS1.csv', index=False)\n", + " \n", + "model_df.head(5)" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", + "id": "7464828b", "metadata": {}, - "outputs": [], "source": [ - "# Path to the watershed shapefile\n", - "watershed = \"./domain_data/East-Taylor_14020001.shp\"\n", - "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", - "\n", - "# Create GeoDataFrame of all available stations\n", - "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", - " metadata_df,\n", - " geometry=gpd.points_from_xy(\n", - " metadata_df.longitude,\n", - " metadata_df.latitude\n", - " ),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", - "\n", - "# Combine watershed polygons into one geometry\n", - "watershed_union = watershed_gdf.geometry.unary_union\n", - "\n", - "# Filter stations that fall within the watershed\n", - "sites_in_watershed = filtered_all_stations_gdf[\n", - " filtered_all_stations_gdf.geometry.within(watershed_union)\n", - "].copy()\n", - "\n", - "sites_in_watershed.reset_index(drop=True, inplace=True)\n", - "\n", - "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", - "\n", - "m = nwm_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", - "m" + "#### Quick plot sanity check \n", + "Plot a simple timeseries of modeled and observed SWE to make sure our data retrieval was successful. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, + "id": "fbe43f6a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAoFxJREFUeJzs3Xd8XXXh//HXuTf3Zu/ZtEn3pC100ZbVAoWyRRBQEESqomxBQeSnwleWoIAWAUWgyCqyVASBMlosZbSF0r1X2ibN3uMm957fHye5oxlN0pvcm+T9fDz66Oesez/JSW7u+36WYZqmiYiIiIiIiIgEnS3UFRARERERERHprxS6RURERERERHqIQreIiIiIiIhID1HoFhEREREREekhCt0iIiIiIiIiPUShW0RERERERKSHKHSLiIiIiIiI9BCFbhEREREREZEeEhHqCoQDj8fDgQMHiI+PxzCMUFdHREREREREwpxpmlRVVZGdnY3N1n57tkI3cODAAXJyckJdDREREREREelj8vLyGDJkSLvHFbqB+Ph4wPpmJSQkBBzzeDwUFRWRnp7e4acXEhq6P+FL9yb86R6FP92j8Kb7E550X8Kf7lF40/3pvMrKSnJycrx5sj0K3eDtUp6QkNBm6K6vrychIUE/dGFI9yd86d6EP92j8Kd7FN50f8KT7kv40z0Kb7o/XXe4Icr6LoqIiIiIiIj0EIVuERERERERkR6i0C0iIiIiIiLSQzSmuwvcbjeNjY2hrob48Xg8NDY2Ul9f36kxJw6HA7vd3gs1ExERERERUejuFNM0KSgooKKiItRVkUOYponH46GqqqrTa6wnJSWRlZWlNdlFRERERKTHKXR3QnV1NU1NTWRkZBATE6OwFkZM06SpqYmIiIjD3hfTNKmtraWwsBCAQYMG9UYVRURERERkAFPoPgy32019fT2DBg0iNTU11NWRQ3QldANER0cDUFhYSEZGhrqai4iIiIhIj9JEaofR2NiIYRjExMSEuioSJC33UuPzRURERESkpyl0d5K6lPcfupciIiIiItJbFLpFREREREREeohCt3Tb0qVLMQyD8vLyTl8zbNgwHnnkkSN63jvvvJNjjjnmiB5DREREJOw01sNnT8Cuj0NdExEJIoXufuzKK6/EMAx+/OMftzp2zTXXYBgGV155Ze9XTERERERaW3Y/vHMb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RMTdgyAiIiLXWrx4MWpra7F169Z28xYuXIijR4/i2LFj7eb985//xOOPP46amhoAV7/T/dRTT6G2tta+TGlpKe644w6cPXsWVqvVPn3atGm466678Nprr/X6/SEiItIKL3cPgIiIiNxLRKDT6QAAOTk5eO2113Ds2DHU19ejtbUVV65cwc8//wyz2dzh7fPy8iAiGDZsmMP0pqYmDBw40OXjJyIiUjM23URERH3c8ePHERUVhfLyctx3331ITU3Fn//8ZwQGBmL//v1ISUlBS0uL09srigKDwYDc3FwYDAaHef7+/q4ePhERkaqx6SYiIurDdu/ejcLCQjz99NM4cuQIWltb8fbbb0Ovv3ral08//dRheaPRCJvN5jBt7NixsNlsqK6uxsSJE2/Z2ImIiLSATTcREVEf0dTUhKqqKthsNpw/fx5fffUV0tPTMXPmTCxatAiFhYVobW3FunXrMGvWLBw4cADr1693WMeQIUPQ2NiIb775BnFxcfDz88OwYcPw29/+FosWLcLbb7+NsWPHoqamBrt378bo0aNx3333uekeExERuR/PXk5ERNRHfPXVVwgLC8OQIUOQnJyMnJwcrF27Ftu2bYPBYMCYMWOwatUqvPHGGxg1ahQ2bdqE9PR0h3VMmDABqampWLBgAYKDg/Hmm28CADZu3IhFixbh2WefRUxMDGbPno3vvvsOERER7rirREREqsGzlxMRERERERG5CN/pJiIiIiIiInIRNt1ERERERERELsKmm4iIiIiIiMhF2HQTERERERERuQibbiIiIiIiIiIXYdNNRERERERE5CJsuomIiIiIiIhchE03ERERERERkYuw6SYiIiIiIiJyETbdRERERERERC7CppuIiIiIiIjIRdh0ExEREREREbnI/wOi5/NV6KggEAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "sites_in_watershed" + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "\n", + "ax.plot(obs_df[\"date\"], model_df[\"380:CO:PFCONUS1\"], label=\"Modeled\", linewidth=2)\n", + "\n", + "ax.plot(obs_df[\"date\"], obs_df[\"380:CO:SNTL\"], label=\"Observed\", linewidth=2)\n", + "\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"SWE (mm)\")\n", + "\n", + "# Date formatting for x-axis\n", + "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", + "ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))\n", + "\n", + "ax.legend(loc='upper left')\n", + "ax.grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()" ] }, { @@ -1315,25 +5250,124 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "# choose a site of interest within the watershed\n", "my_site_code = '380:CO:'\n", "\n", - "############################ THIS BELOW DOESNT WORK BECAUSE CODE IS NOT COMPLETE\n", - "# filter to only that site\n", - "sites_in_watershed[sites_in_watershed['site_id']==my_site_code]" + "# make sure date columns are datetime and set as index for easier plotting and metric calculations\n", + "obs_df[\"date\"] = pd.to_datetime(obs_df[\"date\"])\n", + "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", + "\n", + "obs_df = obs_df.set_index(\"date\")\n", + "model_df = model_df.set_index(\"date\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, + "id": "1c1f5648", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Layout\n", + " .Overlay.I :Overlay\n", + " .Curve.Observed_SWE :Curve [date] (observed)\n", + " .Curve.Modeled_SWE :Curve [date] (modeled)\n", + " .Overlay.II :Overlay\n", + " .Scatter.I :Scatter [observed] (modeled,date)\n", + " .Curve.A_1_colon_1_Line :Curve [x] (y)" + ] + }, + "execution_count": 34, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1442" + } + }, + "output_type": "execute_result" + } + ], "source": [ - "nwm_utils.comparison_plots(combined_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1')" + "plot_utils.comparison_plots(obs_df, model_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1', site_label=None)" ] }, { @@ -1356,15 +5390,111 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Overlay\n", + " .Scatter.I :Scatter [observed] (modeled,color,date,month)\n", + " .Curve.A_1_colon_1_Line :Curve [x] (y)" + ] + }, + "execution_count": 24, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1354" + } + }, + "output_type": "execute_result" + } + ], "source": [ - "combined_df['month'] = combined_df.index.month\n", + "plot = plot_utils.plot_custom_scatter_SWE(\n", + " obs_df,\n", + " model_df,\n", + " f\"{my_site_code}SNTL\",\n", + " f\"{my_site_code}PFCONUS1\",\n", + " site_label=my_site_code,\n", + " highlight_months=[10, 11, 12, 1],\n", + ")\n", "\n", - "plot = nwm_utils.plot_custom_scatter_SWE(combined_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1',\n", - " highlight_months=[10, 11, 12, 1])\n", - "plot " + "plot" ] }, { @@ -1393,7 +5523,7 @@ "- Soil moisture recharge and groundwater contributions\n", "\n", "
\n", - " \n", + " \n", "
\n", "\n", "_Example daily SWE at a single site, showing two important periods in snow processes: accumulation (before peak) and ablation (after peak). The vertical line marks peak SWE._" @@ -1413,24 +5543,106 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 48, "metadata": {}, "outputs": [], "source": [ - "# isolate the columns associated with observations and model predictions.\n", - "# these will be inputs to our same-day comparison function.\n", - "obs_cols = sorted([col for col in combined_df.columns if col.endswith('SNTL')])\n", - "mod_cols = sorted([col for col in combined_df.columns if col.endswith('PFCONUS1')])" + "# # isolate the columns associated with observations and model predictions.\n", + "# # these will be inputs to our same-day comparison function.\n", + "combined_df = pd.concat([obs_df, model_df], axis=1)\n", + "obs_swe_cols = obs_df.columns.tolist()\n", + "mod_swe_cols = model_df.columns.tolist()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 49, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ObservedModeledWater_YearStation
date
2004-04-12304.80229.3069382004380:CO:SNTL
2005-04-12464.82341.9169482005380:CO:SNTL
2004-04-11231.145.5650522004680:CO:SNTL
2005-04-07274.32177.9033702005680:CO:SNTL
\n", + "
" + ], + "text/plain": [ + " Observed Modeled Water_Year Station\n", + "date \n", + "2004-04-12 304.80 229.306938 2004 380:CO:SNTL\n", + "2005-04-12 464.82 341.916948 2005 380:CO:SNTL\n", + "2004-04-11 231.14 5.565052 2004 680:CO:SNTL\n", + "2005-04-07 274.32 177.903370 2005 680:CO:SNTL" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# compute the same-day SWE comparison during the observed peak SWE for each of the observation and modeled sites.\n", - "df_observed_peak = utils.modeled_swe_at_observed_peak(combined_df, obs_cols, mod_cols)\n", + "df_observed_peak = snow_utils.modeled_swe_at_observed_peak(combined_df, obs_swe_cols, mod_swe_cols)\n", "df_observed_peak" ] }, @@ -1443,9 +5655,99 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 50, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":NdOverlay [Source]\n", + " :Scatter [Station] (SWE,Water_Year)" + ] + }, + "execution_count": 50, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1714" + } + }, + "output_type": "execute_result" + } + ], "source": [ "# Rearrange the dataframe to long format for easier plotting\n", "df_long = (\n", @@ -1492,12 +5794,95 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 52, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ObservedWater_YearObserved_DateModeledModeled_DateStation
0304.8020042004-04-12237.8717462004-03-19380:CO:SNTL
1464.8220052005-04-12341.9169482005-04-12380:CO:SNTL
2231.1420042004-04-11169.7645442004-03-09680:CO:SNTL
3274.3220052005-04-07219.4611052005-03-27680:CO:SNTL
\n", + "
" + ], + "text/plain": [ + " Observed Water_Year Observed_Date Modeled Modeled_Date Station\n", + "0 304.80 2004 2004-04-12 237.871746 2004-03-19 380:CO:SNTL\n", + "1 464.82 2005 2005-04-12 341.916948 2005-04-12 380:CO:SNTL\n", + "2 231.14 2004 2004-04-11 169.764544 2004-03-09 680:CO:SNTL\n", + "3 274.32 2005 2005-04-07 219.461105 2005-03-27 680:CO:SNTL" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# compute the different-day SWE comparison for each of the observed and modeled sites.\n", - "df_both_peak = utils.modeled_vs_observed_peak_swe(combined_df, obs_cols, mod_cols)\n", + "df_both_peak = snow_utils.modeled_vs_observed_peak_swe(combined_df, obs_swe_cols, mod_swe_cols)\n", "df_both_peak" ] }, @@ -1510,9 +5895,100 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 53, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Overlay\n", + " .Scatter.I :Scatter [Observed] (Modeled,Station,Water_Year)\n", + " .Curve.A_1_colon_1_Line :Curve [x] (y)" + ] + }, + "execution_count": 53, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1801" + } + }, + "output_type": "execute_result" + } + ], "source": [ "### NEED TO DECIDE HOW TO FORMAT THIS PLOT AND IF WE WANT TO HAVE THE \"SAME_DAY\" PLOT\n", "\n", @@ -1562,7 +6038,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 54, "metadata": {}, "outputs": [], "source": [ @@ -1575,10 +6051,109 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 55, "id": "df02795e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ObservedWater_YearObserved_DateModeledModeled_DateStationPeak_Date_Diff_DaysPeak_SWE_Diff
0304.8020042004-04-12237.8717462004-03-19380:CO:SNTL-24-66.928254
1464.8220052005-04-12341.9169482005-04-12380:CO:SNTL0-122.903052
2231.1420042004-04-11169.7645442004-03-09680:CO:SNTL-33-61.375456
3274.3220052005-04-07219.4611052005-03-27680:CO:SNTL-11-54.858895
\n", + "
" + ], + "text/plain": [ + " Observed Water_Year Observed_Date Modeled Modeled_Date Station \\\n", + "0 304.80 2004 2004-04-12 237.871746 2004-03-19 380:CO:SNTL \n", + "1 464.82 2005 2005-04-12 341.916948 2005-04-12 380:CO:SNTL \n", + "2 231.14 2004 2004-04-11 169.764544 2004-03-09 680:CO:SNTL \n", + "3 274.32 2005 2005-04-07 219.461105 2005-03-27 680:CO:SNTL \n", + "\n", + " Peak_Date_Diff_Days Peak_SWE_Diff \n", + "0 -24 -66.928254 \n", + "1 0 -122.903052 \n", + "2 -33 -61.375456 \n", + "3 -11 -54.858895 " + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df_both_peak" ] @@ -1592,9 +6167,100 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 56, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Layout\n", + " .Bars.I :Bars [Station] (Peak_Date_Diff_Days,Modeled,Observed)\n", + " .Bars.II :Bars [Station] (Peak_SWE_Diff,Modeled,Observed)" + ] + }, + "execution_count": 56, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1887" + } + }, + "output_type": "execute_result" + } + ], "source": [ "# Filter to separate each water year\n", "year1 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].min()]\n", @@ -1661,9 +6327,102 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 58, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Overlay\n", + " .NdOverlay.I :NdOverlay [Station]\n", + " :Scatter [Peak_Date_Diff_Days] (Peak_SWE_Diff,Water_Year)\n", + " .VLine.I :VLine [x,y]\n", + " .HLine.I :HLine [x,y]" + ] + }, + "execution_count": 58, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p2125" + } + }, + "output_type": "execute_result" + } + ], "source": [ "\n", "\n", @@ -1684,7 +6443,7 @@ "vline = hv.VLine(0).opts(color='gray', line_dash='dashed')\n", "hline = hv.HLine(0).opts(color='gray', line_dash='dashed')\n", "\n", - "(scatter * vline * hline).opts(legend_position='top_left', show_grid=True)\n" + "(scatter * vline * hline).opts(legend_position='bottom_left', show_grid=True)\n" ] }, { @@ -1713,11 +6472,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 59, "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'nwm_utils' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[59], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnwm_utils\u001b[49m\u001b[38;5;241m.\u001b[39mcompute_stats(combined_df, \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mCCSS_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmy_site_code\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_swe_m\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mNWM_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmy_site_code\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_swe_m\u001b[39m\u001b[38;5;124m'\u001b[39m)\n", + "\u001b[0;31mNameError\u001b[0m: name 'nwm_utils' is not defined" + ] + } + ], "source": [ "nwm_utils.compute_stats(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')" ] @@ -2005,6 +6776,135 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "markdown", + "id": "9dbf5e39", + "metadata": {}, + "source": [ + "# SUBSET TOOLS - prob not keeping section" + ] + }, + { + "cell_type": "markdown", + "id": "094afff0", + "metadata": {}, + "source": [ + "Use the Subsettools function `define_huc_domain()` to get the actual CONUS1 indices associated with the East-Taylor HUC-O8. It returns a tuple `(imin, jmin, imax, jmax)` of grid indices that define a bounding box containing our region (or point) of interest (Note: (imin, jmin, imax, jmax) are the west, south, east and north boundaries of the box respectively) and a mask for that domain." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "879a230c", + "metadata": {}, + "outputs": [], + "source": [ + "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", + "\n", + "np.save(f'{domain_data_path}domainMask_{huc_8_name}_conus1.npy', mask)\n", + "\n", + "plt.imshow(mask, origin='lower')\n", + "print(ij_bounds)\n", + "print(mask.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "caa6864c", + "metadata": {}, + "source": [ + "Using the domain mask and the i,j PF-CONUS1 indices, we use a hf_hydrodata function to find and save the associated grid cell center lat/lon pair for each grid cell in the domain. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec9d5e3a", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract bounds\n", + "i_min, j_min, i_max, j_max = ij_bounds\n", + "mask_shape = mask.shape #shape of the subset rectangular domain\n", + "\n", + "# Create i/j index ranges\n", + "i_vals = np.arange(i_min, i_max)\n", + "j_vals = np.arange(j_min, j_max)\n", + "\n", + "# Create full 2D grid (note indexing order carefully)\n", + "jj, ii = np.meshgrid(j_vals, i_vals, indexing=\"ij\")" + ] + }, + { + "cell_type": "markdown", + "id": "a3b8be1b", + "metadata": {}, + "source": [ + "Because the function `hf.to_latlon()` finds the coordinates at the lower left corner of a grid cell, we add 0.5 to each i,j index pair to find the **lat/lon at the grid cell center**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af94a11d", + "metadata": {}, + "outputs": [], + "source": [ + "# Compute grid cell centers\n", + "ii_center = ii + 0.5\n", + "jj_center = jj + 0.5\n", + "\n", + "# Convert to lat/lon (vectorized loop)\n", + "lat = np.zeros(mask_shape)\n", + "lon = np.zeros(mask_shape)\n", + "\n", + "for r in range(mask_shape[0]):\n", + " for c in range(mask_shape[1]):\n", + " lat[r, c], lon[r, c] = hf.to_latlon(\"conus1\",\n", + " ii_center[r, c],\n", + " jj_center[r, c])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb47bf8d", + "metadata": {}, + "outputs": [], + "source": [ + "# Save 2D arrays of Lat & Lon\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lon_2d.npy\", lon)\n", + "\n", + "# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \n", + "grid_df = pd.DataFrame({\n", + " \"i\": ii.ravel(),\n", + " \"j\": jj.ravel(),\n", + " \"lat\": lat.ravel(),\n", + " \"lon\": lon.ravel(),\n", + "})\n", + "grid_df.to_csv(f\"{domain_data_path}df_{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.csv\", index=False)\n", + "\n", + "# Save a shapefile of the watershed Lat & Lon\n", + "grid_gdf = gpd.GeoDataFrame(\n", + " grid_df,\n", + " geometry=gpd.points_from_xy(grid_df.lon, grid_df.lat),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "# Save the grid points / GeoDataFrame to a shapefile for later use\n", + "grid_gdf.to_file(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.shp\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d78327b", + "metadata": {}, + "outputs": [], + "source": [ + "grid_df" + ] } ], "metadata": { diff --git a/examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb new file mode 100644 index 0000000..664b2e0 --- /dev/null +++ b/examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb @@ -0,0 +1,2160 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![NWM](../img/NWM.png)\n", + "\n", + "# Use HydroData to Retrieve Modeled and Observed Snow Data for a Watershed of Interest with ParFlow-CONUS Outputs vs Observed Snow Water Equivalent (SWE) - Full Evaluation Workflow\n", + "Authors: Irene Garousi-Nejad (igarousi@cuahsi.org), Danielle Tijerina-Kreuzer (dtijerina@cuahsi.org) \n", + "Last updated: Feb 2026" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Introduction: \n", + "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites. We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. \n", + "\n", + "# FIX THIS: This notebook requires ParFlow-CONUS output, SNOTEL data, and metadata CSVs that are created in the `01_HydroData_collection.ipynb` notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Prepare the Python Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import the libraries needed to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "tags": [] + }, + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.1/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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "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": "p1010" + } + }, + "output_type": "display_data" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "from pathlib import Path\n", + "import holoviews as hv\n", + "import hvplot.pandas\n", + "import hvplot.xarray\n", + "import pyproj\n", + "import pandas as pd\n", + "import numpy as np\n", + "import xarray as xr\n", + "import geopandas as gpd\n", + "from dask.distributed import Client\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.dates as mdates\n", + "import hf_hydrodata as hf\n", + "import subsettools\n", + "\n", + "\n", + "# Import the Evaluation library from the project root.\n", + "sys.path.append(str((Path.cwd().absolute() / \"../../src\").resolve()))\n", + "\n", + "from cssi_evaluation.variables import snow_utils\n", + "from cssi_evaluation.utils import metric_utils\n", + "from cssi_evaluation.utils import evaluation_utils\n", + "\n", + "hv.extension('bokeh')\n", + "\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60a74589", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "19adce70", + "metadata": {}, + "source": [ + "# Get data from Hydrodata - from `dataCollectionHydrodata_parflow.ipynb` notebook and needs to be merged into this notebook" + ] + }, + { + "cell_type": "markdown", + "id": "e86aae63", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "markdown", + "id": "e88ed1ef", + "metadata": {}, + "source": [ + "### 1a. Python Environment \n", + "\n", + "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." + ] + }, + { + "cell_type": "markdown", + "id": "c0f30927", + "metadata": {}, + "source": [ + "Import the libraries needed to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0dfc3fb3", + "metadata": {}, + "outputs": [], + "source": [ + "# import os\n", + "# import sys\n", + "\n", + "# prefix = os.environ['CONDA_PREFIX']\n", + "# os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", + "\n", + "# # add the src directory to the path so we can import evaluation modules\n", + "# sys.path.append('../../src/')\n", + "\n", + "# import sys\n", + "# import pyproj\n", + "# import pandas as pd\n", + "# import numpy as np\n", + "# import xarray as xr\n", + "# import geopandas as gpd\n", + "# from dask.distributed import Client\n", + "# import matplotlib.pyplot as plt\n", + "# import matplotlib.dates as mdates\n", + "# import hf_hydrodata as hf\n", + "# import subsettools\n", + "# import hvplot.xarray\n", + "\n", + "\n", + "# from cssi_evaluation.utils import plot_utils\n", + "\n", + "\n", + "# %load_ext autoreload\n", + "# %autoreload 2\n" + ] + }, + { + "cell_type": "markdown", + "id": "8e058228", + "metadata": {}, + "source": [ + "### 1b. Register Pin and Access HydroData\n", + "\n", + "To access the HydroData catalog you will need to sign up for a [HydroFrame account](https://hydrogen.princeton.edu/signup) (do this only once), [create a 4-digit PIN](https://hydrogen.princeton.edu/pin), and register your pin in order to have access to the HydroData datasets (you will do this in the next code cell below). To note, you PIN will expire after 7 days and will need to recreate it after that time. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a365996d", + "metadata": {}, + "outputs": [], + "source": [ + "# You need to register on https://hydrogen.princeton.edu/pin \n", + "# and run the following with your registered information\n", + "# before you can use the hydrodata utilities\n", + "hf.register_api_pin(\"dtt2@princeton.edu\", \"7837\")" + ] + }, + { + "cell_type": "markdown", + "id": "825c288d", + "metadata": {}, + "source": [ + "### 1c. Dask \n", + "\n", + "We'll use dask to parallelize our code. To manage parallel computation and visualize progress of long-running tasks, we initialize a Dask “cluster,” which defines how many workers are used and how much computing power each worker has. \n", + "\n", + "In this setup, we create a Dask client with `Client(n_workers=6, threads_per_worker=1, memory_limit='2GB')`, which launches a cluster with 6 workers. Each worker uses a single thread, typically mapped to one CPU core, allowing for efficient parallel processing across 6 cores. Each worker also has a memory limit of 2 GB, for a total of up to 12 GB across the cluster.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6f2b08d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dashboard link: http://127.0.0.1:8787/status\n", + "\n" + ] + } + ], + "source": [ + "# use a try accept loop so we only instantiate the client\n", + "# if it doesn't already exist.\n", + "try:\n", + " print('Dashboard link:', client.dashboard_link)\n", + "except: \n", + " # The client should be customized to your workstation resources.\n", + " client = Client(n_workers=6, threads_per_worker=1, memory_limit='2GB') \n", + " print('Dashboard link:', client.dashboard_link)\n", + "print(client)" + ] + }, + { + "cell_type": "markdown", + "id": "b8620cfc", + "metadata": {}, + "source": [ + "## 2. Set Paths" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e50dc99d", + "metadata": {}, + "outputs": [], + "source": [ + "# Start and end times of a water year (to note, these dates were chosen to align with the PFCONUS1 early 2000s runs)\n", + "StartDate = '2003-10-01'\n", + "EndDate = '2005-09-30'\n", + "\n", + "domain_data_path = 'examples/parflow/domain_data/' # path to the model domain data\n", + "\n", + "# Path to save results (obs and mod stands for observation and modeled, respectively)\n", + "OBS_OutputFolder = './obs_outputs_PF' \n", + "MOD_OutputFolder = './mod_outputs_PF'" + ] + }, + { + "cell_type": "markdown", + "id": "feb58871", + "metadata": {}, + "source": [ + "## 3. Retrieve Observed Snow Data " + ] + }, + { + "cell_type": "markdown", + "id": "45ca2832", + "metadata": {}, + "source": [ + "### 3a. Define the watershed of interest\n", + "\n", + "One of the simplest ways to gather data and model output from HydroData is by specifying a [Hydrologic Unit Code](https://www.usgs.gov/national-hydrography/watershed-boundary-dataset). Before we retrieve any hydrologic information, we need to indicate a HUC8 code and use it to gather snow water equivalent (SWE) observations from SNOTEL sites \n", + "\n", + "✏️ If you have a specific HUC8 in mind, you can change the variable `huc_8_code` below." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c8355563", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HUC-8 ID: 14020001\n", + "HUC-8 name: East-Taylor\n" + ] + } + ], + "source": [ + "# ✏️ Specify HUC8 ID and Name for watershed of interest\n", + "huc_8_code = '14020001' # East-Taylor HUC-8\n", + "print(f'HUC-8 ID: {huc_8_code}')\n", + "\n", + "huc_8_name = 'East-Taylor'\n", + "print(f'HUC-8 name: {huc_8_name}')" + ] + }, + { + "cell_type": "markdown", + "id": "5de02c3b", + "metadata": {}, + "source": [ + "Use the Subsettools function `define_huc_domain()` to get the actual CONUS1 indices associated with the East-Taylor HUC-O8. It returns a tuple `(imin, jmin, imax, jmax)` of grid indices that define a bounding box containing our region (or point) of interest (Note: (imin, jmin, imax, jmax) are the west, south, east and north boundaries of the box respectively) and a mask for that domain." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "965bd6ea", + "metadata": {}, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: 'examples/parflow/domain_data/domainMask_East-Taylor_conus1.npy'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[11], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m ij_bounds, mask \u001b[38;5;241m=\u001b[39m subsettools\u001b[38;5;241m.\u001b[39mdefine_huc_domain([huc_8_code], \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mconus1\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m----> 3\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mdomain_data_path\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43mdomainMask_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_name\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_conus1.npy\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmask\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 5\u001b[0m plt\u001b[38;5;241m.\u001b[39mimshow(mask, origin\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlower\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28mprint\u001b[39m(ij_bounds)\n", + "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36msave\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/numpy/lib/npyio.py:518\u001b[0m, in \u001b[0;36msave\u001b[0;34m(file, arr, allow_pickle, fix_imports)\u001b[0m\n\u001b[1;32m 516\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m file\u001b[38;5;241m.\u001b[39mendswith(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m):\n\u001b[1;32m 517\u001b[0m file \u001b[38;5;241m=\u001b[39m file \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m--> 518\u001b[0m file_ctx \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mwb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 520\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m file_ctx \u001b[38;5;28;01mas\u001b[39;00m fid:\n\u001b[1;32m 521\u001b[0m arr \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39masanyarray(arr)\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'examples/parflow/domain_data/domainMask_East-Taylor_conus1.npy'" + ] + } + ], + "source": [ + "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", + "\n", + "np.save(f'{domain_data_path}domainMask_{huc_8_name}_conus1.npy', mask)\n", + "\n", + "plt.imshow(mask, origin='lower')\n", + "print(ij_bounds)\n", + "print(mask.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "61745fb2", + "metadata": {}, + "source": [ + "Using the domain mask and the i,j PF-CONUS1 indices, we use a hf_hydrodata function to find and save the associated grid cell center lat/lon pair for each grid cell in the domain. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1ff5c1d4", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract bounds\n", + "i_min, j_min, i_max, j_max = ij_bounds\n", + "mask_shape = mask.shape #shape of the subset rectangular domain\n", + "\n", + "# Create i/j index ranges\n", + "i_vals = np.arange(i_min, i_max)\n", + "j_vals = np.arange(j_min, j_max)\n", + "\n", + "# Create full 2D grid (note indexing order carefully)\n", + "jj, ii = np.meshgrid(j_vals, i_vals, indexing=\"ij\")" + ] + }, + { + "cell_type": "markdown", + "id": "c6ae90bf", + "metadata": {}, + "source": [ + "Because the function `hf.to_latlon()` finds the coordinates at the lower left corner of a grid cell, we add 0.5 to each i,j index pair to find the **lat/lon at the grid cell center**." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "13fbc81f", + "metadata": {}, + "outputs": [], + "source": [ + "# Compute grid cell centers\n", + "ii_center = ii + 0.5\n", + "jj_center = jj + 0.5\n", + "\n", + "# Convert to lat/lon (vectorized loop)\n", + "lat = np.zeros(mask_shape)\n", + "lon = np.zeros(mask_shape)\n", + "\n", + "for r in range(mask_shape[0]):\n", + " for c in range(mask_shape[1]):\n", + " lat[r, c], lon[r, c] = hf.to_latlon(\"conus1\",\n", + " ii_center[r, c],\n", + " jj_center[r, c])" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "aa20e59f", + "metadata": {}, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: 'examples/parflow/domain_data/East-Taylor_14020001_lat_2d.npy'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[14], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# Save 2D arrays of Lat & Lon\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mdomain_data_path\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_name\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_code\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_lat_2d.npy\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlat\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m np\u001b[38;5;241m.\u001b[39msave(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdomain_data_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;132;01m{\u001b[39;00mhuc_8_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mhuc_8_code\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_lon_2d.npy\u001b[39m\u001b[38;5;124m\"\u001b[39m, lon)\n\u001b[1;32m 5\u001b[0m \u001b[38;5;66;03m# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \u001b[39;00m\n", + "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36msave\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/numpy/lib/npyio.py:518\u001b[0m, in \u001b[0;36msave\u001b[0;34m(file, arr, allow_pickle, fix_imports)\u001b[0m\n\u001b[1;32m 516\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m file\u001b[38;5;241m.\u001b[39mendswith(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m):\n\u001b[1;32m 517\u001b[0m file \u001b[38;5;241m=\u001b[39m file \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m--> 518\u001b[0m file_ctx \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mwb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 520\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m file_ctx \u001b[38;5;28;01mas\u001b[39;00m fid:\n\u001b[1;32m 521\u001b[0m arr \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39masanyarray(arr)\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'examples/parflow/domain_data/East-Taylor_14020001_lat_2d.npy'" + ] + } + ], + "source": [ + "# Save 2D arrays of Lat & Lon\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lon_2d.npy\", lon)\n", + "\n", + "# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \n", + "grid_df = pd.DataFrame({\n", + " \"i\": ii.ravel(),\n", + " \"j\": jj.ravel(),\n", + " \"lat\": lat.ravel(),\n", + " \"lon\": lon.ravel(),\n", + "})\n", + "grid_df.to_csv(f\"{domain_data_path}df_{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.csv\", index=False)\n", + "\n", + "# Save a shapefile of the watershed Lat & Lon\n", + "grid_gdf = gpd.GeoDataFrame(\n", + " grid_df,\n", + " geometry=gpd.points_from_xy(grid_df.lon, grid_df.lat),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "# Save the grid points / GeoDataFrame to a shapefile for later use\n", + "grid_gdf.to_file(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.shp\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "41e8d63a", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'grid_df' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[15], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mgrid_df\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'grid_df' is not defined" + ] + } + ], + "source": [ + "grid_df" + ] + }, + { + "cell_type": "markdown", + "id": "84549c32", + "metadata": {}, + "source": [ + "### 3b. Explore the available SWE data in a watershed " + ] + }, + { + "cell_type": "markdown", + "id": "e088705e", + "metadata": {}, + "source": [ + "
\n", + "

📖 Did you know?

\n", + "

The Snow Telemetry (SNOTEL) network, managed by the USDA Natural Resources Conservation Service (NRCS), monitors snowpack conditions across key watersheds in the western United States to support water supply forecasting and climate monitoring. SNOTEL sites are fully automated stations that continuously measure snow water equivalent (SWE), snow depth, precipitation, temperature, and other meteorological variables throughout the year. Unlike manual snow survey programs, SNOTEL provides high-temporal-resolution observations that enable near–real-time assessment of snowpack evolution and interannual variability. These data are widely used for operational forecasting, drought assessment, and long-term climate analysis.

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "de83d6b6", + "metadata": {}, + "source": [ + "Explore what SWE data is available at sites within the HUC ID you specified that operated during WY2004 and WY2005. If you want to check other variables besides SWE, you can change the `variable` argument name. " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "3aa8210e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
site_idsite_namesite_typeagencystatevariable_nameunitsdatasetvariabletemporal_resolution...latitudelongitudesite_query_urldate_metadata_last_updatedtz_cddoiconus1_iconus1_jconus2_iconus2_j
0380:CO:SNTLButteSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.89435-106.95327https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone942.0650.01372.01601.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.81982-106.58962https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone972.0638.01402.01589.0
\n", + "

2 rows × 24 columns

\n", + "
" + ], + "text/plain": [ + " site_id site_name site_type agency state \\\n", + "0 380:CO:SNTL Butte SNOTEL station NRCS CO \n", + "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO \n", + "\n", + " variable_name units dataset variable \\\n", + "0 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", + "1 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", + "\n", + " temporal_resolution ... latitude longitude \\\n", + "0 daily ... 38.89435 -106.95327 \n", + "1 daily ... 38.81982 -106.58962 \n", + "\n", + " site_query_url \\\n", + "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", + "\n", + " date_metadata_last_updated tz_cd doi conus1_i conus1_j conus2_i conus2_j \n", + "0 2023-03-07 PST None 942.0 650.0 1372.0 1601.0 \n", + "1 2023-03-07 PST None 972.0 638.0 1402.0 1589.0 \n", + "\n", + "[2 rows x 24 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "avail_df = hf.get_site_variables(variable = \"swe\",\n", + " huc_id = [huc_8_code], grid = 'conus1',\n", + " date_start = StartDate, date_end = EndDate)\n", + "\n", + "# View first five records\n", + "avail_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "268915dc", + "metadata": {}, + "source": [ + "### 3c. Map the SNOTEL stations inside the HUC-08 watershed that have available data in the selected time range \n", + "To note here, we are using pre-loaded shape files for the East-Taylor HUC8, which are located in the `/domain_data/` directory." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "f5c95f67", + "metadata": {}, + "outputs": [ + { + "ename": "DriverError", + "evalue": "examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mCPLE_OpenFailedError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32mfiona/ogrext.pyx:136\u001b[0m, in \u001b[0;36mfiona.ogrext.gdal_open_vector\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mfiona/_err.pyx:291\u001b[0m, in \u001b[0;36mfiona._err.exc_wrap_pointer\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mCPLE_OpenFailedError\u001b[0m: examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory", + "\nDuring handling of the above exception, another exception occurred:\n", + "\u001b[0;31mDriverError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[17], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m### Select station locations that fall within the HUC8 watershed\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \n\u001b[1;32m 3\u001b[0m \u001b[38;5;66;03m# Path to the watershed shapefile that was just created\u001b[39;00m\n\u001b[1;32m 4\u001b[0m watershed \u001b[38;5;241m=\u001b[39m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdomain_data_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124mEast-Taylor_14020001.shp\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m----> 5\u001b[0m watershed_gdf \u001b[38;5;241m=\u001b[39m \u001b[43mgpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_file\u001b[49m\u001b[43m(\u001b[49m\u001b[43mwatershed\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mto_crs(epsg\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m4326\u001b[39m)\n\u001b[1;32m 7\u001b[0m \u001b[38;5;66;03m# Create GeoDataFrame of all available stations\u001b[39;00m\n\u001b[1;32m 8\u001b[0m filtered_all_stations_gdf \u001b[38;5;241m=\u001b[39m gpd\u001b[38;5;241m.\u001b[39mGeoDataFrame(\n\u001b[1;32m 9\u001b[0m avail_df,\n\u001b[1;32m 10\u001b[0m geometry\u001b[38;5;241m=\u001b[39mgpd\u001b[38;5;241m.\u001b[39mpoints_from_xy(\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 14\u001b[0m crs\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEPSG:4326\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 15\u001b[0m )\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/geopandas/io/file.py:259\u001b[0m, in \u001b[0;36m_read_file\u001b[0;34m(filename, bbox, mask, rows, engine, **kwargs)\u001b[0m\n\u001b[1;32m 256\u001b[0m path_or_bytes \u001b[38;5;241m=\u001b[39m filename\n\u001b[1;32m 258\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m engine \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfiona\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m--> 259\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_read_file_fiona\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 260\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath_or_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfrom_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbbox\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbbox\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmask\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmask\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrows\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrows\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 261\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 262\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m engine \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpyogrio\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 263\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _read_file_pyogrio(\n\u001b[1;32m 264\u001b[0m path_or_bytes, bbox\u001b[38;5;241m=\u001b[39mbbox, mask\u001b[38;5;241m=\u001b[39mmask, rows\u001b[38;5;241m=\u001b[39mrows, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 265\u001b[0m )\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/geopandas/io/file.py:303\u001b[0m, in \u001b[0;36m_read_file_fiona\u001b[0;34m(path_or_bytes, from_bytes, bbox, mask, rows, where, **kwargs)\u001b[0m\n\u001b[1;32m 300\u001b[0m reader \u001b[38;5;241m=\u001b[39m fiona\u001b[38;5;241m.\u001b[39mopen\n\u001b[1;32m 302\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m fiona_env():\n\u001b[0;32m--> 303\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[43mreader\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath_or_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m features:\n\u001b[1;32m 304\u001b[0m crs \u001b[38;5;241m=\u001b[39m features\u001b[38;5;241m.\u001b[39mcrs_wkt\n\u001b[1;32m 305\u001b[0m \u001b[38;5;66;03m# attempt to get EPSG code\u001b[39;00m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/env.py:457\u001b[0m, in \u001b[0;36mensure_env_with_credentials..wrapper\u001b[0;34m(*args, **kwds)\u001b[0m\n\u001b[1;32m 454\u001b[0m session \u001b[38;5;241m=\u001b[39m DummySession()\n\u001b[1;32m 456\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m env_ctor(session\u001b[38;5;241m=\u001b[39msession):\n\u001b[0;32m--> 457\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/__init__.py:336\u001b[0m, in \u001b[0;36mopen\u001b[0;34m(fp, mode, driver, schema, crs, encoding, layer, vfs, enabled_drivers, crs_wkt, allow_unsupported_drivers, **kwargs)\u001b[0m\n\u001b[1;32m 333\u001b[0m path \u001b[38;5;241m=\u001b[39m parse_path(fp)\n\u001b[1;32m 335\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m mode \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m--> 336\u001b[0m colxn \u001b[38;5;241m=\u001b[39m \u001b[43mCollection\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 337\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 338\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 339\u001b[0m \u001b[43m \u001b[49m\u001b[43mdriver\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdriver\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 340\u001b[0m \u001b[43m \u001b[49m\u001b[43mencoding\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mencoding\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 341\u001b[0m \u001b[43m \u001b[49m\u001b[43mlayer\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlayer\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 342\u001b[0m \u001b[43m \u001b[49m\u001b[43menabled_drivers\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43menabled_drivers\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 343\u001b[0m \u001b[43m \u001b[49m\u001b[43mallow_unsupported_drivers\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mallow_unsupported_drivers\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 344\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 345\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 346\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m mode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 347\u001b[0m colxn \u001b[38;5;241m=\u001b[39m Collection(\n\u001b[1;32m 348\u001b[0m path,\n\u001b[1;32m 349\u001b[0m mode,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 358\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 359\u001b[0m )\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/collection.py:243\u001b[0m, in \u001b[0;36mCollection.__init__\u001b[0;34m(self, path, mode, driver, schema, crs, encoding, layer, vsi, archive, enabled_drivers, crs_wkt, ignore_fields, ignore_geometry, include_fields, wkt_version, allow_unsupported_drivers, **kwargs)\u001b[0m\n\u001b[1;32m 241\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 242\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m Session()\n\u001b[0;32m--> 243\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msession\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 244\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[1;32m 245\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m WritingSession()\n", + "File \u001b[0;32mfiona/ogrext.pyx:588\u001b[0m, in \u001b[0;36mfiona.ogrext.Session.start\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mfiona/ogrext.pyx:143\u001b[0m, in \u001b[0;36mfiona.ogrext.gdal_open_vector\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mDriverError\u001b[0m: examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory" + ] + } + ], + "source": [ + "### Select station locations that fall within the HUC8 watershed\n", + "\n", + "# Path to the watershed shapefile that was just created\n", + "watershed = f'{domain_data_path}East-Taylor_14020001.shp'\n", + "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", + "\n", + "# Create GeoDataFrame of all available stations\n", + "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", + " avail_df,\n", + " geometry=gpd.points_from_xy(\n", + " avail_df.longitude,\n", + " avail_df.latitude\n", + " ),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", + "\n", + "# Combine watershed polygons into one geometry\n", + "watershed_union = watershed_gdf.geometry.unary_union\n", + "\n", + "# Filter stations that fall within the watershed\n", + "sites_in_watershed = filtered_all_stations_gdf[\n", + " filtered_all_stations_gdf.geometry.within(watershed_union)\n", + "].copy()\n", + "\n", + "sites_in_watershed.reset_index(drop=True, inplace=True)\n", + "\n", + "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "06a6b39b", + "metadata": {}, + "source": [ + "Plot these sites on a map. Then, hover over the pins to see the site names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1e3bc39", + "metadata": {}, + "outputs": [], + "source": [ + "## TODO: REPLACE WITH CSSI_EVALUATION.PLOTS FUNCTIONS\n", + "\n", + "# this may take a moment to load, but it should pop up in a new window\n", + "m = plot_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "354fc021", + "metadata": {}, + "source": [ + "## 4. Retrieve SNOTEL point observations and metadata from HydroData \n", + "Use the `hf.get_point_data()` function to retrieve daily, start-of-day SWE from SNOTEL sites:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d74eeccb", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a folder to save observations\n", + "isExist = os.path.exists(OBS_OutputFolder)\n", + "if isExist == True:\n", + " exit\n", + "else:\n", + " os.mkdir(OBS_OutputFolder)" + ] + }, + { + "cell_type": "markdown", + "id": "b1805ac2", + "metadata": {}, + "source": [ + "### 4a. Get HydroData Observed SWE\n", + "Gather the SNOTEL data for all stations within the watershed and save CSV:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0f2beb6", + "metadata": {}, + "outputs": [], + "source": [ + "# Request point observations data\n", + "data_df = hf.get_point_data(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", + " date_start=StartDate, date_end=EndDate,\n", + " huc_id=[huc_8_code], grid='conus1')\n", + " #polygon=watershed_bbox, polygon_crs=watershed_crs)\n", + "\n", + "# save\n", + "data_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL.csv', index=False)\n", + "\n", + "# Ensure date column is datetime\n", + "data_df[\"date\"] = pd.to_datetime(data_df[\"date\"])\n", + "\n", + "# View first five records\n", + "data_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "fb365fbf", + "metadata": {}, + "source": [ + "### 4b. Get Metadata for HydroData Observed SWE\n", + "Also, retrieve the metadata for the same stations we retrieved SWE observations for:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cb40a7c", + "metadata": {}, + "outputs": [], + "source": [ + "# Request site-level attributes for these sites\n", + "metadata_df = hf.get_point_metadata(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", + " date_start=StartDate, date_end=EndDate,\n", + " huc_id=['14020001'], grid='conus1')\n", + "\n", + "# save\n", + "metadata_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL_metadata.csv', index=False)\n", + "\n", + "# View first five records\n", + "metadata_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "d17b371a", + "metadata": {}, + "source": [ + "The metadata file is an important addition to the observations and it is recommended to always gather and save this for the observations you are using (particularly to support reproducibility within an open-science workflow). The saved file has useful attributes like site names, first and last date of available data, lat/lon, and the query URL. \n", + "\n", + "Additionally, the metadata contains **ParFlow-CONUS1 and ParFlow-CONUS2 `i,j` indices, which indicate the exact model domain grid cell the observation aligns with**. This is a useful HydroData feature that removes the need for users to manually match station latitude/longitude coordinates to the appropriate model grid cell, as this spatial mapping is handled directly within HydroData. We will use these indices below to extract PF-CONUS1 modeled SWE for each SNOTEL station in the section below. " + ] + }, + { + "cell_type": "markdown", + "id": "0e50455e", + "metadata": {}, + "source": [ + "## 5. Retrieve ParFlow-CONUS1 Modeled Snow Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "545a9d22", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a folder to save results\n", + "isExist = os.path.exists(MOD_OutputFolder)\n", + "if isExist == True:\n", + " exit\n", + "else:\n", + " os.mkdir(MOD_OutputFolder)" + ] + }, + { + "cell_type": "markdown", + "id": "56eb4bb4", + "metadata": {}, + "source": [ + "The following section retrieves ParFlow-CONUS1 data for each SNOTEL site within our HUC-08 watershed. The code identifies the CONUS1 `i,j` indices associated with each SNOTEL site, indicated in the `metadata_df`. It then extracts the CONUS1 modeled SWE output for the site and the period of interest, returning the result as a DataFrame. To fairly compare with SNOTEL, which reports SWE once daily at the start of the local day, model output is aggregated by day, using the argment `\"temporal_resolution\": \"daily\"`. Finally, the processed data is saved as a CSV file for each site. \n", + "\n", + "### 5a. ParFlow CONUS1 Model Dataset Information\n", + "We can print some information about the model output dataset by using the `hf.get_catalog_entry()` to get the CONUS1 model dataset metadata. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10647da1", + "metadata": {}, + "outputs": [], + "source": [ + "conus1_options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\"\n", + "}\n", + "hf.get_catalog_entry(conus1_options)" + ] + }, + { + "cell_type": "markdown", + "id": "c6fd1306", + "metadata": {}, + "source": [ + "Before we gather model outputs at the specific SNOTEL sites, we can visualize SWE across our HUC-08. This is plotted for one day at 1km lateral resolution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba48a33a", + "metadata": {}, + "outputs": [], + "source": [ + "# retrieve gridded PF-CONUS1 SWE for the entire HUC8 watershed\n", + "grid_swe_options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\",\n", + " \"temporal_resolution\": \"daily\",\n", + " \"start_time\": '2004-04-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", + " \"end_time\": '2004-04-02',\n", + " \"huc_id\": huc_8_code\n", + " }\n", + " \n", + " # Get gridded data\n", + "grid_swe = hf.get_gridded_data(grid_swe_options)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b82b9574", + "metadata": {}, + "outputs": [], + "source": [ + "grid_swe_map = xr.DataArray(grid_swe[0], dims=(\"y\", \"x\"), name=\"SWE\")\n", + "grid_swe_map.hvplot.image(cmap=\"YlGnBu\", colorbar=True, aspect=\"equal\", title=f\"{huc_8_name} Gridded SWE on 2004-04-01\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "73a13787", + "metadata": {}, + "source": [ + "Now, grab the PF-CONUS1 modeled SWE from the SNOTEL site locations. Here we use the CONUS1 i and j indices from the `metadata_df` and grab the SWE from those grid cells. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17143151", + "metadata": {}, + "outputs": [], + "source": [ + "# Copy data_df to model_df so we have the same timestamps and site_id structure\n", + "model_df = data_df.copy()\n", + "\n", + "# Set all non-date columns to NaN to prepare for filling in model data\n", + "non_date_cols = model_df.columns.difference([\"date\"])\n", + "model_df[non_date_cols] = np.nan\n", + "\n", + "# Rename site_id columns for PF outputs \n", + "model_df.columns = [\n", + " col if col == \"date\" else col.replace(\":SNTL\", \"\") + \":PFCONUS1\"\n", + " for col in model_df.columns\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "523bd35c", + "metadata": {}, + "source": [ + "Use the function `hf.get_gridded_data()` and PF-CONUS1 `i,j` indices to select the SWE output for the correct location and time period: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a814204c", + "metadata": {}, + "outputs": [], + "source": [ + "# Loop over each station in metadata_df\n", + "for idx, row in metadata_df.iterrows():\n", + " site_id = row[\"site_id\"] # original SNTL site_id\n", + " col_name = site_id.replace(\":SNTL\", \"\") + \":PFCONUS1\" # corresponding column in model_df\n", + " conus_i = int(row[\"conus1_i\"])\n", + " conus_j = int(row[\"conus1_j\"])\n", + " \n", + " # Build options dict for this station\n", + " options = {\n", + " \"dataset\": \"conus1_baseline_mod\",\n", + " \"variable\": \"swe\",\n", + " \"temporal_resolution\": \"daily\",\n", + " \"start_time\": '2003-10-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", + " \"end_time\": '2005-10-01',\n", + " \"grid_point\": [conus_i, conus_j]\n", + " }\n", + " \n", + " # Get gridded data\n", + " data = hf.get_gridded_data(options)\n", + " \n", + " # Fill column in model_df\n", + " # Convert to numeric in case hf returns lists or other types\n", + " model_df[col_name] = np.squeeze(np.array(data))\n", + "\n", + "# Ensure date column is datetime\n", + "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", + "\n", + "# Save\n", + "model_df.to_csv(f'./{MOD_OutputFolder}/df_{huc_8_name}_{huc_8_code}_PFCONUS1.csv', index=False)\n", + " \n", + "model_df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "7464828b", + "metadata": {}, + "source": [ + "## 6. Quick plot sanity check \n", + "Plot a simple timeseries of modeled and observed SWE to make sure our data retrieval was successful. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fbe43f6a", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "\n", + "ax.plot(data_df[\"date\"], model_df[\"380:CO:PFCONUS1\"], label=\"Modeled\", linewidth=2)\n", + "\n", + "ax.plot(data_df[\"date\"], data_df[\"380:CO:SNTL\"], label=\"Observed\", linewidth=2)\n", + "\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"SWE (mm)\")\n", + "\n", + "# Date formatting for x-axis\n", + "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", + "ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))\n", + "\n", + "ax.legend(loc='upper left')\n", + "ax.grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "da3df109", + "metadata": {}, + "source": [ + "# Start of comparison from old notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Spatial Mapping of the SNOTEL sites \n", + "Before evaluating model performance, we plot the GIS data associated with the records in the combined DataFrame. The map below shows the SNOTEL stations included in the evaluation dataset, along with the watershed boundary used for the model simulations. Hover over the pins to see the site names. \n", + "\n", + "We also print a table of the SNOTEL site metadata to help with the single site selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Path to the watershed shapefile\n", + "watershed = \"./domain_data/East-Taylor_14020001.shp\"\n", + "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", + "\n", + "# Create GeoDataFrame of all available stations\n", + "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", + " metadata_df,\n", + " geometry=gpd.points_from_xy(\n", + " metadata_df.longitude,\n", + " metadata_df.latitude\n", + " ),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", + "\n", + "# Combine watershed polygons into one geometry\n", + "watershed_union = watershed_gdf.geometry.unary_union\n", + "\n", + "# Filter stations that fall within the watershed\n", + "sites_in_watershed = filtered_all_stations_gdf[\n", + " filtered_all_stations_gdf.geometry.within(watershed_union)\n", + "].copy()\n", + "\n", + "sites_in_watershed.reset_index(drop=True, inplace=True)\n", + "\n", + "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", + "\n", + "m = nwm_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sites_in_watershed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Compare Modeled and Observed SWE Timeseries at a Single Site\n", + "\n", + "Once we have both observation data and modeling outpus, it's important to evaluate how well the model reproduces observed data. The following plots are simple timeseries comparisons of **modeled vs. observed** SWE. These types of plots provide a straight-forward visual of how well the observations and simulations agree and are a great start for assessing general model performance. \n", + "\n", + "📊 We include two figures:\n", + "\n", + "1. **Time Series Overlay:** Plots the observed and modeled values together over time. This helps identify:\n", + " - Periods of systematic bias\n", + " - Timing differences in peaks and lows\n", + " - General agreement in trends\n", + "\n", + "2. **Scatter Plot with 1:1 Line:** Plots each modeled value against its corresponding observed value. This highlights:\n", + " - Accuracy across the full range of values\n", + " - Over- or under-prediction patterns\n", + " - Outliers or extreme events" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Review the sites within the watershed from the interactive map above and click on the markers to view the site name and code. Recall, we also printed out the site metadata for all sites within the watershed, which contains the 3-letter site codes.\n", + "\n", + "✏️ Once you’ve identified the site of interest, **enter its site code in the next code cell for `my_site_code`**: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# choose a site of interest within the watershed\n", + "my_site_code = '380:CO:'\n", + "\n", + "############################ THIS BELOW DOESNT WORK BECAUSE CODE IS NOT COMPLETE\n", + "# filter to only that site\n", + "sites_in_watershed[sites_in_watershed['site_id']==my_site_code]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nwm_utils.comparison_plots(combined_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To move beyond an overall summary of daily performance, we replot the modeled vs. observed SWE scatter while highlighting specific months with a distinct color. This gives us more information about the **seasonal model performance**. \n", + "\n", + "Let's customize the scatter plot by allowing you to highlight specific months with a distinct color. The selected months will appear in one color, while all other months will appear in a different color. This customization reveals whether there are **seasonal patterns** in the relationship between observed and modeled SWE, allowing us to distinguish model behavior during the key snowpack phases of accumulation and ablation (melt). Identifying these patterns is important for diagnosing the model’s strengths and limitations during different parts of the snow season.\n", + "\n", + "You can change the list of highlighted months (for example, October–December for early accumulation or March–May for spring melt) to explore in the scatter plot how model performance varies across different parts of the snow season. This seasonal perspective motivates the _peak SWE analysis_ that follows." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 For this example, let's highlight the _early snow accumulation period_ of October - January:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "combined_df['month'] = combined_df.index.month\n", + "\n", + "plot = nwm_utils.plot_custom_scatter_SWE(combined_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1',\n", + " highlight_months=[10, 11, 12, 1])\n", + "plot " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

What does this plot tell us about how well the model performs during the early snow accumulation period at this site?
\n", + "HINT: How close are the green points to the 1:1 line?

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Peak SWE Evaluation at the Watershed Scale \n", + "As we saw in the previous section, how well a model matches observations can differ greatly throughout the year. The following section focuses on **peak SWE** (or maximum SWE) analysis. \n", + "\n", + "**Peak SWE is a key diagnostic for snow-dominated hydrologic systems** because it represents the maximum amount of liquid water stored in the snowpack before the spring melt. Evaluating both the magnitude (quantity) and timing (date) of peak SWE provides insight into whether the model is accurately representing snow accumulation and seasonal energy balance. \n", + "\n", + "Errors in peak SWE can have important hydrologic consequences, as peak accumulation strongly influences:\n", + "- The volume of water available for spring runoff\n", + "- The timing of streamflow peaks\n", + "- Soil moisture recharge and groundwater contributions\n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "_Example daily SWE at a single site, showing two important periods in snow processes: accumulation (before peak) and ablation (after peak). The vertical line marks peak SWE._" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.1 Comparing Modeled and Observed Peak SWE at All Sites in the Watershed\n", + "In this section, we evaluate observed and modeled peak SWE for all stations within our watershed and for all years selected in the `StartDate` and `EndDate` above. \n", + "\n", + "#### 📋 Modeled SWE on the Date of Observed Peak SWE (magnitude) \n", + "This comparison evaluates the modeled SWE on the **specific day when observed SWE reaches its maximum.** By fixing the timing to the observed peak, this comparison isolates errors in SWE magnitude. \n", + "It answers the question: *How much SWE does the model simulate on the day the observed snowpack reaches its maximum?*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# isolate the columns associated with observations and model predictions.\n", + "# these will be inputs to our same-day comparison function.\n", + "obs_cols = sorted([col for col in combined_df.columns if col.endswith('SNTL')])\n", + "mod_cols = sorted([col for col in combined_df.columns if col.endswith('PFCONUS1')])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# compute the same-day SWE comparison during the observed peak SWE for each of the observation and modeled sites.\n", + "df_observed_peak = utils.modeled_swe_at_observed_peak(combined_df, obs_cols, mod_cols)\n", + "df_observed_peak" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 Visualize the amount of SWE on **the day of observed peak SWE occurs** for both the model and observations at each station" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Rearrange the dataframe to long format for easier plotting\n", + "df_long = (\n", + " df_observed_peak\n", + " .reset_index() \n", + " .melt(\n", + " id_vars=['Station', 'Water_Year', 'date'],\n", + " value_vars=['Observed', 'Modeled'],\n", + " var_name='Source',\n", + " value_name='SWE'\n", + " )\n", + ")\n", + "# Create scatter plot of observed and modeled SWE on the day of observed peak SWE\n", + "scatter_obs_peak = df_long.hvplot.scatter(\n", + " x='Station',\n", + " y='SWE',\n", + " by='Source', # Observed vs Modeled\n", + " ylabel='SWE on Observed Peak Day (mm)',\n", + " title='Observed and Modeled SWE on the Day of Observed Peak SWE',\n", + " size=70,\n", + " width=700,\n", + " height=450,\n", + " alpha=0.8,\n", + " hover_cols=['Water_Year'],\n", + " rot=45\n", + ")\n", + "\n", + "# Customize the scatter plot appearance\n", + "scatter_by_station = (\n", + " scatter_obs_peak \n", + " .opts(legend_position='top_right')\n", + ")\n", + "\n", + "scatter_by_station" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 📋 Modeled vs Observed Peak SWE Comparison (timing & magnitude) \n", + "This comparison evaluates the modeled and observed peak SWE values and their corresponding dates independently. Unlike the previous comparison that fixed the timing to the observed peak swe, this analysis shows the actual days of modeled and observed peak SWE, which may occur on different dates. As a result, it captures errors in both **peak SWE magnitude** and **peak timing**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# compute the different-day SWE comparison for each of the observed and modeled sites.\n", + "df_both_peak = utils.modeled_vs_observed_peak_swe(combined_df, obs_cols, mod_cols)\n", + "df_both_peak" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 Visualize the quantity of peak SWE for both the model and observations at each station" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "### NEED TO DECIDE HOW TO FORMAT THIS PLOT AND IF WE WANT TO HAVE THE \"SAME_DAY\" PLOT\n", + "\n", + "# Create the scatter plot\n", + "scatter_plot_both_peak = df_both_peak.hvplot.scatter(\n", + " x='Observed',\n", + " y='Modeled',\n", + " xlabel='Observed SWE (mm)',\n", + " ylabel='Modeled SWE (mm)',\n", + " title='Modeled vs. Observed Peak SWE',\n", + " size=35,\n", + " width=500,\n", + " height=400,\n", + " color='#E69F00',\n", + " hover_cols=['Station', 'Water_Year']\n", + ")#.relabel('Peak SWE')\n", + "\n", + "# Add 1:1 line (perfect match line)\n", + "swe_max = df_both_peak[['Observed', 'Modeled']].max().max()\n", + "\n", + "one_to_one_line = hv.Curve(([0, swe_max], [0, swe_max])).opts(\n", + " color='gray',\n", + " line_dash='dashed',\n", + " line_width=1,\n", + ").relabel('1:1 Line')\n", + "\n", + "# Combine scatter plot and 1:1 line into an Overlay\n", + "scatter_with_line = (scatter_plot_both_peak * one_to_one_line).opts( #scatter_plot_obs_peak * \n", + " legend_position='bottom_right'\n", + ")\n", + "\n", + "scatter_with_line" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 Visualizing Model Error for Peak SWE\n", + "\n", + "The previous scatter plots indicate that the modeled and observed peak SWE magnitude and timing don't always align. Next, we plot the degree to which \n", + "\n", + "The previous scatter plots highlight differences between modeled and observed peak SWE timing and magnitude, but interpreting these variations can be challenging when comparing modeled and observed values directly. To make these differences more explicit, we compute errors in both peak timing and peak SWE magnitude and visualize them directly. This approach clarifies both the direction and magnitude of model bias and facilitates comparison across stations and water years.\n", + "\n", + "First, add columns `Peak_Date_Diff_Days` and `Peak_SWE_Diff` to the DataFrame `df_both_peak` for computed difference in peak SWE date difference and peak SWE quantity between modeled and observed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute the difference in peak SWE days and peak SWE amounts between modeled and observed\n", + "df_both_peak['Peak_Date_Diff_Days'] = (df_both_peak['Modeled_Date'] - \n", + " df_both_peak['Observed_Date']).dt.days\n", + "df_both_peak['Peak_SWE_Diff'] = (df_both_peak['Modeled'] - \n", + " df_both_peak['Observed'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df02795e", + "metadata": {}, + "outputs": [], + "source": [ + "df_both_peak" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 📊 Visualize the error between the modeled and observed peak SWE " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter to separate each water year\n", + "year1 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].min()]\n", + "year2 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].max()]\n", + "\n", + "bar1 = year1.hvplot.bar(\n", + " x='Station',\n", + " y='Peak_Date_Diff_Days',\n", + " rot=45,\n", + " ylabel='Date Difference (days)',\n", + " title=f'Peak SWE Date Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", + " width=400,\n", + " height=400,\n", + " color='Peak_Date_Diff_Days',\n", + " hover_cols=['Modeled', 'Observed']\n", + ")\n", + "bar2 = year1.hvplot.bar(\n", + " x='Station',\n", + " y='Peak_SWE_Diff',\n", + " rot=45,\n", + " ylabel='SWE Difference (m)',\n", + " title=f'Peak SWE Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", + " width=400,\n", + " height=400,\n", + " color='Peak_SWE_Diff',\n", + " hover_cols=['Modeled', 'Observed']\n", + ")\n", + "\n", + "# Combine side by side\n", + "layout = (bar1 + bar2)\n", + "layout.opts(shared_axes=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The left panel shows the timing error (date difference) and the right panel the magnitude error (SWE difference). When we computed the difference in date and SWE quantity above, we took `modeled - observed` so: \n", + "\n", + "| | DATE OF PEAK SWE | PEAK SWE QUANTITY |\n", + "|---|---|---|\n", + "| + Positive Values | modeled AFTER observed | modeled GREATER THAN observed |\n", + "| - Negative Values | modeled BEFORE observed | modeled LESS THAN observed | " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE both earlier and less than the observed peak SWE? Perhaps try changing the my_site_code from earlier in the notebook to rerun nwm_utils.comparison_plots() to see the timeseries for a different station to look at the peak magnitude and timing. \n", + "\n", + "
What happens if you change the year that is plotted?
✏️ Try modifying the bar plot code from bar1 = year1.hvplot.bar to bar1 = year2.hvplot.bar. Don't forget to change the title!

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 📊 Next, we combine the timing and magnitude errors and plot them together for each station." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "scatter = df_both_peak.hvplot.scatter(\n", + " x='Peak_Date_Diff_Days',\n", + " y='Peak_SWE_Diff',\n", + " by='Station', # Water_Year\n", + " xlabel='Peak SWE Timing Error (days)',\n", + " ylabel='Peak SWE Magnitude Error (mm)',\n", + " title='Peak SWE Timing vs Magnitude Error',\n", + " size=80,\n", + " width=600,\n", + " height=400,\n", + " hover_cols=['Water_Year']\n", + ")\n", + "\n", + "# Add reference lines\n", + "vline = hv.VLine(0).opts(color='gray', line_dash='dashed')\n", + "hline = hv.HLine(0).opts(color='gray', line_dash='dashed')\n", + "\n", + "(scatter * vline * hline).opts(legend_position='top_left', show_grid=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "✏️ **Try changing how we view this plot.** \n", + "We can modify a line in the section of code from `by='Station'` to `by='Water_Year'` to better visualize the errors in the different Water Years. \n", + "Are there any patterns that jump out? Which year was modeled peak SWE consistently less than observed peak SWE? " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Compute and Statistics and Error Metrics \n", + "The previous section visualized when and where modeled SWE differs from observations, both in terms of peak SWE timing and magnitude. However, visual inspection alone makes it difficult to compare performance across sites or to summarize model behavior in a consistent or quantifiable way. In this section, we compute commonly used statistical error metrics to quantify model performance, allowing us to objectively assess bias, error magnitude, and variability for sites within the watershed. \n", + "\n", + "Proposed outline (DTK, Jan 2026):\n", + "- Summary metrics at a station\n", + "- Summary metrics at all stations within the watershed\n", + "- Combined timing and magnitude for all stations within the watershed (Condon metric)\n", + "- Focus on timing: summary statistics for single station for accumulation & ablation periods (using the new wrapper: `nwm_utils.compute_stats_period()`)\n", + "- Melt period statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "nwm_utils.compute_stats(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pearson and Spearman correlations are both close to 1, suggesting a strong relationship between observed and modeled SWE. As shown on the timeseries plot, this strong correlation alone does not indicate a \"good\" model. For example, it does not guarantee accurate timing of key events, such as peak SWE or melt onset. Let's compare these as well. The following code uses `report_max_dates_and_values` function to identify the peak SWE value and the date it occurs for both the observed (CCSS) and modeled (NWM) datasets. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why? As you review the results, compare the peak flow amounts and the timing of snowmelt onset. Do you see any significant differences? Which dataset indicates an earlier melt?

\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "summary_table = nwm_utils.report_max_dates_and_values(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n", + "summary_table" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Summary Metrics at Multiple Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "site_codes = ['DAN', 'HRS', 'KIB', 'PDS', 'SLI', 'TUM', 'WHW']\n", + "\n", + "rows = []\n", + "\n", + "for site in site_codes:\n", + " obs_col = f'CCSS_{site}_swe_m'\n", + " mod_col = f'NWM_{site}_swe_m'\n", + "\n", + " stats_table = nwm_utils.compute_stats(combined_df, obs_col, mod_col)\n", + "\n", + " rows.append({\n", + " 'Station': site,\n", + " 'Mean_Obs': stats_table.loc['observed', 'Mean'],\n", + " 'Mean_Mod': stats_table.loc['modeled', 'Mean'],\n", + " 'Bias_m': stats_table.loc['Bias (Modeled - Observed)', 'Mean'],\n", + " 'Pearson_r': stats_table.loc['Pearson Correlation', 'Mean'],\n", + " 'Spearman_r': stats_table.loc['Spearman Correlation', 'Mean'],\n", + " 'NSE': stats_table.loc['Nash-Sutcliffe Efficiency (NSE)', 'Mean'],\n", + " 'KGE': stats_table.loc['Kling-Gupta Efficiency (KGE)', 'Mean']\n", + " })\n", + "\n", + "stats_AllStations = pd.DataFrame(rows)\n", + "\n", + "print('All Stations Statistics Summary:')\n", + "stats_AllStations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats_AllStations.hvplot.bar(\n", + " x='Station',\n", + " y='NSE',\n", + " rot=45,\n", + " ylabel='Nash–Sutcliffe Efficiency',\n", + " title='NSE by Station',\n", + " height=400,\n", + " width=600,\n", + " bar_width=0.5\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats_summary.hvplot.scatter(\n", + " x='Station',\n", + " y='Bias_m',\n", + " size=100,\n", + " rot=45,\n", + " ylabel='Bias (m)',\n", + " title='Mean SWE Bias by Station'\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Combine Magnitude (absolute relative bias) and Timing (Spearman's rho) metrics using the Condon metric (and with all stations, a Condon diagram)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bias1 = evaluation_metrics.bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", + "bias1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "abs_bias = evaluation_metrics.absolute_relative_bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", + "abs_bias" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "srho = evaluation_metrics.spearman_rank(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", + "srho" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "evaluation_metrics.condon(abs_bias, srho)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

\n", + " What is the modeled SWE on the date when the observed SWE reaches its peak?
\n", + " ✏️ Use the code snippet below to find the answer.\n", + "

\n", + "
\n",
+    "  \n",
+    "    # Find date of the peak SWE from observed data\n",
+    "    date_obs_max = combined_df['CCSS_HRS_swe_m'].idxmax()\n",
+    "\n",
+    "    # Get corresponding value of modeled data on that date\n",
+    "    value_mod_at_max_obs = combined_df.loc[date_obs_max, 'NWM_HRS_swe_m']\n",
+    "  
\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Focus on Timing: Melt Period Metrics\n", + "Compare the average melt rate over the full melt period. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following function computes the melt period length by identifying the first date after the peak SWE when SWE drops to zero and remains at zero for at least (`min_zero_days`) consecutive days. This is used to define the end of the melt period. Finally, the function calculates the average melt rate, which represents the rate at which snow disappeared, expressed in meters per day, over the full melt period." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "melt_stats_df = utils.compute_melt_period_statistics(combined_df)\n", + "melt_stats_df.head()\n", + "melt_stats_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "observed_melt_period = nwm_utils.compute_melt_period(combined_df[f'CCSS_{my_site_code}_swe_m'])\n", + "observed_melt_period" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "modeled_melt_period = nwm_utils.compute_melt_period(combined_df[f'NWM_{my_site_code}_swe_m'])\n", + "modeled_melt_period" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "accum_months = [10, 11, 12, 1, 2, 3]\n", + "ablation_months = [4, 5, 6]\n", + "\n", + "accum_stats = nwm_utils.compute_stats_period(\n", + " combined_df,\n", + " f'CCSS_{my_site_code}_swe_m',\n", + " f'NWM_{my_site_code}_swe_m',\n", + " accum_months\n", + ")\n", + "\n", + "accum_stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "ablation_stats = nwm_utils.compute_stats_period(\n", + " combined_df,\n", + " f'CCSS_{my_site_code}_swe_m',\n", + " f'NWM_{my_site_code}_swe_m',\n", + " ablation_months\n", + ")\n", + "\n", + "ablation_stats" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

\n", + " If you recall from earlier, we plotted the timeseries of out selected station. Replot it below. Do the metrics make sense given the visual comparison between modeled and observed? For example, when you look at the timeseries, is the model consistently predicting SWE to be higher or lower than observations? Does this align with the Bias sign (+ or -)?\n", + "

\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nwm_utils.comparison_plots(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "b39724eb", + "metadata": {}, + "source": [ + "# SUBSET TOOLS - prob not keeping section" + ] + }, + { + "cell_type": "markdown", + "id": "0f9d1750", + "metadata": {}, + "source": [ + "Use the Subsettools function `define_huc_domain()` to get the actual CONUS1 indices associated with the East-Taylor HUC-O8. It returns a tuple `(imin, jmin, imax, jmax)` of grid indices that define a bounding box containing our region (or point) of interest (Note: (imin, jmin, imax, jmax) are the west, south, east and north boundaries of the box respectively) and a mask for that domain." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85d66c92", + "metadata": {}, + "outputs": [], + "source": [ + "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", + "\n", + "np.save(f'{domain_data_path}domainMask_{huc_8_name}_conus1.npy', mask)\n", + "\n", + "plt.imshow(mask, origin='lower')\n", + "print(ij_bounds)\n", + "print(mask.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "686a14a3", + "metadata": {}, + "source": [ + "Using the domain mask and the i,j PF-CONUS1 indices, we use a hf_hydrodata function to find and save the associated grid cell center lat/lon pair for each grid cell in the domain. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a4c1d59", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract bounds\n", + "i_min, j_min, i_max, j_max = ij_bounds\n", + "mask_shape = mask.shape #shape of the subset rectangular domain\n", + "\n", + "# Create i/j index ranges\n", + "i_vals = np.arange(i_min, i_max)\n", + "j_vals = np.arange(j_min, j_max)\n", + "\n", + "# Create full 2D grid (note indexing order carefully)\n", + "jj, ii = np.meshgrid(j_vals, i_vals, indexing=\"ij\")" + ] + }, + { + "cell_type": "markdown", + "id": "a11fac65", + "metadata": {}, + "source": [ + "Because the function `hf.to_latlon()` finds the coordinates at the lower left corner of a grid cell, we add 0.5 to each i,j index pair to find the **lat/lon at the grid cell center**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d44df5ab", + "metadata": {}, + "outputs": [], + "source": [ + "# Compute grid cell centers\n", + "ii_center = ii + 0.5\n", + "jj_center = jj + 0.5\n", + "\n", + "# Convert to lat/lon (vectorized loop)\n", + "lat = np.zeros(mask_shape)\n", + "lon = np.zeros(mask_shape)\n", + "\n", + "for r in range(mask_shape[0]):\n", + " for c in range(mask_shape[1]):\n", + " lat[r, c], lon[r, c] = hf.to_latlon(\"conus1\",\n", + " ii_center[r, c],\n", + " jj_center[r, c])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4066d61", + "metadata": {}, + "outputs": [], + "source": [ + "# Save 2D arrays of Lat & Lon\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", + "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lon_2d.npy\", lon)\n", + "\n", + "# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \n", + "grid_df = pd.DataFrame({\n", + " \"i\": ii.ravel(),\n", + " \"j\": jj.ravel(),\n", + " \"lat\": lat.ravel(),\n", + " \"lon\": lon.ravel(),\n", + "})\n", + "grid_df.to_csv(f\"{domain_data_path}df_{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.csv\", index=False)\n", + "\n", + "# Save a shapefile of the watershed Lat & Lon\n", + "grid_gdf = gpd.GeoDataFrame(\n", + " grid_df,\n", + " geometry=gpd.points_from_xy(grid_df.lon, grid_df.lat),\n", + " crs=\"EPSG:4326\"\n", + ")\n", + "# Save the grid points / GeoDataFrame to a shapefile for later use\n", + "grid_gdf.to_file(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.shp\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70296162", + "metadata": {}, + "outputs": [], + "source": [ + "grid_df" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "nwm_env", + "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 +} diff --git a/examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb deleted file mode 100644 index 20d26fc..0000000 --- a/examples/parflow/parflow_swe_point_scale_evaluation_copyAMY_Hydrodata_code.ipynb +++ /dev/null @@ -1,2993 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![NWM](../img/NWM.png)\n", - "\n", - "# Use HydroData to Retrieve Modeled and Observed Snow Data for a Watershed of Interest with ParFlow-CONUS Outputs vs Observed Snow Water Equivalent (SWE) - Full Evaluation Workflow\n", - "Authors: Irene Garousi-Nejad (igarousi@cuahsi.org), Danielle Tijerina-Kreuzer (dtijerina@cuahsi.org) \n", - "Last updated: Feb 2026" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Introduction: \n", - "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites. We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Prepare the Python Environment" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Import the libraries needed to run this notebook:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "ce97d33e", - "metadata": { - "tags": [] - }, - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.1/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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "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" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.jsdelivr.net/npm/@holoviz/geoviews@1.11.0/dist/geoviews.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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.jsdelivr.net/npm/@holoviz/geoviews@1.11.0/dist/geoviews.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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import os\n", - "import sys\n", - "from pathlib import Path\n", - "import holoviews as hv\n", - "import hvplot.pandas\n", - "import hvplot.xarray\n", - "import pyproj\n", - "import pandas as pd\n", - "import numpy as np\n", - "import xarray as xr\n", - "import geopandas as gpd\n", - "from dask.distributed import Client\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib.dates as mdates\n", - "import hf_hydrodata as hf\n", - "import subsettools\n", - "\n", - "\n", - "# Import the Evaluation library from the project root.\n", - "sys.path.append(str((Path.cwd().absolute() / \"../../src\").resolve()))\n", - "\n", - "from cssi_evaluation.variables import snow_utils\n", - "from cssi_evaluation.utils import metric_utils\n", - "from cssi_evaluation.utils import evaluation_utils\n", - "from cssi_evaluation.utils import plot_utils\n", - "\n", - "hv.extension('bokeh')\n", - "\n", - "\n", - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "markdown", - "id": "e86aae63", - "metadata": {}, - "source": [ - "## 1. Setup" - ] - }, - { - "cell_type": "markdown", - "id": "e88ed1ef", - "metadata": {}, - "source": [ - "### 1a. Python Environment \n", - "\n", - "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." - ] - }, - { - "cell_type": "markdown", - "id": "c0f30927", - "metadata": {}, - "source": [ - "Import the libraries needed to run this notebook:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0dfc3fb3", - "metadata": {}, - "outputs": [], - "source": [ - "# import os\n", - "# import sys\n", - "\n", - "# prefix = os.environ['CONDA_PREFIX']\n", - "# os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", - "\n", - "# # add the src directory to the path so we can import evaluation modules\n", - "# sys.path.append('../../src/')\n", - "\n", - "# import sys\n", - "# import pyproj\n", - "# import pandas as pd\n", - "# import numpy as np\n", - "# import xarray as xr\n", - "# import geopandas as gpd\n", - "# from dask.distributed import Client\n", - "# import matplotlib.pyplot as plt\n", - "# import matplotlib.dates as mdates\n", - "# import hf_hydrodata as hf\n", - "# import subsettools\n", - "# import hvplot.xarray\n", - "\n", - "\n", - "# from cssi_evaluation.utils import plot_utils\n", - "\n", - "\n", - "# %load_ext autoreload\n", - "# %autoreload 2\n" - ] - }, - { - "cell_type": "markdown", - "id": "8e058228", - "metadata": {}, - "source": [ - "### 1b. Register Pin and Access HydroData\n", - "\n", - "To access the HydroData catalog you will need to sign up for a [HydroFrame account](https://hydrogen.princeton.edu/signup) (do this only once), [create a 4-digit PIN](https://hydrogen.princeton.edu/pin), and register your pin in order to have access to the HydroData datasets (you will do this in the next code cell below). To note, you PIN will expire after 7 days and will need to recreate it after that time. " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "a365996d", - "metadata": {}, - "outputs": [], - "source": [ - "# You need to register on https://hydrogen.princeton.edu/pin \n", - "# and run the following with your registered information\n", - "# before you can use the hydrodata utilities\n", - "hf.register_api_pin(\"dtt2@princeton.edu\", \"7837\")" - ] - }, - { - "cell_type": "markdown", - "id": "825c288d", - "metadata": {}, - "source": [ - "### 1c. Dask \n", - "\n", - "We'll use dask to parallelize our code. To manage parallel computation and visualize progress of long-running tasks, we initialize a Dask “cluster,” which defines how many workers are used and how much computing power each worker has. \n", - "\n", - "In this setup, we create a Dask client with `Client(n_workers=6, threads_per_worker=1, memory_limit='2GB')`, which launches a cluster with 6 workers. Each worker uses a single thread, typically mapped to one CPU core, allowing for efficient parallel processing across 6 cores. Each worker also has a memory limit of 2 GB, for a total of up to 12 GB across the cluster.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "6f2b08d0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dashboard link: http://127.0.0.1:8787/status\n", - "\n" - ] - } - ], - "source": [ - "# use a try accept loop so we only instantiate the client\n", - "# if it doesn't already exist.\n", - "try:\n", - " print('Dashboard link:', client.dashboard_link)\n", - "except: \n", - " # The client should be customized to your workstation resources.\n", - " client = Client(n_workers=6, threads_per_worker=1, memory_limit='2GB') \n", - " print('Dashboard link:', client.dashboard_link)\n", - "print(client)" - ] - }, - { - "cell_type": "markdown", - "id": "b8620cfc", - "metadata": {}, - "source": [ - "## 2. Set Paths" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e50dc99d", - "metadata": {}, - "outputs": [], - "source": [ - "# Start and end times of a water year (to note, these dates were chosen to align with the PFCONUS1 early 2000s runs)\n", - "StartDate = '2003-10-01'\n", - "EndDate = '2005-09-30'\n", - "\n", - "domain_data_path = './domain_data/' # path to the model domain data\n", - "\n", - "# Path to save results (obs and mod stands for observation and modeled, respectively)\n", - "OBS_OutputFolder = './obs_outputs' \n", - "MOD_OutputFolder = './mod_outputs'" - ] - }, - { - "cell_type": "markdown", - "id": "feb58871", - "metadata": {}, - "source": [ - "## 3. Retrieve Observed Snow Data " - ] - }, - { - "cell_type": "markdown", - "id": "45ca2832", - "metadata": {}, - "source": [ - "### 3a. Define the watershed of interest\n", - "\n", - "One of the simplest ways to gather data and model output from HydroData is by specifying a [Hydrologic Unit Code](https://www.usgs.gov/national-hydrography/watershed-boundary-dataset). Before we retrieve any hydrologic information, we need to indicate a HUC8 code and use it to gather snow water equivalent (SWE) observations from SNOTEL sites \n", - "\n", - "✏️ If you have a specific HUC8 in mind, you can change the variable `huc_8_code` below." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "c8355563", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HUC-8 ID: 14020001\n", - "HUC-8 name: East-Taylor\n" - ] - } - ], - "source": [ - "# ✏️ Specify HUC8 ID and Name for watershed of interest\n", - "huc_8_code = '14020001' # East-Taylor HUC-8\n", - "print(f'HUC-8 ID: {huc_8_code}')\n", - "\n", - "huc_8_name = 'East-Taylor'\n", - "print(f'HUC-8 name: {huc_8_name}')" - ] - }, - { - "cell_type": "markdown", - "id": "4fafdcad", - "metadata": {}, - "source": [ - "# SUBSET TOOLS - prob not keeping section" - ] - }, - { - "cell_type": "markdown", - "id": "5de02c3b", - "metadata": {}, - "source": [ - "Use the Subsettools function `define_huc_domain()` to get the actual CONUS1 indices associated with the East-Taylor HUC-O8. It returns a tuple `(imin, jmin, imax, jmax)` of grid indices that define a bounding box containing our region (or point) of interest (Note: (imin, jmin, imax, jmax) are the west, south, east and north boundaries of the box respectively) and a mask for that domain." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "965bd6ea", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(928, 617, 996, 666)\n", - "(49, 68)\n" - ] - }, - { - "data": { - "image/png": "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zJs3L5544omxLmnMPP7443HNNddEfX19FAqF+PWvf93vfEop1qxZE/X19TFlypRYsmRJ7N27d2yKHQOtra1x+eWXR1VVVZx//vnxkY98JPbv39/vmoneo40bN8acOXOiuro6qqurY+HChfHQQw8Vz0/0/pxMa2trFAqFaG5uLo5N9D6tWbMmCoVCv6Ourq54fqL357i//e1v8alPfSrOO++8mDp1alx66aWxc+fO4vmJ3KeSDh+//OUvo7m5OVavXh27du2K97znPbFs2bL461//OtaljYnDhw/H3LlzY8OGDSc9v3bt2li3bl1s2LAhduzYEXV1dbF06dLo7e3NXOnYaG9vj5UrV8aTTz4ZbW1tcfTo0WhqaorDhw8Xr5noPZoxY0bcfvvt8dRTT8VTTz0V73//+2P58uXFX3gTvT+vtmPHjrjzzjtjzpw5/cb1KeId73hHPP/888Vjz549xXP6E3Ho0KG44oorYtKkSfHQQw/Fvn374v/+7//iDW94Q/GaCd2nYX97XAbvete70g033NBv7KKLLkrf+MY3xqii0hER6f777y8+fvnll1NdXV26/fbbi2P/+c9/Uk1NTfrRj340BhWOva6urhQRqb29PaWkR6dyzjnnpJ/85Cf68yq9vb2psbExtbW1pcWLF6ebb745peR9lFJKt912W5o7d+5Jz+nPK77+9a+nK6+88pTnJ3qfSnbm48iRI7Fz585oamrqN97U1BTbt28fo6pK14EDB6Kzs7NfvyorK2Px4sUTtl/d3d0REXHuuedGhB692rFjx2LLli1x+PDhWLhwof68ysqVK+Pqq6+OD3zgA/3G9ekVzzzzTNTX10dDQ0N8/OMfj2effTYi9Oe4Bx98MObPnx8f+9jH4vzzz4/LLrss7rrrruL5id6nkg0f//jHP+LYsWNRW1vbb7y2tjY6OzvHqKrSdbwn+vWKlFLccsstceWVV8bs2bMjQo+O27NnT5x99tlRWVkZN9xwQ9x///1x8cUX688JtmzZEn/4wx+itbV1wDl9iliwYEH87Gc/i0ceeSTuuuuu6OzsjEWLFsU///lP/fmvZ599NjZu3BiNjY3xyCOPxA033BBf/epX42c/+1lEeB9VjHUBgykUCv0ep5QGjPE/+vWKm266KZ5++un43e9+N+DcRO/R2972tti9e3f861//il/96ldx/fXXR3t7e/H8RO9PR0dH3HzzzbF169Y466yzTnndRO7TsmXLiv99ySWXxMKFC+Otb31rbNq0Kd797ndHxMTuT0TEyy+/HPPnz4+WlpaIiLjsssti7969sXHjxvjMZz5TvG6i9qlkZz7e+MY3xutf//oBCbCrq2tAUiSKd5rrV8RXvvKVePDBB+Oxxx6LGTNmFMf16BWTJ0+OCy+8MObPnx+tra0xd+7c+P73v68//7Vz587o6uqKefPmRUVFRVRUVER7e3v84Ac/iIqKimIvJnqfTjRt2rS45JJL4plnnvE++q/p06fHxRdf3G/s7W9/e3HBxETvU8mGj8mTJ8e8efOira2t33hbW1ssWrRojKoqXQ0NDVFXV9evX0eOHIn29vYJ06+UUtx0001x3333xaOPPhoNDQ39zuvRyaWUoq+vT3/+66qrroo9e/bE7t27i8f8+fPjk5/8ZOzevTve8pa36NOr9PX1xR//+MeYPn2699F/XXHFFQOW+v/pT3+KCy64ICL8Pirp1S5btmxJkyZNSj/96U/Tvn37UnNzc5o2bVr6y1/+MtaljYne3t60a9eutGvXrhQRad26dWnXrl3p4MGDKaWUbr/99lRTU5Puu+++tGfPnvSJT3wiTZ8+PfX09Ixx5Xl8+ctfTjU1NWnbtm3p+eefLx4vvPBC8ZqJ3qNVq1alxx9/PB04cCA9/fTT6Zvf/GZ63etel7Zu3ZpS0p9TOXG1S0r69LWvfS1t27YtPfvss+nJJ59MH/7wh1NVVVXxd/NE709KKf3+979PFRUV6bvf/W565pln0s9//vM0derUdO+99xavmch9KunwkVJKP/zhD9MFF1yQJk+enN75zncWl01ORI899liKiAHH9ddfn1J6ZenWbbfdlurq6lJlZWV673vfm/bs2TO2RWd0st5ERLr77ruL10z0Hn3+858v/n1605velK666qpi8EhJf07l1eFjovfp2muvTdOnT0+TJk1K9fX1acWKFWnv3r3F8xO9P8f95je/SbNnz06VlZXpoosuSnfeeWe/8xO5T4WUUhqbORcAYCIq2Xs+AIDxSfgAALISPgCArIQPACAr4QMAyEr4AACyEj4AgKyEDwAgK+EDAMhK+AAAshI+AICshA8AIKv/B/Uxx4GPSUf5AAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", - "\n", - "np.save(f'{domain_data_path}domainMask_{huc_8_name}_conus1.npy', mask)\n", - "\n", - "plt.imshow(mask, origin='lower')\n", - "print(ij_bounds)\n", - "print(mask.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "61745fb2", - "metadata": {}, - "source": [ - "Using the domain mask and the i,j PF-CONUS1 indices, we use a hf_hydrodata function to find and save the associated grid cell center lat/lon pair for each grid cell in the domain. " - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "1ff5c1d4", - "metadata": {}, - "outputs": [], - "source": [ - "# Extract bounds\n", - "i_min, j_min, i_max, j_max = ij_bounds\n", - "mask_shape = mask.shape #shape of the subset rectangular domain\n", - "\n", - "# Create i/j index ranges\n", - "i_vals = np.arange(i_min, i_max)\n", - "j_vals = np.arange(j_min, j_max)\n", - "\n", - "# Create full 2D grid (note indexing order carefully)\n", - "jj, ii = np.meshgrid(j_vals, i_vals, indexing=\"ij\")" - ] - }, - { - "cell_type": "markdown", - "id": "c6ae90bf", - "metadata": {}, - "source": [ - "Because the function `hf.to_latlon()` finds the coordinates at the lower left corner of a grid cell, we add 0.5 to each i,j index pair to find the **lat/lon at the grid cell center**." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "13fbc81f", - "metadata": {}, - "outputs": [], - "source": [ - "# Compute grid cell centers\n", - "ii_center = ii + 0.5\n", - "jj_center = jj + 0.5\n", - "\n", - "# Convert to lat/lon (vectorized loop)\n", - "lat = np.zeros(mask_shape)\n", - "lon = np.zeros(mask_shape)\n", - "\n", - "for r in range(mask_shape[0]):\n", - " for c in range(mask_shape[1]):\n", - " lat[r, c], lon[r, c] = hf.to_latlon(\"conus1\",\n", - " ii_center[r, c],\n", - " jj_center[r, c])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "aa20e59f", - "metadata": {}, - "outputs": [], - "source": [ - "# Save 2D arrays of Lat & Lon\n", - "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", - "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lon_2d.npy\", lon)\n", - "\n", - "# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \n", - "grid_df = pd.DataFrame({\n", - " \"i\": ii.ravel(),\n", - " \"j\": jj.ravel(),\n", - " \"lat\": lat.ravel(),\n", - " \"lon\": lon.ravel(),\n", - "})\n", - "grid_df.to_csv(f\"{domain_data_path}df_{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.csv\", index=False)\n", - "\n", - "# Save a shapefile of the watershed Lat & Lon\n", - "grid_gdf = gpd.GeoDataFrame(\n", - " grid_df,\n", - " geometry=gpd.points_from_xy(grid_df.lon, grid_df.lat),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "# Save the grid points / GeoDataFrame to a shapefile for later use\n", - "grid_gdf.to_file(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.shp\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "41e8d63a", - "metadata": {}, - "outputs": [], - "source": [ - "grid_df" - ] - }, - { - "cell_type": "markdown", - "id": "0cecfac1", - "metadata": {}, - "source": [ - "# Get SWE from hydrodata" - ] - }, - { - "cell_type": "markdown", - "id": "84549c32", - "metadata": {}, - "source": [ - "### 3b. Explore the available SWE data in a watershed " - ] - }, - { - "cell_type": "markdown", - "id": "e088705e", - "metadata": {}, - "source": [ - "
\n", - "

📖 Did you know?

\n", - "

The Snow Telemetry (SNOTEL) network, managed by the USDA Natural Resources Conservation Service (NRCS), monitors snowpack conditions across key watersheds in the western United States to support water supply forecasting and climate monitoring. SNOTEL sites are fully automated stations that continuously measure snow water equivalent (SWE), snow depth, precipitation, temperature, and other meteorological variables throughout the year. Unlike manual snow survey programs, SNOTEL provides high-temporal-resolution observations that enable near–real-time assessment of snowpack evolution and interannual variability. These data are widely used for operational forecasting, drought assessment, and long-term climate analysis.

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "de83d6b6", - "metadata": {}, - "source": [ - "Explore what SWE data is available at sites within the HUC ID you specified that operated during WY2004 and WY2005. If you want to check other variables besides SWE, you can change the `variable` argument name. " - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "3aa8210e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
site_idsite_namesite_typeagencystatevariable_nameunitsdatasetvariabletemporal_resolution...latitudelongitudesite_query_urldate_metadata_last_updatedtz_cddoiconus1_iconus1_jconus2_iconus2_j
0380:CO:SNTLButteSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.89435-106.95327https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone942.0650.01372.01601.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.81982-106.58962https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone972.0638.01402.01589.0
\n", - "

2 rows × 24 columns

\n", - "
" - ], - "text/plain": [ - " site_id site_name site_type agency state \\\n", - "0 380:CO:SNTL Butte SNOTEL station NRCS CO \n", - "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO \n", - "\n", - " variable_name units dataset variable \\\n", - "0 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", - "1 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", - "\n", - " temporal_resolution ... latitude longitude \\\n", - "0 daily ... 38.89435 -106.95327 \n", - "1 daily ... 38.81982 -106.58962 \n", - "\n", - " site_query_url \\\n", - "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "\n", - " date_metadata_last_updated tz_cd doi conus1_i conus1_j conus2_i conus2_j \n", - "0 2023-03-07 PST None 942.0 650.0 1372.0 1601.0 \n", - "1 2023-03-07 PST None 972.0 638.0 1402.0 1589.0 \n", - "\n", - "[2 rows x 24 columns]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "avail_df = hf.get_site_variables(variable = \"swe\",\n", - " huc_id = [huc_8_code], grid = 'conus1',\n", - " date_start = StartDate, date_end = EndDate)\n", - "\n", - "# View first five records\n", - "avail_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "268915dc", - "metadata": {}, - "source": [ - "### 3c. Map the SNOTEL stations inside the HUC-08 watershed that have available data in the selected time range \n", - "To note here, we are using pre-loaded shape files for the East-Taylor HUC8, which are located in the `/domain_data/` directory." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "f5c95f67", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sites CRS: EPSG:4326\n", - "Total sites in watershed: 2\n" - ] - } - ], - "source": [ - "### Select station locations that fall within the HUC8 watershed\n", - "\n", - "# Path to the watershed shapefile that was just created\n", - "watershed = f'{domain_data_path}East-Taylor_14020001.shp'\n", - "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", - "\n", - "# Create GeoDataFrame of all available stations\n", - "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", - " avail_df,\n", - " geometry=gpd.points_from_xy(\n", - " avail_df.longitude,\n", - " avail_df.latitude\n", - " ),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", - "\n", - "# Combine watershed polygons into one geometry\n", - "watershed_union = watershed_gdf.geometry.unary_union\n", - "\n", - "# Filter stations that fall within the watershed\n", - "sites_in_watershed = filtered_all_stations_gdf[\n", - " filtered_all_stations_gdf.geometry.within(watershed_union)\n", - "].copy()\n", - "\n", - "sites_in_watershed.reset_index(drop=True, inplace=True)\n", - "\n", - "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "06a6b39b", - "metadata": {}, - "source": [ - "Plot these sites on a map. Then, hover over the pins to see the site names." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "a1e3bc39", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# this may take a moment to load, but it should pop up in a new window\n", - "m = plot_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", - "m" - ] - }, - { - "cell_type": "markdown", - "id": "354fc021", - "metadata": {}, - "source": [ - "## 4. Retrieve SNOTEL point observations and metadata from HydroData \n", - "Use the `hf.get_point_data()` function to retrieve daily, start-of-day SWE from SNOTEL sites:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d74eeccb", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a folder to save observations\n", - "isExist = os.path.exists(OBS_OutputFolder)\n", - "if isExist == True:\n", - " exit\n", - "else:\n", - " os.mkdir(OBS_OutputFolder)" - ] - }, - { - "cell_type": "markdown", - "id": "b1805ac2", - "metadata": {}, - "source": [ - "### 4a. Get HydroData Observed SWE\n", - "Gather the SNOTEL data for all stations within the watershed and save CSV:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "f0f2beb6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
date380:CO:SNTL680:CO:SNTL
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", - "
" - ], - "text/plain": [ - " date 380:CO:SNTL 680:CO:SNTL\n", - "0 2003-10-01 0.0 0.0\n", - "1 2003-10-02 0.0 0.0\n", - "2 2003-10-03 0.0 0.0\n", - "3 2003-10-04 0.0 0.0\n", - "4 2003-10-05 0.0 0.0" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Request point observations data\n", - "data_df = hf.get_point_data(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", - " date_start=StartDate, date_end=EndDate,\n", - " huc_id=[huc_8_code], grid='conus1')\n", - " #polygon=watershed_bbox, polygon_crs=watershed_crs)\n", - "\n", - "# save\n", - "data_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL.csv', index=False)\n", - "\n", - "# Ensure date column is datetime\n", - "data_df[\"date\"] = pd.to_datetime(data_df[\"date\"])\n", - "\n", - "# View first five records\n", - "data_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "fb365fbf", - "metadata": {}, - "source": [ - "### 4b. Get Metadata for HydroData Observed SWE\n", - "Also, retrieve the metadata for the same stations we retrieved SWE observations for:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "1cb40a7c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
site_idsite_namesite_typeagencystatelatitudelongitudefirst_date_data_availablelast_date_data_availablerecord_countsite_query_urldate_metadata_last_updatedtz_cddoihuc8conus1_iconus1_jconus2_iconus2_jusda_elevation
0380:CO:SNTLButteSNOTEL stationNRCSCO38.89435-106.953271981-10-012026-03-2116243https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone14020001942.0650.01372.01601.010200.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCO38.81982-106.589621980-08-042026-03-2116666https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone14020001972.0638.01402.01589.09621.0
\n", - "
" - ], - "text/plain": [ - " site_id site_name site_type agency state latitude longitude \\\n", - "0 380:CO:SNTL Butte SNOTEL station NRCS CO 38.89435 -106.95327 \n", - "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO 38.81982 -106.58962 \n", - "\n", - " first_date_data_available last_date_data_available record_count \\\n", - "0 1981-10-01 2026-03-21 16243 \n", - "1 1980-08-04 2026-03-21 16666 \n", - "\n", - " site_query_url \\\n", - "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "\n", - " date_metadata_last_updated tz_cd doi huc8 conus1_i conus1_j \\\n", - "0 2023-03-07 PST None 14020001 942.0 650.0 \n", - "1 2023-03-07 PST None 14020001 972.0 638.0 \n", - "\n", - " conus2_i conus2_j usda_elevation \n", - "0 1372.0 1601.0 10200.0 \n", - "1 1402.0 1589.0 9621.0 " - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Request site-level attributes for these sites\n", - "metadata_df = hf.get_point_metadata(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", - " date_start=StartDate, date_end=EndDate,\n", - " huc_id=['14020001'], grid='conus1')\n", - "\n", - "# save\n", - "metadata_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL_metadata.csv', index=False)\n", - "\n", - "# View first five records\n", - "metadata_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "d17b371a", - "metadata": {}, - "source": [ - "The metadata file is an important addition to the observations and it is recommended to always gather and save this for the observations you are using (particularly to support reproducibility within an open-science workflow). The saved file has useful attributes like site names, first and last date of available data, lat/lon, and the query URL. \n", - "\n", - "Additionally, the metadata contains **ParFlow-CONUS1 and ParFlow-CONUS2 `i,j` indices, which indicate the exact model domain grid cell the observation aligns with**. This is a useful HydroData feature that removes the need for users to manually match station latitude/longitude coordinates to the appropriate model grid cell, as this spatial mapping is handled directly within HydroData. We will use these indices below to extract PF-CONUS1 modeled SWE for each SNOTEL station in the section below. " - ] - }, - { - "cell_type": "markdown", - "id": "0e50455e", - "metadata": {}, - "source": [ - "## 5. Retrieve ParFlow-CONUS1 Modeled Snow Data" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "545a9d22", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a folder to save results\n", - "isExist = os.path.exists(MOD_OutputFolder)\n", - "if isExist == True:\n", - " exit\n", - "else:\n", - " os.mkdir(MOD_OutputFolder)" - ] - }, - { - "cell_type": "markdown", - "id": "56eb4bb4", - "metadata": {}, - "source": [ - "The following section retrieves ParFlow-CONUS1 data for each SNOTEL site within our HUC-08 watershed. The code identifies the CONUS1 `i,j` indices associated with each SNOTEL site, indicated in the `metadata_df`. It then extracts the CONUS1 modeled SWE output for the site and the period of interest, returning the result as a DataFrame. To fairly compare with SNOTEL, which reports SWE once daily at the start of the local day, model output is aggregated by day, using the argment `\"temporal_resolution\": \"daily\"`. Finally, the processed data is saved as a CSV file for each site. \n", - "\n", - "### 5a. ParFlow CONUS1 Model Dataset Information\n", - "We can print some information about the model output dataset by using the `hf.get_catalog_entry()` to get the CONUS1 model dataset metadata. " - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "10647da1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'id': '12', 'dataset': 'conus1_baseline_mod', 'dataset_version': '', 'file_type': 'pfb', 'variable': 'swe', 'dataset_var': 'swe', 'temporal_resolution': 'daily', 'units': 'mm', 'aggregation': 'eod', 'grid': 'conus1', 'path': 'swe.daily.eod.{wy_daynum:03d}.pfb', 'file_grouping': 'wy_daynum', 'entry_start_date': None, 'entry_end_date': None, 'documentation_notes': '', 'site_type': '', 'variable_type': 'surface_water', 'has_z': '', 'dataset_type': 'parflow', 'datasource': 'hydroframe', 'paper_dois': '10.5194/gmd-14-7223-2021', 'dataset_dois': '', 'dataset_start_date': '2002-10-01', 'dataset_end_date': '2006-09-30', 'structure_type': 'gridded', 'has_ensemble': '', 'unit_type': 'length', 'period': 'daily'}" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "conus1_options = {\n", - " \"dataset\": \"conus1_baseline_mod\",\n", - " \"variable\": \"swe\"\n", - "}\n", - "hf.get_catalog_entry(conus1_options)" - ] - }, - { - "cell_type": "markdown", - "id": "c6fd1306", - "metadata": {}, - "source": [ - "Before we gather model outputs at the specific SNOTEL sites, we can visualize SWE across our HUC-08. This is plotted for one day at 1km lateral resolution." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "ba48a33a", - "metadata": {}, - "outputs": [], - "source": [ - "# retrieve gridded PF-CONUS1 SWE for the entire HUC8 watershed\n", - "grid_swe_options = {\n", - " \"dataset\": \"conus1_baseline_mod\",\n", - " \"variable\": \"swe\",\n", - " \"temporal_resolution\": \"daily\",\n", - " \"start_time\": '2004-04-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", - " \"end_time\": '2004-04-02',\n", - " \"huc_id\": huc_8_code\n", - " }\n", - " \n", - " # Get gridded data\n", - "grid_swe = hf.get_gridded_data(grid_swe_options)" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "b82b9574", - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Image [x,y] (SWE)" - ] - }, - "execution_count": 20, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1011" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "grid_swe_map = xr.DataArray(grid_swe[0], dims=(\"y\", \"x\"), name=\"SWE\")\n", - "grid_swe_map.hvplot.image(cmap=\"YlGnBu\", colorbar=True, aspect=\"equal\", title=f\"{huc_8_name} Gridded SWE on 2004-04-01\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "73a13787", - "metadata": {}, - "source": [ - "Now, retrieve the PF-CONUS1 modeled SWE from the SNOTEL site locations. Here we use the CONUS1 i and j indices from the `metadata_df` and grab the SWE from those grid cells. \n", - "\n", - "First, create a copy of the model dataframe (`model_df`) so we have the same data structure:" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "17143151", - "metadata": {}, - "outputs": [], - "source": [ - "# Copy data_df to model_df so we have the same timestamps and site_id structure\n", - "model_df = data_df.copy()\n", - "\n", - "# Set all non-date columns to NaN to prepare for filling in model data\n", - "non_date_cols = model_df.columns.difference([\"date\"])\n", - "model_df[non_date_cols] = np.nan\n", - "\n", - "# Rename site_id columns for PF outputs \n", - "model_df.columns = [\n", - " col if col == \"date\" else col.replace(\":SNTL\", \"\") + \":PFCONUS1\"\n", - " for col in model_df.columns\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "523bd35c", - "metadata": {}, - "source": [ - "Use the function `hf.get_gridded_data()` and PF-CONUS1 `i,j` indices to select the SWE output for the correct location and time period: " - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "a814204c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
date380:CO:PFCONUS1680:CO:PFCONUS1
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", - "
" - ], - "text/plain": [ - " date 380:CO:PFCONUS1 680:CO:PFCONUS1\n", - "0 2003-10-01 0.0 0.0\n", - "1 2003-10-02 0.0 0.0\n", - "2 2003-10-03 0.0 0.0\n", - "3 2003-10-04 0.0 0.0\n", - "4 2003-10-05 0.0 0.0" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Loop over each station in metadata_df\n", - "for idx, row in metadata_df.iterrows():\n", - " site_id = row[\"site_id\"] # original SNTL site_id\n", - " col_name = site_id.replace(\":SNTL\", \"\") + \":PFCONUS1\" # corresponding column in model_df\n", - " conus_i = int(row[\"conus1_i\"])\n", - " conus_j = int(row[\"conus1_j\"])\n", - " \n", - " # Build options dict for this station\n", - " options = {\n", - " \"dataset\": \"conus1_baseline_mod\",\n", - " \"variable\": \"swe\",\n", - " \"temporal_resolution\": \"daily\",\n", - " \"start_time\": '2003-10-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", - " \"end_time\": '2005-10-01',\n", - " \"grid_point\": [conus_i, conus_j]\n", - " }\n", - " \n", - " # Get gridded data\n", - " data = hf.get_gridded_data(options)\n", - " \n", - " # Fill column in model_df\n", - " # Convert to numeric in case hf returns lists or other types\n", - " model_df[col_name] = np.squeeze(np.array(data))\n", - "\n", - "# Ensure date column is datetime\n", - "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", - "\n", - "# Save\n", - "model_df.to_csv(f'./{MOD_OutputFolder}/df_{huc_8_name}_{huc_8_code}_PFCONUS1.csv', index=False)\n", - " \n", - "model_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "7464828b", - "metadata": {}, - "source": [ - "## 6. Quick plot sanity check \n", - "Plot a simple timeseries of modeled and observed SWE to make sure our data retrieval was successful. " - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "fbe43f6a", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAoFxJREFUeJzs3Xd8XXXh//HXuTf3Zu/ZtEn3pC100ZbVAoWyRRBQEESqomxBQeSnwleWoIAWAUWgyCqyVASBMlosZbSF0r1X2ibN3uMm957fHye5oxlN0pvcm+T9fDz66Oesez/JSW7u+36WYZqmiYiIiIiIiIgEnS3UFRARERERERHprxS6RURERERERHqIQreIiIiIiIhID1HoFhEREREREekhCt0iIiIiIiIiPUShW0RERERERKSHKHSLiIiIiIiI9BCFbhEREREREZEeEhHqCoQDj8fDgQMHiI+PxzCMUFdHREREREREwpxpmlRVVZGdnY3N1n57tkI3cODAAXJyckJdDREREREREelj8vLyGDJkSLvHFbqB+Ph4wPpmJSQkBBzzeDwUFRWRnp7e4acXEhq6P+FL9yb86R6FP92j8Kb7E550X8Kf7lF40/3pvMrKSnJycrx5sj0K3eDtUp6QkNBm6K6vrychIUE/dGFI9yd86d6EP92j8Kd7FN50f8KT7kv40z0Kb7o/XXe4Icr6LoqIiIiIiIj0EIVuERERERERkR6i0C0iIiIiIiLSQzSmuwvcbjeNjY2hrob48Xg8NDY2Ul9f36kxJw6HA7vd3gs1ExERERERUejuFNM0KSgooKKiItRVkUOYponH46GqqqrTa6wnJSWRlZWlNdlFRERERKTHKXR3QnV1NU1NTWRkZBATE6OwFkZM06SpqYmIiIjD3hfTNKmtraWwsBCAQYMG9UYVRURERERkAFPoPgy32019fT2DBg0iNTU11NWRQ3QldANER0cDUFhYSEZGhrqai4iIiIhIj9JEaofR2NiIYRjExMSEuioSJC33UuPzRURERESkpyl0d5K6lPcfupciIiIiItJbFLpFREREREREeohCt3Tb0qVLMQyD8vLyTl8zbNgwHnnkkSN63jvvvJNjjjnmiB5DREREJOw01sNnT8Cuj0NdExEJIoXufuzKK6/EMAx+/OMftzp2zTXXYBgGV155Ze9XTERERERaW3Y/vHMb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RMTdgyAiIiLXWrx4MWpra7F169Z28xYuXIijR4/i2LFj7eb985//xOOPP46amhoAV7/T/dRTT6G2tta+TGlpKe644w6cPXsWVqvVPn3atGm466678Nprr/X6/SEiItIKL3cPgIiIiNxLRKDT6QAAOTk5eO2113Ds2DHU19ejtbUVV65cwc8//wyz2dzh7fPy8iAiGDZsmMP0pqYmDBw40OXjJyIiUjM23URERH3c8ePHERUVhfLyctx3331ITU3Fn//8ZwQGBmL//v1ISUlBS0uL09srigKDwYDc3FwYDAaHef7+/q4ePhERkaqx6SYiIurDdu/ejcLCQjz99NM4cuQIWltb8fbbb0Ovv3ral08//dRheaPRCJvN5jBt7NixsNlsqK6uxsSJE2/Z2ImIiLSATTcREVEf0dTUhKqqKthsNpw/fx5fffUV0tPTMXPmTCxatAiFhYVobW3FunXrMGvWLBw4cADr1693WMeQIUPQ2NiIb775BnFxcfDz88OwYcPw29/+FosWLcLbb7+NsWPHoqamBrt378bo0aNx3333uekeExERuR/PXk5ERNRHfPXVVwgLC8OQIUOQnJyMnJwcrF27Ftu2bYPBYMCYMWOwatUqvPHGGxg1ahQ2bdqE9PR0h3VMmDABqampWLBgAYKDg/Hmm28CADZu3IhFixbh2WefRUxMDGbPno3vvvsOERER7rirREREqsGzlxMRERERERG5CN/pJiIiIiIiInIRNt1ERERERERELsKmm4iIiIiIiMhF2HQTERERERERuQibbiIiIiIiIiIXYdNNRERERERE5CJsuomIiIiIiIhchE03ERERERERkYuw6SYiIiIiIiJyETbdRERERERERC7CppuIiIiIiIjIRdh0ExEREREREbnI/wOi5/NV6KggEAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots(figsize=(10, 4))\n", - "\n", - "ax.plot(data_df[\"date\"], model_df[\"380:CO:PFCONUS1\"], label=\"Modeled\", linewidth=2)\n", - "\n", - "ax.plot(data_df[\"date\"], data_df[\"380:CO:SNTL\"], label=\"Observed\", linewidth=2)\n", - "\n", - "ax.set_xlabel(\"Date\")\n", - "ax.set_ylabel(\"SWE (mm)\")\n", - "\n", - "# Date formatting for x-axis\n", - "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", - "ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))\n", - "\n", - "ax.legend(loc='upper left')\n", - "ax.grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()" - ] - }, - { - "cell_type": "markdown", - "id": "da3df109", - "metadata": {}, - "source": [ - "# Start of comparison from old notebook" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Spatial Mapping of the SNOTEL sites \n", - "Before evaluating model performance, we plot the GIS data associated with the records in the combined DataFrame. The map below shows the SNOTEL stations included in the evaluation dataset, along with the watershed boundary used for the model simulations. Hover over the pins to see the site names. \n", - "\n", - "We also print a table of the SNOTEL site metadata to help with the single site selection." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sites CRS: EPSG:4326\n", - "Total sites in watershed: 2\n" - ] - }, - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Path to the watershed shapefile\n", - "watershed = f\"{domain_data_path}/East-Taylor_14020001.shp\"\n", - "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", - "\n", - "# Create GeoDataFrame of all available stations\n", - "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", - " metadata_df,\n", - " geometry=gpd.points_from_xy(\n", - " metadata_df.longitude,\n", - " metadata_df.latitude\n", - " ),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", - "\n", - "# Combine watershed polygons into one geometry\n", - "watershed_union = watershed_gdf.geometry.unary_union\n", - "\n", - "# Filter stations that fall within the watershed\n", - "sites_in_watershed = filtered_all_stations_gdf[\n", - " filtered_all_stations_gdf.geometry.within(watershed_union)\n", - "].copy()\n", - "\n", - "sites_in_watershed.reset_index(drop=True, inplace=True)\n", - "\n", - "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", - "\n", - "m = plot_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", - "m" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sites_in_watershed" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Compare Modeled and Observed SWE Timeseries at a Single Site\n", - "\n", - "Once we have both observation data and modeling outpus, it's important to evaluate how well the model reproduces observed data. The following plots are simple timeseries comparisons of **modeled vs. observed** SWE. These types of plots provide a straight-forward visual of how well the observations and simulations agree and are a great start for assessing general model performance. \n", - "\n", - "📊 We include two figures:\n", - "\n", - "1. **Time Series Overlay:** Plots the observed and modeled values together over time. This helps identify:\n", - " - Periods of systematic bias\n", - " - Timing differences in peaks and lows\n", - " - General agreement in trends\n", - "\n", - "2. **Scatter Plot with 1:1 Line:** Plots each modeled value against its corresponding observed value. This highlights:\n", - " - Accuracy across the full range of values\n", - " - Over- or under-prediction patterns\n", - " - Outliers or extreme events" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Review the sites within the watershed from the interactive map above and click on the markers to view the site name and code. Recall, we also printed out the site metadata for all sites within the watershed, which contains the 3-letter site codes.\n", - "\n", - "✏️ Once you’ve identified the site of interest, **enter its site code in the next code cell for `my_site_code`**: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# choose a site of interest within the watershed\n", - "my_site_code = '380:CO:'\n", - "\n", - "# make sure date columns are datetime and set as index for easier plotting and metric calculations\n", - "data_df[\"date\"] = pd.to_datetime(data_df[\"date\"])\n", - "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", - "\n", - "data_df = data_df.set_index(\"date\")\n", - "model_df = model_df.set_index(\"date\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1c1f5648", - "metadata": {}, - "outputs": [], - "source": [ - "plot_utils.comparison_plots(data_df, model_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1', site_label=None)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To move beyond an overall summary of daily performance, we replot the modeled vs. observed SWE scatter while highlighting specific months with a distinct color. This gives us more information about the **seasonal model performance**. \n", - "\n", - "Let's customize the scatter plot by allowing you to highlight specific months with a distinct color. The selected months will appear in one color, while all other months will appear in a different color. This customization reveals whether there are **seasonal patterns** in the relationship between observed and modeled SWE, allowing us to distinguish model behavior during the key snowpack phases of accumulation and ablation (melt). Identifying these patterns is important for diagnosing the model’s strengths and limitations during different parts of the snow season.\n", - "\n", - "You can change the list of highlighted months (for example, October–December for early accumulation or March–May for spring melt) to explore in the scatter plot how model performance varies across different parts of the snow season. This seasonal perspective motivates the _peak SWE analysis_ that follows." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 For this example, let's highlight the _early snow accumulation period_ of October - January:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "data_df['month'] = data_df.index.month\n", - "model_df['month'] = model_df.index.month\n", - "\n", - "plot = plot_utils.plot_custom_scatter_SWE(\n", - " data_df,\n", - " model_df,\n", - " f\"{my_site_code}SNTL\",\n", - " f\"{my_site_code}PFCONUS1\",\n", - " site_label=my_site_code,\n", - " highlight_months=[10, 11, 12, 1],\n", - ")\n", - "\n", - "plot" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

What does this plot tell us about how well the model performs during the early snow accumulation period at this site?
\n", - "HINT: How close are the green points to the 1:1 line?

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. Peak SWE Evaluation at the Watershed Scale \n", - "As we saw in the previous section, how well a model matches observations can differ greatly throughout the year. The following section focuses on **peak SWE** (or maximum SWE) analysis. \n", - "\n", - "**Peak SWE is a key diagnostic for snow-dominated hydrologic systems** because it represents the maximum amount of liquid water stored in the snowpack before the spring melt. Evaluating both the magnitude (quantity) and timing (date) of peak SWE provides insight into whether the model is accurately representing snow accumulation and seasonal energy balance. \n", - "\n", - "Errors in peak SWE can have important hydrologic consequences, as peak accumulation strongly influences:\n", - "- The volume of water available for spring runoff\n", - "- The timing of streamflow peaks\n", - "- Soil moisture recharge and groundwater contributions\n", - "\n", - "
\n", - " \n", - "
\n", - "\n", - "_Example daily SWE at a single site, showing two important periods in snow processes: accumulation (before peak) and ablation (after peak). The vertical line marks peak SWE._" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.1 Comparing Modeled and Observed Peak SWE at All Sites in the Watershed\n", - "In this section, we evaluate observed and modeled peak SWE for all stations within our watershed and for all years selected in the `StartDate` and `EndDate` above. \n", - "\n", - "#### 📋 Modeled SWE on the Date of Observed Peak SWE (magnitude) \n", - "This comparison evaluates the modeled SWE on the **specific day when observed SWE reaches its maximum.** By fixing the timing to the observed peak, this comparison isolates errors in SWE magnitude. \n", - "It answers the question: *How much SWE does the model simulate on the day the observed snowpack reaches its maximum?*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# isolate the columns associated with observations and model predictions.\n", - "# these will be inputs to our same-day comparison function.\n", - "obs_cols = sorted([col for col in combined_df.columns if col.endswith('SNTL')])\n", - "mod_cols = sorted([col for col in combined_df.columns if col.endswith('PFCONUS1')])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute the same-day SWE comparison during the observed peak SWE for each of the observation and modeled sites.\n", - "df_observed_peak = utils.modeled_swe_at_observed_peak(combined_df, obs_cols, mod_cols)\n", - "df_observed_peak" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 Visualize the amount of SWE on **the day of observed peak SWE occurs** for both the model and observations at each station" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Rearrange the dataframe to long format for easier plotting\n", - "df_long = (\n", - " df_observed_peak\n", - " .reset_index() \n", - " .melt(\n", - " id_vars=['Station', 'Water_Year', 'date'],\n", - " value_vars=['Observed', 'Modeled'],\n", - " var_name='Source',\n", - " value_name='SWE'\n", - " )\n", - ")\n", - "# Create scatter plot of observed and modeled SWE on the day of observed peak SWE\n", - "scatter_obs_peak = df_long.hvplot.scatter(\n", - " x='Station',\n", - " y='SWE',\n", - " by='Source', # Observed vs Modeled\n", - " ylabel='SWE on Observed Peak Day (mm)',\n", - " title='Observed and Modeled SWE on the Day of Observed Peak SWE',\n", - " size=70,\n", - " width=700,\n", - " height=450,\n", - " alpha=0.8,\n", - " hover_cols=['Water_Year'],\n", - " rot=45\n", - ")\n", - "\n", - "# Customize the scatter plot appearance\n", - "scatter_by_station = (\n", - " scatter_obs_peak \n", - " .opts(legend_position='top_right')\n", - ")\n", - "\n", - "scatter_by_station" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 📋 Modeled vs Observed Peak SWE Comparison (timing & magnitude) \n", - "This comparison evaluates the modeled and observed peak SWE values and their corresponding dates independently. Unlike the previous comparison that fixed the timing to the observed peak swe, this analysis shows the actual days of modeled and observed peak SWE, which may occur on different dates. As a result, it captures errors in both **peak SWE magnitude** and **peak timing**." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute the different-day SWE comparison for each of the observed and modeled sites.\n", - "df_both_peak = utils.modeled_vs_observed_peak_swe(combined_df, obs_cols, mod_cols)\n", - "df_both_peak" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 Visualize the quantity of peak SWE for both the model and observations at each station" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "### NEED TO DECIDE HOW TO FORMAT THIS PLOT AND IF WE WANT TO HAVE THE \"SAME_DAY\" PLOT\n", - "\n", - "# Create the scatter plot\n", - "scatter_plot_both_peak = df_both_peak.hvplot.scatter(\n", - " x='Observed',\n", - " y='Modeled',\n", - " xlabel='Observed SWE (mm)',\n", - " ylabel='Modeled SWE (mm)',\n", - " title='Modeled vs. Observed Peak SWE',\n", - " size=35,\n", - " width=500,\n", - " height=400,\n", - " color='#E69F00',\n", - " hover_cols=['Station', 'Water_Year']\n", - ")#.relabel('Peak SWE')\n", - "\n", - "# Add 1:1 line (perfect match line)\n", - "swe_max = df_both_peak[['Observed', 'Modeled']].max().max()\n", - "\n", - "one_to_one_line = hv.Curve(([0, swe_max], [0, swe_max])).opts(\n", - " color='gray',\n", - " line_dash='dashed',\n", - " line_width=1,\n", - ").relabel('1:1 Line')\n", - "\n", - "# Combine scatter plot and 1:1 line into an Overlay\n", - "scatter_with_line = (scatter_plot_both_peak * one_to_one_line).opts( #scatter_plot_obs_peak * \n", - " legend_position='bottom_right'\n", - ")\n", - "\n", - "scatter_with_line" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.2 Visualizing Model Error for Peak SWE\n", - "\n", - "The previous scatter plots indicate that the modeled and observed peak SWE magnitude and timing don't always align. Next, we plot the degree to which \n", - "\n", - "The previous scatter plots highlight differences between modeled and observed peak SWE timing and magnitude, but interpreting these variations can be challenging when comparing modeled and observed values directly. To make these differences more explicit, we compute errors in both peak timing and peak SWE magnitude and visualize them directly. This approach clarifies both the direction and magnitude of model bias and facilitates comparison across stations and water years.\n", - "\n", - "First, add columns `Peak_Date_Diff_Days` and `Peak_SWE_Diff` to the DataFrame `df_both_peak` for computed difference in peak SWE date difference and peak SWE quantity between modeled and observed:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Compute the difference in peak SWE days and peak SWE amounts between modeled and observed\n", - "df_both_peak['Peak_Date_Diff_Days'] = (df_both_peak['Modeled_Date'] - \n", - " df_both_peak['Observed_Date']).dt.days\n", - "df_both_peak['Peak_SWE_Diff'] = (df_both_peak['Modeled'] - \n", - " df_both_peak['Observed'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "df02795e", - "metadata": {}, - "outputs": [], - "source": [ - "df_both_peak" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 Visualize the error between the modeled and observed peak SWE " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Filter to separate each water year\n", - "year1 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].min()]\n", - "year2 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].max()]\n", - "\n", - "bar1 = year1.hvplot.bar(\n", - " x='Station',\n", - " y='Peak_Date_Diff_Days',\n", - " rot=45,\n", - " ylabel='Date Difference (days)',\n", - " title=f'Peak SWE Date Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", - " width=400,\n", - " height=400,\n", - " color='Peak_Date_Diff_Days',\n", - " hover_cols=['Modeled', 'Observed']\n", - ")\n", - "bar2 = year1.hvplot.bar(\n", - " x='Station',\n", - " y='Peak_SWE_Diff',\n", - " rot=45,\n", - " ylabel='SWE Difference (m)',\n", - " title=f'Peak SWE Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", - " width=400,\n", - " height=400,\n", - " color='Peak_SWE_Diff',\n", - " hover_cols=['Modeled', 'Observed']\n", - ")\n", - "\n", - "# Combine side by side\n", - "layout = (bar1 + bar2)\n", - "layout.opts(shared_axes=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The left panel shows the timing error (date difference) and the right panel the magnitude error (SWE difference). When we computed the difference in date and SWE quantity above, we took `modeled - observed` so: \n", - "\n", - "| | DATE OF PEAK SWE | PEAK SWE QUANTITY |\n", - "|---|---|---|\n", - "| + Positive Values | modeled AFTER observed | modeled GREATER THAN observed |\n", - "| - Negative Values | modeled BEFORE observed | modeled LESS THAN observed | " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE both earlier and less than the observed peak SWE? Perhaps try changing the my_site_code from earlier in the notebook to rerun nwm_utils.comparison_plots() to see the timeseries for a different station to look at the peak magnitude and timing. \n", - "\n", - "
What happens if you change the year that is plotted?
✏️ Try modifying the bar plot code from bar1 = year1.hvplot.bar to bar1 = year2.hvplot.bar. Don't forget to change the title!

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 📊 Next, we combine the timing and magnitude errors and plot them together for each station." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "\n", - "scatter = df_both_peak.hvplot.scatter(\n", - " x='Peak_Date_Diff_Days',\n", - " y='Peak_SWE_Diff',\n", - " by='Station', # Water_Year\n", - " xlabel='Peak SWE Timing Error (days)',\n", - " ylabel='Peak SWE Magnitude Error (mm)',\n", - " title='Peak SWE Timing vs Magnitude Error',\n", - " size=80,\n", - " width=600,\n", - " height=400,\n", - " hover_cols=['Water_Year']\n", - ")\n", - "\n", - "# Add reference lines\n", - "vline = hv.VLine(0).opts(color='gray', line_dash='dashed')\n", - "hline = hv.HLine(0).opts(color='gray', line_dash='dashed')\n", - "\n", - "(scatter * vline * hline).opts(legend_position='top_left', show_grid=True)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "✏️ **Try changing how we view this plot.** \n", - "We can modify a line in the section of code from `by='Station'` to `by='Water_Year'` to better visualize the errors in the different Water Years. \n", - "Are there any patterns that jump out? Which year was modeled peak SWE consistently less than observed peak SWE? " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 6. Compute and Statistics and Error Metrics \n", - "The previous section visualized when and where modeled SWE differs from observations, both in terms of peak SWE timing and magnitude. However, visual inspection alone makes it difficult to compare performance across sites or to summarize model behavior in a consistent or quantifiable way. In this section, we compute commonly used statistical error metrics to quantify model performance, allowing us to objectively assess bias, error magnitude, and variability for sites within the watershed. \n", - "\n", - "Proposed outline (DTK, Jan 2026):\n", - "- Summary metrics at a station\n", - "- Summary metrics at all stations within the watershed\n", - "- Combined timing and magnitude for all stations within the watershed (Condon metric)\n", - "- Focus on timing: summary statistics for single station for accumulation & ablation periods (using the new wrapper: `nwm_utils.compute_stats_period()`)\n", - "- Melt period statistics" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "nwm_utils.compute_stats(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Pearson and Spearman correlations are both close to 1, suggesting a strong relationship between observed and modeled SWE. As shown on the timeseries plot, this strong correlation alone does not indicate a \"good\" model. For example, it does not guarantee accurate timing of key events, such as peak SWE or melt onset. Let's compare these as well. The following code uses `report_max_dates_and_values` function to identify the peak SWE value and the date it occurs for both the observed (CCSS) and modeled (NWM) datasets. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why? As you review the results, compare the peak flow amounts and the timing of snowmelt onset. Do you see any significant differences? Which dataset indicates an earlier melt?

\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "summary_table = nwm_utils.report_max_dates_and_values(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n", - "summary_table" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Summary Metrics at Multiple Sites" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "site_codes = ['DAN', 'HRS', 'KIB', 'PDS', 'SLI', 'TUM', 'WHW']\n", - "\n", - "rows = []\n", - "\n", - "for site in site_codes:\n", - " obs_col = f'CCSS_{site}_swe_m'\n", - " mod_col = f'NWM_{site}_swe_m'\n", - "\n", - " stats_table = nwm_utils.compute_stats(combined_df, obs_col, mod_col)\n", - "\n", - " rows.append({\n", - " 'Station': site,\n", - " 'Mean_Obs': stats_table.loc['observed', 'Mean'],\n", - " 'Mean_Mod': stats_table.loc['modeled', 'Mean'],\n", - " 'Bias_m': stats_table.loc['Bias (Modeled - Observed)', 'Mean'],\n", - " 'Pearson_r': stats_table.loc['Pearson Correlation', 'Mean'],\n", - " 'Spearman_r': stats_table.loc['Spearman Correlation', 'Mean'],\n", - " 'NSE': stats_table.loc['Nash-Sutcliffe Efficiency (NSE)', 'Mean'],\n", - " 'KGE': stats_table.loc['Kling-Gupta Efficiency (KGE)', 'Mean']\n", - " })\n", - "\n", - "stats_AllStations = pd.DataFrame(rows)\n", - "\n", - "print('All Stations Statistics Summary:')\n", - "stats_AllStations" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "stats_AllStations.hvplot.bar(\n", - " x='Station',\n", - " y='NSE',\n", - " rot=45,\n", - " ylabel='Nash–Sutcliffe Efficiency',\n", - " title='NSE by Station',\n", - " height=400,\n", - " width=600,\n", - " bar_width=0.5\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "stats_summary.hvplot.scatter(\n", - " x='Station',\n", - " y='Bias_m',\n", - " size=100,\n", - " rot=45,\n", - " ylabel='Bias (m)',\n", - " title='Mean SWE Bias by Station'\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Combine Magnitude (absolute relative bias) and Timing (Spearman's rho) metrics using the Condon metric (and with all stations, a Condon diagram)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bias1 = evaluation_metrics.bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "bias1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "abs_bias = evaluation_metrics.absolute_relative_bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "abs_bias" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "srho = evaluation_metrics.spearman_rank(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "srho" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "evaluation_metrics.condon(abs_bias, srho)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

\n", - " What is the modeled SWE on the date when the observed SWE reaches its peak?
\n", - " ✏️ Use the code snippet below to find the answer.\n", - "

\n", - "
\n",
-    "  \n",
-    "    # Find date of the peak SWE from observed data\n",
-    "    date_obs_max = combined_df['CCSS_HRS_swe_m'].idxmax()\n",
-    "\n",
-    "    # Get corresponding value of modeled data on that date\n",
-    "    value_mod_at_max_obs = combined_df.loc[date_obs_max, 'NWM_HRS_swe_m']\n",
-    "  
\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Focus on Timing: Melt Period Metrics\n", - "Compare the average melt rate over the full melt period. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following function computes the melt period length by identifying the first date after the peak SWE when SWE drops to zero and remains at zero for at least (`min_zero_days`) consecutive days. This is used to define the end of the melt period. Finally, the function calculates the average melt rate, which represents the rate at which snow disappeared, expressed in meters per day, over the full melt period." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "melt_stats_df = utils.compute_melt_period_statistics(combined_df)\n", - "melt_stats_df.head()\n", - "melt_stats_df" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "observed_melt_period = nwm_utils.compute_melt_period(combined_df[f'CCSS_{my_site_code}_swe_m'])\n", - "observed_melt_period" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "modeled_melt_period = nwm_utils.compute_melt_period(combined_df[f'NWM_{my_site_code}_swe_m'])\n", - "modeled_melt_period" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "accum_months = [10, 11, 12, 1, 2, 3]\n", - "ablation_months = [4, 5, 6]\n", - "\n", - "accum_stats = nwm_utils.compute_stats_period(\n", - " combined_df,\n", - " f'CCSS_{my_site_code}_swe_m',\n", - " f'NWM_{my_site_code}_swe_m',\n", - " accum_months\n", - ")\n", - "\n", - "accum_stats" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "ablation_stats = nwm_utils.compute_stats_period(\n", - " combined_df,\n", - " f'CCSS_{my_site_code}_swe_m',\n", - " f'NWM_{my_site_code}_swe_m',\n", - " ablation_months\n", - ")\n", - "\n", - "ablation_stats" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

\n", - " If you recall from earlier, we plotted the timeseries of out selected station. Replot it below. Do the metrics make sense given the visual comparison between modeled and observed? For example, when you look at the timeseries, is the model consistently predicting SWE to be higher or lower than observations? Does this align with the Bias sign (+ or -)?\n", - "

\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nwm_utils.comparison_plots(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "nwm_env", - "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 -} diff --git a/src/cssi_evaluation/utils/plot_utils.py b/src/cssi_evaluation/utils/plot_utils.py index f912677..bace3a0 100644 --- a/src/cssi_evaluation/utils/plot_utils.py +++ b/src/cssi_evaluation/utils/plot_utils.py @@ -872,17 +872,18 @@ def plot_custom_scatter_SWE( df.index.name = "date" - # --- Add month column if needed --- + # compute month from index if highlight_months is not None: - if month_col not in df.columns: - df[month_col] = df.index.month + df[month_col] = df.index.month df["color"] = df[month_col].apply( lambda m: "teal" if m in highlight_months else "tomato" ) color = "color" + hover_cols = ["date", month_col] else: color = "black" + hover_cols = ["date"] label = site_label if site_label else obs_col @@ -892,11 +893,11 @@ def plot_custom_scatter_SWE( y="modeled", xlabel="Observed SWE (mm)", ylabel="Modeled SWE (mm)", - title=f"{label}: Observed vs. Modeled SWE", + title=f"{label} Observed vs. Modeled SWE", size=size, width=width, height=height, - hover_cols=["date", month_col] if highlight_months else ["date"], + hover_cols=hover_cols, color=color, ) diff --git a/src/cssi_evaluation/variables/snow_utils.py b/src/cssi_evaluation/variables/snow_utils.py index 1ecac95..dd6e9c4 100644 --- a/src/cssi_evaluation/variables/snow_utils.py +++ b/src/cssi_evaluation/variables/snow_utils.py @@ -105,6 +105,52 @@ def modeled_swe_at_observed_peak( # concatenate all dataframes together and return return pd.concat(dfs) +# def modeled_swe_at_observed_peak( +# observed_df: pd.DataFrame, +# modeled_df: pd.DataFrame, +# ) -> pd.DataFrame: +# """ +# Compare observed peak SWE to modeled SWE on the same date. +# Assumes both dfs have the SAME column order (stations aligned). +# """ + +# # --- Basic checks --- +# if not isinstance(observed_df.index, pd.DatetimeIndex): +# raise ValueError("observed_df must have a DatetimeIndex") +# if not isinstance(modeled_df.index, pd.DatetimeIndex): +# raise ValueError("modeled_df must have a DatetimeIndex") +# if len(observed_df.columns) != len(modeled_df.columns): +# raise ValueError("DataFrames must have the same number of columns") + +# # --- Align time indices --- +# observed_df, modeled_df = observed_df.align(modeled_df, join="inner") + +# # --- Compute water year vectorized --- +# water_year = compute_water_year_from_index(observed_df.index) + +# results = [] + +# # --- Loop over station columns --- +# for obs_col, mod_col in zip(observed_df.columns, modeled_df.columns): +# dat = pd.DataFrame({ +# "Observed": observed_df[obs_col], +# "Modeled": modeled_df[mod_col], +# "Water_Year": water_year, +# }).dropna() + +# if dat.empty: +# print(f"Skipping {obs_col} (all NaN)") +# continue + +# # --- Peak observed SWE per water year --- +# idx = dat.groupby("Water_Year")["Observed"].idxmax() +# peak = dat.loc[idx].copy() +# peak["Station"] = obs_col + +# results.append(peak) + +# return pd.concat(results) + def modeled_vs_observed_peak_swe( df: pd.DataFrame, obs_swe_cols: list[str], mod_swe_cols: list[str] From 38825839c917898a024b20c7c6bf9cae30f8d781 Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Wed, 25 Mar 2026 17:23:30 -0600 Subject: [PATCH 4/8] add missing file --- .../parflow_swe_point_scale_evaluation.ipynb | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb index be0d1a2..264b2e2 100644 --- a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb +++ b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "70e9e22e", "metadata": {}, "source": [ "![NWM](../img/NWM.png)\n", @@ -13,6 +14,7 @@ }, { "cell_type": "markdown", + "id": "e84e3989", "metadata": {}, "source": [ "#### Introduction: \n", @@ -5220,6 +5222,7 @@ }, { "cell_type": "markdown", + "id": "f0c57cdc", "metadata": {}, "source": [ "## 4. Compare Modeled and Observed SWE Timeseries at a Single Site\n", @@ -5241,6 +5244,7 @@ }, { "cell_type": "markdown", + "id": "602560f1", "metadata": {}, "source": [ "Review the sites within the watershed from the interactive map above and click on the markers to view the site name and code. Recall, we also printed out the site metadata for all sites within the watershed, which contains the 3-letter site codes.\n", @@ -5251,6 +5255,7 @@ { "cell_type": "code", "execution_count": 21, + "id": "473b9326", "metadata": {}, "outputs": [], "source": [ @@ -5372,6 +5377,7 @@ }, { "cell_type": "markdown", + "id": "dfc27ff3", "metadata": {}, "source": [ "To move beyond an overall summary of daily performance, we replot the modeled vs. observed SWE scatter while highlighting specific months with a distinct color. This gives us more information about the **seasonal model performance**. \n", @@ -5383,6 +5389,7 @@ }, { "cell_type": "markdown", + "id": "1354a01b", "metadata": {}, "source": [ "##### 📊 For this example, let's highlight the _early snow accumulation period_ of October - January:" @@ -5391,6 +5398,7 @@ { "cell_type": "code", "execution_count": 24, + "id": "28f3e419", "metadata": {}, "outputs": [ { @@ -5499,6 +5507,7 @@ }, { "cell_type": "markdown", + "id": "c831342f", "metadata": {}, "source": [ "
\n", @@ -5510,6 +5519,7 @@ }, { "cell_type": "markdown", + "id": "aba6c6fe", "metadata": {}, "source": [ "## 5. Peak SWE Evaluation at the Watershed Scale \n", @@ -5531,6 +5541,7 @@ }, { "cell_type": "markdown", + "id": "add14a79", "metadata": {}, "source": [ "### 5.1 Comparing Modeled and Observed Peak SWE at All Sites in the Watershed\n", @@ -5544,6 +5555,7 @@ { "cell_type": "code", "execution_count": 48, + "id": "2e15a789", "metadata": {}, "outputs": [], "source": [ @@ -5557,6 +5569,7 @@ { "cell_type": "code", "execution_count": 49, + "id": "ff514342", "metadata": {}, "outputs": [ { @@ -5648,6 +5661,7 @@ }, { "cell_type": "markdown", + "id": "7e0c6dea", "metadata": {}, "source": [ "##### 📊 Visualize the amount of SWE on **the day of observed peak SWE occurs** for both the model and observations at each station" @@ -5656,6 +5670,7 @@ { "cell_type": "code", "execution_count": 50, + "id": "297f8608", "metadata": {}, "outputs": [ { @@ -5786,6 +5801,7 @@ }, { "cell_type": "markdown", + "id": "60d98757", "metadata": {}, "source": [ "#### 📋 Modeled vs Observed Peak SWE Comparison (timing & magnitude) \n", @@ -5795,6 +5811,7 @@ { "cell_type": "code", "execution_count": 52, + "id": "78907563", "metadata": {}, "outputs": [ { @@ -5888,6 +5905,7 @@ }, { "cell_type": "markdown", + "id": "d5ec9e5d", "metadata": {}, "source": [ "##### 📊 Visualize the quantity of peak SWE for both the model and observations at each station" @@ -5896,6 +5914,7 @@ { "cell_type": "code", "execution_count": 53, + "id": "a93b9fa9", "metadata": {}, "outputs": [ { @@ -6025,6 +6044,7 @@ }, { "cell_type": "markdown", + "id": "ed021831", "metadata": {}, "source": [ "### 5.2 Visualizing Model Error for Peak SWE\n", @@ -6039,6 +6059,7 @@ { "cell_type": "code", "execution_count": 54, + "id": "e46b82f8", "metadata": {}, "outputs": [], "source": [ @@ -6160,6 +6181,7 @@ }, { "cell_type": "markdown", + "id": "b77c566e", "metadata": {}, "source": [ "##### 📊 Visualize the error between the modeled and observed peak SWE " @@ -6168,6 +6190,7 @@ { "cell_type": "code", "execution_count": 56, + "id": "29c68cf0", "metadata": {}, "outputs": [ { @@ -6296,6 +6319,7 @@ }, { "cell_type": "markdown", + "id": "4f707f0c", "metadata": {}, "source": [ "The left panel shows the timing error (date difference) and the right panel the magnitude error (SWE difference). When we computed the difference in date and SWE quantity above, we took `modeled - observed` so: \n", @@ -6308,6 +6332,7 @@ }, { "cell_type": "markdown", + "id": "8729d9ff", "metadata": {}, "source": [ "
\n", @@ -6320,6 +6345,7 @@ }, { "cell_type": "markdown", + "id": "fdabdbd6", "metadata": {}, "source": [ "#### 📊 Next, we combine the timing and magnitude errors and plot them together for each station." @@ -6328,6 +6354,7 @@ { "cell_type": "code", "execution_count": 58, + "id": "66105b84", "metadata": {}, "outputs": [ { @@ -6448,6 +6475,7 @@ }, { "cell_type": "markdown", + "id": "285ec901", "metadata": {}, "source": [ "✏️ **Try changing how we view this plot.** \n", @@ -6457,6 +6485,7 @@ }, { "cell_type": "markdown", + "id": "809e568f", "metadata": {}, "source": [ "## 6. Compute and Statistics and Error Metrics \n", @@ -6473,6 +6502,7 @@ { "cell_type": "code", "execution_count": 59, + "id": "65f95223", "metadata": { "tags": [] }, From d8ffd913262384640a54b9828d2a52cd08faa6c4 Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Wed, 25 Mar 2026 23:42:43 -0600 Subject: [PATCH 5/8] Fixed most of the functions to work with new 2 datafram structure, stats section still needs work --- .../parflow_swe_point_scale_evaluation.ipynb | 907 +++++++++++------- src/cssi_evaluation/variables/snow_utils.py | 750 ++++++++++----- 2 files changed, 1046 insertions(+), 611 deletions(-) diff --git a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb index 264b2e2..51b0b81 100644 --- a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb +++ b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb @@ -100,11 +100,11 @@ "application/vnd.holoviews_exec.v0+json": "", "text/html": [ "
\n", - "
\n", + "
\n", "
\n", "
" + ], + "text/plain": [ + ":Layout\n", + " .Overlay.I :Overlay\n", + " .Curve.Observed_SWE :Curve [date] (observed)\n", + " .Curve.Modeled_SWE :Curve [date] (modeled)\n", + " .Overlay.II :Overlay\n", + " .Scatter.I :Scatter [observed] (modeled,date)\n", + " .Curve.A_1_colon_1_Line :Curve [x] (y)" + ] + }, + "execution_count": 38, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1765" + } + }, + "output_type": "execute_result" + } + ], "source": [ - "grid_df" + "plot_utils.comparison_plots(obs_df, model_df, f'{my_site_code}', f'{my_site_code}', site_label=None)" ] } ], "metadata": { "kernelspec": { - "display_name": "nwm_env", + "display_name": "cssi_evaluation", "language": "python", "name": "python3" }, diff --git a/src/cssi_evaluation/variables/snow_utils.py b/src/cssi_evaluation/variables/snow_utils.py index dd6e9c4..e0a54db 100644 --- a/src/cssi_evaluation/variables/snow_utils.py +++ b/src/cssi_evaluation/variables/snow_utils.py @@ -18,359 +18,577 @@ from cssi_evaluation.utils.dataPrep_utils import compute_water_year -def modeled_swe_at_observed_peak( - df: pd.DataFrame, obs_swe_cols: list[str], mod_swe_cols: list[str] -) -> pd.DataFrame: +def modeled_swe_at_observed_peak(obs_df: pd.DataFrame, model_df: pd.DataFrame) -> pd.DataFrame: """ Extract modeled SWE values on the dates of observed peak (maximum) SWE. - This function evaluates model performance by comparing observed peak SWE - to the modeled SWE on the same calendar date. For each station and water year, - the date of maximum observed SWE is identified, and the modeled SWE value - at that date is extracted. - Parameters - ========== - df: pandas.DataFrame - A pandas dataframe containing columns associated with modeled and observed SWE. The - dataframe must have an datetime[64] index. - obs_swe_cols: list[str] - Names of the columns associated with observed SWE - mod_swe_cols: list[str] - Names of the columns associated with modeled SWE + ---------- + obs_df : pd.DataFrame + Observed SWE time series. Columns are site IDs (e.g., '380:CO:SNTL'), rows are timestamps. + Must have a DatetimeIndex. + model_df : pd.DataFrame + Modeled SWE time series. Columns are site IDs matching obs_df. Rows are timestamps. Returns - ======= - df: pandas.DataFrame - A dataframe containing observed max observed SWE and the modeled SWE at the index of the - maximum observed. The format of the DataFrame will be: + ------- + pd.DataFrame + DataFrame containing, for each site and water year: + - Observed peak SWE + - Modeled SWE at the date of observed peak + - Water year + - Station name (site ID) + """ - Observed Modeled Water_Year Station - - - ... + # Check for DatetimeIndex + if not isinstance(obs_df.index, pd.DatetimeIndex): + raise ValueError("obs_df must have a DatetimeIndex") + if not isinstance(model_df.index, pd.DatetimeIndex): + raise ValueError("model_df must have a DatetimeIndex") - Example: + # Align the dataframes by time + obs_df, model_df = obs_df.align(model_df, join="inner") - Observed Modeled Water_Year Station - 2019-04-18 0.98044 1.0293 2019 CCSS_DAN_swe_m - 2019-04-20 2.12090 1.3598 2019 CCSS_HRS_swe_m - 2019-03-28 0.80264 0.6708 2019 CCSS_KIB_swe_m - 2019-04-07 1.78562 0.9965 2019 CCSS_PDS_swe_m - ... + # Compute water year if not present + if "Water_Year" not in obs_df.columns: + obs_df["Water_Year"] = obs_df.index.map(lambda x: x.year + 1 if x.month > 9 else x.year) - """ + results = [] + + # Loop over each site column + for site in obs_df.columns: + if site == "Water_Year": + continue # skip Water_Year column + + dat = pd.DataFrame({ + "Observed": obs_df[site], + "Modeled": model_df[site], + "Water_Year": obs_df["Water_Year"] + }).dropna() - # compute water year if it doesn't already exist in the dataframe. - # this is needed to properly align the same-day comparison - if "Water_Year" not in df.columns: - compute_water_year(df, inplace=True) - - # check to make sure that the input columns are the same length. - # Raise an exception if they aren't, because our computation will fail. - if len(obs_swe_cols) != len(mod_swe_cols): - raise Exception("Modeled and observed inputs must be the same length") - - # make sure our column data is represented as float64, otherwise - # the pandas operations below will fail. - df = df.apply(pd.to_numeric, errors="coerce").astype("float64") # type: ignore[assignment] - df["Water_Year"] = df["Water_Year"].astype(int) # keep wateryear an integer - - # Loop over each pairwise grouping of obs and mod columns that - # have been provided as inputs. Group data for these stations - # by water year and determine when the maximum value occurs in - # the observation series. Save this value along with the corresponding - # mod value at the same time. - dfs = [] - for obs, mod in zip(obs_swe_cols, mod_swe_cols): - - # get the data for the current obs and mod columns - # but drop all NaN data that may exist. - dat = df.dropna(subset=[obs, mod, "Water_Year"]).copy() - - # if all data is NaN for the current obs, mod combination - # just skip it. if dat.empty: - print(f"Skipping ({obs}, {mod}) because all data is NaN") + print(f"Skipping {site} (all NaN)") continue - idx = dat.groupby("Water_Year")[obs].idxmax() - dat = dat.loc[idx, [obs, mod, "Water_Year"]].copy() + # Find peak observed SWE per water year + idx = dat.groupby("Water_Year")["Observed"].idxmax() + peak = dat.loc[idx].copy() + peak["Station"] = site - dat.rename(columns={obs: "Observed", mod: "Modeled"}, inplace=True) - dat["Station"] = obs + results.append(peak) - dfs.append(dat) + if not results: + return pd.DataFrame() # nothing to return - # concatenate all dataframes together and return - return pd.concat(dfs) + return pd.concat(results) +######### ORIGINAL CUAHSI FUNCTION BELOW: # def modeled_swe_at_observed_peak( -# observed_df: pd.DataFrame, -# modeled_df: pd.DataFrame, +# df: pd.DataFrame, obs_swe_cols: list[str], mod_swe_cols: list[str] # ) -> pd.DataFrame: # """ -# Compare observed peak SWE to modeled SWE on the same date. -# Assumes both dfs have the SAME column order (stations aligned). -# """ +# Extract modeled SWE values on the dates of observed peak (maximum) SWE. + +# This function evaluates model performance by comparing observed peak SWE +# to the modeled SWE on the same calendar date. For each station and water year, +# the date of maximum observed SWE is identified, and the modeled SWE value +# at that date is extracted. + +# Parameters +# ========== +# df: pandas.DataFrame +# A pandas dataframe containing columns associated with modeled and observed SWE. The +# dataframe must have an datetime[64] index. +# obs_swe_cols: list[str] +# Names of the columns associated with observed SWE +# mod_swe_cols: list[str] +# Names of the columns associated with modeled SWE + +# Returns +# ======= +# df: pandas.DataFrame +# A dataframe containing observed max observed SWE and the modeled SWE at the index of the +# maximum observed. The format of the DataFrame will be: + +# Observed Modeled Water_Year Station +# +# +# ... + +# Example: + +# Observed Modeled Water_Year Station +# 2019-04-18 0.98044 1.0293 2019 CCSS_DAN_swe_m +# 2019-04-20 2.12090 1.3598 2019 CCSS_HRS_swe_m +# 2019-03-28 0.80264 0.6708 2019 CCSS_KIB_swe_m +# 2019-04-07 1.78562 0.9965 2019 CCSS_PDS_swe_m +# ... -# # --- Basic checks --- -# if not isinstance(observed_df.index, pd.DatetimeIndex): -# raise ValueError("observed_df must have a DatetimeIndex") -# if not isinstance(modeled_df.index, pd.DatetimeIndex): -# raise ValueError("modeled_df must have a DatetimeIndex") -# if len(observed_df.columns) != len(modeled_df.columns): -# raise ValueError("DataFrames must have the same number of columns") - -# # --- Align time indices --- -# observed_df, modeled_df = observed_df.align(modeled_df, join="inner") - -# # --- Compute water year vectorized --- -# water_year = compute_water_year_from_index(observed_df.index) - -# results = [] - -# # --- Loop over station columns --- -# for obs_col, mod_col in zip(observed_df.columns, modeled_df.columns): -# dat = pd.DataFrame({ -# "Observed": observed_df[obs_col], -# "Modeled": modeled_df[mod_col], -# "Water_Year": water_year, -# }).dropna() +# """ +# # compute water year if it doesn't already exist in the dataframe. +# # this is needed to properly align the same-day comparison +# if "Water_Year" not in df.columns: +# compute_water_year(df, inplace=True) + +# # check to make sure that the input columns are the same length. +# # Raise an exception if they aren't, because our computation will fail. +# if len(obs_swe_cols) != len(mod_swe_cols): +# raise Exception("Modeled and observed inputs must be the same length") + +# # make sure our column data is represented as float64, otherwise +# # the pandas operations below will fail. +# df = df.apply(pd.to_numeric, errors="coerce").astype("float64") # type: ignore[assignment] +# df["Water_Year"] = df["Water_Year"].astype(int) # keep wateryear an integer + +# # Loop over each pairwise grouping of obs and mod columns that +# # have been provided as inputs. Group data for these stations +# # by water year and determine when the maximum value occurs in +# # the observation series. Save this value along with the corresponding +# # mod value at the same time. +# dfs = [] +# for obs, mod in zip(obs_swe_cols, mod_swe_cols): + +# # get the data for the current obs and mod columns +# # but drop all NaN data that may exist. +# dat = df.dropna(subset=[obs, mod, "Water_Year"]).copy() + +# # if all data is NaN for the current obs, mod combination +# # just skip it. # if dat.empty: -# print(f"Skipping {obs_col} (all NaN)") +# print(f"Skipping ({obs}, {mod}) because all data is NaN") # continue -# # --- Peak observed SWE per water year --- -# idx = dat.groupby("Water_Year")["Observed"].idxmax() -# peak = dat.loc[idx].copy() -# peak["Station"] = obs_col +# idx = dat.groupby("Water_Year")[obs].idxmax() +# dat = dat.loc[idx, [obs, mod, "Water_Year"]].copy() -# results.append(peak) +# dat.rename(columns={obs: "Observed", mod: "Modeled"}, inplace=True) +# dat["Station"] = obs -# return pd.concat(results) +# dfs.append(dat) +# # concatenate all dataframes together and return +# return pd.concat(dfs) -def modeled_vs_observed_peak_swe( - df: pd.DataFrame, obs_swe_cols: list[str], mod_swe_cols: list[str] -) -> pd.DataFrame: + +def modeled_vs_observed_peak_swe(obs_df: pd.DataFrame, model_df: pd.DataFrame) -> pd.DataFrame: """ Extract and compare modeled and observed peak (maximum) SWE values and their timing. - This function identifies the dates and magnitudes of peak SWE - independently for both observed and modeled time series. For each station - and water year, it extracts the maximum observed SWE and its occurrence date, - as well as the maximum modeled SWE and its occurrence date. - Parameters - ========== - df: pandas.DataFrame - A pandas dataframe containing columns associated with modeled and observed SWE. The - dataframe must have an datetime[64] index. - obs_swe_cols: list[str] - Names of the columns associated with observed SWE - mod_swe_cols: list[str] - Names of the columns associated with modeled SWE + ---------- + obs_df : pd.DataFrame + Observed SWE time series. Columns are site IDs (e.g., '380:CO:SNTL'), rows are timestamps. + Must have a DatetimeIndex. + model_df : pd.DataFrame + Modeled SWE time series. Columns are site IDs matching obs_df. Rows are timestamps. Returns - ======= - df: pandas.DataFrame - A dataframe containing maximum observed and modeled SWE at their respective times of - occurence. The format of the DataFrame will be: + ------- + pd.DataFrame + DataFrame containing, for each site and water year: + - Observed peak SWE and date + - Modeled peak SWE and date + - Water year + - Station name (site ID) + """ - Observed Observed_Date Modeled Modeled_Date Water_Year Station - 0 - 1 - ... + # Check for DatetimeIndex + if not isinstance(obs_df.index, pd.DatetimeIndex): + raise ValueError("obs_df must have a DatetimeIndex") + if not isinstance(model_df.index, pd.DatetimeIndex): + raise ValueError("model_df must have a DatetimeIndex") - Example: + # Align the dataframes by time + obs_df, model_df = obs_df.align(model_df, join="inner") - Observed Observed_Date Modeled Modeled_Date Water_Year Station - 0 0.98044 2019-04-18 1.0393 2019-04-10 2019 CCSS_DAN_swe_m - 1 0.41910 2020-04-21 0.5206 2020-04-18 2020 CCSS_DAN_swe_m - 2 2.12090 2019-04-20 1.5498 2019-04-03 2019 CCSS_HRS_swe_m - 3 0.89662 2020-04-10 0.5745 2020-04-10 2020 CCSS_HRS_swe_m - ... + # Compute water year if not present + if "Water_Year" not in obs_df.columns: + obs_df["Water_Year"] = obs_df.index.map(lambda x: x.year + 1 if x.month > 9 else x.year) + results = [] - """ + # Loop over each site column + for site in obs_df.columns: + if site == "Water_Year": + continue # skip Water_Year column + + dat = pd.DataFrame({ + "Observed": obs_df[site], + "Modeled": model_df[site], + "Water_Year": obs_df["Water_Year"] + }).dropna() - # compute water year if it doesn't already exist in the dataframe. - # this is needed to properly align the same-day comparison - if "Water_Year" not in df.columns: - compute_water_year(df, inplace=True) - - # check to make sure that the input columns are the same length. - # Raise an exception if they aren't, because our computation will fail. - if len(obs_swe_cols) != len(mod_swe_cols): - raise Exception("Modeled and observed inputs must be the same length") - - # make sure our column data is represented as float64, otherwise - # the pandas operations below will fail. - df = df.apply(pd.to_numeric, errors="coerce").astype("float64") # type: ignore[assignment] - df["Water_Year"] = df["Water_Year"].astype(int) # keep wateryear an integer - - # Loop over each pairwise grouping of obs and mod columns that - # have been provided as inputs. Group data for these stations - # by water year and determine when the maximum value occurs in - # both the observation and modeled series. Save these values - # along with their corresponding times - dfs = [] - for obs, mod in zip(obs_swe_cols, mod_swe_cols): - - # get the data for the current obs and mod columns - # but drop all NaN data that may exist. - dat = df.dropna(subset=[obs, mod, "Water_Year"]).copy() - - # if all data is NaN for the current obs, mod combination - # just skip it. if dat.empty: - print(f"Skipping ({obs}, {mod}) because all data is NaN") + print(f"Skipping {site} (all NaN)") continue - obs_idx = dat.groupby("Water_Year")[obs].idxmax() - obs_dat = dat.loc[obs_idx, [obs, "Water_Year"]].copy() - obs_dat = obs_dat.rename(columns={obs: "Observed"}) - obs_dat["Observed_Date"] = obs_idx.values + # Peak observed SWE + obs_idx = dat.groupby("Water_Year")["Observed"].idxmax() + obs_peak = dat.loc[obs_idx, ["Observed", "Water_Year"]].copy() + obs_peak["Observed_Date"] = obs_idx.values + + # Peak modeled SWE + mod_idx = dat.groupby("Water_Year")["Modeled"].idxmax() + mod_peak = dat.loc[mod_idx, ["Modeled", "Water_Year"]].copy() + mod_peak["Modeled_Date"] = mod_idx.values + + # Merge peaks on Water_Year + peak_df = obs_peak.merge(mod_peak, on="Water_Year", how="outer") + peak_df["Station"] = site + + results.append(peak_df) + + if not results: + return pd.DataFrame() # nothing to return + + return pd.concat(results).sort_values(["Station", "Water_Year"]).reset_index(drop=True) + + +######### ORIGINAL CUAHSI FUNCTION BELOW: +# def modeled_vs_observed_peak_swe( +# df: pd.DataFrame, obs_swe_cols: list[str], mod_swe_cols: list[str] +# ) -> pd.DataFrame: +# """ +# Extract and compare modeled and observed peak (maximum) SWE values and their timing. + +# This function identifies the dates and magnitudes of peak SWE +# independently for both observed and modeled time series. For each station +# and water year, it extracts the maximum observed SWE and its occurrence date, +# as well as the maximum modeled SWE and its occurrence date. + +# Parameters +# ========== +# df: pandas.DataFrame +# A pandas dataframe containing columns associated with modeled and observed SWE. The +# dataframe must have an datetime[64] index. +# obs_swe_cols: list[str] +# Names of the columns associated with observed SWE +# mod_swe_cols: list[str] +# Names of the columns associated with modeled SWE + +# Returns +# ======= +# df: pandas.DataFrame +# A dataframe containing maximum observed and modeled SWE at their respective times of +# occurence. The format of the DataFrame will be: + +# Observed Observed_Date Modeled Modeled_Date Water_Year Station +# 0 +# 1 +# ... + +# Example: + +# Observed Observed_Date Modeled Modeled_Date Water_Year Station +# 0 0.98044 2019-04-18 1.0393 2019-04-10 2019 CCSS_DAN_swe_m +# 1 0.41910 2020-04-21 0.5206 2020-04-18 2020 CCSS_DAN_swe_m +# 2 2.12090 2019-04-20 1.5498 2019-04-03 2019 CCSS_HRS_swe_m +# 3 0.89662 2020-04-10 0.5745 2020-04-10 2020 CCSS_HRS_swe_m +# ... + + +# """ + +# # compute water year if it doesn't already exist in the dataframe. +# # this is needed to properly align the same-day comparison +# if "Water_Year" not in df.columns: +# compute_water_year(df, inplace=True) + +# # check to make sure that the input columns are the same length. +# # Raise an exception if they aren't, because our computation will fail. +# if len(obs_swe_cols) != len(mod_swe_cols): +# raise Exception("Modeled and observed inputs must be the same length") + +# # make sure our column data is represented as float64, otherwise +# # the pandas operations below will fail. +# df = df.apply(pd.to_numeric, errors="coerce").astype("float64") # type: ignore[assignment] +# df["Water_Year"] = df["Water_Year"].astype(int) # keep wateryear an integer + +# # Loop over each pairwise grouping of obs and mod columns that +# # have been provided as inputs. Group data for these stations +# # by water year and determine when the maximum value occurs in +# # both the observation and modeled series. Save these values +# # along with their corresponding times +# dfs = [] +# for obs, mod in zip(obs_swe_cols, mod_swe_cols): + +# # get the data for the current obs and mod columns +# # but drop all NaN data that may exist. +# dat = df.dropna(subset=[obs, mod, "Water_Year"]).copy() + +# # if all data is NaN for the current obs, mod combination +# # just skip it. +# if dat.empty: +# print(f"Skipping ({obs}, {mod}) because all data is NaN") +# continue - mod_idx = dat.groupby("Water_Year")[mod].idxmax() - mod_dat = dat.loc[mod_idx, [mod, "Water_Year"]].copy() - mod_dat = mod_dat.rename(columns={mod: "Modeled"}) - mod_dat["Modeled_Date"] = mod_idx.values +# obs_idx = dat.groupby("Water_Year")[obs].idxmax() +# obs_dat = dat.loc[obs_idx, [obs, "Water_Year"]].copy() +# obs_dat = obs_dat.rename(columns={obs: "Observed"}) +# obs_dat["Observed_Date"] = obs_idx.values - dfs.append( - # combine the observation and modeled sub-dataframes into one - # by joining them on Water_Year. Then add - obs_dat.merge(mod_dat, on="Water_Year", how="outer").assign( - # create a new "Station" column containing the value of the obs - Station=obs - ) - ) +# mod_idx = dat.groupby("Water_Year")[mod].idxmax() +# mod_dat = dat.loc[mod_idx, [mod, "Water_Year"]].copy() +# mod_dat = mod_dat.rename(columns={mod: "Modeled"}) +# mod_dat["Modeled_Date"] = mod_idx.values - # concatenate all dataframes together and return - return pd.concat(dfs).reset_index().drop("index", axis=1) +# dfs.append( +# # combine the observation and modeled sub-dataframes into one +# # by joining them on Water_Year. Then add +# obs_dat.merge(mod_dat, on="Water_Year", how="outer").assign( +# # create a new "Station" column containing the value of the obs +# Station=obs +# ) +# ) +# # concatenate all dataframes together and return +# return pd.concat(dfs).reset_index().drop("index", axis=1) def compute_melt_period( - swe_series: pd.Series, min_zero_days: int = 10 -) -> dict[str, Any]: + swe_df: pd.DataFrame, + min_zero_days: int = 10 +) -> pd.DataFrame: """ - computes the snow melt period for the input Series. - + Computes melt period statistics for each station in a DataFrame. + Parameters - ========== - swe_series: pandas.Series - A pandas series containing SWE values indexed by datetime. - min_zero_days: int -> 10 - The minimum number of consecutive days with zero SWE to consider - when determining the melt end date. - + ---------- + swe_df : pd.DataFrame + SWE DataFrame with datetime index and one column per station. + min_zero_days : int + Minimum consecutive zero SWE days to define melt end. + Returns - ======= - dict[str, Any] - A dictionary containing melt period information with the following keys: - peak_date, peak_swe_m, melt_end_date, melt_period_days, melt_rate_m/d - + ------- + pd.DataFrame + Melt period statistics per station: + columns: Station, Peak_SWE_Date, Peak_SWE_m, Melt_End_Date, Melt_Period_Days, Melt_Rate_m_per_day """ + result = [] + + for station in swe_df.columns: + swe_series = pd.to_numeric(swe_df[station], errors="coerce").dropna() + if swe_series.empty or swe_series.max() == 0: + continue + try: + stats = compute_melt_period(swe_series, min_zero_days=min_zero_days) + result.append({ + "Station": station, + "Peak_SWE_Date": stats["peak_date"], + "Peak_SWE_m": stats["peak_swe_m"], + "Melt_End_Date": stats["melt_end_date"], + "Melt_Period_Days": stats["melt_period_days"], + "Melt_Rate_m_per_day": stats["melt_rate_m/d"], + }) + except ValueError: + continue + + return pd.DataFrame(result) + +# def compute_melt_period( +# swe_series: pd.Series, min_zero_days: int = 10 +# ) -> dict[str, Any]: +# """ +# computes the snow melt period for the input Series. + +# Parameters +# ========== +# swe_series: pandas.Series +# A pandas series containing SWE values indexed by datetime. +# min_zero_days: int -> 10 +# The minimum number of consecutive days with zero SWE to consider +# when determining the melt end date. + +# Returns +# ======= +# dict[str, Any] +# A dictionary containing melt period information with the following keys: +# peak_date, peak_swe_m, melt_end_date, melt_period_days, melt_rate_m/d - peak_date = swe_series.idxmax() - peak_swe = swe_series.max() +# """ - after_peak = swe_series.loc[peak_date:] +# peak_date = swe_series.idxmax() +# peak_swe = swe_series.max() - zero_streak = 0 - melt_end_date = None +# after_peak = swe_series.loc[peak_date:] - for date, value in after_peak.items(): - if value == 0: - zero_streak += 1 - else: - zero_streak = 0 +# zero_streak = 0 +# melt_end_date = None - if zero_streak >= min_zero_days: - melt_end_date = date - break +# for date, value in after_peak.items(): +# if value == 0: +# zero_streak += 1 +# else: +# zero_streak = 0 - if melt_end_date is None: - raise ValueError( - f"Could not find a period of at least {min_zero_days} consecutive zero SWE days after the peak." - ) +# if zero_streak >= min_zero_days: +# melt_end_date = date +# break - melt_period_days = (melt_end_date - peak_date).days +# if melt_end_date is None: +# raise ValueError( +# f"Could not find a period of at least {min_zero_days} consecutive zero SWE days after the peak." +# ) - # Compute melt rate, but handle the case where melt_period_days is zero to avoid division by zero - if melt_period_days == 0: - melt_rate = np.nan - else: - melt_rate = peak_swe / melt_period_days +# melt_period_days = (melt_end_date - peak_date).days - return { - "peak_date": peak_date, - "peak_swe_m": peak_swe, - "melt_end_date": melt_end_date, - "melt_period_days": melt_period_days, - "melt_rate_m/d": melt_rate, - } +# # Compute melt rate, but handle the case where melt_period_days is zero to avoid division by zero +# if melt_period_days == 0: +# melt_rate = np.nan +# else: +# melt_rate = peak_swe / melt_period_days +# return { +# "peak_date": peak_date, +# "peak_swe_m": peak_swe, +# "melt_end_date": melt_end_date, +# "melt_period_days": melt_period_days, +# "melt_rate_m/d": melt_rate, +# } def compute_melt_period_statistics( - df: pd.DataFrame, min_zero_days: int = 10 + obs_df: pd.DataFrame, + model_df: pd.DataFrame, + min_zero_days: int = 10 ) -> pd.DataFrame: """ - Computes melt period statistics for each station and water year in the input DataFrame. + Computes melt period statistics for each station and water year for observed and modeled SWE. Parameters - ========== + ---------- + obs_df : pd.DataFrame + Observed SWE data with datetime index and one column per station. + model_df : pd.DataFrame + Modeled SWE data with datetime index and one column per station. + min_zero_days : int + Minimum consecutive zero days to define the end of melt period. Returns - ======= - pandas.DataFrame - A pandas DataFrame containing melt period statistics with the following columns: - Water_Year, Station, Peak_SWE_Date, Peak_SWE_m, Melt_End_Date, Melt_Period_Days, - Melt_Rate_m_per_day - + ------- + pd.DataFrame + Melt period statistics with columns: + Water_Year, Station, Peak_SWE_Date_Obs, Peak_SWE_m_Obs, Melt_End_Date_Obs, Melt_Period_Days_Obs, Melt_Rate_m_per_day_Obs, + Peak_SWE_Date_Mod, Peak_SWE_m_Mod, Melt_End_Date_Mod, Melt_Period_Days_Mod, Melt_Rate_m_per_day_Mod """ - - # TODO: move ccss columns as an input parameter + result = [] - # Identify CCSS SWE columns - ccss_columns = [ - col for col in df.columns if col.startswith("CCSS_") and col.endswith("_swe_m") - ] + # Align indices between observed and modeled + common_index = obs_df.index.intersection(model_df.index) + obs_df = obs_df.loc[common_index] + model_df = model_df.loc[common_index] - for wy, group in df.groupby("Water_Year"): - for station_col in ccss_columns: + # Identify station columns (assuming same names in obs and mod) + stations = obs_df.columns.intersection(model_df.columns) - # TODO: refactore dropna handling similar to other functions - # Clean series - swe_series = pd.to_numeric(group[station_col], errors="coerce").dropna() + # Ensure Water_Year column exists for grouping + if "Water_Year" not in obs_df.columns: + compute_water_year(obs_df, inplace=True) + if "Water_Year" not in model_df.columns: + compute_water_year(model_df, inplace=True) - # Skip if insufficient data - if swe_series.empty or swe_series.max() == 0: + for wy, obs_group in obs_df.groupby("Water_Year"): + mod_group = model_df.loc[obs_group.index] # Align modeled data + + for station in stations: + # Observed SWE + obs_series = pd.to_numeric(obs_group[station], errors="coerce").dropna() + if obs_series.empty or obs_series.max() == 0: continue try: - # Compute melt period stats - stats = compute_melt_period(swe_series, min_zero_days=min_zero_days) - result.append( - { - "Water_Year": wy, - "Station": station_col, - "Peak_SWE_Date": stats["peak_date"], - "Peak_SWE_m": stats["peak_swe_m"], - "Melt_End_Date": stats["melt_end_date"], - "Melt_Period_Days": stats["melt_period_days"], - "Melt_Rate_m_per_day": stats["melt_rate_m/d"], - } - ) + obs_stats = compute_melt_period(obs_series, min_zero_days=min_zero_days) except ValueError: - # If melt period cannot be determined, skip continue + # Modeled SWE + mod_series = pd.to_numeric(mod_group[station], errors="coerce").dropna() + try: + mod_stats = compute_melt_period(mod_series, min_zero_days=min_zero_days) + except ValueError: + mod_stats = { + "peak_date": None, + "peak_swe_m": None, + "melt_end_date": None, + "melt_period_days": None, + "melt_rate_m/d": None + } + + result.append({ + "Water_Year": wy, + "Station": station, + "Peak_SWE_Date_Obs": obs_stats["peak_date"], + "Peak_SWE_m_Obs": obs_stats["peak_swe_m"], + "Melt_End_Date_Obs": obs_stats["melt_end_date"], + "Melt_Period_Days_Obs": obs_stats["melt_period_days"], + "Melt_Rate_m_per_day_Obs": obs_stats["melt_rate_m/d"], + "Peak_SWE_Date_Mod": mod_stats["peak_date"], + "Peak_SWE_m_Mod": mod_stats["peak_swe_m"], + "Melt_End_Date_Mod": mod_stats["melt_end_date"], + "Melt_Period_Days_Mod": mod_stats["melt_period_days"], + "Melt_Rate_m_per_day_Mod": mod_stats["melt_rate_m/d"], + }) + return pd.DataFrame(result) +# def compute_melt_period_statistics( +# df: pd.DataFrame, min_zero_days: int = 10 +# ) -> pd.DataFrame: +# """ +# Computes melt period statistics for each station and water year in the input DataFrame. + +# Parameters +# ========== + +# Returns +# ======= +# pandas.DataFrame +# A pandas DataFrame containing melt period statistics with the following columns: +# Water_Year, Station, Peak_SWE_Date, Peak_SWE_m, Melt_End_Date, Melt_Period_Days, +# Melt_Rate_m_per_day + +# """ + +# # TODO: move ccss columns as an input parameter +# result = [] + +# # Identify CCSS SWE columns +# ccss_columns = [ +# col for col in df.columns if col.startswith("CCSS_") and col.endswith("_swe_m") +# ] + +# for wy, group in df.groupby("Water_Year"): +# for station_col in ccss_columns: + +# # TODO: refactore dropna handling similar to other functions +# # Clean series +# swe_series = pd.to_numeric(group[station_col], errors="coerce").dropna() + +# # Skip if insufficient data +# if swe_series.empty or swe_series.max() == 0: +# continue + +# try: +# # Compute melt period stats +# stats = compute_melt_period(swe_series, min_zero_days=min_zero_days) +# result.append( +# { +# "Water_Year": wy, +# "Station": station_col, +# "Peak_SWE_Date": stats["peak_date"], +# "Peak_SWE_m": stats["peak_swe_m"], +# "Melt_End_Date": stats["melt_end_date"], +# "Melt_Period_Days": stats["melt_period_days"], +# "Melt_Rate_m_per_day": stats["melt_rate_m/d"], +# } +# ) +# except ValueError: +# # If melt period cannot be determined, skip +# continue + +# return pd.DataFrame(result) + def compute_snow_timing_grid(grid_swe_da, water_year, threshold=1.0, smooth_window=5): """ Compute peak SWE timing, melt-out timing, and snow duration From 1d43fbd921a784c7fd52b8e63f452e8fc658179f Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Mon, 27 Apr 2026 15:31:11 -0600 Subject: [PATCH 6/8] update PF SWE notebook with stats section in NWM; slight reorganization of NWM SWE --- .../nwm/nwm_swe_point_scale_evaluation.ipynb | 147 +- .../parflow_swe_point_scale_evaluation.ipynb | 6267 +---------------- src/cssi_evaluation/utils/evaluation_utils.py | 19 +- src/cssi_evaluation/variables/snow_utils.py | 304 - 4 files changed, 314 insertions(+), 6423 deletions(-) diff --git a/examples/nwm/nwm_swe_point_scale_evaluation.ipynb b/examples/nwm/nwm_swe_point_scale_evaluation.ipynb index c01c054..999dd20 100644 --- a/examples/nwm/nwm_swe_point_scale_evaluation.ipynb +++ b/examples/nwm/nwm_swe_point_scale_evaluation.ipynb @@ -244,7 +244,7 @@ "outputs": [], "source": [ "# Create a folder to save results. Raise an error if the path already exists\n", - "Path(OBS_OutputFolder).mkdir(exist_ok=False)" + "Path(OBS_OutputFolder).mkdir(exist_ok=True)" ] }, { @@ -335,7 +335,7 @@ "outputs": [], "source": [ "# Create a folder to save results. If the folder already exists, an error will be raised.\n", - "Path(MOD_OutputFolder).mkdir(exist_ok=False)\n", + "Path(MOD_OutputFolder).mkdir(exist_ok=True)\n", "\n", "# Download NWM SWE data for the sites within the watershed bounding box, save to /mod_outputs folder\n", "input_crs = 'EPSG:4326' # WGS84 lat/lon. This is the CRS of the input geodataframe (gdf_in_bbox) \n", @@ -842,7 +842,7 @@ "source": [ "
\n", "

🧠 Reflect

\n", - "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE so much earlier and less than observed in Paradise Meadow (PDS)? Perhaps try changing the my_site_code from earlier in the notebook to rerun nwm_utils.comparison_plots() to see the timeseries. \n", + "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE so much earlier and less than observed in Paradise Meadow (PDS)? Perhaps try changing the my_site_code from earlier in the notebook to rerun plot_utils.comparison_plots() to see the timeseries. \n", "\n", "
What happens if you change the year that is plotted?
✏️ Try modifying the bar plot code from bar1 = year1.hvplot.bar to bar1 = year2.hvplot.bar

\n", "
" @@ -1086,6 +1086,56 @@ "metrics_df" ] }, + { + "cell_type": "markdown", + "id": "2cfc514c", + "metadata": {}, + "source": [ + "Look at plots of summary statistics for each station. Here we look at Bias and NSE for each station in the watershed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d6b779b", + "metadata": {}, + "outputs": [], + "source": [ + "# Bias scatter\n", + "scatter = metrics_df.hvplot.scatter(\n", + " x='site_id',\n", + " y='bias',\n", + " size=100,\n", + " rot=45,\n", + " ylabel='Bias (m)',\n", + " title='Mean SWE Bias by Station'\n", + ")\n", + "\n", + "hline = hv.HLine(0).opts(color='black', line_dash='dashed', line_width=1)\n", + "\n", + "scatter * hline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99110e67", + "metadata": {}, + "outputs": [], + "source": [ + "# NSE histogram\n", + "metrics_df.hvplot.bar(\n", + " x='site_id',\n", + " y='nse',\n", + " rot=45,\n", + " ylabel='Nash–Sutcliffe Efficiency',\n", + " title='NSE by Station',\n", + " height=400,\n", + " width=600,\n", + " bar_width=0.5\n", + ")\n" + ] + }, { "cell_type": "markdown", "id": "5c521c83", @@ -1099,6 +1149,30 @@ "
" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd685949", + "metadata": {}, + "outputs": [], + "source": [ + "plot_utils.comparison_plots(obs_df, model_df, f'{my_site_code}', f'{my_site_code}', site_label=None)\n", + "\n", + "# Change the site code to see other Snotel Stations --> e.g., '688:CO:SNTL'\n", + "#plot_utils.comparison_plots(obs_df, model_df, '688:CO:SNTL', '688:CO:SNTL', site_label=None)" + ] + }, + { + "cell_type": "markdown", + "id": "965e400b", + "metadata": {}, + "source": [ + "
\n", + "

🧠 Reflect

\n", + "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why? As you review the results, compare the peak flow amounts and the timing of snowmelt onset. Do you see any significant differences? Which dataset indicates an earlier melt?

\n", + "
" + ] + }, { "cell_type": "markdown", "id": "ce04cad1", @@ -1141,53 +1215,6 @@ "For some sites, Pearson and Spearman correlations are both close to 1, suggesting a strong relationship between observed and modeled SWE. As shown on the timeseries plot, this strong correlation alone does not indicate a \"good\" model. For example, it does not guarantee accurate timing of key events, such as peak SWE or melt onset. Let's compare these as well. The following code uses `report_max_dates_and_values` function to identify the peak SWE value and the date it occurs for both the observed (CCSS) and modeled (NWM) datasets. " ] }, - { - "cell_type": "markdown", - "id": "965e400b", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why? As you review the results, compare the peak flow amounts and the timing of snowmelt onset. Do you see any significant differences? Which dataset indicates an earlier melt?

\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "99110e67", - "metadata": {}, - "outputs": [], - "source": [ - "metrics_df.hvplot.bar(\n", - " x='site_id',\n", - " y='nse',\n", - " rot=45,\n", - " ylabel='Nash–Sutcliffe Efficiency',\n", - " title='NSE by Station',\n", - " height=400,\n", - " width=600,\n", - " bar_width=0.5\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6d6b779b", - "metadata": {}, - "outputs": [], - "source": [ - "metrics_df.hvplot.scatter(\n", - " x='site_id',\n", - " y='bias',\n", - " size=100,\n", - " rot=45,\n", - " ylabel='Bias (m)',\n", - " title='Mean SWE Bias by Station'\n", - ")\n" - ] - }, { "cell_type": "markdown", "id": "dc3c56e9", @@ -1201,19 +1228,11 @@ "id": "310c309c", "metadata": {}, "source": [ + "One way to learn more about the model performance is to combine metrics that tell us about different aspects of model behavior—such as timing, variability, and magnitude—rather than relying on a single summary measure.\n", + "\n", "The Condon diagram separates model performance into quadrants based on two metrics: **Spearman’s rho** (shape/time agreement) and **relative bias** (magnitude error). The horizontal line at 0.5 distinguishes whether the model captures the temporal pattern well (above 0.5 = good shape), while the vertical line is traditionally placed at a relative bias of 1.0, which represents a 100% error. This means the model’s total error is as large as the observed signal itself. This threshold has a clear physical interpretation and is used in the original Condon framework to distinguish acceptable vs. large bias. " ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "10a68672", - "metadata": {}, - "outputs": [], - "source": [ - "%reload_ext autoreload" - ] - }, { "cell_type": "code", "execution_count": null, @@ -1223,14 +1242,6 @@ "source": [ "plot_utils.plot_condon_diagram(metrics_df, variable=\"SWE\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a2ae0058-3437-4ee0-8f0a-5bc1a289da37", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb index 51b0b81..85bf7d5 100644 --- a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb +++ b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb @@ -7,9 +7,11 @@ "source": [ "![NWM](../img/NWM.png)\n", "\n", - "# Use HydroData to Retrieve Modeled and Observed Snow Data for a Watershed of Interest with ParFlow-CONUS Outputs vs Observed Snow Water Equivalent (SWE) - Full Evaluation Workflow\n", + "# Use HydroData to Retrieve Modeled and Observed Snow Data for a Watershed of Interest \n", + "## ParFlow-CONUS Outputs vs Observed Snow Water Equivalent (SWE) \n", + "\n", "Authors: Irene Garousi-Nejad (igarousi@cuahsi.org), Danielle Tijerina-Kreuzer (dtijerina@cuahsi.org) \n", - "Last updated: March 2026" + "Last updated: April 2026" ] }, { @@ -18,7 +20,8 @@ "metadata": {}, "source": [ "#### Introduction: \n", - "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites. We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. " + "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites, implementing a full Model Evaluation Workflow. \n", + "We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. " ] }, { @@ -36,7 +39,7 @@ "source": [ "### 1a. Python Environment \n", "\n", - "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." + "Ensure that the `cssi_evaluation` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `GettingStarted.md` file." ] }, { @@ -49,252 +52,12 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "ce97d33e", "metadata": { "tags": [] }, - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.1/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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "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" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.jsdelivr.net/npm/@holoviz/geoviews@1.11.0/dist/geoviews.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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.jsdelivr.net/npm/@holoviz/geoviews@1.11.0/dist/geoviews.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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "import os\n", "import sys\n", @@ -320,9 +83,7 @@ "sys.path.append(str((Path.cwd().absolute() / \"../../src\").resolve()))\n", "\n", "from cssi_evaluation.variables import snow_utils\n", - "from cssi_evaluation.utils import metric_utils\n", - "from cssi_evaluation.utils import evaluation_utils\n", - "from cssi_evaluation.utils import plot_utils\n", + "from cssi_evaluation.utils import metric_utils, evaluation_utils, plot_utils\n", "\n", "hv.extension('bokeh')\n", "\n", @@ -343,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a365996d", "metadata": {}, "outputs": [], @@ -351,6 +112,7 @@ "# You need to register on https://hydrogen.princeton.edu/pin \n", "# and run the following with your registered information\n", "# before you can use the hydrodata utilities\n", + "#hf.register_api_pin(\"your_email\", \"your_api_pin\")\n", "hf.register_api_pin(\"dtt2@princeton.edu\", \"7837\")" ] }, @@ -368,19 +130,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "6f2b08d0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dashboard link: http://127.0.0.1:8787/status\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "# use a try accept loop so we only instantiate the client\n", "# if it doesn't already exist.\n", @@ -403,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "e50dc99d", "metadata": {}, "outputs": [], @@ -441,25 +194,16 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "c8355563", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HUC-8 ID: 14020001\n", - "HUC-8 name: East-Taylor\n" - ] - } - ], + "outputs": [], "source": [ "# ✏️ Specify HUC8 ID and Name for watershed of interest\n", - "huc_8_code = '14020001' # East-Taylor HUC-8\n", + "huc_8_code = '14010001' # East-Taylor HUC-8\n", "print(f'HUC-8 ID: {huc_8_code}')\n", "\n", - "huc_8_name = 'East-Taylor'\n", + "huc_8_name = 'Colorado-Headwaters'\n", "print(f'HUC-8 name: {huc_8_name}')" ] }, @@ -492,17 +236,13 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "d74eeccb", "metadata": {}, "outputs": [], "source": [ "# Create a folder to save observations\n", - "isExist = os.path.exists(OBS_OutputFolder)\n", - "if isExist == True:\n", - " exit\n", - "else:\n", - " os.mkdir(OBS_OutputFolder)" + "Path(OBS_OutputFolder).mkdir(exist_ok=True)" ] }, { @@ -515,131 +255,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "1cb40a7c", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
site_idsite_namesite_typeagencystatelatitudelongitudefirst_date_data_availablelast_date_data_availablerecord_countsite_query_urldate_metadata_last_updatedtz_cddoihuc8conus1_iconus1_jconus2_iconus2_jusda_elevation
0380:CO:SNTLButteSNOTEL stationNRCSCO38.89435-106.953271981-10-012026-03-2116243https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone14020001942.0650.01372.01601.010200.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCO38.81982-106.589621980-08-042026-03-2116666https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone14020001972.0638.01402.01589.09621.0
\n", - "
" - ], - "text/plain": [ - " site_id site_name site_type agency state latitude longitude \\\n", - "0 380:CO:SNTL Butte SNOTEL station NRCS CO 38.89435 -106.95327 \n", - "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO 38.81982 -106.58962 \n", - "\n", - " first_date_data_available last_date_data_available record_count \\\n", - "0 1981-10-01 2026-03-21 16243 \n", - "1 1980-08-04 2026-03-21 16666 \n", - "\n", - " site_query_url \\\n", - "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "\n", - " date_metadata_last_updated tz_cd doi huc8 conus1_i conus1_j \\\n", - "0 2023-03-07 PST None 14020001 942.0 650.0 \n", - "1 2023-03-07 PST None 14020001 972.0 638.0 \n", - "\n", - " conus2_i conus2_j usda_elevation \n", - "0 1372.0 1601.0 10200.0 \n", - "1 1402.0 1589.0 9621.0 " - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Request site-level attributes for these sites\n", "metadata_df = hf.get_point_metadata(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", @@ -673,7 +292,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "ffd57847", "metadata": {}, "outputs": [], @@ -721,3939 +340,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "02571354", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - " \n", - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "plot_sites_on_map(metadata_df, color_by_site_type=False)" ] @@ -4670,85 +360,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "f0f2beb6", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
date380:CO:SNTL680:CO:SNTL
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", - "
" - ], - "text/plain": [ - " date 380:CO:SNTL 680:CO:SNTL\n", - "0 2003-10-01 0.0 0.0\n", - "1 2003-10-02 0.0 0.0\n", - "2 2003-10-03 0.0 0.0\n", - "3 2003-10-04 0.0 0.0\n", - "4 2003-10-05 0.0 0.0" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Request point observations data\n", "obs_df = hf.get_point_data(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", @@ -4776,17 +391,13 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "545a9d22", "metadata": {}, "outputs": [], "source": [ "# Create a folder to save results\n", - "isExist = os.path.exists(MOD_OutputFolder)\n", - "if isExist == True:\n", - " exit\n", - "else:\n", - " os.mkdir(MOD_OutputFolder)" + "Path(MOD_OutputFolder).mkdir(exist_ok=True)" ] }, { @@ -4802,21 +413,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "10647da1", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'id': '12', 'dataset': 'conus1_baseline_mod', 'dataset_version': '', 'file_type': 'pfb', 'variable': 'swe', 'dataset_var': 'swe', 'temporal_resolution': 'daily', 'units': 'mm', 'aggregation': 'eod', 'grid': 'conus1', 'path': 'swe.daily.eod.{wy_daynum:03d}.pfb', 'file_grouping': 'wy_daynum', 'entry_start_date': None, 'entry_end_date': None, 'documentation_notes': '', 'site_type': '', 'variable_type': 'surface_water', 'has_z': '', 'dataset_type': 'parflow', 'datasource': 'hydroframe', 'paper_dois': '10.5194/gmd-14-7223-2021', 'dataset_dois': '', 'dataset_start_date': '2002-10-01', 'dataset_end_date': '2006-09-30', 'structure_type': 'gridded', 'has_ensemble': '', 'unit_type': 'length', 'period': 'daily'}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "conus1_options = {\n", " \"dataset\": \"conus1_baseline_mod\",\n", @@ -4835,7 +435,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "ba48a33a", "metadata": {}, "outputs": [], @@ -4856,99 +456,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "b82b9574", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Image [x,y] (SWE)" - ] - }, - "execution_count": 14, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1011" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "grid_swe_map = xr.DataArray(grid_swe[0], dims=(\"y\", \"x\"), name=\"SWE\")\n", "grid_swe_map.hvplot.image(cmap=\"YlGnBu\", colorbar=True, aspect=\"equal\", title=f\"{huc_8_name} Gridded SWE on 2004-04-01\")\n" @@ -4964,7 +475,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "17143151", "metadata": {}, "outputs": [], @@ -4990,85 +501,10 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "a814204c", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
date380:CO:SNTL680:CO:SNTL
02003-10-010.00.0
12003-10-020.00.0
22003-10-030.00.0
32003-10-040.00.0
42003-10-050.00.0
\n", - "
" - ], - "text/plain": [ - " date 380:CO:SNTL 680:CO:SNTL\n", - "0 2003-10-01 0.0 0.0\n", - "1 2003-10-02 0.0 0.0\n", - "2 2003-10-03 0.0 0.0\n", - "3 2003-10-04 0.0 0.0\n", - "4 2003-10-05 0.0 0.0" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Loop over each station in metadata_df\n", "for idx, row in metadata_df.iterrows():\n", @@ -5110,42 +546,34 @@ "id": "7464828b", "metadata": {}, "source": [ - "#### Quick plot sanity check \n", + "#### Quick sanity check plot \n", "Plot a simple timeseries of modeled and observed SWE to make sure our data retrieval was successful. " ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "fbe43f6a", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAoFxJREFUeJzs3Xd8XXXh//HXuTf3Zu/ZtEn3pC100ZbVAoWyRRBQEESqomxBQeSnwleWoIAWAUWgyCqyVASBMlosZbSF0r1X2ibN3uMm957fHye5oxlN0pvcm+T9fDz66Oesez/JSW7u+36WYZqmiYiIiIiIiIgEnS3UFRARERERERHprxS6RURERERERHqIQreIiIiIiIhID1HoFhEREREREekhCt0iIiIiIiIiPUShW0RERERERKSHKHSLiIiIiIiI9BCFbhEREREREZEeEhHqCoQDj8fDgQMHiI+PxzCMUFdHREREREREwpxpmlRVVZGdnY3N1n57tkI3cODAAXJyckJdDREREREREelj8vLyGDJkSLvHFbqB+Ph4wPpmJSQkBBzzeDwUFRWRnp7e4acXEhq6P+FL9yb86R6FP92j8Kb7E550X8Kf7lF40/3pvMrKSnJycrx5sj0K3eDtUp6QkNBm6K6vrychIUE/dGFI9yd86d6EP92j8Kd7FN50f8KT7kv40z0Kb7o/XXe4Icr6LoqIiIiIiIj0EIVuERERERERkR6i0C0iIiIiIiLSQzSmuwvcbjeNjY2hrob48Xg8NDY2Ul9f36kxJw6HA7vd3gs1ExERERERUejuFNM0KSgooKKiItRVkUOYponH46GqqqrTa6wnJSWRlZWlNdlFRERERKTHKXR3QnV1NU1NTWRkZBATE6OwFkZM06SpqYmIiIjD3hfTNKmtraWwsBCAQYMG9UYVRURERERkAFPoPgy32019fT2DBg0iNTU11NWRQ3QldANER0cDUFhYSEZGhrqai4iIiIhIj9JEaofR2NiIYRjExMSEuioSJC33UuPzRURERESkpyl0d5K6lPcfupciIiIiItJbFLpFREREREREeohCt3Tb0qVLMQyD8vLyTl8zbNgwHnnkkSN63jvvvJNjjjnmiB5DREREJOw01sNnT8Cuj0NdExEJIoXufuzKK6/EMAx+/OMftzp2zTXXYBgGV155Ze9XTERERERaW3Y/vHMb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RMTdgyAiIiLXWrx4MWpra7F169Z28xYuXIijR4/i2LFj7eb985//xOOPP46amhoAV7/T/dRTT6G2tta+TGlpKe644w6cPXsWVqvVPn3atGm466678Nprr/X6/SEiItIKL3cPgIiIiNxLRKDT6QAAOTk5eO2113Ds2DHU19ejtbUVV65cwc8//wyz2dzh7fPy8iAiGDZsmMP0pqYmDBw40OXjJyIiUjM23URERH3c8ePHERUVhfLyctx3331ITU3Fn//8ZwQGBmL//v1ISUlBS0uL09srigKDwYDc3FwYDAaHef7+/q4ePhERkaqx6SYiIurDdu/ejcLCQjz99NM4cuQIWltb8fbbb0Ovv3ral08//dRheaPRCJvN5jBt7NixsNlsqK6uxsSJE2/Z2ImIiLSATTcREVEf0dTUhKqqKthsNpw/fx5fffUV0tPTMXPmTCxatAiFhYVobW3FunXrMGvWLBw4cADr1693WMeQIUPQ2NiIb775BnFxcfDz88OwYcPw29/+FosWLcLbb7+NsWPHoqamBrt378bo0aNx3333uekeExERuR/PXk5ERNRHfPXVVwgLC8OQIUOQnJyMnJwcrF27Ftu2bYPBYMCYMWOwatUqvPHGGxg1ahQ2bdqE9PR0h3VMmDABqampWLBgAYKDg/Hmm28CADZu3IhFixbh2WefRUxMDGbPno3vvvsOERER7rirREREqsGzlxMRERERERG5CN/pJiIiIiIiInIRNt1ERERERERELsKmm4iIiIiIiMhF2HQTERERERERuQibbiIiIiIiIiIXYdNNRERERERE5CJsuomIiIiIiIhchE03ERERERERkYuw6SYiIiIiIiJyETbdRERERERERC7CppuIiIiIiIjIRdh0ExEREREREbnI/wOi5/NV6KggEAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(10, 4))\n", "\n", - "ax.plot(obs_df[\"date\"], model_df[\"380:CO:SNTL\"], label=\"Modeled\", linewidth=2)\n", + "for site_id in obs_df.columns:\n", + " if site_id == \"date\":\n", + " continue\n", "\n", - "ax.plot(obs_df[\"date\"], obs_df[\"380:CO:SNTL\"], label=\"Observed\", linewidth=2)\n", + " ax.plot(obs_df[\"date\"], obs_df[site_id], label=f\"{site_id} Obs\", linewidth=2)\n", + " ax.plot(obs_df[\"date\"], model_df[site_id], linestyle=\"--\", label=f\"{site_id} Mod\")\n", "\n", "ax.set_xlabel(\"Date\")\n", "ax.set_ylabel(\"SWE (mm)\")\n", "\n", - "# Date formatting for x-axis\n", + "# Date formatting\n", "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", "ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))\n", "\n", - "ax.legend(loc='upper left')\n", + "ax.legend(loc='upper left', fontsize=8)\n", "ax.grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()" @@ -5185,13 +613,13 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "473b9326", "metadata": {}, "outputs": [], "source": [ "# choose a site of interest within the watershed\n", - "my_site_code = '380:CO:SNTL'\n", + "my_site_code = '335:CO:SNTL'\n", "\n", "# make sure date columns are datetime and set as index for easier plotting and metric calculations\n", "obs_df[\"date\"] = pd.to_datetime(obs_df[\"date\"])\n", @@ -5201,107 +629,20 @@ "model_df = model_df.set_index(\"date\")" ] }, + { + "cell_type": "markdown", + "id": "7905a491", + "metadata": {}, + "source": [ + "Use the `comparison_plots` utility function to provide a quicky view of the data." + ] + }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "1c1f5648", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Layout\n", - " .Overlay.I :Overlay\n", - " .Curve.Observed_SWE :Curve [date] (observed)\n", - " .Curve.Modeled_SWE :Curve [date] (modeled)\n", - " .Overlay.II :Overlay\n", - " .Scatter.I :Scatter [observed] (modeled,date)\n", - " .Curve.A_1_colon_1_Line :Curve [x] (y)" - ] - }, - "execution_count": 19, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1081" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "plot_utils.comparison_plots(obs_df, model_df, f'{my_site_code}', f'{my_site_code}', site_label=None)" ] @@ -5328,101 +669,10 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "28f3e419", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Overlay\n", - " .Scatter.I :Scatter [observed] (modeled,color,date,month)\n", - " .Curve.A_1_colon_1_Line :Curve [x] (y)" - ] - }, - "execution_count": 20, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1266" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "plot = plot_utils.plot_custom_scatter_SWE(\n", " obs_df,\n", @@ -5485,117 +735,31 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "2e15a789", "metadata": {}, "outputs": [], "source": [ "# isolate the columns associated with observations and model predictions.\n", "# these will be inputs to our same-day comparison function.\n", - "combined_df = pd.concat([obs_df, model_df], axis=1)\n", "obs_swe_cols = obs_df.columns.tolist()\n", "mod_swe_cols = model_df.columns.tolist()" ] }, { - "cell_type": "code", - "execution_count": 23, - "id": "ff514342", + "cell_type": "markdown", + "id": "3854f098", "metadata": {}, - "outputs": [], "source": [ - "# OLD\n", - "# df_observed_peak = snow_utils.modeled_swe_at_observed_peak(combined_df, obs_swe_cols, mod_swe_cols)\n", - "# df_observed_peak" + "Use the `modeled_swe_at_observed_peak` utility function to perform the same-day SWE comparison." ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "ed342496", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ObservedModeledWater_YearStation
date
2004-04-12304.80229.3069382004380:CO:SNTL
2005-04-12464.82341.9169482005380:CO:SNTL
2004-04-11231.145.5650522004680:CO:SNTL
2005-04-07274.32177.9033702005680:CO:SNTL
\n", - "
" - ], - "text/plain": [ - " Observed Modeled Water_Year Station\n", - "date \n", - "2004-04-12 304.80 229.306938 2004 380:CO:SNTL\n", - "2005-04-12 464.82 341.916948 2005 380:CO:SNTL\n", - "2004-04-11 231.14 5.565052 2004 680:CO:SNTL\n", - "2005-04-07 274.32 177.903370 2005 680:CO:SNTL" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# compute the same-day SWE comparison during the observed peak SWE for each of the observation and modeled sites\n", "df_observed_peak = snow_utils.modeled_swe_at_observed_peak(obs_df, model_df)\n", @@ -5612,100 +776,10 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "297f8608", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":NdOverlay [Source]\n", - " :Scatter [Station] (SWE,Water_Year)" - ] - }, - "execution_count": 25, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1354" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Rearrange the dataframe to long format for easier plotting\n", "df_long = (\n", @@ -5753,93 +827,10 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "78907563", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ObservedWater_YearObserved_DateModeledModeled_DateStation
0304.8020042004-04-12237.8717462004-03-19380:CO:SNTL
1464.8220052005-04-12341.9169482005-04-12380:CO:SNTL
2231.1420042004-04-11169.7645442004-03-09680:CO:SNTL
3274.3220052005-04-07219.4611052005-03-27680:CO:SNTL
\n", - "
" - ], - "text/plain": [ - " Observed Water_Year Observed_Date Modeled Modeled_Date Station\n", - "0 304.80 2004 2004-04-12 237.871746 2004-03-19 380:CO:SNTL\n", - "1 464.82 2005 2005-04-12 341.916948 2005-04-12 380:CO:SNTL\n", - "2 231.14 2004 2004-04-11 169.764544 2004-03-09 680:CO:SNTL\n", - "3 274.32 2005 2005-04-07 219.461105 2005-03-27 680:CO:SNTL" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# compute the different-day SWE comparison for each of the observed and modeled sites.\n", "df_both_peak = snow_utils.modeled_vs_observed_peak_swe(obs_df, model_df)\n", @@ -5856,101 +847,10 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "id": "a93b9fa9", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Overlay\n", - " .Scatter.I :Scatter [Observed] (Modeled,Station,Water_Year)\n", - " .Curve.A_1_colon_1_Line :Curve [x] (y)" - ] - }, - "execution_count": 27, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1441" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "### NEED TO DECIDE HOW TO FORMAT THIS PLOT AND IF WE WANT TO HAVE THE \"SAME_DAY\" PLOT\n", "\n", @@ -6001,7 +901,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "e46b82f8", "metadata": {}, "outputs": [], @@ -6010,115 +910,8 @@ "df_both_peak['Peak_Date_Diff_Days'] = (df_both_peak['Modeled_Date'] - \n", " df_both_peak['Observed_Date']).dt.days\n", "df_both_peak['Peak_SWE_Diff'] = (df_both_peak['Modeled'] - \n", - " df_both_peak['Observed'])" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "df02795e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ObservedWater_YearObserved_DateModeledModeled_DateStationPeak_Date_Diff_DaysPeak_SWE_Diff
0304.8020042004-04-12237.8717462004-03-19380:CO:SNTL-24-66.928254
1464.8220052005-04-12341.9169482005-04-12380:CO:SNTL0-122.903052
2231.1420042004-04-11169.7645442004-03-09680:CO:SNTL-33-61.375456
3274.3220052005-04-07219.4611052005-03-27680:CO:SNTL-11-54.858895
\n", - "
" - ], - "text/plain": [ - " Observed Water_Year Observed_Date Modeled Modeled_Date Station \\\n", - "0 304.80 2004 2004-04-12 237.871746 2004-03-19 380:CO:SNTL \n", - "1 464.82 2005 2005-04-12 341.916948 2005-04-12 380:CO:SNTL \n", - "2 231.14 2004 2004-04-11 169.764544 2004-03-09 680:CO:SNTL \n", - "3 274.32 2005 2005-04-07 219.461105 2005-03-27 680:CO:SNTL \n", - "\n", - " Peak_Date_Diff_Days Peak_SWE_Diff \n", - "0 -24 -66.928254 \n", - "1 0 -122.903052 \n", - "2 -33 -61.375456 \n", - "3 -11 -54.858895 " - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ + " df_both_peak['Observed'])\n", + "\n", "df_both_peak" ] }, @@ -6132,101 +925,10 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "id": "29c68cf0", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Layout\n", - " .Bars.I :Bars [Station] (Peak_Date_Diff_Days,Modeled,Observed)\n", - " .Bars.II :Bars [Station] (Peak_SWE_Diff,Modeled,Observed)" - ] - }, - "execution_count": 30, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1527" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Filter to separate each water year\n", "year1 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].min()]\n", @@ -6280,7 +982,7 @@ "source": [ "
\n", "

🧠 Reflect

\n", - "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE both earlier and less than the observed peak SWE? Perhaps try changing the my_site_code from earlier in the notebook to rerun nwm_utils.comparison_plots() to see the timeseries for a different station to look at the peak magnitude and timing. \n", + "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE both earlier and less than the observed peak SWE? Perhaps try changing the my_site_code from earlier in the notebook to rerun plot_utils.comparison_plots() to see the timeseries for a different station to look at the peak magnitude and timing. \n", "\n", "
What happens if you change the year that is plotted?
✏️ Try modifying the bar plot code from bar1 = year1.hvplot.bar to bar1 = year2.hvplot.bar. Don't forget to change the title!

\n", "
" @@ -6296,103 +998,10 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "66105b84", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Overlay\n", - " .NdOverlay.I :NdOverlay [Station]\n", - " :Scatter [Peak_Date_Diff_Days] (Peak_SWE_Diff,Water_Year)\n", - " .VLine.I :VLine [x,y]\n", - " .HLine.I :HLine [x,y]" - ] - }, - "execution_count": 31, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1661" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "\n", "\n", @@ -6428,729 +1037,291 @@ }, { "cell_type": "markdown", - "id": "809e568f", + "id": "f6a20df4", "metadata": {}, "source": [ - "## 6. Compute and Statistics and Error Metrics \n", - "The previous section visualized when and where modeled SWE differs from observations, both in terms of peak SWE timing and magnitude. However, visual inspection alone makes it difficult to compare performance across sites or to summarize model behavior in a consistent or quantifiable way. In this section, we compute commonly used statistical error metrics to quantify model performance, allowing us to objectively assess bias, error magnitude, and variability for sites within the watershed. \n", - "\n", - "Proposed outline (DTK, Jan 2026):\n", - "- Summary metrics at a station\n", - "- Summary metrics at all stations within the watershed\n", - "- Combined timing and magnitude for all stations within the watershed (Condon metric)\n", - "- Focus on timing: summary statistics for single station for accumulation & ablation periods (using the new wrapper: `nwm_utils.compute_stats_period()`)\n", - "- Melt period statistics" + "### 5.3 Visualizing Model Error for Melt Period\n", + "The function `compute_melt_period_obs_vs_model` summarizes melt behavior for both the observed and modeled SWE time series at each station and for each water year, allowing the two datasets to be compared side by side. For every station-year combination, it identifies the date of peak SWE, then defines the end of the melt period as the first date after that peak when SWE reaches zero and stays at zero for at least `min_zero_days` consecutive days. Using these two dates, it computes the melt period length in days and the average melt rate over the full melt period, expressed in meters per day. The final output is a table that reports these melt timing and melt rate statistics for both observations and model simulations, making it possible to evaluate whether the model melts snow too early or too late, too quickly or too slowly, and how well it reproduces the overall timing and pace of seasonal snow disappearance across sites and years.\n" ] }, { "cell_type": "code", - "execution_count": 34, - "id": "7bff12bf", + "execution_count": null, + "id": "b2636cad", "metadata": {}, "outputs": [], "source": [ - "for col in obs_df.columns:\n", - " if col not in model_df.columns:\n", - " print(f\"{col} missing in model data\")" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "1786afab", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
site_idsite_namelatitudelongitudermsemsepearson_rspearman_rhonsekger_squaredbias_from_rbiaspercent_biasabs_rel_biastotal_differencecondon
0380:CO:SNTLButte38.89435-106.95327NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
1680:CO:SNTLPark Cone38.81982-106.5896260.2661853632.0130990.8435760.8486420.5949810.5152230.5949810.900516-0.390306-39.030580.390306-22900.802492Low bias, good shape
\n", - "
" - ], - "text/plain": [ - " site_id site_name latitude longitude rmse mse \\\n", - "0 380:CO:SNTL Butte 38.89435 -106.95327 NaN NaN \n", - "1 680:CO:SNTL Park Cone 38.81982 -106.58962 60.266185 3632.013099 \n", - "\n", - " pearson_r spearman_rho nse kge r_squared bias_from_r \\\n", - "0 NaN NaN NaN NaN NaN NaN \n", - "1 0.843576 0.848642 0.594981 0.515223 0.594981 0.900516 \n", - "\n", - " bias percent_bias abs_rel_bias total_difference \\\n", - "0 NaN NaN NaN NaN \n", - "1 -0.390306 -39.03058 0.390306 -22900.802492 \n", - "\n", - " condon \n", - "0 \n", - "1 Low bias, good shape " - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "metrics_df = evaluation_utils.calculate_metrics(obs_df, model_df, metadata_df)\n", - "metrics_df.head(5)" + "melt_df_stats = snow_utils.compute_melt_period_obs_vs_model(obs_df, model_df, min_zero_days=10)\n", + "melt_df_stats" ] }, { "cell_type": "markdown", - "id": "e32cb3b5", + "id": "9cc72756", "metadata": {}, "source": [ - "Pearson and Spearman correlations are both close to 1, suggesting a strong relationship between observed and modeled SWE. As shown on the timeseries plot, this strong correlation alone does not indicate a \"good\" model. For example, it does not guarantee accurate timing of key events, such as peak SWE or melt onset. Let's compare these as well. The following code uses `report_max_dates_and_values` function to identify the peak SWE value and the date it occurs for both the observed (CCSS) and modeled (NWM) datasets. " + "From a snow hydrologist’s perspective, this table is very useful because it captures three core aspects of seasonal snow behavior: **peak timing and magnitude**, **melt-out timing**, and **average melt rate**. The timeline or dumbbell view is often the most intuitive for timing diagnostics, while the 1:1 scatter is the best overall summary of performance." ] }, { - "cell_type": "markdown", - "id": "d16a46f4", + "cell_type": "code", + "execution_count": null, + "id": "a2b7829c", "metadata": {}, + "outputs": [], "source": [ - "
\n", - "

🧠 Reflect

\n", - "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why? As you review the results, compare the peak flow amounts and the timing of snowmelt onset. Do you see any significant differences? Which dataset indicates an earlier melt?

\n", - "
" + "plot_utils.plot_scatter_melt_metrics(melt_df_stats)" ] }, { "cell_type": "markdown", - "id": "9373a41d", + "id": "5359941d", "metadata": {}, "source": [ - "# NEED TO FIX" + "While site-level comparisons provide useful detail, it is often more informative to evaluate model behavior at the **process level** to understand systematic tendencies across the watershed. In this step, we assess whether the model tends to melt snow earlier or later than observed by analyzing the difference in melt period length (model minus observed). Positive differences indicate that the model retains snow longer (later melt), while negative differences indicate earlier melt. The following visualization summarizes this behavior across all stations and water years, highlighting both the direction and magnitude of melt timing bias, and providing an overall assessment of whether the model systematically accelerates or delays snowmelt processes." ] }, { "cell_type": "code", - "execution_count": 35, - "id": "4da6834f", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "ename": "KeyError", - "evalue": "'CCSS_380:CO:SNTL_swe_m'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/cssi_evaluation/lib/python3.10/site-packages/pandas/core/indexes/base.py:3812\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3811\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 3812\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_engine\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcasted_key\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3813\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m err:\n", - "File \u001b[0;32mpandas/_libs/index.pyx:167\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32mpandas/_libs/index.pyx:191\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32mpandas/_libs/index.pyx:234\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine._get_loc_duplicates\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32mpandas/_libs/index.pyx:242\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine._maybe_get_bool_indexer\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32mpandas/_libs/index.pyx:134\u001b[0m, in \u001b[0;36mpandas._libs.index._unpack_bool_indexer\u001b[0;34m()\u001b[0m\n", - "\u001b[0;31mKeyError\u001b[0m: 'CCSS_380:CO:SNTL_swe_m'", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[35], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m summary_table \u001b[38;5;241m=\u001b[39m \u001b[43mevaluation_utils\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreport_max_dates_and_values\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcombined_df\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mCCSS_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mmy_site_code\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_swe_m\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mNWM_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mmy_site_code\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_swe_m\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2\u001b[0m summary_table\n", - "File \u001b[0;32m~/Documents/cuahsi_projects/cssi_model_evaluation/cssi_evaluation/src/cssi_evaluation/utils/evaluation_utils.py:186\u001b[0m, in \u001b[0;36mreport_max_dates_and_values\u001b[0;34m(df, col_obs, col_mod)\u001b[0m\n\u001b[1;32m 183\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDataFrame index must be datetime\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 185\u001b[0m \u001b[38;5;66;03m# Find max values and associated dates\u001b[39;00m\n\u001b[0;32m--> 186\u001b[0m max_obs \u001b[38;5;241m=\u001b[39m \u001b[43mdf\u001b[49m\u001b[43m[\u001b[49m\u001b[43mcol_obs\u001b[49m\u001b[43m]\u001b[49m\u001b[38;5;241m.\u001b[39mmax()\n\u001b[1;32m 187\u001b[0m date_obs \u001b[38;5;241m=\u001b[39m df[col_obs]\u001b[38;5;241m.\u001b[39midxmax()\n\u001b[1;32m 189\u001b[0m max_mod \u001b[38;5;241m=\u001b[39m df[col_mod]\u001b[38;5;241m.\u001b[39mmax()\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/cssi_evaluation/lib/python3.10/site-packages/pandas/core/frame.py:4113\u001b[0m, in \u001b[0;36mDataFrame.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 4111\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcolumns\u001b[38;5;241m.\u001b[39mnlevels \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m 4112\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_getitem_multilevel(key)\n\u001b[0;32m-> 4113\u001b[0m indexer \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 4114\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m is_integer(indexer):\n\u001b[1;32m 4115\u001b[0m indexer \u001b[38;5;241m=\u001b[39m [indexer]\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/cssi_evaluation/lib/python3.10/site-packages/pandas/core/indexes/base.py:3819\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3814\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(casted_key, \u001b[38;5;28mslice\u001b[39m) \u001b[38;5;129;01mor\u001b[39;00m (\n\u001b[1;32m 3815\u001b[0m \u001b[38;5;28misinstance\u001b[39m(casted_key, abc\u001b[38;5;241m.\u001b[39mIterable)\n\u001b[1;32m 3816\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28many\u001b[39m(\u001b[38;5;28misinstance\u001b[39m(x, \u001b[38;5;28mslice\u001b[39m) \u001b[38;5;28;01mfor\u001b[39;00m x \u001b[38;5;129;01min\u001b[39;00m casted_key)\n\u001b[1;32m 3817\u001b[0m ):\n\u001b[1;32m 3818\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m InvalidIndexError(key)\n\u001b[0;32m-> 3819\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01merr\u001b[39;00m\n\u001b[1;32m 3820\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m:\n\u001b[1;32m 3821\u001b[0m \u001b[38;5;66;03m# If we have a listlike key, _check_indexing_error will raise\u001b[39;00m\n\u001b[1;32m 3822\u001b[0m \u001b[38;5;66;03m# InvalidIndexError. Otherwise we fall through and re-raise\u001b[39;00m\n\u001b[1;32m 3823\u001b[0m \u001b[38;5;66;03m# the TypeError.\u001b[39;00m\n\u001b[1;32m 3824\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_check_indexing_error(key)\n", - "\u001b[0;31mKeyError\u001b[0m: 'CCSS_380:CO:SNTL_swe_m'" - ] - } - ], + "execution_count": null, + "id": "1b7b75be", + "metadata": {}, + "outputs": [], "source": [ - "summary_table = evaluation_utils.report_max_dates_and_values(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n", - "summary_table" + "# Process-level assessment\n", + "plot_utils.plot_melt_bias_summary(\n", + " melt_df_stats, # Pandas DataFrame containing site melt statistics\n", + " \"Melt_Period_Days_Obs\", # Name of the column corresponding with the observation data\n", + " \"Melt_Period_Days_Mod\", # Name of the column corresponding with the modeled data\n", + " \"Melt Period Length (Obs vs Model)\" # Title of the plot\n", + ")" ] }, { "cell_type": "markdown", - "id": "b3ab4aec", + "id": "809e568f", "metadata": {}, "source": [ - "### Summary Metrics at Multiple Sites" + "## 6. Compute and Statistics and Error Metrics \n", + "The previous section visualized when and where modeled SWE differs from observations, both in terms of peak SWE timing and magnitude. However, visual inspection alone makes it difficult to compare performance across sites or to summarize model behavior in a consistent or quantifiable way. In this section, we compute commonly used statistical error metrics to quantify model performance, allowing us to objectively assess bias, error magnitude, and variability for sites within the watershed. " ] }, { "cell_type": "code", "execution_count": null, - "id": "c709aa56", + "id": "7bff12bf", "metadata": {}, "outputs": [], "source": [ - "site_codes = ['DAN', 'HRS', 'KIB', 'PDS', 'SLI', 'TUM', 'WHW']\n", - "\n", - "rows = []\n", - "\n", - "for site in site_codes:\n", - " obs_col = f'CCSS_{site}_swe_m'\n", - " mod_col = f'NWM_{site}_swe_m'\n", - "\n", - " stats_table = nwm_utils.compute_stats(combined_df, obs_col, mod_col)\n", - "\n", - " rows.append({\n", - " 'Station': site,\n", - " 'Mean_Obs': stats_table.loc['observed', 'Mean'],\n", - " 'Mean_Mod': stats_table.loc['modeled', 'Mean'],\n", - " 'Bias_m': stats_table.loc['Bias (Modeled - Observed)', 'Mean'],\n", - " 'Pearson_r': stats_table.loc['Pearson Correlation', 'Mean'],\n", - " 'Spearman_r': stats_table.loc['Spearman Correlation', 'Mean'],\n", - " 'NSE': stats_table.loc['Nash-Sutcliffe Efficiency (NSE)', 'Mean'],\n", - " 'KGE': stats_table.loc['Kling-Gupta Efficiency (KGE)', 'Mean']\n", - " })\n", - "\n", - "stats_AllStations = pd.DataFrame(rows)\n", - "\n", - "print('All Stations Statistics Summary:')\n", - "stats_AllStations" + "# Check for mismatched site columns between obs and model data\n", + "for col in obs_df.columns:\n", + " if col not in model_df.columns:\n", + " print(f\"{col} missing in model data\")" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "664f7aa6", + "cell_type": "markdown", + "id": "64161df0", "metadata": {}, - "outputs": [], "source": [ - "stats_AllStations.hvplot.bar(\n", - " x='Station',\n", - " y='NSE',\n", - " rot=45,\n", - " ylabel='Nash–Sutcliffe Efficiency',\n", - " title='NSE by Station',\n", - " height=400,\n", - " width=600,\n", - " bar_width=0.5\n", - ")\n" + "### 6.1 Compute Metrics" + ] + }, + { + "cell_type": "markdown", + "id": "21c8ca67", + "metadata": {}, + "source": [ + "Using the modeled and observed dataframes, we can compute summary metrics. The following is some examples demonstrating how to use individual metrics of interest for a specific site, referenced as `my_site_code`." ] }, { "cell_type": "code", "execution_count": null, - "id": "a023b5a2", + "id": "9d0f5c1d", "metadata": {}, "outputs": [], "source": [ - "stats_summary.hvplot.scatter(\n", - " x='Station',\n", - " y='Bias_m',\n", - " size=100,\n", - " rot=45,\n", - " ylabel='Bias (m)',\n", - " title='Mean SWE Bias by Station'\n", - ")\n" + "bias = metric_utils.bias(obs_df.loc[:, f'{my_site_code}'], model_df.loc[:, f'{my_site_code}'])\n", + "abs_bias = metric_utils.absolute_relative_bias(obs_df.loc[:, f'{my_site_code}'], model_df.loc[:, f'{my_site_code}'])\n", + "srho = metric_utils.spearman_rank(obs_df.loc[:, f'{my_site_code}'], model_df.loc[:, f'{my_site_code}'])\n", + "\n", + "print(f\"For site {my_site_code}: bias = {bias}, abs_bias = {abs_bias}, srho = {srho}\")" ] }, { "cell_type": "markdown", - "id": "64ec9885", + "id": "bf7a7432", "metadata": {}, "source": [ - "### Combine Magnitude (absolute relative bias) and Timing (Spearman's rho) metrics using the Condon metric (and with all stations, a Condon diagram)" + "Apply it to all sites using the `calculate_metrics` capability of the evaluation framework." ] }, { "cell_type": "code", "execution_count": null, - "id": "c415ec43", + "id": "1a6cc22b", "metadata": {}, "outputs": [], "source": [ - "bias1 = evaluation_metrics.bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "bias1" + "metrics_df = evaluation_utils.calculate_metrics(\n", + " obs_df, # Pandas DataFrame containing the time series observations. \n", + " model_df, # Pandas DataFrame containing the time series model calculations.\n", + " metadata_df, # Pandas DataFrame containing location and site attribute information.\n", + " metrics_list=None, # List of metrics to calculate. None indicates that all metrics will be computed.\n", + " write_csv=False, # Indicates the the output should not be saved to CSV (default = False)\n", + " csv_path=None, # Indicates the the output should not be saved to CSV (default = None)\n", + ")\n", + "metrics_df" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "30ef2a01", + "cell_type": "markdown", + "id": "0838c8f5", "metadata": {}, - "outputs": [], "source": [ - "abs_bias = evaluation_metrics.absolute_relative_bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "abs_bias" + "Look at plots of summary statistics for each station. Here we look at Bias and NSE for each station in the watershed:" ] }, { "cell_type": "code", "execution_count": null, - "id": "75ef0577", + "id": "d58febb4", "metadata": {}, "outputs": [], "source": [ - "srho = evaluation_metrics.spearman_rank(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "srho" + "# Bias scatter\n", + "scatter = metrics_df.hvplot.scatter(\n", + " x='site_id',\n", + " y='bias',\n", + " size=100,\n", + " rot=45,\n", + " ylabel='Bias (m)',\n", + " title='Mean SWE Bias by Station'\n", + ")\n", + "\n", + "hline = hv.HLine(0).opts(color='black', line_dash='dashed', line_width=1)\n", + "\n", + "scatter * hline" ] }, { "cell_type": "code", "execution_count": null, - "id": "5bf270b3", + "id": "988fcb43", "metadata": {}, "outputs": [], "source": [ - "evaluation_metrics.condon(abs_bias, srho)" + "# NSE histogram\n", + "metrics_df.hvplot.bar(\n", + " x='site_id',\n", + " y='nse',\n", + " rot=45,\n", + " ylabel='Nash–Sutcliffe Efficiency',\n", + " title='NSE by Station',\n", + " height=400,\n", + " width=600,\n", + " bar_width=0.5\n", + ")\n" ] }, { "cell_type": "markdown", - "id": "501cdc6a", + "id": "d0ee45fb", "metadata": {}, "source": [ "
\n", "

🧠 Reflect

\n", "

\n", - " What is the modeled SWE on the date when the observed SWE reaches its peak?
\n", - " ✏️ Use the code snippet below to find the answer.\n", + " If you recall from earlier, we plotted the timeseries of our selected station. Replot it below. Do the metrics make sense given the visual comparison between modeled and observed? For example, when you look at the timeseries, is the model consistently predicting SWE to be higher or lower than observations? Does this align with the Bias sign (+ or -)?\n", "

\n", - "
\n",
-    "  \n",
-    "    # Find date of the peak SWE from observed data\n",
-    "    date_obs_max = combined_df['CCSS_HRS_swe_m'].idxmax()\n",
-    "\n",
-    "    # Get corresponding value of modeled data on that date\n",
-    "    value_mod_at_max_obs = combined_df.loc[date_obs_max, 'NWM_HRS_swe_m']\n",
-    "  
\n", - "
\n" + "
" ] }, { - "cell_type": "markdown", - "id": "37fd6fb0", + "cell_type": "code", + "execution_count": null, + "id": "9fe75736", "metadata": {}, + "outputs": [], "source": [ - "# STOP NEED TO FIX" + "plot_utils.comparison_plots(obs_df, model_df, f'{my_site_code}', f'{my_site_code}', site_label=None)\n", + "\n", + "# Change the site code to see other Snotel Stations --> e.g., '688:CO:SNTL'\n", + "#plot_utils.comparison_plots(obs_df, model_df, '688:CO:SNTL', '688:CO:SNTL', site_label=None)" ] }, { "cell_type": "markdown", - "id": "111cec2e", + "id": "a2d295d2", "metadata": {}, "source": [ - "### Focus on Timing: Melt Period Metrics\n", - "Compare the average melt rate over the full melt period. " + "
\n", + "

🧠 Reflect

\n", + "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why?

\n", + "
" ] }, { "cell_type": "markdown", - "id": "376bfefb", + "id": "ced951d0", "metadata": {}, "source": [ - "The following function computes the melt period length by identifying the first date after the peak SWE when SWE drops to zero and remains at zero for at least (`min_zero_days`) consecutive days. This is used to define the end of the melt period. Finally, the function calculates the average melt rate, which represents the rate at which snow disappeared, expressed in meters per day, over the full melt period." - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "1fd02e84", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Water_YearStationPeak_SWE_Date_ObsPeak_SWE_m_ObsMelt_End_Date_ObsMelt_Period_Days_ObsMelt_Rate_m_per_day_ObsPeak_SWE_Date_ModPeak_SWE_m_ModMelt_End_Date_ModMelt_Period_Days_ModMelt_Rate_m_per_day_Mod
02004380:CO:SNTL2004-04-12304.802004-05-21397.8153852004-03-19237.8717462004-05-28703.398168
12004680:CO:SNTL2004-04-11231.142004-05-18376.2470272004-03-09169.7645442004-04-21433.948013
22005380:CO:SNTL2005-04-12464.822005-06-05548.6077782005-04-12341.9169482005-06-07566.105660
32005680:CO:SNTL2005-04-07274.322005-05-28515.3788242005-03-27219.4611052005-05-05395.627208
\n", - "
" - ], - "text/plain": [ - " Water_Year Station Peak_SWE_Date_Obs Peak_SWE_m_Obs \\\n", - "0 2004 380:CO:SNTL 2004-04-12 304.80 \n", - "1 2004 680:CO:SNTL 2004-04-11 231.14 \n", - "2 2005 380:CO:SNTL 2005-04-12 464.82 \n", - "3 2005 680:CO:SNTL 2005-04-07 274.32 \n", - "\n", - " Melt_End_Date_Obs Melt_Period_Days_Obs Melt_Rate_m_per_day_Obs \\\n", - "0 2004-05-21 39 7.815385 \n", - "1 2004-05-18 37 6.247027 \n", - "2 2005-06-05 54 8.607778 \n", - "3 2005-05-28 51 5.378824 \n", - "\n", - " Peak_SWE_Date_Mod Peak_SWE_m_Mod Melt_End_Date_Mod Melt_Period_Days_Mod \\\n", - "0 2004-03-19 237.871746 2004-05-28 70 \n", - "1 2004-03-09 169.764544 2004-04-21 43 \n", - "2 2005-04-12 341.916948 2005-06-07 56 \n", - "3 2005-03-27 219.461105 2005-05-05 39 \n", - "\n", - " Melt_Rate_m_per_day_Mod \n", - "0 3.398168 \n", - "1 3.948013 \n", - "2 6.105660 \n", - "3 5.627208 " - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "melt_stats_df = snow_utils.compute_melt_period_statistics(obs_df, model_df)\n", - "melt_stats_df.head()\n", - "melt_stats_df" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "c296fda0", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "ename": "AttributeError", - "evalue": "'Series' object has no attribute 'columns'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/5g/gjchp7mx4zjc21zb3x1xn_2m0000gn/T/ipykernel_58765/905691126.py\u001b[0m in \u001b[0;36m?\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mobserved_melt_period\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msnow_utils\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcompute_melt_period\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobs_df\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mobserved_melt_period\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/cuahsi_projects/cssi_model_evaluation/cssi_evaluation/src/cssi_evaluation/variables/snow_utils.py\u001b[0m in \u001b[0;36m?\u001b[0;34m(swe_df, min_zero_days)\u001b[0m\n\u001b[1;32m 380\u001b[0m \u001b[0;34m\"Melt_End_Date\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstats\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"melt_end_date\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 381\u001b[0m \u001b[0;34m\"Melt_Period_Days\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstats\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"melt_period_days\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 382\u001b[0m \u001b[0;34m\"Melt_Rate_m_per_day\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstats\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"melt_rate_m/d\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 383\u001b[0m })\n\u001b[0;32m--> 384\u001b[0;31m \u001b[0;32mexcept\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 385\u001b[0m \u001b[0;32mcontinue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 386\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 387\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/cuahsi_projects/cssi_model_evaluation/cssi_evaluation/src/cssi_evaluation/variables/snow_utils.py\u001b[0m in \u001b[0;36m?\u001b[0;34m(swe_df, min_zero_days)\u001b[0m\n\u001b[1;32m 366\u001b[0m \u001b[0mcolumns\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mStation\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mPeak_SWE_Date\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mPeak_SWE_m\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mMelt_End_Date\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mMelt_Period_Days\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mMelt_Rate_m_per_day\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 367\u001b[0m \"\"\"\n\u001b[1;32m 368\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 369\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 370\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mstation\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mswe_df\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 371\u001b[0m \u001b[0mswe_series\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_numeric\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mswe_df\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstation\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0merrors\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\"coerce\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdropna\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 372\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mswe_series\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mempty\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mswe_series\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 373\u001b[0m \u001b[0;32mcontinue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/cssi_evaluation/lib/python3.10/site-packages/pandas/core/generic.py\u001b[0m in \u001b[0;36m?\u001b[0;34m(self, name)\u001b[0m\n\u001b[1;32m 6317\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mname\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_accessors\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6318\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_info_axis\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_can_hold_identifiers_and_holds_name\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6319\u001b[0m ):\n\u001b[1;32m 6320\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 6321\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mobject\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__getattribute__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mAttributeError\u001b[0m: 'Series' object has no attribute 'columns'" - ] - } - ], - "source": [ - "observed_melt_period = snow_utils.compute_melt_period(obs_df)\n", - "observed_melt_period" + "The metrics above are computed over the entire snow season, including both accumulation and ablation periods. If desired, you can subset the data and recompute the metrics to examine differences between these phases. For example, if we define the **ablation period** as April through June, the following code computes statistics for data within those months. This approach provides a more detailed understanding of how well the model represents snow accumulation versus melt processes and where performance differences may occur." ] }, { "cell_type": "code", "execution_count": null, - "id": "eecc2844", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "modeled_melt_period = nwm_utils.compute_melt_period(combined_df[f'NWM_{my_site_code}_swe_m'])\n", - "modeled_melt_period" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a35c1ec2", + "id": "ba082688", "metadata": {}, "outputs": [], "source": [ - "accum_months = [10, 11, 12, 1, 2, 3]\n", - "ablation_months = [4, 5, 6]\n", + "ablation_months = [4, 5, 6] # 4:April, 5:May, 6:June. \n", + "\n", + "# Create subsets of the aligned dataframes that exclude the ablation months\n", + "obs_df_ablation = obs_df[obs_df.index.month.isin(ablation_months)]\n", + "model_df_ablation = model_df[model_df.index.month.isin(ablation_months)]\n", "\n", - "accum_stats = nwm_utils.compute_stats_period(\n", - " combined_df,\n", - " f'CCSS_{my_site_code}_swe_m',\n", - " f'NWM_{my_site_code}_swe_m',\n", - " accum_months\n", + "# Compute metrics for the ablation period\n", + "metrics_df_ablation = evaluation_utils.calculate_metrics(\n", + " obs_df_ablation,\n", + " model_df_ablation,\n", + " metadata_df,\n", + " metrics_list=None,\n", + " write_csv=False,\n", + " csv_path=None,\n", ")\n", "\n", - "accum_stats" + "metrics_df_ablation" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "7045037a", + "cell_type": "markdown", + "id": "aff0fac4", "metadata": {}, - "outputs": [], "source": [ - "\n", - "ablation_stats = nwm_utils.compute_stats_period(\n", - " combined_df,\n", - " f'CCSS_{my_site_code}_swe_m',\n", - " f'NWM_{my_site_code}_swe_m',\n", - " ablation_months\n", - ")\n", - "\n", - "ablation_stats" + "### 6.2 Combining Magnitude (Absolute Relative Bias) and Timing (Spearman’s Rho) Using the Condon Metric " ] }, { "cell_type": "markdown", - "id": "ff772b6a", + "id": "53396d9b", "metadata": {}, "source": [ - "
\n", - "

🧠 Reflect

\n", - "

\n", - " If you recall from earlier, we plotted the timeseries of out selected station. Replot it below. Do the metrics make sense given the visual comparison between modeled and observed? For example, when you look at the timeseries, is the model consistently predicting SWE to be higher or lower than observations? Does this align with the Bias sign (+ or -)?\n", - "

\n", - "
" + "One way to learn more about the model performance is to combine metrics that tell us about different aspects of model behavior—such as timing, variability, and magnitude—rather than relying on a single summary measure.\n", + "\n", + "The Condon diagram separates model performance into quadrants based on two metrics: **Spearman’s rho** (shape/time agreement) and **relative bias** (magnitude error). The horizontal line at 0.5 distinguishes whether the model captures the temporal pattern well (above 0.5 = good shape), while the vertical line is traditionally placed at a relative bias of 1.0, which represents a 100% error. This means the model’s total error is as large as the observed signal itself. This threshold has a clear physical interpretation and is used in the original Condon framework to distinguish acceptable vs. large bias. " ] }, { "cell_type": "code", - "execution_count": 38, - "id": "f1576024", + "execution_count": null, + "id": "fb9d46d7", "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Layout\n", - " .Overlay.I :Overlay\n", - " .Curve.Observed_SWE :Curve [date] (observed)\n", - " .Curve.Modeled_SWE :Curve [date] (modeled)\n", - " .Overlay.II :Overlay\n", - " .Scatter.I :Scatter [observed] (modeled,date)\n", - " .Curve.A_1_colon_1_Line :Curve [x] (y)" - ] - }, - "execution_count": 38, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1765" - } - }, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "plot_utils.comparison_plots(obs_df, model_df, f'{my_site_code}', f'{my_site_code}', site_label=None)" + "plot_utils.plot_condon_diagram(metrics_df, variable=\"SWE\")" ] } ], diff --git a/src/cssi_evaluation/utils/evaluation_utils.py b/src/cssi_evaluation/utils/evaluation_utils.py index fabb7de..3e2ce96 100644 --- a/src/cssi_evaluation/utils/evaluation_utils.py +++ b/src/cssi_evaluation/utils/evaluation_utils.py @@ -118,10 +118,23 @@ def calculate_metrics( # Initialize empty metrics DataFrame to store calculated comparison metrics. metrics_df = initialize_metrics_df(obs_metadata_df, metrics_list) - num_sites = obs_data_df.shape[1] - 1 # first column is 'date' + # Ensure datetime index for obs_data_df + if "date" in obs_data_df.columns: + obs_data_df["date"] = pd.to_datetime(obs_data_df["date"]) + obs_data_df = obs_data_df.set_index("date") + else: + obs_data_df.index = pd.to_datetime(obs_data_df.index) + + # --- Ensure datetime index for model_data_df --- + if "date" in model_data_df.columns: + model_data_df["date"] = pd.to_datetime(model_data_df["date"]) + model_data_df = model_data_df.set_index("date") + else: + model_data_df.index = pd.to_datetime(model_data_df.index) + + site_cols = obs_data_df.columns - for i in range(num_sites): - site_id = obs_data_df.columns[(i + 1)] + for site_id in site_cols: obs_data = obs_data_df.loc[:, [site_id]].to_numpy() model_data = model_data_df.loc[:, [site_id]].to_numpy() diff --git a/src/cssi_evaluation/variables/snow_utils.py b/src/cssi_evaluation/variables/snow_utils.py index ad0a1d9..109f694 100644 --- a/src/cssi_evaluation/variables/snow_utils.py +++ b/src/cssi_evaluation/variables/snow_utils.py @@ -82,94 +82,6 @@ def modeled_swe_at_observed_peak(obs_df: pd.DataFrame, model_df: pd.DataFrame) - return pd.concat(results) -######### ORIGINAL CUAHSI FUNCTION BELOW: -# def modeled_swe_at_observed_peak( -# df: pd.DataFrame, obs_swe_cols: list[str], mod_swe_cols: list[str] -# ) -> pd.DataFrame: -# """ -# Extract modeled SWE values on the dates of observed peak (maximum) SWE. - -# This function evaluates model performance by comparing observed peak SWE -# to the modeled SWE on the same calendar date. For each station and water year, -# the date of maximum observed SWE is identified, and the modeled SWE value -# at that date is extracted. - -# Parameters -# ========== -# df: pandas.DataFrame -# A pandas dataframe containing columns associated with modeled and observed SWE. The -# dataframe must have an datetime[64] index. -# obs_swe_cols: list[str] -# Names of the columns associated with observed SWE -# mod_swe_cols: list[str] -# Names of the columns associated with modeled SWE - -# Returns -# ======= -# df: pandas.DataFrame -# A dataframe containing observed max observed SWE and the modeled SWE at the index of the -# maximum observed. The format of the DataFrame will be: - -# Observed Modeled Water_Year Station -# -# -# ... - -# Example: - -# Observed Modeled Water_Year Station -# 2019-04-18 0.98044 1.0293 2019 CCSS_DAN_swe_m -# 2019-04-20 2.12090 1.3598 2019 CCSS_HRS_swe_m -# 2019-03-28 0.80264 0.6708 2019 CCSS_KIB_swe_m -# 2019-04-07 1.78562 0.9965 2019 CCSS_PDS_swe_m -# ... - -# """ - -# # compute water year if it doesn't already exist in the dataframe. -# # this is needed to properly align the same-day comparison -# if "Water_Year" not in df.columns: -# compute_water_year(df, inplace=True) - -# # check to make sure that the input columns are the same length. -# # Raise an exception if they aren't, because our computation will fail. -# if len(obs_swe_cols) != len(mod_swe_cols): -# raise Exception("Modeled and observed inputs must be the same length") - -# # make sure our column data is represented as float64, otherwise -# # the pandas operations below will fail. -# df = df.apply(pd.to_numeric, errors="coerce").astype("float64") # type: ignore[assignment] -# df["Water_Year"] = df["Water_Year"].astype(int) # keep wateryear an integer - -# # Loop over each pairwise grouping of obs and mod columns that -# # have been provided as inputs. Group data for these stations -# # by water year and determine when the maximum value occurs in -# # the observation series. Save this value along with the corresponding -# # mod value at the same time. -# dfs = [] -# for obs, mod in zip(obs_swe_cols, mod_swe_cols): - -# # get the data for the current obs and mod columns -# # but drop all NaN data that may exist. -# dat = df.dropna(subset=[obs, mod, "Water_Year"]).copy() - -# # if all data is NaN for the current obs, mod combination -# # just skip it. -# if dat.empty: -# print(f"Skipping ({obs}, {mod}) because all data is NaN") -# continue - -# idx = dat.groupby("Water_Year")[obs].idxmax() -# dat = dat.loc[idx, [obs, mod, "Water_Year"]].copy() - -# dat.rename(columns={obs: "Observed", mod: "Modeled"}, inplace=True) -# dat["Station"] = obs - -# dfs.append(dat) - -# # concatenate all dataframes together and return -# return pd.concat(dfs) - def modeled_vs_observed_peak_swe(obs_df: pd.DataFrame, model_df: pd.DataFrame) -> pd.DataFrame: """ @@ -245,106 +157,6 @@ def modeled_vs_observed_peak_swe(obs_df: pd.DataFrame, model_df: pd.DataFrame) - return pd.concat(results).sort_values(["Station", "Water_Year"]).reset_index(drop=True) -######### ORIGINAL CUAHSI FUNCTION BELOW: -# def modeled_vs_observed_peak_swe( -# df: pd.DataFrame, obs_swe_cols: list[str], mod_swe_cols: list[str] -# ) -> pd.DataFrame: -# """ -# Extract and compare modeled and observed peak (maximum) SWE values and their timing. - -# This function identifies the dates and magnitudes of peak SWE -# independently for both observed and modeled time series. For each station -# and water year, it extracts the maximum observed SWE and its occurrence date, -# as well as the maximum modeled SWE and its occurrence date. - -# Parameters -# ========== -# df: pandas.DataFrame -# A pandas dataframe containing columns associated with modeled and observed SWE. The -# dataframe must have an datetime[64] index. -# obs_swe_cols: list[str] -# Names of the columns associated with observed SWE -# mod_swe_cols: list[str] -# Names of the columns associated with modeled SWE - -# Returns -# ======= -# df: pandas.DataFrame -# A dataframe containing maximum observed and modeled SWE at their respective times of -# occurence. The format of the DataFrame will be: - -# Observed Observed_Date Modeled Modeled_Date Water_Year Station -# 0 -# 1 -# ... - -# Example: - -# Observed Observed_Date Modeled Modeled_Date Water_Year Station -# 0 0.98044 2019-04-18 1.0393 2019-04-10 2019 CCSS_DAN_swe_m -# 1 0.41910 2020-04-21 0.5206 2020-04-18 2020 CCSS_DAN_swe_m -# 2 2.12090 2019-04-20 1.5498 2019-04-03 2019 CCSS_HRS_swe_m -# 3 0.89662 2020-04-10 0.5745 2020-04-10 2020 CCSS_HRS_swe_m -# ... - - -# """ - -# # compute water year if it doesn't already exist in the dataframe. -# # this is needed to properly align the same-day comparison -# if "Water_Year" not in df.columns: -# compute_water_year(df, inplace=True) - -# # check to make sure that the input columns are the same length. -# # Raise an exception if they aren't, because our computation will fail. -# if len(obs_swe_cols) != len(mod_swe_cols): -# raise Exception("Modeled and observed inputs must be the same length") - -# # make sure our column data is represented as float64, otherwise -# # the pandas operations below will fail. -# df = df.apply(pd.to_numeric, errors="coerce").astype("float64") # type: ignore[assignment] -# df["Water_Year"] = df["Water_Year"].astype(int) # keep wateryear an integer - -# # Loop over each pairwise grouping of obs and mod columns that -# # have been provided as inputs. Group data for these stations -# # by water year and determine when the maximum value occurs in -# # both the observation and modeled series. Save these values -# # along with their corresponding times -# dfs = [] -# for obs, mod in zip(obs_swe_cols, mod_swe_cols): - -# # get the data for the current obs and mod columns -# # but drop all NaN data that may exist. -# dat = df.dropna(subset=[obs, mod, "Water_Year"]).copy() - -# # if all data is NaN for the current obs, mod combination -# # just skip it. -# if dat.empty: -# print(f"Skipping ({obs}, {mod}) because all data is NaN") -# continue - -# obs_idx = dat.groupby("Water_Year")[obs].idxmax() -# obs_dat = dat.loc[obs_idx, [obs, "Water_Year"]].copy() -# obs_dat = obs_dat.rename(columns={obs: "Observed"}) -# obs_dat["Observed_Date"] = obs_idx.values - -# mod_idx = dat.groupby("Water_Year")[mod].idxmax() -# mod_dat = dat.loc[mod_idx, [mod, "Water_Year"]].copy() -# mod_dat = mod_dat.rename(columns={mod: "Modeled"}) -# mod_dat["Modeled_Date"] = mod_idx.values - -# dfs.append( -# # combine the observation and modeled sub-dataframes into one -# # by joining them on Water_Year. Then add -# obs_dat.merge(mod_dat, on="Water_Year", how="outer").assign( -# # create a new "Station" column containing the value of the obs -# Station=obs -# ) -# ) - -# # concatenate all dataframes together and return -# return pd.concat(dfs).reset_index().drop("index", axis=1) - def compute_melt_period_single( swe_series: pd.Series, min_zero_days: int = 10 @@ -427,66 +239,6 @@ def compute_melt_period_all_sites( return pd.DataFrame(result) -# def compute_melt_period( -# swe_series: pd.Series, min_zero_days: int = 10 -# ) -> dict[str, Any]: -# """ -# computes the snow melt period for the input Series. - -# Parameters -# ========== -# swe_series: pandas.Series -# A pandas series containing SWE values indexed by datetime. -# min_zero_days: int -> 10 -# The minimum number of consecutive days with zero SWE to consider -# when determining the melt end date. - -# Returns -# ======= -# dict[str, Any] -# A dictionary containing melt period information with the following keys: -# peak_date, peak_swe_m, melt_end_date, melt_period_days, melt_rate_m/d - -# """ - -# peak_date = swe_series.idxmax() -# peak_swe = swe_series.max() - -# after_peak = swe_series.loc[peak_date:] - -# zero_streak = 0 -# melt_end_date = None - -# for date, value in after_peak.items(): -# if value == 0: -# zero_streak += 1 -# else: -# zero_streak = 0 - -# if zero_streak >= min_zero_days: -# melt_end_date = date -# break - -# if melt_end_date is None: -# raise ValueError( -# f"Could not find a period of at least {min_zero_days} consecutive zero SWE days after the peak." -# ) - -# melt_period_days = (melt_end_date - peak_date).days - -# # Compute melt rate, but handle the case where melt_period_days is zero to avoid division by zero -# if melt_period_days == 0: -# melt_rate = np.nan -# else: -# melt_rate = peak_swe / melt_period_days - -# return { -# "peak_date": peak_date, -# "peak_swe_m": peak_swe, -# "melt_end_date": melt_end_date, -# "melt_period_days": melt_period_days, -# "melt_rate_m/d": melt_rate, -# } def compute_melt_period_obs_vs_model( obs_df: pd.DataFrame, @@ -576,62 +328,6 @@ def compute_melt_period_obs_vs_model( return pd.DataFrame(result) -# def compute_melt_period_statistics( -# df: pd.DataFrame, min_zero_days: int = 10 -# ) -> pd.DataFrame: -# """ -# Computes melt period statistics for each station and water year in the input DataFrame. - -# Parameters -# ========== - -# Returns -# ======= -# pandas.DataFrame -# A pandas DataFrame containing melt period statistics with the following columns: -# Water_Year, Station, Peak_SWE_Date, Peak_SWE_m, Melt_End_Date, Melt_Period_Days, -# Melt_Rate_m_per_day - -# """ - -# # TODO: move ccss columns as an input parameter -# result = [] - -# # Identify CCSS SWE columns -# ccss_columns = [ -# col for col in df.columns if col.startswith("CCSS_") and col.endswith("_swe_m") -# ] - -# for wy, group in df.groupby("Water_Year"): -# for station_col in ccss_columns: - -# # TODO: refactore dropna handling similar to other functions -# # Clean series -# swe_series = pd.to_numeric(group[station_col], errors="coerce").dropna() - -# # Skip if insufficient data -# if swe_series.empty or swe_series.max() == 0: -# continue - -# try: -# # Compute melt period stats -# stats = compute_melt_period(swe_series, min_zero_days=min_zero_days) -# result.append( -# { -# "Water_Year": wy, -# "Station": station_col, -# "Peak_SWE_Date": stats["peak_date"], -# "Peak_SWE_m": stats["peak_swe_m"], -# "Melt_End_Date": stats["melt_end_date"], -# "Melt_Period_Days": stats["melt_period_days"], -# "Melt_Rate_m_per_day": stats["melt_rate_m/d"], -# } -# ) -# except ValueError: -# # If melt period cannot be determined, skip -# continue - -# return pd.DataFrame(result) def compute_snow_timing_grid(grid_swe_da, water_year, threshold=1.0, smooth_window=5): """ From ed46b492357a627665fd5ad4f6c69797646220d1 Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Mon, 27 Apr 2026 15:35:33 -0600 Subject: [PATCH 7/8] removed old PF SWE point notebook --- ...rflow_swe_point_scale_evaluation_OLD.ipynb | 2160 ----------------- 1 file changed, 2160 deletions(-) delete mode 100644 examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb diff --git a/examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb deleted file mode 100644 index 664b2e0..0000000 --- a/examples/parflow/parflow_swe_point_scale_evaluation_OLD.ipynb +++ /dev/null @@ -1,2160 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![NWM](../img/NWM.png)\n", - "\n", - "# Use HydroData to Retrieve Modeled and Observed Snow Data for a Watershed of Interest with ParFlow-CONUS Outputs vs Observed Snow Water Equivalent (SWE) - Full Evaluation Workflow\n", - "Authors: Irene Garousi-Nejad (igarousi@cuahsi.org), Danielle Tijerina-Kreuzer (dtijerina@cuahsi.org) \n", - "Last updated: Feb 2026" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Introduction: \n", - "This notebook demonstrates how to perform a point-scale analysis comparing modeled and observed SWE at selected SNOTEL sites. We focus on analyzing model performance both for **a single SNOTEL site** and **watershed-scale behavior for multiple stations**, with particular attention to the **magnitude and timing of peak SWE**. \n", - "\n", - "# FIX THIS: This notebook requires ParFlow-CONUS output, SNOTEL data, and metadata CSVs that are created in the `01_HydroData_collection.ipynb` notebook." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Prepare the Python Environment" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Import the libraries needed to run this notebook:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "tags": [] - }, - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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.1/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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "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": "p1010" - } - }, - "output_type": "display_data" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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 is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = true;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\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.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.1/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.1/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.1/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 = [];\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 inline_js[i].call(root, root.Bokeh);\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 Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\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": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\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 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 let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\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", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" - ] - } - ], - "source": [ - "import os\n", - "import sys\n", - "from pathlib import Path\n", - "import holoviews as hv\n", - "import hvplot.pandas\n", - "import hvplot.xarray\n", - "import pyproj\n", - "import pandas as pd\n", - "import numpy as np\n", - "import xarray as xr\n", - "import geopandas as gpd\n", - "from dask.distributed import Client\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib.dates as mdates\n", - "import hf_hydrodata as hf\n", - "import subsettools\n", - "\n", - "\n", - "# Import the Evaluation library from the project root.\n", - "sys.path.append(str((Path.cwd().absolute() / \"../../src\").resolve()))\n", - "\n", - "from cssi_evaluation.variables import snow_utils\n", - "from cssi_evaluation.utils import metric_utils\n", - "from cssi_evaluation.utils import evaluation_utils\n", - "\n", - "hv.extension('bokeh')\n", - "\n", - "\n", - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "60a74589", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "id": "19adce70", - "metadata": {}, - "source": [ - "# Get data from Hydrodata - from `dataCollectionHydrodata_parflow.ipynb` notebook and needs to be merged into this notebook" - ] - }, - { - "cell_type": "markdown", - "id": "e86aae63", - "metadata": {}, - "source": [ - "## 1. Setup" - ] - }, - { - "cell_type": "markdown", - "id": "e88ed1ef", - "metadata": {}, - "source": [ - "### 1a. Python Environment \n", - "\n", - "Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file." - ] - }, - { - "cell_type": "markdown", - "id": "c0f30927", - "metadata": {}, - "source": [ - "Import the libraries needed to run this notebook:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "0dfc3fb3", - "metadata": {}, - "outputs": [], - "source": [ - "# import os\n", - "# import sys\n", - "\n", - "# prefix = os.environ['CONDA_PREFIX']\n", - "# os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n", - "\n", - "# # add the src directory to the path so we can import evaluation modules\n", - "# sys.path.append('../../src/')\n", - "\n", - "# import sys\n", - "# import pyproj\n", - "# import pandas as pd\n", - "# import numpy as np\n", - "# import xarray as xr\n", - "# import geopandas as gpd\n", - "# from dask.distributed import Client\n", - "# import matplotlib.pyplot as plt\n", - "# import matplotlib.dates as mdates\n", - "# import hf_hydrodata as hf\n", - "# import subsettools\n", - "# import hvplot.xarray\n", - "\n", - "\n", - "# from cssi_evaluation.utils import plot_utils\n", - "\n", - "\n", - "# %load_ext autoreload\n", - "# %autoreload 2\n" - ] - }, - { - "cell_type": "markdown", - "id": "8e058228", - "metadata": {}, - "source": [ - "### 1b. Register Pin and Access HydroData\n", - "\n", - "To access the HydroData catalog you will need to sign up for a [HydroFrame account](https://hydrogen.princeton.edu/signup) (do this only once), [create a 4-digit PIN](https://hydrogen.princeton.edu/pin), and register your pin in order to have access to the HydroData datasets (you will do this in the next code cell below). To note, you PIN will expire after 7 days and will need to recreate it after that time. " - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "a365996d", - "metadata": {}, - "outputs": [], - "source": [ - "# You need to register on https://hydrogen.princeton.edu/pin \n", - "# and run the following with your registered information\n", - "# before you can use the hydrodata utilities\n", - "hf.register_api_pin(\"dtt2@princeton.edu\", \"7837\")" - ] - }, - { - "cell_type": "markdown", - "id": "825c288d", - "metadata": {}, - "source": [ - "### 1c. Dask \n", - "\n", - "We'll use dask to parallelize our code. To manage parallel computation and visualize progress of long-running tasks, we initialize a Dask “cluster,” which defines how many workers are used and how much computing power each worker has. \n", - "\n", - "In this setup, we create a Dask client with `Client(n_workers=6, threads_per_worker=1, memory_limit='2GB')`, which launches a cluster with 6 workers. Each worker uses a single thread, typically mapped to one CPU core, allowing for efficient parallel processing across 6 cores. Each worker also has a memory limit of 2 GB, for a total of up to 12 GB across the cluster.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6f2b08d0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dashboard link: http://127.0.0.1:8787/status\n", - "\n" - ] - } - ], - "source": [ - "# use a try accept loop so we only instantiate the client\n", - "# if it doesn't already exist.\n", - "try:\n", - " print('Dashboard link:', client.dashboard_link)\n", - "except: \n", - " # The client should be customized to your workstation resources.\n", - " client = Client(n_workers=6, threads_per_worker=1, memory_limit='2GB') \n", - " print('Dashboard link:', client.dashboard_link)\n", - "print(client)" - ] - }, - { - "cell_type": "markdown", - "id": "b8620cfc", - "metadata": {}, - "source": [ - "## 2. Set Paths" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "e50dc99d", - "metadata": {}, - "outputs": [], - "source": [ - "# Start and end times of a water year (to note, these dates were chosen to align with the PFCONUS1 early 2000s runs)\n", - "StartDate = '2003-10-01'\n", - "EndDate = '2005-09-30'\n", - "\n", - "domain_data_path = 'examples/parflow/domain_data/' # path to the model domain data\n", - "\n", - "# Path to save results (obs and mod stands for observation and modeled, respectively)\n", - "OBS_OutputFolder = './obs_outputs_PF' \n", - "MOD_OutputFolder = './mod_outputs_PF'" - ] - }, - { - "cell_type": "markdown", - "id": "feb58871", - "metadata": {}, - "source": [ - "## 3. Retrieve Observed Snow Data " - ] - }, - { - "cell_type": "markdown", - "id": "45ca2832", - "metadata": {}, - "source": [ - "### 3a. Define the watershed of interest\n", - "\n", - "One of the simplest ways to gather data and model output from HydroData is by specifying a [Hydrologic Unit Code](https://www.usgs.gov/national-hydrography/watershed-boundary-dataset). Before we retrieve any hydrologic information, we need to indicate a HUC8 code and use it to gather snow water equivalent (SWE) observations from SNOTEL sites \n", - "\n", - "✏️ If you have a specific HUC8 in mind, you can change the variable `huc_8_code` below." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "c8355563", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HUC-8 ID: 14020001\n", - "HUC-8 name: East-Taylor\n" - ] - } - ], - "source": [ - "# ✏️ Specify HUC8 ID and Name for watershed of interest\n", - "huc_8_code = '14020001' # East-Taylor HUC-8\n", - "print(f'HUC-8 ID: {huc_8_code}')\n", - "\n", - "huc_8_name = 'East-Taylor'\n", - "print(f'HUC-8 name: {huc_8_name}')" - ] - }, - { - "cell_type": "markdown", - "id": "5de02c3b", - "metadata": {}, - "source": [ - "Use the Subsettools function `define_huc_domain()` to get the actual CONUS1 indices associated with the East-Taylor HUC-O8. It returns a tuple `(imin, jmin, imax, jmax)` of grid indices that define a bounding box containing our region (or point) of interest (Note: (imin, jmin, imax, jmax) are the west, south, east and north boundaries of the box respectively) and a mask for that domain." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "965bd6ea", - "metadata": {}, - "outputs": [ - { - "ename": "FileNotFoundError", - "evalue": "[Errno 2] No such file or directory: 'examples/parflow/domain_data/domainMask_East-Taylor_conus1.npy'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[11], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m ij_bounds, mask \u001b[38;5;241m=\u001b[39m subsettools\u001b[38;5;241m.\u001b[39mdefine_huc_domain([huc_8_code], \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mconus1\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m----> 3\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mdomain_data_path\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43mdomainMask_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_name\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_conus1.npy\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmask\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 5\u001b[0m plt\u001b[38;5;241m.\u001b[39mimshow(mask, origin\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlower\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28mprint\u001b[39m(ij_bounds)\n", - "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36msave\u001b[0;34m(*args, **kwargs)\u001b[0m\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/numpy/lib/npyio.py:518\u001b[0m, in \u001b[0;36msave\u001b[0;34m(file, arr, allow_pickle, fix_imports)\u001b[0m\n\u001b[1;32m 516\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m file\u001b[38;5;241m.\u001b[39mendswith(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m):\n\u001b[1;32m 517\u001b[0m file \u001b[38;5;241m=\u001b[39m file \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m--> 518\u001b[0m file_ctx \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mwb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 520\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m file_ctx \u001b[38;5;28;01mas\u001b[39;00m fid:\n\u001b[1;32m 521\u001b[0m arr \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39masanyarray(arr)\n", - "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'examples/parflow/domain_data/domainMask_East-Taylor_conus1.npy'" - ] - } - ], - "source": [ - "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", - "\n", - "np.save(f'{domain_data_path}domainMask_{huc_8_name}_conus1.npy', mask)\n", - "\n", - "plt.imshow(mask, origin='lower')\n", - "print(ij_bounds)\n", - "print(mask.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "61745fb2", - "metadata": {}, - "source": [ - "Using the domain mask and the i,j PF-CONUS1 indices, we use a hf_hydrodata function to find and save the associated grid cell center lat/lon pair for each grid cell in the domain. " - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "1ff5c1d4", - "metadata": {}, - "outputs": [], - "source": [ - "# Extract bounds\n", - "i_min, j_min, i_max, j_max = ij_bounds\n", - "mask_shape = mask.shape #shape of the subset rectangular domain\n", - "\n", - "# Create i/j index ranges\n", - "i_vals = np.arange(i_min, i_max)\n", - "j_vals = np.arange(j_min, j_max)\n", - "\n", - "# Create full 2D grid (note indexing order carefully)\n", - "jj, ii = np.meshgrid(j_vals, i_vals, indexing=\"ij\")" - ] - }, - { - "cell_type": "markdown", - "id": "c6ae90bf", - "metadata": {}, - "source": [ - "Because the function `hf.to_latlon()` finds the coordinates at the lower left corner of a grid cell, we add 0.5 to each i,j index pair to find the **lat/lon at the grid cell center**." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "13fbc81f", - "metadata": {}, - "outputs": [], - "source": [ - "# Compute grid cell centers\n", - "ii_center = ii + 0.5\n", - "jj_center = jj + 0.5\n", - "\n", - "# Convert to lat/lon (vectorized loop)\n", - "lat = np.zeros(mask_shape)\n", - "lon = np.zeros(mask_shape)\n", - "\n", - "for r in range(mask_shape[0]):\n", - " for c in range(mask_shape[1]):\n", - " lat[r, c], lon[r, c] = hf.to_latlon(\"conus1\",\n", - " ii_center[r, c],\n", - " jj_center[r, c])" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "aa20e59f", - "metadata": {}, - "outputs": [ - { - "ename": "FileNotFoundError", - "evalue": "[Errno 2] No such file or directory: 'examples/parflow/domain_data/East-Taylor_14020001_lat_2d.npy'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[14], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# Save 2D arrays of Lat & Lon\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mdomain_data_path\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_name\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mhuc_8_code\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m_lat_2d.npy\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlat\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m np\u001b[38;5;241m.\u001b[39msave(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdomain_data_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;132;01m{\u001b[39;00mhuc_8_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mhuc_8_code\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_lon_2d.npy\u001b[39m\u001b[38;5;124m\"\u001b[39m, lon)\n\u001b[1;32m 5\u001b[0m \u001b[38;5;66;03m# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \u001b[39;00m\n", - "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36msave\u001b[0;34m(*args, **kwargs)\u001b[0m\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/numpy/lib/npyio.py:518\u001b[0m, in \u001b[0;36msave\u001b[0;34m(file, arr, allow_pickle, fix_imports)\u001b[0m\n\u001b[1;32m 516\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m file\u001b[38;5;241m.\u001b[39mendswith(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m):\n\u001b[1;32m 517\u001b[0m file \u001b[38;5;241m=\u001b[39m file \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m.npy\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m--> 518\u001b[0m file_ctx \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mwb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 520\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m file_ctx \u001b[38;5;28;01mas\u001b[39;00m fid:\n\u001b[1;32m 521\u001b[0m arr \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39masanyarray(arr)\n", - "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'examples/parflow/domain_data/East-Taylor_14020001_lat_2d.npy'" - ] - } - ], - "source": [ - "# Save 2D arrays of Lat & Lon\n", - "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", - "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lon_2d.npy\", lon)\n", - "\n", - "# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \n", - "grid_df = pd.DataFrame({\n", - " \"i\": ii.ravel(),\n", - " \"j\": jj.ravel(),\n", - " \"lat\": lat.ravel(),\n", - " \"lon\": lon.ravel(),\n", - "})\n", - "grid_df.to_csv(f\"{domain_data_path}df_{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.csv\", index=False)\n", - "\n", - "# Save a shapefile of the watershed Lat & Lon\n", - "grid_gdf = gpd.GeoDataFrame(\n", - " grid_df,\n", - " geometry=gpd.points_from_xy(grid_df.lon, grid_df.lat),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "# Save the grid points / GeoDataFrame to a shapefile for later use\n", - "grid_gdf.to_file(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.shp\")" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "41e8d63a", - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'grid_df' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[15], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mgrid_df\u001b[49m\n", - "\u001b[0;31mNameError\u001b[0m: name 'grid_df' is not defined" - ] - } - ], - "source": [ - "grid_df" - ] - }, - { - "cell_type": "markdown", - "id": "84549c32", - "metadata": {}, - "source": [ - "### 3b. Explore the available SWE data in a watershed " - ] - }, - { - "cell_type": "markdown", - "id": "e088705e", - "metadata": {}, - "source": [ - "
\n", - "

📖 Did you know?

\n", - "

The Snow Telemetry (SNOTEL) network, managed by the USDA Natural Resources Conservation Service (NRCS), monitors snowpack conditions across key watersheds in the western United States to support water supply forecasting and climate monitoring. SNOTEL sites are fully automated stations that continuously measure snow water equivalent (SWE), snow depth, precipitation, temperature, and other meteorological variables throughout the year. Unlike manual snow survey programs, SNOTEL provides high-temporal-resolution observations that enable near–real-time assessment of snowpack evolution and interannual variability. These data are widely used for operational forecasting, drought assessment, and long-term climate analysis.

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "de83d6b6", - "metadata": {}, - "source": [ - "Explore what SWE data is available at sites within the HUC ID you specified that operated during WY2004 and WY2005. If you want to check other variables besides SWE, you can change the `variable` argument name. " - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "3aa8210e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
site_idsite_namesite_typeagencystatevariable_nameunitsdatasetvariabletemporal_resolution...latitudelongitudesite_query_urldate_metadata_last_updatedtz_cddoiconus1_iconus1_jconus2_iconus2_j
0380:CO:SNTLButteSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.89435-106.95327https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone942.0650.01372.01601.0
1680:CO:SNTLPark ConeSNOTEL stationNRCSCODaily start-of-day snow water equivalentmmusda_nrcsswedaily...38.81982-106.58962https://wcc.sc.egov.usda.gov/awdbWebService/we...2023-03-07PSTNone972.0638.01402.01589.0
\n", - "

2 rows × 24 columns

\n", - "
" - ], - "text/plain": [ - " site_id site_name site_type agency state \\\n", - "0 380:CO:SNTL Butte SNOTEL station NRCS CO \n", - "1 680:CO:SNTL Park Cone SNOTEL station NRCS CO \n", - "\n", - " variable_name units dataset variable \\\n", - "0 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", - "1 Daily start-of-day snow water equivalent mm usda_nrcs swe \n", - "\n", - " temporal_resolution ... latitude longitude \\\n", - "0 daily ... 38.89435 -106.95327 \n", - "1 daily ... 38.81982 -106.58962 \n", - "\n", - " site_query_url \\\n", - "0 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "1 https://wcc.sc.egov.usda.gov/awdbWebService/we... \n", - "\n", - " date_metadata_last_updated tz_cd doi conus1_i conus1_j conus2_i conus2_j \n", - "0 2023-03-07 PST None 942.0 650.0 1372.0 1601.0 \n", - "1 2023-03-07 PST None 972.0 638.0 1402.0 1589.0 \n", - "\n", - "[2 rows x 24 columns]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "avail_df = hf.get_site_variables(variable = \"swe\",\n", - " huc_id = [huc_8_code], grid = 'conus1',\n", - " date_start = StartDate, date_end = EndDate)\n", - "\n", - "# View first five records\n", - "avail_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "268915dc", - "metadata": {}, - "source": [ - "### 3c. Map the SNOTEL stations inside the HUC-08 watershed that have available data in the selected time range \n", - "To note here, we are using pre-loaded shape files for the East-Taylor HUC8, which are located in the `/domain_data/` directory." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "f5c95f67", - "metadata": {}, - "outputs": [ - { - "ename": "DriverError", - "evalue": "examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mCPLE_OpenFailedError\u001b[0m Traceback (most recent call last)", - "File \u001b[0;32mfiona/ogrext.pyx:136\u001b[0m, in \u001b[0;36mfiona.ogrext.gdal_open_vector\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32mfiona/_err.pyx:291\u001b[0m, in \u001b[0;36mfiona._err.exc_wrap_pointer\u001b[0;34m()\u001b[0m\n", - "\u001b[0;31mCPLE_OpenFailedError\u001b[0m: examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory", - "\nDuring handling of the above exception, another exception occurred:\n", - "\u001b[0;31mDriverError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[17], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m### Select station locations that fall within the HUC8 watershed\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \n\u001b[1;32m 3\u001b[0m \u001b[38;5;66;03m# Path to the watershed shapefile that was just created\u001b[39;00m\n\u001b[1;32m 4\u001b[0m watershed \u001b[38;5;241m=\u001b[39m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdomain_data_path\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124mEast-Taylor_14020001.shp\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m----> 5\u001b[0m watershed_gdf \u001b[38;5;241m=\u001b[39m \u001b[43mgpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_file\u001b[49m\u001b[43m(\u001b[49m\u001b[43mwatershed\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mto_crs(epsg\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m4326\u001b[39m)\n\u001b[1;32m 7\u001b[0m \u001b[38;5;66;03m# Create GeoDataFrame of all available stations\u001b[39;00m\n\u001b[1;32m 8\u001b[0m filtered_all_stations_gdf \u001b[38;5;241m=\u001b[39m gpd\u001b[38;5;241m.\u001b[39mGeoDataFrame(\n\u001b[1;32m 9\u001b[0m avail_df,\n\u001b[1;32m 10\u001b[0m geometry\u001b[38;5;241m=\u001b[39mgpd\u001b[38;5;241m.\u001b[39mpoints_from_xy(\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 14\u001b[0m crs\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEPSG:4326\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 15\u001b[0m )\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/geopandas/io/file.py:259\u001b[0m, in \u001b[0;36m_read_file\u001b[0;34m(filename, bbox, mask, rows, engine, **kwargs)\u001b[0m\n\u001b[1;32m 256\u001b[0m path_or_bytes \u001b[38;5;241m=\u001b[39m filename\n\u001b[1;32m 258\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m engine \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfiona\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m--> 259\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_read_file_fiona\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 260\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath_or_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfrom_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbbox\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbbox\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmask\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmask\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrows\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrows\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 261\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 262\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m engine \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpyogrio\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 263\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _read_file_pyogrio(\n\u001b[1;32m 264\u001b[0m path_or_bytes, bbox\u001b[38;5;241m=\u001b[39mbbox, mask\u001b[38;5;241m=\u001b[39mmask, rows\u001b[38;5;241m=\u001b[39mrows, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 265\u001b[0m )\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/geopandas/io/file.py:303\u001b[0m, in \u001b[0;36m_read_file_fiona\u001b[0;34m(path_or_bytes, from_bytes, bbox, mask, rows, where, **kwargs)\u001b[0m\n\u001b[1;32m 300\u001b[0m reader \u001b[38;5;241m=\u001b[39m fiona\u001b[38;5;241m.\u001b[39mopen\n\u001b[1;32m 302\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m fiona_env():\n\u001b[0;32m--> 303\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[43mreader\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath_or_bytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m features:\n\u001b[1;32m 304\u001b[0m crs \u001b[38;5;241m=\u001b[39m features\u001b[38;5;241m.\u001b[39mcrs_wkt\n\u001b[1;32m 305\u001b[0m \u001b[38;5;66;03m# attempt to get EPSG code\u001b[39;00m\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/env.py:457\u001b[0m, in \u001b[0;36mensure_env_with_credentials..wrapper\u001b[0;34m(*args, **kwds)\u001b[0m\n\u001b[1;32m 454\u001b[0m session \u001b[38;5;241m=\u001b[39m DummySession()\n\u001b[1;32m 456\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m env_ctor(session\u001b[38;5;241m=\u001b[39msession):\n\u001b[0;32m--> 457\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/__init__.py:336\u001b[0m, in \u001b[0;36mopen\u001b[0;34m(fp, mode, driver, schema, crs, encoding, layer, vfs, enabled_drivers, crs_wkt, allow_unsupported_drivers, **kwargs)\u001b[0m\n\u001b[1;32m 333\u001b[0m path \u001b[38;5;241m=\u001b[39m parse_path(fp)\n\u001b[1;32m 335\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m mode \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m--> 336\u001b[0m colxn \u001b[38;5;241m=\u001b[39m \u001b[43mCollection\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 337\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 338\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 339\u001b[0m \u001b[43m \u001b[49m\u001b[43mdriver\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdriver\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 340\u001b[0m \u001b[43m \u001b[49m\u001b[43mencoding\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mencoding\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 341\u001b[0m \u001b[43m \u001b[49m\u001b[43mlayer\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlayer\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 342\u001b[0m \u001b[43m \u001b[49m\u001b[43menabled_drivers\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43menabled_drivers\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 343\u001b[0m \u001b[43m \u001b[49m\u001b[43mallow_unsupported_drivers\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mallow_unsupported_drivers\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 344\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 345\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 346\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m mode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 347\u001b[0m colxn \u001b[38;5;241m=\u001b[39m Collection(\n\u001b[1;32m 348\u001b[0m path,\n\u001b[1;32m 349\u001b[0m mode,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 358\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 359\u001b[0m )\n", - "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/nwm_env/lib/python3.10/site-packages/fiona/collection.py:243\u001b[0m, in \u001b[0;36mCollection.__init__\u001b[0;34m(self, path, mode, driver, schema, crs, encoding, layer, vsi, archive, enabled_drivers, crs_wkt, ignore_fields, ignore_geometry, include_fields, wkt_version, allow_unsupported_drivers, **kwargs)\u001b[0m\n\u001b[1;32m 241\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 242\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m Session()\n\u001b[0;32m--> 243\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msession\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 244\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[1;32m 245\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m WritingSession()\n", - "File \u001b[0;32mfiona/ogrext.pyx:588\u001b[0m, in \u001b[0;36mfiona.ogrext.Session.start\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32mfiona/ogrext.pyx:143\u001b[0m, in \u001b[0;36mfiona.ogrext.gdal_open_vector\u001b[0;34m()\u001b[0m\n", - "\u001b[0;31mDriverError\u001b[0m: examples/parflow/domain_data/East-Taylor_14020001.shp: No such file or directory" - ] - } - ], - "source": [ - "### Select station locations that fall within the HUC8 watershed\n", - "\n", - "# Path to the watershed shapefile that was just created\n", - "watershed = f'{domain_data_path}East-Taylor_14020001.shp'\n", - "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", - "\n", - "# Create GeoDataFrame of all available stations\n", - "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", - " avail_df,\n", - " geometry=gpd.points_from_xy(\n", - " avail_df.longitude,\n", - " avail_df.latitude\n", - " ),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", - "\n", - "# Combine watershed polygons into one geometry\n", - "watershed_union = watershed_gdf.geometry.unary_union\n", - "\n", - "# Filter stations that fall within the watershed\n", - "sites_in_watershed = filtered_all_stations_gdf[\n", - " filtered_all_stations_gdf.geometry.within(watershed_union)\n", - "].copy()\n", - "\n", - "sites_in_watershed.reset_index(drop=True, inplace=True)\n", - "\n", - "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "06a6b39b", - "metadata": {}, - "source": [ - "Plot these sites on a map. Then, hover over the pins to see the site names." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1e3bc39", - "metadata": {}, - "outputs": [], - "source": [ - "## TODO: REPLACE WITH CSSI_EVALUATION.PLOTS FUNCTIONS\n", - "\n", - "# this may take a moment to load, but it should pop up in a new window\n", - "m = plot_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", - "m" - ] - }, - { - "cell_type": "markdown", - "id": "354fc021", - "metadata": {}, - "source": [ - "## 4. Retrieve SNOTEL point observations and metadata from HydroData \n", - "Use the `hf.get_point_data()` function to retrieve daily, start-of-day SWE from SNOTEL sites:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d74eeccb", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a folder to save observations\n", - "isExist = os.path.exists(OBS_OutputFolder)\n", - "if isExist == True:\n", - " exit\n", - "else:\n", - " os.mkdir(OBS_OutputFolder)" - ] - }, - { - "cell_type": "markdown", - "id": "b1805ac2", - "metadata": {}, - "source": [ - "### 4a. Get HydroData Observed SWE\n", - "Gather the SNOTEL data for all stations within the watershed and save CSV:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f0f2beb6", - "metadata": {}, - "outputs": [], - "source": [ - "# Request point observations data\n", - "data_df = hf.get_point_data(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", - " date_start=StartDate, date_end=EndDate,\n", - " huc_id=[huc_8_code], grid='conus1')\n", - " #polygon=watershed_bbox, polygon_crs=watershed_crs)\n", - "\n", - "# save\n", - "data_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL.csv', index=False)\n", - "\n", - "# Ensure date column is datetime\n", - "data_df[\"date\"] = pd.to_datetime(data_df[\"date\"])\n", - "\n", - "# View first five records\n", - "data_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "fb365fbf", - "metadata": {}, - "source": [ - "### 4b. Get Metadata for HydroData Observed SWE\n", - "Also, retrieve the metadata for the same stations we retrieved SWE observations for:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1cb40a7c", - "metadata": {}, - "outputs": [], - "source": [ - "# Request site-level attributes for these sites\n", - "metadata_df = hf.get_point_metadata(dataset=\"snotel\", variable=\"swe\", temporal_resolution=\"daily\", aggregation=\"sod\",\n", - " date_start=StartDate, date_end=EndDate,\n", - " huc_id=['14020001'], grid='conus1')\n", - "\n", - "# save\n", - "metadata_df.to_csv(f'./{OBS_OutputFolder}/df_{huc_8_name}_{huc_8_code}_SNOTEL_metadata.csv', index=False)\n", - "\n", - "# View first five records\n", - "metadata_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "d17b371a", - "metadata": {}, - "source": [ - "The metadata file is an important addition to the observations and it is recommended to always gather and save this for the observations you are using (particularly to support reproducibility within an open-science workflow). The saved file has useful attributes like site names, first and last date of available data, lat/lon, and the query URL. \n", - "\n", - "Additionally, the metadata contains **ParFlow-CONUS1 and ParFlow-CONUS2 `i,j` indices, which indicate the exact model domain grid cell the observation aligns with**. This is a useful HydroData feature that removes the need for users to manually match station latitude/longitude coordinates to the appropriate model grid cell, as this spatial mapping is handled directly within HydroData. We will use these indices below to extract PF-CONUS1 modeled SWE for each SNOTEL station in the section below. " - ] - }, - { - "cell_type": "markdown", - "id": "0e50455e", - "metadata": {}, - "source": [ - "## 5. Retrieve ParFlow-CONUS1 Modeled Snow Data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "545a9d22", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a folder to save results\n", - "isExist = os.path.exists(MOD_OutputFolder)\n", - "if isExist == True:\n", - " exit\n", - "else:\n", - " os.mkdir(MOD_OutputFolder)" - ] - }, - { - "cell_type": "markdown", - "id": "56eb4bb4", - "metadata": {}, - "source": [ - "The following section retrieves ParFlow-CONUS1 data for each SNOTEL site within our HUC-08 watershed. The code identifies the CONUS1 `i,j` indices associated with each SNOTEL site, indicated in the `metadata_df`. It then extracts the CONUS1 modeled SWE output for the site and the period of interest, returning the result as a DataFrame. To fairly compare with SNOTEL, which reports SWE once daily at the start of the local day, model output is aggregated by day, using the argment `\"temporal_resolution\": \"daily\"`. Finally, the processed data is saved as a CSV file for each site. \n", - "\n", - "### 5a. ParFlow CONUS1 Model Dataset Information\n", - "We can print some information about the model output dataset by using the `hf.get_catalog_entry()` to get the CONUS1 model dataset metadata. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10647da1", - "metadata": {}, - "outputs": [], - "source": [ - "conus1_options = {\n", - " \"dataset\": \"conus1_baseline_mod\",\n", - " \"variable\": \"swe\"\n", - "}\n", - "hf.get_catalog_entry(conus1_options)" - ] - }, - { - "cell_type": "markdown", - "id": "c6fd1306", - "metadata": {}, - "source": [ - "Before we gather model outputs at the specific SNOTEL sites, we can visualize SWE across our HUC-08. This is plotted for one day at 1km lateral resolution." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ba48a33a", - "metadata": {}, - "outputs": [], - "source": [ - "# retrieve gridded PF-CONUS1 SWE for the entire HUC8 watershed\n", - "grid_swe_options = {\n", - " \"dataset\": \"conus1_baseline_mod\",\n", - " \"variable\": \"swe\",\n", - " \"temporal_resolution\": \"daily\",\n", - " \"start_time\": '2004-04-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", - " \"end_time\": '2004-04-02',\n", - " \"huc_id\": huc_8_code\n", - " }\n", - " \n", - " # Get gridded data\n", - "grid_swe = hf.get_gridded_data(grid_swe_options)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b82b9574", - "metadata": {}, - "outputs": [], - "source": [ - "grid_swe_map = xr.DataArray(grid_swe[0], dims=(\"y\", \"x\"), name=\"SWE\")\n", - "grid_swe_map.hvplot.image(cmap=\"YlGnBu\", colorbar=True, aspect=\"equal\", title=f\"{huc_8_name} Gridded SWE on 2004-04-01\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "73a13787", - "metadata": {}, - "source": [ - "Now, grab the PF-CONUS1 modeled SWE from the SNOTEL site locations. Here we use the CONUS1 i and j indices from the `metadata_df` and grab the SWE from those grid cells. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17143151", - "metadata": {}, - "outputs": [], - "source": [ - "# Copy data_df to model_df so we have the same timestamps and site_id structure\n", - "model_df = data_df.copy()\n", - "\n", - "# Set all non-date columns to NaN to prepare for filling in model data\n", - "non_date_cols = model_df.columns.difference([\"date\"])\n", - "model_df[non_date_cols] = np.nan\n", - "\n", - "# Rename site_id columns for PF outputs \n", - "model_df.columns = [\n", - " col if col == \"date\" else col.replace(\":SNTL\", \"\") + \":PFCONUS1\"\n", - " for col in model_df.columns\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "523bd35c", - "metadata": {}, - "source": [ - "Use the function `hf.get_gridded_data()` and PF-CONUS1 `i,j` indices to select the SWE output for the correct location and time period: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a814204c", - "metadata": {}, - "outputs": [], - "source": [ - "# Loop over each station in metadata_df\n", - "for idx, row in metadata_df.iterrows():\n", - " site_id = row[\"site_id\"] # original SNTL site_id\n", - " col_name = site_id.replace(\":SNTL\", \"\") + \":PFCONUS1\" # corresponding column in model_df\n", - " conus_i = int(row[\"conus1_i\"])\n", - " conus_j = int(row[\"conus1_j\"])\n", - " \n", - " # Build options dict for this station\n", - " options = {\n", - " \"dataset\": \"conus1_baseline_mod\",\n", - " \"variable\": \"swe\",\n", - " \"temporal_resolution\": \"daily\",\n", - " \"start_time\": '2003-10-01', ### TO NOTE: the gridded function has exclusive end date, so this is hardcoded for now \n", - " \"end_time\": '2005-10-01',\n", - " \"grid_point\": [conus_i, conus_j]\n", - " }\n", - " \n", - " # Get gridded data\n", - " data = hf.get_gridded_data(options)\n", - " \n", - " # Fill column in model_df\n", - " # Convert to numeric in case hf returns lists or other types\n", - " model_df[col_name] = np.squeeze(np.array(data))\n", - "\n", - "# Ensure date column is datetime\n", - "model_df[\"date\"] = pd.to_datetime(model_df[\"date\"])\n", - "\n", - "# Save\n", - "model_df.to_csv(f'./{MOD_OutputFolder}/df_{huc_8_name}_{huc_8_code}_PFCONUS1.csv', index=False)\n", - " \n", - "model_df.head(5)" - ] - }, - { - "cell_type": "markdown", - "id": "7464828b", - "metadata": {}, - "source": [ - "## 6. Quick plot sanity check \n", - "Plot a simple timeseries of modeled and observed SWE to make sure our data retrieval was successful. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fbe43f6a", - "metadata": {}, - "outputs": [], - "source": [ - "fig, ax = plt.subplots(figsize=(10, 4))\n", - "\n", - "ax.plot(data_df[\"date\"], model_df[\"380:CO:PFCONUS1\"], label=\"Modeled\", linewidth=2)\n", - "\n", - "ax.plot(data_df[\"date\"], data_df[\"380:CO:SNTL\"], label=\"Observed\", linewidth=2)\n", - "\n", - "ax.set_xlabel(\"Date\")\n", - "ax.set_ylabel(\"SWE (mm)\")\n", - "\n", - "# Date formatting for x-axis\n", - "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n", - "ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))\n", - "\n", - "ax.legend(loc='upper left')\n", - "ax.grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()" - ] - }, - { - "cell_type": "markdown", - "id": "da3df109", - "metadata": {}, - "source": [ - "# Start of comparison from old notebook" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Spatial Mapping of the SNOTEL sites \n", - "Before evaluating model performance, we plot the GIS data associated with the records in the combined DataFrame. The map below shows the SNOTEL stations included in the evaluation dataset, along with the watershed boundary used for the model simulations. Hover over the pins to see the site names. \n", - "\n", - "We also print a table of the SNOTEL site metadata to help with the single site selection." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Path to the watershed shapefile\n", - "watershed = \"./domain_data/East-Taylor_14020001.shp\"\n", - "watershed_gdf = gpd.read_file(watershed).to_crs(epsg=4326)\n", - "\n", - "# Create GeoDataFrame of all available stations\n", - "filtered_all_stations_gdf = gpd.GeoDataFrame(\n", - " metadata_df,\n", - " geometry=gpd.points_from_xy(\n", - " metadata_df.longitude,\n", - " metadata_df.latitude\n", - " ),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "print(\"Sites CRS:\", filtered_all_stations_gdf.crs)\n", - "\n", - "# Combine watershed polygons into one geometry\n", - "watershed_union = watershed_gdf.geometry.unary_union\n", - "\n", - "# Filter stations that fall within the watershed\n", - "sites_in_watershed = filtered_all_stations_gdf[\n", - " filtered_all_stations_gdf.geometry.within(watershed_union)\n", - "].copy()\n", - "\n", - "sites_in_watershed.reset_index(drop=True, inplace=True)\n", - "\n", - "print(f\"Total sites in watershed: {len(sites_in_watershed)}\")\n", - "\n", - "m = nwm_utils.plot_sites_within_domain(sites_in_watershed, watershed_gdf, zoom_start=9)\n", - "m" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sites_in_watershed" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Compare Modeled and Observed SWE Timeseries at a Single Site\n", - "\n", - "Once we have both observation data and modeling outpus, it's important to evaluate how well the model reproduces observed data. The following plots are simple timeseries comparisons of **modeled vs. observed** SWE. These types of plots provide a straight-forward visual of how well the observations and simulations agree and are a great start for assessing general model performance. \n", - "\n", - "📊 We include two figures:\n", - "\n", - "1. **Time Series Overlay:** Plots the observed and modeled values together over time. This helps identify:\n", - " - Periods of systematic bias\n", - " - Timing differences in peaks and lows\n", - " - General agreement in trends\n", - "\n", - "2. **Scatter Plot with 1:1 Line:** Plots each modeled value against its corresponding observed value. This highlights:\n", - " - Accuracy across the full range of values\n", - " - Over- or under-prediction patterns\n", - " - Outliers or extreme events" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Review the sites within the watershed from the interactive map above and click on the markers to view the site name and code. Recall, we also printed out the site metadata for all sites within the watershed, which contains the 3-letter site codes.\n", - "\n", - "✏️ Once you’ve identified the site of interest, **enter its site code in the next code cell for `my_site_code`**: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# choose a site of interest within the watershed\n", - "my_site_code = '380:CO:'\n", - "\n", - "############################ THIS BELOW DOESNT WORK BECAUSE CODE IS NOT COMPLETE\n", - "# filter to only that site\n", - "sites_in_watershed[sites_in_watershed['site_id']==my_site_code]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nwm_utils.comparison_plots(combined_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To move beyond an overall summary of daily performance, we replot the modeled vs. observed SWE scatter while highlighting specific months with a distinct color. This gives us more information about the **seasonal model performance**. \n", - "\n", - "Let's customize the scatter plot by allowing you to highlight specific months with a distinct color. The selected months will appear in one color, while all other months will appear in a different color. This customization reveals whether there are **seasonal patterns** in the relationship between observed and modeled SWE, allowing us to distinguish model behavior during the key snowpack phases of accumulation and ablation (melt). Identifying these patterns is important for diagnosing the model’s strengths and limitations during different parts of the snow season.\n", - "\n", - "You can change the list of highlighted months (for example, October–December for early accumulation or March–May for spring melt) to explore in the scatter plot how model performance varies across different parts of the snow season. This seasonal perspective motivates the _peak SWE analysis_ that follows." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 For this example, let's highlight the _early snow accumulation period_ of October - January:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "combined_df['month'] = combined_df.index.month\n", - "\n", - "plot = nwm_utils.plot_custom_scatter_SWE(combined_df, f'{my_site_code}SNTL', f'{my_site_code}PFCONUS1',\n", - " highlight_months=[10, 11, 12, 1])\n", - "plot " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

What does this plot tell us about how well the model performs during the early snow accumulation period at this site?
\n", - "HINT: How close are the green points to the 1:1 line?

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. Peak SWE Evaluation at the Watershed Scale \n", - "As we saw in the previous section, how well a model matches observations can differ greatly throughout the year. The following section focuses on **peak SWE** (or maximum SWE) analysis. \n", - "\n", - "**Peak SWE is a key diagnostic for snow-dominated hydrologic systems** because it represents the maximum amount of liquid water stored in the snowpack before the spring melt. Evaluating both the magnitude (quantity) and timing (date) of peak SWE provides insight into whether the model is accurately representing snow accumulation and seasonal energy balance. \n", - "\n", - "Errors in peak SWE can have important hydrologic consequences, as peak accumulation strongly influences:\n", - "- The volume of water available for spring runoff\n", - "- The timing of streamflow peaks\n", - "- Soil moisture recharge and groundwater contributions\n", - "\n", - "
\n", - " \n", - "
\n", - "\n", - "_Example daily SWE at a single site, showing two important periods in snow processes: accumulation (before peak) and ablation (after peak). The vertical line marks peak SWE._" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.1 Comparing Modeled and Observed Peak SWE at All Sites in the Watershed\n", - "In this section, we evaluate observed and modeled peak SWE for all stations within our watershed and for all years selected in the `StartDate` and `EndDate` above. \n", - "\n", - "#### 📋 Modeled SWE on the Date of Observed Peak SWE (magnitude) \n", - "This comparison evaluates the modeled SWE on the **specific day when observed SWE reaches its maximum.** By fixing the timing to the observed peak, this comparison isolates errors in SWE magnitude. \n", - "It answers the question: *How much SWE does the model simulate on the day the observed snowpack reaches its maximum?*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# isolate the columns associated with observations and model predictions.\n", - "# these will be inputs to our same-day comparison function.\n", - "obs_cols = sorted([col for col in combined_df.columns if col.endswith('SNTL')])\n", - "mod_cols = sorted([col for col in combined_df.columns if col.endswith('PFCONUS1')])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute the same-day SWE comparison during the observed peak SWE for each of the observation and modeled sites.\n", - "df_observed_peak = utils.modeled_swe_at_observed_peak(combined_df, obs_cols, mod_cols)\n", - "df_observed_peak" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 Visualize the amount of SWE on **the day of observed peak SWE occurs** for both the model and observations at each station" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Rearrange the dataframe to long format for easier plotting\n", - "df_long = (\n", - " df_observed_peak\n", - " .reset_index() \n", - " .melt(\n", - " id_vars=['Station', 'Water_Year', 'date'],\n", - " value_vars=['Observed', 'Modeled'],\n", - " var_name='Source',\n", - " value_name='SWE'\n", - " )\n", - ")\n", - "# Create scatter plot of observed and modeled SWE on the day of observed peak SWE\n", - "scatter_obs_peak = df_long.hvplot.scatter(\n", - " x='Station',\n", - " y='SWE',\n", - " by='Source', # Observed vs Modeled\n", - " ylabel='SWE on Observed Peak Day (mm)',\n", - " title='Observed and Modeled SWE on the Day of Observed Peak SWE',\n", - " size=70,\n", - " width=700,\n", - " height=450,\n", - " alpha=0.8,\n", - " hover_cols=['Water_Year'],\n", - " rot=45\n", - ")\n", - "\n", - "# Customize the scatter plot appearance\n", - "scatter_by_station = (\n", - " scatter_obs_peak \n", - " .opts(legend_position='top_right')\n", - ")\n", - "\n", - "scatter_by_station" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 📋 Modeled vs Observed Peak SWE Comparison (timing & magnitude) \n", - "This comparison evaluates the modeled and observed peak SWE values and their corresponding dates independently. Unlike the previous comparison that fixed the timing to the observed peak swe, this analysis shows the actual days of modeled and observed peak SWE, which may occur on different dates. As a result, it captures errors in both **peak SWE magnitude** and **peak timing**." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute the different-day SWE comparison for each of the observed and modeled sites.\n", - "df_both_peak = utils.modeled_vs_observed_peak_swe(combined_df, obs_cols, mod_cols)\n", - "df_both_peak" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 Visualize the quantity of peak SWE for both the model and observations at each station" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "### NEED TO DECIDE HOW TO FORMAT THIS PLOT AND IF WE WANT TO HAVE THE \"SAME_DAY\" PLOT\n", - "\n", - "# Create the scatter plot\n", - "scatter_plot_both_peak = df_both_peak.hvplot.scatter(\n", - " x='Observed',\n", - " y='Modeled',\n", - " xlabel='Observed SWE (mm)',\n", - " ylabel='Modeled SWE (mm)',\n", - " title='Modeled vs. Observed Peak SWE',\n", - " size=35,\n", - " width=500,\n", - " height=400,\n", - " color='#E69F00',\n", - " hover_cols=['Station', 'Water_Year']\n", - ")#.relabel('Peak SWE')\n", - "\n", - "# Add 1:1 line (perfect match line)\n", - "swe_max = df_both_peak[['Observed', 'Modeled']].max().max()\n", - "\n", - "one_to_one_line = hv.Curve(([0, swe_max], [0, swe_max])).opts(\n", - " color='gray',\n", - " line_dash='dashed',\n", - " line_width=1,\n", - ").relabel('1:1 Line')\n", - "\n", - "# Combine scatter plot and 1:1 line into an Overlay\n", - "scatter_with_line = (scatter_plot_both_peak * one_to_one_line).opts( #scatter_plot_obs_peak * \n", - " legend_position='bottom_right'\n", - ")\n", - "\n", - "scatter_with_line" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.2 Visualizing Model Error for Peak SWE\n", - "\n", - "The previous scatter plots indicate that the modeled and observed peak SWE magnitude and timing don't always align. Next, we plot the degree to which \n", - "\n", - "The previous scatter plots highlight differences between modeled and observed peak SWE timing and magnitude, but interpreting these variations can be challenging when comparing modeled and observed values directly. To make these differences more explicit, we compute errors in both peak timing and peak SWE magnitude and visualize them directly. This approach clarifies both the direction and magnitude of model bias and facilitates comparison across stations and water years.\n", - "\n", - "First, add columns `Peak_Date_Diff_Days` and `Peak_SWE_Diff` to the DataFrame `df_both_peak` for computed difference in peak SWE date difference and peak SWE quantity between modeled and observed:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Compute the difference in peak SWE days and peak SWE amounts between modeled and observed\n", - "df_both_peak['Peak_Date_Diff_Days'] = (df_both_peak['Modeled_Date'] - \n", - " df_both_peak['Observed_Date']).dt.days\n", - "df_both_peak['Peak_SWE_Diff'] = (df_both_peak['Modeled'] - \n", - " df_both_peak['Observed'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "df02795e", - "metadata": {}, - "outputs": [], - "source": [ - "df_both_peak" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 📊 Visualize the error between the modeled and observed peak SWE " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Filter to separate each water year\n", - "year1 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].min()]\n", - "year2 = df_both_peak[df_both_peak['Water_Year'] == df_both_peak['Water_Year'].max()]\n", - "\n", - "bar1 = year1.hvplot.bar(\n", - " x='Station',\n", - " y='Peak_Date_Diff_Days',\n", - " rot=45,\n", - " ylabel='Date Difference (days)',\n", - " title=f'Peak SWE Date Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", - " width=400,\n", - " height=400,\n", - " color='Peak_Date_Diff_Days',\n", - " hover_cols=['Modeled', 'Observed']\n", - ")\n", - "bar2 = year1.hvplot.bar(\n", - " x='Station',\n", - " y='Peak_SWE_Diff',\n", - " rot=45,\n", - " ylabel='SWE Difference (m)',\n", - " title=f'Peak SWE Difference {year1[\"Water_Year\"].iloc[0]} (model - obs)',\n", - " width=400,\n", - " height=400,\n", - " color='Peak_SWE_Diff',\n", - " hover_cols=['Modeled', 'Observed']\n", - ")\n", - "\n", - "# Combine side by side\n", - "layout = (bar1 + bar2)\n", - "layout.opts(shared_axes=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The left panel shows the timing error (date difference) and the right panel the magnitude error (SWE difference). When we computed the difference in date and SWE quantity above, we took `modeled - observed` so: \n", - "\n", - "| | DATE OF PEAK SWE | PEAK SWE QUANTITY |\n", - "|---|---|---|\n", - "| + Positive Values | modeled AFTER observed | modeled GREATER THAN observed |\n", - "| - Negative Values | modeled BEFORE observed | modeled LESS THAN observed | " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

Looking at the two plots, what could be some reasons for the model having simulated peak SWE both earlier and less than the observed peak SWE? Perhaps try changing the my_site_code from earlier in the notebook to rerun nwm_utils.comparison_plots() to see the timeseries for a different station to look at the peak magnitude and timing. \n", - "\n", - "
What happens if you change the year that is plotted?
✏️ Try modifying the bar plot code from bar1 = year1.hvplot.bar to bar1 = year2.hvplot.bar. Don't forget to change the title!

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 📊 Next, we combine the timing and magnitude errors and plot them together for each station." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "\n", - "scatter = df_both_peak.hvplot.scatter(\n", - " x='Peak_Date_Diff_Days',\n", - " y='Peak_SWE_Diff',\n", - " by='Station', # Water_Year\n", - " xlabel='Peak SWE Timing Error (days)',\n", - " ylabel='Peak SWE Magnitude Error (mm)',\n", - " title='Peak SWE Timing vs Magnitude Error',\n", - " size=80,\n", - " width=600,\n", - " height=400,\n", - " hover_cols=['Water_Year']\n", - ")\n", - "\n", - "# Add reference lines\n", - "vline = hv.VLine(0).opts(color='gray', line_dash='dashed')\n", - "hline = hv.HLine(0).opts(color='gray', line_dash='dashed')\n", - "\n", - "(scatter * vline * hline).opts(legend_position='top_left', show_grid=True)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "✏️ **Try changing how we view this plot.** \n", - "We can modify a line in the section of code from `by='Station'` to `by='Water_Year'` to better visualize the errors in the different Water Years. \n", - "Are there any patterns that jump out? Which year was modeled peak SWE consistently less than observed peak SWE? " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 6. Compute and Statistics and Error Metrics \n", - "The previous section visualized when and where modeled SWE differs from observations, both in terms of peak SWE timing and magnitude. However, visual inspection alone makes it difficult to compare performance across sites or to summarize model behavior in a consistent or quantifiable way. In this section, we compute commonly used statistical error metrics to quantify model performance, allowing us to objectively assess bias, error magnitude, and variability for sites within the watershed. \n", - "\n", - "Proposed outline (DTK, Jan 2026):\n", - "- Summary metrics at a station\n", - "- Summary metrics at all stations within the watershed\n", - "- Combined timing and magnitude for all stations within the watershed (Condon metric)\n", - "- Focus on timing: summary statistics for single station for accumulation & ablation periods (using the new wrapper: `nwm_utils.compute_stats_period()`)\n", - "- Melt period statistics" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "nwm_utils.compute_stats(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Pearson and Spearman correlations are both close to 1, suggesting a strong relationship between observed and modeled SWE. As shown on the timeseries plot, this strong correlation alone does not indicate a \"good\" model. For example, it does not guarantee accurate timing of key events, such as peak SWE or melt onset. Let's compare these as well. The following code uses `report_max_dates_and_values` function to identify the peak SWE value and the date it occurs for both the observed (CCSS) and modeled (NWM) datasets. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

You now have several performance metrics: Bias, Pearson Correlation, Spearman Correlation, NSE, and KGE. If you had to pick just one metric to summarize model performance, which would you choose—and why? As you review the results, compare the peak flow amounts and the timing of snowmelt onset. Do you see any significant differences? Which dataset indicates an earlier melt?

\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "summary_table = nwm_utils.report_max_dates_and_values(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n", - "summary_table" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Summary Metrics at Multiple Sites" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "site_codes = ['DAN', 'HRS', 'KIB', 'PDS', 'SLI', 'TUM', 'WHW']\n", - "\n", - "rows = []\n", - "\n", - "for site in site_codes:\n", - " obs_col = f'CCSS_{site}_swe_m'\n", - " mod_col = f'NWM_{site}_swe_m'\n", - "\n", - " stats_table = nwm_utils.compute_stats(combined_df, obs_col, mod_col)\n", - "\n", - " rows.append({\n", - " 'Station': site,\n", - " 'Mean_Obs': stats_table.loc['observed', 'Mean'],\n", - " 'Mean_Mod': stats_table.loc['modeled', 'Mean'],\n", - " 'Bias_m': stats_table.loc['Bias (Modeled - Observed)', 'Mean'],\n", - " 'Pearson_r': stats_table.loc['Pearson Correlation', 'Mean'],\n", - " 'Spearman_r': stats_table.loc['Spearman Correlation', 'Mean'],\n", - " 'NSE': stats_table.loc['Nash-Sutcliffe Efficiency (NSE)', 'Mean'],\n", - " 'KGE': stats_table.loc['Kling-Gupta Efficiency (KGE)', 'Mean']\n", - " })\n", - "\n", - "stats_AllStations = pd.DataFrame(rows)\n", - "\n", - "print('All Stations Statistics Summary:')\n", - "stats_AllStations" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "stats_AllStations.hvplot.bar(\n", - " x='Station',\n", - " y='NSE',\n", - " rot=45,\n", - " ylabel='Nash–Sutcliffe Efficiency',\n", - " title='NSE by Station',\n", - " height=400,\n", - " width=600,\n", - " bar_width=0.5\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "stats_summary.hvplot.scatter(\n", - " x='Station',\n", - " y='Bias_m',\n", - " size=100,\n", - " rot=45,\n", - " ylabel='Bias (m)',\n", - " title='Mean SWE Bias by Station'\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Combine Magnitude (absolute relative bias) and Timing (Spearman's rho) metrics using the Condon metric (and with all stations, a Condon diagram)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bias1 = evaluation_metrics.bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "bias1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "abs_bias = evaluation_metrics.absolute_relative_bias(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "abs_bias" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "srho = evaluation_metrics.spearman_rank(combined_df.CCSS_TUM_swe_m, combined_df.NWM_TUM_swe_m)\n", - "srho" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "evaluation_metrics.condon(abs_bias, srho)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

\n", - " What is the modeled SWE on the date when the observed SWE reaches its peak?
\n", - " ✏️ Use the code snippet below to find the answer.\n", - "

\n", - "
\n",
-    "  \n",
-    "    # Find date of the peak SWE from observed data\n",
-    "    date_obs_max = combined_df['CCSS_HRS_swe_m'].idxmax()\n",
-    "\n",
-    "    # Get corresponding value of modeled data on that date\n",
-    "    value_mod_at_max_obs = combined_df.loc[date_obs_max, 'NWM_HRS_swe_m']\n",
-    "  
\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Focus on Timing: Melt Period Metrics\n", - "Compare the average melt rate over the full melt period. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following function computes the melt period length by identifying the first date after the peak SWE when SWE drops to zero and remains at zero for at least (`min_zero_days`) consecutive days. This is used to define the end of the melt period. Finally, the function calculates the average melt rate, which represents the rate at which snow disappeared, expressed in meters per day, over the full melt period." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "melt_stats_df = utils.compute_melt_period_statistics(combined_df)\n", - "melt_stats_df.head()\n", - "melt_stats_df" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "observed_melt_period = nwm_utils.compute_melt_period(combined_df[f'CCSS_{my_site_code}_swe_m'])\n", - "observed_melt_period" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "modeled_melt_period = nwm_utils.compute_melt_period(combined_df[f'NWM_{my_site_code}_swe_m'])\n", - "modeled_melt_period" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "accum_months = [10, 11, 12, 1, 2, 3]\n", - "ablation_months = [4, 5, 6]\n", - "\n", - "accum_stats = nwm_utils.compute_stats_period(\n", - " combined_df,\n", - " f'CCSS_{my_site_code}_swe_m',\n", - " f'NWM_{my_site_code}_swe_m',\n", - " accum_months\n", - ")\n", - "\n", - "accum_stats" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "ablation_stats = nwm_utils.compute_stats_period(\n", - " combined_df,\n", - " f'CCSS_{my_site_code}_swe_m',\n", - " f'NWM_{my_site_code}_swe_m',\n", - " ablation_months\n", - ")\n", - "\n", - "ablation_stats" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

🧠 Reflect

\n", - "

\n", - " If you recall from earlier, we plotted the timeseries of out selected station. Replot it below. Do the metrics make sense given the visual comparison between modeled and observed? For example, when you look at the timeseries, is the model consistently predicting SWE to be higher or lower than observations? Does this align with the Bias sign (+ or -)?\n", - "

\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nwm_utils.comparison_plots(combined_df, f'CCSS_{my_site_code}_swe_m', f'NWM_{my_site_code}_swe_m')\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "id": "b39724eb", - "metadata": {}, - "source": [ - "# SUBSET TOOLS - prob not keeping section" - ] - }, - { - "cell_type": "markdown", - "id": "0f9d1750", - "metadata": {}, - "source": [ - "Use the Subsettools function `define_huc_domain()` to get the actual CONUS1 indices associated with the East-Taylor HUC-O8. It returns a tuple `(imin, jmin, imax, jmax)` of grid indices that define a bounding box containing our region (or point) of interest (Note: (imin, jmin, imax, jmax) are the west, south, east and north boundaries of the box respectively) and a mask for that domain." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "85d66c92", - "metadata": {}, - "outputs": [], - "source": [ - "ij_bounds, mask = subsettools.define_huc_domain([huc_8_code], 'conus1')\n", - "\n", - "np.save(f'{domain_data_path}domainMask_{huc_8_name}_conus1.npy', mask)\n", - "\n", - "plt.imshow(mask, origin='lower')\n", - "print(ij_bounds)\n", - "print(mask.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "686a14a3", - "metadata": {}, - "source": [ - "Using the domain mask and the i,j PF-CONUS1 indices, we use a hf_hydrodata function to find and save the associated grid cell center lat/lon pair for each grid cell in the domain. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4a4c1d59", - "metadata": {}, - "outputs": [], - "source": [ - "# Extract bounds\n", - "i_min, j_min, i_max, j_max = ij_bounds\n", - "mask_shape = mask.shape #shape of the subset rectangular domain\n", - "\n", - "# Create i/j index ranges\n", - "i_vals = np.arange(i_min, i_max)\n", - "j_vals = np.arange(j_min, j_max)\n", - "\n", - "# Create full 2D grid (note indexing order carefully)\n", - "jj, ii = np.meshgrid(j_vals, i_vals, indexing=\"ij\")" - ] - }, - { - "cell_type": "markdown", - "id": "a11fac65", - "metadata": {}, - "source": [ - "Because the function `hf.to_latlon()` finds the coordinates at the lower left corner of a grid cell, we add 0.5 to each i,j index pair to find the **lat/lon at the grid cell center**." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d44df5ab", - "metadata": {}, - "outputs": [], - "source": [ - "# Compute grid cell centers\n", - "ii_center = ii + 0.5\n", - "jj_center = jj + 0.5\n", - "\n", - "# Convert to lat/lon (vectorized loop)\n", - "lat = np.zeros(mask_shape)\n", - "lon = np.zeros(mask_shape)\n", - "\n", - "for r in range(mask_shape[0]):\n", - " for c in range(mask_shape[1]):\n", - " lat[r, c], lon[r, c] = hf.to_latlon(\"conus1\",\n", - " ii_center[r, c],\n", - " jj_center[r, c])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c4066d61", - "metadata": {}, - "outputs": [], - "source": [ - "# Save 2D arrays of Lat & Lon\n", - "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lat_2d.npy\", lat)\n", - "np.save(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_lon_2d.npy\", lon)\n", - "\n", - "# Save the Lat & Lon in a df (as CSV) matched with the ParFlow-CONUS i,j indices \n", - "grid_df = pd.DataFrame({\n", - " \"i\": ii.ravel(),\n", - " \"j\": jj.ravel(),\n", - " \"lat\": lat.ravel(),\n", - " \"lon\": lon.ravel(),\n", - "})\n", - "grid_df.to_csv(f\"{domain_data_path}df_{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.csv\", index=False)\n", - "\n", - "# Save a shapefile of the watershed Lat & Lon\n", - "grid_gdf = gpd.GeoDataFrame(\n", - " grid_df,\n", - " geometry=gpd.points_from_xy(grid_df.lon, grid_df.lat),\n", - " crs=\"EPSG:4326\"\n", - ")\n", - "# Save the grid points / GeoDataFrame to a shapefile for later use\n", - "grid_gdf.to_file(f\"{domain_data_path}{huc_8_name}_{huc_8_code}_conus1_gridPoints_LatLon.shp\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "70296162", - "metadata": {}, - "outputs": [], - "source": [ - "grid_df" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "nwm_env", - "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 -} From 9cfe5aa6270267754732d427b02bf63a350ff909 Mon Sep 17 00:00:00 2001 From: danielletijerina Date: Mon, 4 May 2026 07:54:17 -0600 Subject: [PATCH 8/8] removed hydrodata credentials and replaced with generic statement --- examples/parflow/parflow_swe_point_scale_evaluation.ipynb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb index 85bf7d5..c99c12e 100644 --- a/examples/parflow/parflow_swe_point_scale_evaluation.ipynb +++ b/examples/parflow/parflow_swe_point_scale_evaluation.ipynb @@ -112,8 +112,7 @@ "# You need to register on https://hydrogen.princeton.edu/pin \n", "# and run the following with your registered information\n", "# before you can use the hydrodata utilities\n", - "#hf.register_api_pin(\"your_email\", \"your_api_pin\")\n", - "hf.register_api_pin(\"dtt2@princeton.edu\", \"7837\")" + "hf.register_api_pin(\"your_email\", \"your_api_pin\")" ] }, {