Assignment #08

Unit 08

Until next week, work through the material provided in Unit 8 and solve the following exercises.

This week focuses on gridded data visualization and the task of plotting multiple artists from a data frame. The first exercise provides an opportunity to enhance your proficiency in matrix indexing and for-loops. The second exercise is important for refining your skills in reshaping data frames and efficiently creating informative working plots.

Exercise #08-01: Grid search

A grid search involves systematically searching through a predefined set of parameters to find the best parameter configuration for a specific question or model.

We aim to determine how the azimuth and incline of a PV module impact its energy production. Once again, we can utilize our existing solar module to address this question under idealized conditions and simplified assumptions.

Try to reproduce the following figure as closely as possible. Below, you will find some starting points for your code.

Before you get started with your own code, I have prepared an update of the comp_cos_incidenceangle function from our module solar. Take the following code block that defines two functions and replace the old function comp_cos_incidenceangle in the module. (Optimally, you module solar is located in our own development package mytoolbox).

## Note that the function name starts with a `_`.
#  This indicates that the function is only intended to be used *internally*, so from 
#  other functions of this module, but not from users of the module.
#  This is only a convention, though, and highlights the intent of the programmer
#  to the user. Nevertheless, users can still call the function like any other funtion
#  from the module, if they choose to do so.
def _normalize_angle_difference(a, b):
    """
    Compute and normalize the difference between two angles 'a' and 'b' to the range [0, 360) degrees,
    where 0 represent south, and angles increase clockwise.

    Parameters
    ----------
    a (float): The first angle in degrees.
    b (float): The second angle in degrees.

    Returns
    -------
    float: The normalized angle difference between 'a' and 'b' in degrees, within the range [0, 360),
    where 0 represent south, and angles increase clockwise.
    
    This function calculates the difference between two angles 'a' and 'b' and ensures that
    the result is within the range [0, 360) degrees. It handles cases where the difference
    may cross the boundary of 360 degrees by adding or subtracting 360 as needed.
    """
    delta = a - b
    delta_normalized = np.where(delta < 0, delta + 360, delta)
    delta_normalized = np.where(delta_normalized >= 360, delta_normalized - 360, delta_normalized)
    return delta_normalized


def comp_cos_incidenceangle(elevangle, solarazimuth, incline, pvazimuth=0):
    """
    Compute the cosine of the incidence angle of direct sunlight onto an inclined surface

    Parameters
    ----------
    elevangle: array-like, float
        elevation angle of the sun in degrees
    solarazimuth: array-like, float
        solar azimuth as given by function `comp_solarazimuth()`
    incline: float
        incline of the surface in degrees [0, 90]

    Returns
    -------
    cos_theta: array-like, float
        cosine of incidence angle
    """
    cos_theta = (np.cos(np.deg2rad(incline)) * np.sin(np.deg2rad(elevangle)) + 
                 np.sin(np.deg2rad(incline)) * np.cos(np.deg2rad(elevangle)) * np.cos(np.deg2rad(_normalize_angle_difference(solarazimuth, pvazimuth))))
    cos_theta[cos_theta < 0] = 0
    return cos_theta

After you have done that, start a notebook and try to solve the task. The following code blocks give you some starting points.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from importlib import reload
from mytoolbox import solar
reload(solar)


sdo = pd.read_csv('../06_unit/solar_Dornbirn.csv', parse_dates=True, index_col='datetime')
print(sdo)

We will perform the grid search by calling a function that does all the computations for us. In the following, I define that function and within the function body I have left you an opportunity for exercise. This task is not essential to continue with the remaining tasks. Do not get hung up here. Tip: Use the method pd.Series.diff() and the timedelta method .dt.total_seconds() to compute the time sampling in hours. If the time sampling is not equal between all time steps, raise an error.

You can either put the function into solar or directly into your notebook.

## Define a function that will compute the energy produced by the PV module
def comp_energy_produced(iswr0, beta, pvazimuth, elevangle, azimuth, datetime = None, efficiency=0.2, area=1):
    """
    Calculate the energy produced by a PV module based on solar irradiance data.

    Parameters
    ----------
    iswr0 (pandas.Series): Solar irradiance data onto horizontal surface.
    beta (float): Incline angle of the PV module (in degrees).
    pvazimuth (float): Azimuth angle of the PV module (in degrees, 0 in the south).
    elevangle (array-like): Elevation angle of the sun (in degrees).
    azimuth (array-like): Azimuth angle of the sun (in degrees, 0 in the south).
    datetime (pandas.Series or None, optional): Timestamps associated with the irradiance data. If None, 
        the function assumes regular time intervals based on the index of 'iswr0'.
    efficiency (float, optional): Efficiency of the photovoltaic system (default is 0.2).
    area (float, optional): Area of the PV module (default is 1).

    Returns
    -------
    float: Total energy produced by the photovoltaic system (in Wh).

    If 'datetime' is not provided, the function assumes regular time intervals based on the index of 'iswr0'.
    The function raises a ValueError if the time intervals are irregular.
    """

    if datetime is None:
        datetime = iswr0.index.to_series()

    dt = 1  # hard coded time sampling of 1 hour

    # Replace the hard coded time sampling dt:
    # Retrieve unique time sampling dt of `datetime` in (hours) and 
    # raise an error if datetime is irregularly sampled
    #
    # < your code goes here >
    
    
    # Calculate iswr onto inclined PV module
    iswr_beta = solar.comp_irradiance_incline(iswr0, 
                                              solar.comp_cos_incidenceangle(elevangle, azimuth, beta, pvazimuth),
                                              elevangle)
    # Calculate generated power and energy
    power = iswr_beta * area * efficiency
    energy = power.sum() * dt

    return energy

Finally, you can perform the grid search:

## Perform the grid search
alpha_pv = np.arange(-180, 180, 5)  # azimuth angle of PV module (0 in the south, -90 West, 90 East)
beta_pv = np.arange(0, 95, 5)       # incline of PV module

## Create a grid Z based on the previous PV parameters
## and iterate through each element of Z 
# Each element of Z can be computed by `comp_energy_produced()` 
# The data frame `sdo` holds the elevangle (h), solar azimuth (alpha) and iswr0 (iswr_clearsky)
# The PV parameters are defined above.
# Tip: If you don't know how to setup the loop, read the Notes section of the documentation of np.meshgrid().

# < your code goes here >

## Normalize Z with the maximum value of Z that is not NaN

# < your code goes here >

## Plot the contour map

# < your code goes here >
Exercise #08-02: Box plot with multiple artists

Using the sdo data frame storing solar conditions in Dornbirn in 2023:

  1. Extract the month information from the datetime index and write it to a new column named “month”.
  2. Create a wide data frame with month as columns and elevation angle h as values using the .pivot() method.
  3. Try to reproduce the following figure