diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf31d4c..6c8f00f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased +### Improvement +- Adding new constraints to limit the dynamics (kW/sec) of deferrable loads and battery power. The LP formulation works correctly and a work should be done on integrating the user input parameters to control this functionality. +- Added new constraint to avoid battery discharging to the grid +### Fix +- Bumped version of skforecast from 0.6.0 to 0.8.0. Doing this mainly implies changing how the exogenous data is passed to fit and predict methods. + ## [0.4.10] - 2023-05-21 ### Fix - Fixed wrong name of new cost sensor. diff --git a/config_emhass.yaml b/config_emhass.yaml index b666a2cb..b965f15f 100644 --- a/config_emhass.yaml +++ b/config_emhass.yaml @@ -48,6 +48,10 @@ optim_conf: - lp_solver: 'PULP_CBC_CMD' # set the name of the linear programming solver that will be used - lp_solver_path: 'empty' # set the path to the LP solver - set_nocharge_from_grid: False # avoid battery charging from the grid + - set_nodischarge_to_grid: True # avoid battery discharging to the grid + - set_battery_dynamic: False # add a constraint to limit the dynamic of the battery power in power per time unit + - battery_dynamic_max: 0.9 # maximum dynamic positive power variation in percentage of battery maximum power + - battery_dynamic_min: -0.9 # minimum dynamic negative power variation in percentage of battery maximum power plant_conf: - P_grid_max: 9000 # The maximum power that can be supplied by the utility grid in Watts diff --git a/src/emhass/command_line.py b/src/emhass/command_line.py index 6314adfc..9a0888e8 100644 --- a/src/emhass/command_line.py +++ b/src/emhass/command_line.py @@ -229,7 +229,8 @@ def dayahead_forecast_optim(input_data_dict: dict, logger: logging.Logger, input_data_dict['df_input_data_dayahead'], method=input_data_dict['fcst'].optim_conf['load_cost_forecast_method']) df_input_data_dayahead = input_data_dict['fcst'].get_prod_price_forecast( - df_input_data_dayahead, method=input_data_dict['fcst'].optim_conf['prod_price_forecast_method']) + df_input_data_dayahead, + method=input_data_dict['fcst'].optim_conf['prod_price_forecast_method']) opt_res_dayahead = input_data_dict['opt'].perform_dayahead_forecast_optim( df_input_data_dayahead, input_data_dict['P_PV_forecast'], input_data_dict['P_load_forecast']) # Save CSV file for publish_data diff --git a/src/emhass/optimization.py b/src/emhass/optimization.py index ab94e9a7..dc95cb92 100644 --- a/src/emhass/optimization.py +++ b/src/emhass/optimization.py @@ -230,28 +230,6 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n sense = plp.LpConstraintEQ, rhs = 0) for i in set_I} - - # Optional constraints to avoid charging the battery from the grid and to limit kW/s - if self.optim_conf['set_use_battery']: - if self.optim_conf['set_nocharge_from_grid']: - constraints.update({"constraint_nocharge_from_grid_{}".format(i) : - plp.LpConstraint( - e = P_sto_neg[i] + P_PV[i], - sense = plp.LpConstraintGE, - rhs = 0) - for i in set_I}) - set_battery_dynamic = True - dyn_max = 1000 - dyn_min = -1000 - if set_battery_dynamic: - constraints.update({"constraint_batt_dynamic_max_{}".format(i) : - plp.LpConstraint(e = P_sto_pos[i+1] - P_sto_pos[i], - sense = plp.LpConstraintLE, - rhs = self.timeStep*dyn_max) for i in range(n-1)}) - constraints.update({"constraint_batt_dynamic_min_{}".format(i) : - plp.LpConstraint(e = P_sto_pos[i+1] - P_sto_pos[i], - sense = plp.LpConstraintGE, - rhs = self.timeStep*dyn_min) for i in range(n-1)}) # Two special constraints just for a self-consumption cost function if self.costfun == 'self-consumption': @@ -283,17 +261,16 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n rhs = 0) for i in set_I}) - # Total time of deferrable load + # Treat deferrable loads constraints for k in range(self.optim_conf['num_def_loads']): + # Total time of deferrable load constraints.update({"constraint_defload{}_energy".format(k) : plp.LpConstraint( e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in set_I), sense = plp.LpConstraintEQ, rhs = def_total_hours[k]*self.optim_conf['P_deferrable_nom'][k]) }) - - # Treat deferrable load as a semi-continuous variable - for k in range(self.optim_conf['num_def_loads']): + # Treat deferrable load as a semi-continuous variable if self.optim_conf['treat_def_as_semi_cont'][k]: constraints.update({"constraint_pdef{}_semicont1_{}".format(k, i) : plp.LpConstraint( @@ -307,9 +284,7 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n sense=plp.LpConstraintLE, rhs=0) for i in set_I}) - - # Treat the number of starts for a deferrable load - for k in range(self.optim_conf['num_def_loads']): + # Treat the number of starts for a deferrable load if self.optim_conf['set_def_constant'][k]: constraints.update({"constraint_pdef{}_start1".format(k) : plp.LpConstraint( @@ -338,6 +313,45 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n # The battery constraints if self.optim_conf['set_use_battery']: + # Optional constraints to avoid charging the battery from the grid + if self.optim_conf['set_nocharge_from_grid']: + constraints.update({"constraint_nocharge_from_grid_{}".format(i) : + plp.LpConstraint( + e = P_sto_neg[i] + P_PV[i], + sense = plp.LpConstraintGE, + rhs = 0) + for i in set_I}) + # Optional constraints to avoid discharging the battery to the grid + if self.optim_conf['set_nodischarge_to_grid']: + constraints.update({"constraint_nodischarge_to_grid_{}".format(i) : + plp.LpConstraint( + e = P_grid_neg[i] + P_PV[i], + sense = plp.LpConstraintGE, + rhs = 0) + for i in set_I}) + # Limitation of power dynamics in power per unit of time + if self.optim_conf['set_battery_dynamic']: + constraints.update({"constraint_pos_batt_dynamic_max_{}".format(i) : + plp.LpConstraint(e = P_sto_pos[i+1] - P_sto_pos[i], + sense = plp.LpConstraintLE, + rhs = self.timeStep*self.optim_conf['battery_dynamic_max']*self.plant_conf['Pd_max']) + for i in range(n-1)}) + constraints.update({"constraint_pos_batt_dynamic_min_{}".format(i) : + plp.LpConstraint(e = P_sto_pos[i+1] - P_sto_pos[i], + sense = plp.LpConstraintGE, + rhs = self.timeStep*self.optim_conf['battery_dynamic_min']*self.plant_conf['Pd_max']) + for i in range(n-1)}) + constraints.update({"constraint_neg_batt_dynamic_max_{}".format(i) : + plp.LpConstraint(e = P_sto_neg[i+1] - P_sto_neg[i], + sense = plp.LpConstraintLE, + rhs = self.timeStep*self.optim_conf['battery_dynamic_max']*self.plant_conf['Pc_max']) + for i in range(n-1)}) + constraints.update({"constraint_neg_batt_dynamic_min_{}".format(i) : + plp.LpConstraint(e = P_sto_neg[i+1] - P_sto_neg[i], + sense = plp.LpConstraintGE, + rhs = self.timeStep*self.optim_conf['battery_dynamic_min']*self.plant_conf['Pc_max']) + for i in range(n-1)}) + # Then the classic battery constraints constraints.update({"constraint_pstopos_{}".format(i) : plp.LpConstraint( e=P_sto_pos[i] - self.plant_conf['eta_disch']*self.plant_conf['Pd_max']*E[i], diff --git a/src/emhass/web_server.py b/src/emhass/web_server.py index 4a038ea4..15704be7 100644 --- a/src/emhass/web_server.py +++ b/src/emhass/web_server.py @@ -129,6 +129,10 @@ def build_params(params, options, addon): params['optim_conf'][16]['lp_solver'] = options['lp_solver'] params['optim_conf'][17]['lp_solver_path'] = options['lp_solver_path'] params['optim_conf'][18]['set_nocharge_from_grid'] = options['set_nocharge_from_grid'] + params['optim_conf'][19]['set_nodischarge_to_grid'] = options['set_nodischarge_to_grid'] + params['optim_conf'][20]['set_battery_dynamic'] = options['set_battery_dynamic'] + params['optim_conf'][21]['battery_dynamic_max'] = options['battery_dynamic_max'] + params['optim_conf'][22]['battery_dynamic_min'] = options['battery_dynamic_min'] # Updating variables in plant_conf params['plant_conf'][0]['P_grid_max'] = options['maximum_power_from_grid'] params['plant_conf'][1]['module_model'] = [i['pv_module_model'] for i in options['list_pv_module_model']] diff --git a/tests/test_optimization.py b/tests/test_optimization.py index 4ed60010..50ca4452 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -83,9 +83,11 @@ def test_perform_dayahead_forecast_optim(self): now_precise = datetime.now(self.input_data_dict['retrieve_hass_conf']['time_zone']).replace(second=0, microsecond=0) idx_closest = self.opt_res_dayahead.index.get_indexer([now_precise], method='ffill')[0] idx_closest = self.opt_res_dayahead.index.get_indexer([now_precise], method='nearest')[0] - # Test the battery + # Test the battery, dynamics and grid exchange contraints self.optim_conf.update({'set_use_battery': True}) self.optim_conf.update({'set_nocharge_from_grid': True}) + self.optim_conf.update({'set_battery_dynamic': True}) + self.optim_conf.update({'set_nodischarge_to_grid': True}) self.opt = optimization(self.retrieve_hass_conf, self.optim_conf, self.plant_conf, self.fcst.var_load_cost, self.fcst.var_prod_price, self.costfun, root, logger)