quintel / merit

A system for calculating hourly electricity and heat loads with a merit order
MIT License
3 stars 2 forks source link

Use step-function to model marginal cost of power plants #109

Closed ChaelKruip closed 10 years ago

ChaelKruip commented 10 years ago

As suggested in https://github.com/quintel/merit/issues/107 this improvement will increase the level of realism of electricity price calculations in the current Merit module and allow a self-consistent treatment of virtual 'supply' type interconnector plants.

The following function does the trick (Python code, but very close to pseudo code):

def linear_price_step(capacity):

    y_min = mean_marginal_cost * (1 - cost_spread / 2)
    y_max = mean_marginal_cost * (1 + cost_spread / 2)
    delta_y = y_max - y_min

    marginal_cost = y_min + np.floor(capacity / typical_capacity) * delta_y / (installed_capacity / typical_capacity)

    return marginal_cost

The parameters that describe the function are:

Here is an example with a mean marginal cost of 100 EUR / MWh, an installed capacity of 1000 MW and typical capacity of a plant of 150 MW. The spread is 2% around the mean: figure_1

ChaelKruip commented 10 years ago

Part of https://github.com/quintel/merit/issues/104

ChaelKruip commented 10 years ago

We need to make sure, however, that the end of the step function of the cheaper plant (A) is still lower than the beginning of the more expensive plant (B). So, we should enforce that the spread around the mean marginal cost cannot be larger than the difference between the marginal costs of two plants that are adjacent in the merit order. So this is situation that we should always have:

                                                              
                                                  +------+    
                                                  |      |    
                                            +-----+      |    
                                            |            |    
                                            |            |    
                                      +-----+            |    
                                      |                  |    
                                      |                  |    
                               +------+                  |    
                       +------++                         |    
                       |      ||                         |    
                 +-----+      ||                         |    
                 |            ||                         |    
                 |            ||                         |    
           +-----+            ||                         |    
           |                  ||                         |    
           |                  ||                         |    
    +------+                  ||                         |    
    |                         ||                         |    
    |                         ||                         |    
    |          A              ||            B            |    
    |                         ||                         |    
    |                         ||                         |    
    |                         ||                         |    
    +-------------------------++-------------------------+    
                                                              
antw commented 10 years ago

@ChaelKruip Support for the linear cost function has been added on the "convergence" branch. All producers now accept a cost in the form of:

  1. A numeric marginal_costs attribute, where the cost is the same in every point (the old behaviour).
  2. A cost_curve where the cost may vary by hour.
  3. Using the linear cost function by providing the mean cost and spread.

The commit contains more information (note that there's a typo in the first example: marginal_cost: 100 should be marginal_costs: 100 (plural)).

we should enforce that the spread around the mean marginal cost cannot be larger than the difference between the marginal costs of two plants that are adjacent in the merit order.

I have not added this yet. Since the cost of producers using a cost_curve may vary by hour, we would have to perform this validation for every hour in the year. I would prefer to implement it as a separate pre-calculation step, so that we can run it as part of the ETSource validation and skip it on ETEngine.

ChaelKruip commented 10 years ago

Since the cost of producers using a cost_curve may vary by hour, we would have to perform this validation for every hour in the year.

I think there are two things here that I could have more clear about (sorry for that):

Perhaps we should think about implementing a 'special' function for the 'price-driven' interconnector that checks every hour where in the merit order it needs to participate.

In pseudo code:

For every hour:
1. Find currently producing plant in fixed merit order (as if there was no interconnector)
2. Find marginal cost of price-setting plant (optionally from a step-function)
3. Check if the price of the interconnector is lower than the cost found in 2.
      IF this is the case:
            use interconnector
      ELSE:
            use current plant
antw commented 10 years ago

When ordering the normal plants by marginal cost using option 3 (the step function), only their mean marginal cost (which is the 'old' / 'option 1' marginal cost) need to be known.

This is pretty much how I implemented the step function. When a producer defines it's cost using a step function, the producer is ordered according to it's mean price. I didn't realise option 1 was also a mean price. Perhaps we could change how to set up a linear-cost function producer:

# Old-style producers:
Producer.new(marginal_costs: 100.0)

# Linear-cost function producers
Producer.new(cost_function: { mean: 100.0, spread: 0.02 })  # <- currently
Producer.new(marginal_costs: 100.0, spread: 0.02)           # <- proposed

When a plant has a cost-curve, it can have any conceivable cost at any hour and could be anywhere in the merit order. So, whereas the normal dispatchables are always ordered the same way year round (the merit order is fixed), the 'price-driven' interconnector can pop up anywhere even in the middle of a 'block of plants of the same type' (described as a producer with a cost function).

I was with you right up until the bolded text. :disappointed:

Let me describe my current implementation, and how I understand your request; then you can tell me if I'm wrong.

Old Implementation

Until the convergence project, marginal costs were constants: never changing throughout the year. Therefore, we could sort the producers once prior to calculating the merit order, safe in the knowledge that the order would never change.

New Implementation

If the merit order contains only those producers whose marginal cost is constant (option 1) or cost-function with a mean cost (option 3), we still use the old implementation.

When one or more producers sets it's cost/price using a curve, the marginal cost of the producer may be different in every hour. When this happens, we no longer sort the producers once, but do so for each hour. In pseudo-code:

No cost function producers ≥ 1 cost function producers
sort_producers_by_marginal_cost!
each_hour do |hour|
  calculate_hour!(hour)
end
each_hour do |hour|
  sort_producers_for_hour!(hour)
  calculate_hour!(hour)
end

This ensures that producers using a curve are correctly placed in the merit order. If the producer is very cheap in one hour, it will be used. If it is very expensive in the next, it won't be.

The Request

the 'price-driven' interconnector can pop up anywhere even in the middle of a 'block of plants of the same type' (described as a producer with a cost function).

As I understand it, you're saying that it is acceptable for an interconnect price to be inside the spread of a cost-function producer. We should assign demand to a producer until it becomes uncompetitive with the interconnect, after which demand should be assigned to the interconnect?

For example, let's say we have two dispatchable producers (D1 and D2) with the following cost ranges:

    99.0   99.4   99.8  100.2  100.6  101.0
D1   >------|------|------|------|------>
    min              mean              max

                                                 102.0  102.2  102.4  102.6
D2                                                 >------|------|------>
                                                  min       mean       max

The merit order will be:

  1. D1 (mean 100.0)
  2. D2 (mean 102.3)

Now add an interconnect (IC) with a cost of 100.2 (less than the maximum cost of DI).

    99.0   99.4   99.8  100.2  100.6  101.0
D1   >------|------|------|------|------>
    min              mean              max

IC                       >|<

                                                 102.0  102.2  102.4  100.6
D2                                                 >------|------|------>
                                                  min       mean       max

Ignoring the implementation what are you proposing the result of this should be?

In the current implementation, the merit order will now be:

  1. D1 (mean 100.0)
  2. IC (price 100.2)
  3. D2 (mean 102.3)

Demand will be assigned to D1 until it reaches capacity, then to IC, then to D2.

If I'm understanding correctly, you're saying the desired outcome is to assign demand to D1 until the cost rises to the interconnect price, then to the interconnect, then back to D1 until it reaches capacity, and finally to D2?

  1. D1 (while the price is ≤ 100.2 (IC.cost))
  2. IC
  3. D1 (until max capacity)
  4. D2
ChaelKruip commented 10 years ago

We should assign demand to a producer until it becomes uncompetitive with the interconnect, after which demand should be assigned to the interconnect?

Exactly.

If I'm understanding correctly,...

You are correct again. Do you see any show-stoppers here?

antw commented 10 years ago

Do you see any show-stoppers here?

I know I'm sounding like a broken record, but so long as this isn't needed in ETEngine, I think it will be fine. I can probably implement this and #112 by creating a custom calculator class. That way we can get the desired behaviour for the convergence project without affecting ETE.

ChaelKruip commented 10 years ago

I know I'm sounding like a broken record

But that's my favourite song! :wink: I think we are exploring some possibilities which will come in handy when / if we are going to implement import / export in the future but not more!

antw commented 10 years ago

@ChaelKruip I have a new calculator which we can use for the convergence project. Per your request, it will stop assigning load to a producer when it becomes uncompetitive, and resume later if it becomes competitive again. It will do this for all producers, not just interconnects. Load is assigned to each plant in chunks equal to it's typical capacity and then we look to see if assigning another chunk will be uncompetitive.

Here's an example of two merit order runs; the first with the "standard" MO calculator, and a second with the experimental one. Both tables show a single hour in the calculation (point 1000, February 11th @ 16:00):

Standard Calculator

+----+-----------------------------------------------+--------+---------+--------+
|    | Key                                           | %used  | Load    | M.Cost |
+----+-----------------------------------------------+--------+---------+--------+
| AO | energy_power_wind_turbine_inland              | 100.0% | 140.76  | 0.0    |
| AO | energy_power_hydro_river                      | 100.0% | 36.83   | 0.0    |
| AO | energy_power_wind_turbine_coastal             | 100.0% | 17.16   | 0.0    |
| AO | energy_power_wind_turbine_offshore            | 100.0% | 2.69    | 0.0    |
| AO | households_solar_pv_solar_radiation           | 100.0% | 0.36    | 0.0    |
| AO | buildings_solar_pv_solar_radiation            | 100.0% | 0.18    | 0.0    |
| AO | energy_power_supercritical_waste_mix          | 100.0% | 445.21  | 1.21   |
| AO | agriculture_chp_engine_network_gas            | 100.0% | 3201.09 | 82.28  |
| AO | buildings_collective_chp_network_gas          | 100.0% | 422.14  | 98.54  |
| AO | industry_chp_combined_cycle_gas_power_fuelmix | 100.0% | 1929.22 | 115.22 |
| AO | industry_chp_supercritical_wood_pellets       | 100.0% | 205.48  | 141.69 |
| TR | energy_power_nuclear_gen3_uranium_oxide       | 100.0% | 459.0   | 5.48   |
| TR | energy_power_combined_cycle_coal              | 100.0% | 227.7   | 21.16  |
| TR | energy_power_ultra_supercritical_coal         | 100.0% | 2364.02 | 27.44  |
| TR | energy_chp_ultra_supercritical_coal           | 100.0% | 1469.58 | 30.95  |
| TR | energy_power_combined_cycle_network_gas       | 100.0% | 3601.8  | 46.29  |
| TR | energy_chp_combined_cycle_network_gas         | 100.0% | 1552.32 | 68.32  |  <--
| TR | energy_power_ultra_supercritical_network_gas  | 0.5%   | 16.87   | 67.78  |  <--
| TR | energy_power_turbine_network_gas              | -      | 0.0     | 80.22  |
+----+-----------------------------------------------+--------+---------+--------+

Experimental Calculator

+----+-----------------------------------------------+--------+---------+--------+
|    | Key                                           | %used  | Load    | M.Cost |
+----+-----------------------------------------------+--------+---------+--------+
| AO | energy_power_wind_turbine_inland              | 100.0% | 140.76  | 0.0    |
| AO | energy_power_hydro_river                      | 100.0% | 36.83   | 0.0    |
| AO | energy_power_wind_turbine_coastal             | 100.0% | 17.16   | 0.0    |
| AO | energy_power_wind_turbine_offshore            | 100.0% | 2.69    | 0.0    |
| AO | households_solar_pv_solar_radiation           | 100.0% | 0.36    | 0.0    |
| AO | buildings_solar_pv_solar_radiation            | 100.0% | 0.18    | 0.0    |
| AO | energy_power_supercritical_waste_mix          | 100.0% | 445.21  | 1.21   |
| AO | agriculture_chp_engine_network_gas            | 100.0% | 3201.09 | 82.28  |
| AO | buildings_collective_chp_network_gas          | 100.0% | 422.14  | 98.54  |
| AO | industry_chp_combined_cycle_gas_power_fuelmix | 100.0% | 1929.22 | 115.22 |
| AO | industry_chp_supercritical_wood_pellets       | 100.0% | 205.48  | 141.69 |
| TR | energy_power_nuclear_gen3_uranium_oxide       | 100.0% | 459.0   | 5.48   |
| TR | energy_power_combined_cycle_coal              | 100.0% | 227.7   | 21.16  |
| TR | energy_power_ultra_supercritical_coal         | 100.0% | 2364.02 | 27.44  |
| TR | energy_chp_ultra_supercritical_coal           | 100.0% | 1469.58 | 30.95  |
| TR | energy_power_combined_cycle_network_gas       | 100.0% | 3601.8  | 46.29  |
| TR | energy_chp_combined_cycle_network_gas         | 50.07% | 777.19  | 67.82  |  <--
| TR | energy_power_ultra_supercritical_network_gas  | 23.27% | 792.0   | 68.1   |  <--
| TR | energy_power_turbine_network_gas              | -      | 0.0     | 80.22  |
+----+-----------------------------------------------+--------+---------+--------+

Note how the ultra super-critical network gas load is much higher, because at some point during calculating the hour it becomes cheaper than the CHP.

Debug output:

...
Add 784MW to energy_power_combined_cycle_network_gas @ €45.89
Add 784MW to energy_power_combined_cycle_network_gas @ €46.09
Add 466MW to energy_power_combined_cycle_network_gas @ €46.29
Add 575MW to energy_chp_combined_cycle_network_gas @ €67.31
Add 792MW to energy_power_ultra_supercritical_network_gas @ €67.78
Add 202MW to energy_chp_combined_cycle_network_gas @ €67.82

What do you think? We could restrict it to interconnects if you wish (we will have to if we ever do this on ETE, since it's 3x slower than the normal calculator), but this feels to me like an improvement...

ChaelKruip commented 10 years ago

because at some point during calculating the hour it becomes cheaper...

How can it be that during an hour things change? An hour is the smallest timescale that we use right? This does not seem right to me. A plant that should be completely 'used up' before the next in the merit order start running. The only reason I can think of right now is if the price curves of two plants are actually (incorrectly) overlapping (as discussed in https://github.com/quintel/merit/issues/109#issuecomment-48877444).

Load is assigned to each plant in chunks equal to it's typical capacity

Does that mean that you increase the load of the plant in steps of 'typical_capacity'? I think that could lead to performance issues if there are many small units of a plant. I have some ideas how to circumvent a 'unit-by-unit' approach but we can discuss that if relevant.

ChaelKruip commented 10 years ago

After discussing this with @antw, I understand that my two points above are no bugs but features! The price curves are overlapping on purpose to demonstrate how a price-driven interconnector can 'pop up' in the middle of a block of plants of a certain type.

The performance is not an issue at this point as we are using Merit in stand-alone mode.

Good work @antw!