feat: split capital_cost into investment and FOM costs#1507
Conversation
|
Very elegant solution given the current
I totally agree. |
fneum
left a comment
There was a problem hiding this comment.
Thanks @FabianHofmann for tackling this. It's good that the issue of separating FOM from capital_cost and a way to compute both periodized and overnight investment cost is moving forward!
First, I'll give the reasons why I think this solution unfortunately does not work (sorry!), then I'll think of a solution in a separate comment.
-
A discount rate of zero is a valid input for annualising investment costs and should therefore not be used as a switch. (Example: overnight cost=100, discount rate=0, lifetime=20 give periodized cost of 100/20=5). That's the strongest reason.
-
I don't think overloading
capital_costis good style. The meaning of values incapital_costthereby depend on another attribute and are not directly clear. If you want to do anything with thecapital_costattribute, you will always have to make a case distinction, which is inconvenient. That's a good reason, but less strong than 1.
I would prefer a solution where capital_cost still denotes the annuitized investment costs, and build a solution around that. I like how the FOM are separated from capital_cost and the additional discount_rate attribute.
|
The way I see it, this PR tries to achieve three things:
(1.) and (2.) are relatively straightforward to implement with backwards compatibility and minimal invasiveness to the current use of the A solution for (1.) and (2.) could look like this (recycled and adapted from #371 (comment)):
Overall, I think this offers a good compromise between backward compatibility, invasiveness to the current use of The suggestion does not offer more convenience for calculating annualized |
Isn't that very simple to achieve with a wrapper, that throws an error when both are there? Basically |
Doesn't sound too bad, actually. Just needs to be extended to all other cases of IO and network building / manipulation with a strong consistency check. |
That just starts mixing input and output variables, which we don't currently do by design. But I am not opposed to the idea in general. There is just more to it. In
We can't extend the same check, since in I am mainly wondering how many other use cases we would have, where an attribute could be both output and input. If there are not many, I would rather keep the existing input vs output structure, but assign them dynamically which we could do here with a hidden helper attribute, which stores the info which one was the input. And keep to (or start to properly) enforce only input can be changed, which would be best for any interface ( |
Yes, ideally that does not change.
Yes, that's what I thought. Not sure if that's elegant. Probably not.
Hmm, difficult. The user has to fix it by aligning either |
|
Thanks Lukas for elaborating a bit on the complexity of the I/O. Regarding the consistency_check:
I would still suggest to do exactly that and I don't think it's too bad. If the user specifies both values for some odd reason, that's most likely due to a mistake in their model building pipeline. So in the best case throwing a hard error in the consistency check would only improve their model by forcing the elimination of ambiguities. |
|
thanks all for the comments. I am wondering if there is good way to make the stretch between @lindnemi requirements for convenience which are valid and touch quite some use cases and @fneum's request to keep a clear data model and avoid ambiguity which I also strongly second, why I came up with the overloading approach. Proposal 1: A boolean attribute Like this we:
Note: FOM is independent and simply adds to period capital costs in optimization. # Old behavior (default)
n.add("Generator", "gas", capital_cost=105.8)
# New behavior with overnight costs
n.add("Generator", "wind",
capital_cost=1000, # interpreted as overnight
capital_as_overnight_cost=True,
discount_rate=0.07,
lifetime=25,
fom_cost=20,
)
# → n.c.generators.capital_cost["wind"] ≈ 85.8 (derived)
# → n.c.generators.total_cost["wind"] ≈ 105.8Proposal 2: Introduce an # Old behavior
n.add("Generator", "gas", capital_cost=105.8)
# New behavior
n.add("Generator", "wind",
overnight_cost=1000,
discount_rate=0.07, # 0% is valid here!
lifetime=25,
fom_cost=20,
)
# → optimization uses: 1000 * annuity(0.07, 25) + 20 ≈ 105.8Proposal 2 might be cleaner—no boolean flag, separate attributes with clear semantics, and |
|
Out of proposals 1 and 2, proposal 2 is cleaner. @lindnemi needs to chime in, since he already uses The attribute Not sure I understood the |
For me both is fine. I like Proposal 2 better, even though it will mean a bit more refactoring work on the PyPSA-DE side because there every asset already has What's most important for me, is that capital_cost, overnight_cost, FOM and lifetime are separately accesible in the solved networks (and ideally via n.statistics) |
Add support for overnight investment costs with automatic annuitization: - New attributes: discount_rate, lifetime, fom_cost for extendable components - New pypsa.costs module with annuity(), annuity_factor(), effective_annual_cost() - New statistics methods: investment() and fom() - Backward compatible: discount_rate=0 (default) treats capital_cost as pre-annuitized - Deprecate pypsa.common.annuity in favor of pypsa.costs.annuity Closes #371
- Add overnight_cost attribute (NaN default) that takes precedence when set - Change discount_rate default from 0 to NaN to allow 0% rate (simple depreciation) - Keep capital_cost semantics unchanged for backward compatibility - Update statistics.investment() and costs module for new logic
Disallow overnight_cost with varying investment period durations (nyears). When nyears is uniform, collapse to scalar before calculation. This removes complex np.outer broadcasting logic.
a4c758a to
3723c47
Compare
- Add API reference for pypsa.costs module - Add overnight_cost() and fom() to statistics metrics list - Document overnight cost vs direct capital_cost approaches in objective.md
pypsa/components/array.py
Outdated
| return res | ||
|
|
||
| @property | ||
| def periodized_cost(self) -> xr.DataArray: |
There was a problem hiding this comment.
Why should we put any of this under c.da? Currently c.da is only transforming df data, not giving you any custom properties
There was a problem hiding this comment.
It is not under .da ; it was in one of my previous attempts. but that did not feel good. it is a stand-alone components feature now.
but I guess what you mean is that this is not the best module where to define it? can move it to pypsa/components/components.py
pypsa/common.py
Outdated
|
|
||
|
|
||
| @deprecated( | ||
| deprecated_in="0.35.0", |
There was a problem hiding this comment.
@lkstrp deprecation now aligning with the convention?
Co-authored-by: Fabian Neumann <[email protected]>
|
@fneum I guess two things were not clear:
|
Closes #371.
Changes proposed in this Pull Request
Add support for separating
capital_costinto overnight investment costs and fixed O&M:EDIT (updated summary)
Add support for separating overnight investment costs from fixed O&M costs:
Checklist
docs.docs/release-notes.mdof the upcoming release is included.