From de4540db9ac9caed070200be42c84b8daae495e3 Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Mon, 6 May 2024 15:42:13 +0200 Subject: [PATCH 01/31] added mvp repo for minimal inference workflow --- .gitignore | 13 + minimal_wc_presto/ONNX_conversion.py | 91 ++++ .../backend_inference_example_openeo.ipynb | 307 +++++++++++ minimal_wc_presto/inference.py | 120 +++++ minimal_wc_presto/preprocessing.py | 509 ++++++++++++++++++ minimal_wc_presto/testing.py | 21 + .../udf_worldcereal_inference.py | 96 ++++ 7 files changed, 1157 insertions(+) create mode 100644 minimal_wc_presto/ONNX_conversion.py create mode 100644 minimal_wc_presto/backend_inference_example_openeo.ipynb create mode 100644 minimal_wc_presto/inference.py create mode 100644 minimal_wc_presto/preprocessing.py create mode 100644 minimal_wc_presto/testing.py create mode 100644 minimal_wc_presto/udf_worldcereal_inference.py diff --git a/.gitignore b/.gitignore index c0e944b3..6b75fc22 100755 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,16 @@ notebooks/S1A_IW_GRDH_1SDV_20191026T153410_20191026T153444_029631_035FDA_2640.SA scripts/classification/tenpercent_sparse/.nfs00000000c35c9cfd00000035 download.zip catboost_info/catboost_training.json + +*.cbm +*.pt +*.onnx +*.nc +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip \ No newline at end of file diff --git a/minimal_wc_presto/ONNX_conversion.py b/minimal_wc_presto/ONNX_conversion.py new file mode 100644 index 00000000..5821f963 --- /dev/null +++ b/minimal_wc_presto/ONNX_conversion.py @@ -0,0 +1,91 @@ +#%% Catboost +import catboost +from catboost.utils import convert_to_onnx_object +import onnx + +# Load your CatBoost model +model = catboost.CatBoost() +model.load_model('./model/catboost.cbm') + +onnx_model = convert_to_onnx_object(model) +onnx.save(onnx_model, './model/wc_catboost.onnx') + + + + + +#%% For the pytorch model we need to know the input shape + +import torch +from presto.presto import Presto +from model_class import PrestoFeatureExtractor +import xarray as xr +import numpy as np + +#load the data +ds = xr.open_dataset("./data/belgium_good_2020-12-01_2021-11-30.nc", engine='netcdf4') +arr = ds.drop('crs').to_array(dim='bands') + + +# Load the Presto model +PRESTO_PATH = './model/presto.pt' +presto_model = Presto.load_pretrained(model_path=PRESTO_PATH, strict=False) +presto_extractor = PrestoFeatureExtractor(presto_model) + +#get the required presto input through the feature extractor +input = presto_extractor.create_presto_input(arr) + +x_sample = torch.tensor(np.expand_dims(input[0][0], axis=0), dtype=torch.float32) # Shape matches the shape of eo data in your DataLoader +dw_sample = torch.tensor(np.expand_dims(input[1][0], axis=0), dtype=torch.long) # Shape matches the shape of dynamic_world data in your DataLoader +month_sample = torch.tensor(np.expand_dims(input[2][0], axis = 0), dtype=torch.long) # Shape matches the shape of months data in your DataLoader +latlons_sample = torch.tensor(np.expand_dims(input[3][0], axis = 0), dtype=torch.float32) # Shape matches the shape of latlons data in your DataLoader +mask_sample = torch.tensor(np.expand_dims(input[4][0], axis = 0), dtype=torch.int) + +encoder_model = presto_model.encoder + + + +with torch.no_grad(): + encoder_output = encoder_model( + x_sample, # Add batch dimension + dynamic_world=dw_sample, # Add batch dimension + mask=mask_sample, # Add batch dimension + latlons=latlons_sample, # Add batch dimension + month=month_sample # Add batch dimension + ) + + #%% + +# Export the encoder model to ONNX +torch.onnx.export( + encoder_model, + (x_sample, dw_sample, latlons_sample,mask_sample, month_sample), + './model/wc_presto.onnx', + input_names=["x", "dynamic_world", "latlons", "mask", "month"], + output_names=["output"], + dynamic_axes={ + "x": {0: "batch_size"}, + "dynamic_world": {0: "batch_size"}, + "mask": {0: "batch_size"}, + "latlons": {0: "batch_size"}, + "month": {0: "batch_size"}, + "output": {0: "batch_size"} + } +) +#%% +# Export the model to ONNX +torch.onnx.export( + encoder_model, + (x_sample, dw_sample, latlons_sample, month_sample, mask_sample), + './model/wc_presto.onnx', + input_names=["x", "dynamic_world", "latlons", "month", "mask"], + output_names=["output"], + dynamic_axes={ + "x": {0: "batch_size"}, + "dynamic_world": {0: "batch_size"}, + "mask": {0: "batch_size"}, + "latlons": {0: "batch_size"}, + "month": {0: "batch_size"}, + "output": {0: "batch_size"} + } +) \ No newline at end of file diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb new file mode 100644 index 00000000..3b6a7ad7 --- /dev/null +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ce322de6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "b879f7b4-9a3f-41fc-90d0-ab9cfd25a093", + "metadata": {}, + "source": [ + "### Make OpenEO connection" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Authenticated using refresh token.\n" + ] + } + ], + "source": [ + "import openeo\n", + "connection = openeo.connect(\"https://openeo.dataspace.copernicus.eu/\").authenticate_oidc()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5494c46d", + "metadata": {}, + "outputs": [], + "source": [ + "#Get desired data\n", + "from preprocessing import worldcereal_preprocessed_inputs\n", + "\n", + "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.191984, 51.256920, 5.215158, 51.267661]))\n", + "EXTENT['crs'] = \"EPSG:4326\"\n", + "\n", + "STARTDATE = '2020-11-01'\n", + "ENDDATE = '2021-10-31'\n", + "\n", + "# Set OpenEO classification UDF context based on settings\n", + "CONTEXT = {\n", + " \"startdate\": STARTDATE, # Required\n", + " \"enddate\": ENDDATE, # Required\n", + "}\n", + "\n", + "input_cube = worldcereal_preprocessed_inputs(\n", + " connection,\n", + " EXTENT,\n", + " STARTDATE,\n", + " ENDDATE,\n", + " METEO_collection=None,\n", + " S2_collection= \"SENTINEL2_L2A\",\n", + " S1_collection= \"SENTINEL1_GRD\",\n", + " DEM_collection= \"COPERNICUS_30\"\n", + ")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8f71136c-1252-4786-8609-8bb995da7daf", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-240506caa9a448be8d26ea574243765e': send 'start'\n", + "0:00:13 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", + "0:00:19 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", + "0:00:25 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", + "0:00:33 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", + "0:00:44 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", + "0:00:56 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", + "0:01:12 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:01:31 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:01:55 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:02:34 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:03:12 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:03:58 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:04:57 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:05:59 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:07:00 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:08:05 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:09:05 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:10:16 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", + "0:11:16 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n" + ] + } + ], + "source": [ + "\n", + "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", + "\n", + "prediction = input_cube.apply_neighborhood(\n", + " process=udf,\n", + " size=[\n", + " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", + " ],\n", + " overlap=[\n", + " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", + " ],\n", + ")\n", + "\n", + "\n", + "prediction.execute_batch(outputfile = 'test_output_worldcereal.nc',\n", + " description='world cereal inference',\n", + " job_options={'driver-memory': '1g', \n", + " })\n" + ] + }, + { + "cell_type": "markdown", + "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", + "metadata": {}, + "source": [ + "### Check result" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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226L559/PjZt2hS33HJLbNmypfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtGpiH75y1/GZZddFpdeemlERLzrXe+K7373u/Hkk09GxBu7lrfeemt88YtfjMsuuywiIr7zne9Ee3t7fP/7348rrrgiefkAANSSinYuL7rooti2bVvs27cvIiJ+9atfxeOPPx4f+chHIiLixRdfjO7u7li6dGn5c9ra2mLRokWxY8eO4z5nX19flEqlATcAAOpTRTuX69ati1KpFPPmzYvx48fHsWPH4sYbb4yVK1dGRER3d3dERLS3tw/4vPb29vLH3mrjxo3xD//wD0NZOwAANaaincvvfe97cd9998X9998fTz/9dNx7773xT//0T3HvvfcOeQHr16+P3t7e8q2rq2vIzwUAQHVVtHP5uc99LtatW1f+t5Pz58+PX//617Fx48ZYtWpVdHR0RERET09PzJo1q/x5PT09ce655x73OVtaWqKlpWWIywcAoJZUFJevvfZajBs3cLNz/Pjx0d/fHxERnZ2d0dHREdu2bSvHZKlUip07d8ZVV12Vs+Ia4xriAAD/X0Vx+Rd/8Rdx4403xty5c+O9731v/Pu//3t8/etfj09+8pMREdHU1BRr166Nr3zlK3HmmWdGZ2dnbNiwIWbPnh3Lly8fifUDAFBDKorLLVu2xIYNG+Izn/lMvPzyyzF79uz427/927jhhhvKj7n++uvj1VdfjSuvvDIOHjwYF198cTzyyCMxadKk9MUDAFBbmoo3X16nBpRKpWhra4tX9p0RrVNr/+qUfi0Ote/RA3uqvQSAulY61B/Tz9ofvb290draesLH1n69AQBQN8QlAABpKvo3lwC1zK+/AarPziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAwBQdrwLxLxeHI2I/Sf1+XYuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhjWvwtjjchFeGaxQAAJ8POJQAAacQlAABpxCUAAGnEJQAAacQlAABpxCUAAGmciugkDXaKIqB2OJUYwMmr5D2zdKg/pp91cs9r5xIAgDTiEgCANOISAIA04hIAgDTiEgCANDU7Lf7Rs+ZHc9OEAfeZ+ASGYihne/B+AzSK0T7jjZ1LAADSiEsAANKISwAA0ohLAADSiEsAANLU7LT48bi+NzBaqvV+U80pdddmh7FlpP5u27kEACCNuAQAII24BAAgjbgEACCNuAQAIE1dTYsDNLrRmFIfbEJ0NKbCK/3+TKrD8I323yM7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OMMY0+kQ6NLLB/v7W0t8tO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEtDkC6ephoBUaGnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSOBURAOmccgiGp55P52XnEgCANOISAIA04hIAgDTiEgCANOISAIA0psUBGJJ6mFqFWjfYVHg9s3MJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gAAI6wRp8IHY+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxQEAkmRNhT96YE/K81SDnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ri2OABABbKuHx5R39cQH4ydSwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANI4FREAQ+J0LDQ6r/GhsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gBU3WBTuWNpwpbqyZwKx84lAACJxCUAAGnEJQAAacQlAABpxCUAAGlMiwNQs6o5xWtSvX553VSXnUsAANKISwAA0ohLAADSiEsAANLU3EBPURQREfF6HI0oqrwYAMas0qH+ai+BIXq9OFq1r92or5vS79/4vv63006kqTiZR42i//7v/445c+ZUexkAALxFV1dXnH766Sd8TM3FZX9/fxw4cCCmTp0ahw4dijlz5kRXV1e0trZWe2mMsFKp5HiPEY712OFYjx2OdWMriiIOHToUs2fPjnHjTvyvKmvu1+Ljxo0rF3FTU1NERLS2tnqhjiGO99jhWI8djvXY4Vg3rra2tpN6nIEeAADSiEsAANLUdFy2tLTEl770pWhpaan2UhgFjvfY4ViPHY712OFY879qbqAHAID6VdM7lwAA1BdxCQBAGnEJAEAacQkAQJqajsvbb7893vWud8WkSZNi0aJF8eSTT1Z7SQzTxo0bY+HChTF16tSYOXNmLF++PPbu3TvgMYcPH47Vq1fHjBkz4pRTTokVK1ZET09PlVZMlptvvjmamppi7dq15fsc68bxm9/8Jj7+8Y/HjBkzYvLkyTF//vx46qmnyh8viiJuuOGGmDVrVkyePDmWLl0aL7zwQhVXzFAcO3YsNmzYEJ2dnTF58uR497vfHV/+8pcHXG/asaZm4/Jf//Vf47rrrosvfelL8fTTT8f73//+WLZsWbz88svVXhrDsH379li9enU88cQT8dhjj8XRo0fjwx/+cLz66qvlx1x77bXxox/9KB588MHYvn17HDhwIC6//PIqrprh2rVrV3zrW9+K973vfQPud6wbwyuvvBJLliyJCRMmxE9+8pN47rnn4mtf+1pMnz69/JhbbrklNm/eHHfeeWfs3LkzpkyZEsuWLYvDhw9XceVUatOmTXHHHXfEbbfdFs8//3xs2rQpbrnlltiyZUv5MY41UdSoCy+8sFi9enX5z8eOHStmz55dbNy4sYqrItvLL79cRESxffv2oiiK4uDBg8WECROKBx98sPyY559/voiIYseOHdVaJsNw6NCh4swzzywee+yx4s/+7M+Ka665pigKx7qRfP7zny8uvvjiQT/e399fdHR0FF/96lfL9x08eLBoaWkpvvvd747GEkly6aWXFp/85CcH3Hf55ZcXK1euLIrCseYNNblzeeTIkdi9e3csXbq0fN+4ceNi6dKlsWPHjiqujGy9vb0REXHqqadGRMTu3bvj6NGjA479vHnzYu7cuY59nVq9enVceumlA45phGPdSH74wx/GggUL4mMf+1jMnDkzzjvvvLj77rvLH3/xxReju7t7wLFua2uLRYsWOdZ15qKLLopt27bFvn37IiLiV7/6VTz++OPxkY98JCIca97QXO0FHM/vfve7OHbsWLS3tw+4v729Pf7rv/6rSqsiW39/f6xduzaWLFkS55xzTkREdHd3x8SJE2PatGkDHtve3h7d3d1VWCXD8cADD8TTTz8du3btetvHHOvGsX///rjjjjviuuuuiy984Quxa9eu+OxnPxsTJ06MVatWlY/n8d7THev6sm7duiiVSjFv3rwYP358HDt2LG688cZYuXJlRIRjTUTUaFwyNqxevTqeffbZePzxx6u9FEZAV1dXXHPNNfHYY4/FpEmTqr0cRlB/f38sWLAgbrrppoiIOO+88+LZZ5+NO++8M1atWlXl1ZHpe9/7Xtx3331x//33x3vf+97Ys2dPrF27NmbPnu1YU1aTvxY/7bTTYvz48W+bGu3p6YmOjo4qrYpMa9asiYcffjh+9rOfxemnn16+v6OjI44cORIHDx4c8HjHvv7s3r07Xn755Tj//POjubk5mpubY/v27bF58+Zobm6O9vZ2x7pBzJo1K97znvcMuO/ss8+Ol156KSKifDy9p9e/z33uc7Fu3bq44oorYv78+fHXf/3Xce2118bGjRsjwrHmDTUZlxMnTowLLrggtm3bVr6vv78/tm3bFosXL67iyhiuoihizZo18dBDD8VPf/rT6OzsHPDxCy64ICZMmDDg2O/duzdeeuklx77OXHLJJfHMM8/Enj17yrcFCxbEypUry//tWDeGJUuWvO2UYvv27Yt3vvOdERHR2dkZHR0dA451qVSKnTt3OtZ15rXXXotx4wamw/jx46O/vz8iHGv+n2pPFA3mgQceKFpaWop77rmneO6554orr7yymDZtWtHd3V3tpTEMV111VdHW1lb8/Oc/L37729+Wb6+99lr5MX/3d39XzJ07t/jpT39aPPXUU8XixYuLxYsXV3HVZHnztHhRONaN4sknnyyam5uLG2+8sXjhhReK++67r3jHO95R/Mu//Ev5MTfffHMxbdq04gc/+EHxH//xH8Vll11WdHZ2Fn/4wx+quHIqtWrVquJP/uRPiocffrh48cUXi3/7t38rTjvttOL6668vP8axpmbjsiiKYsuWLcXcuXOLiRMnFhdeeGHxxBNPVHtJDFNEHPe2devW8mP+8Ic/FJ/5zGeK6dOnF+94xzuKj370o8Vvf/vb6i2aNG+NS8e6cfzoRz8qzjnnnKKlpaWYN29ecddddw34eH9/f7Fhw4aivb29aGlpKS655JJi7969VVotQ1UqlYprrrmmmDt3bjFp0qTijDPOKP7+7/++6OvrKz/GsaapKN50Wn0AABiGmvw3lwAA1CdxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAECa/ws8KlmAU0tsngAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "results = result_cube.array.values.squeeze()\n", + "\n", + "f, ax = plt.subplots(1, 1, figsize=(10, 8))\n", + "ax.imshow(results)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "85a73ef1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\VROMPAYH\\AppData\\Local\\Temp\\ipykernel_31284\\122910811.py:21: DeprecationWarning: dropping variables using `drop` is deprecated; use drop_vars.\n", + " arr = ds.drop('crs').to_array(dim='bands')\n" + ] + } + ], + "source": [ + "from pathlib import Path \n", + "import sys\n", + "import urllib.request\n", + "import shutil\n", + "\n", + "import requests\n", + "import xarray as xr\n", + "\n", + "\n", + "#GET DEPENDENCIES\n", + "\n", + " # Generate absolute path for the dependencies folder\n", + "dependencies_dir = Path.cwd() / 'dependencies'\n", + "dependencies_dir.mkdir(exist_ok=True, parents=True)\n", + "\n", + "base_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference'\n", + "dependency_name = \"wc_presto_onnx_dependencies.zip\"\n", + "\n", + "# Download and extract the model file\n", + "modelfile_url = f\"{base_url}/{dependency_name}\"\n", + "modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name)\n", + "shutil.unpack_archive(modelfile, extract_dir=dependencies_dir)\n", + "\n", + "# Add the model directory to system path if it's not already there\n", + "abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0])\n", + "sys.path.append(abs_path)\n", + "\n", + "\n", + "# Get Data\n", + "url = \"https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc\"\n", + "filename = \"belgium_good_2020-12-01_2021-11-30.nc\"\n", + "\n", + "with requests.get(url, stream=True) as r:\n", + " r.raise_for_status()\n", + " with open(filename, 'wb') as f:\n", + " for chunk in r.iter_content(chunk_size=8192):\n", + " f.write(chunk)\n", + "\n", + "# Read the file into xarray\n", + "ds = xr.open_dataset(filename)\n", + "arr = ds.drop('crs').to_array(dim='bands')\n", + "orig_dims = list(arr.dims)\n", + "orig_dims.remove(\"t\")\n", + "\n", + "#Get Presto\n", + "from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost\n", + "\n", + "#bands: 19, t: 12y, : 100x: 100y\n", + "data_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc'\n", + "# Fetch the data from the URL\n", + "response = requests.get(data_url)\n", + "#100x100,128\n", + "presto_path = \"https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt\"\n", + "features = get_presto_features(arr, presto_path) \n", + "\n", + "#Get CATBOOST\n", + "CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx'\n", + "classification = classify_with_catboost(features, orig_dims, CATBOOST_PATH)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5203744b", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "data_array = np.array(classification)\n", + "\n", + "# Plot the data as an image\n", + "plt.imshow(data_array[0], cmap='gray') # Assuming it's a grayscale image\n", + "plt.colorbar() # Add a colorbar for reference\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/minimal_wc_presto/inference.py b/minimal_wc_presto/inference.py new file mode 100644 index 00000000..4707c3fe --- /dev/null +++ b/minimal_wc_presto/inference.py @@ -0,0 +1,120 @@ +#%% import require libraries +import logging +import numpy as np + +import xarray as xr +from openeo.udf import XarrayDataCube + +from mvp_wc_presto.world_cereal_inference import PrestoFeatureExtractor, WorldCerealPredictor + +#TODO; +#how do we expect out code the stay stabile when presto changes? + +from mvp_wc_presto.dataops import ( + BANDS_GROUPS_IDX, + NORMED_BANDS, +) +from mvp_wc_presto.presto import Presto + + +#% Mapping from original band names to Presto names +BAND_MAPPING = { + "B02": "B2", + "B03": "B3", + "B04": "B4", + "B05": "B5", + "B06": "B6", + "B07": "B7", + "B08": "B8", + "B8A": "B8A", + "B11": "B11", + "B12": "B12", + "VH": "VH", + "VV": "VV", + "precipitation-flux": "total_precipitation", + "temperature-mean": "temperature_2m", +} + +# Index to band groups mapping +IDX_TO_BAND_GROUPS = { + NORMED_BANDS[idx]: band_group_idx + for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) + for idx in val +} + +def _setup_logging(): + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + return logger + + +def get_presto_features(inarr: xr.DataArray, presto_path: str) -> xr.DataArray: + """ + Extracts features from input data using Presto. + + Args: + inarr (xr.DataArray): Input data as xarray DataArray. + presto_path (str): Path to the pretrained Presto model. + + Returns: + xr.DataArray: Extracted features as xarray DataArray. + """ + logger = _setup_logging() + logger.info("Extracting features using Presto ...") + presto_model = Presto.load_pretrained(model_path=presto_path, strict=False) + presto_extractor = PrestoFeatureExtractor(presto_model) + logger.warning("EPSG is hardcoded to 32631 for the time being!") + features = presto_extractor.extract_presto_features(inarr, epsg=32631) + return features + + +def classify_with_catboost(features: np.ndarray, orig_dims: list, model_path: str) -> xr.DataArray: + """ + Classifies features using the WorldCereal CatBoost model. + + Args: + features (np.ndarray): Features to be classified. + orig_dims (list): Original dimensions of the input data. + model_path (str): Path to the trained CatBoost model. + + Returns: + xr.DataArray: Classified data as xarray DataArray. + """ + logger = _setup_logging() + logger.info("Predicting class using WorldCereal CatBoost model ...") + + predictor = WorldCerealPredictor() + predictor.load_model(model_path) + predictions = predictor.predict(features) + result_da = predictions.to_xarray().to_array(dim="bands").rename({"lon": "x", "lat": "y"}) + result_da = result_da.transpose(*orig_dims) + result_da = result_da.squeeze('bands') + + return result_da + + + +def apply_datacube(cube: XarrayDataCube) -> XarrayDataCube: + logger = _setup_logging() + logger.info("Applying datacube...") + + inarr = cube.get_array() + + PRESTO_PATH = './model/presto.pt' + CATBOOST_PATH = './model/wc_catboost.onnx' + + orig_dims = list(inarr.dims) + orig_dims.remove("t") + + features = get_presto_features(inarr, PRESTO_PATH) + classification = classify_with_catboost(features, orig_dims, CATBOOST_PATH) # Corrected variable name + + return XarrayDataCube(classification) + + +#test_inference_catboost_presto() + + + + + diff --git a/minimal_wc_presto/preprocessing.py b/minimal_wc_presto/preprocessing.py new file mode 100644 index 00000000..9b295210 --- /dev/null +++ b/minimal_wc_presto/preprocessing.py @@ -0,0 +1,509 @@ +from openeo.processes import array_create, if_, is_nodata, power +from openeo.rest.datacube import DataCube + +COMPOSITE_WINDOW = "month" + + +def get_S1_bands( + connection, + S1_collection, + bbox, + start, + end, + other_bands=None, + preprocess=True, + **processing_options, +): + """Method to add S1 bands to datacube + + Args: + S1_collection (str): name of the S1 collection + other_bands (DataCube): OpenEO datacube to add bands to + + Available processing_options: + s1_orbitdirection + provider + target_epsg + """ + isCreo = "creo" in processing_options.get("provider", "").lower() + orbit_direction = processing_options.get("s1_orbitdirection", None) + composite_window = processing_options.get("composite_window", COMPOSITE_WINDOW) + + # TODO: implement as needed + # if isCreo: + # orbit_direction = catalogue_check_S1(orbit_direction, start, end, bbox) + + if orbit_direction is not None: + properties = { + "sat:orbit_state": lambda orbdir: orbdir == orbit_direction + } # NOQA + else: + properties = {} + + # Load collection + S1bands = connection.load_collection( + S1_collection, + bands=["VH", "VV"], + spatial_extent=bbox, + temporal_extent=[start, end], + properties=properties, + ) + + if S1_collection == "SENTINEL1_GRD": + # compute backscatter if starting from raw GRD, + # otherwise assume preprocessed backscatter + S1bands = S1bands.sar_backscatter( + coefficient="sigma0-ellipsoid", + local_incidence_angle=False, + # DO NOT USE MAPZEN + elevation_model="COPERNICUS_30" if isCreo else None, + options={ + "implementation_version": "2", + "tile_size": 256, + "otb_memory": 1024, + "debug": False, + "elev_geoid": "/opt/openeo-vito-aux-data/egm96.tif", + }, + ) + else: + pass + + # Resample to the S2 spatial resolution + target_epsg = processing_options.get("target_epsg", None) + if target_epsg is not None: + S1bands = S1bands.resample_spatial(projection=target_epsg, resolution=10.0) + + if preprocess: + + # Composite to compositing window + S1bands = S1bands.aggregate_temporal_period( + period=composite_window, reducer="mean" + ) + + # # Linearly interpolate missing values + # Assume Presto handles nodata natively + # S1bands = S1bands.apply_dimension( + # dimension="t", process="array_interpolate_linear" + # ) + + # Scale to int16 + if isCreo: + # for CREO, rescaling also replaces nodata introduced by orfeo + # with a low value + # https://github.com/Open-EO/openeo-geopyspark-driver/issues/293 + # TODO: check if nodata is correctly handled in Orfeo + S1bands = S1bands.apply_dimension( + dimension="bands", + process=lambda x: array_create( + [ + if_( + is_nodata(x[0]), + 1, + power(base=10, p=(10.0 * x[0].log(base=10) + 83.0) / 20.0), + ), + if_( + is_nodata(x[1]), + 1, + power(base=10, p=(10.0 * x[1].log(base=10) + 83.0) / 20.0), + ), + ] + ), + ) + else: + S1bands = S1bands.apply_dimension( + dimension="bands", + process=lambda x: array_create( + [ + power(base=10, p=(10.0 * x[0].log(base=10) + 83.0) / 20.0), + power(base=10, p=(10.0 * x[1].log(base=10) + 83.0) / 20.0), + ] + ), + ) + + S1bands = S1bands.linear_scale_range(1, 65534, 1, 65534) + + # -------------------------------------------------------------------- + # Merge cubes + # -------------------------------------------------------------------- + if other_bands is None: + return S1bands + else: + merged_inputs = other_bands.resample_cube_spatial(S1bands).merge_cubes(S1bands) + return merged_inputs + + +def get_S2_bands( + connection, + S2_collection, + bbox, + start, + end, + masking, + preprocess=True, + other_bands=None, + target_epsg=None, + **processing_options, +): + """Method to get S2 bands and optionally merge with other bands + + Args: + S2_collection (str): name of the S2 collection + other_bands (DataCube): OpenEO datacube to add bands to + + Available processing_options: + s1_orbitdirection + provider + target_epsg + """ + + composite_window = processing_options.get("composite_window", COMPOSITE_WINDOW) + + S2_bands = ["B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B11", "B12"] + if masking not in ["satio", "mask_scl_dilation", None]: + raise ValueError(f"Unknown masking option `{masking}`") + if masking in ["mask_scl_dilation"]: + # Need SCL band to mask + S2_bands.append("SCL") + bands = connection.load_collection( + S2_collection, + bands=S2_bands, + spatial_extent=bbox, + temporal_extent=[start, end], + max_cloud_cover=95, + ) + + # TODO: implement as needed + # S2URL creo only accepts request in EPSG:4326 + # isCreo = "creo" in processing_options.get("provider", "").lower() + # if isCreo: + # catalogue_check_S2(start, end, bbox) + + # NOTE: For now we mask again snow/ice because clouds + # are sometimes marked as SCL value 11! + if masking == "mask_scl_dilation": + # TODO: double check cloud masking parameters + # https://github.com/Open-EO/openeo-geotrellis-extensions/blob/develop/geotrellis-common/src/main/scala/org/openeo/geotrelliscommon/CloudFilterStrategy.scala#L54 # NOQA + bands = bands.process( + "mask_scl_dilation", + data=bands, + scl_band_name="SCL", + kernel1_size=17, + kernel2_size=77, + mask1_values=[2, 4, 5, 6, 7], + mask2_values=[3, 8, 9, 10, 11], + erosion_kernel_size=3, + ).filter_bands(bands.metadata.band_names[:-1]) + #elif masking == "satio": + # Apply satio-based mask + # mask = scl_mask_erode_dilate( + # connection, + # bbox, + # scl_layer_band=S2_collection + ":SCL", + # target_epsg=target_epsg, + # ).resample_cube_spatial(bands) + # bands = bands.mask(mask) + + if preprocess: + # Composite to compositing window + bands = bands.aggregate_temporal_period( + period=composite_window, reducer="median" + ) + # bands = max_ndvi_composite(bands, composite_window=composite_window) + + # TODO: if we would disable it here, nodata values + # will be 65535 and we need to cope with that later + # Linearly interpolate missing values + # bands = bands.apply_dimension(dimension="t", process="array_interpolate_linear") + + # Force UINT16 to avoid overflow issue with S2 data + bands = bands.linear_scale_range(0, 65534, 0, 65534) + + # -------------------------------------------------------------------- + # Merge cubes + # -------------------------------------------------------------------- + if other_bands is None: + return bands + else: + merged_inputs = other_bands.resample_cube_spatial(bands).merge_cubes(bands) + return merged_inputs + + +def get_DEM(connection, DEM_collection, bbox, other_bands=None, **processing_options): + """Method to add DEM to datacube + + Args: + connection (_type_): _description_ + DEM_collection (str): Name of DEM collection + other_bands (DataCube): DataCube to merge DEM into + bbox (_type_): _description_ + + Returns: + DataCube: merged datacube + """ + + dem = connection.load_collection( + DEM_collection, + spatial_extent=bbox, + ) + + # Resample to the S2 spatial resolution + target_epsg = processing_options.get("target_epsg", None) + if target_epsg is not None: + dem = dem.resample_spatial( + projection=target_epsg, resolution=10.0, method="cubic" + ) + + # collection has timestamps which we need to get rid of + dem = dem.max_time() + + # -------------------------------------------------------------------- + # Merge cubes + # -------------------------------------------------------------------- + if other_bands is None: + return dem + else: + merged_inputs = other_bands.merge_cubes(dem) + return merged_inputs + + +def get_meteo( + connection, + METEO_collection, + bbox, + start, + end, + other_bands=None, + target_epsg=None, + **processing_options, +): + # AGERA5 + composite_window = processing_options.get("composite_window", COMPOSITE_WINDOW) + + meteo = connection.load_collection( + METEO_collection, + spatial_extent=bbox, + bands=["temperature-mean", "precipitation-flux"], + temporal_extent=[start, end], + ) + + if target_epsg is not None: + meteo = meteo.resample_spatial( + projection=target_epsg, resolution=10.0, method="bilinear" + ) + + # Composite to desired window. we want to aggregate data with + # different reducers. sum for precipitation within a month and + # mean for the temperature + meteo_temp = meteo.filter_bands(bands=["temperature-mean"]) + meteo_temp = meteo_temp.aggregate_temporal_period( + period=composite_window, reducer="mean" + ) + meteo_temp = meteo_temp.apply_dimension( + dimension="t", process="array_interpolate_linear" + ) + + meteo_prec = meteo.filter_bands(bands=["precipitation-flux"]) + meteo_prec = meteo_prec.aggregate_temporal_period( + period=composite_window, reducer="sum" + ) + meteo_prec = meteo_prec.apply_dimension( + dimension="t", process="array_interpolate_linear" + ) + + meteo = meteo_temp.merge_cubes(meteo_prec) + + # -------------------------------------------------------------------- + # Merge cubes + # or return just meteo + # -------------------------------------------------------------------- + if other_bands is None: + return meteo + else: + merged_inputs = other_bands.merge_cubes(meteo) + return merged_inputs + + +def add_worldcereral_labels(connection, bbox, other_bands): + """ + ['ESA_WORLDCEREAL_ACTIVECROPLAND', + 'ESA_WORLDCEREAL_IRRIGATION', + 'ESA_WORLDCEREAL_TEMPORARYCROPS', + 'ESA_WORLDCEREAL_WINTERCEREALS', + 'ESA_WORLDCEREAL_MAIZE', + 'ESA_WORLDCEREAL_SPRINGCEREALS'] + """ + + temporal = ("2020-09-01T00:00:00Z", "2021-12-31T00:00:00Z") + + # Get temporary crops layer + temporarycrops = ( + connection.load_collection( + "ESA_WORLDCEREAL_TEMPORARYCROPS", + temporal_extent=temporal, + spatial_extent=bbox, + bands=["CLASSIFICATION"], + ) + .rename_labels("bands", ["worldcereal_cropland"]) + .max_time() + ) + temporarycrops = temporarycrops.resample_cube_spatial(other_bands, method="near") + other_bands = other_bands.merge_cubes(temporarycrops) + + # Get maize layer + maize = ( + connection.load_collection( + "ESA_WORLDCEREAL_MAIZE", + temporal_extent=temporal, + spatial_extent=bbox, + bands=["CLASSIFICATION"], + ) + .rename_labels("bands", ["worldcereal_maize"]) + .max_time() + ) + maize = maize.resample_cube_spatial(other_bands, method="near") + other_bands = other_bands.merge_cubes(maize) + + # Get wintercereals layer + wintercereals = ( + connection.load_collection( + "ESA_WORLDCEREAL_WINTERCEREALS", + temporal_extent=temporal, + spatial_extent=bbox, + bands=["CLASSIFICATION"], + ) + .rename_labels("bands", ["worldcereal_wintercereals"]) + .max_time() + ) + wintercereals = wintercereals.resample_cube_spatial(other_bands, method="near") + other_bands = other_bands.merge_cubes(wintercereals) + + # # Get springcereals layer + # springcereals = ( + # connection.load_collection( + # "ESA_WORLDCEREAL_SPRINGCEREALS", + # temporal_extent=temporal, + # spatial_extent=bbox, + # bands=["CLASSIFICATION"], + # ) + # .rename_labels("bands", ["worldcereal_springcereals"]) + # .max_time() + # ) + # springcereals = springcereals.resample_cube_spatial(other_bands, method="near") + # other_bands = other_bands.merge_cubes(springcereals) + + return other_bands + + +def worldcereal_preprocessed_inputs( + connection, + bbox, + start: str, + end: str, + S2_collection="SENTINEL2_L2A", + S1_collection="SENTINEL1_GRD", + DEM_collection="COPERNICUS_30", + METEO_collection="AGERA5", + preprocess=True, + masking="mask_scl_dilation", + worldcereal_labels=False, + **processing_options, +) -> DataCube: + """Main method to get preprocessed inputs from OpenEO for + downstream crop type mapping. + + Args: + connection: OpenEO connection instance + bbox (_type_): _description_ + start (str): Start date for requested input data (yyyy-mm-dd) + end (str): Start date for requested input data (yyyy-mm-dd) + S2_collection (str, optional): Collection name for S2 data. + Defaults to + 'TERRASCOPE_S2_TOC_V2'. + S1_collection (str, optional): Collection name for S1 data. + Defaults to + 'SENTINEL1_GRD'. + DEM_collection (str, optional): Collection name for DEM data. + Defaults to + 'COPERNICUS_30'. + METEO_collection (str, optional): Collection name for + meteo data. Defaults to 'AGERA5'. + preprocess (bool, optional): Apply compositing and interpolation. + Defaults to True. + masking (str, optional): Masking method to be applied. + One of ['satio', 'mask_scl_dilation', None] + Defaults to 'mask_scl_dilation'. + worldcereal_labels (bool, optional): If True, worldcereal 2021 labels + will be added to the datacube. Defaults to False. + + Returns: + DataCube: OpenEO DataCube wich the requested inputs + """ + + bands = None + + # -------------------------------------------------------------------- + # Optical data + # -------------------------------------------------------------------- + + if S2_collection is not None: + bands = get_S2_bands( + connection, + S2_collection, + bbox, + start, + end, + masking, + preprocess=preprocess, + **processing_options, + ) + + # -------------------------------------------------------------------- + # AGERA5 Meteo data + # -------------------------------------------------------------------- + if METEO_collection is not None: + bands = get_meteo( + connection, + METEO_collection, + bbox, + start, + end, + other_bands=bands, + **processing_options, + ) + + # -------------------------------------------------------------------- + # SAR data + # -------------------------------------------------------------------- + if S1_collection is not None: + bands = get_S1_bands( + connection, + S1_collection, + bbox, + start, + end, + other_bands=bands, + **processing_options, + ) + + bands = bands.filter_temporal(start, end) + + # -------------------------------------------------------------------- + # DEM data + # -------------------------------------------------------------------- + if DEM_collection is not None: + bands = get_DEM(connection, DEM_collection, bbox, bands, **processing_options) + + # -------------------------------------------------------------------- + # Worldcereal labels + # -------------------------------------------------------------------- + if worldcereal_labels: + bands = add_worldcereral_labels(connection, bbox, bands) + + # forcing 16bit + bands = bands.linear_scale_range(0, 65534, 0, 65534) + + return bands + + +def worldcereal_raw_inputs(*args, **kwargs): + return worldcereal_preprocessed_inputs(*args, **kwargs, preprocess=False) diff --git a/minimal_wc_presto/testing.py b/minimal_wc_presto/testing.py new file mode 100644 index 00000000..0ad2261c --- /dev/null +++ b/minimal_wc_presto/testing.py @@ -0,0 +1,21 @@ +def test_inference_catboost_presto(): + # Load the result and ground truth + ds = xr.open_dataset("./data/belgium_good_2020-12-01_2021-11-30.nc", engine='netcdf4') + + # Because we downloaded the data, we need to resolve + # an issue with the CRS which has become a band. Let's get rid of it + arr = ds.drop('crs').to_array(dim='bands') + + # Make an OpenEO datacube of this array + udf_cube = XarrayDataCube(arr) + result_cube = apply_datacube(udf_cube) + + # Save the result to NetCDF + result_cube.array.to_netcdf("./data/test_result.nc") + results = result_cube.array.values.squeeze() + + # to a numpy array + gt_dataset = xr.open_dataset("./data/worldcereal_result.nc", engine='netcdf4') + data_variable = gt_dataset['__xarray_dataarray_variable__'] + gt = data_variable.values[0] + assert np.array_equal(results, gt) \ No newline at end of file diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py new file mode 100644 index 00000000..ef9e1905 --- /dev/null +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -0,0 +1,96 @@ +import logging +import urllib.request +import shutil +from pathlib import Path +import sys +import functools +import xarray as xr +from typing import Dict +from openeo.metadata import CollectionMetadata + + +def _setup_logging(): + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + return logger + +@functools.lru_cache(maxsize=6) +def extract_dependencies(base_url: str, dependency_name: str): + + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / 'dependencies' + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) + + return(abs_path) + + +def apply_metadata(input_metadata:CollectionMetadata, context:dict) -> CollectionMetadata: + + xstep = input_metadata.get('x','step') + ystep = input_metadata.get('y','step') + + + new_metadata = { + "x": {"type": "spatial", "axis": "x", "step": xstep, "reference_system": 4326}, + "y": {"type": "spatial", "axis": "y", "step": ystep, "reference_system": 4326}, + } + return CollectionMetadata(new_metadata) + + +def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: + + logger = _setup_logging() + + +# Install PyTorch using pip + + orig_dims = list(cube.dims) + orig_dims.remove("t") + + logger.info("Unzipping dependencies") + base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" + dependency_name = "wc_presto_onnx_dependencies.zip" + + logger.info("Appending depencency") + dep_dir = extract_dependencies(base_url, dependency_name) + sys.path.append(str(dep_dir)) + + + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost + + logger.info("Reading in required libs") + + logger.info("Extracting presto features") + PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" + features = get_presto_features(cube, PRESTO_PATH) + + logger.info("Catboost classification") + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" + classification = classify_with_catboost(features, orig_dims, CATBOOST_PATH) + + + return classification + + + + + + + + + + + + + From c749633354478b7cf38bbdeea4e513d7f30c2ea2 Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Mon, 6 May 2024 19:16:11 +0200 Subject: [PATCH 02/31] minimal presto functionality --- minimal_wc_presto/mvp_wc_presto/__init__.py | 0 minimal_wc_presto/mvp_wc_presto/dataops.py | 165 ++++ minimal_wc_presto/mvp_wc_presto/dataset.py | 385 ++++++++ minimal_wc_presto/mvp_wc_presto/masking.py | 149 +++ minimal_wc_presto/mvp_wc_presto/presto.py | 873 ++++++++++++++++++ minimal_wc_presto/mvp_wc_presto/utils.py | 162 ++++ .../mvp_wc_presto/world_cereal_inference.py | 379 ++++++++ 7 files changed, 2113 insertions(+) create mode 100644 minimal_wc_presto/mvp_wc_presto/__init__.py create mode 100644 minimal_wc_presto/mvp_wc_presto/dataops.py create mode 100644 minimal_wc_presto/mvp_wc_presto/dataset.py create mode 100644 minimal_wc_presto/mvp_wc_presto/masking.py create mode 100644 minimal_wc_presto/mvp_wc_presto/presto.py create mode 100644 minimal_wc_presto/mvp_wc_presto/utils.py create mode 100644 minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py diff --git a/minimal_wc_presto/mvp_wc_presto/__init__.py b/minimal_wc_presto/mvp_wc_presto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/minimal_wc_presto/mvp_wc_presto/dataops.py b/minimal_wc_presto/mvp_wc_presto/dataops.py new file mode 100644 index 00000000..fbc7e58c --- /dev/null +++ b/minimal_wc_presto/mvp_wc_presto/dataops.py @@ -0,0 +1,165 @@ +# This file contains many of the constants +# defined in presto/dataops +import warnings +from collections import OrderedDict +from typing import List +from typing import OrderedDict as OrderedDictType + +import numpy as np +import torch + +""" +For easier normalization of the band values (instead of needing to recompute +the normalization dict with the addition of new data), we provide maximum +values for each band +""" +S1_BANDS = ["VV", "VH"] +# EarthEngine estimates Sentinel-1 values range from -50 to 1 +S1_SHIFT_VALUES = [25.0, 25.0] +S1_DIV_VALUES = [25.0, 25.0] +S2_BANDS = [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B8A", + "B9", + "B10", + "B11", + "B12", +] +S2_SHIFT_VALUES = [float(0.0)] * len(S2_BANDS) +S2_DIV_VALUES = [float(1e4)] * len(S2_BANDS) +ERA5_BANDS = ["temperature_2m", "total_precipitation"] +# for temperature, shift to celcius and then divide by 35 based on notebook (ranges from) +# 37 to -22 degrees celcius +# For rainfall, based on +# https://github.com/nasaharvest/lem/blob/main/notebooks/exploratory_data_analysis.ipynb +ERA5_SHIFT_VALUES = [-272.15, 0.0] +ERA5_DIV_VALUES = [35.0, 0.03] +SRTM_BANDS = ["elevation", "slope"] +# visually gauged 90th percentile from +# https://github.com/nasaharvest/lem/blob/main/notebooks/exploratory_data_analysis.ipynb +SRTM_SHIFT_VALUES = [0.0, 0.0] +SRTM_DIV_VALUES = [2000.0, 50.0] + +DYNAMIC_BANDS = S1_BANDS + S2_BANDS + ERA5_BANDS +STATIC_BANDS = SRTM_BANDS + +DYNAMIC_BANDS_SHIFT = S1_SHIFT_VALUES + S2_SHIFT_VALUES + ERA5_SHIFT_VALUES +DYNAMIC_BANDS_DIV = S1_DIV_VALUES + S2_DIV_VALUES + ERA5_DIV_VALUES + +STATIC_BANDS_SHIFT = SRTM_SHIFT_VALUES +STATIC_BANDS_DIV = SRTM_DIV_VALUES + +# These bands are what is created by the Engineer. If the engineer changes, the bands +# here will need to change (and vice versa) +REMOVED_BANDS = ["B1", "B10"] +RAW_BANDS = DYNAMIC_BANDS + STATIC_BANDS + +BANDS = [x for x in DYNAMIC_BANDS if x not in REMOVED_BANDS] + STATIC_BANDS + ["NDVI"] +# NDVI is between 0 and 1 +ADD_BY = ( + [DYNAMIC_BANDS_SHIFT[i] for i, x in enumerate(DYNAMIC_BANDS) if x not in REMOVED_BANDS] + + STATIC_BANDS_SHIFT + + [0.0] +) +DIVIDE_BY = ( + [DYNAMIC_BANDS_DIV[i] for i, x in enumerate(DYNAMIC_BANDS) if x not in REMOVED_BANDS] + + STATIC_BANDS_DIV + + [1.0] +) + +NUM_TIMESTEPS = 12 +NUM_ORG_BANDS = len(BANDS) +TIMESTEPS_IDX = list(range(NUM_TIMESTEPS)) + +NORMED_BANDS = [x for x in BANDS if x != "B9"] +NUM_BANDS = len(NORMED_BANDS) +BANDS_IDX = list(range(NUM_BANDS)) +BANDS_GROUPS_IDX: OrderedDictType[str, List[int]] = OrderedDict( + { + "S1": [NORMED_BANDS.index(b) for b in S1_BANDS], + "S2_RGB": [NORMED_BANDS.index(b) for b in ["B2", "B3", "B4"]], + "S2_Red_Edge": [NORMED_BANDS.index(b) for b in ["B5", "B6", "B7"]], + "S2_NIR_10m": [NORMED_BANDS.index(b) for b in ["B8"]], + "S2_NIR_20m": [NORMED_BANDS.index(b) for b in ["B8A"]], + "S2_SWIR": [NORMED_BANDS.index(b) for b in ["B11", "B12"]], # Include B10? + "ERA5": [NORMED_BANDS.index(b) for b in ERA5_BANDS], + "SRTM": [NORMED_BANDS.index(b) for b in SRTM_BANDS], + "NDVI": [NORMED_BANDS.index("NDVI")], + } +) + +BAND_EXPANSION = [len(x) for x in BANDS_GROUPS_IDX.values()] +SRTM_INDEX = list(BANDS_GROUPS_IDX.keys()).index("SRTM") + + +class DynamicWorld2020_2021: + class_amount = 9 + + @classmethod + def normalize(cls, x: np.ndarray) -> np.ndarray: + return x + + +class S1_S2_ERA5_SRTM: + @staticmethod + def calculate_ndvi(input_array): + r""" + Given an input array of shape [timestep, bands] or [batches, timesteps, shapes] + where bands == len(bands), returns an array of shape + [timestep, bands + 1] where the extra band is NDVI, + (b08 - b04) / (b08 + b04) + """ + band_1, band_2 = "B8", "B4" + + num_dims = len(input_array.shape) + if num_dims == 2: + band_1_np = input_array[:, NORMED_BANDS.index(band_1)] + band_2_np = input_array[:, NORMED_BANDS.index(band_2)] + elif num_dims == 3: + band_1_np = input_array[:, :, NORMED_BANDS.index(band_1)] + band_2_np = input_array[:, :, NORMED_BANDS.index(band_2)] + else: + raise ValueError(f"Expected num_dims to be 2 or 3 - got {num_dims}") + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="invalid value encountered in true_divide") + # suppress the following warning + # RuntimeWarning: invalid value encountered in true_divide + # for cases where near_infrared + red == 0 + # since this is handled in the where condition + if isinstance(band_1_np, np.ndarray): + return np.where( + (band_1_np + band_2_np) > 0, + (band_1_np - band_2_np) / (band_1_np + band_2_np), + 0, + ) + else: + return torch.where( + (band_1_np + band_2_np) > 0, + (band_1_np - band_2_np) / (band_1_np + band_2_np), + 0, + ) + + @classmethod + def normalize(cls, x): + # remove the b9 band + keep_indices = [idx for idx, val in enumerate(BANDS) if val != "B9"] + if isinstance(x, np.ndarray): + x = ((x + ADD_BY) / DIVIDE_BY).astype(np.float32) + else: + x = (x + torch.tensor(ADD_BY)) / torch.tensor(DIVIDE_BY) + + if len(x.shape) == 2: + x = x[:, keep_indices] + x[:, NORMED_BANDS.index("NDVI")] = cls.calculate_ndvi(x) + else: + x = x[:, :, keep_indices] + x[:, :, NORMED_BANDS.index("NDVI")] = cls.calculate_ndvi(x) + return x diff --git a/minimal_wc_presto/mvp_wc_presto/dataset.py b/minimal_wc_presto/mvp_wc_presto/dataset.py new file mode 100644 index 00000000..a465f876 --- /dev/null +++ b/minimal_wc_presto/mvp_wc_presto/dataset.py @@ -0,0 +1,385 @@ +import logging +from datetime import datetime +from math import modf +from pathlib import Path +from random import sample +from typing import Callable, Dict, List, Optional, Tuple, cast + +import geopandas as gpd +import numpy as np +import pandas as pd +import rioxarray +import xarray as xr +from einops import rearrange, repeat +from pyproj import Transformer +from sklearn.utils.class_weight import compute_class_weight +from torch.utils.data import Dataset + +from .dataops import ( + BANDS, + BANDS_GROUPS_IDX, + NORMED_BANDS, + S1_S2_ERA5_SRTM, + DynamicWorld2020_2021, +) +from .masking import BAND_EXPANSION, MaskedExample, MaskParamsNoDw +from .utils import DEFAULT_SEED, data_dir, load_world_df + +logger = logging.getLogger("__main__") + +IDX_TO_BAND_GROUPS = {} +for band_group_idx, (key, val) in enumerate(BANDS_GROUPS_IDX.items()): + for idx in val: + IDX_TO_BAND_GROUPS[NORMED_BANDS[idx]] = band_group_idx + + +class WorldCerealBase(Dataset): + _NODATAVALUE = 65535 + NUM_TIMESTEPS = 12 + BAND_MAPPING = { + "OPTICAL-B02-ts{}-10m": "B2", + "OPTICAL-B03-ts{}-10m": "B3", + "OPTICAL-B04-ts{}-10m": "B4", + "OPTICAL-B05-ts{}-20m": "B5", + "OPTICAL-B06-ts{}-20m": "B6", + "OPTICAL-B07-ts{}-20m": "B7", + "OPTICAL-B08-ts{}-10m": "B8", + "OPTICAL-B8A-ts{}-20m": "B8A", + "OPTICAL-B11-ts{}-20m": "B11", + "OPTICAL-B12-ts{}-20m": "B12", + "SAR-VH-ts{}-20m": "VH", + "SAR-VV-ts{}-20m": "VV", + "METEO-precipitation_flux-ts{}-100m": "total_precipitation", + "METEO-temperature_mean-ts{}-100m": "temperature_2m", + } + STATIC_BAND_MAPPING = {"DEM-alt-20m": "elevation", "DEM-slo-20m": "slope"} + + def __init__(self, dataframe: pd.DataFrame): + self.df = dataframe + + def __len__(self): + return self.df.shape[0] + + @staticmethod + def target_crop(row_d: Dict) -> int: + # by default, we predict crop vs non crop + return int(row_d["LANDCOVER_LABEL"] == 11) + + @classmethod + def row_to_arrays( + cls, row: pd.Series, target_function: Callable[[Dict], int] + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float, int]: + # https://stackoverflow.com/questions/45783891/is-there-a-way-to-speed-up-the-pandas-getitem-getitem-axis-and-get-label + # This is faster than indexing the series every time! + row_d = pd.Series.to_dict(row) + + latlon = np.array([row_d["lat"], row_d["lon"]], dtype=np.float32) + month = datetime.strptime(row_d["start_date"], "%Y-%m-%d").month - 1 + + eo_data = np.zeros((cls.NUM_TIMESTEPS, len(BANDS))) + # an assumption we make here is that all timesteps for a token + # have the same masking + mask = np.zeros((cls.NUM_TIMESTEPS, len(BANDS_GROUPS_IDX))) + for df_val, presto_val in cls.BAND_MAPPING.items(): + values = np.array([float(row_d[df_val.format(t)]) for t in range(cls.NUM_TIMESTEPS)]) + # this occurs for the DEM values in one point in Fiji + values = np.nan_to_num(values, nan=cls._NODATAVALUE) + idx_valid = values != cls._NODATAVALUE + if presto_val in ["VV", "VH"]: + # convert to dB + idx_valid = idx_valid & (values > 0) + values[idx_valid] = 20 * np.log10(values[idx_valid]) - 83 + elif presto_val == "total_precipitation": + # scaling, and AgERA5 is in mm, Presto expects m + values[idx_valid] = values[idx_valid] / (100 * 1000.0) + elif presto_val == "temperature_2m": + # remove scaling + values[idx_valid] = values[idx_valid] / 100 + mask[:, IDX_TO_BAND_GROUPS[presto_val]] += ~idx_valid + eo_data[:, BANDS.index(presto_val)] = values + for df_val, presto_val in cls.STATIC_BAND_MAPPING.items(): + # this occurs for the DEM values in one point in Fiji + values = np.nan_to_num(row_d[df_val], nan=cls._NODATAVALUE) + idx_valid = values != cls._NODATAVALUE + eo_data[:, BANDS.index(presto_val)] = values + mask[:, IDX_TO_BAND_GROUPS[presto_val]] += ~idx_valid + + return ( + cls.check(eo_data), + mask.astype(bool), + latlon, + month, + target_function(row_d), + ) + + def __getitem__(self, idx): + raise NotImplementedError + + @classmethod + def normalize_and_mask(cls, eo: np.ndarray): + # TODO: this can be removed + keep_indices = [idx for idx, val in enumerate(BANDS) if val != "B9"] + normed_eo = S1_S2_ERA5_SRTM.normalize(eo) + # TODO: fix this. For now, we replicate the previous behaviour + normed_eo = np.where(eo[:, keep_indices] != cls._NODATAVALUE, normed_eo, 0) + return normed_eo + + @staticmethod + def check(array: np.ndarray) -> np.ndarray: + assert not np.isnan(array).any() + return array + + @staticmethod + def split_df( + df: pd.DataFrame, val_sample_ids: Optional[List[str]] = None, val_size: float = 0.2 + ) -> Tuple[pd.DataFrame, pd.DataFrame]: + if val_sample_ids is None: + logger.warning(f"No val_ids; randomly splitting {val_size} to the val set instead") + val, train = np.split( + df.sample(frac=1, random_state=DEFAULT_SEED), [int(val_size * len(df))] + ) + else: + is_val = df.sample_id.isin(val_sample_ids) + logger.info(f"Using {len(is_val) - sum(is_val)} train and {sum(is_val)} val samples") + train = df[~is_val] + val = df[is_val] + return train, val + + +class WorldCerealMaskedDataset(WorldCerealBase): + def __init__(self, dataframe: pd.DataFrame, mask_params: MaskParamsNoDw): + super().__init__(dataframe) + self.mask_params = mask_params + + def __getitem__(self, idx): + # Get the sample + row = self.df.iloc[idx, :] + eo, real_mask_per_token, latlon, month, _ = self.row_to_arrays(row, self.target_crop) + mask_eo, x_eo, y_eo, strat = self.mask_params.mask_data( + self.normalize_and_mask(eo), real_mask_per_token + ) + real_mask_per_variable = np.repeat(real_mask_per_token, BAND_EXPANSION, axis=1) + + dynamic_world = np.ones(self.NUM_TIMESTEPS) * (DynamicWorld2020_2021.class_amount) + mask_dw = np.full(self.NUM_TIMESTEPS, True) + y_dw = dynamic_world.copy() + return MaskedExample( + mask_eo, + mask_dw, + x_eo, + y_eo, + dynamic_world, + y_dw, + month, + latlon, + strat, + real_mask_per_variable, + ) + + +def filter_remove_noncrops(df: pd.DataFrame) -> pd.DataFrame: + crop_labels = [10, 11, 12, 13] + df = df.loc[df.LANDCOVER_LABEL.isin(crop_labels)] + return df + + +def target_maize(row_d) -> int: + # 1200 is maize + return int(row_d["CROPTYPE_LABEL"] == 1200) + + +class WorldCerealLabelledDataset(WorldCerealBase): + # 0: no information, 10: could be both annual or perennial + FILTER_LABELS = [0, 10] + + def __init__( + self, + dataframe: pd.DataFrame, + countries_to_remove: Optional[List[str]] = None, + years_to_remove: Optional[List[int]] = None, + target_function: Optional[Callable[[Dict], int]] = None, + balance: bool = False, + ): + dataframe = dataframe.loc[~dataframe.LANDCOVER_LABEL.isin(self.FILTER_LABELS)] + + if countries_to_remove is not None: + dataframe = self.join_with_world_df(dataframe) + for country in countries_to_remove: + assert dataframe.name.str.contains( + country + ).any(), f"Tried removing {country} but it is not in the dataframe" + dataframe = dataframe[(~dataframe.name.isin(countries_to_remove))] + if years_to_remove is not None: + dataframe["end_date"] = pd.to_datetime(dataframe.end_date) + dataframe = dataframe[(~dataframe.end_date.dt.year.isin(years_to_remove))] + self.target_function = target_function if target_function is not None else self.target_crop + self._class_weights: Optional[np.ndarray] = None + + super().__init__(dataframe) + if balance: + neg_indices, pos_indices = [], [] + for loc_idx, (_, row) in enumerate(self.df.iterrows()): + target = self.target_function(row.to_dict()) + if target == 0: + neg_indices.append(loc_idx) + else: + pos_indices.append(loc_idx) + if len(pos_indices) > len(neg_indices): + self.indices = pos_indices + (len(pos_indices) // len(neg_indices)) * neg_indices + elif len(neg_indices) > len(pos_indices): + self.indices = neg_indices + (len(neg_indices) // len(pos_indices)) * pos_indices + else: + self.indices = neg_indices + pos_indices + else: + self.indices = [i for i in range(len(self.df))] + + @staticmethod + def multiply_list_length_by_float(input_list: List, multiplier: float) -> List: + decimal_part, integer_part = modf(multiplier) + sublist = sample(input_list, k=int(len(input_list) * decimal_part)) + return input_list * int(integer_part) + sublist + + def __len__(self): + return len(self.indices) + + def __getitem__(self, idx): + # Get the sample + df_index = self.indices[idx] + row = self.df.iloc[df_index, :] + eo, mask_per_token, latlon, month, target = self.row_to_arrays(row, self.target_function) + mask_per_variable = np.repeat(mask_per_token, BAND_EXPANSION, axis=1) + return ( + self.normalize_and_mask(eo), + target, + np.ones(self.NUM_TIMESTEPS) * (DynamicWorld2020_2021.class_amount), + latlon, + month, + mask_per_variable, + ) + + @staticmethod + def join_with_world_df(dataframe: pd.DataFrame) -> pd.DataFrame: + world_df = load_world_df() + dataframe = gpd.GeoDataFrame( + data=dataframe, + geometry=gpd.GeoSeries.from_xy(x=dataframe.lon, y=dataframe.lat), + crs="EPSG:4326", + ) + # project to non geographic CRS, otherwise geopandas gives a warning + joined = gpd.sjoin_nearest( + dataframe.to_crs("EPSG:3857"), world_df.to_crs("EPSG:3857"), how="left" + ) + joined = joined[~joined.index.duplicated(keep="first")] + if joined.isna().any(axis=1).any(): + logger.warning("Some coordinates couldn't be matched to a country") + return joined.to_crs("EPSG:4326") + + @property + def class_weights(self) -> np.ndarray: + if self._class_weights is None: + ys = [] + for _, row in self.df.iterrows(): + ys.append(self.target_function(row.to_dict())) + self._class_weights = compute_class_weight( + class_weight="balanced", classes=np.unique(ys), y=ys + ) + return self._class_weights + + +class WorldCerealInferenceDataset(Dataset): + _NODATAVALUE = 65535 + Y = "worldcereal_cropland" + BAND_MAPPING = { + "B02": "B2", + "B03": "B3", + "B04": "B4", + "B05": "B5", + "B06": "B6", + "B07": "B7", + "B08": "B8", + # B8A is missing + "B11": "B11", + "B12": "B12", + "VH": "VH", + "VV": "VV", + "precipitation-flux": "total_precipitation", + "temperature-mean": "temperature_2m", + } + + def __init__(self, path_to_files: Path = data_dir / "inference_areas"): + self.path_to_files = path_to_files + self.all_files = list(self.path_to_files.glob("*.nc")) + + def __len__(self): + return len(self.all_files) + + @classmethod + def nc_to_arrays( + cls, filepath: Path + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + ds = cast(xr.Dataset, rioxarray.open_rasterio(filepath, decode_times=False)) + epsg_coords = ds.rio.crs.to_epsg() + + num_instances = len(ds.x) * len(ds.y) + num_timesteps = len(ds.t) + eo_data = np.zeros((num_instances, num_timesteps, len(BANDS))) + mask = np.zeros((num_instances, num_timesteps, len(BANDS_GROUPS_IDX))) + # for now, B8A is missing + mask[:, :, IDX_TO_BAND_GROUPS["B8A"]] = 1 + + for org_band, presto_val in cls.BAND_MAPPING.items(): + # flatten the values + values = np.swapaxes(ds[org_band].values.reshape((num_timesteps, -1)), 0, 1) + idx_valid = values != cls._NODATAVALUE + + if presto_val in ["VV", "VH"]: + # convert to dB + values = 20 * np.log10(values) - 83 + elif presto_val == "total_precipitation": + # scaling, and AgERA5 is in mm, Presto expects m + values = values / (100 * 1000.0) + elif presto_val == "temperature_2m": + # remove scaling + values = values / 100 + + eo_data[:, :, BANDS.index(presto_val)] = values + mask[:, :, IDX_TO_BAND_GROUPS[presto_val]] += ~idx_valid + + y = rearrange(ds[cls.Y].values, "t x y -> (x y) t") + # -1 because we index from 0 + start_month = (ds.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1) - 1 + months = np.ones((num_instances)) * start_month + + transformer = Transformer.from_crs(f"EPSG:{epsg_coords}", "EPSG:4326", always_xy=True) + lon, lat = transformer.transform(ds.x, ds.y) + + latlons = np.stack( + [np.repeat(lat, repeats=len(lon)), repeat(lon, "c -> (h c)", h=len(lat))], + axis=-1, + ) + + return eo_data, np.repeat(mask, BAND_EXPANSION, axis=-1), latlons, months, y + + def __getitem__(self, idx): + filepath = self.all_files[idx] + eo, mask, latlons, months, y = self.nc_to_arrays(filepath) + + dynamic_world = np.ones((eo.shape[0], eo.shape[1])) * (DynamicWorld2020_2021.class_amount) + + return S1_S2_ERA5_SRTM.normalize(eo), dynamic_world, mask, latlons, months, y + + @staticmethod + def combine_predictions( + latlons: np.ndarray, all_preds: np.ndarray, gt: np.ndarray, ndvi: np.ndarray + ) -> pd.DataFrame: + flat_lat, flat_lon = latlons[:, 0], latlons[:, 1] + if len(all_preds.shape) == 1: + all_preds = np.expand_dims(all_preds, axis=-1) + + data_dict: Dict[str, np.ndarray] = {"lat": flat_lat, "lon": flat_lon} + for i in range(all_preds.shape[1]): + prediction_label = f"prediction_{i}" + data_dict[prediction_label] = all_preds[:, i] + data_dict["ground_truth"] = gt[:, 0] + data_dict["ndvi"] = ndvi + return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) diff --git a/minimal_wc_presto/mvp_wc_presto/masking.py b/minimal_wc_presto/mvp_wc_presto/masking.py new file mode 100644 index 00000000..90d8f835 --- /dev/null +++ b/minimal_wc_presto/mvp_wc_presto/masking.py @@ -0,0 +1,149 @@ +from collections import namedtuple +from dataclasses import dataclass +from random import choice, randint, random, sample +from typing import Any, List, Tuple + +import numpy as np + +from .dataops import ( + BAND_EXPANSION, + BANDS_GROUPS_IDX, + NUM_TIMESTEPS, + SRTM_INDEX, + TIMESTEPS_IDX, +) + +MASK_STRATEGIES = ( + "group_bands", + "random_timesteps", + "chunk_timesteps", + "random_combinations", +) + +MaskedExample = namedtuple( + "MaskedExample", + [ + "mask_eo", + "mask_dw", + "x_eo", + "y_eo", + "x_dw", + "y_dw", + "start_month", + "latlon", + "strategy", + "real_mask", + ], +) + + +def make_mask_no_dw(strategy: str, mask_ratio: float, existing_mask: np.ndarray) -> np.ndarray: + """ + Make a mask for a given strategy and percentage of masked values. + Args: + strategy: The masking strategy to use. One of MASK_STRATEGIES + mask_ratio: The percentage of values to mask. Between 0 and 1. + """ + # we assume that topography is never "naturally" masked + mask = existing_mask.copy() + srtm_mask = False + num_tokens_to_mask = int( + ((NUM_TIMESTEPS * (len(BANDS_GROUPS_IDX) - 1)) + 1) * mask_ratio - sum(sum(mask)) + ) + assert num_tokens_to_mask > 0 + + def mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio): + should_flip = random() < mask_ratio + if should_flip: + srtm_mask = True + num_tokens_to_mask -= 1 + return srtm_mask, num_tokens_to_mask + + def random_masking(mask, num_tokens_to_mask: int): + if num_tokens_to_mask > 0: + # we set SRTM to be True - this way, it won't get randomly assigned. + # at the end of the function, it gets properly assigned + mask[:, SRTM_INDEX] = True + # then, we flatten the mask and dw arrays + all_tokens_mask = mask.flatten() + unmasked_tokens = all_tokens_mask == False + idx = np.flatnonzero(unmasked_tokens) + np.random.shuffle(idx) + idx = idx[:num_tokens_to_mask] + all_tokens_mask[idx] = True + mask = all_tokens_mask.reshape((NUM_TIMESTEPS, len(BANDS_GROUPS_IDX))) + return mask + + # RANDOM BANDS + if strategy == "random_combinations": + srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) + mask = random_masking(mask, num_tokens_to_mask) + + elif strategy == "group_bands": + srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) + # next, we figure out how many tokens we can mask + num_band_groups_to_mask = int(num_tokens_to_mask / NUM_TIMESTEPS) + assert (num_tokens_to_mask - NUM_TIMESTEPS * num_band_groups_to_mask) >= 0 + num_tokens_masked = 0 + # tuple because of mypy, which thinks lists can only hold one type + band_groups: List[Any] = list(range(len(BANDS_GROUPS_IDX))) + band_groups.remove(SRTM_INDEX) + band_groups_to_mask = sample(band_groups, num_band_groups_to_mask) + for band_group in band_groups_to_mask: + num_tokens_masked += int(len(mask[:, band_group]) - sum(mask[:, band_group])) + mask[:, band_group] = True + num_tokens_to_mask -= num_tokens_masked + mask = random_masking(mask, num_tokens_to_mask) + + # RANDOM TIMESTEPS + elif strategy == "random_timesteps": + srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) + # -1 for SRTM + timesteps_to_mask = int(num_tokens_to_mask / (len(BANDS_GROUPS_IDX) - 1)) + max_tokens_masked = (len(BANDS_GROUPS_IDX) - 1) * timesteps_to_mask + timesteps = sample(TIMESTEPS_IDX, k=timesteps_to_mask) + if timesteps_to_mask > 0: + num_tokens_to_mask -= int(max_tokens_masked - sum(sum(mask[timesteps]))) + mask[timesteps] = True + mask = random_masking(mask, num_tokens_to_mask) + elif strategy == "chunk_timesteps": + srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) + # -1 for SRTM + timesteps_to_mask = int(num_tokens_to_mask / (len(BANDS_GROUPS_IDX) - 1)) + if timesteps_to_mask > 0: + max_tokens_masked = (len(BANDS_GROUPS_IDX) - 1) * timesteps_to_mask + start_idx = randint(0, NUM_TIMESTEPS - timesteps_to_mask) + num_tokens_to_mask -= int( + max_tokens_masked - sum(sum(mask[start_idx : start_idx + timesteps_to_mask])) + ) + mask[start_idx : start_idx + timesteps_to_mask] = True # noqa + mask = random_masking(mask, num_tokens_to_mask) + else: + raise ValueError(f"Unknown strategy {strategy} not in {MASK_STRATEGIES}") + + mask[:, SRTM_INDEX] = srtm_mask + return np.repeat(mask, BAND_EXPANSION, axis=1) + + +@dataclass +class MaskParamsNoDw: + strategies: Tuple[str, ...] = ("NDVI",) + ratio: float = 0.5 + + def __post_init__(self): + for strategy in self.strategies: + assert strategy in [ + "group_bands", + "random_timesteps", + "chunk_timesteps", + "random_combinations", + ] + + def mask_data(self, eo_data: np.ndarray, mask: np.ndarray): + strategy = choice(self.strategies) + mask = make_mask_no_dw(strategy=strategy, mask_ratio=self.ratio, existing_mask=mask) + x = eo_data * ~mask + y = np.zeros(eo_data.shape).astype(np.float32) + y[mask] = eo_data[mask] + + return mask, x, y, strategy diff --git a/minimal_wc_presto/mvp_wc_presto/presto.py b/minimal_wc_presto/mvp_wc_presto/presto.py new file mode 100644 index 00000000..8eedb17f --- /dev/null +++ b/minimal_wc_presto/mvp_wc_presto/presto.py @@ -0,0 +1,873 @@ +import math +from copy import deepcopy +from pathlib import Path +from typing import Optional, Sized, Tuple, Union, cast + +import numpy as np +import torch +from einops import repeat +from torch import nn +from torch.jit import Final +from torch.nn import functional as F + +from .dataops import BANDS_GROUPS_IDX, DynamicWorld2020_2021 +from .utils import default_model_path, device + +import io +import requests + + +def param_groups_weight_decay(model: nn.Module, weight_decay=1e-5, no_weight_decay_list=()): + # https://github.com/huggingface/pytorch-image-models/blob/main/timm/optim/optim_factory.py + no_weight_decay_list = set(no_weight_decay_list) + decay = [] + no_decay = [] + for name, param in model.named_parameters(): + if not param.requires_grad: + continue + + if param.ndim <= 1 or name.endswith(".bias") or name in no_weight_decay_list: + no_decay.append(param) + else: + decay.append(param) + + return [ + {"params": no_decay, "weight_decay": 0.0}, + {"params": decay, "weight_decay": weight_decay}, + ] + + +def adjust_learning_rate(optimizer, epoch, warmup_epochs, total_epochs, max_lr, min_lr): + """Decay the learning rate with half-cycle cosine after warmup""" + if epoch < warmup_epochs: + lr = max_lr * epoch / warmup_epochs + else: + lr = min_lr + (max_lr - min_lr) * 0.5 * ( + 1.0 + math.cos(math.pi * (epoch - warmup_epochs) / (total_epochs - warmup_epochs)) + ) + for param_group in optimizer.param_groups: + if "lr_scale" in param_group: + # This is only used during finetuning, and not yet + # implemented in our codebase + param_group["lr"] = lr * param_group["lr_scale"] + else: + param_group["lr"] = lr + return lr + + +class LossWrapper(nn.Module): + def __init__(self, loss: nn.Module): + super().__init__() + self.loss = loss + + def forward(self, pred: torch.Tensor, true: torch.Tensor) -> torch.Tensor: + assert len(pred) == len(true) + if len(pred) == 0: + # len(pred) == 0 -> no inputs are masked, so no + # inputs are passed to the loss + return torch.tensor(0).float().to(device) + return self.loss(pred, true) + + +class Attention(nn.Module): + # https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py + fast_attn: Final[bool] + + def __init__( + self, + dim, + num_heads=8, + qkv_bias=False, + qk_norm=False, + attn_drop=0.0, + proj_drop=0.0, + norm_layer=nn.LayerNorm, + ): + super().__init__() + assert dim % num_heads == 0, "dim should be divisible by num_heads" + self.num_heads = num_heads + self.head_dim = dim // num_heads + self.scale = self.head_dim**-0.5 + self.fast_attn = hasattr(torch.nn.functional, "scaled_dot_product_attention") # FIXME + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() + self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + + def forward(self, x, attn_mask=None): + B, N, C = x.shape + qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4) + q, k, v = qkv.unbind(0) + q, k = self.q_norm(q), self.k_norm(k) + + if self.fast_attn: + if attn_mask is not None: + # todo check + attn_mask = attn_mask[:, None, None].repeat((1, self.num_heads, N, 1)) + x = F.scaled_dot_product_attention( + q, + k, + v, + # a value of True indicates that the element should take part in attention + attn_mask=attn_mask, + dropout_p=self.attn_drop.p, + ) + else: + if attn_mask is not None: + raise NotImplementedError + q = q * self.scale + attn = q @ k.transpose(-2, -1) + attn = attn.softmax(dim=-1) + attn = self.attn_drop(attn) + x = attn @ v + + x = x.transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class Mlp(nn.Module): + """MLP as used in Vision Transformer, MLP-Mixer and related networks""" + + def __init__( + self, + in_features, + hidden_features=None, + out_features=None, + act_layer=nn.GELU, + bias=True, + drop=0.0, + ): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + + self.fc1 = nn.Linear(in_features, hidden_features, bias=bias) + self.act = act_layer() + self.drop1 = nn.Dropout(drop) + self.fc2 = nn.Linear(hidden_features, out_features, bias=bias) + self.drop2 = nn.Dropout(drop) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop1(x) + x = self.fc2(x) + x = self.drop2(x) + return x + + +class LayerScale(nn.Module): + def __init__(self, dim, init_values=1e-5, inplace=False): + super().__init__() + self.inplace = inplace + self.gamma = nn.Parameter(init_values * torch.ones(dim)) + + def forward(self, x): + return x.mul_(self.gamma) if self.inplace else x * self.gamma + + +class Block(nn.Module): + def __init__( + self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + qk_norm=False, + drop=0.0, + attn_drop=0.0, + init_values=None, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + ): + super().__init__() + self.norm1 = norm_layer(dim) + self.attn = Attention( + dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_norm=qk_norm, + attn_drop=attn_drop, + proj_drop=drop, + norm_layer=norm_layer, + ) + self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + + self.norm2 = norm_layer(dim) + self.mlp = Mlp( + in_features=dim, + hidden_features=int(dim * mlp_ratio), + act_layer=act_layer, + drop=drop, + ) + self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + + def forward(self, x, attn_mask=None): + x = x + self.ls1(self.attn(self.norm1(x), attn_mask)) + x = x + self.ls2(self.mlp(self.norm2(x))) + return x + + +def get_sinusoid_encoding_table(positions, d_hid, T=1000): + """Sinusoid position encoding table + positions: int or list of integer, if int range(positions)""" + + if isinstance(positions, int): + positions = list(range(positions)) + + def cal_angle(position, hid_idx): + return position / np.power(T, 2 * (hid_idx // 2) / d_hid) + + def get_posi_angle_vec(position): + return [cal_angle(position, hid_j) for hid_j in range(d_hid)] + + sinusoid_table = np.array([get_posi_angle_vec(pos_i) for pos_i in positions]) + + sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i + sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1 + + if torch.cuda.is_available(): + return torch.FloatTensor(sinusoid_table).cuda() + else: + return torch.FloatTensor(sinusoid_table) + + +def get_month_encoding_table(d_hid): + """Sinusoid month encoding table, for 12 months indexed from 0-11""" + assert d_hid % 2 == 0 + angles = np.arange(0, 13) / (12 / (2 * np.pi)) + + sin_table = np.sin(np.stack([angles for _ in range(d_hid // 2)], axis=-1)) + cos_table = np.cos(np.stack([angles for _ in range(d_hid // 2)], axis=-1)) + month_table = np.concatenate([sin_table[:-1], cos_table[:-1]], axis=-1) + + if torch.cuda.is_available(): + return torch.FloatTensor(month_table).cuda() + else: + return torch.FloatTensor(month_table) + + +def month_to_tensor( + month: Union[torch.Tensor, int], batch_size: int, seq_len: int, device: torch.device +): + if isinstance(month, int): + assert cast(int, month) < 12 + else: + assert max(cast(torch.Tensor, month.flatten())) < 12 + + if isinstance(month, int): + # >>> torch.fmod(torch.tensor([9., 10, 11, 12, 13, 14]), 12) + # tensor([ 9., 10., 11., 0., 1., 2.]) + month = ( + torch.fmod(torch.arange(month, month + seq_len, dtype=torch.long), 12) + .expand(batch_size, seq_len) + .to(device) + ) + elif len(month.shape) == 1: + month = torch.stack( + [torch.fmod(torch.arange(m, m + seq_len, dtype=torch.long), 12) for m in month] + ).to(device) + return month + + +class Encoder(nn.Module): + def __init__( + self, + embedding_size: int = 128, + channel_embed_ratio: float = 0.25, + month_embed_ratio: float = 0.25, + depth=2, + mlp_ratio=2, + num_heads=8, + max_sequence_length=24, + ): + super().__init__() + + self.band_groups = BANDS_GROUPS_IDX + self.embedding_size = embedding_size + + # this is used for the channel embedding + self.band_group_to_idx = { + group_name: idx for idx, (group_name, _) in enumerate(self.band_groups.items()) + } + self.band_group_to_idx["dynamic_world"] = max(self.band_group_to_idx.values()) + 1 + + self.eo_patch_embed = nn.ModuleDict( + { + group_name: nn.Linear(len(group), embedding_size) + for group_name, group in self.band_groups.items() + } + ) + self.dw_embed = nn.Embedding( + num_embeddings=DynamicWorld2020_2021.class_amount + 1, embedding_dim=embedding_size + ) + self.latlon_embed = nn.Linear(3, embedding_size) + + self.blocks = nn.ModuleList( + [ + Block( + embedding_size, + num_heads, + mlp_ratio, + qkv_bias=True, + norm_layer=nn.LayerNorm, + ) + for _ in range(depth) + ] + ) + self.norm = nn.LayerNorm(embedding_size) + + # the positional + monthly + channel embedding + self.max_sequence_length = max_sequence_length + pos_embedding_size = int(embedding_size * (1 - (channel_embed_ratio + month_embed_ratio))) + channel_embedding_size = int(embedding_size * channel_embed_ratio) + month_embedding_size = int(embedding_size * month_embed_ratio) + self.pos_embed = nn.Parameter( + torch.zeros(1, max_sequence_length, pos_embedding_size), requires_grad=False + ) + month_tab = get_month_encoding_table(month_embedding_size) + self.month_embed = nn.Embedding.from_pretrained(month_tab, freeze=True) + self.channel_embed = nn.Embedding( + num_embeddings=len(self.band_groups) + 1, embedding_dim=channel_embedding_size + ) + + self.initialize_weights() + + def initialize_weights(self): + pos_embed = get_sinusoid_encoding_table(self.pos_embed.shape[1], self.pos_embed.shape[-1]) + self.pos_embed.data.copy_(pos_embed) + + # initialize nn.Linear and nn.LayerNorm + self.apply(self._init_weights) + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + # we use xavier_uniform following official JAX ViT: + torch.nn.init.xavier_uniform_(m.weight) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.LayerNorm): + nn.init.constant_(m.bias, 0) + nn.init.constant_(m.weight, 1.0) + + @staticmethod + def cartesian(latlons: torch.Tensor) -> torch.Tensor: + with torch.no_grad(): + # an embedding is calculated for all timesteps. This is then expanded + # for each timestep in the sequence + latlon_radians = latlons * math.pi / 180 + lats, lons = latlon_radians[:, 0], latlon_radians[:, 1] + x = torch.cos(lats) * torch.cos(lons) + y = torch.cos(lats) * torch.sin(lons) + z = torch.sin(lats) + return torch.stack([x, y, z], dim=-1) + + @staticmethod + def mask_tokens(x, mask): + mask = mask.bool() + + # https://stackoverflow.com/a/68621610/2332296 + # move all non-masked values to the front of their rows + sorted_mask, indices = torch.sort((~mask).int(), dim=1, descending=True, stable=True) + x = x.gather(1, indices[:, :, None].expand_as(x)) + # set masked values to 0 (not really necessary since we'll ignore them anyway) + x = x * sorted_mask.unsqueeze(-1) + + # cut off to the length of the longest sequence + max_length = sorted_mask.sum(-1).max() + x = x[:, :max_length] + updated_mask = 1 - sorted_mask[:, :max_length] + + return x, indices, updated_mask + + def forward( + self, + x: torch.Tensor, + dynamic_world: torch.Tensor, + latlons: torch.Tensor, + mask: Optional[torch.Tensor] = None, + month: Union[torch.Tensor, int] = 0, + eval_task: bool = True, + ): + device = x.device + + if mask is None: + mask = torch.zeros_like(x, device=x.device) + + months = month_to_tensor(month, x.shape[0], x.shape[1], device) + month_embedding = self.month_embed(months) + positional_embedding = repeat( + self.pos_embed[:, : x.shape[1], :], "b t d -> (repeat b) t d", repeat=x.shape[0] + ) + + # we assume the number of masked patches is the same + # for all items in the batch. Otherwise things become a headache + all_tokens, all_masks = [], [] + + for channel_group, channel_idxs in self.band_groups.items(): + tokens = self.eo_patch_embed[channel_group](x[:, :, channel_idxs]) + channel_embedding = self.channel_embed( + torch.tensor(self.band_group_to_idx[channel_group]).long().to(device) + ) + channel_embedding = repeat(channel_embedding, "d -> b t d", b=x.shape[0], t=x.shape[1]) + if channel_group == "SRTM": + # for SRTM, we reduce it to a single token instead of + # a token per timestep + channel_wise_positional_embedding = torch.cat( + ( + torch.zeros_like(month_embedding[:, 0:1]), + channel_embedding[:, 0:1], + torch.zeros_like(positional_embedding[:, 0:1]), + ), + dim=-1, + ) + indices = slice(0, 1) + else: + channel_wise_positional_embedding = torch.cat( + (month_embedding, channel_embedding, positional_embedding), dim=-1 + ) + indices = slice(None) + + tokens = tokens[:, indices] + tokens += channel_wise_positional_embedding + all_tokens.append(tokens) + group_mask = torch.max(mask[:, indices, channel_idxs], dim=-1)[0] + all_masks.append(group_mask) + + # then, dynamic world + tokens = self.dw_embed(dynamic_world) + channel_embedding = self.channel_embed( + torch.tensor(self.band_group_to_idx["dynamic_world"]).long().to(device) + ) + channel_embedding = repeat(channel_embedding, "d -> b t d", b=x.shape[0], t=x.shape[1]) + positional_embedding = torch.cat( + (month_embedding, channel_embedding, positional_embedding), dim=-1 + ) + tokens += positional_embedding + all_tokens.append(tokens) + + # now we calculate the mask for these [b, t] tokens + group_mask = dynamic_world == DynamicWorld2020_2021.class_amount + all_masks.append(group_mask) + + x = torch.cat(all_tokens, dim=1) # [batch, timesteps, embedding_dim] + mask = torch.cat(all_masks, dim=1) # [batch, timesteps] + x, orig_indices, upd_mask = self.mask_tokens(x, mask) + + # append latlon tokens + latlon_tokens = self.latlon_embed(self.cartesian(latlons)).unsqueeze(1) + x = torch.cat((latlon_tokens, x), dim=1) + upd_mask = torch.cat((torch.zeros(x.shape[0])[:, None].to(device), upd_mask), dim=1) + orig_indices = torch.cat( + (torch.zeros(x.shape[0])[:, None].to(device).int(), orig_indices + 1), + dim=1, + ) + + # apply Transformer blocks + for blk in self.blocks: + x = blk(x, attn_mask=~upd_mask.bool()) + + # mask will be a boolean of shape [batch, total_num_tokens] + if eval_task: + # set masked tokens to 0 + x_for_mean = x * (1 - upd_mask.unsqueeze(-1)) + x_mean = x_for_mean.sum(dim=1) / torch.sum(1 - upd_mask, -1, keepdim=True) + return self.norm(x_mean) + return self.norm(x), orig_indices, upd_mask + + +class Decoder(nn.Module): + def __init__( + self, + channel_embeddings: nn.Embedding, + encoder_embed_dim=128, + decoder_embed_dim=128, + decoder_depth=2, + decoder_num_heads=8, + mlp_ratio=2, + max_sequence_length=24, + ): + super().__init__() + + self.band_groups = BANDS_GROUPS_IDX + + # this is used for the channel embedding + self.band_group_to_idx = { + group_name: idx for idx, (group_name, _) in enumerate(self.band_groups.items()) + } + self.band_group_to_idx["dynamic_world"] = max(self.band_group_to_idx.values()) + 1 + + self.decoder_embed = nn.Linear(encoder_embed_dim, decoder_embed_dim, bias=True) + + self.mask_token = nn.Parameter(torch.zeros(decoder_embed_dim)) + + self.decoder_blocks = nn.ModuleList( + [ + Block( + decoder_embed_dim, + decoder_num_heads, + mlp_ratio, + qkv_bias=True, + norm_layer=nn.LayerNorm, + ) + for _ in range(decoder_depth) + ] + ) + + self.decoder_norm = nn.LayerNorm(decoder_embed_dim) + + self.eo_decoder_pred = nn.ModuleDict( + { + group_name: nn.Linear(decoder_embed_dim, len(group)) + for group_name, group in self.band_groups.items() + } + ) + self.dw_decoder_pred = nn.Linear(decoder_embed_dim, DynamicWorld2020_2021.class_amount) + + self.channel_embeddings = channel_embeddings + channel_embedding_dims = channel_embeddings.weight.shape[-1] + remaining_embeddings = decoder_embed_dim - channel_embedding_dims + # the positional + monthly + channel embedding + self.max_sequence_length = max_sequence_length + self.pos_embed = nn.Parameter( + torch.zeros(1, max_sequence_length, int(remaining_embeddings) // 2), + requires_grad=False, + ) + month_tab = get_month_encoding_table(int(remaining_embeddings) // 2) + self.month_embed = nn.Embedding.from_pretrained(month_tab, freeze=True) + + self.initialize_weights() + + def initialize_weights(self): + pos_embed = get_sinusoid_encoding_table(self.pos_embed.shape[1], self.pos_embed.shape[-1]) + self.pos_embed.data.copy_(pos_embed) + + # initialize nn.Linear and nn.LayerNorm + self.apply(self._init_weights) + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + # we use xavier_uniform following official JAX ViT: + torch.nn.init.xavier_uniform_(m.weight) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.LayerNorm): + nn.init.constant_(m.bias, 0) + nn.init.constant_(m.weight, 1.0) + + def add_masked_tokens(self, x, orig_indices, x_mask): + all_masked = repeat(self.mask_token, "d -> b t d", b=x.shape[0], t=orig_indices.shape[1]) + mask = torch.cat( + ( + x_mask, + torch.ones((x.shape[0], orig_indices.shape[1] - x.shape[1]), device=device), + ), + dim=-1, + ) + # can't set value on leaf variable + out = all_masked.clone() + # put tokens in full masked tensor (at the first N positions in every row) + out[~mask.bool()] = x[~x_mask.bool()] + # then move them to their original positions + out = out.scatter(1, orig_indices[:, :, None].expand_as(out), out) + return out + + def add_embeddings(self, x, month: Union[torch.Tensor, int]): + num_channel_groups = len(self.band_group_to_idx) + # -2 since we remove srtm and latlon, and -1 since the srtm + # channel group doesn't have timesteps + num_timesteps = int((x.shape[1] - 2) / (num_channel_groups - 1)) + srtm_index = self.band_group_to_idx["SRTM"] * num_timesteps + months = month_to_tensor(month, x.shape[0], num_timesteps, x.device) + + # when we expand the encodings, each channel_group gets num_timesteps + # encodings. However, there is only one SRTM token so we remove the + # excess SRTM encodings + remove_mask = torch.full(size=(num_timesteps * num_channel_groups,), fill_value=False) + remove_mask[torch.arange(num_timesteps - 1) + srtm_index] = True + + month_embedding = repeat( + self.month_embed(months), "b t d -> b (repeat t) d", repeat=num_channel_groups + ) + month_embedding = month_embedding[:, ~remove_mask] + month_embedding[:, srtm_index] = 0 + + positional_embedding = repeat( + self.pos_embed[:, :num_timesteps, :], + "b t d -> (b2 b) (t2 t) d", + b2=x.shape[0], + t2=num_channel_groups, + ) + positional_embedding = positional_embedding[:, ~remove_mask] + positional_embedding[:, srtm_index] = 0 + + channel_embeddings = torch.repeat_interleave( + self.channel_embeddings.weight, repeats=num_timesteps, dim=0 + ) + channel_embeddings = repeat(channel_embeddings, "c d -> b c d", b=x.shape[0]) + channel_embeddings = channel_embeddings[:, ~remove_mask] + + positional_embedding = torch.cat( + (month_embedding, channel_embeddings, positional_embedding), dim=-1 + ) + + # add the zero embedding for the latlon token + positional_embedding = torch.cat( + [torch.zeros_like(positional_embedding[:, 0:1, :]), positional_embedding], dim=1 + ) + + x += positional_embedding + return x + + def reconstruct_inputs(self, x) -> Tuple[torch.Tensor, torch.Tensor]: + # remove the latlon token + x = x[:, 1:, :] + + # split into channel groups + num_channel_groups = len(self.band_group_to_idx) - 1 + num_timesteps = int((x.shape[1] - 1) / num_channel_groups) + srtm_index = self.band_group_to_idx["SRTM"] * num_timesteps + srtm_token = x[:, srtm_index : srtm_index + 1, :] + + mask = torch.full((x.shape[1],), True, device=x.device) + mask[torch.tensor(srtm_index)] = False + x = x[:, mask] + + x = x.view(x.shape[0], num_channel_groups, num_timesteps, x.shape[-1]) + + eo_output, dw_output = [], None + for group_name, idx in self.band_group_to_idx.items(): + if group_name == "SRTM": + eo_output.append( + repeat( + self.eo_decoder_pred[group_name](srtm_token), + "b t d -> b (t2 t) d", + t2=num_timesteps, + ) + ) + else: + if idx > self.band_group_to_idx["SRTM"]: + idx -= 1 + group_tokens = x[:, idx] + if group_name == "dynamic_world": + dw_output = self.dw_decoder_pred(group_tokens) + else: + eo_output.append(self.eo_decoder_pred[group_name](group_tokens)) + + # we can just do this concatenation because the BANDS_GROUP_IDX + # is ordered + return torch.cat(eo_output, dim=-1), cast(torch.Tensor, dw_output) + + def forward(self, x, orig_indices, x_mask, month): + x = self.decoder_embed(x) + x = self.add_masked_tokens(x, orig_indices, x_mask) + x = self.add_embeddings(x, month) + + # apply Transformer blocks + for blk in self.decoder_blocks: + x = blk(x) + x = self.decoder_norm(x) + return self.reconstruct_inputs(x) + + +class PrestoFineTuningModel(nn.Module): + def __init__(self, encoder, head): + super().__init__() + self.encoder: Encoder = deepcopy(encoder) + # make sure the model is trainable, since we can call + # this having called requires_grad_(False) + self.encoder.requires_grad_(True) + # but don't unfreeze the position encoder, which + # shouldn't be trainable + self.encoder.pos_embed.requires_grad_(False) + self.encoder.month_embed.requires_grad_(False) + self.head = head + + def forward( + self, + x: torch.Tensor, + dynamic_world: torch.Tensor, + latlons: torch.Tensor, + mask: Optional[torch.Tensor] = None, + month: Union[torch.Tensor, int] = 0, + ) -> torch.Tensor: + return self.head( + self.encoder( + x=x, + dynamic_world=dynamic_world, + latlons=latlons, + mask=mask, + month=month, + eval_task=True, + ) + ) + + +class FinetuningHead(nn.Module): + def __init__(self, hidden_size: int, num_outputs: int) -> None: + super().__init__() + + self.hidden_size = hidden_size + self.num_outputs = num_outputs + self.linear = nn.Linear(hidden_size, num_outputs) + + def forward(self, x: torch.Tensor): + x = self.linear(x) + return x + + +class Presto(nn.Module): + def __init__(self, encoder, decoder): + super().__init__() + self.encoder: Encoder = encoder + self.decoder: Decoder = decoder + + def forward( + self, + x: torch.Tensor, + dynamic_world: torch.Tensor, + latlons: torch.Tensor, + mask: Optional[torch.Tensor] = None, + month: Union[torch.Tensor, int] = 0, + ) -> torch.Tensor: + x, orig_indices, x_mask = self.encoder( + x=x, + dynamic_world=dynamic_world, + latlons=latlons, + mask=mask, + month=month, + eval_task=False, + ) + + return self.decoder(x, orig_indices, x_mask, month) + + @classmethod + def construct( + cls, + encoder_embedding_size: int = 128, + channel_embed_ratio: float = 0.25, + month_embed_ratio: float = 0.25, + encoder_depth=2, + mlp_ratio=4, + encoder_num_heads=8, + decoder_embedding_size=128, + decoder_depth=2, + decoder_num_heads=8, + max_sequence_length=24, + ): + encoder = Encoder( + embedding_size=encoder_embedding_size, + channel_embed_ratio=channel_embed_ratio, + month_embed_ratio=month_embed_ratio, + depth=encoder_depth, + mlp_ratio=mlp_ratio, + num_heads=encoder_num_heads, + max_sequence_length=max_sequence_length, + ) + decoder = Decoder( + channel_embeddings=encoder.channel_embed, + encoder_embed_dim=encoder_embedding_size, + decoder_embed_dim=decoder_embedding_size, + decoder_depth=decoder_depth, + decoder_num_heads=decoder_num_heads, + mlp_ratio=mlp_ratio, + max_sequence_length=max_sequence_length, + ) + return cls(encoder, decoder) + + def construct_finetuning_model( + self, + num_outputs: int, + ) -> PrestoFineTuningModel: + head = FinetuningHead( + num_outputs=num_outputs, + hidden_size=self.encoder.embedding_size, + ) + model = PrestoFineTuningModel(self.encoder, head).to(self.encoder.pos_embed.device) + model.train() + return model + + @classmethod + def load_pretrained( + cls, model_path: Union[str, Path] = default_model_path, strict: bool = True + ): + model = cls.construct() + model.load_state_dict(torch.load(model_path, map_location=device), strict=strict) + return model + + @classmethod + def load_pretrained_artifactory( + cls, presto_url: str, strict: bool = True + ): + response = requests.get(presto_url) + presto_model_layers = torch.load(io.BytesIO(response.content), map_location=device) + model = cls.construct() + model.load_state_dict(presto_model_layers, strict=strict) + return model + + +def param_groups_lrd( + model: PrestoFineTuningModel, weight_decay=0.05, no_weight_decay_list=[], layer_decay=0.75 +): + """ + Parameter groups for layer-wise lr decay + Following BEiT: https://github.com/microsoft/unilm/blob/master/beit/optim_factory.py#L58 + """ + param_group_names = {} + param_groups = {} + + num_layers = len(cast(Sized, model.encoder.blocks)) + 1 + + layer_scales = list(layer_decay ** (num_layers - i) for i in range(num_layers + 1)) + + for n, p in model.named_parameters(): + if not p.requires_grad: + continue + + # no decay: all 1D parameters and model specific ones + if p.ndim == 1 or n in no_weight_decay_list: + g_decay = "no_decay" + this_decay = 0.0 + else: + g_decay = "decay" + this_decay = weight_decay + + layer_id = get_layer_id_for_rest_finetuning(n, num_layers) + group_name = "layer_%d_%s" % (layer_id, g_decay) + + if group_name not in param_group_names: + this_scale = layer_scales[layer_id] + + param_group_names[group_name] = { + "lr_scale": this_scale, + "weight_decay": this_decay, + "params": [], + } + param_groups[group_name] = { + "lr_scale": this_scale, + "weight_decay": this_decay, + "params": [], + } + + param_group_names[group_name]["params"].append(n) + param_groups[group_name]["params"].append(p) + + return list(param_groups.values()) + + +def get_layer_id_for_rest_finetuning(name, num_layers): + """ + Assign a parameter with its layer id + Following BEiT: https://github.com/microsoft/unilm/blob/master/beit/optim_factory.py#L33 + """ + if "embed" in name: + return 0 + elif name.startswith("encoder.blocks"): + return int(name.split(".")[2]) + 1 + else: + return num_layers diff --git a/minimal_wc_presto/mvp_wc_presto/utils.py b/minimal_wc_presto/mvp_wc_presto/utils.py new file mode 100644 index 00000000..1356407a --- /dev/null +++ b/minimal_wc_presto/mvp_wc_presto/utils.py @@ -0,0 +1,162 @@ +import logging +import os +import sys +from datetime import datetime +from pathlib import Path +from typing import Callable, Dict, List, Optional, Union + +import geopandas as gpd +import pandas as pd +import torch +import xarray as xr + +from .dataops import ( + BANDS, + ERA5_BANDS, + NORMED_BANDS, + REMOVED_BANDS, + S1_BANDS, + S1_S2_ERA5_SRTM, + S2_BANDS, + SRTM_BANDS, + DynamicWorld2020_2021, +) + +logger = logging.getLogger("__main__") + +data_dir = Path(__file__).parent.parent / "data" +config_dir = Path(__file__).parent.parent / "config" +default_model_path = data_dir / "default_model.pt" +device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") +DEFAULT_SEED: int = 42 + + +# From https://gist.github.com/ihoromi4/b681a9088f348942b01711f251e5f964 +def seed_everything(seed: int = DEFAULT_SEED): + import os + import random + + import numpy as np + import torch + + random.seed(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = True + + +def initialize_logging(output_dir: Union[str, Path], to_file=True, logger_name="__main__"): + logger = logging.getLogger(logger_name) + formatter = logging.Formatter( + fmt="%(asctime)s - %(levelname)s - %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", + ) + ch = logging.StreamHandler(stream=sys.stdout) + ch.setLevel(logging.INFO) + ch.setFormatter(formatter) + logger.addHandler(ch) + + logger.setLevel(logging.INFO) + + if to_file: + path = os.path.join(output_dir, "console-output.log") + fh = logging.FileHandler(path) + fh.setLevel(logging.INFO) + fh.setFormatter(formatter) + logger.addHandler(fh) + logger.info("Initialized logging to %s" % path) + return logger + + +def timestamp_dirname(suffix: Optional[str] = None) -> str: + ts = datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f") + return f"{ts}_{suffix}" if suffix is not None else ts + + +def construct_single_presto_input( + s1: Optional[torch.Tensor] = None, + s1_bands: Optional[List[str]] = None, + s2: Optional[torch.Tensor] = None, + s2_bands: Optional[List[str]] = None, + era5: Optional[torch.Tensor] = None, + era5_bands: Optional[List[str]] = None, + srtm: Optional[torch.Tensor] = None, + srtm_bands: Optional[List[str]] = None, + dynamic_world: Optional[torch.Tensor] = None, + normalize: bool = True, +): + """ + Inputs are paired into a tensor input and a list _bands, which describes . + + should have shape (num_timesteps, len(_bands)), with the following bands possible for + each input: + + s1: ["VV", "VH"] + s2: ["B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B8A", "B9", "B10", "B11", "B12"] + era5: ["temperature_2m", "total_precipitation"] + "temperature_2m": Temperature of air at 2m above the surface of land, + sea or in-land waters in Kelvin (K) + "total_precipitation": Accumulated liquid and frozen water, including rain and snow, + that falls to the Earth's surface. Measured in metres (m) + srtm: ["elevation", "slope"] + + dynamic_world is a 1d input of shape (num_timesteps,) representing the dynamic world classes + of each timestep for that pixel + """ + num_timesteps_list = [x.shape[0] for x in [s1, s2, era5, srtm] if x is not None] + if dynamic_world is not None: + num_timesteps_list.append(len(dynamic_world)) + + assert len(num_timesteps_list) > 0 + assert all(num_timesteps_list[0] == timestep for timestep in num_timesteps_list) + num_timesteps = num_timesteps_list[0] + mask, x = torch.ones(num_timesteps, len(BANDS)), torch.zeros(num_timesteps, len(BANDS)) + + for band_group in [ + (s1, s1_bands, S1_BANDS), + (s2, s2_bands, S2_BANDS), + (era5, era5_bands, ERA5_BANDS), + (srtm, srtm_bands, SRTM_BANDS), + ]: + data, input_bands, output_bands = band_group + if data is not None: + assert input_bands is not None + else: + continue + + kept_output_bands = [x for x in output_bands if x not in REMOVED_BANDS] + # construct a mapping from the input bands to the expected bands + kept_input_band_idxs = [i for i, val in enumerate(input_bands) if val in kept_output_bands] + kept_input_band_names = [val for val in input_bands if val in kept_output_bands] + + input_to_output_mapping = [BANDS.index(val) for val in kept_input_band_names] + + x[:, input_to_output_mapping] = data[:, kept_input_band_idxs] + mask[:, input_to_output_mapping] = 0 + + if dynamic_world is None: + dynamic_world = torch.ones(num_timesteps) * (DynamicWorld2020_2021.class_amount) + + keep_indices = [idx for idx, val in enumerate(BANDS) if val != "B9"] + mask = mask[:, keep_indices] + + if normalize: + # normalize includes x = x[:, keep_indices] + x = S1_S2_ERA5_SRTM.normalize(x) + if s2_bands is not None: + if ("B8" in s2_bands) and ("B4" in s2_bands): + mask[:, NORMED_BANDS.index("NDVI")] = 0 + else: + x = x[:, keep_indices] + return x, mask, dynamic_world + + +def load_world_df() -> pd.DataFrame: + # this could be memoized, but it should only be called 2 or 3 times in a run + filename = "world-administrative-boundaries/world-administrative-boundaries.shp" + world_df = gpd.read_file(data_dir / filename) + world_df = world_df.drop(columns=["iso3", "status", "color_code", "iso_3166_1_"]) + return world_df diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py new file mode 100644 index 00000000..3d282884 --- /dev/null +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -0,0 +1,379 @@ +from typing import Dict, Tuple + +import numpy as np +import pandas as pd + +import torch +from torch.utils.data import DataLoader, TensorDataset +from pyproj import Transformer + +import xarray as xr +from einops import repeat +import onnxruntime +import requests + + +from .dataops import ( + BANDS, + BANDS_GROUPS_IDX, + NORMED_BANDS, + S1_S2_ERA5_SRTM, + DynamicWorld2020_2021, +) +from .masking import BAND_EXPANSION +from .presto import Presto +from .utils import device + + + +#% Mapping from original band names to Presto names +BAND_MAPPING = { + "B02": "B2", + "B03": "B3", + "B04": "B4", + "B05": "B5", + "B06": "B6", + "B07": "B7", + "B08": "B8", + "B8A": "B8A", + "B11": "B11", + "B12": "B12", + "VH": "VH", + "VV": "VV", + "precipitation-flux": "total_precipitation", + "temperature-mean": "temperature_2m", +} + +# Index to band groups mapping +IDX_TO_BAND_GROUPS = { + NORMED_BANDS[idx]: band_group_idx + for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) + for idx in val +} + +class WorldCerealPredictor: + def __init__(self): + """ + Initialize an empty WorldCerealPredictor. + """ + self.onnx_session = None + + def load_model(self, model): + """ + Load an ONNX model from the specified path. + + Args: + model_path (str): The path to the ONNX model file. + """ + # Load the dependency into an InferenceSession + self.onnx_session = onnxruntime.InferenceSession(model) + + def predict(self, features: pd.DataFrame) -> pd.DataFrame: + """ + Predicts labels using the provided features DataFrame. + + Args: + features (pd.DataFrame): DataFrame containing the features for prediction. + + Returns: + pd.DataFrame: DataFrame containing the predicted labels. + """ + if self.onnx_session is None: + raise ValueError("Model has not been loaded. Please load a model first.") + + # Prepare input data for ONNX model + outputs = self.onnx_session.run(None, {'features': features.to_numpy().astype(np.float32)}) + + # Threshold for binary conversion + threshold = 0.5 + + # Extract all prediction values and convert them to binary labels + prediction_values = [sublist['True'] for sublist in outputs[1]] + binary_labels = np.array(prediction_values) >= threshold + binary_labels = binary_labels.astype(int) + + # Create DataFrame with binary labels + preds_df = pd.DataFrame(index=features.index, columns=["label"], data=binary_labels) + return preds_df + + + + + +class PrestoFeatureExtractor: + + def __init__(self, model: Presto): + """ + Initialize the PrestoFeatureExtractor with a Presto model. + + Args: + model (Presto): The Presto model used for feature extraction. + """ + self.model = model + + _NODATAVALUE = 65535 + + BAND_MAPPING = { + "B02": "B2", + "B03": "B3", + "B04": "B4", + "B05": "B5", + "B06": "B6", + "B07": "B7", + "B08": "B8", + "B8A": "B8A", + "B11": "B11", + "B12": "B12", + "VH": "VH", + "VV": "VV", + "precipitation-flux": "total_precipitation", + "temperature-mean": "temperature_2m", + } + + @classmethod + def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.ndarray: + """ + Preprocesses the band values based on the given presto_val. + + Args: + values (np.ndarray): Array of band values to preprocess. + presto_val (str): Name of the band for preprocessing. + + Returns: + np.ndarray: Preprocessed array of band values. + """ + if presto_band in ["VV", "VH"]: + # Convert to dB + values = 20 * np.log10(values) - 83 + elif presto_band == "total_precipitation": + # Scale precipitation and convert mm to m + values = values / (100 * 1000.0) + elif presto_band == "temperature_2m": + # Remove scaling + values = values / 100 + return values + + @classmethod + def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: + """ + Extracts EO data and mask arrays from the input xarray.DataArray. + + Args: + inarr (xr.DataArray): Input xarray.DataArray containing EO data. + + Returns: + Tuple[np.ndarray, np.ndarray]: Tuple containing EO data array and mask array. + """ + num_pixels = len(inarr.x) * len(inarr.y) + num_timesteps = len(inarr.t) + + eo_data = np.zeros((num_pixels, num_timesteps, len(BANDS))) + mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) + + for org_band, presto_band in cls.BAND_MAPPING.items(): + if org_band in inarr.coords['bands']: + values = np.swapaxes(inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1) + idx_valid = values != cls._NODATAVALUE + values = cls._preprocess_band_values(values, presto_band) + eo_data[:, :, BANDS.index(presto_band)] = values + mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid + + return eo_data, mask + + + + + @staticmethod + def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: + """ + Extracts latitudes and longitudes from the input xarray.DataArray. + + Args: + inarr (xr.DataArray): Input xarray.DataArray containing spatial coordinates. + epsg (int): EPSG code for coordinate reference system. + + Returns: + np.ndarray: Array containing extracted latitudes and longitudes. + """ + #EPSG:4326 is the supported crs for presto + transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) + lon, lat = transformer.transform(inarr.x, inarr.y) + + # 2D array where each row represents a pair of latitude and longitude coordinates. + return np.stack( + [np.repeat(lat, repeats=len(lon)), repeat(lon, "c -> (h c)", h=len(lat))], + axis=-1, + ) + + @staticmethod + def _extract_months( inarr: xr.DataArray) -> np.ndarray: + """ + Calculate the start month based on the first timestamp in the input array, + and create an array of the same length filled with that start month value. + + Parameters: + - inarr: xarray.DataArray or numpy.ndarray + Input array containing timestamps. + + Returns: + - months: numpy.ndarray + Array of start month values, with the same length as the input array. + """ + num_instances = len(inarr.x) * len(inarr.y) + + start_month = ( + inarr.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1 + ) - 1 + + months = np.ones((num_instances)) * start_month + return months + + def _create_dataloader(self, eo:np.ndarray, dynamic_world:np.ndarray, months:np.ndarray, latlons:np.ndarray, mask:np.ndarray) -> DataLoader: + """ + Create a PyTorch DataLoader for encoding features. + + Args: + eo_data (np.ndarray): Array containing Earth Observation data. + dynamic_world (np.ndarray): Array containing dynamic world data. + latlons (np.ndarray): Array containing latitude and longitude coordinates. + inarr (xr.DataArray): Input xarray.DataArray. + mask (np.ndarray): Array containing masking data. + + Returns: + DataLoader: PyTorch DataLoader for encoding features. + """ + + dl = DataLoader( + TensorDataset( + torch.from_numpy(eo).float(), + torch.from_numpy(dynamic_world).long(), + torch.from_numpy(latlons).float(), + torch.from_numpy(months).long(), + torch.from_numpy(mask).float(), + ), + batch_size=8192, + shuffle=False, + ) + + return dl + + def _create_presto_input( + cls, inarr: xr.DataArray, epsg: int = 4326 + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + + eo_data, mask = cls._extract_eo_data(inarr) + latlons = cls._extract_latlons(inarr, epsg) + months = cls._extract_months(inarr) + dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( + DynamicWorld2020_2021.class_amount + ) + + return ( + S1_S2_ERA5_SRTM.normalize(eo_data), + dynamic_world, + months, + latlons, + np.repeat(mask, BAND_EXPANSION, axis=-1) + ) + + + def _get_encodings(self, dl: DataLoader) -> np.ndarray: + """ + Get encodings from DataLoader. + + Args: + dl (DataLoader): PyTorch DataLoader containing data for encoding. + + Returns: + np.ndarray: Array containing encoded features. + """ + + all_encodings = [] + + for x, dw, latlons, month, variable_mask in dl: + x_f, dw_f, latlons_f, month_f, variable_mask_f = [ + t.to(device) for t in (x, dw, latlons, month, variable_mask) + ] + + with torch.no_grad(): + encodings = ( + self.model.encoder( + x_f, + dynamic_world=dw_f.long(), + mask=variable_mask_f, + latlons=latlons_f, + month=month_f, + ) + .cpu() + .numpy() + ) + + all_encodings.append(encodings) + + return np.concatenate(all_encodings, axis=0) + + @staticmethod + def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFrame: + flat_lat, flat_lon = latlons[:, 0], latlons[:, 1] + if len(encodings.shape) == 1: + encodings = np.expand_dims(encodings, axis=-1) + + data_dict: Dict[str, np.ndarray] = {"lat": flat_lat, "lon": flat_lon} + for i in range(encodings.shape[1]): + encodings_label = f"presto_ft_{i}" + data_dict[encodings_label] = encodings[:, i] + return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) + + + def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326): + eo, dynamic_world, months, latlons, mask = self._create_presto_input(inarr, epsg) + dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) + + features = self._get_encodings(dl) + features = self.combine_encodings(latlons, features) + + return features + + +def get_presto_features(inarr: xr.DataArray, presto_path: str) -> xr.DataArray: + """ + Extracts features from input data using Presto. + + Args: + inarr (xr.DataArray): Input data as xarray DataArray. + presto_path (str): Path to the pretrained Presto model. + + Returns: + xr.DataArray: Extracted features as xarray DataArray. + """ + # Load the model + + presto_model = Presto.load_pretrained_artifactory(presto_url = presto_path, strict=False) + presto_extractor = PrestoFeatureExtractor(presto_model) + features = presto_extractor.extract_presto_features(inarr, epsg=32631) + return features + + +def classify_with_catboost(features: np.ndarray, orig_dims: list, model_path: str) -> xr.DataArray: + """ + Classifies features using the WorldCereal CatBoost model. + + Args: + features (np.ndarray): Features to be classified. + orig_dims (list): Original dimensions of the input data. + model_path (str): Path to the trained CatBoost model. + + Returns: + xr.DataArray: Classified data as xarray DataArray. + """ + + predictor = WorldCerealPredictor() + response = requests.get(model_path) + catboost_model = response.content + + predictor.load_model(catboost_model) + predictions = predictor.predict(features) + result_da = predictions.to_xarray().to_array(dim="bands").rename({"lon": "x", "lat": "y"}) + result_da = result_da.transpose(*orig_dims) + + return result_da \ No newline at end of file From 1020ee4ad4be2f1538557c4a8a49cdb5e56f27b7 Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Tue, 7 May 2024 12:06:30 +0200 Subject: [PATCH 03/31] hv remove pandas to xarray conversion --- .../backend_inference_example_openeo.ipynb | 106 ++++++++++-------- .../mvp_wc_presto/world_cereal_inference.py | 17 ++- .../udf_worldcereal_inference.py | 11 +- 3 files changed, 76 insertions(+), 58 deletions(-) diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index 3b6a7ad7..f8ca92d7 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -1,13 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "ce322de6", - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "markdown", "id": "b879f7b4-9a3f-41fc-90d0-ab9cfd25a093", @@ -18,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", "metadata": { "tags": [] @@ -39,7 +31,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "5494c46d", "metadata": {}, "outputs": [], @@ -75,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "8f71136c-1252-4786-8609-8bb995da7daf", "metadata": { "tags": [] @@ -85,26 +77,55 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-240506caa9a448be8d26ea574243765e': send 'start'\n", - "0:00:13 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", - "0:00:19 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", - "0:00:25 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", - "0:00:33 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", - "0:00:44 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", - "0:00:56 Job 'j-240506caa9a448be8d26ea574243765e': created (progress N/A)\n", - "0:01:12 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:01:31 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:01:55 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:02:34 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:03:12 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:03:58 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:04:57 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:05:59 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:07:00 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:08:05 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:09:05 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:10:16 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n", - "0:11:16 Job 'j-240506caa9a448be8d26ea574243765e': running (progress N/A)\n" + "0:00:00 Job 'j-240506cb2e86484f90a485dd7d7c210d': send 'start'\n", + "0:00:14 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", + "0:00:19 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", + "0:00:26 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", + "0:00:34 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", + "0:00:44 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", + "0:00:56 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", + "0:01:12 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:01:31 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:01:55 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:02:25 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:03:03 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:03:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:04:48 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:05:49 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:06:49 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:07:49 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:08:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:09:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:10:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:11:51 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:12:51 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:13:51 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:14:52 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:15:52 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:16:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:17:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:18:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:19:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:20:54 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:21:54 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", + "0:22:55 Job 'j-240506cb2e86484f90a485dd7d7c210d': error (progress N/A)\n", + "Your batch job 'j-240506cb2e86484f90a485dd7d7c210d' failed. Error logs:\n", + "[{'id': '[1715028605397, 977274]', 'time': '2024-05-06T20:50:05.397Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715028609615, 246593]', 'time': '2024-05-06T20:50:09.615Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715029247077, 96315]', 'time': '2024-05-06T21:00:47.077Z', 'level': 'error', 'message': 'Task 1 in stage 41.0 failed 4 times; aborting job'}, {'id': '[1715029249027, 196096]', 'time': '2024-05-06T21:00:49.027Z', 'level': 'error', 'message': 'OpenEO batch job failed: Exception during Spark execution: java.io.EOFException'}]\n", + "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-240506cb2e86484f90a485dd7d7c210d').logs()`.\n" + ] + }, + { + "ename": "JobFailedException", + "evalue": "Batch job 'j-240506cb2e86484f90a485dd7d7c210d' didn't finish successfully. Status: error (after 0:22:55).", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[4], line 16\u001b[0m\n\u001b[0;32m 1\u001b[0m udf \u001b[38;5;241m=\u001b[39m openeo\u001b[38;5;241m.\u001b[39mUDF\u001b[38;5;241m.\u001b[39mfrom_file(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mudf_worldcereal_inference.py\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 3\u001b[0m prediction \u001b[38;5;241m=\u001b[39m input_cube\u001b[38;5;241m.\u001b[39mapply_neighborhood(\n\u001b[0;32m 4\u001b[0m process\u001b[38;5;241m=\u001b[39mudf,\n\u001b[0;32m 5\u001b[0m size\u001b[38;5;241m=\u001b[39m[\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 12\u001b[0m ],\n\u001b[0;32m 13\u001b[0m )\n\u001b[1;32m---> 16\u001b[0m \u001b[43mprediction\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtest_output_worldcereal.nc\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal inference\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[43mjob_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mdriver-memory\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m1g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 19\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mexecutor-memoryOverhead\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m6g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", + "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-240506cb2e86484f90a485dd7d7c210d' didn't finish successfully. Status: error (after 0:22:55)." ] } ], @@ -127,8 +148,8 @@ "\n", "prediction.execute_batch(outputfile = 'test_output_worldcereal.nc',\n", " description='world cereal inference',\n", - " job_options={'driver-memory': '1g', \n", - " })\n" + " job_options={'driver-memory': '8g',\n", + " 'executor-memoryOverhead':'8g'} )\n" ] }, { @@ -178,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "85a73ef1", "metadata": {}, "outputs": [ @@ -186,7 +207,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "C:\\Users\\VROMPAYH\\AppData\\Local\\Temp\\ipykernel_31284\\122910811.py:21: DeprecationWarning: dropping variables using `drop` is deprecated; use drop_vars.\n", + "C:\\Users\\VROMPAYH\\AppData\\Local\\Temp\\ipykernel_15488\\3384181925.py:41: DeprecationWarning: dropping variables using `drop` is deprecated; use drop_vars.\n", " arr = ds.drop('crs').to_array(dim='bands')\n" ] } @@ -203,7 +224,7 @@ "\n", "#GET DEPENDENCIES\n", "\n", - " # Generate absolute path for the dependencies folder\n", + "# Generate absolute path for the dependencies folder\n", "dependencies_dir = Path.cwd() / 'dependencies'\n", "dependencies_dir.mkdir(exist_ok=True, parents=True)\n", "\n", @@ -212,8 +233,8 @@ "\n", "# Download and extract the model file\n", "modelfile_url = f\"{base_url}/{dependency_name}\"\n", - "modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name)\n", - "shutil.unpack_archive(modelfile, extract_dir=dependencies_dir)\n", + "#modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name)\n", + "#shutil.unpack_archive(modelfile, extract_dir=dependencies_dir)\n", "\n", "# Add the model directory to system path if it's not already there\n", "abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0])\n", @@ -233,8 +254,7 @@ "# Read the file into xarray\n", "ds = xr.open_dataset(filename)\n", "arr = ds.drop('crs').to_array(dim='bands')\n", - "orig_dims = list(arr.dims)\n", - "orig_dims.remove(\"t\")\n", + "map_dims = arr.shape[2:]\n", "\n", "#Get Presto\n", "from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost\n", @@ -249,7 +269,7 @@ "\n", "#Get CATBOOST\n", "CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx'\n", - "classification = classify_with_catboost(features, orig_dims, CATBOOST_PATH)\n", + "classification = classify_with_catboost(features, map_dims, CATBOOST_PATH)\n", "\n" ] }, @@ -272,12 +292,10 @@ ], "source": [ "import matplotlib.pyplot as plt\n", - "import numpy as np\n", "\n", - "data_array = np.array(classification)\n", "\n", "# Plot the data as an image\n", - "plt.imshow(data_array[0], cmap='gray') # Assuming it's a grayscale image\n", + "plt.imshow(classification, cmap='gray') # Assuming it's a grayscale image\n", "plt.colorbar() # Add a colorbar for reference\n", "plt.show()" ] diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py index 3d282884..4fe99309 100644 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -82,7 +82,7 @@ def predict(self, features: pd.DataFrame) -> pd.DataFrame: raise ValueError("Model has not been loaded. Please load a model first.") # Prepare input data for ONNX model - outputs = self.onnx_session.run(None, {'features': features.to_numpy().astype(np.float32)}) + outputs = self.onnx_session.run(None, {'features': features}) # Threshold for binary conversion threshold = 0.5 @@ -92,9 +92,7 @@ def predict(self, features: pd.DataFrame) -> pd.DataFrame: binary_labels = np.array(prediction_values) >= threshold binary_labels = binary_labels.astype(int) - # Create DataFrame with binary labels - preds_df = pd.DataFrame(index=features.index, columns=["label"], data=binary_labels) - return preds_df + return binary_labels @@ -331,6 +329,7 @@ def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326): features = self._get_encodings(dl) features = self.combine_encodings(latlons, features) + features = features.to_numpy() return features @@ -354,13 +353,13 @@ def get_presto_features(inarr: xr.DataArray, presto_path: str) -> xr.DataArray: return features -def classify_with_catboost(features: np.ndarray, orig_dims: list, model_path: str) -> xr.DataArray: +def classify_with_catboost(features: np.ndarray, map_dims: tuple, model_path: str) -> xr.DataArray: """ Classifies features using the WorldCereal CatBoost model. Args: features (np.ndarray): Features to be classified. - orig_dims (list): Original dimensions of the input data. + map_dims (tuple): Original x, y dimensions of the input data. model_path (str): Path to the trained CatBoost model. Returns: @@ -373,7 +372,7 @@ def classify_with_catboost(features: np.ndarray, orig_dims: list, model_path: st predictor.load_model(catboost_model) predictions = predictor.predict(features) - result_da = predictions.to_xarray().to_array(dim="bands").rename({"lon": "x", "lat": "y"}) - result_da = result_da.transpose(*orig_dims) + predictions = np.flip(np.array(predictions.reshape(map_dims)),axis=0) - return result_da \ No newline at end of file + + return predictions \ No newline at end of file diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index ef9e1905..ae88f182 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -53,10 +53,7 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger = _setup_logging() -# Install PyTorch using pip - - orig_dims = list(cube.dims) - orig_dims.remove("t") + map_dims = cube.shape[2:] logger.info("Unzipping dependencies") base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" @@ -64,7 +61,11 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger.info("Appending depencency") dep_dir = extract_dependencies(base_url, dependency_name) + + + #directly add a path to the older pandas version sys.path.append(str(dep_dir)) + sys.path.append(str(dep_dir) + '/pandas') from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost @@ -77,7 +78,7 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger.info("Catboost classification") CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" - classification = classify_with_catboost(features, orig_dims, CATBOOST_PATH) + classification = classify_with_catboost(features, map_dims, CATBOOST_PATH) return classification From bc9bd1a98d4763f44547214ad26b5358eaf55e7d Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Tue, 7 May 2024 21:07:42 +0200 Subject: [PATCH 04/31] Succesful run, todo is fix apply metadata for bands --- .../backend_inference_example_openeo.ipynb | 593 ++++++++++++++++-- .../mvp_wc_presto/world_cereal_inference.py | 12 +- .../udf_worldcereal_inference.py | 35 +- 3 files changed, 578 insertions(+), 62 deletions(-) diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index f8ca92d7..23ed5377 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", "metadata": { "tags": [] @@ -31,7 +31,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "5494c46d", "metadata": {}, "outputs": [], @@ -67,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "8f71136c-1252-4786-8609-8bb995da7daf", "metadata": { "tags": [] @@ -77,55 +77,56 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-240506cb2e86484f90a485dd7d7c210d': send 'start'\n", - "0:00:14 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", - "0:00:19 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", - "0:00:26 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", - "0:00:34 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", - "0:00:44 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", - "0:00:56 Job 'j-240506cb2e86484f90a485dd7d7c210d': created (progress N/A)\n", - "0:01:12 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:01:31 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:01:55 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:02:25 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:03:03 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:03:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:04:48 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:05:49 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:06:49 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:07:49 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:08:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:09:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:10:50 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:11:51 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:12:51 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:13:51 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:14:52 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:15:52 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:16:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:17:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:18:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:19:53 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:20:54 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:21:54 Job 'j-240506cb2e86484f90a485dd7d7c210d': running (progress N/A)\n", - "0:22:55 Job 'j-240506cb2e86484f90a485dd7d7c210d': error (progress N/A)\n", - "Your batch job 'j-240506cb2e86484f90a485dd7d7c210d' failed. Error logs:\n", - "[{'id': '[1715028605397, 977274]', 'time': '2024-05-06T20:50:05.397Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715028609615, 246593]', 'time': '2024-05-06T20:50:09.615Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715029247077, 96315]', 'time': '2024-05-06T21:00:47.077Z', 'level': 'error', 'message': 'Task 1 in stage 41.0 failed 4 times; aborting job'}, {'id': '[1715029249027, 196096]', 'time': '2024-05-06T21:00:49.027Z', 'level': 'error', 'message': 'OpenEO batch job failed: Exception during Spark execution: java.io.EOFException'}]\n", - "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-240506cb2e86484f90a485dd7d7c210d').logs()`.\n" + "0:00:00 Job 'j-2405070d4ed64599bd19488b2ed13b77': send 'start'\n", + "0:00:15 Job 'j-2405070d4ed64599bd19488b2ed13b77': created (progress N/A)\n", + "0:00:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': created (progress N/A)\n", + "0:00:27 Job 'j-2405070d4ed64599bd19488b2ed13b77': created (progress N/A)\n", + "0:00:35 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:00:45 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:00:57 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:01:18 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:01:37 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:02:01 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:02:31 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:03:09 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:04:05 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:05:04 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:06:04 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:07:04 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:08:05 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:09:10 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:10:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:11:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:12:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:13:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:14:12 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:15:12 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:16:13 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:17:13 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:18:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:19:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:20:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:21:28 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:22:31 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:23:32 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", + "0:24:32 Job 'j-2405070d4ed64599bd19488b2ed13b77': error (progress N/A)\n", + "Your batch job 'j-2405070d4ed64599bd19488b2ed13b77' failed. Error logs:\n", + "[{'id': '[1715078827306, 132769]', 'time': '2024-05-07T10:47:07.306Z', 'level': 'error', 'message': 'Error communicating with MapOutputTracker'}, {'id': '[1715079108343, 414667]', 'time': '2024-05-07T10:51:48.343Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715079108662, 939819]', 'time': '2024-05-07T10:51:48.662Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715080070569, 790186]', 'time': '2024-05-07T11:07:50.569Z', 'level': 'error', 'message': 'Task 1 in stage 41.0 failed 4 times; aborting job'}, {'id': '[1715080072928, 108050]', 'time': '2024-05-07T11:07:52.928Z', 'level': 'error', 'message': 'OpenEO batch job failed: Exception during Spark execution: org.apache.spark.api.python.PythonException: Traceback (most recent call last):\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/worker.py\", line 830, in main\\n process()\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/worker.py\", line 822, in process\\n serializer.dump_stream(out_iter, outfile)\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/serializers.py\", line 146, in dump_stream\\n for obj in iterator:\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/util.py\", line 81, in wrapper\\n return f(*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/openeogeotrellis/utils.py\", line 56, in memory_logging_wrapper\\n return function(*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/epsel.py\", line 44, in wrapper\\n return _FUNCTION_POINTERS[key](*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/epsel.py\", line 37, in first_time\\n return f(*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/openeogeotrellis/geopysparkdatacube.py\", line 529, in tile_function\\n result_array = result_array.transpose(*( \\'bands\\', \\'y\\', \\'x\\'))\\n File \"/opt/openeo/lib/python3.8/site-packages/xarray/core/dataarray.py\", line 2154, in transpose\\n dims = tuple(utils.infix_dims(dims, self.dims))\\n File \"/opt/openeo/lib/python3.8/site-packages/xarray/core/utils.py\", line 726, in infix_dims\\n raise ValueError(\\nValueError: (\\'bands\\', \\'y\\', \\'x\\') must be a permuted list of (\\'dim_0\\', \\'dim_1\\'), unless `...` is included\\n'}]\n", + "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-2405070d4ed64599bd19488b2ed13b77').logs()`.\n" ] }, { "ename": "JobFailedException", - "evalue": "Batch job 'j-240506cb2e86484f90a485dd7d7c210d' didn't finish successfully. Status: error (after 0:22:55).", + "evalue": "Batch job 'j-2405070d4ed64599bd19488b2ed13b77' didn't finish successfully. Status: error (after 0:24:33).", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[4], line 16\u001b[0m\n\u001b[0;32m 1\u001b[0m udf \u001b[38;5;241m=\u001b[39m openeo\u001b[38;5;241m.\u001b[39mUDF\u001b[38;5;241m.\u001b[39mfrom_file(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mudf_worldcereal_inference.py\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 3\u001b[0m prediction \u001b[38;5;241m=\u001b[39m input_cube\u001b[38;5;241m.\u001b[39mapply_neighborhood(\n\u001b[0;32m 4\u001b[0m process\u001b[38;5;241m=\u001b[39mudf,\n\u001b[0;32m 5\u001b[0m size\u001b[38;5;241m=\u001b[39m[\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 12\u001b[0m ],\n\u001b[0;32m 13\u001b[0m )\n\u001b[1;32m---> 16\u001b[0m \u001b[43mprediction\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtest_output_worldcereal.nc\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal inference\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[43mjob_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mdriver-memory\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m1g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 19\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mexecutor-memoryOverhead\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m6g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[1;32mIn[3], line 16\u001b[0m\n\u001b[0;32m 1\u001b[0m udf \u001b[38;5;241m=\u001b[39m openeo\u001b[38;5;241m.\u001b[39mUDF\u001b[38;5;241m.\u001b[39mfrom_file(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mudf_worldcereal_inference.py\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 3\u001b[0m prediction \u001b[38;5;241m=\u001b[39m input_cube\u001b[38;5;241m.\u001b[39mapply_neighborhood(\n\u001b[0;32m 4\u001b[0m process\u001b[38;5;241m=\u001b[39mudf,\n\u001b[0;32m 5\u001b[0m size\u001b[38;5;241m=\u001b[39m[\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 12\u001b[0m ],\n\u001b[0;32m 13\u001b[0m )\n\u001b[1;32m---> 16\u001b[0m \u001b[43mprediction\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtest_output_worldcereal.nc\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal inference\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[43mjob_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mdriver-memory\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m8g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 19\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mexecutor-memoryOverhead\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m8g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", - "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-240506cb2e86484f90a485dd7d7c210d' didn't finish successfully. Status: error (after 0:22:55)." + "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-2405070d4ed64599bd19488b2ed13b77' didn't finish successfully. Status: error (after 0:24:33)." ] } ], @@ -207,9 +208,471 @@ "name": "stderr", "output_type": "stream", "text": [ - "C:\\Users\\VROMPAYH\\AppData\\Local\\Temp\\ipykernel_15488\\3384181925.py:41: DeprecationWarning: dropping variables using `drop` is deprecated; use drop_vars.\n", + "C:\\Users\\VROMPAYH\\AppData\\Local\\Temp\\ipykernel_33340\\4003701718.py:44: DeprecationWarning: dropping variables using `drop` is deprecated; use drop_vars.\n", " arr = ds.drop('crs').to_array(dim='bands')\n" ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (bands: 1, y: 100, x: 100)> Size: 40kB\n",
+       "array([[[1, 1, 1, ..., 1, 1, 1],\n",
+       "        [1, 1, 1, ..., 0, 0, 0],\n",
+       "        [1, 1, 1, ..., 0, 0, 0],\n",
+       "        ...,\n",
+       "        [0, 0, 0, ..., 1, 1, 1],\n",
+       "        [0, 0, 0, ..., 1, 1, 1],\n",
+       "        [0, 0, 0, ..., 1, 1, 1]]])\n",
+       "Coordinates:\n",
+       "  * y        (y) float64 800B 6.64e+05 6.64e+05 6.64e+05 ... 6.65e+05 6.65e+05\n",
+       "  * x        (x) float64 800B 5.612e+06 5.612e+06 ... 5.611e+06 5.611e+06\n",
+       "Dimensions without coordinates: bands
" + ], + "text/plain": [ + " Size: 40kB\n", + "array([[[1, 1, 1, ..., 1, 1, 1],\n", + " [1, 1, 1, ..., 0, 0, 0],\n", + " [1, 1, 1, ..., 0, 0, 0],\n", + " ...,\n", + " [0, 0, 0, ..., 1, 1, 1],\n", + " [0, 0, 0, ..., 1, 1, 1],\n", + " [0, 0, 0, ..., 1, 1, 1]]])\n", + "Coordinates:\n", + " * y (y) float64 800B 6.64e+05 6.64e+05 6.64e+05 ... 6.65e+05 6.65e+05\n", + " * x (x) float64 800B 5.612e+06 5.612e+06 ... 5.611e+06 5.611e+06\n", + "Dimensions without coordinates: bands" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -217,6 +680,8 @@ "import sys\n", "import urllib.request\n", "import shutil\n", + "from pyproj import Transformer\n", + "import numpy as np\n", "\n", "import requests\n", "import xarray as xr\n", @@ -240,7 +705,6 @@ "abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0])\n", "sys.path.append(abs_path)\n", "\n", - "\n", "# Get Data\n", "url = \"https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc\"\n", "filename = \"belgium_good_2020-12-01_2021-11-30.nc\"\n", @@ -253,16 +717,22 @@ "\n", "# Read the file into xarray\n", "ds = xr.open_dataset(filename)\n", + "\n", + "\n", "arr = ds.drop('crs').to_array(dim='bands')\n", + "orig_dims = list(arr.dims)\n", + "\n", + "orig_dims.remove(\"t\")\n", "map_dims = arr.shape[2:]\n", "\n", "#Get Presto\n", - "from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost\n", + "from mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost\n", "\n", "#bands: 19, t: 12y, : 100x: 100y\n", "data_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc'\n", "# Fetch the data from the URL\n", "response = requests.get(data_url)\n", + "\n", "#100x100,128\n", "presto_path = \"https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt\"\n", "features = get_presto_features(arr, presto_path) \n", @@ -270,12 +740,18 @@ "#Get CATBOOST\n", "CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx'\n", "classification = classify_with_catboost(features, map_dims, CATBOOST_PATH)\n", - "\n" + "\n", + "#revert to xarray\n", + "transformer = Transformer.from_crs(f\"EPSG:{4326}\", \"EPSG:4326\", always_xy=True)\n", + "longitudes, latitudes = transformer.transform(arr.x, arr.y)\n", + "output = xr.DataArray(np.expand_dims(classification, axis=0), dims=orig_dims, coords={'y': longitudes, 'x': latitudes})\n", + "\n", + "output\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 8, "id": "5203744b", "metadata": {}, "outputs": [ @@ -292,13 +768,36 @@ ], "source": [ "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", "\n", "# Plot the data as an image\n", - "plt.imshow(classification, cmap='gray') # Assuming it's a grayscale image\n", + "plt.imshow(np.array(classification), cmap='gray') # Assuming it's a grayscale image\n", "plt.colorbar() # Add a colorbar for reference\n", "plt.show()" ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b7c81502", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'classification' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[4], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mclassification\u001b[49m\n", + "\u001b[1;31mNameError\u001b[0m: name 'classification' is not defined" + ] + } + ], + "source": [ + "classification" + ] } ], "metadata": { diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py index 4fe99309..a0ea83b7 100644 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -68,7 +68,7 @@ def load_model(self, model): # Load the dependency into an InferenceSession self.onnx_session = onnxruntime.InferenceSession(model) - def predict(self, features: pd.DataFrame) -> pd.DataFrame: + def predict(self, features: np.ndarray) -> np.ndarray: """ Predicts labels using the provided features DataFrame. @@ -323,7 +323,7 @@ def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFram return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) - def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326): + def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> tuple: eo, dynamic_world, months, latlons, mask = self._create_presto_input(inarr, epsg) dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) @@ -334,7 +334,7 @@ def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326): return features -def get_presto_features(inarr: xr.DataArray, presto_path: str) -> xr.DataArray: +def get_presto_features(inarr: xr.DataArray, presto_path: str) -> tuple: """ Extracts features from input data using Presto. @@ -372,7 +372,9 @@ def classify_with_catboost(features: np.ndarray, map_dims: tuple, model_path: st predictor.load_model(catboost_model) predictions = predictor.predict(features) - predictions = np.flip(np.array(predictions.reshape(map_dims)),axis=0) + predictions = np.flip(predictions.reshape(map_dims),axis=0) + output = xr.DataArray(predictions) - return predictions \ No newline at end of file + + return output \ No newline at end of file diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index ae88f182..af0912b2 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -6,7 +6,10 @@ import functools import xarray as xr from typing import Dict -from openeo.metadata import CollectionMetadata +from openeo.metadata import CollectionMetadata, Band +import numpy as np +from pyproj import Transformer +import openeo def _setup_logging(): @@ -34,17 +37,20 @@ def extract_dependencies(base_url: str, dependency_name: str): return(abs_path) +def apply_metadata(metadata:CollectionMetadata, context:dict) -> CollectionMetadata: -def apply_metadata(input_metadata:CollectionMetadata, context:dict) -> CollectionMetadata: - - xstep = input_metadata.get('x','step') - ystep = input_metadata.get('y','step') - + xstep = metadata.get('x','step') + ystep = metadata.get('y','step') new_metadata = { "x": {"type": "spatial", "axis": "x", "step": xstep, "reference_system": 4326}, "y": {"type": "spatial", "axis": "y", "step": ystep, "reference_system": 4326}, - } + "t": {"type": "temporal", "extend": "2020-01-01"} + } + + inserted_band = [openeo.metadata.Band("classification", None, None)] + new_metadata.band_dimension.bands = Band(inserted_band) + return CollectionMetadata(new_metadata) @@ -52,7 +58,8 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger = _setup_logging() - + # shape and indiches for output + orig_dims = list(cube.dims) map_dims = cube.shape[2:] logger.info("Unzipping dependencies") @@ -67,7 +74,6 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: sys.path.append(str(dep_dir)) sys.path.append(str(dep_dir) + '/pandas') - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost logger.info("Reading in required libs") @@ -80,8 +86,17 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" classification = classify_with_catboost(features, map_dims, CATBOOST_PATH) + logger.info("Revert to 4D xarray") + transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) + longitudes, latitudes = transformer.transform(cube.x, cube.y) + + output = np.expand_dims(np.expand_dims(classification, axis = 0) ,axis = 0) + output = xr.DataArray(output, dims=orig_dims, coords={'y': longitudes, 'x': latitudes}) + + return output + + - return classification From 2ec4ebd5e20a2ec8e31087c3203a71a61b6a6e06 Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Wed, 8 May 2024 15:58:09 +0200 Subject: [PATCH 05/31] rework UDF and include presto UDF --- .../backend_inference_example_openeo.ipynb | 714 ++---------------- minimal_wc_presto/dev_testing.py | 83 ++ minimal_wc_presto/inference.py | 120 --- .../mvp_wc_presto/world_cereal_inference.py | 19 +- minimal_wc_presto/udf_presto.py | 94 +++ .../udf_worldcereal_inference.py | 40 +- 6 files changed, 251 insertions(+), 819 deletions(-) create mode 100644 minimal_wc_presto/dev_testing.py delete mode 100644 minimal_wc_presto/inference.py create mode 100644 minimal_wc_presto/udf_presto.py diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index 23ed5377..727a33e5 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -31,7 +31,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "5494c46d", "metadata": {}, "outputs": [], @@ -60,14 +60,12 @@ " S2_collection= \"SENTINEL2_L2A\",\n", " S1_collection= \"SENTINEL1_GRD\",\n", " DEM_collection= \"COPERNICUS_30\"\n", - ")\n", - "\n", - "\n" + ")\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "id": "8f71136c-1252-4786-8609-8bb995da7daf", "metadata": { "tags": [] @@ -77,60 +75,27 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-2405070d4ed64599bd19488b2ed13b77': send 'start'\n", - "0:00:15 Job 'j-2405070d4ed64599bd19488b2ed13b77': created (progress N/A)\n", - "0:00:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': created (progress N/A)\n", - "0:00:27 Job 'j-2405070d4ed64599bd19488b2ed13b77': created (progress N/A)\n", - "0:00:35 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:00:45 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:00:57 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:01:18 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:01:37 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:02:01 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:02:31 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:03:09 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:04:05 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:05:04 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:06:04 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:07:04 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:08:05 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:09:10 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:10:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:11:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:12:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:13:11 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:14:12 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:15:12 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:16:13 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:17:13 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:18:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:19:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:20:20 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:21:28 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:22:31 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:23:32 Job 'j-2405070d4ed64599bd19488b2ed13b77': running (progress N/A)\n", - "0:24:32 Job 'j-2405070d4ed64599bd19488b2ed13b77': error (progress N/A)\n", - "Your batch job 'j-2405070d4ed64599bd19488b2ed13b77' failed. Error logs:\n", - "[{'id': '[1715078827306, 132769]', 'time': '2024-05-07T10:47:07.306Z', 'level': 'error', 'message': 'Error communicating with MapOutputTracker'}, {'id': '[1715079108343, 414667]', 'time': '2024-05-07T10:51:48.343Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715079108662, 939819]', 'time': '2024-05-07T10:51:48.662Z', 'level': 'error', 'message': 'RECEIVED SIGNAL TERM'}, {'id': '[1715080070569, 790186]', 'time': '2024-05-07T11:07:50.569Z', 'level': 'error', 'message': 'Task 1 in stage 41.0 failed 4 times; aborting job'}, {'id': '[1715080072928, 108050]', 'time': '2024-05-07T11:07:52.928Z', 'level': 'error', 'message': 'OpenEO batch job failed: Exception during Spark execution: org.apache.spark.api.python.PythonException: Traceback (most recent call last):\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/worker.py\", line 830, in main\\n process()\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/worker.py\", line 822, in process\\n serializer.dump_stream(out_iter, outfile)\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/serializers.py\", line 146, in dump_stream\\n for obj in iterator:\\n File \"/usr/local/spark/python/lib/pyspark.zip/pyspark/util.py\", line 81, in wrapper\\n return f(*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/openeogeotrellis/utils.py\", line 56, in memory_logging_wrapper\\n return function(*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/epsel.py\", line 44, in wrapper\\n return _FUNCTION_POINTERS[key](*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/epsel.py\", line 37, in first_time\\n return f(*args, **kwargs)\\n File \"/opt/openeo/lib/python3.8/site-packages/openeogeotrellis/geopysparkdatacube.py\", line 529, in tile_function\\n result_array = result_array.transpose(*( \\'bands\\', \\'y\\', \\'x\\'))\\n File \"/opt/openeo/lib/python3.8/site-packages/xarray/core/dataarray.py\", line 2154, in transpose\\n dims = tuple(utils.infix_dims(dims, self.dims))\\n File \"/opt/openeo/lib/python3.8/site-packages/xarray/core/utils.py\", line 726, in infix_dims\\n raise ValueError(\\nValueError: (\\'bands\\', \\'y\\', \\'x\\') must be a permuted list of (\\'dim_0\\', \\'dim_1\\'), unless `...` is included\\n'}]\n", - "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-2405070d4ed64599bd19488b2ed13b77').logs()`.\n" - ] - }, - { - "ename": "JobFailedException", - "evalue": "Batch job 'j-2405070d4ed64599bd19488b2ed13b77' didn't finish successfully. Status: error (after 0:24:33).", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[3], line 16\u001b[0m\n\u001b[0;32m 1\u001b[0m udf \u001b[38;5;241m=\u001b[39m openeo\u001b[38;5;241m.\u001b[39mUDF\u001b[38;5;241m.\u001b[39mfrom_file(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mudf_worldcereal_inference.py\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 3\u001b[0m prediction \u001b[38;5;241m=\u001b[39m input_cube\u001b[38;5;241m.\u001b[39mapply_neighborhood(\n\u001b[0;32m 4\u001b[0m process\u001b[38;5;241m=\u001b[39mudf,\n\u001b[0;32m 5\u001b[0m size\u001b[38;5;241m=\u001b[39m[\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 12\u001b[0m ],\n\u001b[0;32m 13\u001b[0m )\n\u001b[1;32m---> 16\u001b[0m \u001b[43mprediction\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtest_output_worldcereal.nc\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal inference\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[43mjob_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mdriver-memory\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m8g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 19\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mexecutor-memoryOverhead\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m8g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", - "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-2405070d4ed64599bd19488b2ed13b77' didn't finish successfully. Status: error (after 0:24:33)." + "0:00:00 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': send 'start'\n", + "0:00:17 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", + "0:00:22 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", + "0:00:29 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", + "0:00:37 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", + "0:00:47 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", + "0:01:00 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", + "0:01:15 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", + "0:01:35 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n", + "0:01:59 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n", + "0:02:29 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n", + "0:03:07 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n" ] } ], "source": [ + "from datetime import datetime\n", + "\n", + "current_datetime = datetime.now()\n", + "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", + "outputfile = str(formatted_datetime) + '_output_worldcereal.nc'\n", "\n", "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", "\n", @@ -146,33 +111,25 @@ " ],\n", ")\n", "\n", + "prediction = prediction.rename_labels(dimension=\"bands\",target= [\"output_catboost\"])\n", "\n", - "prediction.execute_batch(outputfile = 'test_output_worldcereal.nc',\n", - " description='world cereal inference',\n", - " job_options={'driver-memory': '8g',\n", - " 'executor-memoryOverhead':'8g'} )\n" - ] - }, - { - "cell_type": "markdown", - "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", - "metadata": {}, - "source": [ - "### Check result" + "\n", + "prediction.execute_batch(outputfile = outputfile,\n", + " description='world cereal inference',\n", + " job_options={'driver-memory': '8g',\n", + " 'executor-memoryOverhead':'8g'} )\n" ] }, { "cell_type": "code", "execution_count": 7, - "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", - "metadata": { - "tags": [] - }, + "id": "2cf64980", + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "(126, 166)" ] }, "execution_count": 7, @@ -181,9 +138,9 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAApcAAAKWCAYAAAAcMLI0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAlhElEQVR4nO3dfYyV9Znw8WtgYKDIDIhhBlZox0aDrbS+gBQxu00lZRuzkUq6a0I3bNvUXQtWNKmFbrHZbRWx29aAVqvpos1q3ZqsfbGpxtCWxBYRcenq6oIJPnW2dMZtInOolgGZ+/nDZ8/jKEM5M9fMeZnPJzmJnDlz5jfehzPf/IbrvpuKoigCAAASjKv2AgAAaBziEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTNI/XEt99+e3z1q1+N7u7ueP/73x9btmyJCy+88I9+Xn9/fxw4cCCmTp0aTU1NI7U8AABOUlEUcejQoZg9e3aMG/dH9iaLEfDAAw8UEydOLP75n/+5+M///M/i05/+dDFt2rSip6fnj35uV1dXERFubm5ubm5ubm41duvq6vqjLddUFEURyRYtWhQLFy6M2267LSLe2I2cM2dOXH311bFu3boTfm5vb29MmzYtfv30u6L1FL+1b3QfPWv+iH+Nh/Y9U7WvDcDYMtjPnHpX+n1/vPP8/xMHDx6Mtra2Ez42/dfiR44cid27d8f69evL940bNy6WLl0aO3bseNvj+/r6oq+vr/znQ4cORURE6ynjonWquGx0zU0TRvxrDPY6Go2vDcDY0ujtcjL/ZDH9/8Dvfve7OHbsWLS3tw+4v729Pbq7u9/2+I0bN0ZbW1v5NmfOnOwlAQAwSqqe1+vXr4/e3t7yraurq9pLAgBgiNJ/LX7aaafF+PHjo6enZ8D9PT090dHR8bbHt7S0REtLS/YyAACogvS4nDhxYlxwwQWxbdu2WL58eUS8MdCzbdu2WLNmTfaXo849emDPce9fNvvcij8n62tX6kRrBWBsadSfCa8XRyNi/0k9dkTOc3ndddfFqlWrYsGCBXHhhRfGrbfeGq+++mp84hOfGIkvBwBAjRiRuPyrv/qr+J//+Z+44YYboru7O84999x45JFH3jbkAwBAYxmxK/SsWbPGr8EBAMaYqk+LAwDQOMQlAABpRuzX4jBSBpvEy5r+rtSJvm6jTg0CwGDsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQxrQ4dadaU+FDMZRrpwNAPbNzCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBqnIqImneh0Q4OdxqeeTlEEAI3KziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLQxVkTbYPNjkPANVi5xIAgDTiEgCANOISAIA04hIAgDTiEgCANKbFqTuDTVo38jXHTYUDUC/sXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQxrQ41IFKJ+QBoFrsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQxrQ4DaMRriFeqaF8zybMARhJdi4BAEgjLgEASCMuAQBIIy4BAEgjLgEASGNanIYx2BT0WJwiB4BqsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGqcigjFmsFMzDXYqJwCohJ1LAADSiEsAANKISwAA0ohLAADSiEsAANKYFgciovIpclPnAByPnUsAANKISwAA0ohLAADSiEsAANKISwAA0pgWByLClDcAOexcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJDGtcVpeINdM/vRA3tGdR21zv8PADLYuQQAII24BAAgjbgEACCNuAQAII24BAAgjWlxGp4p6OEZbNoeAI7HziUAAGnEJQAAacQlAABpxCUAAGnEJQAAacQlAABpnIqIhjfYqXScogiARjHSP9NKh/pj+lkn91g7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OY8xg0/MA1K8TvbeP9tlR7FwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkKZmp8U/etb8aG6acFKPdY3oxjOUqTevA6AWDeW9yVkdyFTp62m4P0/tXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQpqkoiqLai3izUqkUbW1t8cq+M6J16vDadyjTdiaOa99oT73VI5OmUN8Ge9/yd7vxVHqsh/LayPg5WDrUH9PP2h+9vb3R2tp6wsfauQQAII24BAAgjbgEACCNuAQAIE1Fcblx48ZYuHBhTJ06NWbOnBnLly+PvXv3DnjM4cOHY/Xq1TFjxow45ZRTYsWKFdHT05O6aAAAalNF0+J//ud/HldccUUsXLgwXn/99fjCF74Qzz77bDz33HMxZcqUiIi46qqr4sc//nHcc8890dbWFmvWrIlx48bFL37xi5P6GqM1LZ41nQXVZHIUxhZT5IxGjxzv9fR6cTR+Hj84qWnx5kq+2COPPDLgz/fcc0/MnDkzdu/eHX/6p38avb298e1vfzvuv//++NCHPhQREVu3bo2zzz47nnjiifjABz5QyZcDAKDODGtrsLe3NyIiTj311IiI2L17dxw9ejSWLl1afsy8efNi7ty5sWPHjuF8KQAA6kBFO5dv1t/fH2vXro0lS5bEOeecExER3d3dMXHixJg2bdqAx7a3t0d3d/dxn6evry/6+vrKfy6VSkNdEgAAVTbkncvVq1fHs88+Gw888MCwFrBx48Zoa2sr3+bMmTOs5wMAoHqGFJdr1qyJhx9+OH72s5/F6aefXr6/o6Mjjhw5EgcPHhzw+J6enujo6Djuc61fvz56e3vLt66urqEsCQCAGlDRr8WLooirr746Hnroofj5z38enZ2dAz5+wQUXxIQJE2Lbtm2xYsWKiIjYu3dvvPTSS7F48eLjPmdLS0u0tLQMcfknNpSJKlPhANSqkZ4KP9HPwKyvXYs/ZzOv4z3S6uHMABXF5erVq+P++++PH/zgBzF16tTyv6Nsa2uLyZMnR1tbW3zqU5+K6667Lk499dRobW2Nq6++OhYvXmxSHABgDKgoLu+4446IiPjgBz844P6tW7fG3/zN30RExDe+8Y0YN25crFixIvr6+mLZsmXxzW9+M2WxAADUtop/Lf7HTJo0KW6//fa4/fbbh7woAADqk2uLAwCQRlwCAJBGXAIAkGbIV+jhDZmnL6jF0zPUokr/n9eTejjFBNC4Mt+D6uk9udK1Zn5vjfi+b+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxYepmhNmjawRp+cAat1QfkZ5vx6eWuyC4R5TO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEt3oAa4XrnJ/q6jTCZ2AjfA0BEbU47MzzHO6alQ/0x/ayT+3w7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS3egKp5vfNGmIJuhO8BgLer9GwqDI2dSwAA0ohLAADSiEsAANKISwAA0ohLAADSNBVFUVR7EW9WKpWira0tXtl3RrRO1b6NInMCu1pTfabIgUYx2Pvoid7nTFSPbW9cW3x/9Pb2Rmtr6wkfq94AAEgjLgEASCMuAQBIIy4BAEgjLgEASOPa4oyKSqcMR2My2/Q3MFZ5/2Mk2bkEACCNuAQAII24BAAgjbgEACCNuAQAII1pcUaF6W8AGBvsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJDGqYhoGI8e2HPc+52iCABGj51LAADSiEsAANKISwAA0ohLAADSiEsAANKYFqcmDTb5fSKmwgGg+uxcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJDGtDg16UST30OZJAdgeAZ7X/aezFvZuQQAII24BAAgjbgEACCNuAQAII24BAAgjWlxapLpQ4Da4n25ttXSWVbsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQpqkoiqLai3izUqkUbW1t8cq+M6J1qvZtdCeabgOgfpkubyylQ/0x/az90dvbG62trSd8rHoDACCNuAQAII24BAAgjbgEACCNuAQAII1rizMqBpsKH2ya0BQ5ANQnO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApBGXAACkcSoiUlV6CiGnHAKAxmLnEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTiEgCANOISAIA0ri1eB0b6+tuPHthT8ee4JjgAcDx2LgEASCMuAQBIIy4BAEgjLgEASCMuAQBIY1qcIRlswtwUOQARg/88GMoZSrJU+jOqmmutZ3YuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhTs9PiHz1rfjQ3TRjWc9TblFe1Jq1NeAMwFtRbF9QrO5cAAKQRlwAApBGXAACkEZcAAKQZVlzefPPN0dTUFGvXri3fd/jw4Vi9enXMmDEjTjnllFixYkX09PQMd50AANSBIU+L79q1K771rW/F+973vgH3X3vttfHjH/84HnzwwWhra4s1a9bE5ZdfHr/4xS+GvdhKDWUKutJrZlc6eWYyG4CxrBavOU6uIe1c/v73v4+VK1fG3XffHdOnTy/f39vbG9/+9rfj61//enzoQx+KCy64ILZu3Rq//OUv44knnkhbNAAAtWlIcbl69eq49NJLY+nSpQPu3717dxw9enTA/fPmzYu5c+fGjh07hrdSAABqXsW/Fn/ggQfi6aefjl27dr3tY93d3TFx4sSYNm3agPvb29uju7v7uM/X19cXfX195T+XSqVKlwQAQI2oaOeyq6srrrnmmrjvvvti0qRJKQvYuHFjtLW1lW9z5sxJeV4AAEZfRXG5e/fuePnll+P888+P5ubmaG5uju3bt8fmzZujubk52tvb48iRI3Hw4MEBn9fT0xMdHR3Hfc7169dHb29v+dbV1TXkbwYAgOqq6Nfil1xySTzzzDMD7vvEJz4R8+bNi89//vMxZ86cmDBhQmzbti1WrFgRERF79+6Nl156KRYvXnzc52xpaYmWlpYhLh8AgFpSUVxOnTo1zjnnnAH3TZkyJWbMmFG+/1Of+lRcd911ceqpp0Zra2tcffXVsXjx4vjABz6Qt+oRVOmpgpxaCADezqmFxq4hn+dyMN/4xjdi3LhxsWLFiujr64tly5bFN7/5zewvAwBADWoqiqKo9iLerFQqRVtbW3wwLovmpgnVXg4AMAR2LhtL6VB/TD9rf/T29kZra+sJH+va4gAApBGXAACkEZcAAKRJH+gBYGw40b+pcyaNscO/reSt7FwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkMa0OABDYiJ8bDEVzsmycwkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGtPiAEBEmAgnh51LAADSiEsAANKISwAA0ohLAADSiEsAANKYFgeAMcZUOCPJziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLA8AYs2z2uce93xQ5GexcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkGbMnoposNMtDHZ6BgAA/jg7lwAApBGXAACkEZcAAKQRlwAApBGXAACkGbPT4qbCAWh0zoxCNdi5BAAgjbgEACCNuAQAII24BAAgjbgEACDNmJ0WB4BGMdhUeNbjoRJ2LgEASCMuAQBIIy4BAEgjLgEASCMuAQBIY1qcIXG9WgDgeOxcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJCmZqfFH9r3TLROHdi+JpFrh2MBMPpcE5x6YOcSAIA04hIAgDTiEgCANOISAIA04hIAgDQ1Oy0OAGOVqXDqmZ1LAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0jRXewEAMFY9emBPtZcA6excAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJCmrq4tXovXYF02+9xqL4ExYLDXvtcf1L5a/NkFI8nOJQAAacQlAABpxCUAAGnEJQAAacQlAABp6mpavBZVOgVouhcAaGR2LgEASCMuAQBIIy4BAEgjLgEASCMuAQBIY1oc6oCzDED9GsrfX9cjp57ZuQQAII24BAAgjbgEACCNuAQAII24BAAgTcVx+Zvf/CY+/vGPx4wZM2Ly5Mkxf/78eOqpp8ofL4oibrjhhpg1a1ZMnjw5li5dGi+88ELqogEAqE0VxeUrr7wSS5YsiQkTJsRPfvKTeO655+JrX/taTJ8+vfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtF5Ljdt2hRz5syJrVu3lu/r7Ows/3dRFHHrrbfGF7/4xbjssssiIuI73/lOtLe3x/e///244oorkpYNAEAtqmjn8oc//GEsWLAgPvaxj8XMmTPjvPPOi7vvvrv88RdffDG6u7tj6dKl5fva2tpi0aJFsWPHjuM+Z19fX5RKpQE3AADqU0VxuX///rjjjjvizDPPjEcffTSuuuqq+OxnPxv33ntvRER0d3dHRER7e/uAz2tvby9/7K02btwYbW1t5ducOXOG8n0AAFADKorL/v7+OP/88+Omm26K8847L6688sr49Kc/HXfeeeeQF7B+/fro7e0t37q6uob8XAAAVFdFcTlr1qx4z3veM+C+s88+O1566aWIiOjo6IiIiJ6engGP6enpKX/srVpaWqK1tXXADQCA+lRRXC5ZsiT27t074L59+/bFO9/5zoh4Y7ino6Mjtm3bVv54qVSKnTt3xuLFixOWCwBALatoWvzaa6+Niy66KG666ab4y7/8y3jyySfjrrvuirvuuisiIpqammLt2rXxla98Jc4888zo7OyMDRs2xOzZs2P58uUjsX4AAGpIRXG5cOHCeOihh2L9+vXxj//4j9HZ2Rm33nprrFy5svyY66+/Pl599dW48sor4+DBg3HxxRfHI488EpMmTUpfPAAAtaWpKIqi2ot4s1KpFG1tbfHKvjOidWrjXZ1y2exzq70EAGrcowf2VHsJMEDpUH9MP2t/9Pb2/tH5mMarNwAAqkZcAgCQpqJ/c8nIqvTXIH7FDgDUGjuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLT7KnBgXAGhkdi4BAEgjLgEASCMuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEgjLgEASOPa4nVg2exzq70EAEZRpe/7jx7YMyLrgKGwcwkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAapyICgDo32KmLnKKIarBzCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAa0+JAwxvKxOxg07dQT0bjdWwinbeycwkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGtPiAMcx2ARspddwNnVOo3Ndc97KziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAw1jNKZTK/0aWWs60dS5SXUqfZ2Nxmujmq8/k+rDc7xj93pxNCL2n9Tn27kEACCNuAQAII24BAAgjbgEACCNuAQAII1pcaDuZF33u54M5XuoxQlihifrtTyU56mn10cjvxfUAzuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLV5D6mkSD2g8ozFJ633u5NTiVHO11pT5mqnF118tHuvhsnMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGqciAhrGYKf0GOz0I414CpBal/X/vBZPKQNDUelrOfN9a6T+Htm5BAAgjbgEACCNuAQAII24BAAgjbgEACCNaXGg4ZkKbzyjcUxNpNeGepiOHk318D3YuQQAII24BAAgjbgEACCNuAQAII24BAAgjWnxUVYPU14AVH6temqfa9uPDjuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLQ7UncEmNV1DnNFQ6RS512XjafRjOtxpeDuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLQ4Ax1HpxGyjTxAzdhzvtVw61B/Tzzq5z7dzCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAa0+IAcBymv2Fo7FwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQprnaC2hUy2afW+0lwJgz2N+7Rw/sGdV1AIxldi4BAEgjLgEASCMuAQBIIy4BAEhTUVweO3YsNmzYEJ2dnTF58uR497vfHV/+8pejKIryY4qiiBtuuCFmzZoVkydPjqVLl8YLL7yQvnAAAGpPRXG5adOmuOOOO+K2226L559/PjZt2hS33HJLbNmypfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtGpiH75y1/GZZddFpdeemlERLzrXe+K7373u/Hkk09GxBu7lrfeemt88YtfjMsuuywiIr7zne9Ee3t7fP/7348rrrgiefkAANSSinYuL7rooti2bVvs27cvIiJ+9atfxeOPPx4f+chHIiLixRdfjO7u7li6dGn5c9ra2mLRokWxY8eO4z5nX19flEqlATcAAOpTRTuX69ati1KpFPPmzYvx48fHsWPH4sYbb4yVK1dGRER3d3dERLS3tw/4vPb29vLH3mrjxo3xD//wD0NZOwAANaaincvvfe97cd9998X9998fTz/9dNx7773xT//0T3HvvfcOeQHr16+P3t7e8q2rq2vIzwUAQHVVtHP5uc99LtatW1f+t5Pz58+PX//617Fx48ZYtWpVdHR0RERET09PzJo1q/x5PT09ce655x73OVtaWqKlpWWIywcAoJZUFJevvfZajBs3cLNz/Pjx0d/fHxERnZ2d0dHREdu2bSvHZKlUip07d8ZVV12Vs+Ia4xriAAD/X0Vx+Rd/8Rdx4403xty5c+O9731v/Pu//3t8/etfj09+8pMREdHU1BRr166Nr3zlK3HmmWdGZ2dnbNiwIWbPnh3Lly8fifUDAFBDKorLLVu2xIYNG+Izn/lMvPzyyzF79uz427/927jhhhvKj7n++uvj1VdfjSuvvDIOHjwYF198cTzyyCMxadKk9MUDAFBbmoo3X16nBpRKpWhra4tX9p0RrVNr/+qUfi0Ote/RA3uqvQSAulY61B/Tz9ofvb290draesLH1n69AQBQN8QlAABpKvo3lwC1zK+/AarPziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAwBQdrwLxLxeHI2I/Sf1+XYuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhjWvwtjjchFeGaxQAAJ8POJQAAacQlAABpxCUAAGnEJQAAacQlAABpxCUAAGmciugkDXaKIqB2OJUYwMmr5D2zdKg/pp91cs9r5xIAgDTiEgCANOISAIA04hIAgDTiEgCANDU7Lf7Rs+ZHc9OEAfeZ+ASGYihne/B+AzSK0T7jjZ1LAADSiEsAANKISwAA0ohLAADSiEsAANLU7LT48bi+NzBaqvV+U80pdddmh7FlpP5u27kEACCNuAQAII24BAAgjbgEACCNuAQAIE1dTYsDNLrRmFIfbEJ0NKbCK/3+TKrD8I323yM7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OMMY0+kQ6NLLB/v7W0t8tO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEtDkC6ephoBUaGnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSOBURAOmccgiGp55P52XnEgCANOISAIA04hIAgDTiEgCANOISAIA0psUBGJJ6mFqFWjfYVHg9s3MJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gAAI6wRp8IHY+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxQEAkmRNhT96YE/K81SDnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ri2OABABbKuHx5R39cQH4ydSwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANI4FREAQ+J0LDQ6r/GhsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gBU3WBTuWNpwpbqyZwKx84lAACJxCUAAGnEJQAAacQlAABpxCUAAGlMiwNQs6o5xWtSvX553VSXnUsAANKISwAA0ohLAADSiEsAANLU3EBPURQREfF6HI0oqrwYAMas0qH+ai+BIXq9OFq1r92or5vS79/4vv63006kqTiZR42i//7v/445c+ZUexkAALxFV1dXnH766Sd8TM3FZX9/fxw4cCCmTp0ahw4dijlz5kRXV1e0trZWe2mMsFKp5HiPEY712OFYjx2OdWMriiIOHToUs2fPjnHjTvyvKmvu1+Ljxo0rF3FTU1NERLS2tnqhjiGO99jhWI8djvXY4Vg3rra2tpN6nIEeAADSiEsAANLUdFy2tLTEl770pWhpaan2UhgFjvfY4ViPHY712OFY879qbqAHAID6VdM7lwAA1BdxCQBAGnEJAEAacQkAQJqajsvbb7893vWud8WkSZNi0aJF8eSTT1Z7SQzTxo0bY+HChTF16tSYOXNmLF++PPbu3TvgMYcPH47Vq1fHjBkz4pRTTokVK1ZET09PlVZMlptvvjmamppi7dq15fsc68bxm9/8Jj7+8Y/HjBkzYvLkyTF//vx46qmnyh8viiJuuOGGmDVrVkyePDmWLl0aL7zwQhVXzFAcO3YsNmzYEJ2dnTF58uR497vfHV/+8pcHXG/asaZm4/Jf//Vf47rrrosvfelL8fTTT8f73//+WLZsWbz88svVXhrDsH379li9enU88cQT8dhjj8XRo0fjwx/+cLz66qvlx1x77bXxox/9KB588MHYvn17HDhwIC6//PIqrprh2rVrV3zrW9+K973vfQPud6wbwyuvvBJLliyJCRMmxE9+8pN47rnn4mtf+1pMnz69/JhbbrklNm/eHHfeeWfs3LkzpkyZEsuWLYvDhw9XceVUatOmTXHHHXfEbbfdFs8//3xs2rQpbrnlltiyZUv5MY41UdSoCy+8sFi9enX5z8eOHStmz55dbNy4sYqrItvLL79cRESxffv2oiiK4uDBg8WECROKBx98sPyY559/voiIYseOHdVaJsNw6NCh4swzzywee+yx4s/+7M+Ka665pigKx7qRfP7zny8uvvjiQT/e399fdHR0FF/96lfL9x08eLBoaWkpvvvd747GEkly6aWXFp/85CcH3Hf55ZcXK1euLIrCseYNNblzeeTIkdi9e3csXbq0fN+4ceNi6dKlsWPHjiqujGy9vb0REXHqqadGRMTu3bvj6NGjA479vHnzYu7cuY59nVq9enVceumlA45phGPdSH74wx/GggUL4mMf+1jMnDkzzjvvvLj77rvLH3/xxReju7t7wLFua2uLRYsWOdZ15qKLLopt27bFvn37IiLiV7/6VTz++OPxkY98JCIca97QXO0FHM/vfve7OHbsWLS3tw+4v729Pf7rv/6rSqsiW39/f6xduzaWLFkS55xzTkREdHd3x8SJE2PatGkDHtve3h7d3d1VWCXD8cADD8TTTz8du3btetvHHOvGsX///rjjjjviuuuuiy984Quxa9eu+OxnPxsTJ06MVatWlY/n8d7THev6sm7duiiVSjFv3rwYP358HDt2LG688cZYuXJlRIRjTUTUaFwyNqxevTqeffbZePzxx6u9FEZAV1dXXHPNNfHYY4/FpEmTqr0cRlB/f38sWLAgbrrppoiIOO+88+LZZ5+NO++8M1atWlXl1ZHpe9/7Xtx3331x//33x3vf+97Ys2dPrF27NmbPnu1YU1aTvxY/7bTTYvz48W+bGu3p6YmOjo4qrYpMa9asiYcffjh+9rOfxemnn16+v6OjI44cORIHDx4c8HjHvv7s3r07Xn755Tj//POjubk5mpubY/v27bF58+Zobm6O9vZ2x7pBzJo1K97znvcMuO/ss8+Ol156KSKifDy9p9e/z33uc7Fu3bq44oorYv78+fHXf/3Xce2118bGjRsjwrHmDTUZlxMnTowLLrggtm3bVr6vv78/tm3bFosXL67iyhiuoihizZo18dBDD8VPf/rT6OzsHPDxCy64ICZMmDDg2O/duzdeeuklx77OXHLJJfHMM8/Enj17yrcFCxbEypUry//tWDeGJUuWvO2UYvv27Yt3vvOdERHR2dkZHR0dA451qVSKnTt3OtZ15rXXXotx4wamw/jx46O/vz8iHGv+n2pPFA3mgQceKFpaWop77rmneO6554orr7yymDZtWtHd3V3tpTEMV111VdHW1lb8/Oc/L37729+Wb6+99lr5MX/3d39XzJ07t/jpT39aPPXUU8XixYuLxYsXV3HVZHnztHhRONaN4sknnyyam5uLG2+8sXjhhReK++67r3jHO95R/Mu//Ev5MTfffHMxbdq04gc/+EHxH//xH8Vll11WdHZ2Fn/4wx+quHIqtWrVquJP/uRPiocffrh48cUXi3/7t38rTjvttOL6668vP8axpmbjsiiKYsuWLcXcuXOLiRMnFhdeeGHxxBNPVHtJDFNEHPe2devW8mP+8Ic/FJ/5zGeK6dOnF+94xzuKj370o8Vvf/vb6i2aNG+NS8e6cfzoRz8qzjnnnKKlpaWYN29ecddddw34eH9/f7Fhw4aivb29aGlpKS655JJi7969VVotQ1UqlYprrrmmmDt3bjFp0qTijDPOKP7+7/++6OvrKz/GsaapKN50Wn0AABiGmvw3lwAA1CdxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAECa/ws8KlmAU0tsngAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -191,613 +148,54 @@ } ], "source": [ + "import xarray as xr\n", "import matplotlib.pyplot as plt\n", - "results = result_cube.array.values.squeeze()\n", "\n", - "f, ax = plt.subplots(1, 1, figsize=(10, 8))\n", - "ax.imshow(results)" + "output = xr.open_dataset('test_output_worldcereal.nc')\n", + "output = output['B02'].to_numpy().squeeze()\n", + "plt.imshow(output)\n", + "\n", + "output.shape" ] }, { - "cell_type": "code", - "execution_count": 1, - "id": "85a73ef1", + "cell_type": "markdown", + "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", "metadata": {}, + "source": [ + "### Check reference" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", + "metadata": { + "tags": [] + }, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\VROMPAYH\\AppData\\Local\\Temp\\ipykernel_33340\\4003701718.py:44: DeprecationWarning: dropping variables using `drop` is deprecated; use drop_vars.\n", - " arr = ds.drop('crs').to_array(dim='bands')\n" - ] - }, { "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (bands: 1, y: 100, x: 100)> Size: 40kB\n",
-       "array([[[1, 1, 1, ..., 1, 1, 1],\n",
-       "        [1, 1, 1, ..., 0, 0, 0],\n",
-       "        [1, 1, 1, ..., 0, 0, 0],\n",
-       "        ...,\n",
-       "        [0, 0, 0, ..., 1, 1, 1],\n",
-       "        [0, 0, 0, ..., 1, 1, 1],\n",
-       "        [0, 0, 0, ..., 1, 1, 1]]])\n",
-       "Coordinates:\n",
-       "  * y        (y) float64 800B 6.64e+05 6.64e+05 6.64e+05 ... 6.65e+05 6.65e+05\n",
-       "  * x        (x) float64 800B 5.612e+06 5.612e+06 ... 5.611e+06 5.611e+06\n",
-       "Dimensions without coordinates: bands
" - ], "text/plain": [ - " Size: 40kB\n", - "array([[[1, 1, 1, ..., 1, 1, 1],\n", - " [1, 1, 1, ..., 0, 0, 0],\n", - " [1, 1, 1, ..., 0, 0, 0],\n", - " ...,\n", - " [0, 0, 0, ..., 1, 1, 1],\n", - " [0, 0, 0, ..., 1, 1, 1],\n", - " [0, 0, 0, ..., 1, 1, 1]]])\n", - "Coordinates:\n", - " * y (y) float64 800B 6.64e+05 6.64e+05 6.64e+05 ... 6.65e+05 6.65e+05\n", - " * x (x) float64 800B 5.612e+06 5.612e+06 ... 5.611e+06 5.611e+06\n", - "Dimensions without coordinates: bands" + "" ] }, - "execution_count": 1, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" - } - ], - "source": [ - "from pathlib import Path \n", - "import sys\n", - "import urllib.request\n", - "import shutil\n", - "from pyproj import Transformer\n", - "import numpy as np\n", - "\n", - "import requests\n", - "import xarray as xr\n", - "\n", - "\n", - "#GET DEPENDENCIES\n", - "\n", - "# Generate absolute path for the dependencies folder\n", - "dependencies_dir = Path.cwd() / 'dependencies'\n", - "dependencies_dir.mkdir(exist_ok=True, parents=True)\n", - "\n", - "base_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference'\n", - "dependency_name = \"wc_presto_onnx_dependencies.zip\"\n", - "\n", - "# Download and extract the model file\n", - "modelfile_url = f\"{base_url}/{dependency_name}\"\n", - "#modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name)\n", - "#shutil.unpack_archive(modelfile, extract_dir=dependencies_dir)\n", - "\n", - "# Add the model directory to system path if it's not already there\n", - "abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0])\n", - "sys.path.append(abs_path)\n", - "\n", - "# Get Data\n", - "url = \"https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc\"\n", - "filename = \"belgium_good_2020-12-01_2021-11-30.nc\"\n", - "\n", - "with requests.get(url, stream=True) as r:\n", - " r.raise_for_status()\n", - " with open(filename, 'wb') as f:\n", - " for chunk in r.iter_content(chunk_size=8192):\n", - " f.write(chunk)\n", - "\n", - "# Read the file into xarray\n", - "ds = xr.open_dataset(filename)\n", - "\n", - "\n", - "arr = ds.drop('crs').to_array(dim='bands')\n", - "orig_dims = list(arr.dims)\n", - "\n", - "orig_dims.remove(\"t\")\n", - "map_dims = arr.shape[2:]\n", - "\n", - "#Get Presto\n", - "from mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost\n", - "\n", - "#bands: 19, t: 12y, : 100x: 100y\n", - "data_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc'\n", - "# Fetch the data from the URL\n", - "response = requests.get(data_url)\n", - "\n", - "#100x100,128\n", - "presto_path = \"https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt\"\n", - "features = get_presto_features(arr, presto_path) \n", - "\n", - "#Get CATBOOST\n", - "CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx'\n", - "classification = classify_with_catboost(features, map_dims, CATBOOST_PATH)\n", - "\n", - "#revert to xarray\n", - "transformer = Transformer.from_crs(f\"EPSG:{4326}\", \"EPSG:4326\", always_xy=True)\n", - "longitudes, latitudes = transformer.transform(arr.x, arr.y)\n", - "output = xr.DataArray(np.expand_dims(classification, axis=0), dims=orig_dims, coords={'y': longitudes, 'x': latitudes})\n", - "\n", - "output\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "5203744b", - "metadata": {}, - "outputs": [ + }, { "data": { - "image/png": "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", + "image/png": "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226L559/PjZt2hS33HJLbNmypfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtGpiH75y1/GZZddFpdeemlERLzrXe+K7373u/Hkk09GxBu7lrfeemt88YtfjMsuuywiIr7zne9Ee3t7fP/7348rrrgiefkAANSSinYuL7rooti2bVvs27cvIiJ+9atfxeOPPx4f+chHIiLixRdfjO7u7li6dGn5c9ra2mLRokWxY8eO4z5nX19flEqlATcAAOpTRTuX69ati1KpFPPmzYvx48fHsWPH4sYbb4yVK1dGRER3d3dERLS3tw/4vPb29vLH3mrjxo3xD//wD0NZOwAANaaincvvfe97cd9998X9998fTz/9dNx7773xT//0T3HvvfcOeQHr16+P3t7e8q2rq2vIzwUAQHVVtHP5uc99LtatW1f+t5Pz58+PX//617Fx48ZYtWpVdHR0RERET09PzJo1q/x5PT09ce655x73OVtaWqKlpWWIywcAoJZUFJevvfZajBs3cLNz/Pjx0d/fHxERnZ2d0dHREdu2bSvHZKlUip07d8ZVV12Vs+Ia4xriAAD/X0Vx+Rd/8Rdx4403xty5c+O9731v/Pu//3t8/etfj09+8pMREdHU1BRr166Nr3zlK3HmmWdGZ2dnbNiwIWbPnh3Lly8fifUDAFBDKorLLVu2xIYNG+Izn/lMvPzyyzF79uz427/927jhhhvKj7n++uvj1VdfjSuvvDIOHjwYF198cTzyyCMxadKk9MUDAFBbmoo3X16nBpRKpWhra4tX9p0RrVNr/+qUfi0Ote/RA3uqvQSAulY61B/Tz9ofvb290draesLH1n69AQBQN8QlAABpKvo3lwC1zK+/AarPziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAwBQdrwLxLxeHI2I/Sf1+XYuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhjWvwtjjchFeGaxQAAJ8POJQAAacQlAABpxCUAAGnEJQAAacQlAABpxCUAAGmciugkDXaKIqB2OJUYwMmr5D2zdKg/pp91cs9r5xIAgDTiEgCANOISAIA04hIAgDTiEgCANDU7Lf7Rs+ZHc9OEAfeZ+ASGYihne/B+AzSK0T7jjZ1LAADSiEsAANKISwAA0ohLAADSiEsAANLU7LT48bi+NzBaqvV+U80pdddmh7FlpP5u27kEACCNuAQAII24BAAgjbgEACCNuAQAIE1dTYsDNLrRmFIfbEJ0NKbCK/3+TKrD8I323yM7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OMMY0+kQ6NLLB/v7W0t8tO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEtDkC6ephoBUaGnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSOBURAOmccgiGp55P52XnEgCANOISAIA04hIAgDTiEgCANOISAIA0psUBGJJ6mFqFWjfYVHg9s3MJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gAAI6wRp8IHY+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxQEAkmRNhT96YE/K81SDnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ri2OABABbKuHx5R39cQH4ydSwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANI4FREAQ+J0LDQ6r/GhsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gBU3WBTuWNpwpbqyZwKx84lAACJxCUAAGnEJQAAacQlAABpxCUAAGlMiwNQs6o5xWtSvX553VSXnUsAANKISwAA0ohLAADSiEsAANLU3EBPURQREfF6HI0oqrwYAMas0qH+ai+BIXq9OFq1r92or5vS79/4vv63006kqTiZR42i//7v/445c+ZUexkAALxFV1dXnH766Sd8TM3FZX9/fxw4cCCmTp0ahw4dijlz5kRXV1e0trZWe2mMsFKp5HiPEY712OFYjx2OdWMriiIOHToUs2fPjnHjTvyvKmvu1+Ljxo0rF3FTU1NERLS2tnqhjiGO99jhWI8djvXY4Vg3rra2tpN6nIEeAADSiEsAANLUdFy2tLTEl770pWhpaan2UhgFjvfY4ViPHY712OFY879qbqAHAID6VdM7lwAA1BdxCQBAGnEJAEAacQkAQJqajsvbb7893vWud8WkSZNi0aJF8eSTT1Z7SQzTxo0bY+HChTF16tSYOXNmLF++PPbu3TvgMYcPH47Vq1fHjBkz4pRTTokVK1ZET09PlVZMlptvvjmamppi7dq15fsc68bxm9/8Jj7+8Y/HjBkzYvLkyTF//vx46qmnyh8viiJuuOGGmDVrVkyePDmWLl0aL7zwQhVXzFAcO3YsNmzYEJ2dnTF58uR497vfHV/+8pcHXG/asaZm4/Jf//Vf47rrrosvfelL8fTTT8f73//+WLZsWbz88svVXhrDsH379li9enU88cQT8dhjj8XRo0fjwx/+cLz66qvlx1x77bXxox/9KB588MHYvn17HDhwIC6//PIqrprh2rVrV3zrW9+K973vfQPud6wbwyuvvBJLliyJCRMmxE9+8pN47rnn4mtf+1pMnz69/JhbbrklNm/eHHfeeWfs3LkzpkyZEsuWLYvDhw9XceVUatOmTXHHHXfEbbfdFs8//3xs2rQpbrnlltiyZUv5MY41UdSoCy+8sFi9enX5z8eOHStmz55dbNy4sYqrItvLL79cRESxffv2oiiK4uDBg8WECROKBx98sPyY559/voiIYseOHdVaJsNw6NCh4swzzywee+yx4s/+7M+Ka665pigKx7qRfP7zny8uvvjiQT/e399fdHR0FF/96lfL9x08eLBoaWkpvvvd747GEkly6aWXFp/85CcH3Hf55ZcXK1euLIrCseYNNblzeeTIkdi9e3csXbq0fN+4ceNi6dKlsWPHjiqujGy9vb0REXHqqadGRMTu3bvj6NGjA479vHnzYu7cuY59nVq9enVceumlA45phGPdSH74wx/GggUL4mMf+1jMnDkzzjvvvLj77rvLH3/xxReju7t7wLFua2uLRYsWOdZ15qKLLopt27bFvn37IiLiV7/6VTz++OPxkY98JCIca97QXO0FHM/vfve7OHbsWLS3tw+4v729Pf7rv/6rSqsiW39/f6xduzaWLFkS55xzTkREdHd3x8SJE2PatGkDHtve3h7d3d1VWCXD8cADD8TTTz8du3btetvHHOvGsX///rjjjjviuuuuiy984Quxa9eu+OxnPxsTJ06MVatWlY/n8d7THev6sm7duiiVSjFv3rwYP358HDt2LG688cZYuXJlRIRjTUTUaFwyNqxevTqeffbZePzxx6u9FEZAV1dXXHPNNfHYY4/FpEmTqr0cRlB/f38sWLAgbrrppoiIOO+88+LZZ5+NO++8M1atWlXl1ZHpe9/7Xtx3331x//33x3vf+97Ys2dPrF27NmbPnu1YU1aTvxY/7bTTYvz48W+bGu3p6YmOjo4qrYpMa9asiYcffjh+9rOfxemnn16+v6OjI44cORIHDx4c8HjHvv7s3r07Xn755Tj//POjubk5mpubY/v27bF58+Zobm6O9vZ2x7pBzJo1K97znvcMuO/ss8+Ol156KSKifDy9p9e/z33uc7Fu3bq44oorYv78+fHXf/3Xce2118bGjRsjwrHmDTUZlxMnTowLLrggtm3bVr6vv78/tm3bFosXL67iyhiuoihizZo18dBDD8VPf/rT6OzsHPDxCy64ICZMmDDg2O/duzdeeuklx77OXHLJJfHMM8/Enj17yrcFCxbEypUry//tWDeGJUuWvO2UYvv27Yt3vvOdERHR2dkZHR0dA451qVSKnTt3OtZ15rXXXotx4wamw/jx46O/vz8iHGv+n2pPFA3mgQceKFpaWop77rmneO6554orr7yymDZtWtHd3V3tpTEMV111VdHW1lb8/Oc/L37729+Wb6+99lr5MX/3d39XzJ07t/jpT39aPPXUU8XixYuLxYsXV3HVZHnztHhRONaN4sknnyyam5uLG2+8sXjhhReK++67r3jHO95R/Mu//Ev5MTfffHMxbdq04gc/+EHxH//xH8Vll11WdHZ2Fn/4wx+quHIqtWrVquJP/uRPiocffrh48cUXi3/7t38rTjvttOL6668vP8axpmbjsiiKYsuWLcXcuXOLiRMnFhdeeGHxxBNPVHtJDFNEHPe2devW8mP+8Ic/FJ/5zGeK6dOnF+94xzuKj370o8Vvf/vb6i2aNG+NS8e6cfzoRz8qzjnnnKKlpaWYN29ecddddw34eH9/f7Fhw4aivb29aGlpKS655JJi7969VVotQ1UqlYprrrmmmDt3bjFp0qTijDPOKP7+7/++6OvrKz/GsaapKN50Wn0AABiGmvw3lwAA1CdxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAECa/ws8KlmAU0tsngAAAABJRU5ErkJggg==", "text/plain": [ - "
" + "
" ] }, "metadata": {}, "output_type": "display_data" } ], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "\n", - "# Plot the data as an image\n", - "plt.imshow(np.array(classification), cmap='gray') # Assuming it's a grayscale image\n", - "plt.colorbar() # Add a colorbar for reference\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b7c81502", - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'classification' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[4], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mclassification\u001b[49m\n", - "\u001b[1;31mNameError\u001b[0m: name 'classification' is not defined" - ] - } - ], - "source": [ - "classification" - ] + "source": [] } ], "metadata": { diff --git a/minimal_wc_presto/dev_testing.py b/minimal_wc_presto/dev_testing.py new file mode 100644 index 00000000..d937f482 --- /dev/null +++ b/minimal_wc_presto/dev_testing.py @@ -0,0 +1,83 @@ +#%% +from pathlib import Path + +from pyproj import Transformer +import numpy as np + +import requests +import xarray as xr + + +#%% GET DEPENDENCIES + +# Generate absolute path for the dependencies folder +dependencies_dir = Path.cwd() / 'dependencies' +dependencies_dir.mkdir(exist_ok=True, parents=True) + +base_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference' +dependency_name = "wc_presto_onnx_dependencies.zip" + +# Download and extract the model file +modelfile_url = f"{base_url}/{dependency_name}" +#modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) +#shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + +#Add the model directory to system path if it's not already there +#abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) +#sys.path.append(abs_path) + +# Get Data +#url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc" +#filename = "belgium_good_2020-12-01_2021-11-30.nc" + +#with requests.get(url, stream=True) as r: +# r.raise_for_status() +# with open(filename, 'wb') as f: +# for chunk in r.iter_content(chunk_size=8192): +# f.write(chunk) + +#%% + +# Read the file into xarray +ds = xr.open_dataset('data/belgium_good_2020-12-01_2021-11-30.nc') + + +arr = ds.drop('crs').to_array(dim='bands') +orig_dims = list(arr.dims) +map_dims = arr.shape[2:] + +#%% Get Presto +from mvp_wc_presto.world_cereal_inference import get_presto_features + +#bands: 19, t: 12y, : 100x: 100y +data_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc' +# Fetch the data from the URL +response = requests.get(data_url) + +#10000,128 +presto_path = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" +features = get_presto_features(arr, presto_path) + +#10000, +from mvp_wc_presto.world_cereal_inference import classify_with_catboost + +CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx' +classification = classify_with_catboost(features, CATBOOST_PATH) + + + +#%%revert to xarray +import matplotlib.pyplot as plt + + + +transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) +longitudes, latitudes = transformer.transform(arr.x, arr.y) +classification = np.flip(classification.reshape(map_dims),axis = 0) +classification = np.expand_dims(np.expand_dims(classification, axis=0),axis = 0) +output = xr.DataArray(classification, dims=orig_dims) + +output = output.to_numpy().squeeze() +plt.imshow(output) + +output.shape \ No newline at end of file diff --git a/minimal_wc_presto/inference.py b/minimal_wc_presto/inference.py deleted file mode 100644 index 4707c3fe..00000000 --- a/minimal_wc_presto/inference.py +++ /dev/null @@ -1,120 +0,0 @@ -#%% import require libraries -import logging -import numpy as np - -import xarray as xr -from openeo.udf import XarrayDataCube - -from mvp_wc_presto.world_cereal_inference import PrestoFeatureExtractor, WorldCerealPredictor - -#TODO; -#how do we expect out code the stay stabile when presto changes? - -from mvp_wc_presto.dataops import ( - BANDS_GROUPS_IDX, - NORMED_BANDS, -) -from mvp_wc_presto.presto import Presto - - -#% Mapping from original band names to Presto names -BAND_MAPPING = { - "B02": "B2", - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - "B8A": "B8A", - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", -} - -# Index to band groups mapping -IDX_TO_BAND_GROUPS = { - NORMED_BANDS[idx]: band_group_idx - for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) - for idx in val -} - -def _setup_logging(): - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - return logger - - -def get_presto_features(inarr: xr.DataArray, presto_path: str) -> xr.DataArray: - """ - Extracts features from input data using Presto. - - Args: - inarr (xr.DataArray): Input data as xarray DataArray. - presto_path (str): Path to the pretrained Presto model. - - Returns: - xr.DataArray: Extracted features as xarray DataArray. - """ - logger = _setup_logging() - logger.info("Extracting features using Presto ...") - presto_model = Presto.load_pretrained(model_path=presto_path, strict=False) - presto_extractor = PrestoFeatureExtractor(presto_model) - logger.warning("EPSG is hardcoded to 32631 for the time being!") - features = presto_extractor.extract_presto_features(inarr, epsg=32631) - return features - - -def classify_with_catboost(features: np.ndarray, orig_dims: list, model_path: str) -> xr.DataArray: - """ - Classifies features using the WorldCereal CatBoost model. - - Args: - features (np.ndarray): Features to be classified. - orig_dims (list): Original dimensions of the input data. - model_path (str): Path to the trained CatBoost model. - - Returns: - xr.DataArray: Classified data as xarray DataArray. - """ - logger = _setup_logging() - logger.info("Predicting class using WorldCereal CatBoost model ...") - - predictor = WorldCerealPredictor() - predictor.load_model(model_path) - predictions = predictor.predict(features) - result_da = predictions.to_xarray().to_array(dim="bands").rename({"lon": "x", "lat": "y"}) - result_da = result_da.transpose(*orig_dims) - result_da = result_da.squeeze('bands') - - return result_da - - - -def apply_datacube(cube: XarrayDataCube) -> XarrayDataCube: - logger = _setup_logging() - logger.info("Applying datacube...") - - inarr = cube.get_array() - - PRESTO_PATH = './model/presto.pt' - CATBOOST_PATH = './model/wc_catboost.onnx' - - orig_dims = list(inarr.dims) - orig_dims.remove("t") - - features = get_presto_features(inarr, PRESTO_PATH) - classification = classify_with_catboost(features, orig_dims, CATBOOST_PATH) # Corrected variable name - - return XarrayDataCube(classification) - - -#test_inference_catboost_presto() - - - - - diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py index a0ea83b7..ed1640d8 100644 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -94,9 +94,7 @@ def predict(self, features: np.ndarray) -> np.ndarray: return binary_labels - - - + class PrestoFeatureExtractor: @@ -178,8 +176,6 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: return eo_data, mask - - @staticmethod def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: @@ -323,7 +319,7 @@ def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFram return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) - def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> tuple: + def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> np.ndarray: eo, dynamic_world, months, latlons, mask = self._create_presto_input(inarr, epsg) dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) @@ -334,7 +330,7 @@ def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> tuple return features -def get_presto_features(inarr: xr.DataArray, presto_path: str) -> tuple: +def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: """ Extracts features from input data using Presto. @@ -353,7 +349,7 @@ def get_presto_features(inarr: xr.DataArray, presto_path: str) -> tuple: return features -def classify_with_catboost(features: np.ndarray, map_dims: tuple, model_path: str) -> xr.DataArray: +def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarray: """ Classifies features using the WorldCereal CatBoost model. @@ -367,14 +363,11 @@ def classify_with_catboost(features: np.ndarray, map_dims: tuple, model_path: st """ predictor = WorldCerealPredictor() - response = requests.get(model_path) + response = requests.get(catboost_path) catboost_model = response.content predictor.load_model(catboost_model) predictions = predictor.predict(features) - predictions = np.flip(predictions.reshape(map_dims),axis=0) - - output = xr.DataArray(predictions) - return output \ No newline at end of file + return predictions \ No newline at end of file diff --git a/minimal_wc_presto/udf_presto.py b/minimal_wc_presto/udf_presto.py new file mode 100644 index 00000000..9c6ed1ef --- /dev/null +++ b/minimal_wc_presto/udf_presto.py @@ -0,0 +1,94 @@ +import logging +import urllib.request +import shutil +from pathlib import Path +import sys +import functools +import xarray as xr +from typing import Dict +import numpy as np +from pyproj import Transformer + + +def _setup_logging(): + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + return logger + +@functools.lru_cache(maxsize=6) +def extract_dependencies(base_url: str, dependency_name: str): + + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / 'dependencies' + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) + + return(abs_path) + + +def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: + + logger = _setup_logging() + + # shape and indiches for output + orig_dims = list(cube.dims) + map_dims = cube.shape[2:] + + logger.info("Unzipping dependencies") + base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" + dependency_name = "wc_presto_onnx_dependencies.zip" + + logger.info("Appending depencency") + dep_dir = extract_dependencies(base_url, dependency_name) + + + #directly add a path to the older pandas version + sys.path.append(str(dep_dir)) + sys.path.append(str(dep_dir) + '/pandas') + + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features + + logger.info("Reading in required libs") + + logger.info("Extracting presto features") + PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" + features = get_presto_features(cube, PRESTO_PATH) + + # go to 128,1,100,100 + presto_dim = map_dims + (128,) + features = features.reshape(presto_dim) + features = np.transpose(features, (3, 0, 1, 2)) + + + transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) + longitudes, latitudes = transformer.transform(cube.x, cube.y) + + features = np.expand_dims(features, axis = 0) + output = xr.DataArray(features, dims=orig_dims, coords={'y': longitudes, 'x': latitudes}) + return output + + + + + + + + + + + + + + + + diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index af0912b2..f94582a2 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -6,10 +6,8 @@ import functools import xarray as xr from typing import Dict -from openeo.metadata import CollectionMetadata, Band import numpy as np from pyproj import Transformer -import openeo def _setup_logging(): @@ -37,22 +35,6 @@ def extract_dependencies(base_url: str, dependency_name: str): return(abs_path) -def apply_metadata(metadata:CollectionMetadata, context:dict) -> CollectionMetadata: - - xstep = metadata.get('x','step') - ystep = metadata.get('y','step') - - new_metadata = { - "x": {"type": "spatial", "axis": "x", "step": xstep, "reference_system": 4326}, - "y": {"type": "spatial", "axis": "y", "step": ystep, "reference_system": 4326}, - "t": {"type": "temporal", "extend": "2020-01-01"} - } - - inserted_band = [openeo.metadata.Band("classification", None, None)] - new_metadata.band_dimension.bands = Band(inserted_band) - - return CollectionMetadata(new_metadata) - def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: @@ -62,36 +44,38 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: orig_dims = list(cube.dims) map_dims = cube.shape[2:] + # Unzip de dependencies on the backend logger.info("Unzipping dependencies") base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" dependency_name = "wc_presto_onnx_dependencies.zip" - - logger.info("Appending depencency") dep_dir = extract_dependencies(base_url, dependency_name) - - #directly add a path to the older pandas version + # Append the dependencies sys.path.append(str(dep_dir)) sys.path.append(str(dep_dir) + '/pandas') - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost - logger.info("Reading in required libs") - + # Run presto inference logger.info("Extracting presto features") PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" features = get_presto_features(cube, PRESTO_PATH) + logger.info(str(features.shape)) + # run catboost classification logger.info("Catboost classification") CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" - classification = classify_with_catboost(features, map_dims, CATBOOST_PATH) + classification = classify_with_catboost(features, CATBOOST_PATH) + logger.info(str(classification.shape)) + # revert to 4D shape for openEO logger.info("Revert to 4D xarray") transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) longitudes, latitudes = transformer.transform(cube.x, cube.y) - output = np.expand_dims(np.expand_dims(classification, axis = 0) ,axis = 0) - output = xr.DataArray(output, dims=orig_dims, coords={'y': longitudes, 'x': latitudes}) + classification = np.flip(classification.reshape(map_dims),axis = 0) + classification = np.expand_dims(np.expand_dims(classification, axis=0),axis = 0) + output = xr.DataArray(classification, dims=orig_dims, coords={'y': longitudes, 'x': latitudes}) + logger.info(str(output.shape)) return output From f17e0a8980160c9710d6cbe0b700c0e2f082059a Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Thu, 16 May 2024 15:51:28 +0200 Subject: [PATCH 06/31] fix: resolve presto specific UDF and include udf_long which does not rely on remote mvp_wc_presto for fast debugging --- .../backend_inference_example_openeo.ipynb | 358 ++++++++- minimal_wc_presto/dev_testing.py | 83 --- minimal_wc_presto/job-results.json | 1 + minimal_wc_presto/preprocessing.py | 5 +- minimal_wc_presto/test_aggregator.ipynb | 696 ++++++++++++++++++ .../udf_long_worldcereal_inference.py | 476 ++++++++++++ minimal_wc_presto/udf_presto.py | 5 +- .../udf_worldcereal_inference.py | 19 +- 8 files changed, 1514 insertions(+), 129 deletions(-) delete mode 100644 minimal_wc_presto/dev_testing.py create mode 100644 minimal_wc_presto/job-results.json create mode 100644 minimal_wc_presto/test_aggregator.ipynb create mode 100644 minimal_wc_presto/udf_long_worldcereal_inference.py diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index 727a33e5..f7b5e02c 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 13, "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", "metadata": { "tags": [] @@ -20,26 +20,44 @@ "name": "stdout", "output_type": "stream", "text": [ + "Authenticated using refresh token.\n", "Authenticated using refresh token.\n" ] } ], "source": [ "import openeo\n", + "\n", + "#token for METEO\n", + "connection_terra = openeo.connect(\"openeo.vito.be\").authenticate_oidc()\n", + "\n", + "#token SENTINEL\n", "connection = openeo.connect(\"https://openeo.dataspace.copernicus.eu/\").authenticate_oidc()" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 16, "id": "5494c46d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "TypeError", + "evalue": "worldcereal_preprocessed_inputs() missing 1 required positional argument: 'end'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[16], line 16\u001b[0m\n\u001b[0;32m 10\u001b[0m \u001b[38;5;66;03m# Set OpenEO classification UDF context based on settings\u001b[39;00m\n\u001b[0;32m 11\u001b[0m CONTEXT \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 12\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstartdate\u001b[39m\u001b[38;5;124m\"\u001b[39m: STARTDATE, \u001b[38;5;66;03m# Required\u001b[39;00m\n\u001b[0;32m 13\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124menddate\u001b[39m\u001b[38;5;124m\"\u001b[39m: ENDDATE, \u001b[38;5;66;03m# Required\u001b[39;00m\n\u001b[0;32m 14\u001b[0m }\n\u001b[1;32m---> 16\u001b[0m s2_cube \u001b[38;5;241m=\u001b[39m \u001b[43mworldcereal_preprocessed_inputs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mconnection\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[43mEXTENT\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 19\u001b[0m \u001b[43m \u001b[49m\u001b[43mSTARTDATE\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 20\u001b[0m \u001b[43m \u001b[49m\u001b[43mENDDATE\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 21\u001b[0m \u001b[43m \u001b[49m\u001b[43mMETEO_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 22\u001b[0m \u001b[43m \u001b[49m\u001b[43mS2_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mSENTINEL2_L2A\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 23\u001b[0m \u001b[43m \u001b[49m\u001b[43mS1_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mSENTINEL1_GRD\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 24\u001b[0m \u001b[43m \u001b[49m\u001b[43mDEM_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mCOPERNICUS_30\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\n\u001b[0;32m 25\u001b[0m \u001b[43m)\u001b[49m\n\u001b[0;32m 27\u001b[0m agera5_cube \u001b[38;5;241m=\u001b[39m worldcereal_preprocessed_inputs(\n\u001b[0;32m 28\u001b[0m connection_terra,\n\u001b[0;32m 29\u001b[0m EXTENT,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 35\u001b[0m DEM_collection\u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 36\u001b[0m )\n", + "\u001b[1;31mTypeError\u001b[0m: worldcereal_preprocessed_inputs() missing 1 required positional argument: 'end'" + ] + } + ], "source": [ "#Get desired data\n", "from preprocessing import worldcereal_preprocessed_inputs\n", "\n", - "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.191984, 51.256920, 5.215158, 51.267661]))\n", + "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.19, 51.25, 5.21, 51.26]))\n", "EXTENT['crs'] = \"EPSG:4326\"\n", "\n", "STARTDATE = '2020-11-01'\n", @@ -51,21 +69,125 @@ " \"enddate\": ENDDATE, # Required\n", "}\n", "\n", - "input_cube = worldcereal_preprocessed_inputs(\n", - " connection,\n", - " EXTENT,\n", - " STARTDATE,\n", - " ENDDATE,\n", + "\n", + "\n", + "s2_cube = worldcereal_preprocessed_inputs(\n", + " connection = connection,\n", + " bbox = EXTENT,\n", + " start = STARTDATE,\n", + " end = ENDDATE,\n", " METEO_collection=None,\n", " S2_collection= \"SENTINEL2_L2A\",\n", " S1_collection= \"SENTINEL1_GRD\",\n", " DEM_collection= \"COPERNICUS_30\"\n", - ")\n" + ")\n", + "\n", + "agera5_cube = worldcereal_preprocessed_inputs(\n", + " connection = connection_terra,\n", + " bbox = EXTENT,\n", + " start = STARTDATE,\n", + " end = ENDDATE,\n", + " METEO_collection=\"AGERA5\",\n", + " S2_collection= None,\n", + " S1_collection= None,\n", + " DEM_collection= None\n", + ")\n", + "\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, + "id": "94969249", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "result_dir = Path.cmd\n", + "\n", + "job = agera5_cube.create_job(\n", + " out_format=\"GTIFF\",\n", + ")\n", + "\n", + "job.get_results().download_files(result_dir)\n", + "result_metadata = job.get_results()\n", + "job_url, = [k[\"href\"] for k in result_metadata.get_metadata()[\"links\"] if k[\"rel\"] == \"canonical\"]\n", + "\n", + "load_stac_cube = s2_cube.load_stac(job_url)\n", + "\n", + "input_cube = s2_cube.merge_cubes(load_stac_cube)\n", + "\n", + "job = input_cube.create_job(out_format=\"NetCDF\")\n", + "job.start_and_wait()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "4aab5695", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Preflight process graph validation raised: [CollectionNotFound] Collection 'AGERA5' does not exist.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-2405155e702e4218aa9dfac9671faaff': send 'start'\n", + "0:00:16 Job 'j-2405155e702e4218aa9dfac9671faaff': created (progress 0%)\n", + "0:00:22 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:00:28 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:00:36 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:00:47 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:01:00 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:01:16 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:01:35 Job 'j-2405155e702e4218aa9dfac9671faaff': error (progress N/A)\n", + "Your batch job 'j-2405155e702e4218aa9dfac9671faaff' failed. Error logs:\n", + "[{'id': '[1715756877175, 557437]', 'time': '2024-05-15T07:07:57.175Z', 'level': 'error', 'message': 'OpenEO batch job failed: CollectionNotFoundException(status_code=404, code=\\'CollectionNotFound\\', message=\"Collection \\'AGERA5\\' does not exist.\", id=\\'no-request\\')'}]\n", + "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-2405155e702e4218aa9dfac9671faaff').logs()`.\n" + ] + }, + { + "ename": "JobFailedException", + "evalue": "Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37).", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[11], line 6\u001b[0m\n\u001b[0;32m 3\u001b[0m formatted_datetime \u001b[38;5;241m=\u001b[39m current_datetime\u001b[38;5;241m.\u001b[39mstrftime(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mY_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mm_\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mH_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mM_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mS\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 4\u001b[0m outputfile_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(formatted_datetime) \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m_input_cube_worldCereal.nc\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m----> 6\u001b[0m \u001b[43minput_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43moutputfile_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 7\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal data collection\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", + "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37)." + ] + } + ], + "source": [ + "from datetime import datetime\n", + "current_datetime = datetime.now()\n", + "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", + "outputfile_name = str(formatted_datetime) + '_input_cube_worldCereal.nc'\n", + "\n", + "input_cube.execute_batch(outputfile = outputfile_name,\n", + " description='world cereal data collection')" + ] + }, + { + "cell_type": "markdown", + "id": "48c9322c", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 3, "id": "8f71136c-1252-4786-8609-8bb995da7daf", "metadata": { "tags": [] @@ -75,27 +197,80 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': send 'start'\n", - "0:00:17 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", - "0:00:22 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", - "0:00:29 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", - "0:00:37 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", - "0:00:47 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", - "0:01:00 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", - "0:01:15 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': created (progress 0%)\n", - "0:01:35 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n", - "0:01:59 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n", - "0:02:29 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n", - "0:03:07 Job 'j-240508a1b54a4f7aa5618afbbf2e6865': running (progress N/A)\n" + "0:00:00 Job 'j-240508de680a4a01bad4dfca194be16b': send 'start'\n", + "0:00:28 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", + "0:00:34 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", + "0:00:41 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:00:55 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:05 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:17 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:33 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:52 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:02:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:02:52 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:03:29 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:04:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:05:15 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:06:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:07:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:08:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:09:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:10:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:11:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:12:19 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:13:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:14:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:15:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:16:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:17:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:18:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:19:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:20:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:21:25 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:22:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:23:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:24:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:25:34 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:26:34 Job 'j-240508de680a4a01bad4dfca194be16b': finished (progress 100%)\n" ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "from datetime import datetime\n", "\n", "current_datetime = datetime.now()\n", "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile = str(formatted_datetime) + '_output_worldcereal.nc'\n", + "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", "\n", "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", "\n", @@ -113,16 +288,15 @@ "\n", "prediction = prediction.rename_labels(dimension=\"bands\",target= [\"output_catboost\"])\n", "\n", - "\n", - "prediction.execute_batch(outputfile = outputfile,\n", + "prediction.execute_batch(outputfile = outputfile_name,\n", " description='world cereal inference',\n", - " job_options={'driver-memory': '8g',\n", + " job_options={'driver-memory': '4g',\n", " 'executor-memoryOverhead':'8g'} )\n" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "id": "2cf64980", "metadata": {}, "outputs": [ @@ -132,13 +306,13 @@ "(126, 166)" ] }, - "execution_count": 7, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGhCAYAAADBddZJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAw+ElEQVR4nO3de3SUVZ7u8acuSeVeuUAqCSQQFBtERATECGfUMd2IHkVFbRxUWl0y2qACfRSZbnC0VdTpVgYvoK4eW0+D2s4oKn3ExqCgY7gloCIYgiIEQhI0JJV7KlX7/OF0tSWoXCpvpSrfz1rvWmTvXW9+v7WweHyvNmOMEQAAgEXskS4AAAD0LoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGCpiIaPJ598UgMHDlRCQoLGjh2rjRs3RrIcAABggYiFj5dffllz5szRPffco/Lyco0YMUITJkxQXV1dpEoCAAAWsEXqxXJjx47VmDFj9MQTT0iSAoGA8vPzddttt+nuu+/+wc8GAgFVV1crNTVVNpvNinIBAMAPMMaoqalJeXl5stt/+NiG06KaQnR2dqqsrEzz5s0LjtntdhUXF6u0tPSw9R0dHero6Aj+vH//fp166qmW1AoAAI5eVVWV+vfv/4NrIhI+vvrqK/n9fnk8npBxj8ejzz777LD1Cxcu1L333nvY+HhdJKfiuq1OAABwdLrk0wf6f0pNTf3RtREJH8dq3rx5mjNnTvBnr9er/Px8ORUnp43wAQBAxP3PRRxHczlERMJHnz595HA4VFtbGzJeW1urnJycw9a7XC65XC6rygMAAN0oIne7xMfHa9SoUSopKQmOBQIBlZSUqKioKBIlAQAAi0TstMucOXM0bdo0jR49WmeddZYWLVqklpYW3XDDDZEqCQAAWCBi4ePnP/+5Dh48qAULFqimpkZnnHGGVq1addhFqAAAILZE7DkfJ8Lr9crtdus8TeKCUwAAeoAu49N7el2NjY1KS0v7wbW82wUAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALBU2MPHwoULNWbMGKWmpio7O1uXXXaZKioqQta0t7drxowZysrKUkpKiiZPnqza2tpwlwIAAHqgsIePtWvXasaMGVq/fr1Wr14tn8+nn/3sZ2ppaQmumT17tt5880298sorWrt2raqrq3XFFVeEuxQAANAD2Ywxpjt/wcGDB5Wdna21a9fqH/7hH9TY2Ki+fftq+fLluvLKKyVJn332mYYOHarS0lKdffbZP7pPr9crt9ut8zRJTltcd5YPAACOQpfx6T29rsbGRqWlpf3g2m6/5qOxsVGSlJmZKUkqKyuTz+dTcXFxcM2QIUNUUFCg0tLSI+6jo6NDXq83ZAMAANGpW8NHIBDQrFmzNG7cOJ122mmSpJqaGsXHxys9PT1krcfjUU1NzRH3s3DhQrnd7uCWn5/fnWUDAIBu1K3hY8aMGdq2bZteeumlE9rPvHnz1NjYGNyqqqrCVCEAALCas7t2PHPmTK1cuVLr1q1T//79g+M5OTnq7OxUQ0NDyNGP2tpa5eTkHHFfLpdLLperu0oFAAAWCvuRD2OMZs6cqddee01r1qxRYWFhyPyoUaMUFxenkpKS4FhFRYX27t2roqKicJcDAAB6mLAf+ZgxY4aWL1+u119/XampqcHrONxutxITE+V2u3XTTTdpzpw5yszMVFpamm677TYVFRUd1Z0uAAAguoU9fCxZskSSdN5554WMP/fcc/rFL34hSXrsscdkt9s1efJkdXR0aMKECXrqqafCXQoAAOiBuv05H92B53wAANCz9KjnfAAAAHwb4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEA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9uzgka1Y6fO7jqavnJycwy6G7+rqUn19fVT2/rfgsWfPHq1evTrk1eux0uv777+vuro6FRQUBL+j9uzZo1/96lcaOHCgpNjotU+fPnI6nT/6HdUd38dRFz7i4+M1atQolZSUBMcCgYBKSkpUVFQUwcpOjDFGM2fO1GuvvaY1a9aosLAwZH7UqFGKi4sL6buiokJ79+6Nur4vuOACffLJJ9q6dWtwGz16tKZOnRr8c6z0Om7cuMNumd65c6cGDBggSSosLFROTk5Ir16vVxs2bIiqXltbW2W3h36dOByO4P9ZxUqf33U0fRUVFamhoUFlZWXBNWvWrFEgENDYsWMtr/lE/C14VFZW6p133lFWVlbIfKz0et111+njjz8O+Y7Ky8vTnXfeqbfffltSbPQaHx+vMWPG/OB3VLf923Pcl6pG0EsvvWRcLpf54x//aLZv326mT59u0tPTTU1NTaRLO2633nqrcbvd5r333jMHDhwIbq2trcE1t9xyiykoKDBr1qwxmzdvNkVFRaaoqCiCVYfPt+92MSZ2et24caNxOp3mgQceMJWVlWbZsmUmKSnJ/OlPfwqueeihh0x6erp5/fXXzccff2wmTZpkCgsLTVtbWwQrPzbTpk0z/fr1MytXrjS7d+82r776qunTp4+56667gmuitc+mpiazZcsWs2XLFiPJPProo2bLli3BOzyOpq8LL7zQjBw50mzYsMF88MEHZvDgweaaa66JVEvf64d67ezsNJdeeqnp37+/2bp1a8j3VEdHR3AfsdDrkXz3bhdjoqPXH+vz1VdfNXFxceaZZ54xlZWV5vHHHzcOh8O8//77wX10x/dxVIYPY4x5/PHHTUFBgYmPjzdnnXWWWb9+faRLOiGSjrg999xzwTVtbW3ml7/8pcnIyDBJSUnm8ssvNwcOHIhc0WH03fARS72++eab5rTTTjMul8sMGTLEPPPMMyHzgUDAzJ8/33g8HuNyucwFF1xgKioqIlTt8fF6veaOO+4wBQUFJiEhwQwaNMj8+te/DvlHKVr7fPfdd4/43+a0adOMMUfX19dff22uueYak5KSYtLS0swNN9xgmpqaItDND/uhXnfv3v2931PvvvtucB+x0OuRHCl8REOvR9PnH/7wB3PyySebhIQEM2LECLNixYqQfXTH97HNmG89ghAAAKCbRd01HwAAILoRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgqf8PdBBXOWBRxK8AAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -151,11 +325,127 @@ "import xarray as xr\n", "import matplotlib.pyplot as plt\n", "\n", - "output = xr.open_dataset('test_output_worldcereal.nc')\n", - "output = output['B02'].to_numpy().squeeze()\n", + "output = xr.open_dataset(outputfile_name)\n", + "output = output['output_catboost'].to_numpy().squeeze()\n", "plt.imshow(output)\n", "\n", - "output.shape" + "output.shape\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5b7bea33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-24051411052f466b911c92ea2d1e7b20': send 'start'\n", + "0:00:29 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:00:35 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:00:44 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:00:53 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:01:11 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:01:28 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:01:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:02:15 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:02:43 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:03:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:04:03 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:04:54 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:05:56 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:06:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:08:01 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:09:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:10:18 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:11:22 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:12:23 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:13:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:14:31 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:15:32 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:16:33 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:17:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:18:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:19:35 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:20:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:21:46 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:22:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:23:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:24:58 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:26:00 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:27:02 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:28:04 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:29:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:30:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:31:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "16:12:29 Job 'j-24051411052f466b911c92ea2d1e7b20': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", + "16:13:03 Job 'j-24051411052f466b911c92ea2d1e7b20': finished (progress 100%)\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from datetime import datetime\n", + "\n", + "current_datetime = datetime.now()\n", + "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", + "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", + "\n", + "presto_list = [\"presto_\" + str(i) for i in range(1, 129)]\n", + "\n", + "\n", + "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", + "\n", + "prediction = input_cube.apply_neighborhood(\n", + " process=udf,\n", + " size=[\n", + " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", + " ],\n", + " overlap=[\n", + " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", + " ],\n", + ")\n", + "\n", + "prediction = prediction.rename_labels(dimension=\"bands\",target= presto_list)\n", + "\n", + "prediction.execute_batch(outputfile = outputfile_name,\n", + " description='world cereal inference',\n", + " job_options={'driver-memory': '4g',\n", + " 'executor-memoryOverhead':'8g'} )" ] }, { diff --git a/minimal_wc_presto/dev_testing.py b/minimal_wc_presto/dev_testing.py deleted file mode 100644 index d937f482..00000000 --- a/minimal_wc_presto/dev_testing.py +++ /dev/null @@ -1,83 +0,0 @@ -#%% -from pathlib import Path - -from pyproj import Transformer -import numpy as np - -import requests -import xarray as xr - - -#%% GET DEPENDENCIES - -# Generate absolute path for the dependencies folder -dependencies_dir = Path.cwd() / 'dependencies' -dependencies_dir.mkdir(exist_ok=True, parents=True) - -base_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference' -dependency_name = "wc_presto_onnx_dependencies.zip" - -# Download and extract the model file -modelfile_url = f"{base_url}/{dependency_name}" -#modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) -#shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - -#Add the model directory to system path if it's not already there -#abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) -#sys.path.append(abs_path) - -# Get Data -#url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc" -#filename = "belgium_good_2020-12-01_2021-11-30.nc" - -#with requests.get(url, stream=True) as r: -# r.raise_for_status() -# with open(filename, 'wb') as f: -# for chunk in r.iter_content(chunk_size=8192): -# f.write(chunk) - -#%% - -# Read the file into xarray -ds = xr.open_dataset('data/belgium_good_2020-12-01_2021-11-30.nc') - - -arr = ds.drop('crs').to_array(dim='bands') -orig_dims = list(arr.dims) -map_dims = arr.shape[2:] - -#%% Get Presto -from mvp_wc_presto.world_cereal_inference import get_presto_features - -#bands: 19, t: 12y, : 100x: 100y -data_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc' -# Fetch the data from the URL -response = requests.get(data_url) - -#10000,128 -presto_path = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" -features = get_presto_features(arr, presto_path) - -#10000, -from mvp_wc_presto.world_cereal_inference import classify_with_catboost - -CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx' -classification = classify_with_catboost(features, CATBOOST_PATH) - - - -#%%revert to xarray -import matplotlib.pyplot as plt - - - -transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) -longitudes, latitudes = transformer.transform(arr.x, arr.y) -classification = np.flip(classification.reshape(map_dims),axis = 0) -classification = np.expand_dims(np.expand_dims(classification, axis=0),axis = 0) -output = xr.DataArray(classification, dims=orig_dims) - -output = output.to_numpy().squeeze() -plt.imshow(output) - -output.shape \ No newline at end of file diff --git a/minimal_wc_presto/job-results.json b/minimal_wc_presto/job-results.json new file mode 100644 index 00000000..bac243a9 --- /dev/null +++ b/minimal_wc_presto/job-results.json @@ -0,0 +1 @@ +{"assets": {"openEO_2020-11-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/5280a7fab73a3af7d65951d1ccc0edc7/openEO_2020-11-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28200.0, "mean": 28200.0, "minimum": 28200.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 3161.0, "mean": 3161.0, "minimum": 3161.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2020-11-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2020-12-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/04cff14b611d54240522833210762931/openEO_2020-12-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27864.0, "mean": 27864.0, "minimum": 27864.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 10083.0, "mean": 10083.0, "minimum": 10083.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2020-12-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-01-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 650, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/256561a0d78d5b22963c5d59f4768cd5/openEO_2021-01-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27609.0, "mean": 27609.0, "minimum": 27609.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 11985.0, "mean": 11985.0, "minimum": 11985.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-01-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-02-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/6259c389f92cda20f278a1c343486931/openEO_2021-02-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27772.0, "mean": 27772.0, "minimum": 27772.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 7615.0, "mean": 7615.0, "minimum": 7615.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-02-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-03-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8a1578c8d890289751276205a0864103/openEO_2021-03-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27964.0, "mean": 27964.0, "minimum": 27964.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 4934.0, "mean": 4934.0, "minimum": 4934.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-03-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-04-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/cd7a84f1e9dcd5107a01a6a3db1d2a90/openEO_2021-04-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27975.0, "mean": 27975.0, "minimum": 27975.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 4408.0, "mean": 4408.0, "minimum": 4408.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-04-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-05-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8cef27e840684882775f0a8b46671209/openEO_2021-05-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28470.0, "mean": 28470.0, "minimum": 28470.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 10904.0, "mean": 10904.0, "minimum": 10904.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-05-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-06-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/a8c32bef4d950e8fe3ef37eaca87ee31/openEO_2021-06-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 29217.0, "mean": 29217.0, "minimum": 29217.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 14132.0, "mean": 14132.0, "minimum": 14132.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-06-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-07-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 650, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3e5e262c7faeb68d52a18f012bf7fe3f/openEO_2021-07-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 29138.0, "mean": 29138.0, "minimum": 29138.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 11926.0, "mean": 11926.0, "minimum": 11926.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-07-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-08-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/0b8d41a9211197d5be684162746fb830/openEO_2021-08-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 29034.0, "mean": 29034.0, "minimum": 29034.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 11072.0, "mean": 11072.0, "minimum": 11072.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-08-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-09-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3448e70d84ed3f2eb6e399a86b2f8b3d/openEO_2021-09-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28970.0, "mean": 28970.0, "minimum": 28970.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 3166.0, "mean": 3166.0, "minimum": 3166.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-09-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-10-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/9c3ea792c00e0a1b63231b8b149f997a/openEO_2021-10-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28489.0, "mean": 28489.0, "minimum": 28489.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 10229.0, "mean": 10229.0, "minimum": 10229.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-10-01Z.tif", "type": "image/tiff; application=geotiff"}}, "description": "Results for batch job j-2405169cad524b05a8f1194330e4c44d", "extent": {"spatial": {"bbox": [[5.19, 51.25, 5.21, 51.26]]}, "temporal": {"interval": [["2020-11-01T00:00:00Z", "2021-10-31T00:00:00Z"]]}}, "id": "j-2405169cad524b05a8f1194330e4c44d", "license": "proprietary", "links": [{"href": "/data/MTDA/AgERA5/2020/20201101/AgERA5_dewpoint-temperature_20201101.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201101/AgERA5_dewpoint-temperature_20201101.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201102/AgERA5_dewpoint-temperature_20201102.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201102/AgERA5_dewpoint-temperature_20201102.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201103/AgERA5_dewpoint-temperature_20201103.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201103/AgERA5_dewpoint-temperature_20201103.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201104/AgERA5_dewpoint-temperature_20201104.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201104/AgERA5_dewpoint-temperature_20201104.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201105/AgERA5_dewpoint-temperature_20201105.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201105/AgERA5_dewpoint-temperature_20201105.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201106/AgERA5_dewpoint-temperature_20201106.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201106/AgERA5_dewpoint-temperature_20201106.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201107/AgERA5_dewpoint-temperature_20201107.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201107/AgERA5_dewpoint-temperature_20201107.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201108/AgERA5_dewpoint-temperature_20201108.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201108/AgERA5_dewpoint-temperature_20201108.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201109/AgERA5_dewpoint-temperature_20201109.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201109/AgERA5_dewpoint-temperature_20201109.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201110/AgERA5_dewpoint-temperature_20201110.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201110/AgERA5_dewpoint-temperature_20201110.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201111/AgERA5_dewpoint-temperature_20201111.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201111/AgERA5_dewpoint-temperature_20201111.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201112/AgERA5_dewpoint-temperature_20201112.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201112/AgERA5_dewpoint-temperature_20201112.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201113/AgERA5_dewpoint-temperature_20201113.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201113/AgERA5_dewpoint-temperature_20201113.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201114/AgERA5_dewpoint-temperature_20201114.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201114/AgERA5_dewpoint-temperature_20201114.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201115/AgERA5_dewpoint-temperature_20201115.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201115/AgERA5_dewpoint-temperature_20201115.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201116/AgERA5_dewpoint-temperature_20201116.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201116/AgERA5_dewpoint-temperature_20201116.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201117/AgERA5_dewpoint-temperature_20201117.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201117/AgERA5_dewpoint-temperature_20201117.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201118/AgERA5_dewpoint-temperature_20201118.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201118/AgERA5_dewpoint-temperature_20201118.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201119/AgERA5_dewpoint-temperature_20201119.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201119/AgERA5_dewpoint-temperature_20201119.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201120/AgERA5_dewpoint-temperature_20201120.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201120/AgERA5_dewpoint-temperature_20201120.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201121/AgERA5_dewpoint-temperature_20201121.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201121/AgERA5_dewpoint-temperature_20201121.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201122/AgERA5_dewpoint-temperature_20201122.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201122/AgERA5_dewpoint-temperature_20201122.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201123/AgERA5_dewpoint-temperature_20201123.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201123/AgERA5_dewpoint-temperature_20201123.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201124/AgERA5_dewpoint-temperature_20201124.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201124/AgERA5_dewpoint-temperature_20201124.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201125/AgERA5_dewpoint-temperature_20201125.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201125/AgERA5_dewpoint-temperature_20201125.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201126/AgERA5_dewpoint-temperature_20201126.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201126/AgERA5_dewpoint-temperature_20201126.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201127/AgERA5_dewpoint-temperature_20201127.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201127/AgERA5_dewpoint-temperature_20201127.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201128/AgERA5_dewpoint-temperature_20201128.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201128/AgERA5_dewpoint-temperature_20201128.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201129/AgERA5_dewpoint-temperature_20201129.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201129/AgERA5_dewpoint-temperature_20201129.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201130/AgERA5_dewpoint-temperature_20201130.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201130/AgERA5_dewpoint-temperature_20201130.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201201/AgERA5_dewpoint-temperature_20201201.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201201/AgERA5_dewpoint-temperature_20201201.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201202/AgERA5_dewpoint-temperature_20201202.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201202/AgERA5_dewpoint-temperature_20201202.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201203/AgERA5_dewpoint-temperature_20201203.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201203/AgERA5_dewpoint-temperature_20201203.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201204/AgERA5_dewpoint-temperature_20201204.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201204/AgERA5_dewpoint-temperature_20201204.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201205/AgERA5_dewpoint-temperature_20201205.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201205/AgERA5_dewpoint-temperature_20201205.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201206/AgERA5_dewpoint-temperature_20201206.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201206/AgERA5_dewpoint-temperature_20201206.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201207/AgERA5_dewpoint-temperature_20201207.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201207/AgERA5_dewpoint-temperature_20201207.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201208/AgERA5_dewpoint-temperature_20201208.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201208/AgERA5_dewpoint-temperature_20201208.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201209/AgERA5_dewpoint-temperature_20201209.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201209/AgERA5_dewpoint-temperature_20201209.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201210/AgERA5_dewpoint-temperature_20201210.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201210/AgERA5_dewpoint-temperature_20201210.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201211/AgERA5_dewpoint-temperature_20201211.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201211/AgERA5_dewpoint-temperature_20201211.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201212/AgERA5_dewpoint-temperature_20201212.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201212/AgERA5_dewpoint-temperature_20201212.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201213/AgERA5_dewpoint-temperature_20201213.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201213/AgERA5_dewpoint-temperature_20201213.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201214/AgERA5_dewpoint-temperature_20201214.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201214/AgERA5_dewpoint-temperature_20201214.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201215/AgERA5_dewpoint-temperature_20201215.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201215/AgERA5_dewpoint-temperature_20201215.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201216/AgERA5_dewpoint-temperature_20201216.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201216/AgERA5_dewpoint-temperature_20201216.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201217/AgERA5_dewpoint-temperature_20201217.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201217/AgERA5_dewpoint-temperature_20201217.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201218/AgERA5_dewpoint-temperature_20201218.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201218/AgERA5_dewpoint-temperature_20201218.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201219/AgERA5_dewpoint-temperature_20201219.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201219/AgERA5_dewpoint-temperature_20201219.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201220/AgERA5_dewpoint-temperature_20201220.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201220/AgERA5_dewpoint-temperature_20201220.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201221/AgERA5_dewpoint-temperature_20201221.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201221/AgERA5_dewpoint-temperature_20201221.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201222/AgERA5_dewpoint-temperature_20201222.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201222/AgERA5_dewpoint-temperature_20201222.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201223/AgERA5_dewpoint-temperature_20201223.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201223/AgERA5_dewpoint-temperature_20201223.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201224/AgERA5_dewpoint-temperature_20201224.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201224/AgERA5_dewpoint-temperature_20201224.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201225/AgERA5_dewpoint-temperature_20201225.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201225/AgERA5_dewpoint-temperature_20201225.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201226/AgERA5_dewpoint-temperature_20201226.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201226/AgERA5_dewpoint-temperature_20201226.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201227/AgERA5_dewpoint-temperature_20201227.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201227/AgERA5_dewpoint-temperature_20201227.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201228/AgERA5_dewpoint-temperature_20201228.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201228/AgERA5_dewpoint-temperature_20201228.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201229/AgERA5_dewpoint-temperature_20201229.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201229/AgERA5_dewpoint-temperature_20201229.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201230/AgERA5_dewpoint-temperature_20201230.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201230/AgERA5_dewpoint-temperature_20201230.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201231/AgERA5_dewpoint-temperature_20201231.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201231/AgERA5_dewpoint-temperature_20201231.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210101/AgERA5_dewpoint-temperature_20210101.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210101/AgERA5_dewpoint-temperature_20210101.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210102/AgERA5_dewpoint-temperature_20210102.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210102/AgERA5_dewpoint-temperature_20210102.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210103/AgERA5_dewpoint-temperature_20210103.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210103/AgERA5_dewpoint-temperature_20210103.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210104/AgERA5_dewpoint-temperature_20210104.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210104/AgERA5_dewpoint-temperature_20210104.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210105/AgERA5_dewpoint-temperature_20210105.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210105/AgERA5_dewpoint-temperature_20210105.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210106/AgERA5_dewpoint-temperature_20210106.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210106/AgERA5_dewpoint-temperature_20210106.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210107/AgERA5_dewpoint-temperature_20210107.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210107/AgERA5_dewpoint-temperature_20210107.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210108/AgERA5_dewpoint-temperature_20210108.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210108/AgERA5_dewpoint-temperature_20210108.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210109/AgERA5_dewpoint-temperature_20210109.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210109/AgERA5_dewpoint-temperature_20210109.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210110/AgERA5_dewpoint-temperature_20210110.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210110/AgERA5_dewpoint-temperature_20210110.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210111/AgERA5_dewpoint-temperature_20210111.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210111/AgERA5_dewpoint-temperature_20210111.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210112/AgERA5_dewpoint-temperature_20210112.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210112/AgERA5_dewpoint-temperature_20210112.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210113/AgERA5_dewpoint-temperature_20210113.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210113/AgERA5_dewpoint-temperature_20210113.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210114/AgERA5_dewpoint-temperature_20210114.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210114/AgERA5_dewpoint-temperature_20210114.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210115/AgERA5_dewpoint-temperature_20210115.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210115/AgERA5_dewpoint-temperature_20210115.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210116/AgERA5_dewpoint-temperature_20210116.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210116/AgERA5_dewpoint-temperature_20210116.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210117/AgERA5_dewpoint-temperature_20210117.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210117/AgERA5_dewpoint-temperature_20210117.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210118/AgERA5_dewpoint-temperature_20210118.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210118/AgERA5_dewpoint-temperature_20210118.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210119/AgERA5_dewpoint-temperature_20210119.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210119/AgERA5_dewpoint-temperature_20210119.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210120/AgERA5_dewpoint-temperature_20210120.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210120/AgERA5_dewpoint-temperature_20210120.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210121/AgERA5_dewpoint-temperature_20210121.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210121/AgERA5_dewpoint-temperature_20210121.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210122/AgERA5_dewpoint-temperature_20210122.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210122/AgERA5_dewpoint-temperature_20210122.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210123/AgERA5_dewpoint-temperature_20210123.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210123/AgERA5_dewpoint-temperature_20210123.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210124/AgERA5_dewpoint-temperature_20210124.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210124/AgERA5_dewpoint-temperature_20210124.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210125/AgERA5_dewpoint-temperature_20210125.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210125/AgERA5_dewpoint-temperature_20210125.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210126/AgERA5_dewpoint-temperature_20210126.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210126/AgERA5_dewpoint-temperature_20210126.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210127/AgERA5_dewpoint-temperature_20210127.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210127/AgERA5_dewpoint-temperature_20210127.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210128/AgERA5_dewpoint-temperature_20210128.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210128/AgERA5_dewpoint-temperature_20210128.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210129/AgERA5_dewpoint-temperature_20210129.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210129/AgERA5_dewpoint-temperature_20210129.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210130/AgERA5_dewpoint-temperature_20210130.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210130/AgERA5_dewpoint-temperature_20210130.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210131/AgERA5_dewpoint-temperature_20210131.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210131/AgERA5_dewpoint-temperature_20210131.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210201/AgERA5_dewpoint-temperature_20210201.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210201/AgERA5_dewpoint-temperature_20210201.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210202/AgERA5_dewpoint-temperature_20210202.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210202/AgERA5_dewpoint-temperature_20210202.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210203/AgERA5_dewpoint-temperature_20210203.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210203/AgERA5_dewpoint-temperature_20210203.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210204/AgERA5_dewpoint-temperature_20210204.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210204/AgERA5_dewpoint-temperature_20210204.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210205/AgERA5_dewpoint-temperature_20210205.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210205/AgERA5_dewpoint-temperature_20210205.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210206/AgERA5_dewpoint-temperature_20210206.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210206/AgERA5_dewpoint-temperature_20210206.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210207/AgERA5_dewpoint-temperature_20210207.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210207/AgERA5_dewpoint-temperature_20210207.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210208/AgERA5_dewpoint-temperature_20210208.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210208/AgERA5_dewpoint-temperature_20210208.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210209/AgERA5_dewpoint-temperature_20210209.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210209/AgERA5_dewpoint-temperature_20210209.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210210/AgERA5_dewpoint-temperature_20210210.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210210/AgERA5_dewpoint-temperature_20210210.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210211/AgERA5_dewpoint-temperature_20210211.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210211/AgERA5_dewpoint-temperature_20210211.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210212/AgERA5_dewpoint-temperature_20210212.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210212/AgERA5_dewpoint-temperature_20210212.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210213/AgERA5_dewpoint-temperature_20210213.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210213/AgERA5_dewpoint-temperature_20210213.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210214/AgERA5_dewpoint-temperature_20210214.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210214/AgERA5_dewpoint-temperature_20210214.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210215/AgERA5_dewpoint-temperature_20210215.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210215/AgERA5_dewpoint-temperature_20210215.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210216/AgERA5_dewpoint-temperature_20210216.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210216/AgERA5_dewpoint-temperature_20210216.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210217/AgERA5_dewpoint-temperature_20210217.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210217/AgERA5_dewpoint-temperature_20210217.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210218/AgERA5_dewpoint-temperature_20210218.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210218/AgERA5_dewpoint-temperature_20210218.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210219/AgERA5_dewpoint-temperature_20210219.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210219/AgERA5_dewpoint-temperature_20210219.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210220/AgERA5_dewpoint-temperature_20210220.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210220/AgERA5_dewpoint-temperature_20210220.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210221/AgERA5_dewpoint-temperature_20210221.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210221/AgERA5_dewpoint-temperature_20210221.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210222/AgERA5_dewpoint-temperature_20210222.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210222/AgERA5_dewpoint-temperature_20210222.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210223/AgERA5_dewpoint-temperature_20210223.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210223/AgERA5_dewpoint-temperature_20210223.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210224/AgERA5_dewpoint-temperature_20210224.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210224/AgERA5_dewpoint-temperature_20210224.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210225/AgERA5_dewpoint-temperature_20210225.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210225/AgERA5_dewpoint-temperature_20210225.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210226/AgERA5_dewpoint-temperature_20210226.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210226/AgERA5_dewpoint-temperature_20210226.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210227/AgERA5_dewpoint-temperature_20210227.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210227/AgERA5_dewpoint-temperature_20210227.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210228/AgERA5_dewpoint-temperature_20210228.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210228/AgERA5_dewpoint-temperature_20210228.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210301/AgERA5_dewpoint-temperature_20210301.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210301/AgERA5_dewpoint-temperature_20210301.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210302/AgERA5_dewpoint-temperature_20210302.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210302/AgERA5_dewpoint-temperature_20210302.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210303/AgERA5_dewpoint-temperature_20210303.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210303/AgERA5_dewpoint-temperature_20210303.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210304/AgERA5_dewpoint-temperature_20210304.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210304/AgERA5_dewpoint-temperature_20210304.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210305/AgERA5_dewpoint-temperature_20210305.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210305/AgERA5_dewpoint-temperature_20210305.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210306/AgERA5_dewpoint-temperature_20210306.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210306/AgERA5_dewpoint-temperature_20210306.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210307/AgERA5_dewpoint-temperature_20210307.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210307/AgERA5_dewpoint-temperature_20210307.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210308/AgERA5_dewpoint-temperature_20210308.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210308/AgERA5_dewpoint-temperature_20210308.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210309/AgERA5_dewpoint-temperature_20210309.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210309/AgERA5_dewpoint-temperature_20210309.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210310/AgERA5_dewpoint-temperature_20210310.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210310/AgERA5_dewpoint-temperature_20210310.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210311/AgERA5_dewpoint-temperature_20210311.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210311/AgERA5_dewpoint-temperature_20210311.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210312/AgERA5_dewpoint-temperature_20210312.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210312/AgERA5_dewpoint-temperature_20210312.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210313/AgERA5_dewpoint-temperature_20210313.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210313/AgERA5_dewpoint-temperature_20210313.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210314/AgERA5_dewpoint-temperature_20210314.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210314/AgERA5_dewpoint-temperature_20210314.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210315/AgERA5_dewpoint-temperature_20210315.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210315/AgERA5_dewpoint-temperature_20210315.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210316/AgERA5_dewpoint-temperature_20210316.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210316/AgERA5_dewpoint-temperature_20210316.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210317/AgERA5_dewpoint-temperature_20210317.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210317/AgERA5_dewpoint-temperature_20210317.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210318/AgERA5_dewpoint-temperature_20210318.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210318/AgERA5_dewpoint-temperature_20210318.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210319/AgERA5_dewpoint-temperature_20210319.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210319/AgERA5_dewpoint-temperature_20210319.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210320/AgERA5_dewpoint-temperature_20210320.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210320/AgERA5_dewpoint-temperature_20210320.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210321/AgERA5_dewpoint-temperature_20210321.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210321/AgERA5_dewpoint-temperature_20210321.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210322/AgERA5_dewpoint-temperature_20210322.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210322/AgERA5_dewpoint-temperature_20210322.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210323/AgERA5_dewpoint-temperature_20210323.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210323/AgERA5_dewpoint-temperature_20210323.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210324/AgERA5_dewpoint-temperature_20210324.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210324/AgERA5_dewpoint-temperature_20210324.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210325/AgERA5_dewpoint-temperature_20210325.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210325/AgERA5_dewpoint-temperature_20210325.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210326/AgERA5_dewpoint-temperature_20210326.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210326/AgERA5_dewpoint-temperature_20210326.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210327/AgERA5_dewpoint-temperature_20210327.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210327/AgERA5_dewpoint-temperature_20210327.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210328/AgERA5_dewpoint-temperature_20210328.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210328/AgERA5_dewpoint-temperature_20210328.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210329/AgERA5_dewpoint-temperature_20210329.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210329/AgERA5_dewpoint-temperature_20210329.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210330/AgERA5_dewpoint-temperature_20210330.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210330/AgERA5_dewpoint-temperature_20210330.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210331/AgERA5_dewpoint-temperature_20210331.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210331/AgERA5_dewpoint-temperature_20210331.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210401/AgERA5_dewpoint-temperature_20210401.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210401/AgERA5_dewpoint-temperature_20210401.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210402/AgERA5_dewpoint-temperature_20210402.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210402/AgERA5_dewpoint-temperature_20210402.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210403/AgERA5_dewpoint-temperature_20210403.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210403/AgERA5_dewpoint-temperature_20210403.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210404/AgERA5_dewpoint-temperature_20210404.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210404/AgERA5_dewpoint-temperature_20210404.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210405/AgERA5_dewpoint-temperature_20210405.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210405/AgERA5_dewpoint-temperature_20210405.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210406/AgERA5_dewpoint-temperature_20210406.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210406/AgERA5_dewpoint-temperature_20210406.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210407/AgERA5_dewpoint-temperature_20210407.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210407/AgERA5_dewpoint-temperature_20210407.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210408/AgERA5_dewpoint-temperature_20210408.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210408/AgERA5_dewpoint-temperature_20210408.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210409/AgERA5_dewpoint-temperature_20210409.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210409/AgERA5_dewpoint-temperature_20210409.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210410/AgERA5_dewpoint-temperature_20210410.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210410/AgERA5_dewpoint-temperature_20210410.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210411/AgERA5_dewpoint-temperature_20210411.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210411/AgERA5_dewpoint-temperature_20210411.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210412/AgERA5_dewpoint-temperature_20210412.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210412/AgERA5_dewpoint-temperature_20210412.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210413/AgERA5_dewpoint-temperature_20210413.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210413/AgERA5_dewpoint-temperature_20210413.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210414/AgERA5_dewpoint-temperature_20210414.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210414/AgERA5_dewpoint-temperature_20210414.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210415/AgERA5_dewpoint-temperature_20210415.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210415/AgERA5_dewpoint-temperature_20210415.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210416/AgERA5_dewpoint-temperature_20210416.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210416/AgERA5_dewpoint-temperature_20210416.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210417/AgERA5_dewpoint-temperature_20210417.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210417/AgERA5_dewpoint-temperature_20210417.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210418/AgERA5_dewpoint-temperature_20210418.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210418/AgERA5_dewpoint-temperature_20210418.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210419/AgERA5_dewpoint-temperature_20210419.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210419/AgERA5_dewpoint-temperature_20210419.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210420/AgERA5_dewpoint-temperature_20210420.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210420/AgERA5_dewpoint-temperature_20210420.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210421/AgERA5_dewpoint-temperature_20210421.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210421/AgERA5_dewpoint-temperature_20210421.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210422/AgERA5_dewpoint-temperature_20210422.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210422/AgERA5_dewpoint-temperature_20210422.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210423/AgERA5_dewpoint-temperature_20210423.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210423/AgERA5_dewpoint-temperature_20210423.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210424/AgERA5_dewpoint-temperature_20210424.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210424/AgERA5_dewpoint-temperature_20210424.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210425/AgERA5_dewpoint-temperature_20210425.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210425/AgERA5_dewpoint-temperature_20210425.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210426/AgERA5_dewpoint-temperature_20210426.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210426/AgERA5_dewpoint-temperature_20210426.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210427/AgERA5_dewpoint-temperature_20210427.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210427/AgERA5_dewpoint-temperature_20210427.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210428/AgERA5_dewpoint-temperature_20210428.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210428/AgERA5_dewpoint-temperature_20210428.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210429/AgERA5_dewpoint-temperature_20210429.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210429/AgERA5_dewpoint-temperature_20210429.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210430/AgERA5_dewpoint-temperature_20210430.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210430/AgERA5_dewpoint-temperature_20210430.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210501/AgERA5_dewpoint-temperature_20210501.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210501/AgERA5_dewpoint-temperature_20210501.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210502/AgERA5_dewpoint-temperature_20210502.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210502/AgERA5_dewpoint-temperature_20210502.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210503/AgERA5_dewpoint-temperature_20210503.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210503/AgERA5_dewpoint-temperature_20210503.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210504/AgERA5_dewpoint-temperature_20210504.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210504/AgERA5_dewpoint-temperature_20210504.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210505/AgERA5_dewpoint-temperature_20210505.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210505/AgERA5_dewpoint-temperature_20210505.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210506/AgERA5_dewpoint-temperature_20210506.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210506/AgERA5_dewpoint-temperature_20210506.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210507/AgERA5_dewpoint-temperature_20210507.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210507/AgERA5_dewpoint-temperature_20210507.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210508/AgERA5_dewpoint-temperature_20210508.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210508/AgERA5_dewpoint-temperature_20210508.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210509/AgERA5_dewpoint-temperature_20210509.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210509/AgERA5_dewpoint-temperature_20210509.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210510/AgERA5_dewpoint-temperature_20210510.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210510/AgERA5_dewpoint-temperature_20210510.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210511/AgERA5_dewpoint-temperature_20210511.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210511/AgERA5_dewpoint-temperature_20210511.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210512/AgERA5_dewpoint-temperature_20210512.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210512/AgERA5_dewpoint-temperature_20210512.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210513/AgERA5_dewpoint-temperature_20210513.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210513/AgERA5_dewpoint-temperature_20210513.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210514/AgERA5_dewpoint-temperature_20210514.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210514/AgERA5_dewpoint-temperature_20210514.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210515/AgERA5_dewpoint-temperature_20210515.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210515/AgERA5_dewpoint-temperature_20210515.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210516/AgERA5_dewpoint-temperature_20210516.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210516/AgERA5_dewpoint-temperature_20210516.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210517/AgERA5_dewpoint-temperature_20210517.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210517/AgERA5_dewpoint-temperature_20210517.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210518/AgERA5_dewpoint-temperature_20210518.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210518/AgERA5_dewpoint-temperature_20210518.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210519/AgERA5_dewpoint-temperature_20210519.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210519/AgERA5_dewpoint-temperature_20210519.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210520/AgERA5_dewpoint-temperature_20210520.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210520/AgERA5_dewpoint-temperature_20210520.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210521/AgERA5_dewpoint-temperature_20210521.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210521/AgERA5_dewpoint-temperature_20210521.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210522/AgERA5_dewpoint-temperature_20210522.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210522/AgERA5_dewpoint-temperature_20210522.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210523/AgERA5_dewpoint-temperature_20210523.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210523/AgERA5_dewpoint-temperature_20210523.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210524/AgERA5_dewpoint-temperature_20210524.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210524/AgERA5_dewpoint-temperature_20210524.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210525/AgERA5_dewpoint-temperature_20210525.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210525/AgERA5_dewpoint-temperature_20210525.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210526/AgERA5_dewpoint-temperature_20210526.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210526/AgERA5_dewpoint-temperature_20210526.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210527/AgERA5_dewpoint-temperature_20210527.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210527/AgERA5_dewpoint-temperature_20210527.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210528/AgERA5_dewpoint-temperature_20210528.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210528/AgERA5_dewpoint-temperature_20210528.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210529/AgERA5_dewpoint-temperature_20210529.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210529/AgERA5_dewpoint-temperature_20210529.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210530/AgERA5_dewpoint-temperature_20210530.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210530/AgERA5_dewpoint-temperature_20210530.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210531/AgERA5_dewpoint-temperature_20210531.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210531/AgERA5_dewpoint-temperature_20210531.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210601/AgERA5_dewpoint-temperature_20210601.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210601/AgERA5_dewpoint-temperature_20210601.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210602/AgERA5_dewpoint-temperature_20210602.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210602/AgERA5_dewpoint-temperature_20210602.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210603/AgERA5_dewpoint-temperature_20210603.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210603/AgERA5_dewpoint-temperature_20210603.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210604/AgERA5_dewpoint-temperature_20210604.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210604/AgERA5_dewpoint-temperature_20210604.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210605/AgERA5_dewpoint-temperature_20210605.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210605/AgERA5_dewpoint-temperature_20210605.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210606/AgERA5_dewpoint-temperature_20210606.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210606/AgERA5_dewpoint-temperature_20210606.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210607/AgERA5_dewpoint-temperature_20210607.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210607/AgERA5_dewpoint-temperature_20210607.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210608/AgERA5_dewpoint-temperature_20210608.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210608/AgERA5_dewpoint-temperature_20210608.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210609/AgERA5_dewpoint-temperature_20210609.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210609/AgERA5_dewpoint-temperature_20210609.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210610/AgERA5_dewpoint-temperature_20210610.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210610/AgERA5_dewpoint-temperature_20210610.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210611/AgERA5_dewpoint-temperature_20210611.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210611/AgERA5_dewpoint-temperature_20210611.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210612/AgERA5_dewpoint-temperature_20210612.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210612/AgERA5_dewpoint-temperature_20210612.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210613/AgERA5_dewpoint-temperature_20210613.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210613/AgERA5_dewpoint-temperature_20210613.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210614/AgERA5_dewpoint-temperature_20210614.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210614/AgERA5_dewpoint-temperature_20210614.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210615/AgERA5_dewpoint-temperature_20210615.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210615/AgERA5_dewpoint-temperature_20210615.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210616/AgERA5_dewpoint-temperature_20210616.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210616/AgERA5_dewpoint-temperature_20210616.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210617/AgERA5_dewpoint-temperature_20210617.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210617/AgERA5_dewpoint-temperature_20210617.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210618/AgERA5_dewpoint-temperature_20210618.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210618/AgERA5_dewpoint-temperature_20210618.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210619/AgERA5_dewpoint-temperature_20210619.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210619/AgERA5_dewpoint-temperature_20210619.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210620/AgERA5_dewpoint-temperature_20210620.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210620/AgERA5_dewpoint-temperature_20210620.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210621/AgERA5_dewpoint-temperature_20210621.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210621/AgERA5_dewpoint-temperature_20210621.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210622/AgERA5_dewpoint-temperature_20210622.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210622/AgERA5_dewpoint-temperature_20210622.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210623/AgERA5_dewpoint-temperature_20210623.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210623/AgERA5_dewpoint-temperature_20210623.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210624/AgERA5_dewpoint-temperature_20210624.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210624/AgERA5_dewpoint-temperature_20210624.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210625/AgERA5_dewpoint-temperature_20210625.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210625/AgERA5_dewpoint-temperature_20210625.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210626/AgERA5_dewpoint-temperature_20210626.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210626/AgERA5_dewpoint-temperature_20210626.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210627/AgERA5_dewpoint-temperature_20210627.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210627/AgERA5_dewpoint-temperature_20210627.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210628/AgERA5_dewpoint-temperature_20210628.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210628/AgERA5_dewpoint-temperature_20210628.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210629/AgERA5_dewpoint-temperature_20210629.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210629/AgERA5_dewpoint-temperature_20210629.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210630/AgERA5_dewpoint-temperature_20210630.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210630/AgERA5_dewpoint-temperature_20210630.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210701/AgERA5_dewpoint-temperature_20210701.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210701/AgERA5_dewpoint-temperature_20210701.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210702/AgERA5_dewpoint-temperature_20210702.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210702/AgERA5_dewpoint-temperature_20210702.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210703/AgERA5_dewpoint-temperature_20210703.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210703/AgERA5_dewpoint-temperature_20210703.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210704/AgERA5_dewpoint-temperature_20210704.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210704/AgERA5_dewpoint-temperature_20210704.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210705/AgERA5_dewpoint-temperature_20210705.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210705/AgERA5_dewpoint-temperature_20210705.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210706/AgERA5_dewpoint-temperature_20210706.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210706/AgERA5_dewpoint-temperature_20210706.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210707/AgERA5_dewpoint-temperature_20210707.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210707/AgERA5_dewpoint-temperature_20210707.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210708/AgERA5_dewpoint-temperature_20210708.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210708/AgERA5_dewpoint-temperature_20210708.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210709/AgERA5_dewpoint-temperature_20210709.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210709/AgERA5_dewpoint-temperature_20210709.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210710/AgERA5_dewpoint-temperature_20210710.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210710/AgERA5_dewpoint-temperature_20210710.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210711/AgERA5_dewpoint-temperature_20210711.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210711/AgERA5_dewpoint-temperature_20210711.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210712/AgERA5_dewpoint-temperature_20210712.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210712/AgERA5_dewpoint-temperature_20210712.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210713/AgERA5_dewpoint-temperature_20210713.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210713/AgERA5_dewpoint-temperature_20210713.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210714/AgERA5_dewpoint-temperature_20210714.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210714/AgERA5_dewpoint-temperature_20210714.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210715/AgERA5_dewpoint-temperature_20210715.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210715/AgERA5_dewpoint-temperature_20210715.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210716/AgERA5_dewpoint-temperature_20210716.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210716/AgERA5_dewpoint-temperature_20210716.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210717/AgERA5_dewpoint-temperature_20210717.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210717/AgERA5_dewpoint-temperature_20210717.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210718/AgERA5_dewpoint-temperature_20210718.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210718/AgERA5_dewpoint-temperature_20210718.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210719/AgERA5_dewpoint-temperature_20210719.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210719/AgERA5_dewpoint-temperature_20210719.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210720/AgERA5_dewpoint-temperature_20210720.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210720/AgERA5_dewpoint-temperature_20210720.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210721/AgERA5_dewpoint-temperature_20210721.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210721/AgERA5_dewpoint-temperature_20210721.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210722/AgERA5_dewpoint-temperature_20210722.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210722/AgERA5_dewpoint-temperature_20210722.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210723/AgERA5_dewpoint-temperature_20210723.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210723/AgERA5_dewpoint-temperature_20210723.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210724/AgERA5_dewpoint-temperature_20210724.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210724/AgERA5_dewpoint-temperature_20210724.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210725/AgERA5_dewpoint-temperature_20210725.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210725/AgERA5_dewpoint-temperature_20210725.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210726/AgERA5_dewpoint-temperature_20210726.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210726/AgERA5_dewpoint-temperature_20210726.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210727/AgERA5_dewpoint-temperature_20210727.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210727/AgERA5_dewpoint-temperature_20210727.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210728/AgERA5_dewpoint-temperature_20210728.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210728/AgERA5_dewpoint-temperature_20210728.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210729/AgERA5_dewpoint-temperature_20210729.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210729/AgERA5_dewpoint-temperature_20210729.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210730/AgERA5_dewpoint-temperature_20210730.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210730/AgERA5_dewpoint-temperature_20210730.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210731/AgERA5_dewpoint-temperature_20210731.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210731/AgERA5_dewpoint-temperature_20210731.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210801/AgERA5_dewpoint-temperature_20210801.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210801/AgERA5_dewpoint-temperature_20210801.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210802/AgERA5_dewpoint-temperature_20210802.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210802/AgERA5_dewpoint-temperature_20210802.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210803/AgERA5_dewpoint-temperature_20210803.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210803/AgERA5_dewpoint-temperature_20210803.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210804/AgERA5_dewpoint-temperature_20210804.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210804/AgERA5_dewpoint-temperature_20210804.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210805/AgERA5_dewpoint-temperature_20210805.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210805/AgERA5_dewpoint-temperature_20210805.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210806/AgERA5_dewpoint-temperature_20210806.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210806/AgERA5_dewpoint-temperature_20210806.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210807/AgERA5_dewpoint-temperature_20210807.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210807/AgERA5_dewpoint-temperature_20210807.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210808/AgERA5_dewpoint-temperature_20210808.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210808/AgERA5_dewpoint-temperature_20210808.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210809/AgERA5_dewpoint-temperature_20210809.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210809/AgERA5_dewpoint-temperature_20210809.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210810/AgERA5_dewpoint-temperature_20210810.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210810/AgERA5_dewpoint-temperature_20210810.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210811/AgERA5_dewpoint-temperature_20210811.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210811/AgERA5_dewpoint-temperature_20210811.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210812/AgERA5_dewpoint-temperature_20210812.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210812/AgERA5_dewpoint-temperature_20210812.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210813/AgERA5_dewpoint-temperature_20210813.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210813/AgERA5_dewpoint-temperature_20210813.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210814/AgERA5_dewpoint-temperature_20210814.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210814/AgERA5_dewpoint-temperature_20210814.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210815/AgERA5_dewpoint-temperature_20210815.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210815/AgERA5_dewpoint-temperature_20210815.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210816/AgERA5_dewpoint-temperature_20210816.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210816/AgERA5_dewpoint-temperature_20210816.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210817/AgERA5_dewpoint-temperature_20210817.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210817/AgERA5_dewpoint-temperature_20210817.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210818/AgERA5_dewpoint-temperature_20210818.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210818/AgERA5_dewpoint-temperature_20210818.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210819/AgERA5_dewpoint-temperature_20210819.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210819/AgERA5_dewpoint-temperature_20210819.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210820/AgERA5_dewpoint-temperature_20210820.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210820/AgERA5_dewpoint-temperature_20210820.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210821/AgERA5_dewpoint-temperature_20210821.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210821/AgERA5_dewpoint-temperature_20210821.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210822/AgERA5_dewpoint-temperature_20210822.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210822/AgERA5_dewpoint-temperature_20210822.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210823/AgERA5_dewpoint-temperature_20210823.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210823/AgERA5_dewpoint-temperature_20210823.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210824/AgERA5_dewpoint-temperature_20210824.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210824/AgERA5_dewpoint-temperature_20210824.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210825/AgERA5_dewpoint-temperature_20210825.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210825/AgERA5_dewpoint-temperature_20210825.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210826/AgERA5_dewpoint-temperature_20210826.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210826/AgERA5_dewpoint-temperature_20210826.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210827/AgERA5_dewpoint-temperature_20210827.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210827/AgERA5_dewpoint-temperature_20210827.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210828/AgERA5_dewpoint-temperature_20210828.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210828/AgERA5_dewpoint-temperature_20210828.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210829/AgERA5_dewpoint-temperature_20210829.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210829/AgERA5_dewpoint-temperature_20210829.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210830/AgERA5_dewpoint-temperature_20210830.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210830/AgERA5_dewpoint-temperature_20210830.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210831/AgERA5_dewpoint-temperature_20210831.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210831/AgERA5_dewpoint-temperature_20210831.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210901/AgERA5_dewpoint-temperature_20210901.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210901/AgERA5_dewpoint-temperature_20210901.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210902/AgERA5_dewpoint-temperature_20210902.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210902/AgERA5_dewpoint-temperature_20210902.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210903/AgERA5_dewpoint-temperature_20210903.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210903/AgERA5_dewpoint-temperature_20210903.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210904/AgERA5_dewpoint-temperature_20210904.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210904/AgERA5_dewpoint-temperature_20210904.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210905/AgERA5_dewpoint-temperature_20210905.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210905/AgERA5_dewpoint-temperature_20210905.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210906/AgERA5_dewpoint-temperature_20210906.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210906/AgERA5_dewpoint-temperature_20210906.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210907/AgERA5_dewpoint-temperature_20210907.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210907/AgERA5_dewpoint-temperature_20210907.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210908/AgERA5_dewpoint-temperature_20210908.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210908/AgERA5_dewpoint-temperature_20210908.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210909/AgERA5_dewpoint-temperature_20210909.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210909/AgERA5_dewpoint-temperature_20210909.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210910/AgERA5_dewpoint-temperature_20210910.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210910/AgERA5_dewpoint-temperature_20210910.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210911/AgERA5_dewpoint-temperature_20210911.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210911/AgERA5_dewpoint-temperature_20210911.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210912/AgERA5_dewpoint-temperature_20210912.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210912/AgERA5_dewpoint-temperature_20210912.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210913/AgERA5_dewpoint-temperature_20210913.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210913/AgERA5_dewpoint-temperature_20210913.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210914/AgERA5_dewpoint-temperature_20210914.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210914/AgERA5_dewpoint-temperature_20210914.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210915/AgERA5_dewpoint-temperature_20210915.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210915/AgERA5_dewpoint-temperature_20210915.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210916/AgERA5_dewpoint-temperature_20210916.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210916/AgERA5_dewpoint-temperature_20210916.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210917/AgERA5_dewpoint-temperature_20210917.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210917/AgERA5_dewpoint-temperature_20210917.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210918/AgERA5_dewpoint-temperature_20210918.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210918/AgERA5_dewpoint-temperature_20210918.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210919/AgERA5_dewpoint-temperature_20210919.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210919/AgERA5_dewpoint-temperature_20210919.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210920/AgERA5_dewpoint-temperature_20210920.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210920/AgERA5_dewpoint-temperature_20210920.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210921/AgERA5_dewpoint-temperature_20210921.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210921/AgERA5_dewpoint-temperature_20210921.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210922/AgERA5_dewpoint-temperature_20210922.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210922/AgERA5_dewpoint-temperature_20210922.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210923/AgERA5_dewpoint-temperature_20210923.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210923/AgERA5_dewpoint-temperature_20210923.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210924/AgERA5_dewpoint-temperature_20210924.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210924/AgERA5_dewpoint-temperature_20210924.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210925/AgERA5_dewpoint-temperature_20210925.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210925/AgERA5_dewpoint-temperature_20210925.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210926/AgERA5_dewpoint-temperature_20210926.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210926/AgERA5_dewpoint-temperature_20210926.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210927/AgERA5_dewpoint-temperature_20210927.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210927/AgERA5_dewpoint-temperature_20210927.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210928/AgERA5_dewpoint-temperature_20210928.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210928/AgERA5_dewpoint-temperature_20210928.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210929/AgERA5_dewpoint-temperature_20210929.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210929/AgERA5_dewpoint-temperature_20210929.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210930/AgERA5_dewpoint-temperature_20210930.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210930/AgERA5_dewpoint-temperature_20210930.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211001/AgERA5_dewpoint-temperature_20211001.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211001/AgERA5_dewpoint-temperature_20211001.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211002/AgERA5_dewpoint-temperature_20211002.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211002/AgERA5_dewpoint-temperature_20211002.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211003/AgERA5_dewpoint-temperature_20211003.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211003/AgERA5_dewpoint-temperature_20211003.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211004/AgERA5_dewpoint-temperature_20211004.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211004/AgERA5_dewpoint-temperature_20211004.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211005/AgERA5_dewpoint-temperature_20211005.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211005/AgERA5_dewpoint-temperature_20211005.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211006/AgERA5_dewpoint-temperature_20211006.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211006/AgERA5_dewpoint-temperature_20211006.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211007/AgERA5_dewpoint-temperature_20211007.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211007/AgERA5_dewpoint-temperature_20211007.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211008/AgERA5_dewpoint-temperature_20211008.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211008/AgERA5_dewpoint-temperature_20211008.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211009/AgERA5_dewpoint-temperature_20211009.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211009/AgERA5_dewpoint-temperature_20211009.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211010/AgERA5_dewpoint-temperature_20211010.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211010/AgERA5_dewpoint-temperature_20211010.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211011/AgERA5_dewpoint-temperature_20211011.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211011/AgERA5_dewpoint-temperature_20211011.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211012/AgERA5_dewpoint-temperature_20211012.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211012/AgERA5_dewpoint-temperature_20211012.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211013/AgERA5_dewpoint-temperature_20211013.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211013/AgERA5_dewpoint-temperature_20211013.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211014/AgERA5_dewpoint-temperature_20211014.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211014/AgERA5_dewpoint-temperature_20211014.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211015/AgERA5_dewpoint-temperature_20211015.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211015/AgERA5_dewpoint-temperature_20211015.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211016/AgERA5_dewpoint-temperature_20211016.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211016/AgERA5_dewpoint-temperature_20211016.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211017/AgERA5_dewpoint-temperature_20211017.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211017/AgERA5_dewpoint-temperature_20211017.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211018/AgERA5_dewpoint-temperature_20211018.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211018/AgERA5_dewpoint-temperature_20211018.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211019/AgERA5_dewpoint-temperature_20211019.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211019/AgERA5_dewpoint-temperature_20211019.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211020/AgERA5_dewpoint-temperature_20211020.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211020/AgERA5_dewpoint-temperature_20211020.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211021/AgERA5_dewpoint-temperature_20211021.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211021/AgERA5_dewpoint-temperature_20211021.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211022/AgERA5_dewpoint-temperature_20211022.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211022/AgERA5_dewpoint-temperature_20211022.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211023/AgERA5_dewpoint-temperature_20211023.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211023/AgERA5_dewpoint-temperature_20211023.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211024/AgERA5_dewpoint-temperature_20211024.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211024/AgERA5_dewpoint-temperature_20211024.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211025/AgERA5_dewpoint-temperature_20211025.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211025/AgERA5_dewpoint-temperature_20211025.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211026/AgERA5_dewpoint-temperature_20211026.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211026/AgERA5_dewpoint-temperature_20211026.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211027/AgERA5_dewpoint-temperature_20211027.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211027/AgERA5_dewpoint-temperature_20211027.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211028/AgERA5_dewpoint-temperature_20211028.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211028/AgERA5_dewpoint-temperature_20211028.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211029/AgERA5_dewpoint-temperature_20211029.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211029/AgERA5_dewpoint-temperature_20211029.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211030/AgERA5_dewpoint-temperature_20211030.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211030/AgERA5_dewpoint-temperature_20211030.tif", "type": "application/json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results", "rel": "self", "type": "application/json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/bbd3f1506c6b5bb453fa5566260f52fe?expires=1716455737", "rel": "canonical", "type": "application/json"}, {"href": "http://ceos.org/ard/files/PFS/SR/v5.0/CARD4L_Product_Family_Specification_Surface_Reflectance-v5.0.pdf", "rel": "card4l-document", "type": "application/pdf"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/9c3ea792c00e0a1b63231b8b149f997a/openEO_2021-10-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/a8c32bef4d950e8fe3ef37eaca87ee31/openEO_2021-06-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/04cff14b611d54240522833210762931/openEO_2020-12-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8a1578c8d890289751276205a0864103/openEO_2021-03-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/0b8d41a9211197d5be684162746fb830/openEO_2021-08-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3448e70d84ed3f2eb6e399a86b2f8b3d/openEO_2021-09-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/5280a7fab73a3af7d65951d1ccc0edc7/openEO_2020-11-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3e5e262c7faeb68d52a18f012bf7fe3f/openEO_2021-07-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/256561a0d78d5b22963c5d59f4768cd5/openEO_2021-01-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/6259c389f92cda20f278a1c343486931/openEO_2021-02-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/cd7a84f1e9dcd5107a01a6a3db1d2a90/openEO_2021-04-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8cef27e840684882775f0a8b46671209/openEO_2021-05-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}], "openeo:status": "finished", "providers": [{"description": "This data was processed on an openEO backend maintained by VITO.", "name": "VITO", "processing:expression": {"expression": {"aggregatetemporalperiod1": {"arguments": {"data": {"from_node": "filterbands1"}, "period": "month", "reducer": {"process_graph": {"mean1": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "mean", "result": true}}}}, "process_id": "aggregate_temporal_period"}, "aggregatetemporalperiod2": {"arguments": {"data": {"from_node": "filterbands2"}, "period": "month", "reducer": {"process_graph": {"sum1": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "sum", "result": true}}}}, "process_id": "aggregate_temporal_period"}, "apply1": {"arguments": {"data": {"from_node": "filtertemporal1"}, "process": {"process_graph": {"linearscalerange1": {"arguments": {"inputMax": 65534, "inputMin": 0, "outputMax": 65534, "outputMin": 0, "x": {"from_parameter": "x"}}, "process_id": "linear_scale_range", "result": true}}}}, "process_id": "apply"}, "applydimension1": {"arguments": {"data": {"from_node": "aggregatetemporalperiod1"}, "dimension": "t", "process": {"process_graph": {"arrayinterpolatelinear1": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "array_interpolate_linear", "result": true}}}}, "process_id": "apply_dimension"}, "applydimension2": {"arguments": {"data": {"from_node": "aggregatetemporalperiod2"}, "dimension": "t", "process": {"process_graph": {"arrayinterpolatelinear2": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "array_interpolate_linear", "result": true}}}}, "process_id": "apply_dimension"}, "filterbands1": {"arguments": {"bands": ["temperature-mean"], "data": {"from_node": "loadcollection1"}}, "process_id": "filter_bands"}, "filterbands2": {"arguments": {"bands": ["precipitation-flux"], "data": {"from_node": "loadcollection1"}}, "process_id": "filter_bands"}, "filtertemporal1": {"arguments": {"data": {"from_node": "mergecubes1"}, "extent": ["2020-11-01", "2021-10-31"]}, "process_id": "filter_temporal"}, "loadcollection1": {"arguments": {"bands": ["temperature-mean", "precipitation-flux"], "featureflags": {"tilesize": 1}, "id": "AGERA5", "spatial_extent": {"crs": "EPSG:4326", "east": 5.21, "north": 51.26, "south": 51.25, "west": 5.19}, "temporal_extent": ["2020-11-01", "2021-10-31"]}, "process_id": "load_collection"}, "mergecubes1": {"arguments": {"cube1": {"from_node": "applydimension1"}, "cube2": {"from_node": "applydimension2"}}, "process_id": "merge_cubes"}, "saveresult1": {"arguments": {"data": {"from_node": "apply1"}, "format": "GTIFF", "options": {}}, "process_id": "save_result", "result": true}}, "format": "openeo"}, "processing:facility": "openEO Geotrellis backend", "processing:software": {"Geotrellis backend": "0.33.1a1"}, "roles": ["processor"]}], "stac_extensions": ["https://stac-extensions.github.io/eo/v1.1.0/schema.json", "https://stac-extensions.github.io/file/v2.1.0/schema.json", "https://stac-extensions.github.io/processing/v1.1.0/schema.json", "https://stac-extensions.github.io/projection/v1.1.0/schema.json"], "stac_version": "1.0.0", "summaries": {}, "type": "Collection"} \ No newline at end of file diff --git a/minimal_wc_presto/preprocessing.py b/minimal_wc_presto/preprocessing.py index 9b295210..7cf4c9a2 100644 --- a/minimal_wc_presto/preprocessing.py +++ b/minimal_wc_presto/preprocessing.py @@ -1,6 +1,6 @@ from openeo.processes import array_create, if_, is_nodata, power from openeo.rest.datacube import DataCube - +import openeo COMPOSITE_WINDOW = "month" @@ -286,6 +286,9 @@ def get_meteo( temporal_extent=[start, end], ) + meteo.result_node().update_arguments(featureflags={"tilesize": 1}) + + if target_epsg is not None: meteo = meteo.resample_spatial( projection=target_epsg, resolution=10.0, method="bilinear" diff --git a/minimal_wc_presto/test_aggregator.ipynb b/minimal_wc_presto/test_aggregator.ipynb new file mode 100644 index 00000000..2087151a --- /dev/null +++ b/minimal_wc_presto/test_aggregator.ipynb @@ -0,0 +1,696 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b879f7b4-9a3f-41fc-90d0-ab9cfd25a093", + "metadata": {}, + "source": [ + "### Make OpenEO connection" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Authenticated using refresh token.\n" + ] + } + ], + "source": [ + "import openeo\n", + "\n", + "#token for METEO\n", + "#connection_terra = openeo.connect(\"openeo.vito.be\").authenticate_oidc()\n", + "\n", + "#token SENTINEL\n", + "connection = openeo.connect(\"https://openeofed.dataspace.copernicus.eu/\").authenticate_oidc()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "5494c46d", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Preflight process graph validation raised: [InternalValidationFailure] Validation failed: BackendLookupFailureException(status_code=400, code='BackendLookupFailure', message=\"Collections across multiple backends ({'cdse', 'terrascope'}): {'COPERNICUS_30', 'SENTINEL2_L2A', 'SENTINEL1_GRD', 'AGERA5'}.\", id='r-24051536a816438ebace84c022cdb826')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'agg-pj-20240515-101812': send 'start'\n", + "0:01:09 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:01:19 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:01:31 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:01:42 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:01:57 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:02:15 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:02:34 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:03:01 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:03:29 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:04:03 Job 'agg-pj-20240515-101812': running (progress 0%)\n", + "0:04:46 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:05:45 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:06:47 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:08:01 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:09:10 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:10:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:11:26 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:12:31 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:13:37 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:14:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:15:44 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:16:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:17:53 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:19:13 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:20:27 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:21:33 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:22:37 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:23:43 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:24:46 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:25:50 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:26:55 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:28:02 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:29:09 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:30:15 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:31:36 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:32:42 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:33:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:34:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:36:05 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:37:10 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:38:24 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:39:29 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:40:34 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:41:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:42:53 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:44:02 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:45:06 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:46:10 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:47:13 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:48:17 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:49:21 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:50:28 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:51:34 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:52:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:53:44 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:54:50 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:55:55 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:56:58 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:58:23 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "0:59:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:00:59 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:02:15 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:03:20 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:04:30 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:05:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:06:43 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:07:46 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:09:00 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:10:05 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:11:08 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:12:21 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:13:36 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:14:50 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:15:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:16:58 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:18:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:19:20 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:20:39 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:21:47 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:22:57 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:24:00 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:25:03 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:26:09 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:27:17 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:28:27 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:29:32 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:30:35 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:31:41 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:32:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:33:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:34:59 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:36:08 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:37:15 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:38:22 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:39:28 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:40:44 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:41:55 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:43:01 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:44:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:45:25 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:46:33 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:47:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:48:53 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:50:01 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:51:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:52:20 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:53:25 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:54:29 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:55:35 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:56:38 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:57:43 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "1:58:43 Job 'agg-pj-20240515-101812': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", + "1:59:22 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "2:00:22 Job 'agg-pj-20240515-101812': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", + "2:00:59 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "2:02:03 Job 'agg-pj-20240515-101812': running (progress 25%)\n", + "2:03:03 Job 'agg-pj-20240515-101812': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", + "2:03:37 Job 'agg-pj-20240515-101812': running (progress 25%)\n" + ] + }, + { + "ename": "OpenEoApiError", + "evalue": "[500] Internal: Server error: KazooTimeoutError('Connection time-out') (ref: r-240515e716d34d9b9e8f1481ece911f9)", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mOpenEoApiError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[22], line 47\u001b[0m\n\u001b[0;32m 18\u001b[0m input_cube \u001b[38;5;241m=\u001b[39m worldcereal_preprocessed_inputs(\n\u001b[0;32m 19\u001b[0m connection \u001b[38;5;241m=\u001b[39m connection,\n\u001b[0;32m 20\u001b[0m bbox \u001b[38;5;241m=\u001b[39m EXTENT,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 26\u001b[0m DEM_collection\u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCOPERNICUS_30\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 27\u001b[0m )\n\u001b[0;32m 29\u001b[0m \u001b[38;5;66;03m#agera5_cube = worldcereal_preprocessed_inputs(\u001b[39;00m\n\u001b[0;32m 30\u001b[0m \u001b[38;5;66;03m# connection = connection_terra,\u001b[39;00m\n\u001b[0;32m 31\u001b[0m \u001b[38;5;66;03m# bbox = EXTENT,\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 44\u001b[0m \u001b[38;5;66;03m# temporal_extent=[STARTDATE, ENDDATE],\u001b[39;00m\n\u001b[0;32m 45\u001b[0m \u001b[38;5;66;03m# )\u001b[39;00m\n\u001b[1;32m---> 47\u001b[0m \u001b[43minput_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtest.nc\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 48\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal data collection\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 49\u001b[0m \u001b[43m \u001b[49m\u001b[43mjob_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43msplit_strategy\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mcrossbackend\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:292\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 289\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m 290\u001b[0m \u001b[38;5;66;03m# TODO: also allow a hard time limit on this infinite poll loop?\u001b[39;00m\n\u001b[0;32m 291\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 292\u001b[0m job_info \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 293\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m requests\u001b[38;5;241m.\u001b[39mConnectionError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 294\u001b[0m soft_error(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConnection error while polling job status: \u001b[39m\u001b[38;5;132;01m{e}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(e\u001b[38;5;241m=\u001b[39me))\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:74\u001b[0m, in \u001b[0;36mBatchJob.describe\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 65\u001b[0m \u001b[38;5;129m@openeo_endpoint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mGET /jobs/\u001b[39m\u001b[38;5;132;01m{job_id}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 66\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdescribe\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mdict\u001b[39m:\n\u001b[0;32m 67\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 68\u001b[0m \u001b[38;5;124;03m Get detailed metadata about a submitted batch job\u001b[39;00m\n\u001b[0;32m 69\u001b[0m \u001b[38;5;124;03m (title, process graph, status, progress, ...).\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 72\u001b[0m \u001b[38;5;124;03m This method was previously called :py:meth:`describe_job`.\u001b[39;00m\n\u001b[0;32m 73\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m---> 74\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconnection\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m/jobs/\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mjob_id\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_status\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m200\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mjson()\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:221\u001b[0m, in \u001b[0;36mRestApiConnection.get\u001b[1;34m(self, path, stream, auth, **kwargs)\u001b[0m\n\u001b[0;32m 212\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget\u001b[39m(\u001b[38;5;28mself\u001b[39m, path: \u001b[38;5;28mstr\u001b[39m, stream: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m, auth: Optional[AuthBase] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Response:\n\u001b[0;32m 213\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 214\u001b[0m \u001b[38;5;124;03m Do GET request to REST API.\u001b[39;00m\n\u001b[0;32m 215\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 219\u001b[0m \u001b[38;5;124;03m :return: response: Response\u001b[39;00m\n\u001b[0;32m 220\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m--> 221\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mget\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mauth\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mauth\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:770\u001b[0m, in \u001b[0;36mConnection.request\u001b[1;34m(self, method, path, headers, auth, check_error, expected_status, **kwargs)\u001b[0m\n\u001b[0;32m 763\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m(Connection, \u001b[38;5;28mself\u001b[39m)\u001b[38;5;241m.\u001b[39mrequest(\n\u001b[0;32m 764\u001b[0m method\u001b[38;5;241m=\u001b[39mmethod, path\u001b[38;5;241m=\u001b[39mpath, headers\u001b[38;5;241m=\u001b[39mheaders, auth\u001b[38;5;241m=\u001b[39mauth,\n\u001b[0;32m 765\u001b[0m check_error\u001b[38;5;241m=\u001b[39mcheck_error, expected_status\u001b[38;5;241m=\u001b[39mexpected_status, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 766\u001b[0m )\n\u001b[0;32m 768\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 769\u001b[0m \u001b[38;5;66;03m# Initial request attempt\u001b[39;00m\n\u001b[1;32m--> 770\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 771\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m OpenEoApiError \u001b[38;5;28;01mas\u001b[39;00m api_exc:\n\u001b[0;32m 772\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m api_exc\u001b[38;5;241m.\u001b[39mhttp_status_code \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m401\u001b[39m, \u001b[38;5;241m403\u001b[39m} \u001b[38;5;129;01mand\u001b[39;00m api_exc\u001b[38;5;241m.\u001b[39mcode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTokenInvalid\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 773\u001b[0m \u001b[38;5;66;03m# Auth token expired: can we refresh?\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:763\u001b[0m, in \u001b[0;36mConnection.request.._request\u001b[1;34m()\u001b[0m\n\u001b[0;32m 762\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_request\u001b[39m():\n\u001b[1;32m--> 763\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mConnection\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 764\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mauth\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mauth\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 765\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheck_error\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcheck_error\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_status\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexpected_status\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 766\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:169\u001b[0m, in \u001b[0;36mRestApiConnection.request\u001b[1;34m(self, method, path, headers, auth, check_error, expected_status, **kwargs)\u001b[0m\n\u001b[0;32m 167\u001b[0m expected_status \u001b[38;5;241m=\u001b[39m ensure_list(expected_status) \u001b[38;5;28;01mif\u001b[39;00m expected_status \u001b[38;5;28;01melse\u001b[39;00m []\n\u001b[0;32m 168\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m check_error \u001b[38;5;129;01mand\u001b[39;00m status \u001b[38;5;241m>\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m400\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m status \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m expected_status:\n\u001b[1;32m--> 169\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_raise_api_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresp\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 170\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m expected_status \u001b[38;5;129;01mand\u001b[39;00m status \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m expected_status:\n\u001b[0;32m 171\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m OpenEoRestError(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mGot status code \u001b[39m\u001b[38;5;132;01m{s!r}\u001b[39;00m\u001b[38;5;124m for `\u001b[39m\u001b[38;5;132;01m{m}\u001b[39;00m\u001b[38;5;124m \u001b[39m\u001b[38;5;132;01m{p}\u001b[39;00m\u001b[38;5;124m` (expected \u001b[39m\u001b[38;5;132;01m{e!r}\u001b[39;00m\u001b[38;5;124m) with body \u001b[39m\u001b[38;5;132;01m{body}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[0;32m 172\u001b[0m m\u001b[38;5;241m=\u001b[39mmethod\u001b[38;5;241m.\u001b[39mupper(), p\u001b[38;5;241m=\u001b[39mpath, s\u001b[38;5;241m=\u001b[39mstatus, e\u001b[38;5;241m=\u001b[39mexpected_status, body\u001b[38;5;241m=\u001b[39mresp\u001b[38;5;241m.\u001b[39mtext)\n\u001b[0;32m 173\u001b[0m )\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:189\u001b[0m, in \u001b[0;36mRestApiConnection._raise_api_error\u001b[1;34m(self, response)\u001b[0m\n\u001b[0;32m 187\u001b[0m error_message \u001b[38;5;241m=\u001b[39m info\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmessage\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m error_code \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(error_code, \u001b[38;5;28mstr\u001b[39m) \u001b[38;5;129;01mand\u001b[39;00m error_message \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(error_message, \u001b[38;5;28mstr\u001b[39m):\n\u001b[1;32m--> 189\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m OpenEoApiError(\n\u001b[0;32m 190\u001b[0m http_status_code\u001b[38;5;241m=\u001b[39mstatus_code,\n\u001b[0;32m 191\u001b[0m code\u001b[38;5;241m=\u001b[39merror_code,\n\u001b[0;32m 192\u001b[0m message\u001b[38;5;241m=\u001b[39merror_message,\n\u001b[0;32m 193\u001b[0m \u001b[38;5;28mid\u001b[39m\u001b[38;5;241m=\u001b[39minfo\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mid\u001b[39m\u001b[38;5;124m\"\u001b[39m),\n\u001b[0;32m 194\u001b[0m url\u001b[38;5;241m=\u001b[39minfo\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124murl\u001b[39m\u001b[38;5;124m\"\u001b[39m),\n\u001b[0;32m 195\u001b[0m )\n\u001b[0;32m 197\u001b[0m \u001b[38;5;66;03m# Failed to parse it as a compliant openEO API error: show body as-is in the exception.\u001b[39;00m\n\u001b[0;32m 198\u001b[0m text \u001b[38;5;241m=\u001b[39m response\u001b[38;5;241m.\u001b[39mtext\n", + "\u001b[1;31mOpenEoApiError\u001b[0m: [500] Internal: Server error: KazooTimeoutError('Connection time-out') (ref: r-240515e716d34d9b9e8f1481ece911f9)" + ] + } + ], + "source": [ + "#Get desired data\n", + "from preprocessing import worldcereal_preprocessed_inputs\n", + "\n", + "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.19, 51.25, 5.21, 51.26]))\n", + "EXTENT['crs'] = \"EPSG:4326\"\n", + "\n", + "STARTDATE = '2020-11-01'\n", + "ENDDATE = '2021-10-31'\n", + "\n", + "# Set OpenEO classification UDF context based on settings\n", + "CONTEXT = {\n", + " \"startdate\": STARTDATE, # Required\n", + " \"enddate\": ENDDATE, # Required\n", + "}\n", + "\n", + "\n", + "\n", + "input_cube = worldcereal_preprocessed_inputs(\n", + " connection = connection,\n", + " bbox = EXTENT,\n", + " start = STARTDATE,\n", + " end = ENDDATE,\n", + " METEO_collection=\"AGERA5\",\n", + " S2_collection= \"SENTINEL2_L2A\",\n", + " S1_collection= \"SENTINEL1_GRD\",\n", + " DEM_collection= \"COPERNICUS_30\"\n", + ")\n", + "\n", + "#agera5_cube = worldcereal_preprocessed_inputs(\n", + "# connection = connection_terra,\n", + "# bbox = EXTENT,\n", + "# start = STARTDATE,\n", + "# end = ENDDATE,\n", + "# METEO_collection=\"AGERA5\",\n", + "# S2_collection= None,\n", + "# S1_collection= None,\n", + "# DEM_collection= None\n", + "#)\n", + "\n", + "#agera5_cube = connection_terra.load_collection(\n", + "# \"AGERA5\",\n", + "# spatial_extent=EXTENT,\n", + "# bands=[\"temperature-mean\", \"precipitation-flux\"],\n", + "# temporal_extent=[STARTDATE, ENDDATE],\n", + "# )\n", + "\n", + "input_cube.execute_batch(outputfile = 'test.nc',\n", + " description='world cereal data collection',\n", + " job_options={\"split_strategy\": \"crossbackend\"})\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "94969249", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-24051575983946539c6694814f39164e': send 'start'\n", + "0:00:30 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", + "0:00:36 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", + "0:00:44 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", + "0:00:53 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", + "0:01:03 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", + "0:01:19 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", + "0:01:35 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", + "0:01:55 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", + "0:02:19 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", + "0:02:52 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", + "0:03:29 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", + "0:04:16 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", + "0:05:24 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", + "0:06:24 Job 'j-24051575983946539c6694814f39164e': finished (progress 100%)\n" + ] + }, + { + "ename": "AttributeError", + "evalue": "'DataCube' object has no attribute 'load_stac'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[11], line 15\u001b[0m\n\u001b[0;32m 13\u001b[0m result_metadata \u001b[38;5;241m=\u001b[39m job\u001b[38;5;241m.\u001b[39mget_results()\n\u001b[0;32m 14\u001b[0m job_url, \u001b[38;5;241m=\u001b[39m [k[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhref\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;28;01mfor\u001b[39;00m k \u001b[38;5;129;01min\u001b[39;00m result_metadata\u001b[38;5;241m.\u001b[39mget_metadata()[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinks\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m k[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrel\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcanonical\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n\u001b[1;32m---> 15\u001b[0m load_stac_cube \u001b[38;5;241m=\u001b[39m \u001b[43ms2_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload_stac\u001b[49m(job_url)\n\u001b[0;32m 17\u001b[0m \u001b[38;5;66;03m#merge the cubes and download\u001b[39;00m\n\u001b[0;32m 18\u001b[0m input_cube \u001b[38;5;241m=\u001b[39m s2_cube\u001b[38;5;241m.\u001b[39mmerge_cubes(load_stac_cube)\n", + "\u001b[1;31mAttributeError\u001b[0m: 'DataCube' object has no attribute 'load_stac'" + ] + } + ], + "source": [ + "from pathlib import Path\n", + "\n", + "# download the agera 5 cube\n", + "result_dir = Path.cwd()\n", + "job = agera5_cube.create_job(\n", + " out_format=\"GTIFF\",\n", + ")\n", + "job.start_and_wait()\n", + "\n", + "job.get_results().download_files(result_dir)\n", + "\n", + "#create a STAC collection from th eobtained cube\n", + "result_metadata = job.get_results()\n", + "job_url, = [k[\"href\"] for k in result_metadata.get_metadata()[\"links\"] if k[\"rel\"] == \"canonical\"]\n", + "load_stac_cube = s2_cube.load_stac(job_url)\n", + "\n", + "#merge the cubes and download\n", + "input_cube = s2_cube.merge_cubes(load_stac_cube)\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "4aab5695", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Preflight process graph validation raised: [CollectionNotFound] Collection 'AGERA5' does not exist.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-2405155e702e4218aa9dfac9671faaff': send 'start'\n", + "0:00:16 Job 'j-2405155e702e4218aa9dfac9671faaff': created (progress 0%)\n", + "0:00:22 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:00:28 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:00:36 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:00:47 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:01:00 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:01:16 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", + "0:01:35 Job 'j-2405155e702e4218aa9dfac9671faaff': error (progress N/A)\n", + "Your batch job 'j-2405155e702e4218aa9dfac9671faaff' failed. Error logs:\n", + "[{'id': '[1715756877175, 557437]', 'time': '2024-05-15T07:07:57.175Z', 'level': 'error', 'message': 'OpenEO batch job failed: CollectionNotFoundException(status_code=404, code=\\'CollectionNotFound\\', message=\"Collection \\'AGERA5\\' does not exist.\", id=\\'no-request\\')'}]\n", + "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-2405155e702e4218aa9dfac9671faaff').logs()`.\n" + ] + }, + { + "ename": "JobFailedException", + "evalue": "Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37).", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[11], line 6\u001b[0m\n\u001b[0;32m 3\u001b[0m formatted_datetime \u001b[38;5;241m=\u001b[39m current_datetime\u001b[38;5;241m.\u001b[39mstrftime(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mY_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mm_\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mH_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mM_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mS\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 4\u001b[0m outputfile_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(formatted_datetime) \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m_input_cube_worldCereal.nc\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m----> 6\u001b[0m \u001b[43minput_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43moutputfile_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 7\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal data collection\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", + "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37)." + ] + } + ], + "source": [ + "from datetime import datetime\n", + "current_datetime = datetime.now()\n", + "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", + "outputfile_name = str(formatted_datetime) + '_input_cube_worldCereal.nc'\n", + "\n", + "input_cube.execute_batch(outputfile = outputfile_name,\n", + " description='world cereal data collection')" + ] + }, + { + "cell_type": "markdown", + "id": "48c9322c", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8f71136c-1252-4786-8609-8bb995da7daf", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-240508de680a4a01bad4dfca194be16b': send 'start'\n", + "0:00:28 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", + "0:00:34 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", + "0:00:41 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:00:55 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:05 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:17 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:33 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:01:52 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", + "0:02:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:02:52 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:03:29 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:04:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:05:15 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:06:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:07:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:08:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:09:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:10:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:11:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:12:19 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:13:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:14:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:15:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:16:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:17:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:18:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:19:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:20:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:21:25 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:22:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:23:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:24:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:25:34 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", + "0:26:34 Job 'j-240508de680a4a01bad4dfca194be16b': finished (progress 100%)\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "current_datetime = datetime.now()\n", + "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", + "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", + "\n", + "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", + "\n", + "prediction = input_cube.apply_neighborhood(\n", + " process=udf,\n", + " size=[\n", + " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", + " ],\n", + " overlap=[\n", + " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", + " ],\n", + ")\n", + "\n", + "prediction = prediction.rename_labels(dimension=\"bands\",target= [\"output_catboost\"])\n", + "\n", + "prediction.execute_batch(outputfile = outputfile_name,\n", + " description='world cereal inference',\n", + " job_options={'driver-memory': '4g',\n", + " 'executor-memoryOverhead':'8g'} )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2cf64980", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(126, 166)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGhCAYAAADBddZJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAw+ElEQVR4nO3de3SUVZ7u8acuSeVeuUAqCSQQFBtERATECGfUMd2IHkVFbRxUWl0y2qACfRSZbnC0VdTpVgYvoK4eW0+D2s4oKn3ExqCgY7gloCIYgiIEQhI0JJV7KlX7/OF0tSWoXCpvpSrfz1rvWmTvXW9+v7WweHyvNmOMEQAAgEXskS4AAAD0LoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGCpiIaPJ598UgMHDlRCQoLGjh2rjRs3RrIcAABggYiFj5dffllz5szRPffco/Lyco0YMUITJkxQXV1dpEoCAAAWsEXqxXJjx47VmDFj9MQTT0iSAoGA8vPzddttt+nuu+/+wc8GAgFVV1crNTVVNpvNinIBAMAPMMaoqalJeXl5stt/+NiG06KaQnR2dqqsrEzz5s0LjtntdhUXF6u0tPSw9R0dHero6Aj+vH//fp166qmW1AoAAI5eVVWV+vfv/4NrIhI+vvrqK/n9fnk8npBxj8ejzz777LD1Cxcu1L333nvY+HhdJKfiuq1OAABwdLrk0wf6f0pNTf3RtREJH8dq3rx5mjNnTvBnr9er/Px8ORUnp43wAQBAxP3PRRxHczlERMJHnz595HA4VFtbGzJeW1urnJycw9a7XC65XC6rygMAAN0oIne7xMfHa9SoUSopKQmOBQIBlZSUqKioKBIlAQAAi0TstMucOXM0bdo0jR49WmeddZYWLVqklpYW3XDDDZEqCQAAWCBi4ePnP/+5Dh48qAULFqimpkZnnHGGVq1addhFqAAAILZE7DkfJ8Lr9crtdus8TeKCUwAAeoAu49N7el2NjY1KS0v7wbW82wUAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALBU2MPHwoULNWbMGKWmpio7O1uXXXaZKioqQta0t7drxowZysrKUkpKiiZPnqza2tpwlwIAAHqgsIePtWvXasaMGVq/fr1Wr14tn8+nn/3sZ2ppaQmumT17tt5880298sorWrt2raqrq3XFFVeEuxQAANAD2Ywxpjt/wcGDB5Wdna21a9fqH/7hH9TY2Ki+fftq+fLluvLKKyVJn332mYYOHarS0lKdffbZP7pPr9crt9ut8zRJTltcd5YPAACOQpfx6T29rsbGRqWlpf3g2m6/5qOxsVGSlJmZKUkqKyuTz+dTcXFxcM2QIUNUUFCg0tLSI+6jo6NDXq83ZAMAANGpW8NHIBDQrFmzNG7cOJ122mmSpJqaGsXHxys9PT1krcfjUU1NzRH3s3DhQrnd7uCWn5/fnWUDAIBu1K3hY8aMGdq2bZteeumlE9rPvHnz1NjYGNyqqqrCVCEAALCas7t2PHPmTK1cuVLr1q1T//79g+M5OTnq7OxUQ0NDyNGP2tpa5eTkHHFfLpdLLperu0oFAAAWCvuRD2OMZs6cqddee01r1qxRYWFhyPyoUaMUFxenkpKS4FhFRYX27t2roqKicJcDAAB6mLAf+ZgxY4aWL1+u119/XampqcHrONxutxITE+V2u3XTTTdpzpw5yszMVFpamm677TYVFRUd1Z0uAAAguoU9fCxZskSSdN5554WMP/fcc/rFL34hSXrsscdkt9s1efJkdXR0aMKECXrqqafCXQoAAOiBuv05H92B53wAANCz9KjnfAAAAHwb4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEA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9uzgka1Y6fO7jqavnJycwy6G7+rqUn19fVT2/rfgsWfPHq1evTrk1eux0uv777+vuro6FRQUBL+j9uzZo1/96lcaOHCgpNjotU+fPnI6nT/6HdUd38dRFz7i4+M1atQolZSUBMcCgYBKSkpUVFQUwcpOjDFGM2fO1GuvvaY1a9aosLAwZH7UqFGKi4sL6buiokJ79+6Nur4vuOACffLJJ9q6dWtwGz16tKZOnRr8c6z0Om7cuMNumd65c6cGDBggSSosLFROTk5Ir16vVxs2bIiqXltbW2W3h36dOByO4P9ZxUqf33U0fRUVFamhoUFlZWXBNWvWrFEgENDYsWMtr/lE/C14VFZW6p133lFWVlbIfKz0et111+njjz8O+Y7Ky8vTnXfeqbfffltSbPQaHx+vMWPG/OB3VLf923Pcl6pG0EsvvWRcLpf54x//aLZv326mT59u0tPTTU1NTaRLO2633nqrcbvd5r333jMHDhwIbq2trcE1t9xyiykoKDBr1qwxmzdvNkVFRaaoqCiCVYfPt+92MSZ2et24caNxOp3mgQceMJWVlWbZsmUmKSnJ/OlPfwqueeihh0x6erp5/fXXzccff2wmTZpkCgsLTVtbWwQrPzbTpk0z/fr1MytXrjS7d+82r776qunTp4+56667gmuitc+mpiazZcsWs2XLFiPJPProo2bLli3BOzyOpq8LL7zQjBw50mzYsMF88MEHZvDgweaaa66JVEvf64d67ezsNJdeeqnp37+/2bp1a8j3VEdHR3AfsdDrkXz3bhdjoqPXH+vz1VdfNXFxceaZZ54xlZWV5vHHHzcOh8O8//77wX10x/dxVIYPY4x5/PHHTUFBgYmPjzdnnXWWWb9+faRLOiGSjrg999xzwTVtbW3ml7/8pcnIyDBJSUnm8ssvNwcOHIhc0WH03fARS72++eab5rTTTjMul8sMGTLEPPPMMyHzgUDAzJ8/33g8HuNyucwFF1xgKioqIlTt8fF6veaOO+4wBQUFJiEhwQwaNMj8+te/DvlHKVr7fPfdd4/43+a0adOMMUfX19dff22uueYak5KSYtLS0swNN9xgmpqaItDND/uhXnfv3v2931PvvvtucB+x0OuRHCl8REOvR9PnH/7wB3PyySebhIQEM2LECLNixYqQfXTH97HNmG89ghAAAKCbRd01HwAAILoRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgqf8PdBBXOWBRxK8AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import xarray as xr\n", + "import matplotlib.pyplot as plt\n", + "\n", + "output = xr.open_dataset(outputfile_name)\n", + "output = output['output_catboost'].to_numpy().squeeze()\n", + "plt.imshow(output)\n", + "\n", + "output.shape\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5b7bea33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-24051411052f466b911c92ea2d1e7b20': send 'start'\n", + "0:00:29 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:00:35 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:00:44 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:00:53 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", + "0:01:11 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:01:28 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:01:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:02:15 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:02:43 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:03:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:04:03 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:04:54 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:05:56 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:06:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:08:01 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:09:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:10:18 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:11:22 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:12:23 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:13:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:14:31 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:15:32 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:16:33 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:17:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:18:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:19:35 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:20:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:21:46 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:22:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:23:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:24:58 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:26:00 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:27:02 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:28:04 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:29:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:30:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "0:31:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", + "16:12:29 Job 'j-24051411052f466b911c92ea2d1e7b20': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", + "16:13:03 Job 'j-24051411052f466b911c92ea2d1e7b20': finished (progress 100%)\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from datetime import datetime\n", + "\n", + "current_datetime = datetime.now()\n", + "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", + "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", + "\n", + "presto_list = [\"presto_\" + str(i) for i in range(1, 129)]\n", + "\n", + "\n", + "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", + "\n", + "prediction = input_cube.apply_neighborhood(\n", + " process=udf,\n", + " size=[\n", + " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", + " ],\n", + " overlap=[\n", + " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", + " ],\n", + ")\n", + "\n", + "prediction = prediction.rename_labels(dimension=\"bands\",target= presto_list)\n", + "\n", + "prediction.execute_batch(outputfile = outputfile_name,\n", + " description='world cereal inference',\n", + " job_options={'driver-memory': '4g',\n", + " 'executor-memoryOverhead':'8g'} )" + ] + }, + { + "cell_type": "markdown", + "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", + "metadata": {}, + "source": [ + "### Check reference" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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226L559/PjZt2hS33HJLbNmypfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtGpiH75y1/GZZddFpdeemlERLzrXe+K7373u/Hkk09GxBu7lrfeemt88YtfjMsuuywiIr7zne9Ee3t7fP/7348rrrgiefkAANSSinYuL7rooti2bVvs27cvIiJ+9atfxeOPPx4f+chHIiLixRdfjO7u7li6dGn5c9ra2mLRokWxY8eO4z5nX19flEqlATcAAOpTRTuX69ati1KpFPPmzYvx48fHsWPH4sYbb4yVK1dGRER3d3dERLS3tw/4vPb29vLH3mrjxo3xD//wD0NZOwAANaaincvvfe97cd9998X9998fTz/9dNx7773xT//0T3HvvfcOeQHr16+P3t7e8q2rq2vIzwUAQHVVtHP5uc99LtatW1f+t5Pz58+PX//617Fx48ZYtWpVdHR0RERET09PzJo1q/x5PT09ce655x73OVtaWqKlpWWIywcAoJZUFJevvfZajBs3cLNz/Pjx0d/fHxERnZ2d0dHREdu2bSvHZKlUip07d8ZVV12Vs+Ia4xriAAD/X0Vx+Rd/8Rdx4403xty5c+O9731v/Pu//3t8/etfj09+8pMREdHU1BRr166Nr3zlK3HmmWdGZ2dnbNiwIWbPnh3Lly8fifUDAFBDKorLLVu2xIYNG+Izn/lMvPzyyzF79uz427/927jhhhvKj7n++uvj1VdfjSuvvDIOHjwYF198cTzyyCMxadKk9MUDAFBbmoo3X16nBpRKpWhra4tX9p0RrVNr/+qUfi0Ote/RA3uqvQSAulY61B/Tz9ofvb290draesLH1n69AQBQN8QlAABpKvo3lwC1zK+/AarPziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAwBQdrwLxLxeHI2I/Sf1+XYuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhjWvwtjjchFeGaxQAAJ8POJQAAacQlAABpxCUAAGnEJQAAacQlAABpxCUAAGmciugkDXaKIqB2OJUYwMmr5D2zdKg/pp91cs9r5xIAgDTiEgCANOISAIA04hIAgDTiEgCANDU7Lf7Rs+ZHc9OEAfeZ+ASGYihne/B+AzSK0T7jjZ1LAADSiEsAANKISwAA0ohLAADSiEsAANLU7LT48bi+NzBaqvV+U80pdddmh7FlpP5u27kEACCNuAQAII24BAAgjbgEACCNuAQAIE1dTYsDNLrRmFIfbEJ0NKbCK/3+TKrD8I323yM7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OMMY0+kQ6NLLB/v7W0t8tO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEtDkC6ephoBUaGnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSOBURAOmccgiGp55P52XnEgCANOISAIA04hIAgDTiEgCANOISAIA0psUBGJJ6mFqFWjfYVHg9s3MJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gAAI6wRp8IHY+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxQEAkmRNhT96YE/K81SDnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ri2OABABbKuHx5R39cQH4ydSwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANI4FREAQ+J0LDQ6r/GhsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gBU3WBTuWNpwpbqyZwKx84lAACJxCUAAGnEJQAAacQlAABpxCUAAGlMiwNQs6o5xWtSvX553VSXnUsAANKISwAA0ohLAADSiEsAANLU3EBPURQREfF6HI0oqrwYAMas0qH+ai+BIXq9OFq1r92or5vS79/4vv63006kqTiZR42i//7v/445c+ZUexkAALxFV1dXnH766Sd8TM3FZX9/fxw4cCCmTp0ahw4dijlz5kRXV1e0trZWe2mMsFKp5HiPEY712OFYjx2OdWMriiIOHToUs2fPjnHjTvyvKmvu1+Ljxo0rF3FTU1NERLS2tnqhjiGO99jhWI8djvXY4Vg3rra2tpN6nIEeAADSiEsAANLUdFy2tLTEl770pWhpaan2UhgFjvfY4ViPHY712OFY879qbqAHAID6VdM7lwAA1BdxCQBAGnEJAEAacQkAQJqajsvbb7893vWud8WkSZNi0aJF8eSTT1Z7SQzTxo0bY+HChTF16tSYOXNmLF++PPbu3TvgMYcPH47Vq1fHjBkz4pRTTokVK1ZET09PlVZMlptvvjmamppi7dq15fsc68bxm9/8Jj7+8Y/HjBkzYvLkyTF//vx46qmnyh8viiJuuOGGmDVrVkyePDmWLl0aL7zwQhVXzFAcO3YsNmzYEJ2dnTF58uR497vfHV/+8pcHXG/asaZm4/Jf//Vf47rrrosvfelL8fTTT8f73//+WLZsWbz88svVXhrDsH379li9enU88cQT8dhjj8XRo0fjwx/+cLz66qvlx1x77bXxox/9KB588MHYvn17HDhwIC6//PIqrprh2rVrV3zrW9+K973vfQPud6wbwyuvvBJLliyJCRMmxE9+8pN47rnn4mtf+1pMnz69/JhbbrklNm/eHHfeeWfs3LkzpkyZEsuWLYvDhw9XceVUatOmTXHHHXfEbbfdFs8//3xs2rQpbrnlltiyZUv5MY41UdSoCy+8sFi9enX5z8eOHStmz55dbNy4sYqrItvLL79cRESxffv2oiiK4uDBg8WECROKBx98sPyY559/voiIYseOHdVaJsNw6NCh4swzzywee+yx4s/+7M+Ka665pigKx7qRfP7zny8uvvjiQT/e399fdHR0FF/96lfL9x08eLBoaWkpvvvd747GEkly6aWXFp/85CcH3Hf55ZcXK1euLIrCseYNNblzeeTIkdi9e3csXbq0fN+4ceNi6dKlsWPHjiqujGy9vb0REXHqqadGRMTu3bvj6NGjA479vHnzYu7cuY59nVq9enVceumlA45phGPdSH74wx/GggUL4mMf+1jMnDkzzjvvvLj77rvLH3/xxReju7t7wLFua2uLRYsWOdZ15qKLLopt27bFvn37IiLiV7/6VTz++OPxkY98JCIca97QXO0FHM/vfve7OHbsWLS3tw+4v729Pf7rv/6rSqsiW39/f6xduzaWLFkS55xzTkREdHd3x8SJE2PatGkDHtve3h7d3d1VWCXD8cADD8TTTz8du3btetvHHOvGsX///rjjjjviuuuuiy984Quxa9eu+OxnPxsTJ06MVatWlY/n8d7THev6sm7duiiVSjFv3rwYP358HDt2LG688cZYuXJlRIRjTUTUaFwyNqxevTqeffbZePzxx6u9FEZAV1dXXHPNNfHYY4/FpEmTqr0cRlB/f38sWLAgbrrppoiIOO+88+LZZ5+NO++8M1atWlXl1ZHpe9/7Xtx3331x//33x3vf+97Ys2dPrF27NmbPnu1YU1aTvxY/7bTTYvz48W+bGu3p6YmOjo4qrYpMa9asiYcffjh+9rOfxemnn16+v6OjI44cORIHDx4c8HjHvv7s3r07Xn755Tj//POjubk5mpubY/v27bF58+Zobm6O9vZ2x7pBzJo1K97znvcMuO/ss8+Ol156KSKifDy9p9e/z33uc7Fu3bq44oorYv78+fHXf/3Xce2118bGjRsjwrHmDTUZlxMnTowLLrggtm3bVr6vv78/tm3bFosXL67iyhiuoihizZo18dBDD8VPf/rT6OzsHPDxCy64ICZMmDDg2O/duzdeeuklx77OXHLJJfHMM8/Enj17yrcFCxbEypUry//tWDeGJUuWvO2UYvv27Yt3vvOdERHR2dkZHR0dA451qVSKnTt3OtZ15rXXXotx4wamw/jx46O/vz8iHGv+n2pPFA3mgQceKFpaWop77rmneO6554orr7yymDZtWtHd3V3tpTEMV111VdHW1lb8/Oc/L37729+Wb6+99lr5MX/3d39XzJ07t/jpT39aPPXUU8XixYuLxYsXV3HVZHnztHhRONaN4sknnyyam5uLG2+8sXjhhReK++67r3jHO95R/Mu//Ev5MTfffHMxbdq04gc/+EHxH//xH8Vll11WdHZ2Fn/4wx+quHIqtWrVquJP/uRPiocffrh48cUXi3/7t38rTjvttOL6668vP8axpmbjsiiKYsuWLcXcuXOLiRMnFhdeeGHxxBNPVHtJDFNEHPe2devW8mP+8Ic/FJ/5zGeK6dOnF+94xzuKj370o8Vvf/vb6i2aNG+NS8e6cfzoRz8qzjnnnKKlpaWYN29ecddddw34eH9/f7Fhw4aivb29aGlpKS655JJi7969VVotQ1UqlYprrrmmmDt3bjFp0qTijDPOKP7+7/++6OvrKz/GsaapKN50Wn0AABiGmvw3lwAA1CdxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAECa/ws8KlmAU0tsngAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py new file mode 100644 index 00000000..3e8cccba --- /dev/null +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -0,0 +1,476 @@ +import logging +import urllib.request +import shutil +from pathlib import Path +import sys +import functools +import xarray as xr +import numpy as np +from pyproj import Transformer + +from typing import Dict, Tuple + +import numpy as np + + + +def _setup_logging(): + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + return logger + +@functools.lru_cache(maxsize=6) +def extract_dependencies(base_url: str, dependency_name: str): + + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / 'dependencies' + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) + + return(abs_path) + + +def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: + + logger = _setup_logging() + logger.info("Shape of input: {}".format(cube.shape)) + + # shape and indiches for output + orig_dims = list(cube.dims) + map_dims = (100,100) + + # Unzip de dependencies on the backend + logger.info("Unzipping dependencies") + base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" + dependency_name = "wc_presto_onnx_dependencies.zip" + dep_dir = extract_dependencies(base_url, dependency_name) + + # Append the dependencies + sys.path.append(str(dep_dir)) + sys.path.append(str(dep_dir) + '/pandas') + + ################################################################################################################### + + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( + BANDS, + BANDS_GROUPS_IDX, + NORMED_BANDS, + S1_S2_ERA5_SRTM, + DynamicWorld2020_2021, + ) + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.masking import BAND_EXPANSION + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device + + import pandas as pd + + import torch + from torch.utils.data import DataLoader, TensorDataset + + from einops import repeat + import onnxruntime + import requests + + + + #% Mapping from original band names to Presto names + BAND_MAPPING = { + "B02": "B2", + "B03": "B3", + "B04": "B4", + "B05": "B5", + "B06": "B6", + "B07": "B7", + "B08": "B8", + "B8A": "B8A", + "B11": "B11", + "B12": "B12", + "VH": "VH", + "VV": "VV", + "precipitation-flux": "total_precipitation", + "temperature-mean": "temperature_2m", + } + + # Index to band groups mapping + IDX_TO_BAND_GROUPS = { + NORMED_BANDS[idx]: band_group_idx + for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) + for idx in val + } + + class WorldCerealPredictor: + def __init__(self): + """ + Initialize an empty WorldCerealPredictor. + """ + self.onnx_session = None + + def load_model(self, model): + """ + Load an ONNX model from the specified path. + + Args: + model_path (str): The path to the ONNX model file. + """ + # Load the dependency into an InferenceSession + self.onnx_session = onnxruntime.InferenceSession(model) + + def predict(self, features: np.ndarray) -> np.ndarray: + """ + Predicts labels using the provided features DataFrame. + + Args: + features (pd.DataFrame): DataFrame containing the features for prediction. + + Returns: + pd.DataFrame: DataFrame containing the predicted labels. + """ + if self.onnx_session is None: + raise ValueError("Model has not been loaded. Please load a model first.") + + # Prepare input data for ONNX model + outputs = self.onnx_session.run(None, {'features': features}) + + # Threshold for binary conversion + threshold = 0.5 + + # Extract all prediction values and convert them to binary labels + prediction_values = [sublist['True'] for sublist in outputs[1]] + binary_labels = np.array(prediction_values) >= threshold + binary_labels = binary_labels.astype(int) + + return binary_labels + + + + class PrestoFeatureExtractor: + + def __init__(self, model: Presto): + """ + Initialize the PrestoFeatureExtractor with a Presto model. + + Args: + model (Presto): The Presto model used for feature extraction. + """ + self.model = model + + _NODATAVALUE = 65535 + + BAND_MAPPING = { + "B02": "B2", + "B03": "B3", + "B04": "B4", + "B05": "B5", + "B06": "B6", + "B07": "B7", + "B08": "B8", + "B8A": "B8A", + "B11": "B11", + "B12": "B12", + "VH": "VH", + "VV": "VV", + "precipitation-flux": "total_precipitation", + "temperature-mean": "temperature_2m", + } + + @classmethod + def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.ndarray: + """ + Preprocesses the band values based on the given presto_val. + + Args: + values (np.ndarray): Array of band values to preprocess. + presto_val (str): Name of the band for preprocessing. + + Returns: + np.ndarray: Preprocessed array of band values. + """ + if presto_band in ["VV", "VH"]: + # Convert to dB + values = 20 * np.log10(values) - 83 + elif presto_band == "total_precipitation": + # Scale precipitation and convert mm to m + values = values / (100 * 1000.0) + elif presto_band == "temperature_2m": + # Remove scaling + values = values / 100 + return values + + @classmethod + def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: + """ + Extracts EO data and mask arrays from the input xarray.DataArray. + + Args: + inarr (xr.DataArray): Input xarray.DataArray containing EO data. + + Returns: + Tuple[np.ndarray, np.ndarray]: Tuple containing EO data array and mask array. + """ + num_pixels = len(inarr.x) * len(inarr.y) + num_timesteps = len(inarr.t) + + eo_data = np.zeros((num_pixels, num_timesteps, len(BANDS))) + mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) + + for org_band, presto_band in cls.BAND_MAPPING.items(): + if org_band in inarr.coords['bands']: + values = np.swapaxes(inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1) + idx_valid = values != cls._NODATAVALUE + values = cls._preprocess_band_values(values, presto_band) + eo_data[:, :, BANDS.index(presto_band)] = values + mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid + + return eo_data, mask + + + @staticmethod + def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: + """ + Extracts latitudes and longitudes from the input xarray.DataArray. + + Args: + inarr (xr.DataArray): Input xarray.DataArray containing spatial coordinates. + epsg (int): EPSG code for coordinate reference system. + + Returns: + np.ndarray: Array containing extracted latitudes and longitudes. + """ + #EPSG:4326 is the supported crs for presto + transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) + lon, lat = transformer.transform(inarr.x, inarr.y) + + + + # 2D array where each row represents a pair of latitude and longitude coordinates. + return np.stack( + [np.repeat(lat, repeats=len(lon)), repeat(lon, "c -> (h c)", h=len(lat))], + axis=-1, + ) + + @staticmethod + def _extract_months( inarr: xr.DataArray) -> np.ndarray: + """ + Calculate the start month based on the first timestamp in the input array, + and create an array of the same length filled with that start month value. + + Parameters: + - inarr: xarray.DataArray or numpy.ndarray + Input array containing timestamps. + + Returns: + - months: numpy.ndarray + Array of start month values, with the same length as the input array. + """ + num_instances = len(inarr.x) * len(inarr.y) + + start_month = ( + inarr.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1 + ) - 1 + + months = np.ones((num_instances)) * start_month + return months + + def _create_dataloader(self, eo:np.ndarray, dynamic_world:np.ndarray, months:np.ndarray, latlons:np.ndarray, mask:np.ndarray) -> DataLoader: + """ + Create a PyTorch DataLoader for encoding features. + + Args: + eo_data (np.ndarray): Array containing Earth Observation data. + dynamic_world (np.ndarray): Array containing dynamic world data. + latlons (np.ndarray): Array containing latitude and longitude coordinates. + inarr (xr.DataArray): Input xarray.DataArray. + mask (np.ndarray): Array containing masking data. + + Returns: + DataLoader: PyTorch DataLoader for encoding features. + """ + + dl = DataLoader( + TensorDataset( + torch.from_numpy(eo).float(), + torch.from_numpy(dynamic_world).long(), + torch.from_numpy(latlons).float(), + torch.from_numpy(months).long(), + torch.from_numpy(mask).float(), + ), + batch_size=8192, + shuffle=False, + ) + + return dl + + def _create_presto_input( + cls, inarr: xr.DataArray, epsg: int = 4326 + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + + eo_data, mask = cls._extract_eo_data(inarr) + latlons = cls._extract_latlons(inarr, epsg) + months = cls._extract_months(inarr) + dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( + DynamicWorld2020_2021.class_amount + ) + + return ( + S1_S2_ERA5_SRTM.normalize(eo_data), + dynamic_world, + months, + latlons, + np.repeat(mask, BAND_EXPANSION, axis=-1) + ) + + + def _get_encodings(self, dl: DataLoader) -> np.ndarray: + """ + Get encodings from DataLoader. + + Args: + dl (DataLoader): PyTorch DataLoader containing data for encoding. + + Returns: + np.ndarray: Array containing encoded features. + """ + + all_encodings = [] + + for x, dw, latlons, month, variable_mask in dl: + x_f, dw_f, latlons_f, month_f, variable_mask_f = [ + t.to(device) for t in (x, dw, latlons, month, variable_mask) + ] + + with torch.no_grad(): + encodings = ( + self.model.encoder( + x_f, + dynamic_world=dw_f.long(), + mask=variable_mask_f, + latlons=latlons_f, + month=month_f, + ) + .cpu() + .numpy() + ) + + all_encodings.append(encodings) + + return np.concatenate(all_encodings, axis=0) + + @staticmethod + def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFrame: + flat_lat, flat_lon = latlons[:, 0], latlons[:, 1] + if len(encodings.shape) == 1: + encodings = np.expand_dims(encodings, axis=-1) + + data_dict: Dict[str, np.ndarray] = {"lat": flat_lat, "lon": flat_lon} + for i in range(encodings.shape[1]): + encodings_label = f"presto_ft_{i}" + data_dict[encodings_label] = encodings[:, i] + return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) + + + def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> np.ndarray: + eo, dynamic_world, months, latlons, mask = self._create_presto_input(inarr, epsg) + dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) + + features = self._get_encodings(dl) + features = self.combine_encodings(latlons, features) + features = features.to_numpy() + + return features + + + def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: + """ + Extracts features from input data using Presto. + + Args: + inarr (xr.DataArray): Input data as xarray DataArray. + presto_path (str): Path to the pretrained Presto model. + + Returns: + xr.DataArray: Extracted features as xarray DataArray. + """ + # Load the model + + presto_model = Presto.load_pretrained_artifactory(presto_url = presto_path, strict=False) + presto_extractor = PrestoFeatureExtractor(presto_model) + features = presto_extractor.extract_presto_features(inarr, epsg=32631) + return features + + + def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarray: + """ + Classifies features using the WorldCereal CatBoost model. + + Args: + features (np.ndarray): Features to be classified. + map_dims (tuple): Original x, y dimensions of the input data. + model_path (str): Path to the trained CatBoost model. + + Returns: + xr.DataArray: Classified data as xarray DataArray. + """ + + predictor = WorldCerealPredictor() + response = requests.get(catboost_path) + catboost_model = response.content + + predictor.load_model(catboost_model) + predictions = predictor.predict(features) + + + return predictions + + + ################################################################################################################### + + + # Run presto inference + logger.info("Extracting presto features") + PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" + features = get_presto_features(cube, PRESTO_PATH) + logger.info("Shape of presto output: {}".format(features.shape)) + + # run catboost classification + logger.info("Catboost classification") + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" + classification = classify_with_catboost(features, CATBOOST_PATH) + logger.info("Shape of classification output: {}".format(classification.shape)) + + # revert to 4D shape for openEO + #logger.info("Revert to 4D xarray") + #transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) + #longitudes, latitudes = transformer.transform(cube.x, cube.y) + + classification = np.flip(classification.reshape(map_dims),axis = 0) + classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) + output = xr.DataArray(classification, dims=orig_dims) + logger.info("Shape of output: {}".format(output.shape)) + + return output + + + + + + + + + + + + + + + + diff --git a/minimal_wc_presto/udf_presto.py b/minimal_wc_presto/udf_presto.py index 9c6ed1ef..c423e390 100644 --- a/minimal_wc_presto/udf_presto.py +++ b/minimal_wc_presto/udf_presto.py @@ -65,15 +65,16 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: features = get_presto_features(cube, PRESTO_PATH) # go to 128,1,100,100 - presto_dim = map_dims + (128,) + presto_dim = map_dims + (128,) features = features.reshape(presto_dim) + features = np.expand_dims(features, axis = 0) features = np.transpose(features, (3, 0, 1, 2)) transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) longitudes, latitudes = transformer.transform(cube.x, cube.y) - features = np.expand_dims(features, axis = 0) + output = xr.DataArray(features, dims=orig_dims, coords={'y': longitudes, 'x': latitudes}) return output diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index f94582a2..6296217d 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -39,10 +39,11 @@ def extract_dependencies(base_url: str, dependency_name: str): def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger = _setup_logging() + logger.info("Shape of input: {}".format(cube.shape)) # shape and indiches for output orig_dims = list(cube.dims) - map_dims = cube.shape[2:] + map_dims = (100,100) # Unzip de dependencies on the backend logger.info("Unzipping dependencies") @@ -59,23 +60,23 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger.info("Extracting presto features") PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" features = get_presto_features(cube, PRESTO_PATH) - logger.info(str(features.shape)) + logger.info("Shape of presto output: {}".format(features.shape)) # run catboost classification logger.info("Catboost classification") CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" classification = classify_with_catboost(features, CATBOOST_PATH) - logger.info(str(classification.shape)) + logger.info("Shape of classification output: {}".format(classification.shape)) # revert to 4D shape for openEO - logger.info("Revert to 4D xarray") - transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) - longitudes, latitudes = transformer.transform(cube.x, cube.y) + #logger.info("Revert to 4D xarray") + #transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) + #longitudes, latitudes = transformer.transform(cube.x, cube.y) classification = np.flip(classification.reshape(map_dims),axis = 0) - classification = np.expand_dims(np.expand_dims(classification, axis=0),axis = 0) - output = xr.DataArray(classification, dims=orig_dims, coords={'y': longitudes, 'x': latitudes}) - logger.info(str(output.shape)) + classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) + output = xr.DataArray(classification, dims=orig_dims) + logger.info("Shape of output: {}".format(output.shape)) return output From 6ae5da29673ca792b7f2d41606064d3cff6f8848 Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Tue, 21 May 2024 10:07:16 +0200 Subject: [PATCH 07/31] fix: test remote inference --- .../backend_inference_example_openeo.ipynb | 566 +++++++++------- minimal_wc_presto/dev_testing.py | 83 +++ .../mvp_wc_presto/world_cereal_inference.py | 4 +- minimal_wc_presto/test_aggregator.ipynb | 633 +++--------------- minimal_wc_presto/test_prestobackend.py | 25 + 5 files changed, 528 insertions(+), 783 deletions(-) create mode 100644 minimal_wc_presto/dev_testing.py create mode 100644 minimal_wc_presto/test_prestobackend.py diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index f7b5e02c..2cdf4c68 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 4, "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", "metadata": { "tags": [] @@ -20,39 +20,33 @@ "name": "stdout", "output_type": "stream", "text": [ - "Authenticated using refresh token.\n", "Authenticated using refresh token.\n" ] } ], "source": [ "import openeo\n", + "from datetime import datetime \n", "\n", - "#token for METEO\n", - "connection_terra = openeo.connect(\"openeo.vito.be\").authenticate_oidc()\n", "\n", "#token SENTINEL\n", "connection = openeo.connect(\"https://openeo.dataspace.copernicus.eu/\").authenticate_oidc()" ] }, + { + "cell_type": "markdown", + "id": "5af70a06", + "metadata": {}, + "source": [ + "Load in Cube without METEO\n" + ] + }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 5, "id": "5494c46d", "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "worldcereal_preprocessed_inputs() missing 1 required positional argument: 'end'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[16], line 16\u001b[0m\n\u001b[0;32m 10\u001b[0m \u001b[38;5;66;03m# Set OpenEO classification UDF context based on settings\u001b[39;00m\n\u001b[0;32m 11\u001b[0m CONTEXT \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 12\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstartdate\u001b[39m\u001b[38;5;124m\"\u001b[39m: STARTDATE, \u001b[38;5;66;03m# Required\u001b[39;00m\n\u001b[0;32m 13\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124menddate\u001b[39m\u001b[38;5;124m\"\u001b[39m: ENDDATE, \u001b[38;5;66;03m# Required\u001b[39;00m\n\u001b[0;32m 14\u001b[0m }\n\u001b[1;32m---> 16\u001b[0m s2_cube \u001b[38;5;241m=\u001b[39m \u001b[43mworldcereal_preprocessed_inputs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mconnection\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[43mEXTENT\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 19\u001b[0m \u001b[43m \u001b[49m\u001b[43mSTARTDATE\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 20\u001b[0m \u001b[43m \u001b[49m\u001b[43mENDDATE\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 21\u001b[0m \u001b[43m \u001b[49m\u001b[43mMETEO_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 22\u001b[0m \u001b[43m \u001b[49m\u001b[43mS2_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mSENTINEL2_L2A\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 23\u001b[0m \u001b[43m \u001b[49m\u001b[43mS1_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mSENTINEL1_GRD\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 24\u001b[0m \u001b[43m \u001b[49m\u001b[43mDEM_collection\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mCOPERNICUS_30\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\n\u001b[0;32m 25\u001b[0m \u001b[43m)\u001b[49m\n\u001b[0;32m 27\u001b[0m agera5_cube \u001b[38;5;241m=\u001b[39m worldcereal_preprocessed_inputs(\n\u001b[0;32m 28\u001b[0m connection_terra,\n\u001b[0;32m 29\u001b[0m EXTENT,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 35\u001b[0m DEM_collection\u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 36\u001b[0m )\n", - "\u001b[1;31mTypeError\u001b[0m: worldcereal_preprocessed_inputs() missing 1 required positional argument: 'end'" - ] - } - ], + "outputs": [], "source": [ "#Get desired data\n", "from preprocessing import worldcereal_preprocessed_inputs\n", @@ -63,15 +57,9 @@ "STARTDATE = '2020-11-01'\n", "ENDDATE = '2021-10-31'\n", "\n", - "# Set OpenEO classification UDF context based on settings\n", - "CONTEXT = {\n", - " \"startdate\": STARTDATE, # Required\n", - " \"enddate\": ENDDATE, # Required\n", - "}\n", - "\n", - "\n", + "#TODO aggregator for including METEO?\n", "\n", - "s2_cube = worldcereal_preprocessed_inputs(\n", + "input_cube = worldcereal_preprocessed_inputs(\n", " connection = connection,\n", " bbox = EXTENT,\n", " start = STARTDATE,\n", @@ -80,97 +68,84 @@ " S2_collection= \"SENTINEL2_L2A\",\n", " S1_collection= \"SENTINEL1_GRD\",\n", " DEM_collection= \"COPERNICUS_30\"\n", - ")\n", - "\n", - "agera5_cube = worldcereal_preprocessed_inputs(\n", - " connection = connection_terra,\n", - " bbox = EXTENT,\n", - " start = STARTDATE,\n", - " end = ENDDATE,\n", - " METEO_collection=\"AGERA5\",\n", - " S2_collection= None,\n", - " S1_collection= None,\n", - " DEM_collection= None\n", - ")\n", - "\n", - "\n" + ")\n" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "94969249", + "cell_type": "markdown", + "id": "da8d05cd", "metadata": {}, - "outputs": [], "source": [ - "from pathlib import Path\n", - "result_dir = Path.cmd\n", - "\n", - "job = agera5_cube.create_job(\n", - " out_format=\"GTIFF\",\n", - ")\n", - "\n", - "job.get_results().download_files(result_dir)\n", - "result_metadata = job.get_results()\n", - "job_url, = [k[\"href\"] for k in result_metadata.get_metadata()[\"links\"] if k[\"rel\"] == \"canonical\"]\n", - "\n", - "load_stac_cube = s2_cube.load_stac(job_url)\n", - "\n", - "input_cube = s2_cube.merge_cubes(load_stac_cube)\n", - "\n", - "job = input_cube.create_job(out_format=\"NetCDF\")\n", - "job.start_and_wait()\n", - "\n" + "Save the input cube" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "id": "4aab5695", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Preflight process graph validation raised: [CollectionNotFound] Collection 'AGERA5' does not exist.\n" - ] - }, { "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-2405155e702e4218aa9dfac9671faaff': send 'start'\n", - "0:00:16 Job 'j-2405155e702e4218aa9dfac9671faaff': created (progress 0%)\n", - "0:00:22 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:00:28 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:00:36 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:00:47 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:01:00 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:01:16 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:01:35 Job 'j-2405155e702e4218aa9dfac9671faaff': error (progress N/A)\n", - "Your batch job 'j-2405155e702e4218aa9dfac9671faaff' failed. Error logs:\n", - "[{'id': '[1715756877175, 557437]', 'time': '2024-05-15T07:07:57.175Z', 'level': 'error', 'message': 'OpenEO batch job failed: CollectionNotFoundException(status_code=404, code=\\'CollectionNotFound\\', message=\"Collection \\'AGERA5\\' does not exist.\", id=\\'no-request\\')'}]\n", - "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-2405155e702e4218aa9dfac9671faaff').logs()`.\n" + "0:00:00 Job 'j-240517a35acc48b697839a923dd5fe56': send 'start'\n", + "0:00:18 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", + "0:00:23 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", + "0:00:30 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", + "0:00:38 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", + "0:00:48 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", + "0:01:02 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", + "0:01:18 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:01:38 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:02:04 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:02:35 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:03:13 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:04:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:04:59 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:05:59 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:07:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:08:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:09:01 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:10:46 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:12:38 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:13:39 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", + "0:14:39 Job 'j-240517a35acc48b697839a923dd5fe56': finished (progress 100%)\n" ] }, { - "ename": "JobFailedException", - "evalue": "Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37).", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[11], line 6\u001b[0m\n\u001b[0;32m 3\u001b[0m formatted_datetime \u001b[38;5;241m=\u001b[39m current_datetime\u001b[38;5;241m.\u001b[39mstrftime(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mY_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mm_\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mH_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mM_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mS\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 4\u001b[0m outputfile_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(formatted_datetime) \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m_input_cube_worldCereal.nc\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m----> 6\u001b[0m \u001b[43minput_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43moutputfile_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 7\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal data collection\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", - "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37)." - ] + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "from datetime import datetime\n", "current_datetime = datetime.now()\n", "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", "outputfile_name = str(formatted_datetime) + '_input_cube_worldCereal.nc'\n", @@ -181,57 +156,79 @@ }, { "cell_type": "markdown", - "id": "48c9322c", + "id": "bc85fadd", "metadata": {}, - "source": [] + "source": [ + "Run the presto UDF and fetch presto features" + ] }, { "cell_type": "code", - "execution_count": 3, - "id": "8f71136c-1252-4786-8609-8bb995da7daf", - "metadata": { - "tags": [] - }, + "execution_count": 7, + "id": "64d37c40", + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-240508de680a4a01bad4dfca194be16b': send 'start'\n", - "0:00:28 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", - "0:00:34 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", - "0:00:41 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:00:55 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:05 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:17 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:33 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:52 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:02:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:02:52 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:03:29 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:04:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:05:15 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:06:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:07:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:08:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:09:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:10:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:11:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:12:19 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:13:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:14:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:15:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:16:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:17:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:18:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:19:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:20:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:21:25 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:22:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:23:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:24:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:25:34 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:26:34 Job 'j-240508de680a4a01bad4dfca194be16b': finished (progress 100%)\n" + "0:00:00 Job 'j-2405171879c44f5aac716b6b0ca23b92': send 'start'\n", + "0:00:17 Job 'j-2405171879c44f5aac716b6b0ca23b92': created (progress 0%)\n", + "0:00:22 Job 'j-2405171879c44f5aac716b6b0ca23b92': created (progress 0%)\n", + "0:00:29 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:00:37 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:00:47 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:01:00 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:01:16 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:01:36 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:02:11 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:02:41 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:03:19 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:04:06 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:05:05 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:06:05 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:07:06 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:08:07 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:09:08 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:10:08 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:11:09 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:12:10 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:13:18 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:14:19 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:15:20 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:16:21 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:17:21 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:18:22 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:19:30 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:20:31 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:21:31 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:22:32 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:23:33 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:24:34 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:25:35 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:26:37 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:27:39 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:28:40 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:29:41 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:30:43 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:31:44 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:32:44 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:33:45 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:34:46 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:35:47 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:36:48 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:37:48 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:38:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:39:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:40:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:41:53 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:42:54 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:43:54 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:44:55 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:46:01 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:47:01 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:48:03 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", + "0:49:04 Job 'j-2405171879c44f5aac716b6b0ca23b92': finished (progress 100%)\n" ] }, { @@ -253,26 +250,27 @@ " }\n", " \n", " \n", - " \n", + " \n", " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 3, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "from datetime import datetime\n", "\n", "current_datetime = datetime.now()\n", "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", + "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", "\n", - "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", + "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", "\n", "prediction = input_cube.apply_neighborhood(\n", " process=udf,\n", @@ -286,102 +284,137 @@ " ],\n", ")\n", "\n", - "prediction = prediction.rename_labels(dimension=\"bands\",target= [\"output_catboost\"])\n", + "presto_list = [\"presto_\" + str(i) for i in range(1, 129)]\n", + "prediction = prediction.rename_labels(dimension=\"bands\",target= presto_list)\n", "\n", "prediction.execute_batch(outputfile = outputfile_name,\n", " description='world cereal inference',\n", " job_options={'driver-memory': '4g',\n", - " 'executor-memoryOverhead':'8g'} )\n" + " 'executor-memoryOverhead':'8g'} )" ] }, { - "cell_type": "code", - "execution_count": 4, - "id": "2cf64980", + "cell_type": "markdown", + "id": "48c9322c", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(126, 166)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGhCAYAAADBddZJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAw+ElEQVR4nO3de3SUVZ7u8acuSeVeuUAqCSQQFBtERATECGfUMd2IHkVFbRxUWl0y2qACfRSZbnC0VdTpVgYvoK4eW0+D2s4oKn3ExqCgY7gloCIYgiIEQhI0JJV7KlX7/OF0tSWoXCpvpSrfz1rvWmTvXW9+v7WweHyvNmOMEQAAgEXskS4AAAD0LoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGCpiIaPJ598UgMHDlRCQoLGjh2rjRs3RrIcAABggYiFj5dffllz5szRPffco/Lyco0YMUITJkxQXV1dpEoCAAAWsEXqxXJjx47VmDFj9MQTT0iSAoGA8vPzddttt+nuu+/+wc8GAgFVV1crNTVVNpvNinIBAMAPMMaoqalJeXl5stt/+NiG06KaQnR2dqqsrEzz5s0LjtntdhUXF6u0tPSw9R0dHero6Aj+vH//fp166qmW1AoAAI5eVVWV+vfv/4NrIhI+vvrqK/n9fnk8npBxj8ejzz777LD1Cxcu1L333nvY+HhdJKfiuq1OAABwdLrk0wf6f0pNTf3RtREJH8dq3rx5mjNnTvBnr9er/Px8ORUnp43wAQBAxP3PRRxHczlERMJHnz595HA4VFtbGzJeW1urnJycw9a7XC65XC6rygMAAN0oIne7xMfHa9SoUSopKQmOBQIBlZSUqKioKBIlAQAAi0TstMucOXM0bdo0jR49WmeddZYWLVqklpYW3XDDDZEqCQAAWCBi4ePnP/+5Dh48qAULFqimpkZnnHGGVq1addhFqAAAILZE7DkfJ8Lr9crtdus8TeKCUwAAeoAu49N7el2NjY1KS0v7wbW82wUAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALBU2MPHwoULNWbMGKWmpio7O1uXXXaZKioqQta0t7drxowZysrKUkpKiiZPnqza2tpwlwIAAHqgsIePtWvXasaMGVq/fr1Wr14tn8+nn/3sZ2ppaQmumT17tt5880298sorWrt2raqrq3XFFVeEuxQAANAD2Ywxpjt/wcGDB5Wdna21a9fqH/7hH9TY2Ki+fftq+fLluvLKKyVJn332mYYOHarS0lKdffbZP7pPr9crt9ut8zRJTltcd5YPAACOQpfx6T29rsbGRqWlpf3g2m6/5qOxsVGSlJmZKUkqKyuTz+dTcXFxcM2QIUNUUFCg0tLSI+6jo6NDXq83ZAMAANGpW8NHIBDQrFmzNG7cOJ122mmSpJqaGsXHxys9PT1krcfjUU1NzRH3s3DhQrnd7uCWn5/fnWUDAIBu1K3hY8aMGdq2bZteeumlE9rPvHnz1NjYGNyqqqrCVCEAALCas7t2PHPmTK1cuVLr1q1T//79g+M5OTnq7OxUQ0NDyNGP2tpa5eTkHHFfLpdLLperu0oFAAAWCvuRD2OMZs6cqddee01r1qxRYWFhyPyoUaMUFxenkpKS4FhFRYX27t2roqKicJcDAAB6mLAf+ZgxY4aWL1+u119/XampqcHrONxutxITE+V2u3XTTTdpzpw5yszMVFpamm677TYVFRUd1Z0uAAAguoU9fCxZskSSdN5554WMP/fcc/rFL34hSXrsscdkt9s1efJkdXR0aMKECXrqqafCXQoAAOiBuv05H92B53wAANCz9KjnfAAAAHwb4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEA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9uzgka1Y6fO7jqavnJycwy6G7+rqUn19fVT2/rfgsWfPHq1evTrk1eux0uv777+vuro6FRQUBL+j9uzZo1/96lcaOHCgpNjotU+fPnI6nT/6HdUd38dRFz7i4+M1atQolZSUBMcCgYBKSkpUVFQUwcpOjDFGM2fO1GuvvaY1a9aosLAwZH7UqFGKi4sL6buiokJ79+6Nur4vuOACffLJJ9q6dWtwGz16tKZOnRr8c6z0Om7cuMNumd65c6cGDBggSSosLFROTk5Ir16vVxs2bIiqXltbW2W3h36dOByO4P9ZxUqf33U0fRUVFamhoUFlZWXBNWvWrFEgENDYsWMtr/lE/C14VFZW6p133lFWVlbIfKz0et111+njjz8O+Y7Ky8vTnXfeqbfffltSbPQaHx+vMWPG/OB3VLf923Pcl6pG0EsvvWRcLpf54x//aLZv326mT59u0tPTTU1NTaRLO2633nqrcbvd5r333jMHDhwIbq2trcE1t9xyiykoKDBr1qwxmzdvNkVFRaaoqCiCVYfPt+92MSZ2et24caNxOp3mgQceMJWVlWbZsmUmKSnJ/OlPfwqueeihh0x6erp5/fXXzccff2wmTZpkCgsLTVtbWwQrPzbTpk0z/fr1MytXrjS7d+82r776qunTp4+56667gmuitc+mpiazZcsWs2XLFiPJPProo2bLli3BOzyOpq8LL7zQjBw50mzYsMF88MEHZvDgweaaa66JVEvf64d67ezsNJdeeqnp37+/2bp1a8j3VEdHR3AfsdDrkXz3bhdjoqPXH+vz1VdfNXFxceaZZ54xlZWV5vHHHzcOh8O8//77wX10x/dxVIYPY4x5/PHHTUFBgYmPjzdnnXWWWb9+faRLOiGSjrg999xzwTVtbW3ml7/8pcnIyDBJSUnm8ssvNwcOHIhc0WH03fARS72++eab5rTTTjMul8sMGTLEPPPMMyHzgUDAzJ8/33g8HuNyucwFF1xgKioqIlTt8fF6veaOO+4wBQUFJiEhwQwaNMj8+te/DvlHKVr7fPfdd4/43+a0adOMMUfX19dff22uueYak5KSYtLS0swNN9xgmpqaItDND/uhXnfv3v2931PvvvtucB+x0OuRHCl8REOvR9PnH/7wB3PyySebhIQEM2LECLNixYqQfXTH97HNmG89ghAAAKCbRd01HwAAILoRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgqf8PdBBXOWBRxK8AAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ - "import xarray as xr\n", - "import matplotlib.pyplot as plt\n", - "\n", - "output = xr.open_dataset(outputfile_name)\n", - "output = output['output_catboost'].to_numpy().squeeze()\n", - "plt.imshow(output)\n", - "\n", - "output.shape\n" + "Calculate the presto features and run the classifier on top" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "5b7bea33", - "metadata": {}, + "execution_count": 8, + "id": "8f71136c-1252-4786-8609-8bb995da7daf", + "metadata": { + "tags": [] + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-24051411052f466b911c92ea2d1e7b20': send 'start'\n", - "0:00:29 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:00:35 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:00:44 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:00:53 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:01:11 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:01:28 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:01:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:02:15 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:02:43 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:03:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:04:03 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:04:54 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:05:56 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:06:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:08:01 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:09:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:10:18 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:11:22 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:12:23 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:13:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:14:31 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:15:32 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:16:33 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:17:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:18:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:19:35 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:20:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:21:46 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:22:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:23:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:24:58 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:26:00 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:27:02 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:28:04 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:29:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:30:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:31:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "16:12:29 Job 'j-24051411052f466b911c92ea2d1e7b20': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", - "16:13:03 Job 'j-24051411052f466b911c92ea2d1e7b20': finished (progress 100%)\n" + "0:00:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': send 'start'\n", + "0:00:16 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", + "0:00:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", + "0:00:28 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", + "0:00:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", + "0:00:49 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", + "0:01:02 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", + "0:01:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", + "0:01:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:02:03 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:02:34 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:03:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:03:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:04:58 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:05:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:07:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:08:01 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:09:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:10:06 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:11:07 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:12:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:13:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:14:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:15:22 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:16:23 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:17:23 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:18:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:19:25 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:20:28 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:21:29 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:22:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:23:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:24:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:25:44 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:26:45 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:27:45 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:28:46 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:29:47 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:30:49 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:31:50 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:32:51 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:33:51 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:34:52 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:35:52 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:36:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:37:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:38:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:39:54 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:40:54 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:41:55 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:42:55 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:44:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:45:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:46:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:47:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:48:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:49:16 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:50:17 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:51:17 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:52:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:53:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:54:19 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:55:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:56:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:57:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:58:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "0:59:22 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:00:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:01:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:02:25 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:03:30 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:04:32 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:05:33 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:06:34 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:07:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:08:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:09:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:10:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:11:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:12:40 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:13:40 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:14:41 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:15:42 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:16:42 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:17:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:18:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:19:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:20:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:21:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:23:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:24:01 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:25:03 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:26:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:27:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:28:05 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:29:06 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:30:07 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:31:08 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:32:08 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:33:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:34:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", + "1:35:14 Job 'j-240517a75f8846a88725dcb3c5da55a5': finished (progress 100%)\n" ] }, { @@ -403,30 +436,26 @@ " }\n", " \n", " \n", - " \n", + " \n", " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 3, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from datetime import datetime\n", "\n", "current_datetime = datetime.now()\n", "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", - "\n", - "presto_list = [\"presto_\" + str(i) for i in range(1, 129)]\n", - "\n", + "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", "\n", - "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", + "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", "\n", "prediction = input_cube.apply_neighborhood(\n", " process=udf,\n", @@ -440,12 +469,87 @@ " ],\n", ")\n", "\n", - "prediction = prediction.rename_labels(dimension=\"bands\",target= presto_list)\n", + "prediction = prediction.rename_labels(dimension=\"bands\",target= [\"output_catboost\"])\n", "\n", "prediction.execute_batch(outputfile = outputfile_name,\n", " description='world cereal inference',\n", " job_options={'driver-memory': '4g',\n", - " 'executor-memoryOverhead':'8g'} )" + " 'executor-memoryOverhead':'8g'} )\n" + ] + }, + { + "cell_type": "markdown", + "id": "1f716b7a", + "metadata": {}, + "source": [ + "Fetch the output and visualise" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2cf64980", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(116, 144)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import xarray as xr\n", + "import matplotlib.pyplot as plt\n", + "\n", + "output = xr.open_dataset('2024_05_17_14_56_51_output_worldcereal.nc')\n", + "output = output['output_catboost'].to_numpy().squeeze()\n", + "plt.imshow(output)\n", + "\n", + "output.shape\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f18b1535", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Size: 134kB\n", + "[16704 values with dtype=float64]\n", + "Coordinates:\n", + " * t (t) datetime64[ns] 8B 1970-01-01\n", + " * x (x) float64 1kB 6.528e+05 6.528e+05 ... 6.542e+05 6.542e+05\n", + " * y (y) float64 928B 5.681e+06 5.681e+06 ... 5.68e+06 5.68e+06\n", + "Attributes:\n", + " long_name: presto_1\n", + " units: \n", + " grid_mapping: crs\n" + ] + } + ], + "source": [ + "presto_ft = xr.open_dataset('2024_05_17_14_00_16_output_presto.nc')\n", + "\n", + "print(presto_ft['presto_1'])\n" ] }, { diff --git a/minimal_wc_presto/dev_testing.py b/minimal_wc_presto/dev_testing.py new file mode 100644 index 00000000..d937f482 --- /dev/null +++ b/minimal_wc_presto/dev_testing.py @@ -0,0 +1,83 @@ +#%% +from pathlib import Path + +from pyproj import Transformer +import numpy as np + +import requests +import xarray as xr + + +#%% GET DEPENDENCIES + +# Generate absolute path for the dependencies folder +dependencies_dir = Path.cwd() / 'dependencies' +dependencies_dir.mkdir(exist_ok=True, parents=True) + +base_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference' +dependency_name = "wc_presto_onnx_dependencies.zip" + +# Download and extract the model file +modelfile_url = f"{base_url}/{dependency_name}" +#modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) +#shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + +#Add the model directory to system path if it's not already there +#abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) +#sys.path.append(abs_path) + +# Get Data +#url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc" +#filename = "belgium_good_2020-12-01_2021-11-30.nc" + +#with requests.get(url, stream=True) as r: +# r.raise_for_status() +# with open(filename, 'wb') as f: +# for chunk in r.iter_content(chunk_size=8192): +# f.write(chunk) + +#%% + +# Read the file into xarray +ds = xr.open_dataset('data/belgium_good_2020-12-01_2021-11-30.nc') + + +arr = ds.drop('crs').to_array(dim='bands') +orig_dims = list(arr.dims) +map_dims = arr.shape[2:] + +#%% Get Presto +from mvp_wc_presto.world_cereal_inference import get_presto_features + +#bands: 19, t: 12y, : 100x: 100y +data_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc' +# Fetch the data from the URL +response = requests.get(data_url) + +#10000,128 +presto_path = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" +features = get_presto_features(arr, presto_path) + +#10000, +from mvp_wc_presto.world_cereal_inference import classify_with_catboost + +CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx' +classification = classify_with_catboost(features, CATBOOST_PATH) + + + +#%%revert to xarray +import matplotlib.pyplot as plt + + + +transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) +longitudes, latitudes = transformer.transform(arr.x, arr.y) +classification = np.flip(classification.reshape(map_dims),axis = 0) +classification = np.expand_dims(np.expand_dims(classification, axis=0),axis = 0) +output = xr.DataArray(classification, dims=orig_dims) + +output = output.to_numpy().squeeze() +plt.imshow(output) + +output.shape \ No newline at end of file diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py index ed1640d8..26760d2e 100644 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -251,7 +251,7 @@ def _create_dataloader(self, eo:np.ndarray, dynamic_world:np.ndarray, months:np. return dl - def _create_presto_input( + def create_presto_input( cls, inarr: xr.DataArray, epsg: int = 4326 ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: @@ -320,7 +320,7 @@ def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFram def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> np.ndarray: - eo, dynamic_world, months, latlons, mask = self._create_presto_input(inarr, epsg) + eo, dynamic_world, months, latlons, mask = self.create_presto_input(inarr, epsg) dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) features = self._get_encodings(dl) diff --git a/minimal_wc_presto/test_aggregator.ipynb b/minimal_wc_presto/test_aggregator.ipynb index 2087151a..1ae4237e 100644 --- a/minimal_wc_presto/test_aggregator.ipynb +++ b/minimal_wc_presto/test_aggregator.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 3, "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", "metadata": { "tags": [] @@ -27,16 +27,13 @@ "source": [ "import openeo\n", "\n", - "#token for METEO\n", - "#connection_terra = openeo.connect(\"openeo.vito.be\").authenticate_oidc()\n", "\n", - "#token SENTINEL\n", "connection = openeo.connect(\"https://openeofed.dataspace.copernicus.eu/\").authenticate_oidc()" ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 5, "id": "5494c46d", "metadata": {}, "outputs": [ @@ -44,378 +41,39 @@ "name": "stderr", "output_type": "stream", "text": [ - "Preflight process graph validation raised: [InternalValidationFailure] Validation failed: BackendLookupFailureException(status_code=400, code='BackendLookupFailure', message=\"Collections across multiple backends ({'cdse', 'terrascope'}): {'COPERNICUS_30', 'SENTINEL2_L2A', 'SENTINEL1_GRD', 'AGERA5'}.\", id='r-24051536a816438ebace84c022cdb826')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'agg-pj-20240515-101812': send 'start'\n", - "0:01:09 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:01:19 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:01:31 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:01:42 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:01:57 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:02:15 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:02:34 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:03:01 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:03:29 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:04:03 Job 'agg-pj-20240515-101812': running (progress 0%)\n", - "0:04:46 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:05:45 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:06:47 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:08:01 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:09:10 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:10:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:11:26 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:12:31 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:13:37 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:14:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:15:44 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:16:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:17:53 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:19:13 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:20:27 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:21:33 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:22:37 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:23:43 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:24:46 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:25:50 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:26:55 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:28:02 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:29:09 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:30:15 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:31:36 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:32:42 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:33:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:34:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:36:05 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:37:10 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:38:24 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:39:29 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:40:34 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:41:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:42:53 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:44:02 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:45:06 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:46:10 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:47:13 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:48:17 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:49:21 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:50:28 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:51:34 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:52:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:53:44 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:54:50 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:55:55 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:56:58 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:58:23 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "0:59:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:00:59 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:02:15 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:03:20 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:04:30 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:05:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:06:43 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:07:46 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:09:00 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:10:05 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:11:08 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:12:21 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:13:36 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:14:50 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:15:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:16:58 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:18:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:19:20 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:20:39 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:21:47 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:22:57 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:24:00 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:25:03 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:26:09 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:27:17 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:28:27 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:29:32 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:30:35 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:31:41 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:32:48 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:33:54 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:34:59 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:36:08 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:37:15 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:38:22 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:39:28 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:40:44 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:41:55 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:43:01 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:44:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:45:25 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:46:33 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:47:40 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:48:53 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:50:01 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:51:14 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:52:20 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:53:25 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:54:29 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:55:35 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:56:38 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:57:43 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "1:58:43 Job 'agg-pj-20240515-101812': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", - "1:59:22 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "2:00:22 Job 'agg-pj-20240515-101812': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", - "2:00:59 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "2:02:03 Job 'agg-pj-20240515-101812': running (progress 25%)\n", - "2:03:03 Job 'agg-pj-20240515-101812': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", - "2:03:37 Job 'agg-pj-20240515-101812': running (progress 25%)\n" + "Preflight process graph validation raised: [InternalValidationFailure] Validation failed: BackendLookupFailureException(status_code=400, code='BackendLookupFailure', message=\"Collections across multiple backends ({'terrascope', 'cdse'}): {'SENTINEL2_L2A', 'AGERA5'}.\", id='r-2405172f25fa4f8bb7f69ca237bd5681')\n" ] }, - { - "ename": "OpenEoApiError", - "evalue": "[500] Internal: Server error: KazooTimeoutError('Connection time-out') (ref: r-240515e716d34d9b9e8f1481ece911f9)", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mOpenEoApiError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[22], line 47\u001b[0m\n\u001b[0;32m 18\u001b[0m input_cube \u001b[38;5;241m=\u001b[39m worldcereal_preprocessed_inputs(\n\u001b[0;32m 19\u001b[0m connection \u001b[38;5;241m=\u001b[39m connection,\n\u001b[0;32m 20\u001b[0m bbox \u001b[38;5;241m=\u001b[39m EXTENT,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 26\u001b[0m DEM_collection\u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCOPERNICUS_30\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 27\u001b[0m )\n\u001b[0;32m 29\u001b[0m \u001b[38;5;66;03m#agera5_cube = worldcereal_preprocessed_inputs(\u001b[39;00m\n\u001b[0;32m 30\u001b[0m \u001b[38;5;66;03m# connection = connection_terra,\u001b[39;00m\n\u001b[0;32m 31\u001b[0m \u001b[38;5;66;03m# bbox = EXTENT,\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 44\u001b[0m \u001b[38;5;66;03m# temporal_extent=[STARTDATE, ENDDATE],\u001b[39;00m\n\u001b[0;32m 45\u001b[0m \u001b[38;5;66;03m# )\u001b[39;00m\n\u001b[1;32m---> 47\u001b[0m \u001b[43minput_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtest.nc\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 48\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal data collection\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 49\u001b[0m \u001b[43m \u001b[49m\u001b[43mjob_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43msplit_strategy\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mcrossbackend\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:292\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 289\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m 290\u001b[0m \u001b[38;5;66;03m# TODO: also allow a hard time limit on this infinite poll loop?\u001b[39;00m\n\u001b[0;32m 291\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 292\u001b[0m job_info \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 293\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m requests\u001b[38;5;241m.\u001b[39mConnectionError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 294\u001b[0m soft_error(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConnection error while polling job status: \u001b[39m\u001b[38;5;132;01m{e}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(e\u001b[38;5;241m=\u001b[39me))\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:74\u001b[0m, in \u001b[0;36mBatchJob.describe\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 65\u001b[0m \u001b[38;5;129m@openeo_endpoint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mGET /jobs/\u001b[39m\u001b[38;5;132;01m{job_id}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 66\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdescribe\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mdict\u001b[39m:\n\u001b[0;32m 67\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 68\u001b[0m \u001b[38;5;124;03m Get detailed metadata about a submitted batch job\u001b[39;00m\n\u001b[0;32m 69\u001b[0m \u001b[38;5;124;03m (title, process graph, status, progress, ...).\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 72\u001b[0m \u001b[38;5;124;03m This method was previously called :py:meth:`describe_job`.\u001b[39;00m\n\u001b[0;32m 73\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m---> 74\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconnection\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m/jobs/\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mjob_id\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_status\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m200\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mjson()\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:221\u001b[0m, in \u001b[0;36mRestApiConnection.get\u001b[1;34m(self, path, stream, auth, **kwargs)\u001b[0m\n\u001b[0;32m 212\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget\u001b[39m(\u001b[38;5;28mself\u001b[39m, path: \u001b[38;5;28mstr\u001b[39m, stream: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m, auth: Optional[AuthBase] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Response:\n\u001b[0;32m 213\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 214\u001b[0m \u001b[38;5;124;03m Do GET request to REST API.\u001b[39;00m\n\u001b[0;32m 215\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 219\u001b[0m \u001b[38;5;124;03m :return: response: Response\u001b[39;00m\n\u001b[0;32m 220\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m--> 221\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mget\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mauth\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mauth\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:770\u001b[0m, in \u001b[0;36mConnection.request\u001b[1;34m(self, method, path, headers, auth, check_error, expected_status, **kwargs)\u001b[0m\n\u001b[0;32m 763\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m(Connection, \u001b[38;5;28mself\u001b[39m)\u001b[38;5;241m.\u001b[39mrequest(\n\u001b[0;32m 764\u001b[0m method\u001b[38;5;241m=\u001b[39mmethod, path\u001b[38;5;241m=\u001b[39mpath, headers\u001b[38;5;241m=\u001b[39mheaders, auth\u001b[38;5;241m=\u001b[39mauth,\n\u001b[0;32m 765\u001b[0m check_error\u001b[38;5;241m=\u001b[39mcheck_error, expected_status\u001b[38;5;241m=\u001b[39mexpected_status, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 766\u001b[0m )\n\u001b[0;32m 768\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 769\u001b[0m \u001b[38;5;66;03m# Initial request attempt\u001b[39;00m\n\u001b[1;32m--> 770\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 771\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m OpenEoApiError \u001b[38;5;28;01mas\u001b[39;00m api_exc:\n\u001b[0;32m 772\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m api_exc\u001b[38;5;241m.\u001b[39mhttp_status_code \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m401\u001b[39m, \u001b[38;5;241m403\u001b[39m} \u001b[38;5;129;01mand\u001b[39;00m api_exc\u001b[38;5;241m.\u001b[39mcode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTokenInvalid\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 773\u001b[0m \u001b[38;5;66;03m# Auth token expired: can we refresh?\u001b[39;00m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:763\u001b[0m, in \u001b[0;36mConnection.request.._request\u001b[1;34m()\u001b[0m\n\u001b[0;32m 762\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_request\u001b[39m():\n\u001b[1;32m--> 763\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mConnection\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 764\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mauth\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mauth\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 765\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheck_error\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcheck_error\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_status\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexpected_status\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 766\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:169\u001b[0m, in \u001b[0;36mRestApiConnection.request\u001b[1;34m(self, method, path, headers, auth, check_error, expected_status, **kwargs)\u001b[0m\n\u001b[0;32m 167\u001b[0m expected_status \u001b[38;5;241m=\u001b[39m ensure_list(expected_status) \u001b[38;5;28;01mif\u001b[39;00m expected_status \u001b[38;5;28;01melse\u001b[39;00m []\n\u001b[0;32m 168\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m check_error \u001b[38;5;129;01mand\u001b[39;00m status \u001b[38;5;241m>\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m400\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m status \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m expected_status:\n\u001b[1;32m--> 169\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_raise_api_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresp\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 170\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m expected_status \u001b[38;5;129;01mand\u001b[39;00m status \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m expected_status:\n\u001b[0;32m 171\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m OpenEoRestError(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mGot status code \u001b[39m\u001b[38;5;132;01m{s!r}\u001b[39;00m\u001b[38;5;124m for `\u001b[39m\u001b[38;5;132;01m{m}\u001b[39;00m\u001b[38;5;124m \u001b[39m\u001b[38;5;132;01m{p}\u001b[39;00m\u001b[38;5;124m` (expected \u001b[39m\u001b[38;5;132;01m{e!r}\u001b[39;00m\u001b[38;5;124m) with body \u001b[39m\u001b[38;5;132;01m{body}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[0;32m 172\u001b[0m m\u001b[38;5;241m=\u001b[39mmethod\u001b[38;5;241m.\u001b[39mupper(), p\u001b[38;5;241m=\u001b[39mpath, s\u001b[38;5;241m=\u001b[39mstatus, e\u001b[38;5;241m=\u001b[39mexpected_status, body\u001b[38;5;241m=\u001b[39mresp\u001b[38;5;241m.\u001b[39mtext)\n\u001b[0;32m 173\u001b[0m )\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:189\u001b[0m, in \u001b[0;36mRestApiConnection._raise_api_error\u001b[1;34m(self, response)\u001b[0m\n\u001b[0;32m 187\u001b[0m error_message \u001b[38;5;241m=\u001b[39m info\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmessage\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m error_code \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(error_code, \u001b[38;5;28mstr\u001b[39m) \u001b[38;5;129;01mand\u001b[39;00m error_message \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(error_message, \u001b[38;5;28mstr\u001b[39m):\n\u001b[1;32m--> 189\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m OpenEoApiError(\n\u001b[0;32m 190\u001b[0m http_status_code\u001b[38;5;241m=\u001b[39mstatus_code,\n\u001b[0;32m 191\u001b[0m code\u001b[38;5;241m=\u001b[39merror_code,\n\u001b[0;32m 192\u001b[0m message\u001b[38;5;241m=\u001b[39merror_message,\n\u001b[0;32m 193\u001b[0m \u001b[38;5;28mid\u001b[39m\u001b[38;5;241m=\u001b[39minfo\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mid\u001b[39m\u001b[38;5;124m\"\u001b[39m),\n\u001b[0;32m 194\u001b[0m url\u001b[38;5;241m=\u001b[39minfo\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124murl\u001b[39m\u001b[38;5;124m\"\u001b[39m),\n\u001b[0;32m 195\u001b[0m )\n\u001b[0;32m 197\u001b[0m \u001b[38;5;66;03m# Failed to parse it as a compliant openEO API error: show body as-is in the exception.\u001b[39;00m\n\u001b[0;32m 198\u001b[0m text \u001b[38;5;241m=\u001b[39m response\u001b[38;5;241m.\u001b[39mtext\n", - "\u001b[1;31mOpenEoApiError\u001b[0m: [500] Internal: Server error: KazooTimeoutError('Connection time-out') (ref: r-240515e716d34d9b9e8f1481ece911f9)" - ] - } - ], - "source": [ - "#Get desired data\n", - "from preprocessing import worldcereal_preprocessed_inputs\n", - "\n", - "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.19, 51.25, 5.21, 51.26]))\n", - "EXTENT['crs'] = \"EPSG:4326\"\n", - "\n", - "STARTDATE = '2020-11-01'\n", - "ENDDATE = '2021-10-31'\n", - "\n", - "# Set OpenEO classification UDF context based on settings\n", - "CONTEXT = {\n", - " \"startdate\": STARTDATE, # Required\n", - " \"enddate\": ENDDATE, # Required\n", - "}\n", - "\n", - "\n", - "\n", - "input_cube = worldcereal_preprocessed_inputs(\n", - " connection = connection,\n", - " bbox = EXTENT,\n", - " start = STARTDATE,\n", - " end = ENDDATE,\n", - " METEO_collection=\"AGERA5\",\n", - " S2_collection= \"SENTINEL2_L2A\",\n", - " S1_collection= \"SENTINEL1_GRD\",\n", - " DEM_collection= \"COPERNICUS_30\"\n", - ")\n", - "\n", - "#agera5_cube = worldcereal_preprocessed_inputs(\n", - "# connection = connection_terra,\n", - "# bbox = EXTENT,\n", - "# start = STARTDATE,\n", - "# end = ENDDATE,\n", - "# METEO_collection=\"AGERA5\",\n", - "# S2_collection= None,\n", - "# S1_collection= None,\n", - "# DEM_collection= None\n", - "#)\n", - "\n", - "#agera5_cube = connection_terra.load_collection(\n", - "# \"AGERA5\",\n", - "# spatial_extent=EXTENT,\n", - "# bands=[\"temperature-mean\", \"precipitation-flux\"],\n", - "# temporal_extent=[STARTDATE, ENDDATE],\n", - "# )\n", - "\n", - "input_cube.execute_batch(outputfile = 'test.nc',\n", - " description='world cereal data collection',\n", - " job_options={\"split_strategy\": \"crossbackend\"})\n" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "94969249", - "metadata": {}, - "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-24051575983946539c6694814f39164e': send 'start'\n", - "0:00:30 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", - "0:00:36 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", - "0:00:44 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", - "0:00:53 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", - "0:01:03 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", - "0:01:19 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", - "0:01:35 Job 'j-24051575983946539c6694814f39164e': queued (progress 0%)\n", - "0:01:55 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", - "0:02:19 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", - "0:02:52 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", - "0:03:29 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", - "0:04:16 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", - "0:05:24 Job 'j-24051575983946539c6694814f39164e': running (progress N/A)\n", - "0:06:24 Job 'j-24051575983946539c6694814f39164e': finished (progress 100%)\n" - ] - }, - { - "ename": "AttributeError", - "evalue": "'DataCube' object has no attribute 'load_stac'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[11], line 15\u001b[0m\n\u001b[0;32m 13\u001b[0m result_metadata \u001b[38;5;241m=\u001b[39m job\u001b[38;5;241m.\u001b[39mget_results()\n\u001b[0;32m 14\u001b[0m job_url, \u001b[38;5;241m=\u001b[39m [k[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhref\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;28;01mfor\u001b[39;00m k \u001b[38;5;129;01min\u001b[39;00m result_metadata\u001b[38;5;241m.\u001b[39mget_metadata()[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinks\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m k[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrel\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcanonical\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n\u001b[1;32m---> 15\u001b[0m load_stac_cube \u001b[38;5;241m=\u001b[39m \u001b[43ms2_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload_stac\u001b[49m(job_url)\n\u001b[0;32m 17\u001b[0m \u001b[38;5;66;03m#merge the cubes and download\u001b[39;00m\n\u001b[0;32m 18\u001b[0m input_cube \u001b[38;5;241m=\u001b[39m s2_cube\u001b[38;5;241m.\u001b[39mmerge_cubes(load_stac_cube)\n", - "\u001b[1;31mAttributeError\u001b[0m: 'DataCube' object has no attribute 'load_stac'" - ] - } - ], - "source": [ - "from pathlib import Path\n", - "\n", - "# download the agera 5 cube\n", - "result_dir = Path.cwd()\n", - "job = agera5_cube.create_job(\n", - " out_format=\"GTIFF\",\n", - ")\n", - "job.start_and_wait()\n", - "\n", - "job.get_results().download_files(result_dir)\n", - "\n", - "#create a STAC collection from th eobtained cube\n", - "result_metadata = job.get_results()\n", - "job_url, = [k[\"href\"] for k in result_metadata.get_metadata()[\"links\"] if k[\"rel\"] == \"canonical\"]\n", - "load_stac_cube = s2_cube.load_stac(job_url)\n", - "\n", - "#merge the cubes and download\n", - "input_cube = s2_cube.merge_cubes(load_stac_cube)\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "4aab5695", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Preflight process graph validation raised: [CollectionNotFound] Collection 'AGERA5' does not exist.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'j-2405155e702e4218aa9dfac9671faaff': send 'start'\n", - "0:00:16 Job 'j-2405155e702e4218aa9dfac9671faaff': created (progress 0%)\n", - "0:00:22 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:00:28 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:00:36 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:00:47 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:01:00 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:01:16 Job 'j-2405155e702e4218aa9dfac9671faaff': running (progress N/A)\n", - "0:01:35 Job 'j-2405155e702e4218aa9dfac9671faaff': error (progress N/A)\n", - "Your batch job 'j-2405155e702e4218aa9dfac9671faaff' failed. Error logs:\n", - "[{'id': '[1715756877175, 557437]', 'time': '2024-05-15T07:07:57.175Z', 'level': 'error', 'message': 'OpenEO batch job failed: CollectionNotFoundException(status_code=404, code=\\'CollectionNotFound\\', message=\"Collection \\'AGERA5\\' does not exist.\", id=\\'no-request\\')'}]\n", - "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-2405155e702e4218aa9dfac9671faaff').logs()`.\n" - ] - }, - { - "ename": "JobFailedException", - "evalue": "Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37).", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[11], line 6\u001b[0m\n\u001b[0;32m 3\u001b[0m formatted_datetime \u001b[38;5;241m=\u001b[39m current_datetime\u001b[38;5;241m.\u001b[39mstrftime(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mY_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mm_\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mH_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mM_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mS\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 4\u001b[0m outputfile_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(formatted_datetime) \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m_input_cube_worldCereal.nc\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m----> 6\u001b[0m \u001b[43minput_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43moutputfile_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 7\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal data collection\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", - "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", - "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-2405155e702e4218aa9dfac9671faaff' didn't finish successfully. Status: error (after 0:01:37)." - ] - } - ], - "source": [ - "from datetime import datetime\n", - "current_datetime = datetime.now()\n", - "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_input_cube_worldCereal.nc'\n", - "\n", - "input_cube.execute_batch(outputfile = outputfile_name,\n", - " description='world cereal data collection')" - ] - }, - { - "cell_type": "markdown", - "id": "48c9322c", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "8f71136c-1252-4786-8609-8bb995da7daf", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'j-240508de680a4a01bad4dfca194be16b': send 'start'\n", - "0:00:28 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", - "0:00:34 Job 'j-240508de680a4a01bad4dfca194be16b': created (progress 0%)\n", - "0:00:41 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:00:55 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:05 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:17 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:33 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:01:52 Job 'j-240508de680a4a01bad4dfca194be16b': queued (progress 0%)\n", - "0:02:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:02:52 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:03:29 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:04:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:05:15 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:06:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:07:16 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:08:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:09:17 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:10:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:11:18 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:12:19 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:13:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:14:21 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:15:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:16:22 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:17:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:18:23 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:19:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:20:24 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:21:25 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:22:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:23:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:24:26 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:25:34 Job 'j-240508de680a4a01bad4dfca194be16b': running (progress N/A)\n", - "0:26:34 Job 'j-240508de680a4a01bad4dfca194be16b': finished (progress 100%)\n" + "0:00:00 Job 'agg-pj-20240517-093353': send 'start'\n", + "0:00:38 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:00:44 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:00:51 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:01:00 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:01:11 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:01:25 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:01:43 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:02:03 Job 'agg-pj-20240517-093353': running (progress 0%)\n", + "0:02:34 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:03:07 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:03:46 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:04:34 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:05:34 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:06:36 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:07:38 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:08:44 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:09:45 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:10:47 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:11:48 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:12:49 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:13:49 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:14:51 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:15:52 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:16:53 Job 'agg-pj-20240517-093353': running (progress 50%)\n", + "0:17:54 Job 'agg-pj-20240517-093353': finished (progress 100%)\n" ] }, { @@ -437,143 +95,85 @@ " }\n", " \n", " \n", - " \n", + " \n", " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 3, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\n", - "current_datetime = datetime.now()\n", - "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", + "#Get desired data\n", + "\n", + "\n", + "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.19, 51.25, 5.20, 51.26]))\n", + "EXTENT['crs'] = \"EPSG:4326\"\n", + "\n", + "STARTDATE = '2020-11-01'\n", + "ENDDATE = '2020-12-31'\n", + "\n", + "# Set OpenEO classification UDF context based on settings\n", + "CONTEXT = {\n", + " \"startdate\": STARTDATE, # Required\n", + " \"enddate\": ENDDATE, # Required\n", + "}\n", "\n", - "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", + "input_cube1 = connection.load_collection(\n", + " collection_id = \"SENTINEL2_L2A\",\n", + " spatial_extent=EXTENT,\n", + " bands = [\"B02\", \"B03\"],\n", + " temporal_extent=[STARTDATE, ENDDATE],\n", + " )\n", "\n", - "prediction = input_cube.apply_neighborhood(\n", - " process=udf,\n", - " size=[\n", - " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", - " ],\n", - " overlap=[\n", - " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", - " ],\n", - ")\n", + "input_cube2 = connection.load_collection(\n", + " collection_id = \"AGERA5\",\n", + " spatial_extent=EXTENT,\n", + " bands=[\"temperature-mean\", \"precipitation-flux\"],\n", + " temporal_extent=[STARTDATE, ENDDATE],\n", + " )\n", "\n", - "prediction = prediction.rename_labels(dimension=\"bands\",target= [\"output_catboost\"])\n", + "cube = input_cube1.merge_cubes(input_cube2)\n", "\n", - "prediction.execute_batch(outputfile = outputfile_name,\n", - " description='world cereal inference',\n", - " job_options={'driver-memory': '4g',\n", - " 'executor-memoryOverhead':'8g'} )\n" + "cube.execute_batch(outputfile = 'test.nc',\n", + " description='world cereal data collection',\n", + " job_options={\"split_strategy\": \"crossbackend\"})\n" ] }, { - "cell_type": "code", - "execution_count": 4, - "id": "2cf64980", + "cell_type": "markdown", + "id": "48c9322c", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(126, 166)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGhCAYAAADBddZJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAw+ElEQVR4nO3de3SUVZ7u8acuSeVeuUAqCSQQFBtERATECGfUMd2IHkVFbRxUWl0y2qACfRSZbnC0VdTpVgYvoK4eW0+D2s4oKn3ExqCgY7gloCIYgiIEQhI0JJV7KlX7/OF0tSWoXCpvpSrfz1rvWmTvXW9+v7WweHyvNmOMEQAAgEXskS4AAAD0LoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGCpiIaPJ598UgMHDlRCQoLGjh2rjRs3RrIcAABggYiFj5dffllz5szRPffco/Lyco0YMUITJkxQXV1dpEoCAAAWsEXqxXJjx47VmDFj9MQTT0iSAoGA8vPzddttt+nuu+/+wc8GAgFVV1crNTVVNpvNinIBAMAPMMaoqalJeXl5stt/+NiG06KaQnR2dqqsrEzz5s0LjtntdhUXF6u0tPSw9R0dHero6Aj+vH//fp166qmW1AoAAI5eVVWV+vfv/4NrIhI+vvrqK/n9fnk8npBxj8ejzz777LD1Cxcu1L333nvY+HhdJKfiuq1OAABwdLrk0wf6f0pNTf3RtREJH8dq3rx5mjNnTvBnr9er/Px8ORUnp43wAQBAxP3PRRxHczlERMJHnz595HA4VFtbGzJeW1urnJycw9a7XC65XC6rygMAAN0oIne7xMfHa9SoUSopKQmOBQIBlZSUqKioKBIlAQAAi0TstMucOXM0bdo0jR49WmeddZYWLVqklpYW3XDDDZEqCQAAWCBi4ePnP/+5Dh48qAULFqimpkZnnHGGVq1addhFqAAAILZE7DkfJ8Lr9crtdus8TeKCUwAAeoAu49N7el2NjY1KS0v7wbW82wUAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALBU2MPHwoULNWbMGKWmpio7O1uXXXaZKioqQta0t7drxowZysrKUkpKiiZPnqza2tpwlwIAAHqgsIePtWvXasaMGVq/fr1Wr14tn8+nn/3sZ2ppaQmumT17tt5880298sorWrt2raqrq3XFFVeEuxQAANAD2Ywxpjt/wcGDB5Wdna21a9fqH/7hH9TY2Ki+fftq+fLluvLKKyVJn332mYYOHarS0lKdffbZP7pPr9crt9ut8zRJTltcd5YPAACOQpfx6T29rsbGRqWlpf3g2m6/5qOxsVGSlJmZKUkqKyuTz+dTcXFxcM2QIUNUUFCg0tLSI+6jo6NDXq83ZAMAANGpW8NHIBDQrFmzNG7cOJ122mmSpJqaGsXHxys9PT1krcfjUU1NzRH3s3DhQrnd7uCWn5/fnWUDAIBu1K3hY8aMGdq2bZteeumlE9rPvHnz1NjYGNyqqqrCVCEAALCas7t2PHPmTK1cuVLr1q1T//79g+M5OTnq7OxUQ0NDyNGP2tpa5eTkHHFfLpdLLperu0oFAAAWCvuRD2OMZs6cqddee01r1qxRYWFhyPyoUaMUFxenkpKS4FhFRYX27t2roqKicJcDAAB6mLAf+ZgxY4aWL1+u119/XampqcHrONxutxITE+V2u3XTTTdpzpw5yszMVFpamm677TYVFRUd1Z0uAAAguoU9fCxZskSSdN5554WMP/fcc/rFL34hSXrsscdkt9s1efJkdXR0aMKECXrqqafCXQoAAOiBuv05H92B53wAANCz9KjnfAAAAHwb4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEA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9uzgka1Y6fO7jqavnJycwy6G7+rqUn19fVT2/rfgsWfPHq1evTrk1eux0uv777+vuro6FRQUBL+j9uzZo1/96lcaOHCgpNjotU+fPnI6nT/6HdUd38dRFz7i4+M1atQolZSUBMcCgYBKSkpUVFQUwcpOjDFGM2fO1GuvvaY1a9aosLAwZH7UqFGKi4sL6buiokJ79+6Nur4vuOACffLJJ9q6dWtwGz16tKZOnRr8c6z0Om7cuMNumd65c6cGDBggSSosLFROTk5Ir16vVxs2bIiqXltbW2W3h36dOByO4P9ZxUqf33U0fRUVFamhoUFlZWXBNWvWrFEgENDYsWMtr/lE/C14VFZW6p133lFWVlbIfKz0et111+njjz8O+Y7Ky8vTnXfeqbfffltSbPQaHx+vMWPG/OB3VLf923Pcl6pG0EsvvWRcLpf54x//aLZv326mT59u0tPTTU1NTaRLO2633nqrcbvd5r333jMHDhwIbq2trcE1t9xyiykoKDBr1qwxmzdvNkVFRaaoqCiCVYfPt+92MSZ2et24caNxOp3mgQceMJWVlWbZsmUmKSnJ/OlPfwqueeihh0x6erp5/fXXzccff2wmTZpkCgsLTVtbWwQrPzbTpk0z/fr1MytXrjS7d+82r776qunTp4+56667gmuitc+mpiazZcsWs2XLFiPJPProo2bLli3BOzyOpq8LL7zQjBw50mzYsMF88MEHZvDgweaaa66JVEvf64d67ezsNJdeeqnp37+/2bp1a8j3VEdHR3AfsdDrkXz3bhdjoqPXH+vz1VdfNXFxceaZZ54xlZWV5vHHHzcOh8O8//77wX10x/dxVIYPY4x5/PHHTUFBgYmPjzdnnXWWWb9+faRLOiGSjrg999xzwTVtbW3ml7/8pcnIyDBJSUnm8ssvNwcOHIhc0WH03fARS72++eab5rTTTjMul8sMGTLEPPPMMyHzgUDAzJ8/33g8HuNyucwFF1xgKioqIlTt8fF6veaOO+4wBQUFJiEhwQwaNMj8+te/DvlHKVr7fPfdd4/43+a0adOMMUfX19dff22uueYak5KSYtLS0swNN9xgmpqaItDND/uhXnfv3v2931PvvvtucB+x0OuRHCl8REOvR9PnH/7wB3PyySebhIQEM2LECLNixYqQfXTH97HNmG89ghAAAKCbRd01HwAAILoRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgqf8PdBBXOWBRxK8AAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ - "import xarray as xr\n", - "import matplotlib.pyplot as plt\n", - "\n", - "output = xr.open_dataset(outputfile_name)\n", - "output = output['output_catboost'].to_numpy().squeeze()\n", - "plt.imshow(output)\n", - "\n", - "output.shape\n" + "### Check reference" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "5b7bea33", - "metadata": {}, + "execution_count": 6, + "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", + "metadata": { + "tags": [] + }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'j-24051411052f466b911c92ea2d1e7b20': send 'start'\n", - "0:00:29 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:00:35 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:00:44 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:00:53 Job 'j-24051411052f466b911c92ea2d1e7b20': created (progress 0%)\n", - "0:01:11 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:01:28 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:01:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:02:15 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:02:43 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:03:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:04:03 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:04:54 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:05:56 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:06:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:08:01 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:09:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:10:18 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:11:22 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:12:23 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:13:24 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:14:31 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:15:32 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:16:33 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:17:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:18:34 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:19:35 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:20:44 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:21:46 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:22:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:23:57 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:24:58 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:26:00 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:27:02 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:28:04 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:29:05 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:30:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "0:31:06 Job 'j-24051411052f466b911c92ea2d1e7b20': running (progress N/A)\n", - "16:12:29 Job 'j-24051411052f466b911c92ea2d1e7b20': Connection error while polling job status: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))\n", - "16:13:03 Job 'j-24051411052f466b911c92ea2d1e7b20': finished (progress 100%)\n" - ] - }, { "data": { "text/html": [ "\n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 3, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from datetime import datetime\n", - "\n", - "current_datetime = datetime.now()\n", - "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", - "\n", - "presto_list = [\"presto_\" + str(i) for i in range(1, 129)]\n", - "\n", - "\n", - "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", - "\n", - "prediction = input_cube.apply_neighborhood(\n", - " process=udf,\n", - " size=[\n", - " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", - " ],\n", - " overlap=[\n", - " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", - " ],\n", - ")\n", - "\n", - "prediction = prediction.rename_labels(dimension=\"bands\",target= presto_list)\n", - "\n", - "prediction.execute_batch(outputfile = outputfile_name,\n", - " description='world cereal inference',\n", - " job_options={'driver-memory': '4g',\n", - " 'executor-memoryOverhead':'8g'} )" - ] - }, - { - "cell_type": "markdown", - "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", - "metadata": {}, - "source": [ - "### Check reference" + "cube" ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAApcAAAKWCAYAAAAcMLI0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAlhElEQVR4nO3dfYyV9Znw8WtgYKDIDIhhBlZox0aDrbS+gBQxu00lZRuzkUq6a0I3bNvUXQtWNKmFbrHZbRWx29aAVqvpos1q3ZqsfbGpxtCWxBYRcenq6oIJPnW2dMZtInOolgGZ+/nDZ8/jKEM5M9fMeZnPJzmJnDlz5jfehzPf/IbrvpuKoigCAAASjKv2AgAAaBziEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTNI/XEt99+e3z1q1+N7u7ueP/73x9btmyJCy+88I9+Xn9/fxw4cCCmTp0aTU1NI7U8AABOUlEUcejQoZg9e3aMG/dH9iaLEfDAAw8UEydOLP75n/+5+M///M/i05/+dDFt2rSip6fnj35uV1dXERFubm5ubm5ubm41duvq6vqjLddUFEURyRYtWhQLFy6M2267LSLe2I2cM2dOXH311bFu3boTfm5vb29MmzYtfv30u6L1FL+1b3QfPWv+iH+Nh/Y9U7WvDcDYMtjPnHpX+n1/vPP8/xMHDx6Mtra2Ez42/dfiR44cid27d8f69evL940bNy6WLl0aO3bseNvj+/r6oq+vr/znQ4cORURE6ynjonWquGx0zU0TRvxrDPY6Go2vDcDY0ujtcjL/ZDH9/8Dvfve7OHbsWLS3tw+4v729Pbq7u9/2+I0bN0ZbW1v5NmfOnOwlAQAwSqqe1+vXr4/e3t7yraurq9pLAgBgiNJ/LX7aaafF+PHjo6enZ8D9PT090dHR8bbHt7S0REtLS/YyAACogvS4nDhxYlxwwQWxbdu2WL58eUS8MdCzbdu2WLNmTfaXo849emDPce9fNvvcij8n62tX6kRrBWBsadSfCa8XRyNi/0k9dkTOc3ndddfFqlWrYsGCBXHhhRfGrbfeGq+++mp84hOfGIkvBwBAjRiRuPyrv/qr+J//+Z+44YYboru7O84999x45JFH3jbkAwBAYxmxK/SsWbPGr8EBAMaYqk+LAwDQOMQlAABpRuzX4jBSBpvEy5r+rtSJvm6jTg0CwGDsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQxrQ4dadaU+FDMZRrpwNAPbNzCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBqnIqImneh0Q4OdxqeeTlEEAI3KziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLQxVkTbYPNjkPANVi5xIAgDTiEgCANOISAIA04hIAgDTiEgCANKbFqTuDTVo38jXHTYUDUC/sXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQxrQ41IFKJ+QBoFrsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQxrQ4DaMRriFeqaF8zybMARhJdi4BAEgjLgEASCMuAQBIIy4BAEgjLgEASGNanIYx2BT0WJwiB4BqsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGqcigjFmsFMzDXYqJwCohJ1LAADSiEsAANKISwAA0ohLAADSiEsAANKYFgciovIpclPnAByPnUsAANKISwAA0ohLAADSiEsAANKISwAA0pgWByLClDcAOexcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJDGtcVpeINdM/vRA3tGdR21zv8PADLYuQQAII24BAAgjbgEACCNuAQAII24BAAgjWlxGp4p6OEZbNoeAI7HziUAAGnEJQAAacQlAABpxCUAAGnEJQAAacQlAABpnIqIhjfYqXScogiARjHSP9NKh/pj+lkn91g7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OY8xg0/MA1K8TvbeP9tlR7FwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkKZmp8U/etb8aG6acFKPdY3oxjOUqTevA6AWDeW9yVkdyFTp62m4P0/tXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQpqkoiqLai3izUqkUbW1t8cq+M6J16vDadyjTdiaOa99oT73VI5OmUN8Ge9/yd7vxVHqsh/LayPg5WDrUH9PP2h+9vb3R2tp6wsfauQQAII24BAAgjbgEACCNuAQAIE1Fcblx48ZYuHBhTJ06NWbOnBnLly+PvXv3DnjM4cOHY/Xq1TFjxow45ZRTYsWKFdHT05O6aAAAalNF0+J//ud/HldccUUsXLgwXn/99fjCF74Qzz77bDz33HMxZcqUiIi46qqr4sc//nHcc8890dbWFmvWrIlx48bFL37xi5P6GqM1LZ41nQXVZHIUxhZT5IxGjxzv9fR6cTR+Hj84qWnx5kq+2COPPDLgz/fcc0/MnDkzdu/eHX/6p38avb298e1vfzvuv//++NCHPhQREVu3bo2zzz47nnjiifjABz5QyZcDAKDODGtrsLe3NyIiTj311IiI2L17dxw9ejSWLl1afsy8efNi7ty5sWPHjuF8KQAA6kBFO5dv1t/fH2vXro0lS5bEOeecExER3d3dMXHixJg2bdqAx7a3t0d3d/dxn6evry/6+vrKfy6VSkNdEgAAVTbkncvVq1fHs88+Gw888MCwFrBx48Zoa2sr3+bMmTOs5wMAoHqGFJdr1qyJhx9+OH72s5/F6aefXr6/o6Mjjhw5EgcPHhzw+J6enujo6Djuc61fvz56e3vLt66urqEsCQCAGlDRr8WLooirr746Hnroofj5z38enZ2dAz5+wQUXxIQJE2Lbtm2xYsWKiIjYu3dvvPTSS7F48eLjPmdLS0u0tLQMcfknNpSJKlPhANSqkZ4KP9HPwKyvXYs/ZzOv4z3S6uHMABXF5erVq+P++++PH/zgBzF16tTyv6Nsa2uLyZMnR1tbW3zqU5+K6667Lk499dRobW2Nq6++OhYvXmxSHABgDKgoLu+4446IiPjgBz844P6tW7fG3/zN30RExDe+8Y0YN25crFixIvr6+mLZsmXxzW9+M2WxAADUtop/Lf7HTJo0KW6//fa4/fbbh7woAADqk2uLAwCQRlwCAJBGXAIAkGbIV+jhDZmnL6jF0zPUokr/n9eTejjFBNC4Mt+D6uk9udK1Zn5vjfi+b+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxYepmhNmjawRp+cAat1QfkZ5vx6eWuyC4R5TO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEt3oAa4XrnJ/q6jTCZ2AjfA0BEbU47MzzHO6alQ/0x/ayT+3w7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS3egKp5vfNGmIJuhO8BgLer9GwqDI2dSwAA0ohLAADSiEsAANKISwAA0ohLAADSNBVFUVR7EW9WKpWira0tXtl3RrRO1b6NInMCu1pTfabIgUYx2Pvoid7nTFSPbW9cW3x/9Pb2Rmtr6wkfq94AAEgjLgEASCMuAQBIIy4BAEgjLgEASOPa4oyKSqcMR2My2/Q3MFZ5/2Mk2bkEACCNuAQAII24BAAgjbgEACCNuAQAII1pcUaF6W8AGBvsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJDGqYhoGI8e2HPc+52iCABGj51LAADSiEsAANKISwAA0ohLAADSiEsAANKYFqcmDTb5fSKmwgGg+uxcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJDGtDg16UST30OZJAdgeAZ7X/aezFvZuQQAII24BAAgjbgEACCNuAQAII24BAAgjWlxapLpQ4Da4n25ttXSWVbsXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQpqkoiqLai3izUqkUbW1t8cq+M6J1qvZtdCeabgOgfpkubyylQ/0x/az90dvbG62trSd8rHoDACCNuAQAII24BAAgjbgEACCNuAQAII1rizMqBpsKH2ya0BQ5ANQnO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApBGXAACkcSoiUlV6CiGnHAKAxmLnEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTiEgCANOISAIA04hIAgDTiEgCANOISAIA0ri1eB0b6+tuPHthT8ee4JjgAcDx2LgEASCMuAQBIIy4BAEgjLgEASCMuAQBIY1qcIRlswtwUOQARg/88GMoZSrJU+jOqmmutZ3YuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhTs9PiHz1rfjQ3TRjWc9TblFe1Jq1NeAMwFtRbF9QrO5cAAKQRlwAApBGXAACkEZcAAKQZVlzefPPN0dTUFGvXri3fd/jw4Vi9enXMmDEjTjnllFixYkX09PQMd50AANSBIU+L79q1K771rW/F+973vgH3X3vttfHjH/84HnzwwWhra4s1a9bE5ZdfHr/4xS+GvdhKDWUKutJrZlc6eWYyG4CxrBavOU6uIe1c/v73v4+VK1fG3XffHdOnTy/f39vbG9/+9rfj61//enzoQx+KCy64ILZu3Rq//OUv44knnkhbNAAAtWlIcbl69eq49NJLY+nSpQPu3717dxw9enTA/fPmzYu5c+fGjh07hrdSAABqXsW/Fn/ggQfi6aefjl27dr3tY93d3TFx4sSYNm3agPvb29uju7v7uM/X19cXfX195T+XSqVKlwQAQI2oaOeyq6srrrnmmrjvvvti0qRJKQvYuHFjtLW1lW9z5sxJeV4AAEZfRXG5e/fuePnll+P888+P5ubmaG5uju3bt8fmzZujubk52tvb48iRI3Hw4MEBn9fT0xMdHR3Hfc7169dHb29v+dbV1TXkbwYAgOqq6Nfil1xySTzzzDMD7vvEJz4R8+bNi89//vMxZ86cmDBhQmzbti1WrFgRERF79+6Nl156KRYvXnzc52xpaYmWlpYhLh8AgFpSUVxOnTo1zjnnnAH3TZkyJWbMmFG+/1Of+lRcd911ceqpp0Zra2tcffXVsXjx4vjABz6Qt+oRVOmpgpxaCADezqmFxq4hn+dyMN/4xjdi3LhxsWLFiujr64tly5bFN7/5zewvAwBADWoqiqKo9iLerFQqRVtbW3wwLovmpgnVXg4AMAR2LhtL6VB/TD9rf/T29kZra+sJH+va4gAApBGXAACkEZcAAKRJH+gBYGw40b+pcyaNscO/reSt7FwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkMa0OABDYiJ8bDEVzsmycwkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGtPiAEBEmAgnh51LAADSiEsAANKISwAA0ohLAADSiEsAANKYFgeAMcZUOCPJziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLA8AYs2z2uce93xQ5GexcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkGbMnoposNMtDHZ6BgAA/jg7lwAApBGXAACkEZcAAKQRlwAApBGXAACkGbPT4qbCAWh0zoxCNdi5BAAgjbgEACCNuAQAII24BAAgjbgEACDNmJ0WB4BGMdhUeNbjoRJ2LgEASCMuAQBIIy4BAEgjLgEASCMuAQBIY1qcIXG9WgDgeOxcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJCmZqfFH9r3TLROHdi+JpFrh2MBMPpcE5x6YOcSAIA04hIAgDTiEgCANOISAIA04hIAgDQ1Oy0OAGOVqXDqmZ1LAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0jRXewEAMFY9emBPtZcA6excAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJCmrq4tXovXYF02+9xqL4ExYLDXvtcf1L5a/NkFI8nOJQAAacQlAABpxCUAAGnEJQAAacQlAABp6mpavBZVOgVouhcAaGR2LgEASCMuAQBIIy4BAEgjLgEASCMuAQBIY1oc6oCzDED9GsrfX9cjp57ZuQQAII24BAAgjbgEACCNuAQAII24BAAgTcVx+Zvf/CY+/vGPx4wZM2Ly5Mkxf/78eOqpp8ofL4oibrjhhpg1a1ZMnjw5li5dGi+88ELqogEAqE0VxeUrr7wSS5YsiQkTJsRPfvKTeO655+JrX/taTJ8+vfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtF5Ljdt2hRz5syJrVu3lu/r7Ows/3dRFHHrrbfGF7/4xbjssssiIuI73/lOtLe3x/e///244oorkpYNAEAtqmjn8oc//GEsWLAgPvaxj8XMmTPjvPPOi7vvvrv88RdffDG6u7tj6dKl5fva2tpi0aJFsWPHjuM+Z19fX5RKpQE3AADqU0VxuX///rjjjjvizDPPjEcffTSuuuqq+OxnPxv33ntvRER0d3dHRER7e/uAz2tvby9/7K02btwYbW1t5ducOXOG8n0AAFADKorL/v7+OP/88+Omm26K8847L6688sr49Kc/HXfeeeeQF7B+/fro7e0t37q6uob8XAAAVFdFcTlr1qx4z3veM+C+s88+O1566aWIiOjo6IiIiJ6engGP6enpKX/srVpaWqK1tXXADQCA+lRRXC5ZsiT27t074L59+/bFO9/5zoh4Y7ino6Mjtm3bVv54qVSKnTt3xuLFixOWCwBALatoWvzaa6+Niy66KG666ab4y7/8y3jyySfjrrvuirvuuisiIpqammLt2rXxla98Jc4888zo7OyMDRs2xOzZs2P58uUjsX4AAGpIRXG5cOHCeOihh2L9+vXxj//4j9HZ2Rm33nprrFy5svyY66+/Pl599dW48sor4+DBg3HxxRfHI488EpMmTUpfPAAAtaWpKIqi2ot4s1KpFG1tbfHKvjOidWrjXZ1y2exzq70EAGrcowf2VHsJMEDpUH9MP2t/9Pb2/tH5mMarNwAAqkZcAgCQpqJ/c8nIqvTXIH7FDgDUGjuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLT7KnBgXAGhkdi4BAEgjLgEASCMuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEgjLgEASOPa4nVg2exzq70EAEZRpe/7jx7YMyLrgKGwcwkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAapyICgDo32KmLnKKIarBzCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAa0+JAwxvKxOxg07dQT0bjdWwinbeycwkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGtPiAMcx2ARspddwNnVOo3Ndc97KziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAw1jNKZTK/0aWWs60dS5SXUqfZ2Nxmujmq8/k+rDc7xj93pxNCL2n9Tn27kEACCNuAQAII24BAAgjbgEACCNuAQAII1pcaDuZF33u54M5XuoxQlihifrtTyU56mn10cjvxfUAzuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLV5D6mkSD2g8ozFJ633u5NTiVHO11pT5mqnF118tHuvhsnMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGqciAhrGYKf0GOz0I414CpBal/X/vBZPKQNDUelrOfN9a6T+Htm5BAAgjbgEACCNuAQAII24BAAgjbgEACCNaXGg4ZkKbzyjcUxNpNeGepiOHk318D3YuQQAII24BAAgjbgEACCNuAQAII24BAAgjWnxUVYPU14AVH6temqfa9uPDjuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLQ7UncEmNV1DnNFQ6RS512XjafRjOtxpeDuXAACkEZcAAKQRlwAApBGXAACkEZcAAKQxLQ4Ax1HpxGyjTxAzdhzvtVw61B/Tzzq5z7dzCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAa0+IAcBymv2Fo7FwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQRlwCAJBGXAIAkEZcAgCQprnaC2hUy2afW+0lwJgz2N+7Rw/sGdV1AIxldi4BAEgjLgEASCMuAQBIIy4BAEhTUVweO3YsNmzYEJ2dnTF58uR497vfHV/+8pejKIryY4qiiBtuuCFmzZoVkydPjqVLl8YLL7yQvnAAAGpPRXG5adOmuOOOO+K2226L559/PjZt2hS33HJLbNmypfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtGpiH75y1/GZZddFpdeemlERLzrXe+K7373u/Hkk09GxBu7lrfeemt88YtfjMsuuywiIr7zne9Ee3t7fP/7348rrrgiefkAANSSinYuL7rooti2bVvs27cvIiJ+9atfxeOPPx4f+chHIiLixRdfjO7u7li6dGn5c9ra2mLRokWxY8eO4z5nX19flEqlATcAAOpTRTuX69ati1KpFPPmzYvx48fHsWPH4sYbb4yVK1dGRER3d3dERLS3tw/4vPb29vLH3mrjxo3xD//wD0NZOwAANaaincvvfe97cd9998X9998fTz/9dNx7773xT//0T3HvvfcOeQHr16+P3t7e8q2rq2vIzwUAQHVVtHP5uc99LtatW1f+t5Pz58+PX//617Fx48ZYtWpVdHR0RERET09PzJo1q/x5PT09ce655x73OVtaWqKlpWWIywcAoJZUFJevvfZajBs3cLNz/Pjx0d/fHxERnZ2d0dHREdu2bSvHZKlUip07d8ZVV12Vs+Ia4xriAAD/X0Vx+Rd/8Rdx4403xty5c+O9731v/Pu//3t8/etfj09+8pMREdHU1BRr166Nr3zlK3HmmWdGZ2dnbNiwIWbPnh3Lly8fifUDAFBDKorLLVu2xIYNG+Izn/lMvPzyyzF79uz427/927jhhhvKj7n++uvj1VdfjSuvvDIOHjwYF198cTzyyCMxadKk9MUDAFBbmoo3X16nBpRKpWhra4tX9p0RrVNr/+qUfi0Ote/RA3uqvQSAulY61B/Tz9ofvb290draesLH1n69AQBQN8QlAABpKvo3lwC1zK+/AarPziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAwBQdrwLxLxeHI2I/Sf1+XYuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhjWvwtjjchFeGaxQAAJ8POJQAAacQlAABpxCUAAGnEJQAAacQlAABpxCUAAGmciugkDXaKIqB2OJUYwMmr5D2zdKg/pp91cs9r5xIAgDTiEgCANOISAIA04hIAgDTiEgCANDU7Lf7Rs+ZHc9OEAfeZ+ASGYihne/B+AzSK0T7jjZ1LAADSiEsAANKISwAA0ohLAADSiEsAANLU7LT48bi+NzBaqvV+U80pdddmh7FlpP5u27kEACCNuAQAII24BAAgjbgEACCNuAQAIE1dTYsDNLrRmFIfbEJ0NKbCK/3+TKrD8I323yM7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OMMY0+kQ6NLLB/v7W0t8tO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEtDkC6ephoBUaGnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSOBURAOmccgiGp55P52XnEgCANOISAIA04hIAgDTiEgCANOISAIA0psUBGJJ6mFqFWjfYVHg9s3MJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gAAI6wRp8IHY+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxQEAkmRNhT96YE/K81SDnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ri2OABABbKuHx5R39cQH4ydSwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANI4FREAQ+J0LDQ6r/GhsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gBU3WBTuWNpwpbqyZwKx84lAACJxCUAAGnEJQAAacQlAABpxCUAAGlMiwNQs6o5xWtSvX553VSXnUsAANKISwAA0ohLAADSiEsAANLU3EBPURQREfF6HI0oqrwYAMas0qH+ai+BIXq9OFq1r92or5vS79/4vv63006kqTiZR42i//7v/445c+ZUexkAALxFV1dXnH766Sd8TM3FZX9/fxw4cCCmTp0ahw4dijlz5kRXV1e0trZWe2mMsFKp5HiPEY712OFYjx2OdWMriiIOHToUs2fPjnHjTvyvKmvu1+Ljxo0rF3FTU1NERLS2tnqhjiGO99jhWI8djvXY4Vg3rra2tpN6nIEeAADSiEsAANLUdFy2tLTEl770pWhpaan2UhgFjvfY4ViPHY712OFY879qbqAHAID6VdM7lwAA1BdxCQBAGnEJAEAacQkAQJqajsvbb7893vWud8WkSZNi0aJF8eSTT1Z7SQzTxo0bY+HChTF16tSYOXNmLF++PPbu3TvgMYcPH47Vq1fHjBkz4pRTTokVK1ZET09PlVZMlptvvjmamppi7dq15fsc68bxm9/8Jj7+8Y/HjBkzYvLkyTF//vx46qmnyh8viiJuuOGGmDVrVkyePDmWLl0aL7zwQhVXzFAcO3YsNmzYEJ2dnTF58uR497vfHV/+8pcHXG/asaZm4/Jf//Vf47rrrosvfelL8fTTT8f73//+WLZsWbz88svVXhrDsH379li9enU88cQT8dhjj8XRo0fjwx/+cLz66qvlx1x77bXxox/9KB588MHYvn17HDhwIC6//PIqrprh2rVrV3zrW9+K973vfQPud6wbwyuvvBJLliyJCRMmxE9+8pN47rnn4mtf+1pMnz69/JhbbrklNm/eHHfeeWfs3LkzpkyZEsuWLYvDhw9XceVUatOmTXHHHXfEbbfdFs8//3xs2rQpbrnlltiyZUv5MY41UdSoCy+8sFi9enX5z8eOHStmz55dbNy4sYqrItvLL79cRESxffv2oiiK4uDBg8WECROKBx98sPyY559/voiIYseOHdVaJsNw6NCh4swzzywee+yx4s/+7M+Ka665pigKx7qRfP7zny8uvvjiQT/e399fdHR0FF/96lfL9x08eLBoaWkpvvvd747GEkly6aWXFp/85CcH3Hf55ZcXK1euLIrCseYNNblzeeTIkdi9e3csXbq0fN+4ceNi6dKlsWPHjiqujGy9vb0REXHqqadGRMTu3bvj6NGjA479vHnzYu7cuY59nVq9enVceumlA45phGPdSH74wx/GggUL4mMf+1jMnDkzzjvvvLj77rvLH3/xxReju7t7wLFua2uLRYsWOdZ15qKLLopt27bFvn37IiLiV7/6VTz++OPxkY98JCIca97QXO0FHM/vfve7OHbsWLS3tw+4v729Pf7rv/6rSqsiW39/f6xduzaWLFkS55xzTkREdHd3x8SJE2PatGkDHtve3h7d3d1VWCXD8cADD8TTTz8du3btetvHHOvGsX///rjjjjviuuuuiy984Quxa9eu+OxnPxsTJ06MVatWlY/n8d7THev6sm7duiiVSjFv3rwYP358HDt2LG688cZYuXJlRIRjTUTUaFwyNqxevTqeffbZePzxx6u9FEZAV1dXXHPNNfHYY4/FpEmTqr0cRlB/f38sWLAgbrrppoiIOO+88+LZZ5+NO++8M1atWlXl1ZHpe9/7Xtx3331x//33x3vf+97Ys2dPrF27NmbPnu1YU1aTvxY/7bTTYvz48W+bGu3p6YmOjo4qrYpMa9asiYcffjh+9rOfxemnn16+v6OjI44cORIHDx4c8HjHvv7s3r07Xn755Tj//POjubk5mpubY/v27bF58+Zobm6O9vZ2x7pBzJo1K97znvcMuO/ss8+Ol156KSKifDy9p9e/z33uc7Fu3bq44oorYv78+fHXf/3Xce2118bGjRsjwrHmDTUZlxMnTowLLrggtm3bVr6vv78/tm3bFosXL67iyhiuoihizZo18dBDD8VPf/rT6OzsHPDxCy64ICZMmDDg2O/duzdeeuklx77OXHLJJfHMM8/Enj17yrcFCxbEypUry//tWDeGJUuWvO2UYvv27Yt3vvOdERHR2dkZHR0dA451qVSKnTt3OtZ15rXXXotx4wamw/jx46O/vz8iHGv+n2pPFA3mgQceKFpaWop77rmneO6554orr7yymDZtWtHd3V3tpTEMV111VdHW1lb8/Oc/L37729+Wb6+99lr5MX/3d39XzJ07t/jpT39aPPXUU8XixYuLxYsXV3HVZHnztHhRONaN4sknnyyam5uLG2+8sXjhhReK++67r3jHO95R/Mu//Ev5MTfffHMxbdq04gc/+EHxH//xH8Vll11WdHZ2Fn/4wx+quHIqtWrVquJP/uRPiocffrh48cUXi3/7t38rTjvttOL6668vP8axpmbjsiiKYsuWLcXcuXOLiRMnFhdeeGHxxBNPVHtJDFNEHPe2devW8mP+8Ic/FJ/5zGeK6dOnF+94xzuKj370o8Vvf/vb6i2aNG+NS8e6cfzoRz8qzjnnnKKlpaWYN29ecddddw34eH9/f7Fhw4aivb29aGlpKS655JJi7969VVotQ1UqlYprrrmmmDt3bjFp0qTijDPOKP7+7/++6OvrKz/GsaapKN50Wn0AABiGmvw3lwAA1CdxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAECa/ws8KlmAU0tsngAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [] } ], "metadata": { diff --git a/minimal_wc_presto/test_prestobackend.py b/minimal_wc_presto/test_prestobackend.py new file mode 100644 index 00000000..93b92a4e --- /dev/null +++ b/minimal_wc_presto/test_prestobackend.py @@ -0,0 +1,25 @@ +#%% + +import xarray as xr +import matplotlib.pyplot as plt + +output = xr.open_dataset('2024_05_17_13_41_40_input_cube_worldCereal.nc') +output = output['B08'].to_numpy().squeeze()[0,:,:].squeeze() +plt.imshow(output) + +#%% + +import xarray as xr +import matplotlib.pyplot as plt +import numpy as np + +output = xr.open_dataset('2024_05_17_14_00_16_output_presto.nc') +output.drop_vars('crs') + +flatten_output = output.to_array() + +#flatten_output = flatten_output.flatten() +#plt.hist(flatten_output) +#plt.show() + +#nan_counts = np.isnan(flatten_output).sum()/np.prod(flatten_output.shape) From 4a3b74b48d0fc8150d19370bc6188017a9ef048e Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Tue, 21 May 2024 20:44:21 +0200 Subject: [PATCH 08/31] fix: dynamic size --- .../mvp_wc_presto/world_cereal_inference.py | 32 +++++++++---------- .../udf_long_worldcereal_inference.py | 3 +- minimal_wc_presto/udf_presto.py | 8 +++-- .../udf_worldcereal_inference.py | 3 +- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py index 26760d2e..299b17c6 100644 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -127,7 +127,7 @@ def __init__(self, model: Presto): } @classmethod - def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.ndarray: + def _preprocess_band_values(self, values: np.ndarray, presto_band: str) -> np.ndarray: """ Preprocesses the band values based on the given presto_val. @@ -150,7 +150,7 @@ def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.nda return values @classmethod - def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: + def _extract_eo_data(self, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: """ Extracts EO data and mask arrays from the input xarray.DataArray. @@ -166,11 +166,11 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: eo_data = np.zeros((num_pixels, num_timesteps, len(BANDS))) mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) - for org_band, presto_band in cls.BAND_MAPPING.items(): + for org_band, presto_band in self.BAND_MAPPING.items(): if org_band in inarr.coords['bands']: values = np.swapaxes(inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1) - idx_valid = values != cls._NODATAVALUE - values = cls._preprocess_band_values(values, presto_band) + idx_valid = values != self._NODATAVALUE + values = self._preprocess_band_values(values, presto_band) eo_data[:, :, BANDS.index(presto_band)] = values mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid @@ -251,13 +251,13 @@ def _create_dataloader(self, eo:np.ndarray, dynamic_world:np.ndarray, months:np. return dl - def create_presto_input( - cls, inarr: xr.DataArray, epsg: int = 4326 + def _create_presto_input( + self, inarr: xr.DataArray, epsg: int ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - eo_data, mask = cls._extract_eo_data(inarr) - latlons = cls._extract_latlons(inarr, epsg) - months = cls._extract_months(inarr) + eo_data, mask = self._extract_eo_data(inarr) + latlons = self._extract_latlons(inarr, epsg) + months = self._extract_months(inarr) dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( DynamicWorld2020_2021.class_amount ) @@ -307,7 +307,7 @@ def _get_encodings(self, dl: DataLoader) -> np.ndarray: return np.concatenate(all_encodings, axis=0) @staticmethod - def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFrame: + def _combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFrame: flat_lat, flat_lon = latlons[:, 0], latlons[:, 1] if len(encodings.shape) == 1: encodings = np.expand_dims(encodings, axis=-1) @@ -319,18 +319,18 @@ def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFram return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) - def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> np.ndarray: - eo, dynamic_world, months, latlons, mask = self.create_presto_input(inarr, epsg) + def extract_presto_features(self, inarr: xr.DataArray, epsg: int)-> np.ndarray: + eo, dynamic_world, months, latlons, mask = self._create_presto_input(inarr, epsg) dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) features = self._get_encodings(dl) - features = self.combine_encodings(latlons, features) + features = self._combine_encodings(latlons, features) features = features.to_numpy() return features -def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: +def get_presto_features(inarr: xr.DataArray, presto_path: str, espg = 32631) -> np.ndarray: """ Extracts features from input data using Presto. @@ -345,7 +345,7 @@ def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: presto_model = Presto.load_pretrained_artifactory(presto_url = presto_path, strict=False) presto_extractor = PrestoFeatureExtractor(presto_model) - features = presto_extractor.extract_presto_features(inarr, epsg=32631) + features = presto_extractor.extract_presto_features(inarr, espg) return features diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index 3e8cccba..9040c36a 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -47,7 +47,8 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: # shape and indiches for output orig_dims = list(cube.dims) - map_dims = (100,100) + map_dims = cube.shape[2:] + cube = cube.fillna(65535) # Unzip de dependencies on the backend logger.info("Unzipping dependencies") diff --git a/minimal_wc_presto/udf_presto.py b/minimal_wc_presto/udf_presto.py index c423e390..95e38c06 100644 --- a/minimal_wc_presto/udf_presto.py +++ b/minimal_wc_presto/udf_presto.py @@ -38,11 +38,13 @@ def extract_dependencies(base_url: str, dependency_name: str): def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: - logger = _setup_logging() + logger = _setup_logging() + # shape and indiches for output orig_dims = list(cube.dims) map_dims = cube.shape[2:] + cube = cube.fillna(65535) logger.info("Unzipping dependencies") base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" @@ -62,7 +64,7 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger.info("Extracting presto features") PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" - features = get_presto_features(cube, PRESTO_PATH) + features = get_presto_features(cube, PRESTO_PATH, 32631) # go to 128,1,100,100 presto_dim = map_dims + (128,) @@ -71,7 +73,7 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: features = np.transpose(features, (3, 0, 1, 2)) - transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) + transformer = Transformer.from_crs(f"EPSG:{32631}", "EPSG:4326", always_xy=True) longitudes, latitudes = transformer.transform(cube.x, cube.y) diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index 6296217d..5fceeb95 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -43,7 +43,8 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: # shape and indiches for output orig_dims = list(cube.dims) - map_dims = (100,100) + map_dims = cube.shape[2:] + cube = cube.fillna(65535) # Unzip de dependencies on the backend logger.info("Unzipping dependencies") From f3f4b1596ca1bf1895dd47446a1eb93a5b08ac6a Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Tue, 21 May 2024 21:22:28 +0200 Subject: [PATCH 09/31] Work in xarray as much as possible --- .../mvp_wc_presto/world_cereal_inference.py | 142 +++++++++--------- .../udf_long_worldcereal_inference.py | 92 +++++------- 2 files changed, 110 insertions(+), 124 deletions(-) diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py index 26760d2e..a72a3b74 100644 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -2,16 +2,12 @@ import numpy as np import pandas as pd - +import requests import torch -from torch.utils.data import DataLoader, TensorDataset -from pyproj import Transformer - import xarray as xr -from einops import repeat -import onnxruntime -import requests - +from einops import rearrange +from pyproj import Transformer +from torch.utils.data import DataLoader, TensorDataset from .dataops import ( BANDS, @@ -24,9 +20,7 @@ from .presto import Presto from .utils import device - - -#% Mapping from original band names to Presto names +# Mapping from original band names to Presto names BAND_MAPPING = { "B02": "B2", "B03": "B3", @@ -50,7 +44,8 @@ for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) for idx in val } - + + class WorldCerealPredictor: def __init__(self): """ @@ -66,6 +61,7 @@ def load_model(self, model): model_path (str): The path to the ONNX model file. """ # Load the dependency into an InferenceSession + import onnxruntime self.onnx_session = onnxruntime.InferenceSession(model) def predict(self, features: np.ndarray) -> np.ndarray: @@ -80,21 +76,20 @@ def predict(self, features: np.ndarray) -> np.ndarray: """ if self.onnx_session is None: raise ValueError("Model has not been loaded. Please load a model first.") - + # Prepare input data for ONNX model - outputs = self.onnx_session.run(None, {'features': features}) - + outputs = self.onnx_session.run(None, {"features": features}) + # Threshold for binary conversion threshold = 0.5 # Extract all prediction values and convert them to binary labels - prediction_values = [sublist['True'] for sublist in outputs[1]] + prediction_values = [sublist["True"] for sublist in outputs[1]] binary_labels = np.array(prediction_values) >= threshold binary_labels = binary_labels.astype(int) return binary_labels - class PrestoFeatureExtractor: @@ -127,7 +122,9 @@ def __init__(self, model: Presto): } @classmethod - def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.ndarray: + def _preprocess_band_values( + cls, values: np.ndarray, presto_band: str + ) -> np.ndarray: """ Preprocesses the band values based on the given presto_val. @@ -148,7 +145,7 @@ def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.nda # Remove scaling values = values / 100 return values - + @classmethod def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: """ @@ -167,8 +164,10 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) for org_band, presto_band in cls.BAND_MAPPING.items(): - if org_band in inarr.coords['bands']: - values = np.swapaxes(inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1) + if org_band in inarr.coords["bands"]: + values = np.swapaxes( + inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1 + ) idx_valid = values != cls._NODATAVALUE values = cls._preprocess_band_values(values, presto_band) eo_data[:, :, BANDS.index(presto_band)] = values @@ -176,7 +175,6 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: return eo_data, mask - @staticmethod def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: """ @@ -189,18 +187,17 @@ def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: Returns: np.ndarray: Array containing extracted latitudes and longitudes. """ - #EPSG:4326 is the supported crs for presto + # EPSG:4326 is the supported crs for presto + lon, lat = np.meshgrid(inarr.x, inarr.y) transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) - lon, lat = transformer.transform(inarr.x, inarr.y) + lon, lat = transformer.transform(lon, lat) + latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") # 2D array where each row represents a pair of latitude and longitude coordinates. - return np.stack( - [np.repeat(lat, repeats=len(lon)), repeat(lon, "c -> (h c)", h=len(lat))], - axis=-1, - ) - + return latlons + @staticmethod - def _extract_months( inarr: xr.DataArray) -> np.ndarray: + def _extract_months(inarr: xr.DataArray) -> np.ndarray: """ Calculate the start month based on the first timestamp in the input array, and create an array of the same length filled with that start month value. @@ -221,8 +218,15 @@ def _extract_months( inarr: xr.DataArray) -> np.ndarray: months = np.ones((num_instances)) * start_month return months - - def _create_dataloader(self, eo:np.ndarray, dynamic_world:np.ndarray, months:np.ndarray, latlons:np.ndarray, mask:np.ndarray) -> DataLoader: + + def _create_dataloader( + self, + eo: np.ndarray, + dynamic_world: np.ndarray, + months: np.ndarray, + latlons: np.ndarray, + mask: np.ndarray, + ) -> DataLoader: """ Create a PyTorch DataLoader for encoding features. @@ -251,7 +255,7 @@ def _create_dataloader(self, eo:np.ndarray, dynamic_world:np.ndarray, months:np. return dl - def create_presto_input( + def _create_presto_input( cls, inarr: xr.DataArray, epsg: int = 4326 ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: @@ -267,10 +271,9 @@ def create_presto_input( dynamic_world, months, latlons, - np.repeat(mask, BAND_EXPANSION, axis=-1) + np.repeat(mask, BAND_EXPANSION, axis=-1), ) - - + def _get_encodings(self, dl: DataLoader) -> np.ndarray: """ Get encodings from DataLoader. @@ -281,7 +284,7 @@ def _get_encodings(self, dl: DataLoader) -> np.ndarray: Returns: np.ndarray: Array containing encoded features. """ - + all_encodings = [] for x, dw, latlons, month, variable_mask in dl: @@ -305,48 +308,46 @@ def _get_encodings(self, dl: DataLoader) -> np.ndarray: all_encodings.append(encodings) return np.concatenate(all_encodings, axis=0) - - @staticmethod - def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFrame: - flat_lat, flat_lon = latlons[:, 0], latlons[:, 1] - if len(encodings.shape) == 1: - encodings = np.expand_dims(encodings, axis=-1) - - data_dict: Dict[str, np.ndarray] = {"lat": flat_lat, "lon": flat_lon} - for i in range(encodings.shape[1]): - encodings_label = f"presto_ft_{i}" - data_dict[encodings_label] = encodings[:, i] - return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) - - - def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> np.ndarray: - eo, dynamic_world, months, latlons, mask = self.create_presto_input(inarr, epsg) + + def extract_presto_features( + self, inarr: xr.DataArray, epsg: int = 4326 + ) -> np.ndarray: + eo, dynamic_world, months, latlons, mask = self._create_presto_input( + inarr, epsg + ) dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) features = self._get_encodings(dl) - features = self.combine_encodings(latlons, features) - features = features.to_numpy() + features = rearrange( + features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) + ) + ft_names = [f"presto_ft_{i}" for i in range(128)] + features = xr.DataArray( + features, coords={"x": inarr.x, "y": inarr.y, "bands": ft_names} + ) return features - + def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: - """ - Extracts features from input data using Presto. + """ + Extracts features from input data using Presto. - Args: - inarr (xr.DataArray): Input data as xarray DataArray. - presto_path (str): Path to the pretrained Presto model. + Args: + inarr (xr.DataArray): Input data as xarray DataArray. + presto_path (str): Path to the pretrained Presto model. - Returns: - xr.DataArray: Extracted features as xarray DataArray. - """ - # Load the model + Returns: + xr.DataArray: Extracted features as xarray DataArray. + """ + # Load the model - presto_model = Presto.load_pretrained_artifactory(presto_url = presto_path, strict=False) - presto_extractor = PrestoFeatureExtractor(presto_model) - features = presto_extractor.extract_presto_features(inarr, epsg=32631) - return features + presto_model = Presto.load_pretrained_artifactory( + presto_url=presto_path, strict=False + ) + presto_extractor = PrestoFeatureExtractor(presto_model) + features = presto_extractor.extract_presto_features(inarr, epsg=32631) + return features def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarray: @@ -369,5 +370,4 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr predictor.load_model(catboost_model) predictions = predictor.predict(features) - - return predictions \ No newline at end of file + return predictions diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index 3e8cccba..19b79fe2 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -1,17 +1,14 @@ +import functools import logging -import urllib.request import shutil -from pathlib import Path import sys -import functools -import xarray as xr -import numpy as np -from pyproj import Transformer - +import urllib.request +from pathlib import Path from typing import Dict, Tuple import numpy as np - +import xarray as xr +from pyproj import Transformer def _setup_logging(): @@ -47,7 +44,6 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: # shape and indiches for output orig_dims = list(cube.dims) - map_dims = (100,100) # Unzip de dependencies on the backend logger.info("Unzipping dependencies") @@ -61,6 +57,10 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: ################################################################################################################### + import onnxruntime + import pandas as pd + import requests + import torch from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( BANDS, BANDS_GROUPS_IDX, @@ -68,24 +68,13 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: S1_S2_ERA5_SRTM, DynamicWorld2020_2021, ) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.masking import BAND_EXPANSION + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.masking import ( + BAND_EXPANSION, + ) from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device - - import pandas as pd - - import torch + from einops import rearrange, repeat from torch.utils.data import DataLoader, TensorDataset - - from einops import repeat - import onnxruntime - import requests - - - - #% Mapping from original band names to Presto names - BAND_MAPPING = { - "B02": "B2", "B03": "B3", "B04": "B4", "B05": "B5", @@ -123,6 +112,7 @@ def load_model(self, model): model_path (str): The path to the ONNX model file. """ # Load the dependency into an InferenceSession + import onnxruntime self.onnx_session = onnxruntime.InferenceSession(model) def predict(self, features: np.ndarray) -> np.ndarray: @@ -246,17 +236,14 @@ def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: Returns: np.ndarray: Array containing extracted latitudes and longitudes. """ - #EPSG:4326 is the supported crs for presto + # EPSG:4326 is the supported crs for presto + lon, lat = np.meshgrid(inarr.x, inarr.y) transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) - lon, lat = transformer.transform(inarr.x, inarr.y) - - + lon, lat = transformer.transform(lon, lat) + latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") # 2D array where each row represents a pair of latitude and longitude coordinates. - return np.stack( - [np.repeat(lat, repeats=len(lon)), repeat(lon, "c -> (h c)", h=len(lat))], - axis=-1, - ) + return latlons @staticmethod def _extract_months( inarr: xr.DataArray) -> np.ndarray: @@ -365,26 +352,22 @@ def _get_encodings(self, dl: DataLoader) -> np.ndarray: return np.concatenate(all_encodings, axis=0) - @staticmethod - def combine_encodings(latlons: np.ndarray, encodings: np.ndarray) -> pd.DataFrame: - flat_lat, flat_lon = latlons[:, 0], latlons[:, 1] - if len(encodings.shape) == 1: - encodings = np.expand_dims(encodings, axis=-1) - - data_dict: Dict[str, np.ndarray] = {"lat": flat_lat, "lon": flat_lon} - for i in range(encodings.shape[1]): - encodings_label = f"presto_ft_{i}" - data_dict[encodings_label] = encodings[:, i] - return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) - - - def extract_presto_features(self, inarr: xr.DataArray, epsg: int = 4326)-> np.ndarray: - eo, dynamic_world, months, latlons, mask = self._create_presto_input(inarr, epsg) + def extract_presto_features( + self, inarr: xr.DataArray, epsg: int = 4326 + ) -> np.ndarray: + eo, dynamic_world, months, latlons, mask = self._create_presto_input( + inarr, epsg + ) dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) features = self._get_encodings(dl) - features = self.combine_encodings(latlons, features) - features = features.to_numpy() + features = rearrange( + features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) + ) + ft_names = [f"presto_ft_{i}" for i in range(128)] + features = xr.DataArray( + features, coords={"x": inarr.x, "y": inarr.y, "bands": ft_names} + ) return features @@ -444,7 +427,9 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr # run catboost classification logger.info("Catboost classification") CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" - classification = classify_with_catboost(features, CATBOOST_PATH) + stacked_features = features.stack(xy=['x', 'y']).transpose() + classification = classify_with_catboost(stacked_features.values, CATBOOST_PATH) + classification = xr.DataArray(classification, coords={'xy': stacked_features.xy}).unstack().expand_dims(dim='bands').expand_dims(dim='t') logger.info("Shape of classification output: {}".format(classification.shape)) # revert to 4D shape for openEO @@ -452,9 +437,10 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr #transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) #longitudes, latitudes = transformer.transform(cube.x, cube.y) - classification = np.flip(classification.reshape(map_dims),axis = 0) - classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) - output = xr.DataArray(classification, dims=orig_dims) + # classification = np.flip(classification.reshape(map_dims),axis = 0) + # classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) + # output = xr.DataArray(classification, dims=orig_dims) + output = classification.transpose(*orig_dims) logger.info("Shape of output: {}".format(output.shape)) return output From e0c1d0554945f4d28437ba10f1b88a9914a04a7c Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Tue, 21 May 2024 21:36:16 +0200 Subject: [PATCH 10/31] Fix typing errors --- .../udf_long_worldcereal_inference.py | 160 ++++++++---------- 1 file changed, 74 insertions(+), 86 deletions(-) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index 19b79fe2..6d210148 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -16,30 +16,32 @@ def _setup_logging(): logger = logging.getLogger(__name__) return logger + @functools.lru_cache(maxsize=6) def extract_dependencies(base_url: str, dependency_name: str): # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / 'dependencies' + dependencies_dir = Path.cwd() / "dependencies" # Create the directory if it doesn't exist dependencies_dir.mkdir(exist_ok=True, parents=True) - # Download and extract the model file modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) + modelfile, _ = urllib.request.urlretrieve( + modelfile_url, filename=dependencies_dir / Path(modelfile_url).name + ) shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) # Add the model directory to system path if it's not already there - abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) + abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) - return(abs_path) + return abs_path -def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: - - logger = _setup_logging() +def apply_datacube(cube: xr.DataArray, context: Dict) -> xr.DataArray: + + logger = _setup_logging() logger.info("Shape of input: {}".format(cube.shape)) # shape and indiches for output @@ -53,7 +55,7 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: # Append the dependencies sys.path.append(str(dep_dir)) - sys.path.append(str(dep_dir) + '/pandas') + sys.path.append(str(dep_dir) + "/pandas") ################################################################################################################### @@ -73,22 +75,8 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: ) from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device - from einops import rearrange, repeat + from einops import rearrange from torch.utils.data import DataLoader, TensorDataset - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - "B8A": "B8A", - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", - } # Index to band groups mapping IDX_TO_BAND_GROUPS = { @@ -96,7 +84,7 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) for idx in val } - + class WorldCerealPredictor: def __init__(self): """ @@ -113,6 +101,7 @@ def load_model(self, model): """ # Load the dependency into an InferenceSession import onnxruntime + self.onnx_session = onnxruntime.InferenceSession(model) def predict(self, features: np.ndarray) -> np.ndarray: @@ -126,23 +115,23 @@ def predict(self, features: np.ndarray) -> np.ndarray: pd.DataFrame: DataFrame containing the predicted labels. """ if self.onnx_session is None: - raise ValueError("Model has not been loaded. Please load a model first.") - + raise ValueError( + "Model has not been loaded. Please load a model first." + ) + # Prepare input data for ONNX model - outputs = self.onnx_session.run(None, {'features': features}) - + outputs = self.onnx_session.run(None, {"features": features}) + # Threshold for binary conversion threshold = 0.5 # Extract all prediction values and convert them to binary labels - prediction_values = [sublist['True'] for sublist in outputs[1]] + prediction_values = [sublist["True"] for sublist in outputs[1]] binary_labels = np.array(prediction_values) >= threshold binary_labels = binary_labels.astype(int) return binary_labels - - class PrestoFeatureExtractor: def __init__(self, model: Presto): @@ -174,7 +163,9 @@ def __init__(self, model: Presto): } @classmethod - def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.ndarray: + def _preprocess_band_values( + cls, values: np.ndarray, presto_band: str + ) -> np.ndarray: """ Preprocesses the band values based on the given presto_val. @@ -195,7 +186,7 @@ def _preprocess_band_values(cls, values: np.ndarray, presto_band: str) -> np.nda # Remove scaling values = values / 100 return values - + @classmethod def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: """ @@ -214,8 +205,12 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) for org_band, presto_band in cls.BAND_MAPPING.items(): - if org_band in inarr.coords['bands']: - values = np.swapaxes(inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1) + if org_band in inarr.coords["bands"]: + values = np.swapaxes( + inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), + 0, + 1, + ) idx_valid = values != cls._NODATAVALUE values = cls._preprocess_band_values(values, presto_band) eo_data[:, :, BANDS.index(presto_band)] = values @@ -223,7 +218,6 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: return eo_data, mask - @staticmethod def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: """ @@ -238,15 +232,17 @@ def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: """ # EPSG:4326 is the supported crs for presto lon, lat = np.meshgrid(inarr.x, inarr.y) - transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) + transformer = Transformer.from_crs( + f"EPSG:{epsg}", "EPSG:4326", always_xy=True + ) lon, lat = transformer.transform(lon, lat) latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") # 2D array where each row represents a pair of latitude and longitude coordinates. return latlons - + @staticmethod - def _extract_months( inarr: xr.DataArray) -> np.ndarray: + def _extract_months(inarr: xr.DataArray) -> np.ndarray: """ Calculate the start month based on the first timestamp in the input array, and create an array of the same length filled with that start month value. @@ -267,8 +263,15 @@ def _extract_months( inarr: xr.DataArray) -> np.ndarray: months = np.ones((num_instances)) * start_month return months - - def _create_dataloader(self, eo:np.ndarray, dynamic_world:np.ndarray, months:np.ndarray, latlons:np.ndarray, mask:np.ndarray) -> DataLoader: + + def _create_dataloader( + self, + eo: np.ndarray, + dynamic_world: np.ndarray, + months: np.ndarray, + latlons: np.ndarray, + mask: np.ndarray, + ) -> DataLoader: """ Create a PyTorch DataLoader for encoding features. @@ -313,10 +316,9 @@ def _create_presto_input( dynamic_world, months, latlons, - np.repeat(mask, BAND_EXPANSION, axis=-1) + np.repeat(mask, BAND_EXPANSION, axis=-1), ) - - + def _get_encodings(self, dl: DataLoader) -> np.ndarray: """ Get encodings from DataLoader. @@ -327,7 +329,7 @@ def _get_encodings(self, dl: DataLoader) -> np.ndarray: Returns: np.ndarray: Array containing encoded features. """ - + all_encodings = [] for x, dw, latlons, month, variable_mask in dl: @@ -351,7 +353,7 @@ def _get_encodings(self, dl: DataLoader) -> np.ndarray: all_encodings.append(encodings) return np.concatenate(all_encodings, axis=0) - + def extract_presto_features( self, inarr: xr.DataArray, epsg: int = 4326 ) -> np.ndarray: @@ -370,26 +372,26 @@ def extract_presto_features( ) return features - def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: - """ - Extracts features from input data using Presto. - - Args: - inarr (xr.DataArray): Input data as xarray DataArray. - presto_path (str): Path to the pretrained Presto model. + """ + Extracts features from input data using Presto. - Returns: - xr.DataArray: Extracted features as xarray DataArray. - """ - # Load the model + Args: + inarr (xr.DataArray): Input data as xarray DataArray. + presto_path (str): Path to the pretrained Presto model. - presto_model = Presto.load_pretrained_artifactory(presto_url = presto_path, strict=False) - presto_extractor = PrestoFeatureExtractor(presto_model) - features = presto_extractor.extract_presto_features(inarr, epsg=32631) - return features + Returns: + xr.DataArray: Extracted features as xarray DataArray. + """ + # Load the model + presto_model = Presto.load_pretrained_artifactory( + presto_url=presto_path, strict=False + ) + presto_extractor = PrestoFeatureExtractor(presto_model) + features = presto_extractor.extract_presto_features(inarr, epsg=32631) + return features def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarray: """ @@ -411,13 +413,10 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr predictor.load_model(catboost_model) predictions = predictor.predict(features) - return predictions - ################################################################################################################### - # Run presto inference logger.info("Extracting presto features") PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" @@ -427,15 +426,20 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr # run catboost classification logger.info("Catboost classification") CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" - stacked_features = features.stack(xy=['x', 'y']).transpose() + stacked_features = features.stack(xy=["x", "y"]).transpose() classification = classify_with_catboost(stacked_features.values, CATBOOST_PATH) - classification = xr.DataArray(classification, coords={'xy': stacked_features.xy}).unstack().expand_dims(dim='bands').expand_dims(dim='t') + classification = ( + xr.DataArray(classification, coords={"xy": stacked_features.xy}) + .unstack() + .expand_dims(dim="bands") + .expand_dims(dim="t") + ) logger.info("Shape of classification output: {}".format(classification.shape)) # revert to 4D shape for openEO - #logger.info("Revert to 4D xarray") - #transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) - #longitudes, latitudes = transformer.transform(cube.x, cube.y) + # logger.info("Revert to 4D xarray") + # transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) + # longitudes, latitudes = transformer.transform(cube.x, cube.y) # classification = np.flip(classification.reshape(map_dims),axis = 0) # classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) @@ -444,19 +448,3 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr logger.info("Shape of output: {}".format(output.shape)) return output - - - - - - - - - - - - - - - - From 433f001e0967c0af79de63bce1781a29a3498349 Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Wed, 22 May 2024 14:55:52 +0200 Subject: [PATCH 11/31] fix: inference --- .../backend_inference_example_openeo.ipynb | 379 ++++++------------ minimal_wc_presto/dev_testing.py | 16 +- minimal_wc_presto/udf_presto.py | 26 +- .../udf_worldcereal_inference.py | 15 +- 4 files changed, 163 insertions(+), 273 deletions(-) diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index 2cdf4c68..8d863f3a 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", "metadata": { "tags": [] @@ -29,6 +29,7 @@ "from datetime import datetime \n", "\n", "\n", + "\n", "#token SENTINEL\n", "connection = openeo.connect(\"https://openeo.dataspace.copernicus.eu/\").authenticate_oidc()" ] @@ -43,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "id": "5494c46d", "metadata": {}, "outputs": [], @@ -51,8 +52,8 @@ "#Get desired data\n", "from preprocessing import worldcereal_preprocessed_inputs\n", "\n", - "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.19, 51.25, 5.21, 51.26]))\n", - "EXTENT['crs'] = \"EPSG:4326\"\n", + "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [664000.0, 5611120.0, 665000.0, 5612120.0]))\n", + "EXTENT['crs'] = \"EPSG:32631\"\n", "\n", "STARTDATE = '2020-11-01'\n", "ENDDATE = '2021-10-31'\n", @@ -81,7 +82,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "4aab5695", "metadata": {}, "outputs": [ @@ -89,60 +90,39 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-240517a35acc48b697839a923dd5fe56': send 'start'\n", - "0:00:18 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:23 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:30 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:38 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:48 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:01:02 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:01:18 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:01:38 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:02:04 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:02:35 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:03:13 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:04:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:04:59 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:05:59 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:07:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:08:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:09:01 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:10:46 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:12:38 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:13:39 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:14:39 Job 'j-240517a35acc48b697839a923dd5fe56': finished (progress 100%)\n" + "0:00:00 Job 'j-2405225aa91d4f9c986e314482d61a1b': send 'start'\n" ] }, { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[3], line 5\u001b[0m\n\u001b[0;32m 2\u001b[0m formatted_datetime \u001b[38;5;241m=\u001b[39m current_datetime\u001b[38;5;241m.\u001b[39mstrftime(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mY_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mm_\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[38;5;124m_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mH_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mM_\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mS\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 3\u001b[0m outputfile_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(formatted_datetime) \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m_input_cube_worldCereal.nc\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m----> 5\u001b[0m \u001b[43minput_cube\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43moutputfile_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal data collection\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:270\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 268\u001b[0m \u001b[38;5;66;03m# TODO: make `max_poll_interval`, `connection_retry_interval` class constants or instance properties?\u001b[39;00m\n\u001b[0;32m 269\u001b[0m print_status(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msend \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mstart\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m--> 270\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 272\u001b[0m \u001b[38;5;66;03m# TODO: also add `wait` method so you can track a job that already has started explicitly\u001b[39;00m\n\u001b[0;32m 273\u001b[0m \u001b[38;5;66;03m# or just rename this method to `wait` and automatically do start if not started yet?\u001b[39;00m\n\u001b[0;32m 274\u001b[0m \n\u001b[0;32m 275\u001b[0m \u001b[38;5;66;03m# Start with fast polling.\u001b[39;00m\n\u001b[0;32m 276\u001b[0m poll_interval \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mmin\u001b[39m(\u001b[38;5;241m5\u001b[39m, max_poll_interval)\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:119\u001b[0m, in \u001b[0;36mBatchJob.start\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 109\u001b[0m \u001b[38;5;129m@openeo_endpoint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPOST /jobs/\u001b[39m\u001b[38;5;132;01m{job_id}\u001b[39;00m\u001b[38;5;124m/results\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 110\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mstart\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 111\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 112\u001b[0m \u001b[38;5;124;03m Start this batch job.\u001b[39;00m\n\u001b[0;32m 113\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 117\u001b[0m \u001b[38;5;124;03m This method was previously called :py:meth:`start_job`.\u001b[39;00m\n\u001b[0;32m 118\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m--> 119\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconnection\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpost\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m/jobs/\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mjob_id\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m/results\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_status\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m202\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 120\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:231\u001b[0m, in \u001b[0;36mRestApiConnection.post\u001b[1;34m(self, path, json, **kwargs)\u001b[0m\n\u001b[0;32m 223\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mpost\u001b[39m(\u001b[38;5;28mself\u001b[39m, path: \u001b[38;5;28mstr\u001b[39m, json: Optional[\u001b[38;5;28mdict\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Response:\n\u001b[0;32m 224\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 225\u001b[0m \u001b[38;5;124;03m Do POST request to REST API.\u001b[39;00m\n\u001b[0;32m 226\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 229\u001b[0m \u001b[38;5;124;03m :return: response: Response\u001b[39;00m\n\u001b[0;32m 230\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m--> 231\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mpost\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mjson\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mjson\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mallow_redirects\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:770\u001b[0m, in \u001b[0;36mConnection.request\u001b[1;34m(self, method, path, headers, auth, check_error, expected_status, **kwargs)\u001b[0m\n\u001b[0;32m 763\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m(Connection, \u001b[38;5;28mself\u001b[39m)\u001b[38;5;241m.\u001b[39mrequest(\n\u001b[0;32m 764\u001b[0m method\u001b[38;5;241m=\u001b[39mmethod, path\u001b[38;5;241m=\u001b[39mpath, headers\u001b[38;5;241m=\u001b[39mheaders, auth\u001b[38;5;241m=\u001b[39mauth,\n\u001b[0;32m 765\u001b[0m check_error\u001b[38;5;241m=\u001b[39mcheck_error, expected_status\u001b[38;5;241m=\u001b[39mexpected_status, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 766\u001b[0m )\n\u001b[0;32m 768\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 769\u001b[0m \u001b[38;5;66;03m# Initial request attempt\u001b[39;00m\n\u001b[1;32m--> 770\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 771\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m OpenEoApiError \u001b[38;5;28;01mas\u001b[39;00m api_exc:\n\u001b[0;32m 772\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m api_exc\u001b[38;5;241m.\u001b[39mhttp_status_code \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m401\u001b[39m, \u001b[38;5;241m403\u001b[39m} \u001b[38;5;129;01mand\u001b[39;00m api_exc\u001b[38;5;241m.\u001b[39mcode \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTokenInvalid\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 773\u001b[0m \u001b[38;5;66;03m# Auth token expired: can we refresh?\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:763\u001b[0m, in \u001b[0;36mConnection.request.._request\u001b[1;34m()\u001b[0m\n\u001b[0;32m 762\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_request\u001b[39m():\n\u001b[1;32m--> 763\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mConnection\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 764\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mauth\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mauth\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 765\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheck_error\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcheck_error\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_status\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexpected_status\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 766\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\connection.py:148\u001b[0m, in \u001b[0;36mRestApiConnection.request\u001b[1;34m(self, method, path, headers, auth, check_error, expected_status, **kwargs)\u001b[0m\n\u001b[0;32m 144\u001b[0m _log\u001b[38;5;241m.\u001b[39mdebug(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mRequest `\u001b[39m\u001b[38;5;132;01m{m}\u001b[39;00m\u001b[38;5;124m \u001b[39m\u001b[38;5;132;01m{u}\u001b[39;00m\u001b[38;5;124m` with headers \u001b[39m\u001b[38;5;132;01m{h}\u001b[39;00m\u001b[38;5;124m, auth \u001b[39m\u001b[38;5;132;01m{a}\u001b[39;00m\u001b[38;5;124m, kwargs \u001b[39m\u001b[38;5;132;01m{k}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[0;32m 145\u001b[0m m\u001b[38;5;241m=\u001b[39mmethod\u001b[38;5;241m.\u001b[39mupper(), u\u001b[38;5;241m=\u001b[39murl, h\u001b[38;5;241m=\u001b[39mheaders \u001b[38;5;129;01mand\u001b[39;00m headers\u001b[38;5;241m.\u001b[39mkeys(), a\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mtype\u001b[39m(auth)\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m, k\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mlist\u001b[39m(kwargs\u001b[38;5;241m.\u001b[39mkeys()))\n\u001b[0;32m 146\u001b[0m )\n\u001b[0;32m 147\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m ContextTimer() \u001b[38;5;28;01mas\u001b[39;00m timer:\n\u001b[1;32m--> 148\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msession\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 149\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 150\u001b[0m \u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 151\u001b[0m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_merged_headers\u001b[49m\u001b[43m(\u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 152\u001b[0m \u001b[43m \u001b[49m\u001b[43mauth\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mauth\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 153\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtimeout\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdefault_timeout\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 154\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[0;32m 155\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 156\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m slow_response_threshold \u001b[38;5;129;01mand\u001b[39;00m timer\u001b[38;5;241m.\u001b[39melapsed() \u001b[38;5;241m>\u001b[39m slow_response_threshold:\n\u001b[0;32m 157\u001b[0m _log\u001b[38;5;241m.\u001b[39mwarning(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSlow response: `\u001b[39m\u001b[38;5;132;01m{m}\u001b[39;00m\u001b[38;5;124m \u001b[39m\u001b[38;5;132;01m{u}\u001b[39;00m\u001b[38;5;124m` took \u001b[39m\u001b[38;5;132;01m{e:.2f}\u001b[39;00m\u001b[38;5;124ms (>\u001b[39m\u001b[38;5;132;01m{t:.2f}\u001b[39;00m\u001b[38;5;124ms)\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[0;32m 158\u001b[0m m\u001b[38;5;241m=\u001b[39mmethod\u001b[38;5;241m.\u001b[39mupper(), u\u001b[38;5;241m=\u001b[39mstr_truncate(url, width\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m64\u001b[39m),\n\u001b[0;32m 159\u001b[0m e\u001b[38;5;241m=\u001b[39mtimer\u001b[38;5;241m.\u001b[39melapsed(), t\u001b[38;5;241m=\u001b[39mslow_response_threshold\n\u001b[0;32m 160\u001b[0m ))\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\requests\\sessions.py:589\u001b[0m, in \u001b[0;36mSession.request\u001b[1;34m(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)\u001b[0m\n\u001b[0;32m 584\u001b[0m send_kwargs \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 585\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtimeout\u001b[39m\u001b[38;5;124m\"\u001b[39m: timeout,\n\u001b[0;32m 586\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mallow_redirects\u001b[39m\u001b[38;5;124m\"\u001b[39m: allow_redirects,\n\u001b[0;32m 587\u001b[0m }\n\u001b[0;32m 588\u001b[0m send_kwargs\u001b[38;5;241m.\u001b[39mupdate(settings)\n\u001b[1;32m--> 589\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprep\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43msend_kwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 591\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m resp\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\requests\\sessions.py:703\u001b[0m, in \u001b[0;36mSession.send\u001b[1;34m(self, request, **kwargs)\u001b[0m\n\u001b[0;32m 700\u001b[0m start \u001b[38;5;241m=\u001b[39m preferred_clock()\n\u001b[0;32m 702\u001b[0m \u001b[38;5;66;03m# Send the request\u001b[39;00m\n\u001b[1;32m--> 703\u001b[0m r \u001b[38;5;241m=\u001b[39m \u001b[43madapter\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 705\u001b[0m \u001b[38;5;66;03m# Total elapsed time of the request (approximately)\u001b[39;00m\n\u001b[0;32m 706\u001b[0m elapsed \u001b[38;5;241m=\u001b[39m preferred_clock() \u001b[38;5;241m-\u001b[39m start\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\requests\\adapters.py:486\u001b[0m, in \u001b[0;36mHTTPAdapter.send\u001b[1;34m(self, request, stream, timeout, verify, cert, proxies)\u001b[0m\n\u001b[0;32m 483\u001b[0m timeout \u001b[38;5;241m=\u001b[39m TimeoutSauce(connect\u001b[38;5;241m=\u001b[39mtimeout, read\u001b[38;5;241m=\u001b[39mtimeout)\n\u001b[0;32m 485\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 486\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43murlopen\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 487\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 488\u001b[0m \u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 489\u001b[0m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 490\u001b[0m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 491\u001b[0m \u001b[43m \u001b[49m\u001b[43mredirect\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 492\u001b[0m \u001b[43m \u001b[49m\u001b[43massert_same_host\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 493\u001b[0m \u001b[43m \u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 494\u001b[0m \u001b[43m \u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 495\u001b[0m \u001b[43m \u001b[49m\u001b[43mretries\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmax_retries\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 496\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 497\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunked\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchunked\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 498\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 500\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (ProtocolError, \u001b[38;5;167;01mOSError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m err:\n\u001b[0;32m 501\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m(err, request\u001b[38;5;241m=\u001b[39mrequest)\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\urllib3\\connectionpool.py:793\u001b[0m, in \u001b[0;36mHTTPConnectionPool.urlopen\u001b[1;34m(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)\u001b[0m\n\u001b[0;32m 790\u001b[0m response_conn \u001b[38;5;241m=\u001b[39m conn \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m release_conn \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 792\u001b[0m \u001b[38;5;66;03m# Make the request on the HTTPConnection object\u001b[39;00m\n\u001b[1;32m--> 793\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_make_request\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 794\u001b[0m \u001b[43m \u001b[49m\u001b[43mconn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 795\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 796\u001b[0m \u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 797\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 798\u001b[0m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 799\u001b[0m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 800\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunked\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchunked\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 801\u001b[0m \u001b[43m \u001b[49m\u001b[43mretries\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mretries\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 802\u001b[0m \u001b[43m \u001b[49m\u001b[43mresponse_conn\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mresponse_conn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 803\u001b[0m \u001b[43m \u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpreload_content\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 804\u001b[0m \u001b[43m \u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdecode_content\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 805\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mresponse_kw\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 806\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 808\u001b[0m \u001b[38;5;66;03m# Everything went great!\u001b[39;00m\n\u001b[0;32m 809\u001b[0m clean_exit \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\urllib3\\connectionpool.py:537\u001b[0m, in \u001b[0;36mHTTPConnectionPool._make_request\u001b[1;34m(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)\u001b[0m\n\u001b[0;32m 535\u001b[0m \u001b[38;5;66;03m# Receive the response from the server\u001b[39;00m\n\u001b[0;32m 536\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 537\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgetresponse\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 538\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (BaseSSLError, \u001b[38;5;167;01mOSError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 539\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_raise_timeout(err\u001b[38;5;241m=\u001b[39me, url\u001b[38;5;241m=\u001b[39murl, timeout_value\u001b[38;5;241m=\u001b[39mread_timeout)\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\urllib3\\connection.py:466\u001b[0m, in \u001b[0;36mHTTPConnection.getresponse\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 463\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mresponse\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m HTTPResponse\n\u001b[0;32m 465\u001b[0m \u001b[38;5;66;03m# Get the response from http.client.HTTPConnection\u001b[39;00m\n\u001b[1;32m--> 466\u001b[0m httplib_response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgetresponse\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 468\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 469\u001b[0m assert_header_parsing(httplib_response\u001b[38;5;241m.\u001b[39mmsg)\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\http\\client.py:1423\u001b[0m, in \u001b[0;36mHTTPConnection.getresponse\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 1421\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 1422\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m-> 1423\u001b[0m \u001b[43mresponse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbegin\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1424\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m:\n\u001b[0;32m 1425\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mclose()\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\http\\client.py:331\u001b[0m, in \u001b[0;36mHTTPResponse.begin\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 329\u001b[0m \u001b[38;5;66;03m# read until we get a non-100 response\u001b[39;00m\n\u001b[0;32m 330\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[1;32m--> 331\u001b[0m version, status, reason \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_read_status\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 332\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m status \u001b[38;5;241m!=\u001b[39m CONTINUE:\n\u001b[0;32m 333\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\http\\client.py:292\u001b[0m, in \u001b[0;36mHTTPResponse._read_status\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 291\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_read_status\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m--> 292\u001b[0m line \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreadline\u001b[49m\u001b[43m(\u001b[49m\u001b[43m_MAXLINE\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124miso-8859-1\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 293\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(line) \u001b[38;5;241m>\u001b[39m _MAXLINE:\n\u001b[0;32m 294\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m LineTooLong(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstatus line\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\socket.py:707\u001b[0m, in \u001b[0;36mSocketIO.readinto\u001b[1;34m(self, b)\u001b[0m\n\u001b[0;32m 705\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m 706\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 707\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_sock\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrecv_into\u001b[49m\u001b[43m(\u001b[49m\u001b[43mb\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 708\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m timeout:\n\u001b[0;32m 709\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_timeout_occurred \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\ssl.py:1252\u001b[0m, in \u001b[0;36mSSLSocket.recv_into\u001b[1;34m(self, buffer, nbytes, flags)\u001b[0m\n\u001b[0;32m 1248\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m flags \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m 1249\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[0;32m 1250\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnon-zero flags not allowed in calls to recv_into() on \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m%\u001b[39m\n\u001b[0;32m 1251\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__class__\u001b[39m)\n\u001b[1;32m-> 1252\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnbytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1253\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 1254\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mrecv_into(buffer, nbytes, flags)\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\ssl.py:1104\u001b[0m, in \u001b[0;36mSSLSocket.read\u001b[1;34m(self, len, buffer)\u001b[0m\n\u001b[0;32m 1102\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 1103\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m buffer \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m-> 1104\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_sslobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1105\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 1106\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_sslobj\u001b[38;5;241m.\u001b[39mread(\u001b[38;5;28mlen\u001b[39m)\n", + "\u001b[1;31mKeyboardInterrupt\u001b[0m: " + ] } ], "source": [ @@ -164,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "64d37c40", "metadata": {}, "outputs": [ @@ -172,63 +152,23 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-2405171879c44f5aac716b6b0ca23b92': send 'start'\n", - "0:00:17 Job 'j-2405171879c44f5aac716b6b0ca23b92': created (progress 0%)\n", - "0:00:22 Job 'j-2405171879c44f5aac716b6b0ca23b92': created (progress 0%)\n", - "0:00:29 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:00:37 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:00:47 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:01:00 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:01:16 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:01:36 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:02:11 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:02:41 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:03:19 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:04:06 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:05:05 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:06:05 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:07:06 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:08:07 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:09:08 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:10:08 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:11:09 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:12:10 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:13:18 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:14:19 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:15:20 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:16:21 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:17:21 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:18:22 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:19:30 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:20:31 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:21:31 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:22:32 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:23:33 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:24:34 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:25:35 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:26:37 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:27:39 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:28:40 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:29:41 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:30:43 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:31:44 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:32:44 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:33:45 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:34:46 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:35:47 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:36:48 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:37:48 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:38:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:39:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:40:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:41:53 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:42:54 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:43:54 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:44:55 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:46:01 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:47:01 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:48:03 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:49:04 Job 'j-2405171879c44f5aac716b6b0ca23b92': finished (progress 100%)\n" + "0:00:00 Job 'j-240522d4b46c4b85a05d7bac60dbd894': send 'start'\n", + "0:00:20 Job 'j-240522d4b46c4b85a05d7bac60dbd894': created (progress 0%)\n", + "0:00:25 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:00:34 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:00:42 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:00:52 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:01:05 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:01:21 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:01:40 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:02:05 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:02:36 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:03:13 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:04:00 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:04:59 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:06:00 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:07:00 Job 'j-240522d4b46c4b85a05d7bac60dbd894': running (progress N/A)\n", + "0:08:01 Job 'j-240522d4b46c4b85a05d7bac60dbd894': finished (progress 100%)\n" ] }, { @@ -250,15 +190,15 @@ " }\n", " \n", " \n", - " \n", + " \n", " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -293,6 +233,43 @@ " 'executor-memoryOverhead':'8g'} )" ] }, + { + "cell_type": "code", + "execution_count": 8, + "id": "88755080", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[ 1.28030717, 1.24561238, 0.89612383, ..., -0.88404161,\n", + " -0.86419362, -0.94284689],\n", + " [ 1.18315971, 1.24996054, 1.00327194, ..., -0.86914986,\n", + " -0.81153351, -0.78880918],\n", + " [ 1.22972143, 1.34015703, 0.4644534 , ..., -0.88735497,\n", + " -0.86509544, -0.75152397],\n", + " ...,\n", + " [-0.04115722, -0.09354208, 0.06488457, ..., 0.52544767,\n", + " 0.68004614, 0.2031192 ],\n", + " [ 0.16457513, 0.18039979, 0.25627238, ..., 0.43219674,\n", + " 0.49971986, 0.24406503],\n", + " [ 0.20367333, 0.1553583 , 0.22572494, ..., -0.35009685,\n", + " -0.44863623, -0.45004168]]])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import xarray as xr\n", + "import matplotlib.pyplot as plt\n", + "\n", + "output = xr.open_dataset(outputfile_name)\n", + "output['presto_1'].values" + ] + }, { "cell_type": "markdown", "id": "48c9322c", @@ -303,7 +280,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "id": "8f71136c-1252-4786-8609-8bb995da7daf", "metadata": { "tags": [] @@ -313,140 +290,44 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': send 'start'\n", - "0:00:16 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:28 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:49 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:01:02 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:01:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:01:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:02:03 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:02:34 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:03:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:03:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:04:58 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:05:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:07:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:08:01 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:09:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:10:06 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:11:07 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:12:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:13:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:14:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:15:22 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:16:23 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:17:23 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:18:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:19:25 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:20:28 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:21:29 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:22:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:23:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:24:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:25:44 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:26:45 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:27:45 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:28:46 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:29:47 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:30:49 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:31:50 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:32:51 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:33:51 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:34:52 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:35:52 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:36:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:37:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:38:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:39:54 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:40:54 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:41:55 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:42:55 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:44:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:45:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:46:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:47:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:48:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:49:16 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:50:17 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:51:17 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:52:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:53:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:54:19 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:55:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:56:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:57:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:58:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:59:22 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:00:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:01:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:02:25 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:03:30 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:04:32 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:05:33 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:06:34 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:07:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:08:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:09:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:10:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:11:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:12:40 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:13:40 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:14:41 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:15:42 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:16:42 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:17:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:18:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:19:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:20:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:21:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:23:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:24:01 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:25:03 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:26:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:27:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:28:05 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:29:06 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:30:07 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:31:08 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:32:08 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:33:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:34:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:35:14 Job 'j-240517a75f8846a88725dcb3c5da55a5': finished (progress 100%)\n" + "0:00:00 Job 'j-2405229dc1614b34a394f847fafe77c2': send 'start'\n", + "0:00:25 Job 'j-2405229dc1614b34a394f847fafe77c2': created (progress 0%)\n", + "0:00:31 Job 'j-2405229dc1614b34a394f847fafe77c2': created (progress 0%)\n", + "0:00:38 Job 'j-2405229dc1614b34a394f847fafe77c2': created (progress 0%)\n", + "0:00:46 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:00:58 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:01:11 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:01:27 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:01:48 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:02:13 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:02:43 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:03:21 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:04:08 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:05:08 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:06:09 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:07:09 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:08:10 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:09:12 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:10:13 Job 'j-2405229dc1614b34a394f847fafe77c2': running (progress N/A)\n", + "0:11:14 Job 'j-2405229dc1614b34a394f847fafe77c2': error (progress N/A)\n", + "Your batch job 'j-2405229dc1614b34a394f847fafe77c2' failed. Error logs:\n", + "[]\n", + "Full logs can be inspected in an openEO (web) editor or with `connection.job('j-2405229dc1614b34a394f847fafe77c2').logs()`.\n" ] }, { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" + "ename": "JobFailedException", + "evalue": "Batch job 'j-2405229dc1614b34a394f847fafe77c2' didn't finish successfully. Status: error (after 0:11:15).", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mJobFailedException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[3], line 21\u001b[0m\n\u001b[0;32m 7\u001b[0m prediction \u001b[38;5;241m=\u001b[39m input_cube\u001b[38;5;241m.\u001b[39mapply_neighborhood(\n\u001b[0;32m 8\u001b[0m process\u001b[38;5;241m=\u001b[39mudf,\n\u001b[0;32m 9\u001b[0m size\u001b[38;5;241m=\u001b[39m[\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 16\u001b[0m ],\n\u001b[0;32m 17\u001b[0m )\n\u001b[0;32m 19\u001b[0m prediction \u001b[38;5;241m=\u001b[39m prediction\u001b[38;5;241m.\u001b[39mrename_labels(dimension\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mbands\u001b[39m\u001b[38;5;124m\"\u001b[39m,target\u001b[38;5;241m=\u001b[39m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124moutput_catboost\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n\u001b[1;32m---> 21\u001b[0m \u001b[43mprediction\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43moutputfile\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43moutputfile_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 22\u001b[0m \u001b[43m \u001b[49m\u001b[43mdescription\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mworld cereal inference\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 23\u001b[0m \u001b[43m \u001b[49m\u001b[43mjob_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mdriver-memory\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m4g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 24\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mexecutor-memoryOverhead\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m:\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m8g\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\datacube.py:2227\u001b[0m, in \u001b[0;36mDataCube.execute_batch\u001b[1;34m(self, outputfile, out_format, print, max_poll_interval, connection_retry_interval, job_options, validate, **format_options)\u001b[0m\n\u001b[0;32m 2224\u001b[0m out_format \u001b[38;5;241m=\u001b[39m guess_format(outputfile)\n\u001b[0;32m 2226\u001b[0m job \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_job(out_format\u001b[38;5;241m=\u001b[39mout_format, job_options\u001b[38;5;241m=\u001b[39mjob_options, validate\u001b[38;5;241m=\u001b[39mvalidate, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mformat_options)\n\u001b[1;32m-> 2227\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_synchronous\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2228\u001b[0m \u001b[43m \u001b[49m\u001b[43moutputfile\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutputfile\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2229\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 2230\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:239\u001b[0m, in \u001b[0;36mBatchJob.run_synchronous\u001b[1;34m(self, outputfile, print, max_poll_interval, connection_retry_interval)\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_synchronous\u001b[39m(\n\u001b[0;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m, outputfile: Union[\u001b[38;5;28mstr\u001b[39m, Path, \u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 236\u001b[0m \u001b[38;5;28mprint\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mprint\u001b[39m, max_poll_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m, connection_retry_interval\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m\n\u001b[0;32m 237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BatchJob:\n\u001b[0;32m 238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Start the job, wait for it to finish and download result\"\"\"\u001b[39;00m\n\u001b[1;32m--> 239\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstart_and_wait\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_poll_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmax_poll_interval\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconnection_retry_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconnection_retry_interval\u001b[49m\n\u001b[0;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 242\u001b[0m \u001b[38;5;66;03m# TODO #135 support multi file result sets too?\u001b[39;00m\n\u001b[0;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m outputfile \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[1;32mc:\\Users\\VROMPAYH\\AppData\\Local\\anaconda3\\envs\\wc_presto\\Lib\\site-packages\\openeo\\rest\\job.py:321\u001b[0m, in \u001b[0;36mBatchJob.start_and_wait\u001b[1;34m(self, print, max_poll_interval, connection_retry_interval, soft_error_max)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs(level\u001b[38;5;241m=\u001b[39mlogging\u001b[38;5;241m.\u001b[39mERROR))\n\u001b[0;32m 318\u001b[0m \u001b[38;5;28mprint\u001b[39m(\n\u001b[0;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFull logs can be inspected in an openEO (web) editor or with `connection.job(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m).logs()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 320\u001b[0m )\n\u001b[1;32m--> 321\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m JobFailedException(\n\u001b[0;32m 322\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBatch job \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m didn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt finish successfully. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (after \u001b[39m\u001b[38;5;132;01m{\u001b[39;00melapsed()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 323\u001b[0m job\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 324\u001b[0m )\n\u001b[0;32m 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", + "\u001b[1;31mJobFailedException\u001b[0m: Batch job 'j-2405229dc1614b34a394f847fafe77c2' didn't finish successfully. Status: error (after 0:11:15)." + ] } ], "source": [ diff --git a/minimal_wc_presto/dev_testing.py b/minimal_wc_presto/dev_testing.py index d937f482..4138680d 100644 --- a/minimal_wc_presto/dev_testing.py +++ b/minimal_wc_presto/dev_testing.py @@ -9,17 +9,17 @@ #%% GET DEPENDENCIES - +import urllib # Generate absolute path for the dependencies folder dependencies_dir = Path.cwd() / 'dependencies' dependencies_dir.mkdir(exist_ok=True, parents=True) -base_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference' +base_url = 'https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies' dependency_name = "wc_presto_onnx_dependencies.zip" # Download and extract the model file modelfile_url = f"{base_url}/{dependency_name}" -#modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) +modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) #shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) #Add the model directory to system path if it's not already there @@ -43,6 +43,7 @@ arr = ds.drop('crs').to_array(dim='bands') +arr[:,:,50:,50:] = np.nan orig_dims = list(arr.dims) map_dims = arr.shape[2:] @@ -64,13 +65,11 @@ CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx' classification = classify_with_catboost(features, CATBOOST_PATH) +#%% - -#%%revert to xarray +#%%plot output import matplotlib.pyplot as plt - - transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) longitudes, latitudes = transformer.transform(arr.x, arr.y) classification = np.flip(classification.reshape(map_dims),axis = 0) @@ -80,4 +79,5 @@ output = output.to_numpy().squeeze() plt.imshow(output) -output.shape \ No newline at end of file +output.shape +# %% diff --git a/minimal_wc_presto/udf_presto.py b/minimal_wc_presto/udf_presto.py index 95e38c06..6b853f43 100644 --- a/minimal_wc_presto/udf_presto.py +++ b/minimal_wc_presto/udf_presto.py @@ -15,7 +15,7 @@ def _setup_logging(): logger = logging.getLogger(__name__) return logger -@functools.lru_cache(maxsize=6) +@functools.lru_cache(maxsize=25) def extract_dependencies(base_url: str, dependency_name: str): # Generate absolute path for the dependencies folder @@ -39,15 +39,19 @@ def extract_dependencies(base_url: str, dependency_name: str): def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger = _setup_logging() + # shape and indiches for output + cube = cube.transpose('bands', 't', 'x', 'y') + cube = cube.fillna(65535) orig_dims = list(cube.dims) map_dims = cube.shape[2:] - cube = cube.fillna(65535) logger.info("Unzipping dependencies") - base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" + #base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" + base_url = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" + dependency_name = "wc_presto_onnx_dependencies.zip" logger.info("Appending depencency") @@ -66,18 +70,22 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" features = get_presto_features(cube, PRESTO_PATH, 32631) - # go to 128,1,100,100 - presto_dim = map_dims + (128,) - features = features.reshape(presto_dim) - features = np.expand_dims(features, axis = 0) - features = np.transpose(features, (3, 0, 1, 2)) + # go to 128, 1,100,100 (time, bands, x, y) + presto_dim = map_dims + (128,) + logger.info(str(features.shape)) + features = features.reshape(presto_dim) #100,100,128 + logger.info(str(features.shape)) + features = np.expand_dims(features, axis = 0) #1,100,100,128 + logger.info(str(features.shape)) + features = np.transpose(features, (3, 0, 1, 2)) #128,1,100,100 + logger.info(str(features.shape)) transformer = Transformer.from_crs(f"EPSG:{32631}", "EPSG:4326", always_xy=True) longitudes, latitudes = transformer.transform(cube.x, cube.y) - output = xr.DataArray(features, dims=orig_dims, coords={'y': longitudes, 'x': latitudes}) + output = xr.DataArray(features, dims=orig_dims, coords={'x': longitudes, 'y': latitudes}) return output diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index 5fceeb95..07384884 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -42,13 +42,14 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger.info("Shape of input: {}".format(cube.shape)) # shape and indiches for output + cube = cube.transpose('bands', 't', 'x', 'y') + cube = cube.fillna(65535) orig_dims = list(cube.dims) map_dims = cube.shape[2:] - cube = cube.fillna(65535) # Unzip de dependencies on the backend logger.info("Unzipping dependencies") - base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" + base_url = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" dependency_name = "wc_presto_onnx_dependencies.zip" dep_dir = extract_dependencies(base_url, dependency_name) @@ -70,13 +71,13 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger.info("Shape of classification output: {}".format(classification.shape)) # revert to 4D shape for openEO - #logger.info("Revert to 4D xarray") - #transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) - #longitudes, latitudes = transformer.transform(cube.x, cube.y) + logger.info("Revert to 4D xarray") + transformer = Transformer.from_crs(f"EPSG:{32631}", "EPSG:4326", always_xy=True) + longitudes, latitudes = transformer.transform(cube.x, cube.y) - classification = np.flip(classification.reshape(map_dims),axis = 0) + classification = classification.reshape(map_dims) classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) - output = xr.DataArray(classification, dims=orig_dims) + output = xr.DataArray(classification, dims=orig_dims, coords={'x': longitudes, 'y': latitudes}) logger.info("Shape of output: {}".format(output.shape)) return output From af151f7b5660e4e64c0906fcb172e854347eb177 Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Wed, 22 May 2024 15:48:55 +0200 Subject: [PATCH 12/31] fix: udf_long --- .../udf_long_worldcereal_inference.py | 19 ++++++++++--------- .../udf_worldcereal_inference.py | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index 9040c36a..cb34a64f 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -46,19 +46,21 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger.info("Shape of input: {}".format(cube.shape)) # shape and indiches for output + cube = cube.transpose('bands', 't', 'x', 'y') + cube = cube.fillna(65535) orig_dims = list(cube.dims) map_dims = cube.shape[2:] - cube = cube.fillna(65535) # Unzip de dependencies on the backend logger.info("Unzipping dependencies") - base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" + base_url = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" dependency_name = "wc_presto_onnx_dependencies.zip" dep_dir = extract_dependencies(base_url, dependency_name) # Append the dependencies sys.path.append(str(dep_dir)) sys.path.append(str(dep_dir) + '/pandas') + ################################################################################################################### @@ -435,7 +437,6 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr ################################################################################################################### - # Run presto inference logger.info("Extracting presto features") PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" @@ -449,13 +450,13 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr logger.info("Shape of classification output: {}".format(classification.shape)) # revert to 4D shape for openEO - #logger.info("Revert to 4D xarray") - #transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) - #longitudes, latitudes = transformer.transform(cube.x, cube.y) + logger.info("Revert to 4D xarray") + transformer = Transformer.from_crs(f"EPSG:{32631}", "EPSG:4326", always_xy=True) + longitudes, latitudes = transformer.transform(cube.x, cube.y) - classification = np.flip(classification.reshape(map_dims),axis = 0) - classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) - output = xr.DataArray(classification, dims=orig_dims) + classification = classification.reshape(map_dims) + classification = np.flip(np.expand_dims(np.expand_dims(classification, axis=0), axis=0)) + output = xr.DataArray(classification, dims=orig_dims, coords={'x': longitudes, 'y': latitudes}) logger.info("Shape of output: {}".format(output.shape)) return output diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index 07384884..6d8a37f4 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -76,7 +76,7 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: longitudes, latitudes = transformer.transform(cube.x, cube.y) classification = classification.reshape(map_dims) - classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) + classification = np.flip(np.expand_dims(np.expand_dims(classification, axis=0), axis=0)) output = xr.DataArray(classification, dims=orig_dims, coords={'x': longitudes, 'y': latitudes}) logger.info("Shape of output: {}".format(output.shape)) From 44f9651efc889fc932c2e1ee5e561ac7d6ef884f Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Thu, 23 May 2024 10:01:49 +0200 Subject: [PATCH 13/31] Updated UDF (still flips result though!) --- .../udf_long_worldcereal_inference.py | 88 ++++++++----------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index 6d210148..bd0c16a4 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -34,33 +34,33 @@ def extract_dependencies(base_url: str, dependency_name: str): shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) # Add the model directory to system path if it's not already there - abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) + abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) # NOQA - return abs_path + # Append the dependencies + sys.path.append(str(abs_path)) + sys.path.append(str(abs_path) + "/pandas") + + return def apply_datacube(cube: xr.DataArray, context: Dict) -> xr.DataArray: + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA + PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA + BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA + DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + logger = _setup_logging() - logger.info("Shape of input: {}".format(cube.shape)) - # shape and indiches for output - orig_dims = list(cube.dims) + # Handle NaN values in Presto compatible way + cube = cube.fillna(65535) # Unzip de dependencies on the backend logger.info("Unzipping dependencies") - base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" - dependency_name = "wc_presto_onnx_dependencies.zip" - dep_dir = extract_dependencies(base_url, dependency_name) - - # Append the dependencies - sys.path.append(str(dep_dir)) - sys.path.append(str(dep_dir) + "/pandas") - - ################################################################################################################### + extract_dependencies(BASE_URL, DEPENDENCY_NAME) + ########################################################################## import onnxruntime - import pandas as pd import requests import torch from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( @@ -100,8 +100,6 @@ def load_model(self, model): model_path (str): The path to the ONNX model file. """ # Load the dependency into an InferenceSession - import onnxruntime - self.onnx_session = onnxruntime.InferenceSession(model) def predict(self, features: np.ndarray) -> np.ndarray: @@ -109,7 +107,7 @@ def predict(self, features: np.ndarray) -> np.ndarray: Predicts labels using the provided features DataFrame. Args: - features (pd.DataFrame): DataFrame containing the features for prediction. + features (pd.ndarray): 2D array containing the features Returns: pd.DataFrame: DataFrame containing the predicted labels. @@ -133,7 +131,6 @@ def predict(self, features: np.ndarray) -> np.ndarray: return binary_labels class PrestoFeatureExtractor: - def __init__(self, model: Presto): """ Initialize the PrestoFeatureExtractor with a Presto model. @@ -303,7 +300,6 @@ def _create_dataloader( def _create_presto_input( cls, inarr: xr.DataArray, epsg: int = 4326 ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - eo_data, mask = cls._extract_eo_data(inarr) latlons = cls._extract_latlons(inarr, epsg) months = cls._extract_months(inarr) @@ -368,7 +364,9 @@ def extract_presto_features( ) ft_names = [f"presto_ft_{i}" for i in range(128)] features = xr.DataArray( - features, coords={"x": inarr.x, "y": inarr.y, "bands": ft_names} + features, + coords={"x": inarr.x, "y": inarr.y, "bands": ft_names}, + dims=["x", "y", "bands"], ) return features @@ -393,12 +391,14 @@ def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: features = presto_extractor.extract_presto_features(inarr, epsg=32631) return features - def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarray: + def classify_with_catboost( + features: xr.DataArray, catboost_path: str + ) -> np.ndarray: """ Classifies features using the WorldCereal CatBoost model. Args: - features (np.ndarray): Features to be classified. + features (xr.DataArray): Features to be classified [x, y, fts] map_dims (tuple): Original x, y dimensions of the input data. model_path (str): Path to the trained CatBoost model. @@ -406,45 +406,33 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr xr.DataArray: Classified data as xarray DataArray. """ + # Stack the features and transpose for feeding to CatBoost + stacked_features = features.stack(xy=["x", "y"]).transpose() + predictor = WorldCerealPredictor() response = requests.get(catboost_path) catboost_model = response.content predictor.load_model(catboost_model) - predictions = predictor.predict(features) + predictions = predictor.predict(stacked_features.values) + + predictions = ( + xr.DataArray(predictions, coords={"xy": stacked_features.xy}, dims=["xy"]) + .unstack() + .expand_dims(dim="bands") + ) return predictions ################################################################################################################### - # Run presto inference + # Run presto feature extraction logger.info("Extracting presto features") - PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" features = get_presto_features(cube, PRESTO_PATH) - logger.info("Shape of presto output: {}".format(features.shape)) - # run catboost classification + # Run catboost classification logger.info("Catboost classification") - CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" - stacked_features = features.stack(xy=["x", "y"]).transpose() - classification = classify_with_catboost(stacked_features.values, CATBOOST_PATH) - classification = ( - xr.DataArray(classification, coords={"xy": stacked_features.xy}) - .unstack() - .expand_dims(dim="bands") - .expand_dims(dim="t") - ) - logger.info("Shape of classification output: {}".format(classification.shape)) - - # revert to 4D shape for openEO - # logger.info("Revert to 4D xarray") - # transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) - # longitudes, latitudes = transformer.transform(cube.x, cube.y) - - # classification = np.flip(classification.reshape(map_dims),axis = 0) - # classification = np.expand_dims(np.expand_dims(classification, axis=0), axis=0) - # output = xr.DataArray(classification, dims=orig_dims) - output = classification.transpose(*orig_dims) - logger.info("Shape of output: {}".format(output.shape)) + classification = classify_with_catboost(features, CATBOOST_PATH) - return output + # Add time dimension and return result + return classification.expand_dims(dim="t") From e0ca6169a63873ea184037886916c2c5cf3d8894 Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Thu, 23 May 2024 15:59:30 +0200 Subject: [PATCH 14/31] user order="F" for reshaping fixes the flipping issue --- minimal_wc_presto/udf_long_worldcereal_inference.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index bd0c16a4..1053bd5f 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -203,8 +203,12 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: for org_band, presto_band in cls.BAND_MAPPING.items(): if org_band in inarr.coords["bands"]: + # Use order "F" to make it work on OpenEO backend! + # TODO: VERIFY WHY THIS IS NEEDED values = np.swapaxes( - inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), + inarr.sel(bands=org_band).values.reshape( + (num_timesteps, -1), order="F" + ), 0, 1, ) From 7968ba03dee704a5ea797d2d580f0b1a0a1708f9 Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Fri, 24 May 2024 10:06:07 +0200 Subject: [PATCH 15/31] Avoid use of rearrange. Bug remains. --- .../udf_long_worldcereal_inference.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index 1053bd5f..5d115e1f 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -75,7 +75,6 @@ def apply_datacube(cube: xr.DataArray, context: Dict) -> xr.DataArray: ) from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device - from einops import rearrange from torch.utils.data import DataLoader, TensorDataset # Index to band groups mapping @@ -206,9 +205,7 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: # Use order "F" to make it work on OpenEO backend! # TODO: VERIFY WHY THIS IS NEEDED values = np.swapaxes( - inarr.sel(bands=org_band).values.reshape( - (num_timesteps, -1), order="F" - ), + inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1, ) @@ -237,7 +234,9 @@ def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: f"EPSG:{epsg}", "EPSG:4326", always_xy=True ) lon, lat = transformer.transform(lon, lat) - latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") + num_pixels = len(inarr.x) * len(inarr.y) + latlons = np.swapaxes(np.stack([lat, lon]), 0, 2).reshape((num_pixels, 2)) + # latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") # 2D array where each row represents a pair of latitude and longitude coordinates. return latlons @@ -363,9 +362,11 @@ def extract_presto_features( dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) features = self._get_encodings(dl) - features = rearrange( - features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) - ) + features = features.reshape((len(inarr.x), len(inarr.y), 128)) + + # features = rearrange( + # features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) + # ) ft_names = [f"presto_ft_{i}" for i in range(128)] features = xr.DataArray( features, From a579be711dc76e138ec1ac804302bf3585c06d38 Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Fri, 24 May 2024 15:37:45 +0200 Subject: [PATCH 16/31] Avoid the use of np.swapaxes --- .../udf_long_worldcereal_inference.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index 5d115e1f..e9717362 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -52,6 +52,10 @@ def apply_datacube(cube: xr.DataArray, context: Dict) -> xr.DataArray: logger = _setup_logging() + # Deterministic ordering + cube = cube.transpose("bands", "t", "x", "y") + orig_dims = list(cube.dims) + # Handle NaN values in Presto compatible way cube = cube.fillna(65535) @@ -75,6 +79,7 @@ def apply_datacube(cube: xr.DataArray, context: Dict) -> xr.DataArray: ) from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device + from einops import rearrange from torch.utils.data import DataLoader, TensorDataset # Index to band groups mapping @@ -202,12 +207,8 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: for org_band, presto_band in cls.BAND_MAPPING.items(): if org_band in inarr.coords["bands"]: - # Use order "F" to make it work on OpenEO backend! - # TODO: VERIFY WHY THIS IS NEEDED - values = np.swapaxes( - inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), - 0, - 1, + values = rearrange( + inarr.sel(bands=org_band).values, "t x y -> (x y) t" ) idx_valid = values != cls._NODATAVALUE values = cls._preprocess_band_values(values, presto_band) @@ -234,9 +235,7 @@ def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: f"EPSG:{epsg}", "EPSG:4326", always_xy=True ) lon, lat = transformer.transform(lon, lat) - num_pixels = len(inarr.x) * len(inarr.y) - latlons = np.swapaxes(np.stack([lat, lon]), 0, 2).reshape((num_pixels, 2)) - # latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") + latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") # 2D array where each row represents a pair of latitude and longitude coordinates. return latlons @@ -362,11 +361,9 @@ def extract_presto_features( dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) features = self._get_encodings(dl) - features = features.reshape((len(inarr.x), len(inarr.y), 128)) - - # features = rearrange( - # features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) - # ) + features = rearrange( + features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) + ) ft_names = [f"presto_ft_{i}" for i in range(128)] features = xr.DataArray( features, @@ -439,5 +436,6 @@ def classify_with_catboost( logger.info("Catboost classification") classification = classify_with_catboost(features, CATBOOST_PATH) - # Add time dimension and return result - return classification.expand_dims(dim="t") + # Add time dimension + classification = classification.expand_dims(dim="t") + return classification From 42218f09e347f2f853c64a74f3d46dd2efa46816 Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Fri, 24 May 2024 16:14:04 +0200 Subject: [PATCH 17/31] Add a comment for clarification --- minimal_wc_presto/udf_long_worldcereal_inference.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py index e9717362..2671c033 100644 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ b/minimal_wc_presto/udf_long_worldcereal_inference.py @@ -52,9 +52,9 @@ def apply_datacube(cube: xr.DataArray, context: Dict) -> xr.DataArray: logger = _setup_logging() - # Deterministic ordering + # The below is required to avoid flipping of the result + # when running on OpenEO backend! cube = cube.transpose("bands", "t", "x", "y") - orig_dims = list(cube.dims) # Handle NaN values in Presto compatible way cube = cube.fillna(65535) @@ -438,4 +438,5 @@ def classify_with_catboost( # Add time dimension classification = classification.expand_dims(dim="t") + return classification From 919391c2657984c2fff12f39083d119e9c74276a Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Fri, 24 May 2024 16:33:41 +0200 Subject: [PATCH 18/31] Updated inference notebook --- .../backend_inference_example_openeo.ipynb | 467 ++++++------------ 1 file changed, 163 insertions(+), 304 deletions(-) diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index 2cdf4c68..a0838ede 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -10,7 +10,20 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, + "id": "7c7532bf-5341-4a6e-a81f-85ded18e6a85", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", "metadata": { "tags": [] @@ -28,7 +41,6 @@ "import openeo\n", "from datetime import datetime \n", "\n", - "\n", "#token SENTINEL\n", "connection = openeo.connect(\"https://openeo.dataspace.copernicus.eu/\").authenticate_oidc()" ] @@ -43,16 +55,19 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "5494c46d", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "#Get desired data\n", "from preprocessing import worldcereal_preprocessed_inputs\n", "\n", - "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.19, 51.25, 5.21, 51.26]))\n", - "EXTENT['crs'] = \"EPSG:4326\"\n", + "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [664000.0, 5611120.0, 665000.0, 5612120.0]))\n", + "EXTENT['crs'] = \"EPSG:32631\"\n", + "EXTENT['srs'] = \"EPSG:32631\"\n", "\n", "STARTDATE = '2020-11-01'\n", "ENDDATE = '2021-10-31'\n", @@ -76,41 +91,41 @@ "id": "da8d05cd", "metadata": {}, "source": [ - "Save the input cube" + "## Save preprocessed inputs\n", + "\n", + "Only required if you want to save the intermediate input cube" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "id": "4aab5695", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-240517a35acc48b697839a923dd5fe56': send 'start'\n", - "0:00:18 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:23 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:30 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:38 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:00:48 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:01:02 Job 'j-240517a35acc48b697839a923dd5fe56': created (progress 0%)\n", - "0:01:18 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:01:38 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:02:04 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:02:35 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:03:13 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:04:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:04:59 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:05:59 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:07:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:08:00 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:09:01 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:10:46 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:12:38 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:13:39 Job 'j-240517a35acc48b697839a923dd5fe56': running (progress N/A)\n", - "0:14:39 Job 'j-240517a35acc48b697839a923dd5fe56': finished (progress 100%)\n" + "0:00:00 Job 'j-2405213e660a4308ac7c9b6300206ec4': send 'start'\n", + "0:00:15 Job 'j-2405213e660a4308ac7c9b6300206ec4': created (progress 0%)\n", + "0:00:21 Job 'j-2405213e660a4308ac7c9b6300206ec4': created (progress 0%)\n", + "0:00:27 Job 'j-2405213e660a4308ac7c9b6300206ec4': created (progress 0%)\n", + "0:00:35 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:00:45 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:00:58 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:01:13 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:01:33 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:01:58 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:02:28 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:03:06 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:03:53 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:04:51 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:05:52 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:06:59 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:08:00 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", + "0:09:01 Job 'j-2405213e660a4308ac7c9b6300206ec4': finished (progress 100%)\n" ] }, { @@ -132,15 +147,15 @@ " }\n", " \n", " \n", - " \n", + " \n", " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 6, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -159,76 +174,43 @@ "id": "bc85fadd", "metadata": {}, "source": [ - "Run the presto UDF and fetch presto features" + "## Run end-to-end inference job\n" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 149, "id": "64d37c40", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-2405171879c44f5aac716b6b0ca23b92': send 'start'\n", - "0:00:17 Job 'j-2405171879c44f5aac716b6b0ca23b92': created (progress 0%)\n", - "0:00:22 Job 'j-2405171879c44f5aac716b6b0ca23b92': created (progress 0%)\n", - "0:00:29 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:00:37 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:00:47 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:01:00 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:01:16 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:01:36 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:02:11 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:02:41 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:03:19 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:04:06 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:05:05 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:06:05 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:07:06 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:08:07 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:09:08 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:10:08 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:11:09 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:12:10 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:13:18 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:14:19 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:15:20 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:16:21 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:17:21 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:18:22 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:19:30 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:20:31 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:21:31 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:22:32 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:23:33 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:24:34 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:25:35 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:26:37 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:27:39 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:28:40 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:29:41 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:30:43 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:31:44 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:32:44 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:33:45 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:34:46 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:35:47 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:36:48 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:37:48 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:38:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:39:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:40:50 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:41:53 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:42:54 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:43:54 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:44:55 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:46:01 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:47:01 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:48:03 Job 'j-2405171879c44f5aac716b6b0ca23b92': running (progress N/A)\n", - "0:49:04 Job 'j-2405171879c44f5aac716b6b0ca23b92': finished (progress 100%)\n" + "0:00:00 Job 'j-24052404d20b4be09dca30c5a09413da': send 'start'\n", + "0:00:21 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", + "0:00:28 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", + "0:00:38 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", + "0:00:46 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", + "0:01:04 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", + "0:01:17 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", + "0:01:33 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", + "0:01:52 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:02:17 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:02:47 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:03:25 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:04:12 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:05:11 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:06:12 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:07:14 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:08:15 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:09:15 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:10:16 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:11:19 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:12:20 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", + "0:13:20 Job 'j-24052404d20b4be09dca30c5a09413da': finished (progress 100%)\n" ] }, { @@ -250,15 +232,15 @@ " }\n", " \n", " \n", - " \n", + " \n", " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 7, + "execution_count": 149, "metadata": {}, "output_type": "execute_result" } @@ -270,7 +252,7 @@ "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", "\n", - "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", + "udf = openeo.UDF.from_file(\"udf_long_worldcereal_inference.py\")\n", "\n", "prediction = input_cube.apply_neighborhood(\n", " process=udf,\n", @@ -284,8 +266,7 @@ " ],\n", ")\n", "\n", - "presto_list = [\"presto_\" + str(i) for i in range(1, 129)]\n", - "prediction = prediction.rename_labels(dimension=\"bands\",target= presto_list)\n", + "prediction = prediction.drop_dimension('t').rename_labels(\"bands\", [\"classification\"])\n", "\n", "prediction.execute_batch(outputfile = outputfile_name,\n", " description='world cereal inference',\n", @@ -295,261 +276,139 @@ }, { "cell_type": "markdown", - "id": "48c9322c", + "id": "1f716b7a", "metadata": {}, "source": [ - "Calculate the presto features and run the classifier on top" + "Fetch the output and visualise" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "8f71136c-1252-4786-8609-8bb995da7daf", + "execution_count": 32, + "id": "2cf64980", "metadata": { "tags": [] }, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': send 'start'\n", - "0:00:16 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:28 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:00:49 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:01:02 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:01:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': created (progress 0%)\n", - "0:01:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:02:03 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:02:34 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:03:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:03:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:04:58 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:05:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:07:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:08:01 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:09:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:10:06 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:11:07 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:12:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:13:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:14:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:15:22 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:16:23 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:17:23 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:18:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:19:25 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:20:28 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:21:29 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:22:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:23:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:24:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:25:44 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:26:45 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:27:45 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:28:46 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:29:47 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:30:49 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:31:50 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:32:51 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:33:51 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:34:52 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:35:52 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:36:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:37:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:38:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:39:54 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:40:54 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:41:55 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:42:55 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:44:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:45:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:46:12 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:47:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:48:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:49:16 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:50:17 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:51:17 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:52:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:53:18 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:54:19 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:55:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:56:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:57:20 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:58:21 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "0:59:22 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:00:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:01:24 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:02:25 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:03:30 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:04:32 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:05:33 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:06:34 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:07:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:08:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:09:35 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:10:38 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:11:39 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:12:40 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:13:40 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:14:41 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:15:42 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:16:42 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:17:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:18:43 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:19:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:20:53 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:21:59 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:23:00 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:24:01 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:25:03 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:26:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:27:04 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:28:05 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:29:06 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:30:07 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:31:08 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:32:08 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:33:11 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:34:13 Job 'j-240517a75f8846a88725dcb3c5da55a5': running (progress N/A)\n", - "1:35:14 Job 'j-240517a75f8846a88725dcb3c5da55a5': finished (progress 100%)\n" - ] + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" }, { "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], + "image/png": "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", "text/plain": [ - "" + "
" ] }, - "execution_count": 8, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ + "import xarray as xr\n", + "import matplotlib.pyplot as plt\n", "\n", - "current_datetime = datetime.now()\n", - "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", - "\n", - "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", - "\n", - "prediction = input_cube.apply_neighborhood(\n", - " process=udf,\n", - " size=[\n", - " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", - " ],\n", - " overlap=[\n", - " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", - " ],\n", - ")\n", - "\n", - "prediction = prediction.rename_labels(dimension=\"bands\",target= [\"output_catboost\"])\n", - "\n", - "prediction.execute_batch(outputfile = outputfile_name,\n", - " description='world cereal inference',\n", - " job_options={'driver-memory': '4g',\n", - " 'executor-memoryOverhead':'8g'} )\n" - ] - }, - { - "cell_type": "markdown", - "id": "1f716b7a", - "metadata": {}, - "source": [ - "Fetch the output and visualise" + "output = xr.open_dataset('2024_05_23_10_00_45_output_presto.nc')\n", + "# output = output['output_catboost'].to_numpy().squeeze()\n", + "# plt.imshow(output)\n", + "plt.imshow(output['classification'])\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "2cf64980", - "metadata": {}, + "execution_count": 125, + "id": "f18b1535", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/plain": [ - "(116, 144)" + "False" ] }, - "execution_count": 10, + "execution_count": 125, "metadata": {}, "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ - "import xarray as xr\n", - "import matplotlib.pyplot as plt\n", + "# # latlons = rearrange(np.stack([lat, lon]), \"c x y -> (x y) c\")\n", + "# # otherlatlons = np.stack([lat, lon]).transpose(1, 2, 0).reshape((len(inarr.x) * len(inarr.y), 2))\n", + "\n", + "# # np.array_equal(latlons, otherlatlons)\n", "\n", - "output = xr.open_dataset('2024_05_17_14_56_51_output_worldcereal.nc')\n", - "output = output['output_catboost'].to_numpy().squeeze()\n", - "plt.imshow(output)\n", + "# x1 = np.swapaxes(np.stack([lat, lon]), 0, 2).reshape((len(inarr.x) * len(inarr.y), 2))\n", + "# x2 = np.transpose(np.stack([lat, lon]), (1, 2, 0)).reshape((len(inarr.x) * len(inarr.y), 2))\n", + "# np.array_equal(x1, x2)\n", "\n", - "output.shape\n" + "# # x = np.random.rand(10, 10, 2)\n", + "# # np.array_equal(x.reshape((100, 2)), rearrange(x, \"x y c -> (x y) c\"))" ] }, { "cell_type": "code", - "execution_count": 15, - "id": "f18b1535", - "metadata": {}, + "execution_count": 145, + "id": "49f7ec42-0782-42f0-bd8c-1f967938a8b0", + "metadata": { + "tags": [] + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - " Size: 134kB\n", - "[16704 values with dtype=float64]\n", + "[[ 0]\n", + " [ 1]\n", + " [ 2]\n", + " ...\n", + " [9997]\n", + " [9998]\n", + " [9999]]\n", + "[[ 0. 0. ]\n", + " [ 1. 0. ]\n", + " [ 2. 0. ]\n", + " ...\n", + " [97. 0.99]\n", + " [98. 0.99]\n", + " [99. 0.99]]\n", + "\n", + "array([[[ 0, 1, 2, ..., 97, 98, 99],\n", + " [ 100, 101, 102, ..., 197, 198, 199],\n", + " [ 200, 201, 202, ..., 297, 298, 299],\n", + " ...,\n", + " [9700, 9701, 9702, ..., 9797, 9798, 9799],\n", + " [9800, 9801, 9802, ..., 9897, 9898, 9899],\n", + " [9900, 9901, 9902, ..., 9997, 9998, 9999]]])\n", "Coordinates:\n", - " * t (t) datetime64[ns] 8B 1970-01-01\n", - " * x (x) float64 1kB 6.528e+05 6.528e+05 ... 6.542e+05 6.542e+05\n", - " * y (y) float64 928B 5.681e+06 5.681e+06 ... 5.68e+06 5.68e+06\n", - "Attributes:\n", - " long_name: presto_1\n", - " units: \n", - " grid_mapping: crs\n" + " * x (x) int64 0 1 2 3 4 5 6 7 8 9 10 ... 90 91 92 93 94 95 96 97 98 99\n", + " * y (y) float64 0.0 0.01 0.02 0.03 0.04 ... 0.95 0.96 0.97 0.98 0.99\n", + "Dimensions without coordinates: t\n" ] } ], "source": [ - "presto_ft = xr.open_dataset('2024_05_17_14_00_16_output_presto.nc')\n", + "array1 = np.arange(100)\n", + "array2 = np.arange(0,1,0.01)\n", + "\n", + "values = np.arange(100*100).reshape((100,100))\n", + "inarr = xr.DataArray(np.expand_dims(values,0), dims=['t', 'x', 'y'], coords={'x': array1, 'y': array2})\n", + "\n", + "print(rearrange(inarr.values, \"t x y -> (x y) t\"))\n", + "\n", + "lon, lat = np.meshgrid(inarr.x, inarr.y)\n", + "print(rearrange(np.stack([lon, lat]), \"c x y -> (x y) c\"))\n", "\n", - "print(presto_ft['presto_1'])\n" + "print(inarr)" ] }, { @@ -594,9 +453,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "worldcereal", "language": "python", - "name": "python3" + "name": "worldcereal" }, "language_info": { "codemirror_mode": { @@ -608,7 +467,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.10.12" } }, "nbformat": 4, From b74ecad6c5d1dfe2ec98f30ab4812f28a0243158 Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Mon, 27 May 2024 15:05:50 +0200 Subject: [PATCH 19/31] Updated inference notebook --- .../backend_inference_example_openeo.ipynb | 140 +----------------- 1 file changed, 5 insertions(+), 135 deletions(-) diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index a0838ede..0bc1b86b 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -284,7 +284,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 152, "id": "2cf64980", "metadata": { "tags": [] @@ -293,16 +293,16 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 32, + "execution_count": 152, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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", + "image/png": "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ClkPzcT8+fPdpk2b3KFDh9yBAwfc9773PVdWVuY+++yz6Dl33HGHKy0tdQ0NDW7//v3uyiuvdHPmzDEcta19+/a5iRMnuksvvdQtW7Ysepx5cu4///mPmzBhgrv55pvd3r173ZEjR9ybb77p/vGPf0TPWbt2rfP7/e6VV15x77//vvvBD37gysvL3RdffGE48uG1Zs0aV1RU5N544w139OhRt23bNjdq1Cj3m9/8JnoO8zQ0ngygWbNmuZqamujXXV1drqSkxNXX1xuOyjtOnDjhJLldu3Y555xra2tzI0eOdNu2bYue8/e//91Jco2NjVbDNNPR0eEmTZrkdu7c6b71rW9FA4h5OuP+++938+bNO+fzkUjEFRcXu1//+tfRY21tbc7n87mXXnppOIboCQsWLHC33nprzLHrr7/eLVmyxDnHPCWC55bgTp06paamJlVVVUWPZWdnq6qqSo2NjYYj84729nZJ0tixYyVJTU1NOn36dMycVVRUqKysLCPnrKamRgsWLIiZD4l5+q/XXntNM2fO1A033KBx48Zp+vTpevbZZ6PPHz16VMFgMGae/H6/Zs+enVHzNGfOHDU0NKilpUWS9P7772v37t265pprJDFPieC53bA/+eQTdXV1KRAIxBwPBAL68MMPjUblHZFIRLW1tZo7d66mTp0qSQoGg8rNzdXo0aNjzg0EAgoGgwajtLN161a99957evfdd3s9xzydceTIEW3cuFErVqzQz3/+c7377ru65557lJubq+rq6uhcnO13MJPmaeXKlQqHw6qoqNCIESPU1dWlNWvWaMmSJZLEPCWA5wIIfaupqdGhQ4e0e/du66F4Tmtrq5YtW6adO3cqLy/PejieFYlENHPmTD3yyCOSpOnTp+vQoUN6+umnVV1dbTw673j55Zf14osvasuWLZoyZYoOHDig2tpalZSUME8J4rkluPPPP18jRozodWVSKBRScXGx0ai8YenSpXrjjTf09ttv64ILLogeLy4u1qlTp9TW1hZzfqbNWVNTk06cOKHLL79cOTk5ysnJ0a5du7R+/Xrl5OQoEAgwT5LGjx+vSy65JObYxRdfrGPHjklSdC4y/Xfw3nvv1cqVK3XjjTdq2rRp+vGPf6zly5ervr5eEvOUCJ4LoNzcXM2YMUMNDQ3RY5FIRA0NDaqsrDQcmR3nnJYuXart27frrbfeUnl5eczzM2bM0MiRI2PmrLm5WceOHcuoObv66qt18OBBHThwIPqYOXOmlixZEv1v5kmaO3dur8v4W1paNGHCBElSeXm5iouLY+YpHA5r7969GTVPn3/+ea9P8xwxYoQikYgk5ikhrK+COJutW7c6n8/nnn/+effBBx+422+/3Y0ePdoFg0HroZm48847nd/vd3/+85/dxx9/HH18/vnn0XPuuOMOV1ZW5t566y23f/9+V1lZ6SorKw1H7Q3dr4Jzjnly7swl6jk5OW7NmjXu8OHD7sUXX3TnnXee+/3vfx89Z+3atW706NHu1VdfdX/961/dwoULM+7y4urqaveNb3wjehn2H//4R3f++ee7++67L3oO8zQ0ngwg55x78sknXVlZmcvNzXWzZs1ye/bssR6SGUlnfWzatCl6zhdffOHuuusuN2bMGHfeeee56667zn388cd2g/aIngHEPJ3x+uuvu6lTpzqfz+cqKircM888E/N8JBJxq1atcoFAwPl8Pnf11Ve75uZmo9HaCIfDbtmyZa6srMzl5eW5Cy+80P3iF79wnZ2d0XOYp6Hh84AAACY81wMCAGQGAggAYIIAAgCYIIAAACYIIACACQIIAGCCAAIAmCCAAAAmCCAAgAkCCABgggACAJj4PyMzfBT6hhL8AAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -315,140 +315,10 @@ "import xarray as xr\n", "import matplotlib.pyplot as plt\n", "\n", - "output = xr.open_dataset('2024_05_23_10_00_45_output_presto.nc')\n", - "# output = output['output_catboost'].to_numpy().squeeze()\n", - "# plt.imshow(output)\n", + "output = xr.open_dataset('2024_05_24_16_16_00_output_presto.nc')\n", "plt.imshow(output['classification'])\n", "\n" ] - }, - { - "cell_type": "code", - "execution_count": 125, - "id": "f18b1535", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 125, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# # latlons = rearrange(np.stack([lat, lon]), \"c x y -> (x y) c\")\n", - "# # otherlatlons = np.stack([lat, lon]).transpose(1, 2, 0).reshape((len(inarr.x) * len(inarr.y), 2))\n", - "\n", - "# # np.array_equal(latlons, otherlatlons)\n", - "\n", - "# x1 = np.swapaxes(np.stack([lat, lon]), 0, 2).reshape((len(inarr.x) * len(inarr.y), 2))\n", - "# x2 = np.transpose(np.stack([lat, lon]), (1, 2, 0)).reshape((len(inarr.x) * len(inarr.y), 2))\n", - "# np.array_equal(x1, x2)\n", - "\n", - "# # x = np.random.rand(10, 10, 2)\n", - "# # np.array_equal(x.reshape((100, 2)), rearrange(x, \"x y c -> (x y) c\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 145, - "id": "49f7ec42-0782-42f0-bd8c-1f967938a8b0", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[ 0]\n", - " [ 1]\n", - " [ 2]\n", - " ...\n", - " [9997]\n", - " [9998]\n", - " [9999]]\n", - "[[ 0. 0. ]\n", - " [ 1. 0. ]\n", - " [ 2. 0. ]\n", - " ...\n", - " [97. 0.99]\n", - " [98. 0.99]\n", - " [99. 0.99]]\n", - "\n", - "array([[[ 0, 1, 2, ..., 97, 98, 99],\n", - " [ 100, 101, 102, ..., 197, 198, 199],\n", - " [ 200, 201, 202, ..., 297, 298, 299],\n", - " ...,\n", - " [9700, 9701, 9702, ..., 9797, 9798, 9799],\n", - " [9800, 9801, 9802, ..., 9897, 9898, 9899],\n", - " [9900, 9901, 9902, ..., 9997, 9998, 9999]]])\n", - "Coordinates:\n", - " * x (x) int64 0 1 2 3 4 5 6 7 8 9 10 ... 90 91 92 93 94 95 96 97 98 99\n", - " * y (y) float64 0.0 0.01 0.02 0.03 0.04 ... 0.95 0.96 0.97 0.98 0.99\n", - "Dimensions without coordinates: t\n" - ] - } - ], - "source": [ - "array1 = np.arange(100)\n", - "array2 = np.arange(0,1,0.01)\n", - "\n", - "values = np.arange(100*100).reshape((100,100))\n", - "inarr = xr.DataArray(np.expand_dims(values,0), dims=['t', 'x', 'y'], coords={'x': array1, 'y': array2})\n", - "\n", - "print(rearrange(inarr.values, \"t x y -> (x y) t\"))\n", - "\n", - "lon, lat = np.meshgrid(inarr.x, inarr.y)\n", - "print(rearrange(np.stack([lon, lat]), \"c x y -> (x y) c\"))\n", - "\n", - "print(inarr)" - ] - }, - { - "cell_type": "markdown", - "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", - "metadata": {}, - "source": [ - "### Check reference" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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226L559/PjZt2hS33HJLbNmypfyYW265JTZv3hx33nln7Ny5M6ZMmRLLli2Lw4cPpy8eAIDaUtGpiH75y1/GZZddFpdeemlERLzrXe+K7373u/Hkk09GxBu7lrfeemt88YtfjMsuuywiIr7zne9Ee3t7fP/7348rrrgiefkAANSSinYuL7rooti2bVvs27cvIiJ+9atfxeOPPx4f+chHIiLixRdfjO7u7li6dGn5c9ra2mLRokWxY8eO4z5nX19flEqlATcAAOpTRTuX69ati1KpFPPmzYvx48fHsWPH4sYbb4yVK1dGRER3d3dERLS3tw/4vPb29vLH3mrjxo3xD//wD0NZOwAANaaincvvfe97cd9998X9998fTz/9dNx7773xT//0T3HvvfcOeQHr16+P3t7e8q2rq2vIzwUAQHVVtHP5uc99LtatW1f+t5Pz58+PX//617Fx48ZYtWpVdHR0RERET09PzJo1q/x5PT09ce655x73OVtaWqKlpWWIywcAoJZUFJevvfZajBs3cLNz/Pjx0d/fHxERnZ2d0dHREdu2bSvHZKlUip07d8ZVV12Vs+Ia4xriAAD/X0Vx+Rd/8Rdx4403xty5c+O9731v/Pu//3t8/etfj09+8pMREdHU1BRr166Nr3zlK3HmmWdGZ2dnbNiwIWbPnh3Lly8fifUDAFBDKorLLVu2xIYNG+Izn/lMvPzyyzF79uz427/927jhhhvKj7n++uvj1VdfjSuvvDIOHjwYF198cTzyyCMxadKk9MUDAFBbmoo3X16nBpRKpWhra4tX9p0RrVNr/+qUfi0Ote/RA3uqvQSAulY61B/Tz9ofvb290draesLH1n69AQBQN8QlAABpKvo3lwC1zK+/AarPziUAAGnEJQAAacQlAABpxCUAAGnEJQAAaUyLAwBQdrwLxLxeHI2I/Sf1+XYuAQBIIy4BAEgjLgEASCMuAQBIIy4BAEhjWvwtjjchFeGaxQAAJ8POJQAAacQlAABpxCUAAGnEJQAAacQlAABpxCUAAGmciugkDXaKIqB2OJUYwMmr5D2zdKg/pp91cs9r5xIAgDTiEgCANOISAIA04hIAgDTiEgCANDU7Lf7Rs+ZHc9OEAfeZ+ASGYihne/B+AzSK0T7jjZ1LAADSiEsAANKISwAA0ohLAADSiEsAANLU7LT48bi+NzBaqvV+U80pdddmh7FlpP5u27kEACCNuAQAII24BAAgjbgEACCNuAQAIE1dTYsDNLrRmFIfbEJ0NKbCK/3+TKrD8I323yM7lwAApBGXAACkEZcAAKQRlwAApBGXAACkMS0OMMY0+kQ6NLLB/v7W0t8tO5cAAKQRlwAApBGXAACkEZcAAKQRlwAApDEtDkC6ephoBUaGnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSOBURAOmccgiGp55P52XnEgCANOISAIA04hIAgDTiEgCANOISAIA0psUBGJJ6mFqFWjfYVHg9s3MJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gAAI6wRp8IHY+cSAIA04hIAgDTiEgCANOISAIA04hIAgDSmxQEAkmRNhT96YE/K81SDnUsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANKISwAA0ri2OABABbKuHx5R39cQH4ydSwAA0ohLAADSiEsAANKISwAA0ohLAADSiEsAANI4FREAQ+J0LDQ6r/GhsXMJAEAacQkAQBpxCQBAGnEJAEAacQkAQBrT4gBU3WBTuWNpwpbqyZwKx84lAACJxCUAAGnEJQAAacQlAABpxCUAAGlMiwNQs6o5xWtSvX553VSXnUsAANKISwAA0ohLAADSiEsAANLU3EBPURQREfF6HI0oqrwYAMas0qH+ai+BIXq9OFq1r92or5vS79/4vv63006kqTiZR42i//7v/445c+ZUexkAALxFV1dXnH766Sd8TM3FZX9/fxw4cCCmTp0ahw4dijlz5kRXV1e0trZWe2mMsFKp5HiPEY712OFYjx2OdWMriiIOHToUs2fPjnHjTvyvKmvu1+Ljxo0rF3FTU1NERLS2tnqhjiGO99jhWI8djvXY4Vg3rra2tpN6nIEeAADSiEsAANLUdFy2tLTEl770pWhpaan2UhgFjvfY4ViPHY712OFY879qbqAHAID6VdM7lwAA1BdxCQBAGnEJAEAacQkAQJqajsvbb7893vWud8WkSZNi0aJF8eSTT1Z7SQzTxo0bY+HChTF16tSYOXNmLF++PPbu3TvgMYcPH47Vq1fHjBkz4pRTTokVK1ZET09PlVZMlptvvjmamppi7dq15fsc68bxm9/8Jj7+8Y/HjBkzYvLkyTF//vx46qmnyh8viiJuuOGGmDVrVkyePDmWLl0aL7zwQhVXzFAcO3YsNmzYEJ2dnTF58uR497vfHV/+8pcHXG/asaZm4/Jf//Vf47rrrosvfelL8fTTT8f73//+WLZsWbz88svVXhrDsH379li9enU88cQT8dhjj8XRo0fjwx/+cLz66qvlx1x77bXxox/9KB588MHYvn17HDhwIC6//PIqrprh2rVrV3zrW9+K973vfQPud6wbwyuvvBJLliyJCRMmxE9+8pN47rnn4mtf+1pMnz69/JhbbrklNm/eHHfeeWfs3LkzpkyZEsuWLYvDhw9XceVUatOmTXHHHXfEbbfdFs8//3xs2rQpbrnlltiyZUv5MY41UdSoCy+8sFi9enX5z8eOHStmz55dbNy4sYqrItvLL79cRESxffv2oiiK4uDBg8WECROKBx98sPyY559/voiIYseOHdVaJsNw6NCh4swzzywee+yx4s/+7M+Ka665pigKx7qRfP7zny8uvvjiQT/e399fdHR0FF/96lfL9x08eLBoaWkpvvvd747GEkly6aWXFp/85CcH3Hf55ZcXK1euLIrCseYNNblzeeTIkdi9e3csXbq0fN+4ceNi6dKlsWPHjiqujGy9vb0REXHqqadGRMTu3bvj6NGjA479vHnzYu7cuY59nVq9enVceumlA45phGPdSH74wx/GggUL4mMf+1jMnDkzzjvvvLj77rvLH3/xxReju7t7wLFua2uLRYsWOdZ15qKLLopt27bFvn37IiLiV7/6VTz++OPxkY98JCIca97QXO0FHM/vfve7OHbsWLS3tw+4v729Pf7rv/6rSqsiW39/f6xduzaWLFkS55xzTkREdHd3x8SJE2PatGkDHtve3h7d3d1VWCXD8cADD8TTTz8du3btetvHHOvGsX///rjjjjviuuuuiy984Quxa9eu+OxnPxsTJ06MVatWlY/n8d7THev6sm7duiiVSjFv3rwYP358HDt2LG688cZYuXJlRIRjTUTUaFwyNqxevTqeffbZePzxx6u9FEZAV1dXXHPNNfHYY4/FpEmTqr0cRlB/f38sWLAgbrrppoiIOO+88+LZZ5+NO++8M1atWlXl1ZHpe9/7Xtx3331x//33x3vf+97Ys2dPrF27NmbPnu1YU1aTvxY/7bTTYvz48W+bGu3p6YmOjo4qrYpMa9asiYcffjh+9rOfxemnn16+v6OjI44cORIHDx4c8HjHvv7s3r07Xn755Tj//POjubk5mpubY/v27bF58+Zobm6O9vZ2x7pBzJo1K97znvcMuO/ss8+Ol156KSKifDy9p9e/z33uc7Fu3bq44oorYv78+fHXf/3Xce2118bGjRsjwrHmDTUZlxMnTowLLrggtm3bVr6vv78/tm3bFosXL67iyhiuoihizZo18dBDD8VPf/rT6OzsHPDxCy64ICZMmDDg2O/duzdeeuklx77OXHLJJfHMM8/Enj17yrcFCxbEypUry//tWDeGJUuWvO2UYvv27Yt3vvOdERHR2dkZHR0dA451qVSKnTt3OtZ15rXXXotx4wamw/jx46O/vz8iHGv+n2pPFA3mgQceKFpaWop77rmneO6554orr7yymDZtWtHd3V3tpTEMV111VdHW1lb8/Oc/L37729+Wb6+99lr5MX/3d39XzJ07t/jpT39aPPXUU8XixYuLxYsXV3HVZHnztHhRONaN4sknnyyam5uLG2+8sXjhhReK++67r3jHO95R/Mu//Ev5MTfffHMxbdq04gc/+EHxH//xH8Vll11WdHZ2Fn/4wx+quHIqtWrVquJP/uRPiocffrh48cUXi3/7t38rTjvttOL6668vP8axpmbjsiiKYsuWLcXcuXOLiRMnFhdeeGHxxBNPVHtJDFNEHPe2devW8mP+8Ic/FJ/5zGeK6dOnF+94xzuKj370o8Vvf/vb6i2aNG+NS8e6cfzoRz8qzjnnnKKlpaWYN29ecddddw34eH9/f7Fhw4aivb29aGlpKS655JJi7969VVotQ1UqlYprrrmmmDt3bjFp0qTijDPOKP7+7/++6OvrKz/GsaapKN50Wn0AABiGmvw3lwAA1CdxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAEAacQkAQBpxCQBAGnEJAECa/ws8KlmAU0tsngAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [] } ], "metadata": { From 7915b935676bdd67061dc1936f5530c330e062a5 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Tue, 28 May 2024 14:05:20 +0200 Subject: [PATCH 20/31] Updating preprocessing to match better kristof's results --- .gitignore | 4 +- src/worldcereal/openeo/preprocessing.py | 94 +++++++++++++++++-------- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 6b75fc22..e33f7077 100755 --- a/.gitignore +++ b/.gitignore @@ -180,4 +180,6 @@ catboost_info/catboost_training.json *.jar *.rar *.tar -*.zip \ No newline at end of file +*.zip + +.notebook-tests/ \ No newline at end of file diff --git a/src/worldcereal/openeo/preprocessing.py b/src/worldcereal/openeo/preprocessing.py index 6991ea41..7a8c723a 100644 --- a/src/worldcereal/openeo/preprocessing.py +++ b/src/worldcereal/openeo/preprocessing.py @@ -13,10 +13,10 @@ from openeo_gfmap.fetching.s1 import build_sentinel1_grd_extractor from openeo_gfmap.fetching.s2 import build_sentinel2_l2a_extractor from openeo_gfmap.preprocessing.compositing import ( - max_ndvi_compositing, + median_compositing, mean_compositing, + sum_compositing ) -from openeo_gfmap.preprocessing.interpolation import linear_interpolation from openeo_gfmap.preprocessing.sar import compress_backscatter_uint16 COMPOSITE_WINDOW = "month" @@ -66,6 +66,14 @@ def raw_datacube_S2( if filter_tile: scl_cube_properties["tileId"] = lambda val: val == filter_tile + # Create the job to extract S2 + extraction_parameters = { + "target_resolution": None, # Disable target resolution + "load_collection": { + "eo:cloud_cover": lambda val: val <= 95.0, + }, + } + scl_cube = connection.load_collection( collection_id="SENTINEL2_L2A", bands=["SCL"], @@ -89,35 +97,29 @@ def raw_datacube_S2( erosion_kernel_size=3, ).rename_labels("bands", ["S2-L2A-SCL_DILATED_MASK"]) - # Compute the distance to cloud and add it to the cube - distance_to_cloud = scl_cube.apply_neighborhood( - process=UDF.from_file(Path(__file__).parent / "udf_distance_to_cloud.py"), - size=[ - {"dimension": "x", "unit": "px", "value": 256}, - {"dimension": "y", "unit": "px", "value": 256}, - {"dimension": "t", "unit": "null", "value": "P1D"}, - ], - overlap=[ - {"dimension": "x", "unit": "px", "value": 16}, - {"dimension": "y", "unit": "px", "value": 16}, - ], - ).rename_labels("bands", ["S2-L2A-DISTANCE-TO-CLOUD"]) + if additional_masks: + # Compute the distance to cloud and add it to the cube + distance_to_cloud = scl_cube.apply_neighborhood( + process=UDF.from_file(Path(__file__).parent / "udf_distance_to_cloud.py"), + size=[ + {"dimension": "x", "unit": "px", "value": 256}, + {"dimension": "y", "unit": "px", "value": 256}, + {"dimension": "t", "unit": "null", "value": "P1D"}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 16}, + {"dimension": "y", "unit": "px", "value": 16}, + ], + ).rename_labels("bands", ["S2-L2A-DISTANCE-TO-CLOUD"]) - additional_masks = scl_dilated_mask.merge_cubes(distance_to_cloud) + additional_masks = scl_dilated_mask.merge_cubes(distance_to_cloud) - # Try filtering using the geometry - if fetch_type == FetchType.TILE: - additional_masks = additional_masks.filter_spatial(spatial_extent.to_geojson()) + # Try filtering using the geometry + if fetch_type == FetchType.TILE: + additional_masks = additional_masks.filter_spatial(spatial_extent.to_geojson()) - # Create the job to extract S2 - extraction_parameters = { - "target_resolution": None, # Disable target resolution - "load_collection": { - "eo:cloud_cover": lambda val: val <= 95.0, - }, - } - if additional_masks: extraction_parameters["pre_merge"] = additional_masks + if filter_tile: extraction_parameters["load_collection"]["tileId"] = ( lambda val: val == filter_tile @@ -193,6 +195,22 @@ def raw_datacube_DEM( return extractor.get_cube(connection, spatial_extent, None) +def raw_datacube_METEO( + connection: Connection, + backend_context: BackendContext, + spatial_extent: SpatialContext, + temporal_extent: TemporalContext, + fetch_type: FetchType, +) -> DataCube: + extractor = build_generic_extractor( + backend_context=backend_context, + bands=["A5-tmean", "A5-precip"], + fetch_type=fetch_type, + collection_name="AGERA5", + ) + return extractor.get_cube(connection, spatial_extent, temporal_extent) + + def worldcereal_preprocessed_inputs_gfmap( connection: Connection, backend_context: BackendContext, @@ -223,8 +241,7 @@ def worldcereal_preprocessed_inputs_gfmap( apply_mask=True, ) - s2_data = max_ndvi_compositing(s2_data, period="month") - s2_data = linear_interpolation(s2_data) + s2_data = median_compositing(s2_data, period="month") # Cast to uint16 s2_data = s2_data.linear_scale_range(0, 65534, 0, 65534) @@ -245,7 +262,6 @@ def worldcereal_preprocessed_inputs_gfmap( ) s1_data = mean_compositing(s1_data, period="month") - s1_data = linear_interpolation(s1_data) s1_data = compress_backscatter_uint16(backend_context, s1_data) dem_data = raw_datacube_DEM( @@ -255,10 +271,26 @@ def worldcereal_preprocessed_inputs_gfmap( fetch_type=FetchType.TILE, ) - dem_data = dem_data.resample_cube_spatial(s2_data, method="cubic") dem_data = dem_data.linear_scale_range(0, 65534, 0, 65534) + # meteo_data = raw_datacube_METEO( + # connection=connection, + # backend_context=backend_context, + # spatial_extent=spatial_extent, + # temporal_extent=temporal_extent, + # fetch_type=FetchType.TILE, + # ) + + # # Perform compositing differently depending on the bands + # mean_temperature = meteo_data.band("A5-tmean") + # mean_temperature = mean_compositing(mean_temperature, period="month") + + # total_precipitation = meteo_data.band("A5-precip") + # total_precipitation = sum_compositing(total_precipitation, period="month") + data = s2_data.merge_cubes(s1_data) data = data.merge_cubes(dem_data) + # data = data.merge_cubes(mean_temperature) + # data = data.merge_cubes(total_precipitation) return data From 005841a9308139ad7326b6e5c66e4c2c052a3531 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Tue, 28 May 2024 16:06:36 +0200 Subject: [PATCH 21/31] Added feature extractor with GFMAP compatibility --- minimal_wc_presto/presto_feature_computer.py | 385 +++++++++++++++++++ minimal_wc_presto/test_presto_fc_gfmap.py | 67 ++++ 2 files changed, 452 insertions(+) create mode 100644 minimal_wc_presto/presto_feature_computer.py create mode 100644 minimal_wc_presto/test_presto_fc_gfmap.py diff --git a/minimal_wc_presto/presto_feature_computer.py b/minimal_wc_presto/presto_feature_computer.py new file mode 100644 index 00000000..c73ba4a0 --- /dev/null +++ b/minimal_wc_presto/presto_feature_computer.py @@ -0,0 +1,385 @@ +"""Feature computer GFMAP compatible to compute Presto embeddings.""" +import functools +import logging +import shutil +import sys +import urllib.request +from pathlib import Path +from typing import Tuple + +import numpy as np +import xarray as xr +from pyproj import Transformer + +from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor + + +class PrestoFeatureExtractor(PatchFeatureExtractor): + """Feature extractor to use Presto model to compute embeddings. + This will generate a datacube with 128 bands, each band representing a + feature from the Presto model. + """ + + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA + PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA + BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA + DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + + _NODATAVALUE = 65535 + + BAND_MAPPING = { + "S2-L2A-B02": "B2", + "S2-L2A-B03": "B3", + "S2-L2A-B04": "B4", + "S2-L2A-B05": "B5", + "S2-L2A-B06": "B6", + "S2-L2A-B07": "B7", + "S2-L2A-B08": "B8", + "S2-L2A-B8A": "B8A", + "S2-L2A-B11": "B11", + "S2-L2A-B12": "B12", + "S1-SIGMA0-VH": "VH", + "S1-SIGMA0-VV": "VV", + "A5-precip": "total_precipitation", + "A5-tmean": "temperature_2m", + } + + def __init__(self): + """ + Initializes the PrestoFeatureExtractor object, starting a logger. + """ + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(PrestoFeatureExtractor.__name__) + self.model = None # To be initialized within the OpenEO environment + + @classmethod + def _preprocess_band_values( + cls, values: np.ndarray, presto_band: str + ) -> np.ndarray: + """ + Preprocesses the band values based on the given presto_val. + + Args: + values (np.ndarray): Array of band values to preprocess. + presto_val (str): Name of the band for preprocessing. + + Returns: + np.ndarray: Preprocessed array of band values. + """ + if presto_band in ["VV", "VH"]: + # Convert to dB + values = 20 * np.log10(values) - 83 + elif presto_band == "total_precipitation": + # Scale precipitation and convert mm to m + values = values / (100 * 1000.0) + elif presto_band == "temperature_2m": + # Remove scaling + values = values / 100 + return values + + @classmethod + def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: + """ + Extracts EO data and mask arrays from the input xarray.DataArray. + + Args: + inarr (xr.DataArray): Input xarray.DataArray containing EO data. + + Returns: + Tuple[np.ndarray, np.ndarray]: Tuple containing EO data array and mask array. + """ + num_pixels = len(inarr.x) * len(inarr.y) + num_timesteps = len(inarr.t) + + eo_data = np.zeros((num_pixels, num_timesteps, len(BANDS))) # pylint: disable=E0602 + mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) # pylint: disable=E0602 + + for org_band, presto_band in cls.BAND_MAPPING.items(): + if org_band in inarr.coords["bands"]: + values = rearrange( # pylint: disable=E0602 + inarr.sel(bands=org_band).values, "t x y -> (x y) t" + ) + idx_valid = values != cls._NODATAVALUE + values = cls._preprocess_band_values(values, presto_band) + eo_data[:, :, BANDS.index(presto_band)] = values # pylint: disable=E0602 + mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid + + return eo_data, mask + + @staticmethod + def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: + """ + Extracts latitudes and longitudes from the input xarray.DataArray. + + Args: + inarr (xr.DataArray): Input xarray.DataArray containing spatial coordinates. + epsg (int): EPSG code for coordinate reference system. + + Returns: + np.ndarray: Array containing extracted latitudes and longitudes. + """ + # EPSG:4326 is the supported crs for presto + lon, lat = np.meshgrid(inarr.x, inarr.y) + transformer = Transformer.from_crs( + f"EPSG:{epsg}", "EPSG:4326", always_xy=True + ) + lon, lat = transformer.transform(lon, lat) + latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") # pylint: disable=E0602 + + # 2D array where each row represents a pair of latitude and longitude coordinates. + return latlons + + @staticmethod + def _extract_months(inarr: xr.DataArray) -> np.ndarray: + """ + Calculate the start month based on the first timestamp in the input array, + and create an array of the same length filled with that start month value. + + Parameters: + - inarr: xarray.DataArray or numpy.ndarray + Input array containing timestamps. + + Returns: + - months: numpy.ndarray + Array of start month values, with the same length as the input array. + """ + num_instances = len(inarr.x) * len(inarr.y) + + start_month = ( + inarr.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1 + ) - 1 + + months = np.ones((num_instances)) * start_month + return months + + def _create_dataloader( + self, + eo: np.ndarray, + dynamic_world: np.ndarray, + months: np.ndarray, + latlons: np.ndarray, + mask: np.ndarray, + ): + """ + Create a PyTorch DataLoader for encoding features. + + Args: + eo_data (np.ndarray): Array containing Earth Observation data. + dynamic_world (np.ndarray): Array containing dynamic world data. + latlons (np.ndarray): Array containing latitude and longitude coordinates. + inarr (xr.DataArray): Input xarray.DataArray. + mask (np.ndarray): Array containing masking data. + + Returns: + DataLoader: PyTorch DataLoader for encoding features. + """ + + # pylint: disable=E0602 + dl = DataLoader( + TensorDataset( + torch.from_numpy(eo).float(), + torch.from_numpy(dynamic_world).long(), + torch.from_numpy(latlons).float(), + torch.from_numpy(months).long(), + torch.from_numpy(mask).float(), + ), + batch_size=8192, + shuffle=False, + ) + # pylint: enable=E0602 + + return dl + + @classmethod + def _create_presto_input( + cls, inarr: xr.DataArray, epsg: int = 4326 + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + eo_data, mask = cls._extract_eo_data(inarr) + latlons = cls._extract_latlons(inarr, epsg) + months = cls._extract_months(inarr) + dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( + DynamicWorld2020_2021.class_amount # pylint: disable=E0602 + ) + + return ( + S1_S2_ERA5_SRTM.normalize(eo_data), # pylint: disable=E0602 + dynamic_world, + months, + latlons, + np.repeat(mask, BAND_EXPANSION, axis=-1), # pylint: disable=E0602 + ) + + def _get_encodings(self, dl) -> np.ndarray: + """ + Get encodings from DataLoader. + + Args: + dl (DataLoader): PyTorch DataLoader containing data for encoding. + + Returns: + np.ndarray: Array containing encoded features. + """ + + all_encodings = [] + + for x, dw, latlons, month, variable_mask in dl: + x_f, dw_f, latlons_f, month_f, variable_mask_f = [ + t.to(device) for t in (x, dw, latlons, month, variable_mask) # pylint: disable=E0602 + ] + + with torch.no_grad(): # pylint: disable=E0602 + encodings = ( + self.model.encoder( + x_f, + dynamic_world=dw_f.long(), + mask=variable_mask_f, + latlons=latlons_f, + month=month_f, + ) + .cpu() + .numpy() + ) + + all_encodings.append(encodings) + + return np.concatenate(all_encodings, axis=0) + + def extract_presto_features( + self, inarr: xr.DataArray, epsg: int = 4326 + ) -> np.ndarray: + """General function to prepare the input data, generate a data loader, + initialize the model, perform the inference and return the features. + """ + eo, dynamic_world, months, latlons, mask = self._create_presto_input( + inarr, epsg + ) + dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) + + features = self._get_encodings(dl) + features = rearrange( # pylint: disable=E0602 + features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) + ) + ft_names = [f"presto_ft_{i}" for i in range(128)] + features = xr.DataArray( + features, + coords={"x": inarr.x, "y": inarr.y, "bands": ft_names}, + dims=["x", "y", "bands"], + ) + + return features + + @classmethod + @functools.lru_cache(maxsize=6) + def extract_dependencies(cls, base_url: str, dependency_name: str): + """Extract the dependencies from the given URL. Unpacking a zip + file in the current working directory. + """ + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / "dependencies" + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve( + modelfile_url, filename=dependencies_dir / Path(modelfile_url).name + ) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) # NOQA + + # Append the dependencies + sys.path.append(str(abs_path)) + sys.path.append(str(abs_path) + "/pandas") + + def get_presto_features(self, inarr: xr.DataArray, presto_path: str) -> np.ndarray: + """ + Extracts features from input data using Presto. + + Args: + inarr (xr.DataArray): Input data as xarray DataArray. + presto_path (str): Path to the pretrained Presto model. + + Returns: + xr.DataArray: Extracted features as xarray DataArray. + """ + presto_model = Presto.load_pretrained_artifactory( # pylint: disable=E0602 + presto_url=presto_path, strict=False + ) + self.model = presto_model + + # Get the local EPSG code + features = self.extract_presto_features(inarr, epsg=self.epsg) + return features + + def output_labels(self) -> list: + """Returns the output labels from this UDF, which is the output labels + of the presto embeddings""" + return [f"presto_ft_{i}" for i in range(128)] + + + def execute(self, inarr: xr.DataArray) -> xr.DataArray: + # The below is required to avoid flipping of the result + # when running on OpenEO backend! + inarr = inarr.transpose("bands", "t", "x", "y") + + # Handle NaN values in Presto compatible way + inarr = inarr.fillna(65535) + + # Unzip de dependencies on the backend + self.logger.info("Unzipping dependencies") + self.extract_dependencies( + self.BASE_URL, + self.DEPENDENCY_NAME + ) + + # pylint: disable=E0401 + # pylint: disable=C0401 + # pylint: disable=C0415 + # pylint: disable=W0601 + # pylint: disable=W0603 + # pylint: disable=reportMissingImports + ########################################################################## + global onnxruntime, requests, torch, BANDS, BANDS_GROUPS_IDX, NORMED_BANDS + global S1_S2_ERA5_SRTM, DynamicWorld2020_2021, BAND_EXPANSION + global IDX_TO_BAND_GROUPS, BAND_EXPANSION, Presto, device, rearrange + global DataLoader, TensorDataset + + import onnxruntime + import requests + import torch + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( + BANDS, + BANDS_GROUPS_IDX, + NORMED_BANDS, + S1_S2_ERA5_SRTM, + DynamicWorld2020_2021, + ) + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.masking import ( + BAND_EXPANSION, + ) + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device + from einops import rearrange + from torch.utils.data import DataLoader, TensorDataset + ########################################################################## + # pylint: enable=E0401 + # pylint: enable=C0401 + # pylint: enable=C0415 + # pylint: enable=W0601 + # pylint: enable=W0603 + # pylint: enable=reportMissingImports + + + # Index to band groups mapping + IDX_TO_BAND_GROUPS = { + NORMED_BANDS[idx]: band_group_idx + for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) + for idx in val + } + + self.logger.info("Extracting presto features") + features = self.get_presto_features(inarr, self.PRESTO_PATH) + return features diff --git a/minimal_wc_presto/test_presto_fc_gfmap.py b/minimal_wc_presto/test_presto_fc_gfmap.py new file mode 100644 index 00000000..4b629899 --- /dev/null +++ b/minimal_wc_presto/test_presto_fc_gfmap.py @@ -0,0 +1,67 @@ +"""Test the presto feature computer running with GFMAP""" +import openeo + +from openeo_gfmap import ( + Backend, BackendContext, BoundingBoxExtent, TemporalContext +) +from openeo_gfmap.features.feature_extractor import apply_feature_extractor + +from presto_feature_computer import PrestoFeatureExtractor + +from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap + +EXTENT = dict(zip(["west", "south", "east", "north"], [664000.0, 5611120.0, 665000.0, 5612120.0])) +EXTENT['crs'] = "EPSG:32631" +EXTENT['srs'] = "EPSG:32631" +STARTDATE = '2020-11-01' +ENDDATE = '2021-10-31' + + +if __name__ == '__main__': + # Test extent + spatial_extent = BoundingBoxExtent( + west=EXTENT['west'], + south=EXTENT['south'], + east=EXTENT['east'], + north=EXTENT['north'], + epsg=32631, + ) + + temporal_extent = TemporalContext( + start_date=STARTDATE, + end_date=ENDDATE, + ) + backend_context = BackendContext(Backend.FED) + + connection = openeo.connect("openeofed.dataspace.copernicus.eu").authenticate_oidc() + + inputs = worldcereal_preprocessed_inputs_gfmap( + connection=connection, + backend_context=backend_context, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + ) + + # Test feature computer + presto_parameters = {} + features = apply_feature_extractor( + feature_extractor_class=PrestoFeatureExtractor, + cube=inputs, + parameters=presto_parameters, + size=[ + {"dimension": "x", "unit": "px", "value": 128}, + {"dimension": "y", "unit": "px", "value": 128}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 0}, + {"dimension": "y", "unit": "px", "value": 0}, + ] + ) + + job = features.create_job(out_format="NetCDF", title="Presto FC GFMAP") + + job.start_and_wait() + + for asset in job.get_results().get_assets(): + if asset.metadata["type"].startswith("application/x-netcdf"): + asset.download("presto_features_gfmap.nc") From f7d09b913817fc55f24be1a087df75a867d9574a Mon Sep 17 00:00:00 2001 From: Hans Vanrompay Date: Wed, 29 May 2024 16:54:24 +0200 Subject: [PATCH 22/31] fix: clean-up + updated dependencies --- .../backend_inference_example_openeo.ipynb | 257 +++++++++++------- minimal_wc_presto/dev_testing.py | 83 ------ minimal_wc_presto/job-results.json | 1 - .../mvp_wc_presto/world_cereal_inference.py | 74 +++-- minimal_wc_presto/test_aggregator.ipynb | 229 ---------------- minimal_wc_presto/test_prestobackend.py | 25 -- minimal_wc_presto/testing.py | 21 -- minimal_wc_presto/udf_presto.py | 36 +-- .../udf_worldcereal_inference.py | 38 +-- 9 files changed, 234 insertions(+), 530 deletions(-) delete mode 100644 minimal_wc_presto/dev_testing.py delete mode 100644 minimal_wc_presto/job-results.json delete mode 100644 minimal_wc_presto/test_aggregator.ipynb delete mode 100644 minimal_wc_presto/test_prestobackend.py delete mode 100644 minimal_wc_presto/testing.py diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb index 0bc1b86b..4a3f7af8 100644 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ b/minimal_wc_presto/backend_inference_example_openeo.ipynb @@ -98,68 +98,12 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "4aab5695", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'j-2405213e660a4308ac7c9b6300206ec4': send 'start'\n", - "0:00:15 Job 'j-2405213e660a4308ac7c9b6300206ec4': created (progress 0%)\n", - "0:00:21 Job 'j-2405213e660a4308ac7c9b6300206ec4': created (progress 0%)\n", - "0:00:27 Job 'j-2405213e660a4308ac7c9b6300206ec4': created (progress 0%)\n", - "0:00:35 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:00:45 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:00:58 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:01:13 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:01:33 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:01:58 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:02:28 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:03:06 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:03:53 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:04:51 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:05:52 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:06:59 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:08:00 Job 'j-2405213e660a4308ac7c9b6300206ec4': running (progress N/A)\n", - "0:09:01 Job 'j-2405213e660a4308ac7c9b6300206ec4': finished (progress 100%)\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "current_datetime = datetime.now()\n", "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", @@ -179,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 149, + "execution_count": 4, "id": "64d37c40", "metadata": { "tags": [] @@ -189,28 +133,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "0:00:00 Job 'j-24052404d20b4be09dca30c5a09413da': send 'start'\n", - "0:00:21 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", - "0:00:28 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", - "0:00:38 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", - "0:00:46 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", - "0:01:04 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", - "0:01:17 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", - "0:01:33 Job 'j-24052404d20b4be09dca30c5a09413da': created (progress 0%)\n", - "0:01:52 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:02:17 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:02:47 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:03:25 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:04:12 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:05:11 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:06:12 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:07:14 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:08:15 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:09:15 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:10:16 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:11:19 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:12:20 Job 'j-24052404d20b4be09dca30c5a09413da': running (progress N/A)\n", - "0:13:20 Job 'j-24052404d20b4be09dca30c5a09413da': finished (progress 100%)\n" + "0:00:00 Job 'j-240529fb722145acadced18905706e6e': send 'start'\n", + "0:00:15 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", + "0:00:20 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", + "0:00:27 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", + "0:00:35 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", + "0:00:45 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", + "0:00:57 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:01:12 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:01:32 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:01:56 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:02:26 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:03:04 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:03:50 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:04:49 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:05:49 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:06:49 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:07:50 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:08:50 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", + "0:09:50 Job 'j-240529fb722145acadced18905706e6e': finished (progress 100%)\n" ] }, { @@ -232,15 +173,15 @@ " }\n", " \n", " \n", - " \n", + " \n", " \n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 149, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -250,9 +191,9 @@ "\n", "current_datetime = datetime.now()\n", "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", + "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", "\n", - "udf = openeo.UDF.from_file(\"udf_long_worldcereal_inference.py\")\n", + "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", "\n", "prediction = input_cube.apply_neighborhood(\n", " process=udf,\n", @@ -284,7 +225,7 @@ }, { "cell_type": "code", - "execution_count": 152, + "execution_count": 5, "id": "2cf64980", "metadata": { "tags": [] @@ -293,16 +234,16 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 152, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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ClkPzcT8+fPdpk2b3KFDh9yBAwfc9773PVdWVuY+++yz6Dl33HGHKy0tdQ0NDW7//v3uyiuvdHPmzDEcta19+/a5iRMnuksvvdQtW7Ysepx5cu4///mPmzBhgrv55pvd3r173ZEjR9ybb77p/vGPf0TPWbt2rfP7/e6VV15x77//vvvBD37gysvL3RdffGE48uG1Zs0aV1RU5N544w139OhRt23bNjdq1Cj3m9/8JnoO8zQ0ngygWbNmuZqamujXXV1drqSkxNXX1xuOyjtOnDjhJLldu3Y555xra2tzI0eOdNu2bYue8/e//91Jco2NjVbDNNPR0eEmTZrkdu7c6b71rW9FA4h5OuP+++938+bNO+fzkUjEFRcXu1//+tfRY21tbc7n87mXXnppOIboCQsWLHC33nprzLHrr7/eLVmyxDnHPCWC55bgTp06paamJlVVVUWPZWdnq6qqSo2NjYYj84729nZJ0tixYyVJTU1NOn36dMycVVRUqKysLCPnrKamRgsWLIiZD4l5+q/XXntNM2fO1A033KBx48Zp+vTpevbZZ6PPHz16VMFgMGae/H6/Zs+enVHzNGfOHDU0NKilpUWS9P7772v37t265pprJDFPieC53bA/+eQTdXV1KRAIxBwPBAL68MMPjUblHZFIRLW1tZo7d66mTp0qSQoGg8rNzdXo0aNjzg0EAgoGgwajtLN161a99957evfdd3s9xzydceTIEW3cuFErVqzQz3/+c7377ru65557lJubq+rq6uhcnO13MJPmaeXKlQqHw6qoqNCIESPU1dWlNWvWaMmSJZLEPCWA5wIIfaupqdGhQ4e0e/du66F4Tmtrq5YtW6adO3cqLy/PejieFYlENHPmTD3yyCOSpOnTp+vQoUN6+umnVV1dbTw673j55Zf14osvasuWLZoyZYoOHDig2tpalZSUME8J4rkluPPPP18jRozodWVSKBRScXGx0ai8YenSpXrjjTf09ttv64ILLogeLy4u1qlTp9TW1hZzfqbNWVNTk06cOKHLL79cOTk5ysnJ0a5du7R+/Xrl5OQoEAgwT5LGjx+vSy65JObYxRdfrGPHjklSdC4y/Xfw3nvv1cqVK3XjjTdq2rRp+vGPf6zly5ervr5eEvOUCJ4LoNzcXM2YMUMNDQ3RY5FIRA0NDaqsrDQcmR3nnJYuXart27frrbfeUnl5eczzM2bM0MiRI2PmrLm5WceOHcuoObv66qt18OBBHThwIPqYOXOmlixZEv1v5kmaO3dur8v4W1paNGHCBElSeXm5iouLY+YpHA5r7969GTVPn3/+ea9P8xwxYoQikYgk5ikhrK+COJutW7c6n8/nnn/+effBBx+422+/3Y0ePdoFg0HroZm48847nd/vd3/+85/dxx9/HH18/vnn0XPuuOMOV1ZW5t566y23f/9+V1lZ6SorKw1H7Q3dr4Jzjnly7swl6jk5OW7NmjXu8OHD7sUXX3TnnXee+/3vfx89Z+3atW706NHu1VdfdX/961/dwoULM+7y4urqaveNb3wjehn2H//4R3f++ee7++67L3oO8zQ0ngwg55x78sknXVlZmcvNzXWzZs1ye/bssR6SGUlnfWzatCl6zhdffOHuuusuN2bMGHfeeee56667zn388cd2g/aIngHEPJ3x+uuvu6lTpzqfz+cqKircM888E/N8JBJxq1atcoFAwPl8Pnf11Ve75uZmo9HaCIfDbtmyZa6srMzl5eW5Cy+80P3iF79wnZ2d0XOYp6Hh84AAACY81wMCAGQGAggAYIIAAgCYIIAAACYIIACACQIIAGCCAAIAmCCAAAAmCCAAgAkCCABgggACAJj4PyMzfBT6hhL8AAAAAElFTkSuQmCC", + "image/png": "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0MaNGyVJ3/72t2OOb9q0STfffLMk6YknnlB2drYWL16szs5OzZ8/X0899VRCBgsASB9xBZBzrt9z8vLytGHDBm3YsGHQgwIApD/2ggMAmEiJj2Poy1D6OPR80B8+BRVIHiogAIAJAggAYIIAAgCYSPkeUE/d1+x79njo+QDAwPV3L+W53lO/cqclHen39amAAAAmCCAAgIm0W4LrS3+X1LJEB3hb979RLpH/n+F670r0z6ECAgCYIIAAACYIIACAibTrAdHHAVLLYP9m+7tEONVlwnsZFRAAwAQBBAAwQQABAEykXQ9oKPraxgeZiftOBoa/l4FhnmJRAQEATBBAAAATBBAAwETa9YD6WqdP9/sGkFyZ9t9PqvUrhuvfJ9XmpadEzstQ54IKCABgggACAJjw7BLcdZOnKSdrpKThK6XTfUkF6S/Vl4e8yItz6pX3qqHeukIFBAAwQQABAEwQQAAAE57tAW1vOajCAvIR8GIPIt1YzLFX+jiWeIcHAJgggAAAJgggAIAJz/aAut8H1F0y10372nqfdXj0998e/414C30d76MCAgCYIIAAACaynHPOehDdhcNh+f1+fdpyYdIvw45nKx6WVwBILLMNRLgjojGTj6i9vV2FhYXnPI8KCABgggACAJgggAAAJjx7GbYF+jxAZqKvY4MKCABgggACAJgggAAAJugBAcg49Hy8gQoIAGCCAAIAmGAJDkBaYFkt9VABAQBMEEAAABMEEADABAEEADBBAAEATBBAAAATBBAAwAQBBAAwQQABAEwQQAAAEwQQAMCEZ/eCu27yNOVkjZQ0+D2een7ENntFAemLv/fUQwUEADBBAAEATHh2CW57y0EVFgwtHynBAcC7qIAAACYIIACAiSEF0Nq1a5WVlaXa2trosS+//FI1NTUqKirSqFGjtHjxYoVCoaGOEwCQZgYdQO+++65++9vf6tJLL405vnz5cr3++uvatm2bdu3apePHj+v6668f8kABAOllUAH02WefacmSJXr22Wc1ZsyY6PH29nY999xzevzxx3XVVVdpxowZ2rRpk9555x3t2bPnrK/V2dmpcDgc8wAApL9BBVBNTY0WLFigqqqqmONNTU06ffp0zPGKigqVlZWpsbHxrK9VX18vv98ffZSWlg5mSACAFBN3AG3dulXvvfee6uvrez0XDAaVm5ur0aNHxxwPBAIKBoNnfb26ujq1t7dHH62trfEOCQCQguK6D6i1tVXLli3Tzp07lZeXl5AB+Hw++Xy+QX9/z+03AGQG7vNLfXFVQE1NTTpx4oQuv/xy5eTkKCcnR7t27dL69euVk5OjQCCgU6dOqa2tLeb7QqGQiouLEzluAECKi6sCuvrqq3Xw4MGYY7fccosqKip0//33q7S0VCNHjlRDQ4MWL14sSWpubtaxY8dUWVmZuFEDAFJeXAFUUFCgqVOnxhz72te+pqKioujx2267TStWrNDYsWNVWFiou+++W5WVlbryyivjGlj33bABQGLZLd0kfC+4J554QtnZ2Vq8eLE6Ozs1f/58PfXUU4n+MQCAFJflnHPWg+guHA7L7/fr21pIBQQgBhVQagh3RDRm8hG1t7ersLDwnOexFxwAwIRnP44BAHrqftsF1VDqowICAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACQIIAGCCAAIAmCCAAAAm2IoHQErq+WnIbM2TeqiAAAAmCCAAgAkCCABgggACAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACbbiAZAW2Jon9VABAQBMEEAAABMEEADABD0gAGmpe0+IfpA3UQEBAEwQQAAAEwQQAMAEPSAAaa/nPUL9oWc0PKiAAAAmCCAAgAkCCABgggACAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACbbiAZDx2HrHBhUQAMAEAQQAMMESHICM13O3bJbkhgcVEADABAEEADBBAAEATNADAoAeuveE6AclDxUQAMAEAQQAMEEAAQBM0AMCgD5wj1DyUAEBAEwQQAAAEwQQAMAEAQQAMEEAAQBMEEAAABNchg0AceCy7MShAgIAmCCAAAAm4g6gjz76SDfddJOKioqUn5+vadOmaf/+/dHnnXNavXq1xo8fr/z8fFVVVenw4cMJHTQAIPXF1QP69NNPNXfuXH3nO9/Rn/70J33961/X4cOHNWbMmOg5jz32mNavX6/NmzervLxcq1at0vz58/XBBx8oLy9vwD9re8tBFRacyceea67J0n0td7h+JgBkqrgC6NFHH1Vpaak2bdoUPVZeXh793845rVu3Tg888IAWLlwoSXrhhRcUCAT0yiuv6MYbb+z1mp2dners7Ix+HQ6H4/4lAACpJ64luNdee00zZ87U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L495fsaMGRo5cmTMnDU3N+vYsWMZNWdXX321Dh48qAMHDkQfM2fO1JIlS6L/m3mS5s6d2+sy/paWFk2YMEGSVF5eruLi4ph5CofD2rt3b0bN0+eff97r0zxHjBihSCQiiXlKCOurIM5m69atzufzueeff9598MEH7vbbb3ejR492wWDQemgm7rzzTuf3+92f//xn9/HHH0cfn3/+efScO+64w5WVlbm33nrL7d+/31VWVrrKykrDUXtD96vgnGOenDtziXpOTo5bs2aNO3z4sHvxxRfdeeed537/+99Hz1m7dq0bPXq0e/XVV91f//pXt3Dhwoy7vLi6utp94xvfiF6G/cc//tGdf/757r777ouewzwNjScDyDnnnnzySVdWVuZyc3PdrFmz3J49e6yHZEbSWR+bNm2KnvPFF1+4u+66y40ZM8add9557rrrrnMff/yx3aA9omcAMU9nvP76627q1KnO5/O5iooK98wzz8Q8H4lE3KpVq1wgEHA+n89dffXVrrm52Wi0NsLhsFu2bJkrKytzeXl57sILL3S/+MUvXGdnZ/Qc5mlo+DwgAIAJz/WAAACZgQACAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACQIIAGCCAAIAmCCAAAAm/g+YP2/ngKIe6gAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -315,17 +256,145 @@ "import xarray as xr\n", "import matplotlib.pyplot as plt\n", "\n", - "output = xr.open_dataset('2024_05_24_16_16_00_output_presto.nc')\n", + "output = xr.open_dataset(outputfile_name)\n", "plt.imshow(output['classification'])\n", "\n" ] + }, + { + "cell_type": "markdown", + "id": "a1f68e9d", + "metadata": {}, + "source": [ + "## Run the Presto UDF" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "20ae2b17", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'j-240529f3a2b540d583b08b62429bb60b': send 'start'\n", + "0:00:14 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", + "0:00:19 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", + "0:00:25 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", + "0:00:34 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", + "0:00:44 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", + "0:01:06 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", + "0:01:23 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:01:42 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:02:06 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:02:36 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:03:14 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:04:00 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:04:59 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:05:59 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:06:59 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:08:00 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:09:00 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", + "0:10:00 Job 'j-240529f3a2b540d583b08b62429bb60b': finished (progress 100%)\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from datetime import datetime\n", + "\n", + "current_datetime = datetime.now()\n", + "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", + "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", + "\n", + "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", + "\n", + "prediction = input_cube.apply_neighborhood(\n", + " process=udf,\n", + " size=[\n", + " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", + " ],\n", + " overlap=[\n", + " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", + " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", + " ],\n", + ")\n", + "\n", + "ft_names = [f\"presto_ft_{i}\" for i in range(128)]\n", + "prediction = prediction.drop_dimension('t').rename_labels(\"bands\", ft_names)\n", + "\n", + "prediction.execute_batch(outputfile = outputfile_name,\n", + " description='world cereal inference',\n", + " job_options={'driver-memory': '4g',\n", + " 'executor-memoryOverhead':'8g'} )" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7b9a580a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Size: 80kB\n", + "[10000 values with dtype=float64]\n", + "Coordinates:\n", + " * x (x) float64 800B 6.64e+05 6.64e+05 6.64e+05 ... 6.65e+05 6.65e+05\n", + " * y (y) float64 800B 5.612e+06 5.612e+06 ... 5.611e+06 5.611e+06\n", + "Attributes:\n", + " long_name: presto_ft_0\n", + " units: \n", + " grid_mapping: crs\n" + ] + } + ], + "source": [ + "presto = xr.open_dataset(outputfile_name)\n", + "print(presto['presto_ft_0'])" + ] } ], "metadata": { "kernelspec": { - "display_name": "worldcereal", + "display_name": "Python 3", "language": "python", - "name": "worldcereal" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -337,7 +406,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/minimal_wc_presto/dev_testing.py b/minimal_wc_presto/dev_testing.py deleted file mode 100644 index 4138680d..00000000 --- a/minimal_wc_presto/dev_testing.py +++ /dev/null @@ -1,83 +0,0 @@ -#%% -from pathlib import Path - -from pyproj import Transformer -import numpy as np - -import requests -import xarray as xr - - -#%% GET DEPENDENCIES -import urllib -# Generate absolute path for the dependencies folder -dependencies_dir = Path.cwd() / 'dependencies' -dependencies_dir.mkdir(exist_ok=True, parents=True) - -base_url = 'https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies' -dependency_name = "wc_presto_onnx_dependencies.zip" - -# Download and extract the model file -modelfile_url = f"{base_url}/{dependency_name}" -modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) -#shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - -#Add the model directory to system path if it's not already there -#abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) -#sys.path.append(abs_path) - -# Get Data -#url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc" -#filename = "belgium_good_2020-12-01_2021-11-30.nc" - -#with requests.get(url, stream=True) as r: -# r.raise_for_status() -# with open(filename, 'wb') as f: -# for chunk in r.iter_content(chunk_size=8192): -# f.write(chunk) - -#%% - -# Read the file into xarray -ds = xr.open_dataset('data/belgium_good_2020-12-01_2021-11-30.nc') - - -arr = ds.drop('crs').to_array(dim='bands') -arr[:,:,50:,50:] = np.nan -orig_dims = list(arr.dims) -map_dims = arr.shape[2:] - -#%% Get Presto -from mvp_wc_presto.world_cereal_inference import get_presto_features - -#bands: 19, t: 12y, : 100x: 100y -data_url = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/belgium_good_2020-12-01_2021-11-30.nc' -# Fetch the data from the URL -response = requests.get(data_url) - -#10000,128 -presto_path = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" -features = get_presto_features(arr, presto_path) - -#10000, -from mvp_wc_presto.world_cereal_inference import classify_with_catboost - -CATBOOST_PATH = 'https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx' -classification = classify_with_catboost(features, CATBOOST_PATH) - -#%% - -#%%plot output -import matplotlib.pyplot as plt - -transformer = Transformer.from_crs(f"EPSG:{4326}", "EPSG:4326", always_xy=True) -longitudes, latitudes = transformer.transform(arr.x, arr.y) -classification = np.flip(classification.reshape(map_dims),axis = 0) -classification = np.expand_dims(np.expand_dims(classification, axis=0),axis = 0) -output = xr.DataArray(classification, dims=orig_dims) - -output = output.to_numpy().squeeze() -plt.imshow(output) - -output.shape -# %% diff --git a/minimal_wc_presto/job-results.json b/minimal_wc_presto/job-results.json deleted file mode 100644 index bac243a9..00000000 --- a/minimal_wc_presto/job-results.json +++ /dev/null @@ -1 +0,0 @@ -{"assets": {"openEO_2020-11-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/5280a7fab73a3af7d65951d1ccc0edc7/openEO_2020-11-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28200.0, "mean": 28200.0, "minimum": 28200.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 3161.0, "mean": 3161.0, "minimum": 3161.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2020-11-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2020-12-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/04cff14b611d54240522833210762931/openEO_2020-12-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27864.0, "mean": 27864.0, "minimum": 27864.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 10083.0, "mean": 10083.0, "minimum": 10083.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2020-12-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-01-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 650, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/256561a0d78d5b22963c5d59f4768cd5/openEO_2021-01-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27609.0, "mean": 27609.0, "minimum": 27609.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 11985.0, "mean": 11985.0, "minimum": 11985.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-01-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-02-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/6259c389f92cda20f278a1c343486931/openEO_2021-02-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27772.0, "mean": 27772.0, "minimum": 27772.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 7615.0, "mean": 7615.0, "minimum": 7615.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-02-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-03-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8a1578c8d890289751276205a0864103/openEO_2021-03-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27964.0, "mean": 27964.0, "minimum": 27964.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 4934.0, "mean": 4934.0, "minimum": 4934.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-03-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-04-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/cd7a84f1e9dcd5107a01a6a3db1d2a90/openEO_2021-04-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 27975.0, "mean": 27975.0, "minimum": 27975.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 4408.0, "mean": 4408.0, "minimum": 4408.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-04-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-05-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8cef27e840684882775f0a8b46671209/openEO_2021-05-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28470.0, "mean": 28470.0, "minimum": 28470.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 10904.0, "mean": 10904.0, "minimum": 10904.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-05-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-06-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/a8c32bef4d950e8fe3ef37eaca87ee31/openEO_2021-06-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 29217.0, "mean": 29217.0, "minimum": 29217.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 14132.0, "mean": 14132.0, "minimum": 14132.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-06-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-07-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 650, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3e5e262c7faeb68d52a18f012bf7fe3f/openEO_2021-07-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 29138.0, "mean": 29138.0, "minimum": 29138.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 11926.0, "mean": 11926.0, "minimum": 11926.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-07-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-08-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/0b8d41a9211197d5be684162746fb830/openEO_2021-08-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 29034.0, "mean": 29034.0, "minimum": 29034.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 11072.0, "mean": 11072.0, "minimum": 11072.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-08-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-09-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 648, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3448e70d84ed3f2eb6e399a86b2f8b3d/openEO_2021-09-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28970.0, "mean": 28970.0, "minimum": 28970.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 3166.0, "mean": 3166.0, "minimum": 3166.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-09-01Z.tif", "type": "image/tiff; application=geotiff"}, "openEO_2021-10-01Z.tif": {"eo:bands": [{"name": "temperature-mean"}, {"name": "precipitation-flux"}], "file:size": 649, "href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/assets/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/9c3ea792c00e0a1b63231b8b149f997a/openEO_2021-10-01Z.tif?expires=1716455737", "proj:bbox": [5.15, 51.25, 5.25, 51.35], "proj:epsg": 4326, "proj:shape": [1, 1], "raster:bands": [{"name": "temperature-mean", "statistics": {"maximum": 28489.0, "mean": 28489.0, "minimum": 28489.0, "stddev": 0.0, "valid_percent": 100.0}}, {"name": "precipitation-flux", "statistics": {"maximum": 10229.0, "mean": 10229.0, "minimum": 10229.0, "stddev": 0.0, "valid_percent": 100.0}}], "roles": ["data"], "title": "openEO_2021-10-01Z.tif", "type": "image/tiff; application=geotiff"}}, "description": "Results for batch job j-2405169cad524b05a8f1194330e4c44d", "extent": {"spatial": {"bbox": [[5.19, 51.25, 5.21, 51.26]]}, "temporal": {"interval": [["2020-11-01T00:00:00Z", "2021-10-31T00:00:00Z"]]}}, "id": "j-2405169cad524b05a8f1194330e4c44d", "license": "proprietary", "links": [{"href": "/data/MTDA/AgERA5/2020/20201101/AgERA5_dewpoint-temperature_20201101.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201101/AgERA5_dewpoint-temperature_20201101.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201102/AgERA5_dewpoint-temperature_20201102.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201102/AgERA5_dewpoint-temperature_20201102.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201103/AgERA5_dewpoint-temperature_20201103.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201103/AgERA5_dewpoint-temperature_20201103.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201104/AgERA5_dewpoint-temperature_20201104.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201104/AgERA5_dewpoint-temperature_20201104.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201105/AgERA5_dewpoint-temperature_20201105.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201105/AgERA5_dewpoint-temperature_20201105.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201106/AgERA5_dewpoint-temperature_20201106.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201106/AgERA5_dewpoint-temperature_20201106.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201107/AgERA5_dewpoint-temperature_20201107.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201107/AgERA5_dewpoint-temperature_20201107.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201108/AgERA5_dewpoint-temperature_20201108.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201108/AgERA5_dewpoint-temperature_20201108.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201109/AgERA5_dewpoint-temperature_20201109.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201109/AgERA5_dewpoint-temperature_20201109.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201110/AgERA5_dewpoint-temperature_20201110.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201110/AgERA5_dewpoint-temperature_20201110.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201111/AgERA5_dewpoint-temperature_20201111.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201111/AgERA5_dewpoint-temperature_20201111.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201112/AgERA5_dewpoint-temperature_20201112.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201112/AgERA5_dewpoint-temperature_20201112.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201113/AgERA5_dewpoint-temperature_20201113.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201113/AgERA5_dewpoint-temperature_20201113.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201114/AgERA5_dewpoint-temperature_20201114.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201114/AgERA5_dewpoint-temperature_20201114.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201115/AgERA5_dewpoint-temperature_20201115.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201115/AgERA5_dewpoint-temperature_20201115.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201116/AgERA5_dewpoint-temperature_20201116.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201116/AgERA5_dewpoint-temperature_20201116.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201117/AgERA5_dewpoint-temperature_20201117.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201117/AgERA5_dewpoint-temperature_20201117.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201118/AgERA5_dewpoint-temperature_20201118.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201118/AgERA5_dewpoint-temperature_20201118.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201119/AgERA5_dewpoint-temperature_20201119.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201119/AgERA5_dewpoint-temperature_20201119.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201120/AgERA5_dewpoint-temperature_20201120.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201120/AgERA5_dewpoint-temperature_20201120.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201121/AgERA5_dewpoint-temperature_20201121.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201121/AgERA5_dewpoint-temperature_20201121.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201122/AgERA5_dewpoint-temperature_20201122.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201122/AgERA5_dewpoint-temperature_20201122.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201123/AgERA5_dewpoint-temperature_20201123.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201123/AgERA5_dewpoint-temperature_20201123.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201124/AgERA5_dewpoint-temperature_20201124.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201124/AgERA5_dewpoint-temperature_20201124.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201125/AgERA5_dewpoint-temperature_20201125.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201125/AgERA5_dewpoint-temperature_20201125.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201126/AgERA5_dewpoint-temperature_20201126.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201126/AgERA5_dewpoint-temperature_20201126.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201127/AgERA5_dewpoint-temperature_20201127.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201127/AgERA5_dewpoint-temperature_20201127.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201128/AgERA5_dewpoint-temperature_20201128.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201128/AgERA5_dewpoint-temperature_20201128.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201129/AgERA5_dewpoint-temperature_20201129.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201129/AgERA5_dewpoint-temperature_20201129.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201130/AgERA5_dewpoint-temperature_20201130.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201130/AgERA5_dewpoint-temperature_20201130.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201201/AgERA5_dewpoint-temperature_20201201.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201201/AgERA5_dewpoint-temperature_20201201.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201202/AgERA5_dewpoint-temperature_20201202.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201202/AgERA5_dewpoint-temperature_20201202.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201203/AgERA5_dewpoint-temperature_20201203.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201203/AgERA5_dewpoint-temperature_20201203.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201204/AgERA5_dewpoint-temperature_20201204.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201204/AgERA5_dewpoint-temperature_20201204.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201205/AgERA5_dewpoint-temperature_20201205.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201205/AgERA5_dewpoint-temperature_20201205.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201206/AgERA5_dewpoint-temperature_20201206.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201206/AgERA5_dewpoint-temperature_20201206.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201207/AgERA5_dewpoint-temperature_20201207.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201207/AgERA5_dewpoint-temperature_20201207.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201208/AgERA5_dewpoint-temperature_20201208.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201208/AgERA5_dewpoint-temperature_20201208.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201209/AgERA5_dewpoint-temperature_20201209.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201209/AgERA5_dewpoint-temperature_20201209.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201210/AgERA5_dewpoint-temperature_20201210.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201210/AgERA5_dewpoint-temperature_20201210.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201211/AgERA5_dewpoint-temperature_20201211.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201211/AgERA5_dewpoint-temperature_20201211.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201212/AgERA5_dewpoint-temperature_20201212.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201212/AgERA5_dewpoint-temperature_20201212.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201213/AgERA5_dewpoint-temperature_20201213.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201213/AgERA5_dewpoint-temperature_20201213.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201214/AgERA5_dewpoint-temperature_20201214.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201214/AgERA5_dewpoint-temperature_20201214.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201215/AgERA5_dewpoint-temperature_20201215.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201215/AgERA5_dewpoint-temperature_20201215.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201216/AgERA5_dewpoint-temperature_20201216.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201216/AgERA5_dewpoint-temperature_20201216.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201217/AgERA5_dewpoint-temperature_20201217.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201217/AgERA5_dewpoint-temperature_20201217.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201218/AgERA5_dewpoint-temperature_20201218.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201218/AgERA5_dewpoint-temperature_20201218.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201219/AgERA5_dewpoint-temperature_20201219.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201219/AgERA5_dewpoint-temperature_20201219.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201220/AgERA5_dewpoint-temperature_20201220.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201220/AgERA5_dewpoint-temperature_20201220.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201221/AgERA5_dewpoint-temperature_20201221.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201221/AgERA5_dewpoint-temperature_20201221.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201222/AgERA5_dewpoint-temperature_20201222.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201222/AgERA5_dewpoint-temperature_20201222.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201223/AgERA5_dewpoint-temperature_20201223.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201223/AgERA5_dewpoint-temperature_20201223.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201224/AgERA5_dewpoint-temperature_20201224.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201224/AgERA5_dewpoint-temperature_20201224.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201225/AgERA5_dewpoint-temperature_20201225.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201225/AgERA5_dewpoint-temperature_20201225.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201226/AgERA5_dewpoint-temperature_20201226.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201226/AgERA5_dewpoint-temperature_20201226.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201227/AgERA5_dewpoint-temperature_20201227.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201227/AgERA5_dewpoint-temperature_20201227.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201228/AgERA5_dewpoint-temperature_20201228.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201228/AgERA5_dewpoint-temperature_20201228.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201229/AgERA5_dewpoint-temperature_20201229.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201229/AgERA5_dewpoint-temperature_20201229.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201230/AgERA5_dewpoint-temperature_20201230.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201230/AgERA5_dewpoint-temperature_20201230.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2020/20201231/AgERA5_dewpoint-temperature_20201231.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2020/20201231/AgERA5_dewpoint-temperature_20201231.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210101/AgERA5_dewpoint-temperature_20210101.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210101/AgERA5_dewpoint-temperature_20210101.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210102/AgERA5_dewpoint-temperature_20210102.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210102/AgERA5_dewpoint-temperature_20210102.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210103/AgERA5_dewpoint-temperature_20210103.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210103/AgERA5_dewpoint-temperature_20210103.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210104/AgERA5_dewpoint-temperature_20210104.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210104/AgERA5_dewpoint-temperature_20210104.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210105/AgERA5_dewpoint-temperature_20210105.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210105/AgERA5_dewpoint-temperature_20210105.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210106/AgERA5_dewpoint-temperature_20210106.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210106/AgERA5_dewpoint-temperature_20210106.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210107/AgERA5_dewpoint-temperature_20210107.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210107/AgERA5_dewpoint-temperature_20210107.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210108/AgERA5_dewpoint-temperature_20210108.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210108/AgERA5_dewpoint-temperature_20210108.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210109/AgERA5_dewpoint-temperature_20210109.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210109/AgERA5_dewpoint-temperature_20210109.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210110/AgERA5_dewpoint-temperature_20210110.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210110/AgERA5_dewpoint-temperature_20210110.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210111/AgERA5_dewpoint-temperature_20210111.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210111/AgERA5_dewpoint-temperature_20210111.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210112/AgERA5_dewpoint-temperature_20210112.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210112/AgERA5_dewpoint-temperature_20210112.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210113/AgERA5_dewpoint-temperature_20210113.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210113/AgERA5_dewpoint-temperature_20210113.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210114/AgERA5_dewpoint-temperature_20210114.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210114/AgERA5_dewpoint-temperature_20210114.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210115/AgERA5_dewpoint-temperature_20210115.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210115/AgERA5_dewpoint-temperature_20210115.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210116/AgERA5_dewpoint-temperature_20210116.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210116/AgERA5_dewpoint-temperature_20210116.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210117/AgERA5_dewpoint-temperature_20210117.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210117/AgERA5_dewpoint-temperature_20210117.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210118/AgERA5_dewpoint-temperature_20210118.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210118/AgERA5_dewpoint-temperature_20210118.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210119/AgERA5_dewpoint-temperature_20210119.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210119/AgERA5_dewpoint-temperature_20210119.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210120/AgERA5_dewpoint-temperature_20210120.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210120/AgERA5_dewpoint-temperature_20210120.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210121/AgERA5_dewpoint-temperature_20210121.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210121/AgERA5_dewpoint-temperature_20210121.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210122/AgERA5_dewpoint-temperature_20210122.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210122/AgERA5_dewpoint-temperature_20210122.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210123/AgERA5_dewpoint-temperature_20210123.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210123/AgERA5_dewpoint-temperature_20210123.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210124/AgERA5_dewpoint-temperature_20210124.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210124/AgERA5_dewpoint-temperature_20210124.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210125/AgERA5_dewpoint-temperature_20210125.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210125/AgERA5_dewpoint-temperature_20210125.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210126/AgERA5_dewpoint-temperature_20210126.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210126/AgERA5_dewpoint-temperature_20210126.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210127/AgERA5_dewpoint-temperature_20210127.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210127/AgERA5_dewpoint-temperature_20210127.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210128/AgERA5_dewpoint-temperature_20210128.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210128/AgERA5_dewpoint-temperature_20210128.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210129/AgERA5_dewpoint-temperature_20210129.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210129/AgERA5_dewpoint-temperature_20210129.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210130/AgERA5_dewpoint-temperature_20210130.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210130/AgERA5_dewpoint-temperature_20210130.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210131/AgERA5_dewpoint-temperature_20210131.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210131/AgERA5_dewpoint-temperature_20210131.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210201/AgERA5_dewpoint-temperature_20210201.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210201/AgERA5_dewpoint-temperature_20210201.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210202/AgERA5_dewpoint-temperature_20210202.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210202/AgERA5_dewpoint-temperature_20210202.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210203/AgERA5_dewpoint-temperature_20210203.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210203/AgERA5_dewpoint-temperature_20210203.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210204/AgERA5_dewpoint-temperature_20210204.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210204/AgERA5_dewpoint-temperature_20210204.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210205/AgERA5_dewpoint-temperature_20210205.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210205/AgERA5_dewpoint-temperature_20210205.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210206/AgERA5_dewpoint-temperature_20210206.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210206/AgERA5_dewpoint-temperature_20210206.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210207/AgERA5_dewpoint-temperature_20210207.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210207/AgERA5_dewpoint-temperature_20210207.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210208/AgERA5_dewpoint-temperature_20210208.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210208/AgERA5_dewpoint-temperature_20210208.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210209/AgERA5_dewpoint-temperature_20210209.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210209/AgERA5_dewpoint-temperature_20210209.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210210/AgERA5_dewpoint-temperature_20210210.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210210/AgERA5_dewpoint-temperature_20210210.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210211/AgERA5_dewpoint-temperature_20210211.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210211/AgERA5_dewpoint-temperature_20210211.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210212/AgERA5_dewpoint-temperature_20210212.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210212/AgERA5_dewpoint-temperature_20210212.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210213/AgERA5_dewpoint-temperature_20210213.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210213/AgERA5_dewpoint-temperature_20210213.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210214/AgERA5_dewpoint-temperature_20210214.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210214/AgERA5_dewpoint-temperature_20210214.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210215/AgERA5_dewpoint-temperature_20210215.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210215/AgERA5_dewpoint-temperature_20210215.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210216/AgERA5_dewpoint-temperature_20210216.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210216/AgERA5_dewpoint-temperature_20210216.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210217/AgERA5_dewpoint-temperature_20210217.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210217/AgERA5_dewpoint-temperature_20210217.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210218/AgERA5_dewpoint-temperature_20210218.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210218/AgERA5_dewpoint-temperature_20210218.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210219/AgERA5_dewpoint-temperature_20210219.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210219/AgERA5_dewpoint-temperature_20210219.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210220/AgERA5_dewpoint-temperature_20210220.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210220/AgERA5_dewpoint-temperature_20210220.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210221/AgERA5_dewpoint-temperature_20210221.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210221/AgERA5_dewpoint-temperature_20210221.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210222/AgERA5_dewpoint-temperature_20210222.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210222/AgERA5_dewpoint-temperature_20210222.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210223/AgERA5_dewpoint-temperature_20210223.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210223/AgERA5_dewpoint-temperature_20210223.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210224/AgERA5_dewpoint-temperature_20210224.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210224/AgERA5_dewpoint-temperature_20210224.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210225/AgERA5_dewpoint-temperature_20210225.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210225/AgERA5_dewpoint-temperature_20210225.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210226/AgERA5_dewpoint-temperature_20210226.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210226/AgERA5_dewpoint-temperature_20210226.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210227/AgERA5_dewpoint-temperature_20210227.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210227/AgERA5_dewpoint-temperature_20210227.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210228/AgERA5_dewpoint-temperature_20210228.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210228/AgERA5_dewpoint-temperature_20210228.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210301/AgERA5_dewpoint-temperature_20210301.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210301/AgERA5_dewpoint-temperature_20210301.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210302/AgERA5_dewpoint-temperature_20210302.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210302/AgERA5_dewpoint-temperature_20210302.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210303/AgERA5_dewpoint-temperature_20210303.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210303/AgERA5_dewpoint-temperature_20210303.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210304/AgERA5_dewpoint-temperature_20210304.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210304/AgERA5_dewpoint-temperature_20210304.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210305/AgERA5_dewpoint-temperature_20210305.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210305/AgERA5_dewpoint-temperature_20210305.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210306/AgERA5_dewpoint-temperature_20210306.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210306/AgERA5_dewpoint-temperature_20210306.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210307/AgERA5_dewpoint-temperature_20210307.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210307/AgERA5_dewpoint-temperature_20210307.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210308/AgERA5_dewpoint-temperature_20210308.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210308/AgERA5_dewpoint-temperature_20210308.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210309/AgERA5_dewpoint-temperature_20210309.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210309/AgERA5_dewpoint-temperature_20210309.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210310/AgERA5_dewpoint-temperature_20210310.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210310/AgERA5_dewpoint-temperature_20210310.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210311/AgERA5_dewpoint-temperature_20210311.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210311/AgERA5_dewpoint-temperature_20210311.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210312/AgERA5_dewpoint-temperature_20210312.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210312/AgERA5_dewpoint-temperature_20210312.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210313/AgERA5_dewpoint-temperature_20210313.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210313/AgERA5_dewpoint-temperature_20210313.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210314/AgERA5_dewpoint-temperature_20210314.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210314/AgERA5_dewpoint-temperature_20210314.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210315/AgERA5_dewpoint-temperature_20210315.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210315/AgERA5_dewpoint-temperature_20210315.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210316/AgERA5_dewpoint-temperature_20210316.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210316/AgERA5_dewpoint-temperature_20210316.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210317/AgERA5_dewpoint-temperature_20210317.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210317/AgERA5_dewpoint-temperature_20210317.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210318/AgERA5_dewpoint-temperature_20210318.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210318/AgERA5_dewpoint-temperature_20210318.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210319/AgERA5_dewpoint-temperature_20210319.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210319/AgERA5_dewpoint-temperature_20210319.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210320/AgERA5_dewpoint-temperature_20210320.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210320/AgERA5_dewpoint-temperature_20210320.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210321/AgERA5_dewpoint-temperature_20210321.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210321/AgERA5_dewpoint-temperature_20210321.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210322/AgERA5_dewpoint-temperature_20210322.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210322/AgERA5_dewpoint-temperature_20210322.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210323/AgERA5_dewpoint-temperature_20210323.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210323/AgERA5_dewpoint-temperature_20210323.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210324/AgERA5_dewpoint-temperature_20210324.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210324/AgERA5_dewpoint-temperature_20210324.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210325/AgERA5_dewpoint-temperature_20210325.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210325/AgERA5_dewpoint-temperature_20210325.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210326/AgERA5_dewpoint-temperature_20210326.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210326/AgERA5_dewpoint-temperature_20210326.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210327/AgERA5_dewpoint-temperature_20210327.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210327/AgERA5_dewpoint-temperature_20210327.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210328/AgERA5_dewpoint-temperature_20210328.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210328/AgERA5_dewpoint-temperature_20210328.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210329/AgERA5_dewpoint-temperature_20210329.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210329/AgERA5_dewpoint-temperature_20210329.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210330/AgERA5_dewpoint-temperature_20210330.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210330/AgERA5_dewpoint-temperature_20210330.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210331/AgERA5_dewpoint-temperature_20210331.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210331/AgERA5_dewpoint-temperature_20210331.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210401/AgERA5_dewpoint-temperature_20210401.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210401/AgERA5_dewpoint-temperature_20210401.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210402/AgERA5_dewpoint-temperature_20210402.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210402/AgERA5_dewpoint-temperature_20210402.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210403/AgERA5_dewpoint-temperature_20210403.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210403/AgERA5_dewpoint-temperature_20210403.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210404/AgERA5_dewpoint-temperature_20210404.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210404/AgERA5_dewpoint-temperature_20210404.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210405/AgERA5_dewpoint-temperature_20210405.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210405/AgERA5_dewpoint-temperature_20210405.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210406/AgERA5_dewpoint-temperature_20210406.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210406/AgERA5_dewpoint-temperature_20210406.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210407/AgERA5_dewpoint-temperature_20210407.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210407/AgERA5_dewpoint-temperature_20210407.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210408/AgERA5_dewpoint-temperature_20210408.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210408/AgERA5_dewpoint-temperature_20210408.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210409/AgERA5_dewpoint-temperature_20210409.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210409/AgERA5_dewpoint-temperature_20210409.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210410/AgERA5_dewpoint-temperature_20210410.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210410/AgERA5_dewpoint-temperature_20210410.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210411/AgERA5_dewpoint-temperature_20210411.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210411/AgERA5_dewpoint-temperature_20210411.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210412/AgERA5_dewpoint-temperature_20210412.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210412/AgERA5_dewpoint-temperature_20210412.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210413/AgERA5_dewpoint-temperature_20210413.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210413/AgERA5_dewpoint-temperature_20210413.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210414/AgERA5_dewpoint-temperature_20210414.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210414/AgERA5_dewpoint-temperature_20210414.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210415/AgERA5_dewpoint-temperature_20210415.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210415/AgERA5_dewpoint-temperature_20210415.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210416/AgERA5_dewpoint-temperature_20210416.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210416/AgERA5_dewpoint-temperature_20210416.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210417/AgERA5_dewpoint-temperature_20210417.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210417/AgERA5_dewpoint-temperature_20210417.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210418/AgERA5_dewpoint-temperature_20210418.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210418/AgERA5_dewpoint-temperature_20210418.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210419/AgERA5_dewpoint-temperature_20210419.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210419/AgERA5_dewpoint-temperature_20210419.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210420/AgERA5_dewpoint-temperature_20210420.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210420/AgERA5_dewpoint-temperature_20210420.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210421/AgERA5_dewpoint-temperature_20210421.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210421/AgERA5_dewpoint-temperature_20210421.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210422/AgERA5_dewpoint-temperature_20210422.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210422/AgERA5_dewpoint-temperature_20210422.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210423/AgERA5_dewpoint-temperature_20210423.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210423/AgERA5_dewpoint-temperature_20210423.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210424/AgERA5_dewpoint-temperature_20210424.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210424/AgERA5_dewpoint-temperature_20210424.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210425/AgERA5_dewpoint-temperature_20210425.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210425/AgERA5_dewpoint-temperature_20210425.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210426/AgERA5_dewpoint-temperature_20210426.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210426/AgERA5_dewpoint-temperature_20210426.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210427/AgERA5_dewpoint-temperature_20210427.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210427/AgERA5_dewpoint-temperature_20210427.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210428/AgERA5_dewpoint-temperature_20210428.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210428/AgERA5_dewpoint-temperature_20210428.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210429/AgERA5_dewpoint-temperature_20210429.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210429/AgERA5_dewpoint-temperature_20210429.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210430/AgERA5_dewpoint-temperature_20210430.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210430/AgERA5_dewpoint-temperature_20210430.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210501/AgERA5_dewpoint-temperature_20210501.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210501/AgERA5_dewpoint-temperature_20210501.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210502/AgERA5_dewpoint-temperature_20210502.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210502/AgERA5_dewpoint-temperature_20210502.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210503/AgERA5_dewpoint-temperature_20210503.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210503/AgERA5_dewpoint-temperature_20210503.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210504/AgERA5_dewpoint-temperature_20210504.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210504/AgERA5_dewpoint-temperature_20210504.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210505/AgERA5_dewpoint-temperature_20210505.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210505/AgERA5_dewpoint-temperature_20210505.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210506/AgERA5_dewpoint-temperature_20210506.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210506/AgERA5_dewpoint-temperature_20210506.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210507/AgERA5_dewpoint-temperature_20210507.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210507/AgERA5_dewpoint-temperature_20210507.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210508/AgERA5_dewpoint-temperature_20210508.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210508/AgERA5_dewpoint-temperature_20210508.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210509/AgERA5_dewpoint-temperature_20210509.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210509/AgERA5_dewpoint-temperature_20210509.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210510/AgERA5_dewpoint-temperature_20210510.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210510/AgERA5_dewpoint-temperature_20210510.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210511/AgERA5_dewpoint-temperature_20210511.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210511/AgERA5_dewpoint-temperature_20210511.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210512/AgERA5_dewpoint-temperature_20210512.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210512/AgERA5_dewpoint-temperature_20210512.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210513/AgERA5_dewpoint-temperature_20210513.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210513/AgERA5_dewpoint-temperature_20210513.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210514/AgERA5_dewpoint-temperature_20210514.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210514/AgERA5_dewpoint-temperature_20210514.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210515/AgERA5_dewpoint-temperature_20210515.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210515/AgERA5_dewpoint-temperature_20210515.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210516/AgERA5_dewpoint-temperature_20210516.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210516/AgERA5_dewpoint-temperature_20210516.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210517/AgERA5_dewpoint-temperature_20210517.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210517/AgERA5_dewpoint-temperature_20210517.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210518/AgERA5_dewpoint-temperature_20210518.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210518/AgERA5_dewpoint-temperature_20210518.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210519/AgERA5_dewpoint-temperature_20210519.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210519/AgERA5_dewpoint-temperature_20210519.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210520/AgERA5_dewpoint-temperature_20210520.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210520/AgERA5_dewpoint-temperature_20210520.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210521/AgERA5_dewpoint-temperature_20210521.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210521/AgERA5_dewpoint-temperature_20210521.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210522/AgERA5_dewpoint-temperature_20210522.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210522/AgERA5_dewpoint-temperature_20210522.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210523/AgERA5_dewpoint-temperature_20210523.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210523/AgERA5_dewpoint-temperature_20210523.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210524/AgERA5_dewpoint-temperature_20210524.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210524/AgERA5_dewpoint-temperature_20210524.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210525/AgERA5_dewpoint-temperature_20210525.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210525/AgERA5_dewpoint-temperature_20210525.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210526/AgERA5_dewpoint-temperature_20210526.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210526/AgERA5_dewpoint-temperature_20210526.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210527/AgERA5_dewpoint-temperature_20210527.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210527/AgERA5_dewpoint-temperature_20210527.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210528/AgERA5_dewpoint-temperature_20210528.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210528/AgERA5_dewpoint-temperature_20210528.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210529/AgERA5_dewpoint-temperature_20210529.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210529/AgERA5_dewpoint-temperature_20210529.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210530/AgERA5_dewpoint-temperature_20210530.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210530/AgERA5_dewpoint-temperature_20210530.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210531/AgERA5_dewpoint-temperature_20210531.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210531/AgERA5_dewpoint-temperature_20210531.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210601/AgERA5_dewpoint-temperature_20210601.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210601/AgERA5_dewpoint-temperature_20210601.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210602/AgERA5_dewpoint-temperature_20210602.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210602/AgERA5_dewpoint-temperature_20210602.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210603/AgERA5_dewpoint-temperature_20210603.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210603/AgERA5_dewpoint-temperature_20210603.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210604/AgERA5_dewpoint-temperature_20210604.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210604/AgERA5_dewpoint-temperature_20210604.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210605/AgERA5_dewpoint-temperature_20210605.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210605/AgERA5_dewpoint-temperature_20210605.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210606/AgERA5_dewpoint-temperature_20210606.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210606/AgERA5_dewpoint-temperature_20210606.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210607/AgERA5_dewpoint-temperature_20210607.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210607/AgERA5_dewpoint-temperature_20210607.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210608/AgERA5_dewpoint-temperature_20210608.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210608/AgERA5_dewpoint-temperature_20210608.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210609/AgERA5_dewpoint-temperature_20210609.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210609/AgERA5_dewpoint-temperature_20210609.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210610/AgERA5_dewpoint-temperature_20210610.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210610/AgERA5_dewpoint-temperature_20210610.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210611/AgERA5_dewpoint-temperature_20210611.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210611/AgERA5_dewpoint-temperature_20210611.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210612/AgERA5_dewpoint-temperature_20210612.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210612/AgERA5_dewpoint-temperature_20210612.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210613/AgERA5_dewpoint-temperature_20210613.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210613/AgERA5_dewpoint-temperature_20210613.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210614/AgERA5_dewpoint-temperature_20210614.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210614/AgERA5_dewpoint-temperature_20210614.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210615/AgERA5_dewpoint-temperature_20210615.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210615/AgERA5_dewpoint-temperature_20210615.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210616/AgERA5_dewpoint-temperature_20210616.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210616/AgERA5_dewpoint-temperature_20210616.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210617/AgERA5_dewpoint-temperature_20210617.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210617/AgERA5_dewpoint-temperature_20210617.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210618/AgERA5_dewpoint-temperature_20210618.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210618/AgERA5_dewpoint-temperature_20210618.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210619/AgERA5_dewpoint-temperature_20210619.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210619/AgERA5_dewpoint-temperature_20210619.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210620/AgERA5_dewpoint-temperature_20210620.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210620/AgERA5_dewpoint-temperature_20210620.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210621/AgERA5_dewpoint-temperature_20210621.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210621/AgERA5_dewpoint-temperature_20210621.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210622/AgERA5_dewpoint-temperature_20210622.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210622/AgERA5_dewpoint-temperature_20210622.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210623/AgERA5_dewpoint-temperature_20210623.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210623/AgERA5_dewpoint-temperature_20210623.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210624/AgERA5_dewpoint-temperature_20210624.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210624/AgERA5_dewpoint-temperature_20210624.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210625/AgERA5_dewpoint-temperature_20210625.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210625/AgERA5_dewpoint-temperature_20210625.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210626/AgERA5_dewpoint-temperature_20210626.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210626/AgERA5_dewpoint-temperature_20210626.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210627/AgERA5_dewpoint-temperature_20210627.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210627/AgERA5_dewpoint-temperature_20210627.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210628/AgERA5_dewpoint-temperature_20210628.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210628/AgERA5_dewpoint-temperature_20210628.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210629/AgERA5_dewpoint-temperature_20210629.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210629/AgERA5_dewpoint-temperature_20210629.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210630/AgERA5_dewpoint-temperature_20210630.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210630/AgERA5_dewpoint-temperature_20210630.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210701/AgERA5_dewpoint-temperature_20210701.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210701/AgERA5_dewpoint-temperature_20210701.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210702/AgERA5_dewpoint-temperature_20210702.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210702/AgERA5_dewpoint-temperature_20210702.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210703/AgERA5_dewpoint-temperature_20210703.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210703/AgERA5_dewpoint-temperature_20210703.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210704/AgERA5_dewpoint-temperature_20210704.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210704/AgERA5_dewpoint-temperature_20210704.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210705/AgERA5_dewpoint-temperature_20210705.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210705/AgERA5_dewpoint-temperature_20210705.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210706/AgERA5_dewpoint-temperature_20210706.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210706/AgERA5_dewpoint-temperature_20210706.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210707/AgERA5_dewpoint-temperature_20210707.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210707/AgERA5_dewpoint-temperature_20210707.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210708/AgERA5_dewpoint-temperature_20210708.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210708/AgERA5_dewpoint-temperature_20210708.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210709/AgERA5_dewpoint-temperature_20210709.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210709/AgERA5_dewpoint-temperature_20210709.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210710/AgERA5_dewpoint-temperature_20210710.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210710/AgERA5_dewpoint-temperature_20210710.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210711/AgERA5_dewpoint-temperature_20210711.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210711/AgERA5_dewpoint-temperature_20210711.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210712/AgERA5_dewpoint-temperature_20210712.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210712/AgERA5_dewpoint-temperature_20210712.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210713/AgERA5_dewpoint-temperature_20210713.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210713/AgERA5_dewpoint-temperature_20210713.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210714/AgERA5_dewpoint-temperature_20210714.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210714/AgERA5_dewpoint-temperature_20210714.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210715/AgERA5_dewpoint-temperature_20210715.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210715/AgERA5_dewpoint-temperature_20210715.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210716/AgERA5_dewpoint-temperature_20210716.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210716/AgERA5_dewpoint-temperature_20210716.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210717/AgERA5_dewpoint-temperature_20210717.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210717/AgERA5_dewpoint-temperature_20210717.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210718/AgERA5_dewpoint-temperature_20210718.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210718/AgERA5_dewpoint-temperature_20210718.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210719/AgERA5_dewpoint-temperature_20210719.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210719/AgERA5_dewpoint-temperature_20210719.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210720/AgERA5_dewpoint-temperature_20210720.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210720/AgERA5_dewpoint-temperature_20210720.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210721/AgERA5_dewpoint-temperature_20210721.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210721/AgERA5_dewpoint-temperature_20210721.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210722/AgERA5_dewpoint-temperature_20210722.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210722/AgERA5_dewpoint-temperature_20210722.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210723/AgERA5_dewpoint-temperature_20210723.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210723/AgERA5_dewpoint-temperature_20210723.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210724/AgERA5_dewpoint-temperature_20210724.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210724/AgERA5_dewpoint-temperature_20210724.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210725/AgERA5_dewpoint-temperature_20210725.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210725/AgERA5_dewpoint-temperature_20210725.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210726/AgERA5_dewpoint-temperature_20210726.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210726/AgERA5_dewpoint-temperature_20210726.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210727/AgERA5_dewpoint-temperature_20210727.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210727/AgERA5_dewpoint-temperature_20210727.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210728/AgERA5_dewpoint-temperature_20210728.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210728/AgERA5_dewpoint-temperature_20210728.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210729/AgERA5_dewpoint-temperature_20210729.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210729/AgERA5_dewpoint-temperature_20210729.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210730/AgERA5_dewpoint-temperature_20210730.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210730/AgERA5_dewpoint-temperature_20210730.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210731/AgERA5_dewpoint-temperature_20210731.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210731/AgERA5_dewpoint-temperature_20210731.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210801/AgERA5_dewpoint-temperature_20210801.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210801/AgERA5_dewpoint-temperature_20210801.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210802/AgERA5_dewpoint-temperature_20210802.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210802/AgERA5_dewpoint-temperature_20210802.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210803/AgERA5_dewpoint-temperature_20210803.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210803/AgERA5_dewpoint-temperature_20210803.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210804/AgERA5_dewpoint-temperature_20210804.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210804/AgERA5_dewpoint-temperature_20210804.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210805/AgERA5_dewpoint-temperature_20210805.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210805/AgERA5_dewpoint-temperature_20210805.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210806/AgERA5_dewpoint-temperature_20210806.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210806/AgERA5_dewpoint-temperature_20210806.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210807/AgERA5_dewpoint-temperature_20210807.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210807/AgERA5_dewpoint-temperature_20210807.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210808/AgERA5_dewpoint-temperature_20210808.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210808/AgERA5_dewpoint-temperature_20210808.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210809/AgERA5_dewpoint-temperature_20210809.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210809/AgERA5_dewpoint-temperature_20210809.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210810/AgERA5_dewpoint-temperature_20210810.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210810/AgERA5_dewpoint-temperature_20210810.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210811/AgERA5_dewpoint-temperature_20210811.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210811/AgERA5_dewpoint-temperature_20210811.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210812/AgERA5_dewpoint-temperature_20210812.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210812/AgERA5_dewpoint-temperature_20210812.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210813/AgERA5_dewpoint-temperature_20210813.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210813/AgERA5_dewpoint-temperature_20210813.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210814/AgERA5_dewpoint-temperature_20210814.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210814/AgERA5_dewpoint-temperature_20210814.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210815/AgERA5_dewpoint-temperature_20210815.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210815/AgERA5_dewpoint-temperature_20210815.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210816/AgERA5_dewpoint-temperature_20210816.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210816/AgERA5_dewpoint-temperature_20210816.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210817/AgERA5_dewpoint-temperature_20210817.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210817/AgERA5_dewpoint-temperature_20210817.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210818/AgERA5_dewpoint-temperature_20210818.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210818/AgERA5_dewpoint-temperature_20210818.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210819/AgERA5_dewpoint-temperature_20210819.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210819/AgERA5_dewpoint-temperature_20210819.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210820/AgERA5_dewpoint-temperature_20210820.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210820/AgERA5_dewpoint-temperature_20210820.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210821/AgERA5_dewpoint-temperature_20210821.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210821/AgERA5_dewpoint-temperature_20210821.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210822/AgERA5_dewpoint-temperature_20210822.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210822/AgERA5_dewpoint-temperature_20210822.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210823/AgERA5_dewpoint-temperature_20210823.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210823/AgERA5_dewpoint-temperature_20210823.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210824/AgERA5_dewpoint-temperature_20210824.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210824/AgERA5_dewpoint-temperature_20210824.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210825/AgERA5_dewpoint-temperature_20210825.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210825/AgERA5_dewpoint-temperature_20210825.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210826/AgERA5_dewpoint-temperature_20210826.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210826/AgERA5_dewpoint-temperature_20210826.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210827/AgERA5_dewpoint-temperature_20210827.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210827/AgERA5_dewpoint-temperature_20210827.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210828/AgERA5_dewpoint-temperature_20210828.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210828/AgERA5_dewpoint-temperature_20210828.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210829/AgERA5_dewpoint-temperature_20210829.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210829/AgERA5_dewpoint-temperature_20210829.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210830/AgERA5_dewpoint-temperature_20210830.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210830/AgERA5_dewpoint-temperature_20210830.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210831/AgERA5_dewpoint-temperature_20210831.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210831/AgERA5_dewpoint-temperature_20210831.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210901/AgERA5_dewpoint-temperature_20210901.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210901/AgERA5_dewpoint-temperature_20210901.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210902/AgERA5_dewpoint-temperature_20210902.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210902/AgERA5_dewpoint-temperature_20210902.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210903/AgERA5_dewpoint-temperature_20210903.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210903/AgERA5_dewpoint-temperature_20210903.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210904/AgERA5_dewpoint-temperature_20210904.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210904/AgERA5_dewpoint-temperature_20210904.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210905/AgERA5_dewpoint-temperature_20210905.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210905/AgERA5_dewpoint-temperature_20210905.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210906/AgERA5_dewpoint-temperature_20210906.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210906/AgERA5_dewpoint-temperature_20210906.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210907/AgERA5_dewpoint-temperature_20210907.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210907/AgERA5_dewpoint-temperature_20210907.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210908/AgERA5_dewpoint-temperature_20210908.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210908/AgERA5_dewpoint-temperature_20210908.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210909/AgERA5_dewpoint-temperature_20210909.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210909/AgERA5_dewpoint-temperature_20210909.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210910/AgERA5_dewpoint-temperature_20210910.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210910/AgERA5_dewpoint-temperature_20210910.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210911/AgERA5_dewpoint-temperature_20210911.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210911/AgERA5_dewpoint-temperature_20210911.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210912/AgERA5_dewpoint-temperature_20210912.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210912/AgERA5_dewpoint-temperature_20210912.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210913/AgERA5_dewpoint-temperature_20210913.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210913/AgERA5_dewpoint-temperature_20210913.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210914/AgERA5_dewpoint-temperature_20210914.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210914/AgERA5_dewpoint-temperature_20210914.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210915/AgERA5_dewpoint-temperature_20210915.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210915/AgERA5_dewpoint-temperature_20210915.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210916/AgERA5_dewpoint-temperature_20210916.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210916/AgERA5_dewpoint-temperature_20210916.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210917/AgERA5_dewpoint-temperature_20210917.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210917/AgERA5_dewpoint-temperature_20210917.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210918/AgERA5_dewpoint-temperature_20210918.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210918/AgERA5_dewpoint-temperature_20210918.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210919/AgERA5_dewpoint-temperature_20210919.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210919/AgERA5_dewpoint-temperature_20210919.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210920/AgERA5_dewpoint-temperature_20210920.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210920/AgERA5_dewpoint-temperature_20210920.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210921/AgERA5_dewpoint-temperature_20210921.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210921/AgERA5_dewpoint-temperature_20210921.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210922/AgERA5_dewpoint-temperature_20210922.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210922/AgERA5_dewpoint-temperature_20210922.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210923/AgERA5_dewpoint-temperature_20210923.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210923/AgERA5_dewpoint-temperature_20210923.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210924/AgERA5_dewpoint-temperature_20210924.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210924/AgERA5_dewpoint-temperature_20210924.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210925/AgERA5_dewpoint-temperature_20210925.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210925/AgERA5_dewpoint-temperature_20210925.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210926/AgERA5_dewpoint-temperature_20210926.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210926/AgERA5_dewpoint-temperature_20210926.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210927/AgERA5_dewpoint-temperature_20210927.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210927/AgERA5_dewpoint-temperature_20210927.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210928/AgERA5_dewpoint-temperature_20210928.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210928/AgERA5_dewpoint-temperature_20210928.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210929/AgERA5_dewpoint-temperature_20210929.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210929/AgERA5_dewpoint-temperature_20210929.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20210930/AgERA5_dewpoint-temperature_20210930.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20210930/AgERA5_dewpoint-temperature_20210930.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211001/AgERA5_dewpoint-temperature_20211001.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211001/AgERA5_dewpoint-temperature_20211001.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211002/AgERA5_dewpoint-temperature_20211002.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211002/AgERA5_dewpoint-temperature_20211002.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211003/AgERA5_dewpoint-temperature_20211003.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211003/AgERA5_dewpoint-temperature_20211003.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211004/AgERA5_dewpoint-temperature_20211004.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211004/AgERA5_dewpoint-temperature_20211004.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211005/AgERA5_dewpoint-temperature_20211005.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211005/AgERA5_dewpoint-temperature_20211005.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211006/AgERA5_dewpoint-temperature_20211006.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211006/AgERA5_dewpoint-temperature_20211006.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211007/AgERA5_dewpoint-temperature_20211007.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211007/AgERA5_dewpoint-temperature_20211007.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211008/AgERA5_dewpoint-temperature_20211008.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211008/AgERA5_dewpoint-temperature_20211008.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211009/AgERA5_dewpoint-temperature_20211009.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211009/AgERA5_dewpoint-temperature_20211009.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211010/AgERA5_dewpoint-temperature_20211010.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211010/AgERA5_dewpoint-temperature_20211010.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211011/AgERA5_dewpoint-temperature_20211011.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211011/AgERA5_dewpoint-temperature_20211011.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211012/AgERA5_dewpoint-temperature_20211012.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211012/AgERA5_dewpoint-temperature_20211012.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211013/AgERA5_dewpoint-temperature_20211013.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211013/AgERA5_dewpoint-temperature_20211013.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211014/AgERA5_dewpoint-temperature_20211014.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211014/AgERA5_dewpoint-temperature_20211014.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211015/AgERA5_dewpoint-temperature_20211015.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211015/AgERA5_dewpoint-temperature_20211015.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211016/AgERA5_dewpoint-temperature_20211016.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211016/AgERA5_dewpoint-temperature_20211016.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211017/AgERA5_dewpoint-temperature_20211017.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211017/AgERA5_dewpoint-temperature_20211017.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211018/AgERA5_dewpoint-temperature_20211018.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211018/AgERA5_dewpoint-temperature_20211018.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211019/AgERA5_dewpoint-temperature_20211019.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211019/AgERA5_dewpoint-temperature_20211019.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211020/AgERA5_dewpoint-temperature_20211020.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211020/AgERA5_dewpoint-temperature_20211020.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211021/AgERA5_dewpoint-temperature_20211021.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211021/AgERA5_dewpoint-temperature_20211021.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211022/AgERA5_dewpoint-temperature_20211022.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211022/AgERA5_dewpoint-temperature_20211022.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211023/AgERA5_dewpoint-temperature_20211023.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211023/AgERA5_dewpoint-temperature_20211023.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211024/AgERA5_dewpoint-temperature_20211024.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211024/AgERA5_dewpoint-temperature_20211024.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211025/AgERA5_dewpoint-temperature_20211025.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211025/AgERA5_dewpoint-temperature_20211025.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211026/AgERA5_dewpoint-temperature_20211026.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211026/AgERA5_dewpoint-temperature_20211026.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211027/AgERA5_dewpoint-temperature_20211027.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211027/AgERA5_dewpoint-temperature_20211027.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211028/AgERA5_dewpoint-temperature_20211028.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211028/AgERA5_dewpoint-temperature_20211028.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211029/AgERA5_dewpoint-temperature_20211029.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211029/AgERA5_dewpoint-temperature_20211029.tif", "type": "application/json"}, {"href": "/data/MTDA/AgERA5/2021/20211030/AgERA5_dewpoint-temperature_20211030.tif", "rel": "derived_from", "title": "Derived from /data/MTDA/AgERA5/2021/20211030/AgERA5_dewpoint-temperature_20211030.tif", "type": "application/json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results", "rel": "self", "type": "application/json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/bbd3f1506c6b5bb453fa5566260f52fe?expires=1716455737", "rel": "canonical", "type": "application/json"}, {"href": "http://ceos.org/ard/files/PFS/SR/v5.0/CARD4L_Product_Family_Specification_Surface_Reflectance-v5.0.pdf", "rel": "card4l-document", "type": "application/pdf"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/9c3ea792c00e0a1b63231b8b149f997a/openEO_2021-10-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/a8c32bef4d950e8fe3ef37eaca87ee31/openEO_2021-06-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/04cff14b611d54240522833210762931/openEO_2020-12-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8a1578c8d890289751276205a0864103/openEO_2021-03-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/0b8d41a9211197d5be684162746fb830/openEO_2021-08-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3448e70d84ed3f2eb6e399a86b2f8b3d/openEO_2021-09-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/5280a7fab73a3af7d65951d1ccc0edc7/openEO_2020-11-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/3e5e262c7faeb68d52a18f012bf7fe3f/openEO_2021-07-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/256561a0d78d5b22963c5d59f4768cd5/openEO_2021-01-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/6259c389f92cda20f278a1c343486931/openEO_2021-02-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/cd7a84f1e9dcd5107a01a6a3db1d2a90/openEO_2021-04-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}, {"href": "https://openeo.vito.be/openeo/1.2/jobs/j-2405169cad524b05a8f1194330e4c44d/results/items/NDA5ZjhmNzVjNDEwYTg4NWZiNmI2NmRjZGIzY2M1ZTJhN2FjMDdkMzhkOTM1ODMwNzNjMmU5OTRmNDVmMjRkOEBlZ2kuZXU=/8cef27e840684882775f0a8b46671209/openEO_2021-05-01Z.tif?expires=1716455737", "rel": "item", "type": "application/geo+json"}], "openeo:status": "finished", "providers": [{"description": "This data was processed on an openEO backend maintained by VITO.", "name": "VITO", "processing:expression": {"expression": {"aggregatetemporalperiod1": {"arguments": {"data": {"from_node": "filterbands1"}, "period": "month", "reducer": {"process_graph": {"mean1": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "mean", "result": true}}}}, "process_id": "aggregate_temporal_period"}, "aggregatetemporalperiod2": {"arguments": {"data": {"from_node": "filterbands2"}, "period": "month", "reducer": {"process_graph": {"sum1": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "sum", "result": true}}}}, "process_id": "aggregate_temporal_period"}, "apply1": {"arguments": {"data": {"from_node": "filtertemporal1"}, "process": {"process_graph": {"linearscalerange1": {"arguments": {"inputMax": 65534, "inputMin": 0, "outputMax": 65534, "outputMin": 0, "x": {"from_parameter": "x"}}, "process_id": "linear_scale_range", "result": true}}}}, "process_id": "apply"}, "applydimension1": {"arguments": {"data": {"from_node": "aggregatetemporalperiod1"}, "dimension": "t", "process": {"process_graph": {"arrayinterpolatelinear1": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "array_interpolate_linear", "result": true}}}}, "process_id": "apply_dimension"}, "applydimension2": {"arguments": {"data": {"from_node": "aggregatetemporalperiod2"}, "dimension": "t", "process": {"process_graph": {"arrayinterpolatelinear2": {"arguments": {"data": {"from_parameter": "data"}}, "process_id": "array_interpolate_linear", "result": true}}}}, "process_id": "apply_dimension"}, "filterbands1": {"arguments": {"bands": ["temperature-mean"], "data": {"from_node": "loadcollection1"}}, "process_id": "filter_bands"}, "filterbands2": {"arguments": {"bands": ["precipitation-flux"], "data": {"from_node": "loadcollection1"}}, "process_id": "filter_bands"}, "filtertemporal1": {"arguments": {"data": {"from_node": "mergecubes1"}, "extent": ["2020-11-01", "2021-10-31"]}, "process_id": "filter_temporal"}, "loadcollection1": {"arguments": {"bands": ["temperature-mean", "precipitation-flux"], "featureflags": {"tilesize": 1}, "id": "AGERA5", "spatial_extent": {"crs": "EPSG:4326", "east": 5.21, "north": 51.26, "south": 51.25, "west": 5.19}, "temporal_extent": ["2020-11-01", "2021-10-31"]}, "process_id": "load_collection"}, "mergecubes1": {"arguments": {"cube1": {"from_node": "applydimension1"}, "cube2": {"from_node": "applydimension2"}}, "process_id": "merge_cubes"}, "saveresult1": {"arguments": {"data": {"from_node": "apply1"}, "format": "GTIFF", "options": {}}, "process_id": "save_result", "result": true}}, "format": "openeo"}, "processing:facility": "openEO Geotrellis backend", "processing:software": {"Geotrellis backend": "0.33.1a1"}, "roles": ["processor"]}], "stac_extensions": ["https://stac-extensions.github.io/eo/v1.1.0/schema.json", "https://stac-extensions.github.io/file/v2.1.0/schema.json", "https://stac-extensions.github.io/processing/v1.1.0/schema.json", "https://stac-extensions.github.io/projection/v1.1.0/schema.json"], "stac_version": "1.0.0", "summaries": {}, "type": "Collection"} \ No newline at end of file diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py index 4bba06fe..26e690a3 100644 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py @@ -1,13 +1,15 @@ from typing import Dict, Tuple import numpy as np -import pandas as pd import requests import torch +from torch.utils.data import DataLoader, TensorDataset + import xarray as xr from einops import rearrange from pyproj import Transformer -from torch.utils.data import DataLoader, TensorDataset + +import onnxruntime as ort from .dataops import ( BANDS, @@ -61,22 +63,22 @@ def load_model(self, model): model_path (str): The path to the ONNX model file. """ # Load the dependency into an InferenceSession - import onnxruntime - - self.onnx_session = onnxruntime.InferenceSession(model) + self.onnx_session = ort.InferenceSession(model) def predict(self, features: np.ndarray) -> np.ndarray: """ Predicts labels using the provided features DataFrame. Args: - features (pd.DataFrame): DataFrame containing the features for prediction. + features (pd.ndarray): 2D array containing the features Returns: pd.DataFrame: DataFrame containing the predicted labels. """ if self.onnx_session is None: - raise ValueError("Model has not been loaded. Please load a model first.") + raise ValueError( + "Model has not been loaded. Please load a model first." + ) # Prepare input data for ONNX model outputs = self.onnx_session.run(None, {"features": features}) @@ -90,10 +92,10 @@ def predict(self, features: np.ndarray) -> np.ndarray: binary_labels = binary_labels.astype(int) return binary_labels - + + class PrestoFeatureExtractor: - def __init__(self, model: Presto): """ Initialize the PrestoFeatureExtractor with a Presto model. @@ -166,8 +168,8 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: for org_band, presto_band in cls.BAND_MAPPING.items(): if org_band in inarr.coords["bands"]: - values = np.swapaxes( - inarr.sel(bands=org_band).values.reshape((num_timesteps, -1)), 0, 1 + values = rearrange( + inarr.sel(bands=org_band).values, "t x y -> (x y) t" ) idx_valid = values != cls._NODATAVALUE values = cls._preprocess_band_values(values, presto_band) @@ -190,7 +192,9 @@ def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: """ # EPSG:4326 is the supported crs for presto lon, lat = np.meshgrid(inarr.x, inarr.y) - transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) + transformer = Transformer.from_crs( + f"EPSG:{epsg}", "EPSG:4326", always_xy=True + ) lon, lat = transformer.transform(lon, lat) latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") @@ -219,6 +223,7 @@ def _extract_months(inarr: xr.DataArray) -> np.ndarray: months = np.ones((num_instances)) * start_month return months + def _create_dataloader( self, @@ -255,14 +260,14 @@ def _create_dataloader( ) return dl - + + def _create_presto_input( - self, inarr: xr.DataArray, epsg: int = 4326 + cls, inarr: xr.DataArray, epsg: int = 4326 ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - - eo_data, mask = self._extract_eo_data(inarr) - latlons = self._extract_latlons(inarr, epsg) - months = self._extract_months(inarr) + eo_data, mask = cls._extract_eo_data(inarr) + latlons = cls._extract_latlons(inarr, epsg) + months = cls._extract_months(inarr) dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( DynamicWorld2020_2021.class_amount ) @@ -274,7 +279,8 @@ def _create_presto_input( latlons, np.repeat(mask, BAND_EXPANSION, axis=-1), ) - + + def _get_encodings(self, dl: DataLoader) -> np.ndarray: """ Get encodings from DataLoader. @@ -309,10 +315,10 @@ def _get_encodings(self, dl: DataLoader) -> np.ndarray: all_encodings.append(encodings) return np.concatenate(all_encodings, axis=0) - + def extract_presto_features( self, inarr: xr.DataArray, epsg: int = 4326 - ) -> np.ndarray: + ) -> xr.DataArray: eo, dynamic_world, months, latlons, mask = self._create_presto_input( inarr, epsg ) @@ -324,13 +330,17 @@ def extract_presto_features( ) ft_names = [f"presto_ft_{i}" for i in range(128)] features = xr.DataArray( - features, coords={"x": inarr.x, "y": inarr.y, "bands": ft_names} + features, + coords={"x": inarr.x, "y": inarr.y, "bands": ft_names}, + dims=["x", "y", "bands"], ) return features + -def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: + +def get_presto_features(inarr: xr.DataArray, presto_path: str) -> xr.DataArray: """ Extracts features from input data using Presto. @@ -346,17 +356,20 @@ def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: presto_model = Presto.load_pretrained_artifactory( presto_url=presto_path, strict=False ) + #TODO flexible espg presto_extractor = PrestoFeatureExtractor(presto_model) features = presto_extractor.extract_presto_features(inarr, epsg=32631) return features -def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarray: +def classify_with_catboost( + features: xr.DataArray, catboost_path: str +) -> xr.DataArray: """ Classifies features using the WorldCereal CatBoost model. Args: - features (np.ndarray): Features to be classified. + features (xr.DataArray): Features to be classified [x, y, fts] map_dims (tuple): Original x, y dimensions of the input data. model_path (str): Path to the trained CatBoost model. @@ -364,11 +377,20 @@ def classify_with_catboost(features: np.ndarray, catboost_path: str) -> np.ndarr xr.DataArray: Classified data as xarray DataArray. """ + # Stack the features and transpose for feeding to CatBoost + stacked_features = features.stack(xy=["x", "y"]).transpose() + predictor = WorldCerealPredictor() response = requests.get(catboost_path) catboost_model = response.content predictor.load_model(catboost_model) - predictions = predictor.predict(features) + predictions = predictor.predict(stacked_features.values) + + predictions = ( + xr.DataArray(predictions, coords={"xy": stacked_features.xy}, dims=["xy"]) + .unstack() + .expand_dims(dim="bands") + ) return predictions diff --git a/minimal_wc_presto/test_aggregator.ipynb b/minimal_wc_presto/test_aggregator.ipynb deleted file mode 100644 index 1ae4237e..00000000 --- a/minimal_wc_presto/test_aggregator.ipynb +++ /dev/null @@ -1,229 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "b879f7b4-9a3f-41fc-90d0-ab9cfd25a093", - "metadata": {}, - "source": [ - "### Make OpenEO connection" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Authenticated using refresh token.\n" - ] - } - ], - "source": [ - "import openeo\n", - "\n", - "\n", - "connection = openeo.connect(\"https://openeofed.dataspace.copernicus.eu/\").authenticate_oidc()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "5494c46d", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Preflight process graph validation raised: [InternalValidationFailure] Validation failed: BackendLookupFailureException(status_code=400, code='BackendLookupFailure', message=\"Collections across multiple backends ({'terrascope', 'cdse'}): {'SENTINEL2_L2A', 'AGERA5'}.\", id='r-2405172f25fa4f8bb7f69ca237bd5681')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'agg-pj-20240517-093353': send 'start'\n", - "0:00:38 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:00:44 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:00:51 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:01:00 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:01:11 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:01:25 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:01:43 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:02:03 Job 'agg-pj-20240517-093353': running (progress 0%)\n", - "0:02:34 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:03:07 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:03:46 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:04:34 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:05:34 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:06:36 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:07:38 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:08:44 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:09:45 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:10:47 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:11:48 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:12:49 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:13:49 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:14:51 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:15:52 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:16:53 Job 'agg-pj-20240517-093353': running (progress 50%)\n", - "0:17:54 Job 'agg-pj-20240517-093353': finished (progress 100%)\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "\n", - "#Get desired data\n", - "\n", - "\n", - "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [5.19, 51.25, 5.20, 51.26]))\n", - "EXTENT['crs'] = \"EPSG:4326\"\n", - "\n", - "STARTDATE = '2020-11-01'\n", - "ENDDATE = '2020-12-31'\n", - "\n", - "# Set OpenEO classification UDF context based on settings\n", - "CONTEXT = {\n", - " \"startdate\": STARTDATE, # Required\n", - " \"enddate\": ENDDATE, # Required\n", - "}\n", - "\n", - "input_cube1 = connection.load_collection(\n", - " collection_id = \"SENTINEL2_L2A\",\n", - " spatial_extent=EXTENT,\n", - " bands = [\"B02\", \"B03\"],\n", - " temporal_extent=[STARTDATE, ENDDATE],\n", - " )\n", - "\n", - "input_cube2 = connection.load_collection(\n", - " collection_id = \"AGERA5\",\n", - " spatial_extent=EXTENT,\n", - " bands=[\"temperature-mean\", \"precipitation-flux\"],\n", - " temporal_extent=[STARTDATE, ENDDATE],\n", - " )\n", - "\n", - "cube = input_cube1.merge_cubes(input_cube2)\n", - "\n", - "cube.execute_batch(outputfile = 'test.nc',\n", - " description='world cereal data collection',\n", - " job_options={\"split_strategy\": \"crossbackend\"})\n" - ] - }, - { - "cell_type": "markdown", - "id": "48c9322c", - "metadata": {}, - "source": [] - }, - { - "cell_type": "markdown", - "id": "5b47a2a0-d5f4-4e39-a924-4bca923400de", - "metadata": {}, - "source": [ - "### Check reference" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "2c63667b-8e43-4640-8c70-41b85ad060d2", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cube" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/minimal_wc_presto/test_prestobackend.py b/minimal_wc_presto/test_prestobackend.py deleted file mode 100644 index 93b92a4e..00000000 --- a/minimal_wc_presto/test_prestobackend.py +++ /dev/null @@ -1,25 +0,0 @@ -#%% - -import xarray as xr -import matplotlib.pyplot as plt - -output = xr.open_dataset('2024_05_17_13_41_40_input_cube_worldCereal.nc') -output = output['B08'].to_numpy().squeeze()[0,:,:].squeeze() -plt.imshow(output) - -#%% - -import xarray as xr -import matplotlib.pyplot as plt -import numpy as np - -output = xr.open_dataset('2024_05_17_14_00_16_output_presto.nc') -output.drop_vars('crs') - -flatten_output = output.to_array() - -#flatten_output = flatten_output.flatten() -#plt.hist(flatten_output) -#plt.show() - -#nan_counts = np.isnan(flatten_output).sum()/np.prod(flatten_output.shape) diff --git a/minimal_wc_presto/testing.py b/minimal_wc_presto/testing.py deleted file mode 100644 index 0ad2261c..00000000 --- a/minimal_wc_presto/testing.py +++ /dev/null @@ -1,21 +0,0 @@ -def test_inference_catboost_presto(): - # Load the result and ground truth - ds = xr.open_dataset("./data/belgium_good_2020-12-01_2021-11-30.nc", engine='netcdf4') - - # Because we downloaded the data, we need to resolve - # an issue with the CRS which has become a band. Let's get rid of it - arr = ds.drop('crs').to_array(dim='bands') - - # Make an OpenEO datacube of this array - udf_cube = XarrayDataCube(arr) - result_cube = apply_datacube(udf_cube) - - # Save the result to NetCDF - result_cube.array.to_netcdf("./data/test_result.nc") - results = result_cube.array.values.squeeze() - - # to a numpy array - gt_dataset = xr.open_dataset("./data/worldcereal_result.nc", engine='netcdf4') - data_variable = gt_dataset['__xarray_dataarray_variable__'] - gt = data_variable.values[0] - assert np.array_equal(results, gt) \ No newline at end of file diff --git a/minimal_wc_presto/udf_presto.py b/minimal_wc_presto/udf_presto.py index 6b853f43..2e12e562 100644 --- a/minimal_wc_presto/udf_presto.py +++ b/minimal_wc_presto/udf_presto.py @@ -40,52 +40,32 @@ def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger = _setup_logging() - - # shape and indiches for output - cube = cube.transpose('bands', 't', 'x', 'y') + # The below is required to avoid flipping of the result + # when running on OpenEO backend! + cube = cube.transpose("bands", "t", "x", "y") + + # Handle NaN values in Presto compatible way cube = cube.fillna(65535) - orig_dims = list(cube.dims) - map_dims = cube.shape[2:] logger.info("Unzipping dependencies") #base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" base_url = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" - dependency_name = "wc_presto_onnx_dependencies.zip" logger.info("Appending depencency") dep_dir = extract_dependencies(base_url, dependency_name) - - - #directly add a path to the older pandas version sys.path.append(str(dep_dir)) - sys.path.append(str(dep_dir) + '/pandas') + #directly add a path to the older pandas version from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features - logger.info("Reading in required libs") logger.info("Extracting presto features") PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" - features = get_presto_features(cube, PRESTO_PATH, 32631) - - # go to 128, 1,100,100 (time, bands, x, y) - presto_dim = map_dims + (128,) - logger.info(str(features.shape)) - features = features.reshape(presto_dim) #100,100,128 - logger.info(str(features.shape)) - features = np.expand_dims(features, axis = 0) #1,100,100,128 - logger.info(str(features.shape)) - features = np.transpose(features, (3, 0, 1, 2)) #128,1,100,100 - logger.info(str(features.shape)) - + output = get_presto_features(cube, PRESTO_PATH) - transformer = Transformer.from_crs(f"EPSG:{32631}", "EPSG:4326", always_xy=True) - longitudes, latitudes = transformer.transform(cube.x, cube.y) - - - output = xr.DataArray(features, dims=orig_dims, coords={'x': longitudes, 'y': latitudes}) + return output diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py index 6d8a37f4..224249e6 100644 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ b/minimal_wc_presto/udf_worldcereal_inference.py @@ -6,8 +6,7 @@ import functools import xarray as xr from typing import Dict -import numpy as np -from pyproj import Transformer + def _setup_logging(): @@ -39,48 +38,41 @@ def extract_dependencies(base_url: str, dependency_name: str): def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: logger = _setup_logging() - logger.info("Shape of input: {}".format(cube.shape)) # shape and indiches for output cube = cube.transpose('bands', 't', 'x', 'y') cube = cube.fillna(65535) - orig_dims = list(cube.dims) - map_dims = cube.shape[2:] + # Unzip de dependencies on the backend logger.info("Unzipping dependencies") base_url = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" dependency_name = "wc_presto_onnx_dependencies.zip" - dep_dir = extract_dependencies(base_url, dependency_name) - # Append the dependencies + logger.info("Adding dependencies") + dep_dir = extract_dependencies(base_url, dependency_name) sys.path.append(str(dep_dir)) - sys.path.append(str(dep_dir) + '/pandas') + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost # Run presto inference - logger.info("Extracting presto features") PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" + + # Run presto feature extraction + logger.info("Extracting presto features") features = get_presto_features(cube, PRESTO_PATH) - logger.info("Shape of presto output: {}".format(features.shape)) - # run catboost classification + # Run catboost classification logger.info("Catboost classification") - CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" classification = classify_with_catboost(features, CATBOOST_PATH) - logger.info("Shape of classification output: {}".format(classification.shape)) - - # revert to 4D shape for openEO - logger.info("Revert to 4D xarray") - transformer = Transformer.from_crs(f"EPSG:{32631}", "EPSG:4326", always_xy=True) - longitudes, latitudes = transformer.transform(cube.x, cube.y) + logger.info("Done") - classification = classification.reshape(map_dims) - classification = np.flip(np.expand_dims(np.expand_dims(classification, axis=0), axis=0)) - output = xr.DataArray(classification, dims=orig_dims, coords={'x': longitudes, 'y': latitudes}) - logger.info("Shape of output: {}".format(output.shape)) + # Add time dimension + classification = classification.expand_dims(dim="t") + logger.info("Done") - return output + return classification From 63722e5d570bff14609aaee2060f7f58761fe3c7 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Fri, 31 May 2024 12:24:35 +0200 Subject: [PATCH 23/31] Added presto feature computer using GFMAP --- minimal_wc_presto/presto_feature_computer.py | 121 +++++++++++++------ minimal_wc_presto/test_presto_fc_gfmap.py | 56 ++++----- src/worldcereal/openeo/preprocessing.py | 13 +- 3 files changed, 118 insertions(+), 72 deletions(-) diff --git a/minimal_wc_presto/presto_feature_computer.py b/minimal_wc_presto/presto_feature_computer.py index c73ba4a0..b0034e2a 100644 --- a/minimal_wc_presto/presto_feature_computer.py +++ b/minimal_wc_presto/presto_feature_computer.py @@ -1,17 +1,9 @@ """Feature computer GFMAP compatible to compute Presto embeddings.""" -import functools -import logging -import shutil -import sys -import urllib.request -from pathlib import Path -from typing import Tuple import numpy as np import xarray as xr -from pyproj import Transformer - from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor +from pyproj import Transformer class PrestoFeatureExtractor(PatchFeatureExtractor): @@ -20,6 +12,10 @@ class PrestoFeatureExtractor(PatchFeatureExtractor): feature from the Presto model. """ + import functools + from pathlib import Path + from typing import Tuple + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA @@ -28,26 +24,46 @@ class PrestoFeatureExtractor(PatchFeatureExtractor): _NODATAVALUE = 65535 BAND_MAPPING = { - "S2-L2A-B02": "B2", - "S2-L2A-B03": "B3", - "S2-L2A-B04": "B4", - "S2-L2A-B05": "B5", - "S2-L2A-B06": "B6", - "S2-L2A-B07": "B7", - "S2-L2A-B08": "B8", + "B02": "B2", + "B03": "B3", + "B04": "B4", + "B05": "B5", + "B06": "B6", + "B07": "B7", + "B08": "B8", + "B8A": "B8A", + "B11": "B11", + "B12": "B12", + "VH": "VH", + "VV": "VV", + "precipitation-flux": "total_precipitation", + "temperature-mean": "temperature_2m", + } + + GFMAP_BAND_MAPPING = { + "S2-L2A-B02": "B02", + "S2-L2A-B03": "B03", + "S2-L2A-B04": "B04", + "S2-L2A-B05": "B05", + "S2-L2A-B06": "B06", + "S2-L2A-B07": "B07", + "S2-L2A-B08": "B08", "S2-L2A-B8A": "B8A", "S2-L2A-B11": "B11", "S2-L2A-B12": "B12", "S1-SIGMA0-VH": "VH", "S1-SIGMA0-VV": "VV", - "A5-precip": "total_precipitation", - "A5-tmean": "temperature_2m", + "COP-DEM": "DEM", + "A5-tmean": "temperature-mean", + "A5-precip": "precipitation-flux", } def __init__(self): """ Initializes the PrestoFeatureExtractor object, starting a logger. """ + import logging + logging.basicConfig(level=logging.INFO) self.logger = logging.getLogger(PrestoFeatureExtractor.__name__) self.model = None # To be initialized within the OpenEO environment @@ -91,17 +107,23 @@ def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: num_pixels = len(inarr.x) * len(inarr.y) num_timesteps = len(inarr.t) - eo_data = np.zeros((num_pixels, num_timesteps, len(BANDS))) # pylint: disable=E0602 - mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) # pylint: disable=E0602 + eo_data = np.zeros( + (num_pixels, num_timesteps, len(BANDS)) + ) # pylint: disable=E0602 + mask = np.zeros( + (num_pixels, num_timesteps, len(BANDS_GROUPS_IDX)) + ) # pylint: disable=E0602 for org_band, presto_band in cls.BAND_MAPPING.items(): if org_band in inarr.coords["bands"]: - values = rearrange( # pylint: disable=E0602 + values = rearrange( # pylint: disable=E0602 inarr.sel(bands=org_band).values, "t x y -> (x y) t" ) idx_valid = values != cls._NODATAVALUE values = cls._preprocess_band_values(values, presto_band) - eo_data[:, :, BANDS.index(presto_band)] = values # pylint: disable=E0602 + eo_data[ + :, :, BANDS.index(presto_band) + ] = values # pylint: disable=E0602 mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid return eo_data, mask @@ -120,11 +142,11 @@ def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: """ # EPSG:4326 is the supported crs for presto lon, lat = np.meshgrid(inarr.x, inarr.y) - transformer = Transformer.from_crs( - f"EPSG:{epsg}", "EPSG:4326", always_xy=True - ) + transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) lon, lat = transformer.transform(lon, lat) - latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") # pylint: disable=E0602 + latlons = rearrange( + np.stack([lat, lon]), "c x y -> (x y) c" + ) # pylint: disable=E0602 # 2D array where each row represents a pair of latitude and longitude coordinates. return latlons @@ -224,7 +246,8 @@ def _get_encodings(self, dl) -> np.ndarray: for x, dw, latlons, month, variable_mask in dl: x_f, dw_f, latlons_f, month_f, variable_mask_f = [ - t.to(device) for t in (x, dw, latlons, month, variable_mask) # pylint: disable=E0602 + t.to(device) + for t in (x, dw, latlons, month, variable_mask) # pylint: disable=E0602 ] with torch.no_grad(): # pylint: disable=E0602 @@ -274,6 +297,11 @@ def extract_dependencies(cls, base_url: str, dependency_name: str): """Extract the dependencies from the given URL. Unpacking a zip file in the current working directory. """ + import shutil + import sys + import urllib.request + from pathlib import Path + # Generate absolute path for the dependencies folder dependencies_dir = Path.cwd() / "dependencies" @@ -288,7 +316,9 @@ def extract_dependencies(cls, base_url: str, dependency_name: str): shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) # Add the model directory to system path if it's not already there - abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) # NOQA + abs_path = str( + dependencies_dir / Path(modelfile_url).name.split(".zip")[0] + ) # NOQA # Append the dependencies sys.path.append(str(abs_path)) @@ -305,35 +335,54 @@ def get_presto_features(self, inarr: xr.DataArray, presto_path: str) -> np.ndarr Returns: xr.DataArray: Extracted features as xarray DataArray. """ + self.logger.info("Loading presto model.") presto_model = Presto.load_pretrained_artifactory( # pylint: disable=E0602 presto_url=presto_path, strict=False ) self.model = presto_model + self.logger.info("Presto model loaded sucessfully. Extracting features.") # Get the local EPSG code features = self.extract_presto_features(inarr, epsg=self.epsg) + self.logger.info("Features extracted.") + # features = self.extract_presto_features(inarr, epsg=32631) # TODO remove hardcoded return features def output_labels(self) -> list: """Returns the output labels from this UDF, which is the output labels of the presto embeddings""" return [f"presto_ft_{i}" for i in range(128)] - def execute(self, inarr: xr.DataArray) -> xr.DataArray: # The below is required to avoid flipping of the result # when running on OpenEO backend! inarr = inarr.transpose("bands", "t", "x", "y") + # Change the band names + new_band_names = [ + self.GFMAP_BAND_MAPPING.get(b.item(), b.item()) for b in inarr.bands + ] + inarr = inarr.assign_coords(bands=new_band_names) + + self.logger.info("Input data shape: %s", inarr.shape) + for band in inarr.bands: + self.logger.info( + "Input data null values for band %s -> %s", + band, + inarr.sel(bands=band).isnull().sum().item(), + ) + # Handle NaN values in Presto compatible way inarr = inarr.fillna(65535) + self.logger.info( + "After filling NaN values, total input data null values: %s", + inarr.isnull().sum().item(), + ) + # Unzip de dependencies on the backend self.logger.info("Unzipping dependencies") - self.extract_dependencies( - self.BASE_URL, - self.DEPENDENCY_NAME - ) + self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) # pylint: disable=E0401 # pylint: disable=C0401 @@ -342,12 +391,11 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: # pylint: disable=W0603 # pylint: disable=reportMissingImports ########################################################################## - global onnxruntime, requests, torch, BANDS, BANDS_GROUPS_IDX, NORMED_BANDS + global requests, torch, BANDS, BANDS_GROUPS_IDX, NORMED_BANDS global S1_S2_ERA5_SRTM, DynamicWorld2020_2021, BAND_EXPANSION global IDX_TO_BAND_GROUPS, BAND_EXPANSION, Presto, device, rearrange global DataLoader, TensorDataset - import onnxruntime import requests import torch from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( @@ -364,6 +412,7 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device from einops import rearrange from torch.utils.data import DataLoader, TensorDataset + ########################################################################## # pylint: enable=E0401 # pylint: enable=C0401 @@ -371,8 +420,6 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: # pylint: enable=W0601 # pylint: enable=W0603 # pylint: enable=reportMissingImports - - # Index to band groups mapping IDX_TO_BAND_GROUPS = { NORMED_BANDS[idx]: band_group_idx diff --git a/minimal_wc_presto/test_presto_fc_gfmap.py b/minimal_wc_presto/test_presto_fc_gfmap.py index 4b629899..efe93496 100644 --- a/minimal_wc_presto/test_presto_fc_gfmap.py +++ b/minimal_wc_presto/test_presto_fc_gfmap.py @@ -1,29 +1,28 @@ """Test the presto feature computer running with GFMAP""" -import openeo -from openeo_gfmap import ( - Backend, BackendContext, BoundingBoxExtent, TemporalContext -) +import openeo +from openeo_gfmap import Backend, BackendContext, BoundingBoxExtent, TemporalContext from openeo_gfmap.features.feature_extractor import apply_feature_extractor - from presto_feature_computer import PrestoFeatureExtractor from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap -EXTENT = dict(zip(["west", "south", "east", "north"], [664000.0, 5611120.0, 665000.0, 5612120.0])) -EXTENT['crs'] = "EPSG:32631" -EXTENT['srs'] = "EPSG:32631" -STARTDATE = '2020-11-01' -ENDDATE = '2021-10-31' +EXTENT = dict( + zip(["west", "south", "east", "north"], [664000.0, 5611120.0, 665000.0, 5612120.0]) +) +EXTENT["crs"] = "EPSG:32631" +EXTENT["srs"] = "EPSG:32631" +STARTDATE = "2020-11-01" +ENDDATE = "2021-10-31" -if __name__ == '__main__': +if __name__ == "__main__": # Test extent spatial_extent = BoundingBoxExtent( - west=EXTENT['west'], - south=EXTENT['south'], - east=EXTENT['east'], - north=EXTENT['north'], + west=EXTENT["west"], + south=EXTENT["south"], + east=EXTENT["east"], + north=EXTENT["north"], epsg=32631, ) @@ -33,7 +32,9 @@ ) backend_context = BackendContext(Backend.FED) - connection = openeo.connect("openeofed.dataspace.copernicus.eu").authenticate_oidc() + connection = openeo.connect( + "https://openeo.creo.vito.be/openeo/" + ).authenticate_oidc() inputs = worldcereal_preprocessed_inputs_gfmap( connection=connection, @@ -43,25 +44,26 @@ ) # Test feature computer - presto_parameters = {} + presto_parameters = { + "rescale_s1": False, # Will be done in the Presto UDF itself! + } + features = apply_feature_extractor( feature_extractor_class=PrestoFeatureExtractor, cube=inputs, parameters=presto_parameters, size=[ - {"dimension": "x", "unit": "px", "value": 128}, - {"dimension": "y", "unit": "px", "value": 128}, + {"dimension": "x", "unit": "px", "value": 100}, + {"dimension": "y", "unit": "px", "value": 100}, ], overlap=[ {"dimension": "x", "unit": "px", "value": 0}, {"dimension": "y", "unit": "px", "value": 0}, - ] + ], ) - job = features.create_job(out_format="NetCDF", title="Presto FC GFMAP") - - job.start_and_wait() - - for asset in job.get_results().get_assets(): - if asset.metadata["type"].startswith("application/x-netcdf"): - asset.download("presto_features_gfmap.nc") + features.execute_batch( + outputfile=".notebook-tests/presto_features_gfmap_nointerp.nc", + out_format="NetCDF", + job_options={"driver-memory": "4g", "executor-memoryOverhead": "8g"}, + ) diff --git a/src/worldcereal/openeo/preprocessing.py b/src/worldcereal/openeo/preprocessing.py index 7a8c723a..87193d4e 100644 --- a/src/worldcereal/openeo/preprocessing.py +++ b/src/worldcereal/openeo/preprocessing.py @@ -12,11 +12,7 @@ from openeo_gfmap.fetching.generic import build_generic_extractor from openeo_gfmap.fetching.s1 import build_sentinel1_grd_extractor from openeo_gfmap.fetching.s2 import build_sentinel2_l2a_extractor -from openeo_gfmap.preprocessing.compositing import ( - median_compositing, - mean_compositing, - sum_compositing -) +from openeo_gfmap.preprocessing.compositing import mean_compositing, median_compositing from openeo_gfmap.preprocessing.sar import compress_backscatter_uint16 COMPOSITE_WINDOW = "month" @@ -116,7 +112,9 @@ def raw_datacube_S2( # Try filtering using the geometry if fetch_type == FetchType.TILE: - additional_masks = additional_masks.filter_spatial(spatial_extent.to_geojson()) + additional_masks = additional_masks.filter_spatial( + spatial_extent.to_geojson() + ) extraction_parameters["pre_merge"] = additional_masks @@ -231,7 +229,6 @@ def worldcereal_preprocessed_inputs_gfmap( "S2-L2A-B06", "S2-L2A-B07", "S2-L2A-B08", - "S2-L2A-B8A", "S2-L2A-B11", "S2-L2A-B12", ], @@ -253,8 +250,8 @@ def worldcereal_preprocessed_inputs_gfmap( spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=[ - "S1-SIGMA0-VV", "S1-SIGMA0-VH", + "S1-SIGMA0-VV", ], fetch_type=FetchType.TILE, target_resolution=10.0, # Compute the backscatter at 20m resolution, then upsample nearest neighbor when merging cubes From 5ed426bcaf149c7a5dcc97061ad5caf2b2f39d69 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Fri, 31 May 2024 16:19:01 +0200 Subject: [PATCH 24/31] UDFs are passing and reformatting for repository --- minimal_wc_presto/presto_feature_computer.py | 432 ------------------- minimal_wc_presto/test_presto_fc_gfmap.py | 33 +- scripts/inference/cropland_mapping.py | 86 +++- src/worldcereal/openeo/feature_extractor.py | 116 +++++ src/worldcereal/openeo/feature_udf.py | 184 -------- src/worldcereal/openeo/inference.py | 65 +++ 6 files changed, 274 insertions(+), 642 deletions(-) delete mode 100644 minimal_wc_presto/presto_feature_computer.py create mode 100644 src/worldcereal/openeo/feature_extractor.py delete mode 100644 src/worldcereal/openeo/feature_udf.py create mode 100644 src/worldcereal/openeo/inference.py diff --git a/minimal_wc_presto/presto_feature_computer.py b/minimal_wc_presto/presto_feature_computer.py deleted file mode 100644 index b0034e2a..00000000 --- a/minimal_wc_presto/presto_feature_computer.py +++ /dev/null @@ -1,432 +0,0 @@ -"""Feature computer GFMAP compatible to compute Presto embeddings.""" - -import numpy as np -import xarray as xr -from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor -from pyproj import Transformer - - -class PrestoFeatureExtractor(PatchFeatureExtractor): - """Feature extractor to use Presto model to compute embeddings. - This will generate a datacube with 128 bands, each band representing a - feature from the Presto model. - """ - - import functools - from pathlib import Path - from typing import Tuple - - CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA - PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA - BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA - DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" - - _NODATAVALUE = 65535 - - BAND_MAPPING = { - "B02": "B2", - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - "B8A": "B8A", - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", - } - - GFMAP_BAND_MAPPING = { - "S2-L2A-B02": "B02", - "S2-L2A-B03": "B03", - "S2-L2A-B04": "B04", - "S2-L2A-B05": "B05", - "S2-L2A-B06": "B06", - "S2-L2A-B07": "B07", - "S2-L2A-B08": "B08", - "S2-L2A-B8A": "B8A", - "S2-L2A-B11": "B11", - "S2-L2A-B12": "B12", - "S1-SIGMA0-VH": "VH", - "S1-SIGMA0-VV": "VV", - "COP-DEM": "DEM", - "A5-tmean": "temperature-mean", - "A5-precip": "precipitation-flux", - } - - def __init__(self): - """ - Initializes the PrestoFeatureExtractor object, starting a logger. - """ - import logging - - logging.basicConfig(level=logging.INFO) - self.logger = logging.getLogger(PrestoFeatureExtractor.__name__) - self.model = None # To be initialized within the OpenEO environment - - @classmethod - def _preprocess_band_values( - cls, values: np.ndarray, presto_band: str - ) -> np.ndarray: - """ - Preprocesses the band values based on the given presto_val. - - Args: - values (np.ndarray): Array of band values to preprocess. - presto_val (str): Name of the band for preprocessing. - - Returns: - np.ndarray: Preprocessed array of band values. - """ - if presto_band in ["VV", "VH"]: - # Convert to dB - values = 20 * np.log10(values) - 83 - elif presto_band == "total_precipitation": - # Scale precipitation and convert mm to m - values = values / (100 * 1000.0) - elif presto_band == "temperature_2m": - # Remove scaling - values = values / 100 - return values - - @classmethod - def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: - """ - Extracts EO data and mask arrays from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing EO data. - - Returns: - Tuple[np.ndarray, np.ndarray]: Tuple containing EO data array and mask array. - """ - num_pixels = len(inarr.x) * len(inarr.y) - num_timesteps = len(inarr.t) - - eo_data = np.zeros( - (num_pixels, num_timesteps, len(BANDS)) - ) # pylint: disable=E0602 - mask = np.zeros( - (num_pixels, num_timesteps, len(BANDS_GROUPS_IDX)) - ) # pylint: disable=E0602 - - for org_band, presto_band in cls.BAND_MAPPING.items(): - if org_band in inarr.coords["bands"]: - values = rearrange( # pylint: disable=E0602 - inarr.sel(bands=org_band).values, "t x y -> (x y) t" - ) - idx_valid = values != cls._NODATAVALUE - values = cls._preprocess_band_values(values, presto_band) - eo_data[ - :, :, BANDS.index(presto_band) - ] = values # pylint: disable=E0602 - mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid - - return eo_data, mask - - @staticmethod - def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: - """ - Extracts latitudes and longitudes from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing spatial coordinates. - epsg (int): EPSG code for coordinate reference system. - - Returns: - np.ndarray: Array containing extracted latitudes and longitudes. - """ - # EPSG:4326 is the supported crs for presto - lon, lat = np.meshgrid(inarr.x, inarr.y) - transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) - lon, lat = transformer.transform(lon, lat) - latlons = rearrange( - np.stack([lat, lon]), "c x y -> (x y) c" - ) # pylint: disable=E0602 - - # 2D array where each row represents a pair of latitude and longitude coordinates. - return latlons - - @staticmethod - def _extract_months(inarr: xr.DataArray) -> np.ndarray: - """ - Calculate the start month based on the first timestamp in the input array, - and create an array of the same length filled with that start month value. - - Parameters: - - inarr: xarray.DataArray or numpy.ndarray - Input array containing timestamps. - - Returns: - - months: numpy.ndarray - Array of start month values, with the same length as the input array. - """ - num_instances = len(inarr.x) * len(inarr.y) - - start_month = ( - inarr.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1 - ) - 1 - - months = np.ones((num_instances)) * start_month - return months - - def _create_dataloader( - self, - eo: np.ndarray, - dynamic_world: np.ndarray, - months: np.ndarray, - latlons: np.ndarray, - mask: np.ndarray, - ): - """ - Create a PyTorch DataLoader for encoding features. - - Args: - eo_data (np.ndarray): Array containing Earth Observation data. - dynamic_world (np.ndarray): Array containing dynamic world data. - latlons (np.ndarray): Array containing latitude and longitude coordinates. - inarr (xr.DataArray): Input xarray.DataArray. - mask (np.ndarray): Array containing masking data. - - Returns: - DataLoader: PyTorch DataLoader for encoding features. - """ - - # pylint: disable=E0602 - dl = DataLoader( - TensorDataset( - torch.from_numpy(eo).float(), - torch.from_numpy(dynamic_world).long(), - torch.from_numpy(latlons).float(), - torch.from_numpy(months).long(), - torch.from_numpy(mask).float(), - ), - batch_size=8192, - shuffle=False, - ) - # pylint: enable=E0602 - - return dl - - @classmethod - def _create_presto_input( - cls, inarr: xr.DataArray, epsg: int = 4326 - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - eo_data, mask = cls._extract_eo_data(inarr) - latlons = cls._extract_latlons(inarr, epsg) - months = cls._extract_months(inarr) - dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( - DynamicWorld2020_2021.class_amount # pylint: disable=E0602 - ) - - return ( - S1_S2_ERA5_SRTM.normalize(eo_data), # pylint: disable=E0602 - dynamic_world, - months, - latlons, - np.repeat(mask, BAND_EXPANSION, axis=-1), # pylint: disable=E0602 - ) - - def _get_encodings(self, dl) -> np.ndarray: - """ - Get encodings from DataLoader. - - Args: - dl (DataLoader): PyTorch DataLoader containing data for encoding. - - Returns: - np.ndarray: Array containing encoded features. - """ - - all_encodings = [] - - for x, dw, latlons, month, variable_mask in dl: - x_f, dw_f, latlons_f, month_f, variable_mask_f = [ - t.to(device) - for t in (x, dw, latlons, month, variable_mask) # pylint: disable=E0602 - ] - - with torch.no_grad(): # pylint: disable=E0602 - encodings = ( - self.model.encoder( - x_f, - dynamic_world=dw_f.long(), - mask=variable_mask_f, - latlons=latlons_f, - month=month_f, - ) - .cpu() - .numpy() - ) - - all_encodings.append(encodings) - - return np.concatenate(all_encodings, axis=0) - - def extract_presto_features( - self, inarr: xr.DataArray, epsg: int = 4326 - ) -> np.ndarray: - """General function to prepare the input data, generate a data loader, - initialize the model, perform the inference and return the features. - """ - eo, dynamic_world, months, latlons, mask = self._create_presto_input( - inarr, epsg - ) - dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) - - features = self._get_encodings(dl) - features = rearrange( # pylint: disable=E0602 - features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) - ) - ft_names = [f"presto_ft_{i}" for i in range(128)] - features = xr.DataArray( - features, - coords={"x": inarr.x, "y": inarr.y, "bands": ft_names}, - dims=["x", "y", "bands"], - ) - - return features - - @classmethod - @functools.lru_cache(maxsize=6) - def extract_dependencies(cls, base_url: str, dependency_name: str): - """Extract the dependencies from the given URL. Unpacking a zip - file in the current working directory. - """ - import shutil - import sys - import urllib.request - from pathlib import Path - - # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / "dependencies" - - # Create the directory if it doesn't exist - dependencies_dir.mkdir(exist_ok=True, parents=True) - - # Download and extract the model file - modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve( - modelfile_url, filename=dependencies_dir / Path(modelfile_url).name - ) - shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - - # Add the model directory to system path if it's not already there - abs_path = str( - dependencies_dir / Path(modelfile_url).name.split(".zip")[0] - ) # NOQA - - # Append the dependencies - sys.path.append(str(abs_path)) - sys.path.append(str(abs_path) + "/pandas") - - def get_presto_features(self, inarr: xr.DataArray, presto_path: str) -> np.ndarray: - """ - Extracts features from input data using Presto. - - Args: - inarr (xr.DataArray): Input data as xarray DataArray. - presto_path (str): Path to the pretrained Presto model. - - Returns: - xr.DataArray: Extracted features as xarray DataArray. - """ - self.logger.info("Loading presto model.") - presto_model = Presto.load_pretrained_artifactory( # pylint: disable=E0602 - presto_url=presto_path, strict=False - ) - self.model = presto_model - self.logger.info("Presto model loaded sucessfully. Extracting features.") - - # Get the local EPSG code - features = self.extract_presto_features(inarr, epsg=self.epsg) - self.logger.info("Features extracted.") - # features = self.extract_presto_features(inarr, epsg=32631) # TODO remove hardcoded - return features - - def output_labels(self) -> list: - """Returns the output labels from this UDF, which is the output labels - of the presto embeddings""" - return [f"presto_ft_{i}" for i in range(128)] - - def execute(self, inarr: xr.DataArray) -> xr.DataArray: - # The below is required to avoid flipping of the result - # when running on OpenEO backend! - inarr = inarr.transpose("bands", "t", "x", "y") - - # Change the band names - new_band_names = [ - self.GFMAP_BAND_MAPPING.get(b.item(), b.item()) for b in inarr.bands - ] - inarr = inarr.assign_coords(bands=new_band_names) - - self.logger.info("Input data shape: %s", inarr.shape) - for band in inarr.bands: - self.logger.info( - "Input data null values for band %s -> %s", - band, - inarr.sel(bands=band).isnull().sum().item(), - ) - - # Handle NaN values in Presto compatible way - inarr = inarr.fillna(65535) - - self.logger.info( - "After filling NaN values, total input data null values: %s", - inarr.isnull().sum().item(), - ) - - # Unzip de dependencies on the backend - self.logger.info("Unzipping dependencies") - self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) - - # pylint: disable=E0401 - # pylint: disable=C0401 - # pylint: disable=C0415 - # pylint: disable=W0601 - # pylint: disable=W0603 - # pylint: disable=reportMissingImports - ########################################################################## - global requests, torch, BANDS, BANDS_GROUPS_IDX, NORMED_BANDS - global S1_S2_ERA5_SRTM, DynamicWorld2020_2021, BAND_EXPANSION - global IDX_TO_BAND_GROUPS, BAND_EXPANSION, Presto, device, rearrange - global DataLoader, TensorDataset - - import requests - import torch - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( - BANDS, - BANDS_GROUPS_IDX, - NORMED_BANDS, - S1_S2_ERA5_SRTM, - DynamicWorld2020_2021, - ) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.masking import ( - BAND_EXPANSION, - ) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device - from einops import rearrange - from torch.utils.data import DataLoader, TensorDataset - - ########################################################################## - # pylint: enable=E0401 - # pylint: enable=C0401 - # pylint: enable=C0415 - # pylint: enable=W0601 - # pylint: enable=W0603 - # pylint: enable=reportMissingImports - # Index to band groups mapping - IDX_TO_BAND_GROUPS = { - NORMED_BANDS[idx]: band_group_idx - for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) - for idx in val - } - - self.logger.info("Extracting presto features") - features = self.get_presto_features(inarr, self.PRESTO_PATH) - return features diff --git a/minimal_wc_presto/test_presto_fc_gfmap.py b/minimal_wc_presto/test_presto_fc_gfmap.py index efe93496..3b649dd6 100644 --- a/minimal_wc_presto/test_presto_fc_gfmap.py +++ b/minimal_wc_presto/test_presto_fc_gfmap.py @@ -3,9 +3,11 @@ import openeo from openeo_gfmap import Backend, BackendContext, BoundingBoxExtent, TemporalContext from openeo_gfmap.features.feature_extractor import apply_feature_extractor -from presto_feature_computer import PrestoFeatureExtractor +from openeo_gfmap.inference.model_inference import apply_model_inference from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap +from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor +from worldcereal.openeo.inference import CroplandClassifier EXTENT = dict( zip(["west", "south", "east", "north"], [664000.0, 5611120.0, 665000.0, 5612120.0]) @@ -15,6 +17,8 @@ STARTDATE = "2020-11-01" ENDDATE = "2021-10-31" +ONNX_DEPS_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openeo/onnx_dependencies_1.16.3.zip" + if __name__ == "__main__": # Test extent @@ -62,8 +66,29 @@ ], ) - features.execute_batch( - outputfile=".notebook-tests/presto_features_gfmap_nointerp.nc", + catboost_parameters = {} + + classes = apply_model_inference( + model_inference_class=CroplandClassifier, + cube=features, + parameters=catboost_parameters, + size=[ + {"dimension": "x", "unit": "px", "value": 100}, + {"dimension": "y", "unit": "px", "value": 100}, + {"dimension": "t", "value": "P1D"}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 0}, + {"dimension": "y", "unit": "px", "value": 0}, + ] + ) + + classes.execute_batch( + outputfile=".notebook-tests/presto_prediction_gfmap.nc", out_format="NetCDF", - job_options={"driver-memory": "4g", "executor-memoryOverhead": "8g"}, + job_options={ + "driver-memory": "4g", + "executor-memoryOverhead": "8g", + "udf-dependency-archives": [f"{ONNX_DEPS_URL}#onnx_deps"], + }, ) diff --git a/scripts/inference/cropland_mapping.py b/scripts/inference/cropland_mapping.py index 6066b092..b26cbd77 100644 --- a/scripts/inference/cropland_mapping.py +++ b/scripts/inference/cropland_mapping.py @@ -1,21 +1,21 @@ """Cropland mapping inference script, demonstrating the use of the GFMAP, Presto and WorldCereal classifiers in a first inference pipeline.""" import argparse +from pathlib import Path + +import openeo from openeo_gfmap import BoundingBoxExtent, TemporalContext -from openeo_gfmap.backend import Backend, BackendContext, cdse_connection -from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor +from openeo_gfmap.backend import Backend, BackendContext +from openeo_gfmap.features.feature_extractor import apply_feature_extractor +from openeo_gfmap.inference.model_inference import apply_model_inference from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap +from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor +from worldcereal.openeo.inference import CroplandClassifier -class PrestoFeatureExtractor(PatchFeatureExtractor): - def __init__(self): - pass - - def extract(self, image): - pass - +ONNX_DEPS_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openeo/onnx_dependencies_1.16.3.zip" if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -27,12 +27,13 @@ def extract(self, image): parser.add_argument("miny", type=float, help="Minimum Y coordinate (south)") parser.add_argument("maxx", type=float, help="Maximum X coordinate (east)") parser.add_argument("maxy", type=float, help="Maximum Y coordinate (north)") + parser.add_argument("--epsg", type=int, default=4326, help="EPSG code for coordiante reference system.") parser.add_argument( "start_date", type=str, help="Starting date for data extraction." ) parser.add_argument("end_date", type=str, help="Ending date for data extraction.") parser.add_argument( - "output_folder", type=str, help="Path to folder where to save results." + "output_path", type=Path, help="Path to folder where to save the resulting NetCDF." ) args = parser.parse_args() @@ -41,29 +42,70 @@ def extract(self, image): miny = args.miny maxx = args.maxx maxy = args.maxy + epsg = args.epsg start_date = args.start_date end_date = args.end_date - spatial_extent = BoundingBoxExtent(minx, miny, maxx, maxy) + spatial_extent = BoundingBoxExtent(minx, miny, maxx, maxy, epsg) temporal_extent = TemporalContext(start_date, end_date) - backend = BackendContext(Backend.CDSE) + backend_context = BackendContext(Backend.FED) + + connection = openeo.connect( + "https://openeo.creo.vito.be/openeo/" + ).authenticate_oidc() # Preparing the input cube for the inference - input_cube = worldcereal_preprocessed_inputs_gfmap( - connection=cdse_connection(), - backend_context=backend, + inputs = worldcereal_preprocessed_inputs_gfmap( + connection=connection, + backend_context=backend_context, spatial_extent=spatial_extent, temporal_extent=temporal_extent, ) - # Start the job and download - job = input_cube.create_job( - title=f"Cropland inference BBOX: {minx} {miny} {maxx} {maxy}", - description="Cropland inference using WorldCereal, Presto and GFMAP classifiers", - out_format="NetCDF", + # Test feature computer + presto_parameters = { + "rescale_s1": False, # Will be done in the Presto UDF itself! + } + + features = apply_feature_extractor( + feature_extractor_class=PrestoFeatureExtractor, + cube=inputs, + parameters=presto_parameters, + size=[ + {"dimension": "x", "unit": "px", "value": 100}, + {"dimension": "y", "unit": "px", "value": 100}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 0}, + {"dimension": "y", "unit": "px", "value": 0}, + ], ) - job.start_and_wait() - job.get_results().download_files(args.output_folder) + catboost_parameters = {} + + classes = apply_model_inference( + model_inference_class=CroplandClassifier, + cube=features, + parameters=catboost_parameters, + size=[ + {"dimension": "x", "unit": "px", "value": 100}, + {"dimension": "y", "unit": "px", "value": 100}, + {"dimension": "t", "value": "P1D"}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 0}, + {"dimension": "y", "unit": "px", "value": 0}, + ] + ) + + classes.execute_batch( + outputfile=args.output_path, + out_format="NetCDF", + job_options={ + "driver-memory": "4g", + "executor-memoryOverhead": "8g", + "udf-dependency-archives": [f"{ONNX_DEPS_URL}#onnx_deps"], + }, + ) diff --git a/src/worldcereal/openeo/feature_extractor.py b/src/worldcereal/openeo/feature_extractor.py new file mode 100644 index 00000000..5a4958e7 --- /dev/null +++ b/src/worldcereal/openeo/feature_extractor.py @@ -0,0 +1,116 @@ +"""Feature computer GFMAP compatible to compute Presto embeddings.""" + +import xarray as xr +from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor + +class PrestoFeatureExtractor(PatchFeatureExtractor): + """Feature extractor to use Presto model to compute embeddings. + This will generate a datacube with 128 bands, each band representing a + feature from the Presto model. + """ + + import functools + from pathlib import Path + from typing import Tuple + + PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA + BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA + DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + + GFMAP_BAND_MAPPING = { + "S2-L2A-B02": "B02", + "S2-L2A-B03": "B03", + "S2-L2A-B04": "B04", + "S2-L2A-B05": "B05", + "S2-L2A-B06": "B06", + "S2-L2A-B07": "B07", + "S2-L2A-B08": "B08", + "S2-L2A-B8A": "B8A", + "S2-L2A-B11": "B11", + "S2-L2A-B12": "B12", + "S1-SIGMA0-VH": "VH", + "S1-SIGMA0-VV": "VV", + "COP-DEM": "DEM", + "A5-tmean": "temperature-mean", + "A5-precip": "precipitation-flux", + } + + def __init__(self): + """ + Initializes the PrestoFeatureExtractor object, starting a logger. + """ + import logging + + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(PrestoFeatureExtractor.__name__) + + + @classmethod + @functools.lru_cache(maxsize=6) + def extract_dependencies(cls, base_url: str, dependency_name: str): + """Extract the dependencies from the given URL. Unpacking a zip + file in the current working directory. + """ + import shutil + import urllib.request + from pathlib import Path + + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / "dependencies" + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve( + modelfile_url, filename=dependencies_dir / Path(modelfile_url).name + ) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str( + dependencies_dir / Path(modelfile_url).name.split(".zip")[0] + ) # NOQA + + return abs_path + + def output_labels(self) -> list: + """Returns the output labels from this UDF, which is the output labels + of the presto embeddings""" + return [f"presto_ft_{i}" for i in range(128)] + + def execute(self, inarr: xr.DataArray) -> xr.DataArray: + import sys + + if self.epsg is None: + raise ValueError( + "EPSG code is required for Presto feature extraction, but was " + "not correctly initialized." + ) + + # The below is required to avoid flipping of the result + # when running on OpenEO backend! + inarr = inarr.transpose("bands", "t", "x", "y") + + # Change the band names + new_band_names = [ + self.GFMAP_BAND_MAPPING.get(b.item(), b.item()) for b in inarr.bands + ] + inarr = inarr.assign_coords(bands=new_band_names) + + # Handle NaN values in Presto compatible way + inarr = inarr.fillna(65535) + + # Unzip de dependencies on the backend + self.logger.info("Unzipping dependencies") + deps_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + + self.logger.info("Appending dependencies") + sys.path.append(str(deps_dir)) + + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features + + self.logger.info("Extracting presto features") + features = get_presto_features(inarr, self.PRESTO_PATH, self.epsg) + return features diff --git a/src/worldcereal/openeo/feature_udf.py b/src/worldcereal/openeo/feature_udf.py deleted file mode 100644 index 4ec815d9..00000000 --- a/src/worldcereal/openeo/feature_udf.py +++ /dev/null @@ -1,184 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -from typing import Dict - -import numpy as np -import pandas as pd -import xarray as xr -from openeo.udf import XarrayDataCube -from satio.collections import XArrayTrainingCollection - -from worldcereal.features.settings import ( - get_cropland_features_meta, - get_default_rsi_meta, -) -from worldcereal.fp import L2AFeaturesProcessor - -sys.path.append("/data/users/Public/driesj/openeo/deps/satio") -sys.path.append("/data/users/Public/driesj/openeo/deps/wc-classification/src") -# sys.path.insert(0,'/data/users/Public/driesj/openeo/deps/tf230') - -wheels = [ - "loguru-0.5.3-py3-none-any.whl", - "aiocontextvars-0.2.2-py2.py3-none-any.whl", - "contextvars-2.4", - "immutables-0.14-cp36-cp36m-manylinux1_x86_64.whl", - "importlib_resources-3.3.0-py2.py3-none-any.whl", -] -for wheel in wheels: - sys.path.append("/data/users/Public/driesj/openeo/deps/" + wheel) - - -classifier_file = "/tmp/worldcereal_croplandextent_lpis_unet.h5" - - -features_meta = get_cropland_features_meta() - - -class L2AFeaturesProcessor10m(L2AFeaturesProcessor): - L2A_BANDS_10M = [ - "B02", - "B03", - "B04", - "B08", - "B05", - "B06", - "B07", - "B8A", - "B11", - "B12", - "SCL", - "sunAzimuthAngles", - "sunZenithAngles", - "viewAzimuthMean", - "viewZenithMean", - ] - L2A_BANDS_DICT_ALL_10M = {10: L2A_BANDS_10M, 20: {"DUMMY"}} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @property - def supported_bands(self): - return L2AFeaturesProcessor10m.L2A_BANDS_DICT_ALL_10M - - -def apply_datacube(cube: XarrayDataCube, context: Dict) -> XarrayDataCube: - """ - This UDF computes WorldCereal features using SatIO. - It works on a spatiotemporal stack for one specific sensor, - currently Sentinel-2 - - @param cube: - @param context: A context dictionary, has to contain 'satio_settings' - @return: - """ - # access the underlying xarray - inarr = cube.get_array() - - # translate openEO dim name into satio convention - inarr = inarr.rename({"t": "timestamp"}) - # satio expects uint16! - inarr = inarr.astype(np.uint16) - - settings = context["satio_settings"] - settings["OPTICAL"]["composite"]["start"] = np.datetime_as_string( - inarr.coords["timestamp"].values.min(), unit="D" - ) - settings["OPTICAL"]["composite"]["end"] = np.datetime_as_string( - inarr.coords["timestamp"].values.max(), unit="D" - ) - - classify = context["classify"] - - collection = XArrayTrainingCollection( - sensor="S2", processing_level="L2A", df=pd.DataFrame(), array=inarr - ) - - from satio.rsindices import RSI_META_S2 - - default_rsi_meta = RSI_META_S2.copy() - rsi_meta = get_default_rsi_meta()["OPTICAL"] - - # in openEO, all bands are provided in 10m for now - # so we need to modify satio defaults - rsi_meta["brightness"] = default_rsi_meta["brightness"] - rsi_meta["brightness"]["native_res"] = 10 - - if "sen2agri_temp_feat" in features_meta.get("OPTICAL", {}): - features_meta["OPTICAL"]["sen2agri_temp_feat"]["parameters"][ - "time_start" - ] = settings["OPTICAL"]["composite"]["start"] - - processor = L2AFeaturesProcessor10m( - collection, - settings["OPTICAL"], - rsi_meta=rsi_meta, - features_meta=features_meta["OPTICAL"], - ) - features = processor.compute_features() - - # Extracted core from worldcereal ClassificationProcessor, - # to be seen what we need to keep - - if classify: - windowsize = 64 - import tensorflow as tf - - # from worldcereal.classification.models import WorldCerealUNET - # unetmodel = WorldCerealUNET(windowsize=64, features= 60) - # unetmodel.model.load_weights(classifier_file) - # classifier = unetmodel.model - classifier = tf.keras.models.load_model(classifier_file) - - xdim = features.data.shape[1] - ydim = features.data.shape[2] - - prediction = np.empty((xdim, ydim)) - - # can be avoided by using openEO apply_neighbourhood - for xStart in range(0, xdim, windowsize): - for yStart in range(0, ydim, windowsize): - # We need to check if we're at the end of the master image - # We have to make sure we have a full subtile - # so we need to expand such tile and the resulting overlap - # with previous subtile is not an issue - if xStart + windowsize > xdim: - xStart = xdim - windowsize - xEnd = xdim - else: - xEnd = xStart + windowsize - if yStart + windowsize > ydim: - yStart = ydim - windowsize - yEnd = ydim - else: - yEnd = yStart + windowsize - - features_patch = features.data[:, xStart:xEnd, yStart:yEnd] - patchprediction = ( - classifier.predict( - features_patch.transpose((1, 2, 0)).reshape( - (1, windowsize * windowsize, -1) - ) - ) - .squeeze() - .reshape((windowsize, windowsize)) - ) - - prediction[xStart:xEnd, yStart:yEnd] = patchprediction - - prediction_xarray = xr.DataArray(prediction.astype(np.float32), dims=["x", "y"]) - - # wrap back to datacube and return - return XarrayDataCube(prediction_xarray) - - else: - features_xarray = xr.DataArray( - features.data.astype(np.float32), - dims=["bands", "x", "y"], - coords={"bands": features.names}, - ) - - # wrap back to datacube and return - return XarrayDataCube(features_xarray) - return XarrayDataCube(features_xarray) diff --git a/src/worldcereal/openeo/inference.py b/src/worldcereal/openeo/inference.py new file mode 100644 index 00000000..49f796e6 --- /dev/null +++ b/src/worldcereal/openeo/inference.py @@ -0,0 +1,65 @@ +"""Model inference on Presto feature for binary classication""" + +import xarray as xr + +from openeo_gfmap.inference.model_inference import ModelInference + +class CroplandClassifier(ModelInference): + import functools + + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA + BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA + DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + + def __init__(self): + import logging + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(WorldCerealInference.__name__) + + @classmethod + @functools.lru_cache(maxsize=6) + def extract_dependencies(cls, base_url: str, dependency_name: str): + from pathlib import Path + import urllib.request + import shutil + + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / 'dependencies' + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) + + return(abs_path) + + def output_labels(self) -> list: + return ["classification"] + + def execute(self, inarr: xr.DataArray) -> xr.DataArray: + import sys + + # shape and indiches for output + inarr = inarr.transpose('bands', 'x', 'y') + + # Unzip de dependencies on the backend + self.logger.info("Unzipping dependencies") + dep_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + + self.logger.info("Adding dependencies") + sys.path.append(str(dep_dir)) + + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import classify_with_catboost + + # Run catboost classification + self.logger.info("Catboost classification") + classification = classify_with_catboost(inarr, self.CATBOOST_PATH) + self.logger.info("Done") + + return classification From b443e8b52243682b686fa8d0e5e00dda5de65b80 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Fri, 31 May 2024 16:26:29 +0200 Subject: [PATCH 25/31] Cleaned up more by deleting a few duplicate codes --- minimal_wc_presto/preprocessing.py | 512 ------------------ minimal_wc_presto/presto_feature_computer.py | 432 --------------- ...sto_fc_gfmap.py => test_cropland_gfmap.py} | 33 +- .../udf_long_worldcereal_inference.py | 442 --------------- minimal_wc_presto/udf_presto.py | 85 --- .../udf_worldcereal_inference.py | 91 ---- scripts/inference/cropland_mapping.py | 93 +++- src/worldcereal/openeo/feature_extractor.py | 118 ++++ src/worldcereal/openeo/feature_udf.py | 184 ------- src/worldcereal/openeo/inference.py | 70 +++ 10 files changed, 287 insertions(+), 1773 deletions(-) delete mode 100644 minimal_wc_presto/preprocessing.py delete mode 100644 minimal_wc_presto/presto_feature_computer.py rename minimal_wc_presto/{test_presto_fc_gfmap.py => test_cropland_gfmap.py} (62%) delete mode 100644 minimal_wc_presto/udf_long_worldcereal_inference.py delete mode 100644 minimal_wc_presto/udf_presto.py delete mode 100644 minimal_wc_presto/udf_worldcereal_inference.py create mode 100644 src/worldcereal/openeo/feature_extractor.py delete mode 100644 src/worldcereal/openeo/feature_udf.py create mode 100644 src/worldcereal/openeo/inference.py diff --git a/minimal_wc_presto/preprocessing.py b/minimal_wc_presto/preprocessing.py deleted file mode 100644 index 7cf4c9a2..00000000 --- a/minimal_wc_presto/preprocessing.py +++ /dev/null @@ -1,512 +0,0 @@ -from openeo.processes import array_create, if_, is_nodata, power -from openeo.rest.datacube import DataCube -import openeo -COMPOSITE_WINDOW = "month" - - -def get_S1_bands( - connection, - S1_collection, - bbox, - start, - end, - other_bands=None, - preprocess=True, - **processing_options, -): - """Method to add S1 bands to datacube - - Args: - S1_collection (str): name of the S1 collection - other_bands (DataCube): OpenEO datacube to add bands to - - Available processing_options: - s1_orbitdirection - provider - target_epsg - """ - isCreo = "creo" in processing_options.get("provider", "").lower() - orbit_direction = processing_options.get("s1_orbitdirection", None) - composite_window = processing_options.get("composite_window", COMPOSITE_WINDOW) - - # TODO: implement as needed - # if isCreo: - # orbit_direction = catalogue_check_S1(orbit_direction, start, end, bbox) - - if orbit_direction is not None: - properties = { - "sat:orbit_state": lambda orbdir: orbdir == orbit_direction - } # NOQA - else: - properties = {} - - # Load collection - S1bands = connection.load_collection( - S1_collection, - bands=["VH", "VV"], - spatial_extent=bbox, - temporal_extent=[start, end], - properties=properties, - ) - - if S1_collection == "SENTINEL1_GRD": - # compute backscatter if starting from raw GRD, - # otherwise assume preprocessed backscatter - S1bands = S1bands.sar_backscatter( - coefficient="sigma0-ellipsoid", - local_incidence_angle=False, - # DO NOT USE MAPZEN - elevation_model="COPERNICUS_30" if isCreo else None, - options={ - "implementation_version": "2", - "tile_size": 256, - "otb_memory": 1024, - "debug": False, - "elev_geoid": "/opt/openeo-vito-aux-data/egm96.tif", - }, - ) - else: - pass - - # Resample to the S2 spatial resolution - target_epsg = processing_options.get("target_epsg", None) - if target_epsg is not None: - S1bands = S1bands.resample_spatial(projection=target_epsg, resolution=10.0) - - if preprocess: - - # Composite to compositing window - S1bands = S1bands.aggregate_temporal_period( - period=composite_window, reducer="mean" - ) - - # # Linearly interpolate missing values - # Assume Presto handles nodata natively - # S1bands = S1bands.apply_dimension( - # dimension="t", process="array_interpolate_linear" - # ) - - # Scale to int16 - if isCreo: - # for CREO, rescaling also replaces nodata introduced by orfeo - # with a low value - # https://github.com/Open-EO/openeo-geopyspark-driver/issues/293 - # TODO: check if nodata is correctly handled in Orfeo - S1bands = S1bands.apply_dimension( - dimension="bands", - process=lambda x: array_create( - [ - if_( - is_nodata(x[0]), - 1, - power(base=10, p=(10.0 * x[0].log(base=10) + 83.0) / 20.0), - ), - if_( - is_nodata(x[1]), - 1, - power(base=10, p=(10.0 * x[1].log(base=10) + 83.0) / 20.0), - ), - ] - ), - ) - else: - S1bands = S1bands.apply_dimension( - dimension="bands", - process=lambda x: array_create( - [ - power(base=10, p=(10.0 * x[0].log(base=10) + 83.0) / 20.0), - power(base=10, p=(10.0 * x[1].log(base=10) + 83.0) / 20.0), - ] - ), - ) - - S1bands = S1bands.linear_scale_range(1, 65534, 1, 65534) - - # -------------------------------------------------------------------- - # Merge cubes - # -------------------------------------------------------------------- - if other_bands is None: - return S1bands - else: - merged_inputs = other_bands.resample_cube_spatial(S1bands).merge_cubes(S1bands) - return merged_inputs - - -def get_S2_bands( - connection, - S2_collection, - bbox, - start, - end, - masking, - preprocess=True, - other_bands=None, - target_epsg=None, - **processing_options, -): - """Method to get S2 bands and optionally merge with other bands - - Args: - S2_collection (str): name of the S2 collection - other_bands (DataCube): OpenEO datacube to add bands to - - Available processing_options: - s1_orbitdirection - provider - target_epsg - """ - - composite_window = processing_options.get("composite_window", COMPOSITE_WINDOW) - - S2_bands = ["B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B11", "B12"] - if masking not in ["satio", "mask_scl_dilation", None]: - raise ValueError(f"Unknown masking option `{masking}`") - if masking in ["mask_scl_dilation"]: - # Need SCL band to mask - S2_bands.append("SCL") - bands = connection.load_collection( - S2_collection, - bands=S2_bands, - spatial_extent=bbox, - temporal_extent=[start, end], - max_cloud_cover=95, - ) - - # TODO: implement as needed - # S2URL creo only accepts request in EPSG:4326 - # isCreo = "creo" in processing_options.get("provider", "").lower() - # if isCreo: - # catalogue_check_S2(start, end, bbox) - - # NOTE: For now we mask again snow/ice because clouds - # are sometimes marked as SCL value 11! - if masking == "mask_scl_dilation": - # TODO: double check cloud masking parameters - # https://github.com/Open-EO/openeo-geotrellis-extensions/blob/develop/geotrellis-common/src/main/scala/org/openeo/geotrelliscommon/CloudFilterStrategy.scala#L54 # NOQA - bands = bands.process( - "mask_scl_dilation", - data=bands, - scl_band_name="SCL", - kernel1_size=17, - kernel2_size=77, - mask1_values=[2, 4, 5, 6, 7], - mask2_values=[3, 8, 9, 10, 11], - erosion_kernel_size=3, - ).filter_bands(bands.metadata.band_names[:-1]) - #elif masking == "satio": - # Apply satio-based mask - # mask = scl_mask_erode_dilate( - # connection, - # bbox, - # scl_layer_band=S2_collection + ":SCL", - # target_epsg=target_epsg, - # ).resample_cube_spatial(bands) - # bands = bands.mask(mask) - - if preprocess: - # Composite to compositing window - bands = bands.aggregate_temporal_period( - period=composite_window, reducer="median" - ) - # bands = max_ndvi_composite(bands, composite_window=composite_window) - - # TODO: if we would disable it here, nodata values - # will be 65535 and we need to cope with that later - # Linearly interpolate missing values - # bands = bands.apply_dimension(dimension="t", process="array_interpolate_linear") - - # Force UINT16 to avoid overflow issue with S2 data - bands = bands.linear_scale_range(0, 65534, 0, 65534) - - # -------------------------------------------------------------------- - # Merge cubes - # -------------------------------------------------------------------- - if other_bands is None: - return bands - else: - merged_inputs = other_bands.resample_cube_spatial(bands).merge_cubes(bands) - return merged_inputs - - -def get_DEM(connection, DEM_collection, bbox, other_bands=None, **processing_options): - """Method to add DEM to datacube - - Args: - connection (_type_): _description_ - DEM_collection (str): Name of DEM collection - other_bands (DataCube): DataCube to merge DEM into - bbox (_type_): _description_ - - Returns: - DataCube: merged datacube - """ - - dem = connection.load_collection( - DEM_collection, - spatial_extent=bbox, - ) - - # Resample to the S2 spatial resolution - target_epsg = processing_options.get("target_epsg", None) - if target_epsg is not None: - dem = dem.resample_spatial( - projection=target_epsg, resolution=10.0, method="cubic" - ) - - # collection has timestamps which we need to get rid of - dem = dem.max_time() - - # -------------------------------------------------------------------- - # Merge cubes - # -------------------------------------------------------------------- - if other_bands is None: - return dem - else: - merged_inputs = other_bands.merge_cubes(dem) - return merged_inputs - - -def get_meteo( - connection, - METEO_collection, - bbox, - start, - end, - other_bands=None, - target_epsg=None, - **processing_options, -): - # AGERA5 - composite_window = processing_options.get("composite_window", COMPOSITE_WINDOW) - - meteo = connection.load_collection( - METEO_collection, - spatial_extent=bbox, - bands=["temperature-mean", "precipitation-flux"], - temporal_extent=[start, end], - ) - - meteo.result_node().update_arguments(featureflags={"tilesize": 1}) - - - if target_epsg is not None: - meteo = meteo.resample_spatial( - projection=target_epsg, resolution=10.0, method="bilinear" - ) - - # Composite to desired window. we want to aggregate data with - # different reducers. sum for precipitation within a month and - # mean for the temperature - meteo_temp = meteo.filter_bands(bands=["temperature-mean"]) - meteo_temp = meteo_temp.aggregate_temporal_period( - period=composite_window, reducer="mean" - ) - meteo_temp = meteo_temp.apply_dimension( - dimension="t", process="array_interpolate_linear" - ) - - meteo_prec = meteo.filter_bands(bands=["precipitation-flux"]) - meteo_prec = meteo_prec.aggregate_temporal_period( - period=composite_window, reducer="sum" - ) - meteo_prec = meteo_prec.apply_dimension( - dimension="t", process="array_interpolate_linear" - ) - - meteo = meteo_temp.merge_cubes(meteo_prec) - - # -------------------------------------------------------------------- - # Merge cubes - # or return just meteo - # -------------------------------------------------------------------- - if other_bands is None: - return meteo - else: - merged_inputs = other_bands.merge_cubes(meteo) - return merged_inputs - - -def add_worldcereral_labels(connection, bbox, other_bands): - """ - ['ESA_WORLDCEREAL_ACTIVECROPLAND', - 'ESA_WORLDCEREAL_IRRIGATION', - 'ESA_WORLDCEREAL_TEMPORARYCROPS', - 'ESA_WORLDCEREAL_WINTERCEREALS', - 'ESA_WORLDCEREAL_MAIZE', - 'ESA_WORLDCEREAL_SPRINGCEREALS'] - """ - - temporal = ("2020-09-01T00:00:00Z", "2021-12-31T00:00:00Z") - - # Get temporary crops layer - temporarycrops = ( - connection.load_collection( - "ESA_WORLDCEREAL_TEMPORARYCROPS", - temporal_extent=temporal, - spatial_extent=bbox, - bands=["CLASSIFICATION"], - ) - .rename_labels("bands", ["worldcereal_cropland"]) - .max_time() - ) - temporarycrops = temporarycrops.resample_cube_spatial(other_bands, method="near") - other_bands = other_bands.merge_cubes(temporarycrops) - - # Get maize layer - maize = ( - connection.load_collection( - "ESA_WORLDCEREAL_MAIZE", - temporal_extent=temporal, - spatial_extent=bbox, - bands=["CLASSIFICATION"], - ) - .rename_labels("bands", ["worldcereal_maize"]) - .max_time() - ) - maize = maize.resample_cube_spatial(other_bands, method="near") - other_bands = other_bands.merge_cubes(maize) - - # Get wintercereals layer - wintercereals = ( - connection.load_collection( - "ESA_WORLDCEREAL_WINTERCEREALS", - temporal_extent=temporal, - spatial_extent=bbox, - bands=["CLASSIFICATION"], - ) - .rename_labels("bands", ["worldcereal_wintercereals"]) - .max_time() - ) - wintercereals = wintercereals.resample_cube_spatial(other_bands, method="near") - other_bands = other_bands.merge_cubes(wintercereals) - - # # Get springcereals layer - # springcereals = ( - # connection.load_collection( - # "ESA_WORLDCEREAL_SPRINGCEREALS", - # temporal_extent=temporal, - # spatial_extent=bbox, - # bands=["CLASSIFICATION"], - # ) - # .rename_labels("bands", ["worldcereal_springcereals"]) - # .max_time() - # ) - # springcereals = springcereals.resample_cube_spatial(other_bands, method="near") - # other_bands = other_bands.merge_cubes(springcereals) - - return other_bands - - -def worldcereal_preprocessed_inputs( - connection, - bbox, - start: str, - end: str, - S2_collection="SENTINEL2_L2A", - S1_collection="SENTINEL1_GRD", - DEM_collection="COPERNICUS_30", - METEO_collection="AGERA5", - preprocess=True, - masking="mask_scl_dilation", - worldcereal_labels=False, - **processing_options, -) -> DataCube: - """Main method to get preprocessed inputs from OpenEO for - downstream crop type mapping. - - Args: - connection: OpenEO connection instance - bbox (_type_): _description_ - start (str): Start date for requested input data (yyyy-mm-dd) - end (str): Start date for requested input data (yyyy-mm-dd) - S2_collection (str, optional): Collection name for S2 data. - Defaults to - 'TERRASCOPE_S2_TOC_V2'. - S1_collection (str, optional): Collection name for S1 data. - Defaults to - 'SENTINEL1_GRD'. - DEM_collection (str, optional): Collection name for DEM data. - Defaults to - 'COPERNICUS_30'. - METEO_collection (str, optional): Collection name for - meteo data. Defaults to 'AGERA5'. - preprocess (bool, optional): Apply compositing and interpolation. - Defaults to True. - masking (str, optional): Masking method to be applied. - One of ['satio', 'mask_scl_dilation', None] - Defaults to 'mask_scl_dilation'. - worldcereal_labels (bool, optional): If True, worldcereal 2021 labels - will be added to the datacube. Defaults to False. - - Returns: - DataCube: OpenEO DataCube wich the requested inputs - """ - - bands = None - - # -------------------------------------------------------------------- - # Optical data - # -------------------------------------------------------------------- - - if S2_collection is not None: - bands = get_S2_bands( - connection, - S2_collection, - bbox, - start, - end, - masking, - preprocess=preprocess, - **processing_options, - ) - - # -------------------------------------------------------------------- - # AGERA5 Meteo data - # -------------------------------------------------------------------- - if METEO_collection is not None: - bands = get_meteo( - connection, - METEO_collection, - bbox, - start, - end, - other_bands=bands, - **processing_options, - ) - - # -------------------------------------------------------------------- - # SAR data - # -------------------------------------------------------------------- - if S1_collection is not None: - bands = get_S1_bands( - connection, - S1_collection, - bbox, - start, - end, - other_bands=bands, - **processing_options, - ) - - bands = bands.filter_temporal(start, end) - - # -------------------------------------------------------------------- - # DEM data - # -------------------------------------------------------------------- - if DEM_collection is not None: - bands = get_DEM(connection, DEM_collection, bbox, bands, **processing_options) - - # -------------------------------------------------------------------- - # Worldcereal labels - # -------------------------------------------------------------------- - if worldcereal_labels: - bands = add_worldcereral_labels(connection, bbox, bands) - - # forcing 16bit - bands = bands.linear_scale_range(0, 65534, 0, 65534) - - return bands - - -def worldcereal_raw_inputs(*args, **kwargs): - return worldcereal_preprocessed_inputs(*args, **kwargs, preprocess=False) diff --git a/minimal_wc_presto/presto_feature_computer.py b/minimal_wc_presto/presto_feature_computer.py deleted file mode 100644 index b0034e2a..00000000 --- a/minimal_wc_presto/presto_feature_computer.py +++ /dev/null @@ -1,432 +0,0 @@ -"""Feature computer GFMAP compatible to compute Presto embeddings.""" - -import numpy as np -import xarray as xr -from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor -from pyproj import Transformer - - -class PrestoFeatureExtractor(PatchFeatureExtractor): - """Feature extractor to use Presto model to compute embeddings. - This will generate a datacube with 128 bands, each band representing a - feature from the Presto model. - """ - - import functools - from pathlib import Path - from typing import Tuple - - CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA - PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA - BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA - DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" - - _NODATAVALUE = 65535 - - BAND_MAPPING = { - "B02": "B2", - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - "B8A": "B8A", - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", - } - - GFMAP_BAND_MAPPING = { - "S2-L2A-B02": "B02", - "S2-L2A-B03": "B03", - "S2-L2A-B04": "B04", - "S2-L2A-B05": "B05", - "S2-L2A-B06": "B06", - "S2-L2A-B07": "B07", - "S2-L2A-B08": "B08", - "S2-L2A-B8A": "B8A", - "S2-L2A-B11": "B11", - "S2-L2A-B12": "B12", - "S1-SIGMA0-VH": "VH", - "S1-SIGMA0-VV": "VV", - "COP-DEM": "DEM", - "A5-tmean": "temperature-mean", - "A5-precip": "precipitation-flux", - } - - def __init__(self): - """ - Initializes the PrestoFeatureExtractor object, starting a logger. - """ - import logging - - logging.basicConfig(level=logging.INFO) - self.logger = logging.getLogger(PrestoFeatureExtractor.__name__) - self.model = None # To be initialized within the OpenEO environment - - @classmethod - def _preprocess_band_values( - cls, values: np.ndarray, presto_band: str - ) -> np.ndarray: - """ - Preprocesses the band values based on the given presto_val. - - Args: - values (np.ndarray): Array of band values to preprocess. - presto_val (str): Name of the band for preprocessing. - - Returns: - np.ndarray: Preprocessed array of band values. - """ - if presto_band in ["VV", "VH"]: - # Convert to dB - values = 20 * np.log10(values) - 83 - elif presto_band == "total_precipitation": - # Scale precipitation and convert mm to m - values = values / (100 * 1000.0) - elif presto_band == "temperature_2m": - # Remove scaling - values = values / 100 - return values - - @classmethod - def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: - """ - Extracts EO data and mask arrays from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing EO data. - - Returns: - Tuple[np.ndarray, np.ndarray]: Tuple containing EO data array and mask array. - """ - num_pixels = len(inarr.x) * len(inarr.y) - num_timesteps = len(inarr.t) - - eo_data = np.zeros( - (num_pixels, num_timesteps, len(BANDS)) - ) # pylint: disable=E0602 - mask = np.zeros( - (num_pixels, num_timesteps, len(BANDS_GROUPS_IDX)) - ) # pylint: disable=E0602 - - for org_band, presto_band in cls.BAND_MAPPING.items(): - if org_band in inarr.coords["bands"]: - values = rearrange( # pylint: disable=E0602 - inarr.sel(bands=org_band).values, "t x y -> (x y) t" - ) - idx_valid = values != cls._NODATAVALUE - values = cls._preprocess_band_values(values, presto_band) - eo_data[ - :, :, BANDS.index(presto_band) - ] = values # pylint: disable=E0602 - mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid - - return eo_data, mask - - @staticmethod - def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: - """ - Extracts latitudes and longitudes from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing spatial coordinates. - epsg (int): EPSG code for coordinate reference system. - - Returns: - np.ndarray: Array containing extracted latitudes and longitudes. - """ - # EPSG:4326 is the supported crs for presto - lon, lat = np.meshgrid(inarr.x, inarr.y) - transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) - lon, lat = transformer.transform(lon, lat) - latlons = rearrange( - np.stack([lat, lon]), "c x y -> (x y) c" - ) # pylint: disable=E0602 - - # 2D array where each row represents a pair of latitude and longitude coordinates. - return latlons - - @staticmethod - def _extract_months(inarr: xr.DataArray) -> np.ndarray: - """ - Calculate the start month based on the first timestamp in the input array, - and create an array of the same length filled with that start month value. - - Parameters: - - inarr: xarray.DataArray or numpy.ndarray - Input array containing timestamps. - - Returns: - - months: numpy.ndarray - Array of start month values, with the same length as the input array. - """ - num_instances = len(inarr.x) * len(inarr.y) - - start_month = ( - inarr.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1 - ) - 1 - - months = np.ones((num_instances)) * start_month - return months - - def _create_dataloader( - self, - eo: np.ndarray, - dynamic_world: np.ndarray, - months: np.ndarray, - latlons: np.ndarray, - mask: np.ndarray, - ): - """ - Create a PyTorch DataLoader for encoding features. - - Args: - eo_data (np.ndarray): Array containing Earth Observation data. - dynamic_world (np.ndarray): Array containing dynamic world data. - latlons (np.ndarray): Array containing latitude and longitude coordinates. - inarr (xr.DataArray): Input xarray.DataArray. - mask (np.ndarray): Array containing masking data. - - Returns: - DataLoader: PyTorch DataLoader for encoding features. - """ - - # pylint: disable=E0602 - dl = DataLoader( - TensorDataset( - torch.from_numpy(eo).float(), - torch.from_numpy(dynamic_world).long(), - torch.from_numpy(latlons).float(), - torch.from_numpy(months).long(), - torch.from_numpy(mask).float(), - ), - batch_size=8192, - shuffle=False, - ) - # pylint: enable=E0602 - - return dl - - @classmethod - def _create_presto_input( - cls, inarr: xr.DataArray, epsg: int = 4326 - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - eo_data, mask = cls._extract_eo_data(inarr) - latlons = cls._extract_latlons(inarr, epsg) - months = cls._extract_months(inarr) - dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( - DynamicWorld2020_2021.class_amount # pylint: disable=E0602 - ) - - return ( - S1_S2_ERA5_SRTM.normalize(eo_data), # pylint: disable=E0602 - dynamic_world, - months, - latlons, - np.repeat(mask, BAND_EXPANSION, axis=-1), # pylint: disable=E0602 - ) - - def _get_encodings(self, dl) -> np.ndarray: - """ - Get encodings from DataLoader. - - Args: - dl (DataLoader): PyTorch DataLoader containing data for encoding. - - Returns: - np.ndarray: Array containing encoded features. - """ - - all_encodings = [] - - for x, dw, latlons, month, variable_mask in dl: - x_f, dw_f, latlons_f, month_f, variable_mask_f = [ - t.to(device) - for t in (x, dw, latlons, month, variable_mask) # pylint: disable=E0602 - ] - - with torch.no_grad(): # pylint: disable=E0602 - encodings = ( - self.model.encoder( - x_f, - dynamic_world=dw_f.long(), - mask=variable_mask_f, - latlons=latlons_f, - month=month_f, - ) - .cpu() - .numpy() - ) - - all_encodings.append(encodings) - - return np.concatenate(all_encodings, axis=0) - - def extract_presto_features( - self, inarr: xr.DataArray, epsg: int = 4326 - ) -> np.ndarray: - """General function to prepare the input data, generate a data loader, - initialize the model, perform the inference and return the features. - """ - eo, dynamic_world, months, latlons, mask = self._create_presto_input( - inarr, epsg - ) - dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) - - features = self._get_encodings(dl) - features = rearrange( # pylint: disable=E0602 - features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) - ) - ft_names = [f"presto_ft_{i}" for i in range(128)] - features = xr.DataArray( - features, - coords={"x": inarr.x, "y": inarr.y, "bands": ft_names}, - dims=["x", "y", "bands"], - ) - - return features - - @classmethod - @functools.lru_cache(maxsize=6) - def extract_dependencies(cls, base_url: str, dependency_name: str): - """Extract the dependencies from the given URL. Unpacking a zip - file in the current working directory. - """ - import shutil - import sys - import urllib.request - from pathlib import Path - - # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / "dependencies" - - # Create the directory if it doesn't exist - dependencies_dir.mkdir(exist_ok=True, parents=True) - - # Download and extract the model file - modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve( - modelfile_url, filename=dependencies_dir / Path(modelfile_url).name - ) - shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - - # Add the model directory to system path if it's not already there - abs_path = str( - dependencies_dir / Path(modelfile_url).name.split(".zip")[0] - ) # NOQA - - # Append the dependencies - sys.path.append(str(abs_path)) - sys.path.append(str(abs_path) + "/pandas") - - def get_presto_features(self, inarr: xr.DataArray, presto_path: str) -> np.ndarray: - """ - Extracts features from input data using Presto. - - Args: - inarr (xr.DataArray): Input data as xarray DataArray. - presto_path (str): Path to the pretrained Presto model. - - Returns: - xr.DataArray: Extracted features as xarray DataArray. - """ - self.logger.info("Loading presto model.") - presto_model = Presto.load_pretrained_artifactory( # pylint: disable=E0602 - presto_url=presto_path, strict=False - ) - self.model = presto_model - self.logger.info("Presto model loaded sucessfully. Extracting features.") - - # Get the local EPSG code - features = self.extract_presto_features(inarr, epsg=self.epsg) - self.logger.info("Features extracted.") - # features = self.extract_presto_features(inarr, epsg=32631) # TODO remove hardcoded - return features - - def output_labels(self) -> list: - """Returns the output labels from this UDF, which is the output labels - of the presto embeddings""" - return [f"presto_ft_{i}" for i in range(128)] - - def execute(self, inarr: xr.DataArray) -> xr.DataArray: - # The below is required to avoid flipping of the result - # when running on OpenEO backend! - inarr = inarr.transpose("bands", "t", "x", "y") - - # Change the band names - new_band_names = [ - self.GFMAP_BAND_MAPPING.get(b.item(), b.item()) for b in inarr.bands - ] - inarr = inarr.assign_coords(bands=new_band_names) - - self.logger.info("Input data shape: %s", inarr.shape) - for band in inarr.bands: - self.logger.info( - "Input data null values for band %s -> %s", - band, - inarr.sel(bands=band).isnull().sum().item(), - ) - - # Handle NaN values in Presto compatible way - inarr = inarr.fillna(65535) - - self.logger.info( - "After filling NaN values, total input data null values: %s", - inarr.isnull().sum().item(), - ) - - # Unzip de dependencies on the backend - self.logger.info("Unzipping dependencies") - self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) - - # pylint: disable=E0401 - # pylint: disable=C0401 - # pylint: disable=C0415 - # pylint: disable=W0601 - # pylint: disable=W0603 - # pylint: disable=reportMissingImports - ########################################################################## - global requests, torch, BANDS, BANDS_GROUPS_IDX, NORMED_BANDS - global S1_S2_ERA5_SRTM, DynamicWorld2020_2021, BAND_EXPANSION - global IDX_TO_BAND_GROUPS, BAND_EXPANSION, Presto, device, rearrange - global DataLoader, TensorDataset - - import requests - import torch - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( - BANDS, - BANDS_GROUPS_IDX, - NORMED_BANDS, - S1_S2_ERA5_SRTM, - DynamicWorld2020_2021, - ) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.masking import ( - BAND_EXPANSION, - ) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device - from einops import rearrange - from torch.utils.data import DataLoader, TensorDataset - - ########################################################################## - # pylint: enable=E0401 - # pylint: enable=C0401 - # pylint: enable=C0415 - # pylint: enable=W0601 - # pylint: enable=W0603 - # pylint: enable=reportMissingImports - # Index to band groups mapping - IDX_TO_BAND_GROUPS = { - NORMED_BANDS[idx]: band_group_idx - for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) - for idx in val - } - - self.logger.info("Extracting presto features") - features = self.get_presto_features(inarr, self.PRESTO_PATH) - return features diff --git a/minimal_wc_presto/test_presto_fc_gfmap.py b/minimal_wc_presto/test_cropland_gfmap.py similarity index 62% rename from minimal_wc_presto/test_presto_fc_gfmap.py rename to minimal_wc_presto/test_cropland_gfmap.py index efe93496..7cf8a6ff 100644 --- a/minimal_wc_presto/test_presto_fc_gfmap.py +++ b/minimal_wc_presto/test_cropland_gfmap.py @@ -3,8 +3,10 @@ import openeo from openeo_gfmap import Backend, BackendContext, BoundingBoxExtent, TemporalContext from openeo_gfmap.features.feature_extractor import apply_feature_extractor -from presto_feature_computer import PrestoFeatureExtractor +from openeo_gfmap.inference.model_inference import apply_model_inference +from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor +from worldcereal.openeo.inference import CroplandClassifier from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap EXTENT = dict( @@ -15,6 +17,8 @@ STARTDATE = "2020-11-01" ENDDATE = "2021-10-31" +ONNX_DEPS_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openeo/onnx_dependencies_1.16.3.zip" + if __name__ == "__main__": # Test extent @@ -62,8 +66,29 @@ ], ) - features.execute_batch( - outputfile=".notebook-tests/presto_features_gfmap_nointerp.nc", + catboost_parameters = {} + + classes = apply_model_inference( + model_inference_class=CroplandClassifier, + cube=features, + parameters=catboost_parameters, + size=[ + {"dimension": "x", "unit": "px", "value": 100}, + {"dimension": "y", "unit": "px", "value": 100}, + {"dimension": "t", "value": "P1D"}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 0}, + {"dimension": "y", "unit": "px", "value": 0}, + ], + ) + + classes.execute_batch( + outputfile=".notebook-tests/presto_prediction_gfmap.nc", out_format="NetCDF", - job_options={"driver-memory": "4g", "executor-memoryOverhead": "8g"}, + job_options={ + "driver-memory": "4g", + "executor-memoryOverhead": "8g", + "udf-dependency-archives": [f"{ONNX_DEPS_URL}#onnx_deps"], + }, ) diff --git a/minimal_wc_presto/udf_long_worldcereal_inference.py b/minimal_wc_presto/udf_long_worldcereal_inference.py deleted file mode 100644 index 2671c033..00000000 --- a/minimal_wc_presto/udf_long_worldcereal_inference.py +++ /dev/null @@ -1,442 +0,0 @@ -import functools -import logging -import shutil -import sys -import urllib.request -from pathlib import Path -from typing import Dict, Tuple - -import numpy as np -import xarray as xr -from pyproj import Transformer - - -def _setup_logging(): - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - return logger - - -@functools.lru_cache(maxsize=6) -def extract_dependencies(base_url: str, dependency_name: str): - - # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / "dependencies" - - # Create the directory if it doesn't exist - dependencies_dir.mkdir(exist_ok=True, parents=True) - - # Download and extract the model file - modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve( - modelfile_url, filename=dependencies_dir / Path(modelfile_url).name - ) - shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - - # Add the model directory to system path if it's not already there - abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) # NOQA - - # Append the dependencies - sys.path.append(str(abs_path)) - sys.path.append(str(abs_path) + "/pandas") - - return - - -def apply_datacube(cube: xr.DataArray, context: Dict) -> xr.DataArray: - - CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA - PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA - BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA - DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" - - logger = _setup_logging() - - # The below is required to avoid flipping of the result - # when running on OpenEO backend! - cube = cube.transpose("bands", "t", "x", "y") - - # Handle NaN values in Presto compatible way - cube = cube.fillna(65535) - - # Unzip de dependencies on the backend - logger.info("Unzipping dependencies") - extract_dependencies(BASE_URL, DEPENDENCY_NAME) - - ########################################################################## - import onnxruntime - import requests - import torch - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.dataops import ( - BANDS, - BANDS_GROUPS_IDX, - NORMED_BANDS, - S1_S2_ERA5_SRTM, - DynamicWorld2020_2021, - ) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.masking import ( - BAND_EXPANSION, - ) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.presto import Presto - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.utils import device - from einops import rearrange - from torch.utils.data import DataLoader, TensorDataset - - # Index to band groups mapping - IDX_TO_BAND_GROUPS = { - NORMED_BANDS[idx]: band_group_idx - for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) - for idx in val - } - - class WorldCerealPredictor: - def __init__(self): - """ - Initialize an empty WorldCerealPredictor. - """ - self.onnx_session = None - - def load_model(self, model): - """ - Load an ONNX model from the specified path. - - Args: - model_path (str): The path to the ONNX model file. - """ - # Load the dependency into an InferenceSession - self.onnx_session = onnxruntime.InferenceSession(model) - - def predict(self, features: np.ndarray) -> np.ndarray: - """ - Predicts labels using the provided features DataFrame. - - Args: - features (pd.ndarray): 2D array containing the features - - Returns: - pd.DataFrame: DataFrame containing the predicted labels. - """ - if self.onnx_session is None: - raise ValueError( - "Model has not been loaded. Please load a model first." - ) - - # Prepare input data for ONNX model - outputs = self.onnx_session.run(None, {"features": features}) - - # Threshold for binary conversion - threshold = 0.5 - - # Extract all prediction values and convert them to binary labels - prediction_values = [sublist["True"] for sublist in outputs[1]] - binary_labels = np.array(prediction_values) >= threshold - binary_labels = binary_labels.astype(int) - - return binary_labels - - class PrestoFeatureExtractor: - def __init__(self, model: Presto): - """ - Initialize the PrestoFeatureExtractor with a Presto model. - - Args: - model (Presto): The Presto model used for feature extraction. - """ - self.model = model - - _NODATAVALUE = 65535 - - BAND_MAPPING = { - "B02": "B2", - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - "B8A": "B8A", - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", - } - - @classmethod - def _preprocess_band_values( - cls, values: np.ndarray, presto_band: str - ) -> np.ndarray: - """ - Preprocesses the band values based on the given presto_val. - - Args: - values (np.ndarray): Array of band values to preprocess. - presto_val (str): Name of the band for preprocessing. - - Returns: - np.ndarray: Preprocessed array of band values. - """ - if presto_band in ["VV", "VH"]: - # Convert to dB - values = 20 * np.log10(values) - 83 - elif presto_band == "total_precipitation": - # Scale precipitation and convert mm to m - values = values / (100 * 1000.0) - elif presto_band == "temperature_2m": - # Remove scaling - values = values / 100 - return values - - @classmethod - def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: - """ - Extracts EO data and mask arrays from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing EO data. - - Returns: - Tuple[np.ndarray, np.ndarray]: Tuple containing EO data array and mask array. - """ - num_pixels = len(inarr.x) * len(inarr.y) - num_timesteps = len(inarr.t) - - eo_data = np.zeros((num_pixels, num_timesteps, len(BANDS))) - mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) - - for org_band, presto_band in cls.BAND_MAPPING.items(): - if org_band in inarr.coords["bands"]: - values = rearrange( - inarr.sel(bands=org_band).values, "t x y -> (x y) t" - ) - idx_valid = values != cls._NODATAVALUE - values = cls._preprocess_band_values(values, presto_band) - eo_data[:, :, BANDS.index(presto_band)] = values - mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid - - return eo_data, mask - - @staticmethod - def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: - """ - Extracts latitudes and longitudes from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing spatial coordinates. - epsg (int): EPSG code for coordinate reference system. - - Returns: - np.ndarray: Array containing extracted latitudes and longitudes. - """ - # EPSG:4326 is the supported crs for presto - lon, lat = np.meshgrid(inarr.x, inarr.y) - transformer = Transformer.from_crs( - f"EPSG:{epsg}", "EPSG:4326", always_xy=True - ) - lon, lat = transformer.transform(lon, lat) - latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") - - # 2D array where each row represents a pair of latitude and longitude coordinates. - return latlons - - @staticmethod - def _extract_months(inarr: xr.DataArray) -> np.ndarray: - """ - Calculate the start month based on the first timestamp in the input array, - and create an array of the same length filled with that start month value. - - Parameters: - - inarr: xarray.DataArray or numpy.ndarray - Input array containing timestamps. - - Returns: - - months: numpy.ndarray - Array of start month values, with the same length as the input array. - """ - num_instances = len(inarr.x) * len(inarr.y) - - start_month = ( - inarr.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1 - ) - 1 - - months = np.ones((num_instances)) * start_month - return months - - def _create_dataloader( - self, - eo: np.ndarray, - dynamic_world: np.ndarray, - months: np.ndarray, - latlons: np.ndarray, - mask: np.ndarray, - ) -> DataLoader: - """ - Create a PyTorch DataLoader for encoding features. - - Args: - eo_data (np.ndarray): Array containing Earth Observation data. - dynamic_world (np.ndarray): Array containing dynamic world data. - latlons (np.ndarray): Array containing latitude and longitude coordinates. - inarr (xr.DataArray): Input xarray.DataArray. - mask (np.ndarray): Array containing masking data. - - Returns: - DataLoader: PyTorch DataLoader for encoding features. - """ - - dl = DataLoader( - TensorDataset( - torch.from_numpy(eo).float(), - torch.from_numpy(dynamic_world).long(), - torch.from_numpy(latlons).float(), - torch.from_numpy(months).long(), - torch.from_numpy(mask).float(), - ), - batch_size=8192, - shuffle=False, - ) - - return dl - - def _create_presto_input( - cls, inarr: xr.DataArray, epsg: int = 4326 - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - eo_data, mask = cls._extract_eo_data(inarr) - latlons = cls._extract_latlons(inarr, epsg) - months = cls._extract_months(inarr) - dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( - DynamicWorld2020_2021.class_amount - ) - - return ( - S1_S2_ERA5_SRTM.normalize(eo_data), - dynamic_world, - months, - latlons, - np.repeat(mask, BAND_EXPANSION, axis=-1), - ) - - def _get_encodings(self, dl: DataLoader) -> np.ndarray: - """ - Get encodings from DataLoader. - - Args: - dl (DataLoader): PyTorch DataLoader containing data for encoding. - - Returns: - np.ndarray: Array containing encoded features. - """ - - all_encodings = [] - - for x, dw, latlons, month, variable_mask in dl: - x_f, dw_f, latlons_f, month_f, variable_mask_f = [ - t.to(device) for t in (x, dw, latlons, month, variable_mask) - ] - - with torch.no_grad(): - encodings = ( - self.model.encoder( - x_f, - dynamic_world=dw_f.long(), - mask=variable_mask_f, - latlons=latlons_f, - month=month_f, - ) - .cpu() - .numpy() - ) - - all_encodings.append(encodings) - - return np.concatenate(all_encodings, axis=0) - - def extract_presto_features( - self, inarr: xr.DataArray, epsg: int = 4326 - ) -> np.ndarray: - eo, dynamic_world, months, latlons, mask = self._create_presto_input( - inarr, epsg - ) - dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) - - features = self._get_encodings(dl) - features = rearrange( - features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) - ) - ft_names = [f"presto_ft_{i}" for i in range(128)] - features = xr.DataArray( - features, - coords={"x": inarr.x, "y": inarr.y, "bands": ft_names}, - dims=["x", "y", "bands"], - ) - - return features - - def get_presto_features(inarr: xr.DataArray, presto_path: str) -> np.ndarray: - """ - Extracts features from input data using Presto. - - Args: - inarr (xr.DataArray): Input data as xarray DataArray. - presto_path (str): Path to the pretrained Presto model. - - Returns: - xr.DataArray: Extracted features as xarray DataArray. - """ - # Load the model - - presto_model = Presto.load_pretrained_artifactory( - presto_url=presto_path, strict=False - ) - presto_extractor = PrestoFeatureExtractor(presto_model) - features = presto_extractor.extract_presto_features(inarr, epsg=32631) - return features - - def classify_with_catboost( - features: xr.DataArray, catboost_path: str - ) -> np.ndarray: - """ - Classifies features using the WorldCereal CatBoost model. - - Args: - features (xr.DataArray): Features to be classified [x, y, fts] - map_dims (tuple): Original x, y dimensions of the input data. - model_path (str): Path to the trained CatBoost model. - - Returns: - xr.DataArray: Classified data as xarray DataArray. - """ - - # Stack the features and transpose for feeding to CatBoost - stacked_features = features.stack(xy=["x", "y"]).transpose() - - predictor = WorldCerealPredictor() - response = requests.get(catboost_path) - catboost_model = response.content - - predictor.load_model(catboost_model) - predictions = predictor.predict(stacked_features.values) - - predictions = ( - xr.DataArray(predictions, coords={"xy": stacked_features.xy}, dims=["xy"]) - .unstack() - .expand_dims(dim="bands") - ) - - return predictions - - ################################################################################################################### - - # Run presto feature extraction - logger.info("Extracting presto features") - features = get_presto_features(cube, PRESTO_PATH) - - # Run catboost classification - logger.info("Catboost classification") - classification = classify_with_catboost(features, CATBOOST_PATH) - - # Add time dimension - classification = classification.expand_dims(dim="t") - - return classification diff --git a/minimal_wc_presto/udf_presto.py b/minimal_wc_presto/udf_presto.py deleted file mode 100644 index 2e12e562..00000000 --- a/minimal_wc_presto/udf_presto.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -import urllib.request -import shutil -from pathlib import Path -import sys -import functools -import xarray as xr -from typing import Dict -import numpy as np -from pyproj import Transformer - - -def _setup_logging(): - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - return logger - -@functools.lru_cache(maxsize=25) -def extract_dependencies(base_url: str, dependency_name: str): - - # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / 'dependencies' - - # Create the directory if it doesn't exist - dependencies_dir.mkdir(exist_ok=True, parents=True) - - - # Download and extract the model file - modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) - shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - - # Add the model directory to system path if it's not already there - abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) - - return(abs_path) - - -def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: - - logger = _setup_logging() - - - # The below is required to avoid flipping of the result - # when running on OpenEO backend! - cube = cube.transpose("bands", "t", "x", "y") - - # Handle NaN values in Presto compatible way - cube = cube.fillna(65535) - - logger.info("Unzipping dependencies") - #base_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/" - base_url = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" - dependency_name = "wc_presto_onnx_dependencies.zip" - - logger.info("Appending depencency") - dep_dir = extract_dependencies(base_url, dependency_name) - sys.path.append(str(dep_dir)) - - #directly add a path to the older pandas version - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features - - - logger.info("Extracting presto features") - PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" - output = get_presto_features(cube, PRESTO_PATH) - - - return output - - - - - - - - - - - - - - - - diff --git a/minimal_wc_presto/udf_worldcereal_inference.py b/minimal_wc_presto/udf_worldcereal_inference.py deleted file mode 100644 index 224249e6..00000000 --- a/minimal_wc_presto/udf_worldcereal_inference.py +++ /dev/null @@ -1,91 +0,0 @@ -import logging -import urllib.request -import shutil -from pathlib import Path -import sys -import functools -import xarray as xr -from typing import Dict - - - -def _setup_logging(): - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - return logger - -@functools.lru_cache(maxsize=6) -def extract_dependencies(base_url: str, dependency_name: str): - - # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / 'dependencies' - - # Create the directory if it doesn't exist - dependencies_dir.mkdir(exist_ok=True, parents=True) - - - # Download and extract the model file - modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve(modelfile_url, filename=dependencies_dir / Path(modelfile_url).name) - shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - - # Add the model directory to system path if it's not already there - abs_path = str(dependencies_dir / Path(modelfile_url).name.split('.zip')[0]) - - return(abs_path) - - -def apply_datacube(cube: xr.DataArray, context:Dict) -> xr.DataArray: - - logger = _setup_logging() - - # shape and indiches for output - cube = cube.transpose('bands', 't', 'x', 'y') - cube = cube.fillna(65535) - - - # Unzip de dependencies on the backend - logger.info("Unzipping dependencies") - base_url = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" - dependency_name = "wc_presto_onnx_dependencies.zip" - - logger.info("Adding dependencies") - dep_dir = extract_dependencies(base_url, dependency_name) - sys.path.append(str(dep_dir)) - - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features, classify_with_catboost - - # Run presto inference - PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" - CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" - - # Run presto feature extraction - logger.info("Extracting presto features") - features = get_presto_features(cube, PRESTO_PATH) - - # Run catboost classification - logger.info("Catboost classification") - classification = classify_with_catboost(features, CATBOOST_PATH) - logger.info("Done") - - # Add time dimension - classification = classification.expand_dims(dim="t") - logger.info("Done") - - return classification - - - - - - - - - - - - - - - - diff --git a/scripts/inference/cropland_mapping.py b/scripts/inference/cropland_mapping.py index 6066b092..d3d44c2f 100644 --- a/scripts/inference/cropland_mapping.py +++ b/scripts/inference/cropland_mapping.py @@ -1,21 +1,19 @@ """Cropland mapping inference script, demonstrating the use of the GFMAP, Presto and WorldCereal classifiers in a first inference pipeline.""" import argparse +from pathlib import Path +import openeo from openeo_gfmap import BoundingBoxExtent, TemporalContext -from openeo_gfmap.backend import Backend, BackendContext, cdse_connection -from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor +from openeo_gfmap.backend import Backend, BackendContext +from openeo_gfmap.features.feature_extractor import apply_feature_extractor +from openeo_gfmap.inference.model_inference import apply_model_inference +from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor +from worldcereal.openeo.inference import CroplandClassifier from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap - -class PrestoFeatureExtractor(PatchFeatureExtractor): - def __init__(self): - pass - - def extract(self, image): - pass - +ONNX_DEPS_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openeo/onnx_dependencies_1.16.3.zip" if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -27,12 +25,20 @@ def extract(self, image): parser.add_argument("miny", type=float, help="Minimum Y coordinate (south)") parser.add_argument("maxx", type=float, help="Maximum X coordinate (east)") parser.add_argument("maxy", type=float, help="Maximum Y coordinate (north)") + parser.add_argument( + "--epsg", + type=int, + default=4326, + help="EPSG code for coordiante reference system.", + ) parser.add_argument( "start_date", type=str, help="Starting date for data extraction." ) parser.add_argument("end_date", type=str, help="Ending date for data extraction.") parser.add_argument( - "output_folder", type=str, help="Path to folder where to save results." + "output_path", + type=Path, + help="Path to folder where to save the resulting NetCDF.", ) args = parser.parse_args() @@ -41,29 +47,70 @@ def extract(self, image): miny = args.miny maxx = args.maxx maxy = args.maxy + epsg = args.epsg start_date = args.start_date end_date = args.end_date - spatial_extent = BoundingBoxExtent(minx, miny, maxx, maxy) + spatial_extent = BoundingBoxExtent(minx, miny, maxx, maxy, epsg) temporal_extent = TemporalContext(start_date, end_date) - backend = BackendContext(Backend.CDSE) + backend_context = BackendContext(Backend.FED) + + connection = openeo.connect( + "https://openeo.creo.vito.be/openeo/" + ).authenticate_oidc() # Preparing the input cube for the inference - input_cube = worldcereal_preprocessed_inputs_gfmap( - connection=cdse_connection(), - backend_context=backend, + inputs = worldcereal_preprocessed_inputs_gfmap( + connection=connection, + backend_context=backend_context, spatial_extent=spatial_extent, temporal_extent=temporal_extent, ) - # Start the job and download - job = input_cube.create_job( - title=f"Cropland inference BBOX: {minx} {miny} {maxx} {maxy}", - description="Cropland inference using WorldCereal, Presto and GFMAP classifiers", - out_format="NetCDF", + # Test feature computer + presto_parameters = { + "rescale_s1": False, # Will be done in the Presto UDF itself! + } + + features = apply_feature_extractor( + feature_extractor_class=PrestoFeatureExtractor, + cube=inputs, + parameters=presto_parameters, + size=[ + {"dimension": "x", "unit": "px", "value": 100}, + {"dimension": "y", "unit": "px", "value": 100}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 0}, + {"dimension": "y", "unit": "px", "value": 0}, + ], + ) + + catboost_parameters = {} + + classes = apply_model_inference( + model_inference_class=CroplandClassifier, + cube=features, + parameters=catboost_parameters, + size=[ + {"dimension": "x", "unit": "px", "value": 100}, + {"dimension": "y", "unit": "px", "value": 100}, + {"dimension": "t", "value": "P1D"}, + ], + overlap=[ + {"dimension": "x", "unit": "px", "value": 0}, + {"dimension": "y", "unit": "px", "value": 0}, + ], ) - job.start_and_wait() - job.get_results().download_files(args.output_folder) + classes.execute_batch( + outputfile=args.output_path, + out_format="NetCDF", + job_options={ + "driver-memory": "4g", + "executor-memoryOverhead": "8g", + "udf-dependency-archives": [f"{ONNX_DEPS_URL}#onnx_deps"], + }, + ) diff --git a/src/worldcereal/openeo/feature_extractor.py b/src/worldcereal/openeo/feature_extractor.py new file mode 100644 index 00000000..9266d26a --- /dev/null +++ b/src/worldcereal/openeo/feature_extractor.py @@ -0,0 +1,118 @@ +"""Feature computer GFMAP compatible to compute Presto embeddings.""" + +import xarray as xr +from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor + + +class PrestoFeatureExtractor(PatchFeatureExtractor): + """Feature extractor to use Presto model to compute embeddings. + This will generate a datacube with 128 bands, each band representing a + feature from the Presto model. + """ + + import functools + from pathlib import Path + from typing import Tuple + + PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA + BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA + DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + + GFMAP_BAND_MAPPING = { + "S2-L2A-B02": "B02", + "S2-L2A-B03": "B03", + "S2-L2A-B04": "B04", + "S2-L2A-B05": "B05", + "S2-L2A-B06": "B06", + "S2-L2A-B07": "B07", + "S2-L2A-B08": "B08", + "S2-L2A-B8A": "B8A", + "S2-L2A-B11": "B11", + "S2-L2A-B12": "B12", + "S1-SIGMA0-VH": "VH", + "S1-SIGMA0-VV": "VV", + "COP-DEM": "DEM", + "A5-tmean": "temperature-mean", + "A5-precip": "precipitation-flux", + } + + def __init__(self): + """ + Initializes the PrestoFeatureExtractor object, starting a logger. + """ + import logging + + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(PrestoFeatureExtractor.__name__) + + @classmethod + @functools.lru_cache(maxsize=6) + def extract_dependencies(cls, base_url: str, dependency_name: str): + """Extract the dependencies from the given URL. Unpacking a zip + file in the current working directory. + """ + import shutil + import urllib.request + from pathlib import Path + + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / "dependencies" + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve( + modelfile_url, filename=dependencies_dir / Path(modelfile_url).name + ) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str( + dependencies_dir / Path(modelfile_url).name.split(".zip")[0] + ) # NOQA + + return abs_path + + def output_labels(self) -> list: + """Returns the output labels from this UDF, which is the output labels + of the presto embeddings""" + return [f"presto_ft_{i}" for i in range(128)] + + def execute(self, inarr: xr.DataArray) -> xr.DataArray: + import sys + + if self.epsg is None: + raise ValueError( + "EPSG code is required for Presto feature extraction, but was " + "not correctly initialized." + ) + + # The below is required to avoid flipping of the result + # when running on OpenEO backend! + inarr = inarr.transpose("bands", "t", "x", "y") + + # Change the band names + new_band_names = [ + self.GFMAP_BAND_MAPPING.get(b.item(), b.item()) for b in inarr.bands + ] + inarr = inarr.assign_coords(bands=new_band_names) + + # Handle NaN values in Presto compatible way + inarr = inarr.fillna(65535) + + # Unzip de dependencies on the backend + self.logger.info("Unzipping dependencies") + deps_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + + self.logger.info("Appending dependencies") + sys.path.append(str(deps_dir)) + + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import ( + get_presto_features, + ) + + self.logger.info("Extracting presto features") + features = get_presto_features(inarr, self.PRESTO_PATH, self.epsg) + return features diff --git a/src/worldcereal/openeo/feature_udf.py b/src/worldcereal/openeo/feature_udf.py deleted file mode 100644 index 4ec815d9..00000000 --- a/src/worldcereal/openeo/feature_udf.py +++ /dev/null @@ -1,184 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -from typing import Dict - -import numpy as np -import pandas as pd -import xarray as xr -from openeo.udf import XarrayDataCube -from satio.collections import XArrayTrainingCollection - -from worldcereal.features.settings import ( - get_cropland_features_meta, - get_default_rsi_meta, -) -from worldcereal.fp import L2AFeaturesProcessor - -sys.path.append("/data/users/Public/driesj/openeo/deps/satio") -sys.path.append("/data/users/Public/driesj/openeo/deps/wc-classification/src") -# sys.path.insert(0,'/data/users/Public/driesj/openeo/deps/tf230') - -wheels = [ - "loguru-0.5.3-py3-none-any.whl", - "aiocontextvars-0.2.2-py2.py3-none-any.whl", - "contextvars-2.4", - "immutables-0.14-cp36-cp36m-manylinux1_x86_64.whl", - "importlib_resources-3.3.0-py2.py3-none-any.whl", -] -for wheel in wheels: - sys.path.append("/data/users/Public/driesj/openeo/deps/" + wheel) - - -classifier_file = "/tmp/worldcereal_croplandextent_lpis_unet.h5" - - -features_meta = get_cropland_features_meta() - - -class L2AFeaturesProcessor10m(L2AFeaturesProcessor): - L2A_BANDS_10M = [ - "B02", - "B03", - "B04", - "B08", - "B05", - "B06", - "B07", - "B8A", - "B11", - "B12", - "SCL", - "sunAzimuthAngles", - "sunZenithAngles", - "viewAzimuthMean", - "viewZenithMean", - ] - L2A_BANDS_DICT_ALL_10M = {10: L2A_BANDS_10M, 20: {"DUMMY"}} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @property - def supported_bands(self): - return L2AFeaturesProcessor10m.L2A_BANDS_DICT_ALL_10M - - -def apply_datacube(cube: XarrayDataCube, context: Dict) -> XarrayDataCube: - """ - This UDF computes WorldCereal features using SatIO. - It works on a spatiotemporal stack for one specific sensor, - currently Sentinel-2 - - @param cube: - @param context: A context dictionary, has to contain 'satio_settings' - @return: - """ - # access the underlying xarray - inarr = cube.get_array() - - # translate openEO dim name into satio convention - inarr = inarr.rename({"t": "timestamp"}) - # satio expects uint16! - inarr = inarr.astype(np.uint16) - - settings = context["satio_settings"] - settings["OPTICAL"]["composite"]["start"] = np.datetime_as_string( - inarr.coords["timestamp"].values.min(), unit="D" - ) - settings["OPTICAL"]["composite"]["end"] = np.datetime_as_string( - inarr.coords["timestamp"].values.max(), unit="D" - ) - - classify = context["classify"] - - collection = XArrayTrainingCollection( - sensor="S2", processing_level="L2A", df=pd.DataFrame(), array=inarr - ) - - from satio.rsindices import RSI_META_S2 - - default_rsi_meta = RSI_META_S2.copy() - rsi_meta = get_default_rsi_meta()["OPTICAL"] - - # in openEO, all bands are provided in 10m for now - # so we need to modify satio defaults - rsi_meta["brightness"] = default_rsi_meta["brightness"] - rsi_meta["brightness"]["native_res"] = 10 - - if "sen2agri_temp_feat" in features_meta.get("OPTICAL", {}): - features_meta["OPTICAL"]["sen2agri_temp_feat"]["parameters"][ - "time_start" - ] = settings["OPTICAL"]["composite"]["start"] - - processor = L2AFeaturesProcessor10m( - collection, - settings["OPTICAL"], - rsi_meta=rsi_meta, - features_meta=features_meta["OPTICAL"], - ) - features = processor.compute_features() - - # Extracted core from worldcereal ClassificationProcessor, - # to be seen what we need to keep - - if classify: - windowsize = 64 - import tensorflow as tf - - # from worldcereal.classification.models import WorldCerealUNET - # unetmodel = WorldCerealUNET(windowsize=64, features= 60) - # unetmodel.model.load_weights(classifier_file) - # classifier = unetmodel.model - classifier = tf.keras.models.load_model(classifier_file) - - xdim = features.data.shape[1] - ydim = features.data.shape[2] - - prediction = np.empty((xdim, ydim)) - - # can be avoided by using openEO apply_neighbourhood - for xStart in range(0, xdim, windowsize): - for yStart in range(0, ydim, windowsize): - # We need to check if we're at the end of the master image - # We have to make sure we have a full subtile - # so we need to expand such tile and the resulting overlap - # with previous subtile is not an issue - if xStart + windowsize > xdim: - xStart = xdim - windowsize - xEnd = xdim - else: - xEnd = xStart + windowsize - if yStart + windowsize > ydim: - yStart = ydim - windowsize - yEnd = ydim - else: - yEnd = yStart + windowsize - - features_patch = features.data[:, xStart:xEnd, yStart:yEnd] - patchprediction = ( - classifier.predict( - features_patch.transpose((1, 2, 0)).reshape( - (1, windowsize * windowsize, -1) - ) - ) - .squeeze() - .reshape((windowsize, windowsize)) - ) - - prediction[xStart:xEnd, yStart:yEnd] = patchprediction - - prediction_xarray = xr.DataArray(prediction.astype(np.float32), dims=["x", "y"]) - - # wrap back to datacube and return - return XarrayDataCube(prediction_xarray) - - else: - features_xarray = xr.DataArray( - features.data.astype(np.float32), - dims=["bands", "x", "y"], - coords={"bands": features.names}, - ) - - # wrap back to datacube and return - return XarrayDataCube(features_xarray) - return XarrayDataCube(features_xarray) diff --git a/src/worldcereal/openeo/inference.py b/src/worldcereal/openeo/inference.py new file mode 100644 index 00000000..ecbbb096 --- /dev/null +++ b/src/worldcereal/openeo/inference.py @@ -0,0 +1,70 @@ +"""Model inference on Presto feature for binary classication""" + +import xarray as xr +from openeo_gfmap.inference.model_inference import ModelInference + + +class CroplandClassifier(ModelInference): + import functools + + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA + BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA + DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + + def __init__(self): + import logging + + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(WorldCerealInference.__name__) + + @classmethod + @functools.lru_cache(maxsize=6) + def extract_dependencies(cls, base_url: str, dependency_name: str): + import shutil + import urllib.request + from pathlib import Path + + # Generate absolute path for the dependencies folder + dependencies_dir = Path.cwd() / "dependencies" + + # Create the directory if it doesn't exist + dependencies_dir.mkdir(exist_ok=True, parents=True) + + # Download and extract the model file + modelfile_url = f"{base_url}/{dependency_name}" + modelfile, _ = urllib.request.urlretrieve( + modelfile_url, filename=dependencies_dir / Path(modelfile_url).name + ) + shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) + + # Add the model directory to system path if it's not already there + abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) + + return abs_path + + def output_labels(self) -> list: + return ["classification"] + + def execute(self, inarr: xr.DataArray) -> xr.DataArray: + import sys + + # shape and indiches for output + inarr = inarr.transpose("bands", "x", "y") + + # Unzip de dependencies on the backend + self.logger.info("Unzipping dependencies") + dep_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + + self.logger.info("Adding dependencies") + sys.path.append(str(dep_dir)) + + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import ( + classify_with_catboost, + ) + + # Run catboost classification + self.logger.info("Catboost classification") + classification = classify_with_catboost(inarr, self.CATBOOST_PATH) + self.logger.info("Done") + + return classification From 3251919ef10443733a64631c15ad6676a812ded3 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Fri, 31 May 2024 16:32:47 +0200 Subject: [PATCH 26/31] Fixed conflicts --- scripts/inference/cropland_mapping.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/scripts/inference/cropland_mapping.py b/scripts/inference/cropland_mapping.py index f7573e8f..d2ac1d49 100644 --- a/scripts/inference/cropland_mapping.py +++ b/scripts/inference/cropland_mapping.py @@ -2,11 +2,6 @@ import argparse from pathlib import Path -<<<<<<< HEAD -======= - -import openeo ->>>>>>> 5ed426bcaf149c7a5dcc97061ad5caf2b2f39d69 import openeo from openeo_gfmap import BoundingBoxExtent, TemporalContext @@ -20,10 +15,6 @@ from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor from worldcereal.openeo.inference import CroplandClassifier -<<<<<<< HEAD -======= - ->>>>>>> 5ed426bcaf149c7a5dcc97061ad5caf2b2f39d69 ONNX_DEPS_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openeo/onnx_dependencies_1.16.3.zip" if __name__ == "__main__": @@ -48,13 +39,9 @@ ) parser.add_argument("end_date", type=str, help="Ending date for data extraction.") parser.add_argument( -<<<<<<< HEAD "output_path", type=Path, help="Path to folder where to save the resulting NetCDF.", -======= - "output_path", type=Path, help="Path to folder where to save the resulting NetCDF." ->>>>>>> 5ed426bcaf149c7a5dcc97061ad5caf2b2f39d69 ) args = parser.parse_args() From 7b7ca4d95e3c0b5f1ce230be9b20048d95510de2 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Mon, 3 Jun 2024 16:18:18 +0200 Subject: [PATCH 27/31] Implemented changed request by kristof --- minimal_wc_presto/ONNX_conversion.py | 91 -- .../backend_inference_example_openeo.ipynb | 414 --------- minimal_wc_presto/mvp_wc_presto/__init__.py | 0 minimal_wc_presto/mvp_wc_presto/dataops.py | 165 ---- minimal_wc_presto/mvp_wc_presto/dataset.py | 385 -------- minimal_wc_presto/mvp_wc_presto/masking.py | 149 --- minimal_wc_presto/mvp_wc_presto/presto.py | 873 ------------------ minimal_wc_presto/mvp_wc_presto/utils.py | 162 ---- .../mvp_wc_presto/world_cereal_inference.py | 396 -------- minimal_wc_presto/test_cropland_gfmap.py | 96 -- scripts/inference/cropland_mapping.py | 3 - src/worldcereal/openeo/feature_extractor.py | 73 +- src/worldcereal/openeo/inference.py | 48 +- src/worldcereal/openeo/preprocessing.py | 40 +- 14 files changed, 75 insertions(+), 2820 deletions(-) delete mode 100644 minimal_wc_presto/ONNX_conversion.py delete mode 100644 minimal_wc_presto/backend_inference_example_openeo.ipynb delete mode 100644 minimal_wc_presto/mvp_wc_presto/__init__.py delete mode 100644 minimal_wc_presto/mvp_wc_presto/dataops.py delete mode 100644 minimal_wc_presto/mvp_wc_presto/dataset.py delete mode 100644 minimal_wc_presto/mvp_wc_presto/masking.py delete mode 100644 minimal_wc_presto/mvp_wc_presto/presto.py delete mode 100644 minimal_wc_presto/mvp_wc_presto/utils.py delete mode 100644 minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py delete mode 100644 minimal_wc_presto/test_cropland_gfmap.py diff --git a/minimal_wc_presto/ONNX_conversion.py b/minimal_wc_presto/ONNX_conversion.py deleted file mode 100644 index 5821f963..00000000 --- a/minimal_wc_presto/ONNX_conversion.py +++ /dev/null @@ -1,91 +0,0 @@ -#%% Catboost -import catboost -from catboost.utils import convert_to_onnx_object -import onnx - -# Load your CatBoost model -model = catboost.CatBoost() -model.load_model('./model/catboost.cbm') - -onnx_model = convert_to_onnx_object(model) -onnx.save(onnx_model, './model/wc_catboost.onnx') - - - - - -#%% For the pytorch model we need to know the input shape - -import torch -from presto.presto import Presto -from model_class import PrestoFeatureExtractor -import xarray as xr -import numpy as np - -#load the data -ds = xr.open_dataset("./data/belgium_good_2020-12-01_2021-11-30.nc", engine='netcdf4') -arr = ds.drop('crs').to_array(dim='bands') - - -# Load the Presto model -PRESTO_PATH = './model/presto.pt' -presto_model = Presto.load_pretrained(model_path=PRESTO_PATH, strict=False) -presto_extractor = PrestoFeatureExtractor(presto_model) - -#get the required presto input through the feature extractor -input = presto_extractor.create_presto_input(arr) - -x_sample = torch.tensor(np.expand_dims(input[0][0], axis=0), dtype=torch.float32) # Shape matches the shape of eo data in your DataLoader -dw_sample = torch.tensor(np.expand_dims(input[1][0], axis=0), dtype=torch.long) # Shape matches the shape of dynamic_world data in your DataLoader -month_sample = torch.tensor(np.expand_dims(input[2][0], axis = 0), dtype=torch.long) # Shape matches the shape of months data in your DataLoader -latlons_sample = torch.tensor(np.expand_dims(input[3][0], axis = 0), dtype=torch.float32) # Shape matches the shape of latlons data in your DataLoader -mask_sample = torch.tensor(np.expand_dims(input[4][0], axis = 0), dtype=torch.int) - -encoder_model = presto_model.encoder - - - -with torch.no_grad(): - encoder_output = encoder_model( - x_sample, # Add batch dimension - dynamic_world=dw_sample, # Add batch dimension - mask=mask_sample, # Add batch dimension - latlons=latlons_sample, # Add batch dimension - month=month_sample # Add batch dimension - ) - - #%% - -# Export the encoder model to ONNX -torch.onnx.export( - encoder_model, - (x_sample, dw_sample, latlons_sample,mask_sample, month_sample), - './model/wc_presto.onnx', - input_names=["x", "dynamic_world", "latlons", "mask", "month"], - output_names=["output"], - dynamic_axes={ - "x": {0: "batch_size"}, - "dynamic_world": {0: "batch_size"}, - "mask": {0: "batch_size"}, - "latlons": {0: "batch_size"}, - "month": {0: "batch_size"}, - "output": {0: "batch_size"} - } -) -#%% -# Export the model to ONNX -torch.onnx.export( - encoder_model, - (x_sample, dw_sample, latlons_sample, month_sample, mask_sample), - './model/wc_presto.onnx', - input_names=["x", "dynamic_world", "latlons", "month", "mask"], - output_names=["output"], - dynamic_axes={ - "x": {0: "batch_size"}, - "dynamic_world": {0: "batch_size"}, - "mask": {0: "batch_size"}, - "latlons": {0: "batch_size"}, - "month": {0: "batch_size"}, - "output": {0: "batch_size"} - } -) \ No newline at end of file diff --git a/minimal_wc_presto/backend_inference_example_openeo.ipynb b/minimal_wc_presto/backend_inference_example_openeo.ipynb deleted file mode 100644 index 4a3f7af8..00000000 --- a/minimal_wc_presto/backend_inference_example_openeo.ipynb +++ /dev/null @@ -1,414 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "b879f7b4-9a3f-41fc-90d0-ab9cfd25a093", - "metadata": {}, - "source": [ - "### Make OpenEO connection" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "7c7532bf-5341-4a6e-a81f-85ded18e6a85", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "f700773b-a843-4ebe-b6ca-8f805b4ee5bf", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Authenticated using refresh token.\n" - ] - } - ], - "source": [ - "import openeo\n", - "from datetime import datetime \n", - "\n", - "#token SENTINEL\n", - "connection = openeo.connect(\"https://openeo.dataspace.copernicus.eu/\").authenticate_oidc()" - ] - }, - { - "cell_type": "markdown", - "id": "5af70a06", - "metadata": {}, - "source": [ - "Load in Cube without METEO\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "5494c46d", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "#Get desired data\n", - "from preprocessing import worldcereal_preprocessed_inputs\n", - "\n", - "EXTENT = dict(zip([\"west\", \"south\", \"east\", \"north\"], [664000.0, 5611120.0, 665000.0, 5612120.0]))\n", - "EXTENT['crs'] = \"EPSG:32631\"\n", - "EXTENT['srs'] = \"EPSG:32631\"\n", - "\n", - "STARTDATE = '2020-11-01'\n", - "ENDDATE = '2021-10-31'\n", - "\n", - "#TODO aggregator for including METEO?\n", - "\n", - "input_cube = worldcereal_preprocessed_inputs(\n", - " connection = connection,\n", - " bbox = EXTENT,\n", - " start = STARTDATE,\n", - " end = ENDDATE,\n", - " METEO_collection=None,\n", - " S2_collection= \"SENTINEL2_L2A\",\n", - " S1_collection= \"SENTINEL1_GRD\",\n", - " DEM_collection= \"COPERNICUS_30\"\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "id": "da8d05cd", - "metadata": {}, - "source": [ - "## Save preprocessed inputs\n", - "\n", - "Only required if you want to save the intermediate input cube" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4aab5695", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "current_datetime = datetime.now()\n", - "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_input_cube_worldCereal.nc'\n", - "\n", - "input_cube.execute_batch(outputfile = outputfile_name,\n", - " description='world cereal data collection')" - ] - }, - { - "cell_type": "markdown", - "id": "bc85fadd", - "metadata": {}, - "source": [ - "## Run end-to-end inference job\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "64d37c40", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'j-240529fb722145acadced18905706e6e': send 'start'\n", - "0:00:15 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", - "0:00:20 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", - "0:00:27 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", - "0:00:35 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", - "0:00:45 Job 'j-240529fb722145acadced18905706e6e': created (progress 0%)\n", - "0:00:57 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:01:12 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:01:32 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:01:56 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:02:26 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:03:04 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:03:50 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:04:49 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:05:49 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:06:49 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:07:50 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:08:50 Job 'j-240529fb722145acadced18905706e6e': running (progress N/A)\n", - "0:09:50 Job 'j-240529fb722145acadced18905706e6e': finished (progress 100%)\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from datetime import datetime\n", - "\n", - "current_datetime = datetime.now()\n", - "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_worldcereal.nc'\n", - "\n", - "udf = openeo.UDF.from_file(\"udf_worldcereal_inference.py\")\n", - "\n", - "prediction = input_cube.apply_neighborhood(\n", - " process=udf,\n", - " size=[\n", - " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", - " ],\n", - " overlap=[\n", - " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", - " ],\n", - ")\n", - "\n", - "prediction = prediction.drop_dimension('t').rename_labels(\"bands\", [\"classification\"])\n", - "\n", - "prediction.execute_batch(outputfile = outputfile_name,\n", - " description='world cereal inference',\n", - " job_options={'driver-memory': '4g',\n", - " 'executor-memoryOverhead':'8g'} )" - ] - }, - { - "cell_type": "markdown", - "id": "1f716b7a", - "metadata": {}, - "source": [ - "Fetch the output and visualise" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "2cf64980", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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0MaNGyVJ3/72t2OOb9q0STfffLMk6YknnlB2drYWL16szs5OzZ8/X0899VRCBgsASB9xBZBzrt9z8vLytGHDBm3YsGHQgwIApD/2ggMAmEiJj2Poy1D6OPR80B8+BRVIHiogAIAJAggAYIIAAgCYSPkeUE/d1+x79njo+QDAwPV3L+W53lO/cqclHen39amAAAAmCCAAgIm0W4LrS3+X1LJEB3hb979RLpH/n+F670r0z6ECAgCYIIAAACYIIACAibTrAdHHAVLLYP9m+7tEONVlwnsZFRAAwAQBBAAwQQABAEykXQ9oKPraxgeZiftOBoa/l4FhnmJRAQEATBBAAAATBBAAwETa9YD6WqdP9/sGkFyZ9t9PqvUrhuvfJ9XmpadEzstQ54IKCABgggACAJjw7BLcdZOnKSdrpKThK6XTfUkF6S/Vl4e8yItz6pX3qqHeukIFBAAwQQABAEwQQAAAE57tAW1vOajCAvIR8GIPIt1YzLFX+jiWeIcHAJgggAAAJgggAIAJz/aAut8H1F0y10372nqfdXj0998e/414C30d76MCAgCYIIAAACaynHPOehDdhcNh+f1+fdpyYdIvw45nKx6WVwBILLMNRLgjojGTj6i9vV2FhYXnPI8KCABgggACAJgggAAAJjx7GbYF+jxAZqKvY4MKCABgggACAJgggAAAJugBAcg49Hy8gQoIAGCCAAIAmGAJDkBaYFkt9VABAQBMEEAAABMEEADABAEEADBBAAEATBBAAAATBBAAwAQBBAAwQQABAEwQQAAAEwQQAMCEZ/eCu27yNOVkjZQ0+D2een7ENntFAemLv/fUQwUEADBBAAEATHh2CW57y0EVFgwtHynBAcC7qIAAACYIIACAiSEF0Nq1a5WVlaXa2trosS+//FI1NTUqKirSqFGjtHjxYoVCoaGOEwCQZgYdQO+++65++9vf6tJLL405vnz5cr3++uvatm2bdu3apePHj+v6668f8kABAOllUAH02WefacmSJXr22Wc1ZsyY6PH29nY999xzevzxx3XVVVdpxowZ2rRpk9555x3t2bPnrK/V2dmpcDgc8wAApL9BBVBNTY0WLFigqqqqmONNTU06ffp0zPGKigqVlZWpsbHxrK9VX18vv98ffZSWlg5mSACAFBN3AG3dulXvvfee6uvrez0XDAaVm5ur0aNHxxwPBAIKBoNnfb26ujq1t7dHH62trfEOCQCQguK6D6i1tVXLli3Tzp07lZeXl5AB+Hw++Xy+QX9/z+03AGQG7vNLfXFVQE1NTTpx4oQuv/xy5eTkKCcnR7t27dL69euVk5OjQCCgU6dOqa2tLeb7QqGQiouLEzluAECKi6sCuvrqq3Xw4MGYY7fccosqKip0//33q7S0VCNHjlRDQ4MWL14sSWpubtaxY8dUWVmZuFEDAFJeXAFUUFCgqVOnxhz72te+pqKioujx2267TStWrNDYsWNVWFiou+++W5WVlbryyivjGlj33bABQGLZLd0kfC+4J554QtnZ2Vq8eLE6Ozs1f/58PfXUU4n+MQCAFJflnHPWg+guHA7L7/fr21pIBQQgBhVQagh3RDRm8hG1t7ersLDwnOexFxwAwIRnP44BAHrqftsF1VDqowICAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACQIIAGCCAAIAmCCAAAAm2IoHQErq+WnIbM2TeqiAAAAmCCAAgAkCCABgggACAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACbbiAZAW2Jon9VABAQBMEEAAABMEEADABD0gAGmpe0+IfpA3UQEBAEwQQAAAEwQQAMAEPSAAaa/nPUL9oWc0PKiAAAAmCCAAgAkCCABgggACAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACbbiAZDx2HrHBhUQAMAEAQQAMMESHICM13O3bJbkhgcVEADABAEEADBBAAEATNADAoAeuveE6AclDxUQAMAEAQQAMEEAAQBM0AMCgD5wj1DyUAEBAEwQQAAAEwQQAMAEAQQAMEEAAQBMEEAAABNchg0AceCy7MShAgIAmCCAAAAm4g6gjz76SDfddJOKioqUn5+vadOmaf/+/dHnnXNavXq1xo8fr/z8fFVVVenw4cMJHTQAIPXF1QP69NNPNXfuXH3nO9/Rn/70J33961/X4cOHNWbMmOg5jz32mNavX6/NmzervLxcq1at0vz58/XBBx8oLy9vwD9re8tBFRacyceea67J0n0td7h+JgBkqrgC6NFHH1Vpaak2bdoUPVZeXh793845rVu3Tg888IAWLlwoSXrhhRcUCAT0yiuv6MYbb+z1mp2dners7Ix+HQ6H4/4lAACpJ64luNdee00zZ87U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L495fsaMGRo5cmTMnDU3N+vYsWMZNWdXX321Dh48qAMHDkQfM2fO1JIlS6L/m3mS5s6d2+sy/paWFk2YMEGSVF5eruLi4ph5CofD2rt3b0bN0+eff97r0zxHjBihSCQiiXlKCOurIM5m69atzufzueeff9598MEH7vbbb3ejR492wWDQemgm7rzzTuf3+92f//xn9/HHH0cfn3/+efScO+64w5WVlbm33nrL7d+/31VWVrrKykrDUXtD96vgnGOenDtziXpOTo5bs2aNO3z4sHvxxRfdeeed537/+99Hz1m7dq0bPXq0e/XVV91f//pXt3Dhwoy7vLi6utp94xvfiF6G/cc//tGdf/757r777ouewzwNjScDyDnnnnzySVdWVuZyc3PdrFmz3J49e6yHZEbSWR+bNm2KnvPFF1+4u+66y40ZM8add9557rrrrnMff/yx3aA9omcAMU9nvP76627q1KnO5/O5iooK98wzz8Q8H4lE3KpVq1wgEHA+n89dffXVrrm52Wi0NsLhsFu2bJkrKytzeXl57sILL3S/+MUvXGdnZ/Qc5mlo+DwgAIAJz/WAAACZgQACAJgggAAAJgggAIAJAggAYIIAAgCYIIAAACYIIACACQIIAGCCAAIAmCCAAAAm/g+YP2/ngKIe6gAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import xarray as xr\n", - "import matplotlib.pyplot as plt\n", - "\n", - "output = xr.open_dataset(outputfile_name)\n", - "plt.imshow(output['classification'])\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "a1f68e9d", - "metadata": {}, - "source": [ - "## Run the Presto UDF" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "20ae2b17", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0:00:00 Job 'j-240529f3a2b540d583b08b62429bb60b': send 'start'\n", - "0:00:14 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", - "0:00:19 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", - "0:00:25 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", - "0:00:34 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", - "0:00:44 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", - "0:01:06 Job 'j-240529f3a2b540d583b08b62429bb60b': created (progress 0%)\n", - "0:01:23 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:01:42 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:02:06 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:02:36 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:03:14 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:04:00 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:04:59 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:05:59 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:06:59 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:08:00 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:09:00 Job 'j-240529f3a2b540d583b08b62429bb60b': running (progress N/A)\n", - "0:10:00 Job 'j-240529f3a2b540d583b08b62429bb60b': finished (progress 100%)\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from datetime import datetime\n", - "\n", - "current_datetime = datetime.now()\n", - "formatted_datetime = current_datetime.strftime(\"%Y_%m_%d_%H_%M_%S\")\n", - "outputfile_name = str(formatted_datetime) + '_output_presto.nc'\n", - "\n", - "udf = openeo.UDF.from_file(\"udf_presto.py\")\n", - "\n", - "prediction = input_cube.apply_neighborhood(\n", - " process=udf,\n", - " size=[\n", - " {\"dimension\": \"x\", \"value\": 100, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 100, \"unit\": \"px\"},\n", - " ],\n", - " overlap=[\n", - " {\"dimension\": \"x\", \"value\": 0, \"unit\": \"px\"},\n", - " {\"dimension\": \"y\", \"value\": 0, \"unit\": \"px\"},\n", - " ],\n", - ")\n", - "\n", - "ft_names = [f\"presto_ft_{i}\" for i in range(128)]\n", - "prediction = prediction.drop_dimension('t').rename_labels(\"bands\", ft_names)\n", - "\n", - "prediction.execute_batch(outputfile = outputfile_name,\n", - " description='world cereal inference',\n", - " job_options={'driver-memory': '4g',\n", - " 'executor-memoryOverhead':'8g'} )" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "7b9a580a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Size: 80kB\n", - "[10000 values with dtype=float64]\n", - "Coordinates:\n", - " * x (x) float64 800B 6.64e+05 6.64e+05 6.64e+05 ... 6.65e+05 6.65e+05\n", - " * y (y) float64 800B 5.612e+06 5.612e+06 ... 5.611e+06 5.611e+06\n", - "Attributes:\n", - " long_name: presto_ft_0\n", - " units: \n", - " grid_mapping: crs\n" - ] - } - ], - "source": [ - "presto = xr.open_dataset(outputfile_name)\n", - "print(presto['presto_ft_0'])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/minimal_wc_presto/mvp_wc_presto/__init__.py b/minimal_wc_presto/mvp_wc_presto/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/minimal_wc_presto/mvp_wc_presto/dataops.py b/minimal_wc_presto/mvp_wc_presto/dataops.py deleted file mode 100644 index fbc7e58c..00000000 --- a/minimal_wc_presto/mvp_wc_presto/dataops.py +++ /dev/null @@ -1,165 +0,0 @@ -# This file contains many of the constants -# defined in presto/dataops -import warnings -from collections import OrderedDict -from typing import List -from typing import OrderedDict as OrderedDictType - -import numpy as np -import torch - -""" -For easier normalization of the band values (instead of needing to recompute -the normalization dict with the addition of new data), we provide maximum -values for each band -""" -S1_BANDS = ["VV", "VH"] -# EarthEngine estimates Sentinel-1 values range from -50 to 1 -S1_SHIFT_VALUES = [25.0, 25.0] -S1_DIV_VALUES = [25.0, 25.0] -S2_BANDS = [ - "B1", - "B2", - "B3", - "B4", - "B5", - "B6", - "B7", - "B8", - "B8A", - "B9", - "B10", - "B11", - "B12", -] -S2_SHIFT_VALUES = [float(0.0)] * len(S2_BANDS) -S2_DIV_VALUES = [float(1e4)] * len(S2_BANDS) -ERA5_BANDS = ["temperature_2m", "total_precipitation"] -# for temperature, shift to celcius and then divide by 35 based on notebook (ranges from) -# 37 to -22 degrees celcius -# For rainfall, based on -# https://github.com/nasaharvest/lem/blob/main/notebooks/exploratory_data_analysis.ipynb -ERA5_SHIFT_VALUES = [-272.15, 0.0] -ERA5_DIV_VALUES = [35.0, 0.03] -SRTM_BANDS = ["elevation", "slope"] -# visually gauged 90th percentile from -# https://github.com/nasaharvest/lem/blob/main/notebooks/exploratory_data_analysis.ipynb -SRTM_SHIFT_VALUES = [0.0, 0.0] -SRTM_DIV_VALUES = [2000.0, 50.0] - -DYNAMIC_BANDS = S1_BANDS + S2_BANDS + ERA5_BANDS -STATIC_BANDS = SRTM_BANDS - -DYNAMIC_BANDS_SHIFT = S1_SHIFT_VALUES + S2_SHIFT_VALUES + ERA5_SHIFT_VALUES -DYNAMIC_BANDS_DIV = S1_DIV_VALUES + S2_DIV_VALUES + ERA5_DIV_VALUES - -STATIC_BANDS_SHIFT = SRTM_SHIFT_VALUES -STATIC_BANDS_DIV = SRTM_DIV_VALUES - -# These bands are what is created by the Engineer. If the engineer changes, the bands -# here will need to change (and vice versa) -REMOVED_BANDS = ["B1", "B10"] -RAW_BANDS = DYNAMIC_BANDS + STATIC_BANDS - -BANDS = [x for x in DYNAMIC_BANDS if x not in REMOVED_BANDS] + STATIC_BANDS + ["NDVI"] -# NDVI is between 0 and 1 -ADD_BY = ( - [DYNAMIC_BANDS_SHIFT[i] for i, x in enumerate(DYNAMIC_BANDS) if x not in REMOVED_BANDS] - + STATIC_BANDS_SHIFT - + [0.0] -) -DIVIDE_BY = ( - [DYNAMIC_BANDS_DIV[i] for i, x in enumerate(DYNAMIC_BANDS) if x not in REMOVED_BANDS] - + STATIC_BANDS_DIV - + [1.0] -) - -NUM_TIMESTEPS = 12 -NUM_ORG_BANDS = len(BANDS) -TIMESTEPS_IDX = list(range(NUM_TIMESTEPS)) - -NORMED_BANDS = [x for x in BANDS if x != "B9"] -NUM_BANDS = len(NORMED_BANDS) -BANDS_IDX = list(range(NUM_BANDS)) -BANDS_GROUPS_IDX: OrderedDictType[str, List[int]] = OrderedDict( - { - "S1": [NORMED_BANDS.index(b) for b in S1_BANDS], - "S2_RGB": [NORMED_BANDS.index(b) for b in ["B2", "B3", "B4"]], - "S2_Red_Edge": [NORMED_BANDS.index(b) for b in ["B5", "B6", "B7"]], - "S2_NIR_10m": [NORMED_BANDS.index(b) for b in ["B8"]], - "S2_NIR_20m": [NORMED_BANDS.index(b) for b in ["B8A"]], - "S2_SWIR": [NORMED_BANDS.index(b) for b in ["B11", "B12"]], # Include B10? - "ERA5": [NORMED_BANDS.index(b) for b in ERA5_BANDS], - "SRTM": [NORMED_BANDS.index(b) for b in SRTM_BANDS], - "NDVI": [NORMED_BANDS.index("NDVI")], - } -) - -BAND_EXPANSION = [len(x) for x in BANDS_GROUPS_IDX.values()] -SRTM_INDEX = list(BANDS_GROUPS_IDX.keys()).index("SRTM") - - -class DynamicWorld2020_2021: - class_amount = 9 - - @classmethod - def normalize(cls, x: np.ndarray) -> np.ndarray: - return x - - -class S1_S2_ERA5_SRTM: - @staticmethod - def calculate_ndvi(input_array): - r""" - Given an input array of shape [timestep, bands] or [batches, timesteps, shapes] - where bands == len(bands), returns an array of shape - [timestep, bands + 1] where the extra band is NDVI, - (b08 - b04) / (b08 + b04) - """ - band_1, band_2 = "B8", "B4" - - num_dims = len(input_array.shape) - if num_dims == 2: - band_1_np = input_array[:, NORMED_BANDS.index(band_1)] - band_2_np = input_array[:, NORMED_BANDS.index(band_2)] - elif num_dims == 3: - band_1_np = input_array[:, :, NORMED_BANDS.index(band_1)] - band_2_np = input_array[:, :, NORMED_BANDS.index(band_2)] - else: - raise ValueError(f"Expected num_dims to be 2 or 3 - got {num_dims}") - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="invalid value encountered in true_divide") - # suppress the following warning - # RuntimeWarning: invalid value encountered in true_divide - # for cases where near_infrared + red == 0 - # since this is handled in the where condition - if isinstance(band_1_np, np.ndarray): - return np.where( - (band_1_np + band_2_np) > 0, - (band_1_np - band_2_np) / (band_1_np + band_2_np), - 0, - ) - else: - return torch.where( - (band_1_np + band_2_np) > 0, - (band_1_np - band_2_np) / (band_1_np + band_2_np), - 0, - ) - - @classmethod - def normalize(cls, x): - # remove the b9 band - keep_indices = [idx for idx, val in enumerate(BANDS) if val != "B9"] - if isinstance(x, np.ndarray): - x = ((x + ADD_BY) / DIVIDE_BY).astype(np.float32) - else: - x = (x + torch.tensor(ADD_BY)) / torch.tensor(DIVIDE_BY) - - if len(x.shape) == 2: - x = x[:, keep_indices] - x[:, NORMED_BANDS.index("NDVI")] = cls.calculate_ndvi(x) - else: - x = x[:, :, keep_indices] - x[:, :, NORMED_BANDS.index("NDVI")] = cls.calculate_ndvi(x) - return x diff --git a/minimal_wc_presto/mvp_wc_presto/dataset.py b/minimal_wc_presto/mvp_wc_presto/dataset.py deleted file mode 100644 index a465f876..00000000 --- a/minimal_wc_presto/mvp_wc_presto/dataset.py +++ /dev/null @@ -1,385 +0,0 @@ -import logging -from datetime import datetime -from math import modf -from pathlib import Path -from random import sample -from typing import Callable, Dict, List, Optional, Tuple, cast - -import geopandas as gpd -import numpy as np -import pandas as pd -import rioxarray -import xarray as xr -from einops import rearrange, repeat -from pyproj import Transformer -from sklearn.utils.class_weight import compute_class_weight -from torch.utils.data import Dataset - -from .dataops import ( - BANDS, - BANDS_GROUPS_IDX, - NORMED_BANDS, - S1_S2_ERA5_SRTM, - DynamicWorld2020_2021, -) -from .masking import BAND_EXPANSION, MaskedExample, MaskParamsNoDw -from .utils import DEFAULT_SEED, data_dir, load_world_df - -logger = logging.getLogger("__main__") - -IDX_TO_BAND_GROUPS = {} -for band_group_idx, (key, val) in enumerate(BANDS_GROUPS_IDX.items()): - for idx in val: - IDX_TO_BAND_GROUPS[NORMED_BANDS[idx]] = band_group_idx - - -class WorldCerealBase(Dataset): - _NODATAVALUE = 65535 - NUM_TIMESTEPS = 12 - BAND_MAPPING = { - "OPTICAL-B02-ts{}-10m": "B2", - "OPTICAL-B03-ts{}-10m": "B3", - "OPTICAL-B04-ts{}-10m": "B4", - "OPTICAL-B05-ts{}-20m": "B5", - "OPTICAL-B06-ts{}-20m": "B6", - "OPTICAL-B07-ts{}-20m": "B7", - "OPTICAL-B08-ts{}-10m": "B8", - "OPTICAL-B8A-ts{}-20m": "B8A", - "OPTICAL-B11-ts{}-20m": "B11", - "OPTICAL-B12-ts{}-20m": "B12", - "SAR-VH-ts{}-20m": "VH", - "SAR-VV-ts{}-20m": "VV", - "METEO-precipitation_flux-ts{}-100m": "total_precipitation", - "METEO-temperature_mean-ts{}-100m": "temperature_2m", - } - STATIC_BAND_MAPPING = {"DEM-alt-20m": "elevation", "DEM-slo-20m": "slope"} - - def __init__(self, dataframe: pd.DataFrame): - self.df = dataframe - - def __len__(self): - return self.df.shape[0] - - @staticmethod - def target_crop(row_d: Dict) -> int: - # by default, we predict crop vs non crop - return int(row_d["LANDCOVER_LABEL"] == 11) - - @classmethod - def row_to_arrays( - cls, row: pd.Series, target_function: Callable[[Dict], int] - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float, int]: - # https://stackoverflow.com/questions/45783891/is-there-a-way-to-speed-up-the-pandas-getitem-getitem-axis-and-get-label - # This is faster than indexing the series every time! - row_d = pd.Series.to_dict(row) - - latlon = np.array([row_d["lat"], row_d["lon"]], dtype=np.float32) - month = datetime.strptime(row_d["start_date"], "%Y-%m-%d").month - 1 - - eo_data = np.zeros((cls.NUM_TIMESTEPS, len(BANDS))) - # an assumption we make here is that all timesteps for a token - # have the same masking - mask = np.zeros((cls.NUM_TIMESTEPS, len(BANDS_GROUPS_IDX))) - for df_val, presto_val in cls.BAND_MAPPING.items(): - values = np.array([float(row_d[df_val.format(t)]) for t in range(cls.NUM_TIMESTEPS)]) - # this occurs for the DEM values in one point in Fiji - values = np.nan_to_num(values, nan=cls._NODATAVALUE) - idx_valid = values != cls._NODATAVALUE - if presto_val in ["VV", "VH"]: - # convert to dB - idx_valid = idx_valid & (values > 0) - values[idx_valid] = 20 * np.log10(values[idx_valid]) - 83 - elif presto_val == "total_precipitation": - # scaling, and AgERA5 is in mm, Presto expects m - values[idx_valid] = values[idx_valid] / (100 * 1000.0) - elif presto_val == "temperature_2m": - # remove scaling - values[idx_valid] = values[idx_valid] / 100 - mask[:, IDX_TO_BAND_GROUPS[presto_val]] += ~idx_valid - eo_data[:, BANDS.index(presto_val)] = values - for df_val, presto_val in cls.STATIC_BAND_MAPPING.items(): - # this occurs for the DEM values in one point in Fiji - values = np.nan_to_num(row_d[df_val], nan=cls._NODATAVALUE) - idx_valid = values != cls._NODATAVALUE - eo_data[:, BANDS.index(presto_val)] = values - mask[:, IDX_TO_BAND_GROUPS[presto_val]] += ~idx_valid - - return ( - cls.check(eo_data), - mask.astype(bool), - latlon, - month, - target_function(row_d), - ) - - def __getitem__(self, idx): - raise NotImplementedError - - @classmethod - def normalize_and_mask(cls, eo: np.ndarray): - # TODO: this can be removed - keep_indices = [idx for idx, val in enumerate(BANDS) if val != "B9"] - normed_eo = S1_S2_ERA5_SRTM.normalize(eo) - # TODO: fix this. For now, we replicate the previous behaviour - normed_eo = np.where(eo[:, keep_indices] != cls._NODATAVALUE, normed_eo, 0) - return normed_eo - - @staticmethod - def check(array: np.ndarray) -> np.ndarray: - assert not np.isnan(array).any() - return array - - @staticmethod - def split_df( - df: pd.DataFrame, val_sample_ids: Optional[List[str]] = None, val_size: float = 0.2 - ) -> Tuple[pd.DataFrame, pd.DataFrame]: - if val_sample_ids is None: - logger.warning(f"No val_ids; randomly splitting {val_size} to the val set instead") - val, train = np.split( - df.sample(frac=1, random_state=DEFAULT_SEED), [int(val_size * len(df))] - ) - else: - is_val = df.sample_id.isin(val_sample_ids) - logger.info(f"Using {len(is_val) - sum(is_val)} train and {sum(is_val)} val samples") - train = df[~is_val] - val = df[is_val] - return train, val - - -class WorldCerealMaskedDataset(WorldCerealBase): - def __init__(self, dataframe: pd.DataFrame, mask_params: MaskParamsNoDw): - super().__init__(dataframe) - self.mask_params = mask_params - - def __getitem__(self, idx): - # Get the sample - row = self.df.iloc[idx, :] - eo, real_mask_per_token, latlon, month, _ = self.row_to_arrays(row, self.target_crop) - mask_eo, x_eo, y_eo, strat = self.mask_params.mask_data( - self.normalize_and_mask(eo), real_mask_per_token - ) - real_mask_per_variable = np.repeat(real_mask_per_token, BAND_EXPANSION, axis=1) - - dynamic_world = np.ones(self.NUM_TIMESTEPS) * (DynamicWorld2020_2021.class_amount) - mask_dw = np.full(self.NUM_TIMESTEPS, True) - y_dw = dynamic_world.copy() - return MaskedExample( - mask_eo, - mask_dw, - x_eo, - y_eo, - dynamic_world, - y_dw, - month, - latlon, - strat, - real_mask_per_variable, - ) - - -def filter_remove_noncrops(df: pd.DataFrame) -> pd.DataFrame: - crop_labels = [10, 11, 12, 13] - df = df.loc[df.LANDCOVER_LABEL.isin(crop_labels)] - return df - - -def target_maize(row_d) -> int: - # 1200 is maize - return int(row_d["CROPTYPE_LABEL"] == 1200) - - -class WorldCerealLabelledDataset(WorldCerealBase): - # 0: no information, 10: could be both annual or perennial - FILTER_LABELS = [0, 10] - - def __init__( - self, - dataframe: pd.DataFrame, - countries_to_remove: Optional[List[str]] = None, - years_to_remove: Optional[List[int]] = None, - target_function: Optional[Callable[[Dict], int]] = None, - balance: bool = False, - ): - dataframe = dataframe.loc[~dataframe.LANDCOVER_LABEL.isin(self.FILTER_LABELS)] - - if countries_to_remove is not None: - dataframe = self.join_with_world_df(dataframe) - for country in countries_to_remove: - assert dataframe.name.str.contains( - country - ).any(), f"Tried removing {country} but it is not in the dataframe" - dataframe = dataframe[(~dataframe.name.isin(countries_to_remove))] - if years_to_remove is not None: - dataframe["end_date"] = pd.to_datetime(dataframe.end_date) - dataframe = dataframe[(~dataframe.end_date.dt.year.isin(years_to_remove))] - self.target_function = target_function if target_function is not None else self.target_crop - self._class_weights: Optional[np.ndarray] = None - - super().__init__(dataframe) - if balance: - neg_indices, pos_indices = [], [] - for loc_idx, (_, row) in enumerate(self.df.iterrows()): - target = self.target_function(row.to_dict()) - if target == 0: - neg_indices.append(loc_idx) - else: - pos_indices.append(loc_idx) - if len(pos_indices) > len(neg_indices): - self.indices = pos_indices + (len(pos_indices) // len(neg_indices)) * neg_indices - elif len(neg_indices) > len(pos_indices): - self.indices = neg_indices + (len(neg_indices) // len(pos_indices)) * pos_indices - else: - self.indices = neg_indices + pos_indices - else: - self.indices = [i for i in range(len(self.df))] - - @staticmethod - def multiply_list_length_by_float(input_list: List, multiplier: float) -> List: - decimal_part, integer_part = modf(multiplier) - sublist = sample(input_list, k=int(len(input_list) * decimal_part)) - return input_list * int(integer_part) + sublist - - def __len__(self): - return len(self.indices) - - def __getitem__(self, idx): - # Get the sample - df_index = self.indices[idx] - row = self.df.iloc[df_index, :] - eo, mask_per_token, latlon, month, target = self.row_to_arrays(row, self.target_function) - mask_per_variable = np.repeat(mask_per_token, BAND_EXPANSION, axis=1) - return ( - self.normalize_and_mask(eo), - target, - np.ones(self.NUM_TIMESTEPS) * (DynamicWorld2020_2021.class_amount), - latlon, - month, - mask_per_variable, - ) - - @staticmethod - def join_with_world_df(dataframe: pd.DataFrame) -> pd.DataFrame: - world_df = load_world_df() - dataframe = gpd.GeoDataFrame( - data=dataframe, - geometry=gpd.GeoSeries.from_xy(x=dataframe.lon, y=dataframe.lat), - crs="EPSG:4326", - ) - # project to non geographic CRS, otherwise geopandas gives a warning - joined = gpd.sjoin_nearest( - dataframe.to_crs("EPSG:3857"), world_df.to_crs("EPSG:3857"), how="left" - ) - joined = joined[~joined.index.duplicated(keep="first")] - if joined.isna().any(axis=1).any(): - logger.warning("Some coordinates couldn't be matched to a country") - return joined.to_crs("EPSG:4326") - - @property - def class_weights(self) -> np.ndarray: - if self._class_weights is None: - ys = [] - for _, row in self.df.iterrows(): - ys.append(self.target_function(row.to_dict())) - self._class_weights = compute_class_weight( - class_weight="balanced", classes=np.unique(ys), y=ys - ) - return self._class_weights - - -class WorldCerealInferenceDataset(Dataset): - _NODATAVALUE = 65535 - Y = "worldcereal_cropland" - BAND_MAPPING = { - "B02": "B2", - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - # B8A is missing - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", - } - - def __init__(self, path_to_files: Path = data_dir / "inference_areas"): - self.path_to_files = path_to_files - self.all_files = list(self.path_to_files.glob("*.nc")) - - def __len__(self): - return len(self.all_files) - - @classmethod - def nc_to_arrays( - cls, filepath: Path - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - ds = cast(xr.Dataset, rioxarray.open_rasterio(filepath, decode_times=False)) - epsg_coords = ds.rio.crs.to_epsg() - - num_instances = len(ds.x) * len(ds.y) - num_timesteps = len(ds.t) - eo_data = np.zeros((num_instances, num_timesteps, len(BANDS))) - mask = np.zeros((num_instances, num_timesteps, len(BANDS_GROUPS_IDX))) - # for now, B8A is missing - mask[:, :, IDX_TO_BAND_GROUPS["B8A"]] = 1 - - for org_band, presto_val in cls.BAND_MAPPING.items(): - # flatten the values - values = np.swapaxes(ds[org_band].values.reshape((num_timesteps, -1)), 0, 1) - idx_valid = values != cls._NODATAVALUE - - if presto_val in ["VV", "VH"]: - # convert to dB - values = 20 * np.log10(values) - 83 - elif presto_val == "total_precipitation": - # scaling, and AgERA5 is in mm, Presto expects m - values = values / (100 * 1000.0) - elif presto_val == "temperature_2m": - # remove scaling - values = values / 100 - - eo_data[:, :, BANDS.index(presto_val)] = values - mask[:, :, IDX_TO_BAND_GROUPS[presto_val]] += ~idx_valid - - y = rearrange(ds[cls.Y].values, "t x y -> (x y) t") - # -1 because we index from 0 - start_month = (ds.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1) - 1 - months = np.ones((num_instances)) * start_month - - transformer = Transformer.from_crs(f"EPSG:{epsg_coords}", "EPSG:4326", always_xy=True) - lon, lat = transformer.transform(ds.x, ds.y) - - latlons = np.stack( - [np.repeat(lat, repeats=len(lon)), repeat(lon, "c -> (h c)", h=len(lat))], - axis=-1, - ) - - return eo_data, np.repeat(mask, BAND_EXPANSION, axis=-1), latlons, months, y - - def __getitem__(self, idx): - filepath = self.all_files[idx] - eo, mask, latlons, months, y = self.nc_to_arrays(filepath) - - dynamic_world = np.ones((eo.shape[0], eo.shape[1])) * (DynamicWorld2020_2021.class_amount) - - return S1_S2_ERA5_SRTM.normalize(eo), dynamic_world, mask, latlons, months, y - - @staticmethod - def combine_predictions( - latlons: np.ndarray, all_preds: np.ndarray, gt: np.ndarray, ndvi: np.ndarray - ) -> pd.DataFrame: - flat_lat, flat_lon = latlons[:, 0], latlons[:, 1] - if len(all_preds.shape) == 1: - all_preds = np.expand_dims(all_preds, axis=-1) - - data_dict: Dict[str, np.ndarray] = {"lat": flat_lat, "lon": flat_lon} - for i in range(all_preds.shape[1]): - prediction_label = f"prediction_{i}" - data_dict[prediction_label] = all_preds[:, i] - data_dict["ground_truth"] = gt[:, 0] - data_dict["ndvi"] = ndvi - return pd.DataFrame(data=data_dict).set_index(["lat", "lon"]) diff --git a/minimal_wc_presto/mvp_wc_presto/masking.py b/minimal_wc_presto/mvp_wc_presto/masking.py deleted file mode 100644 index 90d8f835..00000000 --- a/minimal_wc_presto/mvp_wc_presto/masking.py +++ /dev/null @@ -1,149 +0,0 @@ -from collections import namedtuple -from dataclasses import dataclass -from random import choice, randint, random, sample -from typing import Any, List, Tuple - -import numpy as np - -from .dataops import ( - BAND_EXPANSION, - BANDS_GROUPS_IDX, - NUM_TIMESTEPS, - SRTM_INDEX, - TIMESTEPS_IDX, -) - -MASK_STRATEGIES = ( - "group_bands", - "random_timesteps", - "chunk_timesteps", - "random_combinations", -) - -MaskedExample = namedtuple( - "MaskedExample", - [ - "mask_eo", - "mask_dw", - "x_eo", - "y_eo", - "x_dw", - "y_dw", - "start_month", - "latlon", - "strategy", - "real_mask", - ], -) - - -def make_mask_no_dw(strategy: str, mask_ratio: float, existing_mask: np.ndarray) -> np.ndarray: - """ - Make a mask for a given strategy and percentage of masked values. - Args: - strategy: The masking strategy to use. One of MASK_STRATEGIES - mask_ratio: The percentage of values to mask. Between 0 and 1. - """ - # we assume that topography is never "naturally" masked - mask = existing_mask.copy() - srtm_mask = False - num_tokens_to_mask = int( - ((NUM_TIMESTEPS * (len(BANDS_GROUPS_IDX) - 1)) + 1) * mask_ratio - sum(sum(mask)) - ) - assert num_tokens_to_mask > 0 - - def mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio): - should_flip = random() < mask_ratio - if should_flip: - srtm_mask = True - num_tokens_to_mask -= 1 - return srtm_mask, num_tokens_to_mask - - def random_masking(mask, num_tokens_to_mask: int): - if num_tokens_to_mask > 0: - # we set SRTM to be True - this way, it won't get randomly assigned. - # at the end of the function, it gets properly assigned - mask[:, SRTM_INDEX] = True - # then, we flatten the mask and dw arrays - all_tokens_mask = mask.flatten() - unmasked_tokens = all_tokens_mask == False - idx = np.flatnonzero(unmasked_tokens) - np.random.shuffle(idx) - idx = idx[:num_tokens_to_mask] - all_tokens_mask[idx] = True - mask = all_tokens_mask.reshape((NUM_TIMESTEPS, len(BANDS_GROUPS_IDX))) - return mask - - # RANDOM BANDS - if strategy == "random_combinations": - srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) - mask = random_masking(mask, num_tokens_to_mask) - - elif strategy == "group_bands": - srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) - # next, we figure out how many tokens we can mask - num_band_groups_to_mask = int(num_tokens_to_mask / NUM_TIMESTEPS) - assert (num_tokens_to_mask - NUM_TIMESTEPS * num_band_groups_to_mask) >= 0 - num_tokens_masked = 0 - # tuple because of mypy, which thinks lists can only hold one type - band_groups: List[Any] = list(range(len(BANDS_GROUPS_IDX))) - band_groups.remove(SRTM_INDEX) - band_groups_to_mask = sample(band_groups, num_band_groups_to_mask) - for band_group in band_groups_to_mask: - num_tokens_masked += int(len(mask[:, band_group]) - sum(mask[:, band_group])) - mask[:, band_group] = True - num_tokens_to_mask -= num_tokens_masked - mask = random_masking(mask, num_tokens_to_mask) - - # RANDOM TIMESTEPS - elif strategy == "random_timesteps": - srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) - # -1 for SRTM - timesteps_to_mask = int(num_tokens_to_mask / (len(BANDS_GROUPS_IDX) - 1)) - max_tokens_masked = (len(BANDS_GROUPS_IDX) - 1) * timesteps_to_mask - timesteps = sample(TIMESTEPS_IDX, k=timesteps_to_mask) - if timesteps_to_mask > 0: - num_tokens_to_mask -= int(max_tokens_masked - sum(sum(mask[timesteps]))) - mask[timesteps] = True - mask = random_masking(mask, num_tokens_to_mask) - elif strategy == "chunk_timesteps": - srtm_mask, num_tokens_to_mask = mask_topography(srtm_mask, num_tokens_to_mask, mask_ratio) - # -1 for SRTM - timesteps_to_mask = int(num_tokens_to_mask / (len(BANDS_GROUPS_IDX) - 1)) - if timesteps_to_mask > 0: - max_tokens_masked = (len(BANDS_GROUPS_IDX) - 1) * timesteps_to_mask - start_idx = randint(0, NUM_TIMESTEPS - timesteps_to_mask) - num_tokens_to_mask -= int( - max_tokens_masked - sum(sum(mask[start_idx : start_idx + timesteps_to_mask])) - ) - mask[start_idx : start_idx + timesteps_to_mask] = True # noqa - mask = random_masking(mask, num_tokens_to_mask) - else: - raise ValueError(f"Unknown strategy {strategy} not in {MASK_STRATEGIES}") - - mask[:, SRTM_INDEX] = srtm_mask - return np.repeat(mask, BAND_EXPANSION, axis=1) - - -@dataclass -class MaskParamsNoDw: - strategies: Tuple[str, ...] = ("NDVI",) - ratio: float = 0.5 - - def __post_init__(self): - for strategy in self.strategies: - assert strategy in [ - "group_bands", - "random_timesteps", - "chunk_timesteps", - "random_combinations", - ] - - def mask_data(self, eo_data: np.ndarray, mask: np.ndarray): - strategy = choice(self.strategies) - mask = make_mask_no_dw(strategy=strategy, mask_ratio=self.ratio, existing_mask=mask) - x = eo_data * ~mask - y = np.zeros(eo_data.shape).astype(np.float32) - y[mask] = eo_data[mask] - - return mask, x, y, strategy diff --git a/minimal_wc_presto/mvp_wc_presto/presto.py b/minimal_wc_presto/mvp_wc_presto/presto.py deleted file mode 100644 index 8eedb17f..00000000 --- a/minimal_wc_presto/mvp_wc_presto/presto.py +++ /dev/null @@ -1,873 +0,0 @@ -import math -from copy import deepcopy -from pathlib import Path -from typing import Optional, Sized, Tuple, Union, cast - -import numpy as np -import torch -from einops import repeat -from torch import nn -from torch.jit import Final -from torch.nn import functional as F - -from .dataops import BANDS_GROUPS_IDX, DynamicWorld2020_2021 -from .utils import default_model_path, device - -import io -import requests - - -def param_groups_weight_decay(model: nn.Module, weight_decay=1e-5, no_weight_decay_list=()): - # https://github.com/huggingface/pytorch-image-models/blob/main/timm/optim/optim_factory.py - no_weight_decay_list = set(no_weight_decay_list) - decay = [] - no_decay = [] - for name, param in model.named_parameters(): - if not param.requires_grad: - continue - - if param.ndim <= 1 or name.endswith(".bias") or name in no_weight_decay_list: - no_decay.append(param) - else: - decay.append(param) - - return [ - {"params": no_decay, "weight_decay": 0.0}, - {"params": decay, "weight_decay": weight_decay}, - ] - - -def adjust_learning_rate(optimizer, epoch, warmup_epochs, total_epochs, max_lr, min_lr): - """Decay the learning rate with half-cycle cosine after warmup""" - if epoch < warmup_epochs: - lr = max_lr * epoch / warmup_epochs - else: - lr = min_lr + (max_lr - min_lr) * 0.5 * ( - 1.0 + math.cos(math.pi * (epoch - warmup_epochs) / (total_epochs - warmup_epochs)) - ) - for param_group in optimizer.param_groups: - if "lr_scale" in param_group: - # This is only used during finetuning, and not yet - # implemented in our codebase - param_group["lr"] = lr * param_group["lr_scale"] - else: - param_group["lr"] = lr - return lr - - -class LossWrapper(nn.Module): - def __init__(self, loss: nn.Module): - super().__init__() - self.loss = loss - - def forward(self, pred: torch.Tensor, true: torch.Tensor) -> torch.Tensor: - assert len(pred) == len(true) - if len(pred) == 0: - # len(pred) == 0 -> no inputs are masked, so no - # inputs are passed to the loss - return torch.tensor(0).float().to(device) - return self.loss(pred, true) - - -class Attention(nn.Module): - # https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py - fast_attn: Final[bool] - - def __init__( - self, - dim, - num_heads=8, - qkv_bias=False, - qk_norm=False, - attn_drop=0.0, - proj_drop=0.0, - norm_layer=nn.LayerNorm, - ): - super().__init__() - assert dim % num_heads == 0, "dim should be divisible by num_heads" - self.num_heads = num_heads - self.head_dim = dim // num_heads - self.scale = self.head_dim**-0.5 - self.fast_attn = hasattr(torch.nn.functional, "scaled_dot_product_attention") # FIXME - - self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) - self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() - self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() - self.attn_drop = nn.Dropout(attn_drop) - self.proj = nn.Linear(dim, dim) - self.proj_drop = nn.Dropout(proj_drop) - - def forward(self, x, attn_mask=None): - B, N, C = x.shape - qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4) - q, k, v = qkv.unbind(0) - q, k = self.q_norm(q), self.k_norm(k) - - if self.fast_attn: - if attn_mask is not None: - # todo check - attn_mask = attn_mask[:, None, None].repeat((1, self.num_heads, N, 1)) - x = F.scaled_dot_product_attention( - q, - k, - v, - # a value of True indicates that the element should take part in attention - attn_mask=attn_mask, - dropout_p=self.attn_drop.p, - ) - else: - if attn_mask is not None: - raise NotImplementedError - q = q * self.scale - attn = q @ k.transpose(-2, -1) - attn = attn.softmax(dim=-1) - attn = self.attn_drop(attn) - x = attn @ v - - x = x.transpose(1, 2).reshape(B, N, C) - x = self.proj(x) - x = self.proj_drop(x) - return x - - -class Mlp(nn.Module): - """MLP as used in Vision Transformer, MLP-Mixer and related networks""" - - def __init__( - self, - in_features, - hidden_features=None, - out_features=None, - act_layer=nn.GELU, - bias=True, - drop=0.0, - ): - super().__init__() - out_features = out_features or in_features - hidden_features = hidden_features or in_features - - self.fc1 = nn.Linear(in_features, hidden_features, bias=bias) - self.act = act_layer() - self.drop1 = nn.Dropout(drop) - self.fc2 = nn.Linear(hidden_features, out_features, bias=bias) - self.drop2 = nn.Dropout(drop) - - def forward(self, x): - x = self.fc1(x) - x = self.act(x) - x = self.drop1(x) - x = self.fc2(x) - x = self.drop2(x) - return x - - -class LayerScale(nn.Module): - def __init__(self, dim, init_values=1e-5, inplace=False): - super().__init__() - self.inplace = inplace - self.gamma = nn.Parameter(init_values * torch.ones(dim)) - - def forward(self, x): - return x.mul_(self.gamma) if self.inplace else x * self.gamma - - -class Block(nn.Module): - def __init__( - self, - dim, - num_heads, - mlp_ratio=4.0, - qkv_bias=False, - qk_norm=False, - drop=0.0, - attn_drop=0.0, - init_values=None, - act_layer=nn.GELU, - norm_layer=nn.LayerNorm, - ): - super().__init__() - self.norm1 = norm_layer(dim) - self.attn = Attention( - dim, - num_heads=num_heads, - qkv_bias=qkv_bias, - qk_norm=qk_norm, - attn_drop=attn_drop, - proj_drop=drop, - norm_layer=norm_layer, - ) - self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() - - self.norm2 = norm_layer(dim) - self.mlp = Mlp( - in_features=dim, - hidden_features=int(dim * mlp_ratio), - act_layer=act_layer, - drop=drop, - ) - self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() - - def forward(self, x, attn_mask=None): - x = x + self.ls1(self.attn(self.norm1(x), attn_mask)) - x = x + self.ls2(self.mlp(self.norm2(x))) - return x - - -def get_sinusoid_encoding_table(positions, d_hid, T=1000): - """Sinusoid position encoding table - positions: int or list of integer, if int range(positions)""" - - if isinstance(positions, int): - positions = list(range(positions)) - - def cal_angle(position, hid_idx): - return position / np.power(T, 2 * (hid_idx // 2) / d_hid) - - def get_posi_angle_vec(position): - return [cal_angle(position, hid_j) for hid_j in range(d_hid)] - - sinusoid_table = np.array([get_posi_angle_vec(pos_i) for pos_i in positions]) - - sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i - sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1 - - if torch.cuda.is_available(): - return torch.FloatTensor(sinusoid_table).cuda() - else: - return torch.FloatTensor(sinusoid_table) - - -def get_month_encoding_table(d_hid): - """Sinusoid month encoding table, for 12 months indexed from 0-11""" - assert d_hid % 2 == 0 - angles = np.arange(0, 13) / (12 / (2 * np.pi)) - - sin_table = np.sin(np.stack([angles for _ in range(d_hid // 2)], axis=-1)) - cos_table = np.cos(np.stack([angles for _ in range(d_hid // 2)], axis=-1)) - month_table = np.concatenate([sin_table[:-1], cos_table[:-1]], axis=-1) - - if torch.cuda.is_available(): - return torch.FloatTensor(month_table).cuda() - else: - return torch.FloatTensor(month_table) - - -def month_to_tensor( - month: Union[torch.Tensor, int], batch_size: int, seq_len: int, device: torch.device -): - if isinstance(month, int): - assert cast(int, month) < 12 - else: - assert max(cast(torch.Tensor, month.flatten())) < 12 - - if isinstance(month, int): - # >>> torch.fmod(torch.tensor([9., 10, 11, 12, 13, 14]), 12) - # tensor([ 9., 10., 11., 0., 1., 2.]) - month = ( - torch.fmod(torch.arange(month, month + seq_len, dtype=torch.long), 12) - .expand(batch_size, seq_len) - .to(device) - ) - elif len(month.shape) == 1: - month = torch.stack( - [torch.fmod(torch.arange(m, m + seq_len, dtype=torch.long), 12) for m in month] - ).to(device) - return month - - -class Encoder(nn.Module): - def __init__( - self, - embedding_size: int = 128, - channel_embed_ratio: float = 0.25, - month_embed_ratio: float = 0.25, - depth=2, - mlp_ratio=2, - num_heads=8, - max_sequence_length=24, - ): - super().__init__() - - self.band_groups = BANDS_GROUPS_IDX - self.embedding_size = embedding_size - - # this is used for the channel embedding - self.band_group_to_idx = { - group_name: idx for idx, (group_name, _) in enumerate(self.band_groups.items()) - } - self.band_group_to_idx["dynamic_world"] = max(self.band_group_to_idx.values()) + 1 - - self.eo_patch_embed = nn.ModuleDict( - { - group_name: nn.Linear(len(group), embedding_size) - for group_name, group in self.band_groups.items() - } - ) - self.dw_embed = nn.Embedding( - num_embeddings=DynamicWorld2020_2021.class_amount + 1, embedding_dim=embedding_size - ) - self.latlon_embed = nn.Linear(3, embedding_size) - - self.blocks = nn.ModuleList( - [ - Block( - embedding_size, - num_heads, - mlp_ratio, - qkv_bias=True, - norm_layer=nn.LayerNorm, - ) - for _ in range(depth) - ] - ) - self.norm = nn.LayerNorm(embedding_size) - - # the positional + monthly + channel embedding - self.max_sequence_length = max_sequence_length - pos_embedding_size = int(embedding_size * (1 - (channel_embed_ratio + month_embed_ratio))) - channel_embedding_size = int(embedding_size * channel_embed_ratio) - month_embedding_size = int(embedding_size * month_embed_ratio) - self.pos_embed = nn.Parameter( - torch.zeros(1, max_sequence_length, pos_embedding_size), requires_grad=False - ) - month_tab = get_month_encoding_table(month_embedding_size) - self.month_embed = nn.Embedding.from_pretrained(month_tab, freeze=True) - self.channel_embed = nn.Embedding( - num_embeddings=len(self.band_groups) + 1, embedding_dim=channel_embedding_size - ) - - self.initialize_weights() - - def initialize_weights(self): - pos_embed = get_sinusoid_encoding_table(self.pos_embed.shape[1], self.pos_embed.shape[-1]) - self.pos_embed.data.copy_(pos_embed) - - # initialize nn.Linear and nn.LayerNorm - self.apply(self._init_weights) - - def _init_weights(self, m): - if isinstance(m, nn.Linear): - # we use xavier_uniform following official JAX ViT: - torch.nn.init.xavier_uniform_(m.weight) - if isinstance(m, nn.Linear) and m.bias is not None: - nn.init.constant_(m.bias, 0) - elif isinstance(m, nn.LayerNorm): - nn.init.constant_(m.bias, 0) - nn.init.constant_(m.weight, 1.0) - - @staticmethod - def cartesian(latlons: torch.Tensor) -> torch.Tensor: - with torch.no_grad(): - # an embedding is calculated for all timesteps. This is then expanded - # for each timestep in the sequence - latlon_radians = latlons * math.pi / 180 - lats, lons = latlon_radians[:, 0], latlon_radians[:, 1] - x = torch.cos(lats) * torch.cos(lons) - y = torch.cos(lats) * torch.sin(lons) - z = torch.sin(lats) - return torch.stack([x, y, z], dim=-1) - - @staticmethod - def mask_tokens(x, mask): - mask = mask.bool() - - # https://stackoverflow.com/a/68621610/2332296 - # move all non-masked values to the front of their rows - sorted_mask, indices = torch.sort((~mask).int(), dim=1, descending=True, stable=True) - x = x.gather(1, indices[:, :, None].expand_as(x)) - # set masked values to 0 (not really necessary since we'll ignore them anyway) - x = x * sorted_mask.unsqueeze(-1) - - # cut off to the length of the longest sequence - max_length = sorted_mask.sum(-1).max() - x = x[:, :max_length] - updated_mask = 1 - sorted_mask[:, :max_length] - - return x, indices, updated_mask - - def forward( - self, - x: torch.Tensor, - dynamic_world: torch.Tensor, - latlons: torch.Tensor, - mask: Optional[torch.Tensor] = None, - month: Union[torch.Tensor, int] = 0, - eval_task: bool = True, - ): - device = x.device - - if mask is None: - mask = torch.zeros_like(x, device=x.device) - - months = month_to_tensor(month, x.shape[0], x.shape[1], device) - month_embedding = self.month_embed(months) - positional_embedding = repeat( - self.pos_embed[:, : x.shape[1], :], "b t d -> (repeat b) t d", repeat=x.shape[0] - ) - - # we assume the number of masked patches is the same - # for all items in the batch. Otherwise things become a headache - all_tokens, all_masks = [], [] - - for channel_group, channel_idxs in self.band_groups.items(): - tokens = self.eo_patch_embed[channel_group](x[:, :, channel_idxs]) - channel_embedding = self.channel_embed( - torch.tensor(self.band_group_to_idx[channel_group]).long().to(device) - ) - channel_embedding = repeat(channel_embedding, "d -> b t d", b=x.shape[0], t=x.shape[1]) - if channel_group == "SRTM": - # for SRTM, we reduce it to a single token instead of - # a token per timestep - channel_wise_positional_embedding = torch.cat( - ( - torch.zeros_like(month_embedding[:, 0:1]), - channel_embedding[:, 0:1], - torch.zeros_like(positional_embedding[:, 0:1]), - ), - dim=-1, - ) - indices = slice(0, 1) - else: - channel_wise_positional_embedding = torch.cat( - (month_embedding, channel_embedding, positional_embedding), dim=-1 - ) - indices = slice(None) - - tokens = tokens[:, indices] - tokens += channel_wise_positional_embedding - all_tokens.append(tokens) - group_mask = torch.max(mask[:, indices, channel_idxs], dim=-1)[0] - all_masks.append(group_mask) - - # then, dynamic world - tokens = self.dw_embed(dynamic_world) - channel_embedding = self.channel_embed( - torch.tensor(self.band_group_to_idx["dynamic_world"]).long().to(device) - ) - channel_embedding = repeat(channel_embedding, "d -> b t d", b=x.shape[0], t=x.shape[1]) - positional_embedding = torch.cat( - (month_embedding, channel_embedding, positional_embedding), dim=-1 - ) - tokens += positional_embedding - all_tokens.append(tokens) - - # now we calculate the mask for these [b, t] tokens - group_mask = dynamic_world == DynamicWorld2020_2021.class_amount - all_masks.append(group_mask) - - x = torch.cat(all_tokens, dim=1) # [batch, timesteps, embedding_dim] - mask = torch.cat(all_masks, dim=1) # [batch, timesteps] - x, orig_indices, upd_mask = self.mask_tokens(x, mask) - - # append latlon tokens - latlon_tokens = self.latlon_embed(self.cartesian(latlons)).unsqueeze(1) - x = torch.cat((latlon_tokens, x), dim=1) - upd_mask = torch.cat((torch.zeros(x.shape[0])[:, None].to(device), upd_mask), dim=1) - orig_indices = torch.cat( - (torch.zeros(x.shape[0])[:, None].to(device).int(), orig_indices + 1), - dim=1, - ) - - # apply Transformer blocks - for blk in self.blocks: - x = blk(x, attn_mask=~upd_mask.bool()) - - # mask will be a boolean of shape [batch, total_num_tokens] - if eval_task: - # set masked tokens to 0 - x_for_mean = x * (1 - upd_mask.unsqueeze(-1)) - x_mean = x_for_mean.sum(dim=1) / torch.sum(1 - upd_mask, -1, keepdim=True) - return self.norm(x_mean) - return self.norm(x), orig_indices, upd_mask - - -class Decoder(nn.Module): - def __init__( - self, - channel_embeddings: nn.Embedding, - encoder_embed_dim=128, - decoder_embed_dim=128, - decoder_depth=2, - decoder_num_heads=8, - mlp_ratio=2, - max_sequence_length=24, - ): - super().__init__() - - self.band_groups = BANDS_GROUPS_IDX - - # this is used for the channel embedding - self.band_group_to_idx = { - group_name: idx for idx, (group_name, _) in enumerate(self.band_groups.items()) - } - self.band_group_to_idx["dynamic_world"] = max(self.band_group_to_idx.values()) + 1 - - self.decoder_embed = nn.Linear(encoder_embed_dim, decoder_embed_dim, bias=True) - - self.mask_token = nn.Parameter(torch.zeros(decoder_embed_dim)) - - self.decoder_blocks = nn.ModuleList( - [ - Block( - decoder_embed_dim, - decoder_num_heads, - mlp_ratio, - qkv_bias=True, - norm_layer=nn.LayerNorm, - ) - for _ in range(decoder_depth) - ] - ) - - self.decoder_norm = nn.LayerNorm(decoder_embed_dim) - - self.eo_decoder_pred = nn.ModuleDict( - { - group_name: nn.Linear(decoder_embed_dim, len(group)) - for group_name, group in self.band_groups.items() - } - ) - self.dw_decoder_pred = nn.Linear(decoder_embed_dim, DynamicWorld2020_2021.class_amount) - - self.channel_embeddings = channel_embeddings - channel_embedding_dims = channel_embeddings.weight.shape[-1] - remaining_embeddings = decoder_embed_dim - channel_embedding_dims - # the positional + monthly + channel embedding - self.max_sequence_length = max_sequence_length - self.pos_embed = nn.Parameter( - torch.zeros(1, max_sequence_length, int(remaining_embeddings) // 2), - requires_grad=False, - ) - month_tab = get_month_encoding_table(int(remaining_embeddings) // 2) - self.month_embed = nn.Embedding.from_pretrained(month_tab, freeze=True) - - self.initialize_weights() - - def initialize_weights(self): - pos_embed = get_sinusoid_encoding_table(self.pos_embed.shape[1], self.pos_embed.shape[-1]) - self.pos_embed.data.copy_(pos_embed) - - # initialize nn.Linear and nn.LayerNorm - self.apply(self._init_weights) - - def _init_weights(self, m): - if isinstance(m, nn.Linear): - # we use xavier_uniform following official JAX ViT: - torch.nn.init.xavier_uniform_(m.weight) - if isinstance(m, nn.Linear) and m.bias is not None: - nn.init.constant_(m.bias, 0) - elif isinstance(m, nn.LayerNorm): - nn.init.constant_(m.bias, 0) - nn.init.constant_(m.weight, 1.0) - - def add_masked_tokens(self, x, orig_indices, x_mask): - all_masked = repeat(self.mask_token, "d -> b t d", b=x.shape[0], t=orig_indices.shape[1]) - mask = torch.cat( - ( - x_mask, - torch.ones((x.shape[0], orig_indices.shape[1] - x.shape[1]), device=device), - ), - dim=-1, - ) - # can't set value on leaf variable - out = all_masked.clone() - # put tokens in full masked tensor (at the first N positions in every row) - out[~mask.bool()] = x[~x_mask.bool()] - # then move them to their original positions - out = out.scatter(1, orig_indices[:, :, None].expand_as(out), out) - return out - - def add_embeddings(self, x, month: Union[torch.Tensor, int]): - num_channel_groups = len(self.band_group_to_idx) - # -2 since we remove srtm and latlon, and -1 since the srtm - # channel group doesn't have timesteps - num_timesteps = int((x.shape[1] - 2) / (num_channel_groups - 1)) - srtm_index = self.band_group_to_idx["SRTM"] * num_timesteps - months = month_to_tensor(month, x.shape[0], num_timesteps, x.device) - - # when we expand the encodings, each channel_group gets num_timesteps - # encodings. However, there is only one SRTM token so we remove the - # excess SRTM encodings - remove_mask = torch.full(size=(num_timesteps * num_channel_groups,), fill_value=False) - remove_mask[torch.arange(num_timesteps - 1) + srtm_index] = True - - month_embedding = repeat( - self.month_embed(months), "b t d -> b (repeat t) d", repeat=num_channel_groups - ) - month_embedding = month_embedding[:, ~remove_mask] - month_embedding[:, srtm_index] = 0 - - positional_embedding = repeat( - self.pos_embed[:, :num_timesteps, :], - "b t d -> (b2 b) (t2 t) d", - b2=x.shape[0], - t2=num_channel_groups, - ) - positional_embedding = positional_embedding[:, ~remove_mask] - positional_embedding[:, srtm_index] = 0 - - channel_embeddings = torch.repeat_interleave( - self.channel_embeddings.weight, repeats=num_timesteps, dim=0 - ) - channel_embeddings = repeat(channel_embeddings, "c d -> b c d", b=x.shape[0]) - channel_embeddings = channel_embeddings[:, ~remove_mask] - - positional_embedding = torch.cat( - (month_embedding, channel_embeddings, positional_embedding), dim=-1 - ) - - # add the zero embedding for the latlon token - positional_embedding = torch.cat( - [torch.zeros_like(positional_embedding[:, 0:1, :]), positional_embedding], dim=1 - ) - - x += positional_embedding - return x - - def reconstruct_inputs(self, x) -> Tuple[torch.Tensor, torch.Tensor]: - # remove the latlon token - x = x[:, 1:, :] - - # split into channel groups - num_channel_groups = len(self.band_group_to_idx) - 1 - num_timesteps = int((x.shape[1] - 1) / num_channel_groups) - srtm_index = self.band_group_to_idx["SRTM"] * num_timesteps - srtm_token = x[:, srtm_index : srtm_index + 1, :] - - mask = torch.full((x.shape[1],), True, device=x.device) - mask[torch.tensor(srtm_index)] = False - x = x[:, mask] - - x = x.view(x.shape[0], num_channel_groups, num_timesteps, x.shape[-1]) - - eo_output, dw_output = [], None - for group_name, idx in self.band_group_to_idx.items(): - if group_name == "SRTM": - eo_output.append( - repeat( - self.eo_decoder_pred[group_name](srtm_token), - "b t d -> b (t2 t) d", - t2=num_timesteps, - ) - ) - else: - if idx > self.band_group_to_idx["SRTM"]: - idx -= 1 - group_tokens = x[:, idx] - if group_name == "dynamic_world": - dw_output = self.dw_decoder_pred(group_tokens) - else: - eo_output.append(self.eo_decoder_pred[group_name](group_tokens)) - - # we can just do this concatenation because the BANDS_GROUP_IDX - # is ordered - return torch.cat(eo_output, dim=-1), cast(torch.Tensor, dw_output) - - def forward(self, x, orig_indices, x_mask, month): - x = self.decoder_embed(x) - x = self.add_masked_tokens(x, orig_indices, x_mask) - x = self.add_embeddings(x, month) - - # apply Transformer blocks - for blk in self.decoder_blocks: - x = blk(x) - x = self.decoder_norm(x) - return self.reconstruct_inputs(x) - - -class PrestoFineTuningModel(nn.Module): - def __init__(self, encoder, head): - super().__init__() - self.encoder: Encoder = deepcopy(encoder) - # make sure the model is trainable, since we can call - # this having called requires_grad_(False) - self.encoder.requires_grad_(True) - # but don't unfreeze the position encoder, which - # shouldn't be trainable - self.encoder.pos_embed.requires_grad_(False) - self.encoder.month_embed.requires_grad_(False) - self.head = head - - def forward( - self, - x: torch.Tensor, - dynamic_world: torch.Tensor, - latlons: torch.Tensor, - mask: Optional[torch.Tensor] = None, - month: Union[torch.Tensor, int] = 0, - ) -> torch.Tensor: - return self.head( - self.encoder( - x=x, - dynamic_world=dynamic_world, - latlons=latlons, - mask=mask, - month=month, - eval_task=True, - ) - ) - - -class FinetuningHead(nn.Module): - def __init__(self, hidden_size: int, num_outputs: int) -> None: - super().__init__() - - self.hidden_size = hidden_size - self.num_outputs = num_outputs - self.linear = nn.Linear(hidden_size, num_outputs) - - def forward(self, x: torch.Tensor): - x = self.linear(x) - return x - - -class Presto(nn.Module): - def __init__(self, encoder, decoder): - super().__init__() - self.encoder: Encoder = encoder - self.decoder: Decoder = decoder - - def forward( - self, - x: torch.Tensor, - dynamic_world: torch.Tensor, - latlons: torch.Tensor, - mask: Optional[torch.Tensor] = None, - month: Union[torch.Tensor, int] = 0, - ) -> torch.Tensor: - x, orig_indices, x_mask = self.encoder( - x=x, - dynamic_world=dynamic_world, - latlons=latlons, - mask=mask, - month=month, - eval_task=False, - ) - - return self.decoder(x, orig_indices, x_mask, month) - - @classmethod - def construct( - cls, - encoder_embedding_size: int = 128, - channel_embed_ratio: float = 0.25, - month_embed_ratio: float = 0.25, - encoder_depth=2, - mlp_ratio=4, - encoder_num_heads=8, - decoder_embedding_size=128, - decoder_depth=2, - decoder_num_heads=8, - max_sequence_length=24, - ): - encoder = Encoder( - embedding_size=encoder_embedding_size, - channel_embed_ratio=channel_embed_ratio, - month_embed_ratio=month_embed_ratio, - depth=encoder_depth, - mlp_ratio=mlp_ratio, - num_heads=encoder_num_heads, - max_sequence_length=max_sequence_length, - ) - decoder = Decoder( - channel_embeddings=encoder.channel_embed, - encoder_embed_dim=encoder_embedding_size, - decoder_embed_dim=decoder_embedding_size, - decoder_depth=decoder_depth, - decoder_num_heads=decoder_num_heads, - mlp_ratio=mlp_ratio, - max_sequence_length=max_sequence_length, - ) - return cls(encoder, decoder) - - def construct_finetuning_model( - self, - num_outputs: int, - ) -> PrestoFineTuningModel: - head = FinetuningHead( - num_outputs=num_outputs, - hidden_size=self.encoder.embedding_size, - ) - model = PrestoFineTuningModel(self.encoder, head).to(self.encoder.pos_embed.device) - model.train() - return model - - @classmethod - def load_pretrained( - cls, model_path: Union[str, Path] = default_model_path, strict: bool = True - ): - model = cls.construct() - model.load_state_dict(torch.load(model_path, map_location=device), strict=strict) - return model - - @classmethod - def load_pretrained_artifactory( - cls, presto_url: str, strict: bool = True - ): - response = requests.get(presto_url) - presto_model_layers = torch.load(io.BytesIO(response.content), map_location=device) - model = cls.construct() - model.load_state_dict(presto_model_layers, strict=strict) - return model - - -def param_groups_lrd( - model: PrestoFineTuningModel, weight_decay=0.05, no_weight_decay_list=[], layer_decay=0.75 -): - """ - Parameter groups for layer-wise lr decay - Following BEiT: https://github.com/microsoft/unilm/blob/master/beit/optim_factory.py#L58 - """ - param_group_names = {} - param_groups = {} - - num_layers = len(cast(Sized, model.encoder.blocks)) + 1 - - layer_scales = list(layer_decay ** (num_layers - i) for i in range(num_layers + 1)) - - for n, p in model.named_parameters(): - if not p.requires_grad: - continue - - # no decay: all 1D parameters and model specific ones - if p.ndim == 1 or n in no_weight_decay_list: - g_decay = "no_decay" - this_decay = 0.0 - else: - g_decay = "decay" - this_decay = weight_decay - - layer_id = get_layer_id_for_rest_finetuning(n, num_layers) - group_name = "layer_%d_%s" % (layer_id, g_decay) - - if group_name not in param_group_names: - this_scale = layer_scales[layer_id] - - param_group_names[group_name] = { - "lr_scale": this_scale, - "weight_decay": this_decay, - "params": [], - } - param_groups[group_name] = { - "lr_scale": this_scale, - "weight_decay": this_decay, - "params": [], - } - - param_group_names[group_name]["params"].append(n) - param_groups[group_name]["params"].append(p) - - return list(param_groups.values()) - - -def get_layer_id_for_rest_finetuning(name, num_layers): - """ - Assign a parameter with its layer id - Following BEiT: https://github.com/microsoft/unilm/blob/master/beit/optim_factory.py#L33 - """ - if "embed" in name: - return 0 - elif name.startswith("encoder.blocks"): - return int(name.split(".")[2]) + 1 - else: - return num_layers diff --git a/minimal_wc_presto/mvp_wc_presto/utils.py b/minimal_wc_presto/mvp_wc_presto/utils.py deleted file mode 100644 index 1356407a..00000000 --- a/minimal_wc_presto/mvp_wc_presto/utils.py +++ /dev/null @@ -1,162 +0,0 @@ -import logging -import os -import sys -from datetime import datetime -from pathlib import Path -from typing import Callable, Dict, List, Optional, Union - -import geopandas as gpd -import pandas as pd -import torch -import xarray as xr - -from .dataops import ( - BANDS, - ERA5_BANDS, - NORMED_BANDS, - REMOVED_BANDS, - S1_BANDS, - S1_S2_ERA5_SRTM, - S2_BANDS, - SRTM_BANDS, - DynamicWorld2020_2021, -) - -logger = logging.getLogger("__main__") - -data_dir = Path(__file__).parent.parent / "data" -config_dir = Path(__file__).parent.parent / "config" -default_model_path = data_dir / "default_model.pt" -device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") -DEFAULT_SEED: int = 42 - - -# From https://gist.github.com/ihoromi4/b681a9088f348942b01711f251e5f964 -def seed_everything(seed: int = DEFAULT_SEED): - import os - import random - - import numpy as np - import torch - - random.seed(seed) - os.environ["PYTHONHASHSEED"] = str(seed) - np.random.seed(seed) - torch.manual_seed(seed) - torch.cuda.manual_seed(seed) - torch.backends.cudnn.deterministic = True - torch.backends.cudnn.benchmark = True - - -def initialize_logging(output_dir: Union[str, Path], to_file=True, logger_name="__main__"): - logger = logging.getLogger(logger_name) - formatter = logging.Formatter( - fmt="%(asctime)s - %(levelname)s - %(message)s", - datefmt="%d-%m-%Y %H:%M:%S", - ) - ch = logging.StreamHandler(stream=sys.stdout) - ch.setLevel(logging.INFO) - ch.setFormatter(formatter) - logger.addHandler(ch) - - logger.setLevel(logging.INFO) - - if to_file: - path = os.path.join(output_dir, "console-output.log") - fh = logging.FileHandler(path) - fh.setLevel(logging.INFO) - fh.setFormatter(formatter) - logger.addHandler(fh) - logger.info("Initialized logging to %s" % path) - return logger - - -def timestamp_dirname(suffix: Optional[str] = None) -> str: - ts = datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f") - return f"{ts}_{suffix}" if suffix is not None else ts - - -def construct_single_presto_input( - s1: Optional[torch.Tensor] = None, - s1_bands: Optional[List[str]] = None, - s2: Optional[torch.Tensor] = None, - s2_bands: Optional[List[str]] = None, - era5: Optional[torch.Tensor] = None, - era5_bands: Optional[List[str]] = None, - srtm: Optional[torch.Tensor] = None, - srtm_bands: Optional[List[str]] = None, - dynamic_world: Optional[torch.Tensor] = None, - normalize: bool = True, -): - """ - Inputs are paired into a tensor input and a list _bands, which describes . - - should have shape (num_timesteps, len(_bands)), with the following bands possible for - each input: - - s1: ["VV", "VH"] - s2: ["B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B8A", "B9", "B10", "B11", "B12"] - era5: ["temperature_2m", "total_precipitation"] - "temperature_2m": Temperature of air at 2m above the surface of land, - sea or in-land waters in Kelvin (K) - "total_precipitation": Accumulated liquid and frozen water, including rain and snow, - that falls to the Earth's surface. Measured in metres (m) - srtm: ["elevation", "slope"] - - dynamic_world is a 1d input of shape (num_timesteps,) representing the dynamic world classes - of each timestep for that pixel - """ - num_timesteps_list = [x.shape[0] for x in [s1, s2, era5, srtm] if x is not None] - if dynamic_world is not None: - num_timesteps_list.append(len(dynamic_world)) - - assert len(num_timesteps_list) > 0 - assert all(num_timesteps_list[0] == timestep for timestep in num_timesteps_list) - num_timesteps = num_timesteps_list[0] - mask, x = torch.ones(num_timesteps, len(BANDS)), torch.zeros(num_timesteps, len(BANDS)) - - for band_group in [ - (s1, s1_bands, S1_BANDS), - (s2, s2_bands, S2_BANDS), - (era5, era5_bands, ERA5_BANDS), - (srtm, srtm_bands, SRTM_BANDS), - ]: - data, input_bands, output_bands = band_group - if data is not None: - assert input_bands is not None - else: - continue - - kept_output_bands = [x for x in output_bands if x not in REMOVED_BANDS] - # construct a mapping from the input bands to the expected bands - kept_input_band_idxs = [i for i, val in enumerate(input_bands) if val in kept_output_bands] - kept_input_band_names = [val for val in input_bands if val in kept_output_bands] - - input_to_output_mapping = [BANDS.index(val) for val in kept_input_band_names] - - x[:, input_to_output_mapping] = data[:, kept_input_band_idxs] - mask[:, input_to_output_mapping] = 0 - - if dynamic_world is None: - dynamic_world = torch.ones(num_timesteps) * (DynamicWorld2020_2021.class_amount) - - keep_indices = [idx for idx, val in enumerate(BANDS) if val != "B9"] - mask = mask[:, keep_indices] - - if normalize: - # normalize includes x = x[:, keep_indices] - x = S1_S2_ERA5_SRTM.normalize(x) - if s2_bands is not None: - if ("B8" in s2_bands) and ("B4" in s2_bands): - mask[:, NORMED_BANDS.index("NDVI")] = 0 - else: - x = x[:, keep_indices] - return x, mask, dynamic_world - - -def load_world_df() -> pd.DataFrame: - # this could be memoized, but it should only be called 2 or 3 times in a run - filename = "world-administrative-boundaries/world-administrative-boundaries.shp" - world_df = gpd.read_file(data_dir / filename) - world_df = world_df.drop(columns=["iso3", "status", "color_code", "iso_3166_1_"]) - return world_df diff --git a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py b/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py deleted file mode 100644 index 26e690a3..00000000 --- a/minimal_wc_presto/mvp_wc_presto/world_cereal_inference.py +++ /dev/null @@ -1,396 +0,0 @@ -from typing import Dict, Tuple - -import numpy as np -import requests -import torch -from torch.utils.data import DataLoader, TensorDataset - -import xarray as xr -from einops import rearrange -from pyproj import Transformer - -import onnxruntime as ort - -from .dataops import ( - BANDS, - BANDS_GROUPS_IDX, - NORMED_BANDS, - S1_S2_ERA5_SRTM, - DynamicWorld2020_2021, -) -from .masking import BAND_EXPANSION -from .presto import Presto -from .utils import device - -# Mapping from original band names to Presto names -BAND_MAPPING = { - "B02": "B2", - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - "B8A": "B8A", - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", -} - -# Index to band groups mapping -IDX_TO_BAND_GROUPS = { - NORMED_BANDS[idx]: band_group_idx - for band_group_idx, (_, val) in enumerate(BANDS_GROUPS_IDX.items()) - for idx in val -} - - -class WorldCerealPredictor: - def __init__(self): - """ - Initialize an empty WorldCerealPredictor. - """ - self.onnx_session = None - - def load_model(self, model): - """ - Load an ONNX model from the specified path. - - Args: - model_path (str): The path to the ONNX model file. - """ - # Load the dependency into an InferenceSession - self.onnx_session = ort.InferenceSession(model) - - def predict(self, features: np.ndarray) -> np.ndarray: - """ - Predicts labels using the provided features DataFrame. - - Args: - features (pd.ndarray): 2D array containing the features - - Returns: - pd.DataFrame: DataFrame containing the predicted labels. - """ - if self.onnx_session is None: - raise ValueError( - "Model has not been loaded. Please load a model first." - ) - - # Prepare input data for ONNX model - outputs = self.onnx_session.run(None, {"features": features}) - - # Threshold for binary conversion - threshold = 0.5 - - # Extract all prediction values and convert them to binary labels - prediction_values = [sublist["True"] for sublist in outputs[1]] - binary_labels = np.array(prediction_values) >= threshold - binary_labels = binary_labels.astype(int) - - return binary_labels - - - -class PrestoFeatureExtractor: - def __init__(self, model: Presto): - """ - Initialize the PrestoFeatureExtractor with a Presto model. - - Args: - model (Presto): The Presto model used for feature extraction. - """ - self.model = model - - _NODATAVALUE = 65535 - - BAND_MAPPING = { - "B02": "B2", - "B03": "B3", - "B04": "B4", - "B05": "B5", - "B06": "B6", - "B07": "B7", - "B08": "B8", - "B8A": "B8A", - "B11": "B11", - "B12": "B12", - "VH": "VH", - "VV": "VV", - "precipitation-flux": "total_precipitation", - "temperature-mean": "temperature_2m", - } - - @classmethod - def _preprocess_band_values( - cls, values: np.ndarray, presto_band: str - ) -> np.ndarray: - """ - Preprocesses the band values based on the given presto_val. - - Args: - values (np.ndarray): Array of band values to preprocess. - presto_val (str): Name of the band for preprocessing. - - Returns: - np.ndarray: Preprocessed array of band values. - """ - if presto_band in ["VV", "VH"]: - # Convert to dB - values = 20 * np.log10(values) - 83 - elif presto_band == "total_precipitation": - # Scale precipitation and convert mm to m - values = values / (100 * 1000.0) - elif presto_band == "temperature_2m": - # Remove scaling - values = values / 100 - return values - - @classmethod - def _extract_eo_data(cls, inarr: xr.DataArray) -> Tuple[np.ndarray, np.ndarray]: - """ - Extracts EO data and mask arrays from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing EO data. - - Returns: - Tuple[np.ndarray, np.ndarray]: Tuple containing EO data array and mask array. - """ - num_pixels = len(inarr.x) * len(inarr.y) - num_timesteps = len(inarr.t) - - eo_data = np.zeros((num_pixels, num_timesteps, len(BANDS))) - mask = np.zeros((num_pixels, num_timesteps, len(BANDS_GROUPS_IDX))) - - for org_band, presto_band in cls.BAND_MAPPING.items(): - if org_band in inarr.coords["bands"]: - values = rearrange( - inarr.sel(bands=org_band).values, "t x y -> (x y) t" - ) - idx_valid = values != cls._NODATAVALUE - values = cls._preprocess_band_values(values, presto_band) - eo_data[:, :, BANDS.index(presto_band)] = values - mask[:, :, IDX_TO_BAND_GROUPS[presto_band]] += ~idx_valid - - return eo_data, mask - - @staticmethod - def _extract_latlons(inarr: xr.DataArray, epsg: int) -> np.ndarray: - """ - Extracts latitudes and longitudes from the input xarray.DataArray. - - Args: - inarr (xr.DataArray): Input xarray.DataArray containing spatial coordinates. - epsg (int): EPSG code for coordinate reference system. - - Returns: - np.ndarray: Array containing extracted latitudes and longitudes. - """ - # EPSG:4326 is the supported crs for presto - lon, lat = np.meshgrid(inarr.x, inarr.y) - transformer = Transformer.from_crs( - f"EPSG:{epsg}", "EPSG:4326", always_xy=True - ) - lon, lat = transformer.transform(lon, lat) - latlons = rearrange(np.stack([lat, lon]), "c x y -> (x y) c") - - # 2D array where each row represents a pair of latitude and longitude coordinates. - return latlons - - @staticmethod - def _extract_months(inarr: xr.DataArray) -> np.ndarray: - """ - Calculate the start month based on the first timestamp in the input array, - and create an array of the same length filled with that start month value. - - Parameters: - - inarr: xarray.DataArray or numpy.ndarray - Input array containing timestamps. - - Returns: - - months: numpy.ndarray - Array of start month values, with the same length as the input array. - """ - num_instances = len(inarr.x) * len(inarr.y) - - start_month = ( - inarr.t.values[0].astype("datetime64[M]").astype(int) % 12 + 1 - ) - 1 - - months = np.ones((num_instances)) * start_month - return months - - - def _create_dataloader( - self, - eo: np.ndarray, - dynamic_world: np.ndarray, - months: np.ndarray, - latlons: np.ndarray, - mask: np.ndarray, - ) -> DataLoader: - """ - Create a PyTorch DataLoader for encoding features. - - Args: - eo_data (np.ndarray): Array containing Earth Observation data. - dynamic_world (np.ndarray): Array containing dynamic world data. - latlons (np.ndarray): Array containing latitude and longitude coordinates. - inarr (xr.DataArray): Input xarray.DataArray. - mask (np.ndarray): Array containing masking data. - - Returns: - DataLoader: PyTorch DataLoader for encoding features. - """ - - dl = DataLoader( - TensorDataset( - torch.from_numpy(eo).float(), - torch.from_numpy(dynamic_world).long(), - torch.from_numpy(latlons).float(), - torch.from_numpy(months).long(), - torch.from_numpy(mask).float(), - ), - batch_size=8192, - shuffle=False, - ) - - return dl - - - def _create_presto_input( - cls, inarr: xr.DataArray, epsg: int = 4326 - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - eo_data, mask = cls._extract_eo_data(inarr) - latlons = cls._extract_latlons(inarr, epsg) - months = cls._extract_months(inarr) - dynamic_world = np.ones((eo_data.shape[0], eo_data.shape[1])) * ( - DynamicWorld2020_2021.class_amount - ) - - return ( - S1_S2_ERA5_SRTM.normalize(eo_data), - dynamic_world, - months, - latlons, - np.repeat(mask, BAND_EXPANSION, axis=-1), - ) - - - def _get_encodings(self, dl: DataLoader) -> np.ndarray: - """ - Get encodings from DataLoader. - - Args: - dl (DataLoader): PyTorch DataLoader containing data for encoding. - - Returns: - np.ndarray: Array containing encoded features. - """ - - all_encodings = [] - - for x, dw, latlons, month, variable_mask in dl: - x_f, dw_f, latlons_f, month_f, variable_mask_f = [ - t.to(device) for t in (x, dw, latlons, month, variable_mask) - ] - - with torch.no_grad(): - encodings = ( - self.model.encoder( - x_f, - dynamic_world=dw_f.long(), - mask=variable_mask_f, - latlons=latlons_f, - month=month_f, - ) - .cpu() - .numpy() - ) - - all_encodings.append(encodings) - - return np.concatenate(all_encodings, axis=0) - - def extract_presto_features( - self, inarr: xr.DataArray, epsg: int = 4326 - ) -> xr.DataArray: - eo, dynamic_world, months, latlons, mask = self._create_presto_input( - inarr, epsg - ) - dl = self._create_dataloader(eo, dynamic_world, months, latlons, mask) - - features = self._get_encodings(dl) - features = rearrange( - features, "(x y) c -> x y c", x=len(inarr.x), y=len(inarr.y) - ) - ft_names = [f"presto_ft_{i}" for i in range(128)] - features = xr.DataArray( - features, - coords={"x": inarr.x, "y": inarr.y, "bands": ft_names}, - dims=["x", "y", "bands"], - ) - - return features - - - - -def get_presto_features(inarr: xr.DataArray, presto_path: str) -> xr.DataArray: - """ - Extracts features from input data using Presto. - - Args: - inarr (xr.DataArray): Input data as xarray DataArray. - presto_path (str): Path to the pretrained Presto model. - - Returns: - xr.DataArray: Extracted features as xarray DataArray. - """ - # Load the model - - presto_model = Presto.load_pretrained_artifactory( - presto_url=presto_path, strict=False - ) - #TODO flexible espg - presto_extractor = PrestoFeatureExtractor(presto_model) - features = presto_extractor.extract_presto_features(inarr, epsg=32631) - return features - - -def classify_with_catboost( - features: xr.DataArray, catboost_path: str -) -> xr.DataArray: - """ - Classifies features using the WorldCereal CatBoost model. - - Args: - features (xr.DataArray): Features to be classified [x, y, fts] - map_dims (tuple): Original x, y dimensions of the input data. - model_path (str): Path to the trained CatBoost model. - - Returns: - xr.DataArray: Classified data as xarray DataArray. - """ - - # Stack the features and transpose for feeding to CatBoost - stacked_features = features.stack(xy=["x", "y"]).transpose() - - predictor = WorldCerealPredictor() - response = requests.get(catboost_path) - catboost_model = response.content - - predictor.load_model(catboost_model) - predictions = predictor.predict(stacked_features.values) - - predictions = ( - xr.DataArray(predictions, coords={"xy": stacked_features.xy}, dims=["xy"]) - .unstack() - .expand_dims(dim="bands") - ) - - return predictions diff --git a/minimal_wc_presto/test_cropland_gfmap.py b/minimal_wc_presto/test_cropland_gfmap.py deleted file mode 100644 index 903b056b..00000000 --- a/minimal_wc_presto/test_cropland_gfmap.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Test the presto feature computer running with GFMAP""" - -import openeo -from openeo_gfmap import Backend, BackendContext, BoundingBoxExtent, TemporalContext -from openeo_gfmap.features.feature_extractor import apply_feature_extractor -from openeo_gfmap.inference.model_inference import apply_model_inference - -from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor -from worldcereal.openeo.inference import CroplandClassifier -from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap -from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor -from worldcereal.openeo.inference import CroplandClassifier - -EXTENT = dict( - zip(["west", "south", "east", "north"], [664000.0, 5611120.0, 665000.0, 5612120.0]) -) -EXTENT["crs"] = "EPSG:32631" -EXTENT["srs"] = "EPSG:32631" -STARTDATE = "2020-11-01" -ENDDATE = "2021-10-31" - -ONNX_DEPS_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openeo/onnx_dependencies_1.16.3.zip" - - -if __name__ == "__main__": - # Test extent - spatial_extent = BoundingBoxExtent( - west=EXTENT["west"], - south=EXTENT["south"], - east=EXTENT["east"], - north=EXTENT["north"], - epsg=32631, - ) - - temporal_extent = TemporalContext( - start_date=STARTDATE, - end_date=ENDDATE, - ) - backend_context = BackendContext(Backend.FED) - - connection = openeo.connect( - "https://openeo.creo.vito.be/openeo/" - ).authenticate_oidc() - - inputs = worldcereal_preprocessed_inputs_gfmap( - connection=connection, - backend_context=backend_context, - spatial_extent=spatial_extent, - temporal_extent=temporal_extent, - ) - - # Test feature computer - presto_parameters = { - "rescale_s1": False, # Will be done in the Presto UDF itself! - } - - features = apply_feature_extractor( - feature_extractor_class=PrestoFeatureExtractor, - cube=inputs, - parameters=presto_parameters, - size=[ - {"dimension": "x", "unit": "px", "value": 100}, - {"dimension": "y", "unit": "px", "value": 100}, - ], - overlap=[ - {"dimension": "x", "unit": "px", "value": 0}, - {"dimension": "y", "unit": "px", "value": 0}, - ], - ) - - catboost_parameters = {} - - classes = apply_model_inference( - model_inference_class=CroplandClassifier, - cube=features, - parameters=catboost_parameters, - size=[ - {"dimension": "x", "unit": "px", "value": 100}, - {"dimension": "y", "unit": "px", "value": 100}, - {"dimension": "t", "value": "P1D"}, - ], - overlap=[ - {"dimension": "x", "unit": "px", "value": 0}, - {"dimension": "y", "unit": "px", "value": 0}, - ], - ) - - classes.execute_batch( - outputfile=".notebook-tests/presto_prediction_gfmap.nc", - out_format="NetCDF", - job_options={ - "driver-memory": "4g", - "executor-memoryOverhead": "8g", - "udf-dependency-archives": [f"{ONNX_DEPS_URL}#onnx_deps"], - }, - ) diff --git a/scripts/inference/cropland_mapping.py b/scripts/inference/cropland_mapping.py index d2ac1d49..d3d44c2f 100644 --- a/scripts/inference/cropland_mapping.py +++ b/scripts/inference/cropland_mapping.py @@ -12,8 +12,6 @@ from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor from worldcereal.openeo.inference import CroplandClassifier from worldcereal.openeo.preprocessing import worldcereal_preprocessed_inputs_gfmap -from worldcereal.openeo.feature_extractor import PrestoFeatureExtractor -from worldcereal.openeo.inference import CroplandClassifier ONNX_DEPS_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openeo/onnx_dependencies_1.16.3.zip" @@ -27,7 +25,6 @@ parser.add_argument("miny", type=float, help="Minimum Y coordinate (south)") parser.add_argument("maxx", type=float, help="Maximum X coordinate (east)") parser.add_argument("maxy", type=float, help="Maximum Y coordinate (north)") - parser.add_argument("--epsg", type=int, default=4326, help="EPSG code for coordiante reference system.") parser.add_argument( "--epsg", type=int, diff --git a/src/worldcereal/openeo/feature_extractor.py b/src/worldcereal/openeo/feature_extractor.py index ca49e0b2..053dce27 100644 --- a/src/worldcereal/openeo/feature_extractor.py +++ b/src/worldcereal/openeo/feature_extractor.py @@ -1,19 +1,24 @@ """Feature computer GFMAP compatible to compute Presto embeddings.""" import xarray as xr +from openeo.udf import XarrayDataCube from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor + class PrestoFeatureExtractor(PatchFeatureExtractor): - """Feature extractor to use Presto model to compute embeddings. + """Feature extractor to use Presto model to compute per-pixel embeddings. This will generate a datacube with 128 bands, each band representing a feature from the Presto model. - """ - import functools - from pathlib import Path - from typing import Tuple + Interesting UDF parameters: + - presto_url: A public URL to the Presto model file. A default Presto + version is provided if the parameter is left undefined. + - rescale_s1: Is specifically disabled by default, as the presto + dependencies already take care of the backscatter decompression. If + specified, should be set as `False`. + """ - PRESTO_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA + PRESTO_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" @@ -31,49 +36,10 @@ class PrestoFeatureExtractor(PatchFeatureExtractor): "S1-SIGMA0-VH": "VH", "S1-SIGMA0-VV": "VV", "COP-DEM": "DEM", - "A5-tmean": "temperature-mean", - "A5-precip": "precipitation-flux", + "AGERA5-TMEAN": "temperature-mean", + "AGERA5-PRECIP": "precipitation-flux", } - def __init__(self): - """ - Initializes the PrestoFeatureExtractor object, starting a logger. - """ - import logging - - logging.basicConfig(level=logging.INFO) - self.logger = logging.getLogger(PrestoFeatureExtractor.__name__) - - @classmethod - @functools.lru_cache(maxsize=6) - def extract_dependencies(cls, base_url: str, dependency_name: str): - """Extract the dependencies from the given URL. Unpacking a zip - file in the current working directory. - """ - import shutil - import urllib.request - from pathlib import Path - - # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / "dependencies" - - # Create the directory if it doesn't exist - dependencies_dir.mkdir(exist_ok=True, parents=True) - - # Download and extract the model file - modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve( - modelfile_url, filename=dependencies_dir / Path(modelfile_url).name - ) - shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - - # Add the model directory to system path if it's not already there - abs_path = str( - dependencies_dir / Path(modelfile_url).name.split(".zip")[0] - ) # NOQA - - return abs_path - def output_labels(self) -> list: """Returns the output labels from this UDF, which is the output labels of the presto embeddings""" @@ -87,6 +53,7 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: "EPSG code is required for Presto feature extraction, but was " "not correctly initialized." ) + presto_url = self._parameters.get("presto_url", self.PRESTO_URL) # The below is required to avoid flipping of the result # when running on OpenEO backend! @@ -108,8 +75,16 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: self.logger.info("Appending dependencies") sys.path.append(str(deps_dir)) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import get_presto_features + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import ( + get_presto_features, + ) self.logger.info("Extracting presto features") - features = get_presto_features(inarr, self.PRESTO_PATH, self.epsg) + features = get_presto_features(inarr, presto_url, self.epsg) return features + + def _execute(self, cube: XarrayDataCube, parameters: dict) -> XarrayDataCube: + # Disable S1 rescaling (decompression) by default + if parameters.get("rescale_s1", None) is None: + parameters.update({"rescale_s1": False}) + return super()._execute(cube, parameters) diff --git a/src/worldcereal/openeo/inference.py b/src/worldcereal/openeo/inference.py index 8f981930..70f98005 100644 --- a/src/worldcereal/openeo/inference.py +++ b/src/worldcereal/openeo/inference.py @@ -5,48 +5,28 @@ class CroplandClassifier(ModelInference): - import functools + """Binary crop-land classifier using ONNX to load a catboost model. + + The classifier use the embeddings computed from the Presto Feature + Extractor. + + Interesting UDF parameters: + - classifier_url: A public URL to the ONNX classification model. Default is + the public Presto model. + """ CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" - def __init__(self): - import logging - logging.basicConfig(level=logging.INFO) - self.logger = logging.getLogger(WorldCerealInference.__name__) - - @classmethod - @functools.lru_cache(maxsize=6) - def extract_dependencies(cls, base_url: str, dependency_name: str): - import shutil - import urllib.request - from pathlib import Path - - # Generate absolute path for the dependencies folder - dependencies_dir = Path.cwd() / "dependencies" - - # Create the directory if it doesn't exist - dependencies_dir.mkdir(exist_ok=True, parents=True) - - # Download and extract the model file - modelfile_url = f"{base_url}/{dependency_name}" - modelfile, _ = urllib.request.urlretrieve( - modelfile_url, filename=dependencies_dir / Path(modelfile_url).name - ) - shutil.unpack_archive(modelfile, extract_dir=dependencies_dir) - - # Add the model directory to system path if it's not already there - abs_path = str(dependencies_dir / Path(modelfile_url).name.split(".zip")[0]) - - return abs_path - def output_labels(self) -> list: return ["classification"] def execute(self, inarr: xr.DataArray) -> xr.DataArray: import sys + classifier_url = self._parameters.get("classifier_url", self.CATBOOST_PATH) + # shape and indiches for output inarr = inarr.transpose("bands", "x", "y") @@ -57,11 +37,13 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: self.logger.info("Adding dependencies") sys.path.append(str(dep_dir)) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import classify_with_catboost + from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import ( + classify_with_catboost, + ) # Run catboost classification self.logger.info("Catboost classification") - classification = classify_with_catboost(inarr, self.CATBOOST_PATH) + classification = classify_with_catboost(inarr, classifier_url) self.logger.info("Done") return classification diff --git a/src/worldcereal/openeo/preprocessing.py b/src/worldcereal/openeo/preprocessing.py index 87193d4e..b5ca0987 100644 --- a/src/worldcereal/openeo/preprocessing.py +++ b/src/worldcereal/openeo/preprocessing.py @@ -3,6 +3,7 @@ from openeo import UDF, Connection, DataCube from openeo_gfmap import ( + Backend, BackendContext, BoundingBoxExtent, FetchType, @@ -14,6 +15,7 @@ from openeo_gfmap.fetching.s2 import build_sentinel2_l2a_extractor from openeo_gfmap.preprocessing.compositing import mean_compositing, median_compositing from openeo_gfmap.preprocessing.sar import compress_backscatter_uint16 +from openeo_gfmap.utils.catalogue import UncoveredS1Exception, select_S1_orbitstate COMPOSITE_WINDOW = "month" @@ -162,10 +164,37 @@ def raw_datacube_S1( List of Sentinel-1 bands to extract. fetch_type : FetchType GFMAP Fetch type to use for extraction. + target_resolution : float, optional + Target resolution to resample the data to, by default 20.0. + orbit_direction : Optional[str], optional + Orbit direction to filter the data, by default None. If None and the + backend is in CDSE, then querries the catalogue for the best orbit + direction to use. In the case querrying is unavailable or fails, then + uses "ASCENDING" as a last resort. """ extractor_parameters = { "target_resolution": target_resolution, } + if orbit_direction is None and backend_context.backend in [ + Backend.CDSE, + Backend.CDSE_STAGING, + Backend.FED, + ]: + try: + orbit_direction = select_S1_orbitstate( + backend_context, spatial_extent, temporal_extent + ) + print( + f"Selected orbit direction: {orbit_direction} from max " + "accumulated area overlap between bounds and products." + ) + except UncoveredS1Exception as exc: + orbit_direction = "ASCENDING" + print( + f"Could not find any Sentinel-1 data for the given spatio-temporal context. " + f"Using ASCENDING orbit direction as a last resort. Error: {exc}" + ) + if orbit_direction is not None: extractor_parameters["load_collection"] = { "sat:orbit_state": lambda orbit: orbit == orbit_direction @@ -202,7 +231,7 @@ def raw_datacube_METEO( ) -> DataCube: extractor = build_generic_extractor( backend_context=backend_context, - bands=["A5-tmean", "A5-precip"], + bands=["AGERA5-TMEAN", "AGERA5-PRECIP"], fetch_type=fetch_type, collection_name="AGERA5", ) @@ -243,6 +272,9 @@ def worldcereal_preprocessed_inputs_gfmap( # Cast to uint16 s2_data = s2_data.linear_scale_range(0, 65534, 0, 65534) + # Decide on the orbit direction from the maximum overlapping area of + # available products. + # Extraction of the S1 data s1_data = raw_datacube_S1( connection=connection, @@ -255,7 +287,7 @@ def worldcereal_preprocessed_inputs_gfmap( ], fetch_type=FetchType.TILE, target_resolution=10.0, # Compute the backscatter at 20m resolution, then upsample nearest neighbor when merging cubes - orbit_direction="ASCENDING", + orbit_direction=None, # Make the querry on the catalogue for the best orbit ) s1_data = mean_compositing(s1_data, period="month") @@ -279,10 +311,10 @@ def worldcereal_preprocessed_inputs_gfmap( # ) # # Perform compositing differently depending on the bands - # mean_temperature = meteo_data.band("A5-tmean") + # mean_temperature = meteo_data.band("AGERA5-TMEAN") # mean_temperature = mean_compositing(mean_temperature, period="month") - # total_precipitation = meteo_data.band("A5-precip") + # total_precipitation = meteo_data.band("AGERA5-PRECIP") # total_precipitation = sum_compositing(total_precipitation, period="month") data = s2_data.merge_cubes(s1_data) From 3faef72a32d102931f97e19f343c78d4dcded62c Mon Sep 17 00:00:00 2001 From: Kristof Van Tricht Date: Mon, 3 Jun 2024 18:03:24 +0200 Subject: [PATCH 28/31] make use of external dependency through whl --- src/worldcereal/openeo/feature_extractor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/worldcereal/openeo/feature_extractor.py b/src/worldcereal/openeo/feature_extractor.py index 053dce27..cfcc14cc 100644 --- a/src/worldcereal/openeo/feature_extractor.py +++ b/src/worldcereal/openeo/feature_extractor.py @@ -1,5 +1,10 @@ """Feature computer GFMAP compatible to compute Presto embeddings.""" +# /// script +# dependencies = [ +# "presto @ https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.0.1-py3-none-any.whl" +# ] +# /// import xarray as xr from openeo.udf import XarrayDataCube from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor @@ -75,9 +80,7 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: self.logger.info("Appending dependencies") sys.path.append(str(deps_dir)) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import ( - get_presto_features, - ) + from presto.inference import get_presto_features self.logger.info("Extracting presto features") features = get_presto_features(inarr, presto_url, self.epsg) From 8723fae233da48ce0be1c79c47548bf4049cb433 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Fri, 7 Jun 2024 10:45:49 +0200 Subject: [PATCH 29/31] Changed to work with new openeo way of handling dependencies --- scripts/inference/cropland_mapping.py | 5 +++-- src/worldcereal/openeo/feature_extractor.py | 20 +++++++++++--------- src/worldcereal/openeo/inference.py | 20 +++++++++++++------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/scripts/inference/cropland_mapping.py b/scripts/inference/cropland_mapping.py index d3d44c2f..39791444 100644 --- a/scripts/inference/cropland_mapping.py +++ b/scripts/inference/cropland_mapping.py @@ -58,7 +58,8 @@ backend_context = BackendContext(Backend.FED) connection = openeo.connect( - "https://openeo.creo.vito.be/openeo/" + # "https://openeo.creo.vito.be/openeo/" + "https://openeo-staging.creo.vito.be/openeo/" ).authenticate_oidc() # Preparing the input cube for the inference @@ -111,6 +112,6 @@ job_options={ "driver-memory": "4g", "executor-memoryOverhead": "8g", - "udf-dependency-archives": [f"{ONNX_DEPS_URL}#onnx_deps"], + "logging-threshold": "debug", }, ) diff --git a/src/worldcereal/openeo/feature_extractor.py b/src/worldcereal/openeo/feature_extractor.py index cfcc14cc..193657bb 100644 --- a/src/worldcereal/openeo/feature_extractor.py +++ b/src/worldcereal/openeo/feature_extractor.py @@ -1,10 +1,5 @@ """Feature computer GFMAP compatible to compute Presto embeddings.""" -# /// script -# dependencies = [ -# "presto @ https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.0.1-py3-none-any.whl" -# ] -# /// import xarray as xr from openeo.udf import XarrayDataCube from openeo_gfmap.features.feature_extractor import PatchFeatureExtractor @@ -45,6 +40,13 @@ class PrestoFeatureExtractor(PatchFeatureExtractor): "AGERA5-PRECIP": "precipitation-flux", } + def dependencies(self) -> list: + """Gives the presto dependencies from a wheel with all it's subdependencies.""" + return [ + "torch @ https://download.pytorch.org/whl/cpu/torch-2.0.0%2Bcpu-cp38-cp38-linux_x86_64.whl#sha256=354f281351cddb590990089eced60f866726415f7b287db5105514aa3c5f71ca", + "presto @ https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.0.1-py3-none-any.whl", + ] + def output_labels(self) -> list: """Returns the output labels from this UDF, which is the output labels of the presto embeddings""" @@ -74,11 +76,11 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: inarr = inarr.fillna(65535) # Unzip de dependencies on the backend - self.logger.info("Unzipping dependencies") - deps_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + # self.logger.info("Unzipping dependencies") + # deps_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) - self.logger.info("Appending dependencies") - sys.path.append(str(deps_dir)) + # self.logger.info("Appending dependencies") + # sys.path.append(str(deps_dir)) from presto.inference import get_presto_features diff --git a/src/worldcereal/openeo/inference.py b/src/worldcereal/openeo/inference.py index 70f98005..34c4c4c3 100644 --- a/src/worldcereal/openeo/inference.py +++ b/src/worldcereal/openeo/inference.py @@ -19,6 +19,14 @@ class CroplandClassifier(ModelInference): BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + def dependencies(self) -> list: + """Gives the presto dependencies from a wheel with all it's subdependencies.""" + return [ + "onnxruntime", + "torch @ https://download.pytorch.org/whl/cpu/torch-2.0.0%2Bcpu-cp38-cp38-linux_x86_64.whl#sha256=354f281351cddb590990089eced60f866726415f7b287db5105514aa3c5f71ca", + "presto @ https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.0.1-py3-none-any.whl", + ] + def output_labels(self) -> list: return ["classification"] @@ -31,15 +39,13 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: inarr = inarr.transpose("bands", "x", "y") # Unzip de dependencies on the backend - self.logger.info("Unzipping dependencies") - dep_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + # self.logger.info("Unzipping dependencies") + # dep_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) - self.logger.info("Adding dependencies") - sys.path.append(str(dep_dir)) + # self.logger.info("Adding dependencies") + # sys.path.append(str(dep_dir)) - from dependencies.wc_presto_onnx_dependencies.mvp_wc_presto.world_cereal_inference import ( - classify_with_catboost, - ) + from presto.inference import classify_with_catboost # Run catboost classification self.logger.info("Catboost classification") From df8750998052f5ac75c16f08b830e7adfc9a53ce Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Tue, 11 Jun 2024 13:42:21 +0200 Subject: [PATCH 30/31] Now working with dependency as zip file and presto code packed as wheel file --- scripts/inference/cropland_mapping.py | 7 +-- src/worldcereal/openeo/feature_extractor.py | 44 +++++++++----- src/worldcereal/openeo/inference.py | 64 ++++++++++++++------- 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/scripts/inference/cropland_mapping.py b/scripts/inference/cropland_mapping.py index 39791444..1fece2fd 100644 --- a/scripts/inference/cropland_mapping.py +++ b/scripts/inference/cropland_mapping.py @@ -58,8 +58,7 @@ backend_context = BackendContext(Backend.FED) connection = openeo.connect( - # "https://openeo.creo.vito.be/openeo/" - "https://openeo-staging.creo.vito.be/openeo/" + "https://openeo.creo.vito.be/openeo/" ).authenticate_oidc() # Preparing the input cube for the inference @@ -111,7 +110,7 @@ out_format="NetCDF", job_options={ "driver-memory": "4g", - "executor-memoryOverhead": "8g", - "logging-threshold": "debug", + "executor-memoryOverhead": "12g", + "udf-dependency-archives": [f"{ONNX_DEPS_URL}#onnx_deps"], }, ) diff --git a/src/worldcereal/openeo/feature_extractor.py b/src/worldcereal/openeo/feature_extractor.py index 193657bb..79ed8fe6 100644 --- a/src/worldcereal/openeo/feature_extractor.py +++ b/src/worldcereal/openeo/feature_extractor.py @@ -18,9 +18,10 @@ class PrestoFeatureExtractor(PatchFeatureExtractor): specified, should be set as `False`. """ - PRESTO_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA + PRESTO_MODEL_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA + PRESO_WHL_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.1.0-temp-py3-none-any.whl" BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA - DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + DEPENDENCY_NAME = "worldcereal_deps.zip" GFMAP_BAND_MAPPING = { "S2-L2A-B02": "B02", @@ -40,12 +41,18 @@ class PrestoFeatureExtractor(PatchFeatureExtractor): "AGERA5-PRECIP": "precipitation-flux", } - def dependencies(self) -> list: - """Gives the presto dependencies from a wheel with all it's subdependencies.""" - return [ - "torch @ https://download.pytorch.org/whl/cpu/torch-2.0.0%2Bcpu-cp38-cp38-linux_x86_64.whl#sha256=354f281351cddb590990089eced60f866726415f7b287db5105514aa3c5f71ca", - "presto @ https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.0.1-py3-none-any.whl", - ] + def unpack_presto_wheel(self, wheel_url: str, destination_dir: str) -> list: + import urllib.request + import zipfile + from pathlib import Path + + # Downloads the wheel file + modelfile, _ = urllib.request.urlretrieve( + wheel_url, filename=Path.cwd() / Path(wheel_url).name + ) + with zipfile.ZipFile(modelfile, "r") as zip_ref: + zip_ref.extractall(destination_dir) + return destination_dir def output_labels(self) -> list: """Returns the output labels from this UDF, which is the output labels @@ -54,13 +61,17 @@ def output_labels(self) -> list: def execute(self, inarr: xr.DataArray) -> xr.DataArray: import sys + from pathlib import Path if self.epsg is None: raise ValueError( "EPSG code is required for Presto feature extraction, but was " "not correctly initialized." ) - presto_url = self._parameters.get("presto_url", self.PRESTO_URL) + presto_model_url = self._parameters.get( + "presto_model_url", self.PRESTO_MODEL_URL + ) + presto_wheel_url = self._parameters.get("presot_wheel_url", self.PRESO_WHL_URL) # The below is required to avoid flipping of the result # when running on OpenEO backend! @@ -76,16 +87,21 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: inarr = inarr.fillna(65535) # Unzip de dependencies on the backend - # self.logger.info("Unzipping dependencies") - # deps_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + self.logger.info("Unzipping dependencies") + deps_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + self.logger.info("Unpacking presto wheel") + deps_dir = self.unpack_presto_wheel(presto_wheel_url, deps_dir) + + self.logger.info("Appending dependencies") + sys.path.append(str(deps_dir)) - # self.logger.info("Appending dependencies") - # sys.path.append(str(deps_dir)) + # Debug, print the dependency directory + self.logger.info(f"Dependency directory: {list(Path(deps_dir).iterdir())}") from presto.inference import get_presto_features self.logger.info("Extracting presto features") - features = get_presto_features(inarr, presto_url, self.epsg) + features = get_presto_features(inarr, presto_model_url, self.epsg) return features def _execute(self, cube: XarrayDataCube, parameters: dict) -> XarrayDataCube: diff --git a/src/worldcereal/openeo/inference.py b/src/worldcereal/openeo/inference.py index 34c4c4c3..e1adf289 100644 --- a/src/worldcereal/openeo/inference.py +++ b/src/worldcereal/openeo/inference.py @@ -15,41 +15,61 @@ class CroplandClassifier(ModelInference): the public Presto model. """ + import numpy as np + CATBOOST_PATH = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/wc_catboost.onnx" # NOQA - BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA - DEPENDENCY_NAME = "wc_presto_onnx_dependencies.zip" + + def __init__(self): + super().__init__() + + self.onnx_session = None def dependencies(self) -> list: - """Gives the presto dependencies from a wheel with all it's subdependencies.""" - return [ - "onnxruntime", - "torch @ https://download.pytorch.org/whl/cpu/torch-2.0.0%2Bcpu-cp38-cp38-linux_x86_64.whl#sha256=354f281351cddb590990089eced60f866726415f7b287db5105514aa3c5f71ca", - "presto @ https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.0.1-py3-none-any.whl", - ] + return [] # Disable the dependencies from PIP install def output_labels(self) -> list: return ["classification"] - def execute(self, inarr: xr.DataArray) -> xr.DataArray: - import sys + def predict(self, features: np.ndarray) -> np.ndarray: + """ + Predicts labels using the provided features array. + """ + import numpy as np - classifier_url = self._parameters.get("classifier_url", self.CATBOOST_PATH) + if self.onnx_session is None: + raise ValueError("Model has not been loaded. Please load a model first.") - # shape and indiches for output - inarr = inarr.transpose("bands", "x", "y") + # Prepare input data for ONNX model + outputs = self.onnx_session.run(None, {"features": features}) - # Unzip de dependencies on the backend - # self.logger.info("Unzipping dependencies") - # dep_dir = self.extract_dependencies(self.BASE_URL, self.DEPENDENCY_NAME) + # Threshold for binary conversion + threshold = 0.5 + + # Extract all prediction values and convert them to binary labels + prediction_values = [sublist["True"] for sublist in outputs[1]] + binary_labels = np.array(prediction_values) >= threshold + binary_labels = binary_labels.astype(int) + + return binary_labels + + def execute(self, inarr: xr.DataArray) -> xr.DataArray: + classifier_url = self._parameters.get("classifier_url", self.CATBOOST_PATH) - # self.logger.info("Adding dependencies") - # sys.path.append(str(dep_dir)) + # shape and indices for output ("xy", "bands") + x_coords, y_coords = inarr.x.values, inarr.y.values + inarr = inarr.transpose("bands", "x", "y").stack(xy=["x", "y"]).transpose() - from presto.inference import classify_with_catboost + self.onnx_session = self.load_ort_session(classifier_url) # Run catboost classification - self.logger.info("Catboost classification") - classification = classify_with_catboost(inarr, classifier_url) - self.logger.info("Done") + self.logger.info(f"Catboost classification with input shape: {inarr.shape}") + classification = self.predict(inarr.values) + self.logger.info(f"Classification done with shape: {classification.shape}") + + classification = xr.DataArray( + classification.reshape((1, len(x_coords), len(y_coords))), + dims=["bands", "x", "y"], + coords={"bands": ["classification"], "x": x_coords, "y": y_coords}, + ) return classification From 20746f48dcafc4a0e1cf191fd4e6684251b8d0d3 Mon Sep 17 00:00:00 2001 From: Darius Couchard Date: Wed, 12 Jun 2024 09:53:16 +0200 Subject: [PATCH 31/31] Changed dependencies .zip file --- scripts/inference/cropland_mapping.py | 2 +- src/worldcereal/openeo/feature_extractor.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/inference/cropland_mapping.py b/scripts/inference/cropland_mapping.py index 1fece2fd..03eb7d6c 100644 --- a/scripts/inference/cropland_mapping.py +++ b/scripts/inference/cropland_mapping.py @@ -29,7 +29,7 @@ "--epsg", type=int, default=4326, - help="EPSG code for coordiante reference system.", + help="EPSG code of the input `minx`, `miny`, `maxx`, `maxy` parameters.", ) parser.add_argument( "start_date", type=str, help="Starting date for data extraction." diff --git a/src/worldcereal/openeo/feature_extractor.py b/src/worldcereal/openeo/feature_extractor.py index 79ed8fe6..19037055 100644 --- a/src/worldcereal/openeo/feature_extractor.py +++ b/src/worldcereal/openeo/feature_extractor.py @@ -19,7 +19,7 @@ class PrestoFeatureExtractor(PatchFeatureExtractor): """ PRESTO_MODEL_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal-minimal-inference/presto.pt" # NOQA - PRESO_WHL_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.1.0-temp-py3-none-any.whl" + PRESO_WHL_URL = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/worldcereal/dependencies/presto_worldcereal-0.1.0-py3-none-any.whl" BASE_URL = "https://s3.waw3-1.cloudferro.com/swift/v1/project_dependencies" # NOQA DEPENDENCY_NAME = "worldcereal_deps.zip" @@ -96,9 +96,11 @@ def execute(self, inarr: xr.DataArray) -> xr.DataArray: sys.path.append(str(deps_dir)) # Debug, print the dependency directory - self.logger.info(f"Dependency directory: {list(Path(deps_dir).iterdir())}") + self.logger.info("Dependency directory: %s", list(Path(deps_dir).iterdir())) - from presto.inference import get_presto_features + from presto.inference import ( # pylint: disable=import-outside-toplevel + get_presto_features, + ) self.logger.info("Extracting presto features") features = get_presto_features(inarr, presto_model_url, self.epsg)