Skip to content

Commit 2b43ad6

Browse files
authored
Merge pull request #501 from EnergySystemsModellingLab/carbon_budget_fix
Small improvements to carbon budget algorithm
2 parents 592d1cc + 0e15a3e commit 2b43ad6

3 files changed

Lines changed: 104 additions & 59 deletions

File tree

docs/inputs/toml.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ Example
148148

149149
- `refine_price`: If True, applies an upper limit on the carbon price. Defaults to False.
150150
- `price_too_high_threshold`: upper limit on the carbon price. Defaults to 10.
151+
- `resolution`: Number of decimal places to solve the carbon price to. When using the bisection method, increasing this value may increase the time taken to solve the carbon market. Defaults to 2.
151152

152153

153154
------------------

docs/tutorial-code/carbon-budget/1-carbon-budget/Results/MCAPrices.csv

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,51 +47,51 @@ electricity,all-week,evening,all-year,3.98810000000,R1,5,MUS$2010/PJ,2025
4747
gas,all-week,evening,all-year,0.47220000000,R1,5,MUS$2010/PJ,2025
4848
heat,all-week,evening,all-year,4.91770000000,R1,5,MUS$2010/PJ,2025
4949
CO2f,all-week,evening,all-year,0.21000000000,R1,5,MUS$2010/kt,2025
50-
electricity,all-week,night,all-year,9.02490000000,R1,0,MUS$2010/PJ,2030
50+
electricity,all-week,night,all-year,8.60540000000,R1,0,MUS$2010/PJ,2030
5151
gas,all-week,night,all-year,0.47220000000,R1,0,MUS$2010/PJ,2030
52-
heat,all-week,night,all-year,3.85050000000,R1,0,MUS$2010/PJ,2030
53-
CO2f,all-week,night,all-year,0.18000000000,R1,0,MUS$2010/kt,2030
54-
electricity,all-week,morning,all-year,9.02490000000,R1,1,MUS$2010/PJ,2030
52+
heat,all-week,night,all-year,3.68270000000,R1,0,MUS$2010/PJ,2030
53+
CO2f,all-week,night,all-year,0.17000000000,R1,0,MUS$2010/kt,2030
54+
electricity,all-week,morning,all-year,8.60540000000,R1,1,MUS$2010/PJ,2030
5555
gas,all-week,morning,all-year,0.47220000000,R1,1,MUS$2010/PJ,2030
56-
heat,all-week,morning,all-year,3.85050000000,R1,1,MUS$2010/PJ,2030
57-
CO2f,all-week,morning,all-year,0.18000000000,R1,1,MUS$2010/kt,2030
58-
electricity,all-week,afternoon,all-year,9.02490000000,R1,2,MUS$2010/PJ,2030
56+
heat,all-week,morning,all-year,3.68270000000,R1,1,MUS$2010/PJ,2030
57+
CO2f,all-week,morning,all-year,0.17000000000,R1,1,MUS$2010/kt,2030
58+
electricity,all-week,afternoon,all-year,8.60540000000,R1,2,MUS$2010/PJ,2030
5959
gas,all-week,afternoon,all-year,0.47220000000,R1,2,MUS$2010/PJ,2030
60-
heat,all-week,afternoon,all-year,3.85050000000,R1,2,MUS$2010/PJ,2030
61-
CO2f,all-week,afternoon,all-year,0.18000000000,R1,2,MUS$2010/kt,2030
62-
electricity,all-week,early-peak,all-year,9.02490000000,R1,3,MUS$2010/PJ,2030
60+
heat,all-week,afternoon,all-year,3.68270000000,R1,2,MUS$2010/PJ,2030
61+
CO2f,all-week,afternoon,all-year,0.17000000000,R1,2,MUS$2010/kt,2030
62+
electricity,all-week,early-peak,all-year,8.60540000000,R1,3,MUS$2010/PJ,2030
6363
gas,all-week,early-peak,all-year,0.47220000000,R1,3,MUS$2010/PJ,2030
64-
heat,all-week,early-peak,all-year,3.85050000000,R1,3,MUS$2010/PJ,2030
65-
CO2f,all-week,early-peak,all-year,0.18000000000,R1,3,MUS$2010/kt,2030
66-
electricity,all-week,late-peak,all-year,9.02490000000,R1,4,MUS$2010/PJ,2030
64+
heat,all-week,early-peak,all-year,3.68270000000,R1,3,MUS$2010/PJ,2030
65+
CO2f,all-week,early-peak,all-year,0.17000000000,R1,3,MUS$2010/kt,2030
66+
electricity,all-week,late-peak,all-year,8.60540000000,R1,4,MUS$2010/PJ,2030
6767
gas,all-week,late-peak,all-year,0.47220000000,R1,4,MUS$2010/PJ,2030
68-
heat,all-week,late-peak,all-year,3.85050000000,R1,4,MUS$2010/PJ,2030
69-
CO2f,all-week,late-peak,all-year,0.18000000000,R1,4,MUS$2010/kt,2030
70-
electricity,all-week,evening,all-year,9.02490000000,R1,5,MUS$2010/PJ,2030
68+
heat,all-week,late-peak,all-year,3.68270000000,R1,4,MUS$2010/PJ,2030
69+
CO2f,all-week,late-peak,all-year,0.17000000000,R1,4,MUS$2010/kt,2030
70+
electricity,all-week,evening,all-year,8.60540000000,R1,5,MUS$2010/PJ,2030
7171
gas,all-week,evening,all-year,0.47220000000,R1,5,MUS$2010/PJ,2030
72-
heat,all-week,evening,all-year,3.85050000000,R1,5,MUS$2010/PJ,2030
73-
CO2f,all-week,evening,all-year,0.18000000000,R1,5,MUS$2010/kt,2030
74-
electricity,all-week,night,all-year,11.61590000000,R1,0,MUS$2010/PJ,2035
72+
heat,all-week,evening,all-year,3.68270000000,R1,5,MUS$2010/PJ,2030
73+
CO2f,all-week,evening,all-year,0.17000000000,R1,5,MUS$2010/kt,2030
74+
electricity,all-week,night,all-year,10.91870000000,R1,0,MUS$2010/PJ,2035
7575
gas,all-week,night,all-year,0.47220000000,R1,0,MUS$2010/PJ,2035
76-
heat,all-week,night,all-year,4.88690000000,R1,0,MUS$2010/PJ,2035
77-
CO2f,all-week,night,all-year,0.29000000000,R1,0,MUS$2010/kt,2035
78-
electricity,all-week,morning,all-year,11.61590000000,R1,1,MUS$2010/PJ,2035
76+
heat,all-week,night,all-year,4.60800000000,R1,0,MUS$2010/PJ,2035
77+
CO2f,all-week,night,all-year,0.27000000000,R1,0,MUS$2010/kt,2035
78+
electricity,all-week,morning,all-year,10.91870000000,R1,1,MUS$2010/PJ,2035
7979
gas,all-week,morning,all-year,0.47220000000,R1,1,MUS$2010/PJ,2035
80-
heat,all-week,morning,all-year,4.88690000000,R1,1,MUS$2010/PJ,2035
81-
CO2f,all-week,morning,all-year,0.29000000000,R1,1,MUS$2010/kt,2035
82-
electricity,all-week,afternoon,all-year,11.61590000000,R1,2,MUS$2010/PJ,2035
80+
heat,all-week,morning,all-year,4.60800000000,R1,1,MUS$2010/PJ,2035
81+
CO2f,all-week,morning,all-year,0.27000000000,R1,1,MUS$2010/kt,2035
82+
electricity,all-week,afternoon,all-year,10.91870000000,R1,2,MUS$2010/PJ,2035
8383
gas,all-week,afternoon,all-year,0.47220000000,R1,2,MUS$2010/PJ,2035
84-
heat,all-week,afternoon,all-year,4.88690000000,R1,2,MUS$2010/PJ,2035
85-
CO2f,all-week,afternoon,all-year,0.29000000000,R1,2,MUS$2010/kt,2035
86-
electricity,all-week,early-peak,all-year,11.61590000000,R1,3,MUS$2010/PJ,2035
84+
heat,all-week,afternoon,all-year,4.60800000000,R1,2,MUS$2010/PJ,2035
85+
CO2f,all-week,afternoon,all-year,0.27000000000,R1,2,MUS$2010/kt,2035
86+
electricity,all-week,early-peak,all-year,10.91870000000,R1,3,MUS$2010/PJ,2035
8787
gas,all-week,early-peak,all-year,0.47220000000,R1,3,MUS$2010/PJ,2035
88-
heat,all-week,early-peak,all-year,4.88690000000,R1,3,MUS$2010/PJ,2035
89-
CO2f,all-week,early-peak,all-year,0.29000000000,R1,3,MUS$2010/kt,2035
90-
electricity,all-week,late-peak,all-year,11.61590000000,R1,4,MUS$2010/PJ,2035
88+
heat,all-week,early-peak,all-year,4.60800000000,R1,3,MUS$2010/PJ,2035
89+
CO2f,all-week,early-peak,all-year,0.27000000000,R1,3,MUS$2010/kt,2035
90+
electricity,all-week,late-peak,all-year,10.91870000000,R1,4,MUS$2010/PJ,2035
9191
gas,all-week,late-peak,all-year,0.47220000000,R1,4,MUS$2010/PJ,2035
92-
heat,all-week,late-peak,all-year,4.88690000000,R1,4,MUS$2010/PJ,2035
93-
CO2f,all-week,late-peak,all-year,0.29000000000,R1,4,MUS$2010/kt,2035
94-
electricity,all-week,evening,all-year,11.61590000000,R1,5,MUS$2010/PJ,2035
92+
heat,all-week,late-peak,all-year,4.60800000000,R1,4,MUS$2010/PJ,2035
93+
CO2f,all-week,late-peak,all-year,0.27000000000,R1,4,MUS$2010/kt,2035
94+
electricity,all-week,evening,all-year,10.91870000000,R1,5,MUS$2010/PJ,2035
9595
gas,all-week,evening,all-year,0.47220000000,R1,5,MUS$2010/PJ,2035
96-
heat,all-week,evening,all-year,4.88690000000,R1,5,MUS$2010/PJ,2035
97-
CO2f,all-week,evening,all-year,0.29000000000,R1,5,MUS$2010/kt,2035
96+
heat,all-week,evening,all-year,4.60800000000,R1,5,MUS$2010/PJ,2035
97+
CO2f,all-week,evening,all-year,0.27000000000,R1,5,MUS$2010/kt,2035

src/muse/carbon_budget.py

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def fitting(
7777
price_too_high_threshold: float = 10,
7878
sample_size: int = 5,
7979
fitter: str = "linear",
80+
resolution: int = 2,
8081
) -> float:
8182
"""Used to solve the carbon market.
8283
@@ -95,6 +96,7 @@ def fitting(
9596
price_too_high_threshold: threshold on carbon price
9697
sample_size: sample size for fitting
9798
fitter: method to fit emissions with carbon price
99+
resolution: Number of decimal places to solve the carbon price to
98100
99101
Returns:
100102
new_price: adjusted carbon price to meet budget
@@ -125,12 +127,12 @@ def fitting(
125127
threshold, # type: ignore
126128
)
127129

128-
# Cap price between 0.01 and price_too_high_threshold
130+
# Cap price between 0.0 and price_too_high_threshold
129131
if refine_price:
130132
new_price = min(new_price, price_too_high_threshold)
131-
new_price = max(new_price, 0.01)
133+
new_price = max(new_price, 0.0)
132134

133-
return new_price
135+
return round(new_price, resolution)
134136

135137

136138
def linear_fun(x, a, b):
@@ -297,6 +299,7 @@ def bisection(
297299
max_iterations: int = 5,
298300
tolerance: float = 0.1,
299301
early_termination_count: int = 5,
302+
resolution: int = 2,
300303
) -> float:
301304
"""Applies bisection algorithm to escalate carbon price and meet the budget.
302305
@@ -319,36 +322,45 @@ def bisection(
319322
tolerance: Maximum permitted deviation of emissions from the budget
320323
early_termination_count: Will terminate the loop early if the last n solutions
321324
are the same
325+
resolution: Number of decimal places to solve the carbon price to
322326
323327
Returns:
324328
New value of global carbon price
325329
"""
326330
from logging import getLogger
327331

332+
# Create cache for emissions at different price points
333+
emissions_cache = EmissionsCache(market, equilibrium, commodities)
334+
328335
# Carbon price and emissions threshold in the forecast year
329336
future = market.year[-1]
330337
target = carbon_budget.sel(year=future).values.item()
331338
price = market.prices.sel(year=future, commodity=commodities).mean().values.item()
332339

340+
# Test if emissions are already below the budget without imposing a carbon price
341+
if emissions_cache[0.0] < target:
342+
message = (
343+
f"Emissions for the year {int(future)} are already below the carbon budget "
344+
"without imposing a carbon price. The carbon price has been set to zero."
345+
)
346+
getLogger(__name__).warning(message)
347+
return 0.0
348+
333349
# Initial lower and upper bounds on carbon price for the bisection algorithm
334350
current = market.year[0]
335351
time_exp = int(future - current)
336-
lb_price = 0.01
337-
ub_price = (
338-
max(price, 0.01) * 1.1**time_exp
352+
lb_price = 0.0
353+
epsilon = 10**-resolution # smallest nonzero price
354+
ub_price = round(
355+
(max(price, epsilon) * 1.1**time_exp), resolution
339356
) # i.e. 10% yearly increase on current price
340357

341358
# Bisection loop
342-
emissions_cache = EmissionsCache(market, equilibrium, commodities)
343359
for _ in range(max_iterations): # maximum number of iterations before terminating
344-
# Cap prices between 0.01 and price_too_high_threshold
360+
# Cap prices between 0.0 and price_too_high_threshold
345361
if refine_price:
346362
ub_price = min(ub_price, price_too_high_threshold)
347-
lb_price = max(lb_price, 0.01)
348-
349-
# Round prices to 2dp
350-
lb_price = round(lb_price, 2)
351-
ub_price = round(ub_price, 2)
363+
lb_price = max(lb_price, 0.0)
352364

353365
# Calculate/retrieve carbon emissions at new bounds
354366
lb_price_emissions = emissions_cache[lb_price]
@@ -372,16 +384,35 @@ def bisection(
372384
ub_price,
373385
emissions_cache,
374386
target,
387+
resolution,
375388
)
376389

377390
# If convergence isn't reached, new price is that with emissions closest to
378391
# threshold. If multiple prices are equally close, it returns the lowest price
379-
message = (
380-
f"Carbon budget could not be matched for year {int(future)}. "
381-
"Try increasing the tolerance or sample size."
392+
new_price = min(
393+
emissions_cache, key=lambda k: (abs(emissions_cache[k] - target), k)
382394
)
395+
396+
# Raise warning message
397+
if all(emissions_cache[k] > target for k in emissions_cache):
398+
message = (
399+
f"Carbon budget could not be met for the year {int(future)} "
400+
f"(budget: {target}, emissions: {emissions_cache[new_price]}). "
401+
"This may be because there are no processes available that can meet the "
402+
"budget, or because emissions from capacity installed earlier in the time "
403+
"horizon is preventing the budget from being met. "
404+
"The CO2 price in this year should be interpreted with caution."
405+
)
406+
else:
407+
message = (
408+
f"Carbon budget could not be matched for the year {int(future)} to within "
409+
"the specified tolerance. "
410+
"This is sometimes unavoidable due to a discontinuous emissions landscape "
411+
"which can make the budget unreachable, but can sometimes be "
412+
"fixed by increasing max_iterations, early_termination_count or resolution."
413+
)
383414
getLogger(__name__).warning(message)
384-
return min(emissions_cache, key=lambda k: (abs(emissions_cache[k] - target), k))
415+
return new_price
385416

386417

387418
class EmissionsCache(dict):
@@ -408,6 +439,7 @@ def adjust_bounds(
408439
ub_price: float,
409440
emissions_cache: dict[float, float],
410441
target: float,
442+
resolution: int = 2,
411443
) -> tuple[float, float]:
412444
"""Adjust the bounds of the carbon price for the bisection algorithm.
413445
@@ -420,6 +452,7 @@ def adjust_bounds(
420452
ub_price: Value of carbon price at upper bound
421453
emissions_cache: Dictionary of emissions at different price points
422454
target: Carbon budget
455+
resolution: Number of decimal places to solve the carbon price to
423456
424457
Returns:
425458
New lower and upper bounds for the carbon price.
@@ -454,32 +487,41 @@ def adjust_bounds(
454487
ub_price,
455488
emissions_cache,
456489
target,
490+
resolution,
457491
)
458492

459493

460494
def decrease_bounds(
461-
lb_price: float, ub_price: float, emissions_cache: dict[float, float], target: float
495+
lb_price: float,
496+
ub_price: float,
497+
emissions_cache: dict[float, float],
498+
target: float,
499+
resolution: int = 2,
462500
) -> tuple[float, float]:
463501
"""Decreases the lb of the carbon price, and sets the ub to the previous lb."""
464502
denominator = max(target, 1e-3)
465503
lb_price_emissions = emissions_cache[lb_price]
466504
exponent = (lb_price_emissions - target) / abs(denominator) # will be negative
467505
exponent = max(exponent, -1) # cap exponent at -1
468506
ub_price = lb_price
469-
lb_price = lb_price * np.exp(exponent)
507+
lb_price = round(lb_price * np.exp(exponent), resolution)
470508
return lb_price, ub_price
471509

472510

473511
def increase_bounds(
474-
lb_price: float, ub_price: float, emissions_cache: dict[float, float], target: float
512+
lb_price: float,
513+
ub_price: float,
514+
emissions_cache: dict[float, float],
515+
target: float,
516+
resolution: int = 2,
475517
) -> tuple[float, float]:
476518
"""Increases the ub of the carbon price, and sets the lb to the previous ub."""
477519
denominator = max(target, 1e-3)
478520
ub_price_emissions = emissions_cache[ub_price]
479521
exponent = (ub_price_emissions - target) / abs(denominator) # will be positive
480522
exponent = min(exponent, 1) # cap exponent at 1
481523
lb_price = ub_price
482-
ub_price = ub_price * np.exp(exponent)
524+
ub_price = round(ub_price * np.exp(exponent), resolution)
483525
return lb_price, ub_price
484526

485527

@@ -488,9 +530,10 @@ def bisect_bounds(
488530
ub_price: float,
489531
emissions_cache: dict[float, float],
490532
target: float,
533+
resolution: int = 2,
491534
) -> tuple[float, float]:
492535
"""Bisects the bounds of the carbon price."""
493-
midpoint = round((lb_price + ub_price) / 2.0, 2)
536+
midpoint = round((lb_price + ub_price) / 2.0, resolution)
494537
midpoint_emissions = emissions_cache[midpoint]
495538
if midpoint_emissions < target:
496539
ub_price = midpoint
@@ -504,9 +547,10 @@ def bisect_bounds_inverted(
504547
ub_price: float,
505548
emissions_cache: dict[float, float],
506549
target: float,
550+
resolution: int = 2,
507551
) -> tuple[float, float]:
508552
"""Bisects the bounds of the carbon price, in the case of inverted bounds."""
509-
midpoint = round((lb_price + ub_price) / 2.0, 2)
553+
midpoint = round((lb_price + ub_price) / 2.0, resolution)
510554
midpoint_emissions = emissions_cache[midpoint]
511555
if midpoint_emissions > target:
512556
ub_price = midpoint

0 commit comments

Comments
 (0)