diff --git a/README.md b/README.md index 715c864..1f8cfbe 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ results_dataframe = results.to_dataframe() **4. Plot the resulting timeseries:** -The resulting timeseries (storage charging / discharging, state of charge, solar generation, grid usage, ...) can be easily plotted: +The resulting timeseries (storage charging / discharging, state of charge, PV generation, grid usage, ...) can be easily plotted: ```python results = psa.optimize() diff --git a/docker_configs/dashboard-definitions/PSA.json b/docker_configs/dashboard-definitions/PSA.json index 6385aa2..a724d18 100644 --- a/docker_configs/dashboard-definitions/PSA.json +++ b/docker_configs/dashboard-definitions/PSA.json @@ -300,7 +300,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT\n timestamp,\n grid_usage_kw AS \"Grid\",\n storage_discharge_kw AS \"Storage discharge\",\n solar_generation_kw AS \"Solar generation\",\n consumption_kw AS \"Load\"\nFROM output.timeseries WHERE name = '$opti_name'", + "rawSql": "SELECT\n timestamp,\n grid_usage_kw AS \"Grid\",\n storage_discharge_kw AS \"Storage discharge\",\n pv_generation_kw AS \"pv generation\",\n consumption_kw AS \"Load\"\nFROM output.timeseries WHERE name = '$opti_name'", "refId": "A", "sql": { "columns": [ @@ -392,7 +392,7 @@ "options": { "mode": "exclude", "names": [ - "Solar generation" + "pv generation" ], "prefix": "All except:", "readOnly": true @@ -437,7 +437,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT\n timestamp,\n solar_generation_kw AS \"Solar generation\"\nFROM output.timeseries WHERE name = '$opti_name'", + "rawSql": "SELECT\n timestamp,\n pv_generation_kw AS \"pv generation\"\nFROM output.timeseries WHERE name = '$opti_name'", "refId": "A", "sql": { "columns": [ @@ -826,7 +826,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n solar_invest_eur AS \"Solar investment\"\nFROM output.overview\nWHERE name = '$opti_name'", + "rawSql": "SELECT \n pv_invest_eur AS \"pv investment\"\nFROM output.overview\nWHERE name = '$opti_name'", "refId": "A", "sql": { "columns": [ @@ -911,7 +911,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n solar_capacity_kwp AS \"PV system size\"\nFROM output.overview\nWHERE name = '$opti_name'", + "rawSql": "SELECT \n pv_capacity_kwp AS \"PV system size\"\nFROM output.overview\nWHERE name = '$opti_name'", "refId": "A", "sql": { "columns": [ diff --git a/examples/storage_hourly_fixed_price/config.yml b/examples/storage_hourly_fixed_price/config.yml index 4c7e662..bdbe082 100644 --- a/examples/storage_hourly_fixed_price/config.yml +++ b/examples/storage_hourly_fixed_price/config.yml @@ -6,7 +6,7 @@ name: storage_hourly_fixed_price # name of the optimization hours_per_timestep: 1 # 1 hour timesteps add_storage: True # wether to add storage to optimization or not -add_solar: False # wether to add pv system to optimization or not +allow_additional_pv: False # wether to add pv system to optimization or not auto_opt: False # Wether to automatically start optizmization or not solver: "gurobi" @@ -26,42 +26,15 @@ consumption_value_column: consumption # name of the column price_file_path: # file path where prices are stored as .csv price_value_column: # name of the column where prices are provided -postal_code: # postal code for automtic solar generation calculation, leave empty for own timeseries -# if not provided, solar generation will be calculated with the following parameters -# value column needs to contain values from 0 (no solar generation) to 1 (max solar generation) -solar_file_path: # file path where solar generation is stored as .csv -solar_value_column: # name of the column where solar generation is provided - -####################### -# economic parameters # -####################### -overwrite_price_timeseries: True # Wether to overwrite price timeseries or not -producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier -# value for 2024 taken from: -# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ -# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ - -grid_capacity_price: 130 # capacity price in euro to be paid yearly -grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid -# mean updated with historical inflation of cumulative 26.24% from: -# https://zenodo.org/records/13734730 - -pv_system_lifetime: 30 # pv system lifetime in years -# taken from: -# https://www.mdpi.com/1996-1073/14/14/4278 - -pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak -# taken from: -# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html - -inverter_lifetime: 15 # storage inverter lifetime in years -# taken from: -# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss - -inverter_cost_per_kw: 180 # storage inverter cost in euro per kw -# taken from: -# https://www.sciencedirect.com/science/article/pii/S1876610216310736 +postal_code: # postal code for automtic pv generation calculation, leave empty for own timeseries +# if not provided, pv generation will be calculated with the following parameters +# value column needs to contain values from 0 (no pv generation) to 1 (max pv generation) +new_pv_file_path: # file path where pv generation is stored as .csv +new_pv_value_column: # name of the column where pv generation is provided +################################ +# storage (battery) parameters # +################################ storage_lifetime: 15 # storage lifetime in years # taken from: # "Energiespeicher - Bedarf, Technologien, Integration", Stadler Sterner 2017 @@ -70,13 +43,7 @@ storage_cost_per_kwh: 145 # storage capacity cost in euro per kwh capacity # taken from: # https://www.pem.rwth-aachen.de/cms/pem/der-lehrstuhl/presse-medien/aktuelle-meldungen/~bexlow/battery-monitor-2023-nachfrage-waechst/ -interest_rate: 3 # interes rate in % - -######################## -# technical parameters # -######################## max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) -max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) storage_charge_efficiency: 0.9 # efficiency for charging storage storage_discharge_efficiency: 0.9 # efficiency for discharging storage @@ -89,8 +56,48 @@ storage_discharge_rate: 1 # # taken from: # https://www.sciencedirect.com/science/article/pii/S2590116819300116 +################################# +# storage (inverter) parameters # +################################# inverter_efficiency: 0.95 # efficiency of the storage inverter # taken from: # https://www.sciencedirect.com/science/article/pii/S1364032116306712 +inverter_lifetime: 15 # storage inverter lifetime in years +# taken from: +# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss + +inverter_cost_per_kw: 180 # storage inverter cost in euro per kw +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1876610216310736 + + +######################## +# PV system parameters # +######################## +pv_system_lifetime: 30 # pv system lifetime in years +# taken from: +# https://www.mdpi.com/1996-1073/14/14/4278 + +pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak +# taken from: +# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html + pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² +max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) + +####################### +# economic parameters # +####################### +overwrite_price_timeseries: True # Wether to overwrite price timeseries or not +producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier +# value for 2024 taken from: +# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ +# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ + +grid_capacity_price: 130 # capacity price in euro to be paid yearly +grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid +# mean updated with historical inflation of cumulative 26.24% from: +# https://zenodo.org/records/13734730 + +interest_rate: 3 # interes rate in % diff --git a/examples/storage_hourly_fixed_price/main.py b/examples/storage_hourly_fixed_price/main.py index 87d20f0..0090173 100644 --- a/examples/storage_hourly_fixed_price/main.py +++ b/examples/storage_hourly_fixed_price/main.py @@ -1,6 +1,11 @@ +import logging + from peakshaving_analyzer import PeakShavingAnalyzer, load_yaml_config if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + config = load_yaml_config("./examples/storage_hourly_fixed_price/config.yml") psa = PeakShavingAnalyzer(config=config) results = psa.optimize() + results.print() diff --git a/examples/storage_only_quarterhourly_fixed_price/config.yml b/examples/storage_only_quarterhourly_fixed_price/config.yml index 4cd1390..ac3d4bc 100644 --- a/examples/storage_only_quarterhourly_fixed_price/config.yml +++ b/examples/storage_only_quarterhourly_fixed_price/config.yml @@ -1,10 +1,9 @@ -optimization_parameters: name: storage_only_quarterhourly_fixed_price # name of the optimization -hours_per_timestep: 0.25 # 1 hour timesteps +hours_per_timestep: 0.25 # quarter hour timesteps add_storage: True # wether to add storage to optimization or not -add_solar: False # wether to add pv system to optimization or not +allow_additional_pv: False # wether to add pv system to optimization or not auto_opt: False # Wether to automatically start optizmization or not solver: "gurobi" @@ -24,42 +23,15 @@ consumption_value_column: consumption # name of the column price_file_path: # file path where prices are stored as .csv price_value_column: # name of the column where prices are provided -postal_code: # postal code for automtic solar generation calculation, leave empty for own timeseries -# if not provided, solar generation will be calculated with the following parameters -# value column needs to contain values from 0 (no solar generation) to 1 (max solar generation) -solar_file_path: # file path where solar generation is stored as .csv -solar_value_column: # name of the column where solar generation is provided - -####################### -# economic parameters # -####################### -overwrite_price_timeseries: True # Wether to overwrite price timeseries or not -producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier -# value for 2024 taken from: -# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ -# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ - -grid_capacity_price: 130 # capacity price in euro to be paid yearly -grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid -# mean updated with historical inflation of cumulative 26.24% from: -# https://zenodo.org/records/13734730 - -pv_system_lifetime: 30 # pv system lifetime in years -# taken from: -# https://www.mdpi.com/1996-1073/14/14/4278 - -pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak -# taken from: -# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html - -inverter_lifetime: 15 # storage inverter lifetime in years -# taken from: -# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss - -inverter_cost_per_kw: 180 # storage inverter cost in euro per kw -# taken from: -# https://www.sciencedirect.com/science/article/pii/S1876610216310736 +postal_code: # postal code for automtic pv generation calculation, leave empty for own timeseries +# if not provided, pv generation will be calculated with the following parameters +# value column needs to contain values from 0 (no pv generation) to 1 (max pv generation) +new_pv_file_path: # file path where pv generation is stored as .csv +new_pv_value_column: # name of the column where pv generation is provided +################################ +# storage (battery) parameters # +################################ storage_lifetime: 15 # storage lifetime in years # taken from: # "Energiespeicher - Bedarf, Technologien, Integration", Stadler Sterner 2017 @@ -68,13 +40,7 @@ storage_cost_per_kwh: 145 # storage capacity cost in euro per kwh capacity # taken from: # https://www.pem.rwth-aachen.de/cms/pem/der-lehrstuhl/presse-medien/aktuelle-meldungen/~bexlow/battery-monitor-2023-nachfrage-waechst/ -interest_rate: 3 # interes rate in % - -######################## -# technical parameters # -######################## max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) -max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) storage_charge_efficiency: 0.9 # efficiency for charging storage storage_discharge_efficiency: 0.9 # efficiency for discharging storage @@ -87,8 +53,48 @@ storage_discharge_rate: 1 # # taken from: # https://www.sciencedirect.com/science/article/pii/S2590116819300116 +################################# +# storage (inverter) parameters # +################################# inverter_efficiency: 0.95 # efficiency of the storage inverter # taken from: # https://www.sciencedirect.com/science/article/pii/S1364032116306712 +inverter_lifetime: 15 # storage inverter lifetime in years +# taken from: +# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss + +inverter_cost_per_kw: 180 # storage inverter cost in euro per kw +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1876610216310736 + + +######################## +# PV system parameters # +######################## +pv_system_lifetime: 30 # pv system lifetime in years +# taken from: +# https://www.mdpi.com/1996-1073/14/14/4278 + +pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak +# taken from: +# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html + pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² +max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) + +####################### +# economic parameters # +####################### +overwrite_price_timeseries: True # Wether to overwrite price timeseries or not +producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier +# value for 2024 taken from: +# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ +# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ + +grid_capacity_price: 130 # capacity price in euro to be paid yearly +grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid +# mean updated with historical inflation of cumulative 26.24% from: +# https://zenodo.org/records/13734730 + +interest_rate: 3 # interes rate in % diff --git a/examples/storage_only_quarterhourly_fixed_price/main.py b/examples/storage_only_quarterhourly_fixed_price/main.py index 4d2383d..236a692 100644 --- a/examples/storage_only_quarterhourly_fixed_price/main.py +++ b/examples/storage_only_quarterhourly_fixed_price/main.py @@ -1,6 +1,11 @@ +import logging + from peakshaving_analyzer import PeakShavingAnalyzer, load_yaml_config if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + config = load_yaml_config("./examples/storage_only_quarterhourly_fixed_price/config.yml") psa = PeakShavingAnalyzer(config=config) results = psa.optimize() + results.print() diff --git a/examples/storage_pv_hourly_dynamic_price/config.yml b/examples/storage_pv_hourly_dynamic_price/config.yml index 40af566..d22b170 100644 --- a/examples/storage_pv_hourly_dynamic_price/config.yml +++ b/examples/storage_pv_hourly_dynamic_price/config.yml @@ -6,7 +6,7 @@ name: storage_pv_hourly_dynamic_price # name of the optimization hours_per_timestep: 1 # 1 hour timesteps add_storage: True # wether to add storage to optimization or not -add_solar: True # wether to add pv system to optimization or not +allow_additional_pv: True # wether to add pv system to optimization or not auto_opt: False # Wether to automatically start optizmization or not solver: "gurobi" @@ -19,46 +19,22 @@ verbose: True # Wether to print optimization progress or not # name of the column where timestamps are provided (should be equal in all files) # can be set to None if timestamps are not provided, we will then assume timestamps based on pattern timestamp_column: + consumption_file_path: consumption.csv # file path where consumption is stored as .csv consumption_value_column: consumption # name of the column where consumption is provided price_file_path: price.csv # file path where prices are stored as .csv price_value_column: price # name of the column where prices are provided -postal_code: 52066 # postal code for automtic solar generation calculation, leave empty for own timeseries -# if not provided, solar generation will be calculated with the following parameters -# value column needs to contain values from 0 (no solar generation) to 1 (max solar generation) -solar_file_path: # file path where solar generation is stored as .csv -solar_value_column: # name of the column where solar generation is provided - -####################### -# economic parameters # -####################### -overwrite_price_timeseries: False # Wether to overwrite price timeseries or not -producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier -# value for 2024 taken from: -# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ -# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ - -grid_capacity_price: 130 # capacity price in euro to be paid yearly -grid_energy_price: 0.05 # energy price in euro per kwh to be paid per kwh source from grid - -pv_system_lifetime: 30 # pv system lifetime in years -# taken from: -# https://www.mdpi.com/1996-1073/14/14/4278 - -pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak -# taken from: -# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html - -inverter_lifetime: 15 # storage inverter lifetime in years -# taken from: -# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss - -inverter_cost_per_kw: 180 # storage inverter cost in euro per kw -# taken from: -# https://www.sciencedirect.com/science/article/pii/S1876610216310736 +postal_code: 52066 # postal code for automtic pv generation calculation, leave empty for own timeseries +# if not provided, pv generation will be calculated with the following parameters +# value column needs to contain values from 0 (no pv generation) to 1 (max pv generation) +new_pv_file_path: # file path where pv generation is stored as .csv +new_pv_value_column: # name of the column where pv generation is provided +################################ +# storage (battery) parameters # +################################ storage_lifetime: 15 # storage lifetime in years # taken from: # "Energiespeicher - Bedarf, Technologien, Integration", Stadler Sterner 2017 @@ -67,13 +43,7 @@ storage_cost_per_kwh: 145 # storage capacity cost in euro per kwh capacity # taken from: # https://www.pem.rwth-aachen.de/cms/pem/der-lehrstuhl/presse-medien/aktuelle-meldungen/~bexlow/battery-monitor-2023-nachfrage-waechst/ -interest_rate: 3 # interes rate in % - -######################## -# technical parameters # -######################## max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) -max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) storage_charge_efficiency: 0.9 # efficiency for charging storage storage_discharge_efficiency: 0.9 # efficiency for discharging storage @@ -86,8 +56,48 @@ storage_discharge_rate: 1 # # taken from: # https://www.sciencedirect.com/science/article/pii/S2590116819300116 +################################# +# storage (inverter) parameters # +################################# inverter_efficiency: 0.95 # efficiency of the storage inverter # taken from: # https://www.sciencedirect.com/science/article/pii/S1364032116306712 -pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWp / m² +inverter_lifetime: 15 # storage inverter lifetime in years +# taken from: +# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss + +inverter_cost_per_kw: 180 # storage inverter cost in euro per kw +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1876610216310736 + + +######################## +# PV system parameters # +######################## +pv_system_lifetime: 30 # pv system lifetime in years +# taken from: +# https://www.mdpi.com/1996-1073/14/14/4278 + +pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak +# taken from: +# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html + +pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² +max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) + +####################### +# economic parameters # +####################### +overwrite_price_timeseries: False # Wether to overwrite price timeseries or not +producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier +# value for 2024 taken from: +# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ +# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ + +grid_capacity_price: 130 # capacity price in euro to be paid yearly +grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid +# mean updated with historical inflation of cumulative 26.24% from: +# https://zenodo.org/records/13734730 + +interest_rate: 3 # interes rate in % diff --git a/examples/storage_pv_hourly_dynamic_price/main.py b/examples/storage_pv_hourly_dynamic_price/main.py index b26bc4c..8a28e13 100644 --- a/examples/storage_pv_hourly_dynamic_price/main.py +++ b/examples/storage_pv_hourly_dynamic_price/main.py @@ -1,6 +1,11 @@ +import logging + from peakshaving_analyzer import PeakShavingAnalyzer, load_yaml_config if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + config = load_yaml_config("./examples/storage_pv_hourly_dynamic_price/config.yml") psa = PeakShavingAnalyzer(config=config) results = psa.optimize() + results.print() diff --git a/examples/storage_quarterhourly_dynamic_price/config.yml b/examples/storage_quarterhourly_dynamic_price/config.yml index 706cea7..64969e5 100644 --- a/examples/storage_quarterhourly_dynamic_price/config.yml +++ b/examples/storage_quarterhourly_dynamic_price/config.yml @@ -3,10 +3,10 @@ ########################### name: storage_quarterhourly_dyn_price # name of the optimization -hours_per_timestep: 0.25 # in 15-minute timesteps +hours_per_timestep: 0.25 # quarter hour timesteps add_storage: True # wether to add storage to optimization or not -add_solar: False # wether to add pv system to optimization or not +allow_additional_pv: False # wether to add pv system to optimization or not auto_opt: False # Wether to automatically start optizmization or not solver: "gurobi" @@ -26,43 +26,15 @@ consumption_value_column: consumption # name of the column price_file_path: .price.csv # file path where prices are stored as .csv price_value_column: price # name of the column where prices are provided -postal_code: # postal code for automtic solar generation calculation, leave empty for own timeseries - # if not provided, solar generation will be calculated with the following parameters - # value column needs to contain values from 0 (no solar generation) to 1 (max solar generation) -price_file_path: # file path where solar generation is stored as .csv -price_value_column: # name of the column where solar generation is provided - - -####################### -# economic parameters # -####################### -overwrite_price_timeseries: False # Wether to overwrite price timeseries or not -producer_energy_price: 0.1665 # €/kWh # energy price if sourced from supplier -# value for 2024 taken from: -# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ -# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ - -grid_capacity_price: 101.22 # capacity price in euro to be paid yearly -grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid -# mean updated with historical inflation of cumulative 26.24% from: -# https://zenodo.org/records/13734730 - -pv_system_lifetime: 30 # pv system lifetime in years -# taken from: -# https://www.mdpi.com/1996-1073/14/14/4278 - -pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak -# taken from: -# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html - -inverter_lifetime: 15 # storage inverter lifetime in years -# taken from: -# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss - -inverter_cost_per_kw: 180 # storage inverter cost in euro per kw -# taken from: -# https://www.sciencedirect.com/science/article/pii/S1876610216310736 +postal_code: # postal code for automtic pv generation calculation, leave empty for own timeseries +# if not provided, pv generation will be calculated with the following parameters +# value column needs to contain values from 0 (no pv generation) to 1 (max pv generation) +new_pv_file_path: # file path where pv generation is stored as .csv +new_pv_value_column: # name of the column where pv generation is provided +################################ +# storage (battery) parameters # +################################ storage_lifetime: 15 # storage lifetime in years # taken from: # "Energiespeicher - Bedarf, Technologien, Integration", Stadler Sterner 2017 @@ -71,13 +43,7 @@ storage_cost_per_kwh: 145 # storage capacity cost in euro per kwh capacity # taken from: # https://www.pem.rwth-aachen.de/cms/pem/der-lehrstuhl/presse-medien/aktuelle-meldungen/~bexlow/battery-monitor-2023-nachfrage-waechst/ -interest_rate: 3 # interes rate in % - -######################## -# technical parameters # -######################## max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) -max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) storage_charge_efficiency: 0.9 # efficiency for charging storage storage_discharge_efficiency: 0.9 # efficiency for discharging storage @@ -90,8 +56,48 @@ storage_discharge_rate: 1 # # taken from: # https://www.sciencedirect.com/science/article/pii/S2590116819300116 +################################# +# storage (inverter) parameters # +################################# inverter_efficiency: 0.95 # efficiency of the storage inverter # taken from: # https://www.sciencedirect.com/science/article/pii/S1364032116306712 +inverter_lifetime: 15 # storage inverter lifetime in years +# taken from: +# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss + +inverter_cost_per_kw: 180 # storage inverter cost in euro per kw +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1876610216310736 + + +######################## +# PV system parameters # +######################## +pv_system_lifetime: 30 # pv system lifetime in years +# taken from: +# https://www.mdpi.com/1996-1073/14/14/4278 + +pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak +# taken from: +# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html + pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² +max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) + +####################### +# economic parameters # +####################### +overwrite_price_timeseries: False # Wether to overwrite price timeseries or not +producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier +# value for 2024 taken from: +# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ +# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ + +grid_capacity_price: 130 # capacity price in euro to be paid yearly +grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid +# mean updated with historical inflation of cumulative 26.24% from: +# https://zenodo.org/records/13734730 + +interest_rate: 3 # interes rate in % diff --git a/examples/storage_quarterhourly_dynamic_price/main.py b/examples/storage_quarterhourly_dynamic_price/main.py index 3674f4d..1977a53 100644 --- a/examples/storage_quarterhourly_dynamic_price/main.py +++ b/examples/storage_quarterhourly_dynamic_price/main.py @@ -1,6 +1,11 @@ +import logging + from peakshaving_analyzer import PeakShavingAnalyzer, load_yaml_config if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + config = load_yaml_config("./examples/storage_quarterhourly_dynamic_price/config.yml") psa = PeakShavingAnalyzer(config=config) results = psa.optimize() + results.print() diff --git a/peakshaving_analyzer/PSA.py b/peakshaving_analyzer/PSA.py index 56eba55..5405021 100644 --- a/peakshaving_analyzer/PSA.py +++ b/peakshaving_analyzer/PSA.py @@ -21,8 +21,8 @@ def __init__( self.price_timeseries = config.price_timeseries self.verbose = config.verbose - if self.config.add_solar: - self.solar_generation_timeseries = config.solar_generation_timeseries + if self.config.allow_additional_pv: + self.new_pv_generation_timeseries = config.new_pv_generation_timeseries if config.verbose: log.setLevel(level=logging.INFO) @@ -35,13 +35,17 @@ def __init__( self._add_sink() log.info("Built default ESM.") + if self.config.pv_system_already_exists: + self._add_existing_pv() + log.info("Added existing PV system.") + if self.config.add_storage: self.add_storage() log.info("Added storage.") - if self.config.add_solar: - self.add_solar() - log.info("Added solar.") + if self.config.allow_additional_pv: + self.add_additional_pv() + log.info("Added pv.") def _create_esm(self): self.esm = fn.EnergySystemModel( @@ -108,14 +112,26 @@ def _add_transmission(self): ) ) - def add_solar(self): + def _add_existing_pv(self): + self.esm.add( + fn.Source( + esM=self.esm, + name="Existing PV", + commodity="energy", + hasCapacityVariable=True, + operationRateMax=self.config.existing_pv_generation_timeseries, + capacityMax=self.config.existing_pv_size_kwp, + ) + ) + + def add_additional_pv(self): self.esm.add( fn.Source( esM=self.esm, - name="PV", + name="New PV", commodity="energy", hasCapacityVariable=True, - operationRateFix=self.config.solar_generation_timeseries, + operationRateMax=self.config.new_pv_generation_timeseries, capacityMax=self.config.max_pv_system_size_kwp, investPerCapacity=self.config.pv_system_cost_per_kwp, interestRate=self.config.interest_rate / 100, @@ -141,6 +157,10 @@ def add_storage(self): ) ) + if self.config.max_storage_size_kwh: + max_cap = pd.Series([self.config.max_storage_size_kwh, 0], index=["consumption_site", "grid"]) + else: + max_cap = None self.esm.add( fn.Storage( esM=self.esm, @@ -151,7 +171,7 @@ def add_storage(self): cyclicLifetime=self.config.storage_cyclic_lifetime, chargeEfficiency=self.config.storage_charge_efficiency, dischargeEfficiency=self.config.storage_discharge_efficiency, - capacityMax=self.config.max_storage_size_kwh, + capacityMax=max_cap, economicLifetime=self.config.storage_lifetime, technicalLifetime=self.config.storage_lifetime, chargeRate=self.config.storage_charge_rate, diff --git a/peakshaving_analyzer/__init__.py b/peakshaving_analyzer/__init__.py index d849d94..0bf3b61 100644 --- a/peakshaving_analyzer/__init__.py +++ b/peakshaving_analyzer/__init__.py @@ -8,4 +8,4 @@ """ __all__ = ["PeakShavingAnalyzer", "Config", "Results", "load_yaml_config", "load_oeds_config"] -__version__ = "0.1.7" +__version__ = "0.1.8" diff --git a/peakshaving_analyzer/config.yml b/peakshaving_analyzer/config.yml index e32a184..c016a14 100644 --- a/peakshaving_analyzer/config.yml +++ b/peakshaving_analyzer/config.yml @@ -3,19 +3,19 @@ ########################### name: example_optimization # name of the optimization -hours_per_timestep: 0.25 # in 15-minute timesteps +hours_per_timestep: 0.25 # quarter hour timesteps add_storage: True # wether to add storage to optimization or not -add_solar: False # wether to add pv system to optimization or not +allow_additional_pv: False # wether to add pv system to optimization or not auto_opt: False # Wether to automatically start optizmization or not solver: "gurobi" verbose: True # Wether to print optimization progress or not -############## -# timeseries # -############## +#################################### +# price and consumption timeseries # +#################################### # name of the column where timestamps are provided (should be equal in all files) # can be set to None if timestamps are not provided, we will then assume timestamps based on pattern timestamp_column: @@ -26,11 +26,82 @@ consumption_value_column: consumption # name of the column where consumption price_file_path: # file path where prices are stored as .csv price_value_column: # name of the column where prices are provided -postal_code: 52066 # postal code for automtic solar generation calculation, leave empty for own timeseries -# if not provided, solar generation will be calculated with the following parameters -# value column needs to contain values from 0 (no solar generation) to 1 (max solar generation) -solar_file_path: # file path where solar generation is stored as .csv -solar_value_column: # name of the column where solar generation is provided +################################ +# storage (battery) parameters # +################################ +storage_lifetime: 15 # storage lifetime in years +# taken from: +# "Energiespeicher - Bedarf, Technologien, Integration", Stadler Sterner 2017 + +storage_cost_per_kwh: 145 # storage capacity cost in euro per kwh capacity +# taken from: +# https://www.pem.rwth-aachen.de/cms/pem/der-lehrstuhl/presse-medien/aktuelle-meldungen/~bexlow/battery-monitor-2023-nachfrage-waechst/ + +max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) + +storage_charge_efficiency: 0.9 # efficiency for charging storage +storage_discharge_efficiency: 0.9 # efficiency for discharging storage +# for round trip efficiency of 0.81 +# taken from: +# https://www.sciencedirect.com/science/article/pii/S2352152X23027846 + +storage_charge_rate: 1 # +storage_discharge_rate: 1 # +# taken from: +# https://www.sciencedirect.com/science/article/pii/S2590116819300116 + +################################# +# storage (inverter) parameters # +################################# +inverter_efficiency: 0.95 # efficiency of the storage inverter +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1364032116306712 + +inverter_lifetime: 15 # storage inverter lifetime in years +# taken from: +# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss + +inverter_cost_per_kw: 180 # storage inverter cost in euro per kw +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1876610216310736 + +################################# +# Existing PV system parameters # +################################# +pv_system_already_exists: False # Wether there is an existing PV system or not. If set to 'False', all following parameters will be ignored + +# generation timeseries for existing PV system. +# If you don't have that, see below +existing_pv_file_path: # File path, where PV generation from existing system is stored as .csv. Values should be in kW +existing_pv_column: # Name of the column where existing pv generation is provided. + +# If you don't have a timeseries for your current systems generation, you can provide the system size (in kWp) and your postal code, +# with which we will calculate the generation for you. +existing_pv_size_kwp: # only set this, if you don't have a timeseries for the existing pv system generation +postal_code: # postal code to get generation curve for + +############################ +# New PV system parameters # +############################ +# these values will only be used, if parameter 'allow_additional_pv' is set to true +pv_system_lifetime: 30 # pv system lifetime in years +# taken from: +# https://www.mdpi.com/1996-1073/14/14/4278 + +pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak +# taken from: +# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html + +pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² +max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) + +# in the following, the generation for a new PV system will be defined. +# If there is an existing PV system AND a generation timeseries for this system, we will use the generation curve of the existing system, +# if you choose to not provide a timeseries in the following parameters +# value column needs to contain values from 0 (no pv generation) to 1 (max pv generation) +new_pv_file_path: # file path where pv generation is stored as .csv +new_pv_value_column: # name of the column where pv generation is provided + ####################### # economic parameters # @@ -46,41 +117,8 @@ grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh # mean updated with historical inflation of cumulative 26.24% from: # https://zenodo.org/records/13734730 -pv_system_lifetime: 30 # pv system lifetime in years -# taken from: https://www.mdpi.com/1996-1073/14/14/4278 - -pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak -# taken from: https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html - -inverter_lifetime: 15 # storage inverter lifetime in years -# taken from: 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss - -inverter_cost_per_kw: 180 # storage inverter cost in euro per kw -# taken from: https://www.sciencedirect.com/science/article/pii/S1876610216310736 - storage_lifetime: 15 # storage lifetime in years # taken from: https://link.springer.com/book/10.1007/978-3-662-48893-5 storage_cost_per_kwh: 285 # storage capacity cost in euro per kwh capacity # taken from: https://docs.nrel.gov/docs/fy25osti/93281.pdf - -interest_rate: 2 # interes rate in % - -######################## -# technical parameters # -######################## -max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) -max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) - -storage_charge_efficiency: 0.95 # efficiency for charging storage -storage_discharge_efficiency: 0.95 # efficiency for discharging storage -# for round trip efficiency of 0.9 https://www.sciencedirect.com/science/article/pii/S2352152X23027846 - -storage_charge_rate: 5 # -storage_discharge_rate: 5 # -# is limited by inverter capacity, can be adjusted nontheless - -inverter_efficiency: 0.95 # efficiency of the storage inverter -# taken from: https://www.sciencedirect.com/science/article/pii/S1364032116306712 - -pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² diff --git a/peakshaving_analyzer/input.py b/peakshaving_analyzer/input.py index db6bc53..3e62d36 100644 --- a/peakshaving_analyzer/input.py +++ b/peakshaving_analyzer/input.py @@ -22,40 +22,49 @@ class Config(IOHandler): name: str overwrite_existing_optimization: bool = False add_storage: bool = True - add_solar: bool = False + allow_additional_pv: bool = False auto_opt: bool = False solver: str = "appsi_highs" verbose: bool = False postal_code: int | str | None = None - # timeseries + # general timeseries consumption_timeseries: pd.Series | None = None price_timeseries: pd.Series | None = None - solar_generation_timeseries: pd.Series | None = None - # economic parameters - overwrite_price_timeseries: bool = False - producer_energy_price: float = 0.1665 - grid_capacity_price: float = 101.22 - grid_energy_price: float = 0.046 - pv_system_lifetime: int = 30 - pv_system_cost_per_kwp: float = 1200.0 - inverter_lifetime: int = 15 - inverter_cost_per_kw: float = 180 + # storage system (battery) parameters storage_lifetime: int = 15 storage_cost_per_kwh: float = 285 - interest_rate: float = 2 - - # technical parameters - max_storage_size_kwh: float | None = None storage_charge_efficiency: float = 0.95 storage_discharge_efficiency: float = 0.95 storage_charge_rate: float = 5 storage_cyclic_lifetime: float = 10000 storage_discharge_rate: float = 5 + max_storage_size_kwh: float | None = None + + # storage system (inverter) parameters inverter_efficiency: float = 0.95 + inverter_cost_per_kw: float = 180 + inverter_lifetime: int = 15 + + # Existing PV system parameters + pv_system_already_exists: bool = False + existing_pv_size_kwp: float | None = None + existing_pv_generation_timeseries: pd.Series | None = None + + # New PV system parameters + pv_system_lifetime: int = 30 + pv_system_cost_per_kwp: float = 1200.0 max_pv_system_size_kwp: float | None = None pv_system_kwp_per_m2: float = 0.4 + new_pv_generation_timeseries: pd.Series | None = None + + # economic parameters + overwrite_price_timeseries: bool = False + producer_energy_price: float = 0.1665 + grid_capacity_price: float = 101.22 + grid_energy_price: float = 0.046 + interest_rate: float = 2 # metadata needed for optimization (set by peakshaving analyzer) timestamps: pd.DatetimeIndex | None = None @@ -67,12 +76,12 @@ def timeseries_to_df(self): df["consumption_kw"] = self.consumption_timeseries df["energy_price_eur"] = self.price_timeseries["grid"] - df["solar_generation_kw"] = self.solar_generation_timeseries["consumption_site"] + df["new_pv_generation_kw"] = self.new_pv_generation_timeseries["consumption_site"] return df -def load_yaml_config(config_file_path: Path | str) -> Config: +def load_yaml_config(config_file_path: Path | str, test_mode: bool = False) -> Config: config_path = Path(config_file_path) # read in configuration file @@ -110,22 +119,12 @@ def load_yaml_config(config_file_path: Path | str) -> Config: _read_or_create_price_timeseries(data) log.info("Price timeseries loaded or created") - if data["add_solar"]: - if data["solar_file_path"]: - data["solar_generation_timeseries"] = pd.read_csv(data["config_dir"] / data["solar_file_path"])[ - data.get("solar_value_column", "value") - ] - log.info("Solar generation timeseries loaded") - elif data["postal_code"]: - _fetch_solar_timeseries(data) - log.info("Solar generation timeseries retrieved from brightsky") - else: - msg = "No solar generation timeseries available." - msg += " Setting add_solar to False." - log.warning(msg) - data["add_solar"] = False + # check existing timeseries + _load_pv_timeseries(data) + log.info("PV generation timeseries loaded or created") - _check_timeseries_length(data) + if not test_mode: + _check_timeseries_length(data) _remove_unused_keys(data) @@ -200,8 +199,8 @@ def load_oeds_config( # read or create price timeseries _read_or_create_price_timeseries(data) - # retrieve solar generation timeseries - _fetch_solar_timeseries(data) + # retrieve pv generation timeseries + _load_pv_timeseries(data) # calculate if consumption is over 2500h full load hours is_over_2500h = (data["consumption_timeseries"].sum() / 4) / data["consumption_timeseries"].max() > 2500 @@ -373,34 +372,72 @@ def _read_price_timeseries(data): return df[["consumption_site", "grid"]] -def _fetch_solar_timeseries(data): +def _load_pv_timeseries(data): """ - Read the solar timeseries from brightsky. + Read the pv timeseries from brightsky. Returns: - pd.Series: The solar timeseries. + pd.Series: The pv timeseries. """ - # if we want to add solar / PV... - if data.get("add_solar"): - # and we have a given timeseries for generation - if data.get("solar_generation_timeseries"): - # we dont need to do anything - pass + # existing PV systems + if data.get("pv_system_already_exists"): + # load from CSV if provided + if data.get("existing_pv_file_path"): + pv_gen = pd.read_csv(data["config_dir"] / data["existing_pv_file_path"])[ + data.get("existing_pv_value_column", "value") + ] + pv_gen.rename("consumption_site", inplace=True) + data["existing_pv_size_kwp"] = pv_gen.max() # set existing system size + pv_gen = pv_gen / pv_gen.max() # scale to values from 0 to 1 + log.info("existing pv generation timeseries loaded") + + # load by weather data combined with system size + elif data.get("postal_code") and data.get("existing_pv_size_kwp"): + pv_gen = _fetch_pv_from_brighsky(data) - # if we have a given postal code + # fail and raise warning + else: + msg = "No PV generation timeseries for existing system available." + msg += " Setting pv_system_already_exists to False." + pv_gen = None + log.warning(msg) + data["pv_system_already_exists"] = False + + if pv_gen is not None: + existing_pv_gen_df = _pv_dataframe_from_series(pv_gen) + data["existing_pv_generation_timeseries"] = existing_pv_gen_df + + # if we want to add pv / PV... + if data.get("allow_additional_pv"): + # load from csv if provided + if data.get("new_pv_file_path"): + pv_gen = pd.read_csv(data["config_dir"] / data["new_pv_file_path"])[ + data.get("new_pv_value_column", "value") + ] + pv_gen.rename("consumption_site", inplace=True) + log.info("existing pv generation timeseries loaded") + + # use existing curve if given + elif data.get("existing_pv_generation_timeseries") is not None: + pv_gen = data["existing_pv_generation_timeseries"]["consumption_site"].copy() + + # use postal code if given elif data.get("postal_code"): # fetch the generation timeseries for this from brightsky - data["solar_generation_timeseries"] = _fetch_solar_from_brighsky(data) + pv_gen = _fetch_pv_from_brighsky(data) - # if we dont have a given postal code + # retrieve default for germany if nothing is provided elif not data.get("postal_code"): - # get default solar generation timeseries for germany - data["solar_generation_timeseries"] = _fetch_solar_default(data) + # get default pv generation timeseries for germany + pv_gen = _fetch_pv_default(data) + + new_pv_gen_df = _pv_dataframe_from_series(pv_gen) + data["new_pv_generation_timeseries"] = new_pv_gen_df -def _fetch_solar_from_brighsky(data): - log.info("Fetching solar timeseries from BrightSky API.") +def _fetch_pv_from_brighsky(data) -> pd.Series: + log.info("Fetching pv timeseries from BrightSky API.") # convert postal code to coordinates nomi = pgeocode.Nominatim("de") q = nomi.query_postal_code(data["postal_code"]) @@ -416,13 +453,10 @@ def _fetch_solar_from_brighsky(data): # put data in dataframe df = pd.DataFrame(weather_data["weather"])[["solar"]] - log.info("Solar timeseries data fetched successfully.") + log.info("PV timeseries data fetched successfully.") # rename to location in ESM, add grid column with no operation possible df.rename(columns={"solar": "consumption_site"}, inplace=True) - df["grid"] = 0 - - df.fillna(0, inplace=True) # resample to match hours per timestep if data["hours_per_timestep"] != 1: @@ -435,12 +469,12 @@ def _fetch_solar_from_brighsky(data): # convert from kWh/m2 to kW # kWh/m2/h = kW/m2 = 1000W/m2 - # no converseion necessary, as solar modules are tested with 1000W/m2 + # no conversion necessary, as pv modules are tested with 1000W/m2 - return df + return df["consumption_site"] -def _fetch_solar_default(data): +def _fetch_pv_default(data) -> pd.Series: url = "https://www.renewables.ninja/country_downloads/DE/ninja-pv-country-DE-national-merra2.csv" response = requests.get(url) df = pd.read_csv(BytesIO(response.content), delimiter=",", header=3) @@ -463,7 +497,7 @@ def _fetch_solar_default(data): n_timesteps=data["n_timesteps"], ) - return df + return df["consumption_site"] def _resample_dataframe(df: pd.DataFrame, hours_per_timestep: float, assumed_year: int, n_timesteps: int): @@ -476,7 +510,7 @@ def _resample_dataframe(df: pd.DataFrame, hours_per_timestep: float, assumed_yea pd.DataFrame: the resampled dataframe. """ - log.info("Resampling solar timeseries to match your specifications") + log.info("Resampling pv timeseries to match your specifications") df["timestamp"] = pd.date_range(start=f"{assumed_year}-01-01", periods=len(df), freq="H") @@ -509,7 +543,17 @@ def _resample_dataframe(df: pd.DataFrame, hours_per_timestep: float, assumed_yea df.reset_index(drop=True, inplace=True) - log.info("Successfully resampled solar timeseries.") + log.info("Successfully resampled pv timeseries.") + + return df + + +def _pv_dataframe_from_series(s: pd.Series) -> pd.DataFrame: + df = pd.DataFrame(columns=["consumption_site", "grid"]) + df["consumption_site"] = s.copy() + df["grid"] = 0 + + df.fillna(0, inplace=True) return df @@ -530,9 +574,9 @@ def _check_timeseries_length(data): msg = "Length of price timeseries does not match expected number of timesteps. " msg += f"Expected number of timesteps: {data['n_timesteps']}, given timesteps: {len(data['price_timeseries'])}" raise ValueError(msg) - if "solar_generation_timeseries" in data and len(data["solar_generation_timeseries"]) != data["n_timesteps"]: - msg = "Length of solar timeseries does not match expected number of timesteps. " - msg += f"Expected number of timesteps: {data['n_timesteps']}, given timesteps: {len(data['solar_generation_timeseries'])}" + if "new_pv_generation_timeseries" in data and len(data["new_pv_generation_timeseries"]) != data["n_timesteps"]: + msg = "Length of pv timeseries does not match expected number of timesteps. " + msg += f"Expected number of timesteps: {data['n_timesteps']}, given timesteps: {len(data['new_pv_generation_timeseries'])}" raise ValueError(msg) log.info("Timeseries length check passed.") @@ -548,8 +592,10 @@ def _remove_unused_keys(data): "consumption_value_column", "price_file_path", "price_value_column", - "solar_file_path", - "solar_value_column", + "existing_pv_file_path", + "existing_pv_value_column", + "new_pv_file_path", + "new_pv_value_column", "leap_year", "assumed_year", "config_dir", diff --git a/peakshaving_analyzer/output.py b/peakshaving_analyzer/output.py index c2e3b03..ac6a774 100644 --- a/peakshaving_analyzer/output.py +++ b/peakshaving_analyzer/output.py @@ -22,7 +22,8 @@ class Results(IOHandler): storage_charge_kw: pd.Series | None = None storage_discharge_kw: pd.Series | None = None storage_soc_kwh: pd.Series | None = None - solar_generation_kw: pd.Series | None = None + existing_pv_generation_kw: pd.Series | None = None + new_pv_generation_kw: pd.Series | None = None consumption_kw: pd.Series | None = None energy_price_eur: pd.Series | None = None @@ -42,10 +43,10 @@ class Results(IOHandler): inverter_annuity_eur: float | None = None inverter_capacity_kw: float | None = None - # solar system costs - solar_invest_eur: float | None = None - solar_annuity_eur: float | None = None - solar_capacity_kwp: float | None = None + # pv system costs + new_pv_invest_eur: float | None = None + new_pv_annuity_eur: float | None = None + new_pv_capacity_kwp: float | None = None # total costs total_yearly_costs_eur: float | None = None @@ -64,7 +65,8 @@ def timeseries_to_df(self): df["storage_charge_kw"] = self.storage_charge_kw df["storage_discharge_kw"] = self.storage_discharge_kw df["storage_soc_kwh"] = self.storage_soc_kwh - df["solar_generation_kw"] = self.solar_generation_kw + df["existing_pv_generation_kw"] = self.existing_pv_generation_kw + df["new_pv_generation_kw"] = self.new_pv_generation_kw df["consumption_kw"] = self.consumption_kw df["energy_price_eur"] = self.energy_price_eur @@ -99,7 +101,13 @@ def plot_storage_timeseries(self): self._plot(cols_to_plot=storage_columns) def plot_consumption_timeseries(self): - consumption_columns = ["grid_usage_kw", "storage_discharge_kw", "solar_generation_kw", "consumption_kw"] + consumption_columns = [ + "grid_usage_kw", + "storage_discharge_kw", + "existing_pv_generation_kw", + "new_pv_generation_kw", + "consumption_kw", + ] self._plot(cols_to_plot=consumption_columns) @@ -200,19 +208,33 @@ def _retrieve_timeseries(data: dict[str], esm: fn.EnergySystemModel, config: Con data["storage_discharge_kw"] = pd.Series(0, index=list(range(config.n_timesteps))) data["storage_soc_kwh"] = pd.Series(0, index=list(range(config.n_timesteps))) - if config.add_solar: - data["solar_generation_kw"] = ( + if config.pv_system_already_exists: + data["existing_pv_generation_kw"] = ( _get_optimum_ts( esm=esm, model_name="SourceSinkModel", variable="operationVariablesOptimum", - index=("PV", "consumption_site"), + index=("Existing PV", "consumption_site"), ) / config.hours_per_timestep ) else: - data["solar_generation_kw"] = pd.Series(0, index=list(range(config.n_timesteps))) + data["existing_pv_generation_kw"] = pd.Series(0, index=list(range(config.n_timesteps))) + + if config.allow_additional_pv: + data["new_pv_generation_kw"] = ( + _get_optimum_ts( + esm=esm, + model_name="SourceSinkModel", + variable="operationVariablesOptimum", + index=("New PV", "consumption_site"), + ) + / config.hours_per_timestep + ) + + else: + data["new_pv_generation_kw"] = pd.Series(0, index=list(range(config.n_timesteps))) data["consumption_kw"] = config.consumption_timeseries data["energy_price_eur"] = config.price_timeseries["grid"] @@ -240,10 +262,10 @@ def _retrieve_system_sizes(data: dict, esm: fn.EnergySystemModel) -> None: location="consumption_site", ) - data["solar_capacity_kwp"] = _get_val_from_summary( + data["new_pv_capacity_kwp"] = _get_val_from_summary( esm=esm, model_name="SourceSinkModel", - index=("PV", "capacity", "[kWh]"), + index=("New PV", "capacity", "[kWh]"), location="consumption_site", ) @@ -302,17 +324,17 @@ def _retrieve_system_costs(data: dict[str], esm: fn.EnergySystemModel) -> None: location="consumption_site", ) - # solar data - data["solar_invest_eur"] = _get_val_from_summary( + # pv data + data["new_pv_invest_eur"] = _get_val_from_summary( esm=esm, model_name="SourceSinkModel", - index=("PV", "invest", "[Euro]"), + index=("New PV", "invest", "[Euro]"), location="consumption_site", ) - data["solar_annuity_eur"] = _get_val_from_summary( + data["new_pv_annuity_eur"] = _get_val_from_summary( esm=esm, model_name="SourceSinkModel", - index=("PV", "TAC", "[Euro/a]"), + index=("New PV", "TAC", "[Euro/a]"), location="consumption_site", ) @@ -323,7 +345,7 @@ def _retrieve_system_costs(data: dict[str], esm: fn.EnergySystemModel) -> None: + data["grid_capacity_costs_eur"] + data["storage_annuity_eur"] + data["inverter_annuity_eur"] - + data["solar_annuity_eur"] + + data["new_pv_annuity_eur"] ) - data["total_annuity_eur"] = data["storage_annuity_eur"] + data["inverter_annuity_eur"] + data["solar_annuity_eur"] - data["total_invest_eur"] = data["storage_invest_eur"] + data["inverter_invest_eur"] + data["solar_invest_eur"] + data["total_annuity_eur"] = data["storage_annuity_eur"] + data["inverter_annuity_eur"] + data["new_pv_annuity_eur"] + data["total_invest_eur"] = data["storage_invest_eur"] + data["inverter_invest_eur"] + data["new_pv_invest_eur"] diff --git a/pyproject.toml b/pyproject.toml index 9d53435..e44ab28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "peakshaving-analyzer" -version = "0.1.7" +version = "0.1.8" description = "Peak shaving analysis for industrial load profiles" authors = [ { name = "Christoph Komanns", email = "c.komanns@gmail.com" }, @@ -53,7 +53,7 @@ dev = [ psa = "psa_cli.main:cli" [project.urls] -Issues = "https://github.com/assume-framework/assume/issues" +Issues = "https://github.com/NOWUM/peakshaving_analyzer/issues" Repository = "https://github.com/NOWUM/peakshaving-analyzer" [tool.setuptools.packages.find] diff --git a/tests/input/data/consumption.csv b/tests/input/data/consumption.csv new file mode 100644 index 0000000..0c9cf92 --- /dev/null +++ b/tests/input/data/consumption.csv @@ -0,0 +1,4 @@ +consumption +1 +1 +1 diff --git a/tests/input/data/existing_solar_gen.csv b/tests/input/data/existing_solar_gen.csv new file mode 100644 index 0000000..a2b00b2 --- /dev/null +++ b/tests/input/data/existing_solar_gen.csv @@ -0,0 +1,4 @@ +solar_gen +1 +4 +1 diff --git a/tests/input/data/price.csv b/tests/input/data/price.csv new file mode 100644 index 0000000..13184c9 --- /dev/null +++ b/tests/input/data/price.csv @@ -0,0 +1,4 @@ +price +5 +5 +5 diff --git a/tests/input/test_brightsky_config.yml b/tests/input/test_brightsky_config.yml new file mode 100644 index 0000000..36141dc --- /dev/null +++ b/tests/input/test_brightsky_config.yml @@ -0,0 +1,112 @@ +########################### +# test relevant parameter # +########################### +allow_additional_pv: True # wether to add pv system to optimization or not + +pv_system_already_exists: True +existing_pv_file_path: # File path, where PV generation from existing system is stored as .csv. Values should be in kW +existing_pv_value_column: # Name of the column where existing pv generation is provided. + +existing_pv_size_kwp: 4 # only set this, if you don't have a timeseries for the existing pv system generation +postal_code: 52066 # postal code to get generation curve for + +########################### +# optimization_parameters # +########################### + +name: storage_hourly_fixed_price # name of the optimization + +hours_per_timestep: 1 # 1 hour timesteps + +add_storage: True # wether to add storage to optimization or not + +auto_opt: False # Wether to automatically start optizmization or not +solver: "gurobi" + +verbose: True # Wether to print optimization progress or not + +############## +# timeseries # +############## +# name of the column where timestamps are provided (should be equal in all files) +# can be set to None if timestamps are not provided, we will then assume timestamps based on pattern +timestamp_column: + +consumption_file_path: ./data/consumption.csv # file path where consumption is stored as .csv +consumption_value_column: consumption # name of the column where consumption is provided + +price_file_path: # file path where prices are stored as .csv +price_value_column: # name of the column where prices are provided + +# value column needs to contain values from 0 (no pv generation) to 1 (max pv generation) +new_pv_file_path: # file path where pv generation is stored as .csv +new_pv_value_column: # name of the column where pv generation is provided + +################################ +# storage (battery) parameters # +################################ +storage_lifetime: 15 # storage lifetime in years +# taken from: +# "Energiespeicher - Bedarf, Technologien, Integration", Stadler Sterner 2017 + +storage_cost_per_kwh: 145 # storage capacity cost in euro per kwh capacity +# taken from: +# https://www.pem.rwth-aachen.de/cms/pem/der-lehrstuhl/presse-medien/aktuelle-meldungen/~bexlow/battery-monitor-2023-nachfrage-waechst/ + +max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) + +storage_charge_efficiency: 0.9 # efficiency for charging storage +storage_discharge_efficiency: 0.9 # efficiency for discharging storage +# for round trip efficiency of 0.81 +# taken from: +# https://www.sciencedirect.com/science/article/pii/S2352152X23027846 + +storage_charge_rate: 1 # +storage_discharge_rate: 1 # +# taken from: +# https://www.sciencedirect.com/science/article/pii/S2590116819300116 + +################################# +# storage (inverter) parameters # +################################# +inverter_efficiency: 0.95 # efficiency of the storage inverter +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1364032116306712 + +inverter_lifetime: 15 # storage inverter lifetime in years +# taken from: +# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss + +inverter_cost_per_kw: 180 # storage inverter cost in euro per kw +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1876610216310736 + +######################## +# PV system parameters # +######################## +pv_system_lifetime: 30 # pv system lifetime in years +# taken from: +# https://www.mdpi.com/1996-1073/14/14/4278 + +pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak +# taken from: +# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html + +pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² +max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) + +####################### +# economic parameters # +####################### +overwrite_price_timeseries: True # Wether to overwrite price timeseries or not +producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier +# value for 2024 taken from: +# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ +# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ + +grid_capacity_price: 130 # capacity price in euro to be paid yearly +grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid +# mean updated with historical inflation of cumulative 26.24% from: +# https://zenodo.org/records/13734730 + +interest_rate: 3 # interes rate in % diff --git a/tests/input/test_existing_pv_config.yml b/tests/input/test_existing_pv_config.yml new file mode 100644 index 0000000..d97a637 --- /dev/null +++ b/tests/input/test_existing_pv_config.yml @@ -0,0 +1,114 @@ +########################### +# test relevant parameter # +########################### +allow_additional_pv: True # wether to add pv system to optimization or not + +pv_system_already_exists: True +existing_pv_file_path: ./data/existing_solar_gen.csv # File path, where PV generation from existing system is stored as .csv. Values should be in kW +existing_pv_value_column: solar_gen # Name of the column where existing pv generation is provided. + +existing_pv_size_kwp: # only set this, if you don't have a timeseries for the existing pv system generation +postal_code: # postal code to get generation curve for + +########################### +# optimization_parameters # +########################### + +name: storage_hourly_fixed_price # name of the optimization + +hours_per_timestep: 1 # 1 hour timesteps + +add_storage: True # wether to add storage to optimization or not + +auto_opt: False # Wether to automatically start optizmization or not +solver: "gurobi" + +verbose: True # Wether to print optimization progress or not + +############## +# timeseries # +############## +# name of the column where timestamps are provided (should be equal in all files) +# can be set to None if timestamps are not provided, we will then assume timestamps based on pattern +timestamp_column: + +consumption_file_path: ./data/consumption.csv # file path where consumption is stored as .csv +consumption_value_column: consumption # name of the column where consumption is provided + +price_file_path: # file path where prices are stored as .csv +price_value_column: # name of the column where prices are provided + +postal_code: # postal code for automtic pv generation calculation, leave empty for own timeseries +# if not provided, pv generation will be calculated with the following parameters +# value column needs to contain values from 0 (no pv generation) to 1 (max pv generation) +new_pv_file_path: # file path where pv generation is stored as .csv +new_pv_value_column: # name of the column where pv generation is provided + +################################ +# storage (battery) parameters # +################################ +storage_lifetime: 15 # storage lifetime in years +# taken from: +# "Energiespeicher - Bedarf, Technologien, Integration", Stadler Sterner 2017 + +storage_cost_per_kwh: 145 # storage capacity cost in euro per kwh capacity +# taken from: +# https://www.pem.rwth-aachen.de/cms/pem/der-lehrstuhl/presse-medien/aktuelle-meldungen/~bexlow/battery-monitor-2023-nachfrage-waechst/ + +max_storage_size_kwh: # maximum available storage size in kWh (leave empty for infinite size) + +storage_charge_efficiency: 0.9 # efficiency for charging storage +storage_discharge_efficiency: 0.9 # efficiency for discharging storage +# for round trip efficiency of 0.81 +# taken from: +# https://www.sciencedirect.com/science/article/pii/S2352152X23027846 + +storage_charge_rate: 1 # +storage_discharge_rate: 1 # +# taken from: +# https://www.sciencedirect.com/science/article/pii/S2590116819300116 + +################################# +# storage (inverter) parameters # +################################# +inverter_efficiency: 0.95 # efficiency of the storage inverter +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1364032116306712 + +inverter_lifetime: 15 # storage inverter lifetime in years +# taken from: +# 10.4229/WCPEC-82022-3DV.1.46 Bucher Joss + +inverter_cost_per_kw: 180 # storage inverter cost in euro per kw +# taken from: +# https://www.sciencedirect.com/science/article/pii/S1876610216310736 + +######################## +# PV system parameters # +######################## +pv_system_lifetime: 30 # pv system lifetime in years +# taken from: +# https://www.mdpi.com/1996-1073/14/14/4278 + +pv_system_cost_per_kwp: 1250 # pv system cost in euro per kWpeak +# taken from: +# https://www.ise.fraunhofer.de/de/veroeffentlichungen/studien/studie-stromgestehungskosten-erneuerbare-energien.html + +pv_system_kwp_per_m2: 0.4 # energy obtainable per area in kWpeak per m² +max_pv_system_size_kwp: # maximum available pv system size in kWpeak (leave empty for infinite size) + +####################### +# economic parameters # +####################### +overwrite_price_timeseries: True # Wether to overwrite price timeseries or not +producer_energy_price: 0.15 # €/kWh # energy price if sourced from supplier +# value for 2024 taken from: +# https://www.bdew.de/service/daten-und-grafiken/bdew-strompreisanalyse/ +# https://de.statista.com/statistik/daten/studie/252029/umfrage/industriestrompreise-inkl-stromsteuer-in-deutschland/ + +grid_capacity_price: 130 # capacity price in euro to be paid yearly +grid_energy_price: 0.0460 # energy price in euro per kwh to be paid per kwh source from grid +# mean updated with historical inflation of cumulative 26.24% from: +# https://zenodo.org/records/13734730 + +interest_rate: 3 # interes rate in % diff --git a/tests/input/test_input.py b/tests/input/test_input.py new file mode 100644 index 0000000..b59a760 --- /dev/null +++ b/tests/input/test_input.py @@ -0,0 +1,18 @@ +import pandas as pd + +from peakshaving_analyzer.input import load_yaml_config + + +def test_existing_pv(): + conf = load_yaml_config("./tests/input/test_existing_pv_config.yml", test_mode=True) + + # original values are [1, 4, 1], those should get scaled to [0.25, 1, 0.25] + assert (conf.existing_pv_generation_timeseries["consumption_site"] == pd.Series([0.25, 1, 0.25])).all() + assert (conf.new_pv_generation_timeseries["consumption_site"] == pd.Series([0.25, 1, 0.25])).all() + + +def test_brightsky(): + conf = load_yaml_config("./tests/input/test_brightsky_config.yml", test_mode=True) + + # brightsky should fetch for whole year + assert len(conf.existing_pv_generation_timeseries) == 8760 diff --git a/tests/test_psa.py b/tests/test_psa.py index 0a52727..f31198d 100644 --- a/tests/test_psa.py +++ b/tests/test_psa.py @@ -57,31 +57,31 @@ def test_various_steps(n_timesteps): ) -SOLAR_PROFILE_HOURLY = [0] * 6 + [0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 0.8, 0.6, 0.4, 0.2] + [0] * 6 +PV_PROFILE_HOURLY = [0] * 6 + [0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 0.8, 0.6, 0.4, 0.2] + [0] * 6 -def test_add_solar(): +def test_allow_additional_pv(): n_timesteps = 48 - solar_profile = SOLAR_PROFILE_HOURLY * (1 + 50 // len(SOLAR_PROFILE_HOURLY)) - solar_profile = solar_profile[0:n_timesteps] + pv_profile = PV_PROFILE_HOURLY * (1 + 50 // len(PV_PROFILE_HOURLY)) + pv_profile = pv_profile[0:n_timesteps] config = Config( "test_config", consumption_timeseries=[1] * n_timesteps, hours_per_timestep=1, n_timesteps=n_timesteps, price_timeseries=pd.DataFrame({"grid": [0.3] * n_timesteps, "consumption_site": [0] * n_timesteps}), - add_solar=True, - solar_generation_timeseries=pd.DataFrame({"grid": 0, "consumption_site": solar_profile}), + allow_additional_pv=True, + new_pv_generation_timeseries=pd.DataFrame({"grid": 0, "consumption_site": pv_profile}), interest_rate=0, ) psa = PeakShavingAnalyzer(config=config) results = psa.optimize() # check that an inverter and storage is available - assert results.inverter_capacity_kw >= 5 + assert results.inverter_capacity_kw >= 2 assert results.storage_capacity_kwh >= 10 assert results.grid_capacity_kw == 1 - assert results.solar_capacity_kwp >= 3 + assert results.new_pv_capacity_kwp >= 3 # energy costs are now much lower assert results.energy_costs_eur < 400 @@ -90,7 +90,7 @@ def test_add_solar(): # sum of investment should match assert ( - results.total_invest_eur == results.solar_invest_eur + results.inverter_invest_eur + results.storage_invest_eur + results.total_invest_eur == results.new_pv_invest_eur + results.inverter_invest_eur + results.storage_invest_eur ) # annuities should match @@ -99,7 +99,7 @@ def test_add_solar(): == results.energy_costs_eur + results.grid_energy_costs_eur + results.grid_capacity_costs_eur - + results.solar_annuity_eur + + results.new_pv_annuity_eur + results.inverter_annuity_eur + results.storage_annuity_eur ) @@ -133,3 +133,22 @@ def test_storage_only(): assert results.inverter_capacity_kw == 1 results.storage_capacity_kwh assert results.storage_capacity_kwh == 1 + + +def test_existing_pv(): + config = Config( + "test_config", + consumption_timeseries=[1, 2, 1], + hours_per_timestep=1, + n_timesteps=3, + price_timeseries=pd.DataFrame({"grid": [10, 10, 10], "consumption_site": [0, 0, 0]}), + pv_system_already_exists=True, + existing_pv_size_kwp=1, + existing_pv_generation_timeseries=pd.DataFrame({"consumption_site": [1, 1, 1], "grid": [0, 0, 0]}), + allow_additional_pv=False, + ) + psa = PeakShavingAnalyzer(config=config) + results = psa.optimize() + ts = results.timeseries_to_df() + + assert (ts["existing_pv_generation_kw"] == [1, 1, 1]).all()