def constraints(self, mask, **kwargs): """Default build constraint list method. Used by services that do not have constraints. Args: mask (DataFrame): A boolean array that is true for indices corresponding to time_series data included in the subs data set Returns: A list of constraints that corresponds the battery's physical constraints and its service constraints """ constraint_list = [] if self.duration: power = self.variables_dict[ 'power'] # p_t = charge_t - discharge_t energy = self.variables_dict['ene_load'] uene = self.variables_dict['uene'] udis = self.variables_dict['udis'] uch = self.variables_dict['uch'] # constraints that keep the variables inside their limits constraint_list += [cvx.NonPos(power - self.rated_power)] constraint_list += [cvx.NonPos(-self.rated_power - power)] constraint_list += [cvx.NonPos(-energy)] constraint_list += [ cvx.NonPos(energy - self.operational_max_energy()) ] # uene accounts for change in energy due to participating in sub timestep scale markets constraint_list += [ cvx.Zero(uene + (self.dt * udis) - (self.dt * uch)) ] sub = mask.loc[mask] for day in sub.index.dayofyear.unique(): day_mask = (day == sub.index.dayofyear) # general: e_{t+1} = e_t + (charge_t - discharge_t) * dt = e_t + power_t * dt constraint_list += [ cvx.Zero(energy[day_mask][:-1] + (power[day_mask][:-1] * self.dt) - energy[day_mask][1:]) ] # start of first timestep of the day constraint_list += [ cvx.Zero(energy[day_mask][0] - self.operational_max_energy()) ] # end of the last timestep of the day constraint_list += [ cvx.Zero(energy[day_mask][-1] + (power[day_mask][-1] * self.dt) - self.operational_max_energy()) ] return constraint_list
def constraints(self, mask): constraint_list = super().constraints(mask) elec = self.variables_dict['elec'] steam = self.variables_dict['steam'] hotwater = self.variables_dict['hotwater'] # electric energy and heat (steam + hotwater) energy generated are proportional # and defined by electric_heat_ratio constraint_list += [ cvx.Zero(elec - self.electric_heat_ratio * (steam + hotwater)) ] # to ensure that CHP never produces more steam than it can (no excess steam) if not self.steam_only and not self.hotwater_only: constraint_list += [ cvx.NonPos(steam - self.max_steam_ratio * hotwater) ] # to ensure that the upper limit on CHP size in the size optimization # will be the smallest system that can meet both howater and steam loads # use smallest_size_system_needed() if self.being_sized() and self.site_thermal_load_exists: constraint_list += [ cvx.NonPos(elec - self.smallest_size_system_needed()) ] return constraint_list
def objective_constraints(self, variables, subs, generation, reservations=None): """Default build constraint list method. Used by services that do not have constraints. Args: variables (Dict): dictionary of variables being optimized subs (DataFrame): Subset of time_series data that is being optimized generation (list, Expression): the sum of generation within the system for the subset of time being optimized reservations (Dict): power reservations from dispatch services Returns: constraint_list (list): list of constraints """ constraint_list = [] constraint_list += [cvx.NonPos(-variables['regu_c'])] constraint_list += [cvx.NonPos(-variables['regd_c'])] constraint_list += [cvx.NonPos(-variables['regu_d'])] constraint_list += [cvx.NonPos(-variables['regd_d'])] # p = opt_vars['dis'] - opt_vars['ch'] # constraint_list += [cvx.NonPos(opt_vars['regd_d'] - cvx.pos(p))] # constraint_list += [cvx.NonPos(opt_vars['regu_c'] - cvx.neg(p))] if self.combined_market: constraint_list += [cvx.Zero(variables['regd_d'] + variables['regd_c'] - variables['regu_d'] - variables['regu_c'])] return constraint_list
def constraints(self, mask, load_sum, tot_variable_gen, generator_out_sum, net_ess_power, combined_rating): """build constraint list method for the optimization engine Args: mask (DataFrame): A boolean array that is true for indices corresponding to time_series data included in the subs data set tot_variable_gen (Expression): the sum of the variable/intermittent generation sources load_sum (list, Expression): the sum of load within the system generator_out_sum (list, Expression): the sum of conventional generation within the system net_ess_power (list, Expression): the sum of the net power of all the ESS in the system. flow out into the grid is negative combined_rating (Dictionary): the combined rating of each DER class type Returns: An list of constraints for the optimization variables added to the system of equations """ constraint_list = [] constraint_list += [cvx.NonPos(-self.variables['up_ch'])] constraint_list += [cvx.NonPos(-self.variables['down_ch'])] constraint_list += [cvx.NonPos(-self.variables['up_dis'])] constraint_list += [cvx.NonPos(-self.variables['down_dis'])] if self.combined_market: constraint_list += [ cvx.Zero(self.variables['down_dis'] + self.variables['down_ch'] - self.variables['up_dis'] - self.variables['up_ch']) ] return constraint_list
def objective_constraints(self, variables, mask, load, generation, reservations=None): """ Default build constraint list method. Used by services that do not have constraints. Args: variables (Dict): dictionary of variables being optimized mask (DataFrame): A boolean array that is true for indices corresponding to time_series data included in the subs data set load (list, Expression): the sum of load within the system generation (list, Expression): the sum of generation within the system for the subset of time being optimized reservations (Dict): power reservations from dispatch services Returns: """ constraint_list = [] constraint_list += [cvx.NonPos(-variables['lf_up_c'])] constraint_list += [cvx.NonPos(-variables['lf_do_c'])] constraint_list += [cvx.NonPos(-variables['lf_up_d'])] constraint_list += [cvx.NonPos(-variables['lf_do_d'])] if self.combined_market: constraint_list += [cvx.Zero(variables['lf_do_d'] + variables['lf_do_c'] - variables['lf_up_d'] - variables['lf_up_c'])] return constraint_list
def objective_constraints(self, variables, mask, reservations, mpc_ene=None): """ Builds the master constraint list for the subset of timeseries data being optimized. Args: variables (Dict): Dictionary of variables being optimized mask (DataFrame): A boolean array that is true for indices corresponding to time_series data included in the subs data set reservations (Dict): Dictionary of energy and power reservations required by the services being preformed with the current optimization subset mpc_ene (float): value of energy at end of last opt step (for mpc opt) Returns: A list of constraints that corresponds the battery's physical constraints and its service constraints """ constraint_list = [] size = int(np.sum(mask)) curr_e_cap = self.physical_constraints['ene_max_rated'].value ene_target = self.soc_target * curr_e_cap # optimization variables ene = variables['ene'] dis = variables['dis'] ch = variables['ch'] on_c = variables['on_c'] on_d = variables['on_d'] try: pv_gen = variables['pv_out'] except KeyError: pv_gen = np.zeros(size) try: ice_gen = variables['ice_gen'] except KeyError: ice_gen = np.zeros(size) # create cvx parameters of control constraints (this improves readability in cvx costs and better handling) ene_max = cvx.Parameter( size, value=self.control_constraints['ene_max'].value[mask].values, name='ene_max') ene_min = cvx.Parameter( size, value=self.control_constraints['ene_min'].value[mask].values, name='ene_min') ch_max = cvx.Parameter( size, value=self.control_constraints['ch_max'].value[mask].values, name='ch_max') ch_min = cvx.Parameter( size, value=self.control_constraints['ch_min'].value[mask].values, name='ch_min') dis_max = cvx.Parameter( size, value=self.control_constraints['dis_max'].value[mask].values, name='dis_max') dis_min = cvx.Parameter( size, value=self.control_constraints['dis_min'].value[mask].values, name='dis_min') # energy at the end of the last time step (makes sure that the end of the last time step is ENE_TARGET # TODO: rewrite this if MPC_ENE is not None constraint_list += [ cvx.Zero((ene_target - ene[-1]) - (self.dt * ch[-1] * self.rte) + (self.dt * dis[-1]) - reservations['E'][-1] + (self.dt * ene[-1] * self.sdr * 0.01)) ] # energy generally for every time step constraint_list += [ cvx.Zero(ene[1:] - ene[:-1] - (self.dt * ch[:-1] * self.rte) + (self.dt * dis[:-1]) - reservations['E'][:-1] + (self.dt * ene[:-1] * self.sdr * 0.01)) ] # energy at the beginning of the optimization window -- handles rolling window if mpc_ene is None: constraint_list += [cvx.Zero(ene[0] - ene_target)] else: constraint_list += [cvx.Zero(ene[0] - mpc_ene)] # Keep energy in bounds determined in the constraints configuration function -- making sure our storage meets control constraints constraint_list += [ cvx.NonPos(ene_target - ene_max[-1] + reservations['E_upper'][-1] - variables['ene_max_slack'][-1]) ] constraint_list += [ cvx.NonPos(ene[:-1] - ene_max[:-1] + reservations['E_upper'][:-1] - variables['ene_max_slack'][:-1]) ] constraint_list += [ cvx.NonPos(-ene_target + ene_min[-1] + reservations['E_lower'][-1] - variables['ene_min_slack'][-1]) ] constraint_list += [ cvx.NonPos(ene_min[1:] - ene[1:] + reservations['E_lower'][:-1] - variables['ene_min_slack'][:-1]) ] # Keep charge and discharge power levels within bounds constraint_list += [ cvx.NonPos(-ch_max + ch - dis + reservations['D_min'] + reservations['C_max'] - variables['ch_max_slack']) ] constraint_list += [ cvx.NonPos(-ch + dis + reservations['C_min'] + reservations['D_max'] - dis_max - variables['dis_max_slack']) ] constraint_list += [cvx.NonPos(ch - cvx.multiply(ch_max, on_c))] constraint_list += [cvx.NonPos(dis - cvx.multiply(dis_max, on_d))] # removing the band in between ch_min and dis_min that the battery will not operate in constraint_list += [ cvx.NonPos( cvx.multiply(ch_min, on_c) - ch + reservations['C_min']) ] constraint_list += [ cvx.NonPos( cvx.multiply(dis_min, on_d) - dis + reservations['D_min']) ] # the constraint below limits energy throughput and total discharge to less than or equal to # (number of cycles * energy capacity) per day, for technology warranty purposes # this constraint only applies when optimization window is equal to or greater than 24 hours if self.daily_cycle_limit and size >= 24: sub = mask.loc[mask] for day in sub.index.dayofyear.unique(): day_mask = (day == sub.index.dayofyear) constraint_list += [ cvx.NonPos( cvx.sum(dis[day_mask] * self.dt + cvx.pos(reservations['E'][day_mask])) - self.ene_max_rated * self.daily_cycle_limit) ] elif self.daily_cycle_limit and size < 24: e_logger.info( 'Daily cycle limit did not apply as optimization window is less than 24 hours.' ) # constraints to keep slack variables positive if self.incl_slack: constraint_list += [cvx.NonPos(-variables['ch_max_slack'])] constraint_list += [cvx.NonPos(-variables['ch_min_slack'])] constraint_list += [cvx.NonPos(-variables['dis_max_slack'])] constraint_list += [cvx.NonPos(-variables['dis_min_slack'])] constraint_list += [cvx.NonPos(-variables['ene_max_slack'])] constraint_list += [cvx.NonPos(-variables['ene_min_slack'])] if self.incl_binary: # when dis_min or ch_min has been overwritten (read: increased) by predispatch services, need to force technology to be on # TODO better way to do this??? ind_d = [ i for i in range(size) if self.control_constraints['dis_min'].value[mask].values[i] > self.physical_constraints['dis_min_rated'].value ] ind_c = [ i for i in range(size) if self.control_constraints['ch_min'].value[mask].values[i] > self.physical_constraints['ch_min_rated'].value ] if len(ind_d) > 0: constraint_list += [on_d[ind_d] == 1] # np.ones(len(ind_d)) if len(ind_c) > 0: constraint_list += [on_c[ind_c] == 1] # np.ones(len(ind_c)) # note: cannot operate startup without binary if self.incl_startup: # startup variables are positive constraint_list += [cvx.NonPos(-variables['start_d'])] constraint_list += [cvx.NonPos(-variables['start_c'])] # difference between binary variables determine if started up in previous interval constraint_list += [ cvx.NonPos(cvx.diff(on_d) - variables['start_d'][1:]) ] # first variable not constrained constraint_list += [ cvx.NonPos(cvx.diff(on_c) - variables['start_c'][1:]) ] # first variable not constrained return constraint_list
# Averaging to put it on the vertices grid gradient_norm = manifold.mean_triangles @ gradient_norm @ domain.mean_triangles.T # Dividing by the vertices dual cell area gradient_norm = cp.multiply( gradient_norm, 1 / (manifold.areaVertices[:, np.newaxis] * domain.areaVertices[np.newaxis, :])) gradient_norm = gradient_norm.T / 2 assert gradient_norm.shape == (domain.n_vertices, manifold.n_vertices) # Definition of the dual variables A = divergence B = gradient constraint_dual_A = cp.Zero(A - divergence) constraint_dual_B = cp.Zero(B - gradient) # constraints = [constraint_dual_A, A + gradient_norm <= 0, cp.norm(objective) <= 1] # constraints = [divergence + gradient_norm <= 0, cp.norm(objective) <= 1] constraints = [ constraint_dual_A, constraint_dual_B, divergence + gradient_norm <= 0, cp.norm(objective) <= 1e3 ] obj = cp.Maximize(objective) prob = cp.Problem(obj, constraints) prob.solve(verbose=True)
def min_soe_opt(self, opt_index, der_list): """ Calculates min SOE at every time step for the given DER size Args: opt_index der_list Returns: der_list -- ESSs will have an SOE min if they were sized for reliability """ month_min_soc = {} data_length = len(opt_index) for month in opt_index.month.unique(): outage_mask = month == opt_index.month consts = [] min_soc = {} ana_ind = [a for a in range(data_length) if outage_mask[a] is True] outage_mask = pd.Series(index=opt_index) for outage_ind in ana_ind: outage_end_ind = outage_ind + self.outage_duration outage_mask.iloc[:] = False outage_mask.iloc[outage_ind:outage_end_ind] = True # set up variables var_gen_sum = cvx.Parameter(value=np.zeros( self.outage_duration), shape=self.outage_duration, name='POI-Zero') # at POI dg_sum = cvx.Parameter(value=np.zeros(self.outage_duration), shape=self.outage_duration, name='POI-Zero') net_ess = cvx.Parameter(value=np.zeros(self.outage_duration), shape=self.outage_duration, name='POI-Zero') for der in der_list: # initialize variables der.initialize_variables(self.outage_duration) if der.technology_type == 'Energy Storage System': net_ess += der.get_net_power(outage_mask) # set the soc_target to a CVXPY variable var_name = f"{der.name}{outage_ind}-min_soc" der.soc_target = cvx.Variable(shape=1, name=var_name) min_soc[outage_ind] = der.soc_target # Assuming Soc_init is the soc reservation required for # other services consts += [cvx.NonPos(der.soc_target - 1) ] # check to include ulsoc consts += [ cvx.NonPos(-der.soc_target + (1 - self.soc_init)) ] if der.technology_type == 'Generator': dg_sum += der.get_discharge(outage_mask) if der.technology_type == 'Intermittent Resource': var_gen_sum += der.get_discharge(outage_mask) consts += der.constraints(outage_mask, sizing_for_rel=True, find_min_soe=True) if outage_ind + self.outage_duration > data_length: remaining_out_duration = data_length - outage_ind crit_load_values = np.zeros(self.outage_duration) crit_load_values[0:remaining_out_duration] = \ self.critical_load.loc[outage_mask].values load = cvx.Parameter(value=crit_load_values, shape=self.outage_duration) else: load = cvx.Parameter( value=self.critical_load.loc[outage_mask].values, shape=self.outage_duration) consts += [ cvx.Zero(net_ess + (-1) * dg_sum + (-1) * var_gen_sum + load) ] cost_funcs = sum(min_soc.values()) obj = cvx.Minimize(cost_funcs) prob = cvx.Problem(obj, consts) prob.solve(solver=cvx.GLPK_MI) month_min_soc[month] = min_soc for der in der_list: if der.technology_type == 'Energy Storage System': # TODO multi ESS # Get energy rating energy_rating = der.energy_capacity(True) # Collecting soe array for all ES month_min_soc_array = [] outage_ind = 0 # TODO make sure this is in order for month in month_min_soc.keys(): for hours in range(len(month_min_soc[month])): month_min_soc_array.append( month_min_soc[month][outage_ind].value[0]) outage_ind += 1 month_min_soe_array = (np.array(month_min_soc_array) * energy_rating) self.min_soe_df = pd.DataFrame({'soe': month_min_soe_array}, index=opt_index) return der_list
def constraints(self, mask, sizing_for_rel=False, find_min_soe=False): """Default build constraint list method. Used by services that do not have constraints. Args: mask (DataFrame): A boolean array that is true for indices corresponding to time_series data included in the subs data set Returns: A list of constraints that corresponds the battery's physical constraints and its service constraints """ constraint_list = [] size = int(np.sum(mask)) ene_target = self.soc_target * self.effective_soe_max # this is init_ene # optimization variables ene = self.variables_dict['ene'] dis = self.variables_dict['dis'] ch = self.variables_dict['ch'] uene = self.variables_dict['uene'] udis = self.variables_dict['udis'] uch = self.variables_dict['uch'] on_c = self.variables_dict['on_c'] on_d = self.variables_dict['on_d'] start_c = self.variables_dict['start_c'] start_d = self.variables_dict['start_d'] if sizing_for_rel: constraint_list += [ cvx.Zero(ene[0] - ene_target + (self.dt * dis[0]) - (self.rte * self.dt * ch[0]) - uene[0] + (ene[0] * self.sdr * 0.01)) ] constraint_list += [ cvx.Zero(ene[1:] - ene[:-1] + (self.dt * dis[1:]) - (self.rte * self.dt * ch[1:]) - uene[1:] + (ene[1:] * self.sdr * 0.01)) ] else: # energy at beginning of time step must be the target energy value constraint_list += [cvx.Zero(ene[0] - ene_target)] # energy evolution generally for every time step constraint_list += [ cvx.Zero(ene[1:] - ene[:-1] + (self.dt * dis[:-1]) - (self.rte * self.dt * ch[:-1]) - uene[:-1] + (ene[:-1] * self.sdr * 0.01)) ] # energy at the end of the last time step (makes sure that the end of the last time step is ENE_TARGET constraint_list += [ cvx.Zero(ene_target - ene[-1] + (self.dt * dis[-1]) - (self.rte * self.dt * ch[-1]) - uene[-1] + (ene[-1] * self.sdr * 0.01)) ] # constraints on the ch/dis power constraint_list += [cvx.NonPos(ch - (on_c * self.ch_max_rated))] constraint_list += [cvx.NonPos((on_c * self.ch_min_rated) - ch)] constraint_list += [cvx.NonPos(dis - (on_d * self.dis_max_rated))] constraint_list += [cvx.NonPos((on_d * self.dis_min_rated) - dis)] # constraints on the state of energy constraint_list += [cvx.NonPos(self.effective_soe_min - ene)] constraint_list += [cvx.NonPos(ene - self.effective_soe_max)] # account for -/+ sub-dt energy -- this is the change in energy that the battery experiences as a result of energy option # if sizing for reliability if sizing_for_rel: constraint_list += [cvx.Zero(uene)] else: constraint_list += [ cvx.Zero(uene + (self.dt * udis) - (self.dt * uch * self.rte)) ] # the constraint below limits energy throughput and total discharge to less than or equal to # (number of cycles * energy capacity) per day, for technology warranty purposes # this constraint only applies when optimization window is equal to or greater than 24 hours if self.daily_cycle_limit and size >= 24: sub = mask.loc[mask] for day in sub.index.dayofyear.unique(): day_mask = (day == sub.index.dayofyear) constraint_list += [ cvx.NonPos( cvx.sum(dis[day_mask] + udis[day_mask]) * self.dt - self.ene_max_rated * self.daily_cycle_limit) ] elif self.daily_cycle_limit and size < 24: TellUser.info( 'Daily cycle limit did not apply as optimization window is less than 24 hours.' ) # note: cannot operate startup without binary if self.incl_startup and self.incl_binary: # startup variables are positive constraint_list += [cvx.NonPos(-start_c)] constraint_list += [cvx.NonPos(-start_d)] # difference between binary variables determine if started up in # previous interval constraint_list += [cvx.NonPos(cvx.diff(on_d) - start_d[1:])] constraint_list += [cvx.NonPos(cvx.diff(on_c) - start_c[1:])] return constraint_list
def solve(self, init_mkt_values: pd.Series, verbose: bool = True) -> Dict: """Solve MPO problem. Parameters ---------- init_mkt_values : pd.Series initial market value for each asset, length N. verbose : bool, optional print info, by default True Returns ------- Dict objective : float trade_values : pd.Series, (N, ) trade_weights : pd.DataFrame, (T, N) position_weights: pd.DataFrame, (T, N) """ # normalise weights to 1 nav = sum(init_mkt_values) assert nav > 0 w = cvx.Constant(init_mkt_values.values / nav) if verbose: print(f"Initial weights: {w.value}") # solve for all periods sub_problems = [] z_vars = [] posn_wgts = [] for tau in range(self.forecasts.num_periods()): z = cvx.Variable(w.shape) # next period weights = current weight + trade weights w_next = w + z kwargs = {KEY_WEIGHTS: w_next, KEY_TRADE_WEIGHTS: z, KEY_STEP: tau} obj = self.forecasts.weighted_returns_multi(w_next, start=tau) assert obj.is_dcp() # add tcosts: if self.costs is not None: # cost for all steps step_costs = [tc.eval(**kwargs) for tc in self.costs] if verbose: print(f"Added {len(step_costs)} steps of T-costs.") # validation convex_flags = [tc.is_convex() for tc in step_costs] assert np.all(convex_flags) # add cost to objective function, i.e. reduce returns / rewards obj -= cvx.sum(step_costs) # trades must self fund con_exps = [cvx.Zero(cvx.sum(z))] # add other constraints if self.constraints is not None: cons = [con.eval(**kwargs) for con in self.constraints] con_exps += cons # validate that all constraints are DCP assert np.all((c.is_dcp() for c in con_exps)) # add risk penalty term if given if self.risk_penalty is not None: if verbose: print(f"Add risk penalty term.") p = self.risk_penalty.eval(**kwargs) assert p.is_dcp(), f"p.shape = {p.shape}, DCP = {p.is_dcp()}" obj -= p # obj -= self.risk_penalty.eval(**kwargs) prob = cvx.Problem(cvx.Maximize(obj), constraints=con_exps) sub_problems.append(prob) z_vars.append(z) posn_wgts.append(w_next) w = w_next # print(f"z = {z}") # print(f"w_next = {w_next}") if self.terminal_weights is not None: sub_problems[-1].constraints += [ w_next == self.terminal_weights.values ] obj_value = sum(sub_problems).solve(solver=self.solver, verbose=verbose) trade_weights = {idx: z_vars[idx].value for idx in range(len(z_vars))} trade_weights = pd.DataFrame(trade_weights).T position_wgts = { idx: posn_wgts[idx].value for idx in range(len(posn_wgts)) } position_wgts = pd.DataFrame(position_wgts).T if verbose: print(f"Final objective value = {obj_value}") trade_weights = { idx: z_vars[idx].value for idx in range(len(z_vars)) } trade_weights = pd.DataFrame(trade_weights).T print("\nTrade weights:\n") print(trade_weights.to_string(float_format="{:.1%}".format)) print("\nTurnover:\n") print( trade_weights.abs() .sum(axis=1) .to_string(float_format="{:.1%}".format) ) print("\nPost-Trade Position weights:\n") print(position_wgts.to_string(float_format="{:.1%}".format)) # for idx in range(len(posn_wgts)): # print( # f"Step: {idx} - {posn_wgts[idx].value}, sum = {np.sum(posn_wgts[idx].value):.3e}" # ) trade_values = pd.Series( z_vars[0].value * nav, index=init_mkt_values.index ) output = dict() output["trade_values"] = trade_values output["objective"] = obj_value output["trade_weights"] = trade_weights output["position_weights"] = position_wgts return output
def constraints(self, mask): """Default build constraint list method. Used by services that do not have constraints. Args: mask (DataFrame): A boolean array that is true for indices corresponding to time_series data included in the subs data set Returns: A list of constraints that corresponds the EV requirement to collect the required energy to operate. It also allows flexibility to provide other grid services """ constraint_list = [] self.get_active_times( mask.loc[mask] ) # constructing the array that indicates whether the ev is plugged or not # print(self.plugin_times_index.iloc[0:24]) # print(self.plugout_times_index.iloc[0:24]) # print(self.unplugged_index.iloc[0:24]) # print('Ene target :' + str(self.ene_target)) # print('Charging max :' + str(self.ch_max_rated)) # print('Charging min :' + str(self.ch_min_rated)) # optimization variables ene = self.variables_dict['ene'] ch = self.variables_dict['ch'] uene = self.variables_dict['uene'] uch = self.variables_dict['uch'] on_c = self.variables_dict['on_c'] # collected energy at start time is zero for all start times constraint_list += [cvx.Zero(ene[self.plugin_times_index])] # energy evolution generally for every time step numeric_unplugged_index = pd.Series( range(len(self.unplugged_index)), index=self.unplugged_index.index).loc[self.unplugged_index] ene_ini_window = 0 if numeric_unplugged_index.iloc[ 0] == 0: # energy evolution for the EV, only during plugged times constraint_list += [ cvx.Zero(ene[numeric_unplugged_index.iloc[0]] - ene_ini_window) ] constraint_list += [ cvx.Zero(ene[list(numeric_unplugged_index.iloc[1:])] - ene[list(numeric_unplugged_index.iloc[1:] - 1)] - (self.dt * ch[list(numeric_unplugged_index.iloc[1:] - 1)])) ] # - uene[list(numeric_unplugged_index.iloc[1:]-1)])] else: constraint_list += [ cvx.Zero(ene[list(numeric_unplugged_index)] - ene[list(numeric_unplugged_index - 1)] - (self.dt * ch[list(numeric_unplugged_index - 1)])) ] # - uene[list(numeric_unplugged_index-1)])] # constraint_list += [cvx.Zero(ene[1:] - ene[:-1] - ( self.dt * ch[:-1]) - uene[:-1])] # energy at plugout times must be greater or equal to energy target numeric_plugout_time_index = pd.Series( range(len(self.plugout_times_index)), index=self.plugout_times_index.index).loc[self.plugout_times_index] # the next few lines make sure that the state of energy at the end of the chargign period is equal to the target if numeric_plugout_time_index[0] == 0: constraint_list += [ cvx.Zero(self.ene_target - ene[list(numeric_plugout_time_index.iloc[1:] - 1)] - (self.dt * ch[list(numeric_plugout_time_index.iloc[1:] - 1)])) ] # - uene[list(numeric_plugout_time_index.iloc[1:]-1)])] else: constraint_list += [ cvx.Zero(self.ene_target - ene[list(numeric_plugout_time_index - 1)] - (self.dt * ch[list(numeric_plugout_time_index - 1)])) ] # - uene[list(numeric_plugout_time_index-1)])] constraint_list += [ cvx.Zero(ene[list(numeric_plugout_time_index)] - self.ene_target) ] # constraints on the ch/dis power # make it MILP or not depending on user selection if self.incl_binary: constraint_list += [cvx.NonPos(ch - (on_c * self.ch_max_rated))] constraint_list += [cvx.NonPos((on_c * self.ch_min_rated) - ch)] else: constraint_list += [cvx.NonPos(ch - self.ch_max_rated)] constraint_list += [cvx.NonPos(-ch)] # constraints to make sure that the ev does nothing when it is unplugged constraint_list += [cvx.NonPos(ch[~self.unplugged_index])] # account for -/+ sub-dt energy -- this is the change in energy that the battery experiences as a result of energy option # constraint_list += [cvx.Zero(uene - (uch * self.dt))] constraint_list += [ cvx.Zero(uch) ] # TODO: you can set the variable to be parameters instead -HN constraint_list += [ cvx.Zero(uene) ] # TODO: you can set the variable to be parameters instead -HN return constraint_list
def set_up_optimization(self, opt_window_num, annuity_scalar=1, ignore_der_costs=False): """ Sets up and runs optimization on a subset of time in a year. Called within a loop. Args: opt_window_num (int): the optimization window number that is being solved annuity_scalar (float): a scalar value to be multiplied by any yearly cost or benefit that helps capture the cost/benefit over the entire project lifetime (only to be set iff sizing OR optimizing carrying costs) ignore_der_costs (bool): flag to indicate if we do not want to consider to economics of operating the DERs in our optimization (this flag will never be TRUE if the user indicated the desire to size the DER mix) Returns: functions (dict): functions or objectives of the optimization constraints (list): constraints that define behaviors, constrain variables, etc. that the optimization must meet sub_index (pd.Index): index of the optimization window represented in our optimization """ # used to select rows from time_series relevant to this optimization window mask = self.optimization_levels.predictive == opt_window_num sub_index = self.optimization_levels.loc[mask].index TellUser.info( f"{time.strftime('%H:%M:%S')} Running Optimization Problem starting at {sub_index[0]} hb" ) opt_var_size = int(np.sum(mask)) # set up variables self.poi.initialize_optimization_variables(opt_var_size) self.service_agg.initialize_optimization_variables(opt_var_size) # grab values from the POI that is required to know calculate objective functions and constraints load_sum, var_gen_sum, gen_sum, tot_net_ess, total_soe, agg_p_in, agg_p_out, agg_steam, agg_hotwater, agg_cold = self.poi.get_state_of_system( mask) combined_rating = self.poi.combined_discharge_rating_for_reliability() # set up controller first to collect and provide inputs to the POI funcs, consts = self.service_agg.optimization_problem( mask, load_sum, var_gen_sum, gen_sum, tot_net_ess, combined_rating, annuity_scalar) # add optimization problem portion from the POI temp_objectives, temp_consts = self.poi.optimization_problem( mask, agg_p_in, agg_p_out, agg_steam, agg_hotwater, agg_cold, annuity_scalar) if not ignore_der_costs: # don't ignore der costs funcs.update(temp_objectives) consts += temp_consts # add system requirement constraints (get the subset of data that applies to the current optimization window) discharge_min = self.system_requirements.get('discharge min') if discharge_min is not None: discharge_min_value = discharge_min.get_subset(mask) sub_discharge_min_req = cvx.Parameter(shape=opt_var_size, value=discharge_min_value, name='SysDisMinReq') consts += [cvx.NonPos(sub_discharge_min_req - agg_p_out)] discharge_max = self.system_requirements.get('discharge max') if discharge_max is not None: discharge_max_value = discharge_max.get_subset(mask) sub_discharge_max_req = cvx.Parameter(shape=opt_var_size, value=discharge_max_value, name='SysDisMaxReq') consts += [cvx.NonPos(agg_p_out - sub_discharge_max_req)] charge_max = self.system_requirements.get('charge max') if charge_max is not None: charge_max_value = charge_max.get_subset(mask) sub_charge_max_req = cvx.Parameter(shape=opt_var_size, value=charge_max_value, name='SysChMaxReq') consts += [cvx.NonPos(agg_p_in - sub_charge_max_req)] charge_min = self.system_requirements.get('charge min') if charge_min is not None: charge_min_value = charge_min.get_subset(mask) sub_charge_min_req = cvx.Parameter(shape=opt_var_size, value=charge_min_value, name='SysChMinReq') consts += [cvx.NonPos(sub_charge_min_req - agg_p_in)] energy_min = self.system_requirements.get('energy min') if energy_min is not None: energy_min_value = energy_min.get_subset(mask) sub_energy_min_req = cvx.Parameter(shape=opt_var_size, value=energy_min_value, name='SysEneMinReq') consts += [cvx.NonPos(sub_energy_min_req - total_soe)] energy_max = self.system_requirements.get('energy max') if energy_max is not None: energy_max_value = energy_max.get_subset(mask) sub_energy_max_req = cvx.Parameter(shape=opt_var_size, value=energy_max_value, name='SysEneMaxReq') consts += [cvx.NonPos(total_soe - sub_energy_max_req)] res_dis_d, res_dis_u, res_ch_d, res_ch_u, ue_prov, ue_stor, worst_ue_pro, worst_ue_sto = self.service_agg.aggregate_reservations( mask) sch_dis_d, sch_dis_u, sch_ch_d, sch_ch_u, ue_decr, ue_incr, total_dusoe = self.poi.aggregate_p_schedules( mask) # make sure P schedule matches the P reservations consts += [cvx.NonPos(res_dis_d + (-1) * sch_dis_d)] consts += [cvx.NonPos(res_dis_u + (-1) * sch_dis_u)] consts += [cvx.NonPos(res_ch_u + (-1) * sch_ch_u)] consts += [cvx.NonPos(res_ch_d + (-1) * sch_ch_d)] # match uE delta to uE reservation: energy increase consts += [cvx.Zero(ue_prov + (-1) * ue_decr)] # match uE delta to uE reservation: energy decrease consts += [cvx.Zero(ue_stor + (-1) * ue_incr)] # make sure that the net change in energy is less than the total change in system SOE consts += [cvx.NonPos(total_dusoe + (-1) * ue_prov + (-1) * ue_stor)] # require that SOE +/- worst case stays within bounds of DER mix _, _, soe_limits = self.poi.calculate_system_size() consts += [cvx.NonPos(total_soe + worst_ue_sto - soe_limits[0])] consts += [cvx.NonPos(soe_limits[1] + worst_ue_pro + (-1) * total_soe)] return funcs, consts, sub_index