From cf9a64e279db49f5659e3bb9ec4464c04b5cdd0f Mon Sep 17 00:00:00 2001 From: "Felix C. A. Auer" <10127354+FelixCAAuer@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:16:03 +0200 Subject: [PATCH 1/2] Add tests and ci-automation Rename "example_folder" to "data_folder" --- .github/workflows/ci.yaml | 27 +++++++++++++++ .gitignore | 5 ++- CaseStudy.py | 38 ++++++++++----------- ExcelReader.py | 2 +- ExcelWriter.py | 10 +++--- environment.yml | 14 ++++++++ templates/Power_VRESProfiles-template.xlsx | Bin 22662 -> 0 bytes tests/test_ExcelReaderWriter.py | 36 +++++++++++++++++++ 8 files changed, 106 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 environment.yml delete mode 100644 templates/Power_VRESProfiles-template.xlsx create mode 100644 tests/test_ExcelReaderWriter.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..12ae1c5 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + pull_request: + +jobs: + run_tests: + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4.2.2 + + - name: Set up Conda environment + uses: conda-incubator/setup-miniconda@v3.2.0 + with: + environment-file: InOutModule/environment.yml + activate-environment: InOutModule_env + + - name: Run tests + run: pytest \ No newline at end of file diff --git a/.gitignore b/.gitignore index c4d5a07..53c0264 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ __pycache__/ examples/output # Temporary files -~$*.xlsx \ No newline at end of file +~$*.xlsx + +# Development environment files +.idea/ \ No newline at end of file diff --git a/CaseStudy.py b/CaseStudy.py index 8162dde..aac2af7 100644 --- a/CaseStudy.py +++ b/CaseStudy.py @@ -5,12 +5,12 @@ import numpy as np import pandas as pd -from InOutModule import ExcelReader +import ExcelReader class CaseStudy: - def __init__(self, example_folder: str, do_not_merge_single_node_buses: bool = False, + def __init__(self, data_folder: str, do_not_merge_single_node_buses: bool = False, global_parameters_file: str = "Global_Parameters.xlsx", dGlobal_Parameters: pd.DataFrame = None, power_parameters_file: str = "Power_Parameters.xlsx", dPower_Parameters: pd.DataFrame = None, power_businfo_file: str = "Power_BusInfo.xlsx", dPower_BusInfo: pd.DataFrame = None, @@ -27,7 +27,7 @@ def __init__(self, example_folder: str, do_not_merge_single_node_buses: bool = F 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): - self.example_folder = example_folder if example_folder.endswith("/") else example_folder + "/" + self.data_folder = data_folder if data_folder.endswith("/") else data_folder + "/" self.do_not_merge_single_node_buses = do_not_merge_single_node_buses if dGlobal_Parameters is not None: @@ -46,37 +46,37 @@ def __init__(self, example_folder: str, do_not_merge_single_node_buses: bool = F self.dPower_BusInfo = dPower_BusInfo else: self.power_businfo_file = power_businfo_file - self.dPower_BusInfo = ExcelReader.get_dPower_BusInfo(self.example_folder + self.power_businfo_file) + self.dPower_BusInfo = ExcelReader.get_dPower_BusInfo(self.data_folder + self.power_businfo_file) if dPower_Network is not None: self.dPower_Network = dPower_Network else: self.power_network_file = power_network_file - self.dPower_Network = ExcelReader.get_dPower_Network(self.example_folder + self.power_network_file) + self.dPower_Network = ExcelReader.get_dPower_Network(self.data_folder + self.power_network_file) if dPower_Demand is not None: self.dPower_Demand = dPower_Demand else: self.power_demand_file = power_demand_file - self.dPower_Demand = ExcelReader.get_dPower_Demand(self.example_folder + self.power_demand_file) + self.dPower_Demand = ExcelReader.get_dPower_Demand(self.data_folder + self.power_demand_file) if dPower_WeightsRP is not None: self.dPower_WeightsRP = dPower_WeightsRP else: self.power_weightsrp_file = power_weightsrp_file - self.dPower_WeightsRP = ExcelReader.get_dPower_WeightsRP(self.example_folder + self.power_weightsrp_file) + self.dPower_WeightsRP = ExcelReader.get_dPower_WeightsRP(self.data_folder + self.power_weightsrp_file) if dPower_WeightsK is not None: self.dPower_WeightsK = dPower_WeightsK else: self.power_weightsk_file = power_weightsk_file - self.dPower_WeightsK = ExcelReader.get_dPower_WeightsK(self.example_folder + self.power_weightsk_file) + self.dPower_WeightsK = ExcelReader.get_dPower_WeightsK(self.data_folder + self.power_weightsk_file) if dPower_Hindex is not None: self.dPower_Hindex = dPower_Hindex else: self.power_hindex_file = power_hindex_file - self.dPower_Hindex = ExcelReader.get_dPower_Hindex(self.example_folder + self.power_hindex_file) + self.dPower_Hindex = ExcelReader.get_dPower_Hindex(self.data_folder + self.power_hindex_file) self.rpTransitionMatrixAbsolute, self.rpTransitionMatrixRelativeTo, self.rpTransitionMatrixRelativeFrom = self.get_rpTransitionMatrices() @@ -85,7 +85,7 @@ def __init__(self, example_folder: str, do_not_merge_single_node_buses: bool = F self.dPower_ThermalGen = dPower_ThermalGen else: self.power_thermalgen_file = power_thermalgen_file - self.dPower_ThermalGen = ExcelReader.get_dPower_ThermalGen(self.example_folder + self.power_thermalgen_file) + self.dPower_ThermalGen = ExcelReader.get_dPower_ThermalGen(self.data_folder + self.power_thermalgen_file) if self.dPower_Parameters["pEnableRoR"]: if dPower_RoR is not None: @@ -105,13 +105,13 @@ def __init__(self, example_folder: str, do_not_merge_single_node_buses: bool = F self.dPower_VRES = dPower_VRES else: self.power_vres_file = power_vres_file - self.dPower_VRES = ExcelReader.get_dPower_VRES(self.example_folder + self.power_vres_file) + self.dPower_VRES = ExcelReader.get_dPower_VRES(self.data_folder + self.power_vres_file) if dPower_VRESProfiles is not None: self.dPower_VRESProfiles = dPower_VRESProfiles else: self.power_vresprofiles_file = power_vresprofiles_file - self.dPower_VRESProfiles = ExcelReader.get_dPower_VRESProfiles(self.example_folder + self.power_vresprofiles_file) + self.dPower_VRESProfiles = ExcelReader.get_dPower_VRESProfiles(self.data_folder + self.power_vresprofiles_file) if self.dPower_Parameters["pEnableStorage"]: if dPower_Storage is not None: @@ -143,7 +143,7 @@ def copy(self): return copy.deepcopy(self) def get_dGlobal_Parameters(self): - dGlobal_Parameters = pd.read_excel(self.example_folder + self.global_parameters_file, skiprows=[0, 1]) + dGlobal_Parameters = pd.read_excel(self.data_folder + self.global_parameters_file, skiprows=[0, 1]) dGlobal_Parameters = dGlobal_Parameters.drop(dGlobal_Parameters.columns[0], axis=1) dGlobal_Parameters = dGlobal_Parameters.set_index('Sectors') @@ -154,7 +154,7 @@ def get_dGlobal_Parameters(self): return dGlobal_Parameters def get_dPower_Parameters(self): - dPower_Parameters = pd.read_excel(self.example_folder + self.power_parameters_file, skiprows=[0, 1]) + dPower_Parameters = pd.read_excel(self.data_folder + self.power_parameters_file, skiprows=[0, 1]) dPower_Parameters = dPower_Parameters.drop(dPower_Parameters.columns[0], axis=1) dPower_Parameters = dPower_Parameters.dropna(how="all") dPower_Parameters = dPower_Parameters.set_index('General') @@ -186,14 +186,14 @@ def yesNo_to_bool(df: pd.DataFrame, columns_to_be_changed: list[str]): return df def get_dPower_RoR(self): - dPower_RoR = self.read_generator_data(self.example_folder + self.power_ror_file) + dPower_RoR = self.read_generator_data(self.data_folder + self.power_ror_file) dPower_RoR['InvestCostEUR'] = dPower_RoR['MaxProd'] * 1e-3 * (dPower_RoR['InvestCostPerMW'] * 1e-3 + dPower_RoR['InvestCostPerMWh'] * 1e-3 * dPower_RoR['Ene2PowRatio']) dPower_RoR['MaxProd'] *= 1e-3 return dPower_RoR def get_dPower_Storage(self): - dPower_Storage = self.read_generator_data(self.example_folder + self.power_storage_file) + dPower_Storage = self.read_generator_data(self.data_folder + self.power_storage_file) dPower_Storage['pOMVarCostEUR'] = dPower_Storage['OMVarCost'] * 1e-3 dPower_Storage['IniReserve'] = dPower_Storage['IniReserve'].fillna(0) dPower_Storage['MinReserve'] = dPower_Storage['MinReserve'].fillna(0) @@ -204,7 +204,7 @@ def get_dPower_Storage(self): return dPower_Storage def get_dPower_Inflows(self): - dPower_Inflows = pd.read_excel(self.example_folder + self.power_inflows_file, skiprows=[0, 1, 3, 4, 5]) + dPower_Inflows = pd.read_excel(self.data_folder + self.power_inflows_file, skiprows=[0, 1, 3, 4, 5]) dPower_Inflows = dPower_Inflows.drop(dPower_Inflows.columns[0], axis=1) dPower_Inflows = dPower_Inflows.rename(columns={dPower_Inflows.columns[0]: "rp", dPower_Inflows.columns[1]: "g"}) dPower_Inflows = dPower_Inflows.melt(id_vars=['rp', 'g'], var_name='k', value_name='Inflow') @@ -212,7 +212,7 @@ def get_dPower_Inflows(self): return dPower_Inflows def get_dPower_ImpExpHubs(self): - dPower_ImpExpHubs = pd.read_excel(self.example_folder + self.power_impexphubs_file, skiprows=[0, 1, 3, 4, 5]) + 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']) @@ -238,7 +238,7 @@ def get_dPower_ImpExpHubs(self): 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.example_folder + self.power_impexpprofiles_file, skiprows=[0, 1, 3, 4, 5], sheet_name='Power ImpExpProfiles') + 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') diff --git a/ExcelReader.py b/ExcelReader.py index 70f7e73..7b025e5 100644 --- a/ExcelReader.py +++ b/ExcelReader.py @@ -5,7 +5,7 @@ import pandas as pd from openpyxl import load_workbook -from InOutModule.printer import Printer +from printer import Printer printer = Printer.getInstance() diff --git a/ExcelWriter.py b/ExcelWriter.py index 0d5e38f..4d99aec 100644 --- a/ExcelWriter.py +++ b/ExcelWriter.py @@ -8,10 +8,10 @@ import pandas as pd from openpyxl.utils.dataframe import dataframe_to_rows -import InOutModule.TableDefinition -from InOutModule import ExcelReader, TableDefinition -from InOutModule.TableDefinition import CellStyle, Alignment, Font, Color, Text, Column, NumberFormat, TableDefinition -from InOutModule.printer import Printer +import ExcelReader +import TableDefinition +from TableDefinition import CellStyle, Alignment, Font, Color, Text, Column, NumberFormat, TableDefinition +from printer import Printer package_directory_ExcelWriter = os.path.dirname(os.path.abspath(__file__)) @@ -40,7 +40,7 @@ def __init__(self, excel_definitions_path: str = "TableDefinitions.xml"): pass @staticmethod - def __setCellStyle(cell_style: InOutModule.TableDefinition.CellStyle, target_cell: openpyxl.cell.cell): + def __setCellStyle(cell_style: CellStyle, target_cell: openpyxl.cell.cell): """ Set the cell style of a target cell based on the given cell style. diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..a76f37c --- /dev/null +++ b/environment.yml @@ -0,0 +1,14 @@ +name: InOutModule_env +channels: + - defaults + - conda-forge +dependencies: + - openpyxl=3.1.5 + - pandas=2.2.3 + - pip=24.0 + - pytest=8.4.0 + - python=3.12.2 + - rich=13.7.1 + - rich-argparse=1.6.0 + - pip: + - pulp==2.9.0 diff --git a/templates/Power_VRESProfiles-template.xlsx b/templates/Power_VRESProfiles-template.xlsx deleted file mode 100644 index 26686393495b97f8d6fd88a81b50b4d6d3b81483..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22662 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 Date: Mon, 16 Jun 2025 15:36:07 +0200 Subject: [PATCH 2/2] Fix CI runner for pytest --- .github/workflows/ci.yaml | 13 ++++++++----- printer.py | 2 +- tests/test_ExcelReaderWriter.py | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 12ae1c5..812dd86 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,13 +6,15 @@ on: jobs: run_tests: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash -el {0} strategy: fail-fast: false matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - - name: Test on ${{ matrix.os }} - runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4.2.2 @@ -20,8 +22,9 @@ jobs: - name: Set up Conda environment uses: conda-incubator/setup-miniconda@v3.2.0 with: - environment-file: InOutModule/environment.yml activate-environment: InOutModule_env + environment-file: environment.yml + auto-activate-base: false - name: Run tests - run: pytest \ No newline at end of file + run: python -m pytest \ No newline at end of file diff --git a/printer.py b/printer.py index 04b2d1f..c753835 100644 --- a/printer.py +++ b/printer.py @@ -1,6 +1,5 @@ import datetime -from pyomo import environ as pyo from rich.console import Console @@ -217,6 +216,7 @@ def pprint_zoi_var(var, zoi, index_positions: list = None, decimals: int = 2): :param decimals: The number of decimal places to display for the variable's value. :return: None """ + from pyomo import environ as pyo # Import Pyomo environment for variable handling if index_positions is None: index_positions = [0] diff --git a/tests/test_ExcelReaderWriter.py b/tests/test_ExcelReaderWriter.py index a9b7d1b..e2b4a17 100644 --- a/tests/test_ExcelReaderWriter.py +++ b/tests/test_ExcelReaderWriter.py @@ -6,8 +6,8 @@ printer = Printer.getInstance() -case_study_folder = "../examples/" -ew = ExcelWriter("../TableDefinitions.xml") +case_study_folder = "examples/" +ew = ExcelWriter() combinations = [ ("Power_Hindex", f"{case_study_folder}Power_Hindex.xlsx", ExcelReader.get_dPower_Hindex, ew.write_dPower_Hindex), ("Power_WeightsRP", f"{case_study_folder}Power_WeightsRP.xlsx", ExcelReader.get_dPower_WeightsRP, ew.write_dPower_WeightsRP),