calliope-project / calliope

A multi-scale energy systems modelling framework
https://www.callio.pe
Apache License 2.0
299 stars 94 forks source link

Potential misinterpretation of (internal) cost calculation related to OM cost term #645

Closed sstroemer closed 1 month ago

sstroemer commented 3 months ago

What happened?

Preface: The description below is way too long for how small the potential change is. It might be that I'm misreading the current status, then please ignore and just close this. It might also be entirely intended the way it currently is, in which case a small addition to the documentation of cost_om_annual_investment_fraction would render this obsolete. Text and calculations given at the end may be wrong, and include typos, etc. since time was limited, so please double-check.


Description

Currently, the internal annualisation of costs is done in cost_investment. The way it's done may be different to what some users may expect, based on the naming conventions, and may lead to confusion or misinterpretation of input data (and therefore wrong results).

The main motivation is the following observation of the calculation of cost_investment, which roughly looks like:

$$cost\textunderscore investment = \alpha \cdot (\delta \cdot total\textunderscore capex \cdot (1 + FOM{\%}) + FOM{absolute})$$

Here $\delta$ is some sort of depreciation/annualisation factor ($\alpha$ just allows to generalize that for periods different to a single year) which weights the total capital expenditure (correct), but also the parts of the fixed operation and maintenance costs ($FOM$). The latter may not necessarily be expected by users, because these are often given as either EUR/MW/a (represented by cost_om_annual, here $FOM_{absolute}$, correct) or %/a (represented by cost_om_annual_investment_fraction, here $FOM_{\%}$ - potentially misleading).

For both cases, it is a recurring fixed annual payment, which means it's already "per year" and does not have to be annualised. In the end, this leads to considerably reduced costs if using the "FOM are 2-5% depending on technology" approach (e.g., directly using Danish Energy Agency data would be fine, using the processed PyPSA technology-data would lead to wrong results, since they convert everything to %).

If that is actually a problem (and not intended), the easiest fix could be reworking to:

$$cost\textunderscore investment = \alpha \cdot ((\delta + FOM{\%}) \cdot total\textunderscore capex + FOM{absolute})$$

Everything below just contains additional thoughts, that could be relevant when doing the (breaking) change.

Thoughts

I'd like to point out / propose the following changes:

  1. Renaming of the global "expressions", including some changes to the operating costs.
  2. Changes to the calculation of the annualised costs, followed by changes to the operating costs.
  3. Updates in the documentation to reflect the changes (not discussed, just mentioned for completeness).

Renaming

  1. cost_var could be renamed to cost_operation_var (or something similar) - these are commonly referred to as VOM, variable operation and maintenance costs, which could be reflected by using cost_vom.
  2. Introduction of cost_operation_fix for fixed costs of operation. Similar to the above, these are commonly referred to as FOM, fixed operation and maintenance costs, which could be reflected by using cost_fom.
  3. Renaming of cost_investment to cost_investment_total, to better communicate that this is a (total) sum expression.
  4. Finally, the introduction of cost_investment_total_annualised, indicating that this accounts for annualisation.

Note 1: For consistency purposes, if doing a rename, it could be worth discussing to stick to a common convention for abbreviations. For example either: cost_var and cost_invest, or cost_variable and cost_investment; or any similar choice, but either both abbreviated or both as full names.

Note 2: I get the choice of "depreciation" due to the first two branches of the calculations, but it may be worth to discuss whether the name "annualisation weight" may be confused with the calculation of depreciation_rate in the third case link. This might be one of the main use cases (?), using an "annuity (factor)" for the annualisation of long-term costs under consideration of some cost-of-capital, whereas annualisation_weight only handles the proper scaling for non 1-year modeling periods.

Changing calculations

Initial suggestion

  1. cost_vom is given by the calculation of cost_var as it is.
  2. cost_investment_total is given as "the inner sum of investment costs" of the current cost_investment, roughly sum(cost_investment_flow_cap, over=carriers) + cost_investment_storage_cap + cost_investment_source_cap + cost_investment_area_use + cost_investment_purchase.
  3. cost_fom = annualisation_weight * (sum(cost_om_annual * flow_cap, over=carriers) + cost_investment_total * cost_om_annual_investment_fraction).
  4. cost_investment_total_annualised = annualisation_weight * depreciation_rate * cost_investment_total.
  5. The final / overall costs are then updated to: cost = cost_vom + cost_fom + cost_investment_total_annualised.

Drawbacks:

An alternative way

This looks like a bigger change, which is why I did not want to propose this as the main way to go, but it may be worth considering (steps 1 and 2 are identical):

  1. cost_vom is given by the calculation of cost_var as it is.
  2. cost_investment_total is given as "the inner sum of investment costs" of the current cost_investment, roughly sum(cost_investment_flow_cap, over=carriers) + cost_investment_storage_cap + cost_investment_source_cap + cost_investment_area_use + cost_investment_purchase.
  3. cost_fom = sum(cost_om_annual * flow_cap, over=carriers) + cost_investment_total * cost_om_annual_investment_fraction.
  4. The final / overall costs are then updated to: cost = cost_vom + annualisation_weight * (depreciation_rate * cost_investment_total + cost_fom).

This introduces less global expressions, and annualisation_weight is only needed in one location.

Which operating systems have you used?

Version

latest (4fc6b84fe3f17d6533765c5543192e773deb05a9)

Relevant log output

No response

brynpickering commented 3 months ago

Thanks @sstroemer. The bug looks to have been introduced in v0.7. It is as you suggest in v0.6.10.

Renaming

I don't like vom/fom. Had I not seen them in context in this issue, I wouldn't have known what they are. cost_operation_variable, cost_operation_fixed, cost_investment_total look to me like an reasonable compromise.

Global expressions

It's about more/less expressions, but also the value to the user of those expressions. The current split of investment costs is really just to stop cost_investment being excessively bloated with lots of conditionals. I like your "alternative way" - it's cleaner. Question is, would users prefer to be able to inspect their investment costs before or after annualisation? I don't have a definite answer to this.

sstroemer commented 3 months ago

I don't like vom/fom.

Sorry, that wasn't meant as a "hard suggestion" to adopt these namings, just the assumption that it might be reasonable since stuff like om_annual is already in use, and users may be used to those from stuff like the ens.dk techn. database. I like the explicit ones like cost_operation_fixed much more!

It's about more/less expressions, but also the value to the user of those expressions.

Question is, would users prefer to be able to inspect their investment costs before or after annualisation?

I assume both are of interest: Before gives me the total costs that my system spends, after gives me access to calculate proper annual shares of different cost types.

However, the only reason I brought up the more expressions topic is that implementing these as auxiliary variables with equality constraints may considerably hurt some ways to solve the models. I'm not entirely sure what the reason behind that is (compared to "expressions", that Pyomo and most others support out of the box, which are basically for free).

brynpickering commented 3 months ago

I like the explicit ones like cost_operation_fixed much more!

Great. Let's go with that, then.

I assume both are of interest

Both might be of interest, but it does increase model size in memory (expression objects just take up memory) and increases build time. It probably isn't a considerable increase as investment costs don't have a timeseries dimension, but something we should explore.

I wouldn't have them as auxiliary variables, for sure (that's why all our costs are "expressions", leveraging what Pyomo etc. support out-of-the-box).

sstroemer commented 3 months ago

I wouldn't have them as auxiliary variables, for sure (that's why all our costs are "expressions", leveraging what Pyomo etc. support out-of-the-box).

My bad, I did not check it for v0.7.0, that comment was based on the previous version (where I struggled a lot with stuff like cost(monetary__region2__battery_)). Please ignore that comment!

Both might be of interest, but it does increase model size in memory (expression objects just take up memory) and increases build time. It probably isn't a considerable increase as investment costs don't have a timeseries dimension, but something we should explore.

Just my personal opinion: As you indicated, due to these not being temporal, the memory and time-to-build cost is almost none compared to the rest of the model. Especially since they are not even "large expressions" (as in, they group investment variables, leading to a low amount of variable-coefficient pairs in the underlying data structure). But again, this is just quality-of-life and nothing important.