Exemplo n.º 1
0
    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
Exemplo n.º 2
0
def generate_model(model_data):
    """
    Generate a Pyomo model.

    """
    backend_model = po.ConcreteModel()
    # remove pandas datetime from xarrays, to reduce memory usage on creating pyomo objects
    model_data = datetime_to_string(backend_model, model_data)

    subsets_config = AttrDict.from_yaml_string(model_data.attrs["subsets"])
    build_sets(model_data, backend_model)
    build_params(model_data, backend_model)
    build_variables(backend_model, model_data, subsets_config["variables"])
    build_expressions(backend_model, model_data, subsets_config["expressions"])
    build_constraints(backend_model, model_data, subsets_config["constraints"])
    build_objective(backend_model)
    # FIXME: Optional constraints
    # FIXME re-enable loading custom objectives

    # set datetime data back to datetime dtype
    model_data = string_to_datetime(backend_model, model_data)

    return backend_model
Exemplo n.º 3
0
def pyomo_model_prep(data, timesteps):
    m = pyomo.ConcreteModel()

    # Preparations
    # ============
    # Data import. Syntax to access a value within equation definitions looks
    # like this:
    #
    #     m.storage.loc[site, storage, commodity][attribute]
    #
    m.global_prop = data['global_prop']
    m.site = data['site']
    m.commodity = data['commodity']
    m.process = data['process']
    m.process_commodity = data['process_commodity']
    m.transmission = data['transmission']
    m.storage = data['storage']
    m.demand = data['demand']
    m.supim = data['supim']
    m.timesteps = timesteps

    # Converting Data frames to dict
    m.commodity_dict = m.commodity.to_dict()
    m.demand_dict = m.demand.to_dict()
    m.supim_dict = m.supim.to_dict()

    # process input/output ratios
    m.r_in = m.process_commodity.xs('In', level='Direction')['ratio']
    m.r_out = m.process_commodity.xs('Out', level='Direction')['ratio']
    m.r_in_dict = m.r_in.to_dict()
    m.r_out_dict = m.r_out.to_dict()

    # storages with fixed initial state
    m.stor_init_bound = m.storage['init']
    m.stor_init_bound = m.stor_init_bound[m.stor_init_bound >= 0]

    # storages with fixed energy-to-power ratio
    try:
        m.sto_ep_ratio = m.storage['ep-ratio']
        m.sto_ep_ratio = m.sto_ep_ratio[m.sto_ep_ratio >= 0]
    except:
        m.sto_ep_ratio = pd.DataFrame()

    # derive annuity factor from WACC and depreciation duration
    m.process['annuity-factor'] = (m.process.apply(
        lambda x: annuity_factor(x['depreciation'], x['wacc']), axis=1))
    try:
        m.transmission['annuity-factor'] = (m.transmission.apply(
            lambda x: annuity_factor(x['depreciation'], x['wacc']), axis=1))
    except ValueError:
        pass
    try:
        m.storage['annuity-factor'] = (m.storage.apply(
            lambda x: annuity_factor(x['depreciation'], x['wacc']), axis=1))
    except ValueError:
        pass

    # Converting Data frames to dictionaries
    #
    m.process_dict = m.process.to_dict()  # Changed
    m.transmission_dict = m.transmission.to_dict()  # Changed
    m.storage_dict = m.storage.to_dict()  # Changed
    return m
Exemplo n.º 4
0
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
Exemplo n.º 5
0
def create_model(data, timesteps=None, dt=1, 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'.
        timesteps: optional list of timesteps, default: demand timeseries
        dt: timestep duration in hours (default: 1)
        dual: set True to add dual variables to model (slower); default: False

    Returns:
        a pyomo ConcreteModel object
    """
    m = pyomo.ConcreteModel()
    m.name = 'urbs'
    m.created = datetime.now().strftime('%Y%m%dT%H%M')
    m._data = data

    # Optional
    if not timesteps:
        timesteps = data['demand'].index.tolist()

    # Preparations
    # ============
    # Data import. Syntax to access a value within equation definitions looks
    # like this:
    #
    #     m.storage.loc[site, storage, commodity][attribute]
    #
    m.site = data['site']
    m.commodity = data['commodity']
    m.process = data['process']
    m.process_commodity = data['process_commodity']
    m.transmission = data['transmission']
    m.storage = data['storage']
    m.demand = data['demand']
    m.supim = data['supim']
    m.buy_sell_price = data['buy_sell_price']
    m.timesteps = timesteps
    m.dsm = data['dsm']

    # process input/output ratios
    m.r_in = m.process_commodity.xs('In', level='Direction')['ratio']
    m.r_out = m.process_commodity.xs('Out', level='Direction')['ratio']
    m.r_in_dict = m.r_in.to_dict()
    m.r_out_dict = m.r_out.to_dict()

    # process areas
    m.proc_area = m.process['area-per-cap']
    m.sit_area = m.site['area']
    m.proc_area = m.proc_area[m.proc_area >= 0]
    m.sit_area = m.sit_area[m.sit_area >= 0]

    # input ratios for partial efficiencies
    # only keep those entries whose values are
    # a) positive and
    # b) numeric (implicitely, as NaN or NV compare false against 0)
    m.r_in_min_fraction = m.process_commodity.xs('In', level='Direction')
    m.r_in_min_fraction = m.r_in_min_fraction['ratio-min']
    m.r_in_min_fraction = m.r_in_min_fraction[m.r_in_min_fraction > 0]

    # derive annuity factor from WACC and depreciation duration
    m.process['annuity-factor'] = annuity_factor(m.process['depreciation'],
                                                 m.process['wacc'])
    m.transmission['annuity-factor'] = annuity_factor(
        m.transmission['depreciation'], m.transmission['wacc'])
    m.storage['annuity-factor'] = annuity_factor(m.storage['depreciation'],
                                                 m.storage['wacc'])

    # 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',
        'Startup', '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)')

    # 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 startup & 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)')

    # 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')

    # 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.tm) * 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')

    # 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')

    m.cap_online = pyomo.Var(
        m.t,
        m.pro_partial_tuples,
        within=pyomo.NonNegativeReals,
        doc='Online capacity (MW) of process per timestep')
    m.startup_pro = pyomo.Var(
        m.tm,
        m.pro_partial_tuples,
        within=pyomo.NonNegativeReals,
        doc='Started capacity (MW) of process 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,
        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_online_capacity_min = pyomo.Constraint(
        m.tm,
        m.pro_partial_tuples,
        rule=res_throughput_by_online_capacity_min_rule,
        doc='cap_online * min-fraction <= tau_pro')
    m.res_throughput_by_online_capacity_max = pyomo.Constraint(
        m.tm,
        m.pro_partial_tuples,
        rule=res_throughput_by_online_capacity_max_rule,
        doc='tau_pro <= cap_online')
    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_online * min_fraction * (r - R) / (1 - min_fraction)'
        ' + tau_pro * (R - min_fraction * r) / (1 - min_fraction)')
    m.res_cap_online_by_cap_pro = pyomo.Constraint(
        m.tm,
        m.pro_partial_tuples,
        rule=res_cap_online_by_cap_pro_rule,
        doc='online capacity <= process capacity')
    m.def_startup_capacity = pyomo.Constraint(
        m.tm,
        m.pro_partial_tuples,
        rule=def_startup_capacity_rule,
        doc='startup_capacity[t] >= cap_online[t] - cap_online[t-1]')

    # 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] = storage[t-1] * (1 - discharge) + input - output')
    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')

    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 <= 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 <= 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')

    # possibly: add hack features
    if 'hacks' in data:
        m = add_hacks(m, data['hacks'])

    if dual:
        m.dual = pyomo.Suffix(direction=pyomo.Suffix.IMPORT)
    return m
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
def pyomo_model_prep(data, timesteps):
    '''Performs calculations on the data frames in dictionary "data" for
    further usage by the model.

    Args:
        - data: input data dictionary
        - timesteps: range of modeled timesteps

    Returns:
        a rudimentary pyomo.CancreteModel instance
    '''

    m = pyomo.ConcreteModel()

    # Preparations
    # ============
    # Data import. Syntax to access a value within equation definitions looks
    # like this:
    #
    #     storage.loc[site, storage, commodity][attribute]
    #

    m.mode = identify_mode(data)
    m.timesteps = timesteps
    m.global_prop = data['global_prop']
    commodity = data['commodity']
    process = data['process']

    # create no expansion dataframes
    pro_const_cap = process[process['inst-cap'] == process['cap-up']]

    # create list with all support timeframe values
    m.stf_list = m.global_prop.index.levels[0].tolist()
    # creating list wih cost types
    m.cost_type_list = ['Invest', 'Fixed', 'Variable', 'Fuel', 'Environmental']

    # Converting Data frames to dict
    # Data frames that need to be modified will be converted after modification
    m.site_dict = data['site'].to_dict()
    m.demand_dict = data['demand'].to_dict()
    m.supim_dict = data['supim'].to_dict()

    # additional features
    if m.mode['tra']:
        transmission = data['transmission'].dropna(axis=0, how='all')
        # create no expansion dataframes
        tra_const_cap = transmission[
            transmission['inst-cap'] == transmission['cap-up']]

    if m.mode['sto']:
        storage = data['storage'].dropna(axis=0, how='all')
        # create no expansion dataframes
        sto_const_cap_c = storage[storage['inst-cap-c'] == storage['cap-up-c']]
        sto_const_cap_p = storage[storage['inst-cap-p'] == storage['cap-up-p']]

    if m.mode['dsm']:
        m.dsm_dict = data["dsm"].dropna(axis=0, how='all').to_dict()
    if m.mode['bsp']:
        m.buy_sell_price_dict = \
            data["buy_sell_price"].dropna(axis=0, how='all').to_dict()
        # adding Revenue and Purchase to cost types
        m.cost_type_list.extend(['Revenue', 'Purchase'])
    if m.mode['tve']:
        m.eff_factor_dict = \
            data["eff_factor"].dropna(axis=0, how='all').to_dict()

    # Create columns of support timeframe values
    commodity['support_timeframe'] = (commodity.index.
                                      get_level_values('support_timeframe'))
    process['support_timeframe'] = (process.index.
                                    get_level_values('support_timeframe'))
    if m.mode['tra']:
        transmission['support_timeframe'] = (transmission.index.
                                             get_level_values
                                             ('support_timeframe'))
    if m.mode['sto']:
        storage['support_timeframe'] = (storage.index.
                                        get_level_values('support_timeframe'))

    # installed units for intertemporal planning
    if m.mode['int']:
        m.inst_pro = process['inst-cap']
        m.inst_pro = m.inst_pro[m.inst_pro > 0]
        if m.mode['tra']:
            m.inst_tra = transmission['inst-cap']
            m.inst_tra = m.inst_tra[m.inst_tra > 0]
        if m.mode['sto']:
            m.inst_sto = storage['inst-cap-p']
            m.inst_sto = m.inst_sto[m.inst_sto > 0]

    # process input/output ratios
    m.r_in_dict = (data['process_commodity'].xs('In', level='Direction')
                   ['ratio'].to_dict())
    m.r_out_dict = (data['process_commodity'].xs('Out', level='Direction')
                    ['ratio'].to_dict())

    # process areas
    proc_area = data["process"]['area-per-cap']
    proc_area = proc_area[proc_area >= 0]
    m.proc_area_dict = proc_area.to_dict()

    # input ratios for partial efficiencies
    # only keep those entries whose values are
    # a) positive and
    # b) numeric (implicitely, as NaN or NV compare false against 0)
    r_in_min_fraction = data['process_commodity'].xs('In', level='Direction')
    r_in_min_fraction = r_in_min_fraction['ratio-min']
    r_in_min_fraction = r_in_min_fraction[r_in_min_fraction > 0]
    m.r_in_min_fraction_dict = r_in_min_fraction.to_dict()

    # output ratios for partial efficiencies
    # only keep those entries whose values are
    # a) positive and
    # b) numeric (implicitely, as NaN or NV compare false against 0)
    r_out_min_fraction = data['process_commodity'].xs('Out', level='Direction')
    r_out_min_fraction = r_out_min_fraction['ratio-min']
    r_out_min_fraction = r_out_min_fraction[r_out_min_fraction > 0]
    m.r_out_min_fraction_dict = r_out_min_fraction.to_dict()

    # storages with fixed initial state
    if m.mode['sto']:
        stor_init_bound = storage['init']
        m.stor_init_bound_dict = \
            stor_init_bound[stor_init_bound >= 0].to_dict()

        try:
            # storages with fixed energy-to-power ratio
            sto_ep_ratio = storage['ep-ratio']
            m.sto_ep_ratio_dict = sto_ep_ratio[sto_ep_ratio >= 0].to_dict()
        except KeyError:
            m.sto_ep_ratio_dict = {}

    # derive invcost factor from WACC and depreciation duration
    if m.mode['int']:
        # modify pro_const_cap for intertemporal mode
        for index in tuple(pro_const_cap.index):
            stf_process = process.xs((index[1], index[2]), level=(1, 2))
            if (not stf_process['cap-up'].max(axis=0) ==
                    pro_const_cap.loc[index]['inst-cap']):
                pro_const_cap = pro_const_cap.drop(index)

        # derive invest factor from WACC, depreciation and discount untility
        process['discount'] = (m.global_prop.xs('Discount rate', level=1)
                                .loc[m.global_prop.index.min()[0]]['value'])
        process['stf_min'] = m.global_prop.index.min()[0]
        process['stf_end'] = (m.global_prop.index.max()[0] +
                              m.global_prop.loc[
                              (max(commodity.index.get_level_values
                                   ('support_timeframe').unique()),
                               'Weight')]['value'] - 1)
        process['invcost-factor'] = (process.apply(
                                     lambda x: invcost_factor(
                                         x['depreciation'],
                                         x['wacc'],
                                         x['discount'],
                                         x['support_timeframe'],
                                         x['stf_min']),
                                     axis=1))

        # derive overpay-factor from WACC, depreciation and discount untility
        process['overpay-factor'] = (process.apply(
                                     lambda x: overpay_factor(
                                         x['depreciation'],
                                         x['wacc'],
                                         x['discount'],
                                         x['support_timeframe'],
                                         x['stf_min'],
                                         x['stf_end']),
                                     axis=1))
        process.loc[(process['overpay-factor'] < 0) |
                    (process['overpay-factor']
                     .isnull()), 'overpay-factor'] = 0

        # Derive multiplier for all energy based costs
        commodity['stf_dist'] = (commodity['support_timeframe'].
                                 apply(stf_dist, m=m))
        commodity['discount-factor'] = (commodity['support_timeframe'].
                                        apply(discount_factor, m=m))
        commodity['eff-distance'] = (commodity['stf_dist'].
                                     apply(effective_distance, m=m))
        commodity['cost_factor'] = (commodity['discount-factor'] *
                                    commodity['eff-distance'])
        process['stf_dist'] = (process['support_timeframe'].
                               apply(stf_dist, m=m))
        process['discount-factor'] = (process['support_timeframe'].
                                      apply(discount_factor, m=m))
        process['eff-distance'] = (process['stf_dist'].
                                   apply(effective_distance, m=m))
        process['cost_factor'] = (process['discount-factor'] *
                                  process['eff-distance'])

        # Additional features
        # transmission mode
        if m.mode['tra']:
            # modify tra_const_cap for intertemporal mode
            for index in tuple(tra_const_cap.index):
                stf_transmission = transmission.xs((index[1], index[2], index[3], index[4]),
                                                   level=(1, 2, 3, 4))
                if (not stf_transmission['cap-up'].max(axis=0) ==
                        tra_const_cap.loc[index]['inst-cap']):
                    tra_const_cap = tra_const_cap.drop(index)
            # derive invest factor from WACC, depreciation and
            # discount untility
            transmission['discount'] = (
                m.global_prop.xs('Discount rate', level=1)
                .loc[m.global_prop.index.min()[0]]['value'])
            transmission['stf_min'] = m.global_prop.index.min()[0]
            transmission['stf_end'] = (m.global_prop.index.max()[0] +
                                       m.global_prop.loc[
                                       (max(commodity.index.get_level_values
                                            ('support_timeframe').unique()),
                                        'Weight')]['value'] - 1)
            transmission['invcost-factor'] = (
                transmission.apply(lambda x: invcost_factor(
                    x['depreciation'],
                    x['wacc'],
                    x['discount'],
                    x['support_timeframe'],
                    x['stf_min']),
                    axis=1))
            # derive overpay-factor from WACC, depreciation and
            # discount untility
            transmission['overpay-factor'] = (
                transmission.apply(lambda x: overpay_factor(
                    x['depreciation'],
                    x['wacc'],
                    x['discount'],
                    x['support_timeframe'],
                    x['stf_min'],
                    x['stf_end']),
                    axis=1))
            # Derive multiplier for all energy based costs
            transmission.loc[(transmission['overpay-factor'] < 0) |
                             (transmission['overpay-factor'].isnull()),
                             'overpay-factor'] = 0
            transmission['stf_dist'] = (transmission['support_timeframe'].
                                        apply(stf_dist, m=m))
            transmission['discount-factor'] = (
                transmission['support_timeframe'].apply(discount_factor, m=m))
            transmission['eff-distance'] = (transmission['stf_dist'].
                                            apply(effective_distance, m=m))
            transmission['cost_factor'] = (transmission['discount-factor'] *
                                           transmission['eff-distance'])
        # storage mode
        if m.mode['sto']:
            # modify sto_const_cap_c and sto_const_cap_p for intertemporal mode
            for index in tuple(sto_const_cap_c.index):
                stf_storage = storage.xs((index[1], index[2], index[3]), level=(1, 2, 3))
                if (not stf_storage['cap-up-c'].max(axis=0) ==
                        sto_const_cap_c.loc[index]['inst-cap-c']):
                    sto_const_cap_c = sto_const_cap_c.drop(index)

            for index in tuple(sto_const_cap_p.index):
                stf_storage = storage.xs((index[1], index[2], index[3]), level=(1, 2, 3))
                if (not stf_storage['cap-up-p'].max(axis=0) ==
                        sto_const_cap_p.loc[index]['inst-cap-p']):
                    sto_const_cap_p = sto_const_cap_p.drop(index)

            # derive invest factor from WACC, depreciation and
            # discount untility
            storage['discount'] = m.global_prop.xs('Discount rate', level=1) \
                                   .loc[m.global_prop.index.min()[0]]['value']
            storage['stf_min'] = m.global_prop.index.min()[0]
            storage['stf_end'] = (m.global_prop.index.max()[0] +
                                  m.global_prop.loc[
                                  (max(commodity.index.get_level_values
                                       ('support_timeframe').unique()),
                                   'Weight')]['value'] - 1)
            storage['invcost-factor'] = (
                storage.apply(
                    lambda x: invcost_factor(
                        x['depreciation'],
                        x['wacc'],
                        x['discount'],
                        x['support_timeframe'],
                        x['stf_min']),
                    axis=1))
            storage['overpay-factor'] = (
                storage.apply(lambda x: overpay_factor(
                    x['depreciation'],
                    x['wacc'],
                    x['discount'],
                    x['support_timeframe'],
                    x['stf_min'],
                    x['stf_end']),
                    axis=1))

            storage.loc[(storage['overpay-factor'] < 0) |
                        (storage['overpay-factor'].isnull()),
                        'overpay-factor'] = 0

            storage['stf_dist'] = (storage['support_timeframe']
                                   .apply(stf_dist, m=m))
            storage['discount-factor'] = (storage['support_timeframe']
                                          .apply(discount_factor, m=m))
            storage['eff-distance'] = (storage['stf_dist']
                                       .apply(effective_distance, m=m))
            storage['cost_factor'] = (storage['discount-factor'] *
                                      storage['eff-distance'])
    else:
        # for one year problems
        process['invcost-factor'] = (
            process.apply(
                lambda x: invcost_factor(
                    x['depreciation'],
                    x['wacc']),
                axis=1))

        # cost factor will be set to 1 for non intertemporal problems
        commodity['cost_factor'] = 1
        process['cost_factor'] = 1

        # additional features
        if m.mode['tra']:
            transmission['invcost-factor'] = (
                transmission.apply(lambda x:
                                   invcost_factor(x['depreciation'],
                                                  x['wacc']),
                                   axis=1))
            transmission['cost_factor'] = 1
        if m.mode['sto']:
            storage['invcost-factor'] = (
                storage.apply(lambda x:
                              invcost_factor(x['depreciation'],
                                             x['wacc']),
                              axis=1))
            storage['cost_factor'] = 1

    # Converting Data frames to dictionaries
    m.global_prop_dict = m.global_prop.to_dict()
    m.commodity_dict = commodity.to_dict()
    m.process_dict = process.to_dict()

    # dictionaries for additional features
    if m.mode['tra']:
        m.transmission_dict = transmission.to_dict()
        # DCPF transmission lines are bidirectional and do not have symmetry
        # fix-cost and inv-cost should be multiplied by 2
        if m.mode['dpf']:
            transmission_dc = transmission[transmission['reactance'] > 0]
            m.transmission_dc_dict = transmission_dc.to_dict()
            for t in m.transmission_dc_dict['reactance']:
                m.transmission_dict['inv-cost'][t] = 2 * m.transmission_dict['inv-cost'][t]
                m.transmission_dict['fix-cost'][t] = 2 * m.transmission_dict['fix-cost'][t]

    if m.mode['sto']:
        m.storage_dict = storage.to_dict()

    # update m.mode['exp'] and write dictionaries with constant capacities
    m.mode['exp']['pro'] = identify_expansion(pro_const_cap['inst-cap'],
                                              process['inst-cap'].dropna())
    m.pro_const_cap_dict = pro_const_cap['inst-cap'].to_dict()

    if m.mode['tra']:
        m.mode['exp']['tra'] = identify_expansion(
            tra_const_cap['inst-cap'],
            transmission['inst-cap'].dropna())
        m.tra_const_cap_dict = tra_const_cap['inst-cap'].to_dict()

    if m.mode['sto']:
        m.mode['exp']['sto-c'] = identify_expansion(
            sto_const_cap_c['inst-cap-c'], storage['inst-cap-c'].dropna())
        m.sto_const_cap_c_dict = sto_const_cap_c['inst-cap-c'].to_dict()
        m.mode['exp']['sto-p'] = identify_expansion(
            sto_const_cap_c['inst-cap-p'], storage['inst-cap-p'].dropna())
        m.sto_const_cap_p_dict = sto_const_cap_p['inst-cap-p'].to_dict()

    return m
Exemplo n.º 9
0
Arquivo: input.py Projeto: yabata/urbs
def pyomo_model_prep(data, timesteps):
    m = pyomo.ConcreteModel()

    # Preparations
    # ============
    # Data import. Syntax to access a value within equation definitions looks
    # like this:
    #
    #     m.storage.loc[site, storage, commodity][attribute]
    #
    m.global_prop = data['global_prop'].drop('description', axis=1)
    m.site = data['site']
    m.commodity = data['commodity']
    m.process = data['process']
    m.process_commodity = data['process_commodity']
    m.transmission = data['transmission']
    m.storage = data['storage']
    m.demand = data['demand']
    m.supim = data['supim']
    m.buy_sell_price = data['buy_sell_price']
    m.timesteps = timesteps
    m.dsm = data['dsm']

    # Converting Data frames to dict
    m.commodity_dict = m.commodity.to_dict()  # Changed
    m.demand_dict = m.demand.to_dict()  # Changed
    m.supim_dict = m.supim.to_dict()  # Changed
    m.dsm_dict = m.dsm.to_dict()  # Changed
    m.buy_sell_price_dict = m.buy_sell_price.to_dict()

    # process input/output ratios
    m.r_in = m.process_commodity.xs('In', level='Direction')['ratio']
    m.r_out = m.process_commodity.xs('Out', level='Direction')['ratio']
    m.r_in_dict = m.r_in.to_dict()
    m.r_out_dict = m.r_out.to_dict()

    # process areas
    m.proc_area = m.process['area-per-cap']
    m.sit_area = m.site['area']
    m.proc_area = m.proc_area[m.proc_area >= 0]
    m.sit_area = m.sit_area[m.sit_area >= 0]

    # input ratios for partial efficiencies
    # only keep those entries whose values are
    # a) positive and
    # b) numeric (implicitely, as NaN or NV compare false against 0)
    m.r_in_min_fraction = m.process_commodity.xs('In', level='Direction')
    m.r_in_min_fraction = m.r_in_min_fraction['ratio-min']
    m.r_in_min_fraction = m.r_in_min_fraction[m.r_in_min_fraction > 0]

    # output ratios for partial efficiencies
    # only keep those entries whose values are
    # a) positive and
    # b) numeric (implicitely, as NaN or NV compare false against 0)
    m.r_out_min_fraction = m.process_commodity.xs('Out', level='Direction')
    m.r_out_min_fraction = m.r_out_min_fraction['ratio-min']
    m.r_out_min_fraction = m.r_out_min_fraction[m.r_out_min_fraction > 0]

    # derive annuity factor from WACC and depreciation duration
    m.process['annuity-factor'] = annuity_factor(m.process['depreciation'],
                                                 m.process['wacc'])
    m.transmission['annuity-factor'] = annuity_factor(
        m.transmission['depreciation'], m.transmission['wacc'])
    m.storage['annuity-factor'] = annuity_factor(m.storage['depreciation'],
                                                 m.storage['wacc'])

    # Converting Data frames to dictionaries
    #
    m.process_dict = m.process.to_dict()  # Changed
    m.transmission_dict = m.transmission.to_dict()  # Changed
    m.storage_dict = m.storage.to_dict()  # Changed
    return m
Exemplo n.º 10
0
    def test_collect_mutable_parameters(self):
        model = pc.ConcreteModel()
        model.p = pc.Param(mutable=True)
        model.q = pc.Param([1], mutable=True, initialize=1.0)
        model.r = pc.Param(initialize=1.1, mutable=False)
        model.x = pc.Var()
        for obj in [model.p, model.q[1]]:

            result = EmbeddedSP._collect_mutable_parameters(obj)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(obj + 1)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(2 * (obj + 1))
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(2 * obj)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(2 * obj + 1)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(2 * obj + 1 +
                                                            model.x)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(obj * model.x)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(model.x / obj)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(model.x /
                                                            (2 * obj))
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(
                obj * pc.log(2 * model.x))
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(
                obj * pc.sin(model.r)**model.x)
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

            result = EmbeddedSP._collect_mutable_parameters(
                model.x**(obj * pc.sin(model.r)))
            self.assertTrue(id(obj) in result)
            self.assertEqual(len(result), 1)
            del result

        result = EmbeddedSP._collect_mutable_parameters(1.0)
        self.assertEqual(len(result), 0)
        del result

        result = EmbeddedSP._collect_mutable_parameters(model.p + model.q[1] +
                                                        model.r)
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result

        result = EmbeddedSP._collect_mutable_parameters(model.p + 1 + model.r +
                                                        model.q[1])
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)

        result = EmbeddedSP._collect_mutable_parameters(model.q[1] * 2 *
                                                        (model.p + model.r) +
                                                        model.r)
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result

        result = EmbeddedSP._collect_mutable_parameters(2 * model.x * model.p *
                                                        model.q[1] * model.r)
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result

        result = EmbeddedSP._collect_mutable_parameters(2 * obj * model.q[1] *
                                                        model.r + 1)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 1)
        del result

        result = EmbeddedSP._collect_mutable_parameters(2 * model.q[1] + 1 +
                                                        model.x - model.p)
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result

        result = EmbeddedSP._collect_mutable_parameters(model.r * model.x)
        self.assertEqual(len(result), 0)
        del result

        result = EmbeddedSP._collect_mutable_parameters(model.x / obj)
        self.assertTrue(id(obj) in result)
        self.assertEqual(len(result), 1)
        del result

        result = EmbeddedSP._collect_mutable_parameters(
            model.x / (2 * model.q[1] / model.p))
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result

        result = EmbeddedSP._collect_mutable_parameters(
            (model.p / model.q[1]) * pc.log(2 * model.x))
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result

        result = EmbeddedSP._collect_mutable_parameters(
            model.q[1] * pc.sin(model.p)**(model.x + model.r))
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result

        result = EmbeddedSP._collect_mutable_parameters(
            (model.p + model.x)**(model.q[1] * pc.sin(model.r)))
        self.assertTrue(id(model.p) in result)
        self.assertTrue(id(model.q[1]) in result)
        self.assertEqual(len(result), 2)
        del result
Exemplo n.º 11
0
def pyomo_model_prep(data, timesteps):
    m = pyomo.ConcreteModel()

    m.timesteps = timesteps
    process = data['process']
    transmission = data['transmission']
    storage = data['storage']

    # Converting Data frames to dict
    m.global_prop_dict = (data['global_prop'].drop('description', axis=1)
                          .to_dict())
    m.site_dict = data["site"].to_dict()
    m.commodity_dict = data["commodity"].to_dict()
    m.demand_dict = data["demand"].to_dict()
    m.supim_dict = data["supim"].to_dict()
    m.dsm_dict = data["dsm"].to_dict()
    m.buy_sell_price_dict = data["buy_sell_price"].to_dict()
    m.eff_factor_dict = data["eff_factor"].to_dict()

    # process input/output ratios
    m.r_in_dict = (data['process_commodity'].xs('In', level='Direction')
                   ['ratio'].to_dict())
    m.r_out_dict = (data['process_commodity'].xs('Out', level='Direction')
                    ['ratio'].to_dict())

    # process areas
    proc_area = data["process"]['area-per-cap']
    proc_area = proc_area[proc_area >= 0]
    m.proc_area_dict = proc_area.to_dict()

    # input ratios for partial efficiencies
    # only keep those entries whose values are
    # a) positive and
    # b) numeric (implicitely, as NaN or NV compare false against 0)
    r_in_min_fraction = data['process_commodity'].xs('In', level='Direction')
    r_in_min_fraction = r_in_min_fraction['ratio-min']
    r_in_min_fraction = r_in_min_fraction[r_in_min_fraction > 0]
    m.r_in_min_fraction_dict = r_in_min_fraction.to_dict()

    # output ratios for partial efficiencies
    # only keep those entries whose values are
    # a) positive and
    # b) numeric (implicitely, as NaN or NV compare false against 0)
    r_out_min_fraction = data['process_commodity'].xs('Out', level='Direction')
    r_out_min_fraction = r_out_min_fraction['ratio-min']
    r_out_min_fraction = r_out_min_fraction[r_out_min_fraction > 0]
    m.r_out_min_fraction_dict = r_out_min_fraction.to_dict()

    # storages with fixed initial state
    stor_init_bound = storage['init']
    m.stor_init_bound_dict = stor_init_bound[stor_init_bound >= 0].to_dict()

    # storages with fixed energy-to-power ratio
    try:
        sto_ep_ratio = storage['ep-ratio']
        m.sto_ep_ratio_dict = sto_ep_ratio[sto_ep_ratio >= 0].to_dict()
    except:
        m.sto_ep_ratio_dict = pd.DataFrame()

    # derive annuity factor from WACC and depreciation duration
    process['annuity-factor'] = (
        process.apply(lambda x: annuity_factor(x['depreciation'],
                                               x['wacc']),
                      axis=1))
    try:
        transmission['annuity-factor'] = (
            transmission.apply(lambda x: annuity_factor(x['depreciation'],
                                                        x['wacc']),
                               axis=1))
    except ValueError:
        pass
    try:
        storage['annuity-factor'] = (
            storage.apply(lambda x: annuity_factor(x['depreciation'],
                                                   x['wacc']),
                          axis=1))
    except ValueError:
        pass

    # Converting Data frames to dictionaries
    m.process_dict = process.to_dict()
    m.transmission_dict = transmission.to_dict()
    m.storage_dict = storage.to_dict()
    return m
Exemplo n.º 12
0
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
Exemplo n.º 13
0
def lp_unit_factors(ranges, solver_name, solver_io, solver_tolerance):
    '''Solve an auxiliary LP to find optimal scaling factors for given unit ranges:
    Given a set of units {u1, u2, ..., un}
    and the range of absolute values of each unit 
    { u1 -> [l1, r1], ..., un -> [ln, rn]}
    we want to find scaling factors f1, ..., fn that minimize
    max(fi*ri)/min(fj*rj)
    
    A difficulty arises because each unit is a fraction of two base units: ui = bj/bk
    and we need to retain consistency between scaling factors. 
    Thus we optimize the scaling factors Fj of base units bj and compute factors of "composite" units, i.e.
    
    ui = bj/bk --> fi = Fj/Fk'''

    def bound_rule(model, num1, den1, v1, num2, den2, v2):
        '''Link r, the log of the largest gap, to the scaling factor variables.'''
        def g(acc):
            return model.x[acc] if acc != 'const' else 0
    
        return g(num1) - g(den1) + v1 - g(num2) + g(den2) - v2 <= model.r


    def lower_limits_rule(model, num1, den1, v):
        '''Forbid that unit num1/den1 is scaled too low.'''
        def g(acc):
            return model.x[acc] if acc != 'const' else 0
    
        return g(num1) - g(den1) >= v


    model = po.ConcreteModel()

    '''
    variables
    (a) We create one variable for the log of the scaling factor of each unit
    (b) And one variable r to be minimized 
    '''
    unitvars = [unit for unit in filter(
        lambda u: u != 'const',
        list(set(
            list(map(lambda r: r['num'], ranges)) +
            list(map(lambda r: r['den'], ranges))
        )))
    ]

    model.x = po.Var(unitvars, domain=po.Reals)
    model.r = po.Var(domain=po.Reals)

    '''
    set objective to minimize r
    '''
    model.cost = po.Objective(expr = model.r)
    
    '''
    Constraints:
    limit si - sj + (u_hi){i/j} - sk + sl - (u_lo){k/l} <= r 
    for all pairs of units u{i/j}, u{k/l}
    to find max_{{i/j}, {k/l} \in units}(si*sl/sj*sk (u_hi){i/j} / (u_lo){k/l})
    '''
    ranges_wo_const = [rng for rng in ranges if rng['num'] != 'const' or rng['den'] != 'const']
    boundvals = [
        (r1['num'], r1['den'], math.log(r1['max'], 2), r2['num'], r2['den'], math.log(r2['min'], 2))
        for r1 in ranges for r2 in ranges
    ]
    
    model.bounds = po.Constraint(boundvals, rule=bound_rule)

    solver = SolverFactory(solver_name, solver_io=solver_io)
    solver.solve(model)
    temp_facs = {k: 2**model.x[k]() for k in unitvars}
    maxs = [r['max'] * temp_facs.get(r['num'], 1) / temp_facs.get(r['den'], 1) for r in ranges]
    mins = [r['min'] * temp_facs.get(r['num'], 1) / temp_facs.get(r['den'], 1) for r in ranges]
    best_range = max(maxs)/min(mins)
    print('best range: {}'.format(best_range))

    '''
    ensure that absolute values in model are not scaled below a certain threshold.
    this generally limits the objective function -> need to find good tradeoff.

    Practical Guidelines for Solving Difficult Linear Programs suggests we should ensure that user input consists of values larger than the solver tolerances tol, thus limit values to scaling_tolerance_threshold*tol. Or center the values around 0 if this gives an even higher threshold.

    note: our coefficient rounding below may lead to values 2 times smaller than the set limit, thus limit by 2*scaling_tolerance_threshold*tol 
    '''
    lower_limit = 10*solver_tolerance
    print('setting limit {}'.format(lower_limit))


    violating_mins = set()
    while True:
        changed = False
        factors = {k: 2**model.x[k]() for k in unitvars}
        for rng in ranges_wo_const: 
            num = factors[rng['num']] if rng['num'] != 'const' else 1
            den = factors[rng['den']] if rng['den'] != 'const' else 1
            if rng['min']*num/den < solver_tolerance:
                print('violating {}/{}: {} -> add constr'.format(rng['num'], rng['den'], rng['min']*num/den))
                violating_mins.add((rng['num'], rng['den'], math.log(lower_limit/rng['min'], 2)))
                changed = True
            
        if not changed:
            break
                   
        if not model.component('lower_limits') is None:
            model.del_component(model.lower_limits)
            model.del_component(model.lower_limits_index)
                   
        model.add_component('lower_limits', po.Constraint(list(violating_mins), rule=lower_limits_rule))

        solver.solve(model)    

    '''
    we want all factors to be an exponent of 2 in order not to tamper with precision of user values (c.f. tomlin - on scaling linear programming problems)
    we achieve this by rounding the optimal values we just computed to integers before exponentiating them
    '''
    facs = {k: 2**math.floor(model.x[k]()) for k in unitvars}
    return facs
Exemplo n.º 14
0
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