@@ -375,6 +375,7 @@ def bisection(
375375 price_too_high_threshold : float = 10 ,
376376 tolerance : float = 0.1 ,
377377 early_termination_count : int = 5 ,
378+ ** kwargs ,
378379) -> float :
379380 """Applies bisection algorithm to escalate carbon price and meet the budget.
380381
@@ -399,46 +400,41 @@ def bisection(
399400 tolerance: Maximum permitted deviation of emissions from the budget
400401 early_termination_count: Will terminate the loop early if the last n solutions
401402 are the same
403+ kwargs: Additional arguments (unused)
402404
403405 Returns:
404406 New value of global carbon price
405407 """
406408 from logging import getLogger
407409
408- # We calculate the carbon price and emissions threshold in the forecast year
410+ # Carbon price and emissions threshold in the forecast year
409411 future = market .year [- 1 ]
410- threshold = carbon_budget .sel (year = future ).values .item ()
412+ target = carbon_budget .sel (year = future ).values .item ()
411413 price = market .prices .sel (year = future , commodity = commodities ).mean ().values .item ()
412414
413415 # Initial lower and upper bounds on carbon price for the bisection algorithm
414416 current = market .year [0 ]
415417 time_exp = int (future - current )
416- low = 0.01
417- up = max (price , 0.01 ) * 1.1 ** time_exp # i.e. 10% yearly increase on current price
418+ lb_price = 0.01
419+ ub_price = (
420+ max (price , 0.01 ) * 1.1 ** time_exp
421+ ) # i.e. 10% yearly increase on current price
418422
419423 # Bisection loop
420- emissions_cache : dict [ float , float ] = {} # caches emissions for different prices
424+ emissions_cache = EmissionsCache ( market , equilibrium , commodities )
421425 for _ in range (sample_size ): # maximum number of iterations before terminating
422426 # Cap prices between 0.01 and price_too_high_threshold
423427 if refine_price :
424- up = min (up , price_too_high_threshold )
425- low = max (low , 0.01 )
428+ ub_price = min (ub_price , price_too_high_threshold )
429+ lb_price = max (lb_price , 0.01 )
426430
427431 # Round prices to 2dp
428- low = round (low , 2 )
429- up = round (up , 2 )
430-
431- # Calculate carbon emissions at new bounds
432- if low not in emissions_cache :
433- emissions_cache [low ] = bisect_loop (
434- market , sectors , equilibrium , commodities , low
435- )
436- ub = emissions_cache [low ]
437- if up not in emissions_cache :
438- emissions_cache [up ] = bisect_loop (
439- market , sectors , equilibrium , commodities , up
440- )
441- lb = emissions_cache [up ]
432+ lb_price = round (lb_price , 2 )
433+ ub_price = round (ub_price , 2 )
434+
435+ # Calculate/retrieve carbon emissions at new bounds
436+ lb_price_emissions = emissions_cache [lb_price ]
437+ ub_price_emissions = emissions_cache [ub_price ]
442438
443439 # Terminate early if many consecutive emissions are the same
444440 if len (emissions_cache ) >= early_termination_count + 2 :
@@ -447,23 +443,17 @@ def bisection(
447443 break
448444
449445 # Exit loop if lower or upper bound on emissions is close to threshold
450- if abs (ub - threshold ) <= abs (tolerance * threshold ):
451- return low
452- if abs (lb - threshold ) <= abs (tolerance * threshold ):
453- return up
446+ if abs (lb_price_emissions - target ) <= abs (tolerance * target ):
447+ return lb_price
448+ if abs (ub_price_emissions - target ) <= abs (tolerance * target ):
449+ return ub_price
454450
455451 # Convergence not yet reached -> calculate new bounds
456- low , up = min_max_bisect (
457- low ,
458- lb ,
459- up ,
460- ub ,
461- market ,
462- sectors ,
463- equilibrium ,
464- commodities ,
465- threshold , # type: ignore
466- emissions_cache = emissions_cache ,
452+ lb_price , ub_price = adjust_bounds (
453+ lb_price ,
454+ ub_price ,
455+ emissions_cache ,
456+ target ,
467457 )
468458
469459 # If convergence isn't reached, new price is that with emissions closest to
@@ -473,93 +463,145 @@ def bisection(
473463 "Try increasing the tolerance or sample size."
474464 )
475465 getLogger (__name__ ).warning (message )
476- return min (emissions_cache , key = lambda k : (abs (emissions_cache [k ] - threshold ), k ))
466+ return min (emissions_cache , key = lambda k : (abs (emissions_cache [k ] - target ), k ))
477467
478468
479- def min_max_bisect (
480- low : float ,
481- lb : float ,
482- up : float ,
483- ub : float ,
484- market : xr .Dataset ,
485- sectors : list ,
486- equilibrium : Callable [
487- [xr .Dataset , Sequence [AbstractSector ], int ], FindEquilibriumResults
488- ],
489- commodities : list ,
490- threshold : float ,
491- emissions_cache : dict [float , float ] = {},
492- ):
493- """Refines bisection algorithm to escalate carbon price and meet the budget.
469+ class EmissionsCache (dict ):
470+ """Cache of emissions at different price points for bisection algorithm.
471+
472+ 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.
474+ """
475+
476+ def __init__ (self , market , equilibrium , commodities ):
477+ super ().__init__ ()
478+ self .market = market
479+ self .equilibrium = equilibrium
480+ self .commodities = commodities
481+
482+ def __missing__ (self , price ):
483+ value = bisect_solve_market (
484+ self .market , self .equilibrium , self .commodities , price
485+ )
486+ self [price ] = value
487+ return value
488+
489+
490+ def adjust_bounds (
491+ lb_price : float ,
492+ ub_price : float ,
493+ emissions_cache : dict [float , float ],
494+ target : float ,
495+ ) -> tuple [float , float ]:
496+ """Adjust the bounds of the carbon price for the bisection algorithm.
494497
495498 As emissions can be a discontinuous function of the carbon price, this method is
496499 used to improve the solution search when discontinuities are met, improving the
497500 bounds search.
498501
499502 Arguments:
500- low: Value of carbon price at lower bound
501- lb: Value of emissions at lower bound
502- up: Value of carbon price at upper bound
503- ub: Value of emissions at upper bound
504- market: Market, with the prices, supply, consumption and demand
505- sectors: List of sectors
506- equilibrium: Method for searching market equilibrium
507- commodities: List of carbon-related commodities
508- threshold: Carbon budget
509- emissions_cache: Cache of emissions for carbon prices
503+ lb_price: Value of carbon price at lower bound
504+ ub_price: Value of carbon price at upper bound
505+ emissions_cache: Dictionary of emissions at different price points
506+ target: Carbon budget
510507
511508 Returns:
512- Value of lower and upper global carbon price
509+ New lower and upper bounds for the carbon price.
513510 """
514- denominator = max (threshold , 1e-3 )
515- if lb < threshold and ub < threshold :
516- # Both prices are too high (emissions too low) -> decrease the lower bound
517- exponent = (ub - threshold ) / abs (denominator ) # will be negative
518- exponent = max (exponent , - 1 ) # cap exponent at -1
519- up = low if (ub >= lb ) else up # new upper bound is price with higher emissions
520- low = low * np .exp (exponent )
521-
522- if ub > threshold and lb > threshold :
523- # Both prices are too low (emissions too high) -> increase the upper bound
524- exponent = (lb - threshold ) / abs (denominator ) # will be positive
525- exponent = min (exponent , 1 ) # cap exponent at 1
526- low = up if (ub >= lb ) else low # new lower bound is price with lower emissions
527- up = up * np .exp (exponent )
528-
529- if ub > threshold and lb < threshold :
511+ lb_price_emissions = emissions_cache [lb_price ]
512+ ub_price_emissions = emissions_cache [ub_price ]
513+
514+ if lb_price_emissions < target and ub_price_emissions < target :
515+ # Emissions too low at both prices -> decrease prices
516+ method = decrease_bounds
517+
518+ elif lb_price_emissions > target and ub_price_emissions > target :
519+ # Emissions too high at both prices -> increase prices
520+ method = increase_bounds
521+
522+ elif lb_price_emissions > target and ub_price_emissions < target :
530523 # Threshold is between bounds -> perform bisection
531- midpoint = round ((low + up ) / 2.0 , 2 )
532- m = bisect_loop (market , sectors , equilibrium , commodities , midpoint )
533- emissions_cache [midpoint ] = m
534- if m < threshold :
535- # Midpoint price is too high -> becomes new upper bound
536- up = midpoint
537- else :
538- # Midpoint price is too low -> becomes new lower bound
539- low = midpoint
540-
541- # Inverted bounds (i.e. increasing price leads to increasing emissions)
542- # Unlikely case, but included for completeness
543- if ub < threshold and lb > threshold :
544- midpoint = round ((low + up ) / 2.0 , 2 )
545- m = bisect_loop (market , sectors , equilibrium , commodities , midpoint )
546- emissions_cache [midpoint ] = m
547- if m > threshold :
548- # Midpoint price is too high -> becomes new upper bound
549- up = midpoint
550- else :
551- # Midpoint price is too low -> becomes new lower bound
552- low = midpoint
553-
554- return low , up
555-
556-
557- def bisect_loop (
524+ method = bisect_bounds
525+
526+ elif lb_price_emissions < target and ub_price_emissions > target :
527+ # Inverted bounds (i.e. increasing price leads to increasing emissions)
528+ # Unlikely case, but included for completeness
529+ method = bisect_bounds_inverted
530+
531+ else :
532+ # lb_price_emissions or ub_price_emissions is equal to the target, so we can
533+ # return the bounds unchanged
534+ return lb_price , ub_price
535+
536+ return method (
537+ lb_price ,
538+ ub_price ,
539+ emissions_cache ,
540+ target ,
541+ )
542+
543+
544+ def decrease_bounds (
545+ lb_price : float , ub_price : float , emissions_cache : dict [float , float ], target : float
546+ ) -> tuple [float , float ]:
547+ """Decreases the lb of the carbon price, and sets the ub to the previous lb."""
548+ denominator = max (target , 1e-3 )
549+ lb_price_emissions = emissions_cache [lb_price ]
550+ exponent = (lb_price_emissions - target ) / abs (denominator ) # will be negative
551+ exponent = max (exponent , - 1 ) # cap exponent at -1
552+ ub_price = lb_price
553+ lb_price = lb_price * np .exp (exponent )
554+ return lb_price , ub_price
555+
556+
557+ def increase_bounds (
558+ lb_price : float , ub_price : float , emissions_cache : dict [float , float ], target : float
559+ ) -> tuple [float , float ]:
560+ """Increases the ub of the carbon price, and sets the lb to the previous ub."""
561+ denominator = max (target , 1e-3 )
562+ ub_price_emissions = emissions_cache [ub_price ]
563+ exponent = (ub_price_emissions - target ) / abs (denominator ) # will be positive
564+ exponent = min (exponent , 1 ) # cap exponent at 1
565+ lb_price = ub_price
566+ ub_price = ub_price * np .exp (exponent )
567+ return lb_price , ub_price
568+
569+
570+ def bisect_bounds (
571+ lb_price : float ,
572+ ub_price : float ,
573+ emissions_cache : dict [float , float ],
574+ target : float ,
575+ ) -> tuple [float , float ]:
576+ """Bisects the bounds of the carbon price."""
577+ midpoint = round ((lb_price + ub_price ) / 2.0 , 2 )
578+ midpoint_emissions = emissions_cache [midpoint ]
579+ if midpoint_emissions < target :
580+ ub_price = midpoint
581+ else :
582+ lb_price = midpoint
583+ return lb_price , ub_price
584+
585+
586+ def bisect_bounds_inverted (
587+ lb_price : float ,
588+ ub_price : float ,
589+ emissions_cache : dict [float , float ],
590+ target : float ,
591+ ) -> tuple [float , float ]:
592+ """Bisects the bounds of the carbon price, in the case of inverted bounds."""
593+ midpoint = round ((lb_price + ub_price ) / 2.0 , 2 )
594+ midpoint_emissions = emissions_cache [midpoint ]
595+ if midpoint_emissions > target :
596+ ub_price = midpoint
597+ else :
598+ lb_price = midpoint
599+ return lb_price , ub_price
600+
601+
602+ def bisect_solve_market (
558603 market : xr .Dataset ,
559- sectors : list ,
560- equilibrium : Callable [
561- [xr .Dataset , Sequence [AbstractSector ], int ], FindEquilibriumResults
562- ],
604+ equilibrium : Callable [[xr .Dataset ], FindEquilibriumResults ],
563605 commodities : list ,
564606 new_price : float ,
565607) -> float :
@@ -568,11 +610,10 @@ def bisect_loop(
568610 This updates emissions during iterations.
569611
570612 Arguments:
571- market: Market, with the prices, supply, consumption and demand,
572- sectors: List of sectors,
573- equilibrium: Method for searching market equilibrium,
574- commodities: List of carbon-related commodities,
575- new_price: New carbon price from bisection,
613+ market: Market, with the prices, supply, consumption and demand
614+ equilibrium: Method for searching market equilibrium
615+ commodities: List of carbon-related commodities
616+ new_price: New carbon price from bisection
576617
577618 Returns:
578619 Emissions estimated at the new carbon price.
@@ -582,11 +623,11 @@ def bisect_loop(
582623
583624 # Assign new carbon price and solve market
584625 new_market .prices .loc [{"year" : future , "commodity" : commodities }] = new_price
585- new_market = equilibrium (new_market , sectors ).market
626+ new_market = equilibrium (new_market ).market
586627 new_emissions = (
587628 new_market .supply .sel (year = future , commodity = commodities )
588629 .sum (["region" , "timeslice" , "commodity" ])
589- .round (decimals = 3 )
630+ .round (decimals = 2 )
590631 ).values .item ()
591632
592633 return new_emissions
0 commit comments