Constraint

505 views
Skip to first unread message

DR Mohammed Riadh Habour

unread,
Sep 5, 2023, 6:30:18 AM9/5/23
to pypsa

Hi 

I would to custom a constraint in the pypsa code to to force the link export to distribute a total of 10 GWh and the link import to distribute a total of 9 GWh, in the simulation.

I would like to get an output not concentrate in time, it mean that out put should be variable between 0 and value for all snapshots  

 

The code

import pypsa
from pypsa.optimization.compat import get_var, define_constraints, linexpr

IES2 = pypsa.Network()

start_date = pd.Timestamp("2018-01-01 00:00:00")  
end_date = pd.Timestamp("2018-12-31 23:00:00")  
num_snapshots = (end_date - start_date).days * 24 + 1
IES2.set_snapshots(pd.date_range(start_date, end_date, freq='H'))                    

IES2.add("Bus","power_generation2")  

IES2.add("Load", "IE_Consumptio”, bus   = "power_generation2",p_set = Irish_electrical_demand_2018)


IES2.add("Generator", "Onshore_generators", bus = "power_generation2",
        marginal_cost  = Marginal_cost_onshore_generators,
        capital_cost    = Capital_cost_onshore_generators,                                                    p_nom           = Max_Irish_wind_generation_2018,
        p_max_pu       = Electrical_demand_2018['p_max_pu_onshore'],

        p_min_pu        = Electrical_demand_2018['p_max_pu_onshore'])

# Define bus of electricity generation (All gas turbines, gas fuel)
IES2.add("Bus",
        "All_gas_turbines_gas_fuel")

# Define store of annual electricity generation (All gas turbines, gas fuel)
IES2.add("Store",
        "Gas_elec_store",
        bus        = "All_gas_turbines_gas_fuel",
        e_nom      = Total_dispatch_all_gas_generator_2018_SEAI,
        e_initial  = Total_dispatch_all_gas_generator_2018_SEAI)

# Define all gas turbines generators, gas fuel)
IES2.add("Link",
        "OCGT_and_CCGT",
        bus0             = "All_gas_turbines_gas_fuel",
        bus1             = "power_generation2",
        p_nom_extendable = True,
        marginal_cost    = Marginal_cost_OCGT_and_CCGT,
        capital_cost     = Capital_cost_OCGT_and_CCGT)
####################################
# Add bus hydro pump storage
IES2.add("Bus",
        "pumped_storage_hydro")

# Add storage "Turlough Hill"
IES2.add("Store",
        "TH ",
         bus              = "pumped_storage_hydro",
         capital_cost     = Capital_cost_pump_hydro_storage,
         e_nom_extendable = True,
         e_nom_max        = e_nom_max_TH,
         e_cyclic         = True)

# Add link charger
IES2.add("Link",
         "charging",
        bus0             = "power_generation2",
        bus1             = "pumped_storage_hydro",
        p_nom_extendable = True,
        p_nom_max        = p_nom_TH,
     )

# Add link discharger
IES2.add("Link",
         "discharging",
        bus0             = "pumped_storage_hydro",
        bus1             = "power_generation2",
        p_nom_extendable = True,
        p_nom_max        = p_nom_TH)



# Add bus interconnector
IES2.add("Bus", "Inteconnection2")

# Add "Elect_interchange" store
IES2.add("Store",
        "Elect_interchange2",
        bus              = "Inteconnection2",
        e_nom_extendable = True,
        e_cyclic         = True)

# Add "Export" link with max available output power
IES2.add("Link",
        "Export2",
        bus0             = "power_generation2",
        bus1             = "Inteconnection2",
        capital_cost     = Capital_cost_inerconnector,
        p_nom_extendable = True,
        p_nom_max        = p_nom_Interconnector,
        p_min_pu         = 0.1)

# Add "Import" link with max available output power
IES2.add("Link",
        "Import2",
        bus0             = "Inteconnection2",
        bus1             = "power_generation2",
        p_nom_extendable = True,
        p_nom_max        = p_nom_Interconnector)

# Custom constraints:
# Custom contraint for import and export
def ratio_functionality(IES2,snapshots):
    link_p_nom = get_var(IES2, "Link", "p_nom")
    lhs = linexpr((1.0, link_p_nom["Export2"]),(-1.0, link_p_nom["Import2"]))
    define_constraints(IES2, lhs, "=", 0.0, 'Link', 'ratio')

def fix_ratio(IES2, snapshots):
    vars_link = get_var(IES2, "Link", "p_nom")
    lhs = linexpr((1.0, vars_link.loc["charging"]), (-1.0, vars_link.loc["discharging"]))
    define_constraints(IES2, lhs, "=", 0.0, 'Link', 'fix_ratio')
   
def extra_functionality2(IES2, snapshots):
    ratio_functionality(IES2, snapshots)
    fix_ratio(IES2, snapshots)

IES2.optimize(extra_functionality=extra_functionality2) 

 

DR Mohammed Riadh Habour

unread,
Sep 8, 2023, 2:19:47 PM9/8/23
to pypsa
Any one know a solution for that ? 

barry.m...@dcu.ie

unread,
Sep 12, 2023, 7:12:43 AM9/12/23
to pypsa
Dear Riadh -

You can use a custom constraint (via linopy) to set a specific total flow over a particular Link (called, say 'export') using something like the following:

...
network = pypsa.Network()

# Create the model
...

# Custom constraint to set total flow over "export" Link

total_export = 10.0e3 # MWh: customise as desired

lpmodel = network.optimize.create_model()
export_p_vars = lpmodel.variables['Link-p'].loc[:,'export']
export_p_total = export_p_vars.sum("snapshot")
lpmodel.add_constraints(export_p_total == total_export , name='total export')

...

# Run the optimisation
network.optimize.solve_model()

Similar code can be used to set a total flow in the other (import?) direction.

It's not entirely clear what the overall motivation is in your example model: but note that, with this kind of constraint (especially if it is paired with a similar constraint on the import direction) there is a significant risk that the optimisation will find a "solution" that involves unintended cycling (simultaneous export and import over a single physical interconnector). This is analogous to the problem of unintended storage cycling (USC) on store components. It is generally unphysical and unlikely to be something you want in your solution. While there are techniques to try to control or avoid this, the details will depend on your particular overall application and motivation.

You also mention that you would "like to get an output not concentrated in time". Assuming you are using capacity expansion for the Link(s), with non-zero capital_cost, that will tend to push the solution toward spreading the required flow out in time (thus allowing smaller maximum flow and thus less total capital cost). But more generally, the cost-optimal distribution of the flow on an interconnector over time would reflect differences in cost over time, on either side of it. Conversely, if there are no such differences in cost over time, then the solution will be degenerate (i.e. many different distributions in time will correspond to the same total cost, and the particular one provided in a single optimisation run will just be one fairly arbitrary example from that set of equal cost solutions). 

Kind regards - Barry

DR Mohammed Riadh Habour

unread,
Sep 18, 2023, 11:34:29 AM9/18/23
to pypsa
Dear Barry

Thank you for clarifying 
1/ This kind of constraint , can be applied for different links from different stores without USC ?  

2/ Even when using capacity expansion for the Link(s), with different capital cost, the output of almost links is constant over snapshots, 
are there any solution for that  ? 

Kind regards 

pypsa

unread,
Sep 19, 2023, 5:39:13 AM9/19/23
to pypsa
Hi Riadh -

1. This kind of constraint can be applied to any Link (and could be easily generalised to other components, such as Generators). There is a particular risk of unintended cycling when you have two unidirectional Links in opposite directions between the same two buses. However, I can imagine that this kind of constraint could still give a possibility of unintended cycling in more complicated networks, via more indirect routes: but that would depend on the details of your particular network, and all the constraints and costs at play.

2. I'm not sure I understand. Your original question said you did not want the power flow to be "concentrated in time", so I thought you intended that it should be spread out as much as possible. The limiting case of that is for it to be more or less constant (subject to whatever other constraints or cost factors may be in effect). But now you are saying that it is a problem if the power is constant? If you have some particular pattern in time that you want the power to follow, you could set the Link p_max_pu and p_min_pu both to a specific time series to force this. The p_nom could still be varied (under capacity expansion) to ensure that the total energy matches your custom constraint. However, I should say that it then becomes a bit unclear what the motivation for using LOPF here would be. In general, you would use LOPF because you want to identify a minimal (notional) cost way of meeting a particular set of interacting requirements. But if you already know in advance what (effectively unique) solution you want, then there may be nothing "left" for LOPF to tell you about these Links (and thus no need to resort to devising custom constraints as an indirect way of forcing that "solution").  In the specific case you describe, it begins to seem like you already know, in advance, both the preferred time pattern of the power flow, and the total amount over all snapshots, which together would already effectively tell you the exact capacity and absolute power flow in time at every snapshot: so there is no need or point in  using LOPF (via pypsa or otherwise) to find out either the optimal capacities or those detailed power flows in time. You could already just directly calculate both the respective p_nom_opt values and the power values in time (p0, p1) for these Links without recourse to LOPF. (Of course, there may still be questions of interest about the behaviour of the wider network that these Links are embedded in, where LOPF may indeed be the appropriate tool to answer them...)

Kind regards - Barry

DR Mohammed Riadh Habour

unread,
Sep 27, 2023, 10:35:07 AM9/27/23
to pypsa
Hi Barry

2.When using capacity expansion for the Link(s), with different capital cost,  the output is not concentrating in time , but there is other issue some generators outputs are  constant over all snapshots, whoever It is preferred to have a result  spread out as much as possible 
I tried some particular pattern in time with setting the Link p_max_pu and p_min_pu both to a specific time series, but the output is still constant over all snapshots  
LOPF is used to identify a minimal cost way of meeting a particular set of interacting requirements. I know, in advance, the total energy amount over all snapshots, and I add several constraints to limit the total energy for each generators but I still can not have a result  spread out as much as possible (the results are still constant over all snapshots for almost generators).
Of course I'm not looking for solution that propose known solution before optimisation implemented via p_max_pu. Are there another solutions to force output to spread out as much as possible ? 

Kind regards - Riadh

barry.m...@dcu.ie

unread,
Sep 27, 2023, 12:36:00 PM9/27/23
to pypsa
Hi Riadh -

You say:

  I tried some particular pattern in time with setting the Link p_max_pu and p_min_pu both to a specific time series, but the output is still constant over all snapshots

Could you post a minimal example notebook that demonstrates this?

Thanks - Barry
Reply all
Reply to author
Forward
0 new messages