import datetime
import pandas as pd
import numpy as np
import polars as pl
from ..logger import duration_logging
from ..utils import heating_season, add_parameter_from_building, CP_AIR, RHO_AIR
from ..creation.boundary import EXTERIOR_WALL, INTERIOR_WALL, ROOF, FLOOR
[docs]
@duration_logging
def unified_degree_hours(buildings, boundaries, climate, parameters):
"""
Calculates the unified degree hours for each boundary and building. Unified degree hours are obtained by taking
the sum of the difference between the heating set point and the exterior temperature for each hour of the heating
season during which the exterior temperature is below the heating set point. For walls, roofs and buildings,
the exterior temperature is the air temperature. For floors, it is the ground temperature.
Args:
buildings (GeoDataframe): a GeoDataframe containing the building geometries
boundaries (GeoDataframe): a GeoDataframe containing the boundary geometries
climate (Dataframe): a Dataframe containing climate data
heating_season_start (datetime.time): start of the heating season
heating_season_end (datetime.time): end of the heating season
Returns:
"""
c_hs = parameters.conventional_heating_set_point
a_hs = parameters.actual_heating_set_point
buildings = buildings.with_columns(
conventional_heating_set_point=pl.lit(c_hs),
actual_heating_set_point=pl.lit(a_hs),
)
boundaries = boundaries.with_columns(
conventional_heating_set_point=pl.lit(c_hs),
actual_heating_set_point=pl.lit(a_hs),
)
# we select the temperatures during the heating season
heating_season_index = heating_season(
climate, parameters.heating_season_start, parameters.heating_season_end
)
air_temperature = climate.loc[heating_season_index, "air_temperature"]
ground_temperature = climate.loc[heating_season_index, "ground_temperature"]
# Calculation is done for unique values of the heating set points and then applied to the corresponding boundaries
# and buildings
for mode in ["conventional", "actual"]:
set_point_variable = f"{mode}_heating_set_point"
udh_variable = f"{mode}_unified_degree_hours"
unique_set_points = boundaries[set_point_variable].unique()
udh_boundaries = np.zeros(boundaries.shape[0])
udh_buildings = np.zeros(buildings.shape[0])
for set_point in unique_set_points:
air_unified_degree_hour = (
set_point - air_temperature[air_temperature < set_point]
).sum()
mask = (boundaries["type"].is_in([EXTERIOR_WALL, INTERIOR_WALL, ROOF])) & (
boundaries[set_point_variable] == set_point
)
udh_boundaries[mask] = air_unified_degree_hour
mask = buildings[set_point_variable] == set_point
udh_buildings[mask] = air_unified_degree_hour
ground_unified_degree_hour = (
set_point - ground_temperature[ground_temperature < set_point]
).sum()
mask = (boundaries["type"].is_in([FLOOR])) & (
boundaries[set_point_variable] == set_point
)
udh_boundaries[mask] = ground_unified_degree_hour
boundaries = boundaries.with_columns(**{udh_variable: udh_boundaries})
buildings = buildings.with_columns(**{udh_variable: udh_buildings})
buildings = buildings.with_columns(
heating_season_duration=pl.lit(heating_season_index.shape[0])
)
return buildings, boundaries
[docs]
@duration_logging
def maximal_temperature_difference(buildings, boundaries, climate):
"""
Calculates the maximal temperature difference between the interior of a building and the exterior air during the
heating period
Args:
boundaries:
climate:
Returns:
"""
min_air_temperature = climate["air_temperature"].min()
min_ground_temperature = climate["ground_temperature"].min()
boundaries = boundaries.with_columns(
min_temperature=pl.when(
pl.col("type").is_in([EXTERIOR_WALL, INTERIOR_WALL, ROOF])
)
.then(pl.lit(min_air_temperature))
.otherwise(pl.lit(min_ground_temperature))
).with_columns(
maximal_temperature_difference=(
pl.col("actual_heating_set_point") - pl.col("min_temperature")
).clip(lower_bound=0.0)
)
buildings = buildings.with_columns(
maximal_temperature_difference=(
pl.col("actual_heating_set_point") - min_air_temperature
).clip(lower_bound=0.0)
)
return buildings, boundaries
[docs]
@duration_logging
def adjacency_factor(boundaries):
"""
Calculates a factor to apply to a boundary to model the share of losses due to adjacency. Its value is 1.
for exterior walls, roofs and floors (no loss prevented), 0.2 for interior walls when the other use is residential,
0.8 for commercial use and 1. for other uses.
Args:
boundaries:
Returns:
"""
exterior_bnds = boundaries["type"].is_in([EXTERIOR_WALL, ROOF, FLOOR])
interior_bnds = boundaries["type"].is_in([INTERIOR_WALL])
boundaries = boundaries.with_columns(
adjacency_factor=pl.when(exterior_bnds)
.then(pl.lit(1.0))
.when(
interior_bnds
& (boundaries["adjacency_usage"].cast(pl.String) == "residential")
)
.then(pl.lit(0.2))
.when(
interior_bnds
& (boundaries["adjacency_usage"].cast(pl.String) == "commercial")
)
.then(pl.lit(0.5))
.otherwise(pl.lit(1.0))
)
return boundaries
[docs]
@duration_logging
def boundary_losses(boundaries):
"""Calculates the annual boundary losses by multiplying the U values of the boundaries with their area and unified
degree hours. Calculates the peak boundary losses by multiplying the U values of the boundaries with their area
and maximal temperature difference.
Args:
boundaries (GeoDataframe): a GeoDataframe containing the boundary geometries and parameters
Returns:
"""
boundaries = boundaries.with_columns(
loss_factor=pl.col("u_value") * pl.col("opaque_area")
+ pl.col("window_u_value") * pl.col("window_area")
)
boundaries = boundaries.with_columns(
annual_thermal_losses=pl.col("loss_factor")
* pl.col("adjacency_factor")
* pl.col("actual_unified_degree_hours")
/ 1000.0,
conventional_thermal_losses=pl.col("loss_factor")
* pl.col("adjacency_factor")
* pl.col("conventional_unified_degree_hours")
/ 1000.0,
peak_thermal_losses=pl.col("loss_factor")
* pl.col("adjacency_factor")
* pl.col("maximal_temperature_difference"),
)
return boundaries
[docs]
@duration_logging
def ventilation_losses(buildings):
"""Calculates the ventilation losses for each building by multiplying the unified degree hours by the air change
rate, the volume and the heat capacity of the air
Args:
buildings:
Returns:
"""
buildings = buildings.with_columns(
volume=pl.col("height") * pl.col("floor_area") / pl.col("floor_count")
)
buildings = buildings.with_columns(
annual_ventilation_losses=pl.col("volume")
* pl.col("air_change_rate")
* pl.col("actual_unified_degree_hours")
* CP_AIR
* RHO_AIR
/ (3600.0 * 1000.0),
conventional_ventilation_losses=pl.col("volume")
* pl.col("air_change_rate")
* pl.col("conventional_unified_degree_hours")
* CP_AIR
* RHO_AIR
/ (3600.0 * 1000.0),
peak_ventilation_losses=pl.col("volume")
* pl.col("air_change_rate")
* pl.col("maximal_temperature_difference")
* CP_AIR
* RHO_AIR
/ 3600.0,
)
return buildings
[docs]
def run_models(buildings, boundaries, climate, parameters):
"""
run the models to calculate the thermal losses of a building (ventilation and boundaries)
Args:
buildings (GeoDataframe): a GeoDataframe containing the building geometries and parameters
boundaries (GeoDataframe): a GeoDataframe containing the boundary geometries and parameters
climate (Dataframe): a Dataframe containing climate data
parameters
Returns:
"""
buildings, boundaries = unified_degree_hours(
buildings, boundaries, climate, parameters
)
buildings, boundaries = maximal_temperature_difference(
buildings, boundaries, climate
)
boundaries = adjacency_factor(boundaries)
boundaries = boundary_losses(boundaries)
buildings = ventilation_losses(buildings)
return buildings, boundaries