diff --git a/experimental/scienceai_ee_dataset_search_agent/README.md b/experimental/scienceai_ee_dataset_search_agent/README.md new file mode 100644 index 000000000..1808f76cc --- /dev/null +++ b/experimental/scienceai_ee_dataset_search_agent/README.md @@ -0,0 +1,29 @@ + + +# Earth Engine Dataset Search Agent + +## Overview + +Our mission at Science AI, Google Research, is to enable scientific breakthroughs and discoveries that benefit humanity and radically accelerate scientific progress. One key focus area for us is empowering geospatial analysts and scientists through the power of generative AI and large language models (LLMs). Our aim is to leverage LLMs to assist with planning and code generation in geospatial analysis, dramatically accelerating analyst workflows. + +An important part of automating geospatial workflows is identifying which datasets are most relevant given specific geospatial queries. As such, our initial focus has been to build a dataset search agent to help users find the datasets that are most suitable for their analysis needs. Now more than ever, geospatial analysis and environmental insights are crucial to addressing the risks of climate change, and we hope that empowering analysts to more easily find the right information will have a positive impact on building towards a more sustainable future. + +We're excited to release this experimental dataset search agent to the Earth Engine community, and hope it proves valuable for your geospatial analysis needs. We welcome your feedback, suggestions, and questions at scienceai_ee_dataset_search_agent@googlegroups.com. We would like this tool to grow and evolve with community involvement, so we encourage you to explore, experiment, and build upon it. We'd love to hear how you're using the agent and any improvements you've made! + +## Attribution + +The Earth Engine Dataset Search Agent was developed by the Science AI team in Google Research. The primary developers are Renee Johnston and Eliot Cowan, with design input from Grace Joseph. This notebook was built on the work of Simon Ilyushchenko. The team is also very grateful to our expert advisors, Jeffrey Cardille, Erin Trochim, Morgan Crowley, and Samapriya Roy, for their advice and continued collaboration. diff --git a/experimental/scienceai_ee_dataset_search_agent/scienceai_ee_dataset_search_agent_v0.ipynb b/experimental/scienceai_ee_dataset_search_agent/scienceai_ee_dataset_search_agent_v0.ipynb new file mode 100644 index 000000000..0ad084213 --- /dev/null +++ b/experimental/scienceai_ee_dataset_search_agent/scienceai_ee_dataset_search_agent_v0.ipynb @@ -0,0 +1,4010 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 242, + "status": "ok", + "timestamp": 1726764453092, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "j9jYuqKak7vM", + "outputId": "a9292165-a3cf-4621-8d69-8e3d917774eb" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Copyright 2024 The Earth Engine Community Authors { display-mode: \"form\" }\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gu5qMojEL8do" + }, + "source": [ + "# Earth Engine Dataset Search Agent\n", + "\n", + "## Overview\n", + "This notebook, built by the Science AI team in Google Research, implements the Earth Engine Dataset Search Agent, designed to help users discover datasets within the Earth Engine catalog that are relevant to their geospatial analysis tasks. The core functionality includes:\n", + "\n", + "1. Loading and processing Earth Engine dataset metadata and pre-computed embeddings.\n", + "2. Implementing a dataset search function that uses vector similarity to find relevant datasets based on user queries.\n", + "3. Creating an interactive user interface that displays search results, LLM-generated dataset details, code samples, and map visualizations.\n", + "\n", + "To run the agent, run all cells, and then check out the \"Earth Engine Dataset Search Agent\" section at the end of the notebook.\n", + "\n", + "\n", + "## Setup Details and Billing\n", + "\n", + "You will need:\n", + "\n", + "- A Google cloud project with the Earth Engine API enabled. ([Details](https://developers.google.com/earth-engine/cloud/earthengine_cloud_project_setup)).\n", + "- A Gemini API key. ([Details](https://ai.google.dev/gemini-api/docs/api-key)).\n", + "\n", + "\n", + "Each of the above can be stored in the [colab \"Secrets\" panel](https://medium.com/@parthdasawant/how-to-use-secrets-in-google-colab-450c38e3ec75). Add the following strings as secrets:\n", + "\n", + " - Use `GOOGLE_PROJECT_ID` for the Cloud project id.\n", + " - Use `GOOGLE_API_KEY` for the Gemini API key\n", + "\n", + "## Caveats\n", + "\n", + " - THIS TOOL IS UNSAFE, AS IT AUTOMATICALLY RUNS LLM-GENERATED\n", + "PYTHON CODE! USE AT YOUR OWN RISK.\n", + "\n", + " - This is an early prototype, bugs and unexpected behavior are likely. Code improvements and refactors to follow.\n", + "\n", + " - Currently no spatial or temporal filtering of the datasets occurs as part of the dataset search funcionality. Filtering only happens based on semantic relevance. We hope to encorporate spatiotemporal filtering soon in a future version. Stay tuned.\n", + "\n", + " - The very lightweight use of the TextEmbedding API from VertexAI requires billing to be enabled in your Cloud project. It should be an extremely minimal expense. ([Details](https://cloud.google.com/vertex-ai/generative-ai/pricing)).\n", + "\n", + " - For assistance, please email scienceai_ee_dataset_search_agent@googlegroups.com." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "collapsed": true, + "executionInfo": { + "elapsed": 3192, + "status": "ok", + "timestamp": 1726764456509, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "XMTSCU9qKxgj", + "outputId": "9e1ad97d-e54a-4dd9-ac74-e3af7dce3a4a" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Install Python Libraries\n", + "\n", + "%%capture\n", + "!pip install google_cloud_aiplatform langchain-community langchain_google_genai langchain iso8601" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "collapsed": true, + "executionInfo": { + "elapsed": 6, + "status": "ok", + "timestamp": 1726764456509, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "iq7d8vqEKxgk", + "outputId": "e6007540-fcdb-48d3-fd26-001018c94927" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Imports\n", + "# Standard library imports\n", + "import base64\n", + "import contextlib\n", + "from concurrent import futures\n", + "from contextlib import redirect_stdout\n", + "import dataclasses\n", + "import datetime\n", + "import enum\n", + "import io\n", + "from io import BytesIO\n", + "import json\n", + "import logging\n", + "import math\n", + "import os\n", + "import re\n", + "import shutil\n", + "import sys\n", + "import threading\n", + "import time\n", + "import traceback\n", + "from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence\n", + "\n", + "# Third-party imports\n", + "import dateutil\n", + "import ee\n", + "import geemap\n", + "import google.ai.generativelanguage as glm\n", + "import google.api_core\n", + "from google.api_core import exceptions as google_exceptions\n", + "\n", + "from google.cloud import storage\n", + "from google.colab import output as notebook_output\n", + "from google.colab import userdata\n", + "import google.generativeai as genai\n", + "import IPython\n", + "from IPython.display import HTML, Javascript, display, clear_output\n", + "from ipyleaflet import LayerException\n", + "import ipywidgets as widgets\n", + "import iso8601\n", + "from jinja2 import Template\n", + "import langchain\n", + "from langchain.embeddings.base import Embeddings\n", + "from langchain_google_genai import ChatGoogleGenerativeAI\n", + "import numpy as np\n", + "import pandas as pd\n", + "from PIL import Image\n", + "import requests\n", + "import tenacity\n", + "from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type\n", + "import tqdm\n", + "import vertexai\n", + "from vertexai.preview.language_models import TextEmbeddingModel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 5419, + "status": "ok", + "timestamp": 1726764461923, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "58eOcl7YKLT2", + "outputId": "06b405c8-e359-4c90-907d-dc7e9850a618" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Setup\n", + "project_name = userdata.get('GOOGLE_PROJECT_ID')\n", + "vertex_ai_zone = \"us-central1\"\n", + "genai.configure(api_key=userdata.get('GOOGLE_API_KEY'))\n", + "\n", + "\n", + "ee.Authenticate()\n", + "ee.Initialize(project=project_name)\n", + "storage_client = storage.Client(project=project_name)\n", + "vertexai.init(project=project_name, location=vertex_ai_zone)\n", + "\n", + "# Make sure geemap initialized correctly.\n", + "geemap.common.ee_initialize(project=project_name)\n", + "Map = geemap.Map()\n", + "Map.add(\"layer_manager\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZU8LnyaAwG7s" + }, + "source": [ + "# Define classes for working with the Earth Engine data catalog\n", + "\n", + "These will soon be broken up into their own files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 4, + "status": "ok", + "timestamp": 1726764461923, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "6f0u0IX8vmRF", + "outputId": "0707b547-c093-45bd-ac86-5d5f64b99694" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Helper methods\n", + "def matches_interval(\n", + " collection_interval: tuple[datetime.datetime, datetime.datetime],\n", + " query_interval: tuple[datetime.datetime, datetime.datetime],\n", + "):\n", + " \"\"\"Checks if the collection's datetime interval matches the query datetime interval.\n", + "\n", + " Args:\n", + " collection_interval: Temporal interval of the collection.\n", + " query_interval: a tuple with the query interval start and end\n", + "\n", + " Returns:\n", + " True if the datetime interval matches\n", + " \"\"\"\n", + " start_query, end_query = query_interval\n", + " start_collection, end_collection = collection_interval\n", + " if end_collection is None:\n", + " # End date should always be set in STAC JSON files, but just in case...\n", + " end_collection = datetime.datetime.now(tz=datetime.UTC)\n", + " return end_query \u003e start_collection and start_query \u003c= end_collection\n", + "\n", + "\n", + "\n", + "def matches_datetime(\n", + " collection_interval: tuple[datetime.datetime, Optional[datetime.datetime]],\n", + " query_datetime: datetime.datetime,\n", + "):\n", + " \"\"\"Checks if the collection's datetime interval matches the query datetime.\n", + "\n", + " Args:\n", + " collection_interval: Temporal interval of the collection.\n", + " query_datetime: a datetime coming from a query\n", + "\n", + " Returns:\n", + " True if the datetime interval matches\n", + " \"\"\"\n", + " if collection_interval[1] is None:\n", + " # End date should always be set in STAC JSON files, but just in case...\n", + " end_date = datetime.datetime.now(tz=datetime.UTC)\n", + " else:\n", + " end_date = collection_interval[1]\n", + " return collection_interval[0] \u003c= query_datetime \u003c= end_date\n", + "\n", + "\n", + "@tenacity.retry(\n", + " stop=tenacity.stop_after_attempt(3),\n", + " wait=tenacity.wait_fixed(1),\n", + " retry=tenacity.retry_if_exception_type(LayerException),\n", + " # before_sleep=lambda retry_state: print(f\"LayerException occurred. Retrying in 1 seconds... (Attempt {retry_state.attempt_number}/3)\")\n", + ")\n", + "def run_ee_code(code: str, ee, geemap_instance: geemap.Map):\n", + " try:\n", + " # geemap appears to have some stray print statements.\n", + " _ = io.StringIO()\n", + " with redirect_stdout(_):\n", + " # Note that sometimes the geemap code uses both 'Map' and 'm' to refer to a map instance.\n", + " exec(code, {'ee': ee, 'Map': geemap_instance, 'm': geemap_instance})\n", + " except Exception:\n", + " # Re-raise the exception with the original traceback\n", + " exc_type, exc_value, exc_traceback = sys.exc_info()\n", + " raise exc_value.with_traceback(exc_traceback)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 282, + "status": "ok", + "timestamp": 1726764462201, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "3SG0ahqKJBYW", + "outputId": "7fe4e98b-e587-4731-e99e-fbad70b82cb3" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# @title class BBox()\n", + "@dataclasses.dataclass\n", + "class BBox:\n", + " \"\"\"Class representing a lat/lon bounding box.\"\"\"\n", + " west: float\n", + " south: float\n", + " east: float\n", + " north: float\n", + "\n", + " def is_global(self) -\u003e bool:\n", + " return (\n", + " self.west == -180 and self.south == -90 and\n", + " self.east == 180 and self.north == 90)\n", + "\n", + " @classmethod\n", + " def from_list(cls, bbox_list: list[float]):\n", + " \"\"\"Constructs a BBox from a list of four numbers [west,south,east,north].\"\"\"\n", + " if bbox_list[0] \u003e bbox_list[2]:\n", + " raise ValueError(\n", + " 'The smaller (west) coordinate must be listed first in a bounding box'\n", + " f' corner list. Found {bbox_list}'\n", + " )\n", + " if bbox_list[1] \u003e bbox_list[3]:\n", + " raise ValueError(\n", + " 'The smaller (south) coordinate must be listed first in a bounding'\n", + " f' box corner list. Found {bbox_list}'\n", + " )\n", + " return cls(bbox_list[0], bbox_list[1], bbox_list[2], bbox_list[3])\n", + "\n", + " def to_list(self) -\u003e list[float]:\n", + " return [self.west, self.south, self.east, self.north]\n", + "\n", + " def intersects(self, query_bbox) -\u003e bool:\n", + " \"\"\"Checks if this bbox intersects with the query bbox.\n", + "\n", + " Doesn't handle bboxes extending past the antimeridaian.\n", + "\n", + " Args:\n", + " query_bbox: Bounding box from the query.\n", + "\n", + " Returns:\n", + " True if the two bounding boxes intersect\n", + " \"\"\"\n", + " return (\n", + " query_bbox.west \u003c self.east\n", + " and query_bbox.east \u003e self.west\n", + " and query_bbox.south \u003c self.north\n", + " and query_bbox.north \u003e self.south\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 5, + "status": "ok", + "timestamp": 1726764462202, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "_CxF7Wz5wUZq", + "outputId": "00ccd0b8-ef4a-4dff-b6b6-3b88e23b1c8e" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# @title class Collection()\n", + "class Collection:\n", + " \"\"\"A simple wrapper for a STAC Collection..\"\"\"\n", + " stac_json: dict[str, Any]\n", + "\n", + " def __init__(self, stac_json: dict[str, Any]):\n", + " self.stac_json = stac_json\n", + " if stac_json.get('gee:status') == 'deprecated':\n", + " # Set the STAC 'deprecated' field that we don't set in the jsonnet files\n", + " stac_json['deprecated'] = True\n", + "\n", + " def __getitem__(self, item: str) -\u003e Any:\n", + " return self.stac_json[item]\n", + "\n", + " def get(self, item: str, default: Optional[Any] = None) -\u003e Optional[Any]:\n", + " \"\"\"Matches dict's get by returning None if there is no item.\"\"\"\n", + " return self.stac_json.get(item, default)\n", + "\n", + " def public_id(self) -\u003e str:\n", + " return self['id']\n", + "\n", + " def hyphen_id(self) -\u003e str:\n", + " return self['id'].replace('/', '_')\n", + "\n", + " def get_dataset_type(self) -\u003e str:\n", + " \"\"\"Could be Image, ImageCollection, FeatureCollection, Feature.\"\"\"\n", + " return self['gee:type']\n", + "\n", + " def is_deprecated(self) -\u003e bool:\n", + " \"\"\"Returns True for collections that are deprecated or have a successor.\"\"\"\n", + " if self.get('deprecated', False):\n", + " logging.info('Skipping deprecated collection: %s', self.public_id())\n", + " return True\n", + "\n", + " def datetime_interval(\n", + " self,\n", + " ) -\u003e Iterable[tuple[datetime.datetime, Optional[datetime.datetime]]]:\n", + " \"\"\"Returns datetime objects representing temporal extents.\"\"\"\n", + " for stac_interval in self.stac_json['extent']['temporal']['interval']:\n", + " if not stac_interval[0]:\n", + " raise ValueError(\n", + " 'Expected a non-empty temporal interval start for '\n", + " + self.public_id()\n", + " )\n", + " start_date = iso8601.parse_date(stac_interval[0])\n", + " if stac_interval[1] is not None:\n", + " end_date = iso8601.parse_date(stac_interval[1])\n", + " else:\n", + " end_date = None\n", + " yield (start_date, end_date)\n", + "\n", + " def start(self) -\u003e datetime.datetime:\n", + " return list(self.datetime_interval())[0][0]\n", + "\n", + " def start_str(self) -\u003e datetime.datetime:\n", + " if not self.start():\n", + " return ''\n", + " return self.start().strftime(\"%Y-%m-%d\")\n", + "\n", + " def end(self) -\u003e Optional[datetime.datetime]:\n", + " return list(self.datetime_interval())[0][1]\n", + "\n", + " def end_str(self) -\u003e Optional[datetime.datetime]:\n", + " if not self.end():\n", + " return ''\n", + " return self.end().strftime(\"%Y-%m-%d\")\n", + "\n", + " def bbox_list(self) -\u003e Sequence[BBox]:\n", + " if 'extent' not in self.stac_json:\n", + " # Assume global if nothing listed.\n", + " return (BBox(-180, -90, 180, 90),)\n", + " return tuple([\n", + " BBox.from_list(x)\n", + " for x in self.stac_json['extent']['spatial']['bbox']\n", + " ])\n", + "\n", + " def bands(self) -\u003e List[Dict]:\n", + " summaries = self.stac_json.get('summaries')\n", + " if not summaries:\n", + " return []\n", + " return summaries.get('eo:bands', [])\n", + "\n", + " def spatial_resolution_m(self) -\u003e float:\n", + " summaries = self.stac_json.get('summaries')\n", + " if not summaries:\n", + " return -1\n", + " if summaries.get('gsd'):\n", + " return summaries.get('gsd')[0]\n", + "\n", + " # Hacky fallback for cases where the stac does not follow convention.\n", + " gsd_lst = re.findall(r'\"gsd\": (\\d+)', json.dumps(self.stac_json))\n", + "\n", + " if len(gsd_lst) \u003e 0:\n", + " return float(gsd_lst[0])\n", + "\n", + " return -1\n", + "\n", + "\n", + " def temporal_resolution_str(self) -\u003e str:\n", + " interval_dict = self.stac_json.get('gee:interval')\n", + " if not interval_dict:\n", + " return \"\"\n", + " return f\"{interval_dict['interval']} {interval_dict['unit']}\"\n", + "\n", + "\n", + " def python_code(self)-\u003e str:\n", + " code = self.stac_json.get('code')\n", + " if not code:\n", + " return ''\n", + "\n", + " return code.get('py_code')\n", + "\n", + " def set_python_code(self, code: str):\n", + " if not code:\n", + " self.stac_json['code'] = {'js_code': '', 'py_code': code}\n", + "\n", + " self.stac_json['code']['py_code'] = code\n", + "\n", + " def set_js_code(self, code: str):\n", + " if not code:\n", + " return ''\n", + " js_code = self.stac_json.get('code').get('js_code')\n", + " self.stac_json['code'] = {'js_code': '', 'py_code': code}\n", + "\n", + " def image_preview_url(self):\n", + " for link in self.stac_json['links']:\n", + " if 'rel' in link and link['rel'] == 'preview' and link['type'] == 'image/png':\n", + " return link['href']\n", + " raise ValueError(f\"No preview image found for {id}\")\n", + "\n", + "\n", + " def catalog_url(self):\n", + " links = self.stac_json['links']\n", + " for link in links:\n", + " if 'rel' in link and link['rel'] == 'catalog':\n", + " return link['href']\n", + "\n", + " # Ideally there would be a 'catalog' link but sometimes there isn't.\n", + " base_url = \"https://developers.google.com/earth-engine/datasets/catalog/\"\n", + " if link['href'].startswith(base_url):\n", + " return link['href'].split('#')[0]\n", + "\n", + " logging.warning(f\"No catalog link found for {self.public_id()}\")\n", + " return \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 4, + "status": "ok", + "timestamp": 1726764462202, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "eJVB0cDYweD5", + "outputId": "d1d6bdd6-6877-4727-aa0c-6fe4f8d36ad6" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# @title class CollectionList()\n", + "class CollectionList(Sequence[Collection]):\n", + " \"\"\"List of stac.Collections; can be filtered to return a smaller sublist.\"\"\"\n", + "\n", + " _collections = Sequence[Collection]\n", + "\n", + " def __init__(self, collections: Sequence[Collection]):\n", + " self._collections = tuple(collections)\n", + "\n", + " def __iter__(self):\n", + " return iter(self._collections)\n", + "\n", + " def __getitem__(self, index):\n", + " return self._collections[index]\n", + "\n", + " def __len__(self):\n", + " return len(self._collections)\n", + "\n", + " def __eq__(self, other: object) -\u003e bool:\n", + " if isinstance(other, CollectionList):\n", + " return self._collections == other._collections\n", + " return False\n", + "\n", + " def __hash__(self) -\u003e int:\n", + " return hash(self._collections)\n", + "\n", + " def filter_by_ids(self, ids: Iterable[str]):\n", + " \"\"\"Returns a sublist with only the collections matching the given ids.\"\"\"\n", + " return self.__class__(\n", + " [c for c in self._collections if c.public_id() in ids]\n", + " )\n", + "\n", + " def filter_by_datetime(\n", + " self,\n", + " query_datetime: datetime.datetime,\n", + " ):\n", + " \"\"\"Returns a sublist with the time interval matching the given time.\"\"\"\n", + " result = []\n", + " for collection in self._collections:\n", + " for datetime_interval in collection.datetime_interval():\n", + " if matches_datetime(datetime_interval, query_datetime):\n", + " result.append(collection)\n", + " break\n", + " return self.__class__(result)\n", + "\n", + " def filter_by_interval(\n", + " self,\n", + " query_interval: tuple[datetime.datetime, datetime.datetime],\n", + " ):\n", + " \"\"\"Returns a sublist with the time interval matching the given interval.\"\"\"\n", + " result = []\n", + " for collection in self._collections:\n", + " for datetime_interval in collection.datetime_interval():\n", + " if matches_interval(datetime_interval, query_interval):\n", + " result.append(collection)\n", + " break\n", + " return self.__class__(result)\n", + "\n", + " def filter_by_bounding_box_list(\n", + " self, query_bbox: BBox):\n", + " \"\"\"Returns a sublist with the bbox matching the given bbox.\"\"\"\n", + " result = []\n", + " for collection in self._collections:\n", + " for collection_bbox in collection.bbox_list():\n", + " if collection_bbox.intersects(query_bbox):\n", + " result.append(collection)\n", + " break\n", + " return self.__class__(result)\n", + "\n", + " def filter_by_bounding_box(\n", + " self, query_bbox: BBox):\n", + " \"\"\"Returns a sublist with the bbox matching the given bbox.\"\"\"\n", + " result = []\n", + " for collection in self._collections:\n", + " for collection_bbox in collection.bbox_list():\n", + " if collection_bbox.intersects(query_bbox):\n", + " result.append(collection)\n", + " break\n", + " return self.__class__(result)\n", + "\n", + "\n", + " def start_str(self) -\u003e datetime.datetime:\n", + " return self.start().strftime(\"%Y-%m-%d\")\n", + "\n", + "\n", + " def sort_by_spatial_resolution(self, reverse=False):\n", + " \"\"\"\n", + " Sorts the collections based on their spatial resolution.\n", + " Collections with spatial_resolution_m() == -1 are pushed to the end.\n", + "\n", + " Args:\n", + " reverse (bool): If True, sort in descending order (highest resolution first).\n", + " If False (default), sort in ascending order (lowest resolution first).\n", + "\n", + " Returns:\n", + " CollectionList: A new CollectionList instance with sorted collections.\n", + " \"\"\"\n", + " def sort_key(collection):\n", + " resolution = collection.spatial_resolution_m()\n", + " if resolution == -1:\n", + " return float('inf') if not reverse else float('-inf')\n", + " return resolution\n", + "\n", + " sorted_collections = sorted(\n", + " self._collections,\n", + " key=sort_key,\n", + " reverse=reverse\n", + " )\n", + " return self.__class__(sorted_collections)\n", + "\n", + "\n", + " def limit(self, n: int):\n", + " \"\"\"\n", + " Returns a new CollectionList containing the first n entries.\n", + "\n", + " Args:\n", + " n (int): The number of entries to include in the new list.\n", + "\n", + " Returns:\n", + " CollectionList: A new CollectionList instance with at most n collections.\n", + " \"\"\"\n", + " return self.__class__(self._collections[:n])\n", + "\n", + "\n", + " def to_df(self):\n", + " \"\"\"Converts a collection list to a dataframe with a select set of fields.\"\"\"\n", + "\n", + " rows = []\n", + " for col in self._collections:\n", + " # Remove text in parens in dataset name.\n", + " short_title = re.sub(r'\\([^)]*\\)', '', col.get('title')).strip()\n", + "\n", + " row = {\n", + " 'id': col.public_id(),\n", + " 'name': short_title,\n", + " 'temp_res': col.temporal_resolution_str(),\n", + " 'spatial_res_m': col.spatial_resolution_m(),\n", + " 'earliest': col.start_str(),\n", + " 'latest': col.end_str(),\n", + " 'url': col.catalog_url()\n", + " }\n", + " rows.append(row)\n", + " return pd.DataFrame(rows)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 4, + "status": "ok", + "timestamp": 1726764462202, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "Kp39kZxDwlR3", + "outputId": "81c5788f-0284-4e27-8bbe-365a099194ff" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title class Catalog()\n", + "class Catalog:\n", + " \"\"\"Class containing all collections in the EE STAC catalog.\"\"\"\n", + "\n", + " collections: CollectionList\n", + "\n", + " def __init__(self, storage_client: storage.Client):\n", + " self.collections = CollectionList(self._load_collections(storage_client))\n", + "\n", + " def get_collection(self, id: str) -\u003e Collection:\n", + " \"\"\"Returns the collection with the given id.\"\"\"\n", + " col = self.collections.filter_by_ids([id])\n", + " if len(col) == 0:\n", + " raise ValueError(f'No collection with id {id}')\n", + " return col[0]\n", + "\n", + "\n", + " @tenacity.retry(\n", + " stop=tenacity.stop_after_attempt(5),\n", + " wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),\n", + " retry=tenacity.retry_if_exception_type((\n", + " google_exceptions.GoogleAPICallError,\n", + " google_exceptions.RetryError,\n", + " ConnectionError\n", + " )),\n", + " before_sleep=lambda retry_state: print(\n", + " f\"Error occurred: {str(retry_state.outcome.exception())}\\n\"\n", + " f\"Retrying in {retry_state.next_action.sleep} seconds... \"\n", + " f\"(Attempt {retry_state.attempt_number}/3)\"\n", + " )\n", + " )\n", + " def _read_file(self, file_blob: google.cloud.storage.blob.Blob) -\u003e Collection:\n", + " \"\"\"Reads the contents of a file from the specified bucket.\"\"\"\n", + " file_contents = file_blob.download_as_string().decode()\n", + " return Collection(json.loads(file_contents))\n", + "\n", + " def _read_files(\n", + " self, file_blobs: list[google.cloud.storage.blob.Blob]\n", + " ) -\u003e list[Collection]:\n", + " \"\"\"Processes files in parallel.\"\"\"\n", + " collections = []\n", + " with futures.ThreadPoolExecutor(max_workers=10) as executor:\n", + " file_futures = [\n", + " executor.submit(self._read_file, file_blob)\n", + " for file_blob in file_blobs\n", + " ]\n", + " for future in file_futures:\n", + " collections.append(future.result())\n", + " return collections\n", + "\n", + " def _load_collections(\n", + " self, storage_client: storage.Client\n", + " ) -\u003e Sequence[Collection]:\n", + " \"\"\"Loads all EE STAC JSON files from GCS, with datetimes as objects.\"\"\"\n", + " bucket = storage_client.get_bucket('earthengine-stac')\n", + " files = [\n", + " x\n", + " for x in bucket.list_blobs(prefix='catalog/')\n", + " if x.name.endswith('.json')\n", + " and not x.name.endswith('/catalog.json')\n", + " and not x.name.endswith('/units.json')\n", + " ]\n", + " logging.warning('Found %d files, loading...', len(files))\n", + " collections = self._read_files(files)\n", + "\n", + " code_samples_dict = self._load_all_code_samples(storage_client)\n", + "\n", + " res = []\n", + " for c in collections:\n", + " if c.is_deprecated():\n", + " continue\n", + " c.stac_json['code'] = code_samples_dict.get(c.hyphen_id())\n", + " res.append(c)\n", + " logging.warning(\n", + " 'Loaded %d collections (skipping deprecated ones)', len(res)\n", + " )\n", + " # Returning a tuple for immutability.\n", + " return tuple(res)\n", + "\n", + " def _load_all_code_samples(self, storage_client: storage.Client):\n", + " \"\"\"Loads js + py example scripts from GCS into dict keyed by dataset ID.\"\"\"\n", + "\n", + " # Get json file from GCS bucket\n", + " # 'gs://earthengine-catalog/catalog/example_scripts.json'\n", + " bucket = storage_client.get_bucket('earthengine-catalog')\n", + " blob= bucket.blob('catalog/example_scripts.json')\n", + " file_contents = blob.download_as_string().decode()\n", + " data = json.loads(file_contents)\n", + "\n", + " # Flatten json to get a map from ID (using '_' rather than '/') to code\n", + " # sample.\n", + " all_datasets_by_provider = data[0]['contents']\n", + " code_samples_dict = {}\n", + " for provider in all_datasets_by_provider:\n", + " for dataset in provider['contents']:\n", + " js_code = dataset['code']\n", + " py_code = self._make_python_code_sample(js_code)\n", + "\n", + " code_samples_dict[dataset['name']] = {\n", + " 'js_code': js_code, 'py_code': py_code}\n", + "\n", + " return code_samples_dict\n", + "\n", + " def _make_python_code_sample(self, js_code: str) -\u003e str:\n", + " \"\"\"Converts EE JS code into python.\"\"\"\n", + "\n", + " # geemap appears to have some stray print statements.\n", + " _ = io.StringIO()\n", + " with redirect_stdout(_):\n", + " code_list = geemap.js_snippet_to_py(js_code,\n", + " add_new_cell=False,\n", + " import_ee=False,\n", + " import_geemap=False,\n", + " show_map=False)\n", + " return ''.join(code_list)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gcCwKks05JzB" + }, + "source": [ + "## Test catalog/collection functions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 52 + }, + "executionInfo": { + "elapsed": 7695, + "status": "ok", + "timestamp": 1726764469893, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "77aNEPVKT5hH", + "outputId": "90aa894b-8f72-43b3-a2c7-1137dc2d5e49" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:Found 1010 files, loading...\n", + "WARNING:root:Loaded 812 collections (skipping deprecated ones)\n" + ] + } + ], + "source": [ + "catalog = Catalog(storage_client)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "referenced_widgets": [ + "e4bdead8177e4194b2a596e4bdb0e702", + "c72f776d1fb7468fbc6ec43c6f8e8589", + "d62a2703f511443bae48e400789ccc17", + "8b4ed19598a440e28858c4838ed06043", + "9e55d50ede274e5f8912456ba66a9f3c", + "69056968f897477a8eddbeb93dd93a0c", + "91db0cd102fe48a2bb2e342df27cba21", + "41888a7d97a74a0a9c83758558da3d50", + "e17aa2b98edf45258b81c98967c343f7", + "efee73fcb86641dd97521a9613e4ec3b", + "484af18253bc4438b910df2e25ef1636", + "ce5c7fdecc3c4a66ba5ea0e0959df55d", + "05f810526f824882b8f6a6d9e87c70c3", + "289bd0d46b6543fbb94ccf9fb4e240ab", + "a9bd2e2d9d964571beaeb4c42dd380ad", + "4b8d109eca114cf79c40f143ba9cadfa", + "6b61efb60b4247188672bed869e5567a", + "093c447234294ebb83ad77752b9eaedb", + "605579b93580438fb8d8ab8446e92102", + "1f8230160137489e9d39b90ba91d5660", + "4b7fdc5b73f444fcbd9b44857cab2901", + "ca9f1248e7ba40bebf1573ce6c1bfce1", + "1648ad8722384dcca46bef180adc2f9b", + "99ab765ac7a4410ebc720148a51f1237", + "05274124a41c4818ace6c112cd41867b", + "cd90851ff057475d9a30c6315c472c46", + "2cbf7df517234a099ed6956b46c72418", + "0869a3d3f40d4acb86afd6d9fc16ab13", + "3c5e8ee2df144c04b30f86c168f90881", + "be24aeccc8be4ade93089e9e0eb33d60", + "d2cca746e93a431db3d224a078831c1e", + "53bcb0f0d6914f1abee9c868635adf58", + "d9314a20fce24115b582869711c1a9d1", + "b8f5d1e37ec6449c8a6a24940281d7be", + "3f72199de3964a349b4b203db8b7e3bf", + "c49e780d28634403b2018af1a5a9a7d8", + "48a45bb252e2421d9a05af80bd924ca3", + "35a5db296fd346e5b1f04b1a197c0c14", + "7e3c7ae2669b4ca481e283d5fec3edf8", + "fb1baab4285b4257aed08ef1b8652a06", + "2c5134e78d6e49708a0ca2783baff67a", + "b988c1196e444aab8826a65c9f7fc993", + "390812fca5c243c592c413232fc66130", + "9263af8c9dfb4313b7d059c3c6962cf3", + "cd78b347fcfc461db163f7bae3f9e0d1", + "e09d44ea1cf04cc5b348d05490131da4", + "fc50eff9c06745dd94226a8a7dfc6f12", + "40b61cd635ce410298f5f7563b1847f1", + "c62443fc603b4b09adbdf78b4f352ac7", + "1e10f5b83d2c4d98923f517eb96f2f64", + "d5d6a96d9fc8446186a554463f0a341b", + "a399e62d922b4c30abdb07b2410bcefb", + "1cda5f0356b744d8848b650af8602214", + "0518a7547e38472c827c8cce1abd1931", + "049914303e8643fc89a8bdb664f72ef2", + "e736182e719e41ba9b60d36b971209d4", + "6f98462715a34fa08b3e1f6318f368dd", + "40bbd240a0af4ca3a5af0088c4db46e8", + "610b36d37e1b49978783e75946c0ac3d", + "a46458b4870f42c29b5ebfc26f302406", + "6de5958ce3d947978533fb4dbd2c9209", + "4c9793b7e69846df96a5e82f6f9416a3", + "a5798a3b868b45deadf639a28a5b4b7d", + "dba5490b869c40879ba78d7857c03308", + "69e499f5ac114f10973d83dbfd447061", + "9c4d1521e6a04fb2a25e4558d8407995", + "49146807cb7b4ba2a1712682197a7af3", + "2bbd628f6b384453b31b30249d41d344", + "87120141586342169741606b9f119d6c", + "97f002c039c74ede92f2dc18603faaac", + "aaa004b3510241b2b01677e69edd0e33", + "04f828f641af431eaa0f3bcaa83a28a2", + "c02235857dc3409e9b12c25b0d1bd6d3", + "1b3abdcd036f4cb48db03347ef06fc15", + "d99d576d639a44989a9f918f851cfcf1" + ] + }, + "executionInfo": { + "elapsed": 3037, + "status": "ok", + "timestamp": 1726764472927, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "t_7EfGV9Dl6Q", + "outputId": "3a872eaa-cdee-4daa-fa49-d861ed421cf4" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e4bdead8177e4194b2a596e4bdb0e702", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[36.2841, -112.8598], controls=(WidgetControl(options=['position', 'transparent_bg'], widget=Search…" + ] + }, + "metadata": { + "application/vnd.jupyter.widget-view+json": { + "colab": { + "custom_widget_manager": { + "url": "https://ssl.gstatic.com/colaboratory-static/widgets/colab-cdn-widget-manager/2b70e893a8ba7c0f/manager.min.js" + } + } + } + }, + "output_type": "display_data" + } + ], + "source": [ + "col = catalog.get_collection('CGIAR/SRTM90_V4')\n", + "Map = geemap.Map()\n", + "exec(col.python_code(), {'ee': ee, 'Map': Map, 'm': Map})\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 112 + }, + "executionInfo": { + "elapsed": 326, + "status": "ok", + "timestamp": 1726764473251, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "7D0D-JsNXH-V", + "outputId": "26fcb2ed-ca05-400f-d6b7-3eadd2b1b050" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\u003ctable border=\"1\" class=\"dataframe\"\u003e\n", + " \u003cthead\u003e\n", + " \u003ctr style=\"text-align: right;\"\u003e\n", + " \u003cth\u003e\u003c/th\u003e\n", + " \u003cth\u003eid\u003c/th\u003e\n", + " \u003cth\u003ename\u003c/th\u003e\n", + " \u003cth\u003etemp_res\u003c/th\u003e\n", + " \u003cth\u003espatial_res_m\u003c/th\u003e\n", + " \u003cth\u003eearliest\u003c/th\u003e\n", + " \u003cth\u003elatest\u003c/th\u003e\n", + " \u003cth\u003eurl\u003c/th\u003e\n", + " \u003c/tr\u003e\n", + " \u003c/thead\u003e\n", + " \u003ctbody\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e0\u003c/th\u003e\n", + " \u003ctd\u003eCGIAR/SRTM90_V4\u003c/td\u003e\n", + " \u003ctd\u003eSRTM Digital Elevation Data Version 4\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e90.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-11\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e\u003ca href=\"https://developers.google.com/earth-engine/datasets/catalog/CGIAR_SRTM90_V4\" target=\"_blank\"\u003ehttps://developers.google.com/earth-engine/datasets/catalog/CGIAR_SRTM90_V4\u003c/a\u003e\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e1\u003c/th\u003e\n", + " \u003ctd\u003eCIESIN/GPWv411/GPW_Land_Area\u003c/td\u003e\n", + " \u003ctd\u003eGPWv411: Land Area\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e927.67\u003c/td\u003e\n", + " \u003ctd\u003e2000-01-01\u003c/td\u003e\n", + " \u003ctd\u003e2020-01-01\u003c/td\u003e\n", + " \u003ctd\u003e\u003ca href=\"https://developers.google.com/earth-engine/datasets/catalog/CIESIN_GPWv411_GPW_Land_Area\" target=\"_blank\"\u003ehttps://developers.google.com/earth-engine/datasets/catalog/CIESIN_GPWv411_GPW_Land_Area\u003c/a\u003e\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003c/tbody\u003e\n", + "\u003c/table\u003e" + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "col_list = catalog.collections.filter_by_ids(['CGIAR/SRTM90_V4', 'CIESIN/GPWv411/GPW_Land_Area'])\n", + "col_list\n", + "df = col_list.to_df()\n", + "HTML(df.to_html(render_links=True, escape=False))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "p8KqtixdQUMb" + }, + "source": [ + "# Dataset Search Logic\n", + "\n", + "We load some pre-generated, per-dataset embeddings into a [vector store](https://cloud.google.com/discover/what-is-a-vector-database) as the backbone to our dataset search tool.\n", + "\n", + "This tool can either be leveraged on its own, or invovked by an LLM \"agent\" as demonstrated later on in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 4, + "status": "ok", + "timestamp": 1726764473251, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "Z121WerP5kI-", + "outputId": "00c84f70-e95e-42b0-faf2-bc64bf722c16" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# @title Embeddings existing location\n", + "\n", + "# Pre-built embeddings.\n", + "EMBEDDINGS_CLOUD_PATH = 'gs://earthengine-catalog/embeddings/catalog_embeddings.jsonl'\n", + "\n", + "# Copy embeddings from GCS bucket to a local file\n", + "EMBEDDINGS_LOCAL_PATH = 'catalog_embeddings.jsonl'\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 231, + "status": "ok", + "timestamp": 1726764473479, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "_QgDA889bIQD", + "outputId": "82889b73-5ad0-44eb-d073-e9289978707e" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Embeddings classes and helper methods\n", + "from langchain.embeddings.base import Embeddings\n", + "from langchain.indexes import VectorstoreIndexCreator\n", + "from langchain.schema import Document\n", + "from langchain_core.vectorstores.base import VectorStore\n", + "from langchain.indexes.vectorstore import VectorStoreIndexWrapper\n", + "from langchain_core.language_models.base import BaseLanguageModel\n", + "import numpy as np\n", + "\n", + "\n", + "class PrecomputedEmbeddings(Embeddings):\n", + " def __init__(self, embeddings_dict):\n", + " self.embeddings_dict = embeddings_dict\n", + " self.model = TextEmbeddingModel.from_pretrained(\"google/text-embedding-004\")\n", + "\n", + " def embed_documents(self, texts):\n", + " return [self.embeddings_dict[text] for text in texts]\n", + "\n", + " def embed_query(self, text):\n", + " embeddings = self.model.get_embeddings([text])\n", + " return embeddings[0].values\n", + "\n", + "\n", + "def load_embeddings(gcs_path=EMBEDDINGS_CLOUD_PATH, local_path=EMBEDDINGS_LOCAL_PATH):\n", + " parts = gcs_path.split('/')\n", + " bucket_name = parts[2]\n", + " blob_path = '/'.join(parts[3:])\n", + " bucket = storage_client.get_bucket(bucket_name)\n", + " blob = bucket.blob(blob_path)\n", + " blob.download_to_filename(local_path)\n", + " return local_path\n", + "\n", + "def make_langchain_index(embeddings_df: pd.DataFrame) -\u003e VectorStoreIndexWrapper:\n", + " \"\"\"Creates an index from a dataframe of precomputed embeddings.\"\"\"\n", + " # Create a dictionary mapping texts to their embeddings\n", + " embeddings_dict = dict(zip(embeddings_df['id'], embeddings_df['embedding']))\n", + "\n", + " # Create our custom embeddings class\n", + " precomputed_embeddings = PrecomputedEmbeddings(embeddings_dict)\n", + "\n", + " # Create Langchain Document objects\n", + " documents = []\n", + " for index, row in embeddings_df.iterrows():\n", + " page_content = row['id']\n", + " metadata = {'summary': row['summary'], 'name': row['name']}\n", + " documents.append(Document(page_content=page_content, metadata=metadata))\n", + "\n", + "\n", + " # Create the VectorstoreIndexCreator\n", + " index_creator = VectorstoreIndexCreator(\n", + " embedding=precomputed_embeddings\n", + " )\n", + "\n", + " # Create the index\n", + " return index_creator.from_documents(documents)\n", + "\n", + "# Wrap Langchain embeddings in our own EE dataset wrapper\n", + "class EarthEngineDatasetIndex():\n", + " index: VectorStoreIndexWrapper\n", + " vectorstore: VectorStore\n", + " data_catalog: Catalog\n", + " llm: BaseLanguageModel\n", + "\n", + " def __init__(self, data_catalog, index, llm):\n", + " self.index = index\n", + " self.data_catalog = data_catalog\n", + " self.vectorstore = index.vectorstore\n", + " self.llm = llm\n", + "\n", + "\n", + " @tenacity.retry(\n", + " stop=stop_after_attempt(3),\n", + " wait=wait_exponential(multiplier=1, min=4, max=10),\n", + " retry=retry_if_exception_type((requests.exceptions.RequestException, ConnectionError))\n", + " )\n", + " def find_top_matches(\n", + " self,\n", + " query: str,\n", + " results: int = 10,\n", + " threshold: float = 0.7,\n", + " bounding_box: Optional[list[float]] = None,\n", + " temporal_interval: tuple[datetime.datetime, datetime.datetime] = None) -\u003e CollectionList:\n", + " \"\"\"\n", + " Retrieve relevant dataset from the Earth Engine data catalog.\n", + "\n", + " query: str. The kind of data being searched for. ie 'population'.\n", + " results: int. The number of datasets to return. 4 is recommended.\n", + " threshold: float. The maximum dot product between the query and catalog\n", + " embeddings. Recommended 0.7.\n", + " bounding_box: Optional[list[float]]. The spatial bounding box for the query, in the\n", + " format [lon1, lat1, lon2, lon2]. If None then no spatial filter is appled.\n", + " temporal: Optional[list[Optional[list[int]]]]. If provided, temporal\n", + " constraints are provided as a list of two int lists following the structure\n", + " [[year, month, day], [year, month, day]]. A none can be used to set no\n", + " start or end date. For example [None, [2022,12,31]] will return all datasets\n", + " that have data before 2022-12-31.)\n", + " \"\"\"\n", + " similar_docs = self.index.vectorstore.similarity_search_with_score(query, llm=self.llm, k=results)\n", + " dataset_ids = [doc[0].page_content for doc in similar_docs]\n", + " datasets = self.data_catalog.collections.filter_by_ids(dataset_ids)\n", + " return datasets\n", + "\n", + " @tenacity.retry(\n", + " stop=stop_after_attempt(3),\n", + " wait=wait_exponential(multiplier=1, min=4, max=10),\n", + " retry=retry_if_exception_type((requests.exceptions.RequestException, ConnectionError))\n", + " )\n", + " def find_top_matches_with_score_df(self,\n", + " query: str,\n", + " results: int = 20,\n", + " threshold: float = 0.7,\n", + " bounding_box: Optional[list[float]] = None,\n", + " temporal_interval: tuple[datetime.datetime, datetime.datetime] = None):\n", + " scores_df = self.ids_to_match_scores_df(query, results, bounding_box, temporal_interval)\n", + " dataset_ids = scores_df['id'].tolist()\n", + " col_list = self.data_catalog.collections.filter_by_ids(dataset_ids)\n", + " collection_df = col_list.to_df()\n", + " df = pd.merge(collection_df, scores_df, on='id', how='inner')\n", + " return df.sort_values(by='match_score', ascending=False)\n", + "\n", + " def ids_to_match_scores_df(self, query, results,\n", + " bounding_box: Optional[list[float]] = None,\n", + " temporal_interval: tuple[datetime.datetime, datetime.datetime] = None):\n", + "\n", + " similar_docs = self.index.vectorstore.similarity_search_with_score(query, llm=self.llm, k=results)\n", + " dataset_ids, scores = zip(*[(doc[0].page_content, doc[1]) for doc in similar_docs])\n", + "\n", + " return pd.DataFrame({\n", + " 'id': dataset_ids,\n", + " 'match_score': scores\n", + " })\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rWWfeMwO4lnC" + }, + "source": [ + "## Test dataset search" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 1403, + "status": "ok", + "timestamp": 1726764474880, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "rcTxCYsvaj1V", + "outputId": "1fc67305-bd97-4133-d449-18b9dc6aa049" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"embeddings_df\",\n \"rows\": 867,\n \"fields\": [\n {\n \"column\": \"id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 778,\n \"samples\": [\n \"Oxford/MAP/accessibility_to_healthcare_2019\",\n \"OpenLandMap/SOL/SOL_WATERCONTENT-33KPA_USDA-4B1C_M/v01\",\n \"NOAA/VIIRS/001/VNP46A1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 770,\n \"samples\": [\n \"USFS Tree Canopy Cover v2021-4 (CONUS and OCONUS)\",\n \"DEA Land Cover 1.0.0\",\n \"WWF HydroSHEDS Drainage Direction, 30 Arc-Seconds\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"summary\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 867,\n \"samples\": [\n \"The MODIS MCD15A3H V6.1 product offers 4-day composite data of Leaf Area Index (LAI) and Fraction of Photosynthetically Active Radiation (FPAR) at a 500-meter resolution, utilizing the best pixel available from both the Terra and Aqua satellites within each 4-day period. \\n\\n\\n\\\"Fpar\\\" represents FPAR absorbed by the green elements of a vegetation canopy\\n\\\"Lai\\\" represents One-sided green leaf area per unit ground area in\\nbroadleaf canopies; one-half the total needle surface area per\\nunit ground area in coniferous canopies\\n\\n\\\"FparLai_QC\\\" represents Quality for Lai and Fpar\\n\\\"FparExtra_QC\\\" represents Extra detail quality for LAI and FPAR\\n\\\"FparStdDev\\\" represents Standard deviation of Fpar\\n\\\"LaiStdDev\\\" represents Standard deviation for Lai\\n\",\n \"MYD21C3 offers monthly average land surface temperatures derived from MODIS. It averages cloud-free day and night readings from the MYD21A1 product over an 8-day period, providing LST, quality, viewing angle, and emissivity data. \\n\\n\\n\\\"Count_Day\\\" represents Count of Daytime Input Values\\n\\\"Count_Night\\\" represents Count of Nighttime Input Values\\n\\\"QC_Day\\\" represents Quality Control for Daytime LST and Emissivity\\n\\\"QC_Night\\\" represents Quality Control for Nighttime LST and Emissivity\\n\\\"LST_Day\\\" represents Average Daytime Land Surface Temperature\\n\\\"LST_Night\\\" represents Average Nighttime Land Surface Temperature\\n\\\"LST_Day_err\\\" represents Root-mean-square-error Daytime Land Surface Temperature\\n\\\"LST_Night_err\\\" represents Average Nighttime Land Surface Temperature\\n\\\"Day_view_angle\\\" represents Average Daytime View Zenith Angle\\n\\\"Night_view_angle\\\" represents Average Nighttime View Zenith Angle\\n\\\"Day_view_time\\\" represents Average Daytime View Time (UTC)\\n\\\"Night_view_time\\\" represents Average Nighttime View Time (UTC)\\n\\\"Emis_29_Day\\\" represents Average Daytime Band 29 Emissivity\\n\\\"Emis_29_Night\\\" represents Average Nighttime Band 29 Emissivity\\n\\\"Emis_29_Day_err\\\" represents Root-mean-square-error Daytime Band 29 Emissivity\\n\\\"Emis_29_Night_err\\\" represents Root-mean-square-error Nighttime Band 29 Emissivity\\n\\\"Emis_31_Day\\\" represents Average Daytime Band 31 Emissivity\\n\\\"Emis_31_Night\\\" represents Average Nighttime Band 31 Emissivity\\n\\\"Emis_31_Day_err\\\" represents Root-mean-square-error Daytime Band 31 Emissivity\\n\\\"Emis_31_Night_err\\\" represents Root-mean-square-error Nighttime Band 31 Emissivity\\n\\\"Emis_32_Day\\\" represents Average Daytime Band 32 Emissivity\\n\\\"Emis_32_Night\\\" represents Average Nighttime Band 32 Emissivity\\n\\\"Emis_32_Day_err\\\" represents Root-mean-square-error Daytime Band 32 Emissivity\\n\\\"Emis_32_Night_err\\\" represents Root-mean-square-error Nighttime Band 32 Emissivity\\n\\\"View_Angle\\\" represents MODIS view zenith angle\\n\\\"Percent_land_in_grid\\\" represents Percent of Land Detections in Grid Cell\\n\\\"Clear_sky_days\\\" represents Bitmap of Clear Sky Days (1 = clear, LSB = 1st day)\\n\\\"Clear_sky_nights\\\" represents Bitmap of Clear Sky Nights (1 = clear, LSB = 1st day)\\n\",\n \"The Physiographic Diversity dataset uses elevation data and landform types to identify areas where landform patterns are **most resistant to change** under climate change. This information helps guide climate adaptation planning by highlighting stable areas for conservation and resource management. \\n\\n\\n\\\"b1\\\" represents Physiographic diversity\\n\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"embedding\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe", + "variable_name": "embeddings_df" + }, + "text/html": [ + "\n", + " \u003cdiv id=\"df-49031713-07ff-4c22-9fba-d257afa0f340\" class=\"colab-df-container\"\u003e\n", + " \u003cdiv\u003e\n", + "\u003cstyle scoped\u003e\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "\u003c/style\u003e\n", + "\u003ctable border=\"1\" class=\"dataframe\"\u003e\n", + " \u003cthead\u003e\n", + " \u003ctr style=\"text-align: right;\"\u003e\n", + " \u003cth\u003e\u003c/th\u003e\n", + " \u003cth\u003eid\u003c/th\u003e\n", + " \u003cth\u003ename\u003c/th\u003e\n", + " \u003cth\u003esummary\u003c/th\u003e\n", + " \u003cth\u003eembedding\u003c/th\u003e\n", + " \u003c/tr\u003e\n", + " \u003c/thead\u003e\n", + " \u003ctbody\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e0\u003c/th\u003e\n", + " \u003ctd\u003eAAFC/ACI\u003c/td\u003e\n", + " \u003ctd\u003eCanada AAFC Annual Crop Inventory\u003c/td\u003e\n", + " \u003ctd\u003eAgriculture and Agri-Food Canada annually maps...\u003c/td\u003e\n", + " \u003ctd\u003e[-0.0205919258, 0.0099804802, -0.0294760894000...\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e1\u003c/th\u003e\n", + " \u003ctd\u003eACA/reef_habitat/v2_0\u003c/td\u003e\n", + " \u003ctd\u003eAllen Coral Atlas (ACA) - Geomorphic Zonation ...\u003c/td\u003e\n", + " \u003ctd\u003eThe Allen Coral Atlas is a global, high-resolu...\u003c/td\u003e\n", + " \u003ctd\u003e[0.0040304777, 0.0573143177, -0.0509719588, 0....\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e2\u003c/th\u003e\n", + " \u003ctd\u003eAHN/AHN2_05M_INT\u003c/td\u003e\n", + " \u003ctd\u003eAHN Netherlands 0.5m DEM, Interpolated\u003c/td\u003e\n", + " \u003ctd\u003eThe AHN DEM is a detailed (0.5m resolution) el...\u003c/td\u003e\n", + " \u003ctd\u003e[-0.0115319202, -0.06319545210000001, -0.03415...\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e3\u003c/th\u003e\n", + " \u003ctd\u003eAHN/AHN2_05M_NON\u003c/td\u003e\n", + " \u003ctd\u003eAHN Netherlands 0.5m DEM, Non-Interpolated\u003c/td\u003e\n", + " \u003ctd\u003eThe AHN DEM is a high-resolution (0.5m) model ...\u003c/td\u003e\n", + " \u003ctd\u003e[0.0082498919, -0.0807547569, -0.0562631935, 0...\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e4\u003c/th\u003e\n", + " \u003ctd\u003eAHN/AHN2_05M_RUW\u003c/td\u003e\n", + " \u003ctd\u003eAHN Netherlands 0.5m DEM, Raw Samples\u003c/td\u003e\n", + " \u003ctd\u003eThe AHN DEM is a highly detailed (0.5m resolut...\u003c/td\u003e\n", + " \u003ctd\u003e[-0.0056484384000000006, -0.0696996748, -0.052...\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003c/tbody\u003e\n", + "\u003c/table\u003e\n", + "\u003c/div\u003e\n", + " \u003cdiv class=\"colab-df-buttons\"\u003e\n", + "\n", + " \u003cdiv class=\"colab-df-container\"\u003e\n", + " \u003cbutton class=\"colab-df-convert\" onclick=\"convertToInteractive('df-49031713-07ff-4c22-9fba-d257afa0f340')\"\n", + " title=\"Convert this dataframe to an interactive table.\"\n", + " style=\"display:none;\"\u003e\n", + "\n", + " \u003csvg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\"\u003e\n", + " \u003cpath d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/\u003e\n", + " \u003c/svg\u003e\n", + " \u003c/button\u003e\n", + "\n", + " \u003cstyle\u003e\n", + " .colab-df-container {\n", + " display:flex;\n", + " gap: 12px;\n", + " }\n", + "\n", + " .colab-df-convert {\n", + " background-color: #E8F0FE;\n", + " border: none;\n", + " border-radius: 50%;\n", + " cursor: pointer;\n", + " display: none;\n", + " fill: #1967D2;\n", + " height: 32px;\n", + " padding: 0 0 0 0;\n", + " width: 32px;\n", + " }\n", + "\n", + " .colab-df-convert:hover {\n", + " background-color: #E2EBFA;\n", + " box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n", + " fill: #174EA6;\n", + " }\n", + "\n", + " .colab-df-buttons div {\n", + " margin-bottom: 4px;\n", + " }\n", + "\n", + " [theme=dark] .colab-df-convert {\n", + " background-color: #3B4455;\n", + " fill: #D2E3FC;\n", + " }\n", + "\n", + " [theme=dark] .colab-df-convert:hover {\n", + " background-color: #434B5C;\n", + " box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n", + " filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n", + " fill: #FFFFFF;\n", + " }\n", + " \u003c/style\u003e\n", + "\n", + " \u003cscript\u003e\n", + " const buttonEl =\n", + " document.querySelector('#df-49031713-07ff-4c22-9fba-d257afa0f340 button.colab-df-convert');\n", + " buttonEl.style.display =\n", + " google.colab.kernel.accessAllowed ? 'block' : 'none';\n", + "\n", + " async function convertToInteractive(key) {\n", + " const element = document.querySelector('#df-49031713-07ff-4c22-9fba-d257afa0f340');\n", + " const dataTable =\n", + " await google.colab.kernel.invokeFunction('convertToInteractive',\n", + " [key], {});\n", + " if (!dataTable) return;\n", + "\n", + " const docLinkHtml = 'Like what you see? Visit the ' +\n", + " '\u003ca target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb\u003edata table notebook\u003c/a\u003e'\n", + " + ' to learn more about interactive tables.';\n", + " element.innerHTML = '';\n", + " dataTable['output_type'] = 'display_data';\n", + " await google.colab.output.renderOutput(dataTable, element);\n", + " const docLink = document.createElement('div');\n", + " docLink.innerHTML = docLinkHtml;\n", + " element.appendChild(docLink);\n", + " }\n", + " \u003c/script\u003e\n", + " \u003c/div\u003e\n", + "\n", + "\n", + "\u003cdiv id=\"df-4513c1b2-89ac-4a7e-9847-2f275617e9e8\"\u003e\n", + " \u003cbutton class=\"colab-df-quickchart\" onclick=\"quickchart('df-4513c1b2-89ac-4a7e-9847-2f275617e9e8')\"\n", + " title=\"Suggest charts\"\n", + " style=\"display:none;\"\u003e\n", + "\n", + "\u003csvg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n", + " width=\"24px\"\u003e\n", + " \u003cg\u003e\n", + " \u003cpath d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/\u003e\n", + " \u003c/g\u003e\n", + "\u003c/svg\u003e\n", + " \u003c/button\u003e\n", + "\n", + "\u003cstyle\u003e\n", + " .colab-df-quickchart {\n", + " --bg-color: #E8F0FE;\n", + " --fill-color: #1967D2;\n", + " --hover-bg-color: #E2EBFA;\n", + " --hover-fill-color: #174EA6;\n", + " --disabled-fill-color: #AAA;\n", + " --disabled-bg-color: #DDD;\n", + " }\n", + "\n", + " [theme=dark] .colab-df-quickchart {\n", + " --bg-color: #3B4455;\n", + " --fill-color: #D2E3FC;\n", + " --hover-bg-color: #434B5C;\n", + " --hover-fill-color: #FFFFFF;\n", + " --disabled-bg-color: #3B4455;\n", + " --disabled-fill-color: #666;\n", + " }\n", + "\n", + " .colab-df-quickchart {\n", + " background-color: var(--bg-color);\n", + " border: none;\n", + " border-radius: 50%;\n", + " cursor: pointer;\n", + " display: none;\n", + " fill: var(--fill-color);\n", + " height: 32px;\n", + " padding: 0;\n", + " width: 32px;\n", + " }\n", + "\n", + " .colab-df-quickchart:hover {\n", + " background-color: var(--hover-bg-color);\n", + " box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n", + " fill: var(--button-hover-fill-color);\n", + " }\n", + "\n", + " .colab-df-quickchart-complete:disabled,\n", + " .colab-df-quickchart-complete:disabled:hover {\n", + " background-color: var(--disabled-bg-color);\n", + " fill: var(--disabled-fill-color);\n", + " box-shadow: none;\n", + " }\n", + "\n", + " .colab-df-spinner {\n", + " border: 2px solid var(--fill-color);\n", + " border-color: transparent;\n", + " border-bottom-color: var(--fill-color);\n", + " animation:\n", + " spin 1s steps(1) infinite;\n", + " }\n", + "\n", + " @keyframes spin {\n", + " 0% {\n", + " border-color: transparent;\n", + " border-bottom-color: var(--fill-color);\n", + " border-left-color: var(--fill-color);\n", + " }\n", + " 20% {\n", + " border-color: transparent;\n", + " border-left-color: var(--fill-color);\n", + " border-top-color: var(--fill-color);\n", + " }\n", + " 30% {\n", + " border-color: transparent;\n", + " border-left-color: var(--fill-color);\n", + " border-top-color: var(--fill-color);\n", + " border-right-color: var(--fill-color);\n", + " }\n", + " 40% {\n", + " border-color: transparent;\n", + " border-right-color: var(--fill-color);\n", + " border-top-color: var(--fill-color);\n", + " }\n", + " 60% {\n", + " border-color: transparent;\n", + " border-right-color: var(--fill-color);\n", + " }\n", + " 80% {\n", + " border-color: transparent;\n", + " border-right-color: var(--fill-color);\n", + " border-bottom-color: var(--fill-color);\n", + " }\n", + " 90% {\n", + " border-color: transparent;\n", + " border-bottom-color: var(--fill-color);\n", + " }\n", + " }\n", + "\u003c/style\u003e\n", + "\n", + " \u003cscript\u003e\n", + " async function quickchart(key) {\n", + " const quickchartButtonEl =\n", + " document.querySelector('#' + key + ' button');\n", + " quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n", + " quickchartButtonEl.classList.add('colab-df-spinner');\n", + " try {\n", + " const charts = await google.colab.kernel.invokeFunction(\n", + " 'suggestCharts', [key], {});\n", + " } catch (error) {\n", + " console.error('Error during call to suggestCharts:', error);\n", + " }\n", + " quickchartButtonEl.classList.remove('colab-df-spinner');\n", + " quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n", + " }\n", + " (() =\u003e {\n", + " let quickchartButtonEl =\n", + " document.querySelector('#df-4513c1b2-89ac-4a7e-9847-2f275617e9e8 button');\n", + " quickchartButtonEl.style.display =\n", + " google.colab.kernel.accessAllowed ? 'block' : 'none';\n", + " })();\n", + " \u003c/script\u003e\n", + "\u003c/div\u003e\n", + "\n", + " \u003c/div\u003e\n", + " \u003c/div\u003e\n" + ], + "text/plain": [ + " id name \\\n", + "0 AAFC/ACI Canada AAFC Annual Crop Inventory \n", + "1 ACA/reef_habitat/v2_0 Allen Coral Atlas (ACA) - Geomorphic Zonation ... \n", + "2 AHN/AHN2_05M_INT AHN Netherlands 0.5m DEM, Interpolated \n", + "3 AHN/AHN2_05M_NON AHN Netherlands 0.5m DEM, Non-Interpolated \n", + "4 AHN/AHN2_05M_RUW AHN Netherlands 0.5m DEM, Raw Samples \n", + "\n", + " summary \\\n", + "0 Agriculture and Agri-Food Canada annually maps... \n", + "1 The Allen Coral Atlas is a global, high-resolu... \n", + "2 The AHN DEM is a detailed (0.5m resolution) el... \n", + "3 The AHN DEM is a high-resolution (0.5m) model ... \n", + "4 The AHN DEM is a highly detailed (0.5m resolut... \n", + "\n", + " embedding \n", + "0 [-0.0205919258, 0.0099804802, -0.0294760894000... \n", + "1 [0.0040304777, 0.0573143177, -0.0509719588, 0.... \n", + "2 [-0.0115319202, -0.06319545210000001, -0.03415... \n", + "3 [0.0082498919, -0.0807547569, -0.0562631935, 0... \n", + "4 [-0.0056484384000000006, -0.0696996748, -0.052... " + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load our embeddings data into a dataframe:\n", + "local_path = load_embeddings(EMBEDDINGS_CLOUD_PATH, EMBEDDINGS_LOCAL_PATH)\n", + "embeddings_df = pd.read_json(local_path, lines=True)\n", + "embeddings_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 2889, + "status": "ok", + "timestamp": 1726764477765, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "7TZcYQqnOzkw", + "outputId": "d04a28e1-8744-4e9f-c2dd-e63ca5b5ec02" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "llm = ChatGoogleGenerativeAI(model=\"gemini-1.5-pro\", google_api_key=userdata.get('GOOGLE_API_KEY'))\n", + "\n", + "local_path = load_embeddings(EMBEDDINGS_CLOUD_PATH, EMBEDDINGS_LOCAL_PATH)\n", + "embeddings_df = pd.read_json(local_path, lines=True)\n", + "langchain_index = make_langchain_index(embeddings_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 1350, + "status": "ok", + "timestamp": 1726764479113, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "w-fWWcWbTQYp", + "outputId": "e4a2a203-3a18-4ed7-80b3-b4abbf6304fa" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/81954c9606dcf997/data_table.js\";\n\n const table = window.createDataTable({\n data: [[\"JRC/GSW1_4/Metadata\",\n\"JRC Global Surface Water Metadata, v1.4\",\n\"\",\n{\n 'v': 30.0,\n 'f': \"30.0\",\n },\n\"1984-03-16\",\n\"2022-01-01\",\n\"https://developers.google.com/earth-engine/datasets/catalog/JRC_GSW1_4_Metadata\",\n{\n 'v': 0.6387998892522716,\n 'f': \"0.6387998892522716\",\n }],\n [\"JRC/GSW1_4/GlobalSurfaceWater\",\n\"JRC Global Surface Water Mapping Layers, v1.4\",\n\"\",\n{\n 'v': 30.0,\n 'f': \"30.0\",\n },\n\"1984-03-16\",\n\"2022-01-01\",\n\"https://developers.google.com/earth-engine/datasets/catalog/JRC_GSW1_4_GlobalSurfaceWater\",\n{\n 'v': 0.6291070261774427,\n 'f': \"0.6291070261774427\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level06\",\n\"WWF HydroATLAS Basins Level 06\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level06\",\n{\n 'v': 0.6238799251913513,\n 'f': \"0.6238799251913513\",\n }],\n [\"JRC/GSW1_4/MonthlyHistory\",\n\"JRC Monthly Water History, v1.4\",\n\"1 month\",\n{\n 'v': 30.0,\n 'f': \"30.0\",\n },\n\"1984-03-16\",\n\"2022-01-01\",\n\"https://developers.google.com/earth-engine/datasets/catalog/JRC_GSW1_4_MonthlyHistory\",\n{\n 'v': 0.6214846586347442,\n 'f': \"0.6214846586347442\",\n }],\n [\"CIESIN/GPWv411/GPW_Water_Area\",\n\"GPWv411: Water Area\",\n\"\",\n{\n 'v': 927.67,\n 'f': \"927.67\",\n },\n\"2000-01-01\",\n\"2020-01-01\",\n\"https://developers.google.com/earth-engine/datasets/catalog/CIESIN_GPWv411_GPW_Water_Area\",\n{\n 'v': 0.6087280825236732,\n 'f': \"0.6087280825236732\",\n }],\n [\"JRC/GSW1_4/YearlyHistory\",\n\"JRC Yearly Water Classification History, v1.4\",\n\"1 year\",\n{\n 'v': 30.0,\n 'f': \"30.0\",\n },\n\"1984-03-16\",\n\"2022-01-01\",\n\"https://developers.google.com/earth-engine/datasets/catalog/JRC_GSW1_4_YearlyHistory\",\n{\n 'v': 0.6051872836477298,\n 'f': \"0.6051872836477298\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level07\",\n\"WWF HydroATLAS Basins Level 07\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level07\",\n{\n 'v': 0.600147560134516,\n 'f': \"0.600147560134516\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level11\",\n\"WWF HydroATLAS Basins Level 11\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level11\",\n{\n 'v': 0.596886034870554,\n 'f': \"0.596886034870554\",\n }],\n [\"GOOGLE/GLOBAL_CCDC/V1\",\n\"Google Global Landsat-based CCDC Segments\",\n\"\",\n{\n 'v': 30.0,\n 'f': \"30.0\",\n },\n\"1999-01-01\",\n\"2020-01-01\",\n\"https://developers.google.com/earth-engine/datasets/catalog/GOOGLE_GLOBAL_CCDC_V1\",\n{\n 'v': 0.596182104160852,\n 'f': \"0.596182104160852\",\n }],\n [\"LANDSAT/COMPOSITES/C02/T1_L2_ANNUAL_NDWI\",\n\"Landsat Collection 2 Tier 1 Level 2 Annual NDWI Composite\",\n\"1 year\",\n{\n 'v': 30.0,\n 'f': \"30.0\",\n },\n\"1984-01-01\",\n\"2024-01-01\",\n\"https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_COMPOSITES_C02_T1_L2_ANNUAL_NDWI\",\n{\n 'v': 0.5951855608935324,\n 'f': \"0.5951855608935324\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level09\",\n\"WWF HydroATLAS Basins Level 09\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level09\",\n{\n 'v': 0.5946294118432045,\n 'f': \"0.5946294118432045\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level05\",\n\"WWF HydroATLAS Basins Level 05\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level05\",\n{\n 'v': 0.5922615773854899,\n 'f': \"0.5922615773854899\",\n }],\n [\"GLIMS/20230607\",\n\"GLIMS 2023: Global Land Ice Measurements From Space\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"1750-01-01\",\n\"2023-06-07\",\n\"https://developers.google.com/earth-engine/datasets/catalog/GLIMS_20230607\",\n{\n 'v': 0.5914270935633433,\n 'f': \"0.5914270935633433\",\n }],\n [\"USGS/WBD/2017/HUC08\",\n\"HUC08: USGS Watershed Boundary Dataset of Subbasins\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2017-04-22\",\n\"2017-04-23\",\n\"https://developers.google.com/earth-engine/datasets/catalog/USGS_WBD_2017_HUC08\",\n{\n 'v': 0.5913852354937441,\n 'f': \"0.5913852354937441\",\n }],\n [\"USGS/WBD/2017/HUC08\",\n\"HUC08: USGS Watershed Boundary Dataset of Subbasins\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2017-04-22\",\n\"2017-04-23\",\n\"https://developers.google.com/earth-engine/datasets/catalog/USGS_WBD_2017_HUC08\",\n{\n 'v': 0.5913852354937441,\n 'f': \"0.5913852354937441\",\n }],\n [\"USGS/WBD/2017/HUC08\",\n\"HUC08: USGS Watershed Boundary Dataset of Subbasins\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2017-04-22\",\n\"2017-04-23\",\n\"https://developers.google.com/earth-engine/datasets/catalog/USGS_WBD_2017_HUC08\",\n{\n 'v': 0.5913852354937441,\n 'f': \"0.5913852354937441\",\n }],\n [\"USGS/WBD/2017/HUC08\",\n\"HUC08: USGS Watershed Boundary Dataset of Subbasins\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2017-04-22\",\n\"2017-04-23\",\n\"https://developers.google.com/earth-engine/datasets/catalog/USGS_WBD_2017_HUC08\",\n{\n 'v': 0.5913852354937441,\n 'f': \"0.5913852354937441\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level08\",\n\"WWF HydroATLAS Basins Level 08\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level08\",\n{\n 'v': 0.5889644795170381,\n 'f': \"0.5889644795170381\",\n }],\n [\"projects/sat-io/open-datasets/GLOBathy/GLOBathy_bathymetry\",\n\"GLOBathy Global lakes bathymetry dataset\",\n\"\",\n{\n 'v': 30.0,\n 'f': \"30.0\",\n },\n\"2022-01-26\",\n\"2022-01-26\",\n\"https://developers.google.com/earth-engine/datasets/catalog/projects_sat-io_open-datasets_GLOBathy_GLOBathy_bathymetry\",\n{\n 'v': 0.5861303144882369,\n 'f': \"0.5861303144882369\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level04\",\n\"WWF HydroATLAS Basins Level 04\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level04\",\n{\n 'v': 0.5846991613034396,\n 'f': \"0.5846991613034396\",\n }],\n [\"WWF/HydroATLAS/v1/Basins/level03\",\n\"WWF HydroATLAS Basins Level 03\",\n\"\",\n{\n 'v': -1.0,\n 'f': \"-1.0\",\n },\n\"2000-02-22\",\n\"2000-02-22\",\n\"https://developers.google.com/earth-engine/datasets/catalog/WWF_HydroATLAS_v1_Basins_level03\",\n{\n 'v': 0.584177979074644,\n 'f': \"0.584177979074644\",\n }]],\n columns: [[\"string\", \"id\"], [\"string\", \"name\"], [\"string\", \"temp_res\"], [\"number\", \"spatial_res_m\"], [\"string\", \"earliest\"], [\"string\", \"latest\"], [\"string\", \"url\"], [\"number\", \"match_score\"]],\n columnOptions: [],\n rowsPerPage: 5,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: \"300\",\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n\u003cdiv id=\"df-3dfad0ff-0199-4018-9d80-6589436bdd2c\"\u003e\n \u003cbutton class=\"colab-df-quickchart\" onclick=\"quickchart('df-3dfad0ff-0199-4018-9d80-6589436bdd2c')\"\n title=\"Suggest charts\"\n style=\"display:none;\"\u003e\n \n\u003csvg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n width=\"24px\"\u003e\n \u003cg\u003e\n \u003cpath d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/\u003e\n \u003c/g\u003e\n\u003c/svg\u003e\n \u003c/button\u003e\n \n\u003cstyle\u003e\n .colab-df-quickchart {\n --bg-color: #E8F0FE;\n --fill-color: #1967D2;\n --hover-bg-color: #E2EBFA;\n --hover-fill-color: #174EA6;\n --disabled-fill-color: #AAA;\n --disabled-bg-color: #DDD;\n }\n\n [theme=dark] .colab-df-quickchart {\n --bg-color: #3B4455;\n --fill-color: #D2E3FC;\n --hover-bg-color: #434B5C;\n --hover-fill-color: #FFFFFF;\n --disabled-bg-color: #3B4455;\n --disabled-fill-color: #666;\n }\n\n .colab-df-quickchart {\n background-color: var(--bg-color);\n border: none;\n border-radius: 50%;\n cursor: pointer;\n display: none;\n fill: var(--fill-color);\n height: 32px;\n padding: 0;\n width: 32px;\n }\n\n .colab-df-quickchart:hover {\n background-color: var(--hover-bg-color);\n box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n fill: var(--button-hover-fill-color);\n }\n\n .colab-df-quickchart-complete:disabled,\n .colab-df-quickchart-complete:disabled:hover {\n background-color: var(--disabled-bg-color);\n fill: var(--disabled-fill-color);\n box-shadow: none;\n }\n\n .colab-df-spinner {\n border: 2px solid var(--fill-color);\n border-color: transparent;\n border-bottom-color: var(--fill-color);\n animation:\n spin 1s steps(1) infinite;\n }\n\n @keyframes spin {\n 0% {\n border-color: transparent;\n border-bottom-color: var(--fill-color);\n border-left-color: var(--fill-color);\n }\n 20% {\n border-color: transparent;\n border-left-color: var(--fill-color);\n border-top-color: var(--fill-color);\n }\n 30% {\n border-color: transparent;\n border-left-color: var(--fill-color);\n border-top-color: var(--fill-color);\n border-right-color: var(--fill-color);\n }\n 40% {\n border-color: transparent;\n border-right-color: var(--fill-color);\n border-top-color: var(--fill-color);\n }\n 60% {\n border-color: transparent;\n border-right-color: var(--fill-color);\n }\n 80% {\n border-color: transparent;\n border-right-color: var(--fill-color);\n border-bottom-color: var(--fill-color);\n }\n 90% {\n border-color: transparent;\n border-bottom-color: var(--fill-color);\n }\n }\n\u003c/style\u003e\n\n \u003cscript\u003e\n async function quickchart(key) {\n const quickchartButtonEl =\n document.querySelector('#' + key + ' button');\n quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n quickchartButtonEl.classList.add('colab-df-spinner');\n try {\n const charts = await google.colab.kernel.invokeFunction(\n 'suggestCharts', [key], {});\n } catch (error) {\n console.error('Error during call to suggestCharts:', error);\n }\n quickchartButtonEl.classList.remove('colab-df-spinner');\n quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n }\n (() =\u003e {\n let quickchartButtonEl =\n document.querySelector('#df-3dfad0ff-0199-4018-9d80-6589436bdd2c button');\n quickchartButtonEl.style.display =\n google.colab.kernel.accessAllowed ? 'block' : 'none';\n })();\n \u003c/script\u003e\n\u003c/div\u003e`;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\u003cdiv\u003e\n", + "\u003cstyle scoped\u003e\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "\u003c/style\u003e\n", + "\u003ctable border=\"1\" class=\"dataframe\"\u003e\n", + " \u003cthead\u003e\n", + " \u003ctr style=\"text-align: right;\"\u003e\n", + " \u003cth\u003e\u003c/th\u003e\n", + " \u003cth\u003eid\u003c/th\u003e\n", + " \u003cth\u003ename\u003c/th\u003e\n", + " \u003cth\u003etemp_res\u003c/th\u003e\n", + " \u003cth\u003espatial_res_m\u003c/th\u003e\n", + " \u003cth\u003eearliest\u003c/th\u003e\n", + " \u003cth\u003elatest\u003c/th\u003e\n", + " \u003cth\u003eurl\u003c/th\u003e\n", + " \u003cth\u003ematch_score\u003c/th\u003e\n", + " \u003c/tr\u003e\n", + " \u003c/thead\u003e\n", + " \u003ctbody\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e4\u003c/th\u003e\n", + " \u003ctd\u003eJRC/GSW1_4/Metadata\u003c/td\u003e\n", + " \u003ctd\u003eJRC Global Surface Water Metadata, v1.4\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e30.00\u003c/td\u003e\n", + " \u003ctd\u003e1984-03-16\u003c/td\u003e\n", + " \u003ctd\u003e2022-01-01\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.638800\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e3\u003c/th\u003e\n", + " \u003ctd\u003eJRC/GSW1_4/GlobalSurfaceWater\u003c/td\u003e\n", + " \u003ctd\u003eJRC Global Surface Water Mapping Layers, v1.4\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e30.00\u003c/td\u003e\n", + " \u003ctd\u003e1984-03-16\u003c/td\u003e\n", + " \u003ctd\u003e2022-01-01\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.629107\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e15\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level06\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 06\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.623880\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e5\u003c/th\u003e\n", + " \u003ctd\u003eJRC/GSW1_4/MonthlyHistory\u003c/td\u003e\n", + " \u003ctd\u003eJRC Monthly Water History, v1.4\u003c/td\u003e\n", + " \u003ctd\u003e1 month\u003c/td\u003e\n", + " \u003ctd\u003e30.00\u003c/td\u003e\n", + " \u003ctd\u003e1984-03-16\u003c/td\u003e\n", + " \u003ctd\u003e2022-01-01\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.621485\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e0\u003c/th\u003e\n", + " \u003ctd\u003eCIESIN/GPWv411/GPW_Water_Area\u003c/td\u003e\n", + " \u003ctd\u003eGPWv411: Water Area\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e927.67\u003c/td\u003e\n", + " \u003ctd\u003e2000-01-01\u003c/td\u003e\n", + " \u003ctd\u003e2020-01-01\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.608728\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e6\u003c/th\u003e\n", + " \u003ctd\u003eJRC/GSW1_4/YearlyHistory\u003c/td\u003e\n", + " \u003ctd\u003eJRC Yearly Water Classification History, v1.4\u003c/td\u003e\n", + " \u003ctd\u003e1 year\u003c/td\u003e\n", + " \u003ctd\u003e30.00\u003c/td\u003e\n", + " \u003ctd\u003e1984-03-16\u003c/td\u003e\n", + " \u003ctd\u003e2022-01-01\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.605187\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e16\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level07\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 07\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.600148\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e19\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level11\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 11\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.596886\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e2\u003c/th\u003e\n", + " \u003ctd\u003eGOOGLE/GLOBAL_CCDC/V1\u003c/td\u003e\n", + " \u003ctd\u003eGoogle Global Landsat-based CCDC Segments\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e30.00\u003c/td\u003e\n", + " \u003ctd\u003e1999-01-01\u003c/td\u003e\n", + " \u003ctd\u003e2020-01-01\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.596182\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e7\u003c/th\u003e\n", + " \u003ctd\u003eLANDSAT/COMPOSITES/C02/T1_L2_ANNUAL_NDWI\u003c/td\u003e\n", + " \u003ctd\u003eLandsat Collection 2 Tier 1 Level 2 Annual NDW...\u003c/td\u003e\n", + " \u003ctd\u003e1 year\u003c/td\u003e\n", + " \u003ctd\u003e30.00\u003c/td\u003e\n", + " \u003ctd\u003e1984-01-01\u003c/td\u003e\n", + " \u003ctd\u003e2024-01-01\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.595186\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e18\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level09\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 09\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.594629\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e14\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level05\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 05\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.592262\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e1\u003c/th\u003e\n", + " \u003ctd\u003eGLIMS/20230607\u003c/td\u003e\n", + " \u003ctd\u003eGLIMS 2023: Global Land Ice Measurements From ...\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e1750-01-01\u003c/td\u003e\n", + " \u003ctd\u003e2023-06-07\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.591427\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e10\u003c/th\u003e\n", + " \u003ctd\u003eUSGS/WBD/2017/HUC08\u003c/td\u003e\n", + " \u003ctd\u003eHUC08: USGS Watershed Boundary Dataset of Subb...\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-22\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-23\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.591385\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e11\u003c/th\u003e\n", + " \u003ctd\u003eUSGS/WBD/2017/HUC08\u003c/td\u003e\n", + " \u003ctd\u003eHUC08: USGS Watershed Boundary Dataset of Subb...\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-22\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-23\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.591385\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e9\u003c/th\u003e\n", + " \u003ctd\u003eUSGS/WBD/2017/HUC08\u003c/td\u003e\n", + " \u003ctd\u003eHUC08: USGS Watershed Boundary Dataset of Subb...\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-22\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-23\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.591385\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e8\u003c/th\u003e\n", + " \u003ctd\u003eUSGS/WBD/2017/HUC08\u003c/td\u003e\n", + " \u003ctd\u003eHUC08: USGS Watershed Boundary Dataset of Subb...\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-22\u003c/td\u003e\n", + " \u003ctd\u003e2017-04-23\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.591385\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e17\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level08\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 08\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.588964\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e20\u003c/th\u003e\n", + " \u003ctd\u003eprojects/sat-io/open-datasets/GLOBathy/GLOBath...\u003c/td\u003e\n", + " \u003ctd\u003eGLOBathy Global lakes bathymetry dataset\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e30.00\u003c/td\u003e\n", + " \u003ctd\u003e2022-01-26\u003c/td\u003e\n", + " \u003ctd\u003e2022-01-26\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.586130\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e13\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level04\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 04\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.584699\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003e12\u003c/th\u003e\n", + " \u003ctd\u003eWWF/HydroATLAS/v1/Basins/level03\u003c/td\u003e\n", + " \u003ctd\u003eWWF HydroATLAS Basins Level 03\u003c/td\u003e\n", + " \u003ctd\u003e\u003c/td\u003e\n", + " \u003ctd\u003e-1.00\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003e2000-02-22\u003c/td\u003e\n", + " \u003ctd\u003ehttps://developers.google.com/earth-engine/dat...\u003c/td\u003e\n", + " \u003ctd\u003e0.584178\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \u003c/tbody\u003e\n", + "\u003c/table\u003e\n", + "\u003c/div\u003e" + ], + "text/plain": [ + "\u003cgoogle.colab.data_table.DataTable object\u003e" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from google.colab import data_table\n", + "\n", + "ee_index = EarthEngineDatasetIndex(catalog, langchain_index, llm)\n", + "df = ee_index.find_top_matches_with_score_df(\"Datasets to measure changes in lakes over time.\")\n", + "# make a sample snippet of html for a hyperlink\n", + "\n", + "data_table.DataTable(df, include_index=False, num_rows_per_page=5, min_width=300)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Jd7zCFQYOu3S" + }, + "source": [ + "# Gemini Methods\n", + "\n", + "Python functions that somehow invoke an LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 4, + "status": "ok", + "timestamp": 1726764479113, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "PEhOWg474A-x", + "outputId": "bf8a6b22-0e55-4436-df1f-4d46f9477bfd" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Gemini Function definitions\n", + "import typing_extensions\n", + "\n", + "\n", + "def explain_relevance(\n", + " query: str,\n", + " dataset_id: str,\n", + " catalog: Catalog,\n", + " model_name: str = 'gemini-1.5-pro-latest',\n", + " stream=False):\n", + " \"\"\"Prompts LLM to explain the relevance of a dataset to a query.\"\"\"\n", + "\n", + " stac_json = catalog.get_collection(dataset_id).stac_json\n", + " return explain_relevance_from_stac_json(query, stac_json, model_name, stream)\n", + "\n", + "\n", + "@retry(\n", + " stop=stop_after_attempt(3),\n", + " wait=wait_exponential(multiplier=1, min=4, max=10),\n", + " retry=retry_if_exception_type((requests.exceptions.RequestException, ConnectionError))\n", + ")\n", + "def explain_relevance_from_stac_json(\n", + " query, stac_json, model_name: str = 'gemini-1.5-pro-latest', stream=False):\n", + "\n", + " stac_json_str = json.dumps(stac_json)\n", + "\n", + " prompt = f'''\n", + " I am an Earth Engine user contemplating using a dataset to support\n", + " my investigation of the following query. Provide a concise, paragraph-long\n", + " summary explaining why this dataset may be a good fit for my use case.\n", + " If it does not seem like an appropriate dataset, say so.\n", + " If relevant, call attention to a max of 3 bands that may be of particular interest.\n", + " Weigh the tradeoffs between temporal and spatial resolution, particularly\n", + " if the original query specifies regions of interest, time periods, or\n", + " frequency of data collection. If I have not specified any\n", + " spatial constraints, do your best based on the nature of their query. For example,\n", + " if I'm wanting to study something small, like buildings, I will likely need good spatial resolution.\n", + "\n", + " Here is the original query:\n", + " {query}\n", + "\n", + " Here is the stac json metadata for the dataset:\n", + " {stac_json_str}\n", + " '''\n", + " model = genai.GenerativeModel(model_name)\n", + " response = model.generate_content(prompt, stream=stream)\n", + " if stream:\n", + " return response\n", + " return response.text\n", + "\n", + "@retry(\n", + " stop=stop_after_attempt(3),\n", + " wait=wait_exponential(multiplier=1, min=4, max=10),\n", + " retry=retry_if_exception_type((requests.exceptions.RequestException, ConnectionError))\n", + ")\n", + "def is_valid_question(question, model_name: str = 'gemini-1.5-pro-latest'):\n", + " '''Filters out questions that cannot be answered by a dataset search tool.'''\n", + "\n", + " prompt = f'''\n", + " You are a tool whose job is to determine whether or not the following question\n", + " relates even in a small way to geospatial datasets. Please provide a single\n", + " word answer either True or False.\n", + "\n", + " For example, if the original query is \"hello\" - you should answer False. If\n", + " the original query is \"cheese futures\" you should still answer True because\n", + " the user could be interested in cheese production, and therfore agricultural\n", + " land where cattle might be raised.\n", + "\n", + " Here is the original query:\n", + " {question}\n", + " '''\n", + " model = genai.GenerativeModel(model_name)\n", + " response = model.generate_content(prompt)\n", + " # Err on the side of returning True\n", + " return response.text.lower().strip() != 'false'\n", + "\n", + "\n", + "class CodeThoughts(typing_extensions.TypedDict):\n", + " code: str\n", + " thoughts: str\n", + "\n", + "\n", + "@retry(\n", + " stop=stop_after_attempt(3),\n", + " wait=wait_exponential(multiplier=1, min=4, max=10),\n", + " retry=retry_if_exception_type((requests.exceptions.RequestException, ConnectionError))\n", + ")\n", + "def fix_ee_python_code(\n", + " code: str,\n", + " ee,\n", + " geemap_instance: geemap.Map,\n", + " model_name: str = 'gemini-1.5-pro-latest'):\n", + " \"\"\"Asks a model to do ee python code correction in the event of error.\"\"\"\n", + "\n", + " def create_error_prompt(code, error_msg):\n", + " return f'''\n", + " You are an extremely laconic code correction robot.\n", + " I am attempting to execute the following snippet of python Earth Engine code,\n", + " using a geemap instance:\n", + "\n", + " ```\n", + " {code}\n", + " ```\n", + "\n", + " I have encountered the following error, please fix it. In 1-2 sentences,\n", + " explain your debugging thought process in the 'thoughts' field. Note that\n", + " the setOptions method exists only in the ee javascript library. Code\n", + " referencing that method can be removed.\n", + "\n", + " Include the complete revised code snippet in the code field.\n", + " Do not provide any other comentary in the code field. Do not add any new\n", + " imports to the code snippet.\n", + "\n", + " {error_msg}\n", + " '''\n", + "\n", + " generation_config = {\n", + " \"response_mime_type\": \"application/json\",\n", + " \"response_schema\": CodeThoughts}\n", + "\n", + " model = genai.GenerativeModel(model_name, generation_config=generation_config)\n", + "\n", + " max_attempts = 5\n", + " total_attempts = 0\n", + " broken = True\n", + " while total_attempts \u003c max_attempts and broken:\n", + " try:\n", + " run_ee_code(code, ee, geemap_instance)\n", + " # logging.warning(f'Code success! after {total_attempts} try.')\n", + " return code\n", + " except Exception as e:\n", + " logging.warning('Code execution error, asking Gemini for help.')\n", + "\n", + " gemini_json_fail = True\n", + " while gemini_json_fail:\n", + " response = model.generate_content(create_error_prompt(code, str(e)))\n", + " try:\n", + " code_thoughts = json.loads(response.text)\n", + " gemini_json_fail = False\n", + " except json.JSONDecodeError:\n", + " pass\n", + "\n", + " total_attempts += 1\n", + "\n", + " code = code_thoughts['code']\n", + " thoughts= code_thoughts['thoughts']\n", + " logging.warning(f'Gemini thoughts: \\n{thoughts}')\n", + " # logging.warning(f'Gemini new code: {code}')\n", + " if total_attempts == max_attempts:\n", + " raise Exception(e)\n", + " logging.warning(f'Failed to fix code after {max_attempts} attempts.')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-9DFklmGlnPl" + }, + "source": [ + "## Test Gemini functions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 86 + }, + "executionInfo": { + "elapsed": 4616, + "status": "ok", + "timestamp": 1726764483726, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "bLKZF2UQaPyx", + "outputId": "6a51a39e-ead4-4285-98a2-db44c761928f" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n", + "True\n", + "False\n", + "True\n" + ] + } + ], + "source": [ + "print(is_valid_question('@'))\n", + "print(is_valid_question('bananas'))\n", + "print(is_valid_question(\"What is Taylor Swift's favorite color?\"))\n", + "print(is_valid_question('flowers'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "executionInfo": { + "elapsed": 2451, + "status": "ok", + "timestamp": 1726764486165, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "DfI6tu7Y9Xgh", + "outputId": "d68224b6-bc55-4a03-962d-aa0ef0dd30a9" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "geemap_instance = geemap.Map()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 277 + }, + "executionInfo": { + "elapsed": 4757, + "status": "ok", + "timestamp": 1726764490919, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "s_uH_Gwx8PSI", + "outputId": "6f09fc6d-ca61-4498-e8c8-09ceda6396cd" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:Code execution error, asking Gemini for help.\n", + "WARNING:root:Gemini thoughts: \n", + "The geemap method `addLayerx` does not exist. I replaced it with the correct method, `addLayer`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset = ee.ImageCollection('WorldPop/GP/100m/pop')\n", + "\n", + "visualization = {\n", + " 'bands': ['population'],\n", + " 'min': 0.0,\n", + " 'max': 50.0,\n", + " 'palette': ['24126c', '1fff4f', 'd4ff50']\n", + "}\n", + "\n", + "m.setCenter(113.643, 34.769, 7)\n", + "\n", + "m.addLayer(dataset, visualization, 'Population')\n" + ] + } + ], + "source": [ + "bad_code = \"\"\"\n", + "dataset = ee.ImageCollection('WorldPop/GP/100m/pop')\n", + "\n", + "visualization = {\n", + " 'bands': ['population'],\n", + " 'min': 0.0,\n", + " 'max': 50.0,\n", + " 'palette': ['24126c', '1fff4f', 'd4ff50']\n", + "}\n", + "\n", + "m.setCenter(113.643, 34.769, 7)\n", + "\n", + "m.addLayerx(dataset, visualization, 'Population')\n", + "\"\"\"\n", + "working_code = fix_ee_python_code(bad_code, ee, geemap_instance)\n", + "print(working_code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 106 + }, + "executionInfo": { + "elapsed": 3160, + "status": "ok", + "timestamp": 1726764494066, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "Efh6lThz6dwp", + "outputId": "fe9000bc-5c5c-401e-f436-a13f2be73912" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This dataset, SRTM Digital Elevation Data Version 4, is **not suitable** for measuring lake changes over the past 5 years. \n", + "\n", + "Although it provides global elevation data (\"elevation\" band), which could be used to identify water bodies in combination with other datasets, its key limitation is its **temporal resolution**. It provides data from a single period in 2000, making it impossible to analyze changes over the past 5 years. You would need a dataset with a much higher temporal resolution and more recent data to track lake changes effectively. \n", + "\n" + ] + } + ], + "source": [ + "print(explain_relevance(\"Datasets to measure changes in lakes in the past 5 years.\", 'CGIAR/SRTM90_V4', catalog))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 72 + }, + "executionInfo": { + "elapsed": 5321, + "status": "ok", + "timestamp": 1726764499384, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "TjcwA3tC7xTQ", + "outputId": "c1c5aedf-12bd-4cf0-e01a-e286c71826f2" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The CGIAR/SRTM90_V4 dataset is a good choice for the first part of your flood inundation mapping project: creating a base elevation layer. This dataset, with a spatial resolution of 90 meters, provides global coverage of elevation data, making it suitable for analyzing terrain and identifying areas at risk of flooding. The 'elevation' band will be the most relevant for your analysis. However, keep in mind that this dataset only provides a single snapshot in time (February 2000) and will not provide any information on river flow. You will need to incorporate a time-varying dataset of river flow to create your flood inundation maps. \n", + "\n" + ] + } + ], + "source": [ + "print(explain_relevance(\"Create flood inundation maps based on elevation and river flow data\", 'CGIAR/SRTM90_V4', catalog))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7WVFXC6vIHBy" + }, + "source": [ + "# UI code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 3, + "status": "ok", + "timestamp": 1726764499384, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "j3Q5N4pRIJL_", + "outputId": "43891802-60ec-4b7b-fff7-75b9a1420c4c" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title CSS\n", + "from google.colab import syntax\n", + "# Custom CSS for Material Design styling with enhanced table styling, chat panel, and debug panel\n", + "CSS = syntax.css(\"\"\"\n", + "@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500\u0026display=swap');\n", + "\n", + "body {\n", + " font-family: 'Roboto', sans-serif;\n", + " margin: 0;\n", + " padding: 0;\n", + "}\n", + "\n", + ".main-title {\n", + " font-size: 24px;\n", + " font-weight: 500;\n", + " color: #212121;\n", + " margin-bottom: 16px;\n", + "}\n", + "\n", + ".custom-title {\n", + " font-size: 18px;\n", + " font-weight: 500;\n", + " color: #212121;\n", + " margin-bottom: 12px;\n", + "}\n", + "\n", + ".details-text {\n", + " font-size: 14px;\n", + " color: #616161;\n", + " line-height: 1.5;\n", + "}\n", + "\n", + ".custom-table {\n", + " width: 100%;\n", + " border-collapse: collapse;\n", + " margin-bottom: 24px;\n", + " font-family: 'Roboto', sans-serif;\n", + "}\n", + ".custom-table th, .custom-table td {\n", + " text-align: left;\n", + " padding: 12px;\n", + " border-bottom: 1px solid #E0E0E0;\n", + "}\n", + ".custom-table th {\n", + " background-color: #F5F5F5;\n", + " font-weight: 500;\n", + " color: #212121;\n", + "}\n", + ".custom-table tr:hover {\n", + " background-color: #E3F2FD;\n", + "}\n", + ".custom-table tr.selected {\n", + " background-color: #BBDEFB;\n", + "}\n", + "\n", + "/* Ensure borders are visible */\n", + ".jupyter-widgets.widget-box {\n", + " border: 1px solid #E0E0E0 !important;\n", + " overflow: auto;\n", + "}\n", + "\n", + "/* Make the map span full width */\n", + ".geemap-container {\n", + " width: 100% !important;\n", + " height: 600px !important;\n", + "}\n", + "\n", + "# Disable table clicks while things are loading\n", + ".disabled {\n", + " pointer-events: none;\n", + " /* You might also want to visually indicate the disabled state */\n", + " opacity: 0.5;\n", + " cursor: default;\n", + "}\n", + "\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "executionInfo": { + "elapsed": 316, + "status": "ok", + "timestamp": 1726764499698, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "zXajTHdBIuU5", + "outputId": "0464ccfe-9759-4893-9af7-7df3056c2243" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title UI definition\n", + "import ipywidgets as widgets\n", + "from IPython.display import display, HTML, Javascript\n", + "from jinja2 import Template\n", + "import geemap\n", + "import time\n", + "import uuid\n", + "from google.colab import output\n", + "\n", + "\n", + "class DatasetSearchInterface:\n", + "\n", + " collections: CollectionList\n", + " query: str\n", + " dataset_table: widgets.Widget\n", + " code_output: widgets.Widget\n", + " details_output: widgets.Widget\n", + " map_output: widgets.Widget\n", + " geemap_instance: geemap.Map\n", + "\n", + " # Parent containers for controlling widget visibility.\n", + " details_code_box: widgets.Widget\n", + " map_widget: widgets.Widget\n", + "\n", + "\n", + " def __init__(self, query: str, collections: CollectionList):\n", + "\n", + " self.query = query\n", + " self.collections = collections\n", + "\n", + " # Create the output widgets\n", + " self.code_output = widgets.Output(layout=widgets.Layout(width='50%'))\n", + " self.details_output = widgets.Output(layout=widgets.Layout(height='300px', width='100%'))\n", + "\n", + " # Initialize dataset table\n", + " table_html = self._build_table_html(collections)\n", + " self.dataset_table = widgets.HTML(value=table_html)\n", + "\n", + " _callback_id = 'dataset-select' + str(uuid.uuid4())\n", + " output.register_callback(_callback_id, self.update_outputs)\n", + " self._dataset_select_js_code = self._dataset_select_js_code(_callback_id)\n", + "\n", + "\n", + " # Initialize map\n", + " self.map_output = widgets.Output(layout=widgets.Layout(width='100%'))\n", + " self.geemap_instance = geemap.Map(height='600px', width='100%')\n", + "\n", + "\n", + " def display(self):\n", + " \"\"\"Display the UI in the cell.\"\"\"\n", + " # Create title and description with Material Design styling\n", + " # title = widgets.HTML(value='\u003ch2 class=\"main-title\"\u003eEarth Engine Dataset Explorer\u003c/h2\u003e')\n", + "\n", + " # Wrap outputs in a widget box for border styling\n", + " details_widget = widgets.Box([self.details_output], layout=widgets.Layout(border='1px solid #E0E0E0', padding='10px', margin='5px', width='100%'))\n", + " code_widget = widgets.Box([self.code_output], layout=widgets.Layout(border='1px solid #E0E0E0', padding='10px', margin='5px', width='100%'))\n", + " self.map_widget = widgets.Box([self.map_output], layout=widgets.Layout(border='1px solid #E0E0E0', padding='10px', margin='5px', width='100%', height='600x'))\n", + "\n", + " # Create the vertical box for code and details\n", + " self.details_code_box = widgets.VBox([details_widget, code_widget], layout=widgets.Layout(width='50%', height='600px'))\n", + "\n", + " # Create a horizontal box for map and details/code\n", + " map_details_code_box = widgets.HBox([self.map_widget, self.details_code_box], layout=widgets.Layout(border='1px solid #E0E0E0', padding='10px', margin='5px'))\n", + "\n", + " # Create the main layout with Material Design styling\n", + " main_content = widgets.VBox([\n", + " self.dataset_table,\n", + " map_details_code_box\n", + " ], layout=widgets.Layout(width='100%', border='1px solid #E0E0E0', padding='10px', margin='5px'))\n", + "\n", + " # Add debug panel to the main layout\n", + " main_layout = widgets.VBox([\n", + " # title,\n", + " main_content,\n", + " ], layout=widgets.Layout(height='1500px', width='100%', padding='24px'))\n", + "\n", + " # Display the widget\n", + " display(HTML(f'\u003cstyle\u003e{CSS}\u003c/style\u003e'))\n", + " display(main_layout)\n", + " display(Javascript(self._dataset_select_js_code))\n", + "\n", + "\n", + " def _build_table_html(self, collections: CollectionList):\n", + " # Create the table HTML\n", + " table_html = \"\"\"\n", + " \u003ctable class=\"custom-table\"\u003e\n", + " \u003ctr\u003e\n", + " \u003cth\u003eDataset ID\u003c/th\u003e\n", + " \u003cth\u003e Name \u003c/th\u003e\n", + " \u003cth\u003eTemporal Resolution\u003c/th\u003e\n", + " \u003cth\u003eSpatial Resolution (m)\u003c/th\u003e\n", + " \u003cth\u003eEarliest\u003c/th\u003e\n", + " \u003cth\u003eLatest\u003c/th\u003e\n", + " \u003c/tr\u003e\n", + " \"\"\"\n", + " for dataset in collections:\n", + " table_html += f\"\"\"\n", + " \u003ctr data-dataset=\"{dataset.public_id()}\"\u003e\n", + " \u003ctd\u003e{dataset.public_id()}\u003c/td\u003e\n", + " \u003ctd\u003e{dataset.get('title')}\u003c/td\u003e\n", + " \u003ctd\u003e{dataset.temporal_resolution_str()}\u003c/td\u003e\n", + " \u003ctd\u003e{dataset.spatial_resolution_m()}\u003c/td\u003e\n", + " \u003ctd\u003e{dataset.start_str()}\u003c/td\u003e\n", + " \u003ctd\u003e{dataset.end_str()}\u003c/td\u003e\n", + " \u003c/tr\u003e\n", + " \"\"\"\n", + "\n", + " table_html += \"\u003c/table\u003e\"\n", + " return table_html\n", + "\n", + "\n", + " def update_outputs(self, selected_dataset):\n", + " collection = self.collections.filter_by_ids([selected_dataset])\n", + "\n", + " if not collection:\n", + " self.details_code_box.layout.visibility = 'hidden'\n", + " self.map_widget.layout.visibility = 'hidden'\n", + " return\n", + "\n", + " dataset = collection[0]\n", + "\n", + " # Clear everything when a new dataset is selected.\n", + " self.map_output.clear_output()\n", + " self.code_output.clear_output()\n", + " self.details_output.clear_output()\n", + " # Clear previous layers. Keep only the base layer\n", + " self.geemap_instance.layers = self.geemap_instance.layers[:1]\n", + "\n", + " with self.map_output:\n", + " display(HTML('\u003ch3\u003eLoading...\u003c/h3\u003e'))\n", + " code = fix_ee_python_code(dataset.python_code(), ee, self.geemap_instance)\n", + " llm_thoughts = explain_relevance_from_stac_json(self.query, dataset.stac_json)\n", + "\n", + " # Code and LLM thought content is now fully loaded.\n", + " # We ought to make this asynchronus in another version\n", + " self.map_output.clear_output()\n", + "\n", + " with self.code_output:\n", + " display(HTML('\u003cdiv class=\"custom-title\"\u003eEarth Engine Code\u003c/div\u003e'))\n", + " print(code)\n", + "\n", + " with self.details_output:\n", + " # display(HTML('\u003ch3\u003eThinking...\u003c/h3\u003e'))\n", + " # self.details_output.clear_output()\n", + " display(HTML('\u003cdiv class=\"custom-title\"\u003eThoughts with Gemini\u003c/div\u003e'))\n", + " print(llm_thoughts)\n", + "\n", + " with self.map_output:\n", + " display(self.geemap_instance)\n", + "\n", + " self.details_code_box.layout.visibility = 'visible'\n", + " self.map_widget.layout.visibility = 'visible'\n", + "\n", + "\n", + " def _dataset_select_js_code(self, callback_id):\n", + " \"\"\"Handles a dataset onclick event\"\"\"\n", + " # JavaScript for handling table row selection\n", + " return Template(syntax.javascript(\"\"\"\n", + " function initializeTableInteraction() {\n", + " const table = document.querySelector('.custom-table');\n", + " if (!table) {\n", + " console.error('Table not found');\n", + " return;\n", + " }\n", + "\n", + " function selectRow(row) {\n", + " // Remove selection from previously selected row\n", + " const prevSelected = table.querySelector('tr.selected');\n", + " if (prevSelected) prevSelected.classList.remove('selected');\n", + "\n", + " // Add selection to the new row\n", + " row.classList.add('selected');\n", + " const selectedDataset = row.dataset.dataset;\n", + " console.log('Selected dataset:', selectedDataset);\n", + " google.colab.kernel.invokeFunction('{{callback_id}}', [selectedDataset], {});\n", + "\n", + " }\n", + "\n", + " table.addEventListener('click', (event) =\u003e {\n", + " const row = event.target.closest('tr');\n", + " if (!row || !row.dataset.dataset) return;\n", + " selectRow(row);\n", + " });\n", + "\n", + " // Select the first row by default\n", + " const firstRow = table.querySelector('tr[data-dataset]');\n", + " if (firstRow) {\n", + " selectRow(firstRow);\n", + " }\n", + " }\n", + "\n", + " // Run the initialization function after a short delay to ensure the DOM is ready\n", + " setTimeout(initializeTableInteraction, 1000);\n", + " \"\"\")).render(callback_id=callback_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5WiCCBHzJ1xa" + }, + "source": [ + "# Main Earth Engine Dataset Search Agent\n", + "\n", + "Run the agent interface here." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1500, + "referenced_widgets": [ + "be9cbebd02204d96843fd82c101f0a8a", + "05540777bc934201be16a09f204d6d85", + "399574623bfc44cca15b2d550d535233", + "d15494641eab41788b9d9e028385dde0", + "54b5a7fe66f84f9e84e923ab2f46dbea", + "3da07a41e7054681abf3e9a847ac9aff", + "c1fa225ad6534f31ba032affed872f90", + "cbeb3695656840fc8dc6a45f49a7d1f9", + "15d1f01c450b4cb78b5661a726cfa992", + "af3c57b00a914006a38cfabaad98eb06", + "423f91d5015c47dbbc6c3a4d427f0179", + "47b3ed8aa7214269b28d749453484929", + "2bfff10e3013485ea27c31a99fb1936d", + "fd79761cc5cd41d6bdd83abd48ac7c3f", + "4daf92bd55894a95b25fa6f3226fb4eb", + "67a03a187d084093a91b69a389d99b6a", + "fdf1ac9e97a3402f8125623c8d7bc803", + "5e6ba3152af84401a9b4c49062d2504b", + "f198a54456d44f81aad02b4a604b3012", + "18a178fc289b44d8920e767381bc5b12", + "ec32026f317e4881b585ba1bb30cbe4c", + "c23d0d0fcdba4f758fdcd1194fa6b602", + "290bec7740d045e9ba65b075a9e88aaa", + "1634c0eaa1404b29a74680339cc0619e", + "f6ff43038c0d417a8df3a126297971aa", + "8bdc3f2f99754e56bab198e119e6ffda", + "e58a21f2fd5e4d809f656903efd6a9e5", + "42f5d94d032245609753c11a2946bc6b", + "b1637e99f33b4f7c9202888d242099ec", + "b75cde7307a3498bbaf7554bccf9760c", + "c7b889ce0f5646de9a7a8b22f55815d1", + "e382e5d47f0042bcbcf73fe8a288178a", + "0ef183a157854424aa95262c1379cba0", + "eeaccaf27871414a83af732b252e1d02", + "7bb43e2213394b498bdd8cc46524b236", + "901e3458425b4bca9c4ab6c2edb67936", + "20ef6ee8da664b23872db2c5747938b3", + "b291236594154adb90a765f042c0ff8d", + "c9ecc9c451af49e4a79e43f5b42fde57", + "ab8cbffada374e7ab1c4eb0220986352", + "6d2fb6ae62b5456089e698ca30c178fc", + "7bdf2f2289994c518e0fa8e433838675", + "932fce5f736645be8925878d66135427", + "e5598a0e2f6341a4a6148fd577df3d53", + "52947df762614d49b2527817380f6c2c", + "d0fbe0786d5040c1823263a1e4220285", + "469c899a8c3a47ba9b6be4c70f612d7a", + "2f640cadafda4ab2a6ad4ac7b0a93c9f", + "9780de0fd3ee4afa9c44cf95e98b4525", + "3bfe910a99394255bae58a77fe2ada90", + "19d0f349608248ddbdfb40b2162fcb36", + "2b002980bc5d4bc29605f12d9cccbfce", + "55be280513384a70a8023d1b42879199", + "6ad0a35e4a2e4d29a789b93b00a58afd", + "ae3285312c574c0698aafeb37e27b290", + "aaf40e012a9c415ba6c46fb4af150af2", + "84e0b1dd65544d029366e09803b0c6b5", + "38f976070f0441538372ae0876c13825", + "9391b0cc30cb4c24aa01a1fce2f99085", + "6eeb59d9fca44d83b60fb46df4313bf9", + "7f41c334701047789159d59ff57efac0", + "60cd7f2d56454ff4a51c8fb730058d1c", + "8055c33f09c84ceb836813b8078dea55", + "7d06cc062c8948b9a32a4afd5057ec57", + "b6053e6a667d40e4bd5a91dd44e1fcaa", + "9e034beaedb1485f966168c02c6a7824", + "9a7d11bded7f49a99782bf119a334a36", + "d7e06127560e4dba897366f0f6bfd13a", + "b6ce6d8d7b7444d4b0cf05cfad66551d", + "98f6f1f997274b1fab56ab08d7a19b20", + "0e36cac53af34214b118ffa59cd5240c", + "02a4a48e042941d2a5e87052c3db63b1", + "fbf21ebfe7434a24a98a9d824b040f9f", + "4ef316239486453390b7565adf9a1032", + "64e2a81cf4ce4756a58b8c4f31e5705c", + "b0246fa4e2d54abc85b7c63fa5c5a330", + "3e00a2c5ef9c4df78b726110ac8b14fd", + "ac5011ff174d41dea0dd953c3b5c92cf", + "e20cbe31cf074f7a91e97a527af0367a", + "f7c8df52fbda40468049122b18001cd8", + "f4a9edca25b14b62811dd91a9ea1c3dc", + "cef7edba38a341f695b910f281614e1a", + "50d918e8bc32427799d780601917e45e", + "43a6e9f373dc49778c39ac138799f07f", + "37e0df11ecf545d7b17b6785476f9895", + "9f6f0b37058d42ebbb4c83f644e11ebe", + "4058c0ff7b014bae8422f9c1c39e8d92", + "e818e82460b448468ebab272aef8ed6e", + "d8d73ac057a5428c9221dcc4c928a70f", + "fd8f4e6b84744f9cad56df589677f606", + "61810ce9c4e14e83a690a3a44e884e95", + "090ce05e3da8468aa8ebbe50eaccd895", + "085b5037d3af48408817d9679c1671af", + "8dc7f19cc4874bcca6248e8aef629e03", + "a0702689a6a44adf9a7cb48a001295f0", + "fdc8a89ecce24b26893c9e23d0176973", + "57bd22925e5b4252abd4dd3a9f450c30", + "0a3a2c811ced4b73900497b7b72b022a" + ] + }, + "executionInfo": { + "elapsed": 3260, + "status": "ok", + "timestamp": 1726764502955, + "user": { + "displayName": "Renee Johnston", + "userId": "00065470300030840468" + }, + "user_tz": 420 + }, + "id": "PbE_9QAHKgOP", + "outputId": "34495e16-597b-48e3-8292-b85e30046bcf" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \u003cstyle\u003e\n", + " .geemap-dark {\n", + " --jp-widgets-color: white;\n", + " --jp-widgets-label-color: white;\n", + " --jp-ui-font-color1: white;\n", + " --jp-layout-color2: #454545;\n", + " background-color: #383838;\n", + " }\n", + "\n", + " .geemap-dark .jupyter-button {\n", + " --jp-layout-color3: #383838;\n", + " }\n", + "\n", + " .geemap-colab {\n", + " background-color: var(--colab-primary-surface-color, white);\n", + " }\n", + "\n", + " .geemap-colab .jupyter-button {\n", + " --jp-layout-color3: var(--colab-primary-surface-color, white);\n", + " }\n", + " \u003c/style\u003e\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "window[\"f995ebf8-76a6-11ef-a03c-0242ac1c000c\"] = google.colab.output.setIframeHeight(-1, true, {\"interactive\": true, \"maxHeight\": 99999});\n", + "//# sourceURL=js_011f4fef9c" + ], + "text/plain": [ + "\u003cIPython.core.display.Javascript object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\u003cstyle\u003e\n", + "@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500\u0026display=swap');\n", + "\n", + "body {\n", + " font-family: 'Roboto', sans-serif;\n", + " margin: 0;\n", + " padding: 0;\n", + "}\n", + "\n", + ".main-title {\n", + " font-size: 24px;\n", + " font-weight: 500;\n", + " color: #212121;\n", + " margin-bottom: 16px;\n", + "}\n", + "\n", + ".custom-title {\n", + " font-size: 18px;\n", + " font-weight: 500;\n", + " color: #212121;\n", + " margin-bottom: 12px;\n", + "}\n", + "\n", + ".details-text {\n", + " font-size: 14px;\n", + " color: #616161;\n", + " line-height: 1.5;\n", + "}\n", + "\n", + ".custom-table {\n", + " width: 100%;\n", + " border-collapse: collapse;\n", + " margin-bottom: 24px;\n", + " font-family: 'Roboto', sans-serif;\n", + "}\n", + ".custom-table th, .custom-table td {\n", + " text-align: left;\n", + " padding: 12px;\n", + " border-bottom: 1px solid #E0E0E0;\n", + "}\n", + ".custom-table th {\n", + " background-color: #F5F5F5;\n", + " font-weight: 500;\n", + " color: #212121;\n", + "}\n", + ".custom-table tr:hover {\n", + " background-color: #E3F2FD;\n", + "}\n", + ".custom-table tr.selected {\n", + " background-color: #BBDEFB;\n", + "}\n", + "\n", + "/* Ensure borders are visible */\n", + ".jupyter-widgets.widget-box {\n", + " border: 1px solid #E0E0E0 !important;\n", + " overflow: auto;\n", + "}\n", + "\n", + "/* Make the map span full width */\n", + ".geemap-container {\n", + " width: 100% !important;\n", + " height: 600px !important;\n", + "}\n", + "\n", + "# Disable table clicks while things are loading\n", + ".disabled {\n", + " pointer-events: none;\n", + " /* You might also want to visually indicate the disabled state */\n", + " opacity: 0.5;\n", + " cursor: default;\n", + "}\n", + "\u003c/style\u003e" + ], + "text/plain": [ + "\u003cIPython.core.display.HTML object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "be9cbebd02204d96843fd82c101f0a8a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(VBox(children=(HTML(value='\\n \u003ctable class=\"custom-table\"\u003e\\n \u003ctr\u003e\\n \u003cth\u003eDa…" + ] + }, + "metadata": { + "application/vnd.jupyter.widget-view+json": { + "colab": { + "custom_widget_manager": { + "url": "https://ssl.gstatic.com/colaboratory-static/widgets/colab-cdn-widget-manager/2b70e893a8ba7c0f/manager.min.js" + } + } + } + }, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + " function initializeTableInteraction() {\n", + " const table = document.querySelector('.custom-table');\n", + " if (!table) {\n", + " console.error('Table not found');\n", + " return;\n", + " }\n", + "\n", + " function selectRow(row) {\n", + " // Remove selection from previously selected row\n", + " const prevSelected = table.querySelector('tr.selected');\n", + " if (prevSelected) prevSelected.classList.remove('selected');\n", + "\n", + " // Add selection to the new row\n", + " row.classList.add('selected');\n", + " const selectedDataset = row.dataset.dataset;\n", + " console.log('Selected dataset:', selectedDataset);\n", + " google.colab.kernel.invokeFunction('dataset-select55abdca1-6660-437a-9bbe-07b7c0054874', [selectedDataset], {});\n", + "\n", + " }\n", + "\n", + " table.addEventListener('click', (event) =\u003e {\n", + " const row = event.target.closest('tr');\n", + " if (!row || !row.dataset.dataset) return;\n", + " selectRow(row);\n", + " });\n", + "\n", + " // Select the first row by default\n", + " const firstRow = table.querySelector('tr[data-dataset]');\n", + " if (firstRow) {\n", + " selectRow(firstRow);\n", + " }\n", + " }\n", + "\n", + " // Run the initialization function after a short delay to ensure the DOM is ready\n", + " setTimeout(initializeTableInteraction, 1000);\n", + " " + ], + "text/plain": [ + "\u003cIPython.core.display.Javascript object\u003e" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# @title Enter Question\n", + "from google.colab import output\n", + "output.no_vertical_scroll()\n", + "\n", + "def Question(query):\n", + " if not is_valid_question(query):\n", + " print(\"Invalid question. Please try again.\")\n", + " return\n", + "\n", + " ee_index = EarthEngineDatasetIndex(catalog, langchain_index, llm)\n", + " datasets = ee_index.find_top_matches(query)\n", + " # datasets = datasets.sort_by_spatial_resolution().limit(5)\n", + " datasets = datasets.limit(7)\n", + " # return datasets\n", + " dataset_search = DatasetSearchInterface(query, datasets)\n", + " dataset_search.display()\n", + "\n", + "query = \"How have global land surface temperatures changed over time?\"#@param {type:\"string\"}\n", + "# ds = Question(query)\n", + "# ds.to_df()\n", + "Question(query)" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "gcCwKks05JzB", + "rWWfeMwO4lnC", + "Jd7zCFQYOu3S", + "-9DFklmGlnPl" + ], + "provenance": [ + { + "file_id": "1KFDDAiv5FhjEO_0kLtGIPNV5cSrM0F19", + "timestamp": 1726515109320 + } + ], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}