Why does PyPSA curtail renewable generators

184 views
Skip to first unread message

Sebastian Wurm

unread,
Mar 27, 2025, 12:15:45 PMMar 27
to pypsa
I have a network with a generator modelling the grid access using some pricing curve, a PV, a storage and a constant load.

Why does PyPSA seem to curtail PV production even thought the battery is empty?
As you can see here:Figure_1.png

The PV generator has 0 marginal cost, why does PyPSA choose to avoid this seemingly "free" energy?

    network.add("Generator", "pv_generator",
                bus="pv_bus",
                carrier="electricity",
                control="PQ",
                p_nom_extendable=False,
                capital_cost=10,  
                marginal_cost=0,
                p_nom=90,
                p_min_pu = 0, # set to pv_profile_list to enforce maximum PV usage
                p_max_pu=pv_profile)
               

Jessica Guichard

unread,
Mar 27, 2025, 1:58:03 PMMar 27
to pypsa
I'm not sure about the details of your model, but I observed the same, storage tends to be filled up at the last moment possible and useful, and not as soon as it's possible.
As you can see, you do end up filling up the battery just before you stop having excess PV energy. If the model had started filling up the battery earlier, it would have needed to stop earlier filling up the battery, and curtailment amount would be the same.

In reality, you would most likely not do this, but instead first fill up the battery, and then curtail excess energy, as it would be hard to tell precisely in advance how much energy is produced later. The main difference this time shift would do to your results is when you use storage that has losses over time.   

Concerning the curtailment of "free" energy, if you want to absolutely avoid it, even when it's not useful, you must artificially create some incentive, but also somewhere to sent the excess electricity to. One example would be to apply a negative marginal cost to your energy source, so that not using it costs money (which corresponds to reality in some way). And/or you would need to change your model, allow more storage, for example, or export to grid (which I've modelled as storage that can have negative values (e_min_pu=-1, combined with non negative e_nom)), rather than energy source in my models.

Sebastian Wurm

unread,
Mar 31, 2025, 1:39:54 AMMar 31
to pypsa
I am glad I am not the only one who has observed this, "last second" usage of energy, but I'm still wondering why PyPSA does it that way and if there is a way to avoid this and make it more realistic? i.e. make it use the energy as soon as its available?

I have played around with negative marginal costs but never used negative e_min_pu values, what does that do exactly?

Jessica Guichard

unread,
Mar 31, 2025, 9:54:33 AMMar 31
to Sebastian Wurm, pypsa
I ignore why it is like that, in particular whether it is happening due to a code line(s) in PyPSA or in the optimisation solver.
It's indeed different from what one would have programmed thanks to some algorithm, if one would have written a code to solve your problem, where at some point you would have an "if" choice that tests whether the battery is full or not, which would then immediately lead to filling up the battery, is there is excess electricity and the battery is not full.

Concerning the use of negative e_min_pu values, this is how I've decided to model the "grid" in many of my models.

If you allow both sale and purchase of electricity from the grid, you could either model "purchase from the grid" thanks to a generator with a marginal cost for every MWh delivered by the generator representing the grid, and then have a "store" that represents "sell to the grid" with a marginal cost (money is received when energy is sent to storage) for every MWh sent into the storage representing the grid. You don't want to represent the grid as "demand" element, even if it represents some sort of demand, as otherwise you have to send electricity to the grid. By modelling the grid as "store" and providing a monetary incentive to sent electricity to it, you can allow "free" energy not to be curtailed.

If your model allows sale and purchase at the same price, you could use a single "store" component that is allowed to have negative values (so that you can import from the grid even at the beginning of the year, when not enough has been sent to the grid yet). The reason that one would do that is to track easily throughout the year the cumulative amount of energy sent to or received from the grid, as can be seen on the graph below. Obviously, you can also do that with the first option (generator and store), but you would need a bit more code lines to trace something like the graph below:

image.png

Below is the text that I've put in the "stores.csv" file of my model:

name,bus,e_nom,e_initial,e_nom_extendable,capital_cost,e_min_pu
grid,grid,1000,0,True,0,-1

there is also a "stores-marginal_cost" containing the following first lines (just as an example with a different marginal cost for every time step):

,grid
0,61.45
1,65.73
2,64.96
3,60.47
4,52.5
5,48.98
6,48.95
...

In "links.csv", I have the following line for the link allowing to send energy between the grid and demand:

name,bus0,bus1,efficiency,p_nom,p_nom_extendable,marginal_cost,capital_cost,p_min_pu
grid,grid,demand,1,0,TRUE,0,834,-1

As you can see above, I have a put 0 for marginal cost for the import/export cable, and a capital_cost for the cost of installation of such a connection (only useful, if you want to optimise the cable size, and not a necessity even in that case). but more importantly, I've given a negative value for "p_min_pu" to allow electricity being sent back and forth between the grid and demand with a single link (two different links might create issues, where the model sends electricity through both cables at the same time, unless you put an efficiency below 1, maybe - haven't tried).

--
You received this message because you are subscribed to a topic in the Google Groups "pypsa" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/pypsa/x_cB6qS76Cw/unsubscribe.
To unsubscribe from this group and all its topics, send an email to pypsa+un...@googlegroups.com.
To view this discussion, visit https://groups.google.com/d/msgid/pypsa/29e6d278-1325-4517-b023-92778c725e80n%40googlegroups.com.

Matthew Smith

unread,
Apr 1, 2025, 9:57:42 AMApr 1
to pypsa
Once you've optimised the capacities, you could see what a rolling horizon dispatch optimisation looks like. Where you only give the model, say, 24 hours foresight. You'll see a bit less of this behaviour, at least inter-day.

But overall I don't think this behaviour affects the end result much.

Markus Groissböck

unread,
Apr 11, 2025, 2:22:05 AMApr 11
to pypsa
The curtailment happens in the solver.
From the perspective of math there is no difference in charging earlier or later. This can be changed by e.g., having small negative OPEX and storage losses, this would lead to an earlier charge. 

Hope this helps,
Markus

Sebastian Wurm

unread,
Apr 14, 2025, 3:29:41 AMApr 14
to pypsa
Thank you everyone for your helpful remarks and advice!
Message has been deleted

Grant Chalmers

unread,
Jul 4, 2025, 12:27:26 AMJul 4
to pypsa
Hi,

Adding a small negative to marginal costs and storage losses certainly helps with getting the solver to charge batteries earlier. I guess it impacts other factos too, like preferencing imports over discharging batteries?

Regards,
Grant.

Reply all
Reply to author
Forward
0 new messages