Skip to content

Commit e4332e6

Browse files
authored
Aggregate LCOE and NPV over timeslices for objectives (#635)
* Capital/fixed/variable costs split by production * Fix error in `timeslices` module * Update results files * Add tests * Aggregate LCOE over timeslices for objectives * Update tests * Better test condition * Check cost results for infs or nans * Fix zero division error in timeslices module * Similar change to annual lcoe * Less convoluted way of aggregating timeslices * Add test * Add tests for multi-dimensional timeslice weights * Remove parameter from docstring * Clarify docstrings in timeslice module * Add test for NPV * Extend to NPV * Add test for npv
1 parent 56c5261 commit e4332e6

4 files changed

Lines changed: 103 additions & 30 deletions

File tree

src/muse/costs.py

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def running_costs(
189189
capacity: xr.DataArray,
190190
production: xr.DataArray,
191191
consumption: xr.DataArray,
192+
aggregate_timeslices: bool = False,
192193
) -> xr.DataArray:
193194
"""Total annual running costs (excluding capital costs).
194195
@@ -206,19 +207,26 @@ def running_costs(
206207
_fuel_costs = fuel_costs(technologies, prices, consumption)
207208
_material_costs = material_costs(technologies, prices, consumption)
208209

210+
# Aggregate over timeslices (if required)
211+
if aggregate_timeslices:
212+
_environmental_costs = _environmental_costs.sum("timeslice")
213+
_fuel_costs = _fuel_costs.sum("timeslice")
214+
_material_costs = _material_costs.sum("timeslice")
215+
209216
# Costs associated with capacity and production level (annual)
210217
_fixed_costs = fixed_costs(technologies, capacity)
211218
_variable_costs = variable_costs(technologies, production)
212219

213-
# Split fixed/variable across timeslices in proportion to production
214-
timeslice_level = get_level(production)
215-
tech_activity = production_amplitude(production, technologies)
216-
_fixed_costs = distribute_timeslice(
217-
_fixed_costs, ts=tech_activity, level=timeslice_level
218-
)
219-
_variable_costs = distribute_timeslice(
220-
_variable_costs, ts=tech_activity, level=timeslice_level
221-
)
220+
# Split fixed/variable across timeslices in proportion to production (if required)
221+
if not aggregate_timeslices:
222+
timeslice_level = get_level(production)
223+
tech_activity = production_amplitude(production, technologies)
224+
_fixed_costs = distribute_timeslice(
225+
_fixed_costs, ts=tech_activity, level=timeslice_level
226+
)
227+
_variable_costs = distribute_timeslice(
228+
_variable_costs, ts=tech_activity, level=timeslice_level
229+
)
222230

223231
# Total running costs
224232
result = (
@@ -238,6 +246,7 @@ def net_present_value(
238246
capacity: xr.DataArray,
239247
production: xr.DataArray,
240248
consumption: xr.DataArray,
249+
aggregate_timeslices: bool = False,
241250
) -> xr.DataArray:
242251
"""Net present value (NPV) of the relevant technologies.
243252
@@ -265,23 +274,28 @@ def net_present_value(
265274
production: xr.DataArray with commodity production by the relevant technologies
266275
consumption: xr.DataArray with commodity consumption by the relevant
267276
technologies
277+
aggregate_timeslices: If True, the LCOE is aggregated over timeslices (result
278+
will not have a "timeslice" dimension)
268279
269280
Return:
270281
xr.DataArray with the NPV calculated for the relevant technologies
271282
"""
272283
# Capital costs (lifetime)
273284
_capital_costs = capital_costs(technologies, capacity, method="lifetime")
274285

275-
# Distribute capital costs across timeslices in proportion to production
276-
tech_activity = production_amplitude(production, technologies)
277-
_capital_costs = distribute_timeslice(
278-
_capital_costs, ts=tech_activity, level=get_level(production)
279-
)
286+
# Split capital costs across timeslices in proportion to production (if required)
287+
if not aggregate_timeslices:
288+
tech_activity = production_amplitude(production, technologies)
289+
_capital_costs = distribute_timeslice(
290+
_capital_costs, ts=tech_activity, level=get_level(production)
291+
)
280292

281293
# Revenue (annual)
282294
products = is_enduse(technologies.comm_usage)
283295
prices_non_env = filter_input(prices, commodity=products)
284296
revenues = (production * prices_non_env).sum("commodity")
297+
if aggregate_timeslices:
298+
revenues = revenues.sum("timeslice")
285299

286300
# Running costs (annual)
287301
_running_costs = running_costs(
@@ -290,6 +304,7 @@ def net_present_value(
290304
capacity,
291305
production,
292306
consumption,
307+
aggregate_timeslices,
293308
)
294309

295310
# Calculate running costs and revenues over lifetime
@@ -308,6 +323,7 @@ def net_present_cost(
308323
capacity: xr.DataArray,
309324
production: xr.DataArray,
310325
consumption: xr.DataArray,
326+
aggregate_timeslices: bool = False,
311327
) -> xr.DataArray:
312328
"""Net present cost (NPC) of the relevant technologies.
313329
@@ -325,11 +341,20 @@ def net_present_cost(
325341
production: xr.DataArray with commodity production by the relevant technologies
326342
consumption: xr.DataArray with commodity consumption by the relevant
327343
technologies
344+
aggregate_timeslices: If True, the LCOE is aggregated over timeslices (result
345+
will not have a "timeslice" dimension)
328346
329347
Return:
330348
xr.DataArray with the NPC calculated for the relevant technologies
331349
"""
332-
result = -net_present_value(technologies, prices, capacity, production, consumption)
350+
result = -net_present_value(
351+
technologies,
352+
prices,
353+
capacity,
354+
production,
355+
consumption,
356+
aggregate_timeslices,
357+
)
333358
return result
334359

335360

@@ -340,6 +365,7 @@ def equivalent_annual_cost(
340365
capacity: xr.DataArray,
341366
production: xr.DataArray,
342367
consumption: xr.DataArray,
368+
aggregate_timeslices: bool = False,
343369
) -> xr.DataArray:
344370
"""Equivalent annual costs (or annualized cost) of a technology.
345371
@@ -361,6 +387,8 @@ def equivalent_annual_cost(
361387
production: xr.DataArray with commodity production by the relevant technologies
362388
consumption: xr.DataArray with commodity consumption by the relevant
363389
technologies
390+
aggregate_timeslices: If True, the LCOE is aggregated over timeslices (result
391+
will not have a "timeslice" dimension)
364392
365393
Return:
366394
xr.DataArray with the EAC calculated for the relevant technologies
@@ -371,9 +399,12 @@ def equivalent_annual_cost(
371399
capacity,
372400
production,
373401
consumption,
402+
aggregate_timeslices,
374403
)
375404
crf = capital_recovery_factor(technologies)
376-
result = npc * broadcast_timeslice(crf, level=get_level(production))
405+
if not aggregate_timeslices:
406+
crf = broadcast_timeslice(crf, level=get_level(production))
407+
result = npc * crf
377408
return result
378409

379410

@@ -385,6 +416,7 @@ def levelized_cost_of_energy(
385416
production: xr.DataArray,
386417
consumption: xr.DataArray,
387418
method: str = "lifetime",
419+
aggregate_timeslices: bool = False,
388420
) -> xr.DataArray:
389421
"""Levelized cost of energy (LCOE) of technologies over their lifetime.
390422
@@ -415,6 +447,8 @@ def levelized_cost_of_energy(
415447
consumption: xr.DataArray with commodity consumption by the relevant
416448
technologies
417449
method: "lifetime" or "annual"
450+
aggregate_timeslices: If True, the LCOE is aggregated over timeslices (result
451+
will not have a "timeslice" dimension)
418452
419453
Return:
420454
xr.DataArray with the LCOE calculated for the relevant technologies
@@ -425,15 +459,16 @@ def levelized_cost_of_energy(
425459
# Capital costs (lifetime or annual depending on method)
426460
_capital_costs = capital_costs(technologies, capacity, method)
427461

428-
# Split capital costs across timeslices in proportion to production
429-
tech_activity = production_amplitude(production, technologies)
430-
_capital_costs = distribute_timeslice(
431-
_capital_costs, ts=tech_activity, level=get_level(production)
432-
)
462+
# Split capital costs across timeslices in proportion to production (if required)
463+
if not aggregate_timeslices:
464+
tech_activity = production_amplitude(production, technologies)
465+
_capital_costs = distribute_timeslice(
466+
_capital_costs, ts=tech_activity, level=get_level(production)
467+
)
433468

434469
# Running costs (annual)
435470
_running_costs = running_costs(
436-
technologies, prices, capacity, production, consumption
471+
technologies, prices, capacity, production, consumption, aggregate_timeslices
437472
)
438473

439474
# Production (annual)
@@ -445,6 +480,8 @@ def levelized_cost_of_energy(
445480
"commodity"
446481
) # TODO: is this the correct way to deal with multiple products?
447482
)
483+
if aggregate_timeslices:
484+
prod = prod.sum("timeslice")
448485

449486
# If method is lifetime, have to adjust running costs and production
450487
if method == "lifetime":
@@ -453,7 +490,6 @@ def levelized_cost_of_energy(
453490

454491
# LCOE
455492
result = (_capital_costs + _running_costs) / prod
456-
assert "timeslice" in result.dims
457493
return result
458494

459495

@@ -520,7 +556,6 @@ def annual_to_lifetime(costs: xr.DataArray, technologies: xr.Dataset):
520556
"""
521557
assert "year" not in costs.dims
522558
assert "year" not in technologies.dims
523-
assert "timeslice" in costs.dims
524559
life = technologies.technical_life.astype(int)
525560
iyears = range(life.values.max())
526561
years = xr.DataArray(iyears, coords={"year": iyears}, dims="year")
@@ -529,7 +564,8 @@ def annual_to_lifetime(costs: xr.DataArray, technologies: xr.Dataset):
529564
interest_rate=technologies.interest_rate,
530565
mask=years <= life,
531566
)
532-
rates = broadcast_timeslice(rates, level=get_level(costs))
567+
if "timeslice" in costs.dims:
568+
rates = broadcast_timeslice(rates, level=get_level(costs))
533569
return (costs * rates).sum("year")
534570

535571

src/muse/objectives.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ def annual_levelized_cost_of_energy(
437437
production=production,
438438
consumption=consump,
439439
method="annual",
440+
aggregate_timeslices=True,
440441
)
441442
return results
442443

@@ -482,6 +483,7 @@ def lifetime_levelized_cost_of_energy(
482483
production=production,
483484
consumption=consump,
484485
method="lifetime",
486+
aggregate_timeslices=True,
485487
)
486488
return results
487489

@@ -521,6 +523,7 @@ def net_present_value(
521523
capacity=capacity,
522524
production=production,
523525
consumption=consump,
526+
aggregate_timeslices=True,
524527
)
525528
return results
526529

@@ -560,6 +563,7 @@ def net_present_cost(
560563
capacity=capacity,
561564
production=production,
562565
consumption=consump,
566+
aggregate_timeslices=True,
563567
)
564568
return results
565569

@@ -599,5 +603,6 @@ def equivalent_annual_cost(
599603
capacity=capacity,
600604
production=production,
601605
consumption=consump,
606+
aggregate_timeslices=True,
602607
)
603608
return results

tests/test_costs.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,35 @@ def test_lcoe_zero_production(
342342
_technologies, _prices, _capacity, _production, _consumption, method=method
343343
)
344344
assert (lcoe2.isel(timeslice=0) == 0).all()
345+
346+
347+
@mark.parametrize("method", ["annual", "lifetime"])
348+
def test_lcoe_aggregate(
349+
_technologies, _prices, _capacity, _production, _consumption, method
350+
):
351+
from muse.costs import levelized_cost_of_energy
352+
353+
result = levelized_cost_of_energy(
354+
_technologies,
355+
_prices,
356+
_capacity,
357+
_production,
358+
_consumption,
359+
method=method,
360+
aggregate_timeslices=True,
361+
)
362+
assert set(result.dims) == {"asset", "region", "technology"} # no timeslice dim
363+
364+
365+
def test_npv_aggregate(_technologies, _prices, _capacity, _production, _consumption):
366+
from muse.costs import net_present_value
367+
368+
result = net_present_value(
369+
_technologies,
370+
_prices,
371+
_capacity,
372+
_production,
373+
_consumption,
374+
aggregate_timeslices=True,
375+
)
376+
assert set(result.dims) == {"asset", "region", "technology"} # no timeslice dim

tests/test_objectives.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,35 +179,35 @@ def test_annual_levelized_cost_of_energy(_technologies, _demand, _prices):
179179
from muse.objectives import annual_levelized_cost_of_energy
180180

181181
result = annual_levelized_cost_of_energy(_technologies, _demand, _prices)
182-
assert set(result.dims) == {"replacement", "asset", "timeslice"}
182+
assert set(result.dims) == {"replacement", "asset"}
183183

184184

185185
def test_lifetime_levelized_cost_of_energy(_technologies, _demand, _prices):
186186
from muse.objectives import lifetime_levelized_cost_of_energy
187187

188188
result = lifetime_levelized_cost_of_energy(_technologies, _demand, _prices)
189-
assert set(result.dims) == {"replacement", "asset", "timeslice"}
189+
assert set(result.dims) == {"replacement", "asset"}
190190

191191

192192
def test_net_present_value(_technologies, _demand, _prices):
193193
from muse.objectives import net_present_value
194194

195195
result = net_present_value(_technologies, _demand, _prices)
196-
assert set(result.dims) == {"replacement", "asset", "timeslice"}
196+
assert set(result.dims) == {"replacement", "asset"}
197197

198198

199199
def test_net_present_cost(_technologies, _demand, _prices):
200200
from muse.objectives import net_present_cost
201201

202202
result = net_present_cost(_technologies, _demand, _prices)
203-
assert set(result.dims) == {"replacement", "asset", "timeslice"}
203+
assert set(result.dims) == {"replacement", "asset"}
204204

205205

206206
def test_equivalent_annual_cost(_technologies, _demand, _prices):
207207
from muse.objectives import equivalent_annual_cost
208208

209209
result = equivalent_annual_cost(_technologies, _demand, _prices)
210-
assert set(result.dims) == {"replacement", "asset", "timeslice"}
210+
assert set(result.dims) == {"replacement", "asset"}
211211

212212

213213
def add_var(coordinates, *dims, factor=100.0):

0 commit comments

Comments
 (0)