Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
# 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)
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
    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