Source code for agroecometrics.visualizations

from pathlib import Path
from typing import Optional
import pandas as pd
import numpy as np
import datetime

import matplotlib.pyplot as plt


from agroecometrics import equations, settings

# Gets the acutal labels of columns based on the user settings
LABELS = settings.get_labels()


####    UTIL FUNCTIONS    ####

[docs] def check_png_filename(file_path: Path): """ Validates that the provided file path ends with '.png' and that the directory exists. Args: file_path: A Path object representing the output file path. Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ if not isinstance(file_path, Path): raise TypeError("file_path must be a pathlib.Path object.") if file_path.suffix.lower() != ".png": raise ValueError("The filename must end with '.png'.") if file_path.parent and not file_path.parent.exists(): raise FileNotFoundError(f"The directory '{file_path.parent}' does not exist.")
[docs] def save_plot(file_path: Path): """ Prepares plot to be saved and saves it. Adds labels to plt figure if they exist, saves the plot to the given file_path, and closes the plt plot after saving Args: file_path: A Path object representing the output file path. Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ # Validate input parameters check_png_filename(file_path) handles, labels = plt.gca().get_legend_handles_labels() if handles: # Only add legend if there are labeled elements plt.legend() plt.savefig(file_path, dpi=300, bbox_inches="tight", pad_inches=0.4) plt.close()
#### AIR TEMPERATURE PLOTS ####
[docs] def plot_air_temp(df: pd.DataFrame, T_pred: np.ndarray, file_path: Path): """ Creates a plot of air temperature over the time Args: df: DataFrame with temperature data T_pred: Predicted temperature array file_path: A Path object representing the output file path. Returns: The resolved file path where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ global LABELS # Validate parameters if LABELS['date_norm'] not in df.columns: raise ValueError(f"Date column not found in df. Date column name currently set to {LABELS['date_norm']}") if LABELS['temp_avg'] not in df.columns: raise ValueError(f"Air Temperature column not found in df. Air Temperature column name is currently set to{LABELS['temp_avg']}") check_png_filename(file_path) # Plot actual vs predicted temperature plt.figure(figsize=(8, 4)) plt.scatter(df[LABELS['date_norm']], df[LABELS["temp_avg"]], s=5, color="gray", label="Observed") plt.plot(df[LABELS['date_norm']], T_pred, label="Predicted", color="tomato", linewidth=1) plt.ylabel("Air temperature (Celsius)") plt.xlabel("Date") save_plot(file_path) return file_path.resolve()
#### SOIL TEMPERATURE PLOTS ####
[docs] def plot_yearly_soil_temp(soil_temp: np.ndarray, file_path: Path): """ Creates a plot of modeled soil temperature over a year's time Args: T_soil: A numpy array of soil temperatures for each day of the year in order file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ # Check Parmeters check_png_filename(file_path) # Create the soil temperature plot doy = np.arange(0,365) plt.figure() plt.plot(doy,soil_temp) plt.ylabel("Surface Soil Temperature (Celsius)") plt.xlabel("Day of Year") save_plot(file_path) return file_path.resolve()
[docs] def plot_day_soil_temp( soil_temp: np.ndarray, depths: int, file_path: Path, ): """ Creates a plot of modeled soil temperature at a given depth Args: soil_temp: Is the predicted soil temperatures (°C) depth: Is the depth that the soil temperature predictions are made at. (m) file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ # Check Parameters check_png_filename(file_path) # Create Plot plt.figure() plt.plot(soil_temp, -depths) plt.ylabel("Soil temperature (Celsius)") plt.xlabel("Soil Depth (Centimeters)") save_plot(file_path) return file_path.resolve()
[docs] def plot_3d_soil_temp( doy_grid: np.ndarray, z_grid: np.ndarray, t_grid: np.ndarray, file_path: Path ): """ Creates a 3d plot of soil temperature at different depths over the course of a year Args: doy_gird: A 2d np.ndarray with shape (Nz, 365) containing the day of year for each plot point. z_grid: A 2d np.ndarray with shape (Nz, 365) containing the soil depth for each plot point. t_grid: A 2d np.ndarray with shape (Nz, 365) containing the soil temperature for each plot point. file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ # Check Parameters check_png_filename(file_path) # Create Plot fig = plt.figure(figsize=(10, 6), dpi=80, constrained_layout=True) # 10 inch by 6 inch dpi = dots per inch ax = fig.add_subplot(111, projection="3d") surf = ax.plot_surface(doy_grid, z_grid, t_grid, cmap="viridis", antialiased=False) fig.colorbar(surf, shrink=0.5, aspect=20) frame = surf = ax.plot_wireframe(doy_grid, z_grid, t_grid, linewidth=0.5, color="k", alpha=0.5) # Label x,y, and z axis ax.set_xlabel("Day of the year") ax.set_ylabel("Soil depth [cm]") ax.set_zlabel("Soil temperature (Celsius)") # Set position of the 3D plot ax.view_init(elev=30, azim=35) save_plot(file_path) return file_path.resolve()
[docs] def plot_daily_soil_temp( air_temp: np.ndarray, pred_temp: np.ndarray, depth: int, file_path: Path, soil_temp: Optional[np.ndarray] = None ): """ Creates a plot of air temperature and the predicted soil temperature on a particular date Creates a plot showing the given air temperature and the predicted soil temperature for the given depth. Optionally plots the actual soil temperature for comparision Args: air_temp: The air temperature collected every 5 minutes pred_temp: The soil temperature prediction given in 5 minute intervals depth: The depth that the soil temperature was predicted at file_path: A Path object representing the output file path. soil_temp: The collected soil temperature given in 5 minute intervals Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png' or if the needed label(s) isn't found in the df FileNotFoundError: If the parent directory does not exist. """ # Validate input parameters check_png_filename(file_path) # Generate time intervals time_passed = np.arange(0, 1440, 5) time = time_passed / 60 # Create Plot plt.figure(figsize=(8, 4)) plt.scatter(time, air_temp, s=5, color="gray", label="Air Temperatre") plt.plot(time, pred_temp, label=f"Predicted: {depth}in", color="tomato", linewidth=1) # Add Optional actual soil temperature if soil_temp is not None: plt.scatter(time, soil_temp, s=5, color="blue", label="Soil Temp 4in") # Set labels plt.ylabel("Temperature (Celsius)") plt.xlabel("Time (Hours)") plt.xticks(ticks=[0, 6, 12, 18, 24], labels=["12 AM", "6 AM", "12 PM", "6 PM", "12 AM"]) save_plot(file_path) return file_path.resolve()
[docs] def plot_3d_daily_soil_temp( time_grid: np.ndarray, depth_grid: np.ndarray, temp_grid: np.ndarray, file_path: Path, ): """ Creates a 3D plot of predicted soil temperature over the course of a single day at different depths. Args: time_grid: A 2D numpy array of shape (n_depths, n_times), where each value is time in minutes. depth_grid: A 2D numpy array matching time_grid, where each value is the depth in cm. temp_grid: A 2D numpy array of predicted soil temperatures (same shape as time_grid). file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png' or if the needed label(s) isn't found in the df FileNotFoundError: If the parent directory does not exist. """ check_png_filename(file_path) # Convert minutes to hours for plotting hours_grid = time_grid / 60 depth_grid = -depth_grid # Flip so depth increases downward # Create figure and axis fig = plt.figure(figsize=(10, 6), dpi=100, constrained_layout=True) ax = fig.add_subplot(111, projection='3d') # Surface plot surf = ax.plot_surface(hours_grid, depth_grid, temp_grid, cmap="viridis", antialiased=False) fig.colorbar(surf, shrink=0.5, aspect=20) # Optional: wireframe overlay ax.plot_wireframe(hours_grid, depth_grid, temp_grid, linewidth=0.5, color='k', alpha=0.3) # Axis labels ax.set_xlabel("Hour of Day") ax.set_ylabel("Soil Depth [cm]") ax.set_zlabel("Soil Temperature (°C)") # Set viewing angle ax.view_init(elev=30, azim=35) # Save and return path save_plot(file_path) return file_path.resolve()
#### RAILFALL PLOTS ####
[docs] def plot_rainfall(df: pd.DataFrame, file_path: Path): """ Creates a plot of rainfall and runoff over time. Args: df: DataFrame with cumulative rainfall and runoff file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png' or if the needed label(s) isn't found in the df FileNotFoundError: If the parent directory does not exist. """ global LABELS # Check Parameters if LABELS['date_norm'] not in df.columns: raise ValueError(f"Date column not found in df. Date column name currently set to {LABELS['date_norm']}") if LABELS['rain'] not in df.columns: raise ValueError(f"{LABELS['rain']} not found in the df. Please run the rainfall model first.") if LABELS['runoff'] not in df.columns: raise ValueError(f"{LABELS['runoff']} not found in the df. Please run the rainfall model first.") check_png_filename(file_path) # Create plot with rain and runoff data plt.figure(figsize=(6, 4)) plt.plot(df[LABELS['date_norm']], df[LABELS['rain']], color="navy", label="Rainfall") plt.plot(df[LABELS['date_norm']], df[LABELS['runoff']], color="tomato", label="Runoff") plt.ylabel("Rainfall or Runoff (mm)") save_plot(file_path) return file_path.resolve()
#### EVAPOTRANSPIRATION PLOTS ####
[docs] def plot_evapo_data( df: pd.DataFrame, file_path: Path, model_data: np.ndarray, model_labels: list[str] ): """ Creates a plot of different evapotraspiration data over time Creates a plot over time of the predicted evapotranspiration in mm/day. Creates labels and uses different colors for each of the models. Creates a legend of the model names and colors. Args: df: The DataFrame that the evapotranspiration models were run on. file_path: A Path object representing the output file path. model_data: Is a 2d numpy array of size len(model_labels) x #of days model_labels: Is a list of the names of the models used in model data. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png' or if the needed label(s) isn't found in the df FileNotFoundError: If the parent directory does not exist. """ global LABELS # Check Parameters if len(model_data) != len(model_labels): raise ValueError("You must provide the same number of model labels and model data") if LABELS['date_norm'] not in df.columns: raise ValueError(f"Date column not found in df. Date column name currently set to {LABELS['date_norm']}") check_png_filename(file_path) # Generates a new Plot plt.figure(figsize=(10,4)) # Loop through and plot data from different models for i in range(len(model_data)): data = model_data[i] data_label = model_labels[i] plt.plot(df[LABELS['date_norm']], data, label=data_label) # Adds plot label plt.ylabel("Evapotranspiration (mm/day)") plt.xlabel("Date") save_plot(file_path) return file_path.resolve()
#### GROWING DEGREE DAYS PLOTS ####
[docs] def plot_gdd(df: pd.DataFrame, file_path: Path): """ Creates a plot of the Growing Degree Days that occured over each time segment in the data Args: df: The DataFrame that Growing Degree Days was calculated on. file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png' or if the needed label(s) isn't found in the df FileNotFoundError: If the parent directory does not exist. """ global LABELS # Check Parameters if LABELS['gdd'] not in df.columns: raise ValueError(f"{LABELS['gdd']} was not found in the df. Please run the growing degree days model first") if LABELS['date_norm'] not in df.columns: raise ValueError(f"Date column not found in df. Date column name currently set to {LABELS['date_norm']}") check_png_filename(file_path) # Extract GDD data gdd_data = df[LABELS['gdd']] # Create plot plt.figure(figsize=(6, 4)) plt.plot(df[LABELS["date_norm"]], gdd_data) plt.xlabel("Date") plt.ylabel(f'Growing degree days {chr(176)}C-d)') save_plot(file_path) return file_path.resolve()
[docs] def plot_gdd_sum(df: pd.DataFrame, file_path: Path): """ Creates a plot of the Cumulative Growing Degree Days that occured over the data Args: df: The DataFrame that Growing Degree Days was calculated on. file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png' or if the needed label isn't found in the df FileNotFoundError: If the parent directory does not exist. """ global LABELS # Check parameters if LABELS['gdd_sum'] not in df.columns: raise ValueError(f"{LABELS['gdd_sum']} was not found in the df. Please run the growing degree days ") if LABELS['date_norm'] not in df.columns: raise ValueError(f"Date column not found in df. Date column name currently set to {LABELS['date_norm']}") check_png_filename(file_path) # Extract GDD data gdd_sum_data = df[LABELS['gdd_sum']] # Create plot plt.figure(figsize=(6,2)) plt.plot(df[LABELS['date_norm']], gdd_sum_data) plt.xlabel("Date") plt.ylabel(f'Growing degree days sum {chr(176)}C-d)') save_plot(file_path) return file_path.resolve()
#### PHOTOPERIOD PLOTS ####
[docs] def plot_yearly_photoperiod(latitude: float, file_path: Path): """ Creates a plot of the photoperiod at a specified latitude over a year's time Args: latitude: Latitude in decimal degress. Where the northern hemisphere is positive and the southern hemisphere is negative file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ global LABELS # Check Parameters check_png_filename(file_path) # Set up plot with Title and axes doy = np.arange(0,366) plt.figure(figsize=(6,4)) plt.title('Latitude:' + str(latitude)) plt.xlabel('Day of the year', size=14) plt.ylabel('Photoperiod (hours per day)', size=14) # Calulate photoperiods and adds them to the plot photoperiods, __, __, __, __, __ = equations.photoperiod_at_latitude(latitude, doy) plt.plot(doy, photoperiods, color='k') save_plot(file_path) return file_path.resolve()
[docs] def plot_daily_photoperiod(doys: np.ndarray, file_path: Path): """ Creates a plot of the photoperiod from -45 to 45 degree latitude on the given day of year Args: doys: A np.ndarray of the days of year (0-365) where January 1st is 0 and 365 to perform the calculation file_path: A Path object representing the output file path. Returns: The filename where the plot was saved Raises: TypeError: If file_path is not a Path object. ValueError: If the file extension is not '.png'. FileNotFoundError: If the parent directory does not exist. """ global LABELS # Check Parameters check_png_filename(file_path) # Set up plot with Title and axes plt.figure(figsize=(6,4)) plt.xlabel('Latitude (decimal degrees)', size=14) plt.ylabel('Photoperiod (hours per day)', size=14) # Calculated photoperiods and adds them to the plot latitudes = np.linspace(-45, 45, num=180) # Loop over each day and plot for doy in doys: photoperiod, *_ = equations.photoperiod_on_day(latitudes, doy) plt.plot(latitudes, photoperiod, label=f'DOY {doy}') save_plot(file_path) return file_path.resolve()