From 7e7e6b343a468fefebba16a4798bbad804e554b3 Mon Sep 17 00:00:00 2001 From: "Felix C. A. Auer" <10127354+FelixCAAuer@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:12:51 +0200 Subject: [PATCH 1/4] Add new format for Power_ImportExport --- CaseStudy.py | 106 ++++----------------------- ExcelReader.py | 79 ++++++++++++++++++++ ExcelWriter.py | 56 +++++++++++--- TableDefinition.py | 9 ++- TableDefinitions.xml | 44 +++++++++++ data/example/Power_ImportExport.xlsx | Bin 0 -> 14607 bytes tests/test_ExcelReaderWriter.py | 1 + 7 files changed, 191 insertions(+), 104 deletions(-) create mode 100644 data/example/Power_ImportExport.xlsx diff --git a/CaseStudy.py b/CaseStudy.py index fca9eb9..ef7b0a5 100644 --- a/CaseStudy.py +++ b/CaseStudy.py @@ -17,13 +17,12 @@ class CaseStudy: # Lists of dataframes based on their dependencies - every table should only be present in one of these lists rpk_dependent_dataframes: list[str] = ["dPower_Demand", "dPower_Hindex", - "dPower_ImpExpProfiles", + "dPower_ImportExport", "dPower_Inflows", "dPower_VRESProfiles"] rp_only_dependent_dataframes: list[str] = ["dPower_WeightsRP"] k_only_dependent_dataframes: list[str] = ["dPower_WeightsK"] non_time_dependent_dataframes: list[str] = ["dPower_BusInfo", - "dPower_ImpExpHubs", "dPower_Network", "dPower_Storage", "dPower_ThermalGen", @@ -55,8 +54,7 @@ def __init__(self, power_weightsrp_file: str = "Power_WeightsRP.xlsx", dPower_WeightsRP: pd.DataFrame = None, power_weightsk_file: str = "Power_WeightsK.xlsx", dPower_WeightsK: pd.DataFrame = None, power_hindex_file: str = "Power_Hindex.xlsx", dPower_Hindex: pd.DataFrame = None, - power_impexphubs_file: str = "Power_ImpExpHubs.xlsx", dPower_ImpExpHubs: pd.DataFrame = None, - power_impexpprofiles_file: str = "Power_ImpExpProfiles.xlsx", dPower_ImpExpProfiles: pd.DataFrame = None): + power_importexport_file: str = "Power_ImportExport.xlsx", dPower_ImportExport: pd.DataFrame = None): self.data_folder = str(data_folder) if str(data_folder).endswith("/") else str(data_folder) + "/" self.do_not_scale_units = do_not_scale_units self.do_not_merge_single_node_buses = do_not_merge_single_node_buses @@ -161,20 +159,13 @@ def __init__(self, self.dPower_Inflows = ExcelReader.get_Power_Inflows(self.data_folder + self.power_inflows_file) if self.dPower_Parameters["pEnablePowerImportExport"]: - if dPower_ImpExpHubs is not None: - self.dPower_ImpExpHubs = dPower_ImpExpHubs + if dPower_ImportExport is not None: + self.dPower_ImportExport = dPower_ImportExport else: - self.power_impexphubs_file = power_impexphubs_file - self.dPower_ImpExpHubs = self.get_dPower_ImpExpHubs() - - if dPower_ImpExpProfiles is not None: - self.dPower_ImpExpProfiles = dPower_ImpExpProfiles - else: - self.power_impexpprofiles_file = power_impexpprofiles_file - self.dPower_ImpExpProfiles = self.get_dPower_ImpExpProfiles() + self.power_importexport_file = power_importexport_file + self.dPower_ImportExport = self.get_dPower_ImportExport() else: - self.dPower_ImpExpHubs = None - self.dPower_ImpExpProfiles = None + self.dPower_ImportExport = None if not do_not_merge_single_node_buses: self.merge_single_node_buses() @@ -298,13 +289,10 @@ def scale_dPower_Storage(self): if self.dPower_Storage['DisEffic'].isna().any() or self.dPower_Storage['ChEffic'].isna().any(): raise ValueError("DisEffic and ChEffic in 'Power_Storage.xlsx' must not contain NaN values. Please check the data.") - def scale_dPower_ImpExpHubs(self): - self.dPower_ImpExpHubs["Pmax Import"] *= self.power_scaling_factor - self.dPower_ImpExpHubs["Pmax Export"] *= self.power_scaling_factor - - def scale_dPower_ImpExpProfiles(self): - self.dPower_ImpExpProfiles["ImpExp"] *= self.power_scaling_factor - self.dPower_ImpExpProfiles["Price"] *= self.cost_scaling_factor / self.power_scaling_factor + def scale_dPower_ImportExport(self): + self.dPower_ImportExport["ImpExpMin"] *= self.power_scaling_factor + self.dPower_ImportExport["ImpExpMax"] *= self.power_scaling_factor + self.dPower_ImportExport["ImpExpPrice"] *= self.cost_scaling_factor / self.power_scaling_factor def get_dGlobal_Parameters(self): ExcelReader.check_LEGOExcel_version(self.data_folder + self.global_parameters_file, "v0.1.0", False) @@ -347,76 +335,6 @@ def yesNo_to_bool(df: pd.DataFrame, columns_to_be_changed: list[str]): raise ValueError(f"Value for {column} must be either 'Yes' or 'No'.") return df - def get_dPower_ImpExpHubs(self): - dPower_ImpExpHubs = pd.read_excel(self.data_folder + self.power_impexphubs_file, skiprows=[0, 1, 3, 4, 5]) - dPower_ImpExpHubs = dPower_ImpExpHubs.drop(dPower_ImpExpHubs.columns[0], axis=1) - dPower_ImpExpHubs = dPower_ImpExpHubs.set_index(['hub', 'i']) - - # Validate that all values for "Import Type" and "Export Type" == [Imp/ExpFix or Imp/ExpMax] - errors = dPower_ImpExpHubs[~dPower_ImpExpHubs['Import Type'].isin(['ImpFix', 'ImpMax'])] - if len(errors) > 0: - raise ValueError(f"'Import Type' must be 'ImpFix' or 'ImpMax'. Please check: \n{errors}\n") - errors = dPower_ImpExpHubs[~dPower_ImpExpHubs['Export Type'].isin(['ExpFix', 'ExpMax'])] - if len(errors) > 0: - raise ValueError(f"'Export Type' must be 'ExpFix' or 'ExpMax'. Please check: \n{errors}\n") - - # Validate that for each hub, all connections have the same Import Type and Export Type - errors = dPower_ImpExpHubs.groupby('hub').agg({'Import Type': 'nunique', 'Export Type': 'nunique'}) - errors = errors[(errors['Import Type'] > 1) | (errors['Export Type'] > 1)] - if len(errors) > 0: - raise ValueError(f"Each hub must have the same Import Type (Fix or Max) and the same Export Type (Fix or Max) for each connection. Please check: \n{errors.index}\n") - - # If column 'scenario' is not present, add it - if 'scenario' not in dPower_ImpExpHubs.columns: - dPower_ImpExpHubs['scenario'] = 'ScenarioA' # TODO: Fill this dynamically, once the Excel file is updated - return dPower_ImpExpHubs - - def get_dPower_ImpExpProfiles(self): - with warnings.catch_warnings(action="ignore", category=UserWarning): # Otherwise there is a warning regarding data validation in the Excel-File (see https://stackoverflow.com/questions/53965596/python-3-openpyxl-userwarning-data-validation-extension-not-supported) - dPower_ImpExpProfiles = pd.read_excel(self.data_folder + self.power_impexpprofiles_file, skiprows=[0, 1, 3, 4, 5], sheet_name='Power ImpExpProfiles') - dPower_ImpExpProfiles = dPower_ImpExpProfiles.drop(dPower_ImpExpProfiles.columns[0], axis=1) - dPower_ImpExpProfiles = dPower_ImpExpProfiles.melt(id_vars=['hub', 'rp', 'Type'], var_name='k', value_name='Value') - - # Validate that each multiindex is only present once - dPower_ImpExpProfiles = dPower_ImpExpProfiles.set_index(['hub', 'rp', 'k', 'Type']) - if not dPower_ImpExpProfiles.index.is_unique: - raise ValueError(f"Indices for Imp-/Export values must be unique (i.e., no two entries for the same hub, rp, Type and k). Please check these indices: {dPower_ImpExpProfiles.index[dPower_ImpExpProfiles.index.duplicated(keep=False)]}") - - # Validate that all values for "Type" == [ImpExp, Price] - dPower_ImpExpProfiles = dPower_ImpExpProfiles.reset_index().set_index(['hub', 'rp', 'k']) - errors = dPower_ImpExpProfiles[~dPower_ImpExpProfiles['Type'].isin(['ImpExp', 'Price'])] - if len(errors) > 0: - raise ValueError(f"'Type' must be 'ImpExp' or 'Price'. Please check: \n{errors}\n") - - # Create combined table (with one row for each hub, rp and k) - dPower_ImpExpProfiles = dPower_ImpExpProfiles.pivot(columns="Type", values="Value") - dPower_ImpExpProfiles.columns.name = None # Fix name of columns/indices (which are altered through pivot) - - # Check that Pmax of ImpExpConnections can handle the maximum import and export (for those connections that are ImpFix or ExpFix) - max_import = dPower_ImpExpProfiles[dPower_ImpExpProfiles["ImpExp"] >= 0]["ImpExp"].groupby("hub").max() - max_export = -dPower_ImpExpProfiles[dPower_ImpExpProfiles["ImpExp"] <= 0]["ImpExp"].groupby("hub").min() - - pmax_sum_by_hub = self.dPower_ImpExpHubs.groupby('hub').agg({'Pmax Import': 'sum', 'Pmax Export': 'sum', 'Import Type': 'first', 'Export Type': 'first'}) - import_violations = max_import[(max_import > pmax_sum_by_hub['Pmax Import']) & (pmax_sum_by_hub['Import Type'] == 'ImpFix')] - export_violations = max_export[(max_export > pmax_sum_by_hub['Pmax Export']) & (pmax_sum_by_hub['Export Type'] == 'ExpFix')] - - if not import_violations.empty: - error_information = pd.concat([import_violations, pmax_sum_by_hub['Pmax Import']], axis=1) # Concat Pmax information and maximum import - error_information = error_information[error_information["ImpExp"].notna()] # Only show rows where there is a violation - error_information = error_information.rename(columns={"ImpExp": "Max Import from Profiles", "Pmax Import": "Sum of Pmax Import from Hub Definition"}) # Rename columns for readability - raise ValueError(f"At least one hub has ImpFix imports which exceed the sum of Pmax of all connections. Please check: \n{error_information}\n") - - if not export_violations.empty: - error_information = pd.concat([export_violations, pmax_sum_by_hub['Pmax Export']], axis=1) # Concat Pmax information and maximum export - error_information = error_information[error_information["ImpExp"].notna()] # Only show rows where there is a violation - error_information = error_information.rename(columns={"ImpExp": "Max Export from Profiles", "Pmax Export": "Sum of Pmax Export from Hub Definition"}) # Rename columns for readability - raise ValueError(f"At least one hub has ExpFix exports which exceed the sum of Pmax of all connections. Please check: \n{error_information}\n") - - # If column 'scenario' is not present, add it - if 'scenario' not in dPower_ImpExpProfiles.columns: - dPower_ImpExpProfiles['scenario'] = "ScenarioA" # TODO: Fill this dynamically, once the Excel file is updated - return dPower_ImpExpProfiles - @staticmethod def get_connected_buses(connection_matrix, bus: str): connected_buses = [] @@ -671,6 +589,8 @@ def filter_timesteps(self, start: str, end: str, inplace: bool = False) -> Optio for df_name in CaseStudy.k_dependent_dataframes: if hasattr(case_study, df_name): df = getattr(case_study, df_name) + if df is None: + continue index = df.index.names df_reset = df.reset_index() diff --git a/ExcelReader.py b/ExcelReader.py index c46af00..4deadca 100644 --- a/ExcelReader.py +++ b/ExcelReader.py @@ -3,6 +3,7 @@ import openpyxl import pandas as pd from openpyxl import load_workbook +from openpyxl.utils.cell import get_column_letter from printer import Printer @@ -193,6 +194,84 @@ def get_Power_Hindex(excel_file_path: str, keep_excluded_entries: bool = False, return dPower_Hindex +def get_Power_ImportExport(excel_file_path: str, keep_excluded_entries: bool = False, fail_on_wrong_version: bool = False) -> pd.DataFrame: + """ + Read the dPower_ImportExport data from the Excel file. + :param excel_file_path: Path to the Excel file + :param keep_excluded_entries: Unused but kept for compatibility with other functions + :param fail_on_wrong_version: If True, raise an error if the version of the Excel file does not match the expected version + :return: dPower_ImportExport + """ + if keep_excluded_entries: + printer.warning("'keep_excluded_entries' is set for 'get_Power_ImportExport', although nothing is excluded anyway - please check if this is intended.") + + check_LEGOExcel_version(excel_file_path, "v0.0.1", fail_on_wrong_version) + xls = pd.ExcelFile(excel_file_path) + data = pd.DataFrame() + + for scenario in xls.sheet_names: # Iterate through all sheets, i.e., through all scenarios + # Read row 3 (information about hubs and nodes) + hub_i_df = pd.read_excel(excel_file_path, skiprows=[0, 1, 3], nrows=2, sheet_name=scenario) + hub_i = [] + hubs = [] + i = 6 # Start checking from column 6 (index 5) + while i < hub_i_df.shape[1]: + hubs.append(hub_i_df.columns[i]) + hub_i.append((hub_i_df.columns[i], hub_i_df.columns[i + 1])) + if "Unnamed" not in hub_i_df.columns[i + 2]: + raise ValueError(f"Power_ImportExport: Expected pairs of columns for hub and i, but found an unexpected text '{hub_i_df.columns[i + 2]}' at column index {get_column_letter(i + 3)}. Please check the Excel file format.") + i += 3 # Move to the next pair (skip the "Unnamed" column) + + if len(hubs) != len(set(hubs)): + raise ValueError(f"Power_ImportExport: Found duplicate hub names in the header row. Hubs must be unique. Please check the Excel file.") + + df = pd.read_excel(excel_file_path, skiprows=[0, 1, 2, 4, 5, 6], sheet_name=scenario) + df = df.drop(df.columns[0], axis=1) # Drop the first column (which is empty) + + for i, col in enumerate(df.columns): + if i < 5: + continue # Skip the first five columns + hub = hub_i[(i - 5) // 3][0] + node = hub_i[(i - 5) // 3][1] + + match (i - 5) % 3: + case 0: + if "ImpExpMinimum" not in col: + raise ValueError(f"Power_ImportExport: Expected column 'ImpExpMinimum' at column index {get_column_letter(i + 2)}, but found '{col}'. Please check the Excel file format.") + col_name = "ImpExpMinimum" + case 1: + if "ImpExpMaximum" not in col: + raise ValueError(f"Power_ImportExport: Expected column 'ImpExpMaximum' at column index {get_column_letter(i + 2)}, but found '{col}'. Please check the Excel file format.") + col_name = "ImpExpMaximum" + case 2: + if "ImpExpPrice" not in col: + raise ValueError(f"Power_ImportExport: Expected column 'ImpExpPrice' at column index {get_column_letter(i + 2)}, but found '{col}'. Please check the Excel file format.") + col_name = "ImpExpPrice" + case _: + raise ValueError("This should never happen.") + + if "@" in hub: + raise ValueError(f"Power_ImportExport: Found '@' in hub name {hub}, which is not allowed. Please rename it.") + elif "@" in node: + raise ValueError(f"Power_ImportExport: Found '@' in node name {node}, which is not allowed. Please rename it.") + df = df.rename(columns={col: f"{hub}@{node}@{col_name}"}) + + df = df.melt(id_vars=["id", "rp", "k", "dataPackage", "dataSource"]) + + df[["hub", "i", "valueType"]] = df["variable"].str.split("@", expand=True) # Split the variable column into hub, i and valueType + + df = df.pivot(index=["id", "rp", "k", "dataPackage", "dataSource", "hub", "i"], columns="valueType", values="value") + df.columns.name = None # Fix name of columns/indices (which are altered through pivot) + + df["scenario"] = scenario + + df = df.reset_index().set_index(["hub", "i", "rp", "k"]) # Set multiindex + + data = pd.concat([data, df], ignore_index=False) # Append the DataFrame to the main DataFrame + + return data + + def get_Power_Inflows(excel_file_path: str, keep_excluded_entries: bool = False, fail_on_wrong_version: bool = False) -> pd.DataFrame: """ Read the dPower_Inflows data from the Excel file. diff --git a/ExcelWriter.py b/ExcelWriter.py index cd88198..86ab0da 100644 --- a/ExcelWriter.py +++ b/ExcelWriter.py @@ -44,7 +44,7 @@ def __init__(self, excel_definitions_path: str = None): self.fonts = Font.dict_from_xml(self.xml_root.find("Fonts"), self.colors) self.texts = Text.dict_from_xml(self.xml_root.find("Texts")) self.cell_styles = CellStyle.dict_from_xml(self.xml_root.find("CellStyles"), self.fonts, self.colors, self.number_formats, self.alignments) - self.columns = Column.dict_from_xml(self.xml_root.find("Columns"), self.cell_styles) | Column.dict_from_xml(self.xml_root.find("PivotColumns"), self.cell_styles) + self.columns = Column.dict_from_xml(self.xml_root.find("Columns"), self.cell_styles) | Column.dict_from_xml(self.xml_root.find("GroupedColumns"), self.cell_styles) | Column.dict_from_xml(self.xml_root.find("PivotColumns"), self.cell_styles) self.excel_definitions = TableDefinition.dict_from_xml(self.xml_root.find("TableDefinitions"), self.columns, self.colors, self.cell_styles) pass @@ -85,8 +85,9 @@ def _write_Excel_from_definition(self, data: pd.DataFrame, folder_path: str, exc data = data.copy() # Create a copy of the DataFrame to avoid modifying the original data - # Prepare columns if data should be pivoted + # Prepare columns if data should be pivoted or grouped pivot_columns = [] + grouped_columns = [] target_column = None target_column_index = None for i, column in enumerate(excel_definition.columns): @@ -95,6 +96,8 @@ def _write_Excel_from_definition(self, data: pd.DataFrame, folder_path: str, exc raise ValueError(f"Excel definition '{excel_definition_id}' has (at least) two pivot columns defined: '{target_column.db_name}' and '{column.db_name}'. Only one pivot column is allowed.") target_column = column target_column_index = i + elif column.grouped: + grouped_columns.append(column) else: if column.db_name != "NOEXCL": # Skip first column if it is the (empty and thus unused) placeholder for the excl column pivot_columns.append(column.db_name) @@ -114,6 +117,19 @@ def _write_Excel_from_definition(self, data: pd.DataFrame, folder_path: str, exc data.reset_index(inplace=True) + if len(grouped_columns) > 0: + matchingColumns = [col.matching_index for col in grouped_columns] + matchingColumnsWithoutNone = list(filter(lambda x: x is not None, matchingColumns)) + matchingIndices = data.reset_index().set_index(matchingColumnsWithoutNone).index.unique() + for i in range(len(matchingIndices) - 1): + for col in grouped_columns: + column_templates.append(col) + + # Restructure data to similar shape as Excel + data = data.reset_index().pivot(index=["id", "rp", "k", "dataPackage", "dataSource", "scenario"], columns=matchingColumnsWithoutNone, values=[col.db_name for col in grouped_columns]) + data.columns.name = None # Fix name of columns/indices (which are altered through pivot) + data = data.reset_index().set_index(["id", "rp", "k", "dataPackage", "dataSource"]) + if len(data) == 0: printer.warning(f"No data found for Excel definition '{excel_definition_id}' - writing an empty file.") data = pd.DataFrame(columns=[col.db_name for col in column_templates] + ["scenario"]) @@ -121,6 +137,7 @@ def _write_Excel_from_definition(self, data: pd.DataFrame, folder_path: str, exc for scenario_index, scenario in enumerate(scenarios): scenario_data = data[data["scenario"] == scenario] + no_wrap_description_set = False if scenario_index == 0: ws = wb.active @@ -166,7 +183,12 @@ def _write_Excel_from_definition(self, data: pd.DataFrame, folder_path: str, exc if column.db_name != "NOEXCL": # Skip first column if it is the (empty and thus unused) placeholder for the excl column # Readable name - ws.cell(row=3, column=i + 1, value=column.readable_name) + if not column.grouped: + ws.cell(row=3, column=i + 1, value=column.readable_name) + else: + group_number = (i - 6) // len(grouped_columns) + group_index = (i - 6) % len(grouped_columns) + ws.cell(row=3, column=i + 1, value=str(matchingIndices[group_number][group_index] if group_index < len(matchingIndices[group_number]) else "")) ExcelWriter.__setCellStyle(self.cell_styles["readableName"], ws.cell(row=3, column=i + 1)) # Database name @@ -174,13 +196,16 @@ def _write_Excel_from_definition(self, data: pd.DataFrame, folder_path: str, exc ExcelWriter.__setCellStyle(self.cell_styles["dbName"], ws.cell(row=4, column=i + 1)) # Description - ws.cell(row=5, column=i + 1, value=column.description) - if i != target_column_index: + if not column.grouped or not no_wrap_description_set: + ws.cell(row=5, column=i + 1, value=column.description) + if column.grouped: + no_wrap_description_set = True + if i != target_column_index and not column.grouped: ExcelWriter.__setCellStyle(self.cell_styles["description"], ws.cell(row=5, column=i + 1)) else: # If the column is a pivoted column, set the style without wrapping text - cell_style_withou_wrap_text = deepcopy(self.cell_styles["description"]) - cell_style_withou_wrap_text.alignment.wrap_text = False - ExcelWriter.__setCellStyle(cell_style_withou_wrap_text, ws.cell(row=5, column=i + 1)) + cell_style_without_wrap_text = deepcopy(self.cell_styles["description"]) + cell_style_without_wrap_text.alignment.wrap_text = False + ExcelWriter.__setCellStyle(cell_style_without_wrap_text, ws.cell(row=5, column=i + 1)) # Database behavior if i != 0: # Skip db-behavior for the first column (excl) @@ -198,8 +223,11 @@ def _write_Excel_from_definition(self, data: pd.DataFrame, folder_path: str, exc if col.readable_name is None and j == 0: continue # Skip first column if it is empty, since it is the (unused) placeholder for the excl column if col.db_name == "excl": # Excl. column is written by placing 'X' in lines which should be excluded ws.cell(row=i + 8, column=j + 1, value='X' if isinstance(values[col.db_name], str) or not np.isnan(values[col.db_name]) else None) + elif col.grouped: + group_number = (j - 6) // len(grouped_columns) + ws.cell(row=i + 8, column=j + 1, value=values[col.db_name, *matchingIndices[group_number]]) else: - ws.cell(row=i + 8, column=j + 1, value=values[col.db_name]) + ws.cell(row=i + 8, column=j + 1, value=values[col.db_name].iloc[-1] if isinstance(values[col.db_name], pd.Series) else values[col.db_name]) ExcelWriter.__setCellStyle(col.cell_style, ws.cell(row=i + 8, column=j + 1)) path = folder_path + ("/" if not folder_path.endswith("/") else "") + excel_definition.file_name + ".xlsx" @@ -301,6 +329,15 @@ def write_Power_Hindex(self, dPower_Hindex: pd.DataFrame, folder_path: str) -> N """ self._write_Excel_from_definition(dPower_Hindex, folder_path, "Power_Hindex") + def write_Power_ImportExport(self, dPower_ImportExport: pd.DataFrame, folder_path: str) -> None: + """ + Write the dPower_ImportExport DataFrame to an Excel file in LEGO format. + :param dPower_ImportExport: DataFrame containing the dPower_ImportExport data. + :param folder_path: Path to the folder where the Excel file will be saved. + :return: None + """ + self._write_Excel_from_definition(dPower_ImportExport, folder_path, "Power_ImportExport") + def write_Power_Inflows(self, dPower_Inflows: pd.DataFrame, folder_path: str) -> None: """ Write the dPower_Inflows DataFrame to an Excel file in LEGO format. @@ -493,6 +530,7 @@ def model_to_excel(model: pyomo.core.Model, target_path: str) -> None: ("Power_Demand", f"{args.caseStudyFolder}Power_Demand.xlsx", ExcelReader.get_Power_Demand, ew.write_Power_Demand), ("Power_Demand_KInRows", f"{args.caseStudyFolder}Power_Demand_KInRows.xlsx", ExcelReader.get_Power_Demand_KInRows, ew.write_Power_Demand_KInRows), ("Power_Hindex", f"{args.caseStudyFolder}Power_Hindex.xlsx", ExcelReader.get_Power_Hindex, ew.write_Power_Hindex), + ("Power_ImportExport", f"{args.caseStudyFolder}Power_ImportExport.xlsx", ExcelReader.get_Power_ImportExport, ew.write_Power_ImportExport), ("Power_Inflows", f"{args.caseStudyFolder}Power_Inflows.xlsx", ExcelReader.get_Power_Inflows, ew.write_Power_Inflows), ("Power_Inflows_KInRows", f"{args.caseStudyFolder}Power_Inflows_KInRows.xlsx", ExcelReader.get_Power_Inflows_KInRows, ew.write_Power_Inflows_KInRows), ("Power_Network", f"{args.caseStudyFolder}Power_Network.xlsx", ExcelReader.get_Power_Network, ew.write_Power_Network), diff --git a/TableDefinition.py b/TableDefinition.py index 80b79f1..2cf7942 100644 --- a/TableDefinition.py +++ b/TableDefinition.py @@ -154,7 +154,7 @@ def dict_from_xml(cls, cell_styles: xml.etree.ElementTree.Element, font_dict: Op class Column: - def __init__(self, readable_name: str, db_name: str, description: str, unit: str, column_width: float, cell_style: CellStyle, pivoted: bool, scenario_dependent: bool = False): + def __init__(self, readable_name: str, db_name: str, description: str, unit: str, column_width: float, cell_style: CellStyle, pivoted: bool, scenario_dependent: bool = False, grouped: bool = False, matching_index: Optional[str] = None): self.readable_name = readable_name self.db_name = db_name self.description = description @@ -163,6 +163,8 @@ def __init__(self, readable_name: str, db_name: str, description: str, unit: str self.cell_style = cell_style self.scenario_dependent = scenario_dependent self.pivoted = pivoted + self.grouped = grouped + self.matching_index = matching_index def get_copy_with_scenario_dependent(self, scenario_dependent: bool, color_dict: dict[str, Color]) -> Self: """ @@ -210,6 +212,7 @@ def dict_from_xml(cls, columns: xml.etree.ElementTree.Element, cell_style_dict: unit = column.find("Unit").text column_width = float(column.find("ColumnWidth").text) cell_style = cell_style_dict[column.find("CellStyle").text] if column.find("CellStyle").text is not None else None + matching_index = column.find("MatchingIndex").text if column.tag == "GroupedColumn" else None return_dict[column_id] = Column(readable_name=readable_name, db_name=column_id if column.tag != "PivotColumn" else column.find("DatabaseName").text, @@ -217,7 +220,9 @@ def dict_from_xml(cls, columns: xml.etree.ElementTree.Element, cell_style_dict: unit=unit, column_width=column_width, cell_style=cell_style, - pivoted=column.tag == "PivotColumn") + pivoted=column.tag == "PivotColumn", + grouped=column.tag == "GroupedColumn", + matching_index=matching_index) except KeyError as e: missing_styles = [] for column in columns: diff --git a/TableDefinitions.xml b/TableDefinitions.xml index f3499a6..b4b6f85 100644 --- a/TableDefinitions.xml +++ b/TableDefinitions.xml @@ -104,6 +104,22 @@ + + v0.0.1 + Power - Import/Export Hubs and Profiles + 45.0 + + + + + + + + + + + + v0.1.0 Power - Inflows @@ -929,6 +945,34 @@ + + + Maximum Imp-/Export + + [MW] + 23 + rightInt + i + + + Minimum Imp-/Export + Minimum/Maximum Import/Export (positive numbers are imports, going from hub to the node) and price at this hub. Always specify the hub above 'ImpExpMinimum' and the connected node above 'ImpExpMaximum'. + + [MW] + 23 + rightInt + hub + + + Imp-/Export Price + + [€/MWh] + 23 + rightFloat2 + + + + diff --git a/data/example/Power_ImportExport.xlsx b/data/example/Power_ImportExport.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7bb7d6ceb3c77d36638af3e9b91fc6d155a1c7bd GIT binary patch literal 14607 zcmeIZWmH|u(l)wqCs=T|;O;KLgS)%Cy9a{11$PMU4#C~s9fE}5Zr|G3?>T#KzH{!l zcZ~b!T1jw3Sy;Ydl=w@Ux#NskMkk&MYQP%WeABF zVR~sL+sbqA^+D%6cYiXs_xa7Lky+)OWR`~|`Li9`cq{qxji!bH`=u&s3 zZ+zB2iTSFPPJ#pp0Hl8a08oL%xLMIVnHgIf|M{Qs4{A;{G#!^Y(0ooSC*4eMtl|3E z{2S^}9nD=<>b`zHks$~|R*ST9H;Vo2`(2C;X(S2ZBu=^|jR&0O@Nry3?8gCz`{ge> zgM;9ZgbO#lCHj^5)<*jEO;sjRWI+b3qRO*Q*n&?w^%~}Y9uDszRFt08fN*FjH?)y? ztxZjAdXo90x|KDR&*oM+m|{kH-a0g90+UMe8n(KGb$bNNckf4(B8~GvP*nxVn?vd@ z?ydTsme=`=Cl0yrnEY)RaMTEW$8C#tWi&#zl)8I6Ell}UEC;5h#r*h37lf(HGoLhC zyjiY@Ei8#tl%2Wt-Z)M(ONWirl%273U7Lvt?Ju8nL+Ch{{nAqr+Fv^9bZk!k;km*x z{}A0wNOb4e1Lb}Fp1=$tK+Vo&esmfvNr&>mIRSeKuXRmO>rCwzXDISZe;@d~`7~A% zi>O0J3rg(kM}{}o zxMhN`c_~QINo(3f{FCFsZdOtJg0#@MT^kL|`6G#@{+ACn+^7y*xTZp06E`=p1!PPU zr}wbOGCZaPDoIt^6BE0c5e_7+Yd31%H?m?jBn#gKx%J5E_W zLKTQk-6*%M<|V7W2T8`6>>;>^v_|=fOio8B2hJ6bw!#B}e(j=_4(U`a)f> z{E|K+aF#$%`XCt_ZC*=KM7`T8ZQQ#cZ0#;|1bAofQ^076%X16sJ~{=(Bn2 z8Ga>cRBCxQGPMw&0Tvs!Lp{2GgZ;P9dKxJGQ*JWTb;wWZWvuahdV@d5Vx7n5V-b<# zbl8Ev9S@H|{*E8aA(?uTYp(|WoT<|z(RC0oV)ESN6FkRkMH6)^Hj5#-`;NSb|B#6V zDlb(Xrv0;uMOMid7QSM9CRm8`W_-Rq4>wr$r2aO!iU3cL*QsUp?$Wz}PEKDs?}>ub z<}2Qu=|*-EZ_}=saLsj58_nLw3dZFH7CH(;Rfn;`0j0`#%09bGKFw#4i&E@6BqU>n<<)jGhg~s(Zm6Wbn*2D^&-R3E%rlrH{#?@WQcoWK+7gZe= zymjwoCSKIb2eaUjWzgYJ#0?WL3MF$du@pOgP)qbGb9@(JZa_)KY}?r%u$NJ6LUCz$ z2g=mk%K6R1n9;QGVuKbQhDI|)9c5cu`(*A%T6%>CvCBiUYnR@|v)z!`yFKn7x>wr5 z6+Sm>I}bMT+~S|o-TRP1oJ2rL5^*8XF_?Dce#ON=qJoSi41GKU4KOOi^-FnxYrlYR zrhGqM&d2F1LK7G)qccs0f)-W<4?%?CuycAEZ{ZrT@Id2l{aK00XbxjoIJAqG*+Fi6}y96PpZemCWH3F>x#}SZyg4&=w;s5P5jy~j%glE-6CbLYV-K_ zOdFhbD_rYfI{T>p6C9AX;1Sj`8R8u@Lsc}8U;qw z$ox`JUIcNFh7p=qEV|KX2IYm-<^9Wy@a}$Um+(0z92;LmQn8um!pk@#VQmHH1orq> zW)aM85Lj?yA#@nOHn8RG%E0ohzMLXdb@xVXorHe}`R4=zVvyI@0t*1xhynnZe@-B- zwhoq#X2!-&j`Y7je$OK1=^HkAqgeeD)YteivJHtx{P2)JDMIU7$H}9XsFtl;p_H#- zR(3EU0{J`dXL@vGD@#?%_>{W?d~8wL;D{>PA(W2L&wFGegi!Q2aQ9t+8bi^5Lc zUzPG~9WoxNt&(k~ z51nYL?ZzpjTXY$mo_L{YRn^jHFKLiV3B|JNENMUa5KNmd)Xt2O!WS&uxROmB2lubO z(r=yKTU>?BZC9Q?-R{(QNqG*>J0JEZ)E_4|$~36*-VDz>?chJ{irqPgU7DnrcPk$Z z9KO~k(=6hq+?ZJ`{ydx`GPxX%uX^C7AAE7vjUMAcJ(}?^XQz~?AbnpzDUaPeT})ve z>X1%7kQ*04Ryi}8xZj$nQc*`GDa@qFt-5@wBIB^PyEbfIk=~*CEjo%KV!e`AUujFc zf;S-FloWwEc!or8pZwRgFPxhXt6G05PFejIid8qhNct}@X?V%-s_xsB ztPrNGJM64G8y5Yxmi?y|{XbZDZdrH2Q|*hm_>8nU>H~09pWr0GB2yE`#`Q4F_L#Ps zMDKX8BOpE0Ra5=#Q~hJfnJiK41MUtL(r2XW7QS;9 z83+|=BHKs?i+R0>R92)slc7}qNr6}`t#|UZNg3=*oR_T_Zmm6t)%m2Zx|X|+@9%|o zyS{C>rak1Y%w{Ah(&w0)oO&A8-guvee@&`B9v)pxv#+@Zwa&;wK<*ji^pR`$7GmC6 zk-l_Y)=(6Skt?i!Q^5kWLQ+*~-@-40gipmj#_4O_K9{8JS+r|OBV9(jltkthv6KYB zgQ;<1*)}(8(ZF6&D|ok!7zHWE)X|xql+g>L1JAP}0SC5q~=-EsspNM|in| zI{68e0Xd%h<0-emODWdG?GknqRH5-xh1Q1e zOpKjFX{)NYEeK-A;A!Rk9d zT$x7P0+LQXe;d<<+q~4WS1tNS*{c)X5VNl=%Op#m$UTKha1MFGE%EbT3v(yvJY$|z z$X{RJhJGy%?K3UCzFcl>-Rf%Qx7(^ZOv&umrJ0BJ4TbheB>ik!YU}VCxAG&gyT5d9 zy*1+(!gyg0{b|k;(2}O5+F0<66B(b*LHb%qIN>LJw4%jJH@~f6x>EMTu?5Xw-#x+| zpP6A;@Rm?smmo0^9d-^^Zj7f?<>>Kh_V!!;>5Pka@}S$sRR6Wfa``FjMR12~7uB)Q zhJ3{2^gY_OH_a6u>5*(u1NEmtkiOAcgMxz zg~6|)peYX0kS0q@dEdhT7+WY&vPnQ`VgWHFw&CD|KX$Vn_SKu2072Ges91n}G5`u5 zROe?vOcVep1t6U3%M1bdVXmQo5JCeq!2$V4n#6z$PR`|Np=uDC5#nx5TGtvP(4BbIWd4xc>@h3W@1QiZ?x9I7yz55O)vt$Yy=m`-2p5j z`PqvB(5|Eb5vAeQp+IHc#GnkKfKMU-fsz+BZ~?v#P>IYF!-*gaaDY!(023(yDHmcV zIH;ZrxPagWxSzVVl_EHx_dpRDP`ivw+(1Bjgk$iy$Lt_i-E!+&YSuw!*)dmhkAi3W zg2uLSwCDxlt$0~}q!8V=ABf3>l4k3_|IVrUnNT?^L%z#*y(07m&FNPGz; zA>}JqxWgde8{>2ZrF@0JfTY(KdP5jSl*=pbr!7WxL->iT)6byS6%wc{g^B@9hA5UZ zD8E3FV#XdbTTz^gD+t&?%oyhnnb>kj$mK$yZQKggu<}&)SwJao&=L~_n)le_zt|F& zg7XWkfl}Dn*5X{t!9XeOnCK5Wtl5Ob55eG=?)lp|xk|a_eg+>Gpp|PO8( zSYZQxt}}$}XwGI7_(A372PPEwd(wy-c(BB|d2aAjGlX1W6p44Spkd#jqn8gV ztj~_}bxmAjIg$zQ3&?V0e%@Mfu5AFwz3+Ar0>vQGvnVLMK;AZcuF^Ih6ou%M*NWiY zPydPlVSPT7ulM2_snJY$J^lihu}&-m%2(1HgzR!pSI7`pxsT$0cxFngVhNIMU@(0l z1TI{~>AZwg9B3+}vFfq;1@9?m>~XW-OK@q#0;$BE%<$h{g9a*W7YAJx%-a^pRZ7G` z)|cCpM47EkToqSniv74R8kb~~|7pP8&xZ8yK(yo_%}uV!@C*^C4Nt;P&`jy4I8d9U zRyN^z4-(W|Z6drx9s?=DS0eK|7E)ubaWEFr$f)4_unvDHur&d-WTE} zS{y~98^#4FM5s?w;L(y~VZMAyT<~ZDi$-c36CM<3YMiK;qJqc9Dq7LsuduDFfDzS;*VD*w9vi_kc@8JZ;Zz=4&D z6@5aCvB{Ydw)f3we!|fU>*`lWA1gs%={+5rn5(oszup!y6DBd)UQbeI9f9Gkmn4Wz zAW_;y*MC`oo~Zgh`-px7b)P86Bx_@@)hUu9VNPo9z*EPOsb08U(}Jd%(#lpCcL{p< z<2yr5L7r5B$i*VDd$df>uXFMBv5g2{e#Al}6pbb9ui?Mi<6G(I&H_f|tc z##UOw)xrd1T$M^{;ZJK&7>3)O)t#No?ixyj%SPw!7K*)C@n2 zjD`)z3zr-oAj#_bwEb=pO|lhU7C*jtgjVJqGOj<1g=$8ob}kI9Tuq^_yNjesK1ywcH%q*@Dcs|JIjW)Sd{_fuGQXn-_yU{UjAvh+P{T8Ul| zPz)WujMWt4OIYudcUTKYe^7^ufk!GFD5vt$=Y1I-eEQ0))$FBdIVZ`UaFY>%Ue1>k z(>My77ARiO6B@`x!keAo9In^GxWK8VConD|z}4(^H`v>+4+* zg5@pUSS~}-ZMa`ovf0}*I6GW?p?6!-w{&+yj!m~=Mz_-(c)Oxv&qZuz&yJ zbxmCH6(|1rjnCQh@);HbeJvT7)g}4U=l-_#)O9en`uhr$?vLmCSSiE+2BhH1=_>$y zi=DpTdz{8#xKEOlk=_O6(2b3JNa9*e_Ux}b9bQ9drWFj5mO}K}gn26D33bs^cp#w1 z&HXXz1JbEZ^Og~m9w3NI5D;IyGYL8?vFfPiKTUs%dKIeE{(x0U+iGF6jlsZBN{}ZY z5vX&frhqwD#EsEP<4?(1Po|ByX{!*ZQa_N!Vo*BQks5yOX_<^ri=Rr#BU}?WMo_~2 z&(2lTKT1OnOufU%{Cm*(o1woK*-U?=YRl8tY*xfmgC{UjJPEy~Ydcdb9*+1!5S#{N zXjZVmGnP&QyYvX9>S#Zrv&P{LcycT-tJbU5*viKlWAMk-Bsh^&n|G8EA8*}W#?3zt zINVtKsG^1c9C&tP;^%A9q$xa&irNos-R|I@7%^w;lC>FL^7i$&HEk5TJS;nSM?Lj? z7xKAE?DlzTcXQXf{$%9m?sKm1a}I8Dt(A+Xw-@2rz|Qx;p9j0%_f?qDq9!l z@RoNtK`rH`>%3c<#pCYVD#K&aMgYX+IQaN+^m*xJ2z^NAN~TmVq>!RiXOk0 z5N7Y+&bVY=&mC32oP57^p=Mu}L{Paq!b$Vv%ChbAtCn_T;MiQ)kK2pmYvX&ha>|Nh zXI~w?<`+x^4A%iDj#_>D8DC6#A9{j`@HvB&Bfmfgp30YT{ABL5RSx8Yo@l6dj-!^& zND;pLsWh)AuWI!Ba5?bJ@z#ZntDXt~IDUuyVU9wOYxYtfsBs3H% z;ng}*7kJiAS0=oS4Ob}yvr`wS7|nxwmTRS0#Xqo$ zj|2+01Y)x>`D)iG*~4AVt%l8HR#-PMhtzLzYw4f6*z*-cpY7qU7cTVKHhZ&{`~02= z2*$D2+*^Oq?^WGFF}C1WuEZ?zX3a3~HzjM1iZQj6?%%gDJLjnJuCpPWMAle@;a z*j!>N&#Ep4Ga6~iO7XJki|&#&XX>g-?a(j$3j2n+Pai^pUe%T9X2ezIb!q5fU509P z2cn;g+}X0w1EwOb$Dy85rMoL$+g^z zXo{hMtD7oSRlaXe>~2%qo#xzXkI`5`QX+2FgXBy&mr5uP_CrMAk`p7@3(lZD6O33K-GaEH@>>OtTGAEXgd|do*K@rbPbJxbSpj%22OE_OAT`<2$HMWV}Emi_DdYVHaMB#sGo0xl+rXz59{GDSM;K9vPbl$ zW9gjk!~=orA|(pvC(>_6m=-4q$j3-8!&NYG2vHV-%^S2Z1gWvsvz)OLU+PO*jV(vi zHj=8f8sc4A7TV}f+D!t5>5N02Cw!MGK&craYGK4;TZJnGMHDH!6i1^eZoCdam1aaN zuwvZ7UDkr5Wk9b%yGSjm?#qmUU$^Wu%Mcrt>Op7c3_mE26kuRY@=LPOvosu&Ix+}% zF)9_tSYo2ltoxv;d9bpW`=HRYs>lE;qHs1$9F%Xxh#mT zgo5}H1-%BnAw?fi`j>HMIs;1VwP=wUBXJf^IzkSyV;7Of+@t}ysbi?6U-nV?dywjS zkn%BRjBhQI3f#GKIpJpN=(P18G=K^x6|M`an!{OHnUo{yiDoAaSePoZ&?ViTP9tL} zHNy-?K;rxiM!--%P#A|}L0OrT_M{-!3-Ej^`Y5{VKx!Fy^eF&CVY<+YF+O|`JxqG` zwD`uJ6?<}-$0xQ0j<%gd1Ft0{$#81gGq*md1A$gs7p4rqg{~LXVV?N=920R~EZe|3 z>{_y%&gU4FbIY-jITAF%;Bl9phEB zWW)xiO&EsN&a7V#^(Z%1tBK)}XMnMeDOZ9;XCBQNmJiJoa*>yT^bu#E zYM^`x4F$Z+7tDm)fI!<9Hz zth-}I(ZLk*^{-hiDm;D9&K*ypZ0Mr{2NkBQD7wmVS~KF2%L? z^e)apLrnDuCNZkTwDdTQhl=kVG*5~`qMfSbo7aB8icgcn22+bh1GA{Vq?AI4udR-Y zdgmX&)S%LMNla>7klxulx!u1Nmc>&riagI<_T-q)a>>pRtf_4^lm(Q>3cph4c%KNbWS)KE-lO zOTJWOfRu{~=YiuQ&Q}mJgR!RV!eAjh?JA;DnCG(j5|I4J4H<18?vsN?pi$yw8#0~` zCo2al0^2;+t8?d}P_PE333B!1F{sGDEOx5V7$tJAWltasc&vw!^UC6iH72t{>yE$c z4GbA({4y|^rYO_GF5rk1HH#l7+KwSqoYAs(WOknv2%Chm4Dwsk3!4sC-D-VuS)Q^z zofc_O`<`)gwE`ruy6@}q6wIxTyD36N`IR}wU?_A3x+IAR8uCm%26hNzJ!NBl-unu$ zxUb>}t~Qtv+f(B({x>vyqcD^jKQF;R$`X{!BQ^&!iV*r8ep<(w8`muAP(j4_jqDVMD)m#;sJ#AmJ> zojGb8!2xIZ^bVJ+zcPVV6xAUev8I9v!u_2{d{j=CrD!RlhPV{t{e|hAVjCSy& z#~1e6IqN2=c{^P=R})MA9&cLN#TrQG7`rJ*F;O|-a4g=3k-xnR5l7y{GJ8FrN=jxx z=#&b(2d`kb4B@bol2Sp^4caxH=P0$boe4>Z=;@Ev(q?F-(dg*tc%BG-F}TpAUYCpz zoo#N|teCtf!>#d4yJZmFvUw&m>5=V$t${;#Wmln>eWeqo`Hq3hI2<`v>?B-cgvS08 zZ6$tnCijsgOr4w|wVr}uIM%=(?9{NRBP}28C-=5p78F)&mV61RBBwFOAN%_bH3BKP zYG@(7(!5*!jzx4687^0=txmDD^NvNlynl2vEBlUyu^>_>4`MDT$$JgTiI5o7LoPi(b#zIQ3#C$9lbl#ag=J{gF5AW#kI0*ui| zlpXS^Ky3&Mgv-R3-Qu6^RyGr+`<;sDW-?q@)_z;)RKzR&r#kxNM;7Hi>zv;fpp^)Vj39BT(yc5Y=kVYa)Wvuocs`(c_tCe%*t2z3i_X4UjHpYgo| zMHQ}6EY6*V`l`CRiqwd-gM@8p8^B!{S{?}}t4+sqa?lOzfSP4+vYstxzz(>$t;-^2 znZI4@O;m~@qtJkfY~lVx`HDPNJgA|*sqb!;O3XcyPFHL|_wh<31OJLT z+DezYz2Dp>sd_G7{5I6ey=pH|QGp|L)=aVTC>{0N(Vh-CA&c#lyf`m(pV|ghX&0r}m`NEhr)4`| zZ5sw|e~9<~HDMzb@6JYNV+X&Q#<6I1uemW|i@tZo;WIvVA>3L*e;W2umC}cIc9~YA z#p4S`I?!kBd*;M-%-^ruUdb1CvSyX`&Y0TPMl^7{4wq*YBfjrKaml2wiGZKO%7;C5 zoUz(9Z|hvp_k!ishhOLA<>9jEd&blM_sr(+B*Xt^HtGLcW|QfEGn@aL+5G<_vw5bQ z&-ec-v-!J6{!eC;6ETnhNu<)?9sr*!jtuf%dN~9xB7#w+ZO8?Bm2X@KKq->ybZP4D zF`e)`v)N6kO&G62uH+Q*M`jbJ>CepO)!#Ck#1ro}8{svQHOb9Q2YiW^JCY;VleAgc z&*3mr=Az}Z^F`?0$|yb^&1i=aOX*3-KEVIEAO0>M=n}ASMFOpuT$07PYE-B zF%@^IdKC7C`e$bIpoHMu77PHuK?eYUm4iR_pE^3ZTNyk4kq>Kfon(8sKe~By`mD)fDrpvIt1jlBq@Fsq1F8jPUOPL!&oAtjT55yiR{4HVoo{=E0gDZR2* zy-?DWXudhM32foKa7Ss^T5&TKaxK|t)}7dEyZ>`-vr!@Q5aveejr?2| zIX7GIv%{!OasKD$(kz7)#&Op!cdttpfhzk&^bvqtiWXnvVziM>7z_* zIezWb#xP&31>1YM&JCCnwR_D)>p(gZp;EcR=^wC3&oCB!1N9tADlN(WD9;E}@DZ}$ zb8Nw2s5)h&y7puTM#3f3Vv=vZCwST{paPD>BB_5+?CFBf_2FQx%GS8EgvV4+n$jYf~|PrumD?I4$XZm)-LW;doJ?G zj9Qd9UQ4#ch*Bemj1N@rH3fQvZnLk$ZQ?s5_c=|vY}X!>oqe5`Fm-gvgRMKS=r*~& zlTzXzin5;G84BOB9mycgM)q^3RFsCxCbtu&UAsPnwRJSfBz=gyhcO7aCEefoj?C;O z#aOwqA$F=#sG3DzrZ~EeWwLP8IcK=-^Tm>^(dGC))xPhdrNJN+ad9Ls;-+#?OIPa_ z+Ntt$q0!jf4q|$f^*xUCB3HOe`!9XABOU8|>InI)a2Ho9X-N0l90@JeYkkIF>kW{i zh8Q&!ar8v9!+y|ptxMPTIZSfYhF=_>qjzA>?Xi%QF>i#4Y~F9H%?IkKfvXZ0I<96PEN80IMcQ0&{Ezg(2b`bL}pMq-_ zjM;WA^f4G$L>x3*OD_--VvGXG-%hZENTuVQz3BVgktYC}2nD!An2{}#9_stfz+O1* zj~xUe(BOo`J-71E=s!WA6G4RJ1}k4vkJ={M_IZEQ`$Eb3g8j2+Hi{jGbpm@NI4z)MqcIY zYD+pt!p`S&fz21WCygmW((=YRBQ(gs*S%J0jSx~V0NnDR5@OBzEI7G5_enEUb_M&A zbFmZIKj%H-hZs8ud?B;AA_zJ-#r|k8ncIb`6g2p88=Gq#btkke*`qY2j;`0rtPR9d ziGSC+?w(3&fm$btp3#wLUmY+%IDU)Ds|y(q1>GY&=iVZ6;=eH6gQvKVyLO#|M3S+4 zpvssTng|J7t*l2zqAh|WxULjPMxYk&*VZ|?Da|V9OQIzUeNXV&>x?z^Tec|NoS|Ox zB6O+zmhSeH=gAXU#j((n_bDg)+W38%9fY9O&wRG)DsI#{QG;KV9qZ zr2lrazBp-E;EX0(iQ)hdvp&o%F11-jr$=&n>gxSGns9VN-6EG0Zj)bnCO!Cw|0(W+ zMPCM9@d<9|)PcXk5>qQTAdwbZ#NU}?l)e61^c>e4OeV420%@C}&3}X0DCy_d+!Wjw zp*rWa#Jj4!kND)uXIvc`GOX{ zL0++r6xy659O0$oDBrvrn@ll=6jS;y{W_MJBS;Y{aZPkws?Q3P9q%PY3-PU-JE3d_ zne7#b6UV6{0Il+yFqRz^^#Q}Bk;dXfcg6{tC#m^o>w*z20aj!r?Ghti2BzTC>wu&k zI{I+^5JGAzkipao0j_)p(NwnxJ2$>{E5fM=Du-jL2R|=5Vy#M7T(4+4P*g!uZ~gc{ zQpb%p;e%*Vh&kOp&aLw3V-{32^Jd=tEJyr1dMx?%8#|tk)s?~6*JXjiSF#vSoM!SE z+0%vF4!}cwokDAupSbG%Hk_Ok2q+rhzaLftdf4wjKj3TlzmG1xm416p=ucSyzz?M7 z@6!J|HS|{e?P;37#8-jp|HIjuw*YTpuI{rc-1JVsd`ERSB-}^h>qP?xv|AqDy82SOx{$|;KtlYl^d|MO$ z7hr>b0LELuzbfQ!QT{tsKY%Bq{sUFN1Masd|DCE=ivLHdelHl`qP-3N|7DpUh$Hn| z%l<~yUrT|vfNz8BzW}vpegpm$a=#UQ8wLC&dPDz@nBcA8+gbH5!3~Cg8}Y9>_O0;S yIpQy28K&RD|7FH_i}QB;{R@YS836bXqp_S61n@B2AIBV#05aeJKq<=~PyYui>5)hP literal 0 HcmV?d00001 diff --git a/tests/test_ExcelReaderWriter.py b/tests/test_ExcelReaderWriter.py index bd09443..e33f829 100644 --- a/tests/test_ExcelReaderWriter.py +++ b/tests/test_ExcelReaderWriter.py @@ -16,6 +16,7 @@ ("Power_Demand", f"{case_study_folder}Power_Demand.xlsx", ExcelReader.get_Power_Demand, ew.write_Power_Demand), ("Power_Demand_KInRows", f"{case_study_folder}Power_Demand_KInRows.xlsx", ExcelReader.get_Power_Demand_KInRows, ew.write_Power_Demand_KInRows), ("Power_Hindex", f"{case_study_folder}Power_Hindex.xlsx", ExcelReader.get_Power_Hindex, ew.write_Power_Hindex), + ("Power_ImportExport", f"{case_study_folder}Power_ImportExport.xlsx", ExcelReader.get_Power_ImportExport, ew.write_Power_ImportExport), ("Power_Inflows", f"{case_study_folder}Power_Inflows.xlsx", ExcelReader.get_Power_Inflows, ew.write_Power_Inflows), ("Power_Inflows_KInRows", f"{case_study_folder}Power_Inflows_KInRows.xlsx", ExcelReader.get_Power_Inflows_KInRows, ew.write_Power_Inflows_KInRows), ("Power_Network", f"{case_study_folder}Power_Network.xlsx", ExcelReader.get_Power_Network, ew.write_Power_Network), From 17d0abc48c20ead5f68df1c4b0fdf6e3faad1e75 Mon Sep 17 00:00:00 2001 From: "Felix C. A. Auer" <10127354+FelixCAAuer@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:37:33 +0200 Subject: [PATCH 2/4] Fix CaseStudy for Import/Export --- CaseStudy.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CaseStudy.py b/CaseStudy.py index ef7b0a5..a0c7864 100644 --- a/CaseStudy.py +++ b/CaseStudy.py @@ -163,7 +163,7 @@ def __init__(self, self.dPower_ImportExport = dPower_ImportExport else: self.power_importexport_file = power_importexport_file - self.dPower_ImportExport = self.get_dPower_ImportExport() + self.dPower_ImportExport = ExcelReader.get_Power_ImportExport(self.data_folder + self.power_importexport_file) else: self.dPower_ImportExport = None @@ -199,8 +199,7 @@ def scale_CaseStudy(self): self.scale_dPower_Storage() if self.dPower_Parameters["pEnablePowerImportExport"]: - self.scale_dPower_ImpExpHubs() - self.scale_dPower_ImpExpProfiles() + self.scale_dPower_ImportExport() def remove_scaling(self): self.power_scaling_factor = 1 / self.power_scaling_factor @@ -290,8 +289,8 @@ def scale_dPower_Storage(self): raise ValueError("DisEffic and ChEffic in 'Power_Storage.xlsx' must not contain NaN values. Please check the data.") def scale_dPower_ImportExport(self): - self.dPower_ImportExport["ImpExpMin"] *= self.power_scaling_factor - self.dPower_ImportExport["ImpExpMax"] *= self.power_scaling_factor + self.dPower_ImportExport["ImpExpMinimum"] *= self.power_scaling_factor + self.dPower_ImportExport["ImpExpMaximum"] *= self.power_scaling_factor self.dPower_ImportExport["ImpExpPrice"] *= self.cost_scaling_factor / self.power_scaling_factor def get_dGlobal_Parameters(self): From 846c851a6789defb68477fdb80d1401211b69e86 Mon Sep 17 00:00:00 2001 From: "Felix C. A. Auer" <10127354+FelixCAAuer@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:04:51 +0100 Subject: [PATCH 3/4] Fix comment in ExcelReader --- ExcelReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExcelReader.py b/ExcelReader.py index 4deadca..b392097 100644 --- a/ExcelReader.py +++ b/ExcelReader.py @@ -214,7 +214,7 @@ def get_Power_ImportExport(excel_file_path: str, keep_excluded_entries: bool = F hub_i_df = pd.read_excel(excel_file_path, skiprows=[0, 1, 3], nrows=2, sheet_name=scenario) hub_i = [] hubs = [] - i = 6 # Start checking from column 6 (index 5) + i = 6 # Start checking from column 7 (index 6, zero-based) while i < hub_i_df.shape[1]: hubs.append(hub_i_df.columns[i]) hub_i.append((hub_i_df.columns[i], hub_i_df.columns[i + 1])) From 0bd13c33af179ada3978ec42f7e0c0c70489ddb2 Mon Sep 17 00:00:00 2001 From: "Felix C. A. Auer" <10127354+FelixCAAuer@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:23:22 +0100 Subject: [PATCH 4/4] Adjust format of Power_ImportExport --- data/example/Power_ImportExport.xlsx | Bin 14607 -> 14591 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/example/Power_ImportExport.xlsx b/data/example/Power_ImportExport.xlsx index 7bb7d6ceb3c77d36638af3e9b91fc6d155a1c7bd..bac6077afd8ae41657e5b14bc2066b4225b2a70b 100644 GIT binary patch delta 8619 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 kND8L{jS^)3D@;TG7%lmPm_Yi!E*6GP3Nj#