diff --git a/config/scenario_config_emiMkt_tests.csv b/config/scenario_config_emiMkt_tests.csv new file mode 100644 index 0000000000..925ef1bcc7 --- /dev/null +++ b/config/scenario_config_emiMkt_tests.csv @@ -0,0 +1,34 @@ +title;start;CES_parameters;slurmConfig;regionmapping;extramappings_historic;cm_rcp_scen;subsidizeLearning;cm_prtpScen;capitalMarket;cm_iterative_target_adj;cm_budgetCO2from2020;cm_budgetCO2_absDevTol;carbonprice;carbonpriceRegi;cm_taxCO2_functionalForm;cm_taxCO2_startyear;cm_peakBudgYr;cm_taxCO2_regiDiff;cm_emiscen;c_regi_earlyreti_rate;c_tech_earlyreti_rate;c_NearTermProjectCompletion;cm_fetaxscen;c_agricult_base_shift;cm_bioenergy_SustTax;cm_33EW;cm_33OAE;cm_33_OAE_limit_EEZ;cm_frac_NetNegEmi;c_ccsinjecratescen;c_ccsinjecrateRegi;c_ccscapratescen;cm_CESMkup_build;cm_ind_energy_limit;cm_ind_energy_limit_manual;cm_wasteIncinerationCCSshare;techpol;regipol;cm_emiMktTarget;cm_emiMktTarget_tolerance;cm_slopeParam;cm_GDPpopScen;cm_demScen;cm_oil_scen;cm_gas_scen;cm_coal_scen;c_techAssumptScen;cm_nucscen;cm_so2tax_scen;cm_multigasscen;cm_LU_emi_scen;cm_tradecostBio;trade;cm_trade_SE_exog;cm_import_EU;cm_SEtaxRampUpParam;cm_maxProdBiolc;cm_1stgen_phaseout;cm_bioprod_regi_lim;cm_import_tax;c_SSP_forcing_adjust;cm_APssp;cm_CESMkup_ind;cm_EDGEtr_scen;c_changeProdCost;cm_startyear;path_gdx;path_gdx_ref;path_gdx_bau;path_gdx_refpolicycost;description +SSP2-EU21-NPi2025;0,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;;;;;0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2005;;;;;SSP2-EU21-NPi2025: This National Policies Implemented (NPi) scenario follows the Shared Socioeconomic Pathways 2 called Middle of the Road. The NPi reflects currently implemented policies, while fulfilling near-term feasibility and long-term plausibility assessments. Emissions trajectories are aligned with bottom-up studies on the effect of currently implemented policies. Carbon prices are constant in the mid- and long-term. +SSP2-EU21-PkBudg650;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp20;globallyOptimal;;;9;650;;functionalForm;;;100;2055;6;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.9;;regiCarbonPrice;2020.2030.EU27_regi.all.year.netGHG_LULUCFGrassi_intraRegBunker 2.189, 2020.2030.DEU.all.year.netGHG_noLULUCF_noBunkers 0.441, (2035.2050.EU27_regi, 2035.2045.DEU, 2020.2050.UKI).all.year.netGHG_LULUCFGrassi 0.0001;GLO 0.004;;;;;;;;;;2;;;;;;;;;;;;;Elec_Push;Mix4ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;SSP2-EU21-PkBudg650: This climate policy scenario follows the SSP2. The stylized climate policy scenario assumes a peak budget of 650 Gt CO2 on total CO2 emissions from 2015 to 2100. This is a 1.5C scenario, peak warming is allowed to be at or slightly above 1.5C at median climate sensitivity but returns to values below 1.5C in at least 67 % of scenarios by the end of the century. +SSP2-EU21-PkBudg750;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp20;globallyOptimal;;;9;750;;functionalForm;;;100;2055;6;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.9;;regiCarbonPrice;2020.2030.EU27_regi.all.year.netGHG_LULUCFGrassi_intraRegBunker 2.189, 2020.2030.DEU.all.year.netGHG_noLULUCF_noBunkers 0.441, (2035.2050.EU27_regi, 2035.2045.DEU, 2020.2050.UKI).all.year.netGHG_LULUCFGrassi 0.0001;GLO 0.004;;;;;;;;;;2;;;;;;;;;;;;;Elec_Push;Mix4ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;SSP2-EU21-PkBudg750: This climate policy scenario follows the SSP2. The stylized climate policy scenario assumes a peak budget of 750 Gt CO2 on total CO2 emissions from 2015 to 2100. This is a 1.5C scenario, peak warming is allowed to be at or slightly above 1.5C at median climate sensitivity but returns to values below 1.5C in at least 67 % of scenarios by the end of the century. +SSP2-EU21-PkBudg1000;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;;GLO 0.004;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;SSP2-EU21-PkBudg1000 with NO regional emission targets. Reference run: module 47 does nothing here, so any Nash-loop behaviour it shows (notably the pebiolc trade surplus limit cycle of DATA11_ANALYSIS.md 4) is not the carbon-price controller's doing. Was an accidental duplicate of EU_real_nzero55 in data11. +EU_real_nzero55;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.EU27_regi.all.year.netGHG_LULUCFGrassi_intraRegBunker 2.189, 2020.2030.DEU.all.year.netGHG_noLULUCF_noBunkers 0.441, (2035.2050.EU27_regi, 2035.2045.DEU, 2020.2050.UKI).all.year.netGHG_LULUCFGrassi 0.0001;GLO 0.004;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Real EU 2040-target trajectory (55% 2030 cut) + net-zero 2045/2050, all-market. +EU_real_nzero57;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.EU27_regi.all.year.netGHG_LULUCFGrassi_intraRegBunker 2.092, 2020.2030.DEU.all.year.netGHG_noLULUCF_noBunkers 0.441, (2035.2050.EU27_regi, 2035.2045.DEU, 2020.2050.UKI).all.year.netGHG_LULUCFGrassi 0.0001;GLO 0.004;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Real EU 2040-target trajectory (57% 2030 cut) + net-zero 2045/2050, all-market. +EU_real_nzero59;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.EU27_regi.all.year.netGHG_LULUCFGrassi_intraRegBunker 1.995, 2020.2030.DEU.all.year.netGHG_noLULUCF_noBunkers 0.441, (2035.2050.EU27_regi, 2035.2045.DEU, 2020.2050.UKI).all.year.netGHG_LULUCFGrassi 0.0001;GLO 0.004;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Real EU 2040-target trajectory (59% 2030 cut) + net-zero 2045/2050, all-market. +emiMkt_A_multiRegionSplit;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.DEU.ETS.year.netGHG 0.13, 2020.2030.DEU.ESR.year.netGHG 0.30, 2020.2030.FRA.ETS.year.netGHG 0.06, 2020.2030.FRA.ESR.year.netGHG 0.23, 2020.2030.UKI.ETS.year.netGHG 0.09, 2020.2030.UKI.ESR.year.netGHG 0.26;GLO 0.01;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Multi-region ETS+ESR split @2030 (DEU/FRA/UKI): per-market AND across regions. +emiMkt_B_nonBinding;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.DEU.ETS.year.netGHG 0.40, 2020.2030.DEU.ESR.year.netGHG 0.18;GLO 0.01;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Non-binding market: DEU ETS slack (floors) + ESR tight, tests smallPrice + non-binding flag. +emiMkt_C_budget;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 70, 2020.2050.DEU.all.budget.netGHG 10, 2020.2050.FRA.all.budget.netGHG 0.05;GLO 0.01;maxPrice 10000;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Budget targets incl. infeasible near-zero FRA budget: tests budget-denominator guard + price ceiling + knee rollback. +emiMkt_D_netZeroESR;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2035.2045.DEU.ESR.year.netGHG 0.001;GLO 0.01;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;"DEU ESR net-zero @2045 at 1% tolerance. Regression guard for a reachable net-zero target. Previously carried nearZeroAbsBand 0.005; that knob is gone with the effTol widening, and the evidence was against it anyway - four run sets showed the band holding this target further from its goal than leaving it off did." +emiMkt_E_tightTolSplit;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.DEU.ETS.year.netGHG 0.13, 2020.2030.DEU.ESR.year.netGHG 0.30, 2020.2030.FRA.ETS.year.netGHG 0.06, 2020.2030.FRA.ESR.year.netGHG 0.23, 2020.2030.UKI.ETS.year.netGHG 0.09, 2020.2030.UKI.ESR.year.netGHG 0.26;GLO 0.00005;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Multi-region split at 0.005% tolerance (cm_emiMktTarget_tolerance GLO 0.00005): tests noise-floor widening + graceful termination. +emiMkt_F_nestedGroupMember;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.EU27_regi.all.year.netGHG 2.8, 2020.2030.DEU.ETS.year.netGHG 0.13, 2020.2030.DEU.ESR.year.netGHG 0.30;GLO 0.01;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Nested EU27 all + DEU ETS/ESR @2030: overlapping group/member targets. +emiMkt_G_multiPeriodSplit;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.DEU.ETS.year.netGHG 0.13, 2020.2030.DEU.ESR.year.netGHG 0.30, 2030.2040.DEU.ETS.year.netGHG 0.03, 2030.2040.DEU.ESR.year.netGHG 0.10;GLO 0.01;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;DEU ETS+ESR @2030 and @2040: sequential periods with per-market AND. +emiMkt_D_netZeroESR_tight;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2035.2045.DEU.ESR.year.netGHG 0.001;GLO 0.0005;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;"DEU ESR net-zero @2045 at 0.05% tolerance, i.e. below the Nash noise floor. Regression guard for tier 3: the target cannot reach its tolerance, so it must be frozen by the noise-floor stop and reported unmetFrozen with its real residual - NOT re-scored against a widened band. Previously carried nearZeroAbsBand 0.005 and was A/B-ed against emiMkt_D_tight_noBand; with the widening gone the two arms were identical, so the noBand row was dropped." +emiMkt_mult;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;nzero;GLO 0.01;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Full nzero preset (9 targets EU27/DEU/UKI/JPN/USA/CHA/IND 2030-2070, mixed emi types), drift re-open ON (default): comprehensive stress test + A/B ON-side for emiMkt_mult_noReopen. +emiMkt_mult_noReopen;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;nzero;GLO 0.01;reopenMaxDev 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;A/B partner for emiMkt_mult with drift re-open DISABLED (reopenMaxDev ~0): reproduces the sticky best-achievable hold where IND-2070 froze +3pct. Compare vs emiMkt_mult to see drifted targets steered back. +emiMkt_oscDamp;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2035.2050.(DEU,FRA,UKI).all.year.netGHG 0.001;GLO 0.005;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Multi-region net-zero @2050 at 0.5% tol (oscillates at the Nash noise floor): tests the progressive oscillation dampener (ON by default). +emiMkt_oscNoDamp;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2035.2050.(DEU,FRA,UKI).all.year.netGHG 0.001;GLO 0.005;dampFlipMin 999;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;A/B partner for oscDamp with dampening OFF (dampFlipMin huge): shows the raw oscillation the dampener is meant to tame. +emiMkt_budgetFeas;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 60, 2020.2050.DEU.all.budget.netGHG 8, 2020.2050.FRA.all.budget.netGHG 1;GLO 0.01;;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Stringent-but-FEASIBLE budgets (FRA 1 GtCO2, reachable): tests that the #3 stall guard lets a lagging budget converge instead of freezing it. +emiMkt_E_tight_noRoll;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.DEU.ETS.year.netGHG 0.13, 2020.2030.DEU.ESR.year.netGHG 0.30, 2020.2030.FRA.ETS.year.netGHG 0.06, 2020.2030.FRA.ESR.year.netGHG 0.23, 2020.2030.UKI.ETS.year.netGHG 0.09, 2020.2030.UKI.ESR.year.netGHG 0.26;GLO 0.00005;rollbackBestFrac 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Off-arm for the give-up PRICE ROLLBACK (rollbackBestFrac ~0). RE-SITED from emiMkt_oscDamp on 2026-08-04: oscDamp now converges without invoking a single give-up, so p47_rolledBack was 0 in both arms and the pair came out BIT-IDENTICAL over 34 iterations - the arm was testing nothing. E_tightTolSplit gives up on all 6 targets and rolls back on all 6 (roll 1-2), so the rollback is on the critical path here. Must NOT beat its base. +emiMkt_budgetFeas_noDiv;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 60, 2020.2050.DEU.all.budget.netGHG 8, 2020.2050.FRA.all.budget.netGHG 1;GLO 0.01;divergeFactor 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Off-arm for DIVERGENCE HANDLING (divergeFactor ~0 disables arming, brakes and the divergence stop). Same stringent-but-feasible budgets as emiMkt_budgetFeas. Shows what the divergence machinery is carrying: with it off, a steering target that has spent its re-open budget has NO route to a give-up, so reopenMax bounds re-opens but not run length. Third arm of the budgetFeas trio: full / noBrake / noDiv. +emiMkt_C_budget_lowCeil;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 70, 2020.2050.DEU.all.budget.netGHG 10, 2020.2050.FRA.all.budget.netGHG 0.05;GLO 0.01;maxPrice 2000;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;"A/B for the price ceiling. emiMkt_C_budget carries maxPrice 10000, which data9 never reached (highest price 3856 US$/tCO2) - so that arm IS the no-ceiling control and the old emiMkt_C_budget_noCeil row was a duplicate run. 2000 is below the price this scenario demonstrably demands, so the pair is guaranteed to differ and the question ""does the ceiling stop one target taking the model with it"" can actually be answered." +emiMkt_C_budget_noFloor;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 70, 2020.2050.DEU.all.budget.netGHG 10, 2020.2050.FRA.all.budget.netGHG 0.05;GLO 0.01;maxPrice 10000, budgetDenomFloorFrac 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;A/B for the budget deviation denominator. The floor used to reference pm_emiMktRefYear, which is never assigned for budget targets, so it was a no-op and FRA (0.05 GtCO2, near zero) opened data9 at +20801% and ended at -119%. With the floor now a cumulative 2005-rate budget, this arm turns it back off: it should reproduce the four-digit deviations while emiMkt_C_budget stays on a sane scale. +emiMkt_budgetFeas_noBrake;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 60, 2020.2050.DEU.all.budget.netGHG 8, 2020.2050.FRA.all.budget.netGHG 1;GLO 0.01;divergeBrakeMax 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;A/B for the divergence BRAKE, on the only data9 run where it fired (FRA, twice). Freezes on first detection instead of braking - the data8 behaviour. Third arm of budgetFeas: full / noBrake / noDiv. Note the guard is now divergeBrakeMax >= 1, because `0 lt 1e-9` is true and the sentinel previously still granted one brake. +emiMkt_mult_wideBand;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;nzero;GLO 0.01;enterFrac 1.0, exitFrac 1.5;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;A/B for the convergence bands, on the run where the old ones cost the most. Restores the previous enterFrac 1.0 / exitFrac 1.5, i.e. converge at the tolerance edge and only re-open at 1.5x. That combination created the dead zone: emiMkt_mult JPN sat frozen at 1.10x tolerance for 61 consecutive iterations, outside the tolerance yet not far enough outside to be re-opened. Expect this arm to reproduce the parked JPN and DEU-2045 residuals that the 0.75/1.0 default should now steer back in - and expect it to use FEWER re-opens, which is the cost side of the trade. +emiMkt_mult_aim050;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;nzero;GLO 0.01;enterFrac 0.5, aimMaxTries 5;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;A/B partner for emiMkt_mult restoring the data10 AIM band (enterFrac 0.5 with aimMaxTries raised to 5 to compensate). That pairing was reverted after data10: 0.5 sits at or below the Nash noise amplitude, 21 percent of targets converged through the ACCEPT fallback instead of the aim band, and budgetFeas ran to the iteration cap. This arm must NOT beat its base - if it does, revisit DATA10_ANALYSIS.md section 5. +emiMkt_budgetFeas_aim050;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 60, 2020.2050.DEU.all.budget.netGHG 8, 2020.2050.FRA.all.budget.netGHG 1;GLO 0.01;enterFrac 0.5, aimMaxTries 5;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;"A/B partner for emiMkt_budgetFeas carrying the data10 AIM band (enterFrac 0.5, aimMaxTries 5). budgetFeas is the run that regressed under it - a limit cycle that converged at iterations 34, 62 and 81 and was thrown 1.5-3.9pp out each time the price froze, ending at the cap with 2 targets out against 49 iterations and 0 out in data9. This arm must reproduce that; the base must not." +emiMkt_mult_wideBand_noPark;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;nzero;GLO 0.01;enterFrac 1.0, exitFrac 1.5, parkedStop 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Off-arm for the PARKED-TARGET STOP. Same wide bands as emiMkt_mult_wideBand, with the stop disabled, so it reproduces the data11 deadlock exactly: EU27-2050 held at 1.32x tolerance for 60 iterations, unmet and un-abandoned, run to the cap with blockedBy=regiTarget. Must NOT beat its base. +emiMkt_D_tight_noNFRoll;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2035.2045.DEU.ESR.year.netGHG 0.001;GLO 0.0005;noiseFloorRollback 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Off-arm for the tier-3 FREEZE-AT-THE-WINDOW'S-BEST rollback. Same 0.05% tolerance as emiMkt_D_netZeroESR_tight, freezing wherever the noise-floor stop happens to fire instead of at the window's best. data11 froze at 5.34x tolerance out of a band whose best was 3.9x. +emiMkt_budgetFeas_noRefresh;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 60, 2020.2050.DEU.all.budget.netGHG 8, 2020.2050.FRA.all.budget.netGHG 1;GLO 0.01;reopenRefresh 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Off-arm for the RE-OPEN BUDGET REFRESH (reopenRefresh ~0 = never refresh). RE-SITED from emiMkt_mult on 2026-08-04 for two reasons: no mult target ever exhausts reopenMax, so the refresh had nothing to give back, and mult is the scenario whose run-to-run split invalidated the data12 pair anyway. budgetFeas exhausts the budget on DEU-2050 (reop 3) WITHOUT giving up - giveUp=1 blocks the refresh - and EU27-2050 settles long enough to trigger one, so both halves of the mechanism exist here. Must NOT beat its base. +emiMkt_budgetFeas_refresh8;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2050.EU27_regi.all.budget.netGHG 60, 2020.2050.DEU.all.budget.netGHG 8, 2020.2050.FRA.all.budget.netGHG 1;GLO 0.01;reopenRefresh 8;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;ON-arm for the RE-OPEN BUDGET REFRESH at a threshold it can actually reach: reopenRefresh 8 (= maxWindow) instead of the shipped 24. Across all 28 data12 runs, 43 targets settled long enough and 11 exhausted their budget but NOT ONE did both, so the feature has never been observed to do anything and 24 is effectively untestable inside a 100-iteration cap. Read as a THREE-point comparison: emiMkt_budgetFeas (24, expected inert here), this arm (8, can fire), emiMkt_budgetFeas_noRefresh (off). If 8 does not differ from off, the mechanism does nothing even when it is able to fire. Diagnostic arm - NOT a proposed default. +emiMkt_E_tight_noRollVerify;1,AMT,2;;;./config/regionmapping_21_EU11.csv;./config/extramapping_EU27.csv;rcp26;globallyOptimal;;;9;1000;;functionalForm;;;75;2080;7;;EUR_regi 0.08, USA_regi 0.07, CHA_regi 0.07, CAZ_regi 0.07, JPN_regi 0.07, GLO 0.06;;transformative;;;;;;;;;;;;;;2050.GLO 0.5;;regiCarbonPrice;2020.2030.DEU.ETS.year.netGHG 0.13, 2020.2030.DEU.ESR.year.netGHG 0.30, 2020.2030.FRA.ETS.year.netGHG 0.06, 2020.2030.FRA.ESR.year.netGHG 0.23, 2020.2030.UKI.ETS.year.netGHG 0.09, 2020.2030.UKI.ESR.year.netGHG 0.26;GLO 0.00005;rollbackVerify 1e-9;;;;;;;;;2;;;;;;;;;;;;;;Mix3ICEban;1;2030;SSP2-EU21-NPi2025;SSP2-EU21-NPi2025;;SSP2-EU21-NPi2025;Off-arm for the ROLLBACK VERIFY-AND-UNDO (rollbackVerify ~0). Host is emiMkt_E_tightTolSplit because it is the only data13 scenario where rollbacks are actually undone (6 fired, 4 undone) - the knob is inert anywhere they survive. Until 2026-08-05 the guard was 'gt 0', so this 1e-9 sentinel read as ON and the arm would have been a silent duplicate of its base. Pinned by test_rollverifyoff6. diff --git a/main.gms b/main.gms index a8b310cec2..a2d23d4785 100755 --- a/main.gms +++ b/main.gms @@ -1605,7 +1605,8 @@ $setglobal c_SSP_forcing_adjust forcing_SSP2 !! def = forcing_SSP2 !! regexp $setGlobal cm_regiExoPrice off !! def = off *** cm_regiExoPrice_fromFile "set exogenous co2 tax path for specific regions from another run, require regipol module to be set to regiCarbonPrice (e.g. "PathToGDX.gdx")" $setGlobal cm_regiExoPrice_fromFile off !! def = off -*** cm_emiMktTarget "set a budget or year emission target, for all (all) or specific emission markets (ETS, ESD or other), and specific regions (e.g. DEU) or region groups (e.g. EU27)" +*** cm_emiMktTarget "set a budget or year emission target, for all (all) or specific emission markets (ETS, ESR or other), and specific regions (e.g. DEU) or region groups (e.g. EU27)" +*** Format: '..... ' [GtCO2 or GtCO2eq], comma separated. *** Example on how to use: *** cm_emiMktTarget = '2020.2050.EU27_regi.all.budget.netGHG_noBunkers 72, 2020.2050.DEU.all.year.netGHG_noBunkers 0.1' *** sets a 72 GtCO2eq budget target for European 27 countries (EU27_regi), for all GHG emissions excluding bunkers between 2020 and 2050; and a 100 MtCO2 CO2eq emission target for the year 2050, for Germany" @@ -1613,13 +1614,21 @@ $setGlobal cm_regiExoPrice_fromFile off !! def = off *** loads hard-coded options for regional target scenarios defined in the module '47_regipol/regiCarbonPrice' declarations file. *** The 'nzero' scenario applies declared net-zero targets for countries explicitly handled by the model (DEU, CHA, USA, IND, JPN, UKI, FRA and EU27_regi) *** Requires regiCarbonPrice realization in regipol module +*** Full documentation of the targets and of the carbon price convergence algorithm: tutorials/19_RegionalEmissionTargets.md $setGlobal cm_emiMktTarget off !! def = off *** Tolerance for regipol emission target deviations convergence. *** For budget targets the tolerance is measured relative to the target value. For year targets the tolerance is relative to 2005 emissions. *** def = GLO 0.01, i.e. regipol emission targets must be met within 1% of target deviation +*** Note the Nash iteration itself wobbles by roughly +-1%, so a tolerance far below 1% is asking for less than the noise. *** Example on how to use: *** cm_emiMktTarget_tolerance = 'GLO 0.004, DEU 0.01'. All regional emission targets will be considered converged if they have at most 0.4% of the target deviation, except for Germany that requires 1%. $setGlobal cm_emiMktTarget_tolerance GLO 0.01 !! def = GLO 0.01 +*** cm_slopeParam "overwrite default values of the module 47 regional-target convergence constants (see p47_slopeParam in regiCarbonPrice/datainput.gms and tutorials/19_RegionalEmissionTargets.md section 1.3)" +*** (off): use the default values set in the regiCarbonPrice datainput.gms +*** otherwise: a list of slopeParam elements with the value to overwrite (only the listed ones change), e.g. +*** cm_slopeParam = 'maxPrice 10000, reopenMax 5, enterFrac 0.8' +*** To switch a knob OFF use a negligible value (1e-9), not 0: GAMS drops zero records, so 0 reads as "not listed". +$setGlobal cm_slopeParam off !! def = off *** cm_scaleDemand - Rescaling factor on final energy and usable energy demand, for selected regions and over a phase-in window. *** Requires re-calibration in order to work. *** Example on how to use: diff --git a/modules/47_regipol/module.gms b/modules/47_regipol/module.gms index 781e7685b1..2b15b011e2 100644 --- a/modules/47_regipol/module.gms +++ b/modules/47_regipol/module.gms @@ -13,21 +13,17 @@ *' *' The `regiCarbonPrice` realization has two purposes. First, it allows to determine region specific year or budget targets for CO2 or GHG emissions. *' Second, it comprises region-specific adjustments that are always active in this realization and policies that can be activated by specific switches (see modules/47_regipol/regiCarbonPrice/bounds.gms). -*' On the emissions targets: Emissions targets can be activated via the switch cm_emiMktTarget and there are number of options that can be chosen to specify them -*' like the type of target (annual target or budget), the target year, the regions the target should be applied to, -*' the emissions metric (CO2, GHG, CO2 excl. bunkers/LULUCF etc.) and the emissions market (all, ETS, ESR). For example, setting cm_emiMktTarget to -*' "2020.2030.EU27_regi.all.year.netGHG_noLULUCF_noBunkers 3.16, 2035.2050.EU27_regi.all.year.netCO2 0.001" will apply two emissions targets to the region group "EU27_regi" -*' which consists of all model regions in the EU27 (for the definition of region groups, please see definition of the set regi_group): The first is a target of 3.16 GtCO2eq/yr annual ("year") emissions by 2030 -*' in the metric of net greenhouse gas emissions without LULUCF and bunkers ("netGHG_noLULUCF_noBunkers", for definition of emissions metrics see modules/47_regipol/regiCarbonPrice/postsolve.gms) -*' for all emissions markets ("all"). The second defines a target of 1 Mt CO2/yr annual ("year") emissions by 2050 in the metric of net CO2 emissions ("netCO2") for all emissions markets ("all"). -*' For details on the options please see the description of the switch. -*' When an emissions target is set via this switch, the carbon price in the target years and regions, to which it is applied, is adjusted over nash iterations in REMIND -*' until the desired emissions target is reached within a certain margin of tolerance defined by cm_emiMktTarget_tolerance. Note that also multiple emissions targets can be specified for -*' different years (e.g. the EU's 2030 and 2050 targets). The carbon price trajectory up to the target years are linear and the carbon price -*' is assumed to still increase linearly at a small rate after the last target year. Please check the parameter pm_emiMktTarget_dev_iter to see the emissions target convergence over iterations. -*' Note that these regional emissions targets of the module only overwrite the carbon price trajectory for the regions they are applied to, while the other regions keep the carbon price trajectories -*' that are adjusted in other parts of the model, in particular, from the global emissions target adjustment. -*' This means that these regional targets come on top of the global emissions target that REMIND aims to achieve. +*' On the emissions targets: activated via `cm_emiMktTarget`, which takes one or more targets of the form +*' `..... `, e.g. +*' "2020.2030.EU27_regi.all.year.netGHG_noLULUCF_noBunkers 3.16" for a 3.16 GtCO2eq/yr EU27 target in 2030 covering all +*' emission markets. Targets may be annual or cumulative budgets, apply to a region or a region group, to all markets or to +*' ETS/ESR/other alone, and use any of the emission metrics in the emi_type_47 set (see regiCarbonPrice/postsolve.gms). +*' When a target is set, the carbon price of the affected regions and years is adjusted over the nash iterations until the +*' emissions land within `cm_emiMktTarget_tolerance` of the target. Several target years per region are solved sequentially. +*' These regional targets OVERRIDE the carbon price for their regions only - other regions keep the price they get from the +*' global emissions target - so they come on top of it. `pm_emiMktTarget_dev_iter` is the per-iteration deviation to inspect. +*' How the convergence algorithm works, every case it handles, all of its parameters and how to diagnose a run: +*' tutorials/19_RegionalEmissionTargets.md *' On the regional bounds and adjustments: In the bounds file, there are a number of regionally hard-coded bounds to the model that aim to improve the representation of specific regions in REMIND. *' They come on top of the bounds in the core of REMIND. The difference is that core defines bounds for all regions, while in this module region-specific adjustments are made which have not yet been generalized to all regions *' and are not activated outside of the realization "regiCarbonPrice". diff --git a/modules/47_regipol/none/not_used.txt b/modules/47_regipol/none/not_used.txt index 6cdd252c0c..01c95d4c38 100644 --- a/modules/47_regipol/none/not_used.txt +++ b/modules/47_regipol/none/not_used.txt @@ -85,3 +85,4 @@ pm_emiLULUCF_GrassiShift,input,not needed vm_co2CCS,input,not needed cm_implicitPriceTarget_tolerance,input,not needed cm_implicitPePriceTarget_tolerance,input,not needed +cm_iteration_max,input,not needed diff --git a/modules/47_regipol/regiCarbonPrice/datainput.gms b/modules/47_regipol/regiCarbonPrice/datainput.gms index 358d7f883b..01cbe2090b 100644 --- a/modules/47_regipol/regiCarbonPrice/datainput.gms +++ b/modules/47_regipol/regiCarbonPrice/datainput.gms @@ -19,10 +19,41 @@ pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt) = 0; ***-------------------------------------------------- $IFTHEN.emiMkt not "%cm_emiMktTarget%" == "off" +*** Initialize carbon price convergence algorithm constants. +*** slope fit and step size +p47_slopeParam("maxWindow")=8; p47_slopeParam("minPriceSpread")=1e-2; p47_slopeParam("minR2")=0.5; p47_slopeParam("maxSteep")=5; p47_slopeParam("degenerateThreshold")=1e-2; p47_slopeParam("rescaleCapLo")=0.5; p47_slopeParam("rescaleCapHi")=2; +*** convergence bands +p47_slopeParam("enterFrac")=0.75; p47_slopeParam("aimMaxTries")=3; p47_slopeParam("exitFrac")=1.0; p47_slopeParam("persist")=2; p47_slopeParam("priceEps")=0; +*** deviation normalisation and price ceiling +p47_slopeParam("budgetDenomFloorFrac")=1e-2; p47_slopeParam("maxPrice")=0; +*** give-up stops +p47_slopeParam("noiseFloorMaxDev")=0.02; p47_slopeParam("noiseFloorBandWidth")=0.01; p47_slopeParam("noiseFloorBandWidthRel")=15; p47_slopeParam("noiseFloorRollback")=1; p47_slopeParam("parkedStop")=1; p47_slopeParam("infeasEmiTol")=5e-3; +p47_slopeParam("infeasStallFrac")=0.9; p47_slopeParam("divergeFactor")=10; p47_slopeParam("divergeMinBest")=1.0; p47_slopeParam("divergeBrakeMax")=2; +*** re-open parameters +p47_slopeParam("reopenMaxDev")=0.1; p47_slopeParam("reopenMax")=3; p47_slopeParam("reopenStepCap")=0.05; p47_slopeParam("reopenRefresh")=24; +*** step bound for the squareDev fallbacks +p47_slopeParam("fallbackStepFactor")=2; p47_slopeParam("fallbackStepFloor")=0.05; +*** price rollback on freeze +p47_slopeParam("rollbackBestFrac")=0.5; p47_slopeParam("rollbackMaxAge")=8; p47_slopeParam("rollbackVerify")=1; +*** progressive oscillation dampening +p47_slopeParam("dampFlipMin")=2; p47_slopeParam("dampProgFactor")=0.6; p47_slopeParam("dampFloor")=0.1; +*** optional overwrite of individual constants from the scenario config, e.g. cm_slopeParam = 'maxPrice 10000, reopenMax 5'. +$if not "%cm_slopeParam%" == "off" parameter p_new_slopeParam(slopeParam) "slope/convergence parameters overwritten via cm_slopeParam" / %cm_slopeParam% /; +$if not "%cm_slopeParam%" == "off" p47_slopeParam(slopeParam)$p_new_slopeParam(slopeParam) = p_new_slopeParam(slopeParam); + +*** initialise the state parameters +p47_targetState(targetState,ttot,ttot2,ext_regi,emiMktExt) = 0; +p47_emiMktRefBudget(ttot,ttot2,ext_regi,emiMktExt) = 0; +p47_targetMet(ttot,ext_regi) = 0; +p47_allTargetsMet(ext_regi) = 0; +p47_allTargetsFrozen(ext_regi) = 0; +p47_unmetNoGiveUp(ext_regi) = 0; +p47_currentConvergence_iter(iteration,ttot,ext_regi) = 0; + *** Auxiliar parameters based on emission targets information loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47), !!calculated sets that depends on data parameter - regiEmiMktTarget(ext_regi) = yes; !! assigning values to set containing extended regions that have regional emission targets - regiANDperiodEmiMktTarget_47(ttot2,ext_regi) = yes; !! assigning values to set containing extended regions and terminal years of regional emission targets + regiEmiMktTarget(ext_regi) = yes; !! assigning values to set containing extended regions that have regional emission targets + regiANDperiodEmiMktTarget_47(ttot2,ext_regi) = yes; !! assigning values to set containing extended regions and terminal years of regional emission targets ); *** Calculating set containing regions that should be controlled by a given regional emission target. @@ -46,7 +77,6 @@ $IFTHEN.emiMkt not "%cm_emiMktTarget%" == "off" break$(p47_firstTargetYear(ext_regi)); ); ); - *** Assigning convergence tolerance to active regional targets parameter f47_emiMktTarget_tolerance(ext_regi) "tolerance for regipol emission target deviations convergence [#]" / %cm_emiMktTarget_tolerance% /; pm_emiMktTarget_tolerance(ext_regi)$(regiEmiMktTarget(ext_regi)) = 0.01; !! if no value is assigned to GLO, the default devation tolerance is set to 1% @@ -75,7 +105,6 @@ p47_taxemiMkt_init(ttot,regi,emiMkt)$(p47_taxCO2eq_ref(ttot,regi) and (NOT(p47_t ); ); ); - *** if there is a European regional target, overwrite historical prices for Europe if the historical years are free in the cm_emiMktTarget run. *** in this case, historical prices will reflect the ETS market observed prices instead of the values defined pm_taxCO2eq loop(ext_regi$regiEmiMktTarget(ext_regi), diff --git a/modules/47_regipol/regiCarbonPrice/declarations.gms b/modules/47_regipol/regiCarbonPrice/declarations.gms index 071a9337b2..b55bac5279 100644 --- a/modules/47_regipol/regiCarbonPrice/declarations.gms +++ b/modules/47_regipol/regiCarbonPrice/declarations.gms @@ -13,6 +13,8 @@ Parameter s47_firstFreeYear "value of first free year for the carbon price trajectory" s47_prefreeYear "value of the last non-free year for the carbon price trajectory" + s47_prevTargetYear "terminal year of the closest earlier emiMkt target of the same region (0 if none); the ramp of a later target is anchored here so it cannot overwrite an already converged target year" + s47_slopeWindowStart "window start iteration (an ord of the iteration set) for the current least-squares slope calculation [#]" pm_emiLULUCF_GrassiShift(ttot,all_regi) "difference between Magpie land-use change emissions and UNFCCC emissions in 2015 to correct for national accounting in emissions targets [GtC]" pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt) "deviation of emissions of current iteration from target emissions, for budget target this is the difference normalized by target emissions, while for year targets this is the difference normalized by 2005 emissions [%]" pm_taxemiMkt(ttot,all_regi,all_emiMkt) "CO2 tax path per region and emissions market [T$/GtC]" @@ -51,23 +53,35 @@ $endif.emiMktTargetType pm_emiMktCurrent(ttot,ttot2,ext_regi,emiMktExt) "previous iteration region emissions (from year ttot to ttot2 for budget) [GtCO2 or GtCO2eq]" p47_emiMktCurrent_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "parameter to save pm_emiMktCurrent across iterations [GtCO2 or GtCO2eq]" pm_emiMktRefYear(ttot,ttot2,ext_regi,emiMktExt) "emissions in reference year 2005, used for calculating target deviation of year targets [GtCO2 or GtCO2eq]" + p47_emiMktRefBudget(ttot,ttot2,ext_regi,emiMktExt) "the cumulative emissions this market would have produced from ttot to ttot2 at its 2005 annual rate, budget analogue of pm_emiMktRefYear [GtCO2 or GtCO2eq]" pm_emiMktTarget_dev_iter(iteration, ttot,ttot2,ext_regi,emiMktExt) "parameter to save pm_emiMktTarget_dev across iterations (1 is 100%)" pm_emiMktTarget_tolerance(ext_regi) "tolerance for regipol emission target deviations convergence [#]" *** Parameters necessary to calculate the emission tax rescaling factor - p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) "auxiliary parameter to save the slope corresponding to the observed mitigation derivative regarding to co2tax level changes from the two previous iterations [#]" - p47_factorRescaleSlope_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "parameter to save mitigation curve slope across iterations [#]" - p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) "auxiliary parameter to store reference iteration used for calculating slope of current mititgation cost [#]" + p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) "fitted slope of emissions vs. carbon price over the current window, after the maxSteep cap [GtCO2 per T$/GtC]" + p47_factorRescaleSlope_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "parameter to save p47_factorRescaleSlope across iterations [GtCO2 per T$/GtC]" + p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) "first iteration of the current slope window; the window holds the last maxWindow iterations this target was STEERED, so it can reach past a freeze [#]" pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) "multiplicative tax rescale factor that rescales emiMkt carbon price from iteration to iteration to reach regipol targets [%]" p47_factorRescaleemiMktCO2Tax_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "parameter to save rescale factor across iterations for debugging purposes [%]" - p47_clampedRescaleSlope_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "auxiliary parameter to save the slope value before clamping. Useful for debugging purposes [#]" - p47_dampedFactorRescaleemiMktCO2Tax_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "auxiliary parameter to save the rescale factor value before dampening. Useful for debugging purposes [#]" + p47_slopeAux(slopeTerm) "scratch accumulators and results for the market currently being solved (see slopeTerm set) [mixed units]" + p47_slopeParam(slopeParam) "configuration constants of the convergence algorithm, set in datainput.gms and documented in tutorials/19_RegionalEmissionTargets.md section 3.1 [mixed units]" + p47_slopeTrace_iter(slopeTrace,iteration,ttot,ttot2,ext_regi,emiMktExt) "per-iteration diagnostics: rawSlope, fitR2, preDamp (pre-dampening rescale), rollIter/rollUndo (price rollback and its undo), divBrake, parked [mixed units]" *** Parameters necessary to define the CO2 tax curve shape - p47_targetConverged(ttot,ext_regi) "boolean to store if emission target has converged [0 or 1]" + p47_targetConverged(ttot,ext_regi) "boolean to store if the region-period emission target has converged, i.e. all its markets are FROZEN (AND over its markets) [0 or 1]" p47_targetConverged_iter(iteration,ttot,ext_regi) "parameter to save p47_targetConverged across iterations [0 or 1]" - pm_allTargetsConverged(ext_regi) "boolean to store if all emission targets converged at least once [0 or 1]" - p47_allTargetsConverged_iter(iteration,ext_regi) "parameter to save p47_allTargetsConverged across iterations [0 or 1]" + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "per-market-target FROZEN flag: stop steering this price. NOT a statement that the target was achieved - see p47_marketMet_iter [0 or 1]" + p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) "per-market-target MET flag: |deviation| within the RAW user tolerance, or legitimately slack at the price floor. No hysteresis [0 or 1]" + p47_targetMet(ttot,ext_regi) "boolean to store if the region-period emission target was actually MET (AND over its markets) [0 or 1]" + p47_targetMet_iter(iteration,ttot,ext_regi) "parameter to save p47_targetMet across iterations [0 or 1]" + p47_targetState(targetState,ttot,ttot2,ext_regi,emiMktExt) "persistent state of one market target's convergence state machine, one value per targetState element. The element text in set targetState carries the meaning and units of each [mixed]" + pm_allTargetsConverged(ext_regi) "run-end signal read by module 80: all emission targets of the region frozen AND every one of them met or deliberately given up [0 or 1]" + p47_allTargetsConverged_iter(iteration,ext_regi) "parameter to save pm_allTargetsConverged across iterations [0 or 1]" + p47_allTargetsFrozen(ext_regi) "boolean to store if all emission targets of the region are FROZEN, i.e. none of them is steering its carbon price any more [0 or 1]" + p47_allTargetsFrozen_iter(iteration,ext_regi) "parameter to save p47_allTargetsFrozen across iterations [0 or 1]" + p47_unmetNoGiveUp(ext_regi) "1 if a market target of the region is outside the raw tolerance and NOT given up, i.e. the algorithm still intends to correct it [0 or 1]" + p47_allTargetsMet(ext_regi) "1 if all emission targets of the region are MET within the raw tolerance. Module-internal reporting only, not a termination criterion [0 or 1]" + p47_allTargetsMet_iter(iteration,ext_regi) "parameter to save p47_allTargetsMet across iterations [0 or 1]" p47_firstTargetYear(ext_regi) "first year with a pre defined policy emission target in the region [year]" p47_lastTargetYear(ext_regi) "last year with a pre defined policy emission target in the region [year]" p47_currentConvergencePeriod(ext_regi) "auxiliary parameter to store the current target year being executed by the convergence algorithm [year]" @@ -86,7 +100,7 @@ $endif.emiMktTargetType *' RP: improve formatting of output: always have the iteration separate to allow easy comparison over iterations. *' For non-iteration values show time and regi down, and the other two sets to the right -option pm_emiMktTarget:3:3:3; !! ensure bett +option pm_emiMktTarget:3:3:3; option pm_factorRescaleemiMktCO2Tax:3:3:1; option pm_emiMktCurrent:3:3:1; option pm_emiMktRefYear:3:3:1; @@ -117,7 +131,6 @@ $else.cm_implicitQttyTargetType pm_implicitQttyTarget(ttot,ext_regi,taxType,targetType,qttyTarget,qttyTargetGroup) "quantity target [absolute: TWa or GtC; or percentage: 0.1]" p47_implicitQttyTargetScenario(qttyTargetScenario,ttot,ext_regi,taxType,targetType,qttyTarget,qttyTargetGroup) "hard-coded quantity scenarios types [absolute: TWa or GtC; or percentage: 0.1]" $endif.cm_implicitQttyTargetType - pm_implicitQttyTarget_isLimited(iteration,ttot,ext_regi,qttyTarget,qttyTargetGroup) "1 (one) if there is a hard bound on the model that does not allow the tax to change further the quantity" p47_implicitQttyTarget_initialYear(ext_regi,taxType,targetType,qttyTarget,qttyTargetGroup) "initial year of quantity target for a given region [year]" diff --git a/modules/47_regipol/regiCarbonPrice/postsolve.gms b/modules/47_regipol/regiCarbonPrice/postsolve.gms index 9f4e1fe9db..62e02e920c 100644 --- a/modules/47_regipol/regiCarbonPrice/postsolve.gms +++ b/modules/47_regipol/regiCarbonPrice/postsolve.gms @@ -142,9 +142,10 @@ p47_emiTargetMkt_iter(iteration,ttot,regi, emiMktExt,emi_type_47) = p47_emiTarge *** Emission markets (EU Emission trading system and Effort Sharing) ***-------------------------------------------------- -$IFTHEN.emiMkt not "%cm_emiMktTarget%" == "off" +$IFTHEN.emiMkt not "%cm_emiMktTarget%" == "off" + +*** ------ CO2 Tax Parameter Initialization ------ -*** Removing economy wide co2 tax parameters for regions within the emiMKt controlled targets (this is necessary here to remove any calculation made in other modules after the last run in the postsolve) loop(ext_regi$regiEmiMktTarget(ext_regi), loop(regi$regi_groupExt(ext_regi,regi), *** Removing the economy wide co2 tax parameters for regions within the ETS markets @@ -160,7 +161,8 @@ $IFTHEN.emiMkt not "%cm_emiMktTarget%" == "off" ); ); -*** Calculating the current emission levels... +*** ------ Current Emission Levels & Target Deviation ------ + loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47), *** for budget targets if(sameas(target_type_47,"budget"), !! budget total CO2 target @@ -170,6 +172,13 @@ loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$pm_emiMktTarget( pm_ts(ttot3) * (1 -0.5$(ttot3.val eq ttot.val OR ttot3.val eq ttot2.val)) *(p47_emiTargetMkt(ttot3, regi,emiMktExt,emi_type_47)*sm_c_2_co2) )); +*** Reference budget (2005-end year trapezoidal projection). + p47_emiMktRefBudget(ttot,ttot2,ext_regi,emiMktExt) = + sum(regi$regi_groupExt(ext_regi,regi), + sum(ttot3$((ttot3.val ge ttot.val) AND (ttot3.val le ttot2.val)), + pm_ts(ttot3) * (1 -0.5$(ttot3.val eq ttot.val OR ttot3.val eq ttot2.val)) + *(p47_emiTargetMkt("2005", regi,emiMktExt,emi_type_47)*sm_c_2_co2) + )); *** for year targets elseif sameas(target_type_47,"year"), !! year total CO2 target pm_emiMktCurrent(ttot,ttot2,ext_regi,emiMktExt) = sum(regi$regi_groupExt(ext_regi,regi), p47_emiTargetMkt(ttot2, regi,emiMktExt,emi_type_47)*sm_c_2_co2); @@ -183,7 +192,8 @@ p47_emiMktCurrent_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = pm_emiMktCurre loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47), *** for budget targets, target deviation is difference of current budget to target budget normalized by target budget if(sameas(target_type_47,"budget"), - pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt) = (pm_emiMktCurrent(ttot,ttot2,ext_regi,emiMktExt)-pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) ) / pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47); +*** floor the denominator at budgetDenomFloorFrac * the cumulative 2005-rate budget, so a near-zero budget target cannot blow the relative deviation up. + pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt) = (pm_emiMktCurrent(ttot,ttot2,ext_regi,emiMktExt)-pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) ) / max(abs(pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)), p47_slopeParam("budgetDenomFloorFrac") * p47_emiMktRefBudget(ttot,ttot2,ext_regi,emiMktExt)); ); *** for year targets, target deviation is difference of current emissions in target year to target emissions normalized by 2015 emissions if(sameas(target_type_47,"year"), @@ -192,45 +202,499 @@ loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$pm_emiMktTarget( ); pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt); !!save regional target deviation across iterations for debugging of target convergence issues -*** Checking sequentially if targets converged +*** Saving the best-so-far per market target: the smallest |deviation| reached so far and the iteration that produced it. +loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47), + if((p47_targetState("bestDevIter",ttot,ttot2,ext_regi,emiMktExt) eq 0) + OR (abs(pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt)) lt p47_targetState("bestDevAbs",ttot,ttot2,ext_regi,emiMktExt)), + p47_targetState("bestDevAbs",ttot,ttot2,ext_regi,emiMktExt) = abs(pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt)); + p47_targetState("bestDevIter",ttot,ttot2,ext_regi,emiMktExt) = iteration.val; + ); + +*** only consider the target reached its goal while actively steering its own price, and not as a side effect of other targets changes + if((p47_slopeParam("divergeMinBest") ge 1e-3) + AND (p47_targetState("divArmed",ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (p47_currentConvergence_iter(iteration-1,ttot2,ext_regi) eq 1) + AND (abs(pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt)) + le p47_slopeParam("divergeMinBest") * pm_emiMktTarget_tolerance(ext_regi)), + p47_targetState("divArmed",ttot,ttot2,ext_regi,emiMktExt) = 1; + ); +); + +***----Target Convergence State Machine ------ + +*** Checking sequentially if targets converged (per-market AND aggregation) +*** a region-period is declared converged only when EVERY one of its market targets is, and each target carries its own hysteresis state. +*** The per-target test uses hysteresis + persistence so a target does not flip-flop in and out of the band under Nash noise - each flip would otherwise restart the slope window. loop((ext_regi,ttot2)$regiANDperiodEmiMktTarget_47(ttot2,ext_regi), - p47_targetConverged(ttot2,ext_regi) = 0; + p47_targetConverged(ttot2,ext_regi) = 1; !! AND: assume converged (= all markets frozen), then clear on the first market target still steering + p47_targetMet(ttot2,ext_regi) = 1; !! AND: assume met (= all markets inside the RAW tolerance), then clear on the first market target that is not loop((ttot,emiMktExt,target_type_47,emi_type_47)$((pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47))), + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 0; !! this market target not yet FROZEN this iteration (see the frozen-vs-met note below) + p47_slopeAux("rollIter") = 0; !! iteration whose price path to restore if this target ends up frozen (0 = keep the current price) + p47_slopeAux("wantRoll") = 0; !! set by the branches that GIVE UP on a target, so the rollback block below runs + +*** closest earlier target terminal year of this region. + p47_slopeAux("prevTgtYr") = 0; + loop(ttot3$(regiANDperiodEmiMktTarget_47(ttot3,ext_regi) AND (ttot3.val lt ttot2.val)), + p47_slopeAux("prevTgtYr") = max(p47_slopeAux("prevTgtYr"), ttot3.val); + ); + +*** Rollback verify-and-undo: A rollback is a bet that can fail if other regions moved. +*** Test it on the next iteration; if |dev| got worse, undo it and lock out future rollbacks (rolledBack = 2). + if((p47_slopeParam("rollbackVerify") ge 1) + AND (p47_targetState("rolledBack",ttot,ttot2,ext_regi,emiMktExt) eq 1) + AND (p47_targetState("rollFromIter",ttot,ttot2,ext_regi,emiMktExt) gt 0) + AND (iteration.val eq p47_targetState("rollFromIter",ttot,ttot2,ext_regi,emiMktExt) + 1) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) + gt p47_targetState("rollFromDev",ttot,ttot2,ext_regi,emiMktExt)), + loop(regi$regi_groupExt(ext_regi,regi), + loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), + pm_taxemiMkt(ttot4,regi,emiMkt)$(ttot4.val gt p47_slopeAux("prevTgtYr")) = + sum(iteration2$(iteration2.val eq p47_targetState("rollFromIter",ttot,ttot2,ext_regi,emiMktExt)), pm_taxemiMkt_iteration(iteration2,ttot4,regi,emiMkt)); + ); + ); + p47_targetState("rolledBack",ttot,ttot2,ext_regi,emiMktExt) = 2; !! rollback tried and rejected - never roll this target again + p47_slopeTrace_iter("rollUndo",iteration,ttot,ttot2,ext_regi,emiMktExt) = p47_targetState("rollFromIter",ttot,ttot2,ext_regi,emiMktExt); + put_utility "msg" / "Rollback UNDONE (deviation got worse): restored iteration " p47_targetState("rollFromIter",ttot,ttot2,ext_regi,emiMktExt):3:0 " price for "; + put_utility "msg" / ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl; + ); + +*** Check Convergence: +*** 1. Strictly use raw user tolerance (pm_emiMktTarget_tolerance). Unreachable targets freeze with their real residual. +*** 2. smallPrice: If price hits floor (<1.1 $/tCO2) and emissions are below target, freeze as met (non-binding market). loop(regi$regi_groupExt(ext_regi,regi), - loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), -*** target converged: -*** if the price is at minimal level (<1$/tCO2 + 10% of tolerance) and current emissions are lower than the target (avoid pushing to negative price values) + loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), if((((pm_taxemiMkt(ttot2,regi,emiMkt) - 1*sm_DptCO2_2_TDpGtC) lt 0.1*sm_DptCO2_2_TDpGtC) and (pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt) lt 0)), regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"smallPrice") = YES; - p47_targetConverged(ttot2,ext_regi) = 1; -*** if current absolute emissions minus the target (=deviation) is lower than the tolerance - elseif(abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le pm_emiMktTarget_tolerance(ext_regi)), - regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"lowerThanTolerance") = YES; - p47_targetConverged(ttot2,ext_regi) = 1; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + ); + ); + ); +*** otherwise apply the hysteresis/persistence tolerance test on this target's (aggregate) deviation + if(p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0, +*** HOLD: Keep previously converged target frozen through temporary Nash noise. Re-open only if deviation stays outside the EXIT band for `persist` consecutive iterations. + if(p47_marketConverged_iter(iteration-1,ttot,ttot2,ext_regi,emiMktExt) eq 1, +*** Count iterations outside exit band (exitFrac * tolerance). The EXIT band must be wider than the ENTER band (hysteresis) to prevent minor noise from repeatedly breaking the hold. + p47_slopeAux("outBand") = 0; + loop(iteration2$((iteration2.val le iteration.val) and (iteration2.val gt iteration.val - p47_slopeParam("persist"))), + p47_slopeAux("outBand") = p47_slopeAux("outBand") + + 1$(abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)) gt p47_slopeParam("exitFrac")*pm_emiMktTarget_tolerance(ext_regi)); + ); + p47_slopeAux("holdOk") = 0; +*** Persistence only: Do not check current-iteration deviation; requiring in-band status every single iteration would un-freeze targets on minor noise wobbles. (Runaway targets are caught by divergence checks). + p47_slopeAux("holdOk")$(p47_slopeAux("outBand") lt p47_slopeParam("persist")) = 1; + if(p47_slopeAux("holdOk") eq 1, +*** Label by outcome: Assign lowerThanTolerance if |dev| <= raw tolerance, otherwise bestAchievable. + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"lowerThanTolerance")$(abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le pm_emiMktTarget_tolerance(ext_regi)) = YES; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable")$(abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) gt pm_emiMktTarget_tolerance(ext_regi)) = YES; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + else +*** Hold failed (un-freezing target). 3 Outcomes: +*** (a) |dev| <= reopenMaxDev & budget left: Re-open target (charge reopenMax budget, cap first step). +*** (b) Budget spent: Give up, freeze as bestAchievable, roll back price. +*** (c) |dev| > reopenMaxDev: Stay frozen if given up; otherwise release uncharged (releaseCount). + if((p47_slopeParam("reopenMaxDev") gt 0) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le p47_slopeParam("reopenMaxDev")), + if(p47_targetState("reopenCount",ttot,ttot2,ext_regi,emiMktExt) lt p47_slopeParam("reopenMax"), + p47_targetState("reopenCount",ttot,ttot2,ext_regi,emiMktExt) = p47_targetState("reopenCount",ttot,ttot2,ext_regi,emiMktExt) + 1; + p47_targetState("reopenIter",ttot,ttot2,ext_regi,emiMktExt) = iteration.val; +*** Reset aim attempts (aimTries = 0) so the re-opened target gets a fresh budget to aim for the tighter enterFrac band. + p47_targetState("aimTries",ttot,ttot2,ext_regi,emiMktExt) = 0; + else + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = YES; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) = 1; + p47_slopeTrace_iter("giveUpBy",iteration,ttot,ttot2,ext_regi,emiMktExt) = 4; !! re-open budget spent + p47_slopeAux("wantRoll") = 1; !! giving up after reopenMax corrections -> keep the best state reached + ); +*** Stick hold: Permanently freeze abandoned targets (giveUp = 1) to prevent price runaways. + elseif p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) eq 1, + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = YES; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + else +*** Genuine release: not given up on, but thrown further out than a drift (or re-open switched off via reopenMaxDev). p47_marketConverged_iter stays 0, so the target simply steers again, uncharged. + p47_targetState("releaseCount",ttot,ttot2,ext_regi,emiMktExt) = p47_targetState("releaseCount",ttot,ttot2,ext_regi,emiMktExt) + 1; +*** Reset aimTries = 0 for released targets too so they get a fresh budget to aim for enterFrac. + p47_targetState("aimTries",ttot,ttot2,ext_regi,emiMktExt) = 0; + ); + ); +*** ENTER (Convergence Criteria) - Two ways in: +*** (1) AIM: |dev| <= enterFrac (0.75x tolerance) for `persist` iterations (normal path, gives drift buffer). +*** (2) ACCEPT: |dev| <= 1.0x raw tolerance once aimMaxTries attempts are spent (prevents hanging). +*** An aim attempt (aimTries) counts only iterations actively steered AND inside raw tolerance. + else + p47_slopeAux("inBand") = 0; + loop(iteration2$((iteration2.val le iteration.val) and (iteration2.val gt iteration.val - p47_slopeParam("persist"))), + p47_slopeAux("inBand") = p47_slopeAux("inBand") + + 1$(abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)) le p47_slopeParam("enterFrac")*pm_emiMktTarget_tolerance(ext_regi)); + ); +*** Increment aimTries if steered and inside tolerance; capped at aimMaxTries for accurate reporting. + if((abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le pm_emiMktTarget_tolerance(ext_regi)) + AND (p47_currentConvergence_iter(iteration-1,ttot2,ext_regi) eq 1), + p47_targetState("aimTries",ttot,ttot2,ext_regi,emiMktExt) = + min(p47_targetState("aimTries",ttot,ttot2,ext_regi,emiMktExt) + 1, max(1, p47_slopeParam("aimMaxTries"))); + ); +*** ACCEPT: budget spent and currently inside the tolerance -> stop polishing, hand the solve to the next target + p47_slopeAux("inBand")$((p47_slopeAux("inBand") lt p47_slopeParam("persist")) + AND (p47_slopeParam("aimMaxTries") ge 1) + AND (p47_targetState("aimTries",ttot,ttot2,ext_regi,emiMktExt) ge p47_slopeParam("aimMaxTries")) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) + le pm_emiMktTarget_tolerance(ext_regi))) = p47_slopeParam("persist"); + p47_slopeAux("priceOk") = 1; + if((p47_slopeParam("priceEps") gt 0) and (iteration.val gt 1), +*** require every constituent market price of this target to have settled + loop(regi$regi_groupExt(ext_regi,regi), + loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), + if(abs(pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration(iteration-1,ttot2,regi,emiMkt)) gt p47_slopeParam("priceEps")*pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt), + p47_slopeAux("priceOk") = 0; + ); + ); + ); + ); + if((p47_slopeAux("inBand") ge p47_slopeParam("persist")) and (p47_slopeAux("priceOk") eq 1), +*** raw tolerance - see the identical pair in the HOLD branch above + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"lowerThanTolerance")$(abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le pm_emiMktTarget_tolerance(ext_regi)) = YES; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable")$(abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) gt pm_emiMktTarget_tolerance(ext_regi)) = YES; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + ); + ); + ); + +*** NOISE-FLOOR STOP (Give-Up): If price is settled and deviation is trapped in a narrow band outside tolerance, accept bestAchievable and freeze. (Guarded by |dev| > tolerance; disabled by noiseFloorMaxDev = 0). + if((p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (p47_slopeParam("noiseFloorMaxDev") gt 0) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) gt pm_emiMktTarget_tolerance(ext_regi)) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le p47_slopeParam("noiseFloorMaxDev")), + p47_slopeAux("devMax") = -inf; + p47_slopeAux("devMin") = inf; + p47_slopeAux("settled") = 0; + p47_slopeAux("nWin") = 0; +*** Find the lowest deviation within the current window (window-local best) for potential rollback. + p47_slopeAux("devAbsMin") = inf; + p47_slopeAux("bestWinIter") = 0; + loop(iteration2$((iteration2.val le iteration.val) and (iteration2.val gt iteration.val - p47_slopeParam("maxWindow"))), + p47_slopeAux("devMax") = max(p47_slopeAux("devMax"), pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("devMin") = min(p47_slopeAux("devMin"), pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("nWin") = p47_slopeAux("nWin") + 1; + if((iteration2.val lt iteration.val) + AND (abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)) lt p47_slopeAux("devAbsMin")), + p47_slopeAux("devAbsMin") = abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("bestWinIter") = iteration2.val; + ); +*** price settled = the tax rescale factor stayed within minPriceSpread of 1 (only the already-computed past iterations) + p47_slopeAux("settled") = p47_slopeAux("settled") + + 1$((iteration2.val lt iteration.val) and (abs(p47_factorRescaleemiMktCO2Tax_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt) - 1) le p47_slopeParam("minPriceSpread"))); + ); +*** Fire only when deviation is narrow, price is settled, and full window history exists. + if(((p47_slopeAux("devMax") - p47_slopeAux("devMin")) le max(p47_slopeParam("noiseFloorBandWidth"), p47_slopeParam("noiseFloorBandWidthRel") * pm_emiMktTarget_tolerance(ext_regi))) + AND (p47_slopeAux("settled") ge (p47_slopeAux("nWin") - 1)) + AND (p47_slopeAux("nWin") ge p47_slopeParam("maxWindow")), + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = YES; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) = 1; !! noise-floor stop: a deliberate give-up, must stay frozen + p47_slopeTrace_iter("giveUpBy",iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; !! noise-floor stop +*** Roll back to the best deviation in the window if the entire window was strictly outside tolerance. + if((p47_slopeParam("noiseFloorRollback") ge 0.5) + AND (p47_slopeAux("bestWinIter") gt 0) + AND (p47_slopeAux("devAbsMin") gt pm_emiMktTarget_tolerance(ext_regi)) + AND (p47_slopeAux("devAbsMin") lt abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt))), + p47_slopeAux("wantRoll") = 1; + p47_slopeAux("rollIter") = p47_slopeAux("bestWinIter"); + ); + ); + ); + +*** Parked-target stop: Gives up on targets trapped by hysteresis (held converged, but sitting outside raw tolerance and inside EXIT band). (Dormant when exitFrac = 1.0). + if((p47_slopeParam("parkedStop") ge 0.5) + AND (p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 1) + AND (p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (NOT regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"smallPrice")) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) gt pm_emiMktTarget_tolerance(ext_regi)) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le p47_slopeParam("exitFrac") * pm_emiMktTarget_tolerance(ext_regi)), + p47_slopeAux("devMax") = -inf; + p47_slopeAux("devMin") = inf; + p47_slopeAux("devAbsMin") = inf; + p47_slopeAux("nWin") = 0; + loop(iteration2$((iteration2.val le iteration.val) and (iteration2.val gt iteration.val - p47_slopeParam("maxWindow"))), + p47_slopeAux("devMax") = max(p47_slopeAux("devMax"), pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("devMin") = min(p47_slopeAux("devMin"), pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("devAbsMin") = min(p47_slopeAux("devAbsMin"), abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt))); + p47_slopeAux("nWin") = p47_slopeAux("nWin") + 1; + ); +*** Confirm target is truly parked (devAbsMin > tolerance): Ensures deviation never dipped inside tolerance during the window, leaving noisy oscillating targets alone to finish converging. + if((p47_slopeAux("nWin") ge p47_slopeParam("maxWindow")) + AND (p47_slopeAux("devAbsMin") gt pm_emiMktTarget_tolerance(ext_regi)) + AND ((p47_slopeAux("devMax") - p47_slopeAux("devMin")) le max(p47_slopeParam("noiseFloorBandWidth"), p47_slopeParam("noiseFloorBandWidthRel") * pm_emiMktTarget_tolerance(ext_regi))), + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = YES; + p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) = 1; !! parked stop: a deliberate give-up, must stay frozen + p47_slopeTrace_iter("giveUpBy",iteration,ttot,ttot2,ext_regi,emiMktExt) = 5; !! parked stop + p47_slopeTrace_iter("parked",iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; +*** No rollback needed: Target was frozen for the whole window, so price was constant. + put_utility "msg" / "Parked-target stop: held inside the exit band but outside the tolerance at deviation "; + put_utility "msg" / pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt):10:5 " for " ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl; + ); + ); + +*** Infeasible-target stop: Give up & freeze when price rescale hits the cap for a full window without reaching the target. + if((p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (p47_slopeParam("noiseFloorMaxDev") gt 0) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) gt p47_slopeParam("noiseFloorMaxDev")), + p47_slopeAux("atCap") = 0; + p47_slopeAux("nWin") = 0; + p47_slopeAux("emiMin") = inf; + p47_slopeAux("devAbsMax") = 0; + p47_slopeAux("devMax") = -inf; + p47_slopeAux("devMin") = inf; + loop(iteration2$((iteration2.val lt iteration.val) and (iteration2.val ge iteration.val - p47_slopeParam("maxWindow"))), + p47_slopeAux("nWin") = p47_slopeAux("nWin") + 1; + p47_slopeAux("atCap") = p47_slopeAux("atCap") + + 1$(p47_factorRescaleemiMktCO2Tax_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt) ge (1 - p47_slopeParam("minPriceSpread")) * p47_slopeParam("rescaleCapHi")); + p47_slopeAux("emiMin") = min(p47_slopeAux("emiMin"), p47_emiMktCurrent_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("devAbsMax") = max(p47_slopeAux("devAbsMax"), abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt))); + p47_slopeAux("devMax") = max(p47_slopeAux("devMax"), pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("devMin") = min(p47_slopeAux("devMin"), pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + ); +*** Declare infeasible only if: (a) rescale pinned at max cap for full window, (b) deviation stalled (no longer shrinking), and (c) deviation is one-sided (never crossed target). + if((p47_slopeAux("nWin") ge p47_slopeParam("maxWindow")) AND (p47_slopeAux("atCap") ge p47_slopeAux("nWin")) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) ge p47_slopeParam("infeasStallFrac") * p47_slopeAux("devAbsMax")) + AND (NOT((p47_slopeAux("devMax") gt 0) AND (p47_slopeAux("devMin") lt 0))), + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = YES; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) = 1; !! infeasible-target stop: a deliberate give-up, must stay frozen + p47_slopeTrace_iter("giveUpBy",iteration,ttot,ttot2,ext_regi,emiMktExt) = 2; !! infeasible-target stop + p47_slopeAux("wantRoll") = 1; !! giving up on an (locally) infeasible target -> keep the best state reached +*** Roll back to the "knee": Find the earliest iteration that achieved nearly the same minimum emissions at a much lower, realistic price. (Disabled by infeasEmiTol = 0). + if(p47_slopeParam("infeasEmiTol") gt 0, + p47_slopeAux("kneeIter") = iteration.val; + loop(iteration2$((iteration2.val lt iteration.val) and (iteration2.val ge iteration.val - p47_slopeParam("maxWindow")) + and (p47_emiMktCurrent_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt) + le p47_slopeAux("emiMin") + p47_slopeParam("infeasEmiTol") * max(abs(p47_slopeAux("emiMin")), pm_emiMktRefYear(ttot,ttot2,ext_regi,emiMktExt), p47_emiMktRefBudget(ttot,ttot2,ext_regi,emiMktExt)))), + p47_slopeAux("kneeIter") = min(p47_slopeAux("kneeIter"), iteration2.val); + ); + if(p47_slopeAux("kneeIter") lt iteration.val, + p47_slopeAux("rollIter") = p47_slopeAux("kneeIter"); + ); + ); + ); + ); + +*** DIVERGENCE HANDLING: Catches oscillating runaway targets. Triggers only if ALL 3 hurdles are met: +*** (1) Armed (divArmed = 1), (2) Persistent (dev > divergeFactor * bestDev for `persist` iterations), and (3) Not recovering (deviation stalled). (divergeFactor < 1 disables). + if((p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (p47_slopeParam("divergeFactor") ge 1) + AND (p47_targetState("divArmed",ttot,ttot2,ext_regi,emiMktExt) eq 1), + p47_slopeAux("divOut") = 0; + p47_slopeAux("divMax") = 0; + loop(iteration2$((iteration2.val le iteration.val) and (iteration2.val gt iteration.val - p47_slopeParam("persist"))), + p47_slopeAux("divOut") = p47_slopeAux("divOut") + + 1$(abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)) + gt p47_slopeParam("divergeFactor") + * max(p47_targetState("bestDevAbs",ttot,ttot2,ext_regi,emiMktExt), pm_emiMktTarget_tolerance(ext_regi))); + p47_slopeAux("divMax") = max(p47_slopeAux("divMax"), abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt))); + ); + if((p47_slopeAux("divOut") ge p47_slopeParam("persist")) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) + ge p47_slopeParam("infeasStallFrac") * p47_slopeAux("divMax")), +*** Find the lowest deviation within the recent window (rollbackMaxAge), ensuring we roll back to a recent, relevant price path rather than an ancient one. + p47_slopeAux("divRoll") = 0; + p47_slopeAux("divBest") = abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)); + loop(iteration2$((iteration2.val lt iteration.val) + and (iteration2.val ge iteration.val - max(1, p47_slopeParam("rollbackMaxAge")))), + if(abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)) lt p47_slopeAux("divBest"), + p47_slopeAux("divBest") = abs(pm_emiMktTarget_dev_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("divRoll") = iteration2.val; + ); + ); +*** SENTINEL: Require divergeBrakeMax >= 1 so config sentinel values < 1 mean "freeze on first detection" (no brakes). + if((p47_slopeParam("divergeBrakeMax") ge 1) + AND (p47_targetState("divBrakeCount",ttot,ttot2,ext_regi,emiMktExt) lt p47_slopeParam("divergeBrakeMax")), +*** BRAKE (Reversible): Restore window-local best price and keep steering. Cap next step at reopenStepCap to prevent re-divergence. + p47_targetState("divBrakeCount",ttot,ttot2,ext_regi,emiMktExt) = p47_targetState("divBrakeCount",ttot,ttot2,ext_regi,emiMktExt) + 1; + if(p47_slopeAux("divRoll") gt 0, + loop(regi$regi_groupExt(ext_regi,regi), + loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), + pm_taxemiMkt(ttot4,regi,emiMkt)$(ttot4.val gt p47_slopeAux("prevTgtYr")) = + sum(iteration2$(iteration2.val eq p47_slopeAux("divRoll")), pm_taxemiMkt_iteration(iteration2,ttot4,regi,emiMkt)); + ); + ); + ); + p47_targetState("reopenIter",ttot,ttot2,ext_regi,emiMktExt) = iteration.val; + p47_slopeTrace_iter("divBrake",iteration,ttot,ttot2,ext_regi,emiMktExt) = p47_slopeAux("divRoll"); + put_utility "msg" / "Divergence BRAKE (" p47_targetState("divBrakeCount",ttot,ttot2,ext_regi,emiMktExt):2:0 " of " p47_slopeParam("divergeBrakeMax"):2:0 "): deviation "; + put_utility "msg" / pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt):10:5 " -> price restored to iteration " p47_slopeAux("divRoll"):3:0; + put_utility "msg" / " and still steering, for " ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl; + else +*** BRAKES EXHAUSTED: Target keeps diverging after max brakes. Freeze at best state (giveUp = 1). + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = YES; + p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) = 1; !! divergence stop: a deliberate give-up, must stay frozen + p47_slopeTrace_iter("giveUpBy",iteration,ttot,ttot2,ext_regi,emiMktExt) = 3; !! divergence stop + p47_slopeAux("wantRoll") = 1; + put_utility "msg" / "Divergence STOP (brake budget spent): deviation " pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt):10:5 " far beyond best "; + put_utility "msg" / p47_targetState("bestDevAbs",ttot,ttot2,ext_regi,emiMktExt):10:5 " for " ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl; ); ); ); + +*** PRICE ROLLBACK ON FREEZE: Prefer run-wide best-so-far price if meaningfully better (|dev_best| <= rollbackBestFrac * |dev_now|) and recent enough (rollbackMaxAge). Otherwise keep stop's candidate price. + if((p47_slopeAux("wantRoll") eq 1) + AND (p47_targetState("rolledBack",ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 1) + AND regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable"), + if((p47_slopeParam("rollbackBestFrac") gt 0) + AND (p47_targetState("bestDevIter",ttot,ttot2,ext_regi,emiMktExt) gt 0) + AND (p47_targetState("bestDevIter",ttot,ttot2,ext_regi,emiMktExt) lt iteration.val) + AND ((p47_slopeParam("rollbackMaxAge") le 0) + OR (p47_targetState("bestDevIter",ttot,ttot2,ext_regi,emiMktExt) ge iteration.val - p47_slopeParam("rollbackMaxAge"))) + AND (p47_targetState("bestDevAbs",ttot,ttot2,ext_regi,emiMktExt) + le p47_slopeParam("rollbackBestFrac") * abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt))), + p47_slopeAux("rollIter") = p47_targetState("bestDevIter",ttot,ttot2,ext_regi,emiMktExt); + ); +*** No rollback candidate (disabled, not better, or too old): Keep current price. + if(p47_slopeAux("rollIter") gt 0, + loop(regi$regi_groupExt(ext_regi,regi), + loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), + pm_taxemiMkt(ttot4,regi,emiMkt)$(ttot4.val gt p47_slopeAux("prevTgtYr")) = + sum(iteration2$(iteration2.val eq p47_slopeAux("rollIter")), pm_taxemiMkt_iteration(iteration2,ttot4,regi,emiMkt)); + ); + ); + p47_slopeTrace_iter("rollIter",iteration,ttot,ttot2,ext_regi,emiMktExt) = p47_slopeAux("rollIter"); + p47_targetState("rolledBack",ttot,ttot2,ext_regi,emiMktExt) = 1; +*** Store pre-rollback iteration and deviation to verify and undo on next iteration if worse. + p47_targetState("rollFromIter",ttot,ttot2,ext_regi,emiMktExt) = iteration.val; + p47_targetState("rollFromDev",ttot,ttot2,ext_regi,emiMktExt) = abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)); + put_utility "msg" / "Best-achievable freeze: price rolled back to iteration " p47_slopeAux("rollIter"):3:0 " for "; + put_utility "msg" / ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl; + ); + ); + +*** NOTE: reopenCount is never reset by re-convergence (prevents limit-cycles). It is cleared only after reopenRefresh consecutive settled iterations. + +*** FROZEN vs MET: p47_marketConverged_iter = stop steering price. p47_marketMet_iter = target achieved within raw tolerance (or slack smallPrice). + p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 0; + if((abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) le pm_emiMktTarget_tolerance(ext_regi)) + OR regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"smallPrice"), + p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = 1; + ); + +*** RE-OPEN BUDGET REFRESH: Targets frozen and met for reopenRefresh consecutive iterations earn their re-open budget back (reopenCount = 0). + if((p47_slopeParam("reopenRefresh") ge 1) + AND (p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 1) + AND (p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 1) + AND (p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) eq 0), + p47_targetState("settledCount",ttot,ttot2,ext_regi,emiMktExt) = p47_targetState("settledCount",ttot,ttot2,ext_regi,emiMktExt) + 1; +*** Increment refreshCount only when a spent budget (reopenCount > 0) is actually restored. + if(p47_targetState("settledCount",ttot,ttot2,ext_regi,emiMktExt) ge p47_slopeParam("reopenRefresh"), + if(p47_targetState("reopenCount",ttot,ttot2,ext_regi,emiMktExt) gt 0, + put_utility "msg" / "Re-open budget refreshed after " p47_targetState("settledCount",ttot,ttot2,ext_regi,emiMktExt):3:0 " settled iterations for "; + put_utility "msg" / ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl; + p47_targetState("refreshCount",ttot,ttot2,ext_regi,emiMktExt) = p47_targetState("refreshCount",ttot,ttot2,ext_regi,emiMktExt) + 1; + ); + p47_targetState("reopenCount",ttot,ttot2,ext_regi,emiMktExt) = 0; + p47_targetState("settledCount",ttot,ttot2,ext_regi,emiMktExt) = 0; + ); + else + p47_targetState("settledCount",ttot,ttot2,ext_regi,emiMktExt) = 0; + ); + +*** HONESTY RE-LABEL (Promotion): If a given-up target lands inside raw tolerance (met), promote label to lowerThanTolerance. + if((p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 1) + AND regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") + AND (NOT regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"smallPrice")), + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"lowerThanTolerance") = YES; + ); + +*** HONESTY RE-LABEL (Demotion): Frozen targets outside the EXIT band are re-labelled as unmetFrozen (replaces lowerThanTolerance and bestAchievable). + if((p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 1) + AND (p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (abs(pm_emiMktTarget_dev_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) + gt p47_slopeParam("exitFrac") * pm_emiMktTarget_tolerance(ext_regi)), + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"lowerThanTolerance") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"unmetFrozen") = YES; + ); + +*** AND aggregation: the region-period is not converged if this market target is not converged + if(p47_marketConverged_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0, + p47_targetConverged(ttot2,ext_regi) = 0; + ); +*** ... and separately, not MET if this market target is not met + if(p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0, + p47_targetMet(ttot2,ext_regi) = 0; + ); ); ); p47_targetConverged_iter(iteration,ttot2,ext_regi) = p47_targetConverged(ttot2,ext_regi); !!save regional target converged iteration information for debugging +p47_targetMet_iter(iteration,ttot2,ext_regi) = p47_targetMet(ttot2,ext_regi); !!save regional target MET information (raw tolerance, no hysteresis) across iterations + +*** CAP-TIME RE-CHECK: At the final iteration cap (cm_iteration_max), any target outside raw tolerance becomes unmetAtCap (excluding smallPrice). +if(ord(iteration) eq cm_iteration_max, + loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47), + if((abs(pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt)) gt pm_emiMktTarget_tolerance(ext_regi)) + AND (NOT regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"smallPrice")), + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"lowerThanTolerance") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"unmetFrozen") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"unmetAtCap") = YES; + put_utility "msg" / "Target still outside tolerance at the iteration cap: "; + put_utility "msg" / ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl " dev " pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt):10:5; + ); + ); +); -*** Checking if all targets for the region converged +*** TWO TERMINATION SIGNALS: p47_allTargetsFrozen drives module 47 flow; pm_allTargetsConverged (read by module 80) requires targets to be frozen AND (met OR giveUp). loop(ext_regi$regiEmiMktTarget(ext_regi), - pm_allTargetsConverged(ext_regi) = 1; + p47_allTargetsFrozen(ext_regi) = 1; + p47_allTargetsMet(ext_regi) = 1; loop((ttot)$regiANDperiodEmiMktTarget_47(ttot,ext_regi), if(p47_targetConverged(ttot,ext_regi) eq 0, - pm_allTargetsConverged(ext_regi) = 0; + p47_allTargetsFrozen(ext_regi) = 0; + ); + if(p47_targetMet(ttot,ext_regi) eq 0, + p47_allTargetsMet(ext_regi) = 0; ); ); +*** Unmet AND not given up: run must not end yet + p47_unmetNoGiveUp(ext_regi) = 0; + loop((ttot,ttot2,emiMktExt,target_type_47,emi_type_47)$(pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) + AND (p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0) + AND (NOT regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"smallPrice")) + AND (p47_targetState("giveUp",ttot,ttot2,ext_regi,emiMktExt) eq 0)), + p47_unmetNoGiveUp(ext_regi) = 1; + ); + pm_allTargetsConverged(ext_regi) = p47_allTargetsFrozen(ext_regi)$(p47_unmetNoGiveUp(ext_regi) eq 0); ); p47_allTargetsConverged_iter(iteration,ext_regi) = pm_allTargetsConverged(ext_regi); +p47_allTargetsFrozen_iter(iteration,ext_regi) = p47_allTargetsFrozen(ext_regi); +p47_allTargetsMet_iter(iteration,ext_regi) = p47_allTargetsMet(ext_regi); + +*** UNREACHABLE-TARGET WARNING: Log warning if a region is frozen but targets are unmet. +loop(ext_regi$(regiEmiMktTarget(ext_regi) AND (p47_allTargetsFrozen(ext_regi) eq 1) AND (p47_allTargetsMet(ext_regi) eq 0)), + put_utility "msg" / "### WARNING: region " ext_regi.tl " is CONVERGED but its emission targets are NOT MET."; + put_utility "msg" / "### The convergence algorithm has stopped steering the carbon price of the targets"; + put_utility "msg" / "### listed below; each carries the residual shown. Check regiEmiMktconvergenceType"; + put_utility "msg" / "### (unmetFrozen / unmetAtCap / bestAchievable all carry a residual, lowerThanTolerance"; + put_utility "msg" / "### and smallPrice do not) and p47_marketMet_iter for the per-market verdict."; + loop((ttot,ttot2,emiMktExt,target_type_47,emi_type_47)$(pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) + AND (p47_marketMet_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) eq 0)), + put_utility "msg" / "### unmet: " ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl + " dev " pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt):10:5 + " tolerance " pm_emiMktTarget_tolerance(ext_regi):10:5; +*** END-OF-RUN RE-LABEL: Once fully frozen, re-label any target outside raw tolerance as unmetFrozen. + if(NOT regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"unmetAtCap"), + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"lowerThanTolerance") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"bestAchievable") = NO; + regiEmiMktconvergenceType(iteration,ttot,ttot2,ext_regi,emiMktExt,"unmetFrozen") = YES; + ); + ); +); + +*** ----- Sequential Target Solving & Fit Window Anchor ----- -*** define current target to be solved p47_currentConvergence_iter(iteration,ttot,ext_regi) = 0; loop(ext_regi$regiEmiMktTarget(ext_regi), -*** solving targets sequentially, i.e. only apply target convergence algorithm if previous yearly targets were already achieved - if(not(pm_allTargetsConverged(ext_regi) eq 1), !!no rescale need if all targets already converged -*** define current target to be solved + if(not(p47_allTargetsFrozen(ext_regi) eq 1), !!no rescale need if all targets already frozen loop((ttot)$regiANDperiodEmiMktTarget_47(ttot,ext_regi), p47_currentConvergencePeriod(ext_regi) = ttot.val; break$(p47_targetConverged(ttot,ext_regi) eq 0); !!only run target convergence up to the first year that has not converged @@ -245,124 +709,186 @@ loop(ext_regi$regiEmiMktTarget(ext_regi), ); ); -*** reference iteration from which to calculate the mitigation cost slope. +*** WINDOW ANCHOR: Measured in steered iterations (skipping frozen iterations) to hold up to maxWindow active price-steering iterations. loop((ext_regi,ttot)$regiANDperiodEmiMktTarget_47(ttot,ext_regi), if(ord(iteration) eq 1, p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) = 1; - elseif(NOT(p47_currentConvergence_iter(iteration,ttot,ext_regi) eq p47_currentConvergence_iter(iteration-1,ttot,ext_regi))), !! reset the iteration reference for slope calculation if the target that is being analyzed changes - p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) = ord(iteration); else - p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) = p47_slopeReferenceIteration_iter(iteration-1,ttot,ext_regi); +*** how many steered iterations to walk past before the window starts (keep the LAST maxWindow of them) + p47_slopeAux("nSteer") = sum(iteration2$(iteration2.val le iteration.val), p47_currentConvergence_iter(iteration2,ttot,ext_regi)); + p47_slopeAux("skipSteer") = max(0, p47_slopeAux("nSteer") - p47_slopeParam("maxWindow")); + p47_slopeAux("cntSteer") = 0; +*** default: no steered history yet -> fresh start at this iteration (the squareDev_firstIteration cold-start ramp) + p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) = ord(iteration); +*** walk the steered iterations in order and stop on the (skipSteer+1)-th - the oldest one still inside the +*** window. Written with the conditional-assign + `break$()` idiom used everywhere else in this file. + loop(iteration2$((iteration2.val le iteration.val) AND (p47_currentConvergence_iter(iteration2,ttot,ext_regi) eq 1)), + p47_slopeAux("cntSteer") = p47_slopeAux("cntSteer") + 1; + p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi)$(p47_slopeAux("cntSteer") eq (p47_slopeAux("skipSteer") + 1)) = iteration2.val; + break$(p47_slopeAux("cntSteer") gt p47_slopeAux("skipSteer")); + ); ); ); + +*** ----- Least-Squares Slope & CO2 Tax Rescale Factor ----- + *** resetting rescale factor for the next iteration p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = 0; pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = 0; -*** Calculating the emissions tax rescale factor based on previous iterations emission reduction for current targets +regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,rescaleType) = NO; +*** Calculating the tax rescale factor from a least-squares slope of emissions vs. carbon price over the current window loop(ext_regi$regiEmiMktTarget(ext_regi), loop((ttot2)$(ttot2.val eq p47_currentConvergencePeriod(ext_regi)), if(not(p47_targetConverged(ttot2,ext_regi) eq 1), !!no rescale factor calculation need if the target already converged loop((ttot,emiMktExt,target_type_47,emi_type_47)$(pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)), loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), loop(regi$regiEmiMktTarget2regi_47(ext_regi,regi), -*** if rescale factor was already calculated for ext_regi, there is no need to recalculate it - continue$(pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt)); -*** calculating the rescale factor - loop(iteration2$((iteration2.val le iteration.val) and (iteration2.val eq p47_slopeReferenceIteration_iter(iteration,ttot2,ext_regi))), !!reference iteration for slope calculation -*** if it is the first iteration or the reference iteration changed, initialize the rescale factor based on remaining deviation - if((iteration.val - iteration2.val) eq 0, - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_firstIteration") = YES; - pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); -*** else if for the extreme case of a perfect match with no change between the two iterations emisssion taxes used in the slope calculation, in order to avoid a division by zero error assume the rescale factor based on remaining deviation - elseif((pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt) eq 0) - AND - (pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration("1",ttot2,regi,emiMkt) eq 0)), - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_perfectMatch") = YES; - pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); -*** else if emission tax variation in relation to base iteration is very small, assume the rescale factor based on the remaining deviation to avoid very slow improvements - elseif((abs(pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt)) lt 1e-2) - AND - (abs(pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration("1",ttot2,regi,emiMkt)) lt 1e-2)), - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_smallChange") = YES; +*** if rescale factor was already calculated for ext_regi, there is no need to recalculate it + continue$(sum(rescaleType$regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,rescaleType), 1)); + + s47_slopeWindowStart = p47_slopeReferenceIteration_iter(iteration,ttot2,ext_regi); + +*** CASE 1 - no usable history yet (window holds a single point): fall back to squareDev + if((ord(iteration) - s47_slopeWindowStart) eq 0, + regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_firstIteration") = YES; + pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); + else +*** accumulate least-squares sums of (price, emissions) over the window [s47_slopeWindowStart .. iteration], +*** counting ONLY iterations in which this region-period was actually steering its own price. The anchor +*** above is chosen so that this collects at most maxWindow points. Iterations spent frozen contribute a +*** duplicate price and would flatten the fit - see the window-anchor note for why the window is measured +*** in steered iterations rather than calendar ones. + p47_slopeAux(slopeTerm) = 0; + p47_slopeAux("pMin") = inf; p47_slopeAux("pMax") = -inf; + loop(iteration2$((iteration2.val ge s47_slopeWindowStart) AND (iteration2.val le iteration.val) + AND (p47_currentConvergence_iter(iteration2,ttot2,ext_regi) eq 1)), + p47_slopeAux("n") = p47_slopeAux("n") + 1; + p47_slopeAux("sumP") = p47_slopeAux("sumP") + pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt); + p47_slopeAux("sumE") = p47_slopeAux("sumE") + p47_emiMktCurrent_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt); + p47_slopeAux("sumP2") = p47_slopeAux("sumP2") + sqr(pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt)); + p47_slopeAux("sumE2") = p47_slopeAux("sumE2") + sqr(p47_emiMktCurrent_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)); + p47_slopeAux("sumPE") = p47_slopeAux("sumPE") + pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt) * p47_emiMktCurrent_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt); + p47_slopeAux("pMin") = min(p47_slopeAux("pMin"), pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt)); + p47_slopeAux("pMax") = max(p47_slopeAux("pMax"), pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt)); + ); + p47_slopeAux("denom") = p47_slopeAux("n")*p47_slopeAux("sumP2") - sqr(p47_slopeAux("sumP")); !! = n^2 * var(price); zero only if every window price is identical + p47_slopeAux("num") = p47_slopeAux("n")*p47_slopeAux("sumPE") - p47_slopeAux("sumP")*p47_slopeAux("sumE"); + p47_slopeAux("varE") = p47_slopeAux("n")*p47_slopeAux("sumE2") - sqr(p47_slopeAux("sumE")); + +*** CASE 2 - insufficient price spread across the window: slope is ill-conditioned, use squareDev +*** (replaces the squareDev_perfectMatch, squareDev_smallChange and squareDev_outsideWindow guards) + if((p47_slopeAux("pMax") - p47_slopeAux("pMin")) lt p47_slopeParam("minPriceSpread") * abs(pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt)), + regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_lowPriceSpread") = YES; pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); -*** else, calculate rescale factor using quantities and prices slope changes else -*** if denominator in relation to reference is not close to zero, calculate the price slope in relation to the reference iteration mitigation and price levels - if(NOT(abs(pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt)) lt 1e-2), - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"slope_refIteration") = YES; - p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = - (p47_emiMktCurrent_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) - p47_emiMktCurrent_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt)) - / - (pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration(iteration2,ttot2,regi,emiMkt)); -*** else if denominator in relation to first iteration is not close to zero, calculate the price slope in relation to the first iteration mitigation and price levels instead - elseif(NOT(abs(pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration("1",ttot2,regi,emiMkt)) lt 1e-2)), - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"slope_firstIteration") = YES; - p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = - (p47_emiMktCurrent_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) - p47_emiMktCurrent_iter("1",ttot,ttot2,ext_regi,emiMktExt)) - / - (pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt) - pm_taxemiMkt_iteration("1",ttot2,regi,emiMkt)); -*** else if there is a previous iteration calculated slope, repeat the previous iteration slope - elseif((iteration.val gt 1) and (p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) - p47_slopeReferenceIteration_iter(iteration-1,ttot,ext_regi) eq 0)), - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"slope_repeatPrev") = YES; - p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = p47_factorRescaleSlope_iter(iteration-1,ttot,ttot2,ext_regi,emiMktExt); -*** else slope is not available, set the rescale factor based on remaining deviation - else - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_noSlope") = YES; - pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); + p47_slopeAux("slope") = p47_slopeAux("num") / p47_slopeAux("denom"); + p47_slopeAux("r2") = 1; !! default when emissions do not vary in the window (diverted to unstableFit by the sign check below) + if(p47_slopeAux("varE") gt 0, + p47_slopeAux("r2") = sqr(p47_slopeAux("num")) / (p47_slopeAux("denom") * p47_slopeAux("varE")); ); -*** if we are using the slope - if(NOT(regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_noSlope")), -*** if the slope is positive - if(p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) gt 0, - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,rescaleType) = NO; -*** if there is a previous iteration calculated slope, repeat the previous iteration slope to avoid the positive value because we assume a trade-off between tax and emission levels - if((iteration.val gt 1) and (p47_slopeReferenceIteration_iter(iteration,ttot,ext_regi) - p47_slopeReferenceIteration_iter(iteration-1,ttot,ext_regi) eq 0), - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"slope_repeatPrev_positiveSlope") = YES; - p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = p47_factorRescaleSlope_iter(iteration-1,ttot,ttot2,ext_regi,emiMktExt); -*** else slope is not available, set the rescale factor based on remaining deviation - else - regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_noNonPositiveSlope") = YES; - pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); - ); - ); -*** if we are still using the slope - if(NOT(regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_noNonPositiveSlope")), -*** clamp slopes values to avoid extreme changes (or no change) on a single iteration (avoid corner cases where other parts of the model changes causing undesirable fluctuations on the calculated slope) - if((p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) gt -0.3) OR (p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) lt -5), - p47_clampedRescaleSlope_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt); - ); - p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = max(-5,min(-0.3, p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt))); -*** calculate the tax rescale factor using the above calculated slope - pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = - ( - (pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) - p47_emiMktCurrent_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) - / - (p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) * pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt)) - ) + 1; + p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = p47_slopeAux("slope"); + p47_slopeTrace_iter("fitR2",iteration,ttot,ttot2,ext_regi,emiMktExt) = p47_slopeAux("r2"); + +*** CASE 3 - wrong-sign or poorly-fit slope: unreliable for a Newton step, use squareDev +*** (replaces slope_repeatPrev_positiveSlope and squareDev_noNonPositiveSlope, now keyed off fit quality) + if((p47_slopeAux("slope") ge 0) OR (p47_slopeAux("r2") lt p47_slopeParam("minR2")), + regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_unstableFit") = YES; + pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); + +*** CASE 4 - degenerate: fitted emission response over the window is negligible both vs 2005 emissions +*** AND vs the remaining gap (near-net-zero start). Only for year targets (pm_emiMktRefYear > 0). + elseif((pm_emiMktRefYear(ttot,ttot2,ext_regi,emiMktExt) gt 0) + AND (abs(p47_slopeAux("slope")*(p47_slopeAux("pMax") - p47_slopeAux("pMin"))) lt p47_slopeParam("degenerateThreshold") * pm_emiMktRefYear(ttot,ttot2,ext_regi,emiMktExt)) + AND (abs(p47_slopeAux("slope")*(p47_slopeAux("pMax") - p47_slopeAux("pMin"))) lt p47_slopeParam("degenerateThreshold") * abs(pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) - p47_emiMktCurrent_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)))), + regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_degenerateSlope") = YES; + pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = power(1+pm_emiMktTarget_dev(ttot,ttot2,ext_regi,emiMktExt), 2); + +*** CASE 5 - trustworthy slope: take a Newton step, steepness-capped then trust-region-capped + else + regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"slope") = YES; +*** steepness cap: record the raw slope, then bound |slope| by p47_slopeParam("maxSteep") to avoid an over-tiny step from a noisy very steep fit + if(p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) lt -p47_slopeParam("maxSteep"), + p47_slopeTrace_iter("rawSlope",iteration,ttot,ttot2,ext_regi,emiMktExt) = p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt); ); + p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) = max(-p47_slopeParam("maxSteep"), p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt)); + pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = + ( + (pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) - p47_emiMktCurrent_iter(iteration,ttot,ttot2,ext_regi,emiMktExt)) + / + (p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt) * pm_taxemiMkt_iteration(iteration,ttot2,regi,emiMkt)) + ) + 1; ); ); ); -*** dampen if rescale oscillates - if( (iteration.val > 3) , - if ( ( - ( ( ( p47_factorRescaleemiMktCO2Tax_iter(iteration-1,ttot,ttot2,ext_regi,emiMktExt) - 1 ) - * ( pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) - 1 ) ) < 0 - ) AND !! test if rescale changed from >1 to <1 or vice versa between iteration -1 and current iteration - ( ( ( p47_factorRescaleemiMktCO2Tax_iter(iteration-1,ttot,ttot2,ext_regi,emiMktExt) - 1 ) - * ( p47_factorRescaleemiMktCO2Tax_iter(iteration-2,ttot,ttot2,ext_regi,emiMktExt) -1 ) ) < 0 - ) !! test if rescale changed from >1 to <1 or vice versa between iteration -2 and iteration -1 - ) , - p47_dampedFactorRescaleemiMktCO2Tax_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt); + +*** FALLBACK STEP BOUND: power(1+dev,2) assumes a small relative deviation, so for a near-zero year +*** target or a cumulative budget (|dev| of O(1..25)) it always saturates the trust-region cap - i.e. +*** the fallback takes the LARGEST allowed step exactly when the slope fit became unreliable. Bound it +*** by what the algorithm has recently been able to do: at most fallbackStepFactor x the MEAN +*** |rescale-1| over the window, never below fallbackStepFloor. The MEAN, not the maximum: a maximum +*** ratchets, because one large step licenses the next (one target walked 24k -> 157k US$/tCO2 in five +*** fallback steps that way). squareDev_firstIteration is exempt - it is the cold-start ramp, with no +*** history. fallbackStepFactor = 0 disables the bound. Tutorial section 2.6. + p47_slopeAux("isFallback") = 0; + p47_slopeAux("isFallback")$regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_lowPriceSpread") = 1; + p47_slopeAux("isFallback")$regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_unstableFit") = 1; + p47_slopeAux("isFallback")$regiEmiMktRescaleType(iteration,ttot,ttot2,ext_regi,emiMktExt,"squareDev_degenerateSlope") = 1; + if((p47_slopeAux("isFallback") eq 1) AND (p47_slopeParam("fallbackStepFactor") gt 0), + p47_slopeAux("recentStep") = 0; + p47_slopeAux("nStep") = 0; +*** only iterations in which this target was actually rescaled carry a meaningful step (an inactive target, +*** i.e. another period being solved, has a recorded factor of 0 and must not count as a "full" step) + loop(iteration2$((iteration2.val lt iteration.val) and (iteration2.val ge iteration.val - p47_slopeParam("maxWindow")) + and (p47_factorRescaleemiMktCO2Tax_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt) gt 0)), + p47_slopeAux("recentStep") = p47_slopeAux("recentStep") + abs(p47_factorRescaleemiMktCO2Tax_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt) - 1); + p47_slopeAux("nStep") = p47_slopeAux("nStep") + 1; + ); + if(p47_slopeAux("nStep") gt 0, + p47_slopeAux("stepBound") = max(p47_slopeParam("fallbackStepFloor"), p47_slopeParam("fallbackStepFactor") * p47_slopeAux("recentStep") / p47_slopeAux("nStep")); + pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = + max(1 - p47_slopeAux("stepBound"), min(1 + p47_slopeAux("stepBound"), pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt))); + ); + ); + +*** trust-region cap applied to EVERY rescale path (Newton and all squareDev fallbacks). power(1+dev,2) +*** is unbounded, so without this a far-from-target step explodes the carbon price (~5e9 US$/tCO2 was +*** observed on an infeasible budget, which destabilised the whole run). Tutorial section 2.7. + pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = + max(p47_slopeParam("rescaleCapLo"), min(p47_slopeParam("rescaleCapHi"), pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt))); + +*** POST-RE-OPEN STEP CAP: a re-opened target is by construction already close (|dev| <= reopenMaxDev), +*** so the correcting step must be a nudge, not a fresh search. Caps the FIRST step after a re-open +*** whichever path produced it. reopenStepCap = 0 disables it. Tutorial section 2.7. + if((p47_slopeParam("reopenStepCap") gt 0) AND (p47_targetState("reopenIter",ttot,ttot2,ext_regi,emiMktExt) eq iteration.val), + pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = + max(1 - p47_slopeParam("reopenStepCap"), min(1 + p47_slopeParam("reopenStepCap"), pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt))); + ); + +*** progressive oscillation dampening: count how many times the rescale reversed direction (crossed 1) +*** over the window and shrink the step geometrically with that count, floored at dampFloor, so a +*** persistently over-shooting target settles toward the best achievable - where the noise-floor stop +*** can then accept it. Fewer than dampFlipMin reversals is left undamped. Dampening only SHRINKS a +*** step, never flips its sign or freezes a target, so a mis-detection at worst slows convergence. + if(iteration.val > 3, +*** reversal between the previous and the current (about-to-be-applied) rescale + p47_slopeAux("nFlip") = + 1$(((p47_factorRescaleemiMktCO2Tax_iter(iteration-1,ttot,ttot2,ext_regi,emiMktExt) - 1) * (pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) - 1)) lt 0); +*** plus reversals among the already-recorded rescales in the window + loop(iteration2$((iteration2.val lt iteration.val) and (iteration2.val gt iteration.val - p47_slopeParam("maxWindow")) and (iteration2.val ge 2)), + p47_slopeAux("nFlip") = p47_slopeAux("nFlip") + + 1$(((p47_factorRescaleemiMktCO2Tax_iter(iteration2,ttot,ttot2,ext_regi,emiMktExt) - 1) * (p47_factorRescaleemiMktCO2Tax_iter(iteration2-1,ttot,ttot2,ext_regi,emiMktExt) - 1)) lt 0); + ); + if(p47_slopeAux("nFlip") ge p47_slopeParam("dampFlipMin"), + p47_slopeTrace_iter("preDamp",iteration,ttot,ttot2,ext_regi,emiMktExt) = pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt); pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) = - 1 + ( ( pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) - 1 ) / 2 ) - ; !! this brings the value closer to one. The formulation works reasonably well within the range of 0.5..2 - put_utility "msg" / "Reducing pm_factorRescaleemiMktCO2Tax due to oscillation in the previous 3 iterations: "; - put_utility "msg" / ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl " " pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt):10:3; + 1 + (pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt) - 1) + * max(p47_slopeParam("dampFloor"), power(p47_slopeParam("dampProgFactor"), p47_slopeAux("nFlip") - p47_slopeParam("dampFlipMin") + 1)); + put_utility "msg" / "Progressive oscillation dampening (" p47_slopeAux("nFlip"):2:0 " reversals): "; + put_utility "msg" / ttot.tl " " ttot2.tl " " ext_regi.tl " " emiMktExt.tl " " pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt):10:3; ); - ); - ); + ); + ); ); ); ); @@ -371,10 +897,12 @@ loop(ext_regi$regiEmiMktTarget(ext_regi), p47_factorRescaleSlope_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = p47_factorRescaleSlope(ttot,ttot2,ext_regi,emiMktExt); p47_factorRescaleemiMktCO2Tax_iter(iteration,ttot,ttot2,ext_regi,emiMktExt) = pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt); !!save rescale factor across iterations for debugging of target convergence issues +*** ----- Carbon Price Trajectory & Inter-Period Ramping ----- + *** updating tax values under current targets loop(ext_regi$regiEmiMktTarget(ext_regi), *** solving targets sequentially, i.e. only apply target convergence algorithm if previous yearly targets were already achieved - if(not(pm_allTargetsConverged(ext_regi) eq 1), !!no rescale need if all targets already converged + if(not(p47_allTargetsFrozen(ext_regi) eq 1), !!no rescale need if all targets already frozen (see TWO SIGNALS above: this is the module-internal gate, NOT the module-80 run-end signal) *** updating the emiMkt co2 tax for the first non converged yearly target loop((ttot,ttot2,emiMktExt,target_type_47,emi_type_47)$(pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) AND (ttot2.val eq p47_currentConvergencePeriod(ext_regi))), loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), @@ -383,7 +911,13 @@ loop(ext_regi$regiEmiMktTarget(ext_regi), if((iteration.val eq 1) and (pm_taxemiMkt(ttot2,regi,emiMkt) eq 0), !!intialize price for first iteration if it is missing pm_taxemiMkt(ttot2,regi,emiMkt) = 1* sm_DptCO2_2_TDpGtC; else !!update price using rescaling factor (Minimal aceptable price = 1 dollar/tCO2) - pm_taxemiMkt(ttot2,regi,emiMkt) = max(1* sm_DptCO2_2_TDpGtC, pm_taxemiMkt(ttot2,regi,emiMkt) * pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt)); + pm_taxemiMkt(ttot2,regi,emiMkt) = max(1* sm_DptCO2_2_TDpGtC, pm_taxemiMkt(ttot2,regi,emiMkt) * pm_factorRescaleemiMktCO2Tax(ttot,ttot2,ext_regi,emiMktExt)); + ); +*** optional absolute carbon-price ceiling: an unreachable target would otherwise ramp without bound at +*** rescaleCapHi per iteration. Capping the terminal-year price makes it freeze via the infeasible-target +*** stop (whose recorded rescale factor is unaffected) at a sane price. In US$/tCO2. Tutorial 8.6. + if(p47_slopeParam("maxPrice") gt 0, + pm_taxemiMkt(ttot2,regi,emiMkt) = min(pm_taxemiMkt(ttot2,regi,emiMkt), p47_slopeParam("maxPrice") * sm_DptCO2_2_TDpGtC); ); *** linear price between first free year and current target terminal year loop(ttot3, @@ -392,11 +926,21 @@ loop(ext_regi$regiEmiMktTarget(ext_regi), s47_prefreeYear = ttot3.val; ); if(not(ttot2.val eq p47_firstTargetYear(ext_regi)), !! delay price change by cm_emiMktTargetDelay years for later targets - s47_firstFreeYear = max(s47_firstFreeYear,ttot.val+cm_emiMktTargetDelay) + s47_firstFreeYear = max(s47_firstFreeYear,ttot.val+cm_emiMktTargetDelay); +*** Anchor price ramp at closest earlier target year (starting strictly after it) to prevent overwriting converged prices. + s47_prevTargetYear = 0; + loop(ttot3$(regiANDperiodEmiMktTarget_47(ttot3,ext_regi) AND (ttot3.val lt ttot2.val)), + s47_prevTargetYear = max(s47_prevTargetYear, ttot3.val); + ); + if(s47_prevTargetYear gt 0, + s47_prefreeYear = s47_prevTargetYear; + s47_firstFreeYear = max(s47_firstFreeYear, s47_prevTargetYear + 1); !! never touch the earlier target year itself + ); ); loop(ttot3$(ttot3.val eq s47_prefreeYear), !! ttot3 = beginning of slope; ttot2 = end of slope - pm_taxemiMkt(t,regi,emiMkt)$((t.val ge s47_firstFreeYear) AND (t.val lt ttot2.val)) = pm_taxemiMkt(ttot3,regi,emiMkt) + ((pm_taxemiMkt(ttot2,regi,emiMkt) - pm_taxemiMkt(ttot3,regi,emiMkt))/(ttot2.val-ttot3.val))*(t.val-ttot3.val); + pm_taxemiMkt(t,regi,emiMkt)$((t.val ge s47_firstFreeYear) AND (t.val lt ttot2.val)) = pm_taxemiMkt(ttot3,regi,emiMkt) + ((pm_taxemiMkt(ttot2,regi,emiMkt) - pm_taxemiMkt(ttot3,regi,emiMkt))/(ttot2.val-ttot3.val))*(t.val-ttot3.val); ); +*** END OF TARGET PRICE RAMP (marker used by convergence_tests/run_tests.sh to extract this block) ); ); ); @@ -412,7 +956,7 @@ loop((ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47)$(pm_emiMktTarget *** updating periods after current target loop(ext_regi$regiEmiMktTarget(ext_regi), - if(not(pm_allTargetsConverged(ext_regi) eq 1), !!no rescale need if all targets already converged + if(not(p47_allTargetsFrozen(ext_regi) eq 1), !!no rescale need if all targets already frozen (see TWO SIGNALS above: this is the module-internal gate, NOT the module-80 run-end signal) loop((ttot,ttot2,emiMktExt,target_type_47,emi_type_47)$(pm_emiMktTarget(ttot,ttot2,ext_regi,emiMktExt,target_type_47,emi_type_47) AND (ttot2.val eq p47_currentConvergencePeriod(ext_regi))), loop(emiMkt$emiMktGroup(emiMktExt,emiMkt), loop(regi$regiEmiMktTarget2regi_47(ext_regi,regi), @@ -427,7 +971,12 @@ loop(ext_regi$regiEmiMktTarget(ext_regi), pm_taxemiMkt(t,regi,emiMkt)$((t.val gt ttot2.val) AND (t.val lt ttot3.val)) = pm_taxemiMkt(ttot2,regi,emiMkt) + ((pm_taxemiMkt(ttot3,regi,emiMkt) - pm_taxemiMkt(ttot2,regi,emiMkt))/(ttot3.val-ttot2.val))*(t.val-ttot2.val); !! price in between current target year and next target year *** else if next target was never executed by the algorithm yet, initialize next target value as weighted average convergence price between current target terminal year (ttot2.val) and next target year (p47_nextConvergencePeriod) else - p47_averagetaxemiMkt(t,regi) = +*** default: simple average across markets (guards against division by zero when total +*** net emissions are negligibly small, zero, or net-negative with cancellation) + p47_averagetaxemiMkt(t,regi) = + (pm_taxemiMkt(t,regi,"ETS") + pm_taxemiMkt(t,regi,"ES") + pm_taxemiMkt(t,regi,"other")) / 3; +*** override with emission-weighted average when total net emissions are large enough + p47_averagetaxemiMkt(t,regi)$(abs(p47_emiTargetMkt(t,regi,"ETS",emi_type_47) + p47_emiTargetMkt(t,regi,"ESR",emi_type_47) + p47_emiTargetMkt(t,regi,"other",emi_type_47)) gt 1e-6) = (pm_taxemiMkt(t,regi,"ETS")*p47_emiTargetMkt(t,regi,"ETS",emi_type_47) + pm_taxemiMkt(t,regi,"ES")*p47_emiTargetMkt(t,regi,"ESR",emi_type_47) + pm_taxemiMkt(t,regi,"other")*p47_emiTargetMkt(t,regi,"other",emi_type_47)) / (p47_emiTargetMkt(t,regi,"ETS",emi_type_47) + p47_emiTargetMkt(t,regi,"ESR",emi_type_47) + p47_emiTargetMkt(t,regi,"other",emi_type_47)); @@ -443,6 +992,8 @@ loop(ext_regi$regiEmiMktTarget(ext_regi), ); ); +*** ----- Aggregated Tax Diagnostic Parameters ----- + *** output helper parameter p47_taxemiMkt_AggEmi(ttot,regi)$(sum(emiMkt, vm_co2eqMkt.l(ttot,regi,emiMkt))) = (sum(emiMkt, pm_taxemiMkt(ttot,regi,emiMkt) * vm_co2eqMkt.l(ttot,regi,emiMkt))) / (sum(emiMkt, vm_co2eqMkt.l(ttot,regi,emiMkt))); p47_taxCO2eq_AggEmi(ttot,regi) = pm_taxCO2eqSum(ttot,regi); diff --git a/modules/47_regipol/regiCarbonPrice/sets.gms b/modules/47_regipol/regiCarbonPrice/sets.gms index 631ce0d7fa..b0920300b6 100644 --- a/modules/47_regipol/regiCarbonPrice/sets.gms +++ b/modules/47_regipol/regiCarbonPrice/sets.gms @@ -21,13 +21,87 @@ $ifThen.emiMkt not "%cm_emiMktTarget%" == "off" regiEmiMktTarget(ext_regi) "regions with emiMkt targets" / / regiANDperiodEmiMktTarget_47(ttot,ext_regi) "regions and periods with emiMkt targets" / / regiEmiMktTarget2regi_47(ext_regi,all_regi) "regions controlled by emiMkt market set to ext_regi" / / - rescaleType "emi mkt carbon price scaling factor calculation methods" / - "squareDev_firstIteration", "squareDev_perfectMatch", "squareDev_smallChange", "squareDev_noChange", - "slope_refIteration", "slope_firstIteration", "slope_repeatPrev", "slope_repeatPrev_positiveSlope", - "squareDev_noSlope", "squareDev_noNonPositiveSlope"/ + rescaleType "emi mkt carbon price scaling factor calculation methods" / + squareDev_firstIteration, squareDev_lowPriceSpread, squareDev_unstableFit, squareDev_degenerateSlope, slope / regiEmiMktRescaleType(iteration,ttot,ttot,ext_regi,emiMktExt,rescaleType) "saving scaling type used in iteration" / / - convergenceType "emiMkt target non convergence reason" / "lowerThanTolerance", "smallPrice" / + convergenceType "emiMkt target convergence reason" / + lowerThanTolerance, smallPrice, bestAchievable, unmetAtCap, unmetFrozen / regiEmiMktconvergenceType(iteration,ttot,ttot,ext_regi,emiMktExt,convergenceType) "saving convergence type in iteration" / / + slopeTerm "scratch accumulators and results of the convergence algorithm: least-squares slope terms plus the per-branch window counters" / + n, sumP, sumE, sumP2, sumE2, sumPE, pMin, pMax, denom, num, varE, slope, r2, inBand, priceOk, devMax, devMin, settled, nWin, outBand, nSteer, skipSteer, cntSteer, atCap, emiMin, kneeIter, devAbsMax, devAbsMin, bestWinIter, nFlip, + isFallback, recentStep, nStep, stepBound, rollIter, wantRoll, prevTgtYr, holdOk, divOut, divMax, divBest, divRoll / + slopeParam "configuration constants of the convergence algorithm, documented in tutorials/19_RegionalEmissionTargets.md section 3.1" / +*** slope fit and step size + maxWindow "rolling window length, counted in iterations this target actually STEERED [#]" + minPriceSpread "minimum price spread across the window (fraction of current price) to trust the slope [fraction]" + minR2 "minimum R-squared of the fit to use it for a Newton step [fraction]" + maxSteep "cap on the absolute slope magnitude [#]" + degenerateThreshold "minimum fitted emission change over window as fraction of BOTH 2005 emissions and remaining gap [fraction]" + rescaleCapLo "trust region, lower bound [#]" + rescaleCapHi "trust region, upper bound (2 = price may at most double per iteration) [#]" +*** convergence bands + enterFrac "AIM band: converge at |dev| <= enterFrac * tolerance (deliberately INSIDE tolerance) [fraction]" + aimMaxTries "attempts at AIM band before ACCEPTing anywhere inside tolerance [#]" + exitFrac "EXIT band: hold converged target until |dev| exceeds exitFrac * tolerance (hysteresis) [fraction]" + persist "consecutive iterations required for any state change (1 disables persistence) [#]" + priceEps "optional price-settled gate for entering: max |dprice|/price (0 = off) [fraction]" +*** deviation normalisation and price ceiling + budgetDenomFloorFrac "floors budget deviation denominator at this * CUMULATIVE 2005-rate reference budget [fraction]" + maxPrice "absolute carbon-price ceiling [US$2005/tCO2] (0 = off) [US$2005/tCO2]" +*** give-up stops + noiseFloorMaxDev "noise-floor stop only fires within this |dev| (0 disables tier-3 stops) [fraction]" + noiseFloorBandWidth "absolute floor for narrow-band width [fraction]" + noiseFloorBandWidthRel "tolerance-relative band width; effective width is max(absolute, rel * tolerance) [#]" + noiseFloorRollback "freeze at WINDOW-LOCAL BEST rather than firing iteration (< 0.5 = off) [0 or 1]" + parkedStop "give up on target held converged by hysteresis outside tolerance but inside exit band (< 0.5 = off) [0 or 1]" + infeasEmiTol "knee tolerance for infeasible stop rollback: lowest price with equivalent emissions (0 = off) [fraction]" + infeasStallFrac "infeasible and divergence stops require |dev| now >= this * max |dev| over window (stalled) [fraction]" + divergeFactor "divergence threshold multiplier on max(best |dev| ever reached, tolerance) (< 1 disables) [#]" + divergeMinBest "ARMING band multiplier on tolerance: divergence test only live once reached while steering (< 1e-3 disables) [#]" + divergeBrakeMax "reversible brakes allowed before divergence stop freezes (< 1 = freeze on first detection) [#]" +*** re-open parameters + reopenMaxDev "charge re-open only while |dev| <= this; further out is uncharged release (0 disables re-opening) [fraction]" + reopenMax "charged re-opens per target over whole run [count]" + reopenStepCap "cap on FIRST step after re-open (0 = off) [fraction]" + reopenRefresh "consecutive iterations FROZEN AND MET AND not given up that earn budget back (< 1 = off) [#]" +*** step bound for squareDev fallbacks + fallbackStepFactor "fallback step bound as multiple of MEAN |rescale-1| over window (0 = off) [#]" + fallbackStepFloor "minimum fallback step bound (+-this) [#]" +*** price rollback on freeze + rollbackBestFrac "prefer best-so-far price over stop candidate when at least this factor better (0 = off) [fraction]" + rollbackMaxAge "best-so-far candidate must lie within this many iterations (0 = no age limit) [#]" + rollbackVerify "undo rollback if next iteration is worse, then lock out future rollbacks (< 1 = off) [0 or 1]" +*** progressive oscillation dampening + dampFlipMin "reversals within window before dampening starts [#]" + dampProgFactor "step multiplier per extra reversal: dampProgFactor^(nFlip - dampFlipMin + 1) [fraction]" + dampFloor "lower bound on dampening multiplier [fraction]" + / + slopeTrace "per-iteration diagnostics of the convergence algorithm, saved for debugging" / +*** giveUpBy names WHICH give-up branch froze a target, because the end state alone cannot say: 1 = noise-floor +*** stop, 2 = infeasible-target stop, 3 = divergence stop, 4 = re-open budget spent, 5 = parked stop. Without it, +*** "did this knob's mechanism actually fire?" has to be guessed from reopenCount/divBrakeCount, and three +*** analysis rounds guessed it wrong (tutorial section 3.2 rule 5; CONVERGENCE_ALGORITHM.md 3.18). + rawSlope, fitR2, preDamp, rollIter, rollUndo, divBrake, parked, giveUpBy / + targetState "persistent per-target state of the convergence state machine, documented in tutorials/19_RegionalEmissionTargets.md section 3.1" / +*** budgets and counters + reopenCount "charged re-opens of this market target; run-wide budget (reopenMax), cleared only by reopenRefresh consecutive settled iterations [count]" + "settledCount" "consecutive iterations frozen AND met AND not given up; at reopenRefresh the re-open budget is earned back, any other state resets it [count]" + refreshCount "times this market target actually got a SPENT re-open budget back; a refresh with the budget already at 0 is not counted. Diagnostic only [count]" + releaseCount "times this market target was UN-FROZEN without a re-open being charged (|dev| beyond reopenMaxDev). Diagnostic only [count]" + divBrakeCount "divergence brakes taken on this market target; run-wide budget (divergeBrakeMax), after which the stop freezes instead [count]" + aimTries "attempts spent trying to close the gap between the raw tolerance and the tighter AIM band (enterFrac) [count]" +*** flags + giveUp "1 once a GIVE-UP branch (noise floor, infeasible target, divergence stop, parked stop, re-open budget spent) deliberately froze this market target [0 or 1]" + divArmed "1 once this market target reached |dev| <= divergeMinBest * tolerance while steering its own price; the divergence stop fires only on an armed target [0 or 1]" + rolledBack "rollback state of this market target: 0 = none yet, 1 = rolled back once, 2 = rolled back and UNDONE (never roll again) [0, 1 or 2]" +*** iteration marks + reopenIter "iteration in which this market target was last re-opened or braked; that iteration's step is capped at reopenStepCap [#]" + bestDevIter "iteration that produced bestDevAbs; candidate for the freeze rollback, subject to rollbackMaxAge (0 = none yet) [#]" + rollFromIter "iteration at which the price was rolled back; its stored price path is the pre-rollback state the undo restores (0 = none) [#]" +*** remembered values + bestDevAbs "smallest |target deviation| this market target has reached so far in the run [fraction]" + rollFromDev "|target deviation| at the iteration the rollback fired; the rollback is undone if the next iteration is worse than this [fraction]" + / $ENDIF.emiMkt *** Implicit tax/subsidy necessary to achieve quantity target for primary, secondary, final energy and/or CCS and/or OAE diff --git a/tutorials/19_RegionalEmissionTargets.md b/tutorials/19_RegionalEmissionTargets.md new file mode 100644 index 0000000000..ae2b3ef28f --- /dev/null +++ b/tutorials/19_RegionalEmissionTargets.md @@ -0,0 +1,456 @@ +# Regional emission targets (module `47_regipol`, realization `regiCarbonPrice`) + +This tutorial explains how to make REMIND hit a **regional emission target** (e.g. *"the EU must be at 2.2 GtCO2eq in 2030"*, *"Germany must be net zero in 2045"*) and documents the convergence search algorithm that computes the required carbon prices. + +The code lives in `modules/47_regipol/regiCarbonPrice/`. + +--- + +## Table of contents + +- [1. User Guide](#1-user-guide) + - [1.1 Quickstart: Using Regional Targets](#11-quickstart-using-regional-targets) + - [1.2 Defining a Target: Full Syntax & Metrics](#12-defining-a-target-full-syntax--metrics) + - [1.3 Setting Target Tolerances](#13-setting-target-tolerances) + - [1.4 Verifying Run Outcomes & Reading Results](#14-verifying-run-outcomes--reading-results) + - [1.5 Modeller Caveats & Rules of Thumb](#15-modeller-caveats--rules-of-thumb) +- [2. Algorithm Mechanics](#2-algorithm-mechanics) + - [2.1 Conceptual Overview & Search Loop](#21-conceptual-overview--search-loop) + - [2.2 Carbon Price Path & Ramp Anchoring](#22-carbon-price-path--ramp-anchoring) + - [2.3 Deviation Normalization (Year vs. Budget)](#23-deviation-normalization-year-vs-budget) + - [2.4 Convergence State Machine & Evaluation Bands](#24-convergence-state-machine--evaluation-bands) + - [2.5 Re-Open Budget, Uncharged Release & Settlement Refresh](#25-re-open-budget-uncharged-release--settlement-refresh) + - [2.6 Least-Squares Price Step Fitting & Fallbacks](#26-least-squares-price-step-fitting--fallbacks) + - [2.7 Trust Region, Post-Reopen Cap & Progressive Damping](#27-trust-region-post-reopen-cap--progressive-damping) + - [2.8 Giving Up: The Five Give-Up Branches](#28-giving-up-the-five-give-up-branches) + - [2.9 Price Rollback & Verify-and-Undo](#29-price-rollback--verify-and-undo) + - [2.10 Outcome Classifications & Precedence](#210-outcome-classifications--precedence) +- [3. Developing, Diagnostics & Tuning](#3-developing-diagnostics--tuning) + - [3.1 Diagnostic GDX Containers & Log Signals](#31-diagnostic-gdx-containers--log-signals) + - [3.2 Settled Design Rules & Tests](#32-settled-design-rules--tests) +- [4. The Test Scenario Suite](#4-the-test-scenario-suite) + - [4.1 How the suite is built](#41-how-the-suite-is-built) + - [4.2 Coverage scenarios](#42-coverage-scenarios) + - [4.3 A/B pairs](#43-ab-pairs) + - [4.4 Reference runs](#44-reference-runs) + +--- + +## 1. User Guide + +### 1.1 Quickstart: Using Regional Targets + +REMIND does not know in advance which carbon price makes a region emit a specific target amount. You specify **what** the emissions should be, and REMIND **searches for the price**: after every Nash iteration, the model measures how far emissions are from the target, nudges the carbon price, and solves again. When emissions land inside the specified tolerance and remain there, the search freezes and the run can complete. + +To enforce regional targets, set two switches in your `scenario_config*.csv` (or `main.gms`): + +```gams +regipol = regiCarbonPrice +cm_emiMktTarget = 2020.2030.EU27_regi.all.year.netGHG_noLULUCF_noBunkers 3.16 +``` + +**Syntax breakdown:** +* `2020`: Start year from which the carbon price path may be modified. +* `2030`: Target year (the period the emission limit applies to). +* `EU27_regi`: Region or region group (`DEU`, `EU27_regi`, `GLO`). +* `all`: Emission market (`all`, `ETS`, `ESR`, `other`). +* `year`: Target type (`year` for annual rate, `budget` for cumulative integration). +* `netGHG_noLULUCF_noBunkers`: Emission metric. +* `3.16`: Target value in $\text{GtCO}_2\text{eq}$ per year (or cumulative $\text{GtCO}_2\text{eq}$ for budget targets). + +In words: *"Between 2020 and 2030, move the EU27 carbon price until EU27 net greenhouse gas emissions excluding land use and international transport are 3.16 GtCO2eq in 2030."* + +#### Setting a Budget Target +```gams +cm_emiMktTarget = 2020.2050.EU27_regi.all.budget.netGHG_noBunkers 72 +``` +*"The EU27 may emit a total of 72 GtCO2eq cumulatively between 2020 and 2050."* + +#### Setting Multiple Targets +Separate target definitions with commas. Targets for the same region are solved **sequentially** (earliest target year first): +```gams +cm_emiMktTarget = 2020.2030.EU27_regi.all.year.netGHG_LULUCFGrassi_intraRegBunker 2.221, + 2035.2050.EU27_regi.all.year.netGHG_LULUCFGrassi 0.001, + 2020.2045.DEU.all.year.netGHG_LULUCFGrassi 0.001 +``` +Preset net-zero targets for explicit REMIND countries can be loaded using `cm_implicitQttyTargetType = scenario` and `cm_emiMktTarget = nzero`. + +--- + +### 1.2 Defining a Target: Full Syntax & Metrics + +Full target definition format: +```gams +cm_emiMktTarget = ..... +``` + +#### `` and `` +* `targetYear`: The period the emission target applies to. +* `startYear`: The earliest period whose carbon price the algorithm is allowed to modify. For `budget` targets, `[startYear..targetYear]` also defines the cumulative trapezoidal integration window. + +#### `` & Precedence Rules +Any model region (`DEU`) or region group (`EU27_regi`, `GLO`). +* **Precedence:** If a region is covered by two targets at different aggregation levels (e.g. `DEU` and `EU27_regi`), the **more disaggregated target takes precedence** and controls the regional carbon price (resolved in `datainput.gms` into `regiEmiMktTarget2regi_47`). + +#### `` + +| Value | Covers | +|---|---| +| `all` | ETS + ESR + other (the whole regional economy) | +| `ETS` | Emission Trading System sectors only | +| `ESR` | Effort Sharing Regulation sectors only (internally `ES`) | +| `other` | Everything not in ETS or ESR | + +Prices in `other` are set equal to the `ESR` price whenever an `ESR` or `all` target exists. + +#### `` (`emi_type_47`) +* `netCO2`, `netGHG`: All sectors including LULUCF and international bunkers. +* `*_noLULUCF`, `*_noBunkers`, `*_noLULUCF_noBunkers`: Standard sectoral exclusions. +* `*_LULUCFGrassi`: Land-use emissions shifted via `pm_emiLULUCF_GrassiShift` to match UNFCCC national accounting conventions rather than MAgPIE conventions. +* `*_intraRegBunker`: Retains 35% of bunker emissions (2000–2020 intra-EU average). +* `grossEnCO2_noBunkers`: Energy $\text{CO}_2$ before BECCS/DAC. + +--- + +### 1.3 Setting Target Tolerances + +Set target tolerance via `cm_emiMktTarget_tolerance` (fraction, default `GLO 0.01` = 1%): +```gams +cm_emiMktTarget_tolerance = GLO 0.01, DEU 0.004 +``` + +> [!WARNING]
+> Baseline Nash iterations wobble by roughly $\pm 1\%$. Setting tolerances too low can cause the algorithm to hit noise floor artifacts. + +#### Overriding Algorithm Constants (`cm_slopeParam`) + +Every tuning constant in §2 has a shipped default and can be overwritten per scenario, without touching code: + +```gams +cm_slopeParam = 'maxPrice 10000, reopenMax 5, enterFrac 0.8' +``` + +Unlisted constants keep their defaults. The full list with element texts is in `regiCarbonPrice/sets.gms`; the defaults are in `datainput.gms`; and whatever a run actually used is baked into `p47_slopeParam` in its `fulldata.gdx` — **always check there rather than assuming.** + +> [!WARNING] +> **`0` cannot be written from config** — GAMS drops zero records. To switch a knob off use `1e-9`, which every disableable knob floors explicitly (§3.2 rule 4). Do not invent an "off" value for a knob that has no floor: on a threshold it means *fire on everything*, and on a budget it means *one*. + +--- + +### 1.4 Verifying Run Outcomes & Reading Results + +Three complementary ways to check run results: + +1. **Log Warnings (Fastest):** If a target cannot be reached within tolerance, REMIND prints a warning on every iteration: + ``` + ### WARNING: region EU27_regi is CONVERGED but its emission targets are NOT MET. + ### unmet: 2020 2030 EU27_regi all dev 0.02341 tolerance 0.01000 + ``` + *No warning means every target was met.* +2. **GDX Parameters:** Check `pm_emiMktTarget_dev_iter` (deviation per iteration, `0.01` = 1%) and `regiEmiMktconvergenceType` (outcome labels). +3. **HTML Report:** Run `Rscript output.R` and then select `nashConvergenceReport` to render a html report in your output folder with all information about the run convergence. + +--- + +### 1.5 Modeller Caveats & Rules of Thumb + +* Regional targets **override** the carbon price for target regions. Non-target regions retain global climate policy prices. +* Emission targets require `regipol = regiCarbonPrice`. +* For impossible targets (e.g. net-zero in 2030 for a coal-heavy region), REMIND will not crash. The algorithm will push the price up, notice that emissions stopped responding, freeze the price, and **report the residual**. Always read target deviations before interpreting scenario results. +* Scenario CSV configuration cells are limited to **254 characters** for GAMS switches (`cm_emiMktTarget` and `cm_slopeParam`). + +--- + +## 2. Algorithm Mechanics + +### 2.1 Conceptual Overview & Search Loop + +![One target, one iteration](figures/git-19-loop.png) + +> [!NOTE] +> **Flowchart Legend & Visual Conventions** +> The decision flowcharts throughout this document use a consistent design system to categorize states, shapes, and target termination outcomes: +> +> * **Color Coding (Functional Categories):** +> * **Purple** (`model`): External model solving iteration. +> * **White / Slate** (`measure`): Measurement, fit, and quantitative evaluations. +> * **Orange** (`decide`): Logic gates and conditional branching questions. +> * **Cyan** (`act`): Steering adjustments, price path updates, and rollbacks. +> * **Green** (`done`): Target successfully met or converged (carries no residual). +> * **Red** (`giveup`): Target search aborted / given up (carries a residual). +> +> * **Border Styles & Shapes (State Properties):** +> * **Rounded Rectangles:** Standard process steps, measurements, and actions. +> * **Sharp-Cornered Rectangles:** Decision branch tests. +> * **Dashed Borders:** Reversible or temporary interventions (e.g. the *Divergence Brake*). +> * **Double-Borders:** the target has stopped steering its own price. **Green double-borders are not necessarily permanent** — a frozen, met target re-opens if emissions later drift out of band (§2.5). **Red ones are:** a give-up branch has abandoned the target and only the residual is reported. + +On each Nash iteration, for each target: +1. **Measure:** Calculate relative distance from target (**deviation** $\text{dev}$). +2. **Learn Response:** Fit a least-squares line through recent (price, emission) pairs over the **last 8 steered iterations**. +3. **Compute Step:** Divide remaining gap by slope to get Newton step, and clamp step size within trust regions. +4. **Draw Price Ramp:** Apply new price to target year and linearly interpolate intermediate years. +5. **Check Convergence:** Freeze target price when deviation remains in-band for 2 consecutive iterations (`persist = 2`). + +The run finishes when **all regional targets are frozen and either met or formally given up**. + +--- + +### 2.2 Carbon Price Path & Ramp Anchoring + +Controlled variable: `pm_taxemiMkt(ttot,regi,emiMkt)` in $\text{T}\$/\text{GtC}$ (multiply by $272$ for $\text{US}\$/\text{tCO}_2$). + +* **Price Trajectory:** Target-year price is updated via multiplicative factor `pm_factorRescaleemiMktCO2Tax` (floored at $1\text{ US}\$/\text{tCO}_2$, capped by `maxPrice` if active). Ramps are linearly interpolated from the first free year ($\ge \text{startYear}, \ge \text{cm\_startyear}, \ge 2020$). +* **Post-Target Path:** After the final target year, prices increase by `cm_postTargetIncrease` $\text{US}\$/\text{tCO}_2$/year. +* **Ramp Anchoring:** Ramps of later target years start strictly after the **closest earlier target year of the same region**. + +> [!NOTE] +> **Design Rationale: Target Ramp Anchoring** +> Without anchoring, a later target's ramp draws from the pre-free year and overwrites earlier converged target years. In back-to-back targets (`2030 -> 2040`), unanchored ramps caused infinite limit cycles where 2030 was continuously wiped, re-ramped, and re-converged until the iteration cap. + +--- + +### 2.3 Deviation Normalization (Year vs. Budget) + +Target deviation `pm_emiMktTarget_dev` is expressed as a fraction (`0.01` = 1%). + +#### Year Targets +$$\text{dev} = \frac{\text{Emissions}_{\text{targetYear}} - \text{Target}}{2005\text{ Emissions}}$$ +*Normalizing by 2005 emissions keeps net-zero targets well-conditioned.* + +#### Budget Targets +$$\text{dev} = \frac{\text{Cumulative Emissions} - \text{Target}}{\max\left(|\text{Target}|,\, \text{budgetDenomFloorFrac} \times \text{refBudget}\right)}$$ +*`refBudget` (`p47_emiMktRefBudget`) is the cumulative emissions the market would produce at its 2005 annual rate over the same period.* + +--- + +### 2.4 Convergence State Machine & Evaluation Bands + +The per-target decision graph is divided into two halves based on whether the target was frozen last iteration: + +![Decision graph 1 of 2: target frozen last iteration](figures/git-19-held.png) + +![Decision graph 2 of 2: target steering its price](figures/git-19-steering.png) + +#### Evaluation Bands + +Evaluation bands are exact multiples of specified user tolerance `cm_emiMktTarget_tolerance`: + +| Band | Switch / Parameter | Default | Function | +|---|---|---|---| +| **AIM** | `enterFrac` | `0.75` | Primary convergence threshold (75% of tolerance). Provides safety margin against Nash noise. | +| **ACCEPT** | `aimMaxTries` | `3` | Attempts inside 100% tolerance before accepting without reaching AIM band. | +| **EXIT** | `exitFrac` | `1.00` | Deviation threshold for breaking hold and re-opening target. | +| **Persistence** | `persist` | `2` | Consecutive iterations required to trigger any state change. | + +The gap between **AIM** (`0.75`) and **EXIT** (`1.00`) creates **hysteresis** to prevent flip-flopping. + +#### Non-Binding Market Floor (`smallPrice`) +If a constituent market price is at the floor ($\le 1.1\text{ US}\$/\text{tCO}_2$) and emissions are below target, the market is legitimately slack and frozen as **met**. + +--- + +### 2.5 Re-Open Budget, Uncharged Release & Settlement Refresh + +When a frozen target breaks its hold ($|\text{dev}| > \text{exitFrac} \times \text{tolerance}$ for `persist` consecutive iterations), three outcomes occur: + +| Outcome | Condition | Action | +|---|---|---| +| **Re-Open (Charged)** | $|\text{dev}| \le \text{reopenMaxDev}$ and budget left | Un-freezes target, caps first step at $\pm 5\%$ (`reopenStepCap`), refreshes AIM attempt budget. Charged to `p47_targetState("reopenCount")`. | +| **Budget Spent** | $|\text{dev}| \le \text{reopenMaxDev}$, budget spent (`reopenCount = 3`) | Freeze target, set `p47_targetState("giveUp") = 1`, roll price back to best state. | +| **Uncharged Release** | $|\text{dev}| > \text{reopenMaxDev}$ | Released without charging `reopenCount` (counted in `releaseCount`). | + +#### Settlement Refresh Rule +`reopenMax` (`3`) is a whole-run budget. If a target remains **frozen AND met AND not given up** for `reopenRefresh` (`24`) consecutive iterations (`settledCount`), its re-open budget resets to zero. + +*Guards:* Refresh is disabled if `giveUp = 1` or `reopenRefresh < 1` (`1e-9` sentinel guard). + +--- + +### 2.6 Least-Squares Price Step Fitting & Fallbacks + +The least-squares fit window holds the last `maxWindow` (`8`) **steered iterations** (skipping frozen iterations). + +#### The Five Fitting Cases (`regiEmiMktRescaleType`) + +| Case | Name | Condition | Step Formula | +|---|---|---|---| +| 1 | `squareDev_firstIteration` | Single data point (cold start) | $(1 + \text{dev})^2$ | +| 2 | `squareDev_lowPriceSpread` | Price spread $< \text{minPriceSpread} \times P$ | $(1 + \text{dev})^2$ | +| 3 | `squareDev_unstableFit` | Wrong slope sign ($\ge 0$) or $R^2 < \text{minR2}$ | $(1 + \text{dev})^2$ | +| 4 | `squareDev_degenerateSlope` | Emission response $< \text{degenerateThreshold}$ | $(1 + \text{dev})^2$ | +| 5 | `slope` | Valid Newton fit | $\text{factor} = \frac{\text{Target} - \text{Current}}{\text{Slope} \times P} + 1$ | + +In case 5, slope magnitude is clamped by `maxSteep` ($5.0$). + +#### Fallback Step Bound +Fallback steps (`squareDev`) are bounded by `fallbackStepFactor` ($2.0$) $\times$ the **mean** $|\text{rescale} - 1|$ applied over the window, floored at `fallbackStepFloor` ($0.05$). Using the mean prevents step-size ratcheting. + +--- + +### 2.7 Trust Region, Post-Reopen Cap & Progressive Damping + +* **Trust Region:** All price steps clamped to $[0.5, 2.0]$ (`rescaleCapLo`, `rescaleCapHi`). +* **Post-Reopen Cap:** The first step following a re-open is capped at $\pm 5\%$ (`reopenStepCap`). +* **Progressive Oscillation Damping:** Reversals across 1 in the fit window scale steps down by $\text{dampProgFactor}^{(\text{nFlip} - \text{dampFlipMin} + 1)}$ (floored at `dampFloor = 0.1`). + +--- + +### 2.8 Giving Up: The Five Give-Up Branches + +There are **five** branches that abandon a target. All of them set `p47_targetState("giveUp") = 1`, freeze the price, and report a residual, and all of them record **which one fired** in `p47_slopeTrace_iter("giveUpBy", ...)` — the code in brackets below. That trace is the only way to tell them apart after the fact: the end state cannot, because `reopenCount` and `divBrakeCount` narrow it to two or three candidates and no further. + +1. **Noise-Floor Stop (Tier 3)** `[giveUpBy=1]`**:** $|\text{dev}| \le \text{noiseFloorMaxDev}$, price settled, and deviation trapped in a band narrower than $\max(\text{noiseFloorBandWidth}, \text{noiseFloorBandWidthRel} \times \text{tolerance})$. Freezes at window-local best (`noiseFloorRollback`) **only if the whole window was outside tolerance** — a target dipping inside it is cycling, not on a floor, and its best iterate is a lucky phase. Gated on $|\text{dev}| > \text{tolerance}$. +2. **Infeasible-Target Stop** `[giveUpBy=2]`**:** $|\text{dev}| > \text{noiseFloorMaxDev}$, rescale pinned at cap ($2.0$), deviation stalled ($\ge \text{infeasStallFrac} = 0.9$ of window max), and deviation one-sided over window. Freezes and rolls price back to earliest "knee". +3. **Divergence Stop** `[giveUpBy=3]`**:** Target armed ($\le 1.0\times$ tolerance while steering), persistent excursion ($> 10\times \max(\text{bestDev}, \text{tolerance})$ for 2 iterations), and not recovering. Takes up to `divergeBrakeMax = 2` step-capped, *reversible* brakes before freezing. +4. **Re-Open Budget Spent** `[giveUpBy=4]`**:** The hold broke with $|\text{dev}| \le \text{reopenMaxDev}$ but `reopenMax = 3` charged re-opens are already gone. Rolls back. Easy to overlook because it is reached through the re-open path rather than through a named "stop". +5. **Parked-Target Stop** `[giveUpBy=5]`**:** Catches targets held frozen by hysteresis while sitting outside raw tolerance but inside the exit band (`parkedStop = 1`). **The only branch reachable from the converged state, and the only one that does NOT roll back** — the price was constant for the whole window, so there is nothing better to restore. Dormant at the shipped `exitFrac = 1.0`, where that band is empty. + +**Not a stop, but it forces one:** the optional price ceiling `maxPrice` (default 0 = off) caps target-year prices in $\text{US}\$/\text{tCO}_2$, pushing unreachable targets into the infeasible stop early. + +**Which label you end up with:** all five assign `bestAchievable`, but the honesty re-label then demotes any frozen, unmet target whose $|\text{dev}|$ exceeds $\text{exitFrac} \times \text{tolerance}$ to `unmetFrozen`. At the shipped `exitFrac = 1.0` branches 1–4 always fire outside that band, so **in practice only the parked stop leaves `bestAchievable` standing** (see the decision graphs above). + +--- + +### 2.9 Price Rollback & Verify-and-Undo + +When a give-up branch fires — **all except the parked stop** (§2.8), which has a constant price and nothing to restore — the algorithm selects the best price path: +* **Candidate:** Run-wide best iteration (`p47_targetState("bestDevAbs")`) vs. stop candidate (knee / window best). +* **Selection:** Prefers run-wide best if meaningfully better ($\le \text{rollbackBestFrac} \times |\text{dev}_{\text{now}}|$ = 2x better) and recent ($\le \text{rollbackMaxAge} = 8$ iterations). +* **Verify & Undo:** Checks deviation on the very next iteration. If deviation worsens, `rollbackVerify` **undoes** the rollback and restores the pre-rollback price path (`p47_targetState("rolledBack") = 2`). Set `rollbackVerify` below 1 to disable (§3.2 rule 4). + +> [!NOTE] +> **The rollback is a bet, and it is often lost.** Restoring a price does *not* restore the deviation that price once produced, because the rest of the Nash system has moved on meanwhile. In the reference run set the verify step undid the majority of rollbacks. This is why the verify exists — and why the parked stop, whose price never moved, does not roll back at all. + +--- + +### 2.10 Outcome Classifications & Precedence + +Verdicts saved in `regiEmiMktconvergenceType`: + +$$\text{unmetAtCap} > \text{unmetFrozen} > \text{bestAchievable} > \text{lowerThanTolerance} \quad (\text{smallPrice is exclusive})$$ + +* **Promotion Pass:** Ensures give-up stops landing inside tolerance are labeled `lowerThanTolerance`. +* **Demotion Pass:** Re-labels frozen targets outside exit band as `unmetFrozen` (or `unmetAtCap` at iteration cap). + +Exactly **one** label per target-iteration: each higher-precedence label clears the ones below it, so the last recorded label is the verdict. + +#### Two Termination Signals + +Freezing every price is *not* enough to end the run — the two signals differ, and the distinction is what stops the Nash loop finishing on a target the algorithm still intends to fix: + +| Parameter | Meaning | Read by | +|---|---|---| +| `p47_allTargetsFrozen` | every target has stopped steering | module 47's own control flow | +| `pm_allTargetsConverged` | frozen **AND** (met **OR** given up) | module 80, to allow the run to end | + +So a target sitting outside its tolerance with `giveUp = 0` **blocks run termination**, even while the hysteresis holds it frozen. That is deliberate: breaking a hold takes `persist` consecutive out-of-band iterations, so a run ending on the first one would stop exactly one iteration before the correction it was about to make. + +> [!NOTE] +> A long run is therefore **not** automatically this module's doing. Read `p47_allTargetsConverged_iter` — the iteration from which module 47 stopped refusing — before attributing run length to the target search. It is routinely tens of iterations before the run actually ends, with the remainder belonging to module 80's own convergence criteria. + +--- + +## 3. Developing, Diagnostics & Tuning + +### 3.1 Diagnostic GDX Containers & Log Signals + +All persistent state and diagnostics are stored in three set-indexed containers in `fulldata.gdx`: + +* `p47_slopeParam(slopeParam)`: Configuration constants (element texts documented in `sets.gms`). +* `p47_targetState(targetState, ttot, ttot2, ext_regi, emiMktExt)`: Persistent state machine variables (`giveUp`, `aimTries`, `reopenCount`, `divArmed`, `rolledBack`, `bestDevAbs`). +* `p47_slopeTrace_iter(slopeTrace, iteration, ttot, ttot2, ext_regi, emiMktExt)`: Iteration events (`rawSlope`, `fitR2`, `preDamp`, `rollIter`, `rollUndo`, `divBrake`, `parked`, `giveUpBy`). `giveUpBy` codes which stop froze the target: 1 noise floor, 2 infeasible, 3 divergence, 4 re-open budget spent, 5 parked. + +```bash +# Dump state machine dictionary and final verdicts +gdxdump fulldata.gdx symb=targetState format=csv +gdxdump fulldata.gdx symb=regiEmiMktconvergenceType format=csv +``` + +#### Key Log Messages + +| Log String | Meaning | +|---|---| +| `Progressive oscillation dampening (N reversals)` | Step scaled down due to overshoots | +| `Best-achievable freeze: price rolled back to iteration N` | Give-up stop executed rollback | +| `Rollback UNDONE (deviation got worse)` | Rollback verification failed; pre-rollback state restored | +| `Divergence BRAKE (n of m)` | Reversible divergence brake applied (step capped at 5%) | +| `Re-open budget refreshed after N settled iterations` | Budget restored after sustained settlement | + +--- + +### 3.2 Settled Design Rules & Tests + +1. **Raw Tolerance Yardstick:** Measure all state transitions against raw tolerance. Never widen tolerance dynamically based on oscillation. +2. **Decoupled Mechanisms:** Never gate two mechanisms on one knob (keep `noiseFloorMaxDev` and `parkedStop` separate). +3. **Scenario Test Suite:** `config/scenario_config_emiMkt_tests.csv` paired with off-arms (`cm_slopeParam = ' 1e-9'`). +4. **Sentinel Floors:** GAMS drops zero records, so config cannot write `0` and the off-idiom is `1e-9`. A knob guarded by a bare `gt 0` therefore reads its own off-switch as **ON**, and one used as a budget with `lt` reads it as **one**. Every disableable knob needs an explicit floor (`ge 1`, or `ge 0.5` for a flag) **plus a harness test that drives the sentinel**. Four knobs have been caught by this: `divergeFactor`, `divergeBrakeMax`, `divergeMinBest`, `rollbackVerify` (`test_divoff6`, `test_brakeoff6`, `test_armoff6`, `test_rollverifyoff6`). +5. **Name the Branch, Don't Infer It:** an outcome records *that* a target was abandoned; only `p47_slopeTrace_iter('giveUpBy', ...)` records *by which* stop. `reopenCount` and `divBrakeCount` narrow it to two or three candidates and no further. Read `giveUpBy` before crediting or blaming a knob. +6. **Verify the Mechanism Fired:** an off-arm proves nothing if the knob was dormant in the base run too. Before reading any A/B (base vs. knob-off comparison), check the mechanism's own trace (`rollIter`, `parked`, `giveUpBy`, `rfr`) is non-zero in the base. + +--- + +## 4. The Test Scenario Suite + +`config/scenario_config_emiMkt_tests.csv` is the regression suite for this module: **33 scenarios** that between them exercise every target shape the syntax allows and every mechanism in §2. It is not a set of policy runs — most of these targets are chosen to be *awkward*, and several are deliberately impossible. + +### 4.1 How the suite is built + +Scenarios come in three kinds. + +* **Coverage scenarios** (`emiMkt_A` … `emiMkt_G`) — one target *shape* each. They answer "does the algorithm handle this configuration at all". +* **Realistic scenarios** (`EU_real_*`, `SSP2-EU21-*`) — plausible policy trajectories, to check the module behaves under conditions a modeller would actually create. +* **A/B pairs** — a base run and an **off-arm** that differs in exactly one knob, written `cm_slopeParam = ' 1e-9'`. The off-arm exists to show what the mechanism is worth. **An off-arm may never beat its base**; if it does, the mechanism is not earning its complexity. + +Three rules govern the pairs, each learned the expensive way: + +1. **One *effect* per arm, not one cell.** Two knobs that are entangled make the delta unreadable (`emiMkt_mult_aim050` moves `enterFrac` *and* `aimMaxTries` precisely because the second must be re-sized with the first). +2. **The arm must be hosted where the mechanism actually fires.** An off-arm on a scenario that never triggers the knob is bit-identical to its base and measures nothing. Check the mechanism's trace in the **base** first (§3.2 rule 6). +3. **When a default moves, re-point the arms defined as the old default** — otherwise they silently become duplicates of their base. + +### 4.2 Coverage scenarios + +| Scenario | Target shape | What it is guarding | +|---|---|---| +| `emiMkt_A_multiRegionSplit` | DEU/FRA/UKI, ETS + ESR @2030 | Per-market AND aggregation across several regions at once | +| `emiMkt_B_nonBinding` | DEU ETS slack, ESR tight | The `smallPrice` floor: a market that is legitimately non-binding must freeze as **met**, not be driven to a price it does not need | +| `emiMkt_C_budget` | Three budgets, FRA near-zero | Budget denominator guard, price ceiling, and the infeasible-stop "knee" rollback — FRA is unreachable **by design** | +| `emiMkt_D_netZeroESR` | DEU ESR net-zero @2045, 1% tol | A *reachable* net-zero target must converge normally | +| `emiMkt_D_netZeroESR_tight` | Same target, 0.05% tol | Tolerance below the Nash noise floor. Must be frozen by the noise-floor stop and reported `unmetFrozen` **with its real residual** — never re-scored against a widened band | +| `emiMkt_E_tightTolSplit` | Six markets @0.005% tol | Graceful termination when nothing can converge. Also the only scenario where rollbacks are routinely *undone* | +| `emiMkt_F_nestedGroupMember` | EU27 `all` + DEU ETS/ESR | Overlapping group and member targets — the disaggregation precedence rule (§1.2) | +| `emiMkt_G_multiPeriodSplit` | DEU @2030 **and** @2040 | Sequential periods: the later ramp must not wipe the converged earlier one (§2.2) | +| `emiMkt_mult` | `nzero` preset, 9 targets | Comprehensive stress test across seven regions and mixed metrics | +| `emiMkt_budgetFeas` | Three budgets, all reachable | The stall guard must let a *lagging but feasible* budget converge rather than freezing it | +| `emiMkt_oscDamp` | DEU/FRA/UKI net-zero @2050 | A target that oscillates on the noise floor — the dampener's home ground | + +### 4.3 A/B pairs + +| Base | Off-arm | Knob | Question it answers | +|---|---|---|---| +| `emiMkt_mult` | `_noReopen` | `reopenMaxDev` | Does re-opening a drifted target help, or just churn? | +| `emiMkt_mult` | `_aim050` | `enterFrac`, `aimMaxTries` | Is a tighter AIM band better? (**No** — below the noise floor it is unreachable) | +| `emiMkt_mult` | `_wideBand` | `enterFrac`, `exitFrac` | The old bands, which opened a dead zone between tolerance and exit | +| `emiMkt_mult_wideBand` | `_noPark` | `parkedStop` | Reproduces the deadlock: a target held converged *outside* tolerance is unmet, uncorrectable and un-abandonable, and blocks run end | +| `emiMkt_oscDamp` | `emiMkt_oscNoDamp` | `dampFlipMin` | The raw oscillation the dampener tames | +| `emiMkt_C_budget` | `_lowCeil` | `maxPrice` | A binding ceiling roughly halves the run on an infeasible target, at the cost of a larger residual | +| `emiMkt_C_budget` | `_noFloor` | `budgetDenomFloorFrac` | Without the floor, a near-zero budget target's deviation explodes | +| `emiMkt_budgetFeas` | `_aim050` | `enterFrac`, `aimMaxTries` | The same tighter-AIM question on the run that regressed worst under it | +| `emiMkt_budgetFeas` | `_noBrake` | `divergeBrakeMax` | Freeze on first detection instead of braking | +| `emiMkt_budgetFeas` | `_noDiv` | `divergeFactor` | With divergence handling off, a steering target that has spent its budget has **no route to a give-up** | +| `emiMkt_budgetFeas` | `_noRefresh` / `_refresh8` | `reopenRefresh` | Three-point read: shipped 24 / a threshold reachable in one run / off | +| `emiMkt_D_netZeroESR_tight` | `_noNFRoll` *(as `emiMkt_D_tight_noNFRoll`)* | `noiseFloorRollback` | Freeze at the window's best, or wherever the stop happened to fire? | +| `emiMkt_E_tightTolSplit` | `_noRoll` | `rollbackBestFrac` | Is the run-wide best price worth preferring over the stop's own candidate? | +| `emiMkt_E_tightTolSplit` | `_noRollVerify` | `rollbackVerify` | Should a rollback that looks worse one iteration later be undone? | + +### 4.4 Reference runs + +| Scenario | Role | +|---|---| +| `SSP2-EU21-NPi2025` | Baseline the others start from (`path_gdx`). Carries no targets | +| `SSP2-EU21-PkBudg1000` | **No-target reference.** Module 47 does nothing, so any Nash-loop behaviour it shows is *not* the controller's doing — the cleanest way to separate the two | +| `SSP2-EU21-PkBudg650` / `750` | Realistic EU trajectories at a tight 0.4% tolerance | +| `EU_real_nzero55` / `57` / `59` | The real EU 2040-target trajectory at three 2030 ambition levels, plus net-zero 2045/2050 | + +> [!WARNING] +> **REMIND does not reproduce run to run.** The regional NLPs are non-convex and the solver can settle in either of two local optima, with something timing-dependent in the asynchronous grid dispatch deciding which. Two runs of the *same* scenario can therefore diverge even with byte-identical config. Before reading any A/B, confirm the pair is comparable: find the first iteration whose **deviation** differs and check whether any **rescale factor** issued before that point differed. If none did, the model answered an identical instruction two ways and the delta is not the knob's. diff --git a/tutorials/figures/git-19-held.png b/tutorials/figures/git-19-held.png new file mode 100644 index 0000000000..13fa058a20 Binary files /dev/null and b/tutorials/figures/git-19-held.png differ diff --git a/tutorials/figures/git-19-loop.png b/tutorials/figures/git-19-loop.png new file mode 100644 index 0000000000..9a481c65ec Binary files /dev/null and b/tutorials/figures/git-19-loop.png differ diff --git a/tutorials/figures/git-19-steering.png b/tutorials/figures/git-19-steering.png new file mode 100644 index 0000000000..1c5cba01d8 Binary files /dev/null and b/tutorials/figures/git-19-steering.png differ diff --git a/tutorials/figures/regiCarbonPrice_targets_graph.py b/tutorials/figures/regiCarbonPrice_targets_graph.py new file mode 100644 index 0000000000..22784b448d --- /dev/null +++ b/tutorials/figures/regiCarbonPrice_targets_graph.py @@ -0,0 +1,410 @@ +"""Draw flow graphs for tutorials/19_RegionalEmissionTargets.md with labeled straight connectors, zero overlaps, and automatic box border snapping. + +Regenerate with: python regiCarbonPrice_targets_graph.py +Requires: matplotlib (which brings Pillow, used to keep the pngs under the 220 kB repo file-size limit). + +Outputs: + git-19-loop.png - per-iteration search loop (Section 2.1) + git-19-held.png - decision graph, target WAS FROZEN (Section 2.4 & 2.5) + git-19-steering.png - decision graph, target IS STEERING (Section 2.4 & 2.8) + +THESE FIGURES ASSERT THINGS ABOUT postsolve.gms, so they go stale silently. When editing, re-check against the +code rather than against the previous figure. Three claims were wrong until 2026-08-05: + * the PARKED stop had a rollback edge - it is the ONLY give-up branch that never sets wantRoll; + * the noise-floor / infeasible / divergence stops were labelled bestAchievable - all three fire outside the + tolerance, so the HONESTY RE-LABEL demotes them to unmetFrozen at the shipped exitFrac = 1.0; + * the give-up branches were unnumbered - they now carry their p47_slopeTrace_iter("giveUpBy") code. +Every constant quoted below is a datainput.gms default; verify with +`grep -oE 'p47_slopeParam\\("[a-zA-Z]+"\\)=[0-9.e-]+' datainput.gms`. +""" + +import os + +import matplotlib.pyplot as plt +from matplotlib.patches import FancyArrowPatch +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# REMIND rejects files above 220 kB. matplotlib writes 32-bit RGBA, which costs ~330 kB for a 2000x2000 +# figure. These are flat-fill diagrams of a few dozen distinct colours, so a 256-entry palette holds every +# fill, border and antialiased text edge exactly: the shrink is ~70% with no visible loss. Do NOT dither, +# it adds noise to the flat fills and makes the file BIGGER. +MAX_KB = 220 + + +def _shrink(path): + """Rewrite a matplotlib RGBA png as an 8-bit palette png, and refuse to leave one over the repo limit.""" + with Image.open(path) as img: + img.convert("RGB").quantize(colors=256, dither=Image.Dither.NONE).save(path, optimize=True) + kb = os.path.getsize(path) / 1024 + if kb > MAX_KB: + raise SystemExit(f"{os.path.basename(path)} is {kb:.0f} kB, over the {MAX_KB} kB repo limit. " + f"Lower the savefig dpi or split the figure.") + print(f"wrote {path} ({kb:.0f} kB)") + +# Design system palette (Fill, Border, Text Color) +THEME = { + "model": {"fc": "#F5F3FF", "ec": "#7C3AED", "tc": "#4C1D95"}, # REMIND Model solve (Purple) + "measure": {"fc": "#FFFFFF", "ec": "#475569", "tc": "#0F172A"}, # Measurement / Fit (White / Slate) + "decide": {"fc": "#FFFBEB", "ec": "#D97706", "tc": "#78350F"}, # Decision / Branch (Orange) + "act": {"fc": "#ECFEFF", "ec": "#0891B2", "tc": "#164E63"}, # Steering Action (Cyan) + "done": {"fc": "#F0FDF4", "ec": "#16A34A", "tc": "#14532D"}, # Settled / Frozen (Green) + "giveup": {"fc": "#FEF2F2", "ec": "#DC2626", "tc": "#7F1D1D"}, # Give-up Branch (Red) +} + +EDGE_COLOR = "#475569" +EDGE_LABEL_BG = "#FFFFFF" +EDGE_LABEL_BORDER = "#CBD5E1" + + +def _draw_straight_graph(pos, boxes, edges, xlim, ylim, figsize, title, outfile, fontsize=9.0): + fig, ax = plt.subplots(figsize=figsize, facecolor="#F8FAFC") + ax.set_facecolor("#F8FAFC") + + # 1. Draw Text Boxes first and keep reference to their text patch artist + box_patches = {} + for node, (text, category) in boxes.items(): + x, y = pos[node] + cfg = THEME.get(category, THEME["measure"]) + + lines = text.split("\n") + header = lines[0] + body = "\n".join(lines[1:]) if len(lines) > 1 else "" + + # The header is set in mathtext to get bold inside a mixed-weight box. Mathtext treats "-" and "=" as + # BINARY OPERATORS and pads them, so "Non-Binding" came out "Non - Binding" and "giveUpBy=5" as + # "giveUpBy = 5". Bracing forces them to render as ordinary symbols at their natural width. + header_escaped = (header.replace(" ", "\\ ") + .replace("-", "{-}") + .replace("=", "{=}")) + formatted_text = f"$\\mathbf{{{header_escaped}}}$\n{body}" if body else f"$\\mathbf{{{header_escaped}}}$" + + is_terminal = category in ("done", "giveup") + is_dashed = (node == "brake") + + if category == "decide": + boxstyle = "square,pad=0.42" + else: + boxstyle = "round,pad=0.42,rounding_size=0.50" + + txt_artist = ax.text( + x, y, formatted_text, + fontsize=fontsize, + ha="center", va="center", + fontfamily="sans-serif", + color=cfg["tc"], + linespacing=1.2, + zorder=4, + bbox=dict( + boxstyle=boxstyle, + fc=cfg["fc"], + ec=cfg["ec"], + linewidth=1.25, + linestyle="--" if is_dashed else "-", + ), + ) + box_patches[node] = txt_artist.get_bbox_patch() + + # Draw a double-border style for boxes that terminate the target running + if is_terminal: + if category == "decide": + outer_boxstyle = "square,pad=0.74" + else: + outer_boxstyle = "round,pad=0.74,rounding_size=0.75" + + outer_artist = ax.text( + x, y, formatted_text, + fontsize=fontsize, + ha="center", va="center", + fontfamily="sans-serif", + color="none", + linespacing=1.2, + zorder=3, + bbox=dict( + boxstyle=outer_boxstyle, + fc="none", + ec=cfg["ec"], + linewidth=0.9, + ), + ) + box_patches[node] = outer_artist.get_bbox_patch() + + # Force a canvas draw so bbox locations are computed accurately for patch clipping + fig.canvas.draw() + + # 2. Draw Arrows using patchA/patchB auto-snapping. Straight (rad=0.0) unless the edge supplies a 6th + # element: the loop-closing feedback edge has to bow around the column it would otherwise pass through. + for edge in edges: + u, v, label, lx, ly = edge[:5] + rad = edge[5] if len(edge) > 5 else 0.0 + conn_style = rad if isinstance(rad, str) else f"arc3,rad={rad}" + arrow_style = "->" if (isinstance(rad, str) and "bar" in rad) else "-|>" + + arrow = FancyArrowPatch( + pos[u], pos[v], + connectionstyle=conn_style, + arrowstyle=arrow_style, + mutation_scale=12, + color=EDGE_COLOR, + linewidth=1.2, + patchA=box_patches[u], + patchB=box_patches[v], + shrinkA=2, + shrinkB=2, + zorder=5, + ) + ax.add_patch(arrow) + + # Edge label badge (white card with subtle border, no overlap) + if label: + if lx is None: + lx = (pos[u][0] + pos[v][0]) / 2 + if ly is None: + ly = (pos[u][1] + pos[v][1]) / 2 + ax.text( + lx, ly, label, + fontsize=fontsize - 1.5, + fontweight="medium", + color="#334155", + ha="left", va="center", + multialignment="center", + fontfamily="sans-serif", + bbox=dict( + boxstyle="round,pad=0.22,rounding_size=0.12", + fc=EDGE_LABEL_BG, + ec=EDGE_LABEL_BORDER, + lw=0.8, + alpha=0.95 + ), + zorder=6, + ) + + # 3. Figure Title Banner + ax.text( + 0.5, 0.97, title, + transform=ax.transAxes, + fontsize=12.5, + fontweight="bold", + color="#0F172A", + ha="center", va="top", + fontfamily="sans-serif", + bbox=dict( + boxstyle="round,pad=0.35,rounding_size=0.15", + fc="#FFFFFF", + ec="#E2E8F0", + lw=1.0 + ) + ) + + ax.set_xlim(*xlim) + ax.set_ylim(*ylim) + ax.axis("off") + plt.tight_layout() + + path = os.path.join(HERE, outfile) + plt.savefig(path, dpi=200, facecolor="#F8FAFC", bbox_inches="tight") + plt.close(fig) + _shrink(path) + + +def loop_graph(): + """Figure 1: High-level per-iteration search loop (Section 2.1) - Fully Labeled Straight Connectors.""" + boxes = { + "solve": ("REMIND Nash Iteration\nSolves one global iteration", "model"), + "dev": ("Measure Deviation |dev|\ndev = (emissions - target) / reference\n(Section 2.3)", "measure"), + "check": ("Inside Tolerance?\n|dev| <= enterFrac x tol\nfor persist=2 iterations\n(Section 2.4)", "decide"), + "slope": ("Learn Response\nLeast-squares slope fit over last\nmaxWindow=8 steered iterations\n(Section 2.6)", "measure"), + "step": ("Compute Price Step\nTrust region [0.5, 2.0] & damping\n(Section 2.6 & 2.7)", "act"), + "ramp": ("Redraw Price Path\nLinearly interpolate & anchor ramps\n(Section 2.2)", "act"), + + # An ACT node, so it must not be phrased as a question: the drift decision is taken at `frozen`, this + # box is the un-freezing itself. Decisions are the square orange boxes. + "reopen_node": ("RE-OPEN\nUn-freeze and steer again\n(budget reopenMax=3)\n(Section 2.5)", "act"), + "frozen": ("Target FROZEN\nCarbon price stops moving\nlabel: lowerThanTolerance / smallPrice", "done"), + "giveup": ("Give-Up (1 of 5 branches)\nFreeze price & report residual\np47_slopeTrace_iter(\"giveUpBy\")\nnames which one (Section 2.8)\nlabel: unmetFrozen / bestAchievable", "giveup"), + "end": ("Run Termination\nAll regional targets frozen\n(met or given up)", "done"), + } + + pos = { + "solve": (0.0, 5.0), + "dev": (0.0, 3.6), + "check": (0.0, 2.0), + "slope": (0.0, 0.4), + "step": (0.0, -1.2), + "ramp": (-0.01, -2.8), + + "reopen_node": (4, 3.6), + "frozen": (7, 2.0), + "giveup": (5, 0.4), + "end": (7, -2.8), + } + + # Straight line edges with explicit labels for every path + edges = [ + ("solve", "dev", "solver\noutput", 0.1, 4.4), + ("dev", "check", "evaluate\ndeviation", 0.1, 2.8), + ("check", "slope", "no\n(keep steering)", 0.1, 1.2), + ("slope", "step", "response\nslope", 0.1, -0.4), + ("step", "ramp", "new price factor", 0.1, -2.0), + + ("check", "frozen", "yes\n(in-band)", 2.7, 2), + ("frozen", "reopen_node", "emissions\ndrift out", 4.3, 2.8), + ("reopen_node", "check", "un-freeze\n& steer", 2.6, 2.8), + + ("slope", "giveup", "stuck /\nno response", 1.9, 0.6), + ("frozen", "end", "all targets\nfrozen & met", 7.2, -0.4), + ("giveup", "end", "given up &\nreported", 5, -1.2), + + ("ramp", "solve", "price path\nfeeds the next\nNash iteration", -3.7, 1.05, "bar,armA=-336,armB=-336,fraction=0"), + ] + + return _draw_straight_graph( + pos, boxes, edges, + xlim=(-4, 9), ylim=(-3.5, 6.5), figsize=(10, 10), + title="Section 2.1: Per-Target Search Loop (One Nash Iteration)", + outfile="git-19-loop.png" + ) + + +def held_graph(): + """Figure 2: Decision graph for target FROZEN last iteration (Section 2.4 & 2.5) - Fully Labeled.""" + boxes = { + "start": ("Measure Deviation |dev|\nTarget evaluation\n(Section 2.3)", "measure"), + "small": ("Non-Binding Floor?\nPrice at floor & emissions below target?\n(Section 2.4)", "decide"), + "smallP": ("smallPrice Market\nFrozen & Met\n(non-binding market)\nlabel: smallPrice", "done"), + "was": ("Target Status?\nWas it frozen last iteration?", "decide"), + # A hand-off to the other figure, not an action taken here - so it carries the same neutral "measure" + # styling as that figure's entry node rather than the cyan of a steering action. + "steering": ("Target STEERING\nSee Decision Graph 2\n(Section 2.4)", "measure"), + + "hold": ("Hold Condition?\n|dev| > exitFrac x tol\nfor persist=2 iterations\n(Section 2.4)", "decide"), + "keep": ("Stay FROZEN\nRide out iteration wobble", "done"), + "refresh": ("Settlement Refresh\nSettled for reopenRefresh (24 iter)?\nRe-open budget earned back\n(Section 2.5)", "act"), + "parked": ("PARKED STOP\nHeld full window outside tol\n-> Give up, NO rollback\n(price constant, giveUpBy=5)\nlabel: bestAchievable", "giveup"), + + "reopen": ("Re-Open Budget?\n|dev| <= reopenMaxDev\n& budget left?\n(Section 2.5)", "decide"), + "reopened": ("RE-OPEN (Charged)\nSteer again (first step\ncapped at reopenStepCap=5%)", "act"), + "release": ("RELEASE (Uncharged)\nToo far out to be drift", "act"), + # Keep every body line short: these boxes are auto-sized from their text, so one long line widens the + # box until it collides with its neighbour (this one ran into "RE-OPEN (Charged)"). + "budgetOut": ("BUDGET SPENT\n(reopenMax=3 exhausted)\n-> Give up (giveUpBy=4)\nlabel: unmetFrozen", "giveup"), + "roll": ("PRICE ROLLBACK\nRestore best-so-far or knee price\n(Section 2.9)", "act"), + } + + pos = { + "start": (0.0, 6.4), + "small": (0.0, 4.8), + "smallP": (16.0, 4.8), + "was": (0.0, 3.2), + "steering": (16.0, 3.2), + + "hold": (0.0, 1.4), + "keep": (-12, 1.4), + "refresh": (-18, -0.6), + "parked": (-6.5, -0.6), + + "reopen": (6.2, -0.6), + "budgetOut": (-3.5, -2.8), + "reopened": (6.6, -2.8), + "release": (16, -2.8), + "roll": (-15, -4.8), + } + + edges = [ + ("start", "small", "check\nmarket price", 0.4, 5.7), + ("small", "smallP", "yes\n(slack\nmarket)", 8.2, 5.2), + ("small", "was", "no", 0.4, 4.0), + ("was", "hold", "yes\n(was frozen)", 0.4, 2.3), + ("was", "steering", "no\n(was steering)", 6.2, 3.5), + + ("hold", "keep", "no\n(ride out\nwobble)", -6.2, 1.8), + ("keep", "refresh", "and met\n(24 iters)", -17.6, 0.6), + ("keep", "parked", "full window\noutside tol", -8.4, 0.6), + ("hold", "reopen", "yes\n(exits band)", 4, 0.5), + + ("reopen", "budgetOut", "no:\nbudget spent", 0.0, -1.6), + ("reopen", "reopened", "yes\n(budget left)", 5, -1.6), + ("reopen", "release", "no:\n|dev| too large", 10, -1.6), + + ("budgetOut", "roll", "rollback\nprice", -12.4, -3.6), + ] + + return _draw_straight_graph( + pos, boxes, edges, + xlim=(-24, 22.0), ylim=(-5.8, 8.0), figsize=(10, 10), + title="Decision Graph 1 of 2: Target FROZEN Last Iteration (Section 2.4 & 2.5)", + outfile="git-19-held.png" + ) + + +def steering_graph(): + """Figure 3: Decision graph for target STEERING price (Section 2.4 - 2.8) - Fully Labeled.""" + boxes = { + "enter": ("Target Status\nTarget is STEERING\nits carbon price", "measure"), + "aim": ("AIM Band Check\n|dev| <= enterFrac x tol\nfor persist=2 iterations?\n(Section 2.4)", "decide"), + "conv": ("CONVERGED\nFreeze price\nlabel: lowerThanTolerance", "done"), + "accept": ("ACCEPT Band Check\n|dev| <= tol & aim budget\n(aimMaxTries=3) spent?\n(Section 2.4)", "decide"), + "steer": ("Active Price Steering\nSlope fit -> Capped price step\n(Section 2.6 & 2.7)", "act"), + + # Every stop assigns bestAchievable, but the HONESTY RE-LABEL demotes a frozen, unmet target whose + # |dev| exceeds exitFrac x tolerance to unmetFrozen. All three of these fire strictly OUTSIDE the + # tolerance, so at the shipped exitFrac = 1.0 they always end up unmetFrozen. Only the parked stop + # (Decision Graph 1) leaves bestAchievable standing, because it fires from inside the exit band. + # The giveUpBy code goes in the BODY, not the header: the header is set in mathtext, which pads "=" as + # a relation ("giveUpBy = 1") and bracing does not suppress that in matplotlib's implementation. + "noise": ("NOISE FLOOR STOP\nPrice settled & |dev| trapped\nin narrow band outside tol\n(Section 2.8, giveUpBy=1)\nlabel: unmetFrozen", "giveup"), + "infeas": ("INFEASIBLE TARGET STOP\nStep pinned at cap, dev stalled\n& one-sided over window\n(Section 2.8, giveUpBy=2)\nlabel: unmetFrozen", "giveup"), + "diverge": ("DIVERGENT PATH?\nArmed, dev > divergeFactor x best\nfor 2 iterations, not recovering\n(Section 2.8)", "decide"), + "brake": ("DIVERGENCE BRAKE\n(Reversible)\nRestore window-best price\n& cap step at 5%", "act"), + "divstop": ("DIVERGENCE STOP\nBrakes (divergeBrakeMax=2)\nspent -> Give up\n(Section 2.8, giveUpBy=3)\nlabel: unmetFrozen", "giveup"), + "roll": ("PRICE ROLLBACK\nRestore best price path\n(verified next iteration by rollbackVerify)\n(Section 2.9)", "act"), + } + + pos = { + "enter": (0.0, 6.0), + "aim": (0.0, 4.0), + "conv": (14.0, 3.0), + "accept": (0.0, 2.0), + "steer": (0.0, 0.0), + + "noise": (-12.0, -2), + "infeas": (0.0, -2), + "diverge": (11.5, -2), + "brake": (16, -4.0), + "divstop": (7, -4.0), + "roll": (0.0, -6.0), + } + + edges = [ + ("enter", "aim", "check\nAIM band", 0.4, 5.1), + ("aim", "conv", "yes\n(|dev| <= 0.75x tol)", 6.2, 3.9), + ("aim", "accept", "no", 0.4, 3.1), + ("accept", "conv", "yes\n(|dev| <= 1.0x tol)", 6.3, 2.1), + ("accept", "steer", "no\n(keep steering)", 0.4, 1.1), + + ("steer", "noise", "trapped\nin noise", -8.2, -0.8), + ("steer", "infeas", "pinned\nat cap", 0.4, -0.8), + ("steer", "diverge", "excursion\n> 10x", 6.8, -0.8), + # `brake` sits to the RIGHT (x=16) and `divstop` to the LEFT (x=7), so the label x-coordinates have to + # follow: these two were swapped, putting "brakes left" on the arrow into the STOP and vice versa. + ("diverge", "brake", "brakes\nleft", 14.7, -2.9), + ("diverge", "divstop", "brakes\nspent", 7.4, -2.9), + + ("noise", "roll", "rollback\nprice", -6, -5), + ("infeas", "roll", "rollback\nprice", -0.8, -4.8), + ("divstop", "roll", "rollback\nprice", 5, -5), + ] + + return _draw_straight_graph( + pos, boxes, edges, + xlim=(-20, 20.0), ylim=(-7, 8), figsize=(10, 10), + title="Decision Graph 2 of 2: Target STEERING Its Price (Section 2.4 - 2.8)", + outfile="git-19-steering.png" + ) + + +if __name__ == "__main__": + loop_graph() + held_graph() + steering_graph()