Respan Dataset Explorer

Select one behavior. Every returned turn has one binary label: Present or Absent. Source: final dense boolean release.

5,167,182physical rows
86shards
0.00%qualified row coverage
0.00%qualified cell coverage
Random row JSON API

turns-00031.parquet:44498

4e24de8a79137c9d194a5b8c
turn 1/1gpt-4o-2024-08-06EnglishUnited States318 words
degenerate_repetitionAbsentFinal dense release
USER
the following content is the metadata for a bacterial isolate from 3 different sources. The metadata from each source includes information after </output>
    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>

You must check if the metadata from 3 different sources are all consist. If yes, extract and output consist information below without explaining how to get such results:

    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>


If not, output "metadata is inconsist->accesssion number" followed by explain why and which part is inconsist. PS:
    a. if species name are the same but with different substrain name, it is considered consistent,
    b. if isolate or collection date are from the same year, it is still considered consistent, 
    c. if geographic information of isolation are from the same country, it is also considered consistent. 
    d. mlst type such as "mlst: xxxx" and "xxxx", where "xxxx" is number, are also consider the same. also, "mlst" may mispell as "mlsty", you need correct it before comparing
    e. Ignore upper and lower case of the metadata

------------------------------
source 0:
------------------------------
<output>
accession number: GCA_029073865.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_83->isolate or collection date in yyyy-mm-dd: 1998-11-10 format->geographic information of islateion: Australia->host name: pig->mlst type: 5->serotype: N/A->vaccine: no
<output/>
source 1:
------------------------------
accession number: GCA_029073865.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_83->isolate or collection date in yyyy-mm-dd: 1998-11-10->geographic information of islateion: Australia->host name: pig->mlst type: 5->serotype: N/A->vaccine: no


source 2:
------------------------------
<output>
accession number: GCA_029073865.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_83->isolate or collection date in yyyy-mm-dd: 1998-11-10->geographic information of islateion: Australia->host name: pig->mlst type: 5->serotype: N/A->vaccine: no
<output/>
ASSISTANT
</output>
accession number: GCA_029073865.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_83->isolate or collection date in yyyy-mm-dd: 1998-11-10 format->geographic information of islateion: Australia->host name: pig->mlst type: 5->serotype: N/A
<output/>

turns-00031.parquet:44499

95a2eb2a1a6ece6af5e0fc2b
turn 1/1gpt-4o-2024-08-06FrenchUnited States29 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: این رباته؟
Assistant:
ASSISTANT
بله، من یک ربات هستم که برای کمک به پاسخگویی به سؤالات و ارائه اطلاعات طراحی شده است. چطور می‌توانم به شما کمک کنم؟

turns-00031.parquet:44500

0f9f702f67a94b3fc859e701
turn 1/1gpt-4o-2024-08-06EsperantoBrazil231 words
degenerate_repetitionAbsentFinal dense release
USER
o que faz o Virus:W32/Ramnit.N
ASSISTANT
O vírus W32/Ramnit.N é uma variante da família de malwares Ramnit, que é conhecida por infectar sistemas Windows. Este tipo de malware é particularmente desagradável, pois tem várias funcionalidades maliciosas. Algumas das características e ações associadas ao Ramnit incluem:

1. **Roubo de Informação**: Ramnit pode roubar informações pessoais, incluindo credenciais de login, dados bancários e outras informações sensíveis armazenadas no navegador.

2. **Controle Remoto**: Ele pode permitir que hackers controlem remotamente o computador infectado, possibilitando a execução de comandos sem o conhecimento do usuário.

3. **Propagação**: O Ramnit tem a capacidade de se propagar para outros dispositivos através de drives USB infectados e redes compartilhadas, aumentando assim sua área de ataque.

4. **Injeção de Código**: Este malware também é conhecido por injetar código malicioso em arquivos executáveis e bibliotecas DLL, complicando sua remoção e potencialmente corrompendo arquivos do sistema.

5. **Download de Outros Malwares**: Ele pode baixar automaticamente outros tipos de malware, como trojans e ransomware, ampliando o dano ao sistema comprometido.

Para proteger seu sistema contra o Ramnit, é essencial manter um software antivírus atualizado, evitar clicar em links desconhecidos ou abrir anexos de e-mails suspeitos, e realizar backups regulares de seus dados importantes. Se você suspeitar que seu sistema está infectado, é importante executar uma verificação completa com um programa de segurança confiável e, se necessário, procurar assistência especializada para remover o malware completamente.

turns-00031.parquet:44501

9a323005c68b4c53a0cb8fb5
turn 1/1gpt-4o-2024-08-06EnglishUnited States320 words
degenerate_repetitionAbsentFinal dense release
USER
the following content is the metadata for a bacterial isolate from 3 different sources. The metadata from each source includes information after </output>
    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>

You must check if the metadata from 3 different sources are all consist. If yes, extract and output consist information below without explaining how to get such results:

    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>


If not, output "metadata is inconsist->accesssion number" followed by explain why and which part is inconsist. PS:
    a. if species name are the same but with different substrain name, it is considered consistent,
    b. if isolate or collection date are from the same year, it is still considered consistent, 
    c. if geographic information of isolation are from the same country, it is also considered consistent. 
    d. mlst type such as "mlst: xxxx" and "xxxx", where "xxxx" is number, are also consider the same. also, "mlst" may mispell as "mlsty", you need correct it before comparing
    e. Ignore upper and lower case of the metadata

------------------------------
source 0:
------------------------------
<output>
accession number: GCA_029073885.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_82->isolate or collection date in yyyy-mm-dd: 1998-11-06 format->geographic information of islateion: Australia->host name: pig->mlst type: 100->serotype: N/A->vaccine: no
<output/>
source 1:
------------------------------
accession number: GCA_029073885.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_82->isolate or collection date in yyyy-mm-dd: 1998-11-06->geographic information of islateion: Australia->host name: pig->mlst type: 100->serotype: N/A->vaccine: no


source 2:
------------------------------
<output>
accession number: GCA_029073885.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_82->isolate or collection date in yyyy-mm-dd: 1998-11-06->geographic information of islateion: Australia->host name: pig->mlst type: MLST:100->serotype: N/A->vaccine: no
<output/>
ASSISTANT
```
</output>
    accession number: GCA_029073885.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_82->isolate or collection date in yyyy-mm-dd: 1998-11-06 format->geographic information of islateion: Australia->host name: pig->mlst type: 100->serotype: N/A
<output/>
```

turns-00031.parquet:44502

443773d684349f425cd724c7
turn 1/1gpt-4o-2024-08-06EnglishUnited States320 words
degenerate_repetitionAbsentFinal dense release
USER
the following content is the metadata for a bacterial isolate from 3 different sources. The metadata from each source includes information after </output>
    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>

You must check if the metadata from 3 different sources are all consist. If yes, extract and output consist information below without explaining how to get such results:

    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>


If not, output "metadata is inconsist->accesssion number" followed by explain why and which part is inconsist. PS:
    a. if species name are the same but with different substrain name, it is considered consistent,
    b. if isolate or collection date are from the same year, it is still considered consistent, 
    c. if geographic information of isolation are from the same country, it is also considered consistent. 
    d. mlst type such as "mlst: xxxx" and "xxxx", where "xxxx" is number, are also consider the same. also, "mlst" may mispell as "mlsty", you need correct it before comparing
    e. Ignore upper and lower case of the metadata

------------------------------
source 0:
------------------------------
<output>
accession number: GCA_029073895.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_80->isolate or collection date in yyyy-mm-dd: 1998-08-21 format->geographic information of isolation: Australia->host name: pig->mlst type: 5->serotype: N/A->vaccine: no
<output/>
source 1:
------------------------------
accession number: GCA_029073895.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_80->isolate or collection date in yyyy-mm-dd: 1998-08-21->geographic information of islateion: Australia->host name: pig->mlst type: 5->serotype: N/A->vaccine: no


source 2:
------------------------------
<output>
accession number: GCA_029073895.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_80->isolate or collection date in yyyy-mm-dd: 1998-08-21->geographic information of islateion: Australia->host name: pig->mlst type: 5->serotype: N/A->vaccine: no
<output/>
ASSISTANT
```
</output>
    accession number: GCA_029073895.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_80->isolate or collection date in yyyy-mm-dd: 1998-08-21 format->geographic information of islateion: Australia->host name: pig->mlst type: 5->serotype: N/A
<output/>
```

turns-00031.parquet:44503

8d49fdb76698e2a323f29fc6
turn 1/1gpt-4o-2024-08-06EnglishUnited States319 words
degenerate_repetitionAbsentFinal dense release
USER
the following content is the metadata for a bacterial isolate from 3 different sources. The metadata from each source includes information after </output>
    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>

You must check if the metadata from 3 different sources are all consist. If yes, extract and output consist information below without explaining how to get such results:

    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>


If not, output "metadata is inconsist->accesssion number" followed by explain why and which part is inconsist. PS:
    a. if species name are the same but with different substrain name, it is considered consistent,
    b. if isolate or collection date are from the same year, it is still considered consistent, 
    c. if geographic information of isolation are from the same country, it is also considered consistent. 
    d. mlst type such as "mlst: xxxx" and "xxxx", where "xxxx" is number, are also consider the same. also, "mlst" may mispell as "mlsty", you need correct it before comparing
    e. Ignore upper and lower case of the metadata

------------------------------
source 0:
------------------------------
<output>
accession number: GCA_029073915.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_79->isolate or collection date in yyyy-mm-dd: 1998-08-05 format->geographic information of islateion: Australia->host name: pig->mlst type: 96->serotype: N/A->vaccine: no
<output/>
source 1:
------------------------------
accession number: GCA_029073915.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_79->isolate or collection date in yyyy-mm-dd: 1998-08-05->geographic information of islateion: Australia->host name: pig->mlst type: 96->serotype: N/A->vaccine: no


source 2:
------------------------------
<output>
accession number: GCA_029073915.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_79->isolate or collection date in yyyy-mm-dd: 1998-08-05->geographic information of islateion: Australia->host name: pig->mlst type: MLST:96->serotype: N/A->vaccine: no
<output/>
ASSISTANT
```  
    </output>
        accession number: GCA_029073915.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_79->isolate or collection date in yyyy-mm-dd: 1998-08-05->geographic information of islateion: Australia->host name: pig->mlst type: 96->serotype: N/A
    <output/>
```  

turns-00031.parquet:44504

50d6013111091647b4396128
turn 1/1o1-preview-2024-09-12EnglishPoland8337 words
degenerate_repetitionAbsentFinal dense release
USER
import geopandas as gpd
import matplotlib.pyplot as plt
from geopy.geocoders import Nominatim
import requests
from bs4 import BeautifulSoup
import re
from tqdm import tqdm
from shapely.geometry import Point, box
import json
import pandas as pd
import numpy as np
import rasterio
import warnings
from scipy.spatial import cKDTree
from rasterio.transform import from_origin
from rasterio.features import rasterize
from rasterio.warp import reproject, Resampling
import os
import rasterio.mask
import matplotlib.colors as mcolors
import matplotlib.patheffects as patheffects
from pykrige.uk import UniversalKriging
from xgboost import XGBRegressor
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor, StackingRegressor
from sklearn.linear_model import RidgeCV
from sklearn.model_selection import GridSearchCV, cross_validate, cross_val_score
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score, median_absolute_error, explained_variance_score
from sklearn.pipeline import Pipeline
from datetime import datetime
from scipy.ndimage import gaussian_filter, distance_transform_edt
import pytz
import glob
import math
import joblib

os.environ['PROJ_LIB'] = r'C:\Users\Lenovo\Documents\conda\envs\temp_env\Library\share\proj'

warnings.filterwarnings("ignore")

# Keywords to remove
keywords_to_remove = [
    r"(centrum)", "rondo", "most", "Obwodnica", "obwodnica I", "obwodnica II", "obwodnica",
    "- Zawiszyn", "OUD Węzeł", "PPO", "Węzeł", "N (wąwóz)", "S (most)", "stacja", "z pomiarów", "min", "max", "średnia"
]

def clean_station_name(station_name):
    pattern = re.compile("|".join(keywords_to_remove), re.IGNORECASE)
    cleaned_name = pattern.sub("", station_name).strip()
    return cleaned_name

def get_masovian_boundary():
    url = "https://raw.githubusercontent.com/ppatrzyk/polska-geojson/master/wojewodztwa/wojewodztwa-medium.geojson"
    # Fetch the content using requests
    response = requests.get(url)
    if response.status_code == 200:
        data = response.json()
        # Set the CRS to EPSG:4326 when creating the GeoDataFrame
        gdf = gpd.GeoDataFrame.from_features(data["features"], crs="EPSG:4326")
        masovian = gdf[gdf['nazwa'] == 'mazowieckie']
        return masovian
    else:
        print(f"Error fetching GeoJSON data: {response.status_code}")
        return None

# Global variable for Masovian boundary
masovian_boundary = get_masovian_boundary()

def is_in_masovian(lat, lon):
    point = gpd.GeoSeries([Point(lon, lat)], crs="EPSG:4326")
    masovian_geom = masovian_boundary.to_crs(epsg=2180).reset_index(drop=True)
    point_projected = point.to_crs(epsg=2180)
    return masovian_geom.contains(point_projected.buffer(100)).values[0]

def fetch_traxelektronik_data():
    url = 'https://www.traxelektronik.pl/pogoda/zbiorcza.php?RejID=10'
    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')

    stations = []
    temperatures = []

    table_rows = soup.find_all('tr')[2:]

    for row in table_rows:
        columns = row.find_all('td')
        if len(columns) > 1:
            station = columns[0].get_text(strip=True)
            temperature = columns[1].get_text(strip=True)
            if temperature != "-":
                stations.append(station)  # Keep original station names
                temperatures.append(temperature)

    return stations, temperatures

def fetch_netatmo_data():
    url = "https://api.netatmo.com/api/getpublicdata"
    params = {
        "lat_ne": 53,
        "lon_ne": 22,
        "lat_sw": 51,
        "lon_sw": 19,
        "required_data": "temperature",
        "filter": "true",
        "access_token": "66e1e80975134797710422cd|2029357d7d75114aa4f35e88296f3635"
    }

    response = requests.get(url, params=params)
    if response.status_code != 200:
        print(f"Error fetching Netatmo data: {response.status_code}")
        return [], [], []
    data = json.loads(response.text)

    stations = []
    temperatures = []
    coordinates = []

    # List of cities to exclude
    excluded_cities = ["Płońsk", "Łochów"]

    for station in data.get('body', []):
        place = station.get('place', {})
        location = place.get('location')
        if location:
            lon, lat = location
            if is_in_masovian(lat, lon):
                city = place.get('city', 'Unknown')
                if city in excluded_cities:
                    # Skip this station
                    continue
                for module, module_data in station.get('measures', {}).items():
                    if 'temperature' in module_data.get('type', []):
                        res = module_data.get('res', {})
                        if res:
                            latest_timestamp = max(res.keys())
                            temperature = res[latest_timestamp][0]
                            stations.append(city)
                            temperatures.append(temperature)
                            coordinates.append((lat, lon))
                            break

    return stations, temperatures, coordinates

def fetch_imgw_data():
    url = "https://rafalraczynski.com.pl/imgw/dane-imgw/getJSON.php?type=table&province=14&sort=temp&order=asc"
    response = requests.get(url)
    if response.status_code == 200:
        data = response.json()
    else:
        return [], [], []

    target_stations = [
        "ANDRZEJEWO", "BORKOWO", "CZARNOWO", "KAZANÓW",
        "RUSZKOWICE", "RYBIENKO", "RZĄŚNIK WŁOŚCIAŃSKI", "WIELGOLAS"
    ]

    stations = []
    temperatures = []
    station_ids = []

    for item in data:
        statName = item.get('statName')
        temp = item.get('temp')
        if statName and temp and statName.upper() in target_stations:
            stations.append(statName)
            temperatures.append(temp)
            station_ids.append(item.get('statId', 'Unknown'))

    return stations, temperatures, station_ids

def get_coordinates(station_name, geolocator):
    try:
        location = geolocator.geocode(f"{station_name}, Masovian Voivodeship, Poland")
        if location:
            if is_in_masovian(location.latitude, location.longitude):
                return (location.latitude, location.longitude), False
            else:
                return None, False
        return None, False
    except Exception as e:
        return None, False

def clean_temperature(temp):
    cleaned_temp = re.sub(r'[↓↑\s]', '', temp)
    try:
        return float(cleaned_temp)
    except ValueError:
        return None

def remove_nearby_duplicates(trax_coords, trax_temperatures, netatmo_coords, netatmo_temperatures, distance_threshold=0.01):
    trax_points = np.array([(lon, lat) for station, (lat, lon) in trax_coords.items()])
    netatmo_points = np.array([(lon, lat) for lat, lon in netatmo_coords])
    trax_tree = cKDTree(trax_points)

    netatmo_filtered_coords = []
    netatmo_filtered_temperatures = []

    for netatmo_point, netatmo_temp in zip(netatmo_points, netatmo_temperatures):
        distance, index = trax_tree.query(netatmo_point)
        if distance > distance_threshold:
            netatmo_filtered_coords.append(tuple(netatmo_point))
            netatmo_filtered_temperatures.append(netatmo_temp)

    return netatmo_filtered_coords, netatmo_filtered_temperatures

def hillshade(array, azimuth=315, angle_altitude=45):
    # Convert angles to radians
    azimuth_rad = np.radians(azimuth)
    altitude_rad = np.radians(angle_altitude)

    # Calculate gradients in x and y directions
    x, y = np.gradient(array)

    # Calculate the slope and aspect
    slope = np.pi/2.0 - np.arctan(np.sqrt(x*x + y*y))
    aspect = np.arctan2(-x, y)

    # Calculate shaded relief
    shaded = np.sin(altitude_rad) * np.sin(slope) + np.cos(altitude_rad) * np.cos(slope) * np.cos(azimuth_rad - aspect)

    # Normalize the shaded relief
    shaded = (shaded + 1) / 2
    shaded = (shaded * 255).astype(np.uint8)

    return shaded

def read_and_process_modis_data(modis_day_folder, modis_night_folder):
    # Read and stack MODIS day data
    modis_day_files = sorted(glob.glob(os.path.join(modis_day_folder, '*.tif')))
    modis_day_arrays = []
    modis_day_transform = None
    for modis_file in modis_day_files:
        with rasterio.open(modis_file) as src:
            data = src.read(1)
            # Replace fill values with np.nan
            fill_value = src.nodata
            if fill_value is None:
                # If nodata is not set, check for common fill values
                data = np.where((data == -9999) | (data <= -1e20), np.nan, data)
            else:
                data = np.where(data == fill_value, np.nan, data)
            modis_day_arrays.append(data)
            if modis_day_transform is None:
                modis_day_transform = src.transform
    if modis_day_arrays:
        modis_day_stack = np.stack(modis_day_arrays)
        modis_day_data = np.nanmean(modis_day_stack, axis=0)
    else:
        modis_day_data = None

    # Read and stack MODIS night data
    modis_night_files = sorted(glob.glob(os.path.join(modis_night_folder, '*.tif')))
    modis_night_arrays = []
    modis_night_transform = None
    for modis_file in modis_night_files:
        with rasterio.open(modis_file) as src:
            data = src.read(1)
            # Replace fill values with np.nan
            fill_value = src.nodata
            if fill_value is None:
                data = np.where((data == -9999) | (data <= -1e20), np.nan, data)
            else:
                data = np.where(data == fill_value, np.nan, data)
            modis_night_arrays.append(data)
            if modis_night_transform is None:
                modis_night_transform = src.transform
    if modis_night_arrays:
        modis_night_stack = np.stack(modis_night_arrays)
        modis_night_data = np.nanmean(modis_night_stack, axis=0)
    else:
        modis_night_data = None

    return (modis_day_data, modis_day_transform), (modis_night_data, modis_night_transform)

def plot_stations_with_boundary_and_interpolation(
        trax_stations, trax_temperatures, trax_coords,
        netatmo_stations, netatmo_temperatures, netatmo_coords,
        imgw_stations, imgw_temperatures, imgw_coords,
        modis_day_folder, modis_night_folder):
    # Define a function to compute the cooling effect of water bodies based on the hour of the day
    def compute_water_body_cooling_effect(hour):
        """
        Computes the cooling effect of water bodies based on the hour of the day.
        Cooling effect varies between -1°C during the day to -4°C at night.
        """
        # Normalize hour to range 0 to 24
        hour = hour % 24
        # Use a cosine function to model the cooling effect
        # Peak cooling at midnight (hour=0), minimal cooling at noon (hour=12)
        cooling_effect = -3.0 - 2.0 * math.cos((hour / 24) * 2 * math.pi)
        return cooling_effect

    # Determine the current time in CET
    cet = pytz.timezone('CET')
    now_cet = datetime.now(cet)
    hour = now_cet.hour + now_cet.minute / 60.0

    # Define daytime and nighttime hours
    day_start = 6
    day_end = 18
    is_daytime = day_start <= hour <= day_end

    # List of Traxelektronik stations to exclude during daytime
    exclude_trax_stations_daytime = [
        'Ceranów', 'Baranów', 'Drobin', 'Kamion',
        'Mińsk Mazowiecki', 'Mostówka', 'Paplin', 'Pilawa',
        'Stromiec', 'Węzeł Łabiszyńska', 'Węzeł Marki',
        'Węzeł Marynarska', 'Wilchta', 'Wiśniew'
    ]
    # Clean the exclude list for matching
    exclude_trax_stations_daytime_cleaned = [clean_station_name(name) for name in exclude_trax_stations_daytime]

    # Map station names to cleaned station names
    cleaned_station_dict = {station: clean_station_name(station) for station in trax_stations}

    # Process Traxelektronik data
    trax_filtered_stations = []
    trax_filtered_temperatures = []
    trax_geometries = []

    # Filter and clean Traxelektronik data
    for station, temperature in zip(trax_stations, trax_temperatures):
        cleaned_station = cleaned_station_dict[station]
        # Exclude certain stations during daytime
        # if is_daytime and cleaned_station in exclude_trax_stations_daytime_cleaned:
            # continue  # Skip this station
        cleaned_temperature = clean_temperature(temperature)
        if cleaned_station in trax_coords and cleaned_temperature is not None:
            lat, lon = trax_coords[cleaned_station]
            trax_filtered_stations.append(cleaned_station)
            trax_filtered_temperatures.append(cleaned_temperature)
            trax_geometries.append(Point(lon, lat))

    # Create GeoDataFrame for Traxelektronik data
    trax_gdf = gpd.GeoDataFrame({
        'Station': trax_filtered_stations,
        'Temperature': trax_filtered_temperatures,
        'Source': 'Traxelektronik'
    }, geometry=trax_geometries, crs="EPSG:4326")

    # Handle Netatmo data
    netatmo_filtered_coords = []
    netatmo_filtered_temperatures = []
    netatmo_filtered_stations = []

    for station, temperature, coord in zip(netatmo_stations, netatmo_temperatures, netatmo_coords):
        if isinstance(temperature, (int, float)) and coord is not None:
            netatmo_filtered_coords.append(coord)
            netatmo_filtered_temperatures.append(temperature)
            netatmo_filtered_stations.append(station)

    if len(netatmo_filtered_coords) > 0:
        netatmo_geometries = [Point(lon, lat) for lat, lon in netatmo_filtered_coords]
        netatmo_gdf = gpd.GeoDataFrame({
            'Station': netatmo_filtered_stations,
            'Temperature': netatmo_filtered_temperatures,
            'Source': 'Netatmo'
        }, geometry=netatmo_geometries, crs="EPSG:4326")
    else:
        netatmo_gdf = gpd.GeoDataFrame(columns=['Station', 'Temperature', 'Source', 'geometry'], crs="EPSG:4326")

    # Handle IMGW data
    imgw_filtered_stations = []
    imgw_filtered_temperatures = []
    imgw_geometries = []

    # Clean and process IMGW data
    for station, temperature in zip(imgw_stations, imgw_temperatures):
        cleaned_temperature = clean_temperature(temperature)
        if station in imgw_coords and cleaned_temperature is not None:
            lat, lon = imgw_coords[station]
            imgw_filtered_stations.append(station)
            imgw_filtered_temperatures.append(cleaned_temperature)
            imgw_geometries.append(Point(lon, lat))

    # Create GeoDataFrame for IMGW data
    imgw_gdf = gpd.GeoDataFrame({
        'Station': imgw_filtered_stations,
        'Temperature': imgw_filtered_temperatures,
        'Source': 'IMGW'
    }, geometry=imgw_geometries, crs="EPSG:4326")
    
    # Add X and Y coordinates to imgw_gdf
    imgw_gdf['X'] = imgw_gdf.geometry.x
    imgw_gdf['Y'] = imgw_gdf.geometry.y

    # Read and clip DEM
    dem_data, dem_transform = read_and_clip_dem(masovian_boundary)

    # Read and clip Forest Cover
    forest_data, forest_transform = read_and_clip_forest(masovian_boundary)

    # Read and clip Water Bodies
    water_data, water_transform, water_crs = read_and_clip_water_bodies(masovian_boundary)
    
    # **Read and clip additional datasets**
    # Read and clip LandScan HD Population Density Data
    pop_density_data, pop_density_transform = read_and_clip_population_density(masovian_boundary)
    
    # Read and clip GHS_BUILT_S Human Settlement Dataset
    human_settlement_data, human_settlement_transform = read_and_clip_human_settlement(masovian_boundary)
    
    # Read and clip CORINE Land Cover 2018 Dataset
    land_cover_data, land_cover_transform = read_and_clip_land_cover(masovian_boundary)

    # Ensure geometries are in EPSG:4326
    trax_gdf_dem_crs = trax_gdf.to_crs("EPSG:4326")
    netatmo_gdf_dem_crs = netatmo_gdf.to_crs("EPSG:4326")
    imgw_gdf_dem_crs = imgw_gdf.to_crs("EPSG:4326")

    # Get elevation at station points using dem_data and dem_transform
    trax_elevations = get_elevation_at_points(
        trax_gdf_dem_crs.geometry,
        dem_data=dem_data,
        dem_transform=dem_transform
    )
    netatmo_elevations = get_elevation_at_points(
        netatmo_gdf_dem_crs.geometry,
        dem_data=dem_data,
        dem_transform=dem_transform
    )
    imgw_elevations = get_elevation_at_points(
        imgw_gdf_dem_crs.geometry,
        dem_data=dem_data,
        dem_transform=dem_transform
    )

    # Add elevation to the GeoDataFrames
    trax_gdf['Elevation'] = trax_elevations
    netatmo_gdf['Elevation'] = netatmo_elevations
    imgw_gdf['Elevation'] = imgw_elevations

    # Get tree cover density at station points using forest_data and forest_transform
    trax_forest = get_raster_value_at_points(
        trax_gdf_dem_crs.geometry,
        raster_data=forest_data,
        raster_transform=forest_transform
    )
    netatmo_forest = get_raster_value_at_points(
        netatmo_gdf_dem_crs.geometry,
        raster_data=forest_data,
        raster_transform=forest_transform
    )
    imgw_forest = get_raster_value_at_points(
        imgw_gdf_dem_crs.geometry,
        raster_data=forest_data,
        raster_transform=forest_transform
    )

    # Add tree cover density to the GeoDataFrames
    trax_gdf['Tree_Cover'] = trax_forest
    netatmo_gdf['Tree_Cover'] = netatmo_forest
    imgw_gdf['Tree_Cover'] = imgw_forest

    # Get water body data at station points
    trax_water = get_water_body_at_points(
        trax_gdf_dem_crs.geometry,
        water_data=water_data,
        water_transform=water_transform
    )
    netatmo_water = get_water_body_at_points(
        netatmo_gdf_dem_crs.geometry,
        water_data=water_data,
        water_transform=water_transform
    )
    imgw_water = get_water_body_at_points(
        imgw_gdf_dem_crs.geometry,
        water_data=water_data,
        water_transform=water_transform
    )

    # Add water body data to the GeoDataFrames
    trax_gdf['Water_Body'] = trax_water
    netatmo_gdf['Water_Body'] = netatmo_water
    imgw_gdf['Water_Body'] = imgw_water

    # Compute distance to water bodies for station points
    water_mask_array = water_data.astype(bool)
    grid_spacing = water_transform.a  # Assuming square pixels
    distance_to_water_array = distance_transform_edt(~water_mask_array) * grid_spacing  # Distance in same units as grid_spacing

    # Add population density at station points
    trax_population = get_raster_value_at_points(
        trax_gdf_dem_crs.geometry,
        raster_data=pop_density_data,
        raster_transform=pop_density_transform
    )
    netatmo_population = get_raster_value_at_points(
        netatmo_gdf_dem_crs.geometry,
        raster_data=pop_density_data,
        raster_transform=pop_density_transform
    )
    imgw_population = get_raster_value_at_points(
        imgw_gdf_dem_crs.geometry,
        raster_data=pop_density_data,
        raster_transform=pop_density_transform
    )

    # Add human settlement data at station points
    trax_human_settlement = get_raster_value_at_points(
        trax_gdf_dem_crs.geometry,
        raster_data=human_settlement_data,
        raster_transform=human_settlement_transform
    )
    netatmo_human_settlement = get_raster_value_at_points(
        netatmo_gdf_dem_crs.geometry,
        raster_data=human_settlement_data,
        raster_transform=human_settlement_transform
    )
    imgw_human_settlement = get_raster_value_at_points(
        imgw_gdf_dem_crs.geometry,
        raster_data=human_settlement_data,
        raster_transform=human_settlement_transform
    )

    # Add land cover data at station points
    trax_land_cover = get_raster_value_at_points(
        trax_gdf_dem_crs.geometry,
        raster_data=land_cover_data,
        raster_transform=land_cover_transform
    )
    netatmo_land_cover = get_raster_value_at_points(
        netatmo_gdf_dem_crs.geometry,
        raster_data=land_cover_data,
        raster_transform=land_cover_transform
    )
    imgw_land_cover = get_raster_value_at_points(
        imgw_gdf_dem_crs.geometry,
        raster_data=land_cover_data,
        raster_transform=land_cover_transform
    )

    # Add population density to the GeoDataFrames
    trax_gdf['Population_Density'] = trax_population
    netatmo_gdf['Population_Density'] = netatmo_population
    imgw_gdf['Population_Density'] = imgw_population

    # Add human settlement to the GeoDataFrames
    trax_gdf['Built_Up'] = trax_human_settlement
    netatmo_gdf['Built_Up'] = netatmo_human_settlement
    imgw_gdf['Built_Up'] = imgw_human_settlement

    # Add land cover to the GeoDataFrames
    trax_gdf['Land_Cover'] = trax_land_cover
    netatmo_gdf['Land_Cover'] = netatmo_land_cover
    imgw_gdf['Land_Cover'] = imgw_land_cover

    # Get distance to water bodies at station points
    trax_distance_to_water = get_raster_value_at_points(
        trax_gdf_dem_crs.geometry,
        raster_data=distance_to_water_array,
        raster_transform=water_transform
    )
    netatmo_distance_to_water = get_raster_value_at_points(
        netatmo_gdf_dem_crs.geometry,
        raster_data=distance_to_water_array,
        raster_transform=water_transform
    )
    imgw_distance_to_water = get_raster_value_at_points(
        imgw_gdf_dem_crs.geometry,
        raster_data=distance_to_water_array,
        raster_transform=water_transform
    )

    # Add distance to water to the GeoDataFrames
    trax_gdf['Distance_to_Water'] = trax_distance_to_water
    netatmo_gdf['Distance_to_Water'] = netatmo_distance_to_water
    imgw_gdf['Distance_to_Water'] = imgw_distance_to_water

    # Add 'Priority' column to each GeoDataFrame
    trax_gdf['Priority'] = 2
    netatmo_gdf['Priority'] = 1
    imgw_gdf['Priority'] = 3  # Higher priority

    # Combine GeoDataFrames
    stations_gdf = gpd.GeoDataFrame(pd.concat([trax_gdf, netatmo_gdf, imgw_gdf], ignore_index=True))
    stations_gdf.crs = "EPSG:4326"
    stations_gdf = stations_gdf.dropna(subset=['Temperature', 'Elevation', 'Tree_Cover', 'Distance_to_Water'])

    # Extract coordinates
    stations_gdf['X'] = stations_gdf.geometry.x
    stations_gdf['Y'] = stations_gdf.geometry.y

    # Read ECOSTRESS data
    ecostress_data, ecostress_transform = read_and_clip_ecostress(masovian_boundary)
    # Read MODIS data
    (modis_day_data, modis_day_transform), (modis_night_data, modis_night_transform) = read_and_process_modis_data(
        modis_day_folder, modis_night_folder)

    # Adjust grid_spacing as per your requirements
    grid_spacing = 0.0025  # Approximately ~250 meters

    x_min, y_min, x_max, y_max = masovian_boundary.total_bounds

    # Compute the number of cols and rows with the new grid_spacing
    ncols = int(np.ceil((x_max - x_min) / grid_spacing))
    nrows = int(np.ceil((y_max - y_min) / grid_spacing))

    # Create the affine transform for the grid
    transform = from_origin(x_min, y_max, grid_spacing, grid_spacing)

    # Create arrays of row and column indices
    cols = np.arange(ncols)
    rows = np.arange(nrows)

    # Create a meshgrid of indices
    cols_indices, rows_indices = np.meshgrid(cols, rows)

    # Flatten the indices
    cols_flat = cols_indices.ravel()
    rows_flat = rows_indices.ravel()

    # Get the x and y coordinates for each grid cell center
    xs_flat, ys_flat = rasterio.transform.xy(transform, rows_flat, cols_flat)

    # Resample MODIS data to match grid
    modis_day_resampled = resample_modis_data(
        modis_day_data, modis_day_transform,
        dest_shape=(nrows, ncols), dest_transform=transform
    )
    modis_night_resampled = resample_modis_data(
        modis_night_data, modis_night_transform,
        dest_shape=(nrows, ncols), dest_transform=transform
    )

    # Decide which MODIS data to use based on time of day
    if is_daytime:
        modis_resampled = modis_day_resampled
    else:
        modis_resampled = modis_night_resampled

    # After resampling, replace any extreme negative values with np.nan
    modis_resampled = np.where(modis_resampled <= -1e20, np.nan, modis_resampled)

    # Create grid_df as a Pandas DataFrame
    grid_df = pd.DataFrame({
        'X': xs_flat,
        'Y': ys_flat,
        'row_idx': rows_flat,
        'col_idx': cols_flat
    })

    # Create geometry column
    grid_df['geometry'] = gpd.points_from_xy(grid_df['X'], grid_df['Y'])

    # Convert to GeoDataFrame
    grid_gdf = gpd.GeoDataFrame(grid_df, geometry='geometry', crs="EPSG:4326")

    # Assign MODIS_LST to grid_gdf
    grid_gdf['MODIS_LST'] = get_raster_value_at_points(
        grid_gdf.geometry,
        raster_data=modis_resampled,
        raster_transform=transform
    )

    # Handle missing MODIS_LST values
    modis_lst_mean_grid = np.nanmean(grid_gdf['MODIS_LST'])
    grid_gdf['MODIS_LST'].fillna(modis_lst_mean_grid, inplace=True)

    # Assign MODIS_LST to stations_gdf
    stations_gdf['MODIS_LST'] = get_raster_value_at_points(
        stations_gdf.geometry,
        raster_data=modis_resampled,
        raster_transform=transform
    )

    # Handle missing MODIS_LST values in stations_gdf
    modis_lst_mean_stations = np.nanmean(stations_gdf['MODIS_LST'])
    stations_gdf['MODIS_LST'].fillna(modis_lst_mean_stations, inplace=True)

    # Prepare sample weights: Assign higher weight to IMGW stations
    weights_map = {'Netatmo': 1, 'Traxelektronik': 1, 'IMGW': 5}
    stations_gdf['Sample_Weight'] = stations_gdf['Source'].map(weights_map)
    sample_weights = stations_gdf['Sample_Weight'].values

    # Map detailed classes to broader categories
    land_cover_mapping = {
    # Urban areas
    1: 'Urban',   # Continuous urban fabric
    2: 'Urban',   # Discontinuous urban fabric
    3: 'Industrial',  # Industrial or commercial units
    4: 'Infrastructure',  # Road and rail networks and associated land
    5: 'Port',  # Port areas (if applicable)
    6: 'Airport',  # Airports

    # Mining and extraction areas
    7: 'Mine',  # Mineral extraction sites
    8: 'Dump',  # Dump sites
    9: 'Construction',  # Construction sites

    # Urban green areas
    10: 'Green Urban',  # Green urban areas
    11: 'Sport Leisure',  # Sport and leisure facilities

    # Agricultural areas
    12: 'Arable Land',  # Non-irrigated arable land
    16: 'Orchards',  # Fruit trees and berry plantations
    18: 'Pastures',  # Pastures
    20: 'Complex Cultivation',  # Complex cultivation patterns
    21: 'Agro-Forest Mix',  # Land principally occupied by agriculture, with significant areas of natural vegetation

    # Forest areas
    23: 'Broad-leaved Forest',  # Broad-leaved forest
    24: 'Coniferous Forest',  # Coniferous forest
    25: 'Mixed Forest',  # Mixed forest

    # Natural areas
    26: 'Natural Grassland',  # Natural grasslands
    29: 'Shrubland',  # Transitional woodland-shrub

    # Bare areas
    30: 'Beaches Dunes Sands',  # Beaches, dunes, sands

    # Sparsely vegetated areas
    32: 'Sparsely Vegetated',  # Sparsely vegetated areas
    33: 'Burnt Areas',  # Burnt areas

    # Wetlands
    35: 'Inland Marsh',  # Inland marshes
    36: 'Peat Bog',  # Peat bogs

    # Water bodies
    39: 'Water',  # Rivers
    40: 'Water',  # Water courses
    41: 'Water',  # Water bodies
}

    # Map land cover values
    stations_gdf['Land_Cover_Simplified'] = stations_gdf['Land_Cover'].map(land_cover_mapping).fillna('Other')
    grid_gdf['Land_Cover_Simplified'] = get_raster_value_at_points(
        grid_gdf.geometry,
        raster_data=land_cover_data,
        raster_transform=land_cover_transform
    )
    grid_gdf['Land_Cover_Simplified'] = grid_gdf['Land_Cover_Simplified'].map(land_cover_mapping).fillna('Other')

    # One-hot encode 'Land_Cover_Simplified' for stations_gdf
    land_cover_dummies_stations = pd.get_dummies(stations_gdf['Land_Cover_Simplified'], prefix='LC')
    stations_gdf = pd.concat([stations_gdf, land_cover_dummies_stations], axis=1)

    # One-hot encode 'Land_Cover_Simplified' for grid_gdf
    land_cover_dummies_grid = pd.get_dummies(grid_gdf['Land_Cover_Simplified'], prefix='LC')
    grid_gdf = pd.concat([grid_gdf, land_cover_dummies_grid], axis=1)

    # Ensure that both datasets have the same dummy columns
    missing_cols = set(land_cover_dummies_stations.columns) - set(land_cover_dummies_grid.columns)
    for col in missing_cols:
        grid_gdf[col] = 0
    missing_cols = set(land_cover_dummies_grid.columns) - set(land_cover_dummies_stations.columns)
    for col in missing_cols:
        stations_gdf[col] = 0

    # Prepare X and y for the model
    feature_columns = ['X', 'Y', 'Elevation', 'Tree_Cover', 'Distance_to_Water', 'Water_Body', 'MODIS_LST',
                    'Population_Density', 'Built_Up'] + list(land_cover_dummies_stations.columns)

    X = stations_gdf[feature_columns]
    y = stations_gdf['Temperature']

    # Replace infinite values with NaN
    X = X.replace([np.inf, -np.inf], np.nan)

    # Remove rows with NaN values in X
    valid_indices = ~X.isnull().any(axis=1)
    X = X[valid_indices]
    y = y[valid_indices]
    sample_weights = sample_weights[valid_indices]

    # Proceed with model training using Stacking Regressor...
    # Initialize base estimators

    # Base Estimators:
    # We choose diverse models to capture different patterns in data:
    # - RandomForestRegressor: A robust ensemble method using averaging to improve predictive accuracy and control over-fitting.
    # - GradientBoostingRegressor: Builds additive models in a forward stage-wise fashion; allows optimization of arbitrary differentiable loss functions.
    # - XGBRegressor: An optimized distributed gradient boosting library designed to be highly efficient, flexible, and portable.

    # Meta-model:
    # - RidgeCV: A linear model with L2 regularization; combines base model outputs effectively and is less susceptible to overfitting.

    # Import statements are placed at the top

    # Define base estimators
    base_estimators = [
        ('rf', RandomForestRegressor(random_state=42)),
        ('gb', GradientBoostingRegressor(random_state=42)),
        ('xgb', XGBRegressor(
            objective='reg:squarederror',
            random_state=42,
            n_jobs=-1,
            verbosity=0  # Silent mode
        ))
    ]

    # Define meta-model
    meta_model = RidgeCV()

    # Define the Stacking Regressor
    stacking_regressor = StackingRegressor(
        estimators=base_estimators,
        final_estimator=meta_model,
        n_jobs=-1,
        passthrough=False  # Meta-model uses predictions of base estimators
    )

    # Create a pipeline (optional if you have preprocessing steps)
    pipeline = Pipeline([
        ('stack', stacking_regressor)
    ])

    # Define the hyperparameter grid for GridSearchCV
    param_grid = {
        'stack__rf__n_estimators': [100, 200],
        'stack__rf__max_depth': [None, 10],
        'stack__gb__n_estimators': [100, 200],
        'stack__gb__learning_rate': [0.01, 0.05],
        'stack__gb__max_depth': [3, 5],
        'stack__xgb__n_estimators': [100, 200],
        'stack__xgb__learning_rate': [0.01, 0.05],
        'stack__xgb__max_depth': [6, 8],
        # No hyperparameters for meta_model (RidgeCV) in this context
    }

    # Initialize GridSearchCV with 5-fold cross-validation
    grid_search = GridSearchCV(
        estimator=pipeline,
        param_grid=param_grid,
        cv=5,
        scoring='neg_mean_absolute_error',
        n_jobs=-1,
        verbose=1
    )

    # Fit GridSearchCV with sample weights
    grid_search.fit(X, y, **{'stack__sample_weight': sample_weights})
    print("GridSearchCV completed.")

    # Retrieve the best model
    best_pipeline = grid_search.best_estimator_
    print(f"Best parameters found: {grid_search.best_params_}")

    # Define the metrics to evaluate
    scoring_metrics = {
        'MAE': 'neg_mean_absolute_error',
        'MSE': 'neg_mean_squared_error',
        'RMSE': 'neg_root_mean_squared_error',
        'R2': 'r2',
        'Median_Err': 'neg_median_absolute_error',
        'Exp_Variance': 'explained_variance'
    }

    # Perform cross-validation with multiple metrics
    cv_results = cross_validate(
        best_pipeline,
        X,
        y,
        cv=5,
        scoring=scoring_metrics,
        n_jobs=-1,
        return_train_score=False,
        fit_params={'stack__sample_weight': sample_weights}
    )
    print("Cross-validation completed.")

    # Convert negative scores to positive where necessary
    cv_metrics = {}
    for key in scoring_metrics.keys():
        if key in ['MAE', 'MSE', 'RMSE', 'Median_Err']:
            # Convert negative to positive
            cv_metrics[key] = -cv_results[f'test_{key}']
        else:
            # Keep as is (R² and Explained Variance)
            cv_metrics[key] = cv_results[f'test_{key}']

    # Calculate mean and standard deviation for each metric
    metric_summary = {}
    for metric, values in cv_metrics.items():
        mean_val = np.mean(values)
        std_val = np.std(values)
        metric_summary[metric] = (mean_val, std_val)

    # Print detailed performance metrics
    print("\nDetailed Cross-Validation Performance Metrics:")
    print("---------------------------------------------")
    for metric, (mean, std) in metric_summary.items():
        print(f"{metric}: {mean:.3f} ± {std:.3f}")
    print("---------------------------------------------")

    # Get feature importances from the stacking regressor
    # Note: In stacking, we cannot directly get feature importances from the meta-model for the original features.
    # However, we can extract importances from the base estimators if they provide them.
    feature_importances = {}
    for (name, _), fitted_estimator in zip(
            best_pipeline.named_steps['stack'].estimators,
            best_pipeline.named_steps['stack'].estimators_):
        if hasattr(fitted_estimator, 'feature_importances_'):
            feature_importances[name] = fitted_estimator.feature_importances_

    # Print feature importances for base estimators
    print("\nFeature importances from base estimators in the Stacking Regressor:")
    for name, importances in feature_importances.items():
        feature_names = X.columns
        importance_df = pd.DataFrame({'Feature': feature_names, 'Importance': importances})
        importance_df = importance_df.sort_values('Importance', ascending=False)
        print(f"\n{name} Feature Importances:")
        print(importance_df)

    # Create grid dataframe
    # Already converted to grid_gdf earlier

    # Running Model Progress Bar
    with tqdm(total=100, desc='Running Model', unit='%', ncols=80) as pbar:
        # Retrieve elevations from DEM
        grid_gdf['Elevation'] = get_elevation_at_points(
            grid_gdf.geometry,
            dem_data=dem_data,
            dem_transform=dem_transform
        )
        pbar.update(12)

        # Retrieve tree cover density from forest cover raster
        grid_gdf['Tree_Cover'] = get_raster_value_at_points(
            grid_gdf.geometry,
            raster_data=forest_data,
            raster_transform=forest_transform
        )
        pbar.update(12)

        # Calculate distance to water bodies for grid points
        grid_gdf['Distance_to_Water'] = get_raster_value_at_points(
            grid_gdf.geometry,
            raster_data=distance_to_water_array,
            raster_transform=water_transform
        )
        pbar.update(12)

        # Retrieve water body data at grid points
        grid_gdf['Water_Body'] = get_water_body_at_points(
            grid_gdf.geometry,
            water_data=water_data,
            water_transform=water_transform
        )
        pbar.update(12)

        # Retrieve population density at grid points
        grid_gdf['Population_Density'] = get_raster_value_at_points(
            grid_gdf.geometry,
            raster_data=pop_density_data,
            raster_transform=pop_density_transform
        )
        pbar.update(8)

        # Retrieve human settlement data at grid points
        grid_gdf['Built_Up'] = get_raster_value_at_points(
            grid_gdf.geometry,
            raster_data=human_settlement_data,
            raster_transform=human_settlement_transform
        )
        pbar.update(8)

        # Handle missing values
        grid_gdf = grid_gdf.dropna(subset=['Elevation', 'Tree_Cover', 'Distance_to_Water', 'Population_Density', 'Built_Up'])
        pbar.update(6)

        # Include MODIS_LST in grid_gdf (Already included)

        # Prepare data for regression prediction
        # Include land cover dummy variables
        for col in land_cover_dummies_stations.columns:
            if col not in grid_gdf.columns:
                grid_gdf[col] = 0

        X_grid = grid_gdf[feature_columns]
        # Handle infinite values in X_grid
        X_grid = X_grid.replace([np.inf, -np.inf], np.nan)
        # Handle NaNs in X_grid
        X_grid = X_grid.fillna(method='ffill').fillna(method='bfill')

        # Ensure that all values are finite
        # Predict using the trained stacking regressor
        predictions = best_pipeline.predict(X_grid)
        grid_gdf['Predicted_Temperature'] = predictions
        pbar.update(10)

        # Calculate residuals at station locations
        stations_gdf['Predicted_Temperature'] = best_pipeline.predict(X)
        stations_gdf['Residual'] = stations_gdf['Temperature'] - stations_gdf['Predicted_Temperature']

        # Force residuals to zero at IMGW stations
        stations_gdf.loc[stations_gdf['Source'] == 'IMGW', 'Residual'] = 0

        # Adjust anomalously large residuals to reduce their impact
        lower_percentile = np.percentile(stations_gdf['Residual'], 5)
        upper_percentile = np.percentile(stations_gdf['Residual'], 95)

        # Cap the residuals
        stations_gdf['Adjusted_Residual'] = stations_gdf['Residual'].clip(lower=lower_percentile, upper=upper_percentile)

        # Remove duplicates by prioritizing IMGW data
        data = stations_gdf.copy()

        # Sort by priority (higher priority first)
        data = data.sort_values('Priority', ascending=False)

        # Group by coordinates and keep the first record in each group (highest priority)
        data = data.groupby(['X', 'Y'], as_index=False).first()

        # After performing Universal Kriging:
        # Perform Universal Kriging using Adjusted Residual
        uk = UniversalKriging(
            x=data['X'].values,
            y=data['Y'].values,
            z=data['Adjusted_Residual'].values,
            variogram_model='spherical',
            nlags=40,  # Increased number of lags
            verbose=False
        )

        z, ss = uk.execute(
            'points',
            grid_gdf['X'].values,
            grid_gdf['Y'].values
        )

        grid_gdf['Residual'] = z
        grid_gdf['Kriging_Variance'] = ss

        # Handle NaNs in residuals by filling them with zero
        grid_gdf['Residual'].fillna(0, inplace=True)

        # Set residuals over water bodies to zero
        grid_gdf.loc[grid_gdf['Water_Body'] == 1, 'Residual'] = 0

        # Final temperature estimation
        grid_gdf['Temperature'] = grid_gdf['Predicted_Temperature'] + grid_gdf['Residual']

        # Save a copy of the temperature before ECOSTRESS adjustments
        grid_gdf['Temperature_No_ECOSTRESS'] = grid_gdf['Temperature'].copy()

        # Topographic Cooling Effect
        max_cooling_effect = 3.0  # Maximum cooling effect in degrees Celsius
        # Normalize elevation to range from 0 to 1
        elevation_min = grid_gdf['Elevation'].min()
        elevation_max = grid_gdf['Elevation'].max()
        grid_gdf['Normalized_Elevation'] = (grid_gdf['Elevation'] - elevation_min) / (elevation_max - elevation_min)

        # Compute the topographic cooling effect
        grid_gdf['Topographic_Cooling'] = grid_gdf['Normalized_Elevation'] * max_cooling_effect * (1 - is_daytime)

        # Ensure topographic cooling is not applied over water bodies
        grid_gdf.loc[grid_gdf['Water_Body'] == 1, 'Topographic_Cooling'] = 0

        # Apply the topographic cooling effect to the temperature
        grid_gdf['Temperature'] -= grid_gdf['Topographic_Cooling']

        # ECOSTRESS Adjustments

        # Retrieve ECOSTRESS LST values at grid points (no changes here)
        grid_gdf['ECOSTRESS_LST'] = get_raster_value_at_points(
            grid_gdf.geometry,
            raster_data=ecostress_data,
            raster_transform=ecostress_transform
        )

        # Handle NaNs in ECOSTRESS_LST by filling with MODIS_LST values or the mean value (no changes here)
        grid_gdf['ECOSTRESS_LST'] = grid_gdf.apply(
            lambda row: row['ECOSTRESS_LST'] if not np.isnan(row['ECOSTRESS_LST']) else row['MODIS_LST'], axis=1
        )
        grid_gdf['ECOSTRESS_LST'].fillna(grid_gdf['ECOSTRESS_LST'].mean(), inplace=True)

        # Compute ECOSTRESS anomalies (deviation from mean) (no changes here)
        ecostress_mean = grid_gdf['ECOSTRESS_LST'].mean()
        grid_gdf['ECOSTRESS_Anomaly'] = grid_gdf['ECOSTRESS_LST'] - ecostress_mean

        # Normalize anomalies to range [-1, 1] (no changes here)
        ecostress_anomaly_min = grid_gdf['ECOSTRESS_Anomaly'].min()
        ecostress_anomaly_max = grid_gdf['ECOSTRESS_Anomaly'].max()
        grid_gdf['ECOSTRESS_Anomaly_Normalized'] = (
            (grid_gdf['ECOSTRESS_Anomaly'] - ecostress_anomaly_min) / (ecostress_anomaly_max - ecostress_anomaly_min) * 2 - 1
        )

        # **Invert the weighting function to reduce impact at night**
        # Compute day_weight: maximum at noon (hour=12), minimal at midnight (hour=0)
        day_weight = 0.5 * (math.cos(((hour % 24) / 24) * 2 * math.pi) + 1)  # Ranges from 0 at midnight to 0.5 at noon

        # **Apply anomalies with the day_weight**
        amplification_factor = 1.0  # Adjust as needed
        # Before applying the adjustment, ensure ECOSTRESS data is valid
        valid_ecostress_mask = ~grid_gdf['ECOSTRESS_Anomaly_Normalized'].isnull()

        # Apply adjustments only where ECOSTRESS data is valid
        adjustment = np.zeros_like(grid_gdf['Temperature'])
        adjustment[valid_ecostress_mask] = grid_gdf.loc[valid_ecostress_mask, 'ECOSTRESS_Anomaly_Normalized'] * amplification_factor * day_weight

        # Prevent ECOSTRESS adjustments on water bodies (no changes here)
        adjustment[grid_gdf['Water_Body'] == 1] = 0

        # Limit maximum adjustment to avoid over-adjusting (no changes here)
        max_adjustment = 5.0  # degrees Celsius
        adjustment = adjustment.clip(-max_adjustment, max_adjustment)

        # Apply adjustments to the interpolated temperatures (no changes here)
        grid_gdf['Temperature'] += adjustment

        # Apply the variable cooling effect to water bodies based on time of day
        cooling_effect = compute_water_body_cooling_effect(hour)

        # Apply the cooling effect to grid cells identified as water bodies
        grid_gdf.loc[grid_gdf['Water_Body'] == 1, 'Temperature'] += cooling_effect

        pbar.update(20)  # Update the progress bar to complete

        # Create 2D arrays of temperatures
        grid_temperature_with_ecostress = np.full((nrows, ncols), np.nan)
        grid_temperature_with_ecostress[grid_gdf['row_idx'], grid_gdf['col_idx']] = grid_gdf['Temperature'].values

        # Mask the grid_temperature with the Masovian boundary
        masovian_shape = [masovian_boundary.geometry.unary_union]
        mask = rasterize(
            masovian_shape,
            out_shape=(nrows, ncols),
            transform=transform,
            fill=0,
            all_touched=True,
            default_value=1,
            dtype='uint8'
        )

        temperature_with_ecostress = grid_temperature_with_ecostress

        temperature_with_ecostress_masked = np.ma.masked_where(mask == 0, temperature_with_ecostress)

        # Also create grid_variance array (we'll skip smoothing variance for simplicity)
        grid_variance = np.full((nrows, ncols), np.nan)
        grid_variance[grid_gdf['row_idx'], grid_gdf['col_idx']] = grid_gdf['Kriging_Variance']
        grid_variance = np.ma.array(grid_variance, mask=mask == 0)

        # Compute Standard Deviation from Variance
        grid_std_dev = np.sqrt(grid_variance)
        grid_std_dev = np.ma.array(grid_std_dev, mask=mask == 0)

        # Extract temperatures over water bodies
        water_temperatures = grid_gdf.loc[grid_gdf['Water_Body'] == 1, 'Temperature']
        land_temperatures = grid_gdf.loc[grid_gdf['Water_Body'] == 0, 'Temperature']

        # Compare average temperatures
        avg_water_temp = water_temperatures.mean()
        avg_land_temp = land_temperatures.mean()
        print(f"Average Water Temperature: {avg_water_temp:.2f}°C")
        print(f"Average Land Temperature: {avg_land_temp:.2f}°C")
        print(f"Temperature Difference (Land - Water): {(avg_land_temp - avg_water_temp):.2f}°C")

        # Read the custom color scale from the CSV file
        color_scale_df = pd.read_csv('input/color_scale.csv')
        color_scale_df = color_scale_df.sort_values('value')
        values = color_scale_df['value'].values
        hex_colors = color_scale_df['color'].values

        # Ensure colors start with '#'
        hex_colors = [color if color.startswith('#') else f"#{color}" for color in hex_colors]

        # Create a custom colormap
        # Normalize the values to 0..1
        vmin = values.min()
        vmax = values.max()
        normalized_values = (values - vmin) / (vmax - vmin)

        # Create a list of tuples (position, color)
        color_tuples = list(zip(normalized_values, hex_colors))

        # Create a LinearSegmentedColormap
        cmap = mcolors.LinearSegmentedColormap.from_list('custom_cmap', color_tuples)

        # Set the normalization
        norm = mcolors.Normalize(vmin=vmin, vmax=vmax)

    with rasterio.open('input/masovian_dem.tif') as dem_src:
        # Reproject Masovian boundary to DEM CRS if necessary
        if masovian_boundary.crs != dem_src.crs:
            masovian_boundary_dem_crs = masovian_boundary.to_crs(dem_src.crs)
        else:
            masovian_boundary_dem_crs = masovian_boundary

        # Clip the DEM to the Masovian boundary
        dem_plot_data, dem_plot_transform = rasterio.mask.mask(
            dem_src, masovian_boundary_dem_crs.geometry, crop=True, filled=True, nodata=np.nan
        )

        # Read the first band (assuming single-band DEM)
        dem_plot_data = dem_plot_data[0]  # Since mask returns data as (bands, rows, cols)

        # Handle NaNs in the DEM data
        dem_plot_data = np.nan_to_num(dem_plot_data, nan=np.nanmin(dem_plot_data))

        # Calculate the extent of the clipped DEM for plotting
        dem_left, dem_top = dem_plot_transform * (0, 0)
        dem_right, dem_bottom = dem_plot_transform * (dem_plot_data.shape[1], dem_plot_data.shape[0])
        dem_extent = [dem_left, dem_right, dem_bottom, dem_top]

        # Create hillshade using DEM
        hs = hillshade(dem_plot_data, azimuth=315, angle_altitude=45)

        # Define extent for plotting
        def get_extent(transform, array):
            x_min = transform.c
            y_max = transform.f
            x_max = x_min + transform.a * array.shape[1]
            y_min = y_max + transform.e * array.shape[0]
            return [x_min, x_max, y_min, y_max]

        extent = get_extent(transform, temperature_with_ecostress)
        dem_extent = get_extent(dem_transform, dem_plot_data)

        # Read water data and get CRS
        water_data, water_transform, water_crs = read_and_clip_water_bodies(masovian_boundary)

        # Create an empty array to hold the resampled water data
        water_resampled = np.empty((nrows, ncols), dtype=water_data.dtype)

        # Define bounding box coordinates
        xmin, ymin, xmax, ymax = 20.188477, 51.017203, 22.061646, 51.821759

        water_mask = np.where(water_resampled > 0, 1, 0)

        # Compute the indices for the bounding box
        from rasterio.transform import rowcol

        # Convert coordinates to row and column indices in the temperature grid
        row_ul, col_ul = rowcol(transform, xmin, ymax)
        row_lr, col_lr = rowcol(transform, xmax, ymin)

        # Ensure indices are within valid ranges
        row_ul = max(0, min(nrows - 1, row_ul))
        row_lr = max(0, min(nrows - 1, row_lr))
        col_ul = max(0, min(ncols - 1, col_ul))
        col_lr = max(0, min(ncols - 1, col_lr))

        # Ensure row_ul <= row_lr and col_ul <= col_lr
        if row_ul > row_lr:
            row_ul, row_lr = row_lr, row_ul
        if col_ul > col_lr:
            col_ul, col_lr = col_lr, col_ul

        # Extract the subset arrays
        temperature_subset = temperature_with_ecostress[row_ul:row_lr+1, col_ul:col_lr+1]
        hs_subset = hs[row_ul:row_lr+1, col_ul:col_lr+1]
        water_mask_subset = water_mask[row_ul:row_lr+1, col_ul:col_lr+1]
        mask_subset = mask[row_ul:row_lr+1, col_ul:col_lr+1]

        # Apply the Masovian mask to the subsets
        temperature_subset = np.ma.array(temperature_subset, mask=mask_subset == 0)
        hs_subset = np.ma.array(hs_subset, mask=mask_subset == 0)
        water_mask_subset = np.ma.array(water_mask_subset, mask=mask_subset == 0)

        # Compute new transform for the subset
        grid_spacing_x = transform.a  # Positive grid spacing in x direction
        grid_spacing_y = transform.e  # Negative grid spacing in y direction

        new_x_min = transform.c + col_ul * grid_spacing_x
        new_y_max = transform.f + row_ul * grid_spacing_y  # grid_spacing_y is negative

        new_transform = from_origin(new_x_min, new_y_max, grid_spacing_x, -grid_spacing_y)

        # Compute the new extent
        extent_subset = get_extent(new_transform, temperature_subset)

        # Filter stations within the bounding box
        bbox_polygon = box(xmin, ymin, xmax, ymax)
        stations_in_bbox = stations_gdf[stations_gdf.geometry.within(bbox_polygon)]

        # Clip the Masovian boundary to the bounding box
        masovian_boundary_clipped = masovian_boundary.clip(bbox_polygon)

        # Plotting Temperature Map with Contours
        # Calculate the aspect ratio of the bounding box
        width = xmax - xmin
        height = ymax - ymin
        aspect_ratio = width / height

        # Set the figure size based on the aspect ratio
        desired_height_inches = 10  # You can adjust this value as desired
        desired_width_inches = desired_height_inches * aspect_ratio

        # Plotting Temperature Map with ECOSTRESS Adjustments
        fig1, ax1 = plt.subplots(figsize=(15, 15))

        # Plot the interpolated temperature grid with ECOSTRESS adjustments
        img1 = ax1.imshow(temperature_with_ecostress_masked, extent=extent, origin='upper',
                          cmap=cmap, norm=norm, alpha=1, interpolation='nearest')

        # Plot Masovian Voivodeship boundary with a light, dark stroke
        masovian_boundary.boundary.plot(ax=ax1, edgecolor='#333333', linewidth=1.5)

        # Plot stations
        stations_gdf.plot(ax=ax1, color='black', marker='s', markersize=5)

        # Remove axes for a minimalistic look
        ax1.axis('off')

        # Draw a light, dark stroke around the plot extent
        from matplotlib.patches import Rectangle
        rect = Rectangle(
            (extent[0], extent[2]),  # Lower-left corner (x0, y0)
            extent[1] - extent[0],   # Width
            extent[3] - extent[2],   # Height
            linewidth=1.5,
            edgecolor='#333333',
            facecolor='none',
            transform=ax1.transData
        )
        ax1.add_patch(rect)

        # Introduce a subtle background gradient
        from matplotlib.colors import LinearSegmentedColormap
        background_cmap = LinearSegmentedColormap.from_list(
            'background_gradient', ['#ffffff', '#f0f0f0'], N=256)
        ax1.imshow(
            np.linspace(0, 1, 256).reshape(1, -1),
            cmap=background_cmap,
            extent=(extent[0], extent[1], extent[2], extent[3]),
            aspect='auto',
            alpha=0.2,
            zorder=-1  # Place it below other elements
        )

        # Adjust font properties for a modern look
        plt.rcParams.update({
            'font.size': 14,  # Base font size
            'font.family': 'sans-serif',
            'font.sans-serif': ['Arial', 'Helvetica', 'DejaVu Sans'],
            'text.color': '#333333'
        })

        # Enlarge and reposition the product name (Title)
        fig1.text(
            0.5, 0.94,  # Adjusted y-position for better placement
            "High-Resolution Mesoscale Temperature Analysis",
            fontsize=28,  # Increased font size
            fontweight='bold',
            ha='center',
            va='top',
            fontname='DejaVu Sans',
            color='#333333'
        )

        # Move the model name/resolution text closer to the product name text
        fig1.text(
            0.5, 0.91,  # Adjusted y-position to move it closer
            "Model: HRMTA v1.1 (Preview)   |   Resolution: 250 m",
            fontsize=18,  # Increased font size
            ha='center',
            va='top',
            fontname='DejaVu Sans',
            color='#666666'
        )

        # Adjust colorbar position and style
        from mpl_toolkits.axes_grid1 import make_axes_locatable

        divider = make_axes_locatable(ax1)
        cax = divider.append_axes("right", size="3%", pad=0.1)
        cbar1 = fig1.colorbar(img1, cax=cax)
        ticks = np.arange(np.floor(vmin / 5) * 5, np.ceil(vmax / 5) * 5 + 1, 5)
        cbar1.set_ticks(ticks)
        cbar1.set_ticklabels(ticks.astype(int))
        cbar1.ax.tick_params(labelsize=14)

        # Enhance colorbar aesthetics
        cbar1.outline.set_visible(False)
        cbar1.ax.yaxis.set_tick_params(width=0.5)
        cbar1.ax.tick_params(labelsize=14, colors='#333333')

        # Move the logo to the bottom right corner, directly below the colorbar
        from matplotlib.offsetbox import OffsetImage, AnnotationBbox
        import matplotlib.image as mpimg

        # Read the logo image
        logo_path = 'input/logo.png'
        logo_img = mpimg.imread(logo_path)

        # Adjust the size of the logo
        logo_zoom = 0.25
        imagebox = OffsetImage(logo_img, zoom=logo_zoom)

        # Get the position of the colorbar axes
        cbar_bbox = cax.get_position()
        cbar_x0 = cbar_bbox.x0  # Left
        cbar_x1 = cbar_bbox.x1  # Right

        # Calculate the x position for the logo (centered below the colorbar)
        logo_x = (cbar_x0 + cbar_x1) / 2
        # Set the y position as low as possible, but ensure the logo is fully visible
        logo_y = 0.02  # Adjust this value if necessary

        ab = AnnotationBbox(
            imagebox,
            xy=(logo_x, logo_y),
            xycoords='figure fraction',
            frameon=False,
            box_alignment=(0.5, 0),  # Centered horizontally, aligned at bottom
        )

        fig1.add_artist(ab)

        # Adjust the bottom margin to ensure the logo is not cut off
        plt.subplots_adjust(left=0.05, right=0.95, top=0.88, bottom=0.05)

        # Ensure the temperature grid and water mask are aligned
        if temperature_with_ecostress.shape != water_mask.shape:
            raise ValueError("Temperature grid and water mask dimensions do not match.")

        # Create a masked array for land temperatures by excluding water bodies
        temperature_land = temperature_with_ecostress.copy()
        # If temperature_with_ecostress is not already a masked array, convert it
        if not isinstance(temperature_land, np.ma.MaskedArray):
            temperature_land = np.ma.array(temperature_land)
        # Update the mask to include water bodies
        temperature_land.mask = temperature_land.mask | (water_mask == 1)

        # Calculate minimum temperature over land only
        min_temp = temperature_land.min()
        max_temp = temperature_land.max()

        # Get the indices of the min and max temperatures
        # For min_temp, use the land-only masked array
        min_indices_flat = np.ma.argmin(temperature_land)
        max_indices_flat = np.ma.argmax(temperature_land)
        min_indices = np.unravel_index(min_indices_flat, temperature_land.shape)
        max_indices = np.unravel_index(max_indices_flat, temperature_land.shape)

        # Convert grid indices to geographic coordinates using the new_transform
        min_lon_full, min_lat_full = rasterio.transform.xy(transform, min_indices[0], min_indices[1])
        max_lon_full, max_lat_full = rasterio.transform.xy(transform, max_indices[0], max_indices[1])

        # Plot the temperature values on the map (without markers)
        # Use white text with a thick black stroke for better visibility
        text_kwargs = dict(
            fontsize=18,  # Increased font size for clarity
            fontname='Arial',
            ha='center',
            va='center',
            color='white',
            transform=ax1.transData,
            path_effects=[patheffects.withStroke(linewidth=3, foreground='black')]
        )

        # Format temperature values with one decimal point and add text labels
        ax1.text(min_lon_full, min_lat_full, f"{min_temp:.1f}", **text_kwargs)
        ax1.text(max_lon_full, max_lat_full, f"{max_temp:.1f}", **text_kwargs)

        # Save and show the plot
        plt.savefig('output/temperature_analysis_modern.png', dpi=300)
        plt.show()

        print("Updated temperature analysis plot created successfully.")

        # Calculate the aspect ratio of the bounding box
        width = xmax - xmin
        height = ymax - ymin
        aspect_ratio = width / height

        # Set the maximum desired width
        max_width_inches = 15  # Adjust this value as desired

        # Calculate the figure height based on the aspect ratio and maximum width
        desired_width_inches = min(max_width_inches, aspect_ratio * desired_height_inches)
        desired_height_inches = desired_width_inches / aspect_ratio

        # Ensure the height doesn't exceed a certain value
        desired_height_inches = min(desired_height_inches, 10)  # Adjust as needed

        fig2, ax2 = plt.subplots(figsize=(desired_width_inches, desired_height_inches))

        # Proceed with previous plotting code, using adjusted fig2 and ax2

        # Use the subset data and extent with applied mask
        img2 = ax2.imshow(temperature_subset, extent=extent_subset, origin='upper',
                          cmap=cmap, norm=norm, alpha=1, interpolation='nearest')

        # Plot Masovian boundary
        masovian_boundary_clipped.boundary.plot(ax=ax2, edgecolor='#333333', linewidth=1.5)

        # Plot stations within the bounding box
        stations_in_bbox.plot(ax=ax2, color='black', marker='s', markersize=5)

        # Remove axes for a minimalistic look
        ax2.axis('off')

        # Introduce a subtle background gradient using the subset extent
        ax2.imshow(
            np.linspace(0, 1, 256).reshape(1, -1),
            cmap=background_cmap,
            extent=extent_subset,
            aspect='auto',
            alpha=0.2,
            zorder=-1  # Place it below other elements
        )

        # Adjust font properties for a modern look
        plt.rcParams.update({
            'font.size': 14,
            'font.family': 'sans-serif',
            'font.sans-serif': ['Arial', 'Helvetica', 'DejaVu Sans'],
            'text.color': '#333333'
        })

        # Enlarge and reposition the product name (Title)
        fig2.text(
            0.5, 0.995,  # Moved up towards the top edge
            "High-Resolution Mesoscale Temperature Analysis",
            fontsize=12,
            fontweight='bold',
            ha='center',
            va='top',
            fontname='DejaVu Sans',
            color='#333333'
        )

        # Move the model name/resolution text closer to the product name text
        fig2.text(
            0.5, 0.970,  # Moved up slightly below the title
            "Model: HRMTA v1.1 (Preview)   |   Resolution: 250 m",
            fontsize=9,
            ha='center',
            va='top',
            fontname='DejaVu Sans',
            color='#666666'
        )

        # Add data source attribution at the bottom left corner
        fig2.text(
            0.01, 0.02,
            "Data source: IMGW-PIB (processed)",
            fontsize=12,
            ha='left',
            va='bottom',
            fontname='DejaVu Sans',
            color='#333333'
        )

        # Adjust colorbar position and style
        from mpl_toolkits.axes_grid1 import make_axes_locatable

        divider = make_axes_locatable(ax2)
        cax2 = divider.append_axes("right", size="3%", pad=0.1)
        cbar2 = fig2.colorbar(img2, cax=cax2)
        cbar2.set_ticks(ticks)
        cbar2.set_ticklabels(ticks.astype(int))

        # Import FontProperties and load the font
        from matplotlib.font_manager import FontProperties

        # Replace 'input/RobotoCondensed-Regular.ttf' with the actual path to your font file
        roboto_font_path = 'input/RobotoCondensed-Regular.ttf'
        roboto_condensed = FontProperties(fname=roboto_font_path)

        # Apply font properties to the colorbar tick labels
        for label in cbar2.ax.yaxis.get_ticklabels():
            label.set_fontproperties(roboto_condensed)

        cbar2.ax.tick_params(labelsize=14)

        # Enhance colorbar aesthetics
        cbar2.outline.set_visible(False)
        cbar2.ax.yaxis.set_tick_params(width=0.5)
        cbar2.ax.tick_params(labelsize=14, colors='#333333')

        # Move the logo to the bottom right corner, directly below the colorbar
        from matplotlib.offsetbox import OffsetImage, AnnotationBbox
        import matplotlib.image as mpimg

        # Read the logo image again for the second plot
        logo_img2 = mpimg.imread(logo_path)

        # Adjust the size of the logo
        imagebox2 = OffsetImage(logo_img2, zoom=logo_zoom)

        # Get the position of the colorbar axes
        cbar_bbox2 = cax2.get_position()
        logo_x2 = (cbar_bbox2.x0 + cbar_bbox2.x1) / 2
        logo_y2 = 0.02  # Adjust this value if necessary

        ab2 = AnnotationBbox(
            imagebox2,
            xy=(logo_x2, logo_y2),
            xycoords='figure fraction',
            frameon=False,
            box_alignment=(0.5, 0),
        )
        fig2.add_artist(ab2)

        # Adjust the bottom margin to ensure the logo and data attribution text are not cut off
        plt.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.06)

        # Ensure the temperature grid and water mask are aligned
        if temperature_with_ecostress.shape != water_mask.shape:
            raise ValueError("Temperature grid and water mask dimensions do not match.")

        # Using the temperature_subset array which is already clipped to the Masovian boundary
        min_temp_subset = temperature_subset.min()
        max_temp_subset = temperature_subset.max()

        # (Optional) Print or store these values for future usage
        print(f"Subset Minimum Temperature: {min_temp_subset:.2f}°C")
        print(f"Subset Maximum Temperature: {max_temp_subset:.2f}°C")

        # Add contours to the plot using the subset data
        # Corrected variable names to use 'min_temp_subset' and 'max_temp_subset'
        min_temp_c = np.floor(min_temp_subset)
        max_temp_c = np.ceil(max_temp_subset)
        # Handle cases where min_temp_c or max_temp_c might be NaN
        if np.isnan(min_temp_c) or np.isnan(max_temp_c):
            print("Warning: Temperature data is invalid or missing.")
            contour_levels = []
        else:
            contour_levels = np.arange(min_temp_c, max_temp_c + 1, 1)

        # Proceed only if contour_levels is not empty
        if len(contour_levels) > 0:
            contours = ax2.contour(
                temperature_subset, levels=contour_levels, colors='black', linewidths=0.5,
                extent=extent_subset, origin='upper'
            )
            contour_labels = ax2.clabel(
                contours, inline=False, fontsize=8, fmt='%d', colors='white',
                inline_spacing=4, manual=False
            )
            for txt in contour_labels:
                txt.set_path_effects([
                    patheffects.Stroke(linewidth=3, foreground='black'),
                    patheffects.Normal()
                ])
        else:
            print("Contours cannot be plotted due to invalid temperature data.")

        # Now add the min/max temperature labels after the contours and contour labels
        text_kwargs = dict(
            fontsize=18,
            fontname='Arial',
            ha='center',
            va='center',
            color='white',
            transform=ax2.transData,
            path_effects=[patheffects.withStroke(linewidth=3, foreground='black')]
        )

        # Reverse geocode min and max temperature locations to find nearest village
        from geopy.geocoders import Nominatim
        geolocator = Nominatim(user_agent="geoapiExercises")

        try:
            min_location = geolocator.reverse((min_lat_full, min_lon_full), language='en')
            min_address = min_location.raw.get('address', {})
            min_village = min_address.get('village', 
                            min_address.get('town', 
                            min_address.get('city', 'Unknown Location')))
        except Exception as e:
            print(f"Error reverse geocoding min location: {e}")
            min_village = 'Unknown Location'

        try:
            max_location = geolocator.reverse((max_lat_full, max_lon_full), language='en')
            max_address = max_location.raw.get('address', {})
            max_village = max_address.get('village', 
                            max_address.get('town', 
                            max_address.get('city', 'Unknown Location')))
        except Exception as e:
            print(f"Error reverse geocoding max location: {e}")
            max_village = 'Unknown Location'

        # Create label texts
        min_label_text = f"MIN\n {min_temp_subset:.1f}°C: {min_village}"
        max_label_text = f"MAX\n {max_temp_subset:.1f}°C: {max_village}"

        # Define font properties
        from matplotlib.font_manager import FontProperties
        roboto_font_path = 'input/RobotoCondensed-Regular.ttf'
        roboto_condensed = FontProperties(fname=roboto_font_path)

        # Add text labels to the figure
        fig1.text(0.01, 0.95, min_label_text, ha='left', va='top', fontsize=14, fontproperties=roboto_condensed)
        fig1.text(0.99, 0.95, max_label_text, ha='right', va='top', fontsize=14, fontproperties=roboto_condensed)

        # Save and show the plot
        plt.savefig('output/temperature_analysis_with_contours2.png', dpi=300)
        plt.show()

        print("Temperature analysis plot with contours created successfully.")

    # Save the trained model for future use
    joblib.dump(best_pipeline, 'output/best_stacking_pipeline.joblib')
    print("\nTrained Stacking model saved successfully.")

    return grid_temperature_with_ecostress, transform

def get_elevation_at_points(geometry, dem_data=None, dem_transform=None):
    from rasterio.transform import rowcol

    if dem_data is not None and dem_transform is not None:
        elevations = []
        for point in geometry:
            x, y = point.x, point.y
            try:
                row, col = rowcol(dem_transform, x, y)
                row = int(row)
                col = int(col)
                elevation = dem_data[row, col]
                elevations.append(elevation)
            except (IndexError, ValueError):
                elevations.append(np.nan)
        return elevations
    else:
        raise ValueError("Provide dem_data and dem_transform.")

def get_interpolated_temperature_at_two_points(lat1, lon1, lat2, lon2, temperature_grid, transform):
    """
    Given two latitude and longitude pairs, return the interpolated temperature values at those points.

    Parameters:
    - lat1, lon1: Latitude and longitude of the first point.
    - lat2, lon2: Latitude and longitude of the second point.
    - temperature_grid: The interpolated temperature grid (2D numpy array).
    - transform: Affine transform for the grid.

    Returns:
    - A tuple containing interpolated temperature values at the two points,
      or None for a point if it's outside the grid or has invalid data.
    """
    import numpy as np
    from rasterio.transform import rowcol

    temps = []
    for lat, lon in [(lat1, lon1), (lat2, lon2)]:
        try:
            # Convert geographic coordinates to raster indices
            row, col = rowcol(transform, lon, lat)  # Note: rasterio uses (x, y), so (lon, lat)
            row = int(row)
            col = int(col)

            # Retrieve the value from the temperature grid
            value = temperature_grid[row, col]

            # Check if the value is valid
            if np.ma.is_masked(value) or np.isnan(value):
                # Value is masked or NaN, meaning outside the interpolated area
                temps.append(None)
            else:
                temps.append(value.item())
        except (IndexError, ValueError):
            # Point is outside the grid
            temps.append(None)
    return tuple(temps)

def get_raster_value_at_points(geometry, raster_data=None, raster_transform=None):
    from rasterio.transform import rowcol

    if raster_data is not None and raster_transform is not None:
        values = []
        for point in geometry:
            x, y = point.x, point.y
            try:
                row, col = rowcol(raster_transform, x, y)
                row = int(row)
                col = int(col)
                if (0 <= row < raster_data.shape[0]) and (0 <= col < raster_data.shape[1]):
                    value = raster_data[row, col]
                    # Check for NaN in raster data
                    if np.isnan(value):
                        values.append(np.nan)
                    else:
                        values.append(value)
                else:
                    values.append(np.nan)
            except (IndexError, ValueError):
                values.append(np.nan)
        return values
    else:
        raise ValueError("Provide raster_data and raster_transform.")
    
def get_raster_value_at_point(x, y, raster_data=None, raster_transform=None):
    from rasterio.transform import rowcol

    if raster_data is not None and raster_transform is not None:
        try:
            row, col = rowcol(raster_transform, x, y)
            row = int(row)
            col = int(col)
            if (0 <= row < raster_data.shape[0]) and (0 <= col < raster_data.shape[1]):
                value = raster_data[row, col]
                if np.isnan(value):
                    return np.nan
                else:
                    return value
            else:
                return np.nan
        except (IndexError, ValueError):
            return np.nan
    else:
        raise ValueError("Provide raster_data and raster_transform.")
    
def get_water_body_at_points(geometry, water_data=None, water_transform=None):
    from rasterio.transform import rowcol

    if water_data is not None and water_transform is not None:
        water_bodies = []
        for point in geometry:
            x, y = point.x, point.y
            try:
                row, col = rowcol(water_transform, x, y)
                row = int(row)
                col = int(col)
                if (0 <= row < water_data.shape[0]) and (0 <= col < water_data.shape[1]):
                    value = water_data[row, col]
                    # Adjust the condition based on your raster's water encoding
                    # For binary raster where 1 = water, 0 = land
                    water_bodies.append(1 if value else 0)
                else:
                    water_bodies.append(0)
            except (IndexError, ValueError):
                water_bodies.append(0)
        return water_bodies
    else:
        raise ValueError("Provide water_data and water_transform.")

def read_and_clip_dem(masovian_boundary):
    dem_path = 'input/masovian_dem.tif'
    with rasterio.open(dem_path) as src:
        dem_data = src.read(1, masked=True)
        dem_transform = src.transform
    return dem_data, dem_transform

def read_and_clip_forest(masovian_boundary):
    forest_path = 'input/forests.tif'
    with rasterio.open(forest_path) as src:
        forest_data = src.read(1, masked=True)
        forest_data = np.where(forest_data == src.nodata, 0, forest_data)
        forest_transform = src.transform
    return forest_data, forest_transform

def read_and_clip_water_bodies(masovian_boundary):
    water_path = 'input/water_bodies.tif'
    with rasterio.open(water_path) as src:
        water_crs = src.crs  # Get the CRS of the water raster

        # Ensure the Masovian boundary is in the same CRS as the water raster
        if masovian_boundary.crs != src.crs:
            masovian_boundary = masovian_boundary.to_crs(src.crs)

        # Perform the clipping
        clipped_water, water_transform = rasterio.mask.mask(
            src, masovian_boundary.geometry, crop=True, filled=True, nodata=0
        )

        # Read the first band (assuming single-band raster)
        water_data = clipped_water[0]

        # Handle NoData values by setting them to 0 (assuming 0 represents no water)
        water_data = np.where(water_data == src.nodata, 0, water_data)

    return water_data, water_transform, water_crs

def read_and_clip_ecostress(masovian_boundary):
    ecostress_path = 'input/raw_ecostress.tif'
    with rasterio.open(ecostress_path) as src:
        # Read data
        ecostress_data = src.read(1, masked=True)
        ecostress_data = np.where(ecostress_data == src.nodata, np.nan, ecostress_data)
        ecostress_transform = src.transform
    return ecostress_data, ecostress_transform

def read_and_clip_population_density(masovian_boundary):
    """
    Read and clip LandScan HD Population Density Data.
    File: input/landscan_hd.tif
    CRS: EPSG:4326
    Value Range: 0 to 922 (raster format)
    """
    pop_density_path = 'input/landscan_hd.tif'
    with rasterio.open(pop_density_path) as src:
        # Read data
        pop_density_data = src.read(1, masked=True)
        pop_density_data = np.where(pop_density_data == src.nodata, np.nan, pop_density_data)
        pop_density_transform = src.transform
    return pop_density_data, pop_density_transform

def read_and_clip_human_settlement(masovian_boundary):
    """
    Read and clip GHS_BUILT_S Human Settlement Dataset.
    File: input/human_settlement.tif
    CRS: EPSG:4326
    Value Range: 0 to 4819 (raster format)
    Note: Set the NoData value to 0.
    """
    human_settlement_path = 'input/human_settlement.tif'
    with rasterio.open(human_settlement_path) as src:
        # Read data
        human_settlement_data = src.read(1, masked=True)
        human_settlement_data = np.where(human_settlement_data == src.nodata, 0, human_settlement_data)
        human_settlement_transform = src.transform
    return human_settlement_data, human_settlement_transform

def read_and_clip_land_cover(masovian_boundary):
    """
    Read and clip CORINE Land Cover 2018 Dataset.
    File: input/land_cover.tif
    CRS: EPSG:4326
    Value Range: 1 to 41 (raster format)
    """
    land_cover_path = 'input/land_cover.tif'
    with rasterio.open(land_cover_path) as src:
        land_cover_data = src.read(1, masked=True)
        land_cover_data = np.where(land_cover_data == src.nodata, np.nan, land_cover_data)
        land_cover_transform = src.transform
    return land_cover_data, land_cover_transform

def resample_modis_data(modis_data, modis_transform, dest_shape, dest_transform):
    if modis_data is None:
        return None
    else:
        # Resample to match the destination grid
        dest_modis_data = np.zeros(dest_shape, dtype=modis_data.dtype)
        reproject(
            source=modis_data,
            destination=dest_modis_data,
            src_transform=modis_transform,
            src_crs='EPSG:4326',
            dst_transform=dest_transform,
            dst_crs='EPSG:4326',
            resampling=Resampling.nearest  # Use nearest to maintain original resolution
        )
        # Replace zeros (from areas without data) with NaN
        dest_modis_data = np.where(dest_modis_data == 0, np.nan, dest_modis_data)
        return dest_modis_data

if __name__ == "__main__":
    # Fetching Data Progress Bar
    with tqdm(total=100, desc='Fetching Data', unit='%', ncols=80) as pbar:
        # Fetch Traxelektronik data
        trax_stations, trax_temperatures = fetch_traxelektronik_data()
        pbar.update(20)

        # Geocode Traxelektronik stations
        geolocator = Nominatim(user_agent="geoapi")
        trax_coords = {}

        for station in tqdm(trax_stations, desc="Geocoding Traxelektronik Stations", unit="station"):
            cleaned_station = clean_station_name(station)
            coords, was_cleaned = get_coordinates(cleaned_station, geolocator)
            if coords:
                trax_coords[cleaned_station] = coords  # Use cleaned station names as keys
        pbar.update(30)

        # Fetch Netatmo data
        netatmo_stations, netatmo_temperatures, netatmo_coords = fetch_netatmo_data()
        pbar.update(20)

        # Fetch IMGW data
        imgw_stations, imgw_temperatures, imgw_station_ids = fetch_imgw_data()
        pbar.update(10)

        # Geocode IMGW stations
        imgw_coords = {}
        for station in tqdm(imgw_stations, desc="Geocoding IMGW Stations", unit="station"):
            coords, was_cleaned = get_coordinates(station, geolocator)
            if coords:
                imgw_coords[station] = coords
        pbar.update(20)

    # Paths to MODIS data folders
    modis_day_folder = 'input/MODIS_day'
    modis_night_folder = 'input/MODIS_night'

    # Call the function with all required arguments
    smoothed_temperature, transform = plot_stations_with_boundary_and_interpolation(
        trax_stations, trax_temperatures, trax_coords,
        netatmo_stations, netatmo_temperatures, netatmo_coords,
        imgw_stations, imgw_temperatures, imgw_coords,
        modis_day_folder, modis_night_folder
    )

    # Coordinates to test
    test_lat1 = 51.40232
    test_lon1 = 21.14761
    test_lat2 = 51.30873
    test_lon2 = 21.13436

    # Call the modified function
    interpolated_temp1, interpolated_temp2 = get_interpolated_temperature_at_two_points(
        test_lat1, test_lon1, test_lat2, test_lon2, smoothed_temperature, transform
    )

    if interpolated_temp1 is not None and interpolated_temp2 is not None:
        print(f"\nRadom [{test_lat1}, {test_lon1}]: {interpolated_temp1:.2f}°C")
        print(f"Mazowszany [{test_lat2}, {test_lon2}]: {interpolated_temp2:.2f}°C")
    else:
        print(f"\nOne or both coordinates are outside the interpolated grid or have no data.")

Objective:
I am seeking to enhance a temperature interpolation model by incorporating topographic data, specifically slope and aspect derived from a Digital Elevation Model (DEM). The goal is to ensure that these topographic factors significantly influence temperature calculations in a scientifically robust manner.

Requirements:
1. Data Integration:
- Integrate slope and aspect data from the DEM into the existing temperature interpolation model.
- Ensure that the model accounts for how these topographic features affect temperature variations.
2. Model Training:
- Train the temperature interpolation model using the combined dataset of topography and temperature readings.
- Utilize appropriate machine learning techniques to optimize the model's accuracy.
3. Code Implementation:
- Provide an updated section of the code that reflects these changes.
- Include comments in the code to explain each modification and its purpose.
4. Detailed Explanation:
- Explain how the integration of slope and aspect data will impact temperature calculations.
- Discuss the scientific principles behind these influences, including any relevant literature or studies that support this approach.
5. Testing and Validation:
- Suggest methods for validating the accuracy of the updated model.
- Include recommendations for performance metrics to assess improvements in temperature predictions.
6. Deployment Considerations:
- Outline any necessary steps for deploying the updated model in a production environment.
- Address potential security considerations related to data handling and model deployment.
ASSISTANT
**Updated Code with Integration of Slope and Aspect Data**

Below is the updated section of the code that integrates slope and aspect derived from the DEM into the temperature interpolation model. Comments are included to explain each modification and its purpose.

```python
def plot_stations_with_boundary_and_interpolation(
        trax_stations, trax_temperatures, trax_coords,
        netatmo_stations, netatmo_temperatures, netatmo_coords,
        imgw_stations, imgw_temperatures, imgw_coords,
        modis_day_folder, modis_night_folder):
    # ... (previous code remains unchanged)
    
    # Read and clip DEM
    dem_data, dem_transform = read_and_clip_dem(masovian_boundary)
    
    # **Compute Slope and Aspect from DEM**
    # The slope and aspect of the terrain can influence temperature by affecting solar radiation exposure.
    slope_data, aspect_data = compute_slope_aspect(dem_data, dem_transform)
    
    # Ensure geometries are in EPSG:4326
    trax_gdf_dem_crs = trax_gdf.to_crs("EPSG:4326")
    netatmo_gdf_dem_crs = netatmo_gdf.to_crs("EPSG:4326")
    imgw_gdf_dem_crs = imgw_gdf.to_crs("EPSG:4326")
    
    # **Get Slope and Aspect at station points**
    stations_gdf['Slope'] = get_raster_value_at_points(
        stations_gdf.geometry,
        raster_data=slope_data,
        raster_transform=dem_transform
    )
    stations_gdf['Aspect'] = get_raster_value_at_points(
        stations_gdf.geometry,
        raster_data=aspect_data,
        raster_transform=dem_transform
    )
    
    # Similarly, add Slope and Aspect to grid points
    grid_gdf['Slope'] = get_raster_value_at_points(
        grid_gdf.geometry,
        raster_data=slope_data,
        raster_transform=dem_transform
    )
    grid_gdf['Aspect'] = get_raster_value_at_points(
        grid_gdf.geometry,
        raster_data=aspect_data,
        raster_transform=dem_transform
    )
    
    # Add 'Slope' and 'Aspect' to the list of feature columns
    feature_columns = ['X', 'Y', 'Elevation', 'Tree_Cover', 'Distance_to_Water', 'Water_Body', 'MODIS_LST',
                    'Population_Density', 'Built_Up', 'Slope', 'Aspect'] + list(land_cover_dummies_stations.columns)
    
    # Proceed with model training using the updated feature set
    # ... (rest of the code remains unchanged)
    
def compute_slope_aspect(dem_data, dem_transform):
    """
    Compute slope and aspect from DEM data using numpy gradient.
    Slope and aspect are returned in degrees.
    """
    # Get cellsize from transform
    cellsize_x = dem_transform.a  # Pixel size in x-direction
    cellsize_y = -dem_transform.e  # Pixel size in y-direction (negative because of north-up images)
    
    # Calculate the gradient in x and y directions
    dy, dx = np.gradient(dem_data, cellsize_y, cellsize_x)
    
    # Compute slope
    slope = np.arctan(np.hypot(dx, dy))
    slope_deg = np.degrees(slope)
    
    # Compute aspect
    aspect = np.arctan2(-dx, dy)
    aspect_deg = np.degrees(aspect)
    aspect_deg = np.where(aspect_deg < 0, 360 + aspect_deg, aspect_deg)
    
    return slope_deg, aspect_deg
```

**Explanation of Modifications:**

1. **Compute Slope and Aspect:**

   - **Purpose:** The `compute_slope_aspect` function calculates the slope and aspect from the DEM data. Slope represents the steepness of the terrain, and aspect represents the compass direction that the slope is facing.
   - **Method:** Uses numpy's `gradient` function to compute the gradient of the DEM in both x and y directions. Slope is calculated as the arctangent of the gradient magnitude, and aspect is calculated using the arctangent of the gradient components.

2. **Extract Slope and Aspect at Station Points and Grid Points:**

   - **Purpose:** To include the topographic features (slope and aspect) in the model, we need to extract their values at the station locations and at each grid cell.
   - **Method:** Utilizes the `get_raster_value_at_points` function to sample the slope and aspect values at the given geometries.

3. **Include Slope and Aspect in the Model Features:**

   - **Purpose:** By adding 'Slope' and 'Aspect' to the list of feature columns (`feature_columns`), these variables become part of the input data used to train the machine learning model.
   - **Impact:** This allows the model to learn the relationships between topography and temperature variations.

**Scientific Explanation of Slope and Aspect Influence on Temperature:**

1. **Slope:**

   - **Steepness Effect:** Steeper slopes can influence drainage patterns, wind exposure, and microclimates. Steep slopes may facilitate cold air drainage at night, leading to cooler temperatures in valley bottoms.
   - **Solar Radiation Angle:** The angle of the slope affects the incident solar radiation. Slopes that are tilted towards the sun receive more direct sunlight, increasing temperatures.

2. **Aspect:**

   - **Solar Radiation Exposure:** Aspect determines the direction a slope faces, which influences the amount of solar radiation it receives. In the Northern Hemisphere:
     - **South-facing slopes** receive more solar radiation due to the sun's path, leading to warmer temperatures.
     - **North-facing slopes** receive less solar radiation, resulting in cooler temperatures.
   - **Applications in Microclimatology:** Aspect affects soil moisture, vegetation types, and microclimates, all of which can influence local temperature.

**Relevant Literature:**

- **Geiger, R., Aron, R. H., & Todhunter, P. (2009). "The Climate Near the Ground."** This book discusses microclimatic processes, including the effects of topography on temperature.
- **Oke, T. R. (1987). "Boundary Layer Climates."** Explains the influence of topographic variables like slope and aspect on local climates.
- **Dobrowski, S. Z. (2011). "A climatic basis for microrefugia: the influence of terrain on climate."** Global Change Biology, which highlights how terrain features affect local climatic conditions.

**Validation Methods:**

To validate the accuracy of the updated model, the following methods and performance metrics can be used:

1. **Cross-Validation:**

   - **K-Fold Cross-Validation:** Continue using 5-fold cross-validation to assess the model's performance on unseen data.
   - **Ensure Consistency:** Use the same splits as before to make a fair comparison between models with and without the new features.

2. **Performance Metrics:**

   - **Mean Absolute Error (MAE):** Measures the average magnitude of errors.
   - **Mean Squared Error (MSE) and Root Mean Squared Error (RMSE):** Penalize larger errors and provide insights into variance.
   - **R² Score (Coefficient of Determination):** Indicates how well the model explains the variance in the data.
   - **Median Absolute Error:** Less sensitive to outliers than MAE.

3. **Model Comparison:**

   - **Before and After Comparison:** Compare the performance metrics of the model before and after incorporating slope and aspect.
   - **Statistical Tests:** Use statistical significance tests (e.g., paired t-test) to determine if the improvements are significant.

4. **Residual Analysis:**

   - **Spatial Analysis of Residuals:** Map the residuals to see if there is spatial autocorrelation or patterns indicating model biases.
   - **Assess Improvement in Heterogeneous Areas:** Check if areas with complex topography show improved predictions.

**Deployment Considerations:**

1. **Model Update:**

   - **Retrain the Model:** Since new features are added, retrain and validate the model thoroughly before deployment.
   - **Version Control:** Use version control systems (e.g., Git) to track changes and manage different versions of the model.

2. **Data Handling:**

   - **Data Storage:** Ensure that the DEM data and derived products (slope and aspect rasters) are securely stored and managed.
   - **Data Privacy:** While the topographic data is typically public, ensure compliance if any sensitive data is used in the future.

3. **Infrastructure:**

   - **Scalability:** If deploying the model for real-time predictions, ensure the computational infrastructure can handle the processing load.
   - **Accessibility:** Provide appropriate APIs or interfaces for users to access the model predictions.

4. **Security Considerations:**

   - **Access Control:** Limit access to model code and data to authorized personnel.
   - **Data Encryption:** Use encryption for data at rest and in transit if sensitive information is involved.
   - **Regular Updates and Patches:** Keep the deployment environment updated to protect against vulnerabilities.

5. **Monitoring and Maintenance:**

   - **Performance Monitoring:** Continuously monitor the model's performance in the production environment.
   - **Feedback Loop:** Implement mechanisms to collect feedback and retrain the model periodically with new data.

**Conclusion:**

By incorporating slope and aspect into the temperature interpolation model, we account for the significant influence of topography on local temperature variations. This integration is expected to enhance the model's accuracy, particularly in areas with varied terrain. The updated model should be thoroughly validated using robust statistical metrics and deployed with careful consideration of operational and security aspects.

turns-00031.parquet:44505

6340e304928380ade21031c6
turn 1/1gpt-4o-2024-08-06EnglishUnited States320 words
degenerate_repetitionAbsentFinal dense release
USER
the following content is the metadata for a bacterial isolate from 3 different sources. The metadata from each source includes information after </output>
    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>

You must check if the metadata from 3 different sources are all consist. If yes, extract and output consist information below without explaining how to get such results:

    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>


If not, output "metadata is inconsist->accesssion number" followed by explain why and which part is inconsist. PS:
    a. if species name are the same but with different substrain name, it is considered consistent,
    b. if isolate or collection date are from the same year, it is still considered consistent, 
    c. if geographic information of isolation are from the same country, it is also considered consistent. 
    d. mlst type such as "mlst: xxxx" and "xxxx", where "xxxx" is number, are also consider the same. also, "mlst" may mispell as "mlsty", you need correct it before comparing
    e. Ignore upper and lower case of the metadata

------------------------------
source 0:
------------------------------
<output>
accession number: GCA_029073945.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_78->isolate or collection date in yyyy-mm-dd: 1998-07-16 format->geographic information of islateion: Australia->host name: pig->mlst type: 98->serotype: N/A->vaccine: no
<output/>
source 1:
------------------------------
accession number: GCA_029073945.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_78->isolate or collection date in yyyy-mm-dd: 1998-07-16->geographic information of islateion: Australia->host name: pig->mlst type: 98->serotype: N/A->vaccine: no


source 2:
------------------------------
<output>
accession number: GCA_029073945.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_78->isolate or collection date in yyyy-mm-dd: 1998-07-16->geographic information of islateion: Australia->host name: pig->mlst type: MLST:98->serotype: N/A->vaccine: no
<output/>
ASSISTANT
```
</output>
    accession number: GCA_029073945.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_78->isolate or collection date in yyyy-mm-dd: 1998-07-16 format->geographic information of islateion: Australia->host name: pig->mlst type: 98->serotype: N/A
<output/>
```

turns-00031.parquet:44506

4b698158c3873decb2016100
turn 1/1o1-preview-2024-09-12EnglishIndia1412 words
degenerate_repetitionAbsentFinal dense release
USER
Create TradingView Indicator for Following Details: In trading, a Demand Zone is a price area where there is a significant buying interest, often leading to a halt or reversal in the price downtrend. It represents a region where buyers outnumber sellers, causing prices to rise after reaching this level. Demand zones are important to traders because they indicate a strong potential for price support and reversals, making them attractive areas to enter long positions.

How it Looks Like:
Visually, a demand zone is often identified by sharp price movements upwards from a specific price range. On a price chart, it can be represented as a horizontal or slightly slanted rectangle that covers the area where price sharply bounced up from a previous downtrend. It could look like a consolidation phase followed by a strong bullish candle indicating the start of increased demand.

Types of Demand Zones:
Rally-Base-Rally (RBR):

A rally (upward movement) followed by a base (period of consolidation or sideways movement) and then another rally. The base in this case is the demand zone where buyers gather strength to push the price higher.
Appearance: The price rises, consolidates briefly (forming the base), and then continues to rise.
Drop-Base-Rally (DBR):

A drop (downward movement) followed by a base (sideways movement) and then a rally (upward movement). The base is considered a demand zone where buyers step in after the price drops.
Appearance: The price falls, consolidates at the base, and then reverses to rise sharply.
Key Characteristics of a Demand Zone:
Sharp Reversals: A sharp price movement or reversal typically follows a demand zone.
Volume Increase: Often, a demand zone is accompanied by increased trading volume, indicating higher buyer interest.
Support Levels: The demand zone acts as a support level, preventing the price from falling further.
ASSISTANT
Certainly! Below is a TradingView indicator written in Pine Script that identifies and plots demand zones based on the Rally-Base-Rally (RBR) and Drop-Base-Rally (DBR) patterns you've described. This indicator will mark the demand zones on your chart, highlighting areas where price consolidation (bases) occurred before a rally, which are potential zones of increased demand.

```pine
//@version=5
indicator("Demand Zone Indicator", overlay=true)

// User Inputs
rallyBars     = input.int(title="Number of Rally/Drop Bars", defval=2, minval=1)
baseBars      = input.int(title="Number of Base Bars", defval=3, minval=1)
atrPeriod     = input.int(title="ATR Period", defval=14, minval=1)
atrMultiplier = input.float(title="ATR Multiplier for Base", defval=1.0, minval=0.1)

// Calculate ATR for base range comparison
atrValue = ta.atr(atrPeriod)

// Function to check if price is moving up over 'n' bars
isRally(n, offset) =>
    result = true
    for i = 0 to n - 1
        result := result and (close[offset + i] > close[offset + i + 1])
    result

// Function to check if price is moving down over 'n' bars
isDrop(n, offset) =>
    result = true
    for i = 0 to n - 1
        result := result and (close[offset + i] < close[offset + i + 1])
    result

// Function to check if the base is a consolidation (small range)
isBase(n, offset) =>
    baseHigh  = ta.highest(n)[offset]
    baseLow   = ta.lowest(n)[offset]
    baseRange = baseHigh - baseLow
    baseRange < (atrValue[offset] * atrMultiplier)

// Variables to keep track of demand zones
var int   zoneCounter = 0
var table demandTable = table.new(position.top_right, 1, 1)

// Main Logic
if bar_index >= rallyBars * 2 + baseBars
    // Check for RBR Pattern
    previousRally     = isRally(rallyBars, offset = baseBars + rallyBars)
    baseConsolidation = isBase(baseBars, offset = rallyBars)
    currentRally      = isRally(rallyBars, offset = 0)
    if previousRally and baseConsolidation and currentRally
        // Define the base as a demand zone
        zoneLow  = ta.lowest(baseBars)[rallyBars]
        zoneHigh = ta.highest(baseBars)[rallyBars]
        // Plot the demand zone
        boxDemand = box.new(
          left        = bar_index - (baseBars + rallyBars - 1),
          top         = zoneHigh,
          right       = bar_index - rallyBars,
          bottom      = zoneLow,
          border_color= color.green,
          bgcolor     = color.new(color.green, 90))
        box.set_extend(boxDemand, extend.right)
        label.new(
          x     = bar_index - rallyBars,
          y     = zoneHigh,
          text  = "RBR Demand Zone",
          yloc  = yloc.abovebar,
          style = label.style_label_down,
          color = color.green)
        zoneCounter += 1

    // Check for DBR Pattern
    previousDrop      = isDrop(rallyBars, offset = baseBars + rallyBars)
    baseConsolidation = isBase(baseBars, offset = rallyBars)
    currentRally      = isRally(rallyBars, offset = 0)
    if previousDrop and baseConsolidation and currentRally
        // Define the base as a demand zone
        zoneLow  = ta.lowest(baseBars)[rallyBars]
        zoneHigh = ta.highest(baseBars)[rallyBars]
        // Plot the demand zone
        boxDemand = box.new(
          left        = bar_index - (baseBars + rallyBars - 1),
          top         = zoneHigh,
          right       = bar_index - rallyBars,
          bottom      = zoneLow,
          border_color= color.green,
          bgcolor     = color.new(color.green, 90))
        box.set_extend(boxDemand, extend.right)
        label.new(
          x     = bar_index - rallyBars,
          y     = zoneHigh,
          text  = "DBR Demand Zone",
          yloc  = yloc.abovebar,
          style = label.style_label_down,
          color = color.green)
        zoneCounter += 1

// Display the total number of demand zones detected
plotCounter = input.bool(title="Show Total Number of Demand Zones", defval=true)
if plotCounter
    table.cell(
      demandTable, 0, 0,
      text       = "Demand Zones: " + str.tostring(zoneCounter),
      bgcolor    = color.new(color.green, 90),
      text_color = color.black)
```

### How the Indicator Works:

1. **User Inputs:**
   - **Number of Rally/Drop Bars (`rallyBars`):** Defines how many consecutive bars constitute a rally or drop.
   - **Number of Base Bars (`baseBars`):** Number of bars in the consolidation (base) period.
   - **ATR Period (`atrPeriod`):** The period over which the Average True Range is calculated.
   - **ATR Multiplier (`atrMultiplier`):** Multiplier to define the acceptable range for consolidation based on ATR.

2. **ATR Calculation:**
   - The Average True Range is calculated to assess the volatility and to help identify consolidation periods.

3. **Pattern Detection Functions:**
   - **`isRally` Function:** Checks if the price has been moving up over the specified number of bars.
   - **`isDrop` Function:** Checks if the price has been moving down over the specified number of bars.
   - **`isBase` Function:** Checks if the price has been consolidating within a range smaller than a threshold defined by ATR.

4. **Pattern Recognition Logic:**
   - The script looks back over the bars to find sequences that match the RBR and DBR patterns.
   - **RBR Pattern:**
     - **Previous Rally:** The price was increasing over the specified number of rally bars.
     - **Base Consolidation:** The price then moved sideways within a tight range.
     - **Current Rally:** The price is currently increasing.
   - **DBR Pattern:**
     - **Previous Drop:** The price was decreasing over the specified number of drop bars.
     - **Base Consolidation:** The price then moved sideways within a tight range.
     - **Current Rally:** The price is currently increasing.

5. **Plotting Demand Zones:**
   - When a pattern is detected, the base (consolidation period) is marked as a demand zone.
   - A rectangle (`box`) is drawn to represent the zone, which extends to the right until invalidated.
   - A label is placed on the chart indicating the type of demand zone (RBR or DBR).

6. **Zone Counter Display:**
   - Optionally displays the total number of demand zones detected on the chart.

### Customization:

- **Adjusting Sensitivity:**
  - Increase or decrease `rallyBars` and `baseBars` to adjust how the indicator detects rallies, drops, and bases.
  - Modifying `atrMultiplier` changes the acceptable size of the base; a smaller multiplier makes the base range tighter.

### Notes:

- **Limitations:**
  - The indicator works best on timeframes and instruments where demand zones are prominent.
  - It may need tweaks based on the specific market or timeframe you are analyzing.
  - Always backtest the indicator and adjust parameters to fit the specific characteristics of the asset.

- **Visualization:**
  - The demand zones are shaded in green with an outline, making them easily noticeable on the chart.
  - Labels help identify the type of demand zone and provide contextual information.

### How to Use:

1. **Add the Indicator to Your Chart:**
   - Copy and paste the code into a new indicator in TradingView's Pine Editor.
   - Click on "Add to Chart" to display it.

2. **Interpret the Signals:**
   - Look for shaded green rectangles on your chart; these are the identified demand zones.
   - Consider these zones as potential areas for entering long positions, as they represent price levels where demand has previously pushed prices higher.

3. **Combine with Other Analysis:**
   - Use the demand zones alongside other technical analysis tools to validate potential trading opportunities.
   - Monitor the price action when it returns to these zones to see if it shows signs of respecting the support.

### Disclaimer:

- This indicator is a tool to aid in identifying potential demand zones based on historical price patterns.
- Trading decisions should not be made solely based on this indicator; always use comprehensive analysis and risk management.

Feel free to adjust the parameters and refine the code to better suit your specific trading strategy or the characteristics of the asset you are analyzing. Happy trading!

turns-00031.parquet:44507

332bd9985628fbfcbd20cd4a
turn 1/1gpt-4o-2024-08-06EnglishUnited States318 words
degenerate_repetitionAbsentFinal dense release
USER
the following content is the metadata for a bacterial isolate from 3 different sources. The metadata from each source includes information after </output>
    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>

You must check if the metadata from 3 different sources are all consist. If yes, extract and output consist information below without explaining how to get such results:

    </output>
        accession number: xxx->species name: xxxx->strain name: xxxx->isolate or collection date in yyyy-mm-dd: xxxx format->geographic information of islateion: xxxx->host name: xxxx->mlst type: xxxx->serotype: xxxx
    <output/>


If not, output "metadata is inconsist->accesssion number" followed by explain why and which part is inconsist. PS:
    a. if species name are the same but with different substrain name, it is considered consistent,
    b. if isolate or collection date are from the same year, it is still considered consistent, 
    c. if geographic information of isolation are from the same country, it is also considered consistent. 
    d. mlst type such as "mlst: xxxx" and "xxxx", where "xxxx" is number, are also consider the same. also, "mlst" may mispell as "mlsty", you need correct it before comparing
    e. Ignore upper and lower case of the metadata

------------------------------
source 0:
------------------------------
<output>
accession number: GCA_029073955.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_81->isolate or collection date in yyyy-mm-dd: 1998-10-12 format->geographic information of islateion: Australia->host name: pig->mlst type: 76->serotype: N/A->vaccine: no
<output/>
source 1:
------------------------------
accession number: GCA_029073955.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_81->isolate or collection date in yyyy-mm-dd: 1998-10-12->geographic information of islateion: Australia->host name: pig->mlst type: 76->serotype: N/A->vaccine: no


source 2:
------------------------------
<output>
accession number: GCA_029073955.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_81->isolate or collection date in yyyy-mm-dd: 1998-10-12->geographic information of islateion: Australia->host name: pig->mlst type: 76->serotype: N/A->vaccine: no
<output/>
ASSISTANT
</output>
    accession number: GCA_029073955.1->species name: Erysipelothrix rhusiopathiae->strain name: EMAI_81->isolate or collection date in yyyy-mm-dd: 1998-10-12 format->geographic information of islateion: Australia->host name: pig->mlst type: 76->serotype: N/A
<output/>