From f888f3da315262da28fdb5b4299abd09ef5a8996 Mon Sep 17 00:00:00 2001 From: smartie2076 Date: Tue, 4 May 2021 15:26:43 +0200 Subject: [PATCH 1/4] Bugfix: present_value_of_changing_fuel_price incorrectly calculated - Fixed function call in `D0` - Added pytests for `D1.crf` and `D1.present_value_of_changing_fuel_price` --- src/D0_process_input.py | 14 ++++---- tests/test_D1_economic_functions.py | 53 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 tests/test_D1_economic_functions.py diff --git a/src/D0_process_input.py b/src/D0_process_input.py index 6e9f36e..472f654 100644 --- a/src/D0_process_input.py +++ b/src/D0_process_input.py @@ -150,14 +150,12 @@ def economic_values(experiment): ) if PRICE_FUEL not in experiment: - present_value_changing_fuel_price = ( - economics.present_value_of_changing_fuel_price( - experiment[FUEL_PRICE], - experiment[PROJECT_LIFETIME], - experiment[WACC], - experiment[FUEL_PRICE_CHANGE_ANNUAL], - experiment[CRF], - ) + present_value_changing_fuel_price = economics.present_value_of_changing_fuel_price( + fuel_price=experiment[FUEL_PRICE], + project_lifetime=experiment[PROJECT_LIFETIME], + wacc=experiment[WACC], + crf=experiment[CRF], + fuel_price_change_annual=experiment[FUEL_PRICE_CHANGE_ANNUAL], ) experiment.update({PRICE_FUEL: present_value_changing_fuel_price}) else: diff --git a/tests/test_D1_economic_functions.py b/tests/test_D1_economic_functions.py new file mode 100644 index 0000000..b8dac0d --- /dev/null +++ b/tests/test_D1_economic_functions.py @@ -0,0 +1,53 @@ +from pytest import approx +import src.D1_economic_functions as D1 +import logging + + +def test_crf(): + project_lifetime = 10 + wacc = 0.1 + crf = D1.crf(project_lifetime, wacc) + exp = 0.163 + assert exp == approx( + crf, rel=0.01 + ), f"With a project lifetime of {project_lifetime} and a WACC of {wacc}, the CRF was expected to be {exp}, but it is {crf}." + + +def test_present_value_of_changing_fuel_price_with_fuel_price_change_annual_0(caplog): + fuel_price = 1 + project_lifetime = 2 + wacc = 0.05 + crf = D1.crf(project_lifetime, wacc) + fuel_price_change_annual = 0.0 + with caplog.at_level(logging.INFO): + present_value_of_changing_fuel_price = D1.present_value_of_changing_fuel_price( + fuel_price, project_lifetime, wacc, crf, fuel_price_change_annual + ) + assert ( + "Simulation will run with a fuel price of" in caplog.text + ), f"An logging.info message should have been logged, as otherwise this indicates that the wrong part of the if-statement is entered" + assert ( + fuel_price == present_value_of_changing_fuel_price + ), f"The fuel price should be identical to the present value of the fuel price when there is no annual fuel price change but this is not the case ({fuel_price}/{present_value_of_changing_fuel_price})" + + +def test_present_value_of_changing_fuel_price_with_fuel_price_change_annual_not_0( + caplog, +): + fuel_price = 1 + project_lifetime = 2 + wacc = 0.05 + crf = D1.crf(project_lifetime, wacc) + fuel_price_change_annual = 0.1 + with caplog.at_level(logging.ERROR): + present_value_of_changing_fuel_price = D1.present_value_of_changing_fuel_price( + fuel_price, project_lifetime, wacc, crf, fuel_price_change_annual + ) + assert ( + "This calculation is still faulty and you should check the resulting fuel price." + in caplog.text + ), f"An error message should be displayed to warn the user that using a `fuel_price_change_annual` is not recommended." + exp = (fuel_price + fuel_price * (1 + fuel_price_change_annual) / (1 + wacc)) * crf + assert ( + present_value_of_changing_fuel_price == exp + ), f"The present value of the fuel price when there is an annual fuel price change was expected to be {exp} but is {present_value_of_changing_fuel_price}." From 6b5689b36e2e194b2f581a3f4d76face7ad959f5 Mon Sep 17 00:00:00 2001 From: smartie2076 Date: Tue, 4 May 2021 15:27:08 +0200 Subject: [PATCH 2/4] Formatting with black --- src/B_read_from_files.py | 7 +++---- src/C_sensitivity_experiments.py | 17 +++++---------- src/E_blackouts_central_grid.py | 8 ++++--- src/G2a_oemof_busses_and_componets.py | 12 ++--------- src/G2b_constraints_custom.py | 12 +++++------ src/G3_oemof_evaluate.py | 30 ++++++--------------------- src/G4_output_functions.py | 4 +--- src/H0_multicriteria_analysis.py | 21 +++++++------------ src/cli.py | 6 ++---- tests/test_basic.py | 6 ++++-- 10 files changed, 41 insertions(+), 82 deletions(-) diff --git a/src/B_read_from_files.py b/src/B_read_from_files.py index 3f91d5c..07496dd 100644 --- a/src/B_read_from_files.py +++ b/src/B_read_from_files.py @@ -110,10 +110,9 @@ def process_excel_file(input_excel_file): # -------- Check for, create or empty results directory -----------------------# check_output_directory(settings, input_excel_file) - ( - parameters_constant_units, - parameters_constant_values, - ) = get_parameters_constant(input_excel_file, sheet_input_constant) + (parameters_constant_units, parameters_constant_values,) = get_parameters_constant( + input_excel_file, sheet_input_constant + ) parameters_sensitivity = get_parameters_sensitivity( input_excel_file, sheet_input_sensitivity ) diff --git a/src/C_sensitivity_experiments.py b/src/C_sensitivity_experiments.py index 892faa3..7d74c47 100644 --- a/src/C_sensitivity_experiments.py +++ b/src/C_sensitivity_experiments.py @@ -270,10 +270,7 @@ def get(settings, parameters_constant_values, parameters_sensitivity, project_si sensitivity_array_dict, total_number_of_experiments, ) = all_possible( - settings, - parameters_constant_values, - parameters_sensitivity, - project_sites, + settings, parameters_constant_values, parameters_sensitivity, project_sites, ) elif settings[SENSITIVITY_ALL_COMBINATIONS] is False: @@ -283,10 +280,7 @@ def get(settings, parameters_constant_values, parameters_sensitivity, project_si sensitivity_array_dict, total_number_of_experiments, ) = with_base_case( - settings, - parameters_constant_values, - parameters_sensitivity, - project_sites, + settings, parameters_constant_values, parameters_sensitivity, project_sites, ) else: @@ -339,10 +333,9 @@ def get(settings, parameters_constant_values, parameters_sensitivity, project_si # Get blackout_experiment_s for sensitvitiy # ####################################################### # Creating dict of possible blackout scenarios (combinations of durations frequencies - ( - blackout_experiment_s, - blackout_experiments_count, - ) = blackout(sensitivity_array_dict, parameters_constant_values, settings) + (blackout_experiment_s, blackout_experiments_count,) = blackout( + sensitivity_array_dict, parameters_constant_values, settings + ) # save all Experiments with all used input data to csv csv_dict = deepcopy(sensitivitiy_experiment_s) diff --git a/src/E_blackouts_central_grid.py b/src/E_blackouts_central_grid.py index f1fe586..864ca9c 100644 --- a/src/E_blackouts_central_grid.py +++ b/src/E_blackouts_central_grid.py @@ -106,9 +106,11 @@ def get_blackouts(settings, blackout_experiment_s): in grid_availability_df.columns ): count_of_red_data = count_of_red_data + 1 - name_of_experiment_requested_from_file_dataset = ( - blackout_experiment_s[experiment][EXPERIMENT_NAME] - ) + name_of_experiment_requested_from_file_dataset = blackout_experiment_s[ + experiment + ][ + EXPERIMENT_NAME + ] blackout_result = oemof_extension_for_blackouts( grid_availability_df[ name_of_experiment_requested_from_file_dataset diff --git a/src/G2a_oemof_busses_and_componets.py b/src/G2a_oemof_busses_and_componets.py index 07d2a30..d6ad3e6 100644 --- a/src/G2a_oemof_busses_and_componets.py +++ b/src/G2a_oemof_busses_and_componets.py @@ -412,11 +412,7 @@ def genset_fix_minload( def genset_oem( - micro_grid_system, - bus_fuel, - bus_electricity_ac, - experiment, - number_of_generators, + micro_grid_system, bus_fuel, bus_electricity_ac, experiment, number_of_generators, ): """ Generates fossi-fueled genset "transformer_fuel_generator" for OEM with generator efficiency, @@ -580,11 +576,7 @@ def pointofcoupling_consumption_oem( def storage_fix( - micro_grid_system, - bus_electricity_dc, - experiment, - capacity_storage, - power_storage, + micro_grid_system, bus_electricity_dc, experiment, capacity_storage, power_storage, ): """ Create storage unit "generic_storage" with fixed capacity, diff --git a/src/G2b_constraints_custom.py b/src/G2b_constraints_custom.py index 3c02cbb..debfbff 100644 --- a/src/G2b_constraints_custom.py +++ b/src/G2b_constraints_custom.py @@ -705,9 +705,9 @@ def linear_charge(model, t): expr = 0 if case_dict[STORAGE_FIXED_CAPACITY] != None: if case_dict[STORAGE_FIXED_CAPACITY] is False: # Storage subject to OEM - stored_electricity += ( - model.GenericInvestmentStorageBlock.storage_content[storage, t] - ) + stored_electricity += model.GenericInvestmentStorageBlock.storage_content[ + storage, t + ] elif isinstance( case_dict[STORAGE_FIXED_CAPACITY], float ): # Fixed storage subject to dispatch @@ -814,9 +814,9 @@ def discharge_rule_upper(model, t): expr += model.flow[storage, el_bus, t] # Get stored electricity at t if case_dict[STORAGE_FIXED_CAPACITY] is False: # Storage subject to OEM - stored_electricity += ( - model.GenericInvestmentStorageBlock.storage_content[storage, t] - ) + stored_electricity += model.GenericInvestmentStorageBlock.storage_content[ + storage, t + ] elif isinstance( case_dict[STORAGE_FIXED_CAPACITY], float ): # Fixed storage subject to dispatch diff --git a/src/G3_oemof_evaluate.py b/src/G3_oemof_evaluate.py index 8029e7a..27ede4b 100644 --- a/src/G3_oemof_evaluate.py +++ b/src/G3_oemof_evaluate.py @@ -222,10 +222,7 @@ def get_shortage( demand_supplied = e_flows_df[DEMAND] - shortage annual_value( - TOTAL_DEMAND_SUPPLIED_ANNUAL_KWH, - demand_supplied, - oemof_results, - case_dict, + TOTAL_DEMAND_SUPPLIED_ANNUAL_KWH, demand_supplied, oemof_results, case_dict, ) annual_value( TOTAL_DEMAND_SHORTAGE_ANNUAL_KWH, shortage, oemof_results, case_dict @@ -397,10 +394,7 @@ def get_inverter( e_flows_df = join_e_flows_df(inverter_in, INVERTER_INPUT, e_flows_df) annual_value( - TOTAL_INVERTER_DC_AC_THROUGHPUT_KWH, - inverter_in, - oemof_results, - case_dict, + TOTAL_INVERTER_DC_AC_THROUGHPUT_KWH, inverter_in, oemof_results, case_dict, ) else: oemof_results.update({TOTAL_INVERTER_DC_AC_THROUGHPUT_KWH: 0}) @@ -476,10 +470,7 @@ def get_genset(case_dict, oemof_results, electricity_bus_ac, e_flows_df): total_genset = genset for number in range(2, case_dict[NUMBER_OF_EQUAL_GENERATORS] + 1): genset = electricity_bus_ac[SEQUENCES][ - ( - (TRANSFORMER_GENSET_ + str(number), BUS_ELECTRICITY_AC), - FLOW, - ) + ((TRANSFORMER_GENSET_ + str(number), BUS_ELECTRICITY_AC), FLOW,) ] e_flows_df = join_e_flows_df( genset, "Genset " + str(number) + " generation", e_flows_df @@ -498,10 +489,7 @@ def get_genset(case_dict, oemof_results, electricity_bus_ac, e_flows_df): genset_capacity = 0 for number in range(1, case_dict[NUMBER_OF_EQUAL_GENERATORS] + 1): genset_capacity += electricity_bus_ac[SCALARS][ - ( - (TRANSFORMER_GENSET_ + str(number), BUS_ELECTRICITY_AC), - INVEST, - ) + ((TRANSFORMER_GENSET_ + str(number), BUS_ELECTRICITY_AC), INVEST,) ] oemof_results.update({CAPACITY_GENSET_KW: genset_capacity}) elif isinstance(case_dict[GENSET_FIXED_CAPACITY], float): @@ -586,10 +574,7 @@ def get_storage(case_dict, oemof_results, experiment, results, e_flows_df): ] oemof_results.update( - { - CAPACITY_STORAGE_KWH: storage_capacity, - POWER_STORAGE_KW: storage_power, - } + {CAPACITY_STORAGE_KWH: storage_capacity, POWER_STORAGE_KW: storage_power,} ) elif isinstance(case_dict[STORAGE_FIXED_CAPACITY], float): @@ -654,10 +639,7 @@ def get_national_grid(case_dict, oemof_results, results, e_flows_df, grid_availa results, BUS_ELECTRICITY_NG_CONSUMPTION ) consumption_utility_side = bus_electricity_ng_consumption[SEQUENCES][ - ( - (BUS_ELECTRICITY_NG_CONSUMPTION, TRANSFORMER_PCC_CONSUMPTION), - FLOW, - ) + ((BUS_ELECTRICITY_NG_CONSUMPTION, TRANSFORMER_PCC_CONSUMPTION), FLOW,) ] e_flows_df = join_e_flows_df( consumption_utility_side, diff --git a/src/G4_output_functions.py b/src/G4_output_functions.py index 99a499f..ffca2fe 100644 --- a/src/G4_output_functions.py +++ b/src/G4_output_functions.py @@ -136,9 +136,7 @@ def save_mg_flows(experiment, case_dict, e_flows_df, filename): ] mg_flows = pd.DataFrame( - e_flows_df[DEMAND].values, - columns=[DEMAND], - index=e_flows_df[DEMAND].index, + e_flows_df[DEMAND].values, columns=[DEMAND], index=e_flows_df[DEMAND].index, ) for entry in flows_connected_to_electricity_mg_bus: if entry in e_flows_df.columns: diff --git a/src/H0_multicriteria_analysis.py b/src/H0_multicriteria_analysis.py index a795a59..98e114c 100644 --- a/src/H0_multicriteria_analysis.py +++ b/src/H0_multicriteria_analysis.py @@ -62,12 +62,9 @@ def main_analysis(overallresults, multicriteria_data, settings): ) = format_punctuations(multicriteria_data) # the cases chosen to analyse in the multicriteria analysis are selected - ( - all_results, - cases, - projects_name, - sensibility, - ) = presentation(overallresults, parameters) + (all_results, cases, projects_name, sensibility,) = presentation( + overallresults, parameters + ) # the multicriteria analysis with sensibility parameters can only be realised if all combinations have been calculated if settings[SENSITIVITY_ALL_COMBINATIONS] or not sensibility: @@ -84,10 +81,8 @@ def main_analysis(overallresults, multicriteria_data, settings): # first, a global ranking, for all solutions, is calculated # evaluations are normalized - global_normalized_evaluations = ( - multicriteria_functions.normalize_evaluations( - global_evaluations, weights_criteria, GLOBAL - ) + global_normalized_evaluations = multicriteria_functions.normalize_evaluations( + global_evaluations, weights_criteria, GLOBAL ) all_projects_MCA_data[NORMALIZED_EVALUATIONS][ project @@ -109,10 +104,8 @@ def main_analysis(overallresults, multicriteria_data, settings): # second, each local_evaluations are normalized and a ranking is calculated local_Ls = [] for evaluation in local_evaluations: - local_normalized_evaluation = ( - multicriteria_functions.normalize_evaluations( - evaluation, weights_criteria, LOCAL - ) + local_normalized_evaluation = multicriteria_functions.normalize_evaluations( + evaluation, weights_criteria, LOCAL ) local_Ls_each = multicriteria_functions.rank( local_normalized_evaluation, diff --git a/src/cli.py b/src/cli.py index 9cc2779..cce6e0c 100644 --- a/src/cli.py +++ b/src/cli.py @@ -167,10 +167,8 @@ def main(input_file=None): logging.debug( "Using grid availability timeseries that was randomly generated." ) - blackout_experiment_name = ( - generate_sensitvitiy_experiments.get_blackout_experiment_name( - sensitivity_experiment_s[experiment] - ) + blackout_experiment_name = generate_sensitvitiy_experiments.get_blackout_experiment_name( + sensitivity_experiment_s[experiment] ) sensitivity_experiment_s[experiment].update( { diff --git a/tests/test_basic.py b/tests/test_basic.py index cc29dc1..7c85951 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -3,11 +3,13 @@ from ..src.cli import main + def test_execution_not_terminated(): answer = main(input_file=os.path.join("tests", "inputs", "pytest_test.xlsx")) assert answer == 1, f"Simulation with default inputs terminated." -''' + +""" def test_blacks_main(): # Testing code formatting in main folder r = os.system("black --check /src") @@ -18,4 +20,4 @@ def test_blacks_code_folder(): # Testing code formatting in code folder r = os.system("black --check /tests") assert r == 0, f"Black exited with:\n {r}" -''' \ No newline at end of file +""" From 3c44ddf5c43cb66bb42e9cceb95ecae642a362c1 Mon Sep 17 00:00:00 2001 From: smartie2076 Date: Tue, 4 May 2021 16:02:02 +0200 Subject: [PATCH 3/4] Implement new KPI: `GENSET_HOURS_OF_OPERATION` - Add `GENSET_HOURS_OF_OPERATION` to `constants.py` and in `C1.overall_results_title` - Add function `G3.get_hours_of_operation()` for generator evaluation, including pytests --- src/C_sensitivity_experiments.py | 2 ++ src/G3_oemof_evaluate.py | 30 ++++++++++++++++++++++++++++++ src/constants.py | 1 + tests/test_G3_oemof_evaluation.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 tests/test_G3_oemof_evaluation.py diff --git a/src/C_sensitivity_experiments.py b/src/C_sensitivity_experiments.py index 7d74c47..a6fc14a 100644 --- a/src/C_sensitivity_experiments.py +++ b/src/C_sensitivity_experiments.py @@ -132,6 +132,7 @@ TOTAL_WIND_GENERATION_KWH, TOTAL_GENSET_GENERATION_KWH, CONSUMPTION_FUEL_ANNUAL_L, + GENSET_HOURS_OF_OPERATION, CONSUMPTION_MAIN_GRID_MG_SIDE_ANNUAL_KWH, FEEDIN_MAIN_GRID_MG_SIDE_ANNUAL_KWH, RESULTS_ANNUITIES, @@ -1308,6 +1309,7 @@ def overall_results_title(settings, number_of_project_sites, sensitivity_array_d TOTAL_GENSET_GENERATION_KWH, CONSUMPTION_FUEL_ANNUAL_KWH, CONSUMPTION_FUEL_ANNUAL_L, + GENSET_HOURS_OF_OPERATION, CONSUMPTION_MAIN_GRID_MG_SIDE_ANNUAL_KWH, FEEDIN_MAIN_GRID_MG_SIDE_ANNUAL_KWH, ] diff --git a/src/G3_oemof_evaluate.py b/src/G3_oemof_evaluate.py index 27ede4b..8af664a 100644 --- a/src/G3_oemof_evaluate.py +++ b/src/G3_oemof_evaluate.py @@ -117,6 +117,7 @@ FEED_INTO_MAIN_GRID_MG_SIDE, DEMAND_AC, DEMAND_DC, + GENSET_HOURS_OF_OPERATION, ) @@ -496,9 +497,38 @@ def get_genset(case_dict, oemof_results, electricity_bus_ac, e_flows_df): oemof_results.update({CAPACITY_GENSET_KW: case_dict[GENSET_FIXED_CAPACITY]}) elif case_dict[GENSET_FIXED_CAPACITY] == None: oemof_results.update({CAPACITY_GENSET_KW: 0}) + + # Get hours of operation: + if case_dict[GENSET_FIXED_CAPACITY] != None: + get_hours_of_operation(oemof_results, case_dict, e_flows_df[GENSET_GENERATION]) + else: + oemof_results.update({GENSET_HOURS_OF_OPERATION: 0}) return e_flows_df +def get_hours_of_operation(oemof_results, case_dict, genset_generation_total): + """ + Calculates the total hours of genset generation (aggregated generation) of the evaluated timeframe. + + Parameters + ---------- + oemof_results: dict + Dict of all results of the simulation + + genset_generation_total: pd.Series + Dispatch of the gensets, aggregated + + Returns + ------- + Updates oemof_results with annual value of the GENSET_HOURS_OF_OPERATION. + """ + operation_boolean = genset_generation_total.where( + genset_generation_total == 0, other=1 + ) + annual_value(GENSET_HOURS_OF_OPERATION, operation_boolean, oemof_results, case_dict) + return operation_boolean + + def get_fuel(case_dict, oemof_results, results): logging.debug("Evaluate flow: fuel") if case_dict[GENSET_FIXED_CAPACITY] != None: diff --git a/src/constants.py b/src/constants.py index 7fb0016..f76cce0 100644 --- a/src/constants.py +++ b/src/constants.py @@ -194,6 +194,7 @@ TOTAL_GENSET_GENERATION_KWH = "total_genset_generation_kWh" CONSUMPTION_FUEL_ANNUAL_L = "consumption_fuel_annual_l" CONSUMPTION_MAIN_GRID_MG_SIDE_ANNUAL_KWH = "consumption_main_grid_mg_side_annual_kWh" +GENSET_HOURS_OF_OPERATION = "generator_operation_hours" FEEDIN_MAIN_GRID_MG_SIDE_ANNUAL_KWH = "feedin_main_grid_mg_side_annual_kWh" RESULTS_ANNUITIES = "results_annuities" ANNUITY_PV = "annuity_pv" diff --git a/tests/test_G3_oemof_evaluation.py b/tests/test_G3_oemof_evaluation.py new file mode 100644 index 0000000..75dd7a7 --- /dev/null +++ b/tests/test_G3_oemof_evaluation.py @@ -0,0 +1,28 @@ +import pandas as pd +import numpy as np +from pandas.util.testing import assert_series_equal + +import src.G3_oemof_evaluate as G3 +from src.constants import EVALUATED_DAYS, GENSET_HOURS_OF_OPERATION + + +def test_get_hours_of_operation(): + genset_generation = pd.Series([0, 0, 0, 0.5, 2]) + oemof_results = {} + case_dict = {EVALUATED_DAYS: 5} + operation_boolean = G3.get_hours_of_operation( + oemof_results, case_dict, genset_generation + ) + exp = pd.Series([0, 0, 0, 1, 1]) + assert ( + operation_boolean.sum() == 2 + ), f"It was expected that the number of operation hours in the evaluated timeframe was 2, but it is {operation_boolean.sum()}." + assert_series_equal( + genset_generation.astype(np.float64), exp(np.float64), check_names=False, + ), f"The operational hours pd.Series should be the same when calculated with the function to the expected series." + assert ( + GENSET_HOURS_OF_OPERATION in oemof_results + ), f"Parameter {GENSET_HOURS_OF_OPERATION} is not in the oemof_results, but was expected." + assert oemof_results[GENSET_HOURS_OF_OPERATION] == 2 * ( + 365 / 5 + ), f"Parameter {GENSET_HOURS_OF_OPERATION} is not of expected annual value {2*365/5}, but {oemof_results[GENSET_HOURS_OF_OPERATION]}." From 2650237bca5d0da8241a239e28e08f1750ece93d Mon Sep 17 00:00:00 2001 From: smartie2076 Date: Tue, 4 May 2021 16:07:21 +0200 Subject: [PATCH 4/4] Update changelog.md --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d560e..298932b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,19 +26,23 @@ Here is a template for new release sections ### Added - Version number with `src/version.py` (#150) -- Constant variables in `constants.py`: `INPUT_TEMPLATE_EXCEL_XLSX`(#150) +- Constant variables in `constants.py`: `INPUT_TEMPLATE_EXCEL_XLSX`(#150), `GENSET_HOURS_OF_OPERATION` (#153) +- Added pytests for `D1.crf` and `D1.present_value_of_changing_fuel_price` (#153) +- Implement new KPI: `GENSET_HOURS_OF_OPERATION` with new function `G3.get_hours_of_operation()` for generator evaluation, including pytests (#153) ### Changed - Execute all pytests in Travis `.travis.yml` (#150) - Added version number to `setup.py` (#150) - Moved `main()` from `Offgridders.py` to new file `src/cli.py` (#150) - Enable benchmark tests for Offgridders: Add optional argument `input_file` to `main()` (#150) +- Added `GENSET_HOURS_OF_OPERATION` in `C1.overall_results_title` (#153) ### Removed - ### Fixed - Basic pytest to ensure no termination with test input file (`tests/inputs/pytest_test.xlsx`) (#150) +- `present_value_of_changing_fuel_price` now correctly calculated, fixed function call of `D1.present_value_of_changing_fuel_price` in `D0` (#153) ## [Offgridders V4.6.1] - 2020-11-07