def power_delta_rule(mod, p, tmp): """ This rule is only used in tuning costs, so fine to skip for linked horizon's first timepoint. """ if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[p] ) and (check_boundary_type(mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[p], boundary_type="linear") or check_boundary_type(mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[p], boundary_type="linked")): pass else: return (mod.DR_Shift_Up_MW[p, tmp] - mod.DR_Shift_Down_MW[p, tmp]) - \ (mod.DR_Shift_Up_MW[ p, mod.prev_tmp[tmp, mod.balancing_type_project[p]] ] - mod.DR_Shift_Down_MW[ p, mod.prev_tmp[tmp, mod.balancing_type_project[p]] ])
def energy_tracking_rule(mod, s, tmp): """ **Constraint Name**: GenVarStorHyb_Energy_Tracking_Constraint **Enforced Over**: GEN_VAR_STOR_HYB_OPR_TMPS The energy stored in each timepoint is equal to the energy stored in the previous timepoint minus any discharged power (adjusted for discharging efficiency and timepoint duration) plus any charged power (adjusted for charging efficiency and timepoint duration). """ if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s] ) and check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s], boundary_type="linear", ): return Constraint.Skip else: if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s] ) and check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s], boundary_type="linked", ): prev_tmp_hrs_in_tmp = mod.hrs_in_linked_tmp[0] prev_tmp_starting_energy_in_storage = ( mod.gen_var_stor_hyb_linked_starting_energy_in_storage[s, 0] ) prev_tmp_discharge = mod.gen_var_stor_hyb_linked_discharge[s, 0] prev_tmp_charge = mod.gen_var_stor_hyb_linked_charge[s, 0] else: prev_tmp_hrs_in_tmp = mod.hrs_in_tmp[ mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] prev_tmp_starting_energy_in_storage = ( mod.GenVarStorHyb_Starting_Energy_in_Storage_MWh[ s, mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] ) prev_tmp_discharge = mod.GenVarStorHyb_Discharge_MW[ s, mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] prev_tmp_charge = mod.GenVarStorHyb_Charge_MW[ s, mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] return ( mod.GenVarStorHyb_Starting_Energy_in_Storage_MWh[s, tmp] == prev_tmp_starting_energy_in_storage + prev_tmp_charge * prev_tmp_hrs_in_tmp * mod.gen_var_stor_hyb_charging_efficiency[s] - prev_tmp_discharge * prev_tmp_hrs_in_tmp / mod.gen_var_stor_hyb_discharging_efficiency[s] )
def power_delta_rule(mod, g, tmp): """ This rule is only used in tuning costs, so fine to skip for linked horizon's first timepoint. """ if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g] ) and ( check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linear" ) or check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linked" ) ): pass else: return (mod.Stor_Discharge_MW[g, tmp] - mod.Stor_Charge_MW[g, tmp]) \ - (mod.Stor_Discharge_MW[g, mod.prev_tmp[ tmp, mod.balancing_type_project[g]]] - mod.Stor_Charge_MW[g, mod.prev_tmp[ tmp, mod.balancing_type_project[g]]])
def power_delta_rule(mod, g, tmp): """ Curtailment is counted as part of the ramp here; excludes any ramping from reserve provision. This rule is only used in tuning costs, so fine to skip for linked horizon's first timepoint. """ if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g] ) and ( check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linear" ) or check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linked" ) ): pass else: return \ (mod.Capacity_MW[g, mod.period[tmp]] * mod.Availability_Derate[g, tmp] * mod.gen_var_cap_factor[g, tmp]) \ - (mod.Capacity_MW[g, mod.period[mod.prev_tmp[ tmp, mod.balancing_type_project[g]]]] * mod.Availability_Derate[g, mod.prev_tmp[ tmp, mod.balancing_type_project[g]]] * mod.gen_var_cap_factor[g, mod.prev_tmp[ tmp, mod.balancing_type_project[g]]])
def power_delta_rule(mod, g, tmp): """ Exogenously defined ramp for variable must-take generators. This rule is only used in tuning costs, so fine to skip for linked horizon's first timepoint. """ if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g] ) and (check_boundary_type(mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linear") or check_boundary_type(mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linked")): pass else: return \ (mod.Capacity_MW[g, mod.period[tmp]] * mod.Availability_Derate[g, tmp] * mod.gen_var_must_take_cap_factor[g, tmp]) \ - (mod.Capacity_MW[g, mod.period[mod.prev_tmp[ tmp, mod.balancing_type_project[g]]]] * mod.Availability_Derate[g, mod.prev_tmp[ tmp, mod.balancing_type_project[g]]] * mod.gen_var_must_take_cap_factor[g, mod.prev_tmp[ tmp, mod.balancing_type_project[g]]])
def energy_tracking_rule(mod, s, tmp): """ **Constraint Name**: Stor_Energy_Tracking_Constraint **Enforced Over**: STOR_OPR_TMPS The energy stored in each timepoint is equal to the energy stored in the previous timepoint minus any discharged power (adjusted for discharging efficiency and timepoint duration) plus any charged power (adjusted for charging efficiency and timepoint duration). """ if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s] ) and check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s], boundary_type="linear" ): return Constraint.Skip else: if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s] ) and check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[s], boundary_type="linked" ): prev_tmp_hrs_in_tmp = mod.hrs_in_linked_tmp[0] prev_tmp_starting_energy_in_storage = \ mod.stor_linked_starting_energy_in_storage[s, 0] prev_tmp_discharge = mod.stor_linked_discharge[s, 0] prev_tmp_charge = mod.stor_linked_charge[s, 0] else: prev_tmp_hrs_in_tmp = mod.hrs_in_tmp[ mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] prev_tmp_starting_energy_in_storage = \ mod.Stor_Starting_Energy_in_Storage_MWh[ s, mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] prev_tmp_discharge = \ mod.Stor_Discharge_MW[ s, mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] prev_tmp_charge = \ mod.Stor_Charge_MW[ s, mod.prev_tmp[tmp, mod.balancing_type_project[s]] ] return \ mod.Stor_Starting_Energy_in_Storage_MWh[s, tmp] \ == prev_tmp_starting_energy_in_storage \ + prev_tmp_charge * prev_tmp_hrs_in_tmp \ * mod.stor_charging_efficiency[s] \ - prev_tmp_discharge * prev_tmp_hrs_in_tmp \ / mod.stor_discharging_efficiency[s]
def power_delta_rule(mod, g, tmp): """ This rule is only used in tuning costs, so fine to skip for linked horizon's first timepoint. """ if check_if_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g] ) and (check_boundary_type(mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linear") or check_boundary_type(mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linked")): pass else: return mod.GenHydroMustTake_Gross_Power_MW[g, tmp] \ - mod.GenHydroMustTake_Gross_Power_MW[g, mod.prev_tmp[ tmp, mod.balancing_type_project[g]]]
def determine_relevant_timepoints(mod, g, tmp, min_time): """ :param mod: :param g: :param tmp: :param min_time: :return: the relevant timepoints to look at for the minimum up/down time constraints We need to figure out how far back we need to look, i.e. which timepoints we need to consider when capacity could have been turned on/off that must still be on/off in the current timepoint *t*. Any capacity that was turned on/off between t and t-1 must still be on/off in t, so timepoint *t* is a relevant timepoint. Capacity must also still be on/off if it was turned on/off less than its up/down time ago, i.e. if it was turned on/off between timepoints that are within the min up/down time from the beginning of the current timepoint. Once we reach a timepoint whose duration takes us to a point in time that is removed from the beginning of timepoint *t* by a time greater than or equal to the min_time, that timepoint is not relevant anymore and we have completed our list of relevant timepoints. In a simple case, let's assume all timepoints have durations of 1 hour. Timepoint t-1 is removed from the start of timepoint *t* by an hour, timepoint t-2 by 2 hours, timepoint t-3 by 3 hours, etc. Therefore, if if a generator has a 4-hour minimum up time and was started up in t-3 ( i.e. between t-4 and t-3), then it must still be on in the current timepoint. If it was started up in t-4, it has already been up for 4 hours by the time timepoint *t* begins, so it can be turned off. The relevant timepoints are therefore t-1, t-2, and t-3; we do not need to constrain capacity turned on/off in t-4 or farther in the past. If t-2 has duration of 2-hours, on the other hand, the total duration of the previous three timepoints would be 4 hours and the generator turned on in t-3 should therefore be allowed to turn off in the current timepoint *t*. In this case, the relevant timepoints would be t-1 and t-2. By the time we reach t-3, we will have reached the 4-hour minimum up/down time, so t-3 will not be relevant for the minimum up time constraint in timepoint *t*. """ # The first possible relevant timepoint is the current timepoint relevant_tmps = [tmp] relevant_linked_tmps = [] # The first possible linked timepoint is 0 linked_tmp = 0 # If we have already reached the first timepoint of a horizon in a # linear boundary type we'll just pass, as there are no more relevant # timepoints to add if check_if_boundary_type_and_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linear"): pass # no more relevant timepoints, keep list limited to *t* # If we have already reached the first timepoint in a linked horizon # setting, we'll immediately move on to the linked timepoints without # looking for a previous timepoint elif check_if_boundary_type_and_first_timepoint( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linked"): # Add the first linked timepoint's duration to hours_from_tmp hours_from_tmp = mod.hrs_in_linked_tmp[linked_tmp] # If we haven't exceeded the min time yet, the first linked # timepoint is relevant, so we'll add it and move on to the # next one while hours_from_tmp < min_time: relevant_linked_tmps.append(linked_tmp) # If this is the furthest linked timepoint, break out of # the linked timepoints loop; otherwise, move on to the next # linked timepoint if linked_tmp == mod.furthest_linked_tmp: break else: hours_from_tmp += mod.hrs_in_linked_tmp[linked_tmp] linked_tmp += -1 # If we haven't reached the first timepoint of a linear or linked # horizon, we'll look for the previous timepoint else: # The next possible relevant timepoint is the previous timepoint, # so we'll check its duration (if it's longer than or equal to the # minimum up/down time, we'll break out of the loop immediately) relevant_tmp = mod.prev_tmp[tmp, mod.balancing_type_project[g]] hours_from_tmp = \ mod.hrs_in_tmp[ mod.prev_tmp[tmp, mod.balancing_type_project[g]]] while hours_from_tmp < min_time: # If we haven't exceed the minimum up/down time yet, this timepoint # is relevant and we add it to our list relevant_tmps.append(relevant_tmp) # In a 'linear' horizon setting, once we reach the first # timepoint of the horizon, we break out of the loop since there # are no more timepoints to consider if check_if_boundary_type_and_first_timepoint( mod=mod, tmp=relevant_tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linear"): break # In a 'circular' horizon setting, once we reach timepoint *t*, # we break out of the loop since there are no more timepoints to # consider (we have already added all horizon timepoints as # relevant) elif check_boundary_type( mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g], boundary_type="circular") and relevant_tmp == tmp: break # TODO: only allow the first horizon of a subproblem to have # linked timepoints # In a 'linked' horizon setting, once we reach the first # timepoint of the horizon, we'll start adding the linked # timepoints until we reach the target min time elif check_if_boundary_type_and_first_timepoint( mod=mod, tmp=relevant_tmp, balancing_type=mod.balancing_type_project[g], boundary_type="linked"): # Add the first linked timepoint's duration to hours_from_tmp hours_from_tmp += mod.hrs_in_linked_tmp[linked_tmp] # If we haven't exceeded the min time yet, the first linked # timepoint is relevant, so we'll add it and move on to the # next one while hours_from_tmp < min_time: relevant_linked_tmps.append(linked_tmp) # If this is the furthest linked timepoint, break out of # the linked timepoints loop; otherwise, move on to the # next linked timepoint if linked_tmp == mod.furthest_linked_tmp: break else: hours_from_tmp += mod.hrs_in_linked_tmp[linked_tmp] linked_tmp += -1 # Break out from the outer while loop when done with the # linked timepoints break # Otherwise, we move on to the relevant timepoint's previous # timepoint and will add that timepoint's duration to # hours_from_tmp else: hours_from_tmp += \ mod.hrs_in_tmp[ mod.prev_tmp[ relevant_tmp, mod.balancing_type_project[g] ] ] relevant_tmp = mod.prev_tmp[relevant_tmp, mod.balancing_type_project[g]] return relevant_tmps, relevant_linked_tmps