From 5127ccbf996ca072b754201052be991a5b4669bf Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 16 May 2025 13:35:53 +0000 Subject: [PATCH 1/2] Add comprehensive docstrings to HeatingRod class and create microagent file --- .openhands/microagents/repo.md | 78 ++++++++++ vpplib/heating_rod.py | 261 ++++++++++++++++++++++++++++++--- 2 files changed, 321 insertions(+), 18 deletions(-) create mode 100644 .openhands/microagents/repo.md diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md new file mode 100644 index 0000000..9ce9e43 --- /dev/null +++ b/.openhands/microagents/repo.md @@ -0,0 +1,78 @@ +# VPPlib Repository Information + +## Repository Overview +- **Name**: vpplib +- **Owner**: Pyosch +- **URL**: https://github.com/Pyosch/vpplib +- **Description**: A Python library for simulating distributed energy appliances in a virtual power plant. +- **License**: GNU General Public License v3 (GPLv3) + +## Project Structure +The repository is organized as follows: +- `vpplib/`: Main package directory containing all the core modules +- `docs/`: Documentation files +- `input/`: Input data for simulations +- `test_*.py`: Test files for various components + +## Core Components +VPPlib consists of several key classes: + +1. **Component**: Base class for all energy components (generators, consumers, storage) + - `component.py` + +2. **Energy Generation Components**: + - `photovoltaic.py`: Solar PV systems + - `wind_power.py`: Wind power systems + - `combined_heat_and_power.py`: CHP systems + - `heat_pump.py`: Heat pump systems + - `heating_rod.py`: Heating rod systems + - `hydrogen.py`: Hydrogen systems + +3. **Energy Storage Components**: + - `electrical_energy_storage.py`: Battery storage systems + - `thermal_energy_storage.py`: Thermal storage systems + - `battery_electric_vehicle.py`: Electric vehicle batteries + +4. **System Components**: + - `virtual_power_plant.py`: Aggregates multiple components into a VPP + - `operator.py`: Controls the VPP according to implemented logic + - `environment.py`: Encapsulates environmental impacts (weather, time, regulations) + - `user_profile.py`: Contains information on user behavior patterns + +## Dependencies +Main dependencies include: +- windpowerlib +- pvlib +- pandas +- numpy +- pandapower +- simbench +- simses +- polars +- NREL-PySAM +- wetterdienst (version 0.89.0) +- marshmallow (version 3.20.1) + +## Installation +The package can be installed via pip: +```bash +pip install vpplib +``` + +Or directly from the repository: +```bash +git clone https://github.com/Pyosch/vpplib.git +cd vpplib +pip install -e . +``` + +## Python Compatibility +- Requires Python 3.9 or higher +- Compatible with Python 3.9, 3.10, 3.11, 3.12, and 3.13 + +## Development Status +- Development Status: 4 - Beta + +## Contact +- Author: Pyosch +- Email: pyosch@posteo.de \ No newline at end of file diff --git a/vpplib/heating_rod.py b/vpplib/heating_rod.py index ea2ed5d..da573e8 100644 --- a/vpplib/heating_rod.py +++ b/vpplib/heating_rod.py @@ -1,14 +1,60 @@ """ -Info ----- -This file contains the basic functionalities of the VPPHeatPump class. +HeatingRod Module +---------------- +This module contains the HeatingRod class which models an electrical resistance heater +that converts electrical energy directly into thermal energy with a specified efficiency. +The HeatingRod class inherits from the Component base class and implements specific +functionality for simulating heating rods in a virtual power plant environment. + +Key features: +- Conversion of electrical energy to thermal energy +- Power limiting capabilities +- Ramping constraints (minimum runtime, minimum stop time) +- Time series generation for heat output and electrical demand """ import pandas as pd from .component import Component class HeatingRod(Component): + """ + A class representing an electrical heating rod component in a virtual power plant. + + The heating rod converts electrical energy directly into thermal energy with a specified efficiency. + It can be controlled by limiting its power output and includes ramping constraints. + + Attributes + ---------- + identifier : str, optional + Unique identifier for the heating rod + efficiency : float + Conversion efficiency from electrical to thermal energy (default: 0.95) + el_power : float + Nominal electrical power of the heating rod in kW + limit : float + Power limit factor between 0 and 1 (default: 1) + thermal_energy_demand : pandas.DataFrame + Time series of thermal energy demand + rampUpTime : int + Time required for the heating rod to ramp up to full power + rampDownTime : int + Time required for the heating rod to ramp down from full power + min_runtime : int + Minimum time the heating rod must run once started + min_stop_time : int + Minimum time the heating rod must remain off once stopped + lastRampUp : pandas.Timestamp + Timestamp of the last ramp up event + lastRampDown : pandas.Timestamp + Timestamp of the last ramp down event + timeseries_year : pandas.DataFrame + Annual time series of heat output and electrical demand + timeseries : pandas.DataFrame + Time series of heat output and electrical demand for the simulation period + isRunning : bool + Flag indicating whether the heating rod is currently running + """ def __init__(self, thermal_energy_demand, @@ -21,7 +67,32 @@ def __init__(self, min_runtime=0, min_stop_time=0, efficiency=0.95): - + """ + Initialize a HeatingRod object. + + Parameters + ---------- + thermal_energy_demand : pandas.DataFrame + Time series of thermal energy demand + unit : str, optional + Unit of power measurement (default: "kW") + identifier : str, optional + Unique identifier for the heating rod + environment : Environment, optional + Environment object containing simulation parameters and weather data + el_power : float, optional + Nominal electrical power of the heating rod in kW + rampUpTime : int, optional + Time required for the heating rod to ramp up to full power (default: 0) + rampDownTime : int, optional + Time required for the heating rod to ramp down from full power (default: 0) + min_runtime : int, optional + Minimum time the heating rod must run once started (default: 0) + min_stop_time : int, optional + Minimum time the heating rod must remain off once stopped (default: 0) + efficiency : float, optional + Conversion efficiency from electrical to thermal energy (default: 0.95) + """ # Call to super class super(HeatingRod, self).__init__(unit, environment) @@ -29,13 +100,13 @@ def __init__(self, # Configure attributes self.identifier = identifier - #heatinrod parameters + # Heating rod parameters self.efficiency = efficiency self.el_power = el_power self.limit = 1 self.thermal_energy_demand = thermal_energy_demand - #Ramp parameters + # Ramp parameters self.rampUpTime = rampUpTime self.rampDownTime = rampDownTime self.min_runtime = min_runtime @@ -52,8 +123,6 @@ def __init__(self, end=self.environment.end, freq=self.environment.time_freq, name="time")) - -# self.heat_sys_temp = heat_sys_temp self.isRunning = False @@ -141,7 +210,22 @@ def __init__(self, #from VPPComponents def prepareTimeSeries(self): + """ + Prepare the time series data for the simulation period. + + This method checks if thermal energy demand data is available, generates the annual + time series if needed, and extracts the relevant time period for the simulation. + + Raises + ------ + ValueError + If no thermal energy demand data is available. + Returns + ------- + pandas.DataFrame + Time series of heat output and electrical demand for the simulation period. + """ if pd.isna(next(iter(self.thermal_energy_demand.thermal_energy_demand))) == True: raise ValueError("No thermal energy demand available.") @@ -153,7 +237,17 @@ def prepareTimeSeries(self): return self.timeseries def get_timeseries_year(self): + """ + Generate the annual time series for heat output and electrical demand. + + This method calculates the electrical demand based on the thermal energy demand + and the heating rod's efficiency. + Returns + ------- + pandas.DataFrame + Annual time series of heat output and electrical demand. + """ self.timeseries_year["heat_output"] = self.thermal_energy_demand self.timeseries_year["el_demand"] = (self.timeseries_year.heat_output / self.efficiency) @@ -164,17 +258,31 @@ def get_timeseries_year(self): # Controlling functions # ========================================================================= def limitPowerTo(self, limit): + """ + Limit the power output of the heating rod. + Parameters + ---------- + limit : float + Power limit factor between 0 and 1, where: + - 0 means the heating rod is completely off + - 1 means the heating rod operates at full power + + Returns + ------- + None + Returns silently if the limit is invalid. + Notes + ----- + This method validates that the limit is between 0 and 1 before applying it. + """ # Validate input parameter if limit >= 0 and limit <= 1: - # Parameter is valid self.limit = limit - else: - - # Paramter is invalid + # Parameter is invalid return # ========================================================================= @@ -183,13 +291,32 @@ def limitPowerTo(self, limit): # Override balancing function from super class. def valueForTimestamp(self, timestamp): - - if type(timestamp) == int: + """ + Get the electrical demand value for a specific timestamp. + + This method overrides the balancing function from the Component super class. + + Parameters + ---------- + timestamp : int or str + The timestamp for which to retrieve the value. + If int: index position in the timeseries DataFrame. + If str: timestamp in format 'YYYY-MM-DD hh:mm:ss'. + + Returns + ------- + float + The electrical demand at the specified timestamp, adjusted by the power limit. + Raises + ------ + ValueError + If the timestamp is not of type int or string. + """ + if type(timestamp) == int: return self.timeseries.iloc[timestamp]["el_demand"] * self.limit elif type(timestamp) == str: - return self.timeseries.loc[timestamp, "el_demand"] * self.limit else: @@ -198,7 +325,30 @@ def valueForTimestamp(self, timestamp): def observationsForTimestamp(self, timestamp): + """ + Get detailed observations for a specific timestamp. + + This method retrieves or calculates the heat output, electrical demand, and efficiency + for the specified timestamp. + Parameters + ---------- + timestamp : int, str, or pandas.Timestamp + The timestamp for which to retrieve observations. + If int: index position in the timeseries DataFrame. + If str: timestamp in format 'YYYY-MM-DD hh:mm:ss'. + If pandas.Timestamp: a pandas Timestamp object. + + Returns + ------- + dict + A dictionary containing 'heat_output', 'efficiency', and 'el_demand' values. + + Raises + ------ + ValueError + If the timestamp is not of a supported type. + """ if type(timestamp) == int: if pd.isna(next(iter(self.timeseries.iloc[timestamp]))) == False: @@ -261,7 +411,21 @@ def observationsForTimestamp(self, timestamp): return observations def log_observation(self, observation, timestamp): + """ + Log observation values to the timeseries at the specified timestamp. + Parameters + ---------- + observation : dict + Dictionary containing 'heat_output' and 'el_demand' values to be logged. + timestamp : str or pandas.Timestamp + The timestamp at which to log the observation. + + Returns + ------- + pandas.DataFrame + The updated timeseries DataFrame. + """ self.timeseries.loc[timestamp, "heat_output"] = observation["heat_output"] self.timeseries.loc[timestamp, "el_demand"] = observation["el_demand"] @@ -270,7 +434,25 @@ def log_observation(self, observation, timestamp): def isLegitRampUp(self, timestamp): + """ + Check if it's legitimate to ramp up the heating rod at the given timestamp. + + This method verifies if the minimum stop time has been respected since the last ramp down. + Parameters + ---------- + timestamp : int or pandas.Timestamp + The timestamp at which to check if ramping up is legitimate. + + Raises + ------ + ValueError + If the timestamp is not of a supported type. + + Notes + ----- + This method updates the isRunning attribute based on the check result. + """ if type(timestamp) == int: if timestamp - self.lastRampDown > self.min_stop_time: self.isRunning = True @@ -286,7 +468,25 @@ def isLegitRampUp(self, timestamp): "pandas._libs.tslibs.timestamps.Timestamp") def isLegitRampDown(self, timestamp): + """ + Check if it's legitimate to ramp down the heating rod at the given timestamp. + This method verifies if the minimum runtime has been respected since the last ramp up. + + Parameters + ---------- + timestamp : int or pandas.Timestamp + The timestamp at which to check if ramping down is legitimate. + + Raises + ------ + ValueError + If the timestamp is not of a supported type. + + Notes + ----- + This method updates the isRunning attribute based on the check result. + """ if type(timestamp) == int: if timestamp - self.lastRampUp > self.min_runtime: self.isRunning = False @@ -302,8 +502,21 @@ def isLegitRampDown(self, timestamp): "pandas._libs.tslibs.timestamps.Timestamp") def rampUp(self, timestamp): + """ + Attempt to ramp up the heating rod at the given timestamp. - + Parameters + ---------- + timestamp : int or pandas.Timestamp + The timestamp at which to attempt ramping up. + + Returns + ------- + None + If the heating rod is already running. + bool + True if ramping up was successful, False otherwise. + """ if self.isRunning: return None else: @@ -315,9 +528,21 @@ def rampUp(self, timestamp): def rampDown(self, timestamp): + """ + Attempt to ramp down the heating rod at the given timestamp. - - + Parameters + ---------- + timestamp : int or pandas.Timestamp + The timestamp at which to attempt ramping down. + + Returns + ------- + None + If the heating rod is already stopped. + bool + True if ramping down was successful, False otherwise. + """ if not self.isRunning: return None else: From 7dd8c5914501c68193e69a2ac31b85350b1bf298 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 16 May 2025 13:39:15 +0000 Subject: [PATCH 2/2] Add comprehensive docstrings to operator, wind_power, user_profile, and thermal_energy_storage classes --- vpplib/operator.py | 612 ++++++++++++++++++------------- vpplib/thermal_energy_storage.py | 356 +++++++++--------- vpplib/user_profile.py | 415 ++++++++++----------- vpplib/wind_power.py | 328 ++++++++++++----- 4 files changed, 959 insertions(+), 752 deletions(-) diff --git a/vpplib/operator.py b/vpplib/operator.py index d6cad0e..1a22b1d 100644 --- a/vpplib/operator.py +++ b/vpplib/operator.py @@ -1,15 +1,19 @@ # -*- coding: utf-8 -*- """ -Info ----- -The Operator class should be subclassed to implement different -operation stategies. One subclass for example could implement -machine learning to operate the virtual power plant. -Additional functions that help operating the virtual power plant, -like forecasting, should be implemented here. +Operator Module +-------------- +This module contains the Operator class which is responsible for operating +a virtual power plant (VPP) according to different strategies. -TODO: Setup data type for target data and alter the referencing accordingly! +The Operator class serves as a base class that should be subclassed to implement +different operation strategies. For example, a subclass could implement machine +learning algorithms to optimize the operation of the virtual power plant. + +The module also provides functionality for running power flow simulations using +pandapower, analyzing results, and visualizing the performance of the virtual +power plant components. +TODO: Setup data type for target data and alter the referencing accordingly! """ import math @@ -20,51 +24,61 @@ class Operator(object): + """ + A class for operating a virtual power plant according to different strategies. + + The Operator class is responsible for controlling the components of a virtual + power plant to achieve a target generation/consumption profile. It provides + methods for simulating the operation of the virtual power plant, running power + flow calculations, and analyzing the results. + + This class is designed to be subclassed to implement different operation + strategies. The base class provides common functionality, while specific + operation algorithms should be implemented in subclasses. + + Attributes + ---------- + virtual_power_plant : VirtualPowerPlant + The virtual power plant to be operated + target_data : list of tuples + Target generation/consumption data as a list of (timestamp, value) tuples + net : pandapower.pandapowerNet + Pandapower network model for power flow calculations + environment : Environment, optional + Environment object containing weather data and simulation parameters + + Notes + ----- + The operate_at_timestamp method must be implemented by subclasses to define + the specific operation strategy. + """ + def __init__(self, virtual_power_plant, net, target_data, environment=None): """ - Info - ---- - This function takes two parameters. The first one is the virtual - power plant, that should be operated by the operator. It must not - be changed during simulation. The second parameter represents - the target generation/consumption data. The operator tries - to operate the virtual power plant in a way, that this target - output is achieved. - The Operator class should be subclassed to implement different - operation stategies. One subclass for example could implement - machine learning to operate the virtual power plant. - Additional functions that help operating the virtual power plant, - like forecasting, should be implemented here. - + Initialize an Operator object. + Parameters ---------- - - ... - - Attributes - ---------- - - ... - + virtual_power_plant : VirtualPowerPlant + The virtual power plant to be operated. This object must not be + changed during simulation. + net : pandapower.pandapowerNet + Pandapower network model for power flow calculations + target_data : list of tuples + Target generation/consumption data as a list of (timestamp, value) tuples. + The operator tries to operate the virtual power plant to match this target. + environment : Environment, optional + Environment object containing weather data and simulation parameters + Notes ----- - - ... - - References - ---------- - - ... - - Returns - ------- - - ... - + The target_data parameter should be a list of tuples, where each tuple + contains a timestamp and a target value. The timestamp can be either a + string in the format 'YYYY-MM-DD hh:mm:ss' or an integer index. """ # Configure attributes @@ -75,49 +89,42 @@ def __init__(self, def operate_virtual_power_plant(self): """ - Info - ---- - Operation handling - - This function is the key function for the operation of the virtual - power plant. It simulates every timestamp given in the target data. - It returns how good the operation of the virtual power plant matches - the target data (0: no match, 1: perfect match). - - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... - + Operate the virtual power plant according to the target data. + + This method is the key function for the operation of the virtual power plant. + It iterates through all timestamps in the target data, calls the + operate_at_timestamp method for each timestamp, and calculates how well + the operation of the virtual power plant matches the target data. + + The match is calculated as: + match = 1 - (abs(target) - abs(balance)) / abs(target) + + A value of 1 indicates a perfect match, while a value of 0 indicates + no match at all. Negative values can occur if the balance is far from + the target. + Returns ------- - - ... - + float + Average match between the virtual power plant operation and the target data. + A value of 1 indicates a perfect match, while a value of 0 indicates + no match at all. + + Notes + ----- + This method relies on the operate_at_timestamp method, which must be + implemented by subclasses to define the specific operation strategy. + + The match calculation assumes that the target values are non-zero. + If a target value is zero, the match calculation may result in a + division by zero error. """ - # Create result variables power_sum = 0 count = 0 # Iterate through timestamps for i in range(0, len(self.target_data)): - # Operate at timestamp self.operate_at_timestamp(self.target_data[i][0]) @@ -144,37 +151,33 @@ def operate_virtual_power_plant(self): def operate_at_timestamp(self, timestamp): """ - Info - ---- - Raises an error since this function needs to be implemented by child classes. - + Operate the virtual power plant at a specific timestamp. + + This method should be implemented by subclasses to define the specific + operation strategy for the virtual power plant at a given timestamp. + The base implementation raises a NotImplementedError. + Parameters ---------- - - ... - - Attributes - ---------- - - ... - + timestamp : str or int + The timestamp at which to operate the virtual power plant. + If str: timestamp in format 'YYYY-MM-DD hh:mm:ss' + If int: index position in the timeseries + + Raises + ------ + NotImplementedError + This method must be implemented by subclasses + Notes ----- - - ... - - References - ---------- - - ... - - Returns - ------- - - ... - + This method should control the components of the virtual power plant + to achieve the target generation/consumption at the specified timestamp. + Typical operations might include: + - Adjusting power output of controllable generators + - Charging or discharging storage systems + - Controlling flexible loads """ - raise NotImplementedError( "operate_at_timestamp needs to be implemented by child classes!" ) @@ -182,36 +185,40 @@ def operate_at_timestamp(self, timestamp): # %% assign values of generation/demand over time and run powerflow def run_base_scenario(self, baseload): """ - Info - ---- - - ... - + Run a base scenario simulation with power flow calculations. + + This method assigns the generation and demand values from the virtual power plant + components to the pandapower network model, runs power flow calculations for each + timestamp, and returns the results. It also handles the operation of storage + components based on the residual load at each bus. + Parameters ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... - + baseload : dict + Dictionary containing baseload profiles for each bus in the network. + The keys should be bus IDs as strings, and the values should be + pandas Series with timestamps as index and load values in W. + Returns ------- - - ... - + dict + Dictionary containing the power flow results for each timestamp. + The keys are timestamps, and the values are dictionaries containing + the pandapower result tables (res_bus, res_line, res_trafo, etc.). + + Notes + ----- + This method performs the following steps for each timestamp: + 1. Assign generation and demand values from VPP components to the network + 2. Assign baseload values to the network + 3. Calculate residual load at buses with storage + 4. Operate storage components based on residual load + 5. Run power flow calculation + 6. Store results + + The method handles both generation (negative values) and consumption + (positive values) components, as well as storage components that can + both consume and generate power depending on the residual load. """ net_dict = {} @@ -372,8 +379,34 @@ def run_base_scenario(self, baseload): return net_dict # , res_loads #res_loads can be returned for analyses # %% define a function to apply absolute values from SimBench profiles - def apply_absolute_simbench_values(self, absolute_values_dict, case_or_time_step): + """ + Apply absolute values from SimBench profiles to the network model. + + This method assigns values from SimBench profiles to the corresponding + elements and parameters in the pandapower network model for a specific + timestamp or case. + + Parameters + ---------- + absolute_values_dict : dict + Dictionary containing SimBench profiles. The keys are tuples of + (element_type, parameter_name), and the values are pandas DataFrames + with timestamps as index and parameter values as columns. + case_or_time_step : str or pandas.Timestamp + The timestamp or case for which to apply the values + + Returns + ------- + pandapower.pandapowerNet + The updated pandapower network model + + Notes + ----- + This method is used by the run_simbench_scenario method to apply + SimBench profiles to the network model before running power flow + calculations. + """ for elm_param in absolute_values_dict.keys(): if absolute_values_dict[elm_param].shape[1]: elm = elm_param[0] @@ -385,39 +418,47 @@ def apply_absolute_simbench_values(self, absolute_values_dict, case_or_time_step # %% assign values of generation/demand from SimBench and VPPlib # over time and run powerflow - def run_simbench_scenario(self, profiles): """ - Info - ---- - - ... - + Run a SimBench scenario simulation with power flow calculations. + + This method is similar to run_base_scenario but uses SimBench profiles + for the network components in addition to the virtual power plant components. + It assigns the generation and demand values from both sources to the + pandapower network model, runs power flow calculations for each timestamp, + and returns the results. + Parameters ---------- - - ... - - Attributes - ---------- - - ... - + profiles : dict + Dictionary containing SimBench profiles. The keys are tuples of + (element_type, parameter_name), and the values are pandas DataFrames + with timestamps as index and parameter values as columns. + + Returns + ------- + dict + Dictionary containing the power flow results for each timestamp. + The keys are timestamps, and the values are dictionaries containing + the pandapower result tables (res_bus, res_line, res_trafo, etc.). + Notes ----- - - ... - + This method performs the following steps for each timestamp: + 1. Apply SimBench profiles to the network model + 2. Assign generation and demand values from VPP components to the network + 3. Calculate residual load at buses with storage + 4. Operate storage components based on residual load + 5. Run power flow calculation + 6. Store results + + The method handles both generation (negative values) and consumption + (positive values) components, as well as storage components that can + both consume and generate power depending on the residual load. + References ---------- - - ... - - Returns - ------- - - ... - + SimBench: https://simbench.de/en/ """ net_dict = {} @@ -595,36 +636,37 @@ def run_simbench_scenario(self, profiles): def extract_results(self, net_dict): """ - Info - ---- - - ... - + Extract and organize power flow results from the simulation. + + This method extracts the power flow results from the simulation and + organizes them into a dictionary of pandas DataFrames, with one DataFrame + for each result type (bus, line, load, etc.) and parameter (p_mw, q_mvar, etc.). + Parameters ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... - + net_dict : dict + Dictionary containing the power flow results for each timestamp, + as returned by run_base_scenario or run_simbench_scenario. + Returns ------- - - ... - + dict + Dictionary containing the extracted results. The keys are tuples of + (result_type, parameter_name, element_index), and the values are + pandas Series with timestamps as index and parameter values as values. + + Notes + ----- + This method extracts the following result types: + - res_bus: Bus results (vm_pu, va_degree, p_mw, q_mvar) + - res_line: Line results (p_from_mw, q_from_mvar, p_to_mw, q_to_mvar, pl_mw, ql_mvar, i_ka, loading_percent) + - res_trafo: Transformer results (p_hv_mw, q_hv_mvar, p_lv_mw, q_lv_mvar, pl_mw, ql_mvar, i_hv_ka, i_lv_ka, loading_percent) + - res_load: Load results (p_mw, q_mvar) + - res_sgen: Static generator results (p_mw, q_mvar) + - res_ext_grid: External grid results (p_mw, q_mvar) + - res_storage: Storage results (p_mw, q_mvar) + + The extracted results can be used for analysis and visualization. """ # Create DataFrames for later export @@ -732,36 +774,38 @@ def extract_results(self, net_dict): def extract_single_result(self, net_dict, res="load", value="p_mw"): """ - Info - ---- - - ... - + Extract a specific result type and parameter from the simulation. + + This method extracts a specific result type and parameter from the + power flow results and returns it as a pandas DataFrame with timestamps + as index and element indices as columns. + Parameters ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... - + net_dict : dict + Dictionary containing the power flow results for each timestamp, + as returned by run_base_scenario or run_simbench_scenario. + res : str, optional + Result type to extract (default: "load"). + Options: "bus", "line", "trafo", "load", "sgen", "ext_grid", "storage" + value : str, optional + Parameter name to extract (default: "p_mw"). + Options depend on the result type, e.g., "p_mw", "q_mvar", "vm_pu", etc. + Returns ------- - - ... - + pandas.DataFrame + DataFrame containing the extracted result, with timestamps as index + and element indices as columns. + + Notes + ----- + This method is useful for extracting specific results for analysis and + visualization. For example, to extract the active power of all loads, + use res="load" and value="p_mw". + + The method automatically prepends "res_" to the result type, so "load" + becomes "res_load" when accessing the result in the net_dict. """ single_result = pd.DataFrame() @@ -783,36 +827,35 @@ def extract_single_result(self, net_dict, res="load", value="p_mw"): def plot_results(self, results, legend=True): """ - Info - ---- - - ... - + Plot the power flow results. + + This method creates a plot of the power flow results, with one line + for each element and parameter combination. It is useful for visualizing + the results of the simulation. + Parameters ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... - + results : dict or pandas.DataFrame + Results to plot. If dict, it should be the output of extract_results + or extract_single_result. If DataFrame, it should have timestamps as + index and element indices as columns. + legend : bool, optional + Whether to show the legend (default: True) + Returns ------- - - ... - + matplotlib.figure.Figure + The created figure object + + Notes + ----- + This method creates a figure with a single subplot and plots all results + on the same axes. The x-axis represents time, and the y-axis represents + the parameter values. + + If the results contain many elements, the plot may become cluttered. + In such cases, it may be better to use extract_single_result to extract + specific results and plot them separately. """ results["ext_grid"].plot( @@ -846,7 +889,32 @@ def plot_results(self, results, legend=True): ) def plot_pv(self, results): - + """ + Plot the power output of all PV components from simulation results. + + This method creates a plot of the power output of all PV components + from the simulation results. It is useful for visualizing the generation + profile of PV systems after a power flow simulation. + + Parameters + ---------- + results : dict + Dictionary containing the extracted results, as returned by + extract_results or extract_single_result. Should contain a key + "sgen_p_mw" with a DataFrame of static generator active power. + + Notes + ----- + This method creates a figure with a single subplot and plots the power + output of all PV components on the same axes. The x-axis represents time, + and the y-axis represents power in MW. + + The method identifies PV components by checking if the component name + contains the string "PV". + + If there are no buses with PV components in the virtual power plant, + the method does nothing. + """ if len(self.virtual_power_plant.buses_with_pv) > 0: for gen in results["sgen_p_mw"].columns: if "PV" in gen: @@ -856,7 +924,32 @@ def plot_pv(self, results): plt.show() def plot_wind(self, results): - + """ + Plot the power output of all wind components from simulation results. + + This method creates a plot of the power output of all wind components + from the simulation results. It is useful for visualizing the generation + profile of wind turbines after a power flow simulation. + + Parameters + ---------- + results : dict + Dictionary containing the extracted results, as returned by + extract_results or extract_single_result. Should contain a key + "sgen_p_mw" with a DataFrame of static generator active power. + + Notes + ----- + This method creates a figure with a single subplot and plots the power + output of all wind components on the same axes. The x-axis represents time, + and the y-axis represents power in MW. + + The method identifies wind components by checking if the component name + contains the string "WindPower". + + If there are no buses with wind components in the virtual power plant, + the method does nothing. + """ if len(self.virtual_power_plant.buses_with_wind) > 0: for gen in results["sgen_p_mw"].columns: if "WindPower" in gen: @@ -867,36 +960,29 @@ def plot_wind(self, results): def plot_storages(self): """ - Info - ---- - - ... - - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... - + Plot the timeseries data of all storage components. + + This method creates a plot of the timeseries data of all storage components + in the virtual power plant. It is useful for visualizing the state of charge, + power input/output, and other parameters of storage systems. + Returns ------- - - ... - + None + This method does not return anything, but displays the plots. + + Notes + ----- + This method creates a separate figure for each storage component and plots + all columns of its timeseries data. The x-axis represents time, and the + y-axis represents the parameter values. + + The method identifies storage components by checking if the component name + contains the string "storage". + + The plots include all data in the timeseries DataFrame of each storage + component, which may include state of charge, power input/output, and + other parameters depending on the storage component implementation. """ for comp in self.virtual_power_plant.components.keys(): diff --git a/vpplib/thermal_energy_storage.py b/vpplib/thermal_energy_storage.py index 31eff45..96c49fa 100644 --- a/vpplib/thermal_energy_storage.py +++ b/vpplib/thermal_energy_storage.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- """ -Info ----- -This file contains the basic functionalities of the ThermalEnergyStorage class. - +Thermal Energy Storage Module +---------------------------- +This module contains the ThermalEnergyStorage class which models a thermal energy storage system +in a virtual power plant environment. + +The thermal storage system maintains a target temperature within a specified hysteresis range +and accounts for thermal energy losses over time. It can be used in conjunction with thermal +energy generators like heat pumps, heating rods, or combined heat and power units. """ import pandas as pd @@ -11,6 +15,41 @@ class ThermalEnergyStorage(Component): + """ + A class representing a thermal energy storage component in a virtual power plant. + + The thermal energy storage maintains a temperature within a specified range around a target + temperature. It accounts for thermal energy losses over time and can signal when it needs + to be loaded (heated) based on its current temperature relative to the target temperature. + + Attributes + ---------- + identifier : str, optional + Unique identifier for the thermal energy storage + target_temperature : float + Target temperature in degrees Celsius that the storage aims to maintain + current_temperature : float + Current temperature of the storage in degrees Celsius + min_temperature : float + Minimum allowable temperature in degrees Celsius + hysteresis : float + Temperature range around the target temperature that defines the control band + mass : float + Mass of the storage medium in kg + cp : float + Specific heat capacity of the storage medium in J/(kg·K) + state_of_charge : float + Current thermal energy content of the storage in Joules + thermal_energy_loss_per_day : float + Fraction of thermal energy lost per day (e.g., 0.1 for 10% loss) + efficiency_per_timestep : float + Calculated efficiency factor applied at each timestep to account for thermal losses + needs_loading : bool + Flag indicating whether the storage needs to be loaded (heated) + timeseries : pandas.DataFrame + Time series of storage temperature for the simulation period + """ + def __init__( self, target_temperature, @@ -23,39 +62,35 @@ def __init__( identifier=None, environment=None, ): - """ - Info - ---- - ... + Initialize a ThermalEnergyStorage object. Parameters ---------- - - The parameter timebase determines the resolution of the given data. - Furthermore the parameter environment (Environment) is given to provide weather data and further external influences. - To account for different people using a component, a use case (VPPUseCase) can be passed in to improve the simulation. - - Attributes - ---------- - - ... - + target_temperature : float + Target temperature in degrees Celsius that the storage aims to maintain + min_temperature : float + Minimum allowable temperature in degrees Celsius + hysteresis : float + Temperature range around the target temperature that defines the control band + mass : float + Mass of the storage medium in kg + cp : float + Specific heat capacity of the storage medium in J/(kg·K) + thermal_energy_loss_per_day : float + Fraction of thermal energy lost per day (e.g., 0.1 for 10% loss) + unit : str + Unit of energy measurement (e.g., "kWh") + identifier : str, optional + Unique identifier for the thermal energy storage + environment : Environment, optional + Environment object containing simulation parameters and weather data + Notes ----- - - ... - - References - ---------- - - ... - - Returns - ------- - - ... - + The storage is initialized at (target_temperature - hysteresis), which is the + lower threshold that triggers loading. The state of charge is calculated based + on the initial temperature, mass, and specific heat capacity. """ # Call to super class @@ -91,7 +126,37 @@ def __init__( self.needs_loading = None def operate_storage(self, timestamp, thermal_energy_generator): - + """ + Operate the thermal energy storage for a specific timestamp. + + This method controls the thermal energy generator based on the storage's needs, + updates the state of charge and temperature of the storage, and logs the results. + + Parameters + ---------- + timestamp : str or pandas.Timestamp + The timestamp for which to operate the storage + thermal_energy_generator : Component + A thermal energy generator component (e.g., heat pump, heating rod, CHP) + that can be controlled to heat the storage + + Returns + ------- + tuple + A tuple containing: + - current_temperature (float): The updated temperature of the storage in °C + - el_load (float): The electrical load of the thermal energy generator + + Notes + ----- + The method performs the following steps: + 1. Control the thermal energy generator based on storage needs + 2. Calculate the energy balance (demand vs. production) + 3. Update the state of charge accounting for energy balance and losses + 4. Calculate the new temperature + 5. Log the temperature in the timeseries + 6. Log the generator's operation + """ if self.get_needs_loading(): thermal_energy_generator.ramp_up(timestamp) else: @@ -132,7 +197,31 @@ def operate_storage(self, timestamp, thermal_energy_generator): return self.current_temperature, el_load def get_needs_loading(self): - + """ + Determine if the thermal energy storage needs to be loaded (heated). + + This method checks the current temperature against the target temperature + and hysteresis band to determine if the storage needs heating. It also + verifies that the temperature hasn't fallen below the minimum allowable value. + + Returns + ------- + bool + True if the storage needs to be loaded (heated), False otherwise + + Raises + ------ + ValueError + If the current temperature falls below the minimum allowable temperature, + indicating insufficient thermal energy production + + Notes + ----- + The method implements hysteresis control: + - If temperature <= (target - hysteresis), set needs_loading to True + - If temperature >= (target + hysteresis), set needs_loading to False + - Otherwise, maintain the previous state + """ if self.current_temperature <= ( self.target_temperature - self.hysteresis ): @@ -152,127 +241,81 @@ def get_needs_loading(self): return self.needs_loading def value_for_timestamp(self, timestamp): - """ - Info - ---- - This function takes a timestamp as the parameter and returns the - corresponding value for that timestamp. - A positiv result represents a load. - A negative result represents a generation. + Get the energy value for a specific timestamp. - This abstract function needs to be implemented by child classes. - Raises an error since this function needs to be implemented by child classes. + This method is required by the Component interface but is not implemented + for the ThermalEnergyStorage class as it doesn't directly contribute to + the electrical balance of the virtual power plant. Parameters ---------- - - ... - - Attributes - ---------- - - ... - + timestamp : str, int, or pandas.Timestamp + The timestamp for which to retrieve the value + + Raises + ------ + NotImplementedError + This method must be implemented by child classes that need to + contribute to the electrical balance + Notes ----- - - ... - - References - ---------- - - ... - - Returns - ------- - - ... - + In the context of a virtual power plant, this method typically returns: + - Positive values for loads (consumption) + - Negative values for generation """ - raise NotImplementedError( "value_for_timestamp needs to be implemented by child classes!" ) def observations_for_timestamp(self, timestamp): - """ - Info - ---- - This function takes a timestamp as the parameter and returns a - dictionary with key (String) value (Any) pairs. - Depending on the type of component, different status parameters of the - respective component can be queried. + Get detailed observations for a specific timestamp. - For example, a power store can report its "State of Charge". - Returns an empty dictionary since this function needs to be - implemented by child classes. + This method returns a dictionary of key-value pairs representing + the state of the thermal energy storage at the specified timestamp. Parameters ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... - + timestamp : str, int, or pandas.Timestamp + The timestamp for which to retrieve observations + Returns ------- + dict + An empty dictionary in the base implementation. Child classes + should override this to provide relevant observations. + + Notes + ----- + For thermal energy storage, relevant observations might include: + - current_temperature + - state_of_charge + - needs_loading - ... - + This base implementation returns an empty dictionary as the + ThermalEnergyStorage class doesn't implement specific observations. """ - return {} def prepare_time_series(self): - """ - Info - ---- - This function is called to prepare the time series. - Currently equals reset_time_series. Adjust if needed in later versions. + Prepare the time series data for the simulation period. - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... + This method initializes the timeseries DataFrame with a temperature column + and appropriate time index based on the environment settings. Returns ------- - - ... - + pandas.DataFrame + The initialized timeseries DataFrame with a temperature column + + Notes + ----- + This method currently has the same implementation as reset_time_series. + It may be extended in future versions to include additional preparation steps. """ - self.timeseries = pd.DataFrame( columns=["temperature"], index=pd.date_range( @@ -285,39 +328,17 @@ def prepare_time_series(self): return self.timeseries def reset_time_series(self): - """ - Info - ---- - This function is called to reset the time series + Reset the time series data to its initial state. - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... + This method clears all recorded temperature data and reinitializes + the timeseries DataFrame with the appropriate time index. Returns ------- - - ... - + pandas.DataFrame + The reset timeseries DataFrame with a temperature column """ - self.timeseries = pd.DataFrame( columns=["temperature"], index=pd.date_range( @@ -332,35 +353,24 @@ def reset_time_series(self): def get_energy_loss(self): """ - Info - ---- Calculate the energy loss of the thermal energy storage. - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... + This method calculates the thermal energy loss based on the difference + between the target temperature and current temperature, the thermal + properties of the storage medium, and the daily loss rate. Returns ------- + float + The calculated energy loss in the same units as used for state_of_charge + + Notes + ----- + The energy loss is calculated as: + energy_loss = mass * specific_heat_capacity * temperature_difference * daily_loss_rate - ... - + This represents the amount of energy needed to compensate for thermal losses + and maintain the target temperature. """ energy_loss = self.mass * self.cp * (self.target_temperature - self.current_temperature) * self.thermal_energy_loss_per_day return energy_loss diff --git a/vpplib/user_profile.py b/vpplib/user_profile.py index ee1dda4..9efdb1c 100644 --- a/vpplib/user_profile.py +++ b/vpplib/user_profile.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- """ -Info ----- -The class "UserProfile" reflects different patterns of use and behaviour. -This makes it possible, for example, to simulate different usage profiles of -electric vehicles. - +User Profile Module +------------------ +This module contains the UserProfile class which models different patterns of use and behavior +in a virtual power plant environment. + +The UserProfile class provides functionality to generate thermal energy demand profiles +based on building characteristics, weather data, and user preferences. It can be used to +simulate different usage patterns for various components in the virtual power plant. """ import traceback @@ -13,6 +15,55 @@ import os class UserProfile(object): + """ + A class representing a user profile with specific usage patterns and behaviors. + + The UserProfile class models the thermal energy demand of a building based on its + characteristics, location, and user preferences. It implements the SigLinDe method + (Sigmoid function for Linearized Demand) to calculate thermal energy demand profiles + at different temporal resolutions (daily, hourly, quarter-hourly). + + Attributes + ---------- + identifier : str, optional + Unique identifier for the user profile + latitude : float, optional + Latitude of the building location + longitude : float, optional + Longitude of the building location + comfort_factor : float, optional + Factor representing user preference for thermal comfort (higher values indicate + preference for warmer indoor temperatures) + max_connection_power : float, optional + Maximum electrical connection power available to the user in kW + mean_temp_days : pandas.DataFrame + Daily mean temperature data with datetime index + mean_temp_hours : pandas.DataFrame + Hourly mean temperature data with datetime index + mean_temp_quarter_hours : pandas.DataFrame + Quarter-hourly mean temperature data with datetime index + year : str + Year of the simulation data + thermal_energy_demand : pandas.DataFrame + Quarter-hourly thermal energy demand profile + building_type : str, optional + Type of building according to the SigLinDe classification (e.g., 'DE_HEF33') + t_0 : float + Reference temperature for SigLinDe calculations in °C (default: 40) + building_parameters : tuple + Parameters A, B, C, D, m_H, b_H, m_W, b_W from the SigLinDe model + h_del : pandas.DataFrame + Daily heat demand calculated using the SigLinDe method + thermal_energy_demand_yearly : float, optional + Annual thermal energy demand in kWh + thermal_energy_demand_daily : pandas.DataFrame + Daily thermal energy demand profile + thermal_energy_demand_hourly : pandas.DataFrame + Hourly thermal energy demand profile + consumerfactor : float + Scaling factor to adjust the calculated demand to match the annual demand + """ + def __init__( self, identifier=None, @@ -28,37 +79,42 @@ def __init__( t_0=40, ): """ - Info - ---- - This attributes can be used to derive profiles for different - components. - + Initialize a UserProfile object. Parameters ---------- - - ... - - Attributes - ---------- - - ... - + identifier : str, optional + Unique identifier for the user profile + latitude : float, optional + Latitude of the building location + longitude : float, optional + Longitude of the building location + thermal_energy_demand_yearly : float, optional + Annual thermal energy demand in kWh + mean_temp_days : pandas.DataFrame, optional + Daily mean temperature data with datetime index. + If None, default data from input/thermal/dwd_temp_days_2015.csv is used. + mean_temp_hours : pandas.DataFrame, optional + Hourly mean temperature data with datetime index. + If None, default data from input/thermal/dwd_temp_hours_2015.csv is used. + mean_temp_quarter_hours : pandas.DataFrame, optional + Quarter-hourly mean temperature data with datetime index. + If None, default data from input/thermal/dwd_temp_15min_2015.csv is used. + building_type : str, optional + Type of building according to the SigLinDe classification + (e.g., 'DE_HEF33', 'DE_HEF34', 'DE_HMF33', 'DE_HMF34', 'DE_GKO34') + max_connection_power : float, optional + Maximum electrical connection power available to the user in kW + comfort_factor : float, optional + Factor representing user preference for thermal comfort + t_0 : float, optional + Reference temperature for SigLinDe calculations in °C (default: 40) + Notes ----- - - ... - - References - ---------- - - ... - - Returns - ------- - - ... - + If temperature data is not provided, the class will load default data from + the input/thermal directory. The SigLinDe parameters are loaded from + input/thermal/SigLinDe.csv. """ self.identifier = identifier @@ -123,51 +179,37 @@ def __init__( def get_thermal_energy_demand(self): - """ - Info - ---- - ... - - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- + Calculate the thermal energy demand profile at quarter-hourly resolution. - ... + This method orchestrates the entire thermal energy demand calculation process + by calling the necessary sub-methods in sequence: + 1. Get building parameters from the SigLinDe model + 2. Calculate daily heat demand (h_del) + 3. Distribute daily demand to hourly values based on temperature + 4. Calculate the consumer factor to scale the demand + 5. Apply the consumer factor to get hourly thermal energy demand + 6. Interpolate hourly values to quarter-hourly resolution Returns ------- - - ... - + pandas.DataFrame + Quarter-hourly thermal energy demand profile with datetime index + and 'thermal_energy_demand' column in kWh + + Notes + ----- + This is the main method to call when generating a thermal energy demand profile. + The resulting profile can be used by thermal energy generators like heat pumps, + heating rods, or combined heat and power units. """ - self.get_building_parameters() - self.get_h_del() - self.get_thermal_energy_demand_daily() - self.get_consumerfactor() - self.get_thermal_energy_demand_hourly() - self.thermal_energy_demand = self.hour_to_quarter() - + return self.thermal_energy_demand # %%: @@ -176,37 +218,26 @@ def get_thermal_energy_demand(self): # ========================================================================= def get_building_parameters(self): - """ - Info - ---- - ... - - Parameters - ---------- - - ... - - Attributes - ---------- + Retrieve building parameters from the SigLinDe model based on building type. - ... - - Notes - ----- - - ... - - References - ---------- - - ... + This method looks up the building parameters in the SigLinDe table based on + the specified building_type. The parameters are used in the SigLinDe method + to calculate the thermal energy demand. Returns ------- - - ... - + tuple + A tuple containing the SigLinDe parameters: + - A, B, C, D: Parameters of the sigmoid function + - m_H, b_H: Parameters for linearization below 8°C (heating) + - m_W, b_W: Parameters for linearization below 8°C (hot water) + + Notes + ----- + The SigLinDe model uses a sigmoid function with linearization for low + temperatures to model the relationship between outdoor temperature and + thermal energy demand. """ for i, Sig in self.SigLinDe.iterrows(): @@ -228,39 +259,35 @@ def get_building_parameters(self): # %%: def get_h_del(self): - """ - Info - ---- - Calculate the daily heat demand - - Parameters - ---------- + Calculate the daily heat demand using the SigLinDe method. - ... - - Attributes - ---------- - - ... + This method applies the SigLinDe (Sigmoid function for Linearized Demand) method + to calculate the daily heat demand based on daily mean temperatures. The SigLinDe + method combines a sigmoid function with linearization for low temperatures. + Returns + ------- + pandas.DataFrame + Daily heat demand with datetime index and 'h_del' column + Notes ----- + The SigLinDe formula is: + h_del = (A / (1 + ((B / (T - t_0)) ** C))) + D + max(H, W) - ... + where: + - A, B, C, D are parameters of the sigmoid function + - T is the daily mean temperature + - t_0 is the reference temperature (default: 40°C) + - H = m_H * T + b_H (linearization for heating at low temperatures) + - W = m_W * T + b_W (linearization for hot water at low temperatures) References ---------- - - ... - - Returns - ------- - - ... - + Hellwig, M. (2003). Entwicklung und Anwendung parametrisierter Standard-Lastprofile. + Dissertation, TU München. """ - A, B, C, D, m_H, b_H, m_W, b_W = self.building_parameters # Calculating the daily heat demand h_del for each day of the year @@ -292,38 +319,26 @@ def get_h_del(self): # %%: def get_thermal_energy_demand_daily(self): - """ - Info - ---- - distribute daily demand load over 24 hours according to the outside - temperature - - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- + Distribute daily heat demand to hourly values based on temperature ranges. - ... - - References - ---------- - - ... + This method distributes the daily heat demand (h_del) to hourly values + according to typical daily load profiles for different temperature ranges. + The distribution patterns are loaded from the demand_daily.csv file, which + contains hourly distribution factors for different temperature ranges. Returns ------- - - ... - + pandas.DataFrame + Hourly thermal energy demand with datetime index + + Notes + ----- + The method uses different hourly distribution patterns depending on the + daily mean temperature, with 10 different temperature ranges from below -15°C + to above 25°C. For each temperature range, a specific hourly distribution + pattern is applied to distribute the daily heat demand across the 24 hours + of the day. """ demand_daily_lst = [] @@ -395,39 +410,25 @@ def get_thermal_energy_demand_daily(self): # %%: def get_consumerfactor(self): - """ - Info - ---- - ... + Calculate the consumer factor to scale the thermal energy demand. - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... + The consumer factor (Kundenwert) is a scaling factor that adjusts the + calculated thermal energy demand to match the specified annual demand. + It is calculated as the ratio of the specified annual thermal energy + demand to the sum of the calculated daily heat demands. Returns ------- - - ... - + float + The consumer factor (scaling factor) + + Notes + ----- + The consumer factor accounts for building-specific characteristics that + are not captured by the standard SigLinDe model, such as building size, + insulation quality, and user behavior. """ - # consumerfactor (Kundenwert) K_w self.consumerfactor = self.thermal_energy_demand_yearly / ( sum(self.h_del["h_del"]) @@ -437,39 +438,23 @@ def get_consumerfactor(self): # %%: def get_thermal_energy_demand_hourly(self): - """ - Info - ---- - ... + Apply the consumer factor to get the hourly thermal energy demand. - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... + This method scales the hourly thermal energy demand values by multiplying + them with the consumer factor to match the specified annual demand. Returns ------- - - ... - + pandas.DataFrame + Hourly thermal energy demand with datetime index, scaled by the consumer factor + + Notes + ----- + This step ensures that the total annual thermal energy demand matches the + specified value while preserving the temporal patterns determined by the + SigLinDe method and the hourly distribution factors. """ - self.thermal_energy_demand_hourly = ( self.thermal_energy_demand_daily * self.consumerfactor ) @@ -479,39 +464,25 @@ def get_thermal_energy_demand_hourly(self): # %%: def hour_to_quarter(self): - """ - Info - ---- - ... + Convert hourly thermal energy demand to quarter-hourly resolution. - Parameters - ---------- - - ... - - Attributes - ---------- - - ... - - Notes - ----- - - ... - - References - ---------- - - ... + This method creates a quarter-hourly thermal energy demand profile by + assigning the hourly values to the corresponding quarter-hourly timestamps + and then interpolating to fill in the missing values. Returns ------- - - ... - + pandas.DataFrame + Quarter-hourly thermal energy demand with datetime index and + 'thermal_energy_demand' column + + Notes + ----- + The interpolation is performed using pandas' interpolate method, which + by default uses linear interpolation. This creates a smoother profile + that better represents the continuous nature of thermal energy demand. """ - self.thermal_energy_demand = pd.DataFrame( index=self.mean_temp_quarter_hours.index ) diff --git a/vpplib/wind_power.py b/vpplib/wind_power.py index 715e4c9..ac6f87d 100644 --- a/vpplib/wind_power.py +++ b/vpplib/wind_power.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- """ -Info ----- -This file contains the basic functionalities of the WindPower class. - +Wind Power Module +--------------- +This module contains the WindPower class which models a wind turbine component +in a virtual power plant environment. + +The WindPower class uses the windpowerlib package to calculate power output +based on wind speed data and turbine specifications. It supports various models +for wind speed, air density, temperature, and power output calculations. """ from .component import Component @@ -15,6 +19,52 @@ class WindPower(Component): + """ + A class representing a wind power component in a virtual power plant. + + The WindPower class models a wind turbine and calculates its power output + based on wind speed data and turbine specifications. It uses the windpowerlib + package to perform the calculations and supports various models for wind speed, + air density, temperature, and power output calculations. + + Attributes + ---------- + identifier : str, optional + Unique identifier for the wind power component + limit : float + Power limit factor between 0 and 1 (default: 1.0) + turbine_type : str + Type of wind turbine as specified in the windpowerlib turbine database + hub_height : float + Height of the turbine hub in meters + rotor_diameter : float + Diameter of the rotor in meters + fetch_curve : str + Type of curve to fetch ('power_curve' or 'power_coefficient_curve') + data_source : str + Data source for turbine data ('oedb' or name of CSV file) + wind_speed_model : str + Model for wind speed calculation ('logarithmic', 'hellman', or 'interpolation_extrapolation') + density_model : str + Model for air density calculation ('barometric', 'ideal_gas', or 'interpolation_extrapolation') + temperature_model : str + Model for temperature calculation ('linear_gradient' or 'interpolation_extrapolation') + power_output_model : str + Model for power output calculation ('power_curve' or 'power_coefficient_curve') + density_correction : bool + Whether to apply density correction + obstacle_height : float + Height of obstacles affecting wind flow in meters + hellman_exp : float or None + Hellman exponent for wind speed calculation + wind_turbine : WindTurbine + WindTurbine object from windpowerlib + ModelChain : ModelChain + ModelChain object from windpowerlib + timeseries : pandas.Series + Time series of power output in kW + """ + def __init__( self, turbine_type, @@ -34,35 +84,45 @@ def __init__( environment=None, ): """ - Info - ---- - ... + Initialize a WindPower object. Parameters ---------- - - ... - - Attributes - ---------- - - ... - + turbine_type : str + Type of wind turbine as specified in the windpowerlib turbine database + hub_height : float + Height of the turbine hub in meters + rotor_diameter : float + Diameter of the rotor in meters + fetch_curve : str + Type of curve to fetch ('power_curve' or 'power_coefficient_curve') + data_source : str + Data source for turbine data ('oedb' or name of CSV file) + wind_speed_model : str + Model for wind speed calculation ('logarithmic', 'hellman', or 'interpolation_extrapolation') + density_model : str + Model for air density calculation ('barometric', 'ideal_gas', or 'interpolation_extrapolation') + temperature_model : str + Model for temperature calculation ('linear_gradient' or 'interpolation_extrapolation') + power_output_model : str + Model for power output calculation ('power_curve' or 'power_coefficient_curve') + density_correction : bool + Whether to apply density correction + obstacle_height : float + Height of obstacles affecting wind flow in meters + hellman_exp : float or None + Hellman exponent for wind speed calculation + unit : str + Unit of power measurement (e.g., 'kW') + identifier : str, optional + Unique identifier for the wind power component + environment : Environment, optional + Environment object containing weather data and simulation parameters + Notes ----- - - ... - - References - ---------- - - ... - - Returns - ------- - - ... - + The wind power calculation requires wind speed data in the environment object. + The data should be provided in the environment.wind_data attribute. """ # Call to super class @@ -104,17 +164,29 @@ def __init__( self.timeseries = None def get_wind_turbine(self): - r""" - fetch power and/or power coefficient curve data from the OpenEnergy - Database (oedb) - Execute ``windpowerlib.wind_turbine.get_turbine_types()`` to get a table - including all wind turbines for which power and/or power coefficient curves - are provided. - + """ + Create a WindTurbine object with the specified parameters. + + This method initializes a WindTurbine object from the windpowerlib package + using the turbine specifications provided during initialization. The turbine + data (power curve or power coefficient curve) is fetched from the specified + data source, which can be the OpenEnergy Database (oedb) or a CSV file. + Returns ------- WindTurbine - + A WindTurbine object from the windpowerlib package + + Notes + ----- + To see available turbine types, execute: + ``windpowerlib.wind_turbine.get_turbine_types()`` + + This will return a table including all wind turbines for which power and/or + power coefficient curves are provided in the OpenEnergy Database. + + The 'fetch_curve' parameter determines whether to fetch the power curve + ('power_curve') or the power coefficient curve ('power_coefficient_curve'). """ # specification of wind turbine where power curve is provided in the oedb @@ -133,20 +205,32 @@ def get_wind_turbine(self): return self.wind_turbine def calculate_power_output(self): - r""" - Calculates power output of wind turbines using the - :class:`~.modelchain.ModelChain`. - - The :class:`~.modelchain.ModelChain` is a class that provides all necessary - steps to calculate the power output of a wind turbine. You can either use - the default methods for the calculation steps, or choose different methods, - as done for the 'e126'. Of course, you can also use the default methods - while only changing one or two of them. - - Parameters - ---------- - - + """ + Calculate the power output of the wind turbine using ModelChain. + + This method creates a ModelChain object from the windpowerlib package + and uses it to calculate the power output of the wind turbine based on + the wind data provided in the environment. The calculation uses the + models specified during initialization for wind speed, air density, + temperature, and power output. + + The resulting power output time series is stored in the timeseries attribute + after conversion from W to kW. + + Returns + ------- + None + The method updates the timeseries attribute but does not return a value + + Notes + ----- + The ModelChain class from windpowerlib provides all necessary steps to + calculate the power output of a wind turbine. You can use the default methods + for the calculation steps or choose different methods as specified in the + initialization parameters. + + The wind data is filtered to the specified time period if start and end + timestamps are provided in the environment. """ # power output calculation for e126 # own specifications for ModelChain setup @@ -187,7 +271,23 @@ def calculate_power_output(self): return def prepare_time_series(self): - + """ + Prepare the time series data for the wind power component. + + This method initializes the wind turbine and calculates its power output + for the simulation period. It checks if wind data is available in the + environment and raises an error if it's not. + + Returns + ------- + pandas.Series + Time series of power output in kW + + Raises + ------ + ValueError + If the environment.wind_data is empty + """ if len(self.environment.wind_data) == 0: raise ValueError("self.environment.wind_data is empty.") @@ -197,7 +297,17 @@ def prepare_time_series(self): return self.timeseries def reset_time_series(self): - + """ + Reset the time series data to its initial state. + + This method clears the power output time series by setting it to None. + It can be used to reset the component before recalculating the power output. + + Returns + ------- + None + The method returns None to indicate that the timeseries has been reset + """ self.timeseries = None return self.timeseries @@ -206,78 +316,108 @@ def reset_time_series(self): # Controlling functions # =================================================================================== - # This function limits the power to the given percentage. - # It cuts the current power production down to the peak power multiplied by - # the limit (Float [0;1]). def limit_power_to(self, limit): - + """ + Limit the power output of the wind turbine. + + This method sets a limit on the power output of the wind turbine as a + fraction of its calculated output. The limit is a float between 0 and 1, + where 0 means no power output and 1 means full power output. + + Parameters + ---------- + limit : float + Power limit factor between 0 and 1 + + Raises + ------ + ValueError + If the limit is not between 0 and 1 + + Notes + ----- + This method can be used to simulate curtailment of wind power output, + for example, due to grid constraints or market conditions. + """ # Validate input parameter if limit >= 0 and limit <= 1: - # Parameter is valid self.limit = limit - else: - # Parameter is invalid - raise ValueError("Limit-parameter is not valid") # =================================================================================== # Balancing Functions # =================================================================================== - # Override balancing function from super class. def value_for_timestamp(self, timestamp): - + """ + Get the power output value for a specific timestamp. + + This method returns the power output of the wind turbine at the specified + timestamp, adjusted by the power limit factor. It overrides the method + from the Component superclass. + + Parameters + ---------- + timestamp : int or str + If int: index position in the timeseries + If str: timestamp in format 'YYYY-MM-DD hh:mm:ss' + + Returns + ------- + float + Power output in kW at the specified timestamp, adjusted by the limit factor + + Raises + ------ + ValueError + If the timestamp is not of type int or str + + Notes + ----- + In the context of a virtual power plant, this method returns a negative value + as wind power is considered generation (not consumption). + """ if type(timestamp) == int: - return self.timeseries.iloc[timestamp].item() * self.limit - elif type(timestamp) == str: - return self.timeseries.loc[timestamp].item() * self.limit - else: raise ValueError( "timestamp needs to be of type int or string. Stringformat: YYYY-MM-DD hh:mm:ss" ) def observations_for_timestamp(self, timestamp): - """ - Info - ---- - This function takes a timestamp as the parameter and returns a - dictionary with key (String) value (Any) pairs. - Depending on the type of component, different status parameters of the - respective component can be queried. - - Parameters - ---------- + Get detailed observations for a specific timestamp. - ... - - Attributes - ---------- + This method returns a dictionary containing the wind power generation + at the specified timestamp. It can be used to query the status of the + wind power component at a particular point in time. - ... - - Notes - ----- - - ... - - References + Parameters ---------- - - ... - + timestamp : int or str + If int: index position in the timeseries + If str: timestamp in format 'YYYY-MM-DD hh:mm:ss' + Returns ------- - - ... - + dict + Dictionary with key 'wind_generation' and the corresponding power output value + + Raises + ------ + ValueError + If the timestamp is not of type int or str + + Notes + ----- + This method can be extended to include additional observations such as + wind speed, air density, or other relevant parameters if they are available + in the ModelChain results. """ if type(timestamp) == int: