Skip to content

Commit 79df22a

Browse files
authored
Merge pull request #484 from EnergySystemsModellingLab/carbon_budget_fitting
Improvements to `carbon_budget` module
2 parents 09c91d9 + 1121a74 commit 79df22a

5 files changed

Lines changed: 90 additions & 256 deletions

File tree

docs/inputs/toml.rst

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ a whole.
9090
Carbon market
9191
-------------
9292

93-
This section contains the settings related to the modelling of the carbon market. If omitted, it defaults to not
94-
including the carbon market in the simulation.
93+
This section contains the settings related to the modelling of the carbon market.
94+
If omitted, it defaults to not including the carbon market in the simulation.
9595

9696
Example
9797

@@ -106,46 +106,49 @@ Example
106106
`time_framework` from the main section. If not given or an empty list, then the
107107
carbon market feature is disabled. Defaults to an empty list.
108108

109-
*method*
110-
Method used to equilibrate the carbon market. Available options are `fitting` and `bisection`, however this can be expanded with the `@register_carbon_budget_method` hook in `muse.carbon_budget`.
111-
112-
The market-clearing algorithm iterates over the sectors until the market reaches an equilibrium in the foresight period (the period next to the one analysed).
113-
This is represented by a stable variation of a commodity demand (or price) between iterations below a defined tolerance.
114-
The market-clearing algorithm samples a user-defined set of carbon prices.
115-
116-
When the `fitting` method is selected, this command builds a regression model of the emissions as a function of the carbon price.
117-
It applies to a pool of emissions for all the modelled regions. Therefore, the estimated carbon price applies to all the modelled regions.
118-
The regression model, the method calculates iteratively the emissions at pre-defined carbon price sample values.
119-
The emissions-carbon price couples are used to used to fit the emission-carbon price relation, is uer-defined (ie. linear or exponential fitter).
120-
The new carbon price is estimated as a root of the regression model estimated at the value of the emission equal to the user-defined emission cap in the foresight period.
121-
Alongside the selection of the method, the user can define a `sample_size`, representing the magnitude of the sample for the fitter.
122-
123-
When the `bisection` method is selected, this command applies a bisection method to solve the carbon market.
124-
Similarly to the `fitting` method, the carbon market includes a pool of all the modelled regions. The obtained carbon price
125-
applies to all the regions, as above. This method solves as a typical bisection algorithm.
126-
It is coded independently to use the internal signature of the `register_carbon_budget_method`. The algorithm aims to find a root of
127-
the function emissions-carbon price, as for the carbon price at which the carbon budget is met.
128-
The algorithm iteratively modifies the carbon price and estimates the corresponding emissions.
129-
It stops when the convergence or stop criteria are met.
130-
This happens for example either when the carbon budget or the maximum number of iterations are met.
131-
Alongside the selection of the method, the user can define a `sample_size`, representing the number of iterations of the bisection method.
132-
133109
*commodities*
134110
Commodities that make up the carbon market. Defaults to an empty list.
135111

136112
*control_undershoot*
137-
Whether to control carbon budget undershoots. This parameter allows for carbon tax credit from one year to be passed to the next in the case of less carbon being emitted than the budget. Defaults to True.
113+
Whether to control carbon budget undershoots. This parameter allows for carbon tax credit from one year to be passed to the next in the case of less carbon being emitted than the budget. Defaults to False.
138114

139115
*control_overshoot*
140-
Whether to control carbon budget overshoots. If the amount of carbon emitted is above the carbon budget, this parameter specifies whether this deficit is carried over to the next year. Defaults to True.
116+
Whether to control carbon budget overshoots. If the amount of carbon emitted is above the carbon budget, this parameter specifies whether this deficit is carried over to the next year. Defaults to False.
117+
118+
*method*
119+
Method used to equilibrate the carbon market. Available options are `fitting` and `bisection`, however this can be expanded with the `@register_carbon_budget_method` hook in `muse.carbon_budget`.
120+
121+
These methods solve the market with a number of different carbon prices, aiming to find the carbon price at which emissions (pooled across all regions) are equal to the carbon budget.
122+
The obtained carbon price applies to all regions.
123+
124+
The `fitting` method samples a number of different carbon prices to build a regression model (linear or exponential) of emissions as a function of carbon price.
125+
This regression model is then used to estimate the carbon price at which the carbon budget is met.
126+
127+
The `bisection` method uses an iterative approach to settle on a carbon price.
128+
Starting with a lower and upper-bound carbon price, it iteratively halves this price interval until the carbon budget is met to within a user-defined tolerance, or until the maximum number of iterations is reached.
129+
Generally, this method is more robust for markets with a complex, nonlinear relationship between emissions and carbon price, but may be slower to converge than the `fitting` method.
130+
131+
Defaults to `bisection`.
141132

142133
*method_options*
143-
Additional options for the specific carbon method. In particular, the `refine_price` activate a sanity check on the adjusted carbon price.
144-
The sanity check applies an upper limit on the carbon price obtained from the algorithm (either `fitting` or `bisection`), called
145-
`price_too_high_threshold`, a user-defined threshold based on heuristics on the values of the carbon price, reflecting typical historical trends.
134+
Additional options for the specified carbon method.
135+
136+
Parameters for the `bisection` method:
137+
138+
- `max_iterations`: maximum number of iterations. Defaults to 5.
139+
- `tolerance`: tolerance for convergence. E.g. 0.1 means that the algorithm will terminate when emissions are within 10% of the carbon budget. Defaults to 0.1.
140+
- `early_termination_count`: number of iterations with no change in the carbon price before the algorithm will terminate. Defaults to 5.
141+
142+
Parameters for the `fitting` method:
143+
144+
- `fitter`: the regression model used to approximate model emissions. Predefined options are `linear` (default) and `exponential`. Further options can be defined using the `@register_carbon_budget_fitter` hook in `muse.carbon_budget`.
145+
- `sample_size`: number of price samples used. Defaults to 5.
146+
147+
Shared parameters:
148+
149+
- `refine_price`: If True, applies an upper limit on the carbon price. Defaults to False.
150+
- `price_too_high_threshold`: upper limit on the carbon price. Defaults to 10.
146151

147-
*fitter*
148-
`fitter` specifies the regression model fit. The regression approximates the model emissions. Predefined options are `linear` and `exponential`. Further options can be defined using the `@register_carbon_budget_fitter` hook in `muse.carbon_budget`.
149152

150153
------------------
151154
Global input files

src/muse/carbon_budget.py

Lines changed: 40 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77

88
from muse.mca import FindEquilibriumResults
99
from muse.registration import registrator
10-
from muse.sectors import AbstractSector
1110

1211
CARBON_BUDGET_METHODS_SIGNATURE = Callable[
13-
[xr.Dataset, list, Callable, xr.DataArray, xr.DataArray], float
12+
[xr.Dataset, Callable, xr.DataArray, list], float
1413
]
1514
"""carbon budget fitters signature."""
1615

@@ -71,17 +70,13 @@ def update_carbon_budget(
7170
@register_carbon_budget_method
7271
def fitting(
7372
market: xr.Dataset,
74-
sectors: list,
75-
equilibrium: Callable[
76-
[xr.Dataset, Sequence[AbstractSector]], FindEquilibriumResults
77-
],
73+
equilibrium: Callable[[xr.Dataset], FindEquilibriumResults],
7874
carbon_budget: xr.DataArray,
79-
carbon_price: xr.DataArray,
8075
commodities: list,
81-
sample_size: int = 4,
82-
refine_price: bool = True,
76+
refine_price: bool = False,
8377
price_too_high_threshold: float = 10,
84-
fitter: str = "slinear",
78+
sample_size: int = 5,
79+
fitter: str = "linear",
8580
) -> float:
8681
"""Used to solve the carbon market.
8782
@@ -91,45 +86,37 @@ def fitting(
9186
a fitting of the emission-carbon price relation.
9287
9388
Arguments:
94-
market: Market, with the prices, supply, and consumption,
95-
sectors: list of market sectors,
96-
equilibrium: Method for searching market equilibrium,
97-
carbon_budget: limit on emissions,
98-
carbon_price: current carbon price
99-
commodities: list of commodities to limit (ie. emissions),
100-
sample_size: sample size for fitting,
101-
refine_price: if True, performs checks on estimated carbon price,
102-
price_too_high_threshold: threshold on carbon price,
103-
fitter: method to fit emissions with carbon price.
89+
market: Market, with the prices, supply, and consumption
90+
equilibrium: Method for searching market equilibrium
91+
carbon_budget: limit on emissions
92+
commodities: list of commodities to limit (ie. emissions)
93+
refine_price: Boolean to decide on whether carbon price should be capped, with
94+
the upper bound given by price_too_high_threshold
95+
price_too_high_threshold: threshold on carbon price
96+
sample_size: sample size for fitting
97+
fitter: method to fit emissions with carbon price
10498
10599
Returns:
106100
new_price: adjusted carbon price to meet budget
107101
"""
102+
# Calculate the carbon price and emissions threshold in the forecast year
108103
future = market.year[-1]
104+
threshold = carbon_budget.sel(year=future).values.item()
105+
price = market.prices.sel(year=future, commodity=commodities).mean().values.item()
109106

110-
threshold = carbon_budget.sel(year=future).values
111-
emissions = market.supply.sel(year=future, commodity=commodities).sum().values
112-
price = market.prices.sel(year=future, commodity=commodities).mean().values
107+
# Solve market with current carbon price
108+
emissions = solve_market(market, equilibrium, commodities, price)
113109

114-
# We create a sample of prices at which we want to calculate emissions
110+
# Create a sample of prices at which we want to calculate emissions
115111
sample_prices = create_sample(price, emissions, threshold, sample_size)
116112
sample_emissions = np.zeros_like(sample_prices)
117113
sample_emissions[0] = emissions
118114

119115
# For each sample price, we calculate the new emissions
120-
new_market = None
121116
for i, new_price in enumerate(sample_prices[1:]):
122-
# Reset market and sectors
123-
new_market = market.copy(deep=True)
124-
125-
# Assign new carbon price
126-
new_market.prices.loc[{"year": future, "commodity": commodities}] = new_price
127-
128-
new_market = equilibrium(new_market, sectors).market
129-
130-
sample_emissions[i + 1] = new_market.supply.sel(
131-
year=future, commodity=commodities
132-
).sum(["region", "timeslice", "commodity"])
117+
sample_emissions[i + 1] = solve_market(
118+
market, equilibrium, commodities, new_price
119+
)
133120

134121
# Based on these results, we finally adjust the carbon price
135122
new_price = CARBON_BUDGET_FITTERS[fitter](
@@ -138,72 +125,14 @@ def fitting(
138125
threshold, # type: ignore
139126
)
140127

141-
if refine_price and new_market is not None:
142-
new_price = refine_new_price(
143-
new_market,
144-
carbon_price,
145-
carbon_budget,
146-
sample_prices,
147-
new_price,
148-
commodities,
149-
price_too_high_threshold,
150-
)
128+
# Cap price between 0.01 and price_too_high_threshold
129+
if refine_price:
130+
new_price = min(new_price, price_too_high_threshold)
131+
new_price = max(new_price, 0.01)
151132

152133
return new_price
153134

154135

155-
def refine_new_price(
156-
market: xr.Dataset,
157-
historic_price: xr.DataArray,
158-
carbon_budget: xr.DataArray,
159-
sample: np.ndarray,
160-
price: float,
161-
commodities: list,
162-
price_too_high_threshold: float,
163-
) -> float:
164-
"""Refine the value of the carbon price.
165-
166-
Ensure it is not too high or low compared to heuristic values.
167-
168-
Arguments:
169-
market: Market, with prices, supply, and consumption,
170-
historic_price: DataArray with the historic carbon prices,
171-
carbon_budget: DataArray with the carbon budget,
172-
sample: Sample carbon price points,
173-
price: Current carbon price, to be refined,
174-
commodities: List of carbon-related commodities,
175-
price_too_high_threshold: Threshold to decide what is a price too high.
176-
177-
Returns:
178-
The new carbon price
179-
"""
180-
future = market.year[-1]
181-
182-
emissions = (
183-
market.supply.sel(year=future, commodity=commodities)
184-
.sum(["region", "timeslice", "commodity"])
185-
.values
186-
)
187-
188-
carbon_price = historic_price.sel(year=historic_price.year < future).values
189-
190-
if (carbon_price[-2:] > 0).all():
191-
relative_price_increase = np.diff(carbon_price) / carbon_price[-1]
192-
average = np.mean(relative_price_increase)
193-
else:
194-
average = 0.2
195-
196-
if price > price_too_high_threshold: # * max(min(carbon_price), 0.1):
197-
price = min(price_too_high_threshold, max(sample) * (1 + average))
198-
elif price <= 0:
199-
threshold = carbon_budget.sel(year=future).values
200-
exponent = (emissions - threshold) / threshold
201-
magnitude = max(1 - np.exp(exponent), -0.1)
202-
price = min(sample) * (1 + magnitude)
203-
204-
return price
205-
206-
207136
def linear_fun(x, a, b):
208137
return a + b * x
209138

@@ -230,9 +159,7 @@ def create_sample(carbon_price, current_emissions, budget, size=4):
230159
"""
231160
exponent = (current_emissions - budget) / budget
232161
magnitude = max(1 - np.exp(-exponent), -0.1)
233-
234162
sample = carbon_price * (1 + np.linspace(0, 1, size) * magnitude)
235-
236163
return np.abs(sample)
237164

238165

@@ -278,7 +205,6 @@ def linear_guess_and_weights(
278205
The initial guess and weights
279206
"""
280207
weights = np.abs(emissions - budget)
281-
282208
idx = np.argsort(weights)
283209

284210
p = prices[idx][:2]
@@ -363,19 +289,14 @@ def exp_guess_and_weights(
363289
@register_carbon_budget_method
364290
def bisection(
365291
market: xr.Dataset,
366-
sectors: list,
367-
equilibrium: Callable[
368-
[xr.Dataset, Sequence[AbstractSector], int], FindEquilibriumResults
369-
],
292+
equilibrium: Callable[[xr.Dataset], FindEquilibriumResults],
370293
carbon_budget: xr.DataArray,
371-
carbon_price: xr.DataArray,
372294
commodities: list,
373-
sample_size: int = 2,
374-
refine_price: bool = True,
295+
refine_price: bool = False,
375296
price_too_high_threshold: float = 10,
297+
max_iterations: int = 5,
376298
tolerance: float = 0.1,
377299
early_termination_count: int = 5,
378-
**kwargs,
379300
) -> float:
380301
"""Applies bisection algorithm to escalate carbon price and meet the budget.
381302
@@ -388,19 +309,16 @@ def bisection(
388309
389310
Arguments:
390311
market: Market, with the prices, supply, consumption and demand
391-
sectors: List of sectors
392312
equilibrium: Method for searching market equilibrium
393313
carbon_budget: DataArray with the carbon budget
394-
carbon_price: DataArray with the carbon price
395314
commodities: List of carbon-related commodities
396-
sample_size: Maximum number of iterations for bisection
397315
refine_price: Boolean to decide on whether carbon price should be capped, with
398316
the upper bound given by price_too_high_threshold
399317
price_too_high_threshold: Upper limit for carbon price
318+
max_iterations: Maximum number of iterations for bisection
400319
tolerance: Maximum permitted deviation of emissions from the budget
401320
early_termination_count: Will terminate the loop early if the last n solutions
402321
are the same
403-
kwargs: Additional arguments (unused)
404322
405323
Returns:
406324
New value of global carbon price
@@ -422,7 +340,7 @@ def bisection(
422340

423341
# Bisection loop
424342
emissions_cache = EmissionsCache(market, equilibrium, commodities)
425-
for _ in range(sample_size): # maximum number of iterations before terminating
343+
for _ in range(max_iterations): # maximum number of iterations before terminating
426344
# Cap prices between 0.01 and price_too_high_threshold
427345
if refine_price:
428346
ub_price = min(ub_price, price_too_high_threshold)
@@ -470,7 +388,7 @@ class EmissionsCache(dict):
470388
"""Cache of emissions at different price points for bisection algorithm.
471389
472390
If a price is queried that is not in the cache, it calculates the emissions at that
473-
price using bisect_solve_market and stores the result in the cache.
391+
price using solve_market and stores the result in the cache.
474392
"""
475393

476394
def __init__(self, market, equilibrium, commodities):
@@ -480,9 +398,7 @@ def __init__(self, market, equilibrium, commodities):
480398
self.commodities = commodities
481399

482400
def __missing__(self, price):
483-
value = bisect_solve_market(
484-
self.market, self.equilibrium, self.commodities, price
485-
)
401+
value = solve_market(self.market, self.equilibrium, self.commodities, price)
486402
self[price] = value
487403
return value
488404

@@ -599,30 +515,28 @@ def bisect_bounds_inverted(
599515
return lb_price, ub_price
600516

601517

602-
def bisect_solve_market(
518+
def solve_market(
603519
market: xr.Dataset,
604520
equilibrium: Callable[[xr.Dataset], FindEquilibriumResults],
605521
commodities: list,
606-
new_price: float,
522+
carbon_price: float,
607523
) -> float:
608-
"""Calls market equilibrium iteration in bisection.
609-
610-
This updates emissions during iterations.
524+
"""Solves the market with a new carbon price and returns the emissions.
611525
612526
Arguments:
613527
market: Market, with the prices, supply, consumption and demand
614528
equilibrium: Method for searching market equilibrium
615529
commodities: List of carbon-related commodities
616-
new_price: New carbon price from bisection
530+
carbon_price: New carbon price
617531
618532
Returns:
619-
Emissions estimated at the new carbon price.
533+
Emissions at the new carbon price.
620534
"""
621535
future = market.year[-1]
622536
new_market = market.copy(deep=True)
623537

624538
# Assign new carbon price and solve market
625-
new_market.prices.loc[{"year": future, "commodity": commodities}] = new_price
539+
new_market.prices.loc[{"year": future, "commodity": commodities}] = carbon_price
626540
new_market = equilibrium(new_market).market
627541
new_emissions = (
628542
new_market.supply.sel(year=future, commodity=commodities)

0 commit comments

Comments
 (0)