• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rl-institut / multi-vector-simulator / 8870538658

28 Apr 2024 09:31PM UTC coverage: 75.582% (-1.4%) from 76.96%
8870538658

push

github

web-flow
Merge pull request #971 from rl-institut/fix/black-vulnerability

Fix/black vulnerability

26 of 29 new or added lines in 15 files covered. (89.66%)

826 existing lines in 21 files now uncovered.

5977 of 7908 relevant lines covered (75.58%)

0.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.0
/src/multi_vector_simulator/C2_economic_functions.py
1
"""
2
Module C2 - Economic preprocessing
3
==================================
4

5
Module C2 performs the economic pre-processing of MVS' input parameters.
6
It includes basic economic formulas.
7

8
Functionalities:
9
- Calculate annuity factor
10
- calculate crf depending on year
11
- calculate specific lifetime capex, considering replacement costs and residual value of the asset
12
- calculate annuity from present costs
13
- calculate present costs based on annuity
14
- calculate effective fuel price cost, in case there is a annual fuel price change (this functionality still has to be checked in this module)
15
"""
16

17
import logging
1✔
18
import pandas as pd
1✔
19

20
from multi_vector_simulator.utils.constants import UNIT_HOUR
1✔
21

22
from multi_vector_simulator.utils.constants_json_strings import (
1✔
23
    ANNUITY_FACTOR,
24
    DISPATCH_PRICE,
25
    VALUE,
26
    UNIT,
27
    LIFETIME_PRICE_DISPATCH,
28
)
29

30

31
# annuity factor to calculate present value of cash flows
32
def annuity_factor(project_life, discount_factor):
1✔
33
    r"""
34
    Calculates the annuity factor, which in turn in used to calculate the present value of annuities (instalments)
35

36
    Parameters
37
    ----------
38

39
    project_life: int
40
        time period over which the costs of the system occur
41
    discount_factor: float
42
        weighted average cost of capital, which is the after-tax average cost of various capital sources
43

44
    Returns
45
    -------
46
    annuity_factor: float
47
        financial value "annuity factor".
48
        Dividing a present cost by tha annuity factor returns its annuity,
49
        multiplying an annuity with the annuity factor returns its present value
50

51

52
    Notes
53
    -----
54

55
    .. math::
56

57
        annuity factor = \frac{1}{discount factor} - \frac{1}{
58
        discountfactor \cdot (1 + discount factor)^{project life}}
59

60
    """
61
    if discount_factor != 0:
1✔
62
        annuity_factor = 1 / discount_factor - 1 / (
1✔
63
            discount_factor * (1 + discount_factor) ** project_life
64
        )
65
    else:
UNCOV
66
        annuity_factor = project_life
×
67
    return annuity_factor
1✔
68

69

70
# accounting factor to translate present value to annual cash flows
71
def crf(project_life, discount_factor):
1✔
72
    """
73
    Calculates the capital recovery ratio used to determine the present value of a series of equal payments (annuity)
74

75
    :param project_life: time period over which the costs of the system occur
76
    :param discount_factor: weighted average cost of capital, which is the after-tax average cost of various capital sources
77
    :return: capital recovery factor, a ratio used to calculate the present value of an annuity
78
    """
79
    if discount_factor != 0:
1✔
80
        crf = (discount_factor * (1 + discount_factor) ** project_life) / (
1✔
81
            (1 + discount_factor) ** project_life - 1
82
        )
83
    else:
UNCOV
84
        crf = 1 / project_life
×
85

86
    return crf
1✔
87

88

89
def capex_from_investment(
1✔
90
    investment_t0,
91
    lifetime,
92
    project_life,
93
    discount_factor,
94
    tax,
95
    age_of_asset,
96
    asset_label="",
97
):
98
    """
99
    Calculates the capital expenditures, also known as CapEx.
100

101
    CapEx represent the total funds used to acquire or upgrade an asset.
102
    The specific capex is calculated by taking into account all future cash flows connected to the investment into one unit of the asset.
103
    This includes reinvestments, operation and management costs, dispatch costs as well as a deduction of the residual value at project end.
104
    The residual value is calculated with a linear depreciation of the last investment, ie. as a even share of the last investment over
105
    the lifetime of the asset. The remaining value of the asset is translated in a present value and then deducted.
106

107
    Parameters
108
    ----------
109
    investment_t0: float
110
        first investment at the beginning of the project made at year 0
111
    lifetime: int
112
        time period over which investments and re-investments can occur. can be equal to, longer or shorter than project_life
113
    project_life: int
114
        time period over which the costs of the system occur
115
    discount_factor: float
116
        weighted average cost of capital, which is the after-tax average cost of various capital sources
117
    tax: float
118
        compulsory financial charge paid to the government
119
    age_of_asset: int
120
        age since asset installation in year
121
    asset_label: str
122
        name of the asset
123

124
    Returns
125
    -------
126
    specific_capex: float
127
        Specific capital expenditure for an asset over project lifetime
128

129
    specific_replacement_costs_optimized: float
130
       Specific replacement costs for the asset capacity to be optimized, needed for E2
131

132
    specific_replacement_costs_already_installed: float
133
       replacement costs per unit for the currently already installed assets, needed for E2
134

135
    Notes
136
    -----
137
    Tested with
138
    - test_capex_from_investment_lifetime_equals_project_life()
139
    - test_capex_from_investment_lifetime_smaller_than_project_life()
140
    - test_capex_from_investment_lifetime_bigger_than_project_life()
141
    """
142

143
    first_time_investment = investment_t0 * (1 + tax)
1✔
144
    # Specific replacement costs for the asset capacity to be optimized
145
    specific_replacement_costs_optimized = get_replacement_costs(
1✔
146
        0, project_life, lifetime, first_time_investment, discount_factor
147
    )
148
    # Specific capex for the optimization
149
    specific_capex = first_time_investment + specific_replacement_costs_optimized
1✔
150

151
    # Calculating the replacement costs per unit for the currently already installed assets
152
    specific_replacement_costs_installed = get_replacement_costs(
1✔
153
        age_of_asset,
154
        project_life,
155
        lifetime,
156
        first_time_investment,
157
        discount_factor,
158
        asset_label=asset_label,
159
    )
160
    return (
1✔
161
        specific_capex,
162
        specific_replacement_costs_optimized,
163
        specific_replacement_costs_installed,
164
    )
165

166

167
def get_replacement_costs(
1✔
168
    age_of_asset,
169
    project_lifetime,
170
    asset_lifetime,
171
    first_time_investment,
172
    discount_factor,
173
    asset_label="",
174
):
175
    r"""
176
    Calculating the replacement costs of an asset
177

178
    Parameters
179
    ----------
180
    age_of_asset: int
181
        Age in years of an already installed asset
182

183
    project_lifetime: int
184
        Project duration in years
185

186
    asset_lifetime: int
187
        Lifetime of an asset in years
188

189
    first_time_investment: float
190
        Investment cost of an asset to be installed
191

192
    discount_factor: float
193
        Discount factor of a project
194

195
    asset_label: str
196
        name of the asset
197

198
    Returns
199
    -------
200
    Per-unit replacement costs of an asset. If age_asset == 0, they need to be added to the lifetime_specific_costs of the asset.
201
    If age_asset > 0, it will be needed to calculate the future investment costs of a previously installed asset.
202
    """
203

204
    # Calculate number of investments' rounds
205
    if project_lifetime + age_of_asset == asset_lifetime:
1✔
206
        number_of_investments = 1
1✔
207
    else:
208
        number_of_investments = int(
1✔
209
            round((project_lifetime + age_of_asset) / asset_lifetime + 0.5)
210
        )
211

212
    replacement_costs = 0
1✔
213
    # Latest investment is first investment
214
    latest_investment = first_time_investment
1✔
215
    # Starting from first investment (in the past for installed capacities)
216
    year = -age_of_asset
1✔
217
    if abs(year) > asset_lifetime:
1✔
UNCOV
218
        logging.error(
×
219
            f"The age of the asset `{asset_label}` ({age_of_asset} years) is lower or equal than "
220
            f"the asset lifetime ({asset_lifetime} years). This does not make sense, as a "
221
            f"replacement is imminent or should already have happened. Please check this value."
222
        )
223

224
    present_value_of_capital_expenditures = pd.DataFrame(
1✔
225
        [0 for i in range(0, project_lifetime + 1)],
226
        index=[j for j in range(0, project_lifetime + 1)],
227
    )
228

229
    # Looping over replacements, excluding first_time_investment in year (0 - age_of_asset)
230
    for count_of_replacements in range(1, number_of_investments):
1✔
231
        # replacements taking place after an asset ends its lifetime
232
        year += asset_lifetime
1✔
233
        if year < project_lifetime:
1✔
234
            # Update latest_investment (to be used for residual value)
235
            latest_investment = first_time_investment / (
1✔
236
                (1 + discount_factor) ** (year)
237
            )
238
            # Add latest investment to replacement costs
239
            replacement_costs += latest_investment
1✔
240
            # Update cash flow projection (specific)
241
            present_value_of_capital_expenditures.loc[year] = latest_investment
1✔
242
        elif year == project_lifetime:
1✔
243
            logging.warning(
1✔
244
                f"No asset `{asset_label}` replacement costs are computed for the project's "
245
                f"last year as the asset reach its end-of-life exactly on that year"
246
            )
247

248
    # Calculation of residual value / value at project end
249
    if year != project_lifetime:
1✔
250
        year += asset_lifetime
1✔
251
    if year > project_lifetime:
1✔
252
        # the residual of the capex at the end of the simulation time takes into
253
        linear_depreciation_last_investment = latest_investment / asset_lifetime
1✔
254
        # account the value of the money by dividing by (1 + discount_factor) ** (project_life)
255
        value_at_project_end = (
1✔
256
            linear_depreciation_last_investment
257
            * (year - project_lifetime)
258
            / (1 + discount_factor) ** (project_lifetime)
259
        )
260
        # Subtraction of component value at end of life with last replacement (= number_of_investments - 1)
261
        replacement_costs -= value_at_project_end
1✔
262
        # Update cash flow projection (specific)
263
        present_value_of_capital_expenditures.loc[project_lifetime] = (
1✔
264
            -value_at_project_end
265
        )
266

267
    return replacement_costs
1✔
268

269

270
def annuity(present_value, crf):
1✔
271
    """
272
    Calculates the annuity which is a fixed stream of payments incurred by investments in assets
273

274
    Parameters
275
    ----------
276
    present_value: float
277
        current equivalent value of a set of future cash flows for an asset
278
    crf: float
279
        ratio used to calculate the present value of an annuity
280

281
    Returns
282
    -------
283
    annuity: float
284
        annuity, i.e. payment made at equal intervals
285

286
    Notes
287
    -----
288
    Tested with test_annuity()
289
    """
290
    annuity = present_value * crf
1✔
291
    return annuity
1✔
292

293

294
def present_value_from_annuity(annuity, annuity_factor):
1✔
295
    """
296
    Calculates the present value of future instalments from an annuity
297

298
    Parameters
299
    ----------
300

301
    annuity: float
302
        payment made at equal intervals
303
    annuity_factor: float
304
        financial value
305

306
    Returns
307
    -------
308

309
    present_value: float
310
        present value of future payments from an annuity
311
    """
312
    present_value = annuity * annuity_factor
1✔
313
    return present_value
1✔
314

315

316
'''
317
Currently unused function.
318

319
def fuel_price_present_value(economics,):
320
    """
321
    Calculates the present value of the fuel price over the lifetime of the project, taking into consideration the annual price change
322

323
    Parameters
324
    ----------
325
    economics: dict
326
        dict with fuel data values
327
    :return: present value of the fuel price over the lifetime of the project
328

329
    Notes
330
    -----
331
    Tested with
332
    - test_present_value_from_annuity()
333
    """
334
    cash_flow_fuel_l = 0
335
    fuel_price_i = economics["fuel_price"]
336
    # todo check this calculation again!
337
    if economics["fuel_price_change_annual"] == 0:
338
        economics.update({"price_fuel": fuel_price_i})
339
    else:
340
        for i in range(0, economics[PROJECT_DURATION]):
341
            cash_flow_fuel_l += fuel_price_i / (1 + economics[DISCOUNTFACTOR]) ** (i)
342
            fuel_price_i = fuel_price_i * (1 + economics["fuel_price_change_annual"])
343
        economics.update({"price_fuel": cash_flow_fuel_l * economics[CRF]})
344
'''
345

346

347
def simulation_annuity(annuity, days):
1✔
348
    """
349
    Scaling annuity to timeframe
350
    Updating all annuities above to annuities "for the timeframe", so that optimization is based on more adequate
351
    costs. Includes project_cost_annuity, distribution_grid_cost_annuity, maingrid_extension_cost_annuity for
352
    consistency eventhough these are not used in optimization.
353

354
    Parameters
355
    ----------
356
    annuity: float
357
        Annuity of an asset
358

359
    days: int
360
        Days to be simulated
361

362
    Returns
363
    -------
364
    Simulation annuity that considers the lifetime cost for the optimization of one year duration.
365

366
    Notes
367
    -----
368
    Tested with
369
    - test_simulation_annuity_week
370
    - test_simulation_annuity_year
371
    """
372
    simulation_annuity = annuity / 365 * days
1✔
373
    return simulation_annuity
1✔
374

375

376
def determine_lifetime_price_dispatch(dict_asset, economic_data):
1✔
377
    """
378
    Determines the price of dispatch of an asset LIFETIME_PRICE_DISPATCH and updates the asset info.
379

380
    It takes into account the asset's future expenditures due to dispatch. Depending on the price data provided, another function is executed.
381

382
    Parameters
383
    ----------
384
    dict_asset: dict
385
        Data of an asset
386

387
    economic_data: dict
388
        Economic data, including CRF and ANNUITY_FACTOR
389

390
    Returns
391
    -------
392
    Updates asset dict
393

394
    Notes
395
    -----
396
    Tested with
397
    - test_determine_lifetime_price_dispatch_as_int()
398
    - test_determine_lifetime_price_dispatch_as_float()
399
    - test_determine_lifetime_price_dispatch_as_list()
400
    - test_determine_lifetime_price_dispatch_as_timeseries ()
401
    """
402
    # Dispatch price is provided as a scalar value
403
    if isinstance(dict_asset[DISPATCH_PRICE][VALUE], float) or isinstance(
1✔
404
        dict_asset[DISPATCH_PRICE][VALUE], int
405
    ):
406
        lifetime_price_dispatch = get_lifetime_price_dispatch_one_value(
1✔
407
            dict_asset[DISPATCH_PRICE][VALUE], economic_data
408
        )
409

410
    # Multiple dispatch prices are provided as asset is connected to multiple busses
411
    elif isinstance(dict_asset[DISPATCH_PRICE][VALUE], list):
1✔
412
        lifetime_price_dispatch = get_lifetime_price_dispatch_list(
1✔
413
            dict_asset[DISPATCH_PRICE][VALUE], economic_data
414
        )
415

416
    # Dispatch price is provided as a timeseries
417
    elif isinstance(dict_asset[DISPATCH_PRICE][VALUE], pd.Series):
1✔
418
        lifetime_price_dispatch = get_lifetime_price_dispatch_timeseries(
1✔
419
            dict_asset[DISPATCH_PRICE][VALUE], economic_data
420
        )
421

422
    else:
423
        raise ValueError(
1✔
424
            f"Type of dispatch_price neither int, float, list or pd.Series, but of type {dict_asset[DISPATCH_PRICE][VALUE]}. Is type correct?"
425
        )
426

427
    # Update asset dict
428
    dict_asset.update(
1✔
429
        {
430
            LIFETIME_PRICE_DISPATCH: {
431
                VALUE: lifetime_price_dispatch,
432
                UNIT: dict_asset[UNIT] + "/" + UNIT_HOUR,
433
            }
434
        }
435
    )
436

437

438
def get_lifetime_price_dispatch_one_value(dispatch_price, economic_data):
1✔
439
    """
440
    Lifetime dispatch price is a scalar value that is calulated with the annuity
441

442
    By doing this, the operational expenditures, in the simulation only taken into account for a year,
443
    can be compared to the investment costs.
444

445
    .. math::
446
        lifetime_price_dispatch = DISPATCH_PRICE \cdot ANNUITY_FACTOR
447

448
    Parameters
449
    ----------
450
    dispatch_price: float or int
451
        dispatch_price of the asset
452

453
    economic_data: dict
454
        Economic data
455

456
    Returns
457
    -------
458
    lifetime_price_dispatch: float
459
        Float that the asset dict is to be updated with
460

461
    Notes
462
    -----
463
    Tested with
464
    - test_determine_lifetime_price_dispatch_as_int()
465
    - test_determine_lifetime_price_dispatch_as_float()
466
    - test_get_lifetime_price_dispatch_one_value()
467
    """
468
    lifetime_price_dispatch = dispatch_price * economic_data[ANNUITY_FACTOR][VALUE]
1✔
469
    return lifetime_price_dispatch
1✔
470

471

472
def get_lifetime_price_dispatch_list(dispatch_price, economic_data):
1✔
473
    r"""
474
    Determines the lifetime dispatch price in case that the dispatch price is a list.
475

476
    The dispatch_price can be a list when for example if there are two input flows to a component, eg. water and electricity.
477
    There should be a lifetime_price_dispatch for each of them.
478

479
    .. math::
480
        lifetime\_price\_dispatch\_i = DISPATCH\_PRICE\_i \cdot ANNUITY\_FACTOR \forall i
481

482
    with :math:`i` for all list entries
483

484
    Parameters
485
    ----------
486
    dispatch_price: list
487
        Dispatch prices of the asset as a list
488

489
    economic_data: dict
490
        Economic data
491

492
    Returns
493
    -------
494
    lifetime_price_dispatch: list
495
        List of floats of lifetime dispatch price that the asset will be updated with
496

497

498
    Notes
499
    -----
500
    Tested with
501
    - test_determine_lifetime_price_dispatch_as_list()
502
    - test_get_lifetime_price_dispatch_list()
503
    """
504
    lifetime_price_dispatch = []
1✔
505
    for price_entry in dispatch_price:
1✔
506
        if isinstance(price_entry, float) or isinstance(price_entry, int):
1✔
507
            lifetime_price_dispatch.append(
1✔
508
                price_entry * economic_data[ANNUITY_FACTOR][VALUE]
509
            )
510
        elif isinstance(price_entry, pd.Series):
1✔
511
            lifetime_price_dispatch_entry = get_lifetime_price_dispatch_timeseries(
1✔
512
                price_entry, economic_data
513
            )
514
            lifetime_price_dispatch.append(lifetime_price_dispatch_entry)
1✔
515
        else:
UNCOV
516
            raise ValueError(
×
517
                f"Type of a dispatch_price entry of the list is neither int, float or pd.Series, but of type {type(price_entry)}. Is type correct?"
518
            )
519

520
    return lifetime_price_dispatch
1✔
521

522

523
def get_lifetime_price_dispatch_timeseries(dispatch_price, economic_data):
1✔
524
    r"""
525
    Calculates the lifetime price dispatch for a timeseries.
526
    The dispatch_price can be a timeseries, eg. in case that there is an hourly pricing.
527
    .. math::
528

529
        lifetime\_price\_dispatch(t) = DISPATCH\_PRICE(t) \cdot ANNUITY\_FACTOR \forall t
530

531
    Parameters
532
    ----------
533
    dispatch_price: :class:`pandas.Series`
534
        Dispatch price as a timeseries (eg. electricity prices)
535

536
    economic_data: dict
537
        Dict of economic data
538

539
    Returns
540
    -------
541
    lifetime_price_dispatch: float
542
        Lifetime dispatch price that the asset will be updated with
543

544
    Notes
545
    -----
546
    Tested with
547
        - test_determine_lifetime_price_dispatch_as_timeseries()
548
        - test_get_lifetime_price_dispatch_timeseries()
549

550
    """
551

552
    lifetime_price_dispatch = dispatch_price.multiply(
1✔
553
        economic_data[ANNUITY_FACTOR][VALUE], fill_value=0
554
    )
555
    return lifetime_price_dispatch
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc