Skip to content

cooling_heating_workflow_manager

Classes:

CoolingHeatingWorkflowManager

CoolingHeatingWorkflowManager(placements)

Bases: WorkflowManager

__init_(self, placements)

Initialization of an instance of the generic CoolingHeatingWorkflowManager class.

Parameters:

  • placements

    (pandas Dataframe) –
         The locations that the simulation should be run for.
         Columns must include "lon", "lat" (CRS: 4326) and "capacity"
         -The capacity is the nominal capacity of the Heating/Cooling System
    

Returns:

  • CoolingHeatingWorkflowManager
  • Sources
  • [1] https://www.engineeringtoolbox.com/air-density-specific-weight-d_600.html
  • [2] https://www.engineeringtoolbox.com/air-specific-heat-capacity-d_705.html

Methods:

Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def __init__(self, placements):
    """

    __init_(self, placements)

    Initialization of an instance of the generic CoolingHeatingWorkflowManager class.

    Parameters
    ----------
    placements : pandas Dataframe
                 The locations that the simulation should be run for.
                 Columns must include "lon", "lat" (CRS: 4326) and "capacity"
                 -The capacity is the nominal capacity of the Heating/Cooling System

    Returns
    -------
    CoolingHeatingWorkflowManager

    Sources:
    [1] https://www.engineeringtoolbox.com/air-density-specific-weight-d_600.html
    [2] https://www.engineeringtoolbox.com/air-specific-heat-capacity-d_705.html

    """
    # Do basic workflow construction
    assert all([a in placements.columns for a in ["lon", "lat", "capacity"]]), (
        "Placements must contain the columns lon,lat and capacity"
    )
    super().__init__(placements)

    # Set thermodynamic data [1] for air density, [2] for air heat capacity
    airData = np.array(
        [
            [
                0.0002799,
                0.0002797,
                0.0002795,
                0.0002794,
                0.0002793,
                0.0002793,
                0.0002792,
                0.0002792,
                0.0002793,
                0.0002794,
                0.0002795,
                0.0002796,
                0.0002798,
                0.0002799,
                0.0002802,
                0.0002804,
            ],  # heat capacity cp in kWh/(kgK)
            [
                1.739,
                1.657,
                1.582,
                1.514,
                1.451,
                1.394,
                1.341,
                1.292,
                1.246,
                1.204,
                1.164,
                1.127,
                1.093,
                1.06,
                1.029,
                1,
            ],
        ]
    ).T  # density in kg/m3
    self.airData = pd.DataFrame(
        index=[
            -70,
            -60,
            -50,
            -40,
            -30,
            -20,
            -10,
            0,
            10,
            20,
            30,
            40,
            50,
            60,
            70,
            80,
        ],
        data=airData,
        columns=["cp", "density"],
    )  # index refers to ambient air temperature

    self.evaporationCoolingData = pd.DataFrame(
        data=[[1006, 1860, 2.257 * 10**6, 4184]], columns=["cp_air", "cp_vapor", "evaporationHeat", "cp_water"]
    )  # J/(kg*K), J/(kg*K), J/(kg), J/(kg*K)

adjust_variable_to_long_run_average

adjust_variable_to_long_run_average(
    variable: str,
    source_long_run_average: Union[str, float, ndarray],
    real_long_run_average: Union[str, float, ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
    nodata_fallback: str = "nan",
    nodata_fallback_scaling: float = 1,
    allow_nans: bool = True,
)

Adjusts the average mean of the specified variable to a known long-run-average

Note:

uses the equation: variable[t] = variable[t] * real_long_run_average / source_long_run_average

Parameters:

  • variable

    (str) –

    The variable to be adjusted

  • source_long_run_average

    (Union[str, float, ndarray]) –

    The variable's native long run average (the average in the weather file) - If a string is given, it is expected to be a path to a raster file which can be used to look up the average values from using the coordinates in .placements - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements) or at least (placements)

  • real_long_run_average

    (Union[str, float, ndarray]) –

    The variables 'true' long run average - If a string is given, it is expected to be a path to a raster file which can be used to look up the average values from using the coordinates in .placements - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements) or at least (placements)

  • real_lra_scaling

    (float, default: 1 ) –

    An optional scaling factor to apply to the values derived from real_long_run_average. - This is primarily useful when real_long_run_average is a path to a raster file - By default 1

  • spatial_interpolation

    (str, default: 'linear-spline' ) –

    When either source_long_run_average or real_long_run_average are a path to a raster file, this input specifies which interpolation algorithm should be used - Options are: "near", "linear-spline", "cubic-spline", "average" - By default "linear-spline" - See for more info: geokit.raster.interpolateValues

  • nodata_fallback

    (str, default: 'nan' ) –

    When real_long_run_average has no data, one can decide between different fallback options, by default np.nan: - np.nan or None : return np.nan for missing values in real_long_run_average - float : Apply this float value as a scaling factor for all no-data locations only: source_long_run_average * nodata_fallback. NOTE: A value of 1.0 will return the source lra value in case of missing real lra values (no additional nodata_fallback_scaling applied). - str : Will be interpreted as a filepath to a raster with alternative real_long_run_average values, scaled by nodata_fallback_scaling. - callable : any callable method taking the arguments (all iterables): 'locs' and 'source_long_run_average_value' (the locations as gk.geom.point objects and original value from source data). The output values will be considered as the new real_long_run_average for missing locations only (absolute data, no additional nodata_fallback_scaling applied).

    NOTE: np.nan will also be returned in case that the nodata fallback does not yield values either.

  • nodata_fallback_scaling

    (float, default: 1 ) –

    An optional scaling factor to apply to the values derived from nodata_fallback. - This is primarily useful when nodata_fallback is a path to a raster file - By default 1

  • allow_nans

    (boolean, default: True ) –

    If True, NaN values may remain after scaling, else an error will raised. By default True.

Returns:

Source code in reskit/workflow_manager.py
def adjust_variable_to_long_run_average(
    self,
    variable: str,
    source_long_run_average: Union[str, float, np.ndarray],
    real_long_run_average: Union[str, float, np.ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
    nodata_fallback: str = "nan",
    nodata_fallback_scaling: float = 1,
    allow_nans: bool = True,
):
    """Adjusts the average mean of the specified variable to a known long-run-average

    Note:
    -----
    uses the equation: variable[t] = variable[t] * real_long_run_average / source_long_run_average

    Parameters
    ----------
    variable : str
        The variable to be adjusted

    source_long_run_average : Union[str, float, np.ndarray]
        The variable's native long run average (the average in the weather file)
        - If a string is given, it is expected to be a path to a raster file which can be
        used to look up the average values from using the coordinates in `.placements`
        - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements)
        or at least (placements)

    real_long_run_average : Union[str, float, np.ndarray]
        The variables 'true' long run average
        - If a string is given, it is expected to be a path to a raster file which can be
        used to look up the average values from using the coordinates in `.placements`
        - If a numpy ndarray (or derivative) is given, the shape must be one of (time, placements)
        or at least (placements)

    real_lra_scaling : float, optional
        An optional scaling factor to apply to the values derived from `real_long_run_average`.
        - This is primarily useful when `real_long_run_average` is a path to a raster file
        - By default 1

    spatial_interpolation : str, optional
        When either `source_long_run_average` or `real_long_run_average` are a path to a raster
        file, this input specifies which interpolation algorithm should be used
        - Options are: "near", "linear-spline", "cubic-spline", "average"
        - By default "linear-spline"
        - See for more info: geokit.raster.interpolateValues

    nodata_fallback: float, str, callable, optional
        When real_long_run_average has no data, one can decide between different fallback options, by default np.nan:
        - np.nan or None : return np.nan for missing values in real_long_run_average
        - float : Apply this float value as a scaling factor for all no-data locations only: source_long_run_average * nodata_fallback.
        NOTE: A value of 1.0 will return the source lra value in case of missing real lra values (no additional nodata_fallback_scaling applied).
        - str : Will be interpreted as a filepath to a raster with alternative real_long_run_average values, scaled by nodata_fallback_scaling.
        - callable : any callable method taking the arguments (all iterables): 'locs' and 'source_long_run_average_value'
        (the locations as gk.geom.point objects and original value from source data). The output values will be considered as
        the new real_long_run_average for missing locations only (absolute data, no additional nodata_fallback_scaling applied).

        NOTE: np.nan will also be returned in case that the nodata fallback does not yield values either.

    nodata_fallback_scaling: float
        An optional scaling factor to apply to the values derived from `nodata_fallback`.
        - This is primarily useful when `nodata_fallback` is a path to a raster file
        - By default 1

    allow_nans : boolean, optional
        If True, NaN values may remain after scaling, else an error will raised. By default True.

    Returns
    -------
    WorkflowManager
        Returns the invoking WorkflowManager (for chaining)
    """
    if not (nodata_fallback is None or callable(nodata_fallback) or isinstance(nodata_fallback, (float, int, str))):
        raise TypeError(f"'nodata_fallback' must be a float or a Callable.")

    # first get source values
    if isinstance(source_long_run_average, str):
        # assume raster fp
        source_lra = self.get_scalar_values_from_raster(
            fp=source_long_run_average, spatial_interpolation="linear-spline"
        )
    else:
        source_lra = source_long_run_average

    # then get lng-run average values for scaling
    if isinstance(real_long_run_average, str):
        # assume a raster path
        real_lra = self.get_scalar_values_from_raster(
            fp=real_long_run_average, spatial_interpolation=spatial_interpolation
        )
    else:
        real_lra = real_long_run_average

    # replace missing values with no-data fallback if needed
    if isinstance(nodata_fallback, str) and nodata_fallback.lower() == "source":
        warnings.warn(
            "'source' value for 'nodata_fallback' is deprecated and will be removed soon. Use 1.0 instead.",
            DeprecationWarning,
        )
        nodata_fallback = 1.0
    if isinstance(nodata_fallback, str) and nodata_fallback.lower() == "nan":
        warnings.warn(
            "'nan' value for 'nodata_fallback' is deprecated and will be removed soon. Use np.nan instead.",
            DeprecationWarning,
        )
        nodata_fallback = np.nan
    if any(np.isnan(real_lra)):  # TODO currently all real_lra are replaced by fallback, is this intentional?
        # we are lacking long-run average values
        if nodata_fallback is None or (isinstance(nodata_fallback, float) and np.isnan(nodata_fallback)):
            # nans will be returned for missing lra values
            fallback_lra = np.array([np.nan] * len(real_lra))
        elif isinstance(nodata_fallback, (int, float)):
            # apply factor to source_lra to scale missing values
            fallback_lra = nodata_fallback * source_lra  # no additional scaling
        elif callable(nodata_fallback):
            # apply function to calculate missing values
            fallback_lra = nodata_fallback(
                locs=self.locs, source_long_run_average_value=source_lra
            )  # no additional scaling
        elif isinstance(nodata_fallback, str):
            # assume this is yet another raster path as fallback and extract missing values
            fallback_lra = (
                self.get_scalar_values_from_raster(fp=nodata_fallback, spatial_interpolation=spatial_interpolation)
                * nodata_fallback_scaling
            )

        # divide by real_lra_scaling once to compensate later multiplication below for
        # scaling factor, nodata_fallback should not be multiplied by real_lra_scaling
        fallback_lra = fallback_lra / real_lra_scaling
        # set fallback values where real_lra is nan
        real_lra[np.isnan(real_lra)] = fallback_lra[np.isnan(real_lra)]

    # save LRA as attribute
    self.real_lra = real_lra

    # calculate scaling factor:
    # nan result will stay nan results, as these placements cannot be calculated any more
    factors = real_lra * real_lra_scaling / source_lra
    if any(np.isnan(real_lra)):
        if allow_nans:
            warnings.warn(f"NaN values remaining in real lra after application of nodata_fallback.")
        else:
            raise ValueError(f"Missing values for variable '{variable}' and NaNs not allowed.")

    # write info with missing values to sim_data:
    self.placements[f"missing_values_{basename(real_long_run_average)}_nodata_fallback{nodata_fallback}"] = (
        np.isnan(factors)
    )

    self.sim_data[variable] = factors * self.sim_data[variable]
    self.placements[f"LRA_factor_{variable}"] = factors
    return self

apply_loss_factor

apply_loss_factor(
    loss: Union[float, ndarray, FunctionType],
    variables: Union[str, List[str]] = ["capacity_factor"],
)

Applies a loss factor onto a specified variable

Parameters:

  • loss

    (Union[float, ndarray, FunctionType]) –

    The loss factor(s) to be applied - If a float or a numpy ndarray is given, then the following operation is performed:

    variable = variable * (1 - loss) - If a function is given, then the following operation is performed: variable = variable * (1 - loss(variable) ) - If a numpy ndarray is given, it must be broadcastable to the variable's shape in .sim_data

  • variables

    (Union[str, List[str]], default: ['capacity_factor'] ) –

    The variable or variables to apply the loss factor to - By default ["capacity_factor"]

Returns:

Source code in reskit/workflow_manager.py
def apply_loss_factor(
    self,
    loss: Union[float, np.ndarray, FunctionType],
    variables: Union[str, List[str]] = ["capacity_factor"],
):
    """Applies a loss factor onto a specified variable

    Parameters
    ----------
    loss : Union[float, np.ndarray, FunctionType]
        The loss factor(s) to be applied
        - If a float or a numpy ndarray is given, then the following operation is performed:
        > variable = variable * (1 - loss)
        - If a function is given, then  the following operation is performed:
        > variable = variable * (1 - loss(variable) )
        - If a numpy ndarray is given, it must be broadcastable to the variable's shape in
        `.sim_data`

    variables : Union[str, List[str]], optional
        The variable or variables to apply the loss factor to
        - By default ["capacity_factor"]

    Returns
    -------
    WorkflowManager
        Returns the invoking WorkflowManager (for chaining)
    """
    # filter only existing variables
    _variables = [_var for _var in variables if _var in self.sim_data.keys()]
    if len(_variables) < len(variables):
        warnings.warn(
            f"Loss factor could not be applied to the following requested variables because variables are not in sim_data: {', '.join(sorted(set(variables) - set(_variables)))}"
        )

    for var in _variables:
        if isinstance(loss, FunctionType):
            self.sim_data[var] *= 1 - loss(self.sim_data[var])
        else:
            self.sim_data[var] *= 1 - loss

    return self

calculate_approach_evaporative_cooling

calculate_approach_evaporative_cooling(
    temperatureCoolant: float | int,
    heatTransferDelta: float | int,
    efficiencyCoolingTower: float | int,
)

Calculate the approach temperature for an evaporative-cooling system.

Parameters:

  • temperatureCoolant

    (float | int) –

    Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.

  • heatTransferDelta

    (float | int) –

    Temperature difference required for heat transfer from air to coolant [K]

  • efficiencyCoolingTower

    (float | int) –

    Efficiency of the cooling tower system [0, 1]

Returns:

  • float or ndarray

    Approach temperature for the specified weather conditions.

References

[1] 10.1016/j.ijhydene.2024.11.381

Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def calculate_approach_evaporative_cooling(
    self,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int,
    efficiencyCoolingTower: float | int,
):
    """
    Calculate the approach temperature for an evaporative-cooling system.


    Parameters
    ----------
    temperatureCoolant : float | int
        Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.
    heatTransferDelta : float | int
        Temperature difference required for heat transfer from air to coolant [K]
    efficiencyCoolingTower : float | int
        Efficiency of the cooling tower system [0, 1]

    Returns
    -------
    float or np.ndarray
        Approach temperature for the specified weather conditions.

    References
    ----------
    [1] 10.1016/j.ijhydene.2024.11.381
    """
    # Calculate T_CChot
    T_CChot = temperatureCoolant - heatTransferDelta
    # Calculate T_CCcold using wet bulb temperature approximation
    T_CCcold = T_CChot - efficiencyCoolingTower * (T_CChot - self.sim_data["wet_bulb_temperature"])
    # Calculate approach temperature
    approach_temperature = T_CCcold - self.sim_data["wet_bulb_temperature"]

    self.sim_data["approach_temperature_evaporative_cooling"] = approach_temperature

calculate_capacity_factor_air_cooling

calculate_capacity_factor_air_cooling(
    designTemperature: float | int,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyFan: float | int = 0.7,
    efficiencyPump: float | int = 0.7,
    pressureDropAir: float | int = 261,
    pressureDropWater: float | int = 200000,
)

Calculate the capacity factor of an air-cooling system.

The air-cooling system can only provide the cooling load if the ambient air temperature is lower than the design temperature. If the temperature is above the design temperature, pumps and fans are designed too small to provide the necessary flows and the system can only provide less cooling. The reduction in cooling can be calculated based on the ratio of the design power and the theoretically required power for both, fans and pumps individually. The minimum of these ratios is the capacity factor. If the temperature rises above the coolant temperature minus the heat transfer delta, the system needs to shut off.

Parameters:

  • designTemperature

    (float | int) –

    Design ambient temperature of the cooling system [°C].

  • temperatureCoolant

    (float | int) –

    Temperature of the cooling load (lower temperature if sensible heat transfer) [°C].

  • heatTransferDelta

    (float | int, default: 5 ) –

    Temperature difference required for heat transfer from air to coolant [K]. Default is 5.

  • efficiencyFan

    (float | int, default: 0.7 ) –

    Efficiency of the fan system [0, 1]. Default is 0.7.

  • efficiencyPump

    (float | int, default: 0.7 ) –

    Efficiency of the pump system [0, 1]. Default is 0.7.

  • pressureDropAir

    (float | int, default: 261 ) –

    Pressure drop of air through the channels of the cooling frame [Pa]. Default is 261.

  • pressureDropWater

    (float | int, default: 200000 ) –

    Pressure drop of the water circuit from the heat load to the A-frame [Pa]. Default is 200000.

Returns:

  • None

    The method stores the calculated capacity factor in `self.sim_data["capacity_factor"]'.

Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def calculate_capacity_factor_air_cooling(
    self,
    designTemperature: float | int,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyFan: float | int = 0.7,
    efficiencyPump: float | int = 0.7,
    pressureDropAir: float | int = 261,
    pressureDropWater: float | int = 200000,
):
    """
    Calculate the capacity factor of an air-cooling system.

    The air-cooling system can only provide the cooling load if the ambient air
    temperature is lower than the design temperature. If the temperature is above the design temperature,
    pumps and fans are designed too small to provide the necessary flows and the system can only provide less cooling.
    The reduction in cooling can be calculated based on the ratio of the design power and the theoretically required
    power for both, fans and pumps individually. The minimum of these ratios is the capacity factor.
    If the temperature rises above the coolant temperature minus the heat transfer delta, the system needs to shut off.

    Parameters
    ----------
    designTemperature : float | int
        Design ambient temperature of the cooling system [°C].
    temperatureCoolant : float | int
        Temperature of the cooling load (lower temperature if sensible heat transfer) [°C].
    heatTransferDelta : float | int, optional
        Temperature difference required for heat transfer from air to coolant [K]. Default is 5.
    efficiencyFan : float | int, optional
        Efficiency of the fan system [0, 1]. Default is 0.7.
    efficiencyPump : float | int, optional
        Efficiency of the pump system [0, 1]. Default is 0.7.
    pressureDropAir : float | int, optional
        Pressure drop of air through the channels of the cooling frame [Pa]. Default is 261.
    pressureDropWater : float | int, optional
        Pressure drop of the water circuit from the heat load to the A-frame [Pa]. Default is 200000.

    Returns
    -------
    None
        The method stores the calculated capacity factor in `self.sim_data["capacity_factor"]'.
    """
    # At design point:
    PFanDesign = self.calculate_fan_power_air_cooling(
        temperatureCoolant,
        heatTransferDelta=heatTransferDelta,
        efficiencyFan=efficiencyFan,
        pressureDropAir=pressureDropAir,
        designTemperature=designTemperature,
    )
    PPumpDesign = self.calculate_pump_power_air_cooling(
        temperatureCoolant,
        heatTransferDelta=heatTransferDelta,
        efficiencyPump=efficiencyPump,
        pressureDropWater=pressureDropWater,
        designTemperature=designTemperature,
    )

    self.sim_data["capacity_factor"] = xr.where(
        self.sim_data["surface_air_temperature"] <= designTemperature,
        1,  # Case 1: below design temperature, the system can always provide enough cooling.
        xr.where(
            self.sim_data["surface_air_temperature"] < (temperatureCoolant - heatTransferDelta),
            # Case 2: between design and shut off temperature. The system can not provide sufficient cooling. Its limited by:
            np.minimum(
                PPumpDesign / self.sim_data["conversion_factor_pump_electricity"],  # either the pump
                PFanDesign / self.sim_data["conversion_factor_fan_electricity"],  # or the fan
            ),
            0.0,  # Case 3: too hot. The system needs to shut off.
        ),
    )

calculate_fan_power_air_cooling

calculate_fan_power_air_cooling(
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyFan: float | int = 0.7,
    pressureDropAir: float | int = 261,
    designTemperature: float | int = None,
)

Calculate the fan power demand for an air-cooling system.

This method computes the electrical power required by the fan to transfer heat from the air to a coolant, based on the air properties and the cooling load temperature. It can evaluate either for a given design air temperature or for the time series of ambient air temperatures stored in self.sim_data.

Parameters:

  • temperatureCoolant

    (float | int) –

    Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.

  • heatTransferDelta

    (float | int, default: 5 ) –

    Temperature difference required for heat transfer from air to coolant [K]. Default is 5.

  • efficiencyFan

    (float | int, default: 0.7 ) –

    Efficiency of the fan system [0, 1]. Default is 0.7.

  • pressureDropAir

    (float | int, default: 261 ) –

    Pressure drop of air through the channels of the cooling frame [Pa]. Default is 261.

  • designTemperature

    (float | int, default: None ) –

    If specified, the calculation is only evaluated at this air temperature [°C]. Default is None.

Returns:

  • float or ndarray

    Fan power demand in kWh per kWh of cooling. Returns a single value if designTemperature is provided, or a time series array otherwise.

Notes
  • Uses linear interpolation of air specific heat (cp) and density (rho) from self.airData.
  • Assigns np.inf to any time step where the temperature difference is insufficient for cooling.
  • Stores the time series result in self.sim_data["conversion_factor_fan_electricity"] if designTemperature is None.
References

[1] 10.1016/j.ijhydene.2024.11.381 [2] http://hdl.handle.net/1853/55674

Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def calculate_fan_power_air_cooling(
    self,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyFan: float | int = 0.7,
    pressureDropAir: float | int = 261,
    designTemperature: float | int = None,
):
    """
    Calculate the fan power demand for an air-cooling system.

    This method computes the electrical power required by the fan to transfer heat
    from the air to a coolant, based on the air properties and the cooling load
    temperature. It can evaluate either for a given design air temperature or for
    the time series of ambient air temperatures stored in `self.sim_data`.

    Parameters
    ----------
    temperatureCoolant : float | int
        Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.
    heatTransferDelta : float | int, optional
        Temperature difference required for heat transfer from air to coolant [K]. Default is 5.
    efficiencyFan : float | int, optional
        Efficiency of the fan system [0, 1]. Default is 0.7.
    pressureDropAir : float | int, optional
        Pressure drop of air through the channels of the cooling frame [Pa]. Default is 261.
    designTemperature : float | int, optional
        If specified, the calculation is only evaluated at this air temperature [°C].
        Default is None.

    Returns
    -------
    float or np.ndarray
        Fan power demand in kWh per kWh of cooling. Returns a single value if
        `designTemperature` is provided, or a time series array otherwise.

    Notes
    -----
    - Uses linear interpolation of air specific heat (`cp`) and density (`rho`) from `self.airData`.
    - Assigns `np.inf` to any time step where the temperature difference is insufficient for cooling.
    - Stores the time series result in `self.sim_data["conversion_factor_fan_electricity"]` if `designTemperature` is None.

    References
    ----------
    [1] 10.1016/j.ijhydene.2024.11.381
    [2] http://hdl.handle.net/1853/55674
    """
    if designTemperature:
        airTemp = designTemperature
    else:
        airTemp = self.sim_data["surface_air_temperature"]

    # Build interpolators
    f_cp = interp1d(self.airData.index, self.airData["cp"], kind="linear")
    f_rho = interp1d(self.airData.index, self.airData["density"], kind="linear")
    # interpolate element-wise
    cpAir = f_cp(airTemp)
    densityAir = f_rho(airTemp)

    # Calculate Power demand for 1 kWh of cooling:
    WFan = (
        (1 / (cpAir * (temperatureCoolant - heatTransferDelta - airTemp) * densityAir))
        / efficiencyFan
        * pressureDropAir
    )
    WFan = WFan / 1000 / 3600  # Convert J to kWh
    if designTemperature:
        return -WFan
    else:
        WFan[(temperatureCoolant - heatTransferDelta - airTemp) <= 0] = (
            np.inf
        )  # Assign high value if cooling is not possible
        self.sim_data["conversion_factor_fan_electricity"] = -WFan

calculate_pump_power_air_cooling

calculate_pump_power_air_cooling(
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyPump: float | int = 0.7,
    pressureDropWater: float | int = 200000,
    designTemperature: float | int = None,
)

Calculate the pump power demand for an air-cooling system.

This method computes the electrical power required by the pump to circulate coolant for an air-cooling system, based on the cooling load temperature, pressure drop, and pump efficiency. It can evaluate either for a given design air temperature or for the time series of ambient air temperatures stored in self.sim_data.

Parameters:

  • temperatureCoolant

    (float | int) –

    Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.

  • heatTransferDelta

    (float | int, default: 5 ) –

    Temperature difference required for heat transfer from air to coolant [K]. Default is 5.

  • efficiencyPump

    (float | int, default: 0.7 ) –

    Efficiency of the pump system [0, 1]. Default is 0.7.

  • pressureDropWater

    (float | int, default: 200000 ) –

    Pressure drop of the water circuit between the heat load and the cooling frame [Pa]. Default is 200000.

  • designTemperature

    (float | int, default: None ) –

    If specified, the calculation is only evaluated at this air temperature [°C]. Default is None.

Returns:

  • float or ndarray
  • Pump power demand in kWh per kWh of cooling. Returns a single value if
  • `designTemperature` is provided, or a time series array otherwise.
Notes
  • Assumes constant water density of 1000 kg/m³ and specific heat capacity of 4.186 kJ/(kg·K) = 0.00116 kWh/(kg·K).
  • Assigns np.inf to any time step where the temperature difference is insufficient for cooling.
  • Stores the time series result in self.sim_data["conversion_factor_pump_electricity"] if designTemperature is None.
References

[1] 10.1016/j.ijhydene.2024.11.381 [2] 10.1016/j.enconman.2020.113610

Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def calculate_pump_power_air_cooling(
    self,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyPump: float | int = 0.7,
    pressureDropWater: float | int = 200000,
    designTemperature: float | int = None,
):
    """
    Calculate the pump power demand for an air-cooling system.

    This method computes the electrical power required by the pump to circulate
    coolant for an air-cooling system, based on the cooling load temperature,
    pressure drop, and pump efficiency. It can evaluate either for a given design
    air temperature or for the time series of ambient air temperatures stored in
    `self.sim_data`.

    Parameters
    ----------
    temperatureCoolant : float | int
        Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.
    heatTransferDelta : float | int, optional
        Temperature difference required for heat transfer from air to coolant [K]. Default is 5.
    efficiencyPump : float | int, optional
        Efficiency of the pump system [0, 1]. Default is 0.7.
    pressureDropWater : float | int, optional
        Pressure drop of the water circuit between the heat load and the cooling frame [Pa]. Default is 200000.
    designTemperature : float | int, optional
        If specified, the calculation is only evaluated at this air temperature [°C].
        Default is None.

    Returns
    -------
    float or np.ndarray
    Pump power demand in kWh per kWh of cooling. Returns a single value if
    `designTemperature` is provided, or a time series array otherwise.

    Notes
    -----
    - Assumes constant water density of 1000 kg/m³ and specific heat capacity of 4.186 kJ/(kg·K) = 0.00116 kWh/(kg·K).
    - Assigns `np.inf` to any time step where the temperature difference is insufficient for cooling.
    - Stores the time series result in `self.sim_data["conversion_factor_pump_electricity"]` if `designTemperature` is None.

    References
    ----------
    [1] 10.1016/j.ijhydene.2024.11.381
    [2] 10.1016/j.enconman.2020.113610
    """
    if designTemperature:
        airTemp = designTemperature
    else:
        airTemp = self.sim_data["surface_air_temperature"]

    cp = 0.00116  # kWh/(kgK)
    density = 1000  # kg/m3

    # Calculate Power demand for 1 kWh of cooling:
    WPump = (
        (1 / (cp * (temperatureCoolant - heatTransferDelta - airTemp) * density))
        / efficiencyPump
        * pressureDropWater
    )
    WPump = WPump / 1000 / 3600  # Convert J to kWh
    if designTemperature:
        return -WPump
    else:
        WPump[(temperatureCoolant - heatTransferDelta - airTemp) <= 0] = (
            np.inf
        )  # Assign high value if cooling is not possible
        self.sim_data["conversion_factor_pump_electricity"] = -WPump

calculate_relative_cost_factor_air_cooling

calculate_relative_cost_factor_air_cooling(
    designTemperature: float | int,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyFan: float | int = 0.7,
    efficiencyPump: float | int = 0.7,
    pressureDropAir: float | int = 261,
    pressureDropWater: float | int = 200000,
)

Calculate the relative (air temperature dependent) cost factor of an air-cooling system.

The air-cooling system consists of fans, water pumps, and an A-frame heat exchanger that transfers heat from water to air. The A-frame is assumed to always transfer heat with the specified heatTransferDelta. Fan and pump costs depend on the installed nominal power, which varies with ambient air temperature. The relative cost factor is defined as the ratio of cost at the design temperature to the cost at actual ambient temperatures. To cool the same amount of heat at an air temperature higher than the design air temperature, the CAPEX would rise, since pump and fan capacity would increase.

Parameters:

  • designTemperature

    (float | int) –

    Design ambient temperature of the cooling system [°C].

  • temperatureCoolant

    (float | int) –

    Temperature of the cooling load (lower temperature if sensible heat transfer) [°C].

  • heatTransferDelta

    (float | int, default: 5 ) –

    Temperature difference required for heat transfer from air to coolant [K]. Default is 5.

  • efficiencyFan

    (float | int, default: 0.7 ) –

    Efficiency of the fan system [0, 1]. Default is 0.7.

  • efficiencyPump

    (float | int, default: 0.7 ) –

    Efficiency of the pump system [0, 1]. Default is 0.7.

  • pressureDropAir

    (float | int, default: 261 ) –

    Pressure drop of air through the channels of the cooling frame [Pa]. Default is 261.

  • pressureDropWater

    (float | int, default: 200000 ) –

    Pressure drop of the water circuit from the heat load to the A-frame [Pa]. Default is 200000.

Returns:

  • None
  • The method stores the calculated relative cost factor in `self.sim_data["relative_cost_factor"]`
  • and updates the `self.units` dictionary with air-cooling system units.
Notes
  • Fan and pump CAPEX are calculated based on nominal power at the design temperature and scaled according to ambient conditions [1].
  • A-frame cost is assumed constant and independent of ambient temperature [2].
  • The relative cost factor reflects the relative increase in cost at varying ambient conditions compared to the design point.
References

[1] 10.1016/j.energy.2015.05.081 [2] 10.1016/j.enconman.2020.113610

Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def calculate_relative_cost_factor_air_cooling(
    self,
    designTemperature: float | int,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int = 5,
    efficiencyFan: float | int = 0.7,
    efficiencyPump: float | int = 0.7,
    pressureDropAir: float | int = 261,
    pressureDropWater: float | int = 200000,
):
    """
    Calculate the relative (air temperature dependent) cost factor of an air-cooling system.

    The air-cooling system consists of fans, water pumps, and an A-frame heat exchanger
    that transfers heat from water to air. The A-frame is assumed to always transfer
    heat with the specified `heatTransferDelta`. Fan and pump costs depend on the
    installed nominal power, which varies with ambient air temperature. The relative cost
    factor is defined as the ratio of cost at the design temperature to the cost at
    actual ambient temperatures. To cool the same amount of heat at an air temperature higher than the design air temperature, the CAPEX would rise, since pump and fan capacity would increase.

    Parameters
    ----------
    designTemperature : float | int
        Design ambient temperature of the cooling system [°C].
    temperatureCoolant : float | int
        Temperature of the cooling load (lower temperature if sensible heat transfer) [°C].
    heatTransferDelta : float | int, optional
        Temperature difference required for heat transfer from air to coolant [K]. Default is 5.
    efficiencyFan : float | int, optional
        Efficiency of the fan system [0, 1]. Default is 0.7.
    efficiencyPump : float | int, optional
        Efficiency of the pump system [0, 1]. Default is 0.7.
    pressureDropAir : float | int, optional
        Pressure drop of air through the channels of the cooling frame [Pa]. Default is 261.
    pressureDropWater : float | int, optional
        Pressure drop of the water circuit from the heat load to the A-frame [Pa]. Default is 200000.

    Returns
    -------
    None

    The method stores the calculated relative cost factor in `self.sim_data["relative_cost_factor"]`
    and updates the `self.units` dictionary with air-cooling system units.

    Notes
    -----
    - Fan and pump CAPEX are calculated based on nominal power at the design temperature and
    scaled according to ambient conditions [1].
    - A-frame cost is assumed constant and independent of ambient temperature [2].
    - The relative cost factor reflects the relative increase in cost at varying ambient conditions
    compared to the design point.

    References
    ----------
    [1] 10.1016/j.energy.2015.05.081
    [2] 10.1016/j.enconman.2020.113610
    """
    # At design point:
    PFanDesign = -self.calculate_fan_power_air_cooling(
        temperatureCoolant,
        heatTransferDelta=heatTransferDelta,
        efficiencyFan=efficiencyFan,
        pressureDropAir=pressureDropAir,
        designTemperature=designTemperature,
    )
    CAPEXFanDesign = 2.8 * 12300 * (PFanDesign / 50) ** 0.76  # [1]
    PPumpDesign = -self.calculate_pump_power_air_cooling(
        temperatureCoolant,
        heatTransferDelta=heatTransferDelta,
        efficiencyPump=efficiencyPump,
        pressureDropWater=pressureDropWater,
        designTemperature=designTemperature,
    )
    CAPEXPumpDesign = 2.8 * 3540 * (PPumpDesign) ** 0.71  # [1]

    # At real ambient conditions:
    PFan = -self.sim_data["conversion_factor_fan_electricity"]  # PFan in kW
    CAPEXFan = 2.8 * 12300 * (PFan / 50) ** 0.76  # [1]
    PPump = -self.sim_data["conversion_factor_pump_electricity"]
    CAPEXPump = 2.8 * 3540 * (PPump) ** 0.71  # [1]

    # Air Cooler Cost stays the same in any case:
    alpha = 1.135  # kW/(m2K) [2]
    A = 1 / (alpha * heatTransferDelta)  # needed A-frame size for 1 kW of cooling
    CAPEXAC = 2.8 * 156000 * (A / 200) ** 0.89  # [1]

    CAPEXDesign = CAPEXFanDesign + CAPEXPumpDesign + CAPEXAC
    CAPEX = CAPEXFan + CAPEXPump + CAPEXAC

    self.sim_data["relative_cost_factor"] = CAPEXDesign / CAPEX

    # set the units of an air cooling system:
    units = {
        "capacity": "kW_th",
        "relative_cost_factor": "-",
        "capacity_factor": "-",
        "conversion_factor_electricity": "kWh_el/kWh_th",
        "conversion_factor_fan_electricity": "kWh_el/kWh_th",
        "conversion_factor_pump_electricity": "kWh_el/kWh_th",
        "electricity_input": "kWh_el",
        "electricity_input_fan": "kWh_el",
        "electricity_input_pump": "kWh_el",
        "cooling_output": "kWh_th",
    }
    self.units = OrderedDict(units)

calculate_water_losses_evaporative_cooling

calculate_water_losses_evaporative_cooling(
    temperatureCoolant: float | int,
    heatTransferDelta: float | int,
    efficiencyCoolingTower: float | int,
    factorDriftLosses: float | int = 0.001,
    typical_cycles_blowdown: int = 5,
)

Calculate the water losses for an evaporative-cooling system.

Parameters:

  • temperatureCoolant

    (float | int) –

    Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.

  • heatTransferDelta

    (float | int) –

    Temperature difference required for heat transfer from air to coolant [K]

  • efficiencyCoolingTower

    (float | int) –

    Efficiency of the cooling tower system [0, 1]

  • factorDriftLosses

    (float | int, default: 0.001 ) –

    Drift losses by small water droplets carried away by the exhaust air. Defaults to 0.001. [1]

  • typical_cycles_blowdown

    (int, default: 5 ) –

    after how many cycles the blowdown occurs to prevent accumulation of impurities. Defaults to 5. [2]

Returns:

  • float or ndarray

    Specific water losses of evaporative cooling towers (per kWh of cooling load) for the specified weather conditions.

References

[1] 10.1016/j.ijhydene.2024.11.381 [2] 10.1016/j.enconman.2020.113610

Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def calculate_water_losses_evaporative_cooling(
    self,
    temperatureCoolant: float | int,
    heatTransferDelta: float | int,
    efficiencyCoolingTower: float | int,
    factorDriftLosses: float | int = 0.001,
    typical_cycles_blowdown: int = 5,
):
    """
    Calculate the water losses for an evaporative-cooling system.


    Parameters
    ----------
    temperatureCoolant : float | int
        Temperature of the cooling load (lower temperature if sensible heat transfer) in °C.
    heatTransferDelta : float | int
        Temperature difference required for heat transfer from air to coolant [K]
    efficiencyCoolingTower : float | int
        Efficiency of the cooling tower system [0, 1]
    factorDriftLosses : float | int
        Drift losses by small water droplets carried away by the exhaust air. Defaults to 0.001. [1]
    typical_cycles_blowdown: int
        after how many cycles the blowdown occurs to prevent accumulation of impurities. Defaults to 5. [2]

    Returns
    -------
    float or np.ndarray
        Specific water losses of evaporative cooling towers (per kWh of cooling load) for the specified weather conditions.

    References
    ----------
    [1] 10.1016/j.ijhydene.2024.11.381
    [2] 10.1016/j.enconman.2020.113610
    """
    specific_humidity_inlet = calculate_specific_humidity(
        self.sim_data["surface_air_temperature"],
        self.sim_data["relative_humidity"]
        / 100,  # relative humidity between 0,1 needed to calcaulte the specific humidity
    )
    specific_humidity_outlet = calculate_specific_humidity(
        self.sim_data["wet_bulb_temperature"] + self.sim_data["approach_temperature_evaporative_cooling"],
        np.full_like(self.sim_data["wet_bulb_temperature"], 1.0),
    )

    specific_enthalpy_inlet = self.evaporationCoolingData.loc[0, "cp_air"] * (
        self.sim_data["surface_air_temperature"] + 273.15
    ) + specific_humidity_inlet * (
        self.evaporationCoolingData.loc[0, "cp_vapor"] * (self.sim_data["surface_air_temperature"] + 273.15)
        + self.evaporationCoolingData.loc[0, "evaporationHeat"]
    )  # J/kg
    specific_enthalpy_outlet = self.evaporationCoolingData.loc[0, "cp_air"] * (
        (self.sim_data["wet_bulb_temperature"] + 273.15) + self.sim_data["approach_temperature_evaporative_cooling"]
    ) + specific_humidity_outlet * (
        self.evaporationCoolingData.loc[0, "cp_vapor"]
        * (
            (self.sim_data["wet_bulb_temperature"] + 273.15)
            + self.sim_data["approach_temperature_evaporative_cooling"]
        )
        + self.evaporationCoolingData.loc[0, "evaporationHeat"]
    )  # J/kg

    # calculate needed air mass specific for 1 kWh cooling load
    air_mass = 1 / (
        (specific_enthalpy_outlet - specific_enthalpy_inlet) / 3600000
    )  # enthalpy from J/kg to kWh/kg --> air_mass in kg (per kWh)
    evaporation_loss = air_mass * (specific_humidity_outlet - specific_humidity_inlet)
    self.sim_data["specific_mass_evaporation_loss"] = evaporation_loss

    # drift losses:
    water_mass = 1 / (
        self.evaporationCoolingData.loc[0, "cp_water"] * heatTransferDelta / 3600000
    )  # calcualte total water mass, enthalpy from J/kg to kWh/kg --> water_mass in kg (per kWh)
    drift_losses = water_mass * factorDriftLosses
    self.sim_data["specific_mass_drift_loss"] = drift_losses

    # blowdown losses (periodic discharge of water to prevent accumulation of impurities):
    blowdown_losses = evaporation_loss / (typical_cycles_blowdown - 1)
    self.sim_data["specific_mass_blowdown_loss"] = blowdown_losses

    self.sim_data["conversion_factor_water"] = -(
        evaporation_loss + drift_losses + blowdown_losses
    )  # corresponds to the water losses (therefore negative)

    units = {
        "capacity": "kW_th",
        "conversion_factor_water": "kg_H2O/kWh_th",
        "wet_bulb_temperature": "°C",
        "approach_temperature_evaporative_cooling": "K",
        "specific_mass_evaporation_loss": "kg_H2O/kWh_th",
        "specific_mass_drift_loss": "kg_H2O/kWh_th",
        "specific_mass_blowdown_loss": "kg_H2O/kWh_th",
    }
    self.units = OrderedDict(units)

extract_raster_values_at_placements

extract_raster_values_at_placements(raster, **kwargs)

Extracts pixel values at each of the configured placements from the specified raster file

Source code in reskit/workflow_manager.py
def extract_raster_values_at_placements(self, raster, **kwargs):
    """Extracts pixel values at each of the configured placements from the specified raster file"""
    return gk.raster.interpolateValues(raster, points=self.locs, **kwargs)

get_scalar_values_from_raster

get_scalar_values_from_raster(
    fp, spatial_interpolation, points=None
)

Auxiliary function to extract raster values with NaN fallback options.

Source code in reskit/workflow_manager.py
def get_scalar_values_from_raster(self, fp, spatial_interpolation, points=None):
    """
    Auxiliary function to extract raster values with NaN fallback options.
    """
    assert isfile(fp), f"File '{fp}' in adjust_variable_to_long_run_average() does not exist."
    # execute with warnings filter since values outside of source data would trigger geokit UserWarning every time
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")

        if points is None:
            points = [(loc.lon, loc.lat) for loc in self.locs._locations]
        else:
            assert isinstance(points, list) and all([isinstance(x, tuple) and len(x) == 2 for x in points]), (
                "points must be a list of (lon, lat) tuples."
            )

        # interpolateValues returns a scalar for a single location; ensure a 1-D array
        # so the nan handling below (and callers) work regardless of the number of points
        _lra = np.atleast_1d(gk.raster.interpolateValues(fp, points, mode=spatial_interpolation))
        # if getting values fails, it could be because of interpolation method.
        # these values will be replaced with the nearest interpolation method
        if np.isnan(_lra).any():
            _lra_near = np.atleast_1d(gk.raster.interpolateValues(fp, self.locs, mode="near"))
            _lra[np.isnan(_lra)] = _lra_near[np.isnan(_lra)]
        # still nans, i.e. the cell itself is nan, but maybe its neighbors are not
        # try the (nan)median of the surrounding cells
        if np.isnan(_lra).any():

            def _nanmedian(vals, xOff, yOff):
                """Aux function to mimic the 3 expected inputs in interpolateValues()"""
                return np.nanmedian(vals)

            points = [(loc.lon, loc.lat) for loc in self.locs._locations]
            _lra_near = np.atleast_1d(gk.raster.interpolateValues(fp, points, mode="func", func=_nanmedian))
            _lra[np.isnan(_lra)] = _lra_near[np.isnan(_lra)]
    return _lra

read

read(
    variables: Union[str, List[str]],
    source_type: str,
    source: str,
    set_time_index: bool = False,
    spatial_interpolation_mode: str = "bilinear",
    temporal_reindex_method: str = "nearest",
    time_index_from=None,
    **kwargs,
)

Reads the specified variables from the NetCDF4-style weather dataset, and then extracts those variables for each of the coordinates configured in .placements. The resulting data is then available in .sim_data.

Parameters:

  • variables

    (str or list of strings) –

    The variables (or variables) to be read from the specified source - If a path to a weather source is given, then only the 'standard' variables configured for that source type are available (see the doc string for the weather source you are interested in) - If either 'elevated_wind_speed' or 'surface_wind_speed' is included in the variable list, then the members .elevated_wind_speed_height and .surface_wind_speed_height, respectfully, are also added. These are constants which specify what the 'native' wind speed height is, which depends on the source - A pre-loaded NCSource can also be given, thus allowing for any variable in the source to be specified in the variables list. But the user needs to take care of initializing the NCSource and loading the data they want

  • source_type

    (str) –

    The type of weather datasource which is to be loaded. Can be one of: "ERA5", "SARAH", "MERRA", or 'user' - If a pre-loaded NCSource is given for the source object, then the source_type should be "user"

  • source

    (str or NCSource) –

    The source to read weather variables from

  • set_time_index

    (bool, default: False ) –

    If True, instructs the workflow manager to set the time index to that which is read from the weather source - By default False

  • spatial_interpolation_mode

    (str, default: 'bilinear' ) –

    The spatial interpolation mode to use while reading data from the weather source at each of the placement coordinates - By default "bilinear"

  • temporal_reindex_method

    (str, default: 'nearest' ) –

    In the event of missing data, this algorithm is used to fill in the missing data. - Can be, for example, "nearest", "ffill", "bfill", "interpolate" - By default "nearest"

Returns:

Raises:

  • RuntimeError

    If set_time_index is False but no .time_index exists

  • RuntimeError

    If source_type is unknown

Source code in reskit/workflow_manager.py
def read(
    self,
    variables: Union[str, List[str]],
    source_type: str,
    source: str,
    set_time_index: bool = False,
    spatial_interpolation_mode: str = "bilinear",
    temporal_reindex_method: str = "nearest",
    time_index_from=None,
    **kwargs,
):
    """Reads the specified variables from the NetCDF4-style weather dataset, and then extracts
    those variables for each of the coordinates configured in `.placements`. The resulting
    data is then available in `.sim_data`.

    Parameters
    ----------
    variables : str or list of strings
        The variables (or variables) to be read from the specified source
        - If a path to a weather source is given, then only the 'standard' variables
        configured for that source type are available (see the doc string for the
        weather source you are interested in)
        - If either 'elevated_wind_speed' or 'surface_wind_speed' is included in the
        variable list, then the members `.elevated_wind_speed_height` and
        `.surface_wind_speed_height`, respectfully, are also added. These are constants
        which specify what the 'native' wind speed height is, which depends on the source
        - A pre-loaded NCSource can also be given, thus allowing for any variable in the
        source to be specified in the `variables` list. But the user needs to take care
        of initializing the NCSource and loading the data they want

    source_type : str
        The type of weather datasource which is to be loaded. Can be one of:
        "ERA5", "SARAH", "MERRA", or 'user'
        - If a pre-loaded NCSource is given for the `source` object, then the `source_type`
        should be "user"

    source : str or rk.weather.NCSource
        The source to read weather variables from

    set_time_index : bool, optional
        If True, instructs the workflow manager to set the time index to that which is read
        from the weather source
        - By default False

    spatial_interpolation_mode : str, optional
        The spatial interpolation mode to use while reading data from the weather source at
        each of the placement coordinates
        - By default "bilinear"

    temporal_reindex_method : str, optional
        In the event of missing data, this algorithm is used to fill in the missing data.
        - Can be, for example, "nearest", "ffill", "bfill", "interpolate"
        - By default "nearest"

    Returns
    -------
    WorkflowManager
        Returns the invoking WorkflowManager (for chaining)

    Raises
    ------
    RuntimeError
        If set_time_index is False but no `.time_index` exists
    RuntimeError
        If source_type is unknown
    """
    if not set_time_index and self.time_index is None:
        raise RuntimeError("Time index is not available")

    if not isinstance(variables, list):
        variables = [
            variables,
        ]

    if isinstance(source, str) and source_type != "user":
        storage_format = kwargs.pop("storage_format", None)
        is_zarr = storage_format == "zarr" or source.endswith(".zarr") or source.startswith("gs://")
        if source_type == "ERA5":
            source_constructor = rk_weather.Era5ZarrSource if is_zarr else rk_weather.Era5Source
        elif source_type == "SARAH":
            source_constructor = rk_weather.SarahSource
        elif source_type == "MERRA":
            source_constructor = rk_weather.MerraSource
        elif source_type == "ICON-LAM":
            source_constructor = rk_weather.IconlamSource
        else:
            raise RuntimeError("Unknown source_type")

        if source_type == "ERA5":
            time_slice = kwargs.pop("time_slice", None)
            era5_kwargs = dict(kwargs)
            if time_slice is not None:
                if not is_zarr:
                    raise RuntimeError(
                        "'time_slice' is only supported for Zarr-backed ERA5 sources; support for "
                        "netCDF4-backed ERA5 sources is planned. Until then, restrict the time span "
                        "of netCDF4 ERA5 data by selecting the corresponding files instead."
                    )
                era5_kwargs["time_slice"] = time_slice
            source = source_constructor(source, bounds=self.ext, time_index_from=time_index_from, **era5_kwargs)
        else:
            source = source_constructor(source, bounds=self.ext, **kwargs)

        # Load the requested variables
        source.sload(*variables)

    else:  # Assume source is already an initialized NCSource-like object
        missing_variables = [var for var in variables if var not in source.data]
        if missing_variables:
            if hasattr(source, "sload"):
                source.sload(*missing_variables)
            else:
                raise AssertionError(
                    "The given source has no '.sload()' method and is missing the variable(s): "
                    + ", ".join(missing_variables)
                )

    if set_time_index:
        self.set_time_index(source.time_index)

    # read variables
    for var in variables:
        self.sim_data[var] = source.get(
            var,
            self.locs,  # Manipulate locs here
            interpolation=spatial_interpolation_mode,
            force_as_data_frame=True,
        )

        if not set_time_index:
            self.sim_data[var] = self.sim_data[var].reindex(self.time_index, method=temporal_reindex_method)

        self.sim_data[var] = self.sim_data[var].values

        # Special check for wind speed height
        if var == "elevated_wind_speed":
            self.elevated_wind_speed_height = source.ELEVATED_WIND_SPEED_HEIGHT

        if var == "surface_wind_speed":
            self.surface_wind_speed_height = source.SURFACE_WIND_SPEED_HEIGHT

    return self

register_workflow_parameter

register_workflow_parameter(
    key: str, value: Union[str, float]
)

Add a parameter to the WorkflowManager which will be included in the output XArray dataset

Parameters:

  • key

    (str) –

    The workflow parameter's access key

  • value

    (Union[str, float]) –

    The workflow parameter's value. Only strings and floats are allowed

Source code in reskit/workflow_manager.py
def register_workflow_parameter(self, key: str, value: Union[str, float]):
    """Add a parameter to the WorkflowManager which will be included in the output XArray dataset

    Parameters
    ----------
    key : str
        The workflow parameter's access key

    value : Union[str,float]
        The workflow parameter's value. Only strings and floats are allowed
    """
    self.workflow_parameters[key] = value

set_time_index

set_time_index(times: DatetimeIndex)

Sets the time index of the WorkflowManager

Source code in reskit/workflow_manager.py
def set_time_index(self, times: pd.DatetimeIndex):
    """Sets the time index of the WorkflowManager

    Parameters
    ----------
        times : pd.DatetimeIndex
            The timesteps to use throughout the WorkflowManager's life cycle. The
            length of this dataset must match the shape of data which is loaded into
            the WorkflorManager.sim_data member.
    """
    self.time_index = times

    self._time_sel_ = None
    self._time_index_ = self.time_index.copy()
    self._set_sim_shape()

simulate_air_source_heat_pump

simulate_air_source_heat_pump(
    targetTemperature: float | int = 100,
    secondLawEfficiency: float | int = 0.5,
)

Simulate an air-source heat pump and calculate its coefficient of performance (COP) and conversion factors.

The method computes the COP based on the ambient air temperature and the target supply temperature of the heat pump. It also calculates the electricity conversion factor per unit of thermal energy delivered.

Parameters:

  • targetTemperature

    (float | int, default: 100 ) –

    Target temperature at which the heat should be supplied [°C]. Default is 100.

  • secondLawEfficiency

    (float | int, default: 0.5 ) –

    Second law efficiency of the heat pump [0,1]. Default is 0.5.

Returns:

  • None

    The calculated COP and conversion factors are stored in self.sim_data. The self.units dictionary is also updated to reflect the units of the heat pump.

Notes
  • The electricity conversion factor is calculated as -1/COP [kWh_el/kWh_th].
  • Units set include thermal capacity (kW_th), electricity conversion factor (kWh_el/kWh_th), and electricity input (kWh_el).
Source code in reskit/cooling_heating/workflows/cooling_heating_workflow_manager.py
def simulate_air_source_heat_pump(
    self,
    targetTemperature: float | int = 100,
    secondLawEfficiency: float | int = 0.5,
):
    """
    Simulate an air-source heat pump and calculate its coefficient of performance (COP) and conversion factors.

    The method computes the COP based on the ambient air temperature and the target supply
    temperature of the heat pump. It also calculates the electricity conversion factor per
    unit of thermal energy delivered.

    Parameters
    ----------
    targetTemperature : float | int, optional
        Target temperature at which the heat should be supplied [°C]. Default is 100.
    secondLawEfficiency : float | int, optional
        Second law efficiency of the heat pump [0,1]. Default is 0.5.

    Returns
    -------
    None
        The calculated COP and conversion factors are stored in `self.sim_data`.
        The `self.units` dictionary is also updated to reflect the units of the heat pump.

    Notes
    -----
    - The electricity conversion factor is calculated as -1/COP [kWh_el/kWh_th].
    - Units set include thermal capacity (`kW_th`), electricity conversion factor (`kWh_el/kWh_th`),
    and electricity input (`kWh_el`).
    """
    self.sim_data["COP"] = (
        (targetTemperature + 273.15)
        / (targetTemperature - self.sim_data["surface_air_temperature"])
        * secondLawEfficiency
    )
    self.sim_data["conversion_factor_electricity"] = -1 / self.sim_data["COP"]  # kWhel/kWhth

    # set the units of an air source heat pump:
    units = {
        "capacity": "kW_th",
        "conversion_factor_electricity": "kWh_el/kWh_th",
        "electricity_input": "kWh_el",
        "heat_output": "kWh_th",
        "COP": "-",
    }
    self.units = OrderedDict(units)

spatial_disaggregation

spatial_disaggregation(
    variable: str,
    source_high_resolution: Union[str, float, ndarray],
    source_low_resolution: Union[str, float, ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
)

[summary]

Parameters:

  • variable

    (str) –

    [description]

  • source_long_run_average

    (Union[str, float, ndarray]) –

    [description]

  • real_long_run_average

    (Union[str, float, ndarray]) –

    [description]

  • real_lra_scaling

    (float, default: 1 ) –

    [description], by default 1

  • spatial_interpolation

    (str, default: 'linear-spline' ) –

    [description], by default "linear-spline"

Source code in reskit/workflow_manager.py
def spatial_disaggregation(
    self,
    variable: str,
    source_high_resolution: Union[str, float, np.ndarray],
    source_low_resolution: Union[str, float, np.ndarray],
    real_lra_scaling: float = 1,
    spatial_interpolation: str = "linear-spline",
):
    """[summary]

    Parameters
    ----------
    variable : str
        [description]
    source_long_run_average : Union[str, float, np.ndarray]
        [description]
    real_long_run_average : Union[str, float, np.ndarray]
        [description]
    real_lra_scaling : float, optional
        [description], by default 1
    spatial_interpolation : str, optional
        [description], by default "linear-spline"
    """
    # Get values from high resolution tiff file
    if isinstance(source_high_resolution, str):
        points = [(loc.lon, loc.lat) for loc in self.locs._locations]
        correction_values_high_res = gk.raster.interpolateValues(  # TODO change here
            source_high_resolution, points, mode=spatial_interpolation
        )
        # assert not np.isnan(correction_values_high_res).any() and (correction_values_high_res > 0).all()
    else:
        correction_values_high_res = source_high_resolution

    # Get values from low resolution tiff file (meant over eg. ERA5)
    if isinstance(source_low_resolution, str):
        points = [(loc.lon, loc.lat) for loc in self.locs._locations]
        correction_values_low_res = gk.raster.interpolateValues(  # TODO change here
            source_low_resolution, points, mode=spatial_interpolation
        )
        # assert not np.isnan(correction_values_low_res).any() and (correction_values_low_res > 0).all()
    else:
        correction_values_low_res = source_low_resolution

    # correction factors:
    factors = correction_values_high_res / correction_values_low_res
    factors = np.nan_to_num(factors, nan=1 / real_lra_scaling)
    assert (factors > 0).all()

    # update values
    self.sim_data[variable] = self.sim_data[variable] * factors * real_lra_scaling
    return self

to_netcdf

to_netcdf(
    xds: Dataset,
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> str

Saves an XArray dataset to netCDF4 format

Note: - The .placements data is automatically added to the XArray dataset along the 'locations' dimension - The workflow_parameters data is automatically added as dimensionless variables - The .sim_data is automatically added along the dimensions (time, locations) - The .time_index is automatically added along the dimension 'time'

Parameters:

  • xds

    (Dataset) –

    The XArray dataset to save

  • output_netcdf_path

    (str, default: None ) –

    If given, the XArray dataset will be written to disc at the specified path - By default None

  • output_variables

    (List[str], default: None ) –

    If given, specifies the variables which should be included in the resulting dataset. Otherwise all suitable variables found in .placements, .workflow_parameters, .sim_data, and .time_index will be included - Only variables of numeric or string type are suitable due to NetCDF4 limitations - By default None

  • custom_attributes

    (dict, default: None ) –

    If given, adds the key-value pairs as attributes to the XArray dataset before saving - These will be added in addition to existing attributes - By default None

Returns:

  • output_netcdf_path

    The resulting output_netcdf_path

Source code in reskit/workflow_manager.py
def to_netcdf(
    self,
    xds: xarray.Dataset,
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> str:
    """Saves an XArray dataset to netCDF4 format

    Note:
    - The `.placements` data is automatically added to the XArray dataset along the 'locations' dimension
    - The `workflow_parameters` data is automatically added as dimensionless variables
    - The `.sim_data` is automatically added along the dimensions (time, locations)
    - The `.time_index` is automatically added along the dimension 'time'

    Parameters
    ----------
    xds : xarray.Dataset
        The XArray dataset to save

    output_netcdf_path : str
        If given, the XArray dataset will be written to disc at the specified path
        - By default None

    output_variables : List[str], optional
        If given, specifies the variables which should be included in the resulting
        dataset. Otherwise all suitable variables found in `.placements`, `.workflow_parameters`,
        `.sim_data`, and `.time_index` will be included
        - Only variables of numeric or string type are suitable due to NetCDF4 limitations
        - By default None

    custom_attributes : dict, optional
        If given, adds the key-value pairs as attributes to the XArray dataset before saving
        - These will be added in addition to existing attributes
        - By default None

    Returns
    -------
    output_netcdf_path
        The resulting output_netcdf_path
    """
    encoding = dict()

    # write sim_data
    for key in self.sim_data.keys():
        # check if key in requestet output_variables
        if output_variables is not None:
            if key not in output_variables:
                continue
        encoding[key] = dict(zlib=True)

    # FIXME: WHAT IS THIS FOR?
    # #write sim_data_daily, only if exists
    # if hasattr(self, 'sim_data_daily'):
    #     for key in self.sim_data_daily.keys():
    #         #check if key in requestet output_variables
    #         if output_variables is not None:
    #             if key not in output_variables:
    #                 continue
    #         encoding[key] = dict(zlib=True)

    # Add custom attributes if provided
    if custom_attributes is not None:
        for k, v in custom_attributes.items():
            xds.attrs[k] = v

    if output_netcdf_path is not None:
        xds.to_netcdf(output_netcdf_path, encoding=encoding)
        return output_netcdf_path
    else:
        return xds

to_xarray

to_xarray(
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> Dataset

Generates an XArray dataset from the data currently contained in the WorkflowManager

Note: - The .placements data is automatically added to the XArray dataset along the 'locations' dimension - The workflow_parameters data is automatically added as dimensionless variables - The .sim_data is automatically added along the dimensions (time, locations) - The .time_index is automatically added along the dimension 'time'

Parameters:

  • output_netcdf_path

    (str, default: None ) –

    If given, the XArray dataset will be written to disc at the specified path - By default None

  • output_variables

    (List[str], default: None ) –

    If given, specifies the variables which should be included in the resulting dataset. Otherwise all suitable variables found in .placements, .workflow_parameters, .sim_data, and .time_index will be included - Only variables of numeric or string type are suitable due to NetCDF4 limitations - By default None

  • custom_attributes

    (dict, default: None ) –

    If given, adds the key-value pairs as attributes to the XArray dataset - These will be added in addition to the workflow_parameters - By default None

Returns:

  • Dataset

    The resulting XArray dataset

Source code in reskit/workflow_manager.py
def to_xarray(
    self,
    output_netcdf_path: str = None,
    output_variables: List[str] = None,
    custom_attributes: dict = None,
    _intermediate_dict=False,
) -> xarray.Dataset:
    """Generates an XArray dataset from the data currently contained in the WorkflowManager

    Note:
    - The `.placements` data is automatically added to the XArray dataset along the 'locations' dimension
    - The `workflow_parameters` data is automatically added as dimensionless variables
    - The `.sim_data` is automatically added along the dimensions (time, locations)
    - The `.time_index` is automatically added along the dimension 'time'

    Parameters
    ----------
    output_netcdf_path : str, optional
        If given, the XArray dataset will be written to disc at the specified path
        - By default None

    output_variables : List[str], optional
        If given, specifies the variables which should be included in the resulting
        dataset. Otherwise all suitable variables found in `.placements`, `.workflow_parameters`,
        `.sim_data`, and `.time_index` will be included
        - Only variables of numeric or string type are suitable due to NetCDF4 limitations
        - By default None

    custom_attributes : dict, optional
        If given, adds the key-value pairs as attributes to the XArray dataset
        - These will be added in addition to the workflow_parameters
        - By default None

    Returns
    -------
    xarray.Dataset
        The resulting XArray dataset
    """
    if isinstance(output_variables, str):
        output_variables = [output_variables]
    if isinstance(output_variables, list) and not "RESKit_sim_order" in output_variables:
        output_variables.append("RESKit_sim_order")

    times = self.time_index
    if times[0].tz is not None:
        times = [np.datetime64(dt.tz_convert("UTC").tz_convert(None)) for dt in times]
    times_days = np.unique(pd.DatetimeIndex(times).date).astype("datetime64")
    if times_days[0].astype("datetime64[Y]") != times_days[-1].astype("datetime64[Y]"):
        # old tiles where shifted by 1 hour, so the last day of the previous year also appears. catch this problem whti this if clause
        times_days = times_days[1:]
    xds = OrderedDict()
    encoding = dict()

    if "location_id" in self.placements.columns:
        location_coords = self.placements["location_id"].copy()
        del self.placements["location_id"]
    else:
        location_coords = np.arange(self.placements.shape[0])

    # write placements
    for c in self.placements.columns:
        # check if c in requestet output_variables
        if output_variables is not None:
            if c not in output_variables:
                continue
        if np.issubdtype(self.placements[c].dtype, np.number):
            write = True
        else:
            write = True
            for element in self.placements[c]:
                if not isinstance(element, (str, bytearray)):
                    write = False
                    break
        if write:
            xds[c] = xarray.DataArray(
                self.placements[c],
                dims=["location"],
                coords=dict(location=location_coords),
            )

    # write sim_data
    for key in self.sim_data.keys():
        # check if key in requestet output_variables
        if output_variables is not None:
            if key not in output_variables:
                continue

        tmp = np.full((len(self.time_index), self.locs.count), 0.0, dtype=float)
        tmp[self._time_sel_, :] = self.sim_data[key]

        xds[key] = xarray.DataArray(
            tmp,
            dims=["time", "location"],
            coords=dict(time=times, location=location_coords),
        )
        encoding[key] = dict(zlib=True)

    # write sim_data_daily, only if exists
    if hasattr(self, "sim_data_daily"):
        for key in self.sim_data_daily.keys():
            # check if key in requestet output_variables
            if output_variables is not None:
                if key not in output_variables:
                    continue

            tmp = np.full((len(times_days), self.locs.count), np.nan)
            tmp[:, :] = self.sim_data_daily[key]

            xds[key] = xarray.DataArray(
                tmp,
                dims=["time_days", "location"],
                coords=dict(time_days=times_days, location=location_coords),
            )
            encoding[key] = dict(zlib=True)

    if _intermediate_dict:
        return xds

    xds = xarray.Dataset(xds)

    for k, v in self.workflow_parameters.items():
        xds.attrs[k] = v

    # Add custom attributes if provided
    if custom_attributes is not None:
        for k, v in custom_attributes.items():
            xds.attrs[k] = v

    if output_netcdf_path is not None:
        xds.to_netcdf(output_netcdf_path, encoding=encoding)
        return output_netcdf_path
    else:
        return xds