Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Startup/shutdown curve #219

Merged
merged 20 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 61 additions & 9 deletions egret/model_library/unit_commitment/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,12 +387,12 @@ def warn_about_negative_demand_rule(m, b, t):
## Assert that MUT and MDT are at least 1 in the time units of the model.
## Otherwise, turn on/offs may not be enforced correctly.
def scale_min_uptime(m, g):
scaled_up_time = int(round(m.MinimumUpTime[g] / m.TimePeriodLengthHours))
scaled_up_time = int(math.ceil(m.MinimumUpTime[g] / m.TimePeriodLengthHours))
return min(max(scaled_up_time,1), value(m.NumTimePeriods))
model.ScaledMinimumUpTime = Param(model.ThermalGenerators, within=NonNegativeIntegers, initialize=scale_min_uptime)

def scale_min_downtime(m, g):
scaled_down_time = int(round(m.MinimumDownTime[g] / m.TimePeriodLengthHours))
scaled_down_time = int(math.ceil(m.MinimumDownTime[g] / m.TimePeriodLengthHours))
return min(max(scaled_down_time,1), value(m.NumTimePeriods))
model.ScaledMinimumDownTime = Param(model.ThermalGenerators, within=NonNegativeIntegers, initialize=scale_min_downtime)

Expand Down Expand Up @@ -524,12 +524,61 @@ def t0_unit_on_rule(m, g):

_add_initial_time_periods_on_off_line(model)
_verify_must_run_t0_state_consistency(model)

# For future shutdowns/startups beyond the time-horizon
model.FutureStatus = Param(model.ThermalGenerators,
bknueven marked this conversation as resolved.
Show resolved Hide resolved
within=Reals,
mutable=True,
default=0.,
initialize=thermal_gen_attrs.get('future_status', dict()))

def time_periods_since_last_shutdown_rule(m,g):
if value(m.UnitOnT0[g]):
# longer than any time-horizon we'd consider
return 10000
else:
return int(math.ceil( -value(m.UnitOnT0State[g]) / value(m.TimePeriodLengthHours) ))
model.TimePeriodsSinceShutdown = Param(model.ThermalGenerators, within=PositiveIntegers, mutable=True,
initialize=time_periods_since_last_shutdown_rule)

def time_periods_before_startup_rule(m,g):
if value(m.FutureStatus[g]) <= 0:
# longer than any time-horizon we'd consider
return 10000
else:
return int(math.ceil( value(m.FutureStatus[g]) / value(m.TimePeriodLengthHours) ))
model.TimePeriodsBeforeStartup = Param(model.ThermalGenerators, within=PositiveIntegers, mutable=True,
initialize=time_periods_before_startup_rule)

###############################################
# startup/shutdown curves for each generator. #
###############################################

def startup_curve_init_rule(m,g):
startup_curve = thermal_gens[g].get('startup_curve')
bknueven marked this conversation as resolved.
Show resolved Hide resolved
if startup_curve is None:
return ()
min_down_time = int(math.ceil(m.MinimumDownTime[g] / m.TimePeriodLengthHours))
if len(startup_curve) > min_down_time:
logger.warn(f"Truncating startup_curve longer than scaled minimum down time {min_down_time} for generator {g}")
return startup_curve[0:min_down_time]
model.StartupCurve = Set(model.ThermalGenerators, within=NonNegativeReals, ordered=True, initialize=startup_curve_init_rule)

def shutdown_curve_init_rule(m,g):
shutdown_curve = thermal_gens[g].get('shutdown_curve')
if shutdown_curve is None:
return ()
min_down_time = int(math.ceil(m.MinimumDownTime[g] / m.TimePeriodLengthHours))
if len(shutdown_curve) > min_down_time:
logger.warn(f"Truncating shutdown_curve longer than scaled minimum down time {min_down_time} for generator {g}")
return shutdown_curve[0:min_down_time]
model.ShutdownCurve = Set(model.ThermalGenerators, within=NonNegativeReals, ordered=True, initialize=shutdown_curve_init_rule)

####################################################################
# generator power output at t=0 (initial condition). units are MW. #
####################################################################

def between_limits_validator(m, v, g):
def power_generated_t0_validator(m, v, g):
t = m.TimePeriods.first()

if value(m.UnitOnT0[g]):
Expand All @@ -544,13 +593,16 @@ def between_limits_validator(m, v, g):
return True

else:
return v == 0.
# Generator was off, but could have residual power due to
# start-up/shut-down curve. Therefore, do not be too picky
# as the value doesn't affect any constraints directly
return True

model.PowerGeneratedT0 = Param(model.ThermalGenerators,
within=NonNegativeReals,
validate=between_limits_validator,
mutable=True,
initialize=thermal_gen_attrs['initial_p_output'])
within=NonNegativeReals,
validate=power_generated_t0_validator,
mutable=True,
initialize=thermal_gen_attrs['initial_p_output'])

# limits for time periods in which generators are brought on or off-line.
# must be no less than the generator minimum output.
Expand Down Expand Up @@ -663,7 +715,7 @@ def scale_shutdown_limit_t0(m, g):
else:
return temp + m.MinimumPowerOutputT0[g]
model.ScaledShutdownRampLimitT0 = Param(model.ThermalGenerators, within=NonNegativeReals, initialize=scale_shutdown_limit_t0, mutable=True)

###############################################
# startup cost parameters for each generator. #
###############################################
Expand Down
4 changes: 2 additions & 2 deletions egret/model_library/unit_commitment/power_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ def _get_pg_expr_rule(t):
def pg_expr_rule(block,b):
m = block.parent_block()
# bus b, time t (S)
return sum(m.PowerGenerated[g, t] for g in m.ThermalGeneratorsAtBus[b]) \
return sum(m.PowerGeneratedStartupShutdown[g, t] for g in m.ThermalGeneratorsAtBus[b]) \
+ sum(m.PowerOutputStorage[s, t] for s in m.StorageAtBus[b])\
- sum(m.PowerInputStorage[s, t] for s in m.StorageAtBus[b])\
+ sum(m.NondispatchablePowerUsed[g, t] for g in m.NondispatchableGeneratorsAtBus[b]) \
Expand Down Expand Up @@ -776,7 +776,7 @@ def interface_violation_cost_rule(m,t):
# Power balance at each node (S)
def power_balance(m, b, t):
# bus b, time t (S)
return sum(m.PowerGenerated[g, t] for g in m.ThermalGeneratorsAtBus[b]) \
return sum(m.PowerGeneratedStartupShutdown[g, t] for g in m.ThermalGeneratorsAtBus[b]) \
+ sum(m.PowerOutputStorage[s, t] for s in m.StorageAtBus[b])\
- sum(m.PowerInputStorage[s, t] for s in m.StorageAtBus[b])\
+ sum(m.NondispatchablePowerUsed[g, t] for g in m.NondispatchableGeneratorsAtBus[b]) \
Expand Down
51 changes: 51 additions & 0 deletions egret/model_library/unit_commitment/power_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,53 @@ def reactive_power_bounds_rule(m,g,t):
return (m.MinimumReactivePowerOutput[g,t], m.MaximumReactivePowerOutput[g,t])
model.ReactivePowerGenerated = Var(model.ThermalGenerators, model.TimePeriods, within=Reals, bounds=reactive_power_bounds_rule)

def _add_power_generated_grid(model):
bknueven marked this conversation as resolved.
Show resolved Hide resolved
assert model.InitialTime == 1

# check the status vars first to print a helpful message
if model.status_vars not in ['garver_2bin_vars', 'garver_3bin_vars', 'garver_3bin_relaxed_stop_vars', 'ALS_state_transition_vars']:
for s in model.StartupCurve.values():
if len(s) > 0:
raise RuntimeError(f"Status variable formulation {model.status_vars} is not compatible with startup curves")
for s in model.ShutdownCurve.values():
if len(s) > 0:
raise RuntimeError(f"Status variable formulation {model.status_vars} is not compatible with shutdown curves")

## if we're here, then we can use these 1-bin models
def power_generated_grid_expr_rule(m, g, t):
linear_vars, linear_coefs = m._get_power_generated_lists(m,g,t)
linear_expr = get_linear_expr(m.UnitOn)
return linear_expr(linear_vars=linear_vars, linear_coefs=linear_coefs)

else:
def power_generated_grid_expr_rule(m, g, t):
startup_curve = m.StartupCurve[g]
shutdown_curve = m.ShutdownCurve[g]
time_periods_before_startup = value(m.TimePeriodsBeforeStartup[g])
time_periods_since_shutdown = value(m.TimePeriodsSinceShutdown[g])

future_past_production = 0.
bknueven marked this conversation as resolved.
Show resolved Hide resolved
future_startup_power_index = time_periods_before_startup + m.NumTimePeriods - t
if future_startup_power_index <= len(startup_curve):
future_past_production += startup_curve[future_startup_power_index]

past_shutdown_power_index = time_periods_since_shutdown + t
if past_shutdown_power_index <= len(shutdown_curve):
future_past_production += shutdown_curve[past_shutdown_power_index]

linear_vars, linear_coefs = m._get_power_generated_lists(m,g,t)
for startup_idx in range(1, min( len(startup_curve)+1, m.NumTimePeriods+1-t )):
linear_vars.append(m.UnitStart[g,t+startup_idx])
linear_coefs.append(startup_curve[startup_idx])
for shutdown_idx in range(1, min( len(shutdown_curve)+1, t+1 )):
linear_vars.append(m.UnitStop[g,t-shutdown_idx+1])
linear_coefs.append(shutdown_curve[shutdown_idx])
linear_expr = get_linear_expr(m.UnitOn, m.UnitStart, m.UnitStop)
return linear_expr(linear_vars=linear_vars, linear_coefs=linear_coefs, constant=future_past_production)

model.PowerGeneratedStartupShutdown = Expression(model.ThermalGenerators, model.TimePeriods,
jeanpaulwatson marked this conversation as resolved.
Show resolved Hide resolved
rule=power_generated_grid_expr_rule)

## garver/ME power variables (above minimum)
@add_model_attr(component_name, requires = {'data_loader': None, 'status_vars': None})
def garver_power_vars(model):
Expand Down Expand Up @@ -56,6 +103,8 @@ def power_generated_expr_rule(m, g, t):
model._get_power_generated_lists = lambda m,g,t : ([m.PowerGeneratedAboveMinimum[g,t], m.UnitOn[g,t]], [1., m.MinimumPowerOutput[g,t]])
model._get_negative_power_generated_lists = lambda m,g,t : ([m.PowerGeneratedAboveMinimum[g,t], m.UnitOn[g,t]], [-1., -m.MinimumPowerOutput[g,t]])

_add_power_generated_grid(model)

return

## carrion arroyo power variables (above minimum)
Expand Down Expand Up @@ -83,4 +132,6 @@ def power_generated_expr_rule(m, g, t):
model._get_power_generated_above_minimum_lists = lambda m,g,t : ([m.PowerGenerated[g,t], m.UnitOn[g,t]], [1., -m.MinimumPowerOutput[g,t]])
model._get_negative_power_generated_above_minimum_lists = lambda m,g,t : ([m.PowerGenerated[g,t], m.UnitOn[g,t]], [-1., m.MinimumPowerOutput[g,t]])

_add_power_generated_grid(model)

return
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def security_constraint_model(model):
raise Exception('Unrecognized generator {} for security constraint {} at time {}'.format(g,s,time_keys[i]))

## TODO: Make LinearExpression?
b.pgSecurityExpression[s] = sum( val*model.PowerGenerated[g,t] for g,val in thermal_gen_coefs.items() ) \
b.pgSecurityExpression[s] = sum( val*model.PowerGeneratedStartupShutdown[g,t] for g,val in thermal_gen_coefs.items() ) \
+ sum( val*model.NondispatchablePowerUsed[g,t] for g,val in renewable_gen_coefs.items() ) \
+ sum( val*(model.PowerOutputStorage[s,t]-model.PowerInputStorage[s,t]) for s,val in coefs['storage'].items())

Expand Down
2 changes: 1 addition & 1 deletion egret/models/tests/test_unit_commitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def test_super_tight_uc_model():
_test_uc_model(create_super_tight_unit_commitment_model, relax=True, test_objvals=lp_obj_list)

def test_uc_runner():
test_names = ['tiny_uc_{}'.format(i) for i in range(1,10+1)]
test_names = ['tiny_uc_{}'.format(i) for i in range(1,11+1)]
for test_name in test_names:
input_json_file_name = os.path.join(current_dir, 'uc_test_instances', test_name+'.json')
md_in = ModelData(input_json_file_name)
Expand Down
Loading