Avoidance of circular flows caused by negative grid costs

Hi everyone,

I’m working on a local energy system model that includes a grid connection, photovoltaic (PV) panels, residential demand, EVs, and a battery storage, using oemof solph v0.5.2. The components are connected to a system core which is defined by a AC and DC bus connected by two converters (AC/DC and DC/AC).

As bidirectional flows are modeled by two unidirectional flows I run into the following problem:
Negative energy costs from the grid force the optimizer to buy as much energy as possible and therefore waste energy, if all batteries are fully charged and the residential demand is satisfied.
Since the unidirectional flows are assigned efficiencies, the optimizer generates a circular flow to waste energy through the efficiencies.
While this may still be feasible for some components in reality, a battery cannot physically be charged and discharged at the same time.

To prevent this, I want to avoid circular flows for the storage. For this, I’m using a modified version of limit_active_flow_count(), which can handle NonConvexFlowBlock and InvestNonConvexFlowBlock objects. This method seems to work well for the converter, but I’m running into issues with the battery storage if I try to additionally optimize the storage size already when creating the component without even applying my custom constraint.


self.ess = solph.components.GenericStorage(
    label='ess',
    inputs={self.connected_bus: solph.Flow(
        variable_costs=self.opex_spec,
        nonconvex=scenario.nonconvex,
    )},
    outputs={self.connected_bus: solph.Flow(
        nonconvex=scenario.nonconvex,
    )},
    loss_rate=0,
    balanced=True,
    initial_storage_level=self.ph_init_soc,
    invest_relation_input_capacity=self.chg_crate,
    invest_relation_output_capacity=self.dis_crate,
    inflow_conversion_factor=self.chg_eff,
    outflow_conversion_factor=self.dis_eff,
    investment=solph.Investment(ep_costs=self.epc))

File "C:\Users\user\AppData\Local\anaconda3\envs\oemof_env\Lib\site-packages\pyomo\core\base\indexed_component.py", line 889, in _validate_index
raise KeyError(
    KeyError: 'Index \'("<oemof.solph.buses._bus.Bus: \'dc_bus\'>", "<oemof.solph.components._generic_storage.GenericStorage: \'ess\'>", 0)\' is not valid for indexed component \'InvestmentFlowBlock.total\''

For context, dc_bus is the label for the connected bus in my model.

So here are my questions regarding this issue:

  1. What would be the right way to tackle negative prices and the resulting problems? Is there a better way to ensure that the system behaves physically, avoiding simultaneous inflow and outflow from the battery storage? I’ve tried using marginal costs (variable_costs=1.00E-8), but they didn’t work for me here as the negative grid costs are higher and higher marginal costs would have an unwanted effect on the result of the optimizer. Of course, any other alternative solutions to avoid the computationally expensive binary variables would be appreciated.

  2. How can I fix the error related to the InvestNonConvexFlowBlock? It seems like the model generator is only looking for standard investment flows (InvestmentFlowBlock), not nonconvex flows (InvestNonConvexFlowBlock).

Any advice or insights would be greatly appreciated. Thanks in advance for your help!

Best regards,
Brian

1 Like

Hi @briand,

the problem you are encountering is one that has also been discussed at user meetings. While limit_active_flow_count seems to solve the problem, you might still end up having a similar situation with intermittent charging and discharging serving the same purpose. Also, the binary constraint will come with increased computational complexity.

My personal opinion is that circular flows are a sign that something smells. As such, I would not suppress it but rather find the cause of the smell. In reality, you would probably not operate a battery in such a way because of wearing. In that sense, adding costs to charging and discharging the battery might be realistic, but you’d have to reduce the investment costs to compensate for that. (If not used, the battery deprecates very slowly, thus the annuity is low.) At the same time, it might be advised to add a resistivity to the (real world) system to be able to use the surplus electricity.

Cheers,
Patrik

Hi @pschoen,

thanks for your fast answer.
You’re completly right. Avoiding circular flows in one timestep doesn’t solve the overall problem if there are storages involved.
The suggestion of adding costs for the battery wear seems.
I’m going to try your suggestion try to find a resonable pair of values for the battery wear and the amount to which the investment costs have to be reduced in order to get similar results.
Is there a rule of thumb to estimate the correct costs for the charging and discharging of the storage? If I use the minimum costs from the grid to compute the minimum battery flow costs, this will most likely lead to an unwanted changed behavior for all other timesteps when grid cost is higher. (E.g. selling PV-energy instead of storing it). Therefore I guess it should be some trade-off value and not avoiding all circular flows. Is this correct? Do you have any documents from the user meeting that deal with this question?

Nevertheless: Do you have an idea how to overcome the coding problem described in question 2.?

Best regards
Brian

Unfortunately, this was just oral discussion. Nevertheless, here is a rough sketch of one iterative approach. We divide the costs into calendar ageing (replacement after a fixed period time) and cyclic ageing. Depending on the battery, you can expect a certain number of full cycles and a certain age before you have to replace it. Let’s assume, you have a 5 kWh battery that costs 5000 € and allows for 10000 full cycles or 20 years before it has to be replaced. Also, a linear combination (like 5000 full cycles and 10 years or 2500 cycles and 15 years) would mean a replacement.

If we have both types of ageing contributing equally, that would mean 0.25 € / 5 kWh = 0.05 €/kWh (variable costs for charging) and 2500 €/ 5 kWh / 10 a = 50 €/kWh/a (investment costs for the capacity). Now, if the optimisation yields more then 500 cycles a year, the weight needs to be shifted towards cyclic ageing, or towards calendar ageing in the other case. In total, charging becomes more expensive if its done often and cheaper if it is rarely done. Eventually, your iteration will converge.

(I don’t recall if something has to be done to consider unknown capacities. But I hope you have a starting point now.)

Hi Patrik,
thanks, I’ll use this approach as a starting point!

Hi Brian,

I studied the non convexity due to battery storage losses (which includes the discussion of simultaneous charge and discharge) a few years ago:

(with author’s PDF version with a few bonuses available at Convex Storage Loss Modeling for Optimal Energy Management - Archive ouverte HAL)

Forbidding the bidirectional flow in a battery is indeed a nonconvexity (see Fig 2 extracted from author’s version below: the red domain is nonconvex) that is typically modeled using a MILP (one binary variable for each time step):

Now, since my goal was to escape nonconvexity, I didn’t spend time on the MILP formulation, but it is in many of the references (including articles discussing to what extent it is legal to relax the binary constraints, but in the case of negative prices, as your experiments show, relaxation is wrong).

However, it is likely that indeed the MILP formulation depends on the rated power of the storage (P_{rated} on the figure). As a consequence, if the rated energy of the battery (which is proportional to P_{rated} is most models) isn’t a fixed parameter but rather an optimization variable, this probably creates a bilinearity (something like P_b^+ \leq P_{rated}.\delta with \delta \in {0,1}). However, it may be possible to replace the variable rated power by a fixed upper bound. I don’t know oemof internals so I don’t know I to implement this.

Now, in relation to @pschoen, I agree that including such binary constraints to forbid simultaneous charge and discharge can severely increase the solving time. However, I disagree with the idea that it is “a sign that something smells”. It’s just a situation of “downward flexibility shortage”. Also, I don’t think that including storage aging cost will solve the problem for sure (it may, but not for sure).

One quick and dirty way to circumvent the problem may be (if it doesn’t create a problem elsewhere in your study) to have zero storage losses (100% efficiency including any converter) so that the net effect of charge+discharge is zero. This would:

  1. cut the incentive to use circular flows for the optimization
  2. cancel the consequences of circular flows (no extraneous losses)

Hi @pierre-haessig,

thanks for elaborating this. I do agree with what you say, maybe “something smells” was a bit too sloppy. I’d call “downward flexibility shortage” a problem that should be fixed (not just in the model). So, if the model says that wearing batteries is good thing, there should be a cheaper alternative.

Still, the option to limit simultaneous charging and discharging can be implemented using an additional constraint, namely oemof.solph.constraints.shared_limit. As a second (experimental) option, bidirectional Flows can be an used. Nobody will prevent you from setting the minimum flow to a negative value, so you can do the following:

bus_el = Bus(label="bus_el")
P_max = max(P_in, P_out)
bus_bat = Bus(label="bus_battery"
    inputs= {bus_el: Flow(
        nominal_value=P_max,
        max=P_in/P_max,
        min=-P_out/P_max,  # works although not documented API
    )}
)
battery = GenericStorage(
   inputs = {bus_bat: Flow(nominal_value=P_in)}
   outputs = {bus_bat: Flow(nominal_value=P_out)}
   ...
)

Instead of @pierre-haessig’s formulation with the combined limit, you would have -P_\mathrm{lower} \le P_+ - P_- \le P_\mathrm{upper} (all P_x \ge 0 in this equation). I think this would be a quad-cycle in the sketch, so less restrictive. If the coefficient c_P in Eq. (5) in @pierre-haessig’s paper is zero, the additional bus might be omitted having a battery with only input an that can be negative. Then, you would have strict charge xor discharge without binary constraints but no transformation losses. Also, using solph like this is untested. (Please tell about your results if you try this.)

PS: Thanks @robbie.morrison. In the typeset version I’ve seen that I mixed terms, so I corrected the formula.

Just noting that our site supports the Discourse Math Plugin. So just set your LaTeX in $$ end points. Like this: … [previous example omitted] … I also needed to introduce some markup tweaks and so made this a wiki posting in case corrections are required. :neutral_face:

Later: So let’s settle on — for illustrative purposes — using a markdown code block with LaTeX specified as the programming language:

$$
 -P_\mathrm{lower} \le P_{+} - P_{-} \le P_\mathrm{upper}\ \text{(all}\ P_x \ge 0 \ \text{in this equation)}
$$

Rendering as:

-P_\mathrm{lower} \le P_{+} - P_{-} \le P_\mathrm{upper}\ \text{(all}\ P_x \ge 0 \ \text{in this equation)}
1 Like