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
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
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#