def build_sets(model_data, backend_model): for coord_name, coord_vals in model_data.coords.items(): setattr( backend_model, coord_name, po.Set(initialize=coord_vals.to_index(), ordered=True), )
def add_buy_sell_price(m): # Sets m.com_sell = pyomo.Set(within=m.com, initialize=commodity_subset(m.com_tuples, 'Sell'), doc='Commodities that can be sold') m.com_buy = pyomo.Set(within=m.com, initialize=commodity_subset(m.com_tuples, 'Buy'), doc='Commodities that can be purchased') # Variables m.e_co_sell = pyomo.Var( m.tm, m.com_tuples, within=pyomo.NonNegativeReals, doc='Use of sell commodity source (MW) per timestep') m.e_co_buy = pyomo.Var(m.tm, m.com_tuples, within=pyomo.NonNegativeReals, doc='Use of buy commodity source (MW) per timestep') # Rules m.res_sell_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_sell_step_rule, doc='sell commodity output per step <= commodity.maxperstep') m.res_sell_total = pyomo.Constraint( m.com_tuples, rule=res_sell_total_rule, doc='total sell commodity output <= commodity.max') m.res_buy_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_buy_step_rule, doc='buy commodity output per step <= commodity.maxperstep') m.res_buy_total = pyomo.Constraint( m.com_tuples, rule=res_buy_total_rule, doc='total buy commodity output <= commodity.max') m.res_sell_buy_symmetry = pyomo.Constraint( m.pro_input_tuples, rule=res_sell_buy_symmetry_rule, doc='power connection capacity must be symmetric in both directions') return m
def test_invalid(self): pyomo_model = po.ConcreteModel() pyomo_model.new_set = po.Set(initialize=["a", "b"]) pyomo_model.new_param = po.Param( pyomo_model.new_set, initialize={"a": 1}, mutable=True, within=po.NonNegativeReals, ) assert invalid(pyomo_model.new_param["a"]) is False assert invalid(pyomo_model.new_param["b"]) is True
def create_model(data, dt=1, timesteps=None, dual=False): """Create a pyomo ConcreteModel urbs object from given input data. Args: data: a dict of 6 DataFrames with the keys 'commodity', 'process', 'transmission', 'storage', 'demand' and 'supim'. dt: timestep duration in hours (default: 1) timesteps: optional list of timesteps, default: demand timeseries dual: set True to add dual variables to model (slower); default: False Returns: a pyomo ConcreteModel object """ # Optional if not timesteps: timesteps = data['demand'].index.tolist() m = pyomo_model_prep(data, timesteps) # preparing pyomo model m.name = 'urbs' m.created = datetime.now().strftime('%Y%m%dT%H%M') m._data = data # Parameters # weight = length of year (hours) / length of simulation (hours) # weight scales costs and emissions from length of simulation to a full # year, making comparisons among cost types (invest is annualized, fixed # costs are annual by default, variable costs are scaled by weight) and # among different simulation durations meaningful. m.weight = pyomo.Param( initialize=float(8760) / (len(m.timesteps) * dt), doc='Pre-factor for variable costs and emissions for an annual result') # dt = spacing between timesteps. Required for storage equation that # converts between energy (storage content, e_sto_con) and power (all other # quantities that start with "e_") m.dt = pyomo.Param(initialize=dt, doc='Time step duration (in hours), default: 1') # Sets # ==== # Syntax: m.{name} = Set({domain}, initialize={values}) # where name: set name # domain: set domain for tuple sets, a cartesian set product # values: set values, a list or array of element tuples # generate ordered time step sets m.t = pyomo.Set(initialize=m.timesteps, ordered=True, doc='Set of timesteps') # modelled (i.e. excluding init time step for storage) time steps m.tm = pyomo.Set(within=m.t, initialize=m.timesteps[1:], ordered=True, doc='Set of modelled timesteps') # modelled Demand Side Management time steps (downshift): # downshift effective in tt to compensate for upshift in t m.tt = pyomo.Set(within=m.t, initialize=m.timesteps[1:], ordered=True, doc='Set of additional DSM time steps') # site (e.g. north, middle, south...) m.sit = pyomo.Set( initialize=m.commodity.index.get_level_values('Site').unique(), doc='Set of sites') # commodity (e.g. solar, wind, coal...) m.com = pyomo.Set( initialize=m.commodity.index.get_level_values('Commodity').unique(), doc='Set of commodities') # commodity type (i.e. SupIm, Demand, Stock, Env) m.com_type = pyomo.Set( initialize=m.commodity.index.get_level_values('Type').unique(), doc='Set of commodity types') # process (e.g. Wind turbine, Gas plant, Photovoltaics...) m.pro = pyomo.Set( initialize=m.process.index.get_level_values('Process').unique(), doc='Set of conversion processes') # tranmission (e.g. hvac, hvdc, pipeline...) m.tra = pyomo.Set(initialize=m.transmission.index.get_level_values( 'Transmission').unique(), doc='Set of transmission technologies') # storage (e.g. hydrogen, pump storage) m.sto = pyomo.Set( initialize=m.storage.index.get_level_values('Storage').unique(), doc='Set of storage technologies') # cost_type m.cost_type = pyomo.Set(initialize=[ 'Invest', 'Fixed', 'Variable', 'Fuel', 'Revenue', 'Purchase', 'Environmental' ], doc='Set of cost types (hard-coded)') # tuple sets m.com_tuples = pyomo.Set( within=m.sit * m.com * m.com_type, initialize=m.commodity.index, doc='Combinations of defined commodities, e.g. (Mid,Elec,Demand)') m.pro_tuples = pyomo.Set( within=m.sit * m.pro, initialize=m.process.index, doc='Combinations of possible processes, e.g. (North,Coal plant)') m.tra_tuples = pyomo.Set( within=m.sit * m.sit * m.tra * m.com, initialize=m.transmission.index, doc='Combinations of possible transmissions, e.g. ' '(South,Mid,hvac,Elec)') m.sto_tuples = pyomo.Set( within=m.sit * m.sto * m.com, initialize=m.storage.index, doc='Combinations of possible storage by site, e.g. (Mid,Bat,Elec)') m.dsm_site_tuples = pyomo.Set( within=m.sit * m.com, initialize=m.dsm.index, doc='Combinations of possible dsm by site, e.g. (Mid, Elec)') m.dsm_down_tuples = pyomo.Set( within=m.tm * m.tm * m.sit * m.com, initialize=[(t, tt, site, commodity) for (t, tt, site, commodity) in dsm_down_time_tuples( m.timesteps[1:], m.dsm_site_tuples, m)], doc='Combinations of possible dsm_down combinations, e.g. ' '(5001,5003,Mid,Elec)') # commodity type subsets m.com_supim = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'SupIm'), doc='Commodities that have intermittent (timeseries) input') m.com_stock = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Stock'), doc='Commodities that can be purchased at some site(s)') m.com_sell = pyomo.Set(within=m.com, initialize=commodity_subset(m.com_tuples, 'Sell'), doc='Commodities that can be sold') m.com_buy = pyomo.Set(within=m.com, initialize=commodity_subset(m.com_tuples, 'Buy'), doc='Commodities that can be purchased') m.com_demand = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Demand'), doc='Commodities that have a demand (implies timeseries)') m.com_env = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Env'), doc='Commodities that (might) have a maximum creation limit') # process tuples for area rule m.pro_area_tuples = pyomo.Set( within=m.sit * m.pro, initialize=m.proc_area.index, doc='Processes and Sites with area Restriction') # process input/output m.pro_input_tuples = pyomo.Set( within=m.sit * m.pro * m.com, initialize=[(site, process, commodity) for (site, process) in m.pro_tuples for (pro, commodity) in m.r_in.index if process == pro], doc='Commodities consumed by process by site, e.g. (Mid,PV,Solar)') m.pro_output_tuples = pyomo.Set( within=m.sit * m.pro * m.com, initialize=[(site, process, commodity) for (site, process) in m.pro_tuples for (pro, commodity) in m.r_out.index if process == pro], doc='Commodities produced by process by site, e.g. (Mid,PV,Elec)') # process tuples for maximum gradient feature m.pro_maxgrad_tuples = pyomo.Set( within=m.sit * m.pro, initialize=[(sit, pro) for (sit, pro) in m.pro_tuples if m.process.loc[sit, pro]['max-grad'] < 1.0 / dt], doc='Processes with maximum gradient smaller than timestep length') # process tuples for partial feature m.pro_partial_tuples = pyomo.Set(within=m.sit * m.pro, initialize=[ (site, process) for (site, process) in m.pro_tuples for (pro, _) in m.r_in_min_fraction.index if process == pro ], doc='Processes with partial input') m.pro_partial_input_tuples = pyomo.Set( within=m.sit * m.pro * m.com, initialize=[(site, process, commodity) for (site, process) in m.pro_partial_tuples for (pro, commodity) in m.r_in_min_fraction.index if process == pro], doc='Commodities with partial input ratio, e.g. (Mid,Coal PP,Coal)') m.pro_partial_output_tuples = pyomo.Set( within=m.sit * m.pro * m.com, initialize=[(site, process, commodity) for (site, process) in m.pro_partial_tuples for (pro, commodity) in m.r_out_min_fraction.index if process == pro], doc='Commodities with partial input ratio, e.g. (Mid,Coal PP,CO2)') # process tuples for time variable efficiency m.pro_timevar_output_tuples = pyomo.Set( within=m.sit * m.pro * m.com, initialize=[(site, process, commodity) for (site, process) in m.eff_factor.columns.values for (pro, commodity) in m.r_out.index if process == pro], doc='Outputs of processes with time dependent efficiency') # Variables # costs m.costs = pyomo.Var(m.cost_type, within=pyomo.Reals, doc='Costs by type (EUR/a)') # commodity m.e_co_stock = pyomo.Var( m.tm, m.com_tuples, within=pyomo.NonNegativeReals, doc='Use of stock commodity source (MW) per timestep') m.e_co_sell = pyomo.Var( m.tm, m.com_tuples, within=pyomo.NonNegativeReals, doc='Use of sell commodity source (MW) per timestep') m.e_co_buy = pyomo.Var(m.tm, m.com_tuples, within=pyomo.NonNegativeReals, doc='Use of buy commodity source (MW) per timestep') # process m.cap_pro = pyomo.Var(m.pro_tuples, within=pyomo.NonNegativeReals, doc='Total process capacity (MW)') m.cap_pro_new = pyomo.Var(m.pro_tuples, within=pyomo.NonNegativeReals, doc='New process capacity (MW)') m.tau_pro = pyomo.Var(m.t, m.pro_tuples, within=pyomo.NonNegativeReals, doc='Power flow (MW) through process') m.e_pro_in = pyomo.Var( m.tm, m.pro_tuples, m.com, within=pyomo.NonNegativeReals, doc='Power flow of commodity into process (MW) per timestep') m.e_pro_out = pyomo.Var(m.tm, m.pro_tuples, m.com, within=pyomo.NonNegativeReals, doc='Power flow out of process (MW) per timestep') # transmission m.cap_tra = pyomo.Var(m.tra_tuples, within=pyomo.NonNegativeReals, doc='Total transmission capacity (MW)') m.cap_tra_new = pyomo.Var(m.tra_tuples, within=pyomo.NonNegativeReals, doc='New transmission capacity (MW)') m.e_tra_in = pyomo.Var( m.tm, m.tra_tuples, within=pyomo.NonNegativeReals, doc='Power flow into transmission line (MW) per timestep') m.e_tra_out = pyomo.Var( m.tm, m.tra_tuples, within=pyomo.NonNegativeReals, doc='Power flow out of transmission line (MW) per timestep') # storage m.cap_sto_c = pyomo.Var(m.sto_tuples, within=pyomo.NonNegativeReals, doc='Total storage size (MWh)') m.cap_sto_c_new = pyomo.Var(m.sto_tuples, within=pyomo.NonNegativeReals, doc='New storage size (MWh)') m.cap_sto_p = pyomo.Var(m.sto_tuples, within=pyomo.NonNegativeReals, doc='Total storage power (MW)') m.cap_sto_p_new = pyomo.Var(m.sto_tuples, within=pyomo.NonNegativeReals, doc='New storage power (MW)') m.e_sto_in = pyomo.Var(m.tm, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Power flow into storage (MW) per timestep') m.e_sto_out = pyomo.Var(m.tm, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Power flow out of storage (MW) per timestep') m.e_sto_con = pyomo.Var(m.t, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Energy content of storage (MWh) in timestep') # demand side management m.dsm_up = pyomo.Var(m.tm, m.dsm_site_tuples, within=pyomo.NonNegativeReals, doc='DSM upshift') m.dsm_down = pyomo.Var(m.dsm_down_tuples, within=pyomo.NonNegativeReals, doc='DSM downshift') # Equation declarations # equation bodies are defined in separate functions, referred to here by # their name in the "rule" keyword. # commodity m.res_vertex = pyomo.Constraint( m.tm, m.com_tuples, rule=res_vertex_rule, doc='storage + transmission + process + source + buy - sell == demand') m.res_stock_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_stock_step_rule, doc='stock commodity input per step <= commodity.maxperstep') m.res_stock_total = pyomo.Constraint( m.com_tuples, rule=res_stock_total_rule, doc='total stock commodity input <= commodity.max') m.res_sell_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_sell_step_rule, doc='sell commodity output per step <= commodity.maxperstep') m.res_sell_total = pyomo.Constraint( m.com_tuples, rule=res_sell_total_rule, doc='total sell commodity output <= commodity.max') m.res_buy_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_buy_step_rule, doc='buy commodity output per step <= commodity.maxperstep') m.res_buy_total = pyomo.Constraint( m.com_tuples, rule=res_buy_total_rule, doc='total buy commodity output <= commodity.max') m.res_env_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_env_step_rule, doc='environmental output per step <= commodity.maxperstep') m.res_env_total = pyomo.Constraint( m.com_tuples, rule=res_env_total_rule, doc='total environmental commodity output <= commodity.max') # process m.def_process_capacity = pyomo.Constraint( m.pro_tuples, rule=def_process_capacity_rule, doc='total process capacity = inst-cap + new capacity') m.def_process_input = pyomo.Constraint( m.tm, m.pro_input_tuples - m.pro_partial_input_tuples, rule=def_process_input_rule, doc='process input = process throughput * input ratio') m.def_process_output = pyomo.Constraint( m.tm, (m.pro_output_tuples - m.pro_partial_output_tuples - m.pro_timevar_output_tuples), rule=def_process_output_rule, doc='process output = process throughput * output ratio') m.def_intermittent_supply = pyomo.Constraint( m.tm, m.pro_input_tuples, rule=def_intermittent_supply_rule, doc='process output = process capacity * supim timeseries') m.res_process_throughput_by_capacity = pyomo.Constraint( m.tm, m.pro_tuples, rule=res_process_throughput_by_capacity_rule, doc='process throughput <= total process capacity') m.res_process_maxgrad_lower = pyomo.Constraint( m.tm, m.pro_maxgrad_tuples, rule=res_process_maxgrad_lower_rule, doc='throughput may not decrease faster than maximal gradient') m.res_process_maxgrad_upper = pyomo.Constraint( m.tm, m.pro_maxgrad_tuples, rule=res_process_maxgrad_upper_rule, doc='throughput may not increase faster than maximal gradient') m.res_process_capacity = pyomo.Constraint( m.pro_tuples, rule=res_process_capacity_rule, doc='process.cap-lo <= total process capacity <= process.cap-up') m.res_area = pyomo.Constraint( m.sit, rule=res_area_rule, doc='used process area <= total process area') m.res_sell_buy_symmetry = pyomo.Constraint( m.pro_input_tuples, rule=res_sell_buy_symmetry_rule, doc='power connection capacity must be symmetric in both directions') m.res_throughput_by_capacity_min = pyomo.Constraint( m.tm, m.pro_partial_tuples, rule=res_throughput_by_capacity_min_rule, doc='cap_pro * min-fraction <= tau_pro') m.def_partial_process_input = pyomo.Constraint( m.tm, m.pro_partial_input_tuples, rule=def_partial_process_input_rule, doc='e_pro_in = ' ' cap_pro * min_fraction * (r - R) / (1 - min_fraction)' ' + tau_pro * (R - min_fraction * r) / (1 - min_fraction)') m.def_partial_process_output = pyomo.Constraint( m.tm, (m.pro_partial_output_tuples - (m.pro_partial_output_tuples & m.pro_timevar_output_tuples)), rule=def_partial_process_output_rule, doc='e_pro_out = ' ' cap_pro * min_fraction * (r - R) / (1 - min_fraction)' ' + tau_pro * (R - min_fraction * r) / (1 - min_fraction)') m.def_process_timevar_output = pyomo.Constraint( m.tm, (m.pro_timevar_output_tuples - (m.pro_partial_output_tuples & m.pro_timevar_output_tuples)), rule=def_pro_timevar_output_rule, doc='e_pro_out = tau_pro * r_out * eff_factor') m.def_process_partial_timevar_output = pyomo.Constraint( m.tm, m.pro_partial_output_tuples & m.pro_timevar_output_tuples, rule=def_pro_partial_timevar_output_rule, doc='e_pro_out = tau_pro * r_out * eff_factor') # transmission m.def_transmission_capacity = pyomo.Constraint( m.tra_tuples, rule=def_transmission_capacity_rule, doc='total transmission capacity = inst-cap + new capacity') m.def_transmission_output = pyomo.Constraint( m.tm, m.tra_tuples, rule=def_transmission_output_rule, doc='transmission output = transmission input * efficiency') m.res_transmission_input_by_capacity = pyomo.Constraint( m.tm, m.tra_tuples, rule=res_transmission_input_by_capacity_rule, doc='transmission input <= total transmission capacity') m.res_transmission_capacity = pyomo.Constraint( m.tra_tuples, rule=res_transmission_capacity_rule, doc='transmission.cap-lo <= total transmission capacity <= ' 'transmission.cap-up') m.res_transmission_symmetry = pyomo.Constraint( m.tra_tuples, rule=res_transmission_symmetry_rule, doc='total transmission capacity must be symmetric in both directions') # storage m.def_storage_state = pyomo.Constraint( m.tm, m.sto_tuples, rule=def_storage_state_rule, doc='storage[t] = (1 - sd) * storage[t-1] + in * eff_i - out / eff_o') m.def_storage_power = pyomo.Constraint( m.sto_tuples, rule=def_storage_power_rule, doc='storage power = inst-cap + new power') m.def_storage_capacity = pyomo.Constraint( m.sto_tuples, rule=def_storage_capacity_rule, doc='storage capacity = inst-cap + new capacity') m.res_storage_input_by_power = pyomo.Constraint( m.tm, m.sto_tuples, rule=res_storage_input_by_power_rule, doc='storage input <= storage power') m.res_storage_output_by_power = pyomo.Constraint( m.tm, m.sto_tuples, rule=res_storage_output_by_power_rule, doc='storage output <= storage power') m.res_storage_state_by_capacity = pyomo.Constraint( m.t, m.sto_tuples, rule=res_storage_state_by_capacity_rule, doc='storage content <= storage capacity') m.res_storage_power = pyomo.Constraint( m.sto_tuples, rule=res_storage_power_rule, doc='storage.cap-lo-p <= storage power <= storage.cap-up-p') m.res_storage_capacity = pyomo.Constraint( m.sto_tuples, rule=res_storage_capacity_rule, doc='storage.cap-lo-c <= storage capacity <= storage.cap-up-c') m.res_initial_and_final_storage_state = pyomo.Constraint( m.t, m.sto_tuples, rule=res_initial_and_final_storage_state_rule, doc='storage content initial == and final >= storage.init * capacity') # costs m.def_costs = pyomo.Constraint(m.cost_type, rule=def_costs_rule, doc='main cost function by cost type') m.obj = pyomo.Objective(rule=obj_rule, sense=pyomo.minimize, doc='minimize(cost = sum of all cost types)') # demand side management m.def_dsm_variables = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=def_dsm_variables_rule, doc='DSMup * efficiency factor n == DSMdo (summed)') m.res_dsm_upward = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_upward_rule, doc='DSMup <= Cup (threshold capacity of DSMup)') m.res_dsm_downward = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_downward_rule, doc='DSMdo (summed) <= Cdo (threshold capacity of DSMdo)') m.res_dsm_maximum = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_maximum_rule, doc='DSMup + DSMdo (summed) <= max(Cup,Cdo)') m.res_dsm_recovery = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_recovery_rule, doc='DSMup(t, t + recovery time R) <= Cup * delay time L') m.res_global_co2_limit = pyomo.Constraint( rule=res_global_co2_limit_rule, doc='total co2 commodity output <= Global CO2 limit') if dual: m.dual = pyomo.Suffix(direction=pyomo.Suffix.IMPORT) return m
def group_set(group_type): try: group = model.config_model.group_fraction[group_type].keys() except (TypeError, KeyError): group = [] return po.Set(initialize=group)
def generate_model(model_data): """ Generate a Pyomo model. """ backend_model = po.ConcreteModel() # Sets for coord in list(model_data.coords): set_data = list(model_data.coords[coord].data) # Ensure that time steps are pandas.Timestamp objects if isinstance(set_data[0], np.datetime64): set_data = pd.to_datetime(set_data) setattr(backend_model, coord, po.Set(initialize=set_data, ordered=True)) # "Parameters" model_data_dict = { "data": { k: v.to_series().dropna().replace("inf", np.inf).to_dict() for k, v in model_data.data_vars.items() if v.attrs["is_result"] == 0 or v.attrs.get("operate_param", 0) == 1 }, "dims": { k: v.dims for k, v in model_data.data_vars.items() if v.attrs["is_result"] == 0 or v.attrs.get("operate_param", 0) == 1 }, "sets": list(model_data.coords), "attrs": {k: v for k, v in model_data.attrs.items() if k is not "defaults"}, } # Dims in the dict's keys are ordered as in model_data, which is enforced # in model_data generation such that timesteps are always last and the # remainder of dims are in alphabetic order backend_model.__calliope_model_data = model_data_dict backend_model.__calliope_defaults = AttrDict.from_yaml_string( model_data.attrs["defaults"]) backend_model.__calliope_run_config = AttrDict.from_yaml_string( model_data.attrs["run_config"]) for k, v in model_data_dict["data"].items(): if k in backend_model.__calliope_defaults.keys(): setattr( backend_model, k, po.Param(*[ getattr(backend_model, i) for i in model_data_dict["dims"][k] ], initialize=v, mutable=True, default=backend_model.__calliope_defaults[k]), ) # In operate mode, e.g. energy_cap is a parameter, not a decision variable, # so add those in. elif (backend_model.__calliope_run_config["mode"] == "operate" and model_data[k].attrs.get("operate_param") == 1): setattr( backend_model, k, po.Param( getattr(backend_model, model_data_dict["dims"][k][0]), initialize=v, mutable=True, ), ) else: # no default value to look up setattr( backend_model, k, po.Param(*[ getattr(backend_model, i) for i in model_data_dict["dims"][k] ], initialize=v, mutable=True), ) for option_name, option_val in backend_model.__calliope_run_config[ "objective_options"].items(): if option_name == "cost_class": objective_cost_class = { k: v for k, v in option_val.items() if k in backend_model.costs } backend_model.objective_cost_class = po.Param( backend_model.costs, initialize=objective_cost_class, mutable=True) else: setattr(backend_model, "objective_" + option_name, option_val) # Variables load_function( "calliope.backend.pyomo.variables.initialize_decision_variables")( backend_model) # Constraints constraints_to_add = [ i.split(".py")[0] for i in os.listdir(constraints.__path__[0]) if not i.startswith("_") and not i.startswith(".") ] # The list is sorted to ensure that some constraints are added after pyomo # expressions have been created in other constraint files. # Ordering is given by the number assigned to the variable ORDER within each # file (higher number = added later). try: constraints_to_add.sort(key=lambda x: load_function( "calliope.backend.pyomo.constraints." + x + ".ORDER")) except AttributeError as e: raise AttributeError( "{}. This attribute must be set to an integer value based " "on the order in which the constraints in the file {}.py should be " "loaded relative to constraints in other constraint files. If order " "does not matter, set ORDER to a value of 10.".format( e.args[0], e.args[0].split(".")[-1].split("'")[0])) logger.info("constraints are loaded in the following order: {}".format( constraints_to_add)) for c in constraints_to_add: load_function("calliope.backend.pyomo.constraints." + c + ".load_constraints")(backend_model) # FIXME: Optional constraints # optional_constraints = model_data.attrs['constraints'] # if optional_constraints: # for c in optional_constraints: # self.add_constraint(load_function(c)) # Objective function # FIXME re-enable loading custom objectives # fetch objective function by name, pass through objective options # if they are present objective_function = ("calliope.backend.pyomo.objective." + backend_model.__calliope_run_config["objective"]) load_function(objective_function)(backend_model) return backend_model
def create_model(data, dt=1, timesteps=None, objective='cost', dual=True): """Create a pyomo ConcreteModel urbs object from given input data. Args: - data: a dict of up to 12 - dt: timestep duration in hours (default: 1) - timesteps: optional list of timesteps, default: demand timeseries - objective: Either "cost" or "CO2" for choice of objective function, default: "cost" - dual: set True to add dual variables to model output (marginally slower), default: True Returns: a pyomo ConcreteModel object """ # Optional if not timesteps: timesteps = data['demand'].index.tolist() m = pyomo_model_prep(data, timesteps) # preparing pyomo model m.name = 'urbs' m.created = datetime.now().strftime('%Y%m%dT%H%M') m._data = data # Parameters # weight = length of year (hours) / length of simulation (hours) # weight scales costs and emissions from length of simulation to a full # year, making comparisons among cost types (invest is annualized, fixed # costs are annual by default, variable costs are scaled by weight) and # among different simulation durations meaningful. m.weight = pyomo.Param( initialize=float(8760) / (len(m.timesteps) * dt), doc='Pre-factor for variable costs and emissions for an annual result') # dt = spacing between timesteps. Required for storage equation that # converts between energy (storage content, e_sto_con) and power (all other # quantities that start with "e_") m.dt = pyomo.Param(initialize=dt, doc='Time step duration (in hours), default: 1') # import objective function information m.obj = pyomo.Param( initialize=objective, doc='Specification of minimized quantity, default: "cost"') # Sets # ==== # Syntax: m.{name} = Set({domain}, initialize={values}) # where name: set name # domain: set domain for tuple sets, a cartesian set product # values: set values, a list or array of element tuples # generate ordered time step sets m.t = pyomo.Set(initialize=m.timesteps, ordered=True, doc='Set of timesteps') # modelled (i.e. excluding init time step for storage) time steps m.tm = pyomo.Set(within=m.t, initialize=m.timesteps[1:], ordered=True, doc='Set of modelled timesteps') # support timeframes (e.g. 2020, 2030...) indexlist = set() for key in m.commodity_dict["price"]: indexlist.add(tuple(key)[0]) m.stf = pyomo.Set(initialize=indexlist, doc='Set of modeled support timeframes (e.g. years)') # site (e.g. north, middle, south...) indexlist = set() for key in m.commodity_dict["price"]: indexlist.add(tuple(key)[1]) m.sit = pyomo.Set(initialize=indexlist, doc='Set of sites') # commodity (e.g. solar, wind, coal...) indexlist = set() for key in m.commodity_dict["price"]: indexlist.add(tuple(key)[2]) m.com = pyomo.Set(initialize=indexlist, doc='Set of commodities') # commodity type (i.e. SupIm, Demand, Stock, Env) indexlist = set() for key in m.commodity_dict["price"]: indexlist.add(tuple(key)[3]) m.com_type = pyomo.Set(initialize=indexlist, doc='Set of commodity types') # process (e.g. Wind turbine, Gas plant, Photovoltaics...) indexlist = set() for key in m.process_dict["inv-cost"]: indexlist.add(tuple(key)[2]) m.pro = pyomo.Set(initialize=indexlist, doc='Set of conversion processes') # cost_type m.cost_type = pyomo.Set(initialize=m.cost_type_list, doc='Set of cost types (hard-coded)') # tuple sets m.sit_tuples = pyomo.Set( within=m.stf * m.sit, initialize=tuple(m.site_dict["area"].keys()), doc='Combinations of support timeframes and sites') m.com_tuples = pyomo.Set( within=m.stf * m.sit * m.com * m.com_type, initialize=tuple(m.commodity_dict["price"].keys()), doc='Combinations of defined commodities, e.g. (2018,Mid,Elec,Demand)') m.pro_tuples = pyomo.Set( within=m.stf * m.sit * m.pro, initialize=tuple(m.process_dict["inv-cost"].keys()), doc='Combinations of possible processes, e.g. (2018,North,Coal plant)') m.com_stock = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Stock'), doc='Commodities that can be purchased at some site(s)') if m.mode['int']: # tuples for operational status of technologies m.operational_pro_tuples = pyomo.Set( within=m.sit * m.pro * m.stf * m.stf, initialize=[(sit, pro, stf, stf_later) for (sit, pro, stf, stf_later) in op_pro_tuples(m.pro_tuples, m)], doc='Processes that are still operational through stf_later' '(and the relevant years following), if built in stf' 'in stf.') # tuples for rest lifetime of installed capacities of technologies m.inst_pro_tuples = pyomo.Set( within=m.sit * m.pro * m.stf, initialize=[(sit, pro, stf) for (sit, pro, stf) in inst_pro_tuples(m)], doc='Installed processes that are still operational through stf') # commodity type subsets m.com_supim = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'SupIm'), doc='Commodities that have intermittent (timeseries) input') m.com_demand = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Demand'), doc='Commodities that have a demand (implies timeseries)') m.com_env = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Env'), doc='Commodities that (might) have a maximum creation limit') # process tuples for area rule m.pro_area_tuples = pyomo.Set( within=m.stf * m.sit * m.pro, initialize=tuple(m.proc_area_dict.keys()), doc='Processes and Sites with area Restriction') # process input/output m.pro_input_tuples = pyomo.Set( within=m.stf * m.sit * m.pro * m.com, initialize=[(stf, site, process, commodity) for (stf, site, process) in m.pro_tuples for (s, pro, commodity) in tuple(m.r_in_dict.keys()) if process == pro and s == stf], doc='Commodities consumed by process by site,' 'e.g. (2020,Mid,PV,Solar)') m.pro_output_tuples = pyomo.Set( within=m.stf * m.sit * m.pro * m.com, initialize=[(stf, site, process, commodity) for (stf, site, process) in m.pro_tuples for (s, pro, commodity) in tuple(m.r_out_dict.keys()) if process == pro and s == stf], doc='Commodities produced by process by site, e.g. (2020,Mid,PV,Elec)') # process tuples for maximum gradient feature m.pro_maxgrad_tuples = pyomo.Set( within=m.stf * m.sit * m.pro, initialize=[(stf, sit, pro) for (stf, sit, pro) in m.pro_tuples if m.process_dict['max-grad'][stf, sit, pro] < 1.0 / dt], doc='Processes with maximum gradient smaller than timestep length') # process tuples for partial feature m.pro_partial_tuples = pyomo.Set( within=m.stf * m.sit * m.pro, initialize=[(stf, site, process) for (stf, site, process) in m.pro_tuples for (s, pro, _) in tuple(m.r_in_min_fraction_dict.keys()) if process == pro and s == stf], doc='Processes with partial input') m.pro_partial_input_tuples = pyomo.Set( within=m.stf * m.sit * m.pro * m.com, initialize=[(stf, site, process, commodity) for (stf, site, process) in m.pro_partial_tuples for (s, pro, commodity) in tuple(m.r_in_min_fraction_dict.keys()) if process == pro and s == stf], doc='Commodities with partial input ratio,' 'e.g. (2020,Mid,Coal PP,Coal)') m.pro_partial_output_tuples = pyomo.Set( within=m.stf * m.sit * m.pro * m.com, initialize=[(stf, site, process, commodity) for (stf, site, process) in m.pro_partial_tuples for (s, pro, commodity) in tuple(m.r_out_min_fraction_dict.keys()) if process == pro and s == stf], doc='Commodities with partial input ratio, e.g. (Mid,Coal PP,CO2)') # Variables # costs m.costs = pyomo.Var(m.cost_type, within=pyomo.Reals, doc='Costs by type (EUR/a)') # commodity m.e_co_stock = pyomo.Var( m.tm, m.com_tuples, within=pyomo.NonNegativeReals, doc='Use of stock commodity source (MW) per timestep') # process m.cap_pro_new = pyomo.Var(m.pro_tuples, within=pyomo.NonNegativeReals, doc='New process capacity (MW)') # process capacity as expression object # (variable if expansion is possible, else static) m.cap_pro = pyomo.Expression(m.pro_tuples, rule=def_process_capacity_rule, doc='total process capacity') m.tau_pro = pyomo.Var(m.t, m.pro_tuples, within=pyomo.NonNegativeReals, doc='Power flow (MW) through process') m.e_pro_in = pyomo.Var( m.tm, m.pro_input_tuples, within=pyomo.NonNegativeReals, doc='Power flow of commodity into process (MW) per timestep') m.e_pro_out = pyomo.Var(m.tm, m.pro_output_tuples, within=pyomo.NonNegativeReals, doc='Power flow out of process (MW) per timestep') # Add additional features # called features are declared in distinct files in features folder if m.mode['tra']: if m.mode['dpf']: m = transmission.add_transmission_dc(m) else: m = add_transmission(m) if m.mode['sto']: m = add_storage(m) if m.mode['dsm']: m = add_dsm(m) if m.mode['bsp']: m = add_buy_sell_price(m) if m.mode['tve']: m = add_time_variable_efficiency(m) else: m.pro_timevar_output_tuples = pyomo.Set( within=m.stf * m.sit * m.pro * m.com, doc='empty set needed for (partial) process output') # Equation declarations # equation bodies are defined in separate functions, referred to here by # their name in the "rule" keyword. # commodity m.res_vertex = pyomo.Constraint( m.tm, m.com_tuples, rule=res_vertex_rule, doc='storage + transmission + process + source + buy - sell == demand') m.res_stock_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_stock_step_rule, doc='stock commodity input per step <= commodity.maxperstep') m.res_stock_total = pyomo.Constraint( m.com_tuples, rule=res_stock_total_rule, doc='total stock commodity input <= commodity.max') m.res_env_step = pyomo.Constraint( m.tm, m.com_tuples, rule=res_env_step_rule, doc='environmental output per step <= commodity.maxperstep') m.res_env_total = pyomo.Constraint( m.com_tuples, rule=res_env_total_rule, doc='total environmental commodity output <= commodity.max') # process m.def_process_input = pyomo.Constraint( m.tm, m.pro_input_tuples - m.pro_partial_input_tuples, rule=def_process_input_rule, doc='process input = process throughput * input ratio') m.def_process_output = pyomo.Constraint( m.tm, (m.pro_output_tuples - m.pro_partial_output_tuples - m.pro_timevar_output_tuples), rule=def_process_output_rule, doc='process output = process throughput * output ratio') m.def_intermittent_supply = pyomo.Constraint( m.tm, m.pro_input_tuples, rule=def_intermittent_supply_rule, doc='process output = process capacity * supim timeseries') m.res_process_throughput_by_capacity = pyomo.Constraint( m.tm, m.pro_tuples, rule=res_process_throughput_by_capacity_rule, doc='process throughput <= total process capacity') m.res_process_maxgrad_lower = pyomo.Constraint( m.tm, m.pro_maxgrad_tuples, rule=res_process_maxgrad_lower_rule, doc='throughput may not decrease faster than maximal gradient') m.res_process_maxgrad_upper = pyomo.Constraint( m.tm, m.pro_maxgrad_tuples, rule=res_process_maxgrad_upper_rule, doc='throughput may not increase faster than maximal gradient') m.res_process_capacity = pyomo.Constraint( m.pro_tuples, rule=res_process_capacity_rule, doc='process.cap-lo <= total process capacity <= process.cap-up') m.res_area = pyomo.Constraint( m.sit_tuples, rule=res_area_rule, doc='used process area <= total process area') m.res_throughput_by_capacity_min = pyomo.Constraint( m.tm, m.pro_partial_tuples, rule=res_throughput_by_capacity_min_rule, doc='cap_pro * min-fraction <= tau_pro') m.def_partial_process_input = pyomo.Constraint( m.tm, m.pro_partial_input_tuples, rule=def_partial_process_input_rule, doc='e_pro_in = ' ' cap_pro * min_fraction * (r - R) / (1 - min_fraction)' ' + tau_pro * (R - min_fraction * r) / (1 - min_fraction)') m.def_partial_process_output = pyomo.Constraint( m.tm, (m.pro_partial_output_tuples - (m.pro_partial_output_tuples & m.pro_timevar_output_tuples)), rule=def_partial_process_output_rule, doc='e_pro_out = ' ' cap_pro * min_fraction * (r - R) / (1 - min_fraction)' ' + tau_pro * (R - min_fraction * r) / (1 - min_fraction)') if m.mode['int']: m.res_global_co2_limit = pyomo.Constraint( m.stf, rule=res_global_co2_limit_rule, doc='total co2 commodity output <= global.prop CO2 limit') # costs m.def_costs = pyomo.Constraint(m.cost_type, rule=def_costs_rule, doc='main cost function by cost type') # objective and global constraints if m.obj.value == 'cost': if m.mode['int']: m.res_global_co2_budget = pyomo.Constraint( rule=res_global_co2_budget_rule, doc='total co2 commodity output <= global.prop CO2 budget') else: m.res_global_co2_limit = pyomo.Constraint( m.stf, rule=res_global_co2_limit_rule, doc='total co2 commodity output <= Global CO2 limit') m.objective_function = pyomo.Objective( rule=cost_rule, sense=pyomo.minimize, doc='minimize(cost = sum of all cost types)') elif m.obj.value == 'CO2': m.res_global_cost_limit = pyomo.Constraint( rule=res_global_cost_limit_rule, doc='total costs <= Global cost limit') m.objective_function = pyomo.Objective( rule=co2_rule, sense=pyomo.minimize, doc='minimize total CO2 emissions') else: raise NotImplementedError("Non-implemented objective quantity. Set " "either 'cost' or 'CO2' as the objective in " "runme.py!") if dual: m.dual = pyomo.Suffix(direction=pyomo.Suffix.IMPORT) return m
def generate_model(model_data): """ Generate a Pyomo model. """ backend_model = po.ConcreteModel() # Sets for coord in list(model_data.coords): set_data = list(model_data.coords[coord].data) # Ensure that time steps are pandas.Timestamp objects if isinstance(set_data[0], np.datetime64): set_data = pd.to_datetime(set_data) setattr(backend_model, coord, po.Set(initialize=set_data, ordered=True)) # "Parameters" model_data_dict = { 'data': { k: v.to_series().dropna().replace('inf', np.inf).to_dict() for k, v in model_data.data_vars.items() if v.attrs['is_result'] == 0 or v.attrs.get('operate_param', 0) == 1 }, 'dims': { k: v.dims for k, v in model_data.data_vars.items() if v.attrs['is_result'] == 0 or v.attrs.get('operate_param', 0) == 1 }, 'sets': list(model_data.coords), 'attrs': {k: v for k, v in model_data.attrs.items() if k is not 'defaults'} } # Dims in the dict's keys are ordered as in model_data, which is enforced # in model_data generation such that timesteps are always last and the # remainder of dims are in alphabetic order backend_model.__calliope_model_data = model_data_dict backend_model.__calliope_defaults = AttrDict.from_yaml_string( model_data.attrs['defaults']) backend_model.__calliope_run_config = AttrDict.from_yaml_string( model_data.attrs['run_config']) for k, v in model_data_dict['data'].items(): if k in backend_model.__calliope_defaults.keys(): setattr( backend_model, k, po.Param(*[ getattr(backend_model, i) for i in model_data_dict['dims'][k] ], initialize=v, mutable=True, default=backend_model.__calliope_defaults[k])) # In operate mode, e.g. energy_cap is a parameter, not a decision variable, # so add those in. elif (backend_model.__calliope_run_config['mode'] == 'operate' and model_data[k].attrs.get('operate_param') == 1): setattr( backend_model, k, po.Param(getattr(backend_model, model_data_dict['dims'][k][0]), initialize=v, mutable=True)) else: # no default value to look up setattr( backend_model, k, po.Param(*[ getattr(backend_model, i) for i in model_data_dict['dims'][k] ], initialize=v, mutable=True)) # Variables load_function( 'calliope.backend.pyomo.variables.initialize_decision_variables')( backend_model) # Constraints constraints_to_add = [ i.split('.py')[0] for i in os.listdir(constraints.__path__[0]) if not i.startswith('_') and not i.startswith('.') ] # The list is sorted to ensure that some constraints are added after pyomo # expressions have been created in other constraint files. # Ordering is given by the number assigned to the variable ORDER within each # file (higher number = added later). try: constraints_to_add.sort(key=lambda x: load_function( 'calliope.backend.pyomo.constraints.' + x + '.ORDER')) except AttributeError as e: raise AttributeError( '{}. This attribute must be set to an integer value based ' 'on the order in which the constraints in the file {}.py should be ' 'loaded relative to constraints in other constraint files. If order ' 'does not matter, set ORDER to a value of 10.'.format( e.args[0], e.args[0].split('.')[-1].split("'")[0])) logger.info('constraints are loaded in the following order: {}'.format( constraints_to_add)) for c in constraints_to_add: load_function('calliope.backend.pyomo.constraints.' + c + '.load_constraints')(backend_model) # FIXME: Optional constraints # optional_constraints = model_data.attrs['constraints'] # if optional_constraints: # for c in optional_constraints: # self.add_constraint(load_function(c)) # Objective function # FIXME re-enable loading custom objectives # fetch objective function by name, pass through objective options # if they are present objective_function = ('calliope.backend.pyomo.objective.' + backend_model.__calliope_run_config['objective']) objective_args = backend_model.__calliope_run_config['objective_options'] load_function(objective_function)(backend_model, **objective_args) return backend_model
def generate_model(model_data): """ Generate a Pyomo model. """ backend_model = po.ConcreteModel() mode = model_data.attrs['run.mode'] # 'plan' or 'operate' backend_model.mode = mode # Sets for coord in list(model_data.coords): set_data = list(model_data.coords[coord].data) # Ensure that time steps are pandas.Timestamp objects if isinstance(set_data[0], np.datetime64): set_data = pd.to_datetime(set_data) setattr( backend_model, coord, po.Set(initialize=set_data, ordered=True) ) # "Parameters" model_data_dict = { 'data': { k: v.to_series().dropna().replace('inf', np.inf).to_dict() for k, v in model_data.data_vars.items() if v.attrs['is_result'] == 0 or v.attrs.get('operate_param', 0) == 1 }, 'dims': { k: v.dims for k, v in model_data.data_vars.items() if v.attrs['is_result'] == 0 or v.attrs.get('operate_param', 0) == 1 }, 'sets': list(model_data.coords), 'attrs': {k: v for k, v in model_data.attrs.items() if k is not 'defaults'} } # Dims in the dict's keys are ordered as in model_data, which is enforced # in model_data generation such that timesteps are always last and the # remainder of dims are in alphabetic order backend_model.__calliope_model_data__ = model_data_dict backend_model.__calliope_defaults__ = ( ruamel.yaml.load(model_data.attrs['defaults'], Loader=ruamel.yaml.Loader) ) for k, v in model_data_dict['data'].items(): if k in backend_model.__calliope_defaults__.keys(): setattr( backend_model, k, po.Param(*[getattr(backend_model, i) for i in model_data_dict['dims'][k]], initialize=v, mutable=True, default=backend_model.__calliope_defaults__[k]) ) elif k == 'timestep_resolution' or k == 'timestep_weights': # no default value to look up setattr( backend_model, k, po.Param(backend_model.timesteps, initialize=v, mutable=True) ) elif mode == 'operate' and model_data[k].attrs.get('operate_param') == 1: setattr( backend_model, k, po.Param(getattr(backend_model, model_data_dict['dims'][k][0]), initialize=v, mutable=True) ) # Variables load_function( 'calliope.backend.pyomo.variables.initialize_decision_variables' )(backend_model) # Constraints constraints_to_add = [ 'energy_balance.load_constraints', 'dispatch.load_constraints', 'network.load_constraints', 'costs.load_constraints', 'policy.load_constraints' ] if mode != 'operate': constraints_to_add.append('capacity.load_constraints') if hasattr(backend_model, 'loc_techs_conversion'): constraints_to_add.append('conversion.load_constraints') if hasattr(backend_model, 'loc_techs_conversion_plus'): constraints_to_add.append('conversion_plus.load_constraints') if hasattr(backend_model, 'loc_techs_milp') or hasattr(backend_model, 'loc_techs_purchase'): constraints_to_add.append('milp.load_constraints') # Export comes last as it can add to the cost expression, this could be # overwritten if it doesn't come last if hasattr(backend_model, 'loc_techs_export'): constraints_to_add.append('export.load_constraints') for c in constraints_to_add: load_function( 'calliope.backend.pyomo.constraints.' + c )(backend_model) # FIXME: Optional constraints # optional_constraints = model_data.attrs['constraints'] # if optional_constraints: # for c in optional_constraints: # self.add_constraint(load_function(c)) # Objective function objective_name = model_data.attrs['run.objective'] objective_function = 'calliope.backend.pyomo.objective.' + objective_name load_function(objective_function)(backend_model) # delattr(backend_model, '__calliope_model_data__') return backend_model
def add_transmission_dc(m): # defining transmission tuple sets for transport and DCPF model separately tra_tuples = set() tra_tuples_dc = set() for key in m.transmission_dict['reactance']: tra_tuples.add(tuple(key)) for key in m.transmission_dc_dict['reactance']: tra_tuples_dc.add(tuple(key)) tra_tuples_tp = tra_tuples - tra_tuples_dc tra_tuples_dc = remove_duplicate_transmission(tra_tuples_dc) tra_tuples = tra_tuples_dc | tra_tuples_tp # tranmission (e.g. hvac, hvdc, pipeline...) indexlist = set() for key in m.transmission_dict["eff"]: indexlist.add(tuple(key)[3]) m.tra = pyomo.Set( initialize=indexlist, doc='Set of transmission technologies') # Transport and DCPF transmission tuples m.tra_tuples = pyomo.Set( within=m.stf * m.sit * m.sit * m.tra * m.com, initialize=tuple(tra_tuples), doc='Combinations of possible transmissions,' 'without duplicate dc transmissions' ' e.g. (2020,South,Mid,hvac,Elec)') # DCPF transmission tuples m.tra_tuples_dc = pyomo.Set( within=m.stf * m.sit * m.sit * m.tra * m.com, initialize=tuple(tra_tuples_dc), doc='Combinations of possible bidirectional dc' 'transmissions, e.g. (2020,South,Mid,hvac,Elec)') # Transport transmission tuples m.tra_tuples_tp = pyomo.Set( within=m.stf * m.sit * m.sit * m.tra * m.com, initialize=tuple(tra_tuples_tp), doc='Combinations of possible transport transmissions,' 'e.g. (2020,South,Mid,hvac,Elec)') if m.mode['int']: m.operational_tra_tuples = pyomo.Set( within=m.sit * m.sit * m.tra * m.com * m.stf * m.stf, initialize=[(sit, sit_, tra, com, stf, stf_later) for (sit, sit_, tra, com, stf, stf_later) in op_tra_tuples(m.tra_tuples, m)], doc='Transmissions that are still operational through stf_later' '(and the relevant years following), if built in stf' 'in stf.') m.inst_tra_tuples = pyomo.Set( within=m.sit * m.sit * m.tra * m.com * m.stf, initialize=[(sit, sit_, tra, com, stf) for (sit, sit_, tra, com, stf) in inst_tra_tuples(m)], doc='Installed transmissions that are still operational' 'through stf') # Variables m.cap_tra_new = pyomo.Var( m.tra_tuples, within=pyomo.NonNegativeReals, doc='New transmission capacity (MW)') # transmission capacity as expression object m.cap_tra = pyomo.Expression( m.tra_tuples, rule=def_transmission_capacity_rule, doc='total transmission capacity') m.e_tra_abs = pyomo.Var( m.tm, m.tra_tuples_dc, within=pyomo.NonNegativeReals, doc='Absolute power flow on transmission line (MW) per timestep') m.e_tra_in = pyomo.Var( m.tm, m.tra_tuples, within=e_tra_domain_rule, doc='Power flow into transmission line (MW) per timestep') m.e_tra_out = pyomo.Var( m.tm, m.tra_tuples, within=e_tra_domain_rule, doc='Power flow out of transmission line (MW) per timestep') m.voltage_angle = pyomo.Var( m.tm, m.stf, m.sit, within=pyomo.Reals, doc='Voltage angle of a site') # transmission m.def_transmission_output = pyomo.Constraint( m.tm, m.tra_tuples, rule=def_transmission_output_rule, doc='transmission output = transmission input * efficiency') m.def_dc_power_flow = pyomo.Constraint( m.tm, m.tra_tuples_dc, rule=def_dc_power_flow_rule, doc='transmission output = (angle(in)-angle(out))/ 57.2958 ' '* -1 *(-1/reactance) * (base voltage)^2') m.def_angle_limit = pyomo.Constraint( m.tm, m.tra_tuples_dc, rule=def_angle_limit_rule, doc='-angle limit < angle(in) - angle(out) < angle limit') m.e_tra_abs1 = pyomo.Constraint( m.tm, m.tra_tuples_dc, rule=e_tra_abs_rule1, doc='transmission dc input <= absolute transmission dc input') m.e_tra_abs2 = pyomo.Constraint( m.tm, m.tra_tuples_dc, rule=e_tra_abs_rule2, doc='-transmission dc input <= absolute transmission dc input') m.res_transmission_input_by_capacity = pyomo.Constraint( m.tm, m.tra_tuples, rule=res_transmission_input_by_capacity_rule, doc='transmission input <= total transmission capacity') m.res_transmission_dc_input_by_capacity = pyomo.Constraint( m.tm, m.tra_tuples_dc, rule=res_transmission_dc_input_by_capacity_rule, doc='-dcpf transmission input <= total transmission capacity') m.res_transmission_capacity = pyomo.Constraint( m.tra_tuples, rule=res_transmission_capacity_rule, doc='transmission.cap-lo <= total transmission capacity <= ' 'transmission.cap-up') m.res_transmission_symmetry = pyomo.Constraint( m.tra_tuples_tp, rule=res_transmission_symmetry_rule, doc='total transmission capacity must be symmetric in both directions') return m
def add_transmission(m): # tranmission (e.g. hvac, hvdc, pipeline...) indexlist = set() for key in m.transmission_dict["eff"]: indexlist.add(tuple(key)[3]) m.tra = pyomo.Set( initialize=indexlist, doc='Set of transmission technologies') # transmission tuples m.tra_tuples = pyomo.Set( within=m.stf * m.sit * m.sit * m.tra * m.com, initialize=tuple(m.transmission_dict["eff"].keys()), doc='Combinations of possible transmissions, e.g. ' '(2020,South,Mid,hvac,Elec)') if m.mode['int']: m.operational_tra_tuples = pyomo.Set( within=m.sit * m.sit * m.tra * m.com * m.stf * m.stf, initialize=[(sit, sit_, tra, com, stf, stf_later) for (sit, sit_, tra, com, stf, stf_later) in op_tra_tuples(m.tra_tuples, m)], doc='Transmissions that are still operational through stf_later' '(and the relevant years following), if built in stf' 'in stf.') m.inst_tra_tuples = pyomo.Set( within=m.sit * m.sit * m.tra * m.com * m.stf, initialize=[(sit, sit_, tra, com, stf) for (sit, sit_, tra, com, stf) in inst_tra_tuples(m)], doc='Installed transmissions that are still operational' 'through stf') # Variables m.cap_tra_new = pyomo.Var( m.tra_tuples, within=pyomo.NonNegativeReals, doc='New transmission capacity (MW)') # transmission capacity as expression object m.cap_tra = pyomo.Expression( m.tra_tuples, rule=def_transmission_capacity_rule, doc='total transmission capacity') m.e_tra_in = pyomo.Var( m.tm, m.tra_tuples, within=pyomo.NonNegativeReals, doc='Power flow into transmission line (MW) per timestep') m.e_tra_out = pyomo.Var( m.tm, m.tra_tuples, within=pyomo.NonNegativeReals, doc='Power flow out of transmission line (MW) per timestep') # transmission m.def_transmission_output = pyomo.Constraint( m.tm, m.tra_tuples, rule=def_transmission_output_rule, doc='transmission output = transmission input * efficiency') m.res_transmission_input_by_capacity = pyomo.Constraint( m.tm, m.tra_tuples, rule=res_transmission_input_by_capacity_rule, doc='transmission input <= total transmission capacity') m.res_transmission_capacity = pyomo.Constraint( m.tra_tuples, rule=res_transmission_capacity_rule, doc='transmission.cap-lo <= total transmission capacity <= ' 'transmission.cap-up') m.res_transmission_symmetry = pyomo.Constraint( m.tra_tuples, rule=res_transmission_symmetry_rule, doc='total transmission capacity must be symmetric in both directions') return m
def add_storage(m): # storage (e.g. hydrogen, pump storage) indexlist = set() for key in m.storage_dict["eff-in"]: indexlist.add(tuple(key)[2]) m.sto = pyomo.Set(initialize=indexlist, doc='Set of storage technologies') # storage tuples m.sto_tuples = pyomo.Set(within=m.stf * m.sit * m.sto * m.com, initialize=tuple(m.storage_dict["eff-in"].keys()), doc='Combinations of possible storage by site,' 'e.g. (2020,Mid,Bat,Elec)') # tuples for intertemporal operation if m.mode['int']: m.operational_sto_tuples = pyomo.Set( within=m.sit * m.sto * m.com * m.stf * m.stf, initialize=[(sit, sto, com, stf, stf_later) for (sit, sto, com, stf, stf_later) in op_sto_tuples(m.sto_tuples, m)], doc='Processes that are still operational through stf_later' '(and the relevant years following), if built in stf' 'in stf.') m.inst_sto_tuples = pyomo.Set( within=m.sit * m.sto * m.com * m.stf, initialize=[(sit, sto, com, stf) for (sit, sto, com, stf) in inst_sto_tuples(m)], doc='Installed storages that are still operational through stf') # storage tuples for storages with fixed initial state m.sto_init_bound_tuples = pyomo.Set( within=m.stf * m.sit * m.sto * m.com, initialize=tuple(m.stor_init_bound_dict.keys()), doc='storages with fixed initial state') # storage tuples for storages with given energy to power ratio m.sto_ep_ratio_tuples = pyomo.Set( within=m.stf * m.sit * m.sto * m.com, initialize=tuple(m.sto_ep_ratio_dict.keys()), doc='storages with given energy to power ratio') # Variables m.cap_sto_c_new = pyomo.Var(m.sto_tuples, within=pyomo.NonNegativeReals, doc='New storage size (MWh)') m.cap_sto_p_new = pyomo.Var(m.sto_tuples, within=pyomo.NonNegativeReals, doc='New storage power (MW)') # storage capacities as expression objects m.cap_sto_c = pyomo.Expression(m.sto_tuples, rule=def_storage_capacity_rule, doc='Total storage size (MWh)') m.cap_sto_p = pyomo.Expression(m.sto_tuples, rule=def_storage_power_rule, doc='Total storage power (MW)') m.e_sto_in = pyomo.Var(m.tm, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Power flow into storage (MW) per timestep') m.e_sto_out = pyomo.Var(m.tm, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Power flow out of storage (MW) per timestep') m.e_sto_con = pyomo.Var(m.t, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Energy content of storage (MWh) in timestep') # storage rules m.def_storage_state = pyomo.Constraint( m.tm, m.sto_tuples, rule=def_storage_state_rule, doc='storage[t] = (1 - sd) * storage[t-1] + in * eff_i - out / eff_o') m.res_storage_input_by_power = pyomo.Constraint( m.tm, m.sto_tuples, rule=res_storage_input_by_power_rule, doc='storage input <= storage power') m.res_storage_output_by_power = pyomo.Constraint( m.tm, m.sto_tuples, rule=res_storage_output_by_power_rule, doc='storage output <= storage power') m.res_storage_state_by_capacity = pyomo.Constraint( m.t, m.sto_tuples, rule=res_storage_state_by_capacity_rule, doc='storage content <= storage capacity') m.res_storage_power = pyomo.Constraint( m.sto_tuples, rule=res_storage_power_rule, doc='storage.cap-lo-p <= storage power <= storage.cap-up-p') m.res_storage_capacity = pyomo.Constraint( m.sto_tuples, rule=res_storage_capacity_rule, doc='storage.cap-lo-c <= storage capacity <= storage.cap-up-c') m.def_initial_storage_state = pyomo.Constraint( m.sto_init_bound_tuples, rule=def_initial_storage_state_rule, doc='storage content initial == and final >= storage.init * capacity') m.res_storage_state_cyclicity = pyomo.Constraint( m.sto_tuples, rule=res_storage_state_cyclicity_rule, doc='storage content initial <= final, both variable') m.def_storage_energy_power_ratio = pyomo.Constraint( m.sto_ep_ratio_tuples, rule=def_storage_energy_power_ratio_rule, doc='storage capacity = storage power * storage E2P ratio') return m
def create_model(data, dt=1, timesteps=None, dual=False): """Create a pyomo ConcreteModel urbs object from given input data. Args: data: a dict of 6 DataFrames with the keys 'commodity', 'process', 'transmission', 'storage', 'demand' and 'supim'. dt: timestep duration in hours (default: 1) timesteps: optional list of timesteps, default: demand timeseries dual: set True to add dual variables to model (slower); default: False Returns: a pyomo ConcreteModel object """ # Optional if not timesteps: timesteps = data['demand'].index.tolist() m = pyomo_model_prep(data, timesteps) # preparing pyomo model m.name = 'urbs' m.created = datetime.now().strftime('%Y%m%dT%H%M') m._data = data # Parameters # weight = length of year (hours) / length of simulation (hours) # weight scales costs and emissions from length of simulation to a full # year, making comparisons among cost types (invest is annualized, fixed # costs are annual by default, variable costs are scaled by weight) and # among different simulation durations meaningful. m.weight = pyomo.Param( initialize=float(8760) / (len(m.timesteps) * dt), doc='Pre-factor for variable costs and emissions for an annual result') # dt = spacing between timesteps. Required for storage equation that # converts between energy (storage content, e_sto_con) and power (all other # quantities that start with "e_") m.dt = pyomo.Param( initialize=dt, doc='Time step duration (in hours), default: 1') # Sets # ==== # Syntax: m.{name} = Set({domain}, initialize={values}) # where name: set name # domain: set domain for tuple sets, a cartesian set product # values: set values, a list or array of element tuples # generate ordered time step sets m.t = pyomo.Set( initialize=m.timesteps, ordered=True, doc='Set of timesteps') # modelled (i.e. excluding init time step for storage) time steps m.tm = pyomo.Set( within=m.t, initialize=m.timesteps[1:], ordered=True, doc='Set of modelled timesteps') # site (e.g. north, middle, south...) m.sit = pyomo.Set( initialize=m.commodity.index.get_level_values('Site').unique(), doc='Set of sites') # commodity (e.g. solar, wind, coal...) m.com = pyomo.Set( initialize=m.commodity.index.get_level_values('Commodity').unique(), doc='Set of commodities') # commodity type (i.e. SupIm, Demand, Stock, Env) m.com_type = pyomo.Set( initialize=m.commodity.index.get_level_values('Type').unique(), doc='Set of commodity types') # process (e.g. Wind turbine, Gas plant, Photovoltaics...) m.pro = pyomo.Set( initialize=m.process.index.get_level_values('Process').unique(), doc='Set of conversion processes') # tranmission (e.g. hvac, hvdc, pipeline...) m.tra = pyomo.Set( initialize=m.transmission.index.get_level_values('Transmission') .unique(), doc='Set of transmission technologies') # storage (e.g. hydrogen, pump storage) m.sto = pyomo.Set( initialize=m.storage.index.get_level_values('Storage').unique(), doc='Set of storage technologies') # cost_type m.cost_type = pyomo.Set( initialize=['Invest', 'Fixed', 'Variable', 'Fuel', 'Environmental'], doc='Set of cost types (hard-coded)') # tuple sets m.com_tuples = pyomo.Set( within=m.sit*m.com*m.com_type, initialize=m.commodity.index, doc='Combinations of defined commodities, e.g. (Mid,Elec,Demand)') m.pro_tuples = pyomo.Set( within=m.sit*m.pro, initialize=m.process.index, doc='Combinations of possible processes, e.g. (North,Coal plant)') m.tra_tuples = pyomo.Set( within=m.sit*m.sit*m.tra*m.com, initialize=m.transmission.index, doc='Combinations of possible transmissions, e.g. ' '(South,Mid,hvac,Elec)') m.sto_tuples = pyomo.Set( within=m.sit*m.sto*m.com, initialize=m.storage.index, doc='Combinations of possible storage by site, e.g. (Mid,Bat,Elec)') # commodity type subsets m.com_supim = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'SupIm'), doc='Commodities that have intermittent (timeseries) input') m.com_stock = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Stock'), doc='Commodities that can be purchased at some site(s)') m.com_demand = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Demand'), doc='Commodities that have a demand (implies timeseries)') m.com_env = pyomo.Set( within=m.com, initialize=commodity_subset(m.com_tuples, 'Env'), doc='Commodities that (might) have a maximum creation limit') # process input/output m.pro_input_tuples = pyomo.Set( within=m.sit*m.pro*m.com, initialize=[(site, process, commodity) for (site, process) in m.pro_tuples for (pro, commodity) in m.r_in.index if process == pro], doc='Commodities consumed by process by site, e.g. (Mid,PV,Solar)') m.pro_output_tuples = pyomo.Set( within=m.sit*m.pro*m.com, initialize=[(site, process, commodity) for (site, process) in m.pro_tuples for (pro, commodity) in m.r_out.index if process == pro], doc='Commodities produced by process by site, e.g. (Mid,PV,Elec)') # storage tuples for storages with fixed initial state m.sto_init_bound_tuples = pyomo.Set( within=m.sit*m.sto*m.com, initialize=m.stor_init_bound.index, doc='storages with fixed initial state') # storage tuples for storages with given energy to power ratio m.sto_ep_ratio_tuples = pyomo.Set( within=m.sit*m.sto*m.com, initialize=m.sto_ep_ratio.index, doc='storages with given energy to power ratio') # Variables # costs m.costs = pyomo.Var( m.cost_type, within=pyomo.Reals, doc='Costs by type (EUR/a)') # commodity m.e_co_stock = pyomo.Var( m.tm, m.com_tuples, within=pyomo.NonNegativeReals, doc='Use of stock commodity source (MW) per timestep') # process m.cap_pro = pyomo.Var( m.pro_tuples, within=pyomo.NonNegativeReals, doc='Total process capacity (MW)') m.cap_pro_new = pyomo.Var( m.pro_tuples, within=pyomo.NonNegativeReals, doc='New process capacity (MW)') m.tau_pro = pyomo.Var( m.t, m.pro_tuples, within=pyomo.NonNegativeReals, doc='Power flow (MW) through process') m.e_pro_in = pyomo.Var( m.tm, m.pro_tuples, m.com, within=pyomo.NonNegativeReals, doc='Power flow of commodity into process (MW) per timestep') m.e_pro_out = pyomo.Var( m.tm, m.pro_tuples, m.com, within=pyomo.NonNegativeReals, doc='Power flow out of process (MW) per timestep') # transmission m.cap_tra = pyomo.Var( m.tra_tuples, within=pyomo.NonNegativeReals, doc='Total transmission capacity (MW)') m.cap_tra_new = pyomo.Var( m.tra_tuples, within=pyomo.NonNegativeReals, doc='New transmission capacity (MW)') m.e_tra_in = pyomo.Var( m.tm, m.tra_tuples, within=pyomo.NonNegativeReals, doc='Power flow into transmission line (MW) per timestep') m.e_tra_out = pyomo.Var( m.tm, m.tra_tuples, within=pyomo.NonNegativeReals, doc='Power flow out of transmission line (MW) per timestep') # storage m.cap_sto_c = pyomo.Var( m.sto_tuples, within=pyomo.NonNegativeReals, doc='Total storage size (MWh)') m.cap_sto_c_new = pyomo.Var( m.sto_tuples, within=pyomo.NonNegativeReals, doc='New storage size (MWh)') m.cap_sto_p = pyomo.Var( m.sto_tuples, within=pyomo.NonNegativeReals, doc='Total storage power (MW)') m.cap_sto_p_new = pyomo.Var( m.sto_tuples, within=pyomo.NonNegativeReals, doc='New storage power (MW)') m.e_sto_in = pyomo.Var( m.tm, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Power flow into storage (MW) per timestep') m.e_sto_out = pyomo.Var( m.tm, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Power flow out of storage (MW) per timestep') m.e_sto_con = pyomo.Var( m.t, m.sto_tuples, within=pyomo.NonNegativeReals, doc='Energy content of storage (MWh) in timestep') # Equation declarations # equation bodies are defined in separate functions, referred to here by # their name in the "rule" keyword. # commodity m.res_vertex = pyomo.Constraint( m.tm, m.com_tuples, rule=res_vertex_rule, doc='storage + transmission + process + source == demand') # process m.def_process_capacity = pyomo.Constraint( m.pro_tuples, rule=def_process_capacity_rule, doc='total process capacity = inst-cap + new capacity') m.def_process_input = pyomo.Constraint( m.tm, m.pro_input_tuples, rule=def_process_input_rule, doc='process input = process throughput * input ratio') m.def_process_output = pyomo.Constraint( m.tm, m.pro_output_tuples, rule=def_process_output_rule, doc='process output = process throughput * output ratio') m.def_intermittent_supply = pyomo.Constraint( m.tm, m.pro_input_tuples, rule=def_intermittent_supply_rule, doc='process output = process capacity * supim timeseries') m.res_process_throughput_by_capacity = pyomo.Constraint( m.tm, m.pro_tuples, rule=res_process_throughput_by_capacity_rule, doc='process throughput <= total process capacity') m.res_process_capacity = pyomo.Constraint( m.pro_tuples, rule=res_process_capacity_rule, doc='process.cap-lo <= total process capacity <= process.cap-up') # transmission m.def_transmission_capacity = pyomo.Constraint( m.tra_tuples, rule=def_transmission_capacity_rule, doc='total transmission capacity = inst-cap + new capacity') m.def_transmission_output = pyomo.Constraint( m.tm, m.tra_tuples, rule=def_transmission_output_rule, doc='transmission output = transmission input * efficiency') m.res_transmission_input_by_capacity = pyomo.Constraint( m.tm, m.tra_tuples, rule=res_transmission_input_by_capacity_rule, doc='transmission input <= total transmission capacity') m.res_transmission_capacity = pyomo.Constraint( m.tra_tuples, rule=res_transmission_capacity_rule, doc='transmission.cap-lo <= total transmission capacity <= ' 'transmission.cap-up') m.res_transmission_symmetry = pyomo.Constraint( m.tra_tuples, rule=res_transmission_symmetry_rule, doc='total transmission capacity must be symmetric in both directions') # storage m.def_storage_state = pyomo.Constraint( m.tm, m.sto_tuples, rule=def_storage_state_rule, doc='storage[t] = (1 - sd) * storage[t-1] + in * eff_i - out / eff_o') m.def_storage_power = pyomo.Constraint( m.sto_tuples, rule=def_storage_power_rule, doc='storage power = inst-cap + new power') m.def_storage_capacity = pyomo.Constraint( m.sto_tuples, rule=def_storage_capacity_rule, doc='storage capacity = inst-cap + new capacity') m.res_storage_input_by_power = pyomo.Constraint( m.tm, m.sto_tuples, rule=res_storage_input_by_power_rule, doc='storage input <= storage power') m.res_storage_output_by_power = pyomo.Constraint( m.tm, m.sto_tuples, rule=res_storage_output_by_power_rule, doc='storage output <= storage power') m.res_storage_state_by_capacity = pyomo.Constraint( m.t, m.sto_tuples, rule=res_storage_state_by_capacity_rule, doc='storage content <= storage capacity') m.res_storage_power = pyomo.Constraint( m.sto_tuples, rule=res_storage_power_rule, doc='storage.cap-lo-p <= storage power <= storage.cap-up-p') m.res_storage_capacity = pyomo.Constraint( m.sto_tuples, rule=res_storage_capacity_rule, doc='storage.cap-lo-c <= storage capacity <= storage.cap-up-c') m.res_initial_and_final_storage_state = pyomo.Constraint( m.t, m.sto_init_bound_tuples, rule=res_initial_and_final_storage_state_rule, doc='storage content initial == and final >= storage.init * capacity') m.res_initial_and_final_storage_state_var = pyomo.Constraint( m.t, m.sto_tuples - m.sto_init_bound_tuples, rule=res_initial_and_final_storage_state_var_rule, doc='storage content initial <= final, both variable') m.def_storage_energy_power_ratio = pyomo.Constraint( m.sto_ep_ratio_tuples, rule=def_storage_energy_power_ratio_rule, doc='storage capacity = storage power * storage E2P ratio') # costs m.def_costs = pyomo.Constraint( m.cost_type, rule=def_costs_rule, doc='main cost function by cost type') m.obj = pyomo.Objective( rule=obj_rule, sense=pyomo.minimize, doc='minimize(cost = sum of all cost types)') # global m.res_global_co2_limit = pyomo.Constraint( rule=res_global_co2_limit_rule, doc='total co2 commodity output <= Global CO2 limit') if dual: m.dual = pyomo.Suffix(direction=pyomo.Suffix.IMPORT) return m
def create_model(ems_local): """ create one optimization instance and parameterize it with the input data in ems model Args: - ems_local: ems model which has been parameterized Return: - m: optimization model instance created according to ems model """ # record the time t0 = tm.time() # get all the data from the external file # ems_local = ems_loc(initialize=True, path='C:/Users/ge57vam/emsflex/opentumflex/ems01_ems.txt') devices = ems_local['devices'] # read data from excel file # print('Data Read. time: ' + "{:.1f}".format(tm.time() - t0) + ' s\n') # print('Prepare Data ...\n') t = tm.time() time_interval = ems_local['time_data'][ 't_inval'] # x minutes for one time step # write in the time series from the data df_time_series = ems_local['fcst'] time_series = pd.DataFrame.from_dict(df_time_series) # time = time_series.index.values # print('Data Prepared. time: ' + "{:.1f}".format(tm.time() - t0) + ' s\n') # system # get the initial time step # time_step_initial = parameter.loc['System']['value'] time_step_initial = ems_local['time_data']['isteps'] # time_step_end = int(60 / time_interval * 24) time_step_end = ems_local['time_data']['nsteps'] timesteps = np.arange(time_step_initial, time_step_end) # timestep_1 = timesteps[0] # timesteps = timesteps_all[time_step_initial:time_step_end] t_dn = 4 # 6*time_step_end/96 t_up = 4 # 6*time_step_end/96 timesteps_dn = timesteps[time_step_initial + 1:time_step_end - t_dn] timesteps_up = timesteps[time_step_initial + 1:time_step_end - t_up] # 15 min for every timestep/ timestep by one hour # create the concrete model p2e = time_interval / 60 # create the model object m m = pyen.ConcreteModel() # create the parameter # print('Define Model ...\n') m.t = pyen.Set(ordered=True, initialize=timesteps) m.t_DN = pyen.Set(ordered=True, initialize=timesteps_dn) m.t_UP = pyen.Set(ordered=True, initialize=timesteps_up) # heat_storage sto_param = devices['sto'] m.sto_max_cont = pyen.Param(initialize=sto_param['stocap']) m.SOC_init = pyen.Param(initialize=sto_param['initSOC']) m.temp_min = pyen.Param(initialize=sto_param['mintemp']) m.temp_max = pyen.Param(initialize=sto_param['maxtemp']) # battery bat_param = devices['bat'] m.bat_cont_max = pyen.Param(initialize=bat_param['stocap']) m.bat_SOC_init = pyen.Param(initialize=bat_param['initSOC']) m.bat_power_max = pyen.Param(initialize=bat_param['maxpow']) m.bat_eta = pyen.Param(initialize=bat_param['eta']) # heat pump hp_param = devices['hp'] hp_elec_cap = pd.DataFrame.from_dict(hp_param['powmap']) hp_cop = pd.DataFrame.from_dict(hp_param['COP']) hp_supply_temp = hp_param['supply_temp'] m.hp_ther_pow = pyen.Param(m.t, initialize=1, mutable=True, within=pyen.NonNegativeReals) m.hp_COP = pyen.Param(m.t, initialize=1, mutable=True, within=pyen.NonNegativeReals) m.hp_elec_pow = pyen.Param(m.t, initialize=1, mutable=True, within=pyen.NonNegativeReals) m.T_DN = pyen.Param(initialize=t_dn, mutable=True) m.T_UP = pyen.Param(initialize=t_up, mutable=True) m.hp_themInertia = pyen.Param(initialize=hp_param['thermInertia']) m.hp_minTemp = pyen.Param(initialize=hp_param['minTemp']) m.hp_maxTemp = pyen.Param(initialize=hp_param['maxTemp']) m.hp_heatgain = pyen.Param(initialize=hp_param['heatgain']) # elec_vehicle ev_param = devices['ev'] ev_aval = ev_param['aval'] m.ev_min_pow = pyen.Param(initialize=ev_param['minpow']) m.ev_max_pow = pyen.Param(initialize=ev_param['maxpow']) m.ev_sto_cap = pyen.Param(initialize=ev_param['stocap']) m.ev_eta = pyen.Param(initialize=ev_param['eta']) m.ev_aval = pyen.Param(m.t, initialize=1, mutable=True) m.ev_charg_amount = m.ev_sto_cap * (ev_param['endSOC'][-1] - ev_param['initSOC'][0]) / 100 ev_soc_init = ev_param['initSOC'] ev_soc_end = ev_param['endSOC'] ev_init_soc_check = ev_param['init_soc_check'] ev_end_soc_check = ev_param['end_soc_check'] # boilder boil_param = devices['boiler'] m.boiler_max_cap = pyen.Param(initialize=boil_param['maxpow']) m.boiler_eff = pyen.Param(initialize=boil_param['eta']) # CHP chp_param = devices['chp'] m.chp_elec_effic = pyen.Param(m.t, initialize=chp_param['eta'][0]) m.chp_ther_effic = pyen.Param(m.t, initialize=chp_param['eta'][1]) m.chp_elec_run = pyen.Param(m.t, initialize=chp_param['maxpow']) m.chp_heat_run = pyen.Param(m.t, initialize=0, mutable=True) m.chp_gas_run = pyen.Param(m.t, initialize=0, mutable=True) # solar pv_param = devices['pv'] # m.pv_effic = pyen.Param(initialize=pv_param['eta']) m.pv_peak_power = pyen.Param(initialize=pv_param['maxpow']) m.solar = pyen.Param(m.t, initialize=1, mutable=True) # for t in m.t_UP: # m.t_dn[t] = t_dn # m.t_up[t] = t_dn # price m.ele_price_in, m.ele_price_out, m.gas_price = (pyen.Param(m.t, initialize=1, mutable=True) for i in range(3)) # lastprofil m.lastprofil_heat, m.lastprofil_elec = (pyen.Param(m.t, initialize=1, mutable=True) for i in range(2)) for t in m.t: # weather data m.ele_price_in[t] = time_series.loc[t]['ele_price_in'] m.gas_price[t] = time_series.loc[t]['gas_price'] m.ele_price_out[t] = time_series.loc[t]['ele_price_out'] m.lastprofil_heat[t] = time_series.loc[t]['load_heat'] m.lastprofil_elec[t] = time_series.loc[t]['load_elec'] m.solar[t] = time_series.loc[t]['solar_power'] # fill the ev availability m.ev_aval[t] = ev_aval[t] # calculate the spline function for thermal power of heat pump spl_elec_pow = UnivariateSpline( list(map(float, hp_elec_cap.columns.values)), list(hp_elec_cap.loc[hp_supply_temp, :])) m.hp_elec_pow[t] = spl_elec_pow(time_series.loc[t]['temperature'] + 273.15).item(0) # calculate the spline function for COP of heat pump spl_cop = UnivariateSpline(list(map(float, hp_cop.columns.values)), list(hp_cop.loc[hp_supply_temp, :])) m.hp_COP[t] = spl_cop(time_series.loc[t]['temperature'] + 273.15).item(0) m.hp_ther_pow[t] = m.hp_elec_pow[t] * m.hp_COP[t] # calculate the chp electric and thermal power when it's running m.chp_heat_run[ t] = m.chp_elec_run[t] / m.chp_elec_effic[t] * m.chp_ther_effic[t] m.chp_gas_run[t] = m.chp_elec_run[t] / m.chp_elec_effic[t] # m.ele_price = ele_price # Variables m.hp_run = pyen.Var(m.t, within=pyen.Boolean, doc='operation of the heat pump') m.CHP_run = pyen.Var(m.t, within=pyen.Boolean, doc='operation of the CHP') m.ev_power = pyen.Var(m.t, within=pyen.NonNegativeReals, bounds=(ev_param['minpow'], ev_param['maxpow']), doc='power of the EV') m.boiler_cap, m.PV_cap, m.elec_import, m.elec_export, m.bat_cont, m.sto_e_cont, m.bat_pow_pos, m.bat_pow_neg, \ m.ev_cont, m.ev_var_pow, m.soc_diff, m.roomtemp = (pyen.Var(m.t, within=pyen.NonNegativeReals) for i in range(12)) m.sto_e_pow, m.costs, m.heatextra = (pyen.Var(m.t, within=pyen.Reals) for i in range(3)) # Constrains # heat_storage def sto_e_cont_def_rule(m, t): if t > m.t[1]: return m.sto_e_cont[t] == m.sto_e_cont[t - 1] + m.sto_e_pow[t] * p2e else: return m.sto_e_cont[ t] == m.sto_max_cont * m.SOC_init / 100 + m.sto_e_pow[t] * p2e m.sto_e_cont_def = pyen.Constraint(m.t, rule=sto_e_cont_def_rule, doc='heat_storage_balance') def heat_balance_rule(m, t): return m.boiler_cap[t] + m.CHP_run[t] * m.chp_heat_run[t] + \ m.hp_run[t] * m.hp_ther_pow[t] - m.lastprofil_heat[t] - m.sto_e_pow[t] == 0 m.heat_power_balance = pyen.Constraint(m.t, rule=heat_balance_rule, doc='heat_storage_balance') # the room in the building # def heat_room_rule(m, t): # if t > m.t[1]: # return m.roomtemp[t] == m.roomtemp[t - 1] + (m.heatextra[t] + m.hp_heatgain) / m.hp_themInertia # else: # return m.roomtemp[t] == 23 # # m.heat_room_balance = pyen.Constraint(m.t, rule=heat_room_rule, doc='heat_room_balance') # # def heat_room_maxtemp_rule(m, t): # return m.roomtemp[t] <= m.hp_maxTemp # # m.heat_room_maxtemp = pyen.Constraint(m.t, rule=heat_room_maxtemp_rule) # # def heat_room_mintemp_rule(m, t): # return m.roomtemp[t] >= m.hp_minTemp # # m.heat_room_mintemp = pyen.Constraint(m.t, rule=heat_room_mintemp_rule) # # def heat_room_end_rule(m, t): # if t == m.t[-1]: # return m.roomtemp[t] == 23 # else: # return Constraint.Skip # # m.heat_room_end = pyen.Constraint(m.t, rule=heat_room_end_rule) # battery def battery_e_cont_def_rule(m, t): if t > m.t[1]: return m.bat_cont[t] == m.bat_cont[ t - 1] + (m.bat_pow_pos[t] * m.bat_eta - m.bat_pow_neg[t] / m.bat_eta) * p2e else: return m.bat_cont[t] == m.bat_cont_max * m.bat_SOC_init / 100 + ( m.bat_pow_pos[t] * m.bat_eta - m.bat_pow_neg[t] / m.bat_eta) * p2e m.bat_e_cont_def = pyen.Constraint(m.t, rule=battery_e_cont_def_rule, doc='battery_balance') def elec_balance_rule(m, t): return m.elec_import[t] + m.CHP_run[t] * m.chp_elec_run[t] + m.PV_cap[t] * m.solar[t] - \ m.elec_export[t] - m.hp_run[t] * m.hp_elec_pow[t] - m.lastprofil_elec[t] - \ (m.bat_pow_pos[t] - m.bat_pow_neg[t]) - m.ev_power[t] == 0 m.elec_power_balance = pyen.Constraint(m.t, rule=elec_balance_rule, doc='elec_balance') def cost_sum_rule(m, t): return m.costs[t] == p2e * ( m.boiler_cap[t] / m.boiler_eff * m.gas_price[t] + m.CHP_run[t] * m.chp_gas_run[t] * m.gas_price[t] + m.elec_import[t] * m.ele_price_in[t] - m.elec_export[t] * m.ele_price_out[t]) + m.soc_diff[t] * 1000 m.cost_sum = pyen.Constraint(m.t, rule=cost_sum_rule) # ev battery balance def ev_cont_def_rule(m, t): if t > m.t[1]: return m.ev_cont[t] == m.ev_cont[ t - 1] + m.ev_power[t] * p2e * m.ev_eta - m.ev_var_pow[t] else: return m.ev_cont[t] == m.ev_sto_cap * ev_soc_init[ 0] / 100 + m.ev_power[t] * p2e * m.ev_eta m.ev_cont_def = pyen.Constraint(m.t, rule=ev_cont_def_rule, doc='EV_balance') def EV_end_soc_rule(m, t): return m.ev_cont[ t] >= m.ev_sto_cap * ev_end_soc_check[t] / 100 - m.soc_diff[t] m.EV_end_soc_def = pyen.Constraint(m.t, rule=EV_end_soc_rule) def EV_init_soc_rule(m, t): return m.ev_cont[t] <= m.ev_sto_cap * ev_init_soc_check[t] / 100 m.EV_init_soc_def = pyen.Constraint(m.t, rule=EV_init_soc_rule) def EV_aval_rule(m, t): return m.ev_power[t] <= m.ev_aval[t] * m.ev_max_pow m.EV_aval_def = pyen.Constraint(m.t, rule=EV_aval_rule) # hp def hp_min_still_t_rule(m, t): return (m.hp_run[t - 1] - m.hp_run[t]) * m.T_DN <= m.T_DN - ( m.hp_run[t] + m.hp_run[t + 1] + m.hp_run[t + 2] + m.hp_run[t + 3]) # m.hp_min_still_t_def = pyen.Constraint(m.t_DN, rule=hp_min_still_t_rule) def hp_min_lauf_t_rule(m, t): return (m.hp_run[t] - m.hp_run[t - 1]) * m.T_UP <= m.hp_run[t] + m.hp_run[t + 1] \ + m.hp_run[t + 2] + m.hp_run[t + 3] # m.hp_min_lauf_t_def = pyen.Constraint(m.t_UP, rule=hp_min_lauf_t_rule) def chp_min_still_t_rule(m, t): return (m.CHP_run[t - 1] - m.CHP_run[t]) * m.T_DN <= m.T_DN - ( m.CHP_run[t] + m.CHP_run[t + 1]) # m.chp_min_still_t_def = pyen.Constraint(m.t_DN, rule=chp_min_still_t_rule) def chp_min_lauf_t_rule(m, t): return (m.CHP_run[t] - m.CHP_run[t - 1]) * m.T_UP <= m.CHP_run[t] + m.CHP_run[t + 1] # m.chp_min_lauf_t_def = pyen.Constraint(m.t_UP, rule=chp_min_lauf_t_rule) # boiler def boiler_max_cap_rule(m, t): return m.boiler_cap[t] <= m.boiler_max_cap m.boiler_max_cap_def = pyen.Constraint(m.t, rule=boiler_max_cap_rule) # PV def pv_max_cap_rule(m, t): return m.PV_cap[t] <= m.pv_peak_power m.pv_max_cap_def = pyen.Constraint(m.t, rule=pv_max_cap_rule) # elec_import def elec_import_rule(m, t): return m.elec_import[t] <= 50 * 5000 m.elec_import_def = pyen.Constraint(m.t, rule=elec_import_rule) # elec_export def elec_export_rule(m, t): return m.elec_export[t] <= 50 * 5000 m.elec_export_def = pyen.Constraint(m.t, rule=elec_export_rule) # storage # storage content if m.sto_max_cont > 0: def sto_e_cont_min_rule(m, t): return m.sto_e_cont[t] / m.sto_max_cont >= 0.1 m.sto_e_cont_min = pyen.Constraint(m.t, rule=sto_e_cont_min_rule) def sto_e_cont_max_rule(m, t): return m.sto_e_cont[t] / m.sto_max_cont <= 0.9 m.sto_e_cont_max = pyen.Constraint(m.t, rule=sto_e_cont_max_rule) if m.bat_cont_max > 0: def bat_e_cont_min_rule(m, t): return m.bat_cont[t] / m.bat_cont_max >= 0.1 m.bat_e_cont_min = pyen.Constraint(m.t, rule=bat_e_cont_min_rule) def bat_e_cont_max_rule(m, t): return m.bat_cont[t] / m.bat_cont_max <= 0.9 m.bat_e_cont_max = pyen.Constraint(m.t, rule=bat_e_cont_max_rule) # storage power def sto_e_max_pow_rule_1(m, t): return m.sto_e_pow[t] <= m.sto_max_cont m.sto_e_pow_max_1 = pyen.Constraint(m.t, rule=sto_e_max_pow_rule_1) def sto_e_max_pow_rule_2(m, t): return m.sto_e_pow[t] >= -m.sto_max_cont m.sto_e_pow_max_2 = pyen.Constraint(m.t, rule=sto_e_max_pow_rule_2) def bat_e_max_pow_rule_1(m, t): return m.bat_pow_pos[t] <= min(m.bat_power_max, m.bat_cont_max) m.bat_e_pow_max_1 = pyen.Constraint(m.t, rule=bat_e_max_pow_rule_1) def bat_e_max_pow_rule_2(m, t): return m.bat_pow_neg[t] <= min(m.bat_power_max, m.bat_cont_max) m.bat_e_pow_max_2 = pyen.Constraint(m.t, rule=bat_e_max_pow_rule_2) # end state of storage and battery m.sto_e_cont_end = pyen.Constraint( expr=(m.sto_e_cont[m.t[-1]] >= 0.5 * m.sto_max_cont)) m.bat_e_cont_end = pyen.Constraint( expr=(m.bat_cont[m.t[-1]] >= 0.5 * m.bat_cont_max)) def obj_rule(m): # Return sum of total costs over all cost types. # Simply calculates the sum of m.costs over all m.cost_types. return pyen.summation(m.costs) m.obj = pyen.Objective(sense=pyen.minimize, rule=obj_rule, doc='Sum costs by cost type') return m
def add_dsm(m): # modelled Demand Side Management time steps (downshift): # downshift effective in tt to compensate for upshift in t m.tt = pyomo.Set(within=m.t, initialize=m.timesteps[1:], ordered=True, doc='Set of additional DSM time steps') # DSM Tuples m.dsm_site_tuples = pyomo.Set( within=m.stf * m.sit * m.com, initialize=tuple(m.dsm_dict["delay"].keys()), doc='Combinations of possible dsm by site, e.g. ' '(2020, Mid, Elec)') m.dsm_down_tuples = pyomo.Set( within=m.tm * m.tm * m.stf * m.sit * m.com, initialize=[(t, tt, stf, site, commodity) for (t, tt, stf, site, commodity) in dsm_down_time_tuples( m.timesteps[1:], m.dsm_site_tuples, m)], doc='Combinations of possible dsm_down combinations, e.g. ' '(5001,5003,2020,Mid,Elec)') # Variables m.dsm_up = pyomo.Var(m.tm, m.dsm_site_tuples, within=pyomo.NonNegativeReals, doc='DSM upshift') m.dsm_down = pyomo.Var(m.dsm_down_tuples, within=pyomo.NonNegativeReals, doc='DSM downshift') # DSM rules m.def_dsm_variables = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=def_dsm_variables_rule, doc='DSMup * efficiency factor n == DSMdo (summed)') m.res_dsm_upward = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_upward_rule, doc='DSMup <= Cup (threshold capacity of DSMup)') m.res_dsm_downward = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_downward_rule, doc='DSMdo (summed) <= Cdo (threshold capacity of DSMdo)') m.res_dsm_maximum = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_maximum_rule, doc='DSMup + DSMdo (summed) <= max(Cup,Cdo)') m.res_dsm_recovery = pyomo.Constraint( m.tm, m.dsm_site_tuples, rule=res_dsm_recovery_rule, doc='DSMup(t, t + recovery time R) <= Cup * delay time L') return m
def create_model(vertex, edge, params={}, timesteps=[]): """return a DHMIN model instance from nodes and edges DataFrame Args: vertex: DataFrame of vertex with index and attributes edges: DataFrame of edges with (Vertex1, Vertex2) MultiIndex and attributes params: dict of cost and technical parameters timesteps: list of timestep tuples (duration, scaling factor) Returns: m: a coopr.pyomo ConcreteModel object Usage: see rundh.py The optional argument params can be used to specify any of the technical and cost parameters. The optional argument timesteps is given, DHMIN is run in multi- seasonal mode that includes a simplified time model. Each (t,p) tuple encodes a time interval of length (1 hour)*t and relative peak power requirement (peak)*p of all consumers. Note that sum(t) must be equal to 8760. The inequalities 0 <= t <= 8760 and 0 <= p <= 1 are to be respected. """ m = pyomo.ConcreteModel() m.name = 'DHMIN' # DATA PREPARATION tech_parameters = { 'c_fix': 600, # (€/m) fixed pipe investment 'c_var': 0.015, # (€/kW/m) variable pipe investment 'c_om': 5, # (€/m) operation & maintenance 'r_heat': 0.07, # (€/kWh) retail price for heat 'annuity': anf(40, 0.06), # (%) annuity factor (years, interest) 'thermal_loss_fix': 20e-3, # (kW/m) fixed thermal losses 'thermal_loss_var': 1e-7, # (kW/kW/m) variable thermal losses 'concurrence': 1, # (%) concurrence effect } # Entity edge contains column 'Edge' as index. This model (in contrast to # the old GAMS version) does not use the 'Edge' ID on its own, so remove the # edge ID from the index ('Edge', 'Vertex1', 'Vertex2') edges = edge.reset_index('Edge') # replace default parameter values with user-defined ones, if specified tech_parameters.update(params) # make edges symmetric by duplicating each row (i,j) to (j,i) edges_tmp = edges edges_tmp.index.names = ['Vertex2', 'Vertex1'] edges_tmp = edges_tmp.reorder_levels(['Vertex1', 'Vertex2']) edges = edges_tmp.append(edges, verify_integrity=True) del edges_tmp # derive list of neighbours for each vertex m.neighbours = {} for (i, j) in edges.index: m.neighbours.setdefault(i, []) m.neighbours[i].append(j) # m.vertices = vertex.copy() m.edges = edges.copy() cost_types = [ 'network', # pipe construction, maintenance 'heat', # heating plants, operation 'revenue', # sold heat ] # derive subset of source vertices, i.e. those with column 'init' set to 1 source_vertex = vertex[vertex.init == 1].index # timestep preparation if timesteps: # extend timesteps with (name, duration, scaling factor) tuples and # add a near-zero (here: 1 hour) legnth, nominal power timestep 'Pmax' timesteps = [('t{}'.format(t[0]), t[0], t[1]) for t in timesteps] timesteps.append(('Pmax', 1 , 1)) # now get a list of all source nodes # for each source, add a non-availability timestep ('v0', 1, 1) # and set availability matrix so that 'v0' is off in that timestep availability = np.ones((len(timesteps) + len(source_vertex), len(source_vertex)), dtype=np.int) for i, v0 in enumerate(source_vertex): availability[len(timesteps), i] = 0 timesteps.append(('v{}'.format(v0), 1, 1)) else: # no timesteps: create single dummy timestep with 100% availability timesteps = [('t0', 1, 1)] availability = np.ones((1, len(source_vertex)), dtype=np.int) # MODEL # Sets m.vertex = pyomo.Set(initialize=vertex.index) m.edge = pyomo.Set(within=m.vertex*m.vertex, initialize=edges.index) m.cost_types = pyomo.Set(initialize=cost_types) m.tech_params = pyomo.Set(initialize=tech_parameters.keys()) m.timesteps = pyomo.Set(initialize=[t[0] for t in timesteps]) m.source_vertex = pyomo.Set(initialize=source_vertex) # Parameters m.tech_parameters = pyomo.Param(m.tech_params, initialize=tech_parameters) # derive delta and eta from edge attributes m.delta = pyomo.Param(m.edge, initialize=dict( edges['peak'] * edges['cnct_quota'] * tech_parameters['concurrence'] + edges['length'] * tech_parameters['thermal_loss_fix'] )) m.eta = pyomo.Param(m.edge, initialize=dict( 1 - (edges['length'] * tech_parameters['thermal_loss_var']) )) # cost coefficients for objective function # k_fix: power-independent investment and operation & maintenance costs for # pipes (EUR/a) m.k_fix = pyomo.Param(m.edge, initialize=dict( edges['length'] * 0.5 # x and Pmax are forced in both directions (i,j),(j,i) * tech_parameters['c_fix'] * tech_parameters['annuity'] * (1 - edges['pipe_exist']) + edges['length'] * 0.5 # x and Pmax are forced in both directions (i,j),(j,i) * tech_parameters['c_om'] )) # k_var: power-dependent pipe investment costs (EUR/kW/a) m.k_var = pyomo.Param(m.edge, initialize=dict( edges['length'] * 0.5 # x and Pmax are forced in both directions (i,j),(j,i) * tech_parameters['c_var'] * tech_parameters['annuity'] * (1- edges['pipe_exist']) )) # k_heat: costs for heat generation (EUR/h) # as the source-term for power flow is lowered by concurrence effect (cf. # m.delta), for conversion to energy integral, it must be removed again m.k_heat = pyomo.Param(m.vertex, initialize=dict( vertex['cost_heat'] / tech_parameters['concurrence'] )) # r_heat: revenue for heat delivery (EUR/h) # m.r_heat = pyomo.Param(m.edge, initialize=dict( edges['peak'] * 0.5 # x and Pmax are forced in both directions (i,j),(j,i) * edges['cnct_quota'] * tech_parameters['r_heat'] )) m.availability = pyomo.Param(m.source_vertex, m.timesteps, initialize={ (s,t[0]): availability[x,y] for y,s in enumerate(source_vertex) for x,t in enumerate(timesteps) }) m.dt = pyomo.Param(m.timesteps, initialize={t[0]:t[1] for t in timesteps}) m.scaling_factor = pyomo.Param(m.timesteps, initialize={t[0]:t[2] for t in timesteps}) # Variables m.costs = pyomo.Var(m.cost_types) m.x = pyomo.Var(m.edge, within=pyomo.Binary) m.Pmax = pyomo.Var(m.edge, within=pyomo.NonNegativeReals) m.Pin = pyomo.Var(m.edge, m.timesteps, within=pyomo.NonNegativeReals) m.Pot = pyomo.Var(m.edge, m.timesteps, within=pyomo.NonNegativeReals) m.Q = pyomo.Var(m.vertex, m.timesteps, within=pyomo.NonNegativeReals) m.y = pyomo.Var(m.edge, m.timesteps, within=pyomo.Binary) m.energy_conservation = pyomo.Constraint( m.vertex, m.timesteps, doc='Power flow is conserved in vertex', rule=energy_conservation_rule) m.demand_satisfaction = pyomo.Constraint( m.edge, m.timesteps, doc='Peak demand (delta) must be satisfied in edge, if pipe is built', rule=demand_satisfaction_rule) m.pipe_capacity = pyomo.Constraint( m.edge, m.timesteps, doc='Power flow is smaller than pipe capacity Pmax', rule=pipe_capacity_rule) m.pipe_usage = pyomo.Constraint( m.edge, m.timesteps, doc='Power flow through pipe=0 if y[i,j,t]=0', rule=pipe_usage_rule) m.must_build = pyomo.Constraint( m.edge, doc='Pipe must be built if must_build == 1', rule=must_build_rule) m.build_capacity = pyomo.Constraint( m.edge, doc='Pipe capacity Pmax must be smaller than edge attribute cap_max', rule=build_capacity_rule) m.unidirectionality = pyomo.Constraint( m.edge, m.timesteps, doc='Power flow only in one direction per timestep', rule=unidirectionality_rule) m.symmetry_x = pyomo.Constraint( m.edge, doc='Pipe may be used in both directions, if built', rule=symmetry_x_rule) m.symmetry_Pmax = pyomo.Constraint( m.edge, doc='Pipe has same capacity in both directions, if built', rule=symmetry_Pmax_rule) m.built_then_use = pyomo.Constraint( m.edge, m.timesteps, doc='Demand must be satisfied from at least one direction, if built', rule=built_then_use_rule) m.source_vertices = pyomo.Constraint( m.vertex, m.timesteps, doc='Non-zero source term Q is only allowed in source vertices', rule=source_vertices_rule) # Objective m.def_costs = pyomo.Constraint( m.cost_types, doc='Cost definitions by type', rule=cost_rule) m.obj = pyomo.Objective( sense=pyomo.minimize, doc='Minimize costs = network + heat - revenue', rule=obj_rule) return m