From 182ee1e18a2e5c4c9ade569a1ef1479b615d8d55 Mon Sep 17 00:00:00 2001 From: florianh Date: Mon, 6 Jul 2026 14:15:52 +0200 Subject: [PATCH] nzb: scenario_config CSV + setScenario single-run launcher Add an FSEC-style base+delta scenario registry and a one-run launcher so the NZB families can be driven from a CSV instead of R branching: - config/scenario_config_nzb.csv: nzb_base + 7 family columns (gC_wdpa, gC_30by30, gC_half, gA_RoW2C, gB_price, gF_freeze, g0_npi), deltas only. - scripts/start/nzb_launch.R: resolves nocc + nzb_base + one family via gms::setScenario (list-valued forcesuff21 collapsed back to a string after each overlay), and enables the parametric cap from either a reported-AFOLU target (cap = target + 63, the reported<->cap offset) or a raw cap value. Same family definitions as nzb_export_grid.R; verified field-equivalent across all seven. --- config/scenario_config_nzb.csv | 30 ++++++++ scripts/start/nzb_launch.R | 136 +++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 config/scenario_config_nzb.csv create mode 100644 scripts/start/nzb_launch.R diff --git a/config/scenario_config_nzb.csv b/config/scenario_config_nzb.csv new file mode 100644 index 000000000..ff55a4057 --- /dev/null +++ b/config/scenario_config_nzb.csv @@ -0,0 +1,30 @@ +f;nzb_base;gC_wdpa;gC_30by30;gC_half;gA_RoW2C;gB_price;gF_freeze;g0_npi +gms$ghg_policy;cap_apr26_reg;;;;;;; +gms$c56_cap_policy;all;;;;;;; +gms$s56_source_bounds_on;0;;;;;;; +gms$policy_countries56;BRA;;;;;;; +gms$s21_force_selfsuff;1;;;;;;; +gms$forcesuff21;wood, woodfuel, sugr_cane, cassav_sp, molasses;;;;;;wood, woodfuel, sugr_cane, cassav_sp, molasses, fibres, soybean, groundnut, sugar, oilcakes, livst_pig, maiz, livst_chick, others, livst_rum, alcohol, oils, rice_pro, livst_milk, cottn_pro, brans, livst_egg; +gms$policy_countries21;BRA;;;;;;; +gms$c32_aff_policy;npi;;;;;;; +gms$c35_ad_policy;npi;;;;;;; +gms$c35_aolc_policy;npi;;;;;;; +gms$c_timesteps;5year2050;;;;;;; +gms$c22_protect_scenario_noselect;none;;;;;;; +gms$policy_countries22;BRA;;;;;;; +gms$s57_maxmac_n_soil;;106;106;106;106;-1;106;-1 +gms$s57_maxmac_n_awms;;106;106;106;106;-1;106;-1 +gms$s57_maxmac_ch4_rice;;106;106;106;106;-1;106;-1 +gms$s57_maxmac_ch4_entferm;;106;106;106;106;-1;106;-1 +gms$s57_maxmac_ch4_awms;;106;106;106;106;-1;106;-1 +gms$s57_maxmac_fadein;;1;1;1;1;0;1;0 +gms$s57_maxmac_fadein_start;;2025;2025;2025;2025;;2025; +gms$s57_maxmac_fadein_end;;2050;2050;2050;2050;;2050; +gms$c22_protect_scenario;;none;30by30;PBL_HalfEarth;none;none;none;none +gms$s22_restore_land;;0;1;1;0;0;0;0 +gms$c56_pollutant_prices;;none;none;none;none;R34M410-SSP2-PkBudg650;none;none +gms$c56_pollutant_prices_noselect;;R34M410-SSP2-NPi2025;R34M410-SSP2-NPi2025;R34M410-SSP2-NPi2025;R34M410-SSP2-PkBudg1000;R34M410-SSP2-NPi2025;R34M410-SSP2-NPi2025;R34M410-SSP2-NPi2025 +gms$s56_c_price_induced_aff;;0;0;0;1;1;0;0 +gms$scen_countries60;;;;;BRA;;; +gms$c60_2ndgen_biodem;;;;;R34M410-SSP2-NPi2025;;; +gms$c60_2ndgen_biodem_noselect;;;;;R34M410-SSP2-PkBudg1000;;; diff --git a/scripts/start/nzb_launch.R b/scripts/start/nzb_launch.R new file mode 100644 index 000000000..da6ccdb56 --- /dev/null +++ b/scripts/start/nzb_launch.R @@ -0,0 +1,136 @@ +## nzb_launch.R ================================================================ +## Launch ONE Net-Zero-Brazil scenario, defined as a column in +## config/scenario_config_nzb.csv (FSEC-style base+delta), with the cap supplied +## at launch. Companion to the batch grid scripts/start/nzb_export_grid.R -- same +## families, but one run per call and driven by the CSV instead of R branching. +## +## INPUTS (env vars, or key=value CLI args): +## scenario (NZB_SCENARIO) required. A column of scenario_config_nzb.csv: +## gC_wdpa | gC_30by30 | gC_half | gA_RoW2C | gB_price | gF_freeze | g0_npi +## target (NZB_TARGET) optional. Desired REPORTED AFOLU 2050 (GWP100AR6|Land, Mt CO2eq). +## The script sets the emissions cap to cap = target + OFFSET_MT (see OFFSET +## below) and enables the parametric ramp 2035->2050. This is the platform knob. +## cap (NZB_CAP) optional. RAW emissions cap value (s56_emis_cap_target, Mt CO2eq), +## set verbatim -- bypasses the offset. Reported AFOLU then comes out ~ cap - OFFSET_MT. +## -> Give EITHER target OR cap (not both). Omit both -> the scenario's no-cap reference run. +## +## OFFSET (reported <-> cap). The cap bounds vm_emissions_reg, a SUPERSET of the reported +## GWP100AR6|Land AFOLU, so reported ~ cap - OFFSET_MT with OFFSET_MT = 63 Mt (median across +## families; ~+50 of it is the intact-peatland CH4+DOC slice the cap includes but the report +## excludes). Hence cap = target + 63. The per-family spread is ~57-71 and the reachable-target +## range is per family -- see NZB_scenario_config_approach.md. Retune via OFFSET_MT below. +## +## LAUNCH (recommended) -- run the script DIRECTLY, bypassing start.R. This avoids +## start.R's interactive "Update now? (Y/n)" renv prompt entirely (that check lives only +## in start.R, not .Rprofile); renv still activates via .Rprofile and start_run() submits +## the model. No submit= needed. +## NZB_SCENARIO=gF_freeze NZB_TARGET=-100 Rscript scripts/start/nzb_launch.R # target reported AFOLU +## NZB_SCENARIO=gF_freeze NZB_CAP=-37 Rscript scripts/start/nzb_launch.R # raw cap (= -100 + 63) +## NZB_SCENARIO=g0_npi Rscript scripts/start/nzb_launch.R # no cap -> ref run +## (CLI args also work here: Rscript scripts/start/nzb_launch.R scenario=gF_freeze target=-100) +## +## ALTERNATIVE via start.R -- pass submit=direct (else start.R prompts for a submission +## type) and it will run the "Update now?" check first: +## NZB_SCENARIO=gF_freeze NZB_CAP=-37 Rscript start.R runscripts=nzb_launch submit=direct +## CLI scenario=/cap= reach the script only with submit=direct (in-process source); +## background/SLURM submit modes spawn a fresh Rscript that drops CLI args -- env vars +## are inherited and always work. +## +## The CSV is the scenario registry; a platform edits/adds columns or just picks a +## column + cap. No config text is generated as code -> nothing to mis-template. +## ============================================================================ +library(lucode2); library(gms); library(magpie4) +source("config/default.cfg"); source("scripts/start_functions.R") + +SCEN_CSV <- "config/scenario_config_nzb.csv" + +# ---- read launch args (CLI key=value first, else NZB_* env) ------------------------- +.args <- commandArgs(trailingOnly = TRUE) +getarg <- function(key) { + hit <- grep(paste0("^", key, "="), .args, value = TRUE) + if (length(hit)) return(sub(paste0("^", key, "="), "", hit[1])) + e <- Sys.getenv(paste0("NZB_", toupper(key)), ""); if (nzchar(e)) e else NA_character_ +} +scenario <- getarg("scenario") +target_raw <- getarg("target") +cap_raw <- getarg("cap") +have <- function(x) !is.na(x) && nzchar(x) + +valid <- setdiff(colnames(read.csv2(SCEN_CSV, check.names = FALSE, nrows = 1))[-1], c("", "nzb_base")) +if (is.na(scenario)) + stop("nzb_launch: no scenario given. Set NZB_SCENARIO= (or scenario=).\n", + " columns: ", paste(valid, collapse = ", ")) +if (!scenario %in% valid) + stop("nzb_launch: scenario '", scenario, "' is not a column of ", SCEN_CSV, ".\n", + " columns: ", paste(valid, collapse = ", ")) + +# ---- resolve the scenario: nocc (native) + nzb_base + the chosen family ------------- +# setScenario is MAgPIE's central scenario mechanism (base+delta overlay, as in FSEC), +# so the whole NZB registry rides it. One wrinkle: setScenario splits any comma-bearing +# cell into a character VECTOR -- here the list-valued GAMS *set* switch forcesuff21 (a +# subset of k_trade). A vector serialises to a valid GAMS set list via manipulateConfig +# (exactly like the ~249-element policy_countries* sets), so the MODEL is fine either +# way. But setScenario's own scalar re-comparison `if (from != to & to != "")` of an +# already-vectorised value on a SECOND overlaid column errors ("the condition has length +# > 1"). So we overlay the columns ONE AT A TIME and collapse forcesuff21 back to the +# single comma-string that default.cfg / nzb_export_grid.R use, after each call. The +# collapse round-trips the cell verbatim ('a, b' -> c('a',' b') -> 'a, b') and is a +# no-op when the family cell is empty (forcesuff21 already a length-1 string from base). +cfg <- setScenario(cfg, "nocc") # native climate-off scenario +for (col in c("nzb_base", scenario)) { + cfg <- setScenario(cfg, col, scenario_config = SCEN_CSV) # scalar deltas overlaid natively + cfg$gms$forcesuff21 <- paste(cfg$gms$forcesuff21, collapse = ",") # comma-list switch: vector -> single string +} + +# ---- common run settings (not policy levers; same for every NZB run) ---------------- +cfg$input <- c(regional = "rev4.131.9001BRA_H13_C200_W3_MapbiomasIBGE_5638d5dc_magpie.tgz", + cellular = "rev4.131.9001BRA_H13_C200_W3_MapbiomasIBGE_5638d5dc_d8411e75_cellularmagpie_c200_MRI-ESM2-0-ssp245_lpjml-8e6c5eb1_clusterweight-d0236589.tgz", + validation = "rev4.131.9001BRA_H13_C200_W3_MapbiomasIBGE_5638d5dc_92e02314_validation.tgz", + additional = "additional_data_rev4.65.tgz", + calibration = "calibration_BRA_H13_C200_W3_MapbiomasIBGE_18Jun26.tgz") +cfg$force_download <- FALSE; cfg$force_replace <- TRUE +cfg$recalibrate <- FALSE; cfg$recalibrate_landconversion_cost <- FALSE +cfg$output <- c("rds_report"); cfg$results_folder <- "output/:title:"; cfg$sequential <- FALSE + +# ---- cap: from a reported-AFOLU target (cap = target + OFFSET_MT) or a raw cap value ------ +# OFFSET_MT is the reported<->cap offset (see header): reported GWP100AR6|Land AFOLU ~ cap - +# OFFSET_MT, so cap = target_reported + OFFSET_MT. +63 Mt = median across families (~+50 is the +# intact-peatland CH4+DOC slice the cap includes but the report excludes). Retune here if needed. +OFFSET_MT <- 63 + +if (have(target_raw) && have(cap_raw)) + stop("nzb_launch: give EITHER target= (reported AFOLU) OR cap= (raw s56_emis_cap_target), not both.") + +cap <- NA_real_; from_target <- FALSE +if (have(target_raw)) { + target <- suppressWarnings(as.numeric(target_raw)) + if (!is.finite(target)) stop("nzb_launch: target='", target_raw, "' is not a number.") + cap <- target + OFFSET_MT; from_target <- TRUE + message(sprintf("nzb_launch: reported-AFOLU target %+g Mt -> cap %+g Mt (offset +%g)", + target, cap, OFFSET_MT)) +} else if (have(cap_raw)) { + cap <- suppressWarnings(as.numeric(cap_raw)) + if (!is.finite(cap)) stop("nzb_launch: cap='", cap_raw, "' is not a number.") +} + +if (is.finite(cap)) { + if (cap < -300 || cap > 600) + warning("nzb_launch: cap ", cap, " Mt is outside the tested band [-300, 600]; ", + "may be infeasible (on-slack) or non-binding. See the per-family reach table.") + cfg$gms$s56_emis_cap_parametric <- 1 + cfg$gms$s56_emis_cap_start_year <- 2035 + cfg$gms$s56_emis_cap_start_value <- 600 + cfg$gms$s56_emis_cap_target_year <- 2050 + cfg$gms$s56_emis_cap_target <- cap + cfg$title <- sprintf("nzb_%s_%s", scenario, + if (from_target) sprintf("tgt%s%d", if (target < 0) "M" else "P", abs(round(target))) + else sprintf("cap%s%d", if (cap < 0) "M" else "P", abs(round(cap)))) +} else { + # neither target nor cap: honour whatever the CSV/default set (parametric off = ref run) + cfg$title <- if (isTRUE(cfg$gms$s56_emis_cap_parametric == 1)) + sprintf("nzb_%s_capCSV", scenario) else sprintf("nzb_%s_ref", scenario) +} + +message("nzb_launch: scenario=", scenario, " title=", cfg$title, + " cap=", if (is.finite(cap)) cap else "(none/ref)") +start_run(cfg, codeCheck = FALSE)