{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "912ed572-1658-406b-976c-cd6de2d4e89e",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<script type=\"esms-options\">{\"shimMode\": true}</script><style>*[data-root-id],\n",
       "*[data-root-id] > * {\n",
       "  box-sizing: border-box;\n",
       "  font-family: var(--jp-ui-font-family);\n",
       "  font-size: var(--jp-ui-font-size1);\n",
       "  color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
       "}\n",
       "\n",
       "/* Override VSCode background color */\n",
       ".cell-output-ipywidget-background:has(\n",
       "    > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
       "  ),\n",
       ".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
       "  background-color: transparent !important;\n",
       "}\n",
       "</style>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/javascript": "(function(root) {\n  function now() {\n    return new Date();\n  }\n\n  const force = true;\n  const py_version = '3.6.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n  const reloading = false;\n  const Bokeh = root.Bokeh;\n\n  // Set a timeout for this load but only if we are not already initializing\n  if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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      // Don't load bokeh if it is still initializing\n      console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n      return null;\n    } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n      // There is nothing to load\n      run_callbacks();\n      return null;\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(e) {\n      const src_el = e.srcElement\n      console.error(\"failed to load \" + (src_el.href || src_el.src));\n    }\n\n    const skip = [];\n    if (window.requirejs) {\n      window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n      root._bokeh_is_loading = css_urls.length + 0;\n    } else {\n      root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n    }\n\n    const existing_stylesheets = []\n    const links = document.getElementsByTagName('link')\n    for (let i = 0; i < links.length; i++) {\n      const link = links[i]\n      if (link.href != null) {\n        existing_stylesheets.push(link.href)\n      }\n    }\n    for (let i = 0; i < css_urls.length; i++) {\n      const url = css_urls[i];\n      const escaped = encodeURI(url)\n      if (existing_stylesheets.indexOf(escaped) !== -1) {\n        on_load()\n        continue;\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    }    var existing_scripts = []\n    const scripts = document.getElementsByTagName('script')\n    for (let i = 0; i < scripts.length; i++) {\n      var script = scripts[i]\n      if (script.src != null) {\n        existing_scripts.push(script.src)\n      }\n    }\n    for (let i = 0; i < js_urls.length; i++) {\n      const url = js_urls[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\n      }\n      const 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 (let i = 0; i < js_modules.length; i++) {\n      const url = js_modules[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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      const url = js_exports[name];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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  const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.6.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.6.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.6.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.6.0.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\"];\n  const js_modules = [];\n  const js_exports = {};\n  const css_urls = [];\n  const 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 (let i = 0; i < inline_js.length; i++) {\n        try {\n          inline_js[i].call(root, root.Bokeh);\n        } catch(e) {\n          if (!reloading) {\n            throw e;\n          }\n        }\n      }\n      // Cache old bokeh versions\n      if (Bokeh != undefined && !reloading) {\n        var NewBokeh = root.Bokeh;\n        if (Bokeh.versions === undefined) {\n          Bokeh.versions = new Map();\n        }\n        if (NewBokeh.version !== Bokeh.version) {\n          Bokeh.versions.set(NewBokeh.version, NewBokeh)\n        }\n        root.Bokeh = Bokeh;\n      }\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      // If the timeout and bokeh was not successfully loaded we reset\n      // everything and try loading again\n      root._bokeh_timeout = Date.now() + 5000;\n      root._bokeh_is_initializing = false;\n      root._bokeh_onload_callbacks = undefined;\n      root._bokeh_is_loading = 0\n      console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n      load_or_wait();\n    } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n      setTimeout(load_or_wait, 100);\n    } else {\n      root._bokeh_is_initializing = true\n      root._bokeh_onload_callbacks = []\n      const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n      if (!reloading && !bokeh_loaded) {\n        if (root.Bokeh) {\n          root.Bokeh = undefined;\n        }\n        console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n      }\n      load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n        console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n        run_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            console.log(message)\n            var content = {data: message.data, comm_id};\n            var buffers = []\n            for (var buffer of message.buffers || []) {\n              buffers.push(new DataView(buffer))\n            }\n            var metadata = message.metadata || {};\n            var msg = {content, buffers, metadata}\n            msg_handler(msg);\n            return messages.next().then(processIteratorResult);\n          }\n          return messages.next().then(processIteratorResult);\n        })\n      }\n    }\n\n    JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n      if (comm_id in window.PyViz.comms) {\n        return window.PyViz.comms[comm_id];\n      } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n        var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n        var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n        if (msg_handler) {\n          comm.on_msg(msg_handler);\n        }\n      } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n        var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n        comm.open();\n        if (msg_handler) {\n          comm.onMsg = msg_handler;\n        }\n      } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n        var comm_promise = google.colab.kernel.comms.open(comm_id)\n        comm_promise.then((comm) => {\n          window.PyViz.comms[comm_id] = comm;\n          if (msg_handler) {\n            var messages = comm.messages[Symbol.asyncIterator]();\n            function processIteratorResult(result) {\n              var message = result.value;\n              var content = {data: message.data};\n              var metadata = message.metadata || {comm_id};\n              var msg = {content, metadata}\n              msg_handler(msg);\n              return messages.next().then(processIteratorResult);\n            }\n            return messages.next().then(processIteratorResult);\n          }\n        }) \n        var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n          return comm_promise.then((comm) => {\n            comm.send(data, metadata, buffers, disposeOnDone);\n          });\n        };\n        var comm = {\n          send: sendClosure\n        };\n      }\n      window.PyViz.comms[comm_id] = comm;\n      return comm;\n    }\n    window.PyViz.comm_manager = new JupyterCommManager();\n    \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n  var div = document.createElement(\"div\");\n  var script = document.createElement(\"script\");\n  node.appendChild(div);\n  node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n  var output_area = handle.output_area;\n  var output = handle.output;\n  if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n    return\n  }\n  var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n  var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n  if (id !== undefined) {\n    var nchildren = toinsert.length;\n    var html_node = toinsert[nchildren-1].children[0];\n    html_node.innerHTML = output.data[HTML_MIME_TYPE];\n    var scripts = [];\n    var nodelist = html_node.querySelectorAll(\"script\");\n    for (var i in nodelist) {\n      if (nodelist.hasOwnProperty(i)) {\n        scripts.push(nodelist[i])\n      }\n    }\n\n    scripts.forEach( function (oldScript) {\n      var newScript = document.createElement(\"script\");\n      var attrs = [];\n      var nodemap = oldScript.attributes;\n      for (var j in nodemap) {\n        if (nodemap.hasOwnProperty(j)) {\n          attrs.push(nodemap[j])\n        }\n      }\n      attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n      newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n      oldScript.parentNode.replaceChild(newScript, oldScript);\n    });\n    if (JS_MIME_TYPE in output.data) {\n      toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n    }\n    output_area._hv_plot_id = id;\n    if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n      window.PyViz.plot_index[id] = Bokeh.index[id];\n    } else {\n      window.PyViz.plot_index[id] = null;\n    }\n  } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n    var bk_div = document.createElement(\"div\");\n    bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n    var script_attrs = bk_div.children[0].attributes;\n    for (var i = 0; i < script_attrs.length; i++) {\n      toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n    }\n    // store reference to server id on output_area\n    output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n  }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n  var id = handle.cell.output_area._hv_plot_id;\n  var server_id = handle.cell.output_area._bokeh_server_id;\n  if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n  var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n  if (server_id !== null) {\n    comm.send({event_type: 'server_delete', 'id': server_id});\n    return;\n  } else if (comm !== null) {\n    comm.send({event_type: 'delete', 'id': id});\n  }\n  delete PyViz.plot_index[id];\n  if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n    var doc = window.Bokeh.index[id].model.document\n    doc.clear();\n    const i = window.Bokeh.documents.indexOf(doc);\n    if (i > -1) {\n      window.Bokeh.documents.splice(i, 1);\n    }\n  }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n  delete PyViz.comms[\"hv-extension-comm\"];\n  window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n  handle_clear_output(event, {cell: {output_area: handle.output_area}})\n  handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n  function append_mime(data, metadata, element) {\n    // create a DOM node to render to\n    var toinsert = this.create_output_subarea(\n    metadata,\n    CLASS_NAME,\n    EXEC_MIME_TYPE\n    );\n    this.keyboard_manager.register_events(toinsert);\n    // Render to node\n    var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n    render(props, toinsert[0]);\n    element.append(toinsert);\n    return toinsert\n  }\n\n  events.on('output_added.OutputArea', handle_add_output);\n  events.on('output_updated.OutputArea', handle_update_output);\n  events.on('clear_output.CodeCell', handle_clear_output);\n  events.on('delete.Cell', handle_clear_output);\n  events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n  OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n    safe: true,\n    index: 0\n  });\n}\n\nif (window.Jupyter !== undefined) {\n  try {\n    var events = require('base/js/events');\n    var OutputArea = require('notebook/js/outputarea').OutputArea;\n    if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n      register_renderer(events, OutputArea);\n    }\n  } catch(err) {\n  }\n}\n",
      "application/vnd.holoviews_load.v0+json": ""
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.holoviews_exec.v0+json": "",
      "text/html": [
       "<div id='p1002'>\n",
       "  <div id=\"e7a25486-f2ce-483d-8f42-d33eecb79100\" data-root-id=\"p1002\" style=\"display: contents;\"></div>\n",
       "</div>\n",
       "<script type=\"application/javascript\">(function(root) {\n",
       "  var docs_json = {\"5e5c7c0b-7ee1-40a3-863b-5db5f02bc22a\":{\"version\":\"3.6.0\",\"title\":\"Bokeh Application\",\"roots\":[{\"type\":\"object\",\"name\":\"panel.models.browser.BrowserInfo\",\"id\":\"p1002\"},{\"type\":\"object\",\"name\":\"panel.models.comm_manager.CommManager\",\"id\":\"p1003\",\"attributes\":{\"plot_id\":\"p1002\",\"comm_id\":\"2b6981524e854e08b5a0490bd616fea1\",\"client_comm_id\":\"e0ad859aff6f4a5c8df2fb51f1859487\"}}],\"defs\":[{\"type\":\"model\",\"name\":\"ReactiveHTML1\"},{\"type\":\"model\",\"name\":\"FlexBox1\",\"properties\":[{\"name\":\"align_content\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"align_items\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"flex_direction\",\"kind\":\"Any\",\"default\":\"row\"},{\"name\":\"flex_wrap\",\"kind\":\"Any\",\"default\":\"wrap\"},{\"name\":\"gap\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"justify_content\",\"kind\":\"Any\",\"default\":\"flex-start\"}]},{\"type\":\"model\",\"name\":\"FloatPanel1\",\"properties\":[{\"name\":\"config\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"contained\",\"kind\":\"Any\",\"default\":true},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"right-top\"},{\"name\":\"offsetx\",\"kind\":\"Any\",\"default\":null},{\"name\":\"offsety\",\"kind\":\"Any\",\"default\":null},{\"name\":\"theme\",\"kind\":\"Any\",\"default\":\"primary\"},{\"name\":\"status\",\"kind\":\"Any\",\"default\":\"normalized\"}]},{\"type\":\"model\",\"name\":\"GridStack1\",\"properties\":[{\"name\":\"mode\",\"kind\":\"Any\",\"default\":\"warn\"},{\"name\":\"ncols\",\"kind\":\"Any\",\"default\":null},{\"name\":\"nrows\",\"kind\":\"Any\",\"default\":null},{\"name\":\"allow_resize\",\"kind\":\"Any\",\"default\":true},{\"name\":\"allow_drag\",\"kind\":\"Any\",\"default\":true},{\"name\":\"state\",\"kind\":\"Any\",\"default\":[]}]},{\"type\":\"model\",\"name\":\"drag1\",\"properties\":[{\"name\":\"slider_width\",\"kind\":\"Any\",\"default\":5},{\"name\":\"slider_color\",\"kind\":\"Any\",\"default\":\"black\"},{\"name\":\"value\",\"kind\":\"Any\",\"default\":50}]},{\"type\":\"model\",\"name\":\"click1\",\"properties\":[{\"name\":\"terminal_output\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"debug_name\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"clears\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"FastWrapper1\",\"properties\":[{\"name\":\"object\",\"kind\":\"Any\",\"default\":null},{\"name\":\"style\",\"kind\":\"Any\",\"default\":null}]},{\"type\":\"model\",\"name\":\"NotificationAreaBase1\",\"properties\":[{\"name\":\"js_events\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"bottom-right\"},{\"name\":\"_clear\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"NotificationArea1\",\"properties\":[{\"name\":\"js_events\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"notifications\",\"kind\":\"Any\",\"default\":[]},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"bottom-right\"},{\"name\":\"_clear\",\"kind\":\"Any\",\"default\":0},{\"name\":\"types\",\"kind\":\"Any\",\"default\":[{\"type\":\"map\",\"entries\":[[\"type\",\"warning\"],[\"background\",\"#ffc107\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-exclamation-triangle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]},{\"type\":\"map\",\"entries\":[[\"type\",\"info\"],[\"background\",\"#007bff\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-info-circle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]}]}]},{\"type\":\"model\",\"name\":\"Notification\",\"properties\":[{\"name\":\"background\",\"kind\":\"Any\",\"default\":null},{\"name\":\"duration\",\"kind\":\"Any\",\"default\":3000},{\"name\":\"icon\",\"kind\":\"Any\",\"default\":null},{\"name\":\"message\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"notification_type\",\"kind\":\"Any\",\"default\":null},{\"name\":\"_destroyed\",\"kind\":\"Any\",\"default\":false}]},{\"type\":\"model\",\"name\":\"TemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"BootstrapTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"TemplateEditor1\",\"properties\":[{\"name\":\"layout\",\"kind\":\"Any\",\"default\":[]}]},{\"type\":\"model\",\"name\":\"MaterialTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"ReactiveESM1\"},{\"type\":\"model\",\"name\":\"JSComponent1\"},{\"type\":\"model\",\"name\":\"ReactComponent1\"},{\"type\":\"model\",\"name\":\"AnyWidgetComponent1\"},{\"type\":\"model\",\"name\":\"request_value1\",\"properties\":[{\"name\":\"fill\",\"kind\":\"Any\",\"default\":\"none\"},{\"name\":\"_synced\",\"kind\":\"Any\",\"default\":null},{\"name\":\"_request_sync\",\"kind\":\"Any\",\"default\":0}]}]}};\n",
       "  var render_items = [{\"docid\":\"5e5c7c0b-7ee1-40a3-863b-5db5f02bc22a\",\"roots\":{\"p1002\":\"e7a25486-f2ce-483d-8f42-d33eecb79100\"},\"root_ids\":[\"p1002\"]}];\n",
       "  var docs = Object.values(docs_json)\n",
       "  if (!docs) {\n",
       "    return\n",
       "  }\n",
       "  const py_version = docs[0].version.replace('rc', '-rc.').replace('.dev', '-dev.')\n",
       "  async function embed_document(root) {\n",
       "    var Bokeh = get_bokeh(root)\n",
       "    await Bokeh.embed.embed_items_notebook(docs_json, render_items);\n",
       "    for (const render_item of render_items) {\n",
       "      for (const root_id of render_item.root_ids) {\n",
       "\tconst id_el = document.getElementById(root_id)\n",
       "\tif (id_el.children.length && id_el.children[0].hasAttribute('data-root-id')) {\n",
       "\t  const root_el = id_el.children[0]\n",
       "\t  root_el.id = root_el.id + '-rendered'\n",
       "\t  for (const child of root_el.children) {\n",
       "            // Ensure JupyterLab does not capture keyboard shortcuts\n",
       "            // see: https://jupyterlab.readthedocs.io/en/4.1.x/extension/notebook.html#keyboard-interaction-model\n",
       "\t    child.setAttribute('data-lm-suppress-shortcuts', 'true')\n",
       "\t  }\n",
       "\t}\n",
       "      }\n",
       "    }\n",
       "  }\n",
       "  function get_bokeh(root) {\n",
       "    if (root.Bokeh === undefined) {\n",
       "      return null\n",
       "    } else if (root.Bokeh.version !== py_version) {\n",
       "      if (root.Bokeh.versions === undefined || !root.Bokeh.versions.has(py_version)) {\n",
       "\treturn null\n",
       "      }\n",
       "      return root.Bokeh.versions.get(py_version);\n",
       "    } else if (root.Bokeh.version === py_version) {\n",
       "      return root.Bokeh\n",
       "    }\n",
       "    return null\n",
       "  }\n",
       "  function is_loaded(root) {\n",
       "    var Bokeh = get_bokeh(root)\n",
       "    return (Bokeh != null && Bokeh.Panel !== undefined)\n",
       "  }\n",
       "  if (is_loaded(root)) {\n",
       "    embed_document(root);\n",
       "  } else {\n",
       "    var attempts = 0;\n",
       "    var timer = setInterval(function(root) {\n",
       "      if (is_loaded(root)) {\n",
       "        clearInterval(timer);\n",
       "        embed_document(root);\n",
       "      } else if (document.readyState == \"complete\") {\n",
       "        attempts++;\n",
       "        if (attempts > 200) {\n",
       "          clearInterval(timer);\n",
       "\t  var Bokeh = get_bokeh(root)\n",
       "\t  if (Bokeh == null || Bokeh.Panel == null) {\n",
       "            console.warn(\"Panel: ERROR: Unable to run Panel code because Bokeh or Panel library is missing\");\n",
       "\t  } else {\n",
       "\t    console.warn(\"Panel: WARNING: Attempting to render but not all required libraries could be resolved.\")\n",
       "\t    embed_document(root)\n",
       "\t  }\n",
       "        }\n",
       "      }\n",
       "    }, 25, root)\n",
       "  }\n",
       "})(window);</script>"
      ]
     },
     "metadata": {
      "application/vnd.holoviews_exec.v0+json": {
       "id": "p1002"
      }
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<script type=\"esms-options\">{\"shimMode\": true}</script><style>*[data-root-id],\n",
       "*[data-root-id] > * {\n",
       "  box-sizing: border-box;\n",
       "  font-family: var(--jp-ui-font-family);\n",
       "  font-size: var(--jp-ui-font-size1);\n",
       "  color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
       "}\n",
       "\n",
       "/* Override VSCode background color */\n",
       ".cell-output-ipywidget-background:has(\n",
       "    > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
       "  ),\n",
       ".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
       "  background-color: transparent !important;\n",
       "}\n",
       "</style>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/javascript": "(function(root) {\n  function now() {\n    return new Date();\n  }\n\n  const force = false;\n  const py_version = '3.6.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n  const reloading = true;\n  const Bokeh = root.Bokeh;\n\n  // Set a timeout for this load but only if we are not already initializing\n  if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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      // Don't load bokeh if it is still initializing\n      console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n      return null;\n    } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n      // There is nothing to load\n      run_callbacks();\n      return null;\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(e) {\n      const src_el = e.srcElement\n      console.error(\"failed to load \" + (src_el.href || src_el.src));\n    }\n\n    const skip = [];\n    if (window.requirejs) {\n      window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n      root._bokeh_is_loading = css_urls.length + 0;\n    } else {\n      root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n    }\n\n    const existing_stylesheets = []\n    const links = document.getElementsByTagName('link')\n    for (let i = 0; i < links.length; i++) {\n      const link = links[i]\n      if (link.href != null) {\n        existing_stylesheets.push(link.href)\n      }\n    }\n    for (let i = 0; i < css_urls.length; i++) {\n      const url = css_urls[i];\n      const escaped = encodeURI(url)\n      if (existing_stylesheets.indexOf(escaped) !== -1) {\n        on_load()\n        continue;\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    }    var existing_scripts = []\n    const scripts = document.getElementsByTagName('script')\n    for (let i = 0; i < scripts.length; i++) {\n      var script = scripts[i]\n      if (script.src != null) {\n        existing_scripts.push(script.src)\n      }\n    }\n    for (let i = 0; i < js_urls.length; i++) {\n      const url = js_urls[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\n      }\n      const 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 (let i = 0; i < js_modules.length; i++) {\n      const url = js_modules[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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      const url = js_exports[name];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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  const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n  const js_modules = [];\n  const js_exports = {};\n  const css_urls = [];\n  const 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 (let i = 0; i < inline_js.length; i++) {\n        try {\n          inline_js[i].call(root, root.Bokeh);\n        } catch(e) {\n          if (!reloading) {\n            throw e;\n          }\n        }\n      }\n      // Cache old bokeh versions\n      if (Bokeh != undefined && !reloading) {\n        var NewBokeh = root.Bokeh;\n        if (Bokeh.versions === undefined) {\n          Bokeh.versions = new Map();\n        }\n        if (NewBokeh.version !== Bokeh.version) {\n          Bokeh.versions.set(NewBokeh.version, NewBokeh)\n        }\n        root.Bokeh = Bokeh;\n      }\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      // If the timeout and bokeh was not successfully loaded we reset\n      // everything and try loading again\n      root._bokeh_timeout = Date.now() + 5000;\n      root._bokeh_is_initializing = false;\n      root._bokeh_onload_callbacks = undefined;\n      root._bokeh_is_loading = 0\n      console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n      load_or_wait();\n    } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n      setTimeout(load_or_wait, 100);\n    } else {\n      root._bokeh_is_initializing = true\n      root._bokeh_onload_callbacks = []\n      const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n      if (!reloading && !bokeh_loaded) {\n        if (root.Bokeh) {\n          root.Bokeh = undefined;\n        }\n        console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n      }\n      load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n        console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n        run_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            console.log(message)\n            var content = {data: message.data, comm_id};\n            var buffers = []\n            for (var buffer of message.buffers || []) {\n              buffers.push(new DataView(buffer))\n            }\n            var metadata = message.metadata || {};\n            var msg = {content, buffers, metadata}\n            msg_handler(msg);\n            return messages.next().then(processIteratorResult);\n          }\n          return messages.next().then(processIteratorResult);\n        })\n      }\n    }\n\n    JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n      if (comm_id in window.PyViz.comms) {\n        return window.PyViz.comms[comm_id];\n      } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n        var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n        var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n        if (msg_handler) {\n          comm.on_msg(msg_handler);\n        }\n      } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n        var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n        comm.open();\n        if (msg_handler) {\n          comm.onMsg = msg_handler;\n        }\n      } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n        var comm_promise = google.colab.kernel.comms.open(comm_id)\n        comm_promise.then((comm) => {\n          window.PyViz.comms[comm_id] = comm;\n          if (msg_handler) {\n            var messages = comm.messages[Symbol.asyncIterator]();\n            function processIteratorResult(result) {\n              var message = result.value;\n              var content = {data: message.data};\n              var metadata = message.metadata || {comm_id};\n              var msg = {content, metadata}\n              msg_handler(msg);\n              return messages.next().then(processIteratorResult);\n            }\n            return messages.next().then(processIteratorResult);\n          }\n        }) \n        var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n          return comm_promise.then((comm) => {\n            comm.send(data, metadata, buffers, disposeOnDone);\n          });\n        };\n        var comm = {\n          send: sendClosure\n        };\n      }\n      window.PyViz.comms[comm_id] = comm;\n      return comm;\n    }\n    window.PyViz.comm_manager = new JupyterCommManager();\n    \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n  var div = document.createElement(\"div\");\n  var script = document.createElement(\"script\");\n  node.appendChild(div);\n  node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n  var output_area = handle.output_area;\n  var output = handle.output;\n  if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n    return\n  }\n  var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n  var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n  if (id !== undefined) {\n    var nchildren = toinsert.length;\n    var html_node = toinsert[nchildren-1].children[0];\n    html_node.innerHTML = output.data[HTML_MIME_TYPE];\n    var scripts = [];\n    var nodelist = html_node.querySelectorAll(\"script\");\n    for (var i in nodelist) {\n      if (nodelist.hasOwnProperty(i)) {\n        scripts.push(nodelist[i])\n      }\n    }\n\n    scripts.forEach( function (oldScript) {\n      var newScript = document.createElement(\"script\");\n      var attrs = [];\n      var nodemap = oldScript.attributes;\n      for (var j in nodemap) {\n        if (nodemap.hasOwnProperty(j)) {\n          attrs.push(nodemap[j])\n        }\n      }\n      attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n      newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n      oldScript.parentNode.replaceChild(newScript, oldScript);\n    });\n    if (JS_MIME_TYPE in output.data) {\n      toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n    }\n    output_area._hv_plot_id = id;\n    if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n      window.PyViz.plot_index[id] = Bokeh.index[id];\n    } else {\n      window.PyViz.plot_index[id] = null;\n    }\n  } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n    var bk_div = document.createElement(\"div\");\n    bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n    var script_attrs = bk_div.children[0].attributes;\n    for (var i = 0; i < script_attrs.length; i++) {\n      toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n    }\n    // store reference to server id on output_area\n    output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n  }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n  var id = handle.cell.output_area._hv_plot_id;\n  var server_id = handle.cell.output_area._bokeh_server_id;\n  if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n  var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n  if (server_id !== null) {\n    comm.send({event_type: 'server_delete', 'id': server_id});\n    return;\n  } else if (comm !== null) {\n    comm.send({event_type: 'delete', 'id': id});\n  }\n  delete PyViz.plot_index[id];\n  if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n    var doc = window.Bokeh.index[id].model.document\n    doc.clear();\n    const i = window.Bokeh.documents.indexOf(doc);\n    if (i > -1) {\n      window.Bokeh.documents.splice(i, 1);\n    }\n  }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n  delete PyViz.comms[\"hv-extension-comm\"];\n  window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n  handle_clear_output(event, {cell: {output_area: handle.output_area}})\n  handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n  function append_mime(data, metadata, element) {\n    // create a DOM node to render to\n    var toinsert = this.create_output_subarea(\n    metadata,\n    CLASS_NAME,\n    EXEC_MIME_TYPE\n    );\n    this.keyboard_manager.register_events(toinsert);\n    // Render to node\n    var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n    render(props, toinsert[0]);\n    element.append(toinsert);\n    return toinsert\n  }\n\n  events.on('output_added.OutputArea', handle_add_output);\n  events.on('output_updated.OutputArea', handle_update_output);\n  events.on('clear_output.CodeCell', handle_clear_output);\n  events.on('delete.Cell', handle_clear_output);\n  events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n  OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n    safe: true,\n    index: 0\n  });\n}\n\nif (window.Jupyter !== undefined) {\n  try {\n    var events = require('base/js/events');\n    var OutputArea = require('notebook/js/outputarea').OutputArea;\n    if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n      register_renderer(events, OutputArea);\n    }\n  } catch(err) {\n  }\n}\n",
      "application/vnd.holoviews_load.v0+json": ""
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<script type=\"esms-options\">{\"shimMode\": true}</script><style>*[data-root-id],\n",
       "*[data-root-id] > * {\n",
       "  box-sizing: border-box;\n",
       "  font-family: var(--jp-ui-font-family);\n",
       "  font-size: var(--jp-ui-font-size1);\n",
       "  color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
       "}\n",
       "\n",
       "/* Override VSCode background color */\n",
       ".cell-output-ipywidget-background:has(\n",
       "    > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
       "  ),\n",
       ".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
       "  background-color: transparent !important;\n",
       "}\n",
       "</style>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/javascript": "(function(root) {\n  function now() {\n    return new Date();\n  }\n\n  const force = false;\n  const py_version = '3.6.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n  const reloading = true;\n  const Bokeh = root.Bokeh;\n\n  // Set a timeout for this load but only if we are not already initializing\n  if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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      // Don't load bokeh if it is still initializing\n      console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n      return null;\n    } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n      // There is nothing to load\n      run_callbacks();\n      return null;\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(e) {\n      const src_el = e.srcElement\n      console.error(\"failed to load \" + (src_el.href || src_el.src));\n    }\n\n    const skip = [];\n    if (window.requirejs) {\n      window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n      root._bokeh_is_loading = css_urls.length + 0;\n    } else {\n      root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n    }\n\n    const existing_stylesheets = []\n    const links = document.getElementsByTagName('link')\n    for (let i = 0; i < links.length; i++) {\n      const link = links[i]\n      if (link.href != null) {\n        existing_stylesheets.push(link.href)\n      }\n    }\n    for (let i = 0; i < css_urls.length; i++) {\n      const url = css_urls[i];\n      const escaped = encodeURI(url)\n      if (existing_stylesheets.indexOf(escaped) !== -1) {\n        on_load()\n        continue;\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    }    var existing_scripts = []\n    const scripts = document.getElementsByTagName('script')\n    for (let i = 0; i < scripts.length; i++) {\n      var script = scripts[i]\n      if (script.src != null) {\n        existing_scripts.push(script.src)\n      }\n    }\n    for (let i = 0; i < js_urls.length; i++) {\n      const url = js_urls[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\n      }\n      const 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 (let i = 0; i < js_modules.length; i++) {\n      const url = js_modules[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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      const url = js_exports[name];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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  const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n  const js_modules = [];\n  const js_exports = {};\n  const css_urls = [];\n  const 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 (let i = 0; i < inline_js.length; i++) {\n        try {\n          inline_js[i].call(root, root.Bokeh);\n        } catch(e) {\n          if (!reloading) {\n            throw e;\n          }\n        }\n      }\n      // Cache old bokeh versions\n      if (Bokeh != undefined && !reloading) {\n        var NewBokeh = root.Bokeh;\n        if (Bokeh.versions === undefined) {\n          Bokeh.versions = new Map();\n        }\n        if (NewBokeh.version !== Bokeh.version) {\n          Bokeh.versions.set(NewBokeh.version, NewBokeh)\n        }\n        root.Bokeh = Bokeh;\n      }\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      // If the timeout and bokeh was not successfully loaded we reset\n      // everything and try loading again\n      root._bokeh_timeout = Date.now() + 5000;\n      root._bokeh_is_initializing = false;\n      root._bokeh_onload_callbacks = undefined;\n      root._bokeh_is_loading = 0\n      console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n      load_or_wait();\n    } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n      setTimeout(load_or_wait, 100);\n    } else {\n      root._bokeh_is_initializing = true\n      root._bokeh_onload_callbacks = []\n      const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n      if (!reloading && !bokeh_loaded) {\n        if (root.Bokeh) {\n          root.Bokeh = undefined;\n        }\n        console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n      }\n      load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n        console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n        run_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            console.log(message)\n            var content = {data: message.data, comm_id};\n            var buffers = []\n            for (var buffer of message.buffers || []) {\n              buffers.push(new DataView(buffer))\n            }\n            var metadata = message.metadata || {};\n            var msg = {content, buffers, metadata}\n            msg_handler(msg);\n            return messages.next().then(processIteratorResult);\n          }\n          return messages.next().then(processIteratorResult);\n        })\n      }\n    }\n\n    JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n      if (comm_id in window.PyViz.comms) {\n        return window.PyViz.comms[comm_id];\n      } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n        var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n        var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n        if (msg_handler) {\n          comm.on_msg(msg_handler);\n        }\n      } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n        var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n        comm.open();\n        if (msg_handler) {\n          comm.onMsg = msg_handler;\n        }\n      } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n        var comm_promise = google.colab.kernel.comms.open(comm_id)\n        comm_promise.then((comm) => {\n          window.PyViz.comms[comm_id] = comm;\n          if (msg_handler) {\n            var messages = comm.messages[Symbol.asyncIterator]();\n            function processIteratorResult(result) {\n              var message = result.value;\n              var content = {data: message.data};\n              var metadata = message.metadata || {comm_id};\n              var msg = {content, metadata}\n              msg_handler(msg);\n              return messages.next().then(processIteratorResult);\n            }\n            return messages.next().then(processIteratorResult);\n          }\n        }) \n        var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n          return comm_promise.then((comm) => {\n            comm.send(data, metadata, buffers, disposeOnDone);\n          });\n        };\n        var comm = {\n          send: sendClosure\n        };\n      }\n      window.PyViz.comms[comm_id] = comm;\n      return comm;\n    }\n    window.PyViz.comm_manager = new JupyterCommManager();\n    \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n  var div = document.createElement(\"div\");\n  var script = document.createElement(\"script\");\n  node.appendChild(div);\n  node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n  var output_area = handle.output_area;\n  var output = handle.output;\n  if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n    return\n  }\n  var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n  var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n  if (id !== undefined) {\n    var nchildren = toinsert.length;\n    var html_node = toinsert[nchildren-1].children[0];\n    html_node.innerHTML = output.data[HTML_MIME_TYPE];\n    var scripts = [];\n    var nodelist = html_node.querySelectorAll(\"script\");\n    for (var i in nodelist) {\n      if (nodelist.hasOwnProperty(i)) {\n        scripts.push(nodelist[i])\n      }\n    }\n\n    scripts.forEach( function (oldScript) {\n      var newScript = document.createElement(\"script\");\n      var attrs = [];\n      var nodemap = oldScript.attributes;\n      for (var j in nodemap) {\n        if (nodemap.hasOwnProperty(j)) {\n          attrs.push(nodemap[j])\n        }\n      }\n      attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n      newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n      oldScript.parentNode.replaceChild(newScript, oldScript);\n    });\n    if (JS_MIME_TYPE in output.data) {\n      toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n    }\n    output_area._hv_plot_id = id;\n    if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n      window.PyViz.plot_index[id] = Bokeh.index[id];\n    } else {\n      window.PyViz.plot_index[id] = null;\n    }\n  } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n    var bk_div = document.createElement(\"div\");\n    bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n    var script_attrs = bk_div.children[0].attributes;\n    for (var i = 0; i < script_attrs.length; i++) {\n      toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n    }\n    // store reference to server id on output_area\n    output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n  }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n  var id = handle.cell.output_area._hv_plot_id;\n  var server_id = handle.cell.output_area._bokeh_server_id;\n  if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n  var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n  if (server_id !== null) {\n    comm.send({event_type: 'server_delete', 'id': server_id});\n    return;\n  } else if (comm !== null) {\n    comm.send({event_type: 'delete', 'id': id});\n  }\n  delete PyViz.plot_index[id];\n  if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n    var doc = window.Bokeh.index[id].model.document\n    doc.clear();\n    const i = window.Bokeh.documents.indexOf(doc);\n    if (i > -1) {\n      window.Bokeh.documents.splice(i, 1);\n    }\n  }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n  delete PyViz.comms[\"hv-extension-comm\"];\n  window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n  handle_clear_output(event, {cell: {output_area: handle.output_area}})\n  handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n  function append_mime(data, metadata, element) {\n    // create a DOM node to render to\n    var toinsert = this.create_output_subarea(\n    metadata,\n    CLASS_NAME,\n    EXEC_MIME_TYPE\n    );\n    this.keyboard_manager.register_events(toinsert);\n    // Render to node\n    var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n    render(props, toinsert[0]);\n    element.append(toinsert);\n    return toinsert\n  }\n\n  events.on('output_added.OutputArea', handle_add_output);\n  events.on('output_updated.OutputArea', handle_update_output);\n  events.on('clear_output.CodeCell', handle_clear_output);\n  events.on('delete.Cell', handle_clear_output);\n  events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n  OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n    safe: true,\n    index: 0\n  });\n}\n\nif (window.Jupyter !== undefined) {\n  try {\n    var events = require('base/js/events');\n    var OutputArea = require('notebook/js/outputarea').OutputArea;\n    if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n      register_renderer(events, OutputArea);\n    }\n  } catch(err) {\n  }\n}\n",
      "application/vnd.holoviews_load.v0+json": ""
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<script type=\"esms-options\">{\"shimMode\": true}</script><style>*[data-root-id],\n",
       "*[data-root-id] > * {\n",
       "  box-sizing: border-box;\n",
       "  font-family: var(--jp-ui-font-family);\n",
       "  font-size: var(--jp-ui-font-size1);\n",
       "  color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
       "}\n",
       "\n",
       "/* Override VSCode background color */\n",
       ".cell-output-ipywidget-background:has(\n",
       "    > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
       "  ),\n",
       ".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
       "  background-color: transparent !important;\n",
       "}\n",
       "</style>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/javascript": "(function(root) {\n  function now() {\n    return new Date();\n  }\n\n  const force = false;\n  const py_version = '3.6.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n  const reloading = true;\n  const Bokeh = root.Bokeh;\n\n  // Set a timeout for this load but only if we are not already initializing\n  if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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      // Don't load bokeh if it is still initializing\n      console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n      return null;\n    } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n      // There is nothing to load\n      run_callbacks();\n      return null;\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(e) {\n      const src_el = e.srcElement\n      console.error(\"failed to load \" + (src_el.href || src_el.src));\n    }\n\n    const skip = [];\n    if (window.requirejs) {\n      window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n      root._bokeh_is_loading = css_urls.length + 0;\n    } else {\n      root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n    }\n\n    const existing_stylesheets = []\n    const links = document.getElementsByTagName('link')\n    for (let i = 0; i < links.length; i++) {\n      const link = links[i]\n      if (link.href != null) {\n        existing_stylesheets.push(link.href)\n      }\n    }\n    for (let i = 0; i < css_urls.length; i++) {\n      const url = css_urls[i];\n      const escaped = encodeURI(url)\n      if (existing_stylesheets.indexOf(escaped) !== -1) {\n        on_load()\n        continue;\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    }    var existing_scripts = []\n    const scripts = document.getElementsByTagName('script')\n    for (let i = 0; i < scripts.length; i++) {\n      var script = scripts[i]\n      if (script.src != null) {\n        existing_scripts.push(script.src)\n      }\n    }\n    for (let i = 0; i < js_urls.length; i++) {\n      const url = js_urls[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\n      }\n      const 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 (let i = 0; i < js_modules.length; i++) {\n      const url = js_modules[i];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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      const url = js_exports[name];\n      const escaped = encodeURI(url)\n      if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n        if (!window.requirejs) {\n          on_load();\n        }\n        continue;\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  const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n  const js_modules = [];\n  const js_exports = {};\n  const css_urls = [];\n  const 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 (let i = 0; i < inline_js.length; i++) {\n        try {\n          inline_js[i].call(root, root.Bokeh);\n        } catch(e) {\n          if (!reloading) {\n            throw e;\n          }\n        }\n      }\n      // Cache old bokeh versions\n      if (Bokeh != undefined && !reloading) {\n        var NewBokeh = root.Bokeh;\n        if (Bokeh.versions === undefined) {\n          Bokeh.versions = new Map();\n        }\n        if (NewBokeh.version !== Bokeh.version) {\n          Bokeh.versions.set(NewBokeh.version, NewBokeh)\n        }\n        root.Bokeh = Bokeh;\n      }\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      // If the timeout and bokeh was not successfully loaded we reset\n      // everything and try loading again\n      root._bokeh_timeout = Date.now() + 5000;\n      root._bokeh_is_initializing = false;\n      root._bokeh_onload_callbacks = undefined;\n      root._bokeh_is_loading = 0\n      console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n      load_or_wait();\n    } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n      setTimeout(load_or_wait, 100);\n    } else {\n      root._bokeh_is_initializing = true\n      root._bokeh_onload_callbacks = []\n      const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n      if (!reloading && !bokeh_loaded) {\n        if (root.Bokeh) {\n          root.Bokeh = undefined;\n        }\n        console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n      }\n      load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n        console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n        run_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            console.log(message)\n            var content = {data: message.data, comm_id};\n            var buffers = []\n            for (var buffer of message.buffers || []) {\n              buffers.push(new DataView(buffer))\n            }\n            var metadata = message.metadata || {};\n            var msg = {content, buffers, metadata}\n            msg_handler(msg);\n            return messages.next().then(processIteratorResult);\n          }\n          return messages.next().then(processIteratorResult);\n        })\n      }\n    }\n\n    JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n      if (comm_id in window.PyViz.comms) {\n        return window.PyViz.comms[comm_id];\n      } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n        var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n        var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n        if (msg_handler) {\n          comm.on_msg(msg_handler);\n        }\n      } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n        var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n        comm.open();\n        if (msg_handler) {\n          comm.onMsg = msg_handler;\n        }\n      } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n        var comm_promise = google.colab.kernel.comms.open(comm_id)\n        comm_promise.then((comm) => {\n          window.PyViz.comms[comm_id] = comm;\n          if (msg_handler) {\n            var messages = comm.messages[Symbol.asyncIterator]();\n            function processIteratorResult(result) {\n              var message = result.value;\n              var content = {data: message.data};\n              var metadata = message.metadata || {comm_id};\n              var msg = {content, metadata}\n              msg_handler(msg);\n              return messages.next().then(processIteratorResult);\n            }\n            return messages.next().then(processIteratorResult);\n          }\n        }) \n        var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n          return comm_promise.then((comm) => {\n            comm.send(data, metadata, buffers, disposeOnDone);\n          });\n        };\n        var comm = {\n          send: sendClosure\n        };\n      }\n      window.PyViz.comms[comm_id] = comm;\n      return comm;\n    }\n    window.PyViz.comm_manager = new JupyterCommManager();\n    \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n  var div = document.createElement(\"div\");\n  var script = document.createElement(\"script\");\n  node.appendChild(div);\n  node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n  var output_area = handle.output_area;\n  var output = handle.output;\n  if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n    return\n  }\n  var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n  var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n  if (id !== undefined) {\n    var nchildren = toinsert.length;\n    var html_node = toinsert[nchildren-1].children[0];\n    html_node.innerHTML = output.data[HTML_MIME_TYPE];\n    var scripts = [];\n    var nodelist = html_node.querySelectorAll(\"script\");\n    for (var i in nodelist) {\n      if (nodelist.hasOwnProperty(i)) {\n        scripts.push(nodelist[i])\n      }\n    }\n\n    scripts.forEach( function (oldScript) {\n      var newScript = document.createElement(\"script\");\n      var attrs = [];\n      var nodemap = oldScript.attributes;\n      for (var j in nodemap) {\n        if (nodemap.hasOwnProperty(j)) {\n          attrs.push(nodemap[j])\n        }\n      }\n      attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n      newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n      oldScript.parentNode.replaceChild(newScript, oldScript);\n    });\n    if (JS_MIME_TYPE in output.data) {\n      toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n    }\n    output_area._hv_plot_id = id;\n    if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n      window.PyViz.plot_index[id] = Bokeh.index[id];\n    } else {\n      window.PyViz.plot_index[id] = null;\n    }\n  } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n    var bk_div = document.createElement(\"div\");\n    bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n    var script_attrs = bk_div.children[0].attributes;\n    for (var i = 0; i < script_attrs.length; i++) {\n      toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n    }\n    // store reference to server id on output_area\n    output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n  }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n  var id = handle.cell.output_area._hv_plot_id;\n  var server_id = handle.cell.output_area._bokeh_server_id;\n  if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n  var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n  if (server_id !== null) {\n    comm.send({event_type: 'server_delete', 'id': server_id});\n    return;\n  } else if (comm !== null) {\n    comm.send({event_type: 'delete', 'id': id});\n  }\n  delete PyViz.plot_index[id];\n  if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n    var doc = window.Bokeh.index[id].model.document\n    doc.clear();\n    const i = window.Bokeh.documents.indexOf(doc);\n    if (i > -1) {\n      window.Bokeh.documents.splice(i, 1);\n    }\n  }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n  delete PyViz.comms[\"hv-extension-comm\"];\n  window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n  handle_clear_output(event, {cell: {output_area: handle.output_area}})\n  handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n  function append_mime(data, metadata, element) {\n    // create a DOM node to render to\n    var toinsert = this.create_output_subarea(\n    metadata,\n    CLASS_NAME,\n    EXEC_MIME_TYPE\n    );\n    this.keyboard_manager.register_events(toinsert);\n    // Render to node\n    var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n    render(props, toinsert[0]);\n    element.append(toinsert);\n    return toinsert\n  }\n\n  events.on('output_added.OutputArea', handle_add_output);\n  events.on('output_updated.OutputArea', handle_update_output);\n  events.on('clear_output.CodeCell', handle_clear_output);\n  events.on('delete.Cell', handle_clear_output);\n  events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n  OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n    safe: true,\n    index: 0\n  });\n}\n\nif (window.Jupyter !== undefined) {\n  try {\n    var events = require('base/js/events');\n    var OutputArea = require('notebook/js/outputarea').OutputArea;\n    if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n      register_renderer(events, OutputArea);\n    }\n  } catch(err) {\n  }\n}\n",
      "application/vnd.holoviews_load.v0+json": ""
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ All modules loaded successfully\n",
      "CPU times: user 19.3 s, sys: 3.25 s, total: 22.6 s\n",
      "Wall time: 14.9 s\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "%matplotlib inline\n",
    "\n",
    "import importlib\n",
    "import new_import_ODC  \n",
    "\n",
    "importlib.reload(new_import_ODC)\n",
    "\n",
    "from new_import_ODC import *\n",
    "\n",
    "print(\"✅ All modules loaded successfully\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d824dc4f-994b-4d1c-8d24-ce6674da141c",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ AWS credentials found in environment variables\n",
      "✅ Dask cluster initialized\n",
      "   Cluster: LocalCluster(e8e6e31b, 'tcp://127.0.0.1:45313', workers=4, threads=40, memory=128.00 GiB)\n",
      "✅ S3 access configured (requester_pays=True)\n",
      "✅ Datacube connected (metadata only)\n",
      "\n",
      "======================================================================\n",
      "CPU times: user 1.19 s, sys: 69.2 ms, total: 1.26 s\n",
      "Wall time: 6.3 s\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "import os\n",
    "import sys\n",
    "\n",
    "# =====================================================================\n",
    "# AWS CREDENTIALS SETUP\n",
    "# =====================================================================\n",
    "# Ưu tiên load theo thứ tự:\n",
    "# 1. Environment variables (CSIRO server - tự động)\n",
    "# 2. File .env trong thư mục dự án (local machine)\n",
    "# 3. ~/.aws/credentials (AWS CLI config)\n",
    "# =====================================================================\n",
    "\n",
    "def load_env_file(path):\n",
    "    \"\"\"Load key=value từ file .env\"\"\"\n",
    "    loaded = {}\n",
    "    try:\n",
    "        with open(path) as f:\n",
    "            for line in f:\n",
    "                line = line.strip()\n",
    "                if line and not line.startswith('#') and '=' in line:\n",
    "                    k, _, v = line.partition('=')\n",
    "                    os.environ.setdefault(k.strip(), v.strip())\n",
    "                    loaded[k.strip()] = True\n",
    "    except Exception:\n",
    "        pass\n",
    "    return loaded\n",
    "\n",
    "# 1. Thử load từ .env local\n",
    "env_file = os.path.join(os.path.dirname(os.path.abspath(\".\")), \n",
    "                         os.getcwd(), \".env\")\n",
    "env_file2 = \".env\"\n",
    "for ef in [env_file2, os.path.expanduser(\"~/.env\")]:\n",
    "    if os.path.exists(ef):\n",
    "        loaded = load_env_file(ef)\n",
    "        if loaded:\n",
    "            print(f\"✅ AWS credentials loaded from {ef}\")\n",
    "            break\n",
    "else:\n",
    "    # 2. Kiểm tra env vars đã có chưa\n",
    "    if os.environ.get(\"AWS_ACCESS_KEY_ID\"):\n",
    "        print(\"✅ AWS credentials found in environment variables\")\n",
    "    else:\n",
    "        # 3. Thử import qua boto3 (sẽ dùng ~/.aws/credentials)\n",
    "        try:\n",
    "            import boto3\n",
    "            identity = boto3.client(\"sts\").get_caller_identity()\n",
    "            print(f\"✅ AWS credentials loaded from ~/.aws/credentials\")\n",
    "            print(f\"   Account: {identity['Account']}\")\n",
    "        except Exception:\n",
    "            print(\"⚠️  WARNING: No AWS credentials found!\")\n",
    "            print(\"   → Tạo file .env từ template: cp .env.template .env\")\n",
    "            print(\"   → Điền AWS_ACCESS_KEY_ID và AWS_SECRET_ACCESS_KEY\")\n",
    "            print(\"   → Hoặc chạy: aws configure\")\n",
    "\n",
    "# Explicitly set GDAL/rasterio env vars từ AWS creds nếu có\n",
    "if os.environ.get(\"AWS_ACCESS_KEY_ID\"):\n",
    "    os.environ[\"AWS_REQUEST_PAYER\"] = \"requester\"\n",
    "    os.environ.setdefault(\"AWS_DEFAULT_REGION\", \"ap-southeast-2\")\n",
    "    os.environ[\"GDAL_DISABLE_READDIR_ON_OPEN\"] = \"EMPTY_DIR\"\n",
    "    os.environ[\"CPL_VSIL_CURL_ALLOWED_EXTENSIONS\"] = \"TIF,tif,jp2,JP2\"\n",
    "\n",
    "# Cấu hình Dask local\n",
    "from dask.distributed import Client, LocalCluster\n",
    "\n",
    "try:\n",
    "    cluster = LocalCluster(n_workers=4)\n",
    "    client = Client(cluster)\n",
    "    print(\"✅ Dask cluster initialized\")\n",
    "    print(f\"   Cluster: {cluster}\")\n",
    "except Exception as e:\n",
    "    print(f\"⚠️  Dask cluster error: {e}\")\n",
    "    client = None\n",
    "    cluster = None\n",
    "\n",
    "# Cấu hình S3 access cho rasterio/GDAL\n",
    "try:\n",
    "    configure_s3_access(aws_unsigned=False, requester_pays=True, client=client)\n",
    "    print(\"✅ S3 access configured (requester_pays=True)\")\n",
    "except Exception as e:\n",
    "    print(f\"⚠️  S3 config warning: {e}\")\n",
    "\n",
    "# Khai báo Datacube\n",
    "import datacube\n",
    "try:\n",
    "    dc = datacube.Datacube()\n",
    "    print(\"✅ Datacube connected (metadata only)\")\n",
    "except Exception as e:\n",
    "    print(f\"⚠️  Datacube connection not critical: {e}\")\n",
    "    dc = None\n",
    "\n",
    "print(\"\\n\" + \"=\"*70)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "1e113730",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "GETTING SENTINEL-2 SCENE METADATA\n",
      "======================================================================\n",
      "\n",
      "[1] Loading metadata from datacube...\n",
      "    ✅ Found 180337 scenes\n",
      "\n",
      "[2] Selected scene: S2B_56MNC_20230525_0_L2A\n",
      "    Date: 2023-05-25 00:17:32.223000+00:00\n",
      "\n",
      "[3] Available bands:\n",
      "    - aot: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - nir: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - red: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - scl: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - wvp: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - blue: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - green: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - nir08: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - nir09: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - swir16: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - swir22: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - coastal: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - rededge1: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - rededge2: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "    - rededge3: s3://sentinel-cogs/sentinel-s2-l2a-cogs/56/M/NC/2023/5/S2B_56MNC_20230525_0_L2A/\n",
      "======================================================================\n",
      "CPU times: user 1min 23s, sys: 9.56 s, total: 1min 33s\n",
      "Wall time: 1min 58s\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "# 🔧 Get Sentinel-2 scene metadata from datacube\n",
    "print(\"=\"*70)\n",
    "print(\"GETTING SENTINEL-2 SCENE METADATA\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "date_range = (\"2023-03-01\", \"2023-12-31\")\n",
    "longtitude_range = (105.5, 106.4)\n",
    "latitude_range = (9.2, 10.0)\n",
    "\n",
    "try:\n",
    "    print(f\"\\n[1] Loading metadata from datacube...\")\n",
    "    datasets = list(dc.find_datasets(product='s2_l2a', time=date_range))\n",
    "    print(f\"    ✅ Found {len(datasets)} scenes\")\n",
    "    \n",
    "    if datasets:\n",
    "        selected = datasets[0]\n",
    "        print(f\"\\n[2] Selected scene: {selected.metadata.label}\")\n",
    "        scene_datetime = selected.time.begin if hasattr(selected.time, 'begin') else selected.time\n",
    "        print(f\"    Date: {scene_datetime}\")\n",
    "        \n",
    "        # Display measurement paths\n",
    "        print(f\"\\n[3] Available bands:\")\n",
    "        for name, measurement in selected.measurements.items():\n",
    "            print(f\"    - {name}: {measurement['path'][:80]}\")\n",
    "            \n",
    "except Exception as e:\n",
    "    print(f\"❌ Error: {e}\")\n",
    "    import traceback\n",
    "    traceback.print_exc()\n",
    "\n",
    "print(\"=\"*70)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3cd69645",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "CHECKING FOR CACHED DATASET\n",
      "======================================================================\n",
      "\n",
      "⏳ Cache file not found: dataset_cache/sentinel2_timeseries_40scenes.nc\n",
      "   Will download from S3 and save cache\n",
      "   (Next run will use cache automatically)\n",
      "======================================================================\n",
      "CPU times: user 910 μs, sys: 0 ns, total: 910 μs\n",
      "Wall time: 5.23 ms\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "# 🔍 CHECK IF DATASET CACHE EXISTS (Skip download if available)\n",
    "print(\"=\"*70)\n",
    "print(\"CHECKING FOR CACHED DATASET\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "import os\n",
    "import xarray as xr\n",
    "\n",
    "cache_dir = \"dataset_cache\"\n",
    "cache_file = f\"{cache_dir}/sentinel2_timeseries_40scenes.nc\"\n",
    "\n",
    "use_cache = False\n",
    "\n",
    "if os.path.exists(cache_file):\n",
    "    print(f\"\\n✅ Cache file found: {cache_file}\")\n",
    "    \n",
    "    # Get file info\n",
    "    file_size_gb = os.path.getsize(cache_file) / (1024**3)\n",
    "    print(f\"   File size: {file_size_gb:.2f} GB\")\n",
    "    \n",
    "    # Try to load\n",
    "    try:\n",
    "        print(f\"\\n🔄 Loading dataset from cache...\")\n",
    "        data = xr.open_dataset(cache_file)\n",
    "        \n",
    "        print(f\"✅ Dataset loaded from cache!\")\n",
    "        print(f\"   Total scenes: {len(data['time'])}\")\n",
    "        print(f\"   Variables: {len(data.data_vars)}\")\n",
    "        print(f\"   Dimensions: {dict(data.dims)}\")\n",
    "        print(f\"\\n   ⏭️  Skipping S3 download (using cached data)\")\n",
    "        \n",
    "        use_cache = True\n",
    "        \n",
    "    except Exception as e:\n",
    "        print(f\"❌ Error loading cache: {e}\")\n",
    "        print(f\"   Will download fresh data from S3\")\n",
    "        use_cache = False\n",
    "else:\n",
    "    print(f\"\\n⏳ Cache file not found: {cache_file}\")\n",
    "    print(f\"   Will download from S3 and save cache\")\n",
    "    print(f\"   (Next run will use cache automatically)\")\n",
    "\n",
    "print(\"=\"*70)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "435f9f78-a9a4-4226-86ca-d4bec42d454e",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "LOADING SENTINEL-2 DATA VIA dc.load() (ODC NATIVE)\n",
      "======================================================================\n",
      "\n",
      "[1] Finding native CRS...\n",
      "   CRS: EPSG:32648\n",
      "\n",
      "[2] Loading SCL (cloud mask) to filter cloudy scenes...\n",
      "No datasets require offset correction\n",
      "❌ Error: Empty list of variables to be set\n",
      "======================================================================\n",
      "CPU times: user 2.02 s, sys: 210 ms, total: 2.23 s\n",
      "Wall time: 3.02 s\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Traceback (most recent call last):\n",
      "  File \"<timed exec>\", line 38, in <module>\n",
      "  File \"/home/jovyan/easi-notebooks/easi_tools/load_s2l2a.py\", line 227, in load_s2l2a_with_offset\n",
      "    xx = apply_correction_to_data(data, this_offset)\n",
      "         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
      "  File \"/home/jovyan/easi-notebooks/easi_tools/load_s2l2a.py\", line 145, in apply_correction_to_data\n",
      "    ds[refl_vars] = ds[refl_vars].where(mask) * scale_factor\n",
      "    ~~^^^^^^^^^^^\n",
      "  File \"/env/lib/python3.12/site-packages/xarray/core/dataset.py\", line 1649, in __setitem__\n",
      "    raise ValueError(\"Empty list of variables to be set\")\n",
      "ValueError: Empty list of variables to be set\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "# 💾 LOAD SENTINEL-2 DATA VIA dc.load() - ODC NATIVE APPROACH\n",
    "print(\"=\"*70)\n",
    "print(\"LOADING SENTINEL-2 DATA VIA dc.load() (ODC NATIVE)\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "import xarray as xr\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import os\n",
    "\n",
    "MAX_SCENES = 40       # Chỉ lấy 40 cảnh ít mây nhất\n",
    "CLOUD_MAX  = 30       # Bỏ cảnh có >30% mây\n",
    "# SCL cloud values: 3=cloud shadow, 8=med cloud, 9=high cloud, 10=cirrus\n",
    "CLOUD_VALS = [3, 8, 9, 10]\n",
    "\n",
    "try:\n",
    "    # ===== CHECK IF SHOULD SKIP DOWNLOAD =====\n",
    "    if use_cache and 'data' in dir() and data is not None:\n",
    "        print(f\"\\n✅ Using cached dataset - skipping download!\")\n",
    "        print(f\"   Variables: {len(data.data_vars)}\")\n",
    "        print(f\"   Shape: {data.dims}\")\n",
    "        display(data)\n",
    "\n",
    "    else:\n",
    "        # Query base\n",
    "        query = {\n",
    "            'product': 's2_l2a',\n",
    "            'time': date_range,\n",
    "            'x': longtitude_range,\n",
    "            'y': latitude_range,\n",
    "        }\n",
    "\n",
    "        print(f\"\\n[1] Finding native CRS...\")\n",
    "        try:\n",
    "            native_crs = notebook_utils.mostcommon_crs(dc, query)\n",
    "        except Exception:\n",
    "            native_crs = 'EPSG:32648'\n",
    "        print(f\"   CRS: {native_crs}\")\n",
    "\n",
    "        # ===== STEP 2: LOAD SCL via dc.load() directly (no offset correction needed) =====\n",
    "        print(f\"\\n[2] Loading SCL (cloud mask) via dc.load() to filter cloudy scenes...\")\n",
    "        scl_lazy = dc.load(\n",
    "            product='s2_l2a',\n",
    "            time=date_range,\n",
    "            x=longtitude_range,\n",
    "            y=latitude_range,\n",
    "            measurements=['scl'],\n",
    "            output_crs=native_crs,\n",
    "            resolution=(-60, 60),       # 60m → nhanh hơn nhiều\n",
    "            group_by='solar_day',\n",
    "            dask_chunks={'x': 1024, 'y': 1024},\n",
    "        )\n",
    "        scl_data = scl_lazy.compute()\n",
    "        print(f\"   ✅ SCL loaded: {len(scl_data.time)} total scenes\")\n",
    "\n",
    "        # ===== STEP 3: CALCULATE CLOUD COVER PER SCENE =====\n",
    "        print(f\"\\n[3] Calculating cloud cover per scene...\")\n",
    "        scl_arr = scl_data['scl'].values   # (time, y, x)\n",
    "\n",
    "        cloud_pcts = []\n",
    "        for t in range(scl_arr.shape[0]):\n",
    "            scene = scl_arr[t]\n",
    "            valid = scene > 0\n",
    "            n_valid = valid.sum()\n",
    "            if n_valid == 0:\n",
    "                cloud_pcts.append(100.0)\n",
    "            else:\n",
    "                n_cloud = np.isin(scene, CLOUD_VALS).sum()\n",
    "                cloud_pcts.append(float(n_cloud / n_valid * 100))\n",
    "\n",
    "        cloud_pcts = np.array(cloud_pcts)\n",
    "        times = scl_data.time.values\n",
    "\n",
    "        ok_mask  = cloud_pcts <= CLOUD_MAX\n",
    "        ok_times = times[ok_mask]\n",
    "        ok_pcts  = cloud_pcts[ok_mask]\n",
    "\n",
    "        print(f\"   Total scenes    : {len(times)}\")\n",
    "        print(f\"   Cloud ≤{CLOUD_MAX}%   : {len(ok_times)} scenes\")\n",
    "\n",
    "        if len(ok_times) == 0:\n",
    "            raise ValueError(f\"No scenes with cloud cover ≤{CLOUD_MAX}%. Try increasing CLOUD_MAX.\")\n",
    "\n",
    "        # Sort by cloud cover, take MAX_SCENES best, then sort chronologically\n",
    "        sort_idx  = np.argsort(ok_pcts)[:MAX_SCENES]\n",
    "        sel_times = ok_times[sort_idx]\n",
    "        sel_pcts  = ok_pcts[sort_idx]\n",
    "        chron_idx = np.argsort(sel_times)\n",
    "        sel_times = sel_times[chron_idx]\n",
    "        sel_pcts  = sel_pcts[chron_idx]\n",
    "\n",
    "        print(f\"\\n   Selected {len(sel_times)} least-cloudy scenes:\")\n",
    "        for t, pct in zip(sel_times[:5], sel_pcts[:5]):\n",
    "            print(f\"     {str(t)[:10]}  cloud={pct:.1f}%\")\n",
    "        if len(sel_times) > 5:\n",
    "            print(f\"     ... ({len(sel_times)-5} more)\")\n",
    "\n",
    "        # ===== STEP 4: LOAD SELECTED SCENES (reflectance bands via load_s2l2a_with_offset) =====\n",
    "        measurements = ['blue', 'green', 'red', 'nir', 'swir16', 'swir22',\n",
    "                        'coastal', 'rededge1', 'rededge2', 'rededge3', 'scl']\n",
    "\n",
    "        t_start = str(sel_times[0])[:10]\n",
    "        t_end   = str(sel_times[-1])[:10]\n",
    "\n",
    "        print(f\"\\n[4] Loading {len(sel_times)} scenes at 10m: {t_start} → {t_end} ...\")\n",
    "        data_lazy = load_s2l2a_with_offset(dc, {\n",
    "            **query,\n",
    "            'time': (t_start, t_end),\n",
    "            'measurements': measurements,\n",
    "            'output_crs': native_crs,\n",
    "            'resolution': (-10, 10),\n",
    "            'group_by': 'solar_day',\n",
    "            'dask_chunks': {'x': 1024, 'y': 1024},\n",
    "        })\n",
    "\n",
    "        # Filter to only selected timestamps\n",
    "        sel_times_pd = pd.DatetimeIndex(sel_times)\n",
    "        data_lazy = data_lazy.sel(time=data_lazy.time.isin(sel_times_pd))\n",
    "\n",
    "        print(f\"   Lazy dataset: {dict(data_lazy.dims)}\")\n",
    "\n",
    "        # Size estimate\n",
    "        n_time  = len(data_lazy.time)\n",
    "        ny      = data_lazy.dims.get('y', 0)\n",
    "        nx      = data_lazy.dims.get('x', 0)\n",
    "        n_bands = len([m for m in measurements if m != 'scl'])\n",
    "        size_gb = n_time * ny * nx * n_bands * 2 / 1e9\n",
    "        print(f\"   Estimated size: {size_gb:.1f} GB  ({n_time} scenes × {ny}×{nx} × {n_bands} bands)\")\n",
    "\n",
    "        if size_gb > 40:\n",
    "            keep = max(1, int(40 / (size_gb / n_time)))\n",
    "            print(f\"\\n   ⚠️  Too large ({size_gb:.1f} GB) → reducing to {keep} scenes...\")\n",
    "            data_lazy = data_lazy.isel(time=slice(0, keep))\n",
    "            n_time  = len(data_lazy.time)\n",
    "            size_gb = n_time * ny * nx * n_bands * 2 / 1e9\n",
    "            print(f\"   Reduced: {size_gb:.1f} GB  ({n_time} scenes)\")\n",
    "\n",
    "        # ===== STEP 5: CLOUD MASK & COMPUTE =====\n",
    "        print(f\"\\n[5] Applying pixel-level cloud mask (SCL)...\")\n",
    "        scl_band    = data_lazy['scl']\n",
    "        good_pixels = ~scl_band.isin(CLOUD_VALS)\n",
    "        data_masked = data_lazy.where(good_pixels)\n",
    "\n",
    "        print(f\"\\n[6] Computing (Dask download — may take several minutes)...\")\n",
    "        data = data_masked.compute()\n",
    "        print(f\"✅ Data computed! {dict(data.dims)}\")\n",
    "\n",
    "        # ===== STEP 6: SPECTRAL INDICES =====\n",
    "        print(f\"\\n[7] Calculating spectral indices...\")\n",
    "\n",
    "        if 'nir' in data and 'red' in data:\n",
    "            nir = data['nir'].astype(float)\n",
    "            red = data['red'].astype(float)\n",
    "            data['ndvi'] = ((nir - red) / (nir + red + 1e-8)).astype(np.float32)\n",
    "            print(f\"   ✅ NDVI\")\n",
    "\n",
    "        if 'swir16' in data and 'nir' in data:\n",
    "            swir = data['swir16'].astype(float)\n",
    "            nir  = data['nir'].astype(float)\n",
    "            data['ndbi'] = ((swir - nir) / (swir + nir + 1e-8)).astype(np.float32)\n",
    "            data['ndwi'] = ((nir - swir) / (nir + swir + 1e-8)).astype(np.float32)\n",
    "            print(f\"   ✅ NDBI, NDWI\")\n",
    "\n",
    "        if 'nir' in data and 'red' in data and 'blue' in data:\n",
    "            nir  = data['nir'].astype(float)\n",
    "            red  = data['red'].astype(float)\n",
    "            blue = data['blue'].astype(float)\n",
    "            data['evi'] = (2.5 * (nir - red) / (nir + 6*red - 7.5*blue + 1)).astype(np.float32)\n",
    "            print(f\"   ✅ EVI\")\n",
    "\n",
    "        # ===== STEP 7: TEMPORAL AGGREGATES =====\n",
    "        print(f\"\\n[8] Computing temporal aggregate features...\")\n",
    "        added = 0\n",
    "        if 'ndvi' in data:\n",
    "            ts = data['ndvi']\n",
    "            data['ndvi_min']   = ts.min(dim='time');   added += 1\n",
    "            data['ndvi_max']   = ts.max(dim='time');   added += 1\n",
    "            data['ndvi_mean']  = ts.mean(dim='time');  added += 1\n",
    "            data['ndvi_std']   = ts.std(dim='time');   added += 1\n",
    "            data['ndvi_range'] = data['ndvi_max'] - data['ndvi_min']; added += 1\n",
    "        for idx_name in ['ndbi', 'ndwi', 'evi']:\n",
    "            if idx_name in data:\n",
    "                data[f'{idx_name}_mean'] = data[idx_name].mean(dim='time');  added += 1\n",
    "        print(f\"   ✅ {added} aggregate features added\")\n",
    "\n",
    "        # ===== STEP 8: SAVE CACHE =====\n",
    "        print(f\"\\n[9] Saving cache: {cache_file}\")\n",
    "        try:\n",
    "            os.makedirs(cache_dir, exist_ok=True)\n",
    "            data.to_netcdf(cache_file, engine='netcdf4')\n",
    "            size_saved = os.path.getsize(cache_file) / 1e9\n",
    "            print(f\"   ✅ Cached ({size_saved:.2f} GB)\")\n",
    "        except Exception as e:\n",
    "            print(f\"   ⚠️ Cache save failed: {e}\")\n",
    "\n",
    "        print(f\"\\n{'='*70}\")\n",
    "        print(f\"✅ Dataset ready!\")\n",
    "        print(f\"   Scenes     : {len(data.time)}\")\n",
    "        print(f\"   Variables  : {len(data.data_vars)}\")\n",
    "        print(f\"   Spatial    : {data.dims.get('x',0)} × {data.dims.get('y',0)} px @ 10m\")\n",
    "        print(f\"{'='*70}\")\n",
    "        display(data)\n",
    "\n",
    "    # ===== EXTRACT NDVI =====\n",
    "    if 'ndvi_mean' in data.data_vars:\n",
    "        ndvi = data['ndvi_mean']\n",
    "        print(f\"\\n✅ NDVI (mean) shape: {ndvi.shape}\")\n",
    "    elif 'ndvi' in data.data_vars:\n",
    "        ndvi = data['ndvi'].isel(time=0)\n",
    "        print(f\"\\n✅ NDVI (t=0) shape: {ndvi.shape}\")\n",
    "    else:\n",
    "        ndvi = None\n",
    "        print(f\"\\n❌ NDVI not found in dataset\")\n",
    "\n",
    "except Exception as e:\n",
    "    print(f\"❌ Error: {e}\")\n",
    "    import traceback\n",
    "    traceback.print_exc()\n",
    "    data = None\n",
    "    ndvi = None\n",
    "\n",
    "print(\"=\"*70)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "d2585562-88aa-4c7d-bf70-1f6affcf65d4",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "TRAINING DATA SETUP\n",
      "======================================================================\n",
      "\n",
      "[1] Loading training data: train/ST_training data_updated_1130points_new.shp\n",
      "    ❌ Error: train/ST_training data_updated_1130points_new.shp: No such file or directory\n",
      "\n",
      "[2] Label mapping:\n",
      "    0: Lua tom\n",
      "    1: Lua\n",
      "    2: CHN\n",
      "    3: CLN\n",
      "    4: TS\n",
      "    5: Song\n",
      "    6: Dat xay dung\n",
      "    7: Rung\n",
      "\n",
      "======================================================================\n"
     ]
    }
   ],
   "source": [
    "# 🎯 LOAD TRAINING DATA & EXTRACT FEATURES\n",
    "print(\"=\"*70)\n",
    "print(\"TRAINING DATA SETUP\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "# Load training points\n",
    "train_path = \"train/ST_training data_updated_1130points_new.shp\"\n",
    "print(f\"\\n[1] Loading training data: {train_path}\")\n",
    "\n",
    "try:\n",
    "    train = load_train_data(train_path)\n",
    "    print(f\"    ✅ Loaded {len(train)} training points\")\n",
    "    print(f\"    Columns: {list(train.columns)}\")\n",
    "    train.head()\n",
    "except Exception as e:\n",
    "    print(f\"    ❌ Error: {e}\")\n",
    "    train = None\n",
    "\n",
    "# Label mapping\n",
    "label_mapping = {\n",
    "    \"Lua tom\": \"0\",\n",
    "    \"Lua\": \"1\",\n",
    "    \"CHN\": \"2\",\n",
    "    \"CLN\": \"3\",\n",
    "    \"TS\": \"4\",\n",
    "    \"Song\": \"5\",\n",
    "    \"Dat xay dung\": \"6\",\n",
    "    \"Rung\": \"7\",\n",
    "}\n",
    "\n",
    "print(f\"\\n[2] Label mapping:\")\n",
    "for label, code in label_mapping.items():\n",
    "    print(f\"    {code}: {label}\")\n",
    "\n",
    "print(\"\\n\" + \"=\"*70)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "2e955884-d4af-422d-a8e6-d436199540e0",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "LAND USE CLASSIFICATION TRAINING\n",
      "======================================================================\n",
      "\n",
      "🎯 Mục tiêu: Dự đoán phân loại sử dụng đất (8 lớp)\n",
      "   - NDVI/NDWI/NDBI/EVI là INPUT FEATURES\n",
      "   - Sau khi predict xong → có thể hiển thị NDVI map như chỉ số phụ\n",
      "======================================================================\n",
      "❌ Missing training data or satellite data\n",
      "\n",
      "======================================================================\n",
      "📝 NOTE: Model này dự đoán PHÂN LOẠI SỬ DỤNG ĐẤT (8 lớp)\n",
      "    NDVI là một trong các features đầu vào, không phải mục tiêu dự đoán\n",
      "    Sau khi predict → có thể hiển thị NDVI map như chỉ số phụ\n",
      "======================================================================\n",
      "CPU times: user 179 μs, sys: 0 ns, total: 179 μs\n",
      "Wall time: 168 μs\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "# 🤖 LAND USE CLASSIFICATION MODEL TRAINING (MỤC TIÊU CHÍNH)\n",
    "print(\"=\"*70)\n",
    "print(\"LAND USE CLASSIFICATION TRAINING\")\n",
    "print(\"=\"*70)\n",
    "print(\"\\n🎯 Mục tiêu: Dự đoán phân loại sử dụng đất (8 lớp)\")\n",
    "print(\"   - NDVI/NDWI/NDBI/EVI là INPUT FEATURES\")\n",
    "print(\"   - Sau khi predict xong → có thể hiển thị NDVI map như chỉ số phụ\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "if train is not None and data is not None:\n",
    "    print(\"\\n[1] Extracting MULTIPLE features from satellite data...\")\n",
    "    print(\"    (Sử dụng nhiều spectral indices để cải thiện accuracy)\")\n",
    "    \n",
    "    try:\n",
    "        # Extract features at training point locations\n",
    "        X = []\n",
    "        y = []\n",
    "        \n",
    "        # Available features from data\n",
    "        available_features = ['ndvi_mean', 'ndvi_min', 'ndvi_max', 'ndvi_std', 'ndvi_range',\n",
    "                             'ndwi_mean', 'ndbi_mean', 'evi_mean']\n",
    "        \n",
    "        # Check which features are actually available\n",
    "        features_to_use = [f for f in available_features if f in data.data_vars]\n",
    "        \n",
    "        if not features_to_use:\n",
    "            print(\"    ❌ No spectral features found in dataset!\")\n",
    "            print(\"    Available variables:\", list(data.data_vars))\n",
    "            model = None\n",
    "        else:\n",
    "            print(f\"    Using {len(features_to_use)} features: {features_to_use}\")\n",
    "            \n",
    "            for idx, point in train.iterrows():\n",
    "                try:\n",
    "                    # Extract all available features at this point\n",
    "                    feature_vec = []\n",
    "                    for feat_name in features_to_use:\n",
    "                        feat_val = float(data[feat_name].sel(\n",
    "                            x=point.geometry.x, \n",
    "                            y=point.geometry.y, \n",
    "                            method='nearest'\n",
    "                        ).values)\n",
    "                        feature_vec.append(feat_val)\n",
    "                    \n",
    "                    # Get label\n",
    "                    label = label_mapping[point.Hientrang]\n",
    "                    \n",
    "                    # Only add if no NaN values\n",
    "                    if not np.isnan(feature_vec).any():\n",
    "                        X.append(feature_vec)\n",
    "                        y.append(int(label))\n",
    "                except Exception as e:\n",
    "                    # Skip points with errors\n",
    "                    continue\n",
    "            \n",
    "            if len(X) > 0:\n",
    "                X = np.array(X)\n",
    "                y = np.array(y)\n",
    "                print(f\"    ✅ Extracted {len(X)} samples with {X.shape[1]} features each\")\n",
    "                \n",
    "                # Show feature statistics\n",
    "                print(f\"\\n    Feature statistics:\")\n",
    "                for i, feat_name in enumerate(features_to_use):\n",
    "                    print(f\"      {feat_name:15s}: mean={X[:,i].mean():.3f}, std={X[:,i].std():.3f}\")\n",
    "                \n",
    "                # Split data\n",
    "                print(f\"\\n[2] Splitting data (80-20)...\")\n",
    "                from sklearn.model_selection import train_test_split\n",
    "                X_train, X_test, y_train, y_test = train_test_split(\n",
    "                    X, y, test_size=0.2, random_state=42, stratify=y\n",
    "                )\n",
    "                print(f\"    Train: {len(X_train)}, Test: {len(X_test)}\")\n",
    "                \n",
    "                # Show class distribution\n",
    "                unique, counts = np.unique(y_train, return_counts=True)\n",
    "                print(f\"\\n    Class distribution in training set:\")\n",
    "                for cls, count in zip(unique, counts):\n",
    "                    cls_name = [k for k, v in label_mapping.items() if v == str(cls)][0]\n",
    "                    print(f\"      {cls}: {cls_name:15s} - {count:4d} samples ({count/len(y_train)*100:.1f}%)\")\n",
    "                \n",
    "                # Train model\n",
    "                print(f\"\\n[3] Training Random Forest for LAND USE CLASSIFICATION...\")\n",
    "                from sklearn.ensemble import RandomForestClassifier\n",
    "                from sklearn.metrics import accuracy_score, classification_report\n",
    "                \n",
    "                model = RandomForestClassifier(\n",
    "                    n_estimators=200,  # More trees for better accuracy\n",
    "                    max_depth=30,\n",
    "                    min_samples_split=5,\n",
    "                    random_state=42,\n",
    "                    n_jobs=-1,\n",
    "                    verbose=1\n",
    "                )\n",
    "                model.fit(X_train, y_train)\n",
    "                \n",
    "                # Evaluate\n",
    "                y_pred = model.predict(X_test)\n",
    "                accuracy = accuracy_score(y_test, y_pred)\n",
    "                \n",
    "                print(f\"\\n    ✅ Model trained!\")\n",
    "                print(f\"    Training accuracy: {model.score(X_train, y_train)*100:.2f}%\")\n",
    "                print(f\"    Testing accuracy: {accuracy*100:.2f}%\")\n",
    "                \n",
    "                # Show feature importance\n",
    "                print(f\"\\n    Feature importance:\")\n",
    "                importances = model.feature_importances_\n",
    "                indices = np.argsort(importances)[::-1]\n",
    "                for i, idx in enumerate(indices):\n",
    "                    print(f\"      {i+1}. {features_to_use[idx]:15s}: {importances[idx]:.4f}\")\n",
    "                \n",
    "                # Classification report\n",
    "                print(f\"\\n[4] Classification Report:\")\n",
    "                class_names = [k for k, v in sorted(label_mapping.items(), key=lambda x: x[1])]\n",
    "                print(classification_report(y_test, y_pred, target_names=class_names, zero_division=0))\n",
    "                \n",
    "            else:\n",
    "                print(f\"    ❌ No samples extracted\")\n",
    "                model = None\n",
    "                \n",
    "    except Exception as e:\n",
    "        print(f\"    ❌ Error: {e}\")\n",
    "        import traceback\n",
    "        traceback.print_exc()\n",
    "        model = None\n",
    "else:\n",
    "    print(\"❌ Missing training data or satellite data\")\n",
    "    model = None\n",
    "\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"📝 NOTE: Model này dự đoán PHÂN LOẠI SỬ DỤNG ĐẤT (8 lớp)\")\n",
    "print(\"    NDVI là một trong các features đầu vào, không phải mục tiêu dự đoán\")\n",
    "print(\"    Sau khi predict → có thể hiển thị NDVI map như chỉ số phụ\")\n",
    "print(\"=\"*70)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f1a14379-ed6e-4897-9ca4-2669743fab40",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "MODEL SAVING\n",
      "======================================================================\n",
      "❌ No model to save\n",
      "======================================================================\n"
     ]
    }
   ],
   "source": [
    "# 💾 SAVE MODEL WITH METADATA\n",
    "print(\"=\"*70)\n",
    "print(\"MODEL SAVING\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "if model is not None:\n",
    "    print(\"\\n🔄 Saving trained LAND USE CLASSIFICATION model with metadata...\")\n",
    "    try:\n",
    "        from datetime import datetime\n",
    "        \n",
    "        # Prepare metadata for ModelManager\n",
    "        metadata = {\n",
    "            \"timestamp\": datetime.now().isoformat(),\n",
    "            \"data_source\": \"Local S3 ODC (Open Data Cube)\",\n",
    "            \"collections\": [\"sentinel-2-l2a\"],\n",
    "            \"features\": features_to_use,  # All features used\n",
    "            \"feature_mode\": \"extended\",  # Using extended aggregate features\n",
    "            \"training_samples\": len(X_train),\n",
    "            \"testing_samples\": len(X_test),\n",
    "            \"test_size\": 0.2,\n",
    "            \"train_accuracy\": float(model.score(X_train, y_train)),\n",
    "            \"test_accuracy\": float(accuracy),\n",
    "            \"model_type\": \"random_forest\",\n",
    "            \"device\": \"cpu\",\n",
    "            \"n_estimators\": 200,\n",
    "            \"max_depth\": 30,\n",
    "            \"learning_rate\": None,\n",
    "            \"cnn_epochs\": None,\n",
    "            \"n_features\": X_train.shape[1],\n",
    "            \"n_classes\": len(np.unique(y)),\n",
    "            \"class_names\": list(label_mapping.keys()),\n",
    "            \"classification_report\": classification_report(y_test, y_pred, \n",
    "                                                          target_names=class_names, \n",
    "                                                          output_dict=True,\n",
    "                                                          zero_division=0),\n",
    "            \"bbox\": None,\n",
    "            \"time_range\": f\"{date_range[0]}/{date_range[1]}\",\n",
    "            \"resolution\": 10,\n",
    "            \"notes\": \"LAND USE CLASSIFICATION model trained from 01.train_ODC.ipynb. Predicts 8 land use classes using multiple spectral indices. NDVI is one of the input features, not the prediction target.\"\n",
    "        }\n",
    "        \n",
    "        # Save model with metadata using updated save_model function\n",
    "        save_model(\"model_land_use_odc.joblib\", model, metadata=metadata, label_encoder=None)\n",
    "        \n",
    "        print(\"✅ Model saved to model_train/model_land_use_odc.joblib\")\n",
    "        print(f\"   - Purpose: Land Use Classification (8 classes)\")\n",
    "        print(f\"   - Features: {len(features_to_use)} ({', '.join(features_to_use[:3])}...)\")\n",
    "        print(f\"   - Train Accuracy: {metadata['train_accuracy']*100:.2f}%\")\n",
    "        print(f\"   - Test Accuracy: {metadata['test_accuracy']*100:.2f}%\")\n",
    "        print(f\"   - Classes: {metadata['n_classes']}\")\n",
    "        print(f\"\\n📝 NDVI là một trong các features, không phải prediction target\")\n",
    "        print(f\"   Sau khi predict → có thể tính NDVI map riêng để hiển thị\")\n",
    "    except Exception as e:\n",
    "        print(f\"❌ Error saving model: {e}\")\n",
    "        import traceback\n",
    "        traceback.print_exc()\n",
    "else:\n",
    "    print(\"❌ No model to save\")\n",
    "\n",
    "print(\"=\"*70)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4a8579f4",
   "metadata": {},
   "source": [
    "# 📖 Hướng dẫn sử dụng Model\n",
    "\n",
    "## Mục đích của Model\n",
    "\n",
    "Model này được train để **DỰ ĐOÁN PHÂN LOẠI SỬ DỤNG ĐẤT** với 8 lớp:\n",
    "\n",
    "1. **Lua tom** (0) - Lúa tôm\n",
    "2. **Lua** (1) - Lúa\n",
    "3. **CHN** (2) - Cây hàng năm\n",
    "4. **CLN** (3) - Cây lâu năm  \n",
    "5. **TS** (4) - Thủy sản\n",
    "6. **Song** (5) - Sông\n",
    "7. **Dat xay dung** (6) - Đất xây dựng\n",
    "8. **Rung** (7) - Rừng\n",
    "\n",
    "## Features đầu vào\n",
    "\n",
    "Model sử dụng **nhiều spectral indices** làm features:\n",
    "- NDVI (mean, min, max, std, range)\n",
    "- NDWI (mean)\n",
    "- NDBI (mean)\n",
    "- EVI (mean)\n",
    "\n",
    "## NDVI là gì trong hệ thống này?\n",
    "\n",
    "⚠️ **QUAN TRỌNG**: NDVI **KHÔNG PHẢI** là mục tiêu dự đoán!\n",
    "\n",
    "- **NDVI là INPUT FEATURE**: Một trong các chỉ số dùng để train model\n",
    "- **Mục tiêu dự đoán**: Phân loại sử dụng đất (8 lớp)\n",
    "- **NDVI map**: Có thể hiển thị NDVI map như chỉ số phụ sau khi predict xong\n",
    "\n",
    "## Workflow Prediction\n",
    "\n",
    "```python\n",
    "# 1. Load model\n",
    "model, label_encoder, metadata = model_manager.load_model(\"model_land_use_odc.joblib\")\n",
    "\n",
    "# 2. Extract features từ satellite data\n",
    "features = extract_features(satellite_data)  # NDVI, NDWI, NDBI, EVI\n",
    "\n",
    "# 3. Predict land use classification\n",
    "land_use_prediction = model.predict(features)\n",
    "# → Kết quả: Mảng với giá trị 0-7 (8 lớp sử dụng đất)\n",
    "\n",
    "# 4. (Optional) Tính NDVI map riêng để hiển thị\n",
    "ndvi_map = (NIR - Red) / (NIR + Red)\n",
    "# → NDVI map chỉ để visualize, không phải prediction target\n",
    "```\n",
    "\n",
    "## So sánh với approach cũ\n",
    "\n",
    "| Approach | Features | Target | NDVI Role |\n",
    "|----------|----------|--------|-----------|\n",
    "| ❌ Cũ (sai) | Chỉ NDVI | 8 lớp đất | Input duy nhất |\n",
    "| ✅ Mới (đúng) | NDVI + NDWI + NDBI + EVI | 8 lớp đất | Một trong nhiều features |\n",
    "\n",
    "## Test Model\n",
    "\n",
    "```python\n",
    "# Test với website\n",
    "# 1. Upload model_land_use_odc.joblib lên server\n",
    "# 2. Chọn model trong prediction interface\n",
    "# 3. Chọn vùng và thời gian\n",
    "# 4. System sẽ tự động:\n",
    "#    - Extract features (NDVI, NDWI, NDBI, EVI)\n",
    "#    - Predict land use classification\n",
    "#    - (Optional) Generate NDVI visualization map\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "33dd516d-9824-499e-96b9-5cd9224c194c",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "CLEANUP\n",
      "======================================================================\n",
      "\n",
      "🔄 Closing Dask client and cluster...\n",
      "✅ Cleanup complete\n",
      "\n",
      "======================================================================\n",
      "✅ PIPELINE COMPLETE\n",
      "======================================================================\n"
     ]
    }
   ],
   "source": [
    "# 🛑 CLEANUP\n",
    "print(\"=\"*70)\n",
    "print(\"CLEANUP\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "print(\"\\n🔄 Closing Dask client and cluster...\")\n",
    "try:\n",
    "    client.close()\n",
    "    cluster.close()\n",
    "    print(\"✅ Cleanup complete\")\n",
    "except Exception as e:\n",
    "    print(f\"⚠️  Error during cleanup: {e}\")\n",
    "\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"✅ PIPELINE COMPLETE\")\n",
    "print(\"=\"*70)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
