From f9dc2ede20914313a1d79efdb35822d22e6ff6a7 Mon Sep 17 00:00:00 2001 From: AKSHARVAGHASIYA Date: Sun, 12 Apr 2026 17:56:13 +0530 Subject: [PATCH] Feat #230: Add temperature and humidity GEE pipeline and API --- computing/api.py | 29 +++++ computing/temperature_humidity/__init__.py | 1 + computing/temperature_humidity/temp_humid.py | 127 +++++++++++++++++++ computing/urls.py | 5 + 4 files changed, 162 insertions(+) create mode 100644 computing/temperature_humidity/__init__.py create mode 100644 computing/temperature_humidity/temp_humid.py diff --git a/computing/api.py b/computing/api.py index 58ff16f2..c02b8d14 100644 --- a/computing/api.py +++ b/computing/api.py @@ -32,6 +32,7 @@ from .cropping_intensity.cropping_intensity import generate_cropping_intensity from .surface_water_bodies.swb import generate_swb_layer from .drought.drought import calculate_drought +from .temperature_humidity.temp_humid import calculate_temperature_humidity from .terrain_descriptor.terrain_clusters import generate_terrain_clusters from .terrain_descriptor.terrain_raster_fabdem import generate_terrain_raster_clip from computing.misc.drainage_lines import clip_drainage_lines @@ -545,6 +546,34 @@ def generate_drought_layer(request): print("Exception in generate_drought_layer api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@api_view(["POST"]) +@schema(None) +def generate_temperature_humidity_layer(request): + print("Inside generate_temperature_humidity_layer") + try: + state = request.data.get("state") + district = request.data.get("district") + block = request.data.get("block") + year = request.data.get("year", 2023) + gee_account_id = request.data.get("gee_account_id") + calculate_temperature_humidity.apply_async( + kwargs={ + "state": state, + "district": district, + "block": block, + "year": year, + "gee_account_id": gee_account_id, + }, + queue="nrm", + ) + return Response( + {"Success": "generate_temperature_humidity_layer task initiated"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in generate_temperature_humidity_layer api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @api_view(["POST"]) @schema(None) diff --git a/computing/temperature_humidity/__init__.py b/computing/temperature_humidity/__init__.py new file mode 100644 index 00000000..a6131c10 --- /dev/null +++ b/computing/temperature_humidity/__init__.py @@ -0,0 +1 @@ +# init diff --git a/computing/temperature_humidity/temp_humid.py b/computing/temperature_humidity/temp_humid.py new file mode 100644 index 00000000..950b953c --- /dev/null +++ b/computing/temperature_humidity/temp_humid.py @@ -0,0 +1,127 @@ +import ee +from computing.utils import ( + sync_fc_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from utilities.constants import GEE_PATHS +from utilities.gee_utils import ( + ee_initialize, + valid_gee_text, + get_gee_dir_path, + is_gee_asset_exists, + make_asset_public, + check_task_status +) +from computing.models import Dataset +from nrm_app.celery import app + +@app.task(bind=True) +def calculate_temperature_humidity( + self, + state=None, + district=None, + block=None, + app_type="MWS", + year=2023, + gee_account_id=None, +): + """ + Generate coarse field level (~5km) temperature and humidity + raster layers and vectorize them onto the MWS ROI. + """ + ee_initialize(gee_account_id) + + asset_suffix = valid_gee_text(district.lower()) + "_" + valid_gee_text(block.lower()) + asset_folder_list = [state, district, block] + + roi_path = ( + get_gee_dir_path( + asset_folder_list, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"] + ) + + f"filtered_mws_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_uid" + ) + + roi = ee.FeatureCollection(roi_path) + + # MOD11C3 provides monthly LST at 0.05 degrees (~5km) + modis_lst = ee.ImageCollection('MODIS/061/MOD11C3') \ + .filterDate(f'{year}-01-01', f'{year}-12-31') \ + .select('LST_Day_CMG') \ + .mean() + + # Convert LST from Kelvin to Celsius (scale factor is 0.02) + # Formula: (DN * 0.02) - 273.15 + temperature = modis_lst.multiply(0.02).subtract(273.15).rename('mean_temperature') + + # ERA5 Land Monthly Aggregated for Humidity + era5 = ee.ImageCollection('ECMWF/ERA5_LAND/MONTHLY_AGGR') \ + .filterDate(f'{year}-01-01', f'{year}-12-31') \ + .mean() + + t = era5.select('temperature_2m').subtract(273.15) + td = era5.select('dewpoint_temperature_2m').subtract(273.15) + + # August-Roche-Magnus approximation for Relative Humidity + # RH = 100 * (exp((17.625 * Td) / (243.04 + Td)) / exp((17.625 * T) / (243.04 + T))) + expr = '100 * (exp((17.625 * Td) / (243.04 + Td)) / exp((17.625 * T) / (243.04 + T)))' + humidity = era5.expression(expr, { + 'T': t, 'Td': td + }).rename('mean_humidity') + + composite = temperature.addBands(humidity).clip(roi.geometry()) + + # Calculate Mean Temperature and Humidity per ROI polygon + reduced = composite.reduceRegions(**{ + 'collection': roi, + 'reducer': ee.Reducer.mean(), + 'scale': 5000, + }) + + # Add Area + def add_area(f): + area_km2 = f.geometry().area().divide(1e6) + return f.set('Area_km2', area_km2) + + final_fc = reduced.map(add_area) + + dst_filename = f"temp_humid_{asset_suffix}_{year}" + asset_id = ( + get_gee_dir_path( + asset_folder_list, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"] + ) + + dst_filename + ) + + if not is_gee_asset_exists(asset_id): + task = ee.batch.Export.table.toAsset(**{ + 'collection': final_fc, + 'description': f'TempHumid_{asset_suffix}_{year}', + 'assetId': asset_id + }) + task.start() + print(f"Started Export Table Task: {task.id}") + check_task_status([task.id]) + + make_asset_public(asset_id) + + layer_name = f"{asset_suffix}_temp_humid_{year}" + + dataset, _ = Dataset.objects.get_or_create( + name="Temperature_Humidity", + defaults={'layer_type': 'vector', 'workspace': 'climate'} + ) + + layer_id = save_layer_info_to_db( + state, district, block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name=dataset.name, + algorithm="TEMP_HUMID_ALGORITHM", + ) + + res = sync_fc_to_geoserver(ee.FeatureCollection(asset_id), state, layer_name, dataset.workspace) + if res and type(res) == dict and res.get("status_code") == 201 and layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return True diff --git a/computing/urls.py b/computing/urls.py index 4071f964..f6a4ebb1 100644 --- a/computing/urls.py +++ b/computing/urls.py @@ -35,6 +35,11 @@ api.generate_drought_layer, name="generate_drought_layer", ), + path( + "generate_temperature_humidity_layer/", + api.generate_temperature_humidity_layer, + name="generate_temperature_humidity_layer", + ), path( "generate_terrain_descriptor/", api.generate_terrain_descriptor,