Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use representative year for multi-year-periods in multi-period-investment #992

Open
nailend opened this issue Oct 12, 2023 · 8 comments · May be fixed by #993
Open

Use representative year for multi-year-periods in multi-period-investment #992

nailend opened this issue Oct 12, 2023 · 8 comments · May be fixed by #993

Comments

@nailend
Copy link
Contributor

nailend commented Oct 12, 2023

The yet implemented multi-period-investment was designed by the assumption to provide time series for every year (equivalent period) within the optimization horizon. Due to a long optimization horizon (~60 years), we need to use representative years/time series for the years within a multi-year-period.

Fortunately, @jokochems well-designed approach already facilitates most of this. Currently, only the flow costs (variable and fixed) for the flows are not handled accordingly. This could be done by adjusting values for the objective weighting but might collide with its use in the TSAM integration (as discussed in #987) which we also want to use.

Therefore, I propose the boolean-flag use_representative_year_in_multi_year_periods for solph.EnergySystem .
This would lead to a further condition at the calculation for the variable_costs, here.

In case it's True, variable costs would be calculated like this:

            for i, o in m.FLOWS:
                if m.flows[i, o].variable_costs[0] is not None:
                    if self.use_representative_year_in_multi_year_periods:
                        for p, timesteps in m.TIMESTEPS_IN_PERIOD.items():
                            # sum variable costs of representative year
                            variable_costs_increment = sum(
                                    m.flow[i, o, p, t]
                                    * m.objective_weighting[t]
                                    * m.flows[i, o].variable_costs[t]
                                    for t in timesteps
                            )
                            # discount to present value
                            variable_costs_increment =  
                                    variable_costs_increment 
                                    * ((1 + m.discount_rate) ** - m.es.periods_years[p])
                                )
                            # add and discount for every implicit year
                            variable_costs += sum(
                                variable_costs_increment
                                * ((1 + m.discount_rate) ** (-pp))
                                for pp in range(0, get_period_duration(p))
                            )
                    else:
                        ...

The variable costs for the representative/explicit year (with timeseries provided) would then be discounted and used for the implicit years of the period (without timeseries provided).

Further get_period_duration() from #982 would need some adaption for multi-year-periods as well:

def get_period_duration(self, period):
   
   period = self.period[period]
   if period < len(self.periods):
     duration = (
        self.periods[period+1].min().year
         - self.periods[period].min().year
         )
   elif period == len(self.periods):
     duration =  (
           self.periods[period].max().year
            - self.periods[period].min().year
            + 1
           )
   else:
     raise KeyError()

    return duration
@nailend nailend self-assigned this Oct 12, 2023
@nailend nailend linked a pull request Oct 12, 2023 that will close this issue
6 tasks
@jokochems
Copy link
Member

Hello @nailend,

I see what you are aiming for. Maybe I have not fully understood it, but wouldn't what I just commented for #987 maybe also address and potentially even solve this problem?

  • If you use one representative year to represent 10 years, timeincrement would be 1, right?
  • If you then explicitly specified an objective_weighting of 10, I think the current implementation would already cover it, wouldn't it?
  • Of course, you have to take care of proper discounting. To do so, you might either use real values to get rid of the discount_rate, i.e. you can set it to 0 in case your discount rate reflects only inflation rates. (There are some discussions about proper "social" discount rates, but this is what I think makes sense and use for my analyses.) Or if you have nominal cost values, you could pass a time series of present values of your costs in hourly resolution which you have to calculate in a preprocessing step. Of course, the latter is somewhat hacky, so I see why you want to address this inside the code base.

For the suggested adjustment in the variable_costs calculation, I see no problems except for some additional code duplication.
For the adjustment of the method get_period_duration(), I fear that we might run into trouble for the last period. If its length was only one year, let's assume its year 60, we would have range(60, 60), so an empty object. This now is problematic for investments in the last period as this range object is also needed for iteration for the investment annuities and the fixed costs and used in an annuity routine which cannot deal with a duration of 0, see #982.

Long story short: One should think of a safe(r) way for get_period_duration(), the remainder sounds reasonable.

@nailend
Copy link
Contributor Author

nailend commented Oct 16, 2023

  • If you use one representative year to represent 10 years, timeincrement would be 1, right?

yes!

  • If you then explicitly specified an objective_weighting of 10, I think the current implementation would already cover it, wouldn't it?

I guess, I haven't fully understood, what the effect of changing 'objective_weighting means for the discount rate.

  • Of course, you have to take care of proper discounting.
    ...
    ... the latter is somewhat hacky, so I see why you want to address this inside the code base.

Exactly!

For the suggested adjustment in the variable_costs calculation, I see no problems except for some additional code duplication. For the adjustment of the method get_period_duration(), I fear that we might run into trouble for the last period. If its length was only one year, let's assume its year 60, we would have range(60, 60), so an empty object. This now is problematic for investments in the last period as this range object is also needed for iteration for the investment annuities and the fixed costs and used in an annuity routine which cannot deal with a duration of 0, see #982.

I am not sure which range() you are referring to, but in calculation of the variable_costs the range should be range(0,1) and therefore just add variable costs for the last year without discounting it. (I have adapted the function slightly to be able to use -1 for selecting the last period). With this, I also don't see any complications with the investment_flow_block. Maybe you can link the range() you are referring to?

@nailend
Copy link
Contributor Author

nailend commented Oct 16, 2023

@jokochems I think I mixed up the if conditions. In the last period, your former code will be used, so there should be no problem if there was none before. Just in all other periods, I changed the method.

Sorry for the confusion!

@p-snft
Copy link
Member

p-snft commented Oct 18, 2023

I don't really get the issue. Can't you e.g. just use six representative years, each standing for ten years of your optimisation horizon? (If the issue is resolved, please close it.)

@nailend
Copy link
Contributor Author

nailend commented Oct 18, 2023

I don't really get the issue. Can't you e.g. just use six representative years, each standing for ten years of your optimisation horizon? (If the issue is resolved, please close it.)

That's exactly what I want to do, but currently the variable costs for the implicit years would not be accounted for. You can probably also do this with the objective_weighting, but I am not sure how to derive the correct value for six years and the discounting (6 * (1 + m.discount_rate) ** (-1) * (1 + m.discount_rate) ** (-2) .... * (1 + m.discount_rate) ** (-6) ?)

I would also see this as a frequently used feature for long term multi-period-investments and therefore useful. It's not that much of code changes, is it?

@p-snft
Copy link
Member

p-snft commented Oct 19, 2023

Currently, no costs would be considered automatically for the implicit years. Probably, this is also the most transparent way to work. Disregarding the current implementation, I would suggest the following solution:

CAPEX:

  1. Increase the discount rate, e.g. from 0.02 to 0.22 (1.22 = 1.02^10).
  2. Give lifetimes in multiples of 10 years, so that the deprecation matches.

OPEX:

  1. Calculate the discounted prices for every year.
  2. Use sums over the 10 year periods for optimisation.

In my opinion, the way to implement this is to disable automatic discounting for the OPEX. This would be useful anyway, as you often get prognosis for future energy costs discounted to today's values.

@jokochems
Copy link
Member

Currently, no costs would be considered automatically for the implicit years. Probably, this is also the most transparent way to work. Disregarding the current implementation, I would suggest the following solution:

CAPEX:

  1. Increase the discount rate, e.g. from 0.02 to 0.22 (1.22 = 1.02^10).
  2. Give lifetimes in multiples of 10 years, so that the deprecation matches.

OPEX:

  1. Calculate the discounted prices for every year.
  2. Use sums over the 10 year periods for optimisation.

In my opinion, the way to implement this is to disable automatic discounting for the OPEX. This would be useful anyway, as you often get prognosis for future energy costs discounted to today's values.

Hi, @p-snft. Thank you. I do see your point and I think, I commented similar stuff above.

I have some remarks:

  • Concerning the OPEX: Pre-compiling a 10 year (or whatever) time series of discounted variable costs is just not that handy which I see as a justification for @nailend's developments. I think, one can argue to place them outside solph, e.g. in oemof.tools, but I think, there might be a use case as long-term simulations blow up computation times and unless you have a big cluster computing node at hand, you might go for something with reduced complexity, e.g. using representative years.
  • On the remark on costs discounted to today's values (i.e. costs in real terms). I do agree. This is what you'll mostly find in the literature and what makes life easier. It is also is supported in the current implementation when simply setting the energy system's discount_rate attribute to 0.
  • The idea was/is to have a flexible framework where the user could feed in, both real or nominal values. Then the only thing she/he had to take care of would be choosing a suitable discount_rate rather than doing extensive precalculations

Also, I like your ideas on structural improvements and am looking forward towards the discussions at the next dev meeting. Thank you!

@jokochems
Copy link
Member

I am not sure which range() you are referring to, but in calculation of the variable_costs the range should be range(0,1) and therefore just add variable costs for the last year without discounting it. (I have adapted the function slightly to be able to use -1 for selecting the last period). With this, I also don't see any complications with the investment_flow_block. Maybe you can link the range() you are referring to?

I meant the ones to control for fixed costs being limited to the optimization horizon, for instance here: https://github.com/oemof/oemof-solph/blob/119e9cbab35f8289fcde3e0539486d53dd7e99bb/src/oemof/solph/flows/_investment_flow_block.py#L1025C29-L1028C30

The other remark was on the newly introduced end_year_of_optimization attribute for an energy system and the problem if this is the same as the start year of the last period.

But I think, this has been resolved as you altered the implementation of get_period_duration. I think, it even makes more sense as you defined it now - thank you. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants