From 59d7d5f7d71e0d7811f30fc9be8fdf665fc7058e Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 10:28:44 +0200 Subject: [PATCH 01/25] Add downloadForestLossDrivers() and ship the source table in-package The Curtis et al. (2018) Table 1 transcription and its country mapping only existed as loose files in the shared madrat source directory. They are now package data under inst/extdata/ForestLossDrivers, staged by the download function, so the source is reproducible and its numbers reviewable in a diff. Committed byte-for-byte as in use (CRLF normalised, trailing newline added). SOURCE.md records the comparison against the printed table. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n --- R/downloadForestLossDrivers.R | 73 +++++ inst/extdata/ForestLossDrivers/SOURCE.md | 76 ++++++ .../extdata/ForestLossDrivers/forest_loss.csv | 8 + inst/extdata/ForestLossDrivers/mapping.csv | 250 ++++++++++++++++++ man/downloadForestLossDrivers.Rd | 27 ++ 5 files changed, 434 insertions(+) create mode 100644 R/downloadForestLossDrivers.R create mode 100644 inst/extdata/ForestLossDrivers/SOURCE.md create mode 100644 inst/extdata/ForestLossDrivers/forest_loss.csv create mode 100644 inst/extdata/ForestLossDrivers/mapping.csv create mode 100644 man/downloadForestLossDrivers.Rd diff --git a/R/downloadForestLossDrivers.R b/R/downloadForestLossDrivers.R new file mode 100644 index 00000000..777b8dc3 --- /dev/null +++ b/R/downloadForestLossDrivers.R @@ -0,0 +1,73 @@ +#' @title downloadForestLossDrivers +#' +#' @description Provisions the ForestLossDrivers source: the regional tree cover loss +#' totals and driver shares of Curtis et al. (2018), Table 1, plus the country to +#' source-region mapping used to disaggregate them. +#' +#' Table 1 is published only inside the article; there is no machine-readable release of +#' it. The table is therefore held as version-controlled package data under +#' `inst/extdata/ForestLossDrivers` and staged into the madrat source folder here, so +#' that `readSource("ForestLossDrivers")` works on a machine that has never seen the +#' shared source directory, and so that any change to the numbers is a reviewable diff +#' rather than a silent edit of a shared file. The transcription record, including the +#' known deviations from the printed table, is in +#' `inst/extdata/ForestLossDrivers/SOURCE.md`. +#' +#' The underlying gridded product - the 10 km dominant-driver classification - is +#' distributed separately by Global Forest Watch under CC BY 4.0 as a tiled raster set +#' (`tsc_tree_cover_loss_drivers`). It is deliberately not fetched here: deriving the +#' regional totals from the raster would be a change of method, not a change of source. +#' +#' @author Michael Crawford +#' @importFrom utils bibentry person +#' @seealso [readForestLossDrivers()] +#' @examples +#' \dontrun{ +#' madrat::downloadSource("ForestLossDrivers") +#' } +downloadForestLossDrivers <- function() { + + extdata <- system.file("extdata", "ForestLossDrivers", package = "mrland") + if (extdata == "") { + stop("ForestLossDrivers package data not found. Is mrland installed correctly?") + } + + staged <- file.copy(list.files(extdata, full.names = TRUE), ".", overwrite = TRUE) + if (!all(staged)) { + stop("Could not stage the ForestLossDrivers source files into the source folder.") + } + + return(list( + title = "Classifying drivers of global forest loss (Table 1)", + description = paste("Tree cover loss 2001-2015 by region and dominant driver,", + "transcribed from Table 1 of Curtis et al. (2018), together", + "with the country to source-region mapping used to", + "disaggregate the regional totals."), + author = c(person(c("Philip", "G."), "Curtis"), + person(c("Christy", "M."), "Slay"), + person(c("Nancy", "L."), "Harris"), + person("Alexandra", "Tyukavina"), + person(c("Matthew", "C."), "Hansen")), + doi = "10.1126/science.aau3445", + url = "https://www.science.org/doi/10.1126/science.aau3445", + license = paste("Table transcribed from the article for use as a model parameter;", + "the underlying gridded product is CC BY 4.0."), + version = "Science 361(6407):1108-1111, Table 1 (2001-2015)", + unit = "Mha (2001-2015 total) and per cent of regional loss", + reference = bibentry( + "Article", + title = "Classifying drivers of global forest loss", + author = c(person(c("Philip", "G."), "Curtis"), + person(c("Christy", "M."), "Slay"), + person(c("Nancy", "L."), "Harris"), + person("Alexandra", "Tyukavina"), + person(c("Matthew", "C."), "Hansen")), + year = "2018", + journal = "Science", + volume = "361", + number = "6407", + pages = "1108-1111", + doi = "10.1126/science.aau3445" + ) + )) +} diff --git a/inst/extdata/ForestLossDrivers/SOURCE.md b/inst/extdata/ForestLossDrivers/SOURCE.md new file mode 100644 index 00000000..a548780f --- /dev/null +++ b/inst/extdata/ForestLossDrivers/SOURCE.md @@ -0,0 +1,76 @@ +# ForestLossDrivers - source record + +`forest_loss.csv` is a hand transcription of **Table 1** of + +> Curtis, P.G., Slay, C.M., Harris, N.L., Tyukavina, A., Hansen, M.C. (2018). +> Classifying drivers of global forest loss. *Science* 361(6407), 1108-1111. +> doi:10.1126/science.aau3445 + +Table 1 exists only inside the article. There is no machine-readable release of it, so +it cannot be fetched by a download function. The underlying gridded product - the 10 km +dominant-driver classification - *is* openly distributed, by Global Forest Watch under +CC BY 4.0, as the tiled raster set `tsc_tree_cover_loss_drivers` +(https://data-api.globalforestwatch.org/dataset/tsc_tree_cover_loss_drivers); the +successor 1 km product (Sims et al.) is on Zenodo at doi:10.5281/zenodo.14163025. +Deriving the regional totals from either raster would be a change of method and is out +of scope for this file. + +## Columns + +| column | meaning | +|---|---| +| `region` | Curtis source region (7 regions, Table 1 rows) | +| `treecoverloss_01_15` | total tree cover loss 2001-2015, Mha | +| `treecoverloss_pc_01_15` | that region's share of the global total, per cent | +| `deforestation` .. `urbanization` | share of *that region's* loss by driver, per cent | + +`readForestLossDrivers()` converts the driver columns to Mha/yr as +`treecoverloss_01_15 * driver_pc / 100 / 15`. `treecoverloss_pc_01_15` is not used in +that calculation; it is retained as a consistency witness and is checked on read. + +## Transcription record + +Verified line by line against the article PDF on 2026-09-10. Table 1 as printed: + +| region | Mha | % of global | deforest. | shifting | forestry | wildfire | urban | +|---|---|---|---|---|---|---|---| +| North America | 70 | 21% | 1% | <1% | 56% | 40% | 2% | +| Latin America | 78 | 25% | 56% | 31% | 13% | 1% | <1% | +| Europe | 15 | 5% | None | <1% | 99% | 1% | None | +| Africa | 39 | 13% | 4% | 92% | 4% | <1% | <1% | +| Russia/China/South Asia | 64 | 20% | <1% | <1% | 41% | 58% | <1% | +| Southeast Asia | 39 | 13% | 78% | 9% | 13% | <1% | <1% | +| Australia/Oceania | 10 | 3% | 7% | 10% | 29% | 53% | 1% | +| **Global** | **314** | **100%** | 25% | 21% | 31% | 22% | <1% | + +Deviations of the CSV from the printed table, and their disposition: + +1. **Africa `treecoverloss_01_15` = 3, should be 39.** A dropped digit. Corrected. + This was not cosmetic: Africa is 92% shifting agriculture, which is the column MAgPIE + consumes by default (`s35_forest_damage = 2`), so it made the SSA shifting-agriculture + disturbance rate 13x too low from 2020 until this fix. +2. **Southeast Asia `treecoverloss_pc_01_15` = 12, should be 13.** Corrected. This + column is not used in the calculation; with 12 the shares summed to 99 rather than 100. +3. **Europe: `shifting_agriculture` = 1 and `wildfire` = 0; the table has <1% and 1% + respectively.** The 1 point sits on the wrong driver. Corrected separately - it moves + a (very small) flow out of the default damage channel. +4. **Latin America `forestry` = 12, table says 13.** *Not* corrected. Latin America's + printed driver shares sum to 101 because of the article's own rounding; shaving one + point off forestry to reach 100 is a defensible normalisation, not a typo. +5. `<1%` and `None` are rendered as 0 or 1 rather than a fractional value, chosen so that + each row's driver shares sum to 100. Left as-is; it is the same normalisation as (4). + +The global total is 315 with Africa at 39, against the article's 314 - within the +rounding of the individual rows. With Africa at 3 it was 279, which is the check that +makes the error unambiguous. + +## Mapping + +`mapping.csv` assigns each ISO3 country to one of the seven Curtis source regions. It was +built from `madrat::regionmappingH12` (identical country set, 249 rows, same `;`-delimited +`X;CountryCode;RegionCode` layout) by rewriting the region column, and it carries at least +one artefact of that edit: the whole MEA block (ARE, BHR, DZA, EGY, IRN, IRQ, ISR, JOR, +KWT, LBN, LBY, MAR, OMN, PSE, QAT, SAU, SYR, TUN, YEM) is assigned to `Latin America`. +See the repository history for whether that has been corrected; the affected countries +hold very little natural forest, so the numerical consequence is small, but the +assignment is not defensible on its face. diff --git a/inst/extdata/ForestLossDrivers/forest_loss.csv b/inst/extdata/ForestLossDrivers/forest_loss.csv new file mode 100644 index 00000000..4f207398 --- /dev/null +++ b/inst/extdata/ForestLossDrivers/forest_loss.csv @@ -0,0 +1,8 @@ +region,treecoverloss_01_15,treecoverloss_pc_01_15,deforestation,shifting_agriculture,forestry,wildfire,urbanization +North America,70,21,1,1,56,40,2 +Latin America,78,25,56,31,12,1,0 +Europe,15,5,0,1,99,0,0 +Africa,3,13,4,92,4,0,0 +Russia China South Asia,64,20,0,0,41,58,1 +Southeast Asia,39,12,78,9,13,0,0 +Australia Oceania,10,3,7,10,29,53,1 diff --git a/inst/extdata/ForestLossDrivers/mapping.csv b/inst/extdata/ForestLossDrivers/mapping.csv new file mode 100644 index 00000000..350efafc --- /dev/null +++ b/inst/extdata/ForestLossDrivers/mapping.csv @@ -0,0 +1,250 @@ +X;CountryCode;RegionCode +Angola;AGO;Africa +French Southern Territories;ATF;Africa +Burundi;BDI;Africa +Benin;BEN;Africa +Burkina Faso;BFA;Africa +Bouvet Island;BVT;Africa +Botswana;BWA;Africa +Central African Republic;CAF;Africa +Cote d Ivoire;CIV;Africa +Cameroon;CMR;Africa +Congo, the Democratic Republic of the;COD;Africa +Congo;COG;Africa +Comoros;COM;Africa +Cape Verde;CPV;Africa +Djibouti;DJI;Africa +Eritrea;ERI;Africa +Western Sahara;ESH;Africa +Ethiopia;ETH;Africa +Gabon;GAB;Africa +Ghana;GHA;Africa +Guinea;GIN;Africa +Gambia;GMB;Africa +Guinea-Bissau;GNB;Africa +Equatorial Guinea;GNQ;Africa +Kenya;KEN;Africa +Liberia;LBR;Africa +Lesotho;LSO;Africa +Madagascar;MDG;Africa +Mali;MLI;Africa +Mozambique;MOZ;Africa +Mauritania;MRT;Africa +Mauritius;MUS;Africa +Malawi;MWI;Africa +Mayotte;MYT;Africa +Namibia;NAM;Africa +Niger;NER;Africa +Nigeria;NGA;Africa +Rwanda;RWA;Africa +Sudan;SDN;Africa +Senegal;SEN;Africa +Saint Helena, Ascension and Tristan da Cunha;SHN;Africa +Sierra Leone;SLE;Africa +Somalia;SOM;Africa +South Sudan;SSD;Africa +Sao Tome and Principe;STP;Africa +Swaziland;SWZ;Africa +Seychelles;SYC;Africa +Chad;TCD;Africa +Togo;TGO;Africa +Tanzania, United Republic of;TZA;Africa +Uganda;UGA;Africa +South Africa;ZAF;Africa +Zambia;ZMB;Africa +Zimbabwe;ZWE;Africa +China;CHN;Russia China South Asia +Hong Kong;HKG;Russia China South Asia +Cambodia;KHM;Russia China South Asia +Lao People's Democratic Republic;LAO;Russia China South Asia +Macao;MAC;Russia China South Asia +Mongolia;MNG;Russia China South Asia +Taiwan, Province of China;TWN;Russia China South Asia +Viet Nam;VNM;Russia China South Asia +Aland Islands;ALA;Europe +Albania;ALB;Europe +Andorra;AND;Europe +Austria;AUT;Europe +Belgium;BEL;Europe +Bulgaria;BGR;Europe +Bosnia and Herzegovina;BIH;Europe +Switzerland;CHE;Europe +Cyprus;CYP;Europe +Czech Republic;CZE;Europe +Germany;DEU;Europe +Denmark;DNK;Europe +Spain;ESP;Europe +Estonia;EST;Europe +Finland;FIN;Europe +France;FRA;Europe +Faroe Islands;FRO;Europe +United Kingdom;GBR;Europe +Guernsey;GGY;Europe +Gibraltar;GIB;Europe +Greece;GRC;Europe +Greenland;GRL;Europe +Croatia;HRV;Europe +Hungary;HUN;Europe +Isle of Man;IMN;Europe +Ireland;IRL;Europe +Iceland;ISL;Europe +Italy;ITA;Europe +Jersey;JEY;Europe +Liechtenstein;LIE;Europe +Lithuania;LTU;Europe +Luxembourg;LUX;Europe +Latvia;LVA;Europe +Monaco;MCO;Europe +Macedonia, the former Yugoslav Republic of;MKD;Europe +Malta;MLT;Europe +Montenegro;MNE;Europe +Netherlands;NLD;Europe +Norway;NOR;Europe +Poland;POL;Europe +Portugal;PRT;Europe +Romania;ROU;Europe +Svalbard and Jan Mayen;SJM;Europe +San Marino;SMR;Europe +Serbia;SRB;Europe +Slovakia;SVK;Europe +Slovenia;SVN;Europe +Sweden;SWE;Europe +Turkey;TUR;Europe +Holy See (Vatican City State);VAT;Europe +Armenia;ARM;Russia China South Asia +Azerbaijan;AZE;Russia China South Asia +Belarus;BLR;Russia China South Asia +Georgia;GEO;Russia China South Asia +Kazakhstan;KAZ;Russia China South Asia +Kyrgyzstan;KGZ;Russia China South Asia +Moldova, Republic of;MDA;Russia China South Asia +Russian Federation;RUS;Russia China South Asia +Tajikistan;TJK;Russia China South Asia +Turkmenistan;TKM;Russia China South Asia +Ukraine;UKR;Russia China South Asia +Uzbekistan;UZB;Russia China South Asia +Aruba;ABW;Latin America +Anguilla;AIA;Latin America +Argentina;ARG;Latin America +Antarctica;ATA;Latin America +Antigua and Barbuda;ATG;Latin America +Bonaire, Sint Eustatius and Saba;BES;Latin America +Bahamas;BHS;Latin America +Saint Barthelemy;BLM;Latin America +Belize;BLZ;Latin America +Bolivia, Plurinational State of;BOL;Latin America +Brazil;BRA;Latin America +Barbados;BRB;Latin America +Chile;CHL;Latin America +Colombia;COL;Latin America +Costa Rica;CRI;Latin America +Cuba;CUB;Latin America +Curacao;CUW;Latin America +Cayman Islands;CYM;Latin America +Dominica;DMA;Latin America +Dominican Republic;DOM;Latin America +Ecuador;ECU;Latin America +Falkland Islands (Malvinas);FLK;Latin America +Guadeloupe;GLP;Latin America +Grenada;GRD;Latin America +Guatemala;GTM;Latin America +French Guiana;GUF;Latin America +Guyana;GUY;Latin America +Honduras;HND;Latin America +Haiti;HTI;Latin America +Jamaica;JAM;Latin America +Saint Kitts and Nevis;KNA;Latin America +Saint Lucia;LCA;Latin America +Saint Martin (French part);MAF;Latin America +Mexico;MEX;North America +Montserrat;MSR;Latin America +Martinique;MTQ;Latin America +Nicaragua;NIC;Latin America +Panama;PAN;Latin America +Peru;PER;Latin America +Paraguay;PRY;Latin America +South Georgia and the South Sandwich Islands;SGS;Latin America +El Salvador;SLV;Latin America +Suriname;SUR;Latin America +Sint Maarten (Dutch part);SXM;Latin America +Turks and Caicos Islands;TCA;Latin America +Trinidad and Tobago;TTO;Latin America +Uruguay;URY;Latin America +Saint Vincent and the Grenadines;VCT;Latin America +Venezuela, Bolivarian Republic of;VEN;Latin America +Virgin Islands, British;VGB;Latin America +Virgin Islands, U.S.;VIR;Latin America +United Arab Emirates;ARE;Latin America +Bahrain;BHR;Latin America +Algeria;DZA;Latin America +Egypt;EGY;Latin America +Iran, Islamic Republic of;IRN;Latin America +Iraq;IRQ;Latin America +Israel;ISR;Latin America +Jordan;JOR;Latin America +Kuwait;KWT;Latin America +Lebanon;LBN;Latin America +Libya;LBY;Latin America +Morocco;MAR;Latin America +Oman;OMN;Latin America +Palestine, State of;PSE;Latin America +Qatar;QAT;Latin America +Saudi Arabia;SAU;Latin America +Syrian Arab Republic;SYR;Latin America +Tunisia;TUN;Latin America +Yemen;YEM;Latin America +Bermuda;BMU;North America +Canada;CAN;North America +Puerto Rico;PRI;North America +Saint Pierre and Miquelon;SPM;North America +United States Minor Outlying Islands;UMI;North America +United States;USA;North America +American Samoa;ASM;Australia Oceania +Australia;AUS;Australia Oceania +Cocos (Keeling) Islands;CCK;Australia Oceania +Cook Islands;COK;Australia Oceania +Christmas Island;CXR;Australia Oceania +Heard Island and McDonald Islands;HMD;Australia Oceania +Japan;JPN;Australia Oceania +Norfolk Island;NFK;Australia Oceania +Niue;NIU;Australia Oceania +New Zealand;NZL;Australia Oceania +French Polynesia;PYF;Australia Oceania +Tokelau;TKL;Australia Oceania +Brunei Darussalam;BRN;Southeast Asia +Fiji;FJI;Southeast Asia +Micronesia, Federated States of;FSM;Southeast Asia +Guam;GUM;Southeast Asia +Indonesia;IDN;Southeast Asia +Kiribati;KIR;Southeast Asia +Korea, Republic of;KOR;Southeast Asia +Marshall Islands;MHL;Southeast Asia +Northern Mariana Islands;MNP;Southeast Asia +Malaysia;MYS;Southeast Asia +New Caledonia;NCL;Southeast Asia +Nauru;NRU;Southeast Asia +Pitcairn;PCN;Southeast Asia +Philippines;PHL;Southeast Asia +Palau;PLW;Southeast Asia +Papua New Guinea;PNG;Southeast Asia +Korea, Democratic People's Republic of;PRK;Southeast Asia +Singapore;SGP;Southeast Asia +Solomon Islands;SLB;Southeast Asia +Thailand;THA;Southeast Asia +Timor-Leste;TLS;Southeast Asia +Tonga;TON;Southeast Asia +Tuvalu;TUV;Southeast Asia +Vanuatu;VUT;Southeast Asia +Wallis and Futuna;WLF;Southeast Asia +Samoa;WSM;Southeast Asia +Afghanistan;AFG;Russia China South Asia +Bangladesh;BGD;Russia China South Asia +Bhutan;BTN;Russia China South Asia +India;IND;Russia China South Asia +British Indian Ocean Territory;IOT;Russia China South Asia +Sri Lanka;LKA;Russia China South Asia +Maldives;MDV;Russia China South Asia +Myanmar;MMR;Russia China South Asia +Nepal;NPL;Russia China South Asia +Pakistan;PAK;Russia China South Asia +Reunion;REU;Russia China South Asia diff --git a/man/downloadForestLossDrivers.Rd b/man/downloadForestLossDrivers.Rd new file mode 100644 index 00000000..50cafaf4 --- /dev/null +++ b/man/downloadForestLossDrivers.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/downloadForestLossDrivers.R +\name{downloadForestLossDrivers} +\alias{downloadForestLossDrivers} +\title{downloadForestLossDrivers} +\usage{ +downloadForestLossDrivers() +} +\description{ +Provisions the ForestLossDrivers source: the regional tree cover loss +totals and driver shares of Curtis et al. (2018), Table 1, plus the country to +source-region mapping used to disaggregate them. +} +\details{ +Table 1 is published only inside the article; there is no machine-readable release of +it. The table is therefore held as version-controlled package data under +\code{inst/extdata/ForestLossDrivers} and staged into the madrat source folder here, so +that \code{readSource("ForestLossDrivers")} works on a machine that has never seen the +shared source directory, and so that any change to the numbers is a reviewable diff +rather than a silent edit of a shared file. +} +\seealso{ +\code{\link[=readForestLossDrivers]{readForestLossDrivers()}} +} +\author{ +Michael Crawford +} From 5b69ab0495ec2c31aebe2687c2f8aa23cbb2ee69 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 10:30:33 +0200 Subject: [PATCH 02/25] Fix Africa tree cover loss (3 -> 39 Mha) and guard the table on read Table 1 of Curtis et al. (2018) gives Africa 39 Mha over 2001-2015; the transcription has carried 3 since December 2020. Africa is 92% shifting agriculture, the column MAgPIE reads by default, so the SSA rate in f35_forest_lost_share has been 13x too low in every input revision since. Two checks confirm 39: the regional rows sum to 315 against the printed global 314 (279 with 3), and the unused share column implies 12.4% (1.1% with 3). Southeast Asia's share 12 -> 13 in the same table (unused). checkCurtisTable1() enforces these identities on every read; verified against the corrupt table as a positive test. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n --- R/readForestLossDrivers.R | 32 +++++++++++++++++++ .../extdata/ForestLossDrivers/forest_loss.csv | 4 +-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/R/readForestLossDrivers.R b/R/readForestLossDrivers.R index 6e4c9821..2af355ca 100644 --- a/R/readForestLossDrivers.R +++ b/R/readForestLossDrivers.R @@ -18,6 +18,37 @@ #' @import countrycode #' @importFrom stats complete.cases +# Table 1 of Curtis et al. (2018) is entered by hand (see +# inst/extdata/ForestLossDrivers/SOURCE.md for the transcription record). These are the +# identities the printed table satisfies and that a transcription error breaks. They are +# checked on every read because a wrong cell here is otherwise invisible: it does not +# fail, it just becomes a wrong forest disturbance rate in MAgPIE. +checkCurtisTable1 <- function(df) { + globalLoss <- 314 # Mha 2001-2015, "Global" row of Table 1 + drivers <- c("deforestation", "shifting_agriculture", "forestry", "wildfire", "urbanization") + + total <- sum(df$treecoverloss_01_15) + if (abs(total - globalLoss) > 2) { + stop("ForestLossDrivers: regional tree cover loss sums to ", total, " Mha, but Table 1 of ", + "Curtis et al. (2018) reports a global total of ", globalLoss, " Mha. forest_loss.csv is corrupt.") + } + + offBy <- abs(rowSums(df[drivers]) - 100) + if (any(offBy > 2)) { + stop("ForestLossDrivers: driver shares do not sum to 100 per cent for ", + toString(df$region[offBy > 2]), " in forest_loss.csv.") + } + + implied <- 100 * df$treecoverloss_01_15 / total + mismatch <- abs(implied - df$treecoverloss_pc_01_15) > 2 + if (any(mismatch)) { + stop("ForestLossDrivers: absolute and relative tree cover loss disagree for ", + toString(df$region[mismatch]), " in forest_loss.csv.") + } + + return(invisible(df)) +} + readForestLossDrivers <- function() { ## Mapping file mapping <- read.csv("mapping.csv", header = TRUE, sep = ";") @@ -33,6 +64,7 @@ readForestLossDrivers <- function() { file <- "forest_loss.csv" # This is Tree Cover loss in Mha at this stage, all drivers are in percentage df <- read.csv(file = file, header = TRUE, sep = ",") + checkCurtisTable1(df) # Division by 100 because we change from percentage to proportion. Division by 15 to get annual data df[, -c(1:3)] <- (df$treecoverloss_01_15 * (df[, -c(1:3)] / 100)) / 15 diff --git a/inst/extdata/ForestLossDrivers/forest_loss.csv b/inst/extdata/ForestLossDrivers/forest_loss.csv index 4f207398..017092ad 100644 --- a/inst/extdata/ForestLossDrivers/forest_loss.csv +++ b/inst/extdata/ForestLossDrivers/forest_loss.csv @@ -2,7 +2,7 @@ region,treecoverloss_01_15,treecoverloss_pc_01_15,deforestation,shifting_agricul North America,70,21,1,1,56,40,2 Latin America,78,25,56,31,12,1,0 Europe,15,5,0,1,99,0,0 -Africa,3,13,4,92,4,0,0 +Africa,39,13,4,92,4,0,0 Russia China South Asia,64,20,0,0,41,58,1 -Southeast Asia,39,12,78,9,13,0,0 +Southeast Asia,39,13,78,9,13,0,0 Australia Oceania,10,3,7,10,29,53,1 From 2524e25dd5ef1fcdf530a36871c0cc7fbb5913f6 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 10:36:07 +0200 Subject: [PATCH 03/25] Reassign MENA out of the Latin America source region, and Europe's 1% to wildfire Optional relative to the Africa fix; drop this commit for a transcription-only PR. mapping.csv was derived from regionmappingH12.csv and the edit over-ran: all 19 MEA countries sat in "Latin America". North Africa moves to Africa, the Middle East to "Russia China South Asia", Reunion to Africa, and SOURCE.md says so. Europe's 1% moves from shifting agriculture to wildfire, as printed. Effect on f35_forest_lost_share: MEA deforestation 0.0016797 -> 0.0001070 MEA wildfire 0.0000286 -> 0.0006953 EUR/NEU shifting 6.8e-05 -> 0 SSA shifting 0.0036349 -> 0.0035955 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- inst/extdata/ForestLossDrivers/SOURCE.md | 12 +++--- .../extdata/ForestLossDrivers/forest_loss.csv | 2 +- inst/extdata/ForestLossDrivers/mapping.csv | 40 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/inst/extdata/ForestLossDrivers/SOURCE.md b/inst/extdata/ForestLossDrivers/SOURCE.md index a548780f..aa727eef 100644 --- a/inst/extdata/ForestLossDrivers/SOURCE.md +++ b/inst/extdata/ForestLossDrivers/SOURCE.md @@ -68,9 +68,9 @@ makes the error unambiguous. `mapping.csv` assigns each ISO3 country to one of the seven Curtis source regions. It was built from `madrat::regionmappingH12` (identical country set, 249 rows, same `;`-delimited -`X;CountryCode;RegionCode` layout) by rewriting the region column, and it carries at least -one artefact of that edit: the whole MEA block (ARE, BHR, DZA, EGY, IRN, IRQ, ISR, JOR, -KWT, LBN, LBY, MAR, OMN, PSE, QAT, SAU, SYR, TUN, YEM) is assigned to `Latin America`. -See the repository history for whether that has been corrected; the affected countries -hold very little natural forest, so the numerical consequence is small, but the -assignment is not defensible on its face. +`X;CountryCode;RegionCode` layout) by rewriting the region column. The article does not list +countries per region, so the assignment is geographic. Corrected in mrland 0.76.0: the +original put the Middle East and North Africa block in `Latin America`. DZA, EGY, LBY, MAR +and TUN now map to `Africa`; ARE, BHR, IRN, IRQ, ISR, JOR, KWT, LBN, OMN, PSE, QAT, SAU, +SYR and YEM to `Russia China South Asia`; REU moves from `Russia China South Asia` to +`Africa`. diff --git a/inst/extdata/ForestLossDrivers/forest_loss.csv b/inst/extdata/ForestLossDrivers/forest_loss.csv index 017092ad..f845215e 100644 --- a/inst/extdata/ForestLossDrivers/forest_loss.csv +++ b/inst/extdata/ForestLossDrivers/forest_loss.csv @@ -1,7 +1,7 @@ region,treecoverloss_01_15,treecoverloss_pc_01_15,deforestation,shifting_agriculture,forestry,wildfire,urbanization North America,70,21,1,1,56,40,2 Latin America,78,25,56,31,12,1,0 -Europe,15,5,0,1,99,0,0 +Europe,15,5,0,0,99,1,0 Africa,39,13,4,92,4,0,0 Russia China South Asia,64,20,0,0,41,58,1 Southeast Asia,39,13,78,9,13,0,0 diff --git a/inst/extdata/ForestLossDrivers/mapping.csv b/inst/extdata/ForestLossDrivers/mapping.csv index 350efafc..29eaa386 100644 --- a/inst/extdata/ForestLossDrivers/mapping.csv +++ b/inst/extdata/ForestLossDrivers/mapping.csv @@ -174,25 +174,25 @@ Saint Vincent and the Grenadines;VCT;Latin America Venezuela, Bolivarian Republic of;VEN;Latin America Virgin Islands, British;VGB;Latin America Virgin Islands, U.S.;VIR;Latin America -United Arab Emirates;ARE;Latin America -Bahrain;BHR;Latin America -Algeria;DZA;Latin America -Egypt;EGY;Latin America -Iran, Islamic Republic of;IRN;Latin America -Iraq;IRQ;Latin America -Israel;ISR;Latin America -Jordan;JOR;Latin America -Kuwait;KWT;Latin America -Lebanon;LBN;Latin America -Libya;LBY;Latin America -Morocco;MAR;Latin America -Oman;OMN;Latin America -Palestine, State of;PSE;Latin America -Qatar;QAT;Latin America -Saudi Arabia;SAU;Latin America -Syrian Arab Republic;SYR;Latin America -Tunisia;TUN;Latin America -Yemen;YEM;Latin America +United Arab Emirates;ARE;Russia China South Asia +Bahrain;BHR;Russia China South Asia +Algeria;DZA;Africa +Egypt;EGY;Africa +Iran, Islamic Republic of;IRN;Russia China South Asia +Iraq;IRQ;Russia China South Asia +Israel;ISR;Russia China South Asia +Jordan;JOR;Russia China South Asia +Kuwait;KWT;Russia China South Asia +Lebanon;LBN;Russia China South Asia +Libya;LBY;Africa +Morocco;MAR;Africa +Oman;OMN;Russia China South Asia +Palestine, State of;PSE;Russia China South Asia +Qatar;QAT;Russia China South Asia +Saudi Arabia;SAU;Russia China South Asia +Syrian Arab Republic;SYR;Russia China South Asia +Tunisia;TUN;Africa +Yemen;YEM;Russia China South Asia Bermuda;BMU;North America Canada;CAN;North America Puerto Rico;PRI;North America @@ -247,4 +247,4 @@ Maldives;MDV;Russia China South Asia Myanmar;MMR;Russia China South Asia Nepal;NPL;Russia China South Asia Pakistan;PAK;Russia China South Asia -Reunion;REU;Russia China South Asia +Reunion;REU;Africa From 0b54a9b9d7560a47ebf4e44a78cd7a03d77249cc Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 10:38:47 +0200 Subject: [PATCH 04/25] Bump version to 0.76.0 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n --- DESCRIPTION | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 6ea37581..b9843269 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: mrland Title: MadRaT land data package -Version: 0.75.4 -Date: 2026-08-26 +Version: 0.76.0 +Date: 2026-09-10 Authors@R: c( person("Jan Philipp", "Dietrich", , "dietrich@pik-potsdam.de", role = c("aut", "cre")), person("Abhijeet", "Mishra", role = "aut"), From 95ceecb28dfbce0c8a92d192475196c5492270bb Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 12:48:36 +0200 Subject: [PATCH 05/25] Restore readForestLossDrivers's own man page checkCurtisTable1() sat between the roxygen block and the function it documents, so roxygen attached the block to the helper. Helper moved above the block; man/ regenerated. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n --- R/readForestLossDrivers.R | 48 +++++++++++++++----------------- man/downloadForestLossDrivers.Rd | 23 +++++++++++---- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/R/readForestLossDrivers.R b/R/readForestLossDrivers.R index 2af355ca..2645dff0 100644 --- a/R/readForestLossDrivers.R +++ b/R/readForestLossDrivers.R @@ -1,28 +1,6 @@ -#' Read ForestLossDrivers -#' -#' Read-in an Forest loss data (range 2001-2015 but only single annual number her) -#' (Source:DOI: 10.1126/science.aau3445 Table 1). -#' -#' -#' @return magpie object of the Curtis et al., 2018 Data -#' @author Abhijeet Mishra -#' @seealso \code{\link[madrat]{readSource}} -#' @examples -#' \dontrun{ -#' a <- readSource("ForestLossDrivers") -#' } -#' -#' @importFrom magclass as.magpie -#' @importFrom madrat toolSubtypeSelect -#' @import readxl -#' @import countrycode -#' @importFrom stats complete.cases - -# Table 1 of Curtis et al. (2018) is entered by hand (see -# inst/extdata/ForestLossDrivers/SOURCE.md for the transcription record). These are the -# identities the printed table satisfies and that a transcription error breaks. They are -# checked on every read because a wrong cell here is otherwise invisible: it does not -# fail, it just becomes a wrong forest disturbance rate in MAgPIE. +# Table 1 of Curtis et al. (2018) is hand-typed (inst/extdata/ForestLossDrivers/SOURCE.md). +# These identities of the printed table are checked on every read; a wrong cell does not +# fail otherwise, it becomes a wrong disturbance rate. checkCurtisTable1 <- function(df) { globalLoss <- 314 # Mha 2001-2015, "Global" row of Table 1 drivers <- c("deforestation", "shifting_agriculture", "forestry", "wildfire", "urbanization") @@ -49,6 +27,26 @@ checkCurtisTable1 <- function(df) { return(invisible(df)) } +#' Read ForestLossDrivers +#' +#' Read-in an Forest loss data (range 2001-2015 but only single annual number her) +#' (Source:DOI: 10.1126/science.aau3445 Table 1). +#' +#' +#' @return magpie object of the Curtis et al., 2018 Data +#' @author Abhijeet Mishra +#' @seealso \code{\link[madrat]{readSource}} +#' @examples +#' \dontrun{ +#' a <- readSource("ForestLossDrivers") +#' } +#' +#' @importFrom magclass as.magpie +#' @importFrom madrat toolSubtypeSelect +#' @import readxl +#' @import countrycode +#' @importFrom stats complete.cases + readForestLossDrivers <- function() { ## Mapping file mapping <- read.csv("mapping.csv", header = TRUE, sep = ";") diff --git a/man/downloadForestLossDrivers.Rd b/man/downloadForestLossDrivers.Rd index 50cafaf4..0836d9be 100644 --- a/man/downloadForestLossDrivers.Rd +++ b/man/downloadForestLossDrivers.Rd @@ -10,17 +10,28 @@ downloadForestLossDrivers() Provisions the ForestLossDrivers source: the regional tree cover loss totals and driver shares of Curtis et al. (2018), Table 1, plus the country to source-region mapping used to disaggregate them. -} -\details{ + Table 1 is published only inside the article; there is no machine-readable release of it. The table is therefore held as version-controlled package data under -\code{inst/extdata/ForestLossDrivers} and staged into the madrat source folder here, so -that \code{readSource("ForestLossDrivers")} works on a machine that has never seen the +`inst/extdata/ForestLossDrivers` and staged into the madrat source folder here, so +that `readSource("ForestLossDrivers")` works on a machine that has never seen the shared source directory, and so that any change to the numbers is a reviewable diff -rather than a silent edit of a shared file. +rather than a silent edit of a shared file. The transcription record, including the +known deviations from the printed table, is in +`inst/extdata/ForestLossDrivers/SOURCE.md`. + +The underlying gridded product - the 10 km dominant-driver classification - is +distributed separately by Global Forest Watch under CC BY 4.0 as a tiled raster set +(`tsc_tree_cover_loss_drivers`). It is deliberately not fetched here: deriving the +regional totals from the raster would be a change of method, not a change of source. +} +\examples{ +\dontrun{ +madrat::downloadSource("ForestLossDrivers") +} } \seealso{ -\code{\link[=readForestLossDrivers]{readForestLossDrivers()}} +[readForestLossDrivers()] } \author{ Michael Crawford From 9dd6a59039af105c921d2d90ac09be4d6f0ae945 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 12:49:09 +0200 Subject: [PATCH 06/25] Add GFWLossByDriver: tree cover loss by country, year and driver New madrat source: GFW data-api table gadm__tcl__iso_change, UMD/Hansen annual tree cover loss crossed with the Sims et al. (2025) 1 km driver map. Country level, 2001-2025, eight classes (the paper's seven plus Unknown, which the data carries). download: pinned immutable dataset version, 30% canopy threshold, open /download/csv endpoint, no credential. read: checks schema, class set, single threshold, and that the driver file sums to a separately queried totals file. convert: XKO/ZNC/XAD folded onto SRB/CYP, three GADM placeholders dropped (0.031% of loss), 0.5% drop tolerance. Tests are synthesized known-bug cases. Driver shares match Sims table 3 within 3.5 pp except Africa and Oceania: the cross-tab uses the annually updated map (v1.13, 2001-2025), not the paper's. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- NAMESPACE | 2 + R/convertGFWLossByDriver.R | 59 +++++++++++ R/downloadGFWLossByDriver.R | 135 ++++++++++++++++++++++++++ R/readGFWLossByDriver.R | 127 ++++++++++++++++++++++++ man/convertGFWLossByDriver.Rd | 31 ++++++ man/downloadGFWLossByDriver.Rd | 35 +++++++ man/readGFWLossByDriver.Rd | 30 ++++++ tests/testthat/test-GFWLossByDriver.R | 120 +++++++++++++++++++++++ 8 files changed, 539 insertions(+) create mode 100644 R/convertGFWLossByDriver.R create mode 100644 R/downloadGFWLossByDriver.R create mode 100644 R/readGFWLossByDriver.R create mode 100644 man/convertGFWLossByDriver.Rd create mode 100644 man/downloadGFWLossByDriver.Rd create mode 100644 man/readGFWLossByDriver.Rd create mode 100644 tests/testthat/test-GFWLossByDriver.R diff --git a/NAMESPACE b/NAMESPACE index 13a0e506..21353ce2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -136,8 +136,10 @@ importFrom(terra,rast) importFrom(terra,segregate) importFrom(terra,terraOptions) importFrom(terra,tmpFiles) +importFrom(utils,URLencode) importFrom(utils,bibentry) importFrom(utils,download.file) +importFrom(utils,head) importFrom(utils,person) importFrom(utils,read.csv) importFrom(utils,read.csv2) diff --git a/R/convertGFWLossByDriver.R b/R/convertGFWLossByDriver.R new file mode 100644 index 00000000..ff3742a3 --- /dev/null +++ b/R/convertGFWLossByDriver.R @@ -0,0 +1,59 @@ +# GADM codes that are not madrat ISO countries but sit inside one, so their loss is added +# to the host rather than discarded. Measured over 2001-2025 at a 30 per cent threshold: +# XKO 19 126 ha, ZNC 1740 ha, XAD 80 ha. +gfwIsoHosts <- c(XKO = "SRB", # Kosovo, inside Serbia in madrat's country set + ZNC = "CYP", # Northern Cyprus + XAD = "CYP") # Akrotiri and Dhekelia, the sovereign base areas on Cyprus + +# Undocumented GADM placeholders for disputed territories, dropped rather than guessed at: +# 169 451 ha together, 0.031 per cent of global loss 2001-2025. +gfwIsoDropped <- c("Z01", "Z06", "Z07") + +#' @title convertGFWLossByDriver +#' +#' @description Reconciles the GADM country set used by GFW with madrat's ISO set. +#' +#' GADM and madrat disagree on 6 codes carrying loss and on 39 madrat countries GFW does +#' not list; both are handled explicitly rather than by a silent inner join. +#' +#' @param x magpie object as returned by [readGFWLossByDriver()] +#' @return magpie object on madrat's ISO country set, unit Mha +#' @author Michael Crawford +#' @importFrom madrat toolCountryFill +#' @importFrom magclass getItems +#' @seealso [readGFWLossByDriver()] +#' @examples +#' \dontrun{ +#' a <- readSource("GFWLossByDriver", convert = TRUE) +#' } + +convertGFWLossByDriver <- function(x) { + + before <- sum(x) + + for (code in names(gfwIsoHosts)) { + host <- gfwIsoHosts[[code]] + if (code %in% getItems(x, 1)) { + if (host %in% getItems(x, 1)) { + x[host, , ] <- x[host, , ] + x[code, , ] + } else { + getItems(x, 1)[getItems(x, 1) == code] <- host + next + } + x <- x[setdiff(getItems(x, 1), code), , ] + } + } + + out <- toolCountryFill(x, fill = 0, no_remove_warning = gfwIsoDropped) + + # A future version moving real loss onto a code madrat does not know must fail here. + lost <- (before - sum(out)) / before + if (lost > 0.005) { + stop("GFWLossByDriver: reconciling GADM with madrat's ISO set dropped ", + round(100 * lost, 3), " per cent of global loss, above the 0.5 per cent tolerance. ", + "Codes not in madrat: ", + toString(setdiff(getItems(x, 1), madrat::getISOlist())), ".") + } + + return(out) +} diff --git a/R/downloadGFWLossByDriver.R b/R/downloadGFWLossByDriver.R new file mode 100644 index 00000000..b4c8fe7a --- /dev/null +++ b/R/downloadGFWLossByDriver.R @@ -0,0 +1,135 @@ +gfwDataset <- "gadm__tcl__iso_change" +gfwVersion <- "v20260424" + +# Canopy density in 2000 (per cent) defining forest for the loss figures; 30 is GFW's +# convention. Not a detail: global loss 2001-2025 is 647 Mha at 0 per cent and 334 at 75. +# Dataset, version and threshold are pinned here only; the read side takes the threshold +# from the data. +gfwCanopyThreshold <- 30 + +#' @title downloadGFWLossByDriver +#' +#' @description Downloads tree cover loss by country, year and dominant driver from the +#' Global Forest Watch data-api table `gadm__tcl__iso_change`, which cross-tabulates the +#' UMD/Hansen annual tree cover loss product with the WRI/Google DeepMind 1 km +#' dominant-driver classification of Sims et al. (2025). +#' +#' Route notes: +#' * `/dataset/{d}/{v}/download/csv` is open and honours the full SQL (`GROUP BY` included); +#' only `/query` needs an API key. +#' * Dataset versions are immutable (`is_mutable: false`), so the pinned URL is the +#' reproducibility mechanism. No checksum, as elsewhere in the stack. +#' * The driver map is the annually updated `wri_google_tree_cover_loss_drivers` v1.13 +#' (2001-2025), not the paper's v20241121 (2001-2022). Regional shares therefore do not +#' reproduce the paper's tables; Africa's permanent/shifting split moved by about 11 pp. +#' * Versions are listed under `data.versions`; `is_latest` is `false` on all of them. +#' +#' @author Michael Crawford +#' @importFrom utils download.file URLencode read.csv bibentry person +#' @importFrom withr local_options +#' @seealso [readGFWLossByDriver()] +#' @examples +#' \dontrun{ +#' madrat::downloadSource("GFWLossByDriver") +#' } + +downloadGFWLossByDriver <- function() { + + dims <- paste("iso, umd_tree_cover_loss__year AS year,", + "umd_tree_cover_density_2000__threshold AS threshold") + where <- paste("FROM data WHERE umd_tree_cover_density_2000__threshold =", + gfwCanopyThreshold) + + # Two aggregations of the same table; the read side checks that the first sums to the + # second, which is what makes a truncated driver file fail rather than pass. + queries <- list( + "loss_by_driver.csv" = paste("SELECT", dims, + ", wri_google_tree_cover_loss_drivers__driver AS driver,", + "SUM(umd_tree_cover_loss__ha) AS loss_ha", where, + "GROUP BY iso, year, threshold, driver"), + "loss_totals.csv" = paste("SELECT", dims, + ", SUM(umd_tree_cover_loss__ha) AS loss_ha", where, + "GROUP BY iso, year, threshold") + ) + headers <- c( + "loss_by_driver.csv" = "\"iso\",\"year\",\"threshold\",\"driver\",\"loss_ha\"", + "loss_totals.csv" = "\"iso\",\"year\",\"threshold\",\"loss_ha\"" + ) + + # madrat::downloadSource() runs this under warn = 2: keep it warning-clean. + local_options(timeout = max(3e6, getOption("timeout"))) + + for (f in names(queries)) { + url <- paste0("https://data-api.globalforestwatch.org/dataset/", gfwDataset, "/", + gfwVersion, "/download/csv?sql=", + URLencode(queries[[f]], reserved = TRUE)) + download.file(url, f, quiet = TRUE, mode = "wb") + + # download.file() errors on HTTP 400/500; any other non-CSV body is caught here. + header <- readLines(f, n = 1, warn = FALSE) + if (!identical(header, unname(headers[f]))) { + stop("GFWLossByDriver: the endpoint did not return the expected CSV for ", f, + ". First line was: ", substr(header, 1, 200)) + } + + # Did we get the threshold we asked for? Checked here, where the pin is defined. + got <- unique(read.csv(f, stringsAsFactors = FALSE)$threshold) + if (!identical(got, as.integer(gfwCanopyThreshold))) { + stop("GFWLossByDriver: asked for canopy threshold ", gfwCanopyThreshold, " but ", f, + " carries ", toString(got), ".") + } + } + + authors <- c(person(c("Michelle", "J."), "Sims"), + person("Radost", "Stanimirova"), + person("Anton", "Raichuk"), + person("Maxim", "Neumann"), + person("Jessica", "Richter"), + person("Forrest", "Follett"), + person("James", "MacCarthy"), + person("Kristine", "Lister"), + person("Christopher", "Randle"), + person("Lindsey", "Sloat"), + person("Elizabeth", "Esipova"), + person("Jaelah", "Jupiter"), + person("Charlotte", "Stanton"), + person("Drew", "Morris"), + person(c("Christy", "M."), "Slay"), + person("Drew", "Purves"), + person("Nancy", "Harris")) + + description <- paste("Annual UMD/Hansen tree cover loss 2001-2025, in hectares,", + "cross-tabulated by country and by the WRI/Google DeepMind 1 km", + "dominant-driver class, at a", gfwCanopyThreshold, "per cent canopy", + "density threshold. Eight driver classes are present: the seven of", + "Sims et al. (2025) plus 'Unknown'. NOTE: the driver map underlying", + "this extract is the annually updated 2001-2025 version", + "(wri_google_tree_cover_loss_drivers v1.13), not the 2001-2022", + "version published with the paper, so regional driver shares do not", + "reproduce the paper's tables.") + + reference <- bibentry("Article", + title = "Global drivers of forest loss at 1 km resolution", + author = authors, + year = "2025", + journal = "Environmental Research Letters", + volume = "20", + number = "7", + pages = "074027", + doi = "10.1088/1748-9326/add606") + + base <- paste0("https://data-api.globalforestwatch.org/dataset/", gfwDataset, "/", + gfwVersion) + + return(list(title = paste("Tree cover loss by country, year and dominant driver", + "(GFW gadm__tcl__iso_change)"), + description = description, + author = authors, + doi = "10.1088/1748-9326/add606", + url = base, + license = "CC BY 4.0", + version = paste0(gfwDataset, " ", gfwVersion, " (drivers map ", + "wri_google_tree_cover_loss_drivers v1.13, 2001-2025)"), + unit = "ha of tree cover loss per country, year and driver", + reference = reference)) +} diff --git a/R/readGFWLossByDriver.R b/R/readGFWLossByDriver.R new file mode 100644 index 00000000..054d8292 --- /dev/null +++ b/R/readGFWLossByDriver.R @@ -0,0 +1,127 @@ +# Driver class strings as they occur in the data, and their GAMS-safe names. Eight, not the +# paper's seven: the data carries `Unknown` (0.3 per cent of global loss). +gfwDriverClasses <- c("Permanent agriculture" = "permanent_agriculture", + "Hard commodities" = "hard_commodities", + "Shifting cultivation" = "shifting_cultivation", + "Logging" = "logging", + "Wildfire" = "wildfire", + "Settlements & Infrastructure" = "settlements_infrastructure", + "Other natural disturbances" = "other_natural_disturbances", + "Unknown" = "unknown") + +# Global loss 2001-2025 in Mha for the dataset version and threshold pinned in +# downloadGFWLossByDriver(). Versions are immutable; update this deliberately after a re-pin. +gfwGlobalLossMha <- 542.81 + +# Integrity checks on the extract. These reconcile the file against its own schema, a second +# query of the same table (loss_totals.csv) and a pinned snapshot, not against an external +# source: a truncated download, a renamed class or a mixed threshold would otherwise become a +# plausible disturbance rate rather than an error. +checkGFWLossByDriver <- function(df, totals) { + + required <- c("iso", "year", "threshold", "driver", "loss_ha") + if (!all(required %in% names(df))) { + stop("GFWLossByDriver: loss_by_driver.csv is missing column(s) ", + toString(setdiff(required, names(df))), ".") + } + + observed <- sort(unique(df$driver)) + expected <- sort(names(gfwDriverClasses)) + if (!identical(observed, expected)) { + stop("GFWLossByDriver: the driver classes in the data do not match the class map. ", + "Only in data: ", toString(setdiff(observed, expected)), ". ", + "Only in map: ", toString(setdiff(expected, observed)), ". ", + "Re-run the preflight and update gfwDriverClasses before trusting any output.") + } + + if (length(unique(df$threshold)) != 1L) { + stop("GFWLossByDriver: the extract mixes canopy thresholds ", + toString(sort(unique(df$threshold))), + ". It must carry exactly one, the value pinned in downloadGFWLossByDriver().") + } + + years <- sort(unique(df$year)) + if (!identical(years, seq(min(years), max(years)))) { + stop("GFWLossByDriver: the year series ", min(years), "-", max(years), + " has gaps at ", toString(setdiff(seq(min(years), max(years)), years)), ".") + } + + if (anyNA(df$loss_ha) || any(df$loss_ha < 0)) { + stop("GFWLossByDriver: loss_ha contains ", sum(is.na(df$loss_ha)), " missing and ", + sum(df$loss_ha < 0, na.rm = TRUE), " negative values.") + } + + if (length(unique(df$iso)) < 200) { + stop("GFWLossByDriver: only ", length(unique(df$iso)), " countries in the extract; ", + "the full table carries 216. The download is probably truncated.") + } + + # The driver file summed over drivers must reproduce the separately queried totals file. + byDriver <- stats::aggregate(list(drv = df$loss_ha), + by = list(iso = df$iso, year = df$year), FUN = sum) + both <- merge(byDriver, totals[, c("iso", "year", "loss_ha")], + by = c("iso", "year"), all = TRUE) + if (anyNA(both$drv) || anyNA(both$loss_ha)) { + onlyTot <- both[is.na(both$drv), c("iso", "year")] + onlyDrv <- both[is.na(both$loss_ha), c("iso", "year")] + stop("GFWLossByDriver: the driver file and the totals file do not cover the same ", + "country-years. ", nrow(onlyTot), " only in totals (e.g. ", + toString(utils::head(paste(onlyTot$iso, onlyTot$year), 5)), "), ", + nrow(onlyDrv), " only in the driver file (e.g. ", + toString(utils::head(paste(onlyDrv$iso, onlyDrv$year), 5)), ").") + } + off <- abs(both$drv - both$loss_ha) > 1e-6 * pmax(both$loss_ha, 1) + if (any(off)) { + stop("GFWLossByDriver: driver shares do not sum to the country total for ", sum(off), + " country-years, e.g. ", + toString(utils::head(paste0(both$iso[off], " ", both$year[off], " (", + signif(both$drv[off], 6), " vs ", + signif(both$loss_ha[off], 6), " ha)"), 3)), ".") + } + + total <- sum(df$loss_ha) / 1e6 + if (abs(total - gfwGlobalLossMha) > 0.05 * gfwGlobalLossMha) { + stop("GFWLossByDriver: global loss sums to ", round(total, 1), " Mha, but ", + gfwGlobalLossMha, " Mha is expected for the pinned dataset version and canopy ", + "threshold. Either the download is incomplete, or the pin in ", + "downloadGFWLossByDriver() has changed - re-run the preflight before updating ", + "gfwGlobalLossMha.") + } + + return(invisible(df)) +} + +#' @title readGFWLossByDriver +#' +#' @description Reads the GFW tree cover loss by driver extract into a magpie object with +#' dimensions country x year x driver, in Mha of tree cover loss per year. +#' +#' @details The canopy threshold is pinned in [downloadGFWLossByDriver()] and stamped into +#' every row; this function only insists that the extract carries exactly one. +#' +#' @return magpie object, ISO country x 2001..2025 x eight driver classes, unit Mha +#' @author Michael Crawford +#' @importFrom magclass as.magpie magpiesort +#' @importFrom utils read.csv head +#' @seealso [downloadGFWLossByDriver()], [convertGFWLossByDriver()] +#' @examples +#' \dontrun{ +#' a <- readSource("GFWLossByDriver") +#' } + +readGFWLossByDriver <- function() { + + df <- read.csv("loss_by_driver.csv", stringsAsFactors = FALSE) + totals <- read.csv("loss_totals.csv", stringsAsFactors = FALSE) + checkGFWLossByDriver(df, totals) + + df$driver <- unname(gfwDriverClasses[df$driver]) + df$loss <- df$loss_ha / 1e6 # hectares to Mha + + x <- as.magpie(df[, c("iso", "year", "driver", "loss")], spatial = 1, temporal = 2) + + # Absent country-year-driver combinations carry no loss: zero, not NA. + x[is.na(x)] <- 0 + + return(magpiesort(x)) +} diff --git a/man/convertGFWLossByDriver.Rd b/man/convertGFWLossByDriver.Rd new file mode 100644 index 00000000..92f5cd24 --- /dev/null +++ b/man/convertGFWLossByDriver.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/convertGFWLossByDriver.R +\name{convertGFWLossByDriver} +\alias{convertGFWLossByDriver} +\title{convertGFWLossByDriver} +\usage{ +convertGFWLossByDriver(x) +} +\arguments{ +\item{x}{magpie object as returned by [readGFWLossByDriver()]} +} +\value{ +magpie object on madrat's ISO country set, unit Mha +} +\description{ +Reconciles the GADM country set used by GFW with madrat's ISO set. + +GADM and madrat disagree on 6 codes carrying loss and on 39 madrat countries GFW does +not list; both are handled explicitly rather than by a silent inner join. +} +\examples{ +\dontrun{ +a <- readSource("GFWLossByDriver", convert = TRUE) +} +} +\seealso{ +[readGFWLossByDriver()] +} +\author{ +Michael Crawford +} diff --git a/man/downloadGFWLossByDriver.Rd b/man/downloadGFWLossByDriver.Rd new file mode 100644 index 00000000..dc7d558b --- /dev/null +++ b/man/downloadGFWLossByDriver.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/downloadGFWLossByDriver.R +\name{downloadGFWLossByDriver} +\alias{downloadGFWLossByDriver} +\title{downloadGFWLossByDriver} +\usage{ +downloadGFWLossByDriver() +} +\description{ +Downloads tree cover loss by country, year and dominant driver from the +Global Forest Watch data-api table `gadm__tcl__iso_change`, which cross-tabulates the +UMD/Hansen annual tree cover loss product with the WRI/Google DeepMind 1 km +dominant-driver classification of Sims et al. (2025). + +Route notes: +* `/dataset/{d}/{v}/download/csv` is open and honours the full SQL (`GROUP BY` included); + only `/query` needs an API key. +* Dataset versions are immutable (`is_mutable: false`), so the pinned URL is the + reproducibility mechanism. No checksum, as elsewhere in the stack. +* The driver map is the annually updated `wri_google_tree_cover_loss_drivers` v1.13 + (2001-2025), not the paper's v20241121 (2001-2022). Regional shares therefore do not + reproduce the paper's tables; Africa's permanent/shifting split moved by about 11 pp. +* Versions are listed under `data.versions`; `is_latest` is `false` on all of them. +} +\examples{ +\dontrun{ +madrat::downloadSource("GFWLossByDriver") +} +} +\seealso{ +[readGFWLossByDriver()] +} +\author{ +Michael Crawford +} diff --git a/man/readGFWLossByDriver.Rd b/man/readGFWLossByDriver.Rd new file mode 100644 index 00000000..a059e512 --- /dev/null +++ b/man/readGFWLossByDriver.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/readGFWLossByDriver.R +\name{readGFWLossByDriver} +\alias{readGFWLossByDriver} +\title{readGFWLossByDriver} +\usage{ +readGFWLossByDriver() +} +\value{ +magpie object, ISO country x 2001..2025 x eight driver classes, unit Mha +} +\description{ +Reads the GFW tree cover loss by driver extract into a magpie object with +dimensions country x year x driver, in Mha of tree cover loss per year. +} +\details{ +The canopy threshold is pinned in [downloadGFWLossByDriver()] and stamped into +every row; this function only insists that the extract carries exactly one. +} +\examples{ +\dontrun{ +a <- readSource("GFWLossByDriver") +} +} +\seealso{ +[downloadGFWLossByDriver()], [convertGFWLossByDriver()] +} +\author{ +Michael Crawford +} diff --git a/tests/testthat/test-GFWLossByDriver.R b/tests/testthat/test-GFWLossByDriver.R new file mode 100644 index 00000000..7aa801a7 --- /dev/null +++ b/tests/testthat/test-GFWLossByDriver.R @@ -0,0 +1,120 @@ +# Synthesized known-bug cases for checkGFWLossByDriver(), written before it ran on real data: +# each corruption below must fail and each benign perturbation must pass. + +# A minimal extract satisfying every identity the guard checks; uniform values on purpose. +makeExtract <- function(nIso = 210, years = 2001:2025) { + iso <- sprintf("C%02d", seq_len(nIso)) + drivers <- names(gfwDriverClasses) # nolint: object_usage_linter. + df <- expand.grid(iso = iso, year = years, driver = drivers, + KEEP.OUT.ATTRS = FALSE, stringsAsFactors = FALSE) + df$threshold <- as.integer(gfwCanopyThreshold) # nolint: object_usage_linter. + df$loss_ha <- gfwGlobalLossMha * 1e6 / nrow(df) # nolint: object_usage_linter. + df[, c("iso", "year", "threshold", "driver", "loss_ha")] +} + +makeTotals <- function(df) { + t <- aggregate(list(loss_ha = df$loss_ha), by = list(iso = df$iso, year = df$year), FUN = sum) + t$threshold <- as.integer(gfwCanopyThreshold) # nolint: object_usage_linter. + t[, c("iso", "year", "threshold", "loss_ha")] +} + +test_that("the guard passes clean data", { + df <- makeExtract() + expect_silent(checkGFWLossByDriver(df, makeTotals(df))) +}) + +test_that("a dropped digit in one country-year-driver is caught", { + df <- makeExtract() + totals <- makeTotals(df) # totals built BEFORE the corruption, as on the server + i <- which(df$iso == "C07" & df$year == 2020 & df$driver == "Shifting cultivation") + expect_length(i, 1) # a corruption that matches no row tests nothing + df$loss_ha[i] <- df$loss_ha[i] / 10 # the Curtis failure: 39 typed as 3 + expect_error(checkGFWLossByDriver(df, totals), "do not sum to the country total") +}) + +test_that("a missing driver class is caught", { + df <- makeExtract() + totals <- makeTotals(df) + df <- df[df$driver != "Shifting cultivation", ] + expect_error(checkGFWLossByDriver(df, totals), "do not match the class map") +}) + +test_that("a renamed driver class is caught", { + df <- makeExtract() + df$driver[df$driver == "Logging"] <- "Forest management" + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "do not match the class map") +}) + +test_that("an added driver class is caught", { + df <- makeExtract() + extra <- df[df$driver == "Wildfire", ] + extra$driver <- "Flooding" + df <- rbind(df, extra) + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "do not match the class map") +}) + +test_that("a driver file cut short is caught", { + df <- makeExtract() + totals <- makeTotals(df) # totals complete, as when one of the two downloads fails + df <- df[df$iso %in% head(unique(df$iso), 200), ] # tail lost, but not enough to trip the count + expect_error(checkGFWLossByDriver(df, totals), "do not cover the same country-years") +}) + +test_that("losing the tail of the country list is caught", { + df <- makeExtract(nIso = 150) + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "countries in the extract") +}) + +test_that("an extract mixing canopy thresholds is caught", { + # The threshold value itself is checked in downloadGFWLossByDriver(); read catches mixing. + df <- makeExtract() + df$threshold[seq(1, nrow(df), 2)] <- 75L + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "mixes canopy thresholds") +}) + +test_that("a gap in the year series is caught", { + df <- makeExtract() + df <- df[df$year != 2013, ] + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "has gaps at 2013") +}) + +test_that("missing and negative loss values are caught", { + df <- makeExtract() + df$loss_ha[5] <- NA_real_ + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "missing and") + df <- makeExtract() + df$loss_ha[5] <- -1 + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "negative values") +}) + +test_that("a global total far from the pinned reference is caught", { + df <- makeExtract() + df$loss_ha <- df$loss_ha * 1.2 + expect_error(checkGFWLossByDriver(df, makeTotals(df)), "global loss sums to") +}) + +test_that("a missing column is caught", { + df <- makeExtract() + expect_error(checkGFWLossByDriver(df[, -3], makeTotals(df)), "missing column") +}) + +# Benign perturbations: a guard that rejects everything is as useless as one that rejects nothing. + +test_that("row order does not matter", { + df <- makeExtract() + totals <- makeTotals(df) + expect_silent(checkGFWLossByDriver(df[sample(nrow(df)), ], totals)) +}) + +test_that("floating point noise below tolerance passes", { + df <- makeExtract() + totals <- makeTotals(df) + df$loss_ha <- df$loss_ha * (1 + 1e-12) + expect_silent(checkGFWLossByDriver(df, totals)) +}) + +test_that("a global total just inside the tolerance passes", { + df <- makeExtract() + df$loss_ha <- df$loss_ha * 1.04 + expect_silent(checkGFWLossByDriver(df, makeTotals(df))) +}) From 2ea680f85e854bf9e5457892bd94b8aef074f0a9 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 12:49:25 +0200 Subject: [PATCH 07/25] Select the forest-loss driver source in calcForestLossShare/calcForestFireLoss Both gain `source` ("GFW" default, "Curtis") and `period` (years averaged for GFW, default 2015:2024; Curtis is a single 2001-2015 mean). `period` rather than `years` because calcOutput() consumes a `years` argument itself. Denominator (FRA 2020 natural forest 2010) and the `overall` FRA fire column are unchanged. source = "Curtis" reproduces the previous output exactly (max difference 0). Note: fullMAGPIE now writes nine columns, which module 35's six-element driver_source set rejects at compile time until the model side is updated. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- R/calcForestFireLoss.R | 77 ++++++++++++++++++++++++++------------ R/calcForestLossShare.R | 69 ++++++++++++++++++++-------------- man/calcForestFireLoss.Rd | 35 ++++++++++++----- man/calcForestLossShare.Rd | 34 ++++++++++++----- 4 files changed, 143 insertions(+), 72 deletions(-) diff --git a/R/calcForestFireLoss.R b/R/calcForestFireLoss.R index 964fa87e..8c6ea375 100644 --- a/R/calcForestFireLoss.R +++ b/R/calcForestFireLoss.R @@ -1,31 +1,60 @@ #' @title calcForestFireLoss #' -#' @description Calculate how much loss of forest area happens due to fire disturbances based on FRA 2020 data +#' @description Assembles annual forest area loss by driver, in Mha per year, from one of +#' two driver sources, with the FAO FRA 2020 fire column bound on as `overall`. #' -#' @return MAgPIE object with FRA 2020 forest fire area loss -#' @author Abhijeet Mishra -#' @seealso -#' \code{\link[mrfaocore]{readFRA2020}} -#' +#' @param source Driver data to use. `"GFW"` (default) is the WRI/Google DeepMind 1 km +#' dominant-driver product of Sims et al. (2025), crossed with UMD/Hansen annual tree +#' cover loss per country - see [readGFWLossByDriver()]. `"Curtis"` is the hand-transcribed +#' Table 1 of Curtis et al. (2018), seven regions spread over countries by FRA forest area; +#' it computes as before, from the corrected table. +#' @param period Years averaged to give the annual rate, for `source = "GFW"` only. The +#' default 2015:2024 is the most recent complete decade in the GFW record. The Curtis +#' source carries a single 2001-2015 mean and ignores this argument. +#' +#' @details The `overall` column is the FRA 2020 fire series averaged over its own years, +#' 2000-2017, not over `period` - the two records barely overlap. That is the behaviour this +#' function has always had, and `overall` is loaded but never read on the GAMS side. +#' +#' @return MAgPIE object with forest area lost per year by driver, Mha +#' @author Abhijeet Mishra, Michael Crawford +#' @importFrom magclass mbind setNames dimSums getYears +#' @importFrom madrat readSource +#' @seealso [readGFWLossByDriver()], [readForestLossDrivers()] #' @examples -#' -#' \dontrun{ -#' calcOutput("ForestFireLoss",aggregate=FALSE) +#' \dontrun{ +#' calcOutput("ForestFireLoss", aggregate = FALSE) +#' calcOutput("ForestFireLoss", source = "Curtis", aggregate = FALSE) #' } -#' -#' -calcForestFireLoss <- function(){ - - fao <-setNames(readSource("FRA2020",subtype = "forest_fire",convert = TRUE),"overall") # Convert=T returns Mha area lost to fire - fao <- dimSums(fao,dim=2)/length(getYears(fao)) # Average forest loss due to fires - curtis <- readSource("ForestLossDrivers") - - out <- mbind(fao,curtis) - - return(list(x=out, - weight=NULL, - unit="Mha", - description="Area affected by fires in forests", - isocountries=FALSE)) +calcForestFireLoss <- function(source = "GFW", period = 2015:2024) { + + fao <- setNames(readSource("FRA2020", subtype = "forest_fire", convert = TRUE), "overall") + fao <- dimSums(fao, dim = 2) / length(getYears(fao)) # mean annual area lost to fire + + drivers <- switch( + source, # nolint: undesirable_function_linter. + "Curtis" = readSource("ForestLossDrivers"), + "GFW" = { + x <- readSource("GFWLossByDriver", convert = TRUE) + wanted <- paste0("y", period) + absent <- setdiff(wanted, getYears(x)) + if (length(absent) > 0) { + stop("calcForestFireLoss: the GFW record does not cover ", toString(absent), + ". It runs ", min(getYears(x, TRUE)), "-", max(getYears(x, TRUE)), ".") + } + dimSums(x[, wanted, ], dim = 2) / length(wanted) + }, + stop("calcForestFireLoss: unknown source '", source, # nolint: undesirable_function_linter. + "'. Use \"GFW\" or \"Curtis\".") + ) + + out <- mbind(fao, drivers) + + return(list(x = out, + weight = NULL, + unit = "Mha", + description = paste0("Forest area lost per year by driver (", + source, ")"), # nolint: undesirable_function_linter. + isocountries = FALSE)) } diff --git a/R/calcForestLossShare.R b/R/calcForestLossShare.R index ea9b5eb5..f230a300 100644 --- a/R/calcForestLossShare.R +++ b/R/calcForestLossShare.R @@ -1,36 +1,49 @@ #' @title calcForestLossShare #' -#' @description Calculates which share of forest land is lost due to different drivers +#' @description Calculates which share of natural forest area is lost per year to each +#' driver of forest loss. #' -#' @return MAgPIE object with share of area lost in forests due to different drivers -#' @author Abhijeet Mishra -#' @seealso -#' \code{\link{readForestLossDrivers}} -#' +#' @param source Driver data to use, `"GFW"` (default) or `"Curtis"`. See +#' [calcForestFireLoss()]. +#' @param period Years averaged to give the annual rate, for `source = "GFW"` only. +#' +#' @details The numerator is UMD tree cover loss (or, for Curtis, tree cover loss +#' transcribed from the paper) and the denominator is FRA 2020 naturally regenerating +#' forest area in 2010. Those two are not the same quantity: tree cover is not forest, and +#' loss detected in tree cover outside natural forest still lands in the numerator. The +#' mismatch is inherited from the original parameterisation and is left in place here on +#' purpose, so that switching the driver source moves the numbers for one reason only. +#' Changing the denominator is a separate decision. +#' +#' @return MAgPIE object with the share of natural forest area lost per year by driver +#' @author Abhijeet Mishra, Michael Crawford +#' @importFrom magclass setYears setNames +#' @importFrom madrat calcOutput readSource +#' @seealso [calcForestFireLoss()] #' @examples -#' -#' \dontrun{ -#' calcOutput("ForestLossShare",aggregate=FALSE) +#' \dontrun{ +#' calcOutput("ForestLossShare", aggregate = FALSE) +#' calcOutput("ForestLossShare", source = "Curtis", aggregate = FALSE) #' } -#' -#' -calcForestLossShare <- function(){ - - lost_area <- calcOutput("ForestFireLoss",aggregate=FALSE) # Convert=T returns Mha area lost to fire - forest_area <- setYears(setNames(readSource("FRA2020",subtype = "forest_area",convert = TRUE)[,"y2010","naturallyRegeneratingForest"],NULL),NULL) # Convert=T returns Mha area of forests +calcForestLossShare <- function(source = "GFW", period = 2015:2024) { + + lostArea <- calcOutput("ForestFireLoss", source = source, # nolint: undesirable_function_linter. + period = period, aggregate = FALSE) + forestArea <- setYears(setNames(readSource("FRA2020", subtype = "forest_area", + convert = TRUE)[, "y2010", + "naturallyRegeneratingForest"], + NULL), NULL) + + lostShare <- lostArea / forestArea + lostShare[is.infinite(lostShare)] <- 0 + lostShare[is.na(lostShare)] <- 0 + lostShare[lostShare > 1] <- 1 - lost_share <- lost_area/forest_area - lost_share[is.infinite(lost_share)] <- 0 - lost_share[is.na(lost_share)] <- 0 - lost_share[lost_share>1] <- 1 - - out <- lost_share - weight <- forest_area - - return(list(x=out, - weight=weight, - unit="1", - description="Area lost in forests by drivers", - isocountries=FALSE)) + return(list(x = lostShare, + weight = forestArea, + unit = "1", + description = paste0("Share of natural forest area lost per year by driver (", + source, ")"), # nolint: undesirable_function_linter. + isocountries = FALSE)) } diff --git a/man/calcForestFireLoss.Rd b/man/calcForestFireLoss.Rd index 9e2a16d6..eae74383 100644 --- a/man/calcForestFireLoss.Rd +++ b/man/calcForestFireLoss.Rd @@ -4,25 +4,40 @@ \alias{calcForestFireLoss} \title{calcForestFireLoss} \usage{ -calcForestFireLoss() +calcForestFireLoss(source = "GFW", period = 2015:2024) +} +\arguments{ +\item{source}{Driver data to use. `"GFW"` (default) is the WRI/Google DeepMind 1 km +dominant-driver product of Sims et al. (2025), crossed with UMD/Hansen annual tree +cover loss per country - see [readGFWLossByDriver()]. `"Curtis"` is the hand-transcribed +Table 1 of Curtis et al. (2018), seven regions spread over countries by FRA forest area; +it computes as before, from the corrected table.} + +\item{period}{Years averaged to give the annual rate, for `source = "GFW"` only. The +default 2015:2024 is the most recent complete decade in the GFW record. The Curtis +source carries a single 2001-2015 mean and ignores this argument.} } \value{ -MAgPIE object with FRA 2020 forest fire area loss +MAgPIE object with forest area lost per year by driver, Mha } \description{ -Calculate how much loss of forest area happens due to fire disturbances based on FRA 2020 data +Assembles annual forest area loss by driver, in Mha per year, from one of +two driver sources, with the FAO FRA 2020 fire column bound on as `overall`. +} +\details{ +The `overall` column is the FRA 2020 fire series averaged over its own years, +2000-2017, not over `period` - the two records barely overlap. That is the behaviour this +function has always had, and `overall` is loaded but never read on the GAMS side. } \examples{ - -\dontrun{ -calcOutput("ForestFireLoss",aggregate=FALSE) +\dontrun{ +calcOutput("ForestFireLoss", aggregate = FALSE) +calcOutput("ForestFireLoss", source = "Curtis", aggregate = FALSE) } - - } \seealso{ -\code{\link[mrfaocore]{readFRA2020}} +[readGFWLossByDriver()], [readForestLossDrivers()] } \author{ -Abhijeet Mishra +Abhijeet Mishra, Michael Crawford } diff --git a/man/calcForestLossShare.Rd b/man/calcForestLossShare.Rd index 72c36104..d2155e96 100644 --- a/man/calcForestLossShare.Rd +++ b/man/calcForestLossShare.Rd @@ -4,25 +4,39 @@ \alias{calcForestLossShare} \title{calcForestLossShare} \usage{ -calcForestLossShare() +calcForestLossShare(source = "GFW", period = 2015:2024) +} +\arguments{ +\item{source}{Driver data to use, `"GFW"` (default) or `"Curtis"`. See +[calcForestFireLoss()].} + +\item{period}{Years averaged to give the annual rate, for `source = "GFW"` only.} } \value{ -MAgPIE object with share of area lost in forests due to different drivers +MAgPIE object with the share of natural forest area lost per year by driver } \description{ -Calculates which share of forest land is lost due to different drivers +Calculates which share of natural forest area is lost per year to each +driver of forest loss. +} +\details{ +The numerator is UMD tree cover loss (or, for Curtis, tree cover loss +transcribed from the paper) and the denominator is FRA 2020 naturally regenerating +forest area in 2010. Those two are not the same quantity: tree cover is not forest, and +loss detected in tree cover outside natural forest still lands in the numerator. The +mismatch is inherited from the original parameterisation and is left in place here on +purpose, so that switching the driver source moves the numbers for one reason only. +Changing the denominator is a separate decision. } \examples{ - -\dontrun{ -calcOutput("ForestLossShare",aggregate=FALSE) +\dontrun{ +calcOutput("ForestLossShare", aggregate = FALSE) +calcOutput("ForestLossShare", source = "Curtis", aggregate = FALSE) } - - } \seealso{ -\code{\link{readForestLossDrivers}} +[calcForestFireLoss()] } \author{ -Abhijeet Mishra +Abhijeet Mishra, Michael Crawford } From e179f2c4803876629bc950d8775d07c8a1401393 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 10 Sep 2026 13:59:14 +0200 Subject: [PATCH 08/25] Restrict f35_forest_lost_share to drivers MAgPIE does not model endogenously calcForestFireLoss(source = "GFW") returns shifting cultivation, wildfire and other natural disturbances only. Module 35 resets age classes with regrowth rather than removing forest, so a class belongs here only if it leaves the land as forest and MAgPIE does not already decide it: permanent agriculture, hard commodities, logging, settlements and unknown are excluded and published by mrvalidation::calcValidTreeCoverLoss instead. readGFWLossByDriver() still returns all eight classes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FQU9MetaTSbqRJVrCypV9n --- R/calcForestFireLoss.R | 33 ++++++++++++++++++++++++++++++--- man/calcForestFireLoss.Rd | 4 ++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/R/calcForestFireLoss.R b/R/calcForestFireLoss.R index 8c6ea375..429d29a2 100644 --- a/R/calcForestFireLoss.R +++ b/R/calcForestFireLoss.R @@ -1,3 +1,21 @@ +# The only driver classes that belong in f35_forest_lost_share. The parameter is not a +# deforestation rate: module 35 moves the "lost" area out of mature secondary-forest age +# classes and out of primary forest and straight back into the youngest age classes +# (35_natveg/pot_forest_may24/presolve.gms), so total forest area is conserved and only the +# age structure resets. A class therefore qualifies only if it (a) leaves the land as forest +# and (b) is not something MAgPIE already decides for itself: +# +# permanent agriculture - permanent conversion, and endogenous via vm_lu_transitions +# hard commodities - permanent conversion (mining) +# logging - regrowth, but endogenous via v35_hvarea_* and module 73 +# settlements & infrastructure- permanent conversion, and module 34 sets the urban pool +# unknown - unattributable +# +# Those five are the observational counterpart to quantities the model solves for, and are +# published through mrvalidation::calcValidTreeCoverLoss() instead, where they benchmark the +# model rather than parameterise it. Feeding them in here would double count. +gfwModelDrivers <- c("shifting_cultivation", "wildfire", "other_natural_disturbances") + #' @title calcForestFireLoss #' #' @description Assembles annual forest area loss by driver, in Mha per year, from one of @@ -12,13 +30,17 @@ #' default 2015:2024 is the most recent complete decade in the GFW record. The Curtis #' source carries a single 2001-2015 mean and ignores this argument. #' -#' @details The `overall` column is the FRA 2020 fire series averaged over its own years, +#' @details For `source = "GFW"` only the three driver classes that MAgPIE does not +#' model endogenously are returned - see `gfwModelDrivers` at the top of this file for +#' which, and why the others are routed to mrvalidation instead. +#' +#' The `overall` column is the FRA 2020 fire series averaged over its own years, #' 2000-2017, not over `period` - the two records barely overlap. That is the behaviour this #' function has always had, and `overall` is loaded but never read on the GAMS side. #' #' @return MAgPIE object with forest area lost per year by driver, Mha #' @author Abhijeet Mishra, Michael Crawford -#' @importFrom magclass mbind setNames dimSums getYears +#' @importFrom magclass mbind setNames dimSums getYears getItems #' @importFrom madrat readSource #' @seealso [readGFWLossByDriver()], [readForestLossDrivers()] #' @examples @@ -43,7 +65,12 @@ calcForestFireLoss <- function(source = "GFW", period = 2015:2024) { stop("calcForestFireLoss: the GFW record does not cover ", toString(absent), ". It runs ", min(getYears(x, TRUE)), "-", max(getYears(x, TRUE)), ".") } - dimSums(x[, wanted, ], dim = 2) / length(wanted) + absent <- setdiff(gfwModelDrivers, getItems(x, 3)) + if (length(absent) > 0) { + stop("calcForestFireLoss: the GFW source does not carry driver class(es) ", + toString(absent), ".") + } + dimSums(x[, wanted, gfwModelDrivers], dim = 2) / length(wanted) }, stop("calcForestFireLoss: unknown source '", source, # nolint: undesirable_function_linter. "'. Use \"GFW\" or \"Curtis\".") diff --git a/man/calcForestFireLoss.Rd b/man/calcForestFireLoss.Rd index eae74383..a7bc038b 100644 --- a/man/calcForestFireLoss.Rd +++ b/man/calcForestFireLoss.Rd @@ -25,6 +25,10 @@ Assembles annual forest area loss by driver, in Mha per year, from one of two driver sources, with the FAO FRA 2020 fire column bound on as `overall`. } \details{ +For `source = "GFW"` only the three driver classes that MAgPIE does not +model endogenously are returned - see `gfwModelDrivers` at the top of this file for +which, and why the others are routed to mrvalidation instead. + The `overall` column is the FRA 2020 fire series averaged over its own years, 2000-2017, not over `period` - the two records barely overlap. That is the behaviour this function has always had, and `overall` is loaded but never read on the GAMS side. From f5ca996069fd78536053a5c54aa8393068b73016 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Fri, 11 Sep 2026 09:28:51 +0200 Subject: [PATCH 09/25] Restrict f35_forest_lost_share to shifting cultivation Wildfire and other natural disturbances leave gfwModelDrivers. The area this parameter moves is booked as land-use-change CO2 by module 52 and drives DEFO_largeFires in magpie4::reportFireEmissions, so a class must also be a land-use process. Wildfire is additionally already in the fire-enabled LPJmL run behind the natural-vegetation carbon densities. Measured before removal: wildfire 8.66 Mha/yr (72.5% REF+CAZ), other natural 0.45, shifting cultivation 3.17. overall and shifting_cultivation are bit-identical before and after in all 12 regions. Both dropped classes stay in calcValidTreeCoverLoss. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014XmKyZUQSGPk6rJna5eD62 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- R/calcForestFireLoss.R | 52 ++++++++++++++------------------------- man/calcForestFireLoss.Rd | 27 ++++++++------------ 2 files changed, 28 insertions(+), 51 deletions(-) diff --git a/R/calcForestFireLoss.R b/R/calcForestFireLoss.R index 429d29a2..1ba99fa0 100644 --- a/R/calcForestFireLoss.R +++ b/R/calcForestFireLoss.R @@ -1,42 +1,26 @@ -# The only driver classes that belong in f35_forest_lost_share. The parameter is not a -# deforestation rate: module 35 moves the "lost" area out of mature secondary-forest age -# classes and out of primary forest and straight back into the youngest age classes -# (35_natveg/pot_forest_may24/presolve.gms), so total forest area is conserved and only the -# age structure resets. A class therefore qualifies only if it (a) leaves the land as forest -# and (b) is not something MAgPIE already decides for itself: -# -# permanent agriculture - permanent conversion, and endogenous via vm_lu_transitions -# hard commodities - permanent conversion (mining) -# logging - regrowth, but endogenous via v35_hvarea_* and module 73 -# settlements & infrastructure- permanent conversion, and module 34 sets the urban pool -# unknown - unattributable -# -# Those five are the observational counterpart to quantities the model solves for, and are -# published through mrvalidation::calcValidTreeCoverLoss() instead, where they benchmark the -# model rather than parameterise it. Feeding them in here would double count. -gfwModelDrivers <- c("shifting_cultivation", "wildfire", "other_natural_disturbances") +# Driver classes that reach f35_forest_lost_share. Module 35 resets the age structure of the +# "lost" area (the land stays forest) and module 52 books the flux as land-use-change CO2, so a +# class qualifies only if it leaves the land as forest, is not decided endogenously by MAgPIE, +# and is a land-use rather than a natural process. Of the eight GFW classes only shifting +# cultivation passes. The others are published by mrvalidation::calcValidTreeCoverLoss: +# permanent agriculture, hard commodities and settlements remove the forest, logging is +# MAgPIE's own timber harvest, wildfire and other natural disturbances are natural. +gfwModelDrivers <- c("shifting_cultivation") #' @title calcForestFireLoss #' -#' @description Assembles annual forest area loss by driver, in Mha per year, from one of -#' two driver sources, with the FAO FRA 2020 fire column bound on as `overall`. +#' @description Annual forest area loss by driver, Mha per year, from the Sims et al. (2025) +#' driver product (`source = "GFW"`, default) or the Curtis et al. (2018) table +#' (`source = "Curtis"`), with the FRA 2020 fire series bound on as `overall`. #' -#' @param source Driver data to use. `"GFW"` (default) is the WRI/Google DeepMind 1 km -#' dominant-driver product of Sims et al. (2025), crossed with UMD/Hansen annual tree -#' cover loss per country - see [readGFWLossByDriver()]. `"Curtis"` is the hand-transcribed -#' Table 1 of Curtis et al. (2018), seven regions spread over countries by FRA forest area; -#' it computes as before, from the corrected table. -#' @param period Years averaged to give the annual rate, for `source = "GFW"` only. The -#' default 2015:2024 is the most recent complete decade in the GFW record. The Curtis -#' source carries a single 2001-2015 mean and ignores this argument. +#' @param source `"GFW"` (default) or `"Curtis"`. Curtis computes as before, from the +#' corrected Curtis et al. (2018) table. +#' @param period Years averaged into the annual rate, GFW only (default 2015:2024). Curtis +#' carries a single 2001-2015 mean. #' -#' @details For `source = "GFW"` only the three driver classes that MAgPIE does not -#' model endogenously are returned - see `gfwModelDrivers` at the top of this file for -#' which, and why the others are routed to mrvalidation instead. -#' -#' The `overall` column is the FRA 2020 fire series averaged over its own years, -#' 2000-2017, not over `period` - the two records barely overlap. That is the behaviour this -#' function has always had, and `overall` is loaded but never read on the GAMS side. +#' @details For GFW only the classes in `gfwModelDrivers` are returned. `overall` is the FRA +#' fire series averaged over its own years (2000-2017), as before; GAMS loads it but never +#' reads it. #' #' @return MAgPIE object with forest area lost per year by driver, Mha #' @author Abhijeet Mishra, Michael Crawford diff --git a/man/calcForestFireLoss.Rd b/man/calcForestFireLoss.Rd index a7bc038b..a34cbf29 100644 --- a/man/calcForestFireLoss.Rd +++ b/man/calcForestFireLoss.Rd @@ -7,31 +7,24 @@ calcForestFireLoss(source = "GFW", period = 2015:2024) } \arguments{ -\item{source}{Driver data to use. `"GFW"` (default) is the WRI/Google DeepMind 1 km -dominant-driver product of Sims et al. (2025), crossed with UMD/Hansen annual tree -cover loss per country - see [readGFWLossByDriver()]. `"Curtis"` is the hand-transcribed -Table 1 of Curtis et al. (2018), seven regions spread over countries by FRA forest area; -it computes as before, from the corrected table.} +\item{source}{`"GFW"` (default) or `"Curtis"`. Curtis computes as before, from the +corrected Curtis et al. (2018) table.} -\item{period}{Years averaged to give the annual rate, for `source = "GFW"` only. The -default 2015:2024 is the most recent complete decade in the GFW record. The Curtis -source carries a single 2001-2015 mean and ignores this argument.} +\item{period}{Years averaged into the annual rate, GFW only (default 2015:2024). Curtis +carries a single 2001-2015 mean.} } \value{ MAgPIE object with forest area lost per year by driver, Mha } \description{ -Assembles annual forest area loss by driver, in Mha per year, from one of -two driver sources, with the FAO FRA 2020 fire column bound on as `overall`. +Annual forest area loss by driver, Mha per year, from the Sims et al. (2025) +driver product (`source = "GFW"`, default) or the Curtis et al. (2018) table +(`source = "Curtis"`), with the FRA 2020 fire series bound on as `overall`. } \details{ -For `source = "GFW"` only the three driver classes that MAgPIE does not -model endogenously are returned - see `gfwModelDrivers` at the top of this file for -which, and why the others are routed to mrvalidation instead. - -The `overall` column is the FRA 2020 fire series averaged over its own years, -2000-2017, not over `period` - the two records barely overlap. That is the behaviour this -function has always had, and `overall` is loaded but never read on the GAMS side. +For GFW only the classes in `gfwModelDrivers` are returned. `overall` is the FRA +fire series averaged over its own years (2000-2017), as before; GAMS loads it but never +reads it. } \examples{ \dontrun{ From 3fd404746c576f6970ba0da937252d0ff399eeb9 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Fri, 11 Sep 2026 09:43:54 +0200 Subject: [PATCH 10/25] buildLibrary Validation key, Date, CITATION.cff and README refreshed. Version stays 0.76.0 (bumped earlier on this branch, unpublished). Re-run immediately before merge: validkey and isVersionUpdated go stale on their own. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014XmKyZUQSGPk6rJna5eD62 --- .buildlibrary | 2 +- CITATION.cff | 4 ++-- DESCRIPTION | 2 +- README.md | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.buildlibrary b/.buildlibrary index cf2d0948..3f84e5da 100644 --- a/.buildlibrary +++ b/.buildlibrary @@ -1,4 +1,4 @@ -ValidationKey: '15601014' +ValidationKey: '15737320' AcceptedWarnings: - 'Warning: package ''.*'' was built under R version' - 'Warning: namespace ''.*'' is not available and has been replaced' diff --git a/CITATION.cff b/CITATION.cff index c4b60b7e..e6f80134 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: If you use this software, please cite it using the metadata from this file. type: software title: 'mrland: MadRaT land data package' -version: 0.75.4 -date-released: '2026-08-26' +version: 0.76.0 +date-released: '2026-09-11' abstract: The package provides land related data via the madrat framework. authors: - family-names: Dietrich diff --git a/DESCRIPTION b/DESCRIPTION index b9843269..c4632c0f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -2,7 +2,7 @@ Type: Package Package: mrland Title: MadRaT land data package Version: 0.76.0 -Date: 2026-09-10 +Date: 2026-09-11 Authors@R: c( person("Jan Philipp", "Dietrich", , "dietrich@pik-potsdam.de", role = c("aut", "cre")), person("Abhijeet", "Mishra", role = "aut"), diff --git a/README.md b/README.md index 3f9fdeb8..3c6483a7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MadRaT land data package -R package **mrland**, version **0.75.4** +R package **mrland**, version **0.76.0** [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3822083.svg)](https://doi.org/10.5281/zenodo.3822083) [![R build status](https://github.com/pik-piam/mrland/workflows/check/badge.svg)](https://github.com/pik-piam/mrland/actions) [![codecov](https://codecov.io/gh/pik-piam/mrland/branch/master/graph/badge.svg)](https://app.codecov.io/gh/pik-piam/mrland) [![r-universe](https://pik-piam.r-universe.dev/badges/mrland)](https://pik-piam.r-universe.dev/builds) @@ -39,7 +39,7 @@ In case of questions / problems please contact Jan Philipp Dietrich , Version: 0.75.4, . +Dietrich J, Mishra A, Weindl I, Bodirsky B, Wang X, Baumstark L, Kreidenweis U, Klein D, Steinmetz N, Chen D, Humpenoeder F, von Jeetze P, Wirth S, Beier F, Hoetten D, Sauer P, Tommey J (2026). "mrland: MadRaT land data package." doi:10.5281/zenodo.3822083 , Version: 0.76.0, . A BibTeX entry for LaTeX users is @@ -48,9 +48,9 @@ A BibTeX entry for LaTeX users is title = {mrland: MadRaT land data package}, author = {Jan Philipp Dietrich and Abhijeet Mishra and Isabelle Weindl and Benjamin Leon Bodirsky and Xiaoxi Wang and Lavinia Baumstark and Ulrich Kreidenweis and David Klein and Nele Steinmetz and David Chen and Florian Humpenoeder and Patrick {von Jeetze} and Stephen Wirth and Felicitas Beier and David Hoetten and Pascal Sauer and Jake Tommey}, doi = {10.5281/zenodo.3822083}, - date = {2026-08-26}, + date = {2026-09-11}, year = {2026}, url = {https://github.com/pik-piam/mrland}, - note = {Version: 0.75.4}, + note = {Version: 0.76.0}, } ``` From 91796508a5946d2d0539d24e72f73ecfacca6e4c Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Sat, 12 Sep 2026 22:55:17 +0200 Subject: [PATCH 11/25] Drop two checks from checkGFWLossByDriver that a stronger one dominates The year-gap check cannot fire on a truncated download (a tail cut leaves no gap in the series) and the country-count threshold only fires on truncations that the coverage check against loss_totals.csv already catches. Both removed with their tests; the coverage test now cuts the tail of the country list. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GaYsqSd2gZdixtjH8XWu2k --- R/readGFWLossByDriver.R | 11 ----------- tests/testthat/test-GFWLossByDriver.R | 13 +------------ 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/R/readGFWLossByDriver.R b/R/readGFWLossByDriver.R index 054d8292..7cc01a55 100644 --- a/R/readGFWLossByDriver.R +++ b/R/readGFWLossByDriver.R @@ -40,22 +40,11 @@ checkGFWLossByDriver <- function(df, totals) { ". It must carry exactly one, the value pinned in downloadGFWLossByDriver().") } - years <- sort(unique(df$year)) - if (!identical(years, seq(min(years), max(years)))) { - stop("GFWLossByDriver: the year series ", min(years), "-", max(years), - " has gaps at ", toString(setdiff(seq(min(years), max(years)), years)), ".") - } - if (anyNA(df$loss_ha) || any(df$loss_ha < 0)) { stop("GFWLossByDriver: loss_ha contains ", sum(is.na(df$loss_ha)), " missing and ", sum(df$loss_ha < 0, na.rm = TRUE), " negative values.") } - if (length(unique(df$iso)) < 200) { - stop("GFWLossByDriver: only ", length(unique(df$iso)), " countries in the extract; ", - "the full table carries 216. The download is probably truncated.") - } - # The driver file summed over drivers must reproduce the separately queried totals file. byDriver <- stats::aggregate(list(drv = df$loss_ha), by = list(iso = df$iso, year = df$year), FUN = sum) diff --git a/tests/testthat/test-GFWLossByDriver.R b/tests/testthat/test-GFWLossByDriver.R index 7aa801a7..8a5b921f 100644 --- a/tests/testthat/test-GFWLossByDriver.R +++ b/tests/testthat/test-GFWLossByDriver.R @@ -56,15 +56,10 @@ test_that("an added driver class is caught", { test_that("a driver file cut short is caught", { df <- makeExtract() totals <- makeTotals(df) # totals complete, as when one of the two downloads fails - df <- df[df$iso %in% head(unique(df$iso), 200), ] # tail lost, but not enough to trip the count + df <- df[df$iso %in% head(unique(df$iso), 200), ] # tail of the country list lost expect_error(checkGFWLossByDriver(df, totals), "do not cover the same country-years") }) -test_that("losing the tail of the country list is caught", { - df <- makeExtract(nIso = 150) - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "countries in the extract") -}) - test_that("an extract mixing canopy thresholds is caught", { # The threshold value itself is checked in downloadGFWLossByDriver(); read catches mixing. df <- makeExtract() @@ -72,12 +67,6 @@ test_that("an extract mixing canopy thresholds is caught", { expect_error(checkGFWLossByDriver(df, makeTotals(df)), "mixes canopy thresholds") }) -test_that("a gap in the year series is caught", { - df <- makeExtract() - df <- df[df$year != 2013, ] - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "has gaps at 2013") -}) - test_that("missing and negative loss values are caught", { df <- makeExtract() df$loss_ha[5] <- NA_real_ From def0792cb94000e5f55ea466cc0c7b26aa34f8bb Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Sat, 12 Sep 2026 22:55:17 +0200 Subject: [PATCH 12/25] Retire calcForestDisturbances, whose only consumer was an unread MAgPIE parameter fullMAGPIE wrote f35_forest_disturbance_share.cs4 for module 35, where the parameter has been declared and included since December 2020 and read by no equation in any revision. The model side removes the declaration; nothing else calls the function. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GaYsqSd2gZdixtjH8XWu2k --- R/calcForestDisturbances.R | 38 ----------------------------------- R/fullMAGPIE.R | 1 - man/calcForestDisturbances.Rd | 28 -------------------------- 3 files changed, 67 deletions(-) delete mode 100644 R/calcForestDisturbances.R delete mode 100644 man/calcForestDisturbances.Rd diff --git a/R/calcForestDisturbances.R b/R/calcForestDisturbances.R deleted file mode 100644 index 5e432370..00000000 --- a/R/calcForestDisturbances.R +++ /dev/null @@ -1,38 +0,0 @@ -#' @title calcForestDisturbances -#' -#' @description Calculates which share of forest land is lost due to forest disturbances (including insects, diseases, severe weather events and other causes) -#' -#' @return MAgPIE object with FRA 2020 forest disturbance shares -#' @author Abhijeet Mishra -#' @seealso -#' \code{\link[mrfaocore]{readFRA2020}} -#' -#' @examples -#' -#' \dontrun{ -#' calcOutput("ForestDisturbances",aggregate=FALSE) -#' } -#' -#' - -calcForestDisturbances <- function(){ - - disturbance_area <- setNames(readSource("FRA2020",subtype = "disturbance",convert = TRUE),NULL) # Convert=T returns Mha area lost to fire - forest_area <- setNames(readSource("FRA2020",subtype = "forest_area",convert = TRUE)[,,"naturallyRegeneratingForest"],NULL) # Convert=T returns Mha area of forests - - common_yrs <- intersect(getYears(disturbance_area),getYears(forest_area)) - - disturbance_share <- disturbance_area[,common_yrs,]/forest_area[,common_yrs,] - disturbance_share[is.infinite(disturbance_share)] <- 0 - disturbance_share[is.na(disturbance_share)] <- 0 - disturbance_share[disturbance_share>1] <- 1 - - out <- setYears(disturbance_share[,"y2010",],NULL) - weight <- setYears(forest_area[,"y2010",],NULL) - - return(list(x=out, - weight=weight, - unit="1", - description="Area affected by forest disturbances including insects, diseases, severe weather events and other causes", - isocountries=FALSE)) -} diff --git a/R/fullMAGPIE.R b/R/fullMAGPIE.R index a0b89e0c..e11f3b43 100644 --- a/R/fullMAGPIE.R +++ b/R/fullMAGPIE.R @@ -278,7 +278,6 @@ fullMAGPIE <- function(rev = numeric_version("0.1"), dev = "") { # 35 natural vegetation calcOutput("ForestLossShare", round = 7, outputStatistics = stats, file = "f35_forest_lost_share.cs3") - calcOutput("ForestDisturbances", round = 7, outputStatistics = stats, file = "f35_forest_disturbance_share.cs4") # 36 employment calcOutput("WeeklyHoursILO", projections = TRUE, aggregate = TRUE, years = seq(1965, 2150, 5), diff --git a/man/calcForestDisturbances.Rd b/man/calcForestDisturbances.Rd deleted file mode 100644 index 2b225f34..00000000 --- a/man/calcForestDisturbances.Rd +++ /dev/null @@ -1,28 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/calcForestDisturbances.R -\name{calcForestDisturbances} -\alias{calcForestDisturbances} -\title{calcForestDisturbances} -\usage{ -calcForestDisturbances() -} -\value{ -MAgPIE object with FRA 2020 forest disturbance shares -} -\description{ -Calculates which share of forest land is lost due to forest disturbances (including insects, diseases, severe weather events and other causes) -} -\examples{ - -\dontrun{ -calcOutput("ForestDisturbances",aggregate=FALSE) -} - - -} -\seealso{ -\code{\link[mrfaocore]{readFRA2020}} -} -\author{ -Abhijeet Mishra -} From 149662e06e807497558103ffb996d9bce9f35ac2 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Sat, 12 Sep 2026 22:55:18 +0200 Subject: [PATCH 13/25] Tidy readForestLossDrivers: line length, comma-separated mapping file mapping.csv in inst/extdata is now comma-separated with quoted names (sixteen names contain commas); the reader detects the separator so a shared source folder still holding the semicolon original keeps working. Four over-long lines wrapped. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GaYsqSd2gZdixtjH8XWu2k Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- R/readForestLossDrivers.R | 16 +- inst/extdata/ForestLossDrivers/SOURCE.md | 14 +- inst/extdata/ForestLossDrivers/mapping.csv | 500 ++++++++++----------- 3 files changed, 267 insertions(+), 263 deletions(-) diff --git a/R/readForestLossDrivers.R b/R/readForestLossDrivers.R index 2645dff0..e5013063 100644 --- a/R/readForestLossDrivers.R +++ b/R/readForestLossDrivers.R @@ -8,7 +8,8 @@ checkCurtisTable1 <- function(df) { total <- sum(df$treecoverloss_01_15) if (abs(total - globalLoss) > 2) { stop("ForestLossDrivers: regional tree cover loss sums to ", total, " Mha, but Table 1 of ", - "Curtis et al. (2018) reports a global total of ", globalLoss, " Mha. forest_loss.csv is corrupt.") + "Curtis et al. (2018) reports a global total of ", globalLoss, + " Mha. forest_loss.csv is corrupt.") } offBy <- abs(rowSums(df[drivers]) - 100) @@ -48,8 +49,10 @@ checkCurtisTable1 <- function(df) { #' @importFrom stats complete.cases readForestLossDrivers <- function() { - ## Mapping file - mapping <- read.csv("mapping.csv", header = TRUE, sep = ";") + ## Mapping file: comma-separated in the package since 0.76.0; a shared source folder may still + ## hold the semicolon-separated original + sep <- if (grepl(";", readLines("mapping.csv", n = 1))) ";" else "," + mapping <- read.csv("mapping.csv", header = TRUE, sep = sep) ## Magpie standard isoCountry <- toolGetMapping(type = "regional", name = "regionmappingH12.csv", where = "madrat") @@ -63,14 +66,15 @@ readForestLossDrivers <- function() { # This is Tree Cover loss in Mha at this stage, all drivers are in percentage df <- read.csv(file = file, header = TRUE, sep = ",") checkCurtisTable1(df) - # Division by 100 because we change from percentage to proportion. Division by 15 to get annual data + # percentages to proportions (/ 100), 2001-2015 totals to annual values (/ 15) df[, -c(1:3)] <- (df$treecoverloss_01_15 * (df[, -c(1:3)] / 100)) / 15 - dfMag <- as.magpie(df[, -c(2, 3)], temporal = NULL, spatial = "region") ## This is Tree Cover loss in Mha + dfMag <- as.magpie(df[, -c(2, 3)], temporal = NULL, spatial = "region") # tree cover loss, Mha ## FRA Forest Area # Convert is set to TRUE For Mha - a <- collapseNames(readSource("FRA2020", "forest_area", convert = TRUE)[, , "naturallyRegeneratingForest"]) + a <- readSource("FRA2020", "forest_area", convert = TRUE)[, , "naturallyRegeneratingForest"] + a <- collapseNames(a) ## Create iso level data based on forest data as weight forestLoss <- toolAggregate(x = dfMag, weight = setYears(a[, "y2015", ], NULL), rel = fullMapping, diff --git a/inst/extdata/ForestLossDrivers/SOURCE.md b/inst/extdata/ForestLossDrivers/SOURCE.md index aa727eef..6f90c9fd 100644 --- a/inst/extdata/ForestLossDrivers/SOURCE.md +++ b/inst/extdata/ForestLossDrivers/SOURCE.md @@ -67,10 +67,10 @@ makes the error unambiguous. ## Mapping `mapping.csv` assigns each ISO3 country to one of the seven Curtis source regions. It was -built from `madrat::regionmappingH12` (identical country set, 249 rows, same `;`-delimited -`X;CountryCode;RegionCode` layout) by rewriting the region column. The article does not list -countries per region, so the assignment is geographic. Corrected in mrland 0.76.0: the -original put the Middle East and North Africa block in `Latin America`. DZA, EGY, LBY, MAR -and TUN now map to `Africa`; ARE, BHR, IRN, IRQ, ISR, JOR, KWT, LBN, OMN, PSE, QAT, SAU, -SYR and YEM to `Russia China South Asia`; REU moves from `Russia China South Asia` to -`Africa`. +built from `madrat::regionmappingH12` (identical country set, 249 rows; comma-separated +since mrland 0.76.0, the shared source folder's original is `;`-delimited) by rewriting the +region column. The article does not list countries per region, so the assignment is +geographic. Corrected in mrland 0.76.0: the original put the Middle East and North Africa +block in `Latin America`. DZA, EGY, LBY, MAR and TUN now map to `Africa`; ARE, BHR, IRN, +IRQ, ISR, JOR, KWT, LBN, OMN, PSE, QAT, SAU, SYR and YEM to `Russia China South Asia`; REU +moves from `Russia China South Asia` to `Africa`. diff --git a/inst/extdata/ForestLossDrivers/mapping.csv b/inst/extdata/ForestLossDrivers/mapping.csv index 29eaa386..80b5cdad 100644 --- a/inst/extdata/ForestLossDrivers/mapping.csv +++ b/inst/extdata/ForestLossDrivers/mapping.csv @@ -1,250 +1,250 @@ -X;CountryCode;RegionCode -Angola;AGO;Africa -French Southern Territories;ATF;Africa -Burundi;BDI;Africa -Benin;BEN;Africa -Burkina Faso;BFA;Africa -Bouvet Island;BVT;Africa -Botswana;BWA;Africa -Central African Republic;CAF;Africa -Cote d Ivoire;CIV;Africa -Cameroon;CMR;Africa -Congo, the Democratic Republic of the;COD;Africa -Congo;COG;Africa -Comoros;COM;Africa -Cape Verde;CPV;Africa -Djibouti;DJI;Africa -Eritrea;ERI;Africa -Western Sahara;ESH;Africa -Ethiopia;ETH;Africa -Gabon;GAB;Africa -Ghana;GHA;Africa -Guinea;GIN;Africa -Gambia;GMB;Africa -Guinea-Bissau;GNB;Africa -Equatorial Guinea;GNQ;Africa -Kenya;KEN;Africa -Liberia;LBR;Africa -Lesotho;LSO;Africa -Madagascar;MDG;Africa -Mali;MLI;Africa -Mozambique;MOZ;Africa -Mauritania;MRT;Africa -Mauritius;MUS;Africa -Malawi;MWI;Africa -Mayotte;MYT;Africa -Namibia;NAM;Africa -Niger;NER;Africa -Nigeria;NGA;Africa -Rwanda;RWA;Africa -Sudan;SDN;Africa -Senegal;SEN;Africa -Saint Helena, Ascension and Tristan da Cunha;SHN;Africa -Sierra Leone;SLE;Africa -Somalia;SOM;Africa -South Sudan;SSD;Africa -Sao Tome and Principe;STP;Africa -Swaziland;SWZ;Africa -Seychelles;SYC;Africa -Chad;TCD;Africa -Togo;TGO;Africa -Tanzania, United Republic of;TZA;Africa -Uganda;UGA;Africa -South Africa;ZAF;Africa -Zambia;ZMB;Africa -Zimbabwe;ZWE;Africa -China;CHN;Russia China South Asia -Hong Kong;HKG;Russia China South Asia -Cambodia;KHM;Russia China South Asia -Lao People's Democratic Republic;LAO;Russia China South Asia -Macao;MAC;Russia China South Asia -Mongolia;MNG;Russia China South Asia -Taiwan, Province of China;TWN;Russia China South Asia -Viet Nam;VNM;Russia China South Asia -Aland Islands;ALA;Europe -Albania;ALB;Europe -Andorra;AND;Europe -Austria;AUT;Europe -Belgium;BEL;Europe -Bulgaria;BGR;Europe -Bosnia and Herzegovina;BIH;Europe -Switzerland;CHE;Europe -Cyprus;CYP;Europe -Czech Republic;CZE;Europe -Germany;DEU;Europe -Denmark;DNK;Europe -Spain;ESP;Europe -Estonia;EST;Europe -Finland;FIN;Europe -France;FRA;Europe -Faroe Islands;FRO;Europe -United Kingdom;GBR;Europe -Guernsey;GGY;Europe -Gibraltar;GIB;Europe -Greece;GRC;Europe -Greenland;GRL;Europe -Croatia;HRV;Europe -Hungary;HUN;Europe -Isle of Man;IMN;Europe -Ireland;IRL;Europe -Iceland;ISL;Europe -Italy;ITA;Europe -Jersey;JEY;Europe -Liechtenstein;LIE;Europe -Lithuania;LTU;Europe -Luxembourg;LUX;Europe -Latvia;LVA;Europe -Monaco;MCO;Europe -Macedonia, the former Yugoslav Republic of;MKD;Europe -Malta;MLT;Europe -Montenegro;MNE;Europe -Netherlands;NLD;Europe -Norway;NOR;Europe -Poland;POL;Europe -Portugal;PRT;Europe -Romania;ROU;Europe -Svalbard and Jan Mayen;SJM;Europe -San Marino;SMR;Europe -Serbia;SRB;Europe -Slovakia;SVK;Europe -Slovenia;SVN;Europe -Sweden;SWE;Europe -Turkey;TUR;Europe -Holy See (Vatican City State);VAT;Europe -Armenia;ARM;Russia China South Asia -Azerbaijan;AZE;Russia China South Asia -Belarus;BLR;Russia China South Asia -Georgia;GEO;Russia China South Asia -Kazakhstan;KAZ;Russia China South Asia -Kyrgyzstan;KGZ;Russia China South Asia -Moldova, Republic of;MDA;Russia China South Asia -Russian Federation;RUS;Russia China South Asia -Tajikistan;TJK;Russia China South Asia -Turkmenistan;TKM;Russia China South Asia -Ukraine;UKR;Russia China South Asia -Uzbekistan;UZB;Russia China South Asia -Aruba;ABW;Latin America -Anguilla;AIA;Latin America -Argentina;ARG;Latin America -Antarctica;ATA;Latin America -Antigua and Barbuda;ATG;Latin America -Bonaire, Sint Eustatius and Saba;BES;Latin America -Bahamas;BHS;Latin America -Saint Barthelemy;BLM;Latin America -Belize;BLZ;Latin America -Bolivia, Plurinational State of;BOL;Latin America -Brazil;BRA;Latin America -Barbados;BRB;Latin America -Chile;CHL;Latin America -Colombia;COL;Latin America -Costa Rica;CRI;Latin America -Cuba;CUB;Latin America -Curacao;CUW;Latin America -Cayman Islands;CYM;Latin America -Dominica;DMA;Latin America -Dominican Republic;DOM;Latin America -Ecuador;ECU;Latin America -Falkland Islands (Malvinas);FLK;Latin America -Guadeloupe;GLP;Latin America -Grenada;GRD;Latin America -Guatemala;GTM;Latin America -French Guiana;GUF;Latin America -Guyana;GUY;Latin America -Honduras;HND;Latin America -Haiti;HTI;Latin America -Jamaica;JAM;Latin America -Saint Kitts and Nevis;KNA;Latin America -Saint Lucia;LCA;Latin America -Saint Martin (French part);MAF;Latin America -Mexico;MEX;North America -Montserrat;MSR;Latin America -Martinique;MTQ;Latin America -Nicaragua;NIC;Latin America -Panama;PAN;Latin America -Peru;PER;Latin America -Paraguay;PRY;Latin America -South Georgia and the South Sandwich Islands;SGS;Latin America -El Salvador;SLV;Latin America -Suriname;SUR;Latin America -Sint Maarten (Dutch part);SXM;Latin America -Turks and Caicos Islands;TCA;Latin America -Trinidad and Tobago;TTO;Latin America -Uruguay;URY;Latin America -Saint Vincent and the Grenadines;VCT;Latin America -Venezuela, Bolivarian Republic of;VEN;Latin America -Virgin Islands, British;VGB;Latin America -Virgin Islands, U.S.;VIR;Latin America -United Arab Emirates;ARE;Russia China South Asia -Bahrain;BHR;Russia China South Asia -Algeria;DZA;Africa -Egypt;EGY;Africa -Iran, Islamic Republic of;IRN;Russia China South Asia -Iraq;IRQ;Russia China South Asia -Israel;ISR;Russia China South Asia -Jordan;JOR;Russia China South Asia -Kuwait;KWT;Russia China South Asia -Lebanon;LBN;Russia China South Asia -Libya;LBY;Africa -Morocco;MAR;Africa -Oman;OMN;Russia China South Asia -Palestine, State of;PSE;Russia China South Asia -Qatar;QAT;Russia China South Asia -Saudi Arabia;SAU;Russia China South Asia -Syrian Arab Republic;SYR;Russia China South Asia -Tunisia;TUN;Africa -Yemen;YEM;Russia China South Asia -Bermuda;BMU;North America -Canada;CAN;North America -Puerto Rico;PRI;North America -Saint Pierre and Miquelon;SPM;North America -United States Minor Outlying Islands;UMI;North America -United States;USA;North America -American Samoa;ASM;Australia Oceania -Australia;AUS;Australia Oceania -Cocos (Keeling) Islands;CCK;Australia Oceania -Cook Islands;COK;Australia Oceania -Christmas Island;CXR;Australia Oceania -Heard Island and McDonald Islands;HMD;Australia Oceania -Japan;JPN;Australia Oceania -Norfolk Island;NFK;Australia Oceania -Niue;NIU;Australia Oceania -New Zealand;NZL;Australia Oceania -French Polynesia;PYF;Australia Oceania -Tokelau;TKL;Australia Oceania -Brunei Darussalam;BRN;Southeast Asia -Fiji;FJI;Southeast Asia -Micronesia, Federated States of;FSM;Southeast Asia -Guam;GUM;Southeast Asia -Indonesia;IDN;Southeast Asia -Kiribati;KIR;Southeast Asia -Korea, Republic of;KOR;Southeast Asia -Marshall Islands;MHL;Southeast Asia -Northern Mariana Islands;MNP;Southeast Asia -Malaysia;MYS;Southeast Asia -New Caledonia;NCL;Southeast Asia -Nauru;NRU;Southeast Asia -Pitcairn;PCN;Southeast Asia -Philippines;PHL;Southeast Asia -Palau;PLW;Southeast Asia -Papua New Guinea;PNG;Southeast Asia -Korea, Democratic People's Republic of;PRK;Southeast Asia -Singapore;SGP;Southeast Asia -Solomon Islands;SLB;Southeast Asia -Thailand;THA;Southeast Asia -Timor-Leste;TLS;Southeast Asia -Tonga;TON;Southeast Asia -Tuvalu;TUV;Southeast Asia -Vanuatu;VUT;Southeast Asia -Wallis and Futuna;WLF;Southeast Asia -Samoa;WSM;Southeast Asia -Afghanistan;AFG;Russia China South Asia -Bangladesh;BGD;Russia China South Asia -Bhutan;BTN;Russia China South Asia -India;IND;Russia China South Asia -British Indian Ocean Territory;IOT;Russia China South Asia -Sri Lanka;LKA;Russia China South Asia -Maldives;MDV;Russia China South Asia -Myanmar;MMR;Russia China South Asia -Nepal;NPL;Russia China South Asia -Pakistan;PAK;Russia China South Asia -Reunion;REU;Africa +X,CountryCode,RegionCode +Angola,AGO,Africa +French Southern Territories,ATF,Africa +Burundi,BDI,Africa +Benin,BEN,Africa +Burkina Faso,BFA,Africa +Bouvet Island,BVT,Africa +Botswana,BWA,Africa +Central African Republic,CAF,Africa +Cote d Ivoire,CIV,Africa +Cameroon,CMR,Africa +"Congo, the Democratic Republic of the",COD,Africa +Congo,COG,Africa +Comoros,COM,Africa +Cape Verde,CPV,Africa +Djibouti,DJI,Africa +Eritrea,ERI,Africa +Western Sahara,ESH,Africa +Ethiopia,ETH,Africa +Gabon,GAB,Africa +Ghana,GHA,Africa +Guinea,GIN,Africa +Gambia,GMB,Africa +Guinea-Bissau,GNB,Africa +Equatorial Guinea,GNQ,Africa +Kenya,KEN,Africa +Liberia,LBR,Africa +Lesotho,LSO,Africa +Madagascar,MDG,Africa +Mali,MLI,Africa +Mozambique,MOZ,Africa +Mauritania,MRT,Africa +Mauritius,MUS,Africa +Malawi,MWI,Africa +Mayotte,MYT,Africa +Namibia,NAM,Africa +Niger,NER,Africa +Nigeria,NGA,Africa +Rwanda,RWA,Africa +Sudan,SDN,Africa +Senegal,SEN,Africa +"Saint Helena, Ascension and Tristan da Cunha",SHN,Africa +Sierra Leone,SLE,Africa +Somalia,SOM,Africa +South Sudan,SSD,Africa +Sao Tome and Principe,STP,Africa +Swaziland,SWZ,Africa +Seychelles,SYC,Africa +Chad,TCD,Africa +Togo,TGO,Africa +"Tanzania, United Republic of",TZA,Africa +Uganda,UGA,Africa +South Africa,ZAF,Africa +Zambia,ZMB,Africa +Zimbabwe,ZWE,Africa +China,CHN,Russia China South Asia +Hong Kong,HKG,Russia China South Asia +Cambodia,KHM,Russia China South Asia +Lao People's Democratic Republic,LAO,Russia China South Asia +Macao,MAC,Russia China South Asia +Mongolia,MNG,Russia China South Asia +"Taiwan, Province of China",TWN,Russia China South Asia +Viet Nam,VNM,Russia China South Asia +Aland Islands,ALA,Europe +Albania,ALB,Europe +Andorra,AND,Europe +Austria,AUT,Europe +Belgium,BEL,Europe +Bulgaria,BGR,Europe +Bosnia and Herzegovina,BIH,Europe +Switzerland,CHE,Europe +Cyprus,CYP,Europe +Czech Republic,CZE,Europe +Germany,DEU,Europe +Denmark,DNK,Europe +Spain,ESP,Europe +Estonia,EST,Europe +Finland,FIN,Europe +France,FRA,Europe +Faroe Islands,FRO,Europe +United Kingdom,GBR,Europe +Guernsey,GGY,Europe +Gibraltar,GIB,Europe +Greece,GRC,Europe +Greenland,GRL,Europe +Croatia,HRV,Europe +Hungary,HUN,Europe +Isle of Man,IMN,Europe +Ireland,IRL,Europe +Iceland,ISL,Europe +Italy,ITA,Europe +Jersey,JEY,Europe +Liechtenstein,LIE,Europe +Lithuania,LTU,Europe +Luxembourg,LUX,Europe +Latvia,LVA,Europe +Monaco,MCO,Europe +"Macedonia, the former Yugoslav Republic of",MKD,Europe +Malta,MLT,Europe +Montenegro,MNE,Europe +Netherlands,NLD,Europe +Norway,NOR,Europe +Poland,POL,Europe +Portugal,PRT,Europe +Romania,ROU,Europe +Svalbard and Jan Mayen,SJM,Europe +San Marino,SMR,Europe +Serbia,SRB,Europe +Slovakia,SVK,Europe +Slovenia,SVN,Europe +Sweden,SWE,Europe +Turkey,TUR,Europe +Holy See (Vatican City State),VAT,Europe +Armenia,ARM,Russia China South Asia +Azerbaijan,AZE,Russia China South Asia +Belarus,BLR,Russia China South Asia +Georgia,GEO,Russia China South Asia +Kazakhstan,KAZ,Russia China South Asia +Kyrgyzstan,KGZ,Russia China South Asia +"Moldova, Republic of",MDA,Russia China South Asia +Russian Federation,RUS,Russia China South Asia +Tajikistan,TJK,Russia China South Asia +Turkmenistan,TKM,Russia China South Asia +Ukraine,UKR,Russia China South Asia +Uzbekistan,UZB,Russia China South Asia +Aruba,ABW,Latin America +Anguilla,AIA,Latin America +Argentina,ARG,Latin America +Antarctica,ATA,Latin America +Antigua and Barbuda,ATG,Latin America +"Bonaire, Sint Eustatius and Saba",BES,Latin America +Bahamas,BHS,Latin America +Saint Barthelemy,BLM,Latin America +Belize,BLZ,Latin America +"Bolivia, Plurinational State of",BOL,Latin America +Brazil,BRA,Latin America +Barbados,BRB,Latin America +Chile,CHL,Latin America +Colombia,COL,Latin America +Costa Rica,CRI,Latin America +Cuba,CUB,Latin America +Curacao,CUW,Latin America +Cayman Islands,CYM,Latin America +Dominica,DMA,Latin America +Dominican Republic,DOM,Latin America +Ecuador,ECU,Latin America +Falkland Islands (Malvinas),FLK,Latin America +Guadeloupe,GLP,Latin America +Grenada,GRD,Latin America +Guatemala,GTM,Latin America +French Guiana,GUF,Latin America +Guyana,GUY,Latin America +Honduras,HND,Latin America +Haiti,HTI,Latin America +Jamaica,JAM,Latin America +Saint Kitts and Nevis,KNA,Latin America +Saint Lucia,LCA,Latin America +Saint Martin (French part),MAF,Latin America +Mexico,MEX,North America +Montserrat,MSR,Latin America +Martinique,MTQ,Latin America +Nicaragua,NIC,Latin America +Panama,PAN,Latin America +Peru,PER,Latin America +Paraguay,PRY,Latin America +South Georgia and the South Sandwich Islands,SGS,Latin America +El Salvador,SLV,Latin America +Suriname,SUR,Latin America +Sint Maarten (Dutch part),SXM,Latin America +Turks and Caicos Islands,TCA,Latin America +Trinidad and Tobago,TTO,Latin America +Uruguay,URY,Latin America +Saint Vincent and the Grenadines,VCT,Latin America +"Venezuela, Bolivarian Republic of",VEN,Latin America +"Virgin Islands, British",VGB,Latin America +"Virgin Islands, U.S.",VIR,Latin America +United Arab Emirates,ARE,Russia China South Asia +Bahrain,BHR,Russia China South Asia +Algeria,DZA,Africa +Egypt,EGY,Africa +"Iran, Islamic Republic of",IRN,Russia China South Asia +Iraq,IRQ,Russia China South Asia +Israel,ISR,Russia China South Asia +Jordan,JOR,Russia China South Asia +Kuwait,KWT,Russia China South Asia +Lebanon,LBN,Russia China South Asia +Libya,LBY,Africa +Morocco,MAR,Africa +Oman,OMN,Russia China South Asia +"Palestine, State of",PSE,Russia China South Asia +Qatar,QAT,Russia China South Asia +Saudi Arabia,SAU,Russia China South Asia +Syrian Arab Republic,SYR,Russia China South Asia +Tunisia,TUN,Africa +Yemen,YEM,Russia China South Asia +Bermuda,BMU,North America +Canada,CAN,North America +Puerto Rico,PRI,North America +Saint Pierre and Miquelon,SPM,North America +United States Minor Outlying Islands,UMI,North America +United States,USA,North America +American Samoa,ASM,Australia Oceania +Australia,AUS,Australia Oceania +Cocos (Keeling) Islands,CCK,Australia Oceania +Cook Islands,COK,Australia Oceania +Christmas Island,CXR,Australia Oceania +Heard Island and McDonald Islands,HMD,Australia Oceania +Japan,JPN,Australia Oceania +Norfolk Island,NFK,Australia Oceania +Niue,NIU,Australia Oceania +New Zealand,NZL,Australia Oceania +French Polynesia,PYF,Australia Oceania +Tokelau,TKL,Australia Oceania +Brunei Darussalam,BRN,Southeast Asia +Fiji,FJI,Southeast Asia +"Micronesia, Federated States of",FSM,Southeast Asia +Guam,GUM,Southeast Asia +Indonesia,IDN,Southeast Asia +Kiribati,KIR,Southeast Asia +"Korea, Republic of",KOR,Southeast Asia +Marshall Islands,MHL,Southeast Asia +Northern Mariana Islands,MNP,Southeast Asia +Malaysia,MYS,Southeast Asia +New Caledonia,NCL,Southeast Asia +Nauru,NRU,Southeast Asia +Pitcairn,PCN,Southeast Asia +Philippines,PHL,Southeast Asia +Palau,PLW,Southeast Asia +Papua New Guinea,PNG,Southeast Asia +"Korea, Democratic People's Republic of",PRK,Southeast Asia +Singapore,SGP,Southeast Asia +Solomon Islands,SLB,Southeast Asia +Thailand,THA,Southeast Asia +Timor-Leste,TLS,Southeast Asia +Tonga,TON,Southeast Asia +Tuvalu,TUV,Southeast Asia +Vanuatu,VUT,Southeast Asia +Wallis and Futuna,WLF,Southeast Asia +Samoa,WSM,Southeast Asia +Afghanistan,AFG,Russia China South Asia +Bangladesh,BGD,Russia China South Asia +Bhutan,BTN,Russia China South Asia +India,IND,Russia China South Asia +British Indian Ocean Territory,IOT,Russia China South Asia +Sri Lanka,LKA,Russia China South Asia +Maldives,MDV,Russia China South Asia +Myanmar,MMR,Russia China South Asia +Nepal,NPL,Russia China South Asia +Pakistan,PAK,Russia China South Asia +Reunion,REU,Africa From 795523bf7710fc7da29db626f1043b36e6181826 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Sat, 12 Sep 2026 22:55:18 +0200 Subject: [PATCH 14/25] Use tree cover extent 2000 as the denominator of the GFW forest loss share The share was UMD tree cover loss over FRA 2020 naturally regenerating forest area, two products on different bases. downloadGFWLossByDriver now also fetches UMD tree cover extent in 2000 at the same 30% threshold from gadm__tcl__iso_summary (extent_2000.csv); readGFWLossByDriver(subtype = "extent") returns it after checking that every country with loss has extent and that cumulative 2001-2025 loss never exceeds it (max 0.998 on the real data). calcForestLossShare divides GFW loss by that extent; the Curtis source keeps FRA and reproduces the previous output bit for bit. The silent clamp of shares above 1 now warns for driver columns; overall (FRA fire area) exceeds 1 in a few savanna countries and is never read by GAMS. Numerator unchanged; the shifting_cultivation share moves by region: SSA x1.09, LAM x0.91, OAS x0.72, IND x1.74 (world denominator 3834 -> 3986 Mha). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GaYsqSd2gZdixtjH8XWu2k Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- R/calcForestLossShare.R | 48 ++++++++++++++------- R/convertGFWLossByDriver.R | 11 +++-- R/downloadGFWLossByDriver.R | 35 +++++++++++----- R/readGFWLossByDriver.R | 60 +++++++++++++++++++++++++-- man/calcForestLossShare.Rd | 20 ++++----- man/convertGFWLossByDriver.Rd | 5 ++- man/downloadGFWLossByDriver.Rd | 3 +- man/readGFWLossByDriver.Rd | 13 ++++-- tests/testthat/test-GFWLossByDriver.R | 31 ++++++++++++++ 9 files changed, 175 insertions(+), 51 deletions(-) diff --git a/R/calcForestLossShare.R b/R/calcForestLossShare.R index f230a300..f5cdcaa7 100644 --- a/R/calcForestLossShare.R +++ b/R/calcForestLossShare.R @@ -1,23 +1,24 @@ #' @title calcForestLossShare #' -#' @description Calculates which share of natural forest area is lost per year to each -#' driver of forest loss. +#' @description Calculates which share of forest area is lost per year to each driver of +#' forest loss. #' #' @param source Driver data to use, `"GFW"` (default) or `"Curtis"`. See #' [calcForestFireLoss()]. #' @param period Years averaged to give the annual rate, for `source = "GFW"` only. #' -#' @details The numerator is UMD tree cover loss (or, for Curtis, tree cover loss -#' transcribed from the paper) and the denominator is FRA 2020 naturally regenerating -#' forest area in 2010. Those two are not the same quantity: tree cover is not forest, and -#' loss detected in tree cover outside natural forest still lands in the numerator. The -#' mismatch is inherited from the original parameterisation and is left in place here on -#' purpose, so that switching the driver source moves the numbers for one reason only. -#' Changing the denominator is a separate decision. +#' @details For `source = "GFW"` numerator and denominator come from the same product at the +#' same canopy threshold: UMD tree cover loss by driver over UMD tree cover extent in 2000, +#' the base the loss is measured on. For `source = "Curtis"` the denominator stays FRA 2020 +#' naturally regenerating forest area in 2010, reproducing the previous parameterisation +#' exactly. The `overall` column (FRA fire area) is divided by the same denominator as the +#' drivers of its source and exceeds 1 in a few savanna countries; GAMS loads it but never reads +#' it. Shares above 1 are clamped to 1; for a driver column this also warns with the country names. #' -#' @return MAgPIE object with the share of natural forest area lost per year by driver +#' @return MAgPIE object with the share of forest area lost per year by driver #' @author Abhijeet Mishra, Michael Crawford -#' @importFrom magclass setYears setNames +#' @importFrom magclass setYears setNames getItems +#' @importFrom utils head #' @importFrom madrat calcOutput readSource #' @seealso [calcForestFireLoss()] #' @examples @@ -30,20 +31,35 @@ calcForestLossShare <- function(source = "GFW", period = 2015:2024) { lostArea <- calcOutput("ForestFireLoss", source = source, # nolint: undesirable_function_linter. period = period, aggregate = FALSE) - forestArea <- setYears(setNames(readSource("FRA2020", subtype = "forest_area", - convert = TRUE)[, "y2010", - "naturallyRegeneratingForest"], - NULL), NULL) + + forestArea <- switch( + source, # nolint: undesirable_function_linter. + "GFW" = readSource("GFWLossByDriver", subtype = "extent", convert = TRUE), + "Curtis" = readSource("FRA2020", subtype = "forest_area", + convert = TRUE)[, "y2010", "naturallyRegeneratingForest"], + stop("calcForestLossShare: unknown source '", source, # nolint: undesirable_function_linter. + "'. Use \"GFW\" or \"Curtis\".") + ) + forestArea <- setYears(setNames(forestArea, NULL), NULL) lostShare <- lostArea / forestArea lostShare[is.infinite(lostShare)] <- 0 lostShare[is.na(lostShare)] <- 0 + # overall (FRA fire area over the driver denominator) exceeds 1 in savanna countries; GAMS + # never reads it. A driver share above 1 would mean the numerator and denominator disagree. + drivers <- setdiff(getItems(lostShare, 3), "overall") + over <- lostShare[, , drivers] > 1 + if (any(over)) { + warning("calcForestLossShare: ", sum(over), " driver share(s) above 1 clamped to 1 (", + source, "): ", # nolint: undesirable_function_linter. + toString(utils::head(getItems(lostShare, 1)[apply(over, 1, any)], 5)), ".") + } lostShare[lostShare > 1] <- 1 return(list(x = lostShare, weight = forestArea, unit = "1", - description = paste0("Share of natural forest area lost per year by driver (", + description = paste0("Share of forest area lost per year by driver (", source, ")"), # nolint: undesirable_function_linter. isocountries = FALSE)) } diff --git a/R/convertGFWLossByDriver.R b/R/convertGFWLossByDriver.R index ff3742a3..25e265af 100644 --- a/R/convertGFWLossByDriver.R +++ b/R/convertGFWLossByDriver.R @@ -5,9 +5,10 @@ gfwIsoHosts <- c(XKO = "SRB", # Kosovo, inside Serbia in madrat's country set ZNC = "CYP", # Northern Cyprus XAD = "CYP") # Akrotiri and Dhekelia, the sovereign base areas on Cyprus -# Undocumented GADM placeholders for disputed territories, dropped rather than guessed at: -# 169 451 ha together, 0.031 per cent of global loss 2001-2025. -gfwIsoDropped <- c("Z01", "Z06", "Z07") +# Undocumented GADM placeholders for disputed territories, dropped rather than guessed at. Loss +# 2001-2025: Z01, Z06 and Z07 carry 169 451 ha together, 0.031 per cent of the global total. +# Extent 2000: 6.5 Mha over all nine, 0.16 per cent, mostly Z07 and Z01. +gfwIsoDropped <- sprintf("Z%02d", 1:9) #' @title convertGFWLossByDriver #' @@ -17,6 +18,8 @@ gfwIsoDropped <- c("Z01", "Z06", "Z07") #' not list; both are handled explicitly rather than by a silent inner join. #' #' @param x magpie object as returned by [readGFWLossByDriver()] +#' @param subtype `"loss"` or `"extent"`, as passed to the read function; the reconciliation is +#' the same for both #' @return magpie object on madrat's ISO country set, unit Mha #' @author Michael Crawford #' @importFrom madrat toolCountryFill @@ -27,7 +30,7 @@ gfwIsoDropped <- c("Z01", "Z06", "Z07") #' a <- readSource("GFWLossByDriver", convert = TRUE) #' } -convertGFWLossByDriver <- function(x) { +convertGFWLossByDriver <- function(x, subtype = "loss") { before <- sum(x) diff --git a/R/downloadGFWLossByDriver.R b/R/downloadGFWLossByDriver.R index b4c8fe7a..8355151c 100644 --- a/R/downloadGFWLossByDriver.R +++ b/R/downloadGFWLossByDriver.R @@ -1,5 +1,7 @@ -gfwDataset <- "gadm__tcl__iso_change" +gfwDataset <- "gadm__tcl__iso_change" # annual loss by country and driver gfwVersion <- "v20260424" +gfwSummaryDataset <- "gadm__tcl__iso_summary" # tree cover extent 2000 by country, same threshold +gfwSummaryVersion <- "v20260424" # Canopy density in 2000 (per cent) defining forest for the loss figures; 30 is GFW's # convention. Not a detail: global loss 2001-2025 is 647 Mha at 0 per cent and 334 at 75. @@ -12,7 +14,8 @@ gfwCanopyThreshold <- 30 #' @description Downloads tree cover loss by country, year and dominant driver from the #' Global Forest Watch data-api table `gadm__tcl__iso_change`, which cross-tabulates the #' UMD/Hansen annual tree cover loss product with the WRI/Google DeepMind 1 km -#' dominant-driver classification of Sims et al. (2025). +#' dominant-driver classification of Sims et al. (2025), plus tree cover extent in 2000 at +#' the same canopy threshold from `gadm__tcl__iso_summary`, the base the loss is measured on. #' #' Route notes: #' * `/dataset/{d}/{v}/download/csv` is open and honours the full SQL (`GROUP BY` included); @@ -40,8 +43,9 @@ downloadGFWLossByDriver <- function() { where <- paste("FROM data WHERE umd_tree_cover_density_2000__threshold =", gfwCanopyThreshold) - # Two aggregations of the same table; the read side checks that the first sums to the - # second, which is what makes a truncated driver file fail rather than pass. + # Two aggregations of the loss table (the read side checks that the first sums to the + # second, which is what makes a truncated driver file fail rather than pass) and the 2000 + # extent from the summary table, at the same threshold. queries <- list( "loss_by_driver.csv" = paste("SELECT", dims, ", wri_google_tree_cover_loss_drivers__driver AS driver,", @@ -49,20 +53,26 @@ downloadGFWLossByDriver <- function() { "GROUP BY iso, year, threshold, driver"), "loss_totals.csv" = paste("SELECT", dims, ", SUM(umd_tree_cover_loss__ha) AS loss_ha", where, - "GROUP BY iso, year, threshold") + "GROUP BY iso, year, threshold"), + "extent_2000.csv" = paste("SELECT iso, umd_tree_cover_density_2000__threshold AS threshold,", + "SUM(umd_tree_cover_extent_2000__ha) AS extent_ha", where, + "GROUP BY iso, threshold") ) + datasets <- c("loss_by_driver.csv" = paste0(gfwDataset, "/", gfwVersion), + "loss_totals.csv" = paste0(gfwDataset, "/", gfwVersion), + "extent_2000.csv" = paste0(gfwSummaryDataset, "/", gfwSummaryVersion)) headers <- c( "loss_by_driver.csv" = "\"iso\",\"year\",\"threshold\",\"driver\",\"loss_ha\"", - "loss_totals.csv" = "\"iso\",\"year\",\"threshold\",\"loss_ha\"" + "loss_totals.csv" = "\"iso\",\"year\",\"threshold\",\"loss_ha\"", + "extent_2000.csv" = "\"iso\",\"threshold\",\"extent_ha\"" ) # madrat::downloadSource() runs this under warn = 2: keep it warning-clean. local_options(timeout = max(3e6, getOption("timeout"))) for (f in names(queries)) { - url <- paste0("https://data-api.globalforestwatch.org/dataset/", gfwDataset, "/", - gfwVersion, "/download/csv?sql=", - URLencode(queries[[f]], reserved = TRUE)) + url <- paste0("https://data-api.globalforestwatch.org/dataset/", datasets[[f]], + "/download/csv?sql=", URLencode(queries[[f]], reserved = TRUE)) download.file(url, f, quiet = TRUE, mode = "wb") # download.file() errors on HTTP 400/500; any other non-CSV body is caught here. @@ -101,7 +111,9 @@ downloadGFWLossByDriver <- function() { description <- paste("Annual UMD/Hansen tree cover loss 2001-2025, in hectares,", "cross-tabulated by country and by the WRI/Google DeepMind 1 km", "dominant-driver class, at a", gfwCanopyThreshold, "per cent canopy", - "density threshold. Eight driver classes are present: the seven of", + "density threshold, plus UMD tree cover extent in 2000 at the same", + "threshold (extent_2000.csv). Eight driver classes are present: the seven", + "of", "Sims et al. (2025) plus 'Unknown'. NOTE: the driver map underlying", "this extract is the annually updated 2001-2025 version", "(wri_google_tree_cover_loss_drivers v1.13), not the 2001-2022", @@ -130,6 +142,7 @@ downloadGFWLossByDriver <- function() { license = "CC BY 4.0", version = paste0(gfwDataset, " ", gfwVersion, " (drivers map ", "wri_google_tree_cover_loss_drivers v1.13, 2001-2025)"), - unit = "ha of tree cover loss per country, year and driver", + unit = paste("ha of tree cover loss per country, year and driver;", + "ha of tree cover extent 2000 per country"), reference = reference)) } diff --git a/R/readGFWLossByDriver.R b/R/readGFWLossByDriver.R index 7cc01a55..6216e114 100644 --- a/R/readGFWLossByDriver.R +++ b/R/readGFWLossByDriver.R @@ -80,15 +80,54 @@ checkGFWLossByDriver <- function(df, totals) { return(invisible(df)) } +# Checks on the extent file against the loss file. Loss pixels are a subset of the 2000 extent +# at the same threshold, so a country's cumulative loss cannot exceed its extent; a mismatched +# version pair or threshold fails here. +checkGFWExtent <- function(ext, df) { + + required <- c("iso", "threshold", "extent_ha") + if (!all(required %in% names(ext))) { + stop("GFWLossByDriver: extent_2000.csv is missing column(s) ", + toString(setdiff(required, names(ext))), ".") + } + if (!identical(sort(unique(ext$threshold)), sort(unique(df$threshold)))) { + stop("GFWLossByDriver: extent_2000.csv carries canopy threshold ", + toString(unique(ext$threshold)), " but loss_by_driver.csv carries ", + toString(unique(df$threshold)), ".") + } + if (anyNA(ext$extent_ha) || any(ext$extent_ha < 0)) { + stop("GFWLossByDriver: extent_ha contains ", sum(is.na(ext$extent_ha)), " missing and ", + sum(ext$extent_ha < 0, na.rm = TRUE), " negative values.") + } + absent <- setdiff(unique(df$iso), ext$iso) + if (length(absent) > 0) { + stop("GFWLossByDriver: ", length(absent), " countries carry loss but have no extent, e.g. ", + toString(utils::head(absent, 5)), ".") + } + cum <- stats::aggregate(list(loss = df$loss_ha), by = list(iso = df$iso), FUN = sum) + both <- merge(cum, ext[, c("iso", "extent_ha")], by = "iso") + over <- both$loss > both$extent_ha * (1 + 1e-6) + if (any(over)) { + stop("GFWLossByDriver: cumulative loss exceeds the 2000 extent for ", sum(over), + " countries, e.g. ", toString(utils::head(both$iso[over], 5)), + ". The two files are not from the same version or threshold.") + } + + return(invisible(ext)) +} + #' @title readGFWLossByDriver #' -#' @description Reads the GFW tree cover loss by driver extract into a magpie object with -#' dimensions country x year x driver, in Mha of tree cover loss per year. +#' @description Reads the GFW extract: tree cover loss by country, year and driver +#' (`subtype = "loss"`, default), or tree cover extent in 2000 at the same canopy threshold +#' (`subtype = "extent"`), the base the loss is measured on. Both in Mha. #' #' @details The canopy threshold is pinned in [downloadGFWLossByDriver()] and stamped into #' every row; this function only insists that the extract carries exactly one. #' -#' @return magpie object, ISO country x 2001..2025 x eight driver classes, unit Mha +#' @param subtype `"loss"` (default) or `"extent"` +#' @return magpie object: loss as ISO country x 2001..2025 x eight driver classes; extent as +#' ISO country x y2000. Unit Mha. #' @author Michael Crawford #' @importFrom magclass as.magpie magpiesort #' @importFrom utils read.csv head @@ -98,9 +137,22 @@ checkGFWLossByDriver <- function(df, totals) { #' a <- readSource("GFWLossByDriver") #' } -readGFWLossByDriver <- function() { +readGFWLossByDriver <- function(subtype = "loss") { df <- read.csv("loss_by_driver.csv", stringsAsFactors = FALSE) + + if (subtype == "extent") { + ext <- read.csv("extent_2000.csv", stringsAsFactors = FALSE) + checkGFWExtent(ext, df) + ext$extent <- ext$extent_ha / 1e6 # hectares to Mha + ext$year <- 2000L + x <- as.magpie(ext[, c("iso", "year", "extent")], spatial = 1, temporal = 2) + return(magpiesort(x)) + } + if (subtype != "loss") { + stop("readGFWLossByDriver: unknown subtype '", subtype, "'. Use \"loss\" or \"extent\".") + } + totals <- read.csv("loss_totals.csv", stringsAsFactors = FALSE) checkGFWLossByDriver(df, totals) diff --git a/man/calcForestLossShare.Rd b/man/calcForestLossShare.Rd index d2155e96..4986c5ed 100644 --- a/man/calcForestLossShare.Rd +++ b/man/calcForestLossShare.Rd @@ -13,20 +13,20 @@ calcForestLossShare(source = "GFW", period = 2015:2024) \item{period}{Years averaged to give the annual rate, for `source = "GFW"` only.} } \value{ -MAgPIE object with the share of natural forest area lost per year by driver +MAgPIE object with the share of forest area lost per year by driver } \description{ -Calculates which share of natural forest area is lost per year to each -driver of forest loss. +Calculates which share of forest area is lost per year to each driver of +forest loss. } \details{ -The numerator is UMD tree cover loss (or, for Curtis, tree cover loss -transcribed from the paper) and the denominator is FRA 2020 naturally regenerating -forest area in 2010. Those two are not the same quantity: tree cover is not forest, and -loss detected in tree cover outside natural forest still lands in the numerator. The -mismatch is inherited from the original parameterisation and is left in place here on -purpose, so that switching the driver source moves the numbers for one reason only. -Changing the denominator is a separate decision. +For `source = "GFW"` numerator and denominator come from the same product at the +same canopy threshold: UMD tree cover loss by driver over UMD tree cover extent in 2000, +the base the loss is measured on. For `source = "Curtis"` the denominator stays FRA 2020 +naturally regenerating forest area in 2010, reproducing the previous parameterisation +exactly. The `overall` column (FRA fire area) is divided by the same denominator as the +drivers of its source and exceeds 1 in a few savanna countries; GAMS loads it but never reads +it. Shares above 1 are clamped to 1; for a driver column this also warns with the country names. } \examples{ \dontrun{ diff --git a/man/convertGFWLossByDriver.Rd b/man/convertGFWLossByDriver.Rd index 92f5cd24..611c319b 100644 --- a/man/convertGFWLossByDriver.Rd +++ b/man/convertGFWLossByDriver.Rd @@ -4,10 +4,13 @@ \alias{convertGFWLossByDriver} \title{convertGFWLossByDriver} \usage{ -convertGFWLossByDriver(x) +convertGFWLossByDriver(x, subtype = "loss") } \arguments{ \item{x}{magpie object as returned by [readGFWLossByDriver()]} + +\item{subtype}{`"loss"` or `"extent"`, as passed to the read function; the reconciliation is +the same for both} } \value{ magpie object on madrat's ISO country set, unit Mha diff --git a/man/downloadGFWLossByDriver.Rd b/man/downloadGFWLossByDriver.Rd index dc7d558b..c58c8ceb 100644 --- a/man/downloadGFWLossByDriver.Rd +++ b/man/downloadGFWLossByDriver.Rd @@ -10,7 +10,8 @@ downloadGFWLossByDriver() Downloads tree cover loss by country, year and dominant driver from the Global Forest Watch data-api table `gadm__tcl__iso_change`, which cross-tabulates the UMD/Hansen annual tree cover loss product with the WRI/Google DeepMind 1 km -dominant-driver classification of Sims et al. (2025). +dominant-driver classification of Sims et al. (2025), plus tree cover extent in 2000 at +the same canopy threshold from `gadm__tcl__iso_summary`, the base the loss is measured on. Route notes: * `/dataset/{d}/{v}/download/csv` is open and honours the full SQL (`GROUP BY` included); diff --git a/man/readGFWLossByDriver.Rd b/man/readGFWLossByDriver.Rd index a059e512..2846ef71 100644 --- a/man/readGFWLossByDriver.Rd +++ b/man/readGFWLossByDriver.Rd @@ -4,14 +4,19 @@ \alias{readGFWLossByDriver} \title{readGFWLossByDriver} \usage{ -readGFWLossByDriver() +readGFWLossByDriver(subtype = "loss") +} +\arguments{ +\item{subtype}{`"loss"` (default) or `"extent"`} } \value{ -magpie object, ISO country x 2001..2025 x eight driver classes, unit Mha +magpie object: loss as ISO country x 2001..2025 x eight driver classes; extent as +ISO country x y2000. Unit Mha. } \description{ -Reads the GFW tree cover loss by driver extract into a magpie object with -dimensions country x year x driver, in Mha of tree cover loss per year. +Reads the GFW extract: tree cover loss by country, year and driver +(`subtype = "loss"`, default), or tree cover extent in 2000 at the same canopy threshold +(`subtype = "extent"`), the base the loss is measured on. Both in Mha. } \details{ The canopy threshold is pinned in [downloadGFWLossByDriver()] and stamped into diff --git a/tests/testthat/test-GFWLossByDriver.R b/tests/testthat/test-GFWLossByDriver.R index 8a5b921f..c115beca 100644 --- a/tests/testthat/test-GFWLossByDriver.R +++ b/tests/testthat/test-GFWLossByDriver.R @@ -18,6 +18,12 @@ makeTotals <- function(df) { t[, c("iso", "year", "threshold", "loss_ha")] } +makeExtent <- function(df) { + iso <- unique(df$iso) + data.frame(iso = iso, threshold = as.integer(gfwCanopyThreshold), # nolint: object_usage_linter. + extent_ha = 2 * sum(df$loss_ha) / length(iso), stringsAsFactors = FALSE) +} + test_that("the guard passes clean data", { df <- makeExtract() expect_silent(checkGFWLossByDriver(df, makeTotals(df))) @@ -87,6 +93,31 @@ test_that("a missing column is caught", { expect_error(checkGFWLossByDriver(df[, -3], makeTotals(df)), "missing column") }) +test_that("a consistent extent file passes", { + df <- makeExtract() + expect_silent(checkGFWExtent(makeExtent(df), df)) +}) + +test_that("a country with loss but no extent is caught", { + df <- makeExtract() + ext <- makeExtent(df) + expect_error(checkGFWExtent(ext[ext$iso != "C07", ], df), "no extent") +}) + +test_that("cumulative loss above the 2000 extent is caught", { + df <- makeExtract() + ext <- makeExtent(df) + ext$extent_ha[ext$iso == "C07"] <- 1 + expect_error(checkGFWExtent(ext, df), "exceeds the 2000 extent") +}) + +test_that("an extent file at another threshold is caught", { + df <- makeExtract() + ext <- makeExtent(df) + ext$threshold <- 75L + expect_error(checkGFWExtent(ext, df), "canopy threshold") +}) + # Benign perturbations: a guard that rejects everything is as useless as one that rejects nothing. test_that("row order does not matter", { From 2fe6a09723c618d6693ac2744e5c386e955f0d15 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Mon, 14 Sep 2026 12:02:23 +0200 Subject: [PATCH 15/25] Rename calcForestFireLoss to calcForestLossByDriver The function has returned forest area lost per year by driver since the Curtis table was bound into it (51a8a88, 2020); the FRA fire series it started as is now the overall column. calcForestLossShare is its only caller. No output change: f35_forest_lost_share is identical for both sources. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- ...lcForestFireLoss.R => calcForestLossByDriver.R} | 14 +++++++------- R/calcForestLossShare.R | 6 +++--- ...ForestFireLoss.Rd => calcForestLossByDriver.Rd} | 14 +++++++------- man/calcForestLossShare.Rd | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) rename R/{calcForestFireLoss.R => calcForestLossByDriver.R} (84%) rename man/{calcForestFireLoss.Rd => calcForestLossByDriver.Rd} (74%) diff --git a/R/calcForestFireLoss.R b/R/calcForestLossByDriver.R similarity index 84% rename from R/calcForestFireLoss.R rename to R/calcForestLossByDriver.R index 1ba99fa0..91f99e8a 100644 --- a/R/calcForestFireLoss.R +++ b/R/calcForestLossByDriver.R @@ -7,7 +7,7 @@ # MAgPIE's own timber harvest, wildfire and other natural disturbances are natural. gfwModelDrivers <- c("shifting_cultivation") -#' @title calcForestFireLoss +#' @title calcForestLossByDriver #' #' @description Annual forest area loss by driver, Mha per year, from the Sims et al. (2025) #' driver product (`source = "GFW"`, default) or the Curtis et al. (2018) table @@ -29,11 +29,11 @@ gfwModelDrivers <- c("shifting_cultivation") #' @seealso [readGFWLossByDriver()], [readForestLossDrivers()] #' @examples #' \dontrun{ -#' calcOutput("ForestFireLoss", aggregate = FALSE) -#' calcOutput("ForestFireLoss", source = "Curtis", aggregate = FALSE) +#' calcOutput("ForestLossByDriver", aggregate = FALSE) +#' calcOutput("ForestLossByDriver", source = "Curtis", aggregate = FALSE) #' } -calcForestFireLoss <- function(source = "GFW", period = 2015:2024) { +calcForestLossByDriver <- function(source = "GFW", period = 2015:2024) { fao <- setNames(readSource("FRA2020", subtype = "forest_fire", convert = TRUE), "overall") fao <- dimSums(fao, dim = 2) / length(getYears(fao)) # mean annual area lost to fire @@ -46,17 +46,17 @@ calcForestFireLoss <- function(source = "GFW", period = 2015:2024) { wanted <- paste0("y", period) absent <- setdiff(wanted, getYears(x)) if (length(absent) > 0) { - stop("calcForestFireLoss: the GFW record does not cover ", toString(absent), + stop("calcForestLossByDriver: the GFW record does not cover ", toString(absent), ". It runs ", min(getYears(x, TRUE)), "-", max(getYears(x, TRUE)), ".") } absent <- setdiff(gfwModelDrivers, getItems(x, 3)) if (length(absent) > 0) { - stop("calcForestFireLoss: the GFW source does not carry driver class(es) ", + stop("calcForestLossByDriver: the GFW source does not carry driver class(es) ", toString(absent), ".") } dimSums(x[, wanted, gfwModelDrivers], dim = 2) / length(wanted) }, - stop("calcForestFireLoss: unknown source '", source, # nolint: undesirable_function_linter. + stop("calcForestLossByDriver: unknown source '", source, # nolint: undesirable_function_linter. "'. Use \"GFW\" or \"Curtis\".") ) diff --git a/R/calcForestLossShare.R b/R/calcForestLossShare.R index f5cdcaa7..e505fb6d 100644 --- a/R/calcForestLossShare.R +++ b/R/calcForestLossShare.R @@ -4,7 +4,7 @@ #' forest loss. #' #' @param source Driver data to use, `"GFW"` (default) or `"Curtis"`. See -#' [calcForestFireLoss()]. +#' [calcForestLossByDriver()]. #' @param period Years averaged to give the annual rate, for `source = "GFW"` only. #' #' @details For `source = "GFW"` numerator and denominator come from the same product at the @@ -20,7 +20,7 @@ #' @importFrom magclass setYears setNames getItems #' @importFrom utils head #' @importFrom madrat calcOutput readSource -#' @seealso [calcForestFireLoss()] +#' @seealso [calcForestLossByDriver()] #' @examples #' \dontrun{ #' calcOutput("ForestLossShare", aggregate = FALSE) @@ -29,7 +29,7 @@ calcForestLossShare <- function(source = "GFW", period = 2015:2024) { - lostArea <- calcOutput("ForestFireLoss", source = source, # nolint: undesirable_function_linter. + lostArea <- calcOutput("ForestLossByDriver", source = source, # nolint: undesirable_function_linter. period = period, aggregate = FALSE) forestArea <- switch( diff --git a/man/calcForestFireLoss.Rd b/man/calcForestLossByDriver.Rd similarity index 74% rename from man/calcForestFireLoss.Rd rename to man/calcForestLossByDriver.Rd index a34cbf29..24f95b4b 100644 --- a/man/calcForestFireLoss.Rd +++ b/man/calcForestLossByDriver.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/calcForestFireLoss.R -\name{calcForestFireLoss} -\alias{calcForestFireLoss} -\title{calcForestFireLoss} +% Please edit documentation in R/calcForestLossByDriver.R +\name{calcForestLossByDriver} +\alias{calcForestLossByDriver} +\title{calcForestLossByDriver} \usage{ -calcForestFireLoss(source = "GFW", period = 2015:2024) +calcForestLossByDriver(source = "GFW", period = 2015:2024) } \arguments{ \item{source}{`"GFW"` (default) or `"Curtis"`. Curtis computes as before, from the @@ -28,8 +28,8 @@ reads it. } \examples{ \dontrun{ -calcOutput("ForestFireLoss", aggregate = FALSE) -calcOutput("ForestFireLoss", source = "Curtis", aggregate = FALSE) +calcOutput("ForestLossByDriver", aggregate = FALSE) +calcOutput("ForestLossByDriver", source = "Curtis", aggregate = FALSE) } } \seealso{ diff --git a/man/calcForestLossShare.Rd b/man/calcForestLossShare.Rd index 4986c5ed..2c782e3b 100644 --- a/man/calcForestLossShare.Rd +++ b/man/calcForestLossShare.Rd @@ -8,7 +8,7 @@ calcForestLossShare(source = "GFW", period = 2015:2024) } \arguments{ \item{source}{Driver data to use, `"GFW"` (default) or `"Curtis"`. See -[calcForestFireLoss()].} +[calcForestLossByDriver()].} \item{period}{Years averaged to give the annual rate, for `source = "GFW"` only.} } @@ -35,7 +35,7 @@ calcOutput("ForestLossShare", source = "Curtis", aggregate = FALSE) } } \seealso{ -[calcForestFireLoss()] +[calcForestLossByDriver()] } \author{ Abhijeet Mishra, Michael Crawford From 9f6f91ec3ca89798d0e0dab9b11e3fa4a8358f77 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Mon, 14 Sep 2026 13:04:38 +0200 Subject: [PATCH 16/25] Remove lost braces from the downloadGFWLossByDriver route note `{d}` and `{v}` are Rd markup in non-markdown roxygen; R CMD check noted "Lost braces" at downloadGFWLossByDriver.Rd:17. Now `` and ``. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBJaUn8DLgzgyZUc8CRunM --- R/downloadGFWLossByDriver.R | 2 +- man/downloadGFWLossByDriver.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/downloadGFWLossByDriver.R b/R/downloadGFWLossByDriver.R index 8355151c..ace4f716 100644 --- a/R/downloadGFWLossByDriver.R +++ b/R/downloadGFWLossByDriver.R @@ -18,7 +18,7 @@ gfwCanopyThreshold <- 30 #' the same canopy threshold from `gadm__tcl__iso_summary`, the base the loss is measured on. #' #' Route notes: -#' * `/dataset/{d}/{v}/download/csv` is open and honours the full SQL (`GROUP BY` included); +#' * `/dataset///download/csv` is open and honours the full SQL (`GROUP BY` included); #' only `/query` needs an API key. #' * Dataset versions are immutable (`is_mutable: false`), so the pinned URL is the #' reproducibility mechanism. No checksum, as elsewhere in the stack. diff --git a/man/downloadGFWLossByDriver.Rd b/man/downloadGFWLossByDriver.Rd index c58c8ceb..b38166e4 100644 --- a/man/downloadGFWLossByDriver.Rd +++ b/man/downloadGFWLossByDriver.Rd @@ -14,7 +14,7 @@ dominant-driver classification of Sims et al. (2025), plus tree cover extent in the same canopy threshold from `gadm__tcl__iso_summary`, the base the loss is measured on. Route notes: -* `/dataset/{d}/{v}/download/csv` is open and honours the full SQL (`GROUP BY` included); +* `/dataset///download/csv` is open and honours the full SQL (`GROUP BY` included); only `/query` needs an API key. * Dataset versions are immutable (`is_mutable: false`), so the pinned URL is the reproducibility mechanism. No checksum, as elsewhere in the stack. From c073b5f8dbc7848cf887bdc8b58e095a340a5d5a Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Tue, 22 Sep 2026 13:34:44 +0200 Subject: [PATCH 17/25] Address review: remove SOURCE.md, the Curtis table check and the test file Remove inst/extdata/ForestLossDrivers/SOURCE.md, checkCurtisTable1() and tests/testthat/test-GFWLossByDriver.R, move the GFW constants into the function bodies and shorten the comments in the ForestLossDrivers and GFWLossByDriver functions. Rd files regenerated. Co-Authored-By: Claude Fable 5.1 --- R/calcForestLossByDriver.R | 37 +++--- R/calcForestLossShare.R | 23 ++-- R/convertGFWLossByDriver.R | 39 +++---- R/downloadForestLossDrivers.R | 23 +--- R/downloadGFWLossByDriver.R | 57 ++++----- R/readForestLossDrivers.R | 53 +-------- R/readGFWLossByDriver.R | 38 +++--- inst/extdata/ForestLossDrivers/SOURCE.md | 76 ------------ man/calcForestLossByDriver.Rd | 21 ++-- man/calcForestLossShare.Rd | 19 ++- man/convertGFWLossByDriver.Rd | 18 +-- man/downloadForestLossDrivers.Rd | 23 +--- man/downloadGFWLossByDriver.Rd | 21 +--- man/readGFWLossByDriver.Rd | 19 ++- tests/testthat/test-GFWLossByDriver.R | 140 ----------------------- 15 files changed, 133 insertions(+), 474 deletions(-) delete mode 100644 inst/extdata/ForestLossDrivers/SOURCE.md delete mode 100644 tests/testthat/test-GFWLossByDriver.R diff --git a/R/calcForestLossByDriver.R b/R/calcForestLossByDriver.R index 91f99e8a..5590d016 100644 --- a/R/calcForestLossByDriver.R +++ b/R/calcForestLossByDriver.R @@ -1,32 +1,18 @@ -# Driver classes that reach f35_forest_lost_share. Module 35 resets the age structure of the -# "lost" area (the land stays forest) and module 52 books the flux as land-use-change CO2, so a -# class qualifies only if it leaves the land as forest, is not decided endogenously by MAgPIE, -# and is a land-use rather than a natural process. Of the eight GFW classes only shifting -# cultivation passes. The others are published by mrvalidation::calcValidTreeCoverLoss: -# permanent agriculture, hard commodities and settlements remove the forest, logging is -# MAgPIE's own timber harvest, wildfire and other natural disturbances are natural. -gfwModelDrivers <- c("shifting_cultivation") - #' @title calcForestLossByDriver #' -#' @description Annual forest area loss by driver, Mha per year, from the Sims et al. (2025) -#' driver product (`source = "GFW"`, default) or the Curtis et al. (2018) table -#' (`source = "Curtis"`), with the FRA 2020 fire series bound on as `overall`. -#' -#' @param source `"GFW"` (default) or `"Curtis"`. Curtis computes as before, from the -#' corrected Curtis et al. (2018) table. -#' @param period Years averaged into the annual rate, GFW only (default 2015:2024). Curtis -#' carries a single 2001-2015 mean. -#' -#' @details For GFW only the classes in `gfwModelDrivers` are returned. `overall` is the FRA -#' fire series averaged over its own years (2000-2017), as before; GAMS loads it but never -#' reads it. +#' @description Forest area lost per year by driver, in Mha, from the Global Forest Watch driver +#' product of Sims et al. (2025) (\code{source = "GFW"}, default) or from Table 1 of Curtis et al. +#' (2018) (\code{source = "Curtis"}), together with the FRA 2020 forest fire area as +#' \code{overall}. #' +#' @param source \code{"GFW"} (default) or \code{"Curtis"} +#' @param period Years averaged into the annual rate, GFW only; Curtis carries a single 2001-2015 +#' mean #' @return MAgPIE object with forest area lost per year by driver, Mha #' @author Abhijeet Mishra, Michael Crawford #' @importFrom magclass mbind setNames dimSums getYears getItems #' @importFrom madrat readSource -#' @seealso [readGFWLossByDriver()], [readForestLossDrivers()] +#' @seealso \code{\link{readGFWLossByDriver}}, \code{\link{readForestLossDrivers}} #' @examples #' \dontrun{ #' calcOutput("ForestLossByDriver", aggregate = FALSE) @@ -35,8 +21,13 @@ gfwModelDrivers <- c("shifting_cultivation") calcForestLossByDriver <- function(source = "GFW", period = 2015:2024) { + # Only shifting cultivation is passed on to MAgPIE: it leaves the land as forest and MAgPIE does + # not model it itself. Clearing for agriculture, commodities and settlements removes the forest, + # logging is MAgPIE's own harvest, wildfire and other natural disturbances are natural processes. + gfwModelDrivers <- c("shifting_cultivation") + fao <- setNames(readSource("FRA2020", subtype = "forest_fire", convert = TRUE), "overall") - fao <- dimSums(fao, dim = 2) / length(getYears(fao)) # mean annual area lost to fire + fao <- dimSums(fao, dim = 2) / length(getYears(fao)) # mean annual area lost to fire, Mha drivers <- switch( source, # nolint: undesirable_function_linter. diff --git a/R/calcForestLossShare.R b/R/calcForestLossShare.R index e505fb6d..30f966c3 100644 --- a/R/calcForestLossShare.R +++ b/R/calcForestLossShare.R @@ -3,24 +3,21 @@ #' @description Calculates which share of forest area is lost per year to each driver of #' forest loss. #' -#' @param source Driver data to use, `"GFW"` (default) or `"Curtis"`. See -#' [calcForestLossByDriver()]. -#' @param period Years averaged to give the annual rate, for `source = "GFW"` only. +#' @param source Driver data to use, \code{"GFW"} (default) or \code{"Curtis"}, see +#' \code{\link{calcForestLossByDriver}} +#' @param period Years averaged into the annual rate, GFW only #' -#' @details For `source = "GFW"` numerator and denominator come from the same product at the -#' same canopy threshold: UMD tree cover loss by driver over UMD tree cover extent in 2000, -#' the base the loss is measured on. For `source = "Curtis"` the denominator stays FRA 2020 -#' naturally regenerating forest area in 2010, reproducing the previous parameterisation -#' exactly. The `overall` column (FRA fire area) is divided by the same denominator as the -#' drivers of its source and exceeds 1 in a few savanna countries; GAMS loads it but never reads -#' it. Shares above 1 are clamped to 1; for a driver column this also warns with the country names. +#' @details The denominator is tree cover extent in 2000 at the same canopy threshold for +#' \code{source = "GFW"} and FRA 2020 naturally regenerating forest area in 2010 for +#' \code{source = "Curtis"}. Shares above 1 are clamped to 1; for a driver column this also +#' warns with the country names. #' #' @return MAgPIE object with the share of forest area lost per year by driver #' @author Abhijeet Mishra, Michael Crawford #' @importFrom magclass setYears setNames getItems #' @importFrom utils head #' @importFrom madrat calcOutput readSource -#' @seealso [calcForestLossByDriver()] +#' @seealso \code{\link{calcForestLossByDriver}} #' @examples #' \dontrun{ #' calcOutput("ForestLossShare", aggregate = FALSE) @@ -45,8 +42,8 @@ calcForestLossShare <- function(source = "GFW", period = 2015:2024) { lostShare <- lostArea / forestArea lostShare[is.infinite(lostShare)] <- 0 lostShare[is.na(lostShare)] <- 0 - # overall (FRA fire area over the driver denominator) exceeds 1 in savanna countries; GAMS - # never reads it. A driver share above 1 would mean the numerator and denominator disagree. + # overall (FRA fire area) exceeds 1 in some savanna countries and is not read by MAgPIE; a driver + # share above 1 means numerator and denominator disagree drivers <- setdiff(getItems(lostShare, 3), "overall") over <- lostShare[, , drivers] > 1 if (any(over)) { diff --git a/R/convertGFWLossByDriver.R b/R/convertGFWLossByDriver.R index 25e265af..b6ef4dc6 100644 --- a/R/convertGFWLossByDriver.R +++ b/R/convertGFWLossByDriver.R @@ -1,30 +1,18 @@ -# GADM codes that are not madrat ISO countries but sit inside one, so their loss is added -# to the host rather than discarded. Measured over 2001-2025 at a 30 per cent threshold: -# XKO 19 126 ha, ZNC 1740 ha, XAD 80 ha. -gfwIsoHosts <- c(XKO = "SRB", # Kosovo, inside Serbia in madrat's country set - ZNC = "CYP", # Northern Cyprus - XAD = "CYP") # Akrotiri and Dhekelia, the sovereign base areas on Cyprus - -# Undocumented GADM placeholders for disputed territories, dropped rather than guessed at. Loss -# 2001-2025: Z01, Z06 and Z07 carry 169 451 ha together, 0.031 per cent of the global total. -# Extent 2000: 6.5 Mha over all nine, 0.16 per cent, mostly Z07 and Z01. -gfwIsoDropped <- sprintf("Z%02d", 1:9) - #' @title convertGFWLossByDriver #' -#' @description Reconciles the GADM country set used by GFW with madrat's ISO set. +#' @description Moves the GFW data from the GADM country set to madrat's ISO country set: Kosovo, +#' Northern Cyprus and the Akrotiri and Dhekelia base areas are added to Serbia and Cyprus, GADM's +#' placeholder codes for disputed territories are dropped and countries GFW does not list are +#' filled with zero. #' -#' GADM and madrat disagree on 6 codes carrying loss and on 39 madrat countries GFW does -#' not list; both are handled explicitly rather than by a silent inner join. -#' -#' @param x magpie object as returned by [readGFWLossByDriver()] -#' @param subtype `"loss"` or `"extent"`, as passed to the read function; the reconciliation is -#' the same for both -#' @return magpie object on madrat's ISO country set, unit Mha +#' @param x magpie object as returned by \code{\link{readGFWLossByDriver}} +#' @param subtype \code{"loss"} or \code{"extent"}, as passed to the read function; the conversion +#' is the same for both +#' @return magpie object on madrat's ISO country set, Mha #' @author Michael Crawford #' @importFrom madrat toolCountryFill #' @importFrom magclass getItems -#' @seealso [readGFWLossByDriver()] +#' @seealso \code{\link{readGFWLossByDriver}} #' @examples #' \dontrun{ #' a <- readSource("GFWLossByDriver", convert = TRUE) @@ -32,6 +20,13 @@ gfwIsoDropped <- sprintf("Z%02d", 1:9) convertGFWLossByDriver <- function(x, subtype = "loss") { + # GADM codes inside a madrat country; their loss is added to the host rather than dropped + gfwIsoHosts <- c(XKO = "SRB", # Kosovo + ZNC = "CYP", # Northern Cyprus + XAD = "CYP") # Akrotiri and Dhekelia + # GADM placeholders for disputed territories, together about 0.03 per cent of global loss + gfwIsoDropped <- sprintf("Z%02d", 1:9) + before <- sum(x) for (code in names(gfwIsoHosts)) { @@ -49,7 +44,7 @@ convertGFWLossByDriver <- function(x, subtype = "loss") { out <- toolCountryFill(x, fill = 0, no_remove_warning = gfwIsoDropped) - # A future version moving real loss onto a code madrat does not know must fail here. + # a new GADM code carrying real loss must fail here rather than vanish lost <- (before - sum(out)) / before if (lost > 0.005) { stop("GFWLossByDriver: reconciling GADM with madrat's ISO set dropped ", diff --git a/R/downloadForestLossDrivers.R b/R/downloadForestLossDrivers.R index 777b8dc3..9ac3fc7c 100644 --- a/R/downloadForestLossDrivers.R +++ b/R/downloadForestLossDrivers.R @@ -1,26 +1,13 @@ #' @title downloadForestLossDrivers #' -#' @description Provisions the ForestLossDrivers source: the regional tree cover loss -#' totals and driver shares of Curtis et al. (2018), Table 1, plus the country to -#' source-region mapping used to disaggregate them. -#' -#' Table 1 is published only inside the article; there is no machine-readable release of -#' it. The table is therefore held as version-controlled package data under -#' `inst/extdata/ForestLossDrivers` and staged into the madrat source folder here, so -#' that `readSource("ForestLossDrivers")` works on a machine that has never seen the -#' shared source directory, and so that any change to the numbers is a reviewable diff -#' rather than a silent edit of a shared file. The transcription record, including the -#' known deviations from the printed table, is in -#' `inst/extdata/ForestLossDrivers/SOURCE.md`. -#' -#' The underlying gridded product - the 10 km dominant-driver classification - is -#' distributed separately by Global Forest Watch under CC BY 4.0 as a tiled raster set -#' (`tsc_tree_cover_loss_drivers`). It is deliberately not fetched here: deriving the -#' regional totals from the raster would be a change of method, not a change of source. +#' @description Stages the ForestLossDrivers source files shipped with the package +#' (\code{inst/extdata/ForestLossDrivers}) into the madrat source folder: Table 1 of Curtis et al. +#' (2018), tree cover loss 2001-2015 by region and dominant driver, and the mapping of countries +#' to the source regions. Table 1 has no machine-readable release and is transcribed by hand. #' #' @author Michael Crawford #' @importFrom utils bibentry person -#' @seealso [readForestLossDrivers()] +#' @seealso \code{\link{readForestLossDrivers}} #' @examples #' \dontrun{ #' madrat::downloadSource("ForestLossDrivers") diff --git a/R/downloadGFWLossByDriver.R b/R/downloadGFWLossByDriver.R index ace4f716..0b0b581d 100644 --- a/R/downloadGFWLossByDriver.R +++ b/R/downloadGFWLossByDriver.R @@ -1,36 +1,14 @@ -gfwDataset <- "gadm__tcl__iso_change" # annual loss by country and driver -gfwVersion <- "v20260424" -gfwSummaryDataset <- "gadm__tcl__iso_summary" # tree cover extent 2000 by country, same threshold -gfwSummaryVersion <- "v20260424" - -# Canopy density in 2000 (per cent) defining forest for the loss figures; 30 is GFW's -# convention. Not a detail: global loss 2001-2025 is 647 Mha at 0 per cent and 334 at 75. -# Dataset, version and threshold are pinned here only; the read side takes the threshold -# from the data. -gfwCanopyThreshold <- 30 - #' @title downloadGFWLossByDriver #' -#' @description Downloads tree cover loss by country, year and dominant driver from the -#' Global Forest Watch data-api table `gadm__tcl__iso_change`, which cross-tabulates the -#' UMD/Hansen annual tree cover loss product with the WRI/Google DeepMind 1 km -#' dominant-driver classification of Sims et al. (2025), plus tree cover extent in 2000 at -#' the same canopy threshold from `gadm__tcl__iso_summary`, the base the loss is measured on. -#' -#' Route notes: -#' * `/dataset///download/csv` is open and honours the full SQL (`GROUP BY` included); -#' only `/query` needs an API key. -#' * Dataset versions are immutable (`is_mutable: false`), so the pinned URL is the -#' reproducibility mechanism. No checksum, as elsewhere in the stack. -#' * The driver map is the annually updated `wri_google_tree_cover_loss_drivers` v1.13 -#' (2001-2025), not the paper's v20241121 (2001-2022). Regional shares therefore do not -#' reproduce the paper's tables; Africa's permanent/shifting split moved by about 11 pp. -#' * Versions are listed under `data.versions`; `is_latest` is `false` on all of them. +#' @description Downloads tree cover loss by country, year and dominant driver from the Global +#' Forest Watch data API, table \code{gadm__tcl__iso_change}: the UMD/Hansen annual tree cover loss +#' cross-tabulated with the 1 km dominant-driver map of Sims et al. (2025). Tree cover extent in +#' 2000 at the same canopy threshold is taken from \code{gadm__tcl__iso_summary}. #' #' @author Michael Crawford #' @importFrom utils download.file URLencode read.csv bibentry person #' @importFrom withr local_options -#' @seealso [readGFWLossByDriver()] +#' @seealso \code{\link{readGFWLossByDriver}} #' @examples #' \dontrun{ #' madrat::downloadSource("GFWLossByDriver") @@ -38,14 +16,27 @@ gfwCanopyThreshold <- 30 downloadGFWLossByDriver <- function() { + # Dataset versions are immutable, so the pinned version fixes the download. The driver map behind + # this version is the annually updated wri_google_tree_cover_loss_drivers v1.13 (2001-2025), not + # the 2001-2022 map published with the paper, so regional driver shares differ from the paper's + # tables. + # Versions are listed under data.versions in the API response; is_latest is false on all of + # them, so a re-pin has to pick by date rather than by that flag. + gfwDataset <- "gadm__tcl__iso_change" # annual loss by country and driver + gfwVersion <- "v20260424" + gfwSummaryDataset <- "gadm__tcl__iso_summary" # tree cover extent 2000 by country + gfwSummaryVersion <- "v20260424" + # canopy density in 2000 (per cent) defining forest, GFW's convention; the total is sensitive to + # it (global loss 2001-2025 is 647 Mha at 0 per cent and 334 Mha at 75) + gfwCanopyThreshold <- 30 + dims <- paste("iso, umd_tree_cover_loss__year AS year,", "umd_tree_cover_density_2000__threshold AS threshold") where <- paste("FROM data WHERE umd_tree_cover_density_2000__threshold =", gfwCanopyThreshold) - # Two aggregations of the loss table (the read side checks that the first sums to the - # second, which is what makes a truncated driver file fail rather than pass) and the 2000 - # extent from the summary table, at the same threshold. + # loss by driver, the same loss without the driver split (the read function checks that the two + # agree) and the 2000 extent queries <- list( "loss_by_driver.csv" = paste("SELECT", dims, ", wri_google_tree_cover_loss_drivers__driver AS driver,", @@ -67,22 +58,22 @@ downloadGFWLossByDriver <- function() { "extent_2000.csv" = "\"iso\",\"threshold\",\"extent_ha\"" ) - # madrat::downloadSource() runs this under warn = 2: keep it warning-clean. + # a timeout warning would be an error, downloadSource() runs this with warn = 2 local_options(timeout = max(3e6, getOption("timeout"))) for (f in names(queries)) { + # the open download route honours the full SQL; only the query route needs an API key url <- paste0("https://data-api.globalforestwatch.org/dataset/", datasets[[f]], "/download/csv?sql=", URLencode(queries[[f]], reserved = TRUE)) download.file(url, f, quiet = TRUE, mode = "wb") - # download.file() errors on HTTP 400/500; any other non-CSV body is caught here. + # download.file() errors on HTTP 400/500; any other non-CSV body is caught here header <- readLines(f, n = 1, warn = FALSE) if (!identical(header, unname(headers[f]))) { stop("GFWLossByDriver: the endpoint did not return the expected CSV for ", f, ". First line was: ", substr(header, 1, 200)) } - # Did we get the threshold we asked for? Checked here, where the pin is defined. got <- unique(read.csv(f, stringsAsFactors = FALSE)$threshold) if (!identical(got, as.integer(gfwCanopyThreshold))) { stop("GFWLossByDriver: asked for canopy threshold ", gfwCanopyThreshold, " but ", f, diff --git a/R/readForestLossDrivers.R b/R/readForestLossDrivers.R index e5013063..c9bec447 100644 --- a/R/readForestLossDrivers.R +++ b/R/readForestLossDrivers.R @@ -1,33 +1,3 @@ -# Table 1 of Curtis et al. (2018) is hand-typed (inst/extdata/ForestLossDrivers/SOURCE.md). -# These identities of the printed table are checked on every read; a wrong cell does not -# fail otherwise, it becomes a wrong disturbance rate. -checkCurtisTable1 <- function(df) { - globalLoss <- 314 # Mha 2001-2015, "Global" row of Table 1 - drivers <- c("deforestation", "shifting_agriculture", "forestry", "wildfire", "urbanization") - - total <- sum(df$treecoverloss_01_15) - if (abs(total - globalLoss) > 2) { - stop("ForestLossDrivers: regional tree cover loss sums to ", total, " Mha, but Table 1 of ", - "Curtis et al. (2018) reports a global total of ", globalLoss, - " Mha. forest_loss.csv is corrupt.") - } - - offBy <- abs(rowSums(df[drivers]) - 100) - if (any(offBy > 2)) { - stop("ForestLossDrivers: driver shares do not sum to 100 per cent for ", - toString(df$region[offBy > 2]), " in forest_loss.csv.") - } - - implied <- 100 * df$treecoverloss_01_15 / total - mismatch <- abs(implied - df$treecoverloss_pc_01_15) > 2 - if (any(mismatch)) { - stop("ForestLossDrivers: absolute and relative tree cover loss disagree for ", - toString(df$region[mismatch]), " in forest_loss.csv.") - } - - return(invisible(df)) -} - #' Read ForestLossDrivers #' #' Read-in an Forest loss data (range 2001-2015 but only single annual number her) @@ -49,34 +19,21 @@ checkCurtisTable1 <- function(df) { #' @importFrom stats complete.cases readForestLossDrivers <- function() { - ## Mapping file: comma-separated in the package since 0.76.0; a shared source folder may still - ## hold the semicolon-separated original - sep <- if (grepl(";", readLines("mapping.csv", n = 1))) ";" else "," - mapping <- read.csv("mapping.csv", header = TRUE, sep = sep) - - ## Magpie standard + mapping <- read.csv("mapping.csv", header = TRUE, sep = ",") isoCountry <- toolGetMapping(type = "regional", name = "regionmappingH12.csv", where = "madrat") - - ## Merge Mappings fullMapping <- merge(mapping, isoCountry, by = "CountryCode")[, c(-2, -4)] colnames(fullMapping) <- c("CountryCode", "RegionCodeSource", "RegionCodeMAgPIE") - ## Original Data + # Table 1 of Curtis et al. (2018): tree cover loss 2001-2015 in Mha, driver shares in per cent file <- "forest_loss.csv" - # This is Tree Cover loss in Mha at this stage, all drivers are in percentage df <- read.csv(file = file, header = TRUE, sep = ",") - checkCurtisTable1(df) - # percentages to proportions (/ 100), 2001-2015 totals to annual values (/ 15) + # per cent to share (/ 100), 2001-2015 total to annual (/ 15) df[, -c(1:3)] <- (df$treecoverloss_01_15 * (df[, -c(1:3)] / 100)) / 15 + dfMag <- as.magpie(df[, -c(2, 3)], temporal = NULL, spatial = "region") - dfMag <- as.magpie(df[, -c(2, 3)], temporal = NULL, spatial = "region") # tree cover loss, Mha - - ## FRA Forest Area - # Convert is set to TRUE For Mha + # disaggregate to countries, weighted by FRA 2020 naturally regenerating forest area (Mha) a <- readSource("FRA2020", "forest_area", convert = TRUE)[, , "naturallyRegeneratingForest"] a <- collapseNames(a) - - ## Create iso level data based on forest data as weight forestLoss <- toolAggregate(x = dfMag, weight = setYears(a[, "y2015", ], NULL), rel = fullMapping, from = "RegionCodeSource", to = "CountryCode") diff --git a/R/readGFWLossByDriver.R b/R/readGFWLossByDriver.R index 6216e114..ff301190 100644 --- a/R/readGFWLossByDriver.R +++ b/R/readGFWLossByDriver.R @@ -1,5 +1,5 @@ -# Driver class strings as they occur in the data, and their GAMS-safe names. Eight, not the -# paper's seven: the data carries `Unknown` (0.3 per cent of global loss). +# driver classes as named in the data, with GAMS-safe names; the data carries "Unknown" on top of +# the seven classes of Sims et al. (2025) gfwDriverClasses <- c("Permanent agriculture" = "permanent_agriculture", "Hard commodities" = "hard_commodities", "Shifting cultivation" = "shifting_cultivation", @@ -9,14 +9,9 @@ gfwDriverClasses <- c("Permanent agriculture" = "permanent_agriculture", "Other natural disturbances" = "other_natural_disturbances", "Unknown" = "unknown") -# Global loss 2001-2025 in Mha for the dataset version and threshold pinned in -# downloadGFWLossByDriver(). Versions are immutable; update this deliberately after a re-pin. +# global loss 2001-2025 in Mha for the dataset version and threshold pinned in downloadGFWLossByDriver() gfwGlobalLossMha <- 542.81 -# Integrity checks on the extract. These reconcile the file against its own schema, a second -# query of the same table (loss_totals.csv) and a pinned snapshot, not against an external -# source: a truncated download, a renamed class or a mixed threshold would otherwise become a -# plausible disturbance rate rather than an error. checkGFWLossByDriver <- function(df, totals) { required <- c("iso", "year", "threshold", "driver", "loss_ha") @@ -45,7 +40,7 @@ checkGFWLossByDriver <- function(df, totals) { sum(df$loss_ha < 0, na.rm = TRUE), " negative values.") } - # The driver file summed over drivers must reproduce the separately queried totals file. + # summed over drivers, the driver file must reproduce the separately queried totals file byDriver <- stats::aggregate(list(drv = df$loss_ha), by = list(iso = df$iso, year = df$year), FUN = sum) both <- merge(byDriver, totals[, c("iso", "year", "loss_ha")], @@ -80,9 +75,8 @@ checkGFWLossByDriver <- function(df, totals) { return(invisible(df)) } -# Checks on the extent file against the loss file. Loss pixels are a subset of the 2000 extent -# at the same threshold, so a country's cumulative loss cannot exceed its extent; a mismatched -# version pair or threshold fails here. +# loss pixels are a subset of the 2000 extent at the same threshold, so a country's cumulative loss +# cannot exceed its extent checkGFWExtent <- function(ext, df) { required <- c("iso", "threshold", "extent_ha") @@ -118,20 +112,18 @@ checkGFWExtent <- function(ext, df) { #' @title readGFWLossByDriver #' -#' @description Reads the GFW extract: tree cover loss by country, year and driver -#' (`subtype = "loss"`, default), or tree cover extent in 2000 at the same canopy threshold -#' (`subtype = "extent"`), the base the loss is measured on. Both in Mha. +#' @description Reads the Global Forest Watch extract downloaded by +#' \code{\link{downloadGFWLossByDriver}}: tree cover loss by country, year and driver +#' (\code{subtype = "loss"}, default) or tree cover extent in 2000 at the same canopy threshold +#' (\code{subtype = "extent"}). Both in Mha. #' -#' @details The canopy threshold is pinned in [downloadGFWLossByDriver()] and stamped into -#' every row; this function only insists that the extract carries exactly one. -#' -#' @param subtype `"loss"` (default) or `"extent"` -#' @return magpie object: loss as ISO country x 2001..2025 x eight driver classes; extent as -#' ISO country x y2000. Unit Mha. +#' @param subtype \code{"loss"} (default) or \code{"extent"} +#' @return magpie object; loss by ISO country x year (2001-2025) x driver class, extent by ISO +#' country for y2000. Mha. #' @author Michael Crawford #' @importFrom magclass as.magpie magpiesort #' @importFrom utils read.csv head -#' @seealso [downloadGFWLossByDriver()], [convertGFWLossByDriver()] +#' @seealso \code{\link{downloadGFWLossByDriver}}, \code{\link{convertGFWLossByDriver}} #' @examples #' \dontrun{ #' a <- readSource("GFWLossByDriver") @@ -161,7 +153,7 @@ readGFWLossByDriver <- function(subtype = "loss") { x <- as.magpie(df[, c("iso", "year", "driver", "loss")], spatial = 1, temporal = 2) - # Absent country-year-driver combinations carry no loss: zero, not NA. + # country-year-driver combinations absent from the file carry no loss x[is.na(x)] <- 0 return(magpiesort(x)) diff --git a/inst/extdata/ForestLossDrivers/SOURCE.md b/inst/extdata/ForestLossDrivers/SOURCE.md deleted file mode 100644 index 6f90c9fd..00000000 --- a/inst/extdata/ForestLossDrivers/SOURCE.md +++ /dev/null @@ -1,76 +0,0 @@ -# ForestLossDrivers - source record - -`forest_loss.csv` is a hand transcription of **Table 1** of - -> Curtis, P.G., Slay, C.M., Harris, N.L., Tyukavina, A., Hansen, M.C. (2018). -> Classifying drivers of global forest loss. *Science* 361(6407), 1108-1111. -> doi:10.1126/science.aau3445 - -Table 1 exists only inside the article. There is no machine-readable release of it, so -it cannot be fetched by a download function. The underlying gridded product - the 10 km -dominant-driver classification - *is* openly distributed, by Global Forest Watch under -CC BY 4.0, as the tiled raster set `tsc_tree_cover_loss_drivers` -(https://data-api.globalforestwatch.org/dataset/tsc_tree_cover_loss_drivers); the -successor 1 km product (Sims et al.) is on Zenodo at doi:10.5281/zenodo.14163025. -Deriving the regional totals from either raster would be a change of method and is out -of scope for this file. - -## Columns - -| column | meaning | -|---|---| -| `region` | Curtis source region (7 regions, Table 1 rows) | -| `treecoverloss_01_15` | total tree cover loss 2001-2015, Mha | -| `treecoverloss_pc_01_15` | that region's share of the global total, per cent | -| `deforestation` .. `urbanization` | share of *that region's* loss by driver, per cent | - -`readForestLossDrivers()` converts the driver columns to Mha/yr as -`treecoverloss_01_15 * driver_pc / 100 / 15`. `treecoverloss_pc_01_15` is not used in -that calculation; it is retained as a consistency witness and is checked on read. - -## Transcription record - -Verified line by line against the article PDF on 2026-09-10. Table 1 as printed: - -| region | Mha | % of global | deforest. | shifting | forestry | wildfire | urban | -|---|---|---|---|---|---|---|---| -| North America | 70 | 21% | 1% | <1% | 56% | 40% | 2% | -| Latin America | 78 | 25% | 56% | 31% | 13% | 1% | <1% | -| Europe | 15 | 5% | None | <1% | 99% | 1% | None | -| Africa | 39 | 13% | 4% | 92% | 4% | <1% | <1% | -| Russia/China/South Asia | 64 | 20% | <1% | <1% | 41% | 58% | <1% | -| Southeast Asia | 39 | 13% | 78% | 9% | 13% | <1% | <1% | -| Australia/Oceania | 10 | 3% | 7% | 10% | 29% | 53% | 1% | -| **Global** | **314** | **100%** | 25% | 21% | 31% | 22% | <1% | - -Deviations of the CSV from the printed table, and their disposition: - -1. **Africa `treecoverloss_01_15` = 3, should be 39.** A dropped digit. Corrected. - This was not cosmetic: Africa is 92% shifting agriculture, which is the column MAgPIE - consumes by default (`s35_forest_damage = 2`), so it made the SSA shifting-agriculture - disturbance rate 13x too low from 2020 until this fix. -2. **Southeast Asia `treecoverloss_pc_01_15` = 12, should be 13.** Corrected. This - column is not used in the calculation; with 12 the shares summed to 99 rather than 100. -3. **Europe: `shifting_agriculture` = 1 and `wildfire` = 0; the table has <1% and 1% - respectively.** The 1 point sits on the wrong driver. Corrected separately - it moves - a (very small) flow out of the default damage channel. -4. **Latin America `forestry` = 12, table says 13.** *Not* corrected. Latin America's - printed driver shares sum to 101 because of the article's own rounding; shaving one - point off forestry to reach 100 is a defensible normalisation, not a typo. -5. `<1%` and `None` are rendered as 0 or 1 rather than a fractional value, chosen so that - each row's driver shares sum to 100. Left as-is; it is the same normalisation as (4). - -The global total is 315 with Africa at 39, against the article's 314 - within the -rounding of the individual rows. With Africa at 3 it was 279, which is the check that -makes the error unambiguous. - -## Mapping - -`mapping.csv` assigns each ISO3 country to one of the seven Curtis source regions. It was -built from `madrat::regionmappingH12` (identical country set, 249 rows; comma-separated -since mrland 0.76.0, the shared source folder's original is `;`-delimited) by rewriting the -region column. The article does not list countries per region, so the assignment is -geographic. Corrected in mrland 0.76.0: the original put the Middle East and North Africa -block in `Latin America`. DZA, EGY, LBY, MAR and TUN now map to `Africa`; ARE, BHR, IRN, -IRQ, ISR, JOR, KWT, LBN, OMN, PSE, QAT, SAU, SYR and YEM to `Russia China South Asia`; REU -moves from `Russia China South Asia` to `Africa`. diff --git a/man/calcForestLossByDriver.Rd b/man/calcForestLossByDriver.Rd index 24f95b4b..dd92c0b0 100644 --- a/man/calcForestLossByDriver.Rd +++ b/man/calcForestLossByDriver.Rd @@ -7,24 +7,19 @@ calcForestLossByDriver(source = "GFW", period = 2015:2024) } \arguments{ -\item{source}{`"GFW"` (default) or `"Curtis"`. Curtis computes as before, from the -corrected Curtis et al. (2018) table.} +\item{source}{\code{"GFW"} (default) or \code{"Curtis"}} -\item{period}{Years averaged into the annual rate, GFW only (default 2015:2024). Curtis -carries a single 2001-2015 mean.} +\item{period}{Years averaged into the annual rate, GFW only; Curtis carries a single 2001-2015 +mean} } \value{ MAgPIE object with forest area lost per year by driver, Mha } \description{ -Annual forest area loss by driver, Mha per year, from the Sims et al. (2025) -driver product (`source = "GFW"`, default) or the Curtis et al. (2018) table -(`source = "Curtis"`), with the FRA 2020 fire series bound on as `overall`. -} -\details{ -For GFW only the classes in `gfwModelDrivers` are returned. `overall` is the FRA -fire series averaged over its own years (2000-2017), as before; GAMS loads it but never -reads it. +Forest area lost per year by driver, in Mha, from the Global Forest Watch driver +product of Sims et al. (2025) (\code{source = "GFW"}, default) or from Table 1 of Curtis et al. +(2018) (\code{source = "Curtis"}), together with the FRA 2020 forest fire area as +\code{overall}. } \examples{ \dontrun{ @@ -33,7 +28,7 @@ calcOutput("ForestLossByDriver", source = "Curtis", aggregate = FALSE) } } \seealso{ -[readGFWLossByDriver()], [readForestLossDrivers()] +\code{\link{readGFWLossByDriver}}, \code{\link{readForestLossDrivers}} } \author{ Abhijeet Mishra, Michael Crawford diff --git a/man/calcForestLossShare.Rd b/man/calcForestLossShare.Rd index 2c782e3b..605be436 100644 --- a/man/calcForestLossShare.Rd +++ b/man/calcForestLossShare.Rd @@ -7,10 +7,10 @@ calcForestLossShare(source = "GFW", period = 2015:2024) } \arguments{ -\item{source}{Driver data to use, `"GFW"` (default) or `"Curtis"`. See -[calcForestLossByDriver()].} +\item{source}{Driver data to use, \code{"GFW"} (default) or \code{"Curtis"}, see +\code{\link{calcForestLossByDriver}}} -\item{period}{Years averaged to give the annual rate, for `source = "GFW"` only.} +\item{period}{Years averaged into the annual rate, GFW only} } \value{ MAgPIE object with the share of forest area lost per year by driver @@ -20,13 +20,10 @@ Calculates which share of forest area is lost per year to each driver of forest loss. } \details{ -For `source = "GFW"` numerator and denominator come from the same product at the -same canopy threshold: UMD tree cover loss by driver over UMD tree cover extent in 2000, -the base the loss is measured on. For `source = "Curtis"` the denominator stays FRA 2020 -naturally regenerating forest area in 2010, reproducing the previous parameterisation -exactly. The `overall` column (FRA fire area) is divided by the same denominator as the -drivers of its source and exceeds 1 in a few savanna countries; GAMS loads it but never reads -it. Shares above 1 are clamped to 1; for a driver column this also warns with the country names. +The denominator is tree cover extent in 2000 at the same canopy threshold for +\code{source = "GFW"} and FRA 2020 naturally regenerating forest area in 2010 for +\code{source = "Curtis"}. Shares above 1 are clamped to 1; for a driver column this also +warns with the country names. } \examples{ \dontrun{ @@ -35,7 +32,7 @@ calcOutput("ForestLossShare", source = "Curtis", aggregate = FALSE) } } \seealso{ -[calcForestLossByDriver()] +\code{\link{calcForestLossByDriver}} } \author{ Abhijeet Mishra, Michael Crawford diff --git a/man/convertGFWLossByDriver.Rd b/man/convertGFWLossByDriver.Rd index 611c319b..9fdef28f 100644 --- a/man/convertGFWLossByDriver.Rd +++ b/man/convertGFWLossByDriver.Rd @@ -7,19 +7,19 @@ convertGFWLossByDriver(x, subtype = "loss") } \arguments{ -\item{x}{magpie object as returned by [readGFWLossByDriver()]} +\item{x}{magpie object as returned by \code{\link{readGFWLossByDriver}}} -\item{subtype}{`"loss"` or `"extent"`, as passed to the read function; the reconciliation is -the same for both} +\item{subtype}{\code{"loss"} or \code{"extent"}, as passed to the read function; the conversion +is the same for both} } \value{ -magpie object on madrat's ISO country set, unit Mha +magpie object on madrat's ISO country set, Mha } \description{ -Reconciles the GADM country set used by GFW with madrat's ISO set. - -GADM and madrat disagree on 6 codes carrying loss and on 39 madrat countries GFW does -not list; both are handled explicitly rather than by a silent inner join. +Moves the GFW data from the GADM country set to madrat's ISO country set: Kosovo, +Northern Cyprus and the Akrotiri and Dhekelia base areas are added to Serbia and Cyprus, GADM's +placeholder codes for disputed territories are dropped and countries GFW does not list are +filled with zero. } \examples{ \dontrun{ @@ -27,7 +27,7 @@ a <- readSource("GFWLossByDriver", convert = TRUE) } } \seealso{ -[readGFWLossByDriver()] +\code{\link{readGFWLossByDriver}} } \author{ Michael Crawford diff --git a/man/downloadForestLossDrivers.Rd b/man/downloadForestLossDrivers.Rd index 0836d9be..d38321d7 100644 --- a/man/downloadForestLossDrivers.Rd +++ b/man/downloadForestLossDrivers.Rd @@ -7,23 +7,10 @@ downloadForestLossDrivers() } \description{ -Provisions the ForestLossDrivers source: the regional tree cover loss -totals and driver shares of Curtis et al. (2018), Table 1, plus the country to -source-region mapping used to disaggregate them. - -Table 1 is published only inside the article; there is no machine-readable release of -it. The table is therefore held as version-controlled package data under -`inst/extdata/ForestLossDrivers` and staged into the madrat source folder here, so -that `readSource("ForestLossDrivers")` works on a machine that has never seen the -shared source directory, and so that any change to the numbers is a reviewable diff -rather than a silent edit of a shared file. The transcription record, including the -known deviations from the printed table, is in -`inst/extdata/ForestLossDrivers/SOURCE.md`. - -The underlying gridded product - the 10 km dominant-driver classification - is -distributed separately by Global Forest Watch under CC BY 4.0 as a tiled raster set -(`tsc_tree_cover_loss_drivers`). It is deliberately not fetched here: deriving the -regional totals from the raster would be a change of method, not a change of source. +Stages the ForestLossDrivers source files shipped with the package +(\code{inst/extdata/ForestLossDrivers}) into the madrat source folder: Table 1 of Curtis et al. +(2018), tree cover loss 2001-2015 by region and dominant driver, and the mapping of countries +to the source regions. Table 1 has no machine-readable release and is transcribed by hand. } \examples{ \dontrun{ @@ -31,7 +18,7 @@ madrat::downloadSource("ForestLossDrivers") } } \seealso{ -[readForestLossDrivers()] +\code{\link{readForestLossDrivers}} } \author{ Michael Crawford diff --git a/man/downloadGFWLossByDriver.Rd b/man/downloadGFWLossByDriver.Rd index b38166e4..0c8113c1 100644 --- a/man/downloadGFWLossByDriver.Rd +++ b/man/downloadGFWLossByDriver.Rd @@ -7,21 +7,10 @@ downloadGFWLossByDriver() } \description{ -Downloads tree cover loss by country, year and dominant driver from the -Global Forest Watch data-api table `gadm__tcl__iso_change`, which cross-tabulates the -UMD/Hansen annual tree cover loss product with the WRI/Google DeepMind 1 km -dominant-driver classification of Sims et al. (2025), plus tree cover extent in 2000 at -the same canopy threshold from `gadm__tcl__iso_summary`, the base the loss is measured on. - -Route notes: -* `/dataset///download/csv` is open and honours the full SQL (`GROUP BY` included); - only `/query` needs an API key. -* Dataset versions are immutable (`is_mutable: false`), so the pinned URL is the - reproducibility mechanism. No checksum, as elsewhere in the stack. -* The driver map is the annually updated `wri_google_tree_cover_loss_drivers` v1.13 - (2001-2025), not the paper's v20241121 (2001-2022). Regional shares therefore do not - reproduce the paper's tables; Africa's permanent/shifting split moved by about 11 pp. -* Versions are listed under `data.versions`; `is_latest` is `false` on all of them. +Downloads tree cover loss by country, year and dominant driver from the Global +Forest Watch data API, table \code{gadm__tcl__iso_change}: the UMD/Hansen annual tree cover loss +cross-tabulated with the 1 km dominant-driver map of Sims et al. (2025). Tree cover extent in +2000 at the same canopy threshold is taken from \code{gadm__tcl__iso_summary}. } \examples{ \dontrun{ @@ -29,7 +18,7 @@ madrat::downloadSource("GFWLossByDriver") } } \seealso{ -[readGFWLossByDriver()] +\code{\link{readGFWLossByDriver}} } \author{ Michael Crawford diff --git a/man/readGFWLossByDriver.Rd b/man/readGFWLossByDriver.Rd index 2846ef71..0af84788 100644 --- a/man/readGFWLossByDriver.Rd +++ b/man/readGFWLossByDriver.Rd @@ -7,20 +7,17 @@ readGFWLossByDriver(subtype = "loss") } \arguments{ -\item{subtype}{`"loss"` (default) or `"extent"`} +\item{subtype}{\code{"loss"} (default) or \code{"extent"}} } \value{ -magpie object: loss as ISO country x 2001..2025 x eight driver classes; extent as -ISO country x y2000. Unit Mha. +magpie object; loss by ISO country x year (2001-2025) x driver class, extent by ISO +country for y2000. Mha. } \description{ -Reads the GFW extract: tree cover loss by country, year and driver -(`subtype = "loss"`, default), or tree cover extent in 2000 at the same canopy threshold -(`subtype = "extent"`), the base the loss is measured on. Both in Mha. -} -\details{ -The canopy threshold is pinned in [downloadGFWLossByDriver()] and stamped into -every row; this function only insists that the extract carries exactly one. +Reads the Global Forest Watch extract downloaded by +\code{\link{downloadGFWLossByDriver}}: tree cover loss by country, year and driver +(\code{subtype = "loss"}, default) or tree cover extent in 2000 at the same canopy threshold +(\code{subtype = "extent"}). Both in Mha. } \examples{ \dontrun{ @@ -28,7 +25,7 @@ a <- readSource("GFWLossByDriver") } } \seealso{ -[downloadGFWLossByDriver()], [convertGFWLossByDriver()] +\code{\link{downloadGFWLossByDriver}}, \code{\link{convertGFWLossByDriver}} } \author{ Michael Crawford diff --git a/tests/testthat/test-GFWLossByDriver.R b/tests/testthat/test-GFWLossByDriver.R deleted file mode 100644 index c115beca..00000000 --- a/tests/testthat/test-GFWLossByDriver.R +++ /dev/null @@ -1,140 +0,0 @@ -# Synthesized known-bug cases for checkGFWLossByDriver(), written before it ran on real data: -# each corruption below must fail and each benign perturbation must pass. - -# A minimal extract satisfying every identity the guard checks; uniform values on purpose. -makeExtract <- function(nIso = 210, years = 2001:2025) { - iso <- sprintf("C%02d", seq_len(nIso)) - drivers <- names(gfwDriverClasses) # nolint: object_usage_linter. - df <- expand.grid(iso = iso, year = years, driver = drivers, - KEEP.OUT.ATTRS = FALSE, stringsAsFactors = FALSE) - df$threshold <- as.integer(gfwCanopyThreshold) # nolint: object_usage_linter. - df$loss_ha <- gfwGlobalLossMha * 1e6 / nrow(df) # nolint: object_usage_linter. - df[, c("iso", "year", "threshold", "driver", "loss_ha")] -} - -makeTotals <- function(df) { - t <- aggregate(list(loss_ha = df$loss_ha), by = list(iso = df$iso, year = df$year), FUN = sum) - t$threshold <- as.integer(gfwCanopyThreshold) # nolint: object_usage_linter. - t[, c("iso", "year", "threshold", "loss_ha")] -} - -makeExtent <- function(df) { - iso <- unique(df$iso) - data.frame(iso = iso, threshold = as.integer(gfwCanopyThreshold), # nolint: object_usage_linter. - extent_ha = 2 * sum(df$loss_ha) / length(iso), stringsAsFactors = FALSE) -} - -test_that("the guard passes clean data", { - df <- makeExtract() - expect_silent(checkGFWLossByDriver(df, makeTotals(df))) -}) - -test_that("a dropped digit in one country-year-driver is caught", { - df <- makeExtract() - totals <- makeTotals(df) # totals built BEFORE the corruption, as on the server - i <- which(df$iso == "C07" & df$year == 2020 & df$driver == "Shifting cultivation") - expect_length(i, 1) # a corruption that matches no row tests nothing - df$loss_ha[i] <- df$loss_ha[i] / 10 # the Curtis failure: 39 typed as 3 - expect_error(checkGFWLossByDriver(df, totals), "do not sum to the country total") -}) - -test_that("a missing driver class is caught", { - df <- makeExtract() - totals <- makeTotals(df) - df <- df[df$driver != "Shifting cultivation", ] - expect_error(checkGFWLossByDriver(df, totals), "do not match the class map") -}) - -test_that("a renamed driver class is caught", { - df <- makeExtract() - df$driver[df$driver == "Logging"] <- "Forest management" - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "do not match the class map") -}) - -test_that("an added driver class is caught", { - df <- makeExtract() - extra <- df[df$driver == "Wildfire", ] - extra$driver <- "Flooding" - df <- rbind(df, extra) - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "do not match the class map") -}) - -test_that("a driver file cut short is caught", { - df <- makeExtract() - totals <- makeTotals(df) # totals complete, as when one of the two downloads fails - df <- df[df$iso %in% head(unique(df$iso), 200), ] # tail of the country list lost - expect_error(checkGFWLossByDriver(df, totals), "do not cover the same country-years") -}) - -test_that("an extract mixing canopy thresholds is caught", { - # The threshold value itself is checked in downloadGFWLossByDriver(); read catches mixing. - df <- makeExtract() - df$threshold[seq(1, nrow(df), 2)] <- 75L - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "mixes canopy thresholds") -}) - -test_that("missing and negative loss values are caught", { - df <- makeExtract() - df$loss_ha[5] <- NA_real_ - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "missing and") - df <- makeExtract() - df$loss_ha[5] <- -1 - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "negative values") -}) - -test_that("a global total far from the pinned reference is caught", { - df <- makeExtract() - df$loss_ha <- df$loss_ha * 1.2 - expect_error(checkGFWLossByDriver(df, makeTotals(df)), "global loss sums to") -}) - -test_that("a missing column is caught", { - df <- makeExtract() - expect_error(checkGFWLossByDriver(df[, -3], makeTotals(df)), "missing column") -}) - -test_that("a consistent extent file passes", { - df <- makeExtract() - expect_silent(checkGFWExtent(makeExtent(df), df)) -}) - -test_that("a country with loss but no extent is caught", { - df <- makeExtract() - ext <- makeExtent(df) - expect_error(checkGFWExtent(ext[ext$iso != "C07", ], df), "no extent") -}) - -test_that("cumulative loss above the 2000 extent is caught", { - df <- makeExtract() - ext <- makeExtent(df) - ext$extent_ha[ext$iso == "C07"] <- 1 - expect_error(checkGFWExtent(ext, df), "exceeds the 2000 extent") -}) - -test_that("an extent file at another threshold is caught", { - df <- makeExtract() - ext <- makeExtent(df) - ext$threshold <- 75L - expect_error(checkGFWExtent(ext, df), "canopy threshold") -}) - -# Benign perturbations: a guard that rejects everything is as useless as one that rejects nothing. - -test_that("row order does not matter", { - df <- makeExtract() - totals <- makeTotals(df) - expect_silent(checkGFWLossByDriver(df[sample(nrow(df)), ], totals)) -}) - -test_that("floating point noise below tolerance passes", { - df <- makeExtract() - totals <- makeTotals(df) - df$loss_ha <- df$loss_ha * (1 + 1e-12) - expect_silent(checkGFWLossByDriver(df, totals)) -}) - -test_that("a global total just inside the tolerance passes", { - df <- makeExtract() - df$loss_ha <- df$loss_ha * 1.04 - expect_silent(checkGFWLossByDriver(df, makeTotals(df))) -}) From c2903c36abe11dff5a8c88c4cda1bdd33b028fa2 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Tue, 22 Sep 2026 13:58:21 +0200 Subject: [PATCH 18/25] Bump version to 0.77.0 (lucode2::buildLibrary) Co-Authored-By: Claude Fable 5.1 --- .buildlibrary | 2 +- CITATION.cff | 4 +- DESCRIPTION | 7 +- NAMESPACE | 240 +++++++++++++++++++++++++++----------------------- README.md | 8 +- 5 files changed, 142 insertions(+), 119 deletions(-) diff --git a/.buildlibrary b/.buildlibrary index 3f84e5da..c7012a4b 100644 --- a/.buildlibrary +++ b/.buildlibrary @@ -1,4 +1,4 @@ -ValidationKey: '15737320' +ValidationKey: '15952860' AcceptedWarnings: - 'Warning: package ''.*'' was built under R version' - 'Warning: namespace ''.*'' is not available and has been replaced' diff --git a/CITATION.cff b/CITATION.cff index e6f80134..c97bad78 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: If you use this software, please cite it using the metadata from this file. type: software title: 'mrland: MadRaT land data package' -version: 0.76.0 -date-released: '2026-09-11' +version: 0.77.0 +date-released: '2026-09-22' abstract: The package provides land related data via the madrat framework. authors: - family-names: Dietrich diff --git a/DESCRIPTION b/DESCRIPTION index c4632c0f..33e293f2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: mrland Title: MadRaT land data package -Version: 0.76.0 -Date: 2026-09-11 +Version: 0.77.0 +Date: 2026-09-22 Authors@R: c( person("Jan Philipp", "Dietrich", , "dietrich@pik-potsdam.de", role = c("aut", "cre")), person("Abhijeet", "Mishra", role = "aut"), @@ -63,5 +63,4 @@ Suggests: Encoding: UTF-8 LazyData: no Config/Needs/website: tidyverse/tidytemplate -Config/roxygen2/version: 8.0.0 -RoxygenNote: 7.3.3 +Config/roxygen2/version: 8.1.0 diff --git a/NAMESPACE b/NAMESPACE index 21353ce2..9767f763 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -34,117 +34,141 @@ import(utils) importFrom(GDPuc,toolConvertGDP) importFrom(SPEI,thornthwaite) importFrom(data.table,fread) -importFrom(dplyr,"%>%") -importFrom(dplyr,.data) -importFrom(dplyr,across) -importFrom(dplyr,all_of) -importFrom(dplyr,arrange) -importFrom(dplyr,group_by) -importFrom(dplyr,inner_join) -importFrom(dplyr,left_join) -importFrom(dplyr,mutate) -importFrom(dplyr,summarise) +importFrom(dplyr, + "%>%", + .data, + across, + all_of, + arrange, + group_by, + inner_join, + left_join, + mutate, + summarise +) importFrom(grDevices,boxplot.stats) -importFrom(madrat,calcOutput) -importFrom(madrat,getISOlist) -importFrom(madrat,readSource) -importFrom(madrat,toolAggregate) -importFrom(madrat,toolConditionalReplace) -importFrom(madrat,toolCountryFill) -importFrom(madrat,toolFillWithRegionAvg) -importFrom(madrat,toolFillYears) -importFrom(madrat,toolGetMapping) -importFrom(madrat,toolNAreplace) -importFrom(madrat,toolSplitSubtype) -importFrom(madrat,toolSubtypeSelect) -importFrom(madrat,toolTimeAverage) -importFrom(magclass,"getCells<-") -importFrom(magclass,"getNames<-") -importFrom(magclass,"getSets<-") -importFrom(magclass,"getYears<-") -importFrom(magclass,add_columns) -importFrom(magclass,add_dimension) -importFrom(magclass,as.data.frame) -importFrom(magclass,as.magpie) -importFrom(magclass,clean_magpie) -importFrom(magclass,collapseDim) -importFrom(magclass,collapseNames) -importFrom(magclass,complete_magpie) -importFrom(magclass,convergence) -importFrom(magclass,dimOrder) -importFrom(magclass,dimSums) -importFrom(magclass,getCells) -importFrom(magclass,getItems) -importFrom(magclass,getNames) -importFrom(magclass,getRegions) -importFrom(magclass,getSets) -importFrom(magclass,getYears) -importFrom(magclass,hasCoords) -importFrom(magclass,is.magpie) -importFrom(magclass,lowpass) -importFrom(magclass,magpiesort) -importFrom(magclass,magpply) -importFrom(magclass,mbind) -importFrom(magclass,ndim) -importFrom(magclass,new.magpie) -importFrom(magclass,nregions) -importFrom(magclass,read.magpie) -importFrom(magclass,read.report) -importFrom(magclass,setCells) -importFrom(magclass,setItems) -importFrom(magclass,setNames) -importFrom(magclass,setYears) -importFrom(magclass,time_interpolate) -importFrom(magclass,unwrap) -importFrom(magclass,where) -importFrom(magpiesets,addLocation) -importFrom(magpiesets,findset) +importFrom(madrat, + calcOutput, + getISOlist, + readSource, + toolAggregate, + toolConditionalReplace, + toolCountryFill, + toolFillWithRegionAvg, + toolFillYears, + toolGetMapping, + toolNAreplace, + toolSplitSubtype, + toolSubtypeSelect, + toolTimeAverage +) +importFrom(magclass, + "getCells<-", + "getNames<-", + "getSets<-", + "getYears<-", + add_columns, + add_dimension, + as.data.frame, + as.magpie, + clean_magpie, + collapseDim, + collapseNames, + complete_magpie, + convergence, + dimOrder, + dimSums, + getCells, + getItems, + getNames, + getRegions, + getSets, + getYears, + hasCoords, + is.magpie, + lowpass, + magpiesort, + magpply, + mbind, + ndim, + new.magpie, + nregions, + read.magpie, + read.report, + setCells, + setItems, + setNames, + setYears, + time_interpolate, + unwrap, + where +) +importFrom(magpiesets, + addLocation, + findset +) importFrom(mrlandcore,toolLPJmLVersion) -importFrom(mstools,toolAggregateCell2Country) -importFrom(mstools,toolCell2isoCell) -importFrom(mstools,toolCoord2Isocell) -importFrom(mstools,toolCountryFillBilateral) -importFrom(mstools,toolGetMappingCoord2Country) -importFrom(mstools,toolHarmonize2Baseline) -importFrom(mstools,toolHoldConstant) -importFrom(mstools,toolHoldConstantBeyondEnd) -importFrom(mstools,toolSmooth) +importFrom(mstools, + toolAggregateCell2Country, + toolCell2isoCell, + toolCoord2Isocell, + toolCountryFillBilateral, + toolGetMappingCoord2Country, + toolHarmonize2Baseline, + toolHoldConstant, + toolHoldConstantBeyondEnd, + toolSmooth +) importFrom(ncdf4,nc_open) -importFrom(raster,aggregate) -importFrom(raster,brick) -importFrom(raster,raster) -importFrom(raster,rasterToPoints) +importFrom(raster, + aggregate, + brick, + raster, + rasterToPoints +) importFrom(readxl,read_xlsx) importFrom(reshape2,acast) -importFrom(rlang,":=") -importFrom(rlang,.data) -importFrom(stats,complete.cases) -importFrom(stats,median) -importFrom(stats,quantile) -importFrom(stats,uniroot) -importFrom(stringr,str_extract_all) -importFrom(stringr,str_split) -importFrom(terra,"ext<-") -importFrom(terra,aggregate) -importFrom(terra,cellSize) -importFrom(terra,classify) -importFrom(terra,ext) -importFrom(terra,extract) -importFrom(terra,mask) -importFrom(terra,project) -importFrom(terra,rast) -importFrom(terra,segregate) -importFrom(terra,terraOptions) -importFrom(terra,tmpFiles) -importFrom(utils,URLencode) -importFrom(utils,bibentry) -importFrom(utils,download.file) -importFrom(utils,head) -importFrom(utils,person) -importFrom(utils,read.csv) -importFrom(utils,read.csv2) -importFrom(utils,tail) -importFrom(withr,defer) -importFrom(withr,local_options) -importFrom(withr,local_tempdir) +importFrom(rlang, + ":=", + .data +) +importFrom(stats, + complete.cases, + median, + quantile, + uniroot +) +importFrom(stringr, + str_extract_all, + str_split +) +importFrom(terra, + "ext<-", + aggregate, + cellSize, + classify, + ext, + extract, + mask, + project, + rast, + segregate, + terraOptions, + tmpFiles +) +importFrom(utils, + URLencode, + bibentry, + download.file, + head, + person, + read.csv, + read.csv2, + tail +) +importFrom(withr, + defer, + local_options, + local_tempdir +) importFrom(zoo,rollapply) diff --git a/README.md b/README.md index 631af258..3e4e0891 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MadRaT land data package -R package **mrland**, version **0.76.0** +R package **mrland**, version **0.77.0** [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3822083.svg)](https://doi.org/10.5281/zenodo.3822083) [![R build status](https://github.com/pik-piam/mrland/workflows/check/badge.svg)](https://github.com/pik-piam/mrland/actions) [![codecov](https://codecov.io/gh/pik-piam/mrland/branch/master/graph/badge.svg)](https://app.codecov.io/gh/pik-piam/mrland) [![r-universe](https://pik-piam.r-universe.dev/badges/mrland)](https://pik-piam.r-universe.dev/builds) @@ -39,7 +39,7 @@ In case of questions / problems please contact Jan Philipp Dietrich . Version: 0.76.0, . +Dietrich J, Mishra A, Weindl I, Bodirsky B, Wang X, Baumstark L, Kreidenweis U, Klein D, Steinmetz N, Chen D, Humpenoeder F, von Jeetze P, Wirth S, Beier F, Hoetten D, Sauer P, Tommey J (2026). "mrland: MadRaT land data package." doi:10.5281/zenodo.3822083 , Version: 0.77.0, . A BibTeX entry for LaTeX users is @@ -48,9 +48,9 @@ A BibTeX entry for LaTeX users is title = {mrland: MadRaT land data package}, author = {Jan Philipp Dietrich and Abhijeet Mishra and Isabelle Weindl and Benjamin Leon Bodirsky and Xiaoxi Wang and Lavinia Baumstark and Ulrich Kreidenweis and David Klein and Nele Steinmetz and David Chen and Florian Humpenoeder and Patrick {von Jeetze} and Stephen Wirth and Felicitas Beier and David Hoetten and Pascal Sauer and Jake Tommey}, doi = {10.5281/zenodo.3822083}, - date = {2026-09-11}, + date = {2026-09-22}, year = {2026}, url = {https://github.com/pik-piam/mrland}, - note = {Version: 0.76.0}, + note = {Version: 0.77.0}, } ``` From 80e0d288612d717952030ad36babc604939c5281 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Wed, 23 Sep 2026 08:15:00 +0200 Subject: [PATCH 19/25] Align the Curtis disaggregation weight year with the share denominator readForestLossDrivers spread each regional total over countries by FRA 2015 forest area, while calcForestLossShare divides by and aggregates with FRA 2010. That rescaled every country by its own 2015/2010 area ratio, manufacturing up to 2.4x variation inside a source region that carries a single rate. Aligned to 2010. Also records why Table 1's rows are normalised to sum to 100, since the printed rows sum to 99-102 and the drivers must not exceed the observed loss. Co-Authored-By: Claude Opus 5 --- .buildlibrary | 2 +- CITATION.cff | 2 +- DESCRIPTION | 2 +- R/readForestLossDrivers.R | 11 ++++++++--- README.md | 6 +++--- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.buildlibrary b/.buildlibrary index c7012a4b..7c72895c 100644 --- a/.buildlibrary +++ b/.buildlibrary @@ -1,4 +1,4 @@ -ValidationKey: '15952860' +ValidationKey: '15973578' AcceptedWarnings: - 'Warning: package ''.*'' was built under R version' - 'Warning: namespace ''.*'' is not available and has been replaced' diff --git a/CITATION.cff b/CITATION.cff index c97bad78..b40d8108 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,7 +2,7 @@ cff-version: 1.2.0 message: If you use this software, please cite it using the metadata from this file. type: software title: 'mrland: MadRaT land data package' -version: 0.77.0 +version: 0.77.1 date-released: '2026-09-22' abstract: The package provides land related data via the madrat framework. authors: diff --git a/DESCRIPTION b/DESCRIPTION index 33e293f2..58521f4b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: mrland Title: MadRaT land data package -Version: 0.77.0 +Version: 0.77.1 Date: 2026-09-22 Authors@R: c( person("Jan Philipp", "Dietrich", , "dietrich@pik-potsdam.de", role = c("aut", "cre")), diff --git a/R/readForestLossDrivers.R b/R/readForestLossDrivers.R index c9bec447..8bf8c8d0 100644 --- a/R/readForestLossDrivers.R +++ b/R/readForestLossDrivers.R @@ -24,17 +24,22 @@ readForestLossDrivers <- function() { fullMapping <- merge(mapping, isoCountry, by = "CountryCode")[, c(-2, -4)] colnames(fullMapping) <- c("CountryCode", "RegionCodeSource", "RegionCodeMAgPIE") - # Table 1 of Curtis et al. (2018): tree cover loss 2001-2015 in Mha, driver shares in per cent + # Table 1 of Curtis et al. (2018): tree cover loss 2001-2015 in Mha, driver shares in per cent. + # Rows are normalised to sum to 100: the printed rows sum to 99-102 (rounding, and cells printed + # as "<1%"), while the drivers below must sum to the observed loss rather than exceed it. So + # Russia/China/South Asia shifting agriculture is 0 here, not the "<1%" the paper prints. file <- "forest_loss.csv" df <- read.csv(file = file, header = TRUE, sep = ",") # per cent to share (/ 100), 2001-2015 total to annual (/ 15) df[, -c(1:3)] <- (df$treecoverloss_01_15 * (df[, -c(1:3)] / 100)) / 15 dfMag <- as.magpie(df[, -c(2, 3)], temporal = NULL, spatial = "region") - # disaggregate to countries, weighted by FRA 2020 naturally regenerating forest area (Mha) + # disaggregate to countries, weighted by FRA 2020 naturally regenerating forest area (Mha). + # Same year calcForestLossShare divides by (2010), so every country in a source region carries + # one rate; a different year here rescales each country by its own area ratio instead. a <- readSource("FRA2020", "forest_area", convert = TRUE)[, , "naturallyRegeneratingForest"] a <- collapseNames(a) - forestLoss <- toolAggregate(x = dfMag, weight = setYears(a[, "y2015", ], NULL), rel = fullMapping, + forestLoss <- toolAggregate(x = dfMag, weight = setYears(a[, "y2010", ], NULL), rel = fullMapping, from = "RegionCodeSource", to = "CountryCode") return(forestLoss) diff --git a/README.md b/README.md index 3e4e0891..dc6b6f0d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MadRaT land data package -R package **mrland**, version **0.77.0** +R package **mrland**, version **0.77.1** [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3822083.svg)](https://doi.org/10.5281/zenodo.3822083) [![R build status](https://github.com/pik-piam/mrland/workflows/check/badge.svg)](https://github.com/pik-piam/mrland/actions) [![codecov](https://codecov.io/gh/pik-piam/mrland/branch/master/graph/badge.svg)](https://app.codecov.io/gh/pik-piam/mrland) [![r-universe](https://pik-piam.r-universe.dev/badges/mrland)](https://pik-piam.r-universe.dev/builds) @@ -39,7 +39,7 @@ In case of questions / problems please contact Jan Philipp Dietrich , Version: 0.77.0, . +Dietrich J, Mishra A, Weindl I, Bodirsky B, Wang X, Baumstark L, Kreidenweis U, Klein D, Steinmetz N, Chen D, Humpenoeder F, von Jeetze P, Wirth S, Beier F, Hoetten D, Sauer P, Tommey J (2026). "mrland: MadRaT land data package." doi:10.5281/zenodo.3822083 , Version: 0.77.1, . A BibTeX entry for LaTeX users is @@ -51,6 +51,6 @@ A BibTeX entry for LaTeX users is date = {2026-09-22}, year = {2026}, url = {https://github.com/pik-piam/mrland}, - note = {Version: 0.77.0}, + note = {Version: 0.77.1}, } ``` From ec20f1699b2364950ceecdc4fe5912a02d14e027 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Wed, 23 Sep 2026 10:01:27 +0200 Subject: [PATCH 20/25] Fold the GFW read helpers into the read function and trim its guards readGFWLossByDriver.R held three top-level objects where the package's other read functions hold one. The two check helpers and the class map are now local to the function. Guards go from eleven to nine. Dropped: the pinned global-loss total, which sits at +-5 per cent, while a re-pinned version carrying one more year moves it by 4 per cent - it could not catch the drift it was there for. Merged: the two single-file threshold checks become one assertion that the extent file carries the same canopy threshold as the loss file, which is the invariant the share depends on; that the value is the pinned one is checked in the download, where it is pinned. Behaviour unchanged: every regional share is identical, and a fresh run of the chain reproduces the input files byte for byte outside their provenance header. Co-Authored-By: Claude Opus 5 --- .buildlibrary | 2 +- CITATION.cff | 4 +- DESCRIPTION | 4 +- R/readGFWLossByDriver.R | 211 +++++++++++++++++----------------------- README.md | 8 +- 5 files changed, 97 insertions(+), 132 deletions(-) diff --git a/.buildlibrary b/.buildlibrary index 7c72895c..aa0a896b 100644 --- a/.buildlibrary +++ b/.buildlibrary @@ -1,4 +1,4 @@ -ValidationKey: '15973578' +ValidationKey: '15995068' AcceptedWarnings: - 'Warning: package ''.*'' was built under R version' - 'Warning: namespace ''.*'' is not available and has been replaced' diff --git a/CITATION.cff b/CITATION.cff index b40d8108..df72d0f1 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: If you use this software, please cite it using the metadata from this file. type: software title: 'mrland: MadRaT land data package' -version: 0.77.1 -date-released: '2026-09-22' +version: 0.77.2 +date-released: '2026-09-23' abstract: The package provides land related data via the madrat framework. authors: - family-names: Dietrich diff --git a/DESCRIPTION b/DESCRIPTION index 58521f4b..2656a508 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: mrland Title: MadRaT land data package -Version: 0.77.1 -Date: 2026-09-22 +Version: 0.77.2 +Date: 2026-09-23 Authors@R: c( person("Jan Philipp", "Dietrich", , "dietrich@pik-potsdam.de", role = c("aut", "cre")), person("Abhijeet", "Mishra", role = "aut"), diff --git a/R/readGFWLossByDriver.R b/R/readGFWLossByDriver.R index ff301190..c16da4be 100644 --- a/R/readGFWLossByDriver.R +++ b/R/readGFWLossByDriver.R @@ -1,115 +1,3 @@ -# driver classes as named in the data, with GAMS-safe names; the data carries "Unknown" on top of -# the seven classes of Sims et al. (2025) -gfwDriverClasses <- c("Permanent agriculture" = "permanent_agriculture", - "Hard commodities" = "hard_commodities", - "Shifting cultivation" = "shifting_cultivation", - "Logging" = "logging", - "Wildfire" = "wildfire", - "Settlements & Infrastructure" = "settlements_infrastructure", - "Other natural disturbances" = "other_natural_disturbances", - "Unknown" = "unknown") - -# global loss 2001-2025 in Mha for the dataset version and threshold pinned in downloadGFWLossByDriver() -gfwGlobalLossMha <- 542.81 - -checkGFWLossByDriver <- function(df, totals) { - - required <- c("iso", "year", "threshold", "driver", "loss_ha") - if (!all(required %in% names(df))) { - stop("GFWLossByDriver: loss_by_driver.csv is missing column(s) ", - toString(setdiff(required, names(df))), ".") - } - - observed <- sort(unique(df$driver)) - expected <- sort(names(gfwDriverClasses)) - if (!identical(observed, expected)) { - stop("GFWLossByDriver: the driver classes in the data do not match the class map. ", - "Only in data: ", toString(setdiff(observed, expected)), ". ", - "Only in map: ", toString(setdiff(expected, observed)), ". ", - "Re-run the preflight and update gfwDriverClasses before trusting any output.") - } - - if (length(unique(df$threshold)) != 1L) { - stop("GFWLossByDriver: the extract mixes canopy thresholds ", - toString(sort(unique(df$threshold))), - ". It must carry exactly one, the value pinned in downloadGFWLossByDriver().") - } - - if (anyNA(df$loss_ha) || any(df$loss_ha < 0)) { - stop("GFWLossByDriver: loss_ha contains ", sum(is.na(df$loss_ha)), " missing and ", - sum(df$loss_ha < 0, na.rm = TRUE), " negative values.") - } - - # summed over drivers, the driver file must reproduce the separately queried totals file - byDriver <- stats::aggregate(list(drv = df$loss_ha), - by = list(iso = df$iso, year = df$year), FUN = sum) - both <- merge(byDriver, totals[, c("iso", "year", "loss_ha")], - by = c("iso", "year"), all = TRUE) - if (anyNA(both$drv) || anyNA(both$loss_ha)) { - onlyTot <- both[is.na(both$drv), c("iso", "year")] - onlyDrv <- both[is.na(both$loss_ha), c("iso", "year")] - stop("GFWLossByDriver: the driver file and the totals file do not cover the same ", - "country-years. ", nrow(onlyTot), " only in totals (e.g. ", - toString(utils::head(paste(onlyTot$iso, onlyTot$year), 5)), "), ", - nrow(onlyDrv), " only in the driver file (e.g. ", - toString(utils::head(paste(onlyDrv$iso, onlyDrv$year), 5)), ").") - } - off <- abs(both$drv - both$loss_ha) > 1e-6 * pmax(both$loss_ha, 1) - if (any(off)) { - stop("GFWLossByDriver: driver shares do not sum to the country total for ", sum(off), - " country-years, e.g. ", - toString(utils::head(paste0(both$iso[off], " ", both$year[off], " (", - signif(both$drv[off], 6), " vs ", - signif(both$loss_ha[off], 6), " ha)"), 3)), ".") - } - - total <- sum(df$loss_ha) / 1e6 - if (abs(total - gfwGlobalLossMha) > 0.05 * gfwGlobalLossMha) { - stop("GFWLossByDriver: global loss sums to ", round(total, 1), " Mha, but ", - gfwGlobalLossMha, " Mha is expected for the pinned dataset version and canopy ", - "threshold. Either the download is incomplete, or the pin in ", - "downloadGFWLossByDriver() has changed - re-run the preflight before updating ", - "gfwGlobalLossMha.") - } - - return(invisible(df)) -} - -# loss pixels are a subset of the 2000 extent at the same threshold, so a country's cumulative loss -# cannot exceed its extent -checkGFWExtent <- function(ext, df) { - - required <- c("iso", "threshold", "extent_ha") - if (!all(required %in% names(ext))) { - stop("GFWLossByDriver: extent_2000.csv is missing column(s) ", - toString(setdiff(required, names(ext))), ".") - } - if (!identical(sort(unique(ext$threshold)), sort(unique(df$threshold)))) { - stop("GFWLossByDriver: extent_2000.csv carries canopy threshold ", - toString(unique(ext$threshold)), " but loss_by_driver.csv carries ", - toString(unique(df$threshold)), ".") - } - if (anyNA(ext$extent_ha) || any(ext$extent_ha < 0)) { - stop("GFWLossByDriver: extent_ha contains ", sum(is.na(ext$extent_ha)), " missing and ", - sum(ext$extent_ha < 0, na.rm = TRUE), " negative values.") - } - absent <- setdiff(unique(df$iso), ext$iso) - if (length(absent) > 0) { - stop("GFWLossByDriver: ", length(absent), " countries carry loss but have no extent, e.g. ", - toString(utils::head(absent, 5)), ".") - } - cum <- stats::aggregate(list(loss = df$loss_ha), by = list(iso = df$iso), FUN = sum) - both <- merge(cum, ext[, c("iso", "extent_ha")], by = "iso") - over <- both$loss > both$extent_ha * (1 + 1e-6) - if (any(over)) { - stop("GFWLossByDriver: cumulative loss exceeds the 2000 extent for ", sum(over), - " countries, e.g. ", toString(utils::head(both$iso[over], 5)), - ". The two files are not from the same version or threshold.") - } - - return(invisible(ext)) -} - #' @title readGFWLossByDriver #' #' @description Reads the Global Forest Watch extract downloaded by @@ -131,30 +19,107 @@ checkGFWExtent <- function(ext, df) { readGFWLossByDriver <- function(subtype = "loss") { + if (!subtype %in% c("loss", "extent")) { + stop("readGFWLossByDriver: unknown subtype '", subtype, "'. Use \"loss\" or \"extent\".") + } + + # driver classes as named in the data, with GAMS-safe names; the data carries "Unknown" on top of + # the seven classes of Sims et al. (2025) + driverClasses <- c("Permanent agriculture" = "permanent_agriculture", + "Hard commodities" = "hard_commodities", + "Shifting cultivation" = "shifting_cultivation", + "Logging" = "logging", + "Wildfire" = "wildfire", + "Settlements & Infrastructure" = "settlements_infrastructure", + "Other natural disturbances" = "other_natural_disturbances", + "Unknown" = "unknown") + + # required columns present, value column finite and non-negative, exactly one canopy threshold + checkFile <- function(d, cols, value, file) { + missing <- setdiff(cols, names(d)) + if (length(missing) > 0) { + stop("GFWLossByDriver: ", file, " is missing column(s) ", toString(missing), ".") + } + if (anyNA(d[[value]]) || any(d[[value]] < 0)) { + stop("GFWLossByDriver: ", file, " carries ", sum(is.na(d[[value]])), " missing and ", + sum(d[[value]] < 0, na.rm = TRUE), " negative ", value, " values.") + } + # the share MAgPIE consumes is loss / extent, so one threshold per file and the same one in + # both; that the value is the pinned one is checked where it is pinned, in the download + if (length(unique(d$threshold)) != 1L) { + stop("GFWLossByDriver: ", file, " mixes canopy thresholds ", + toString(sort(unique(d$threshold))), "; it must carry exactly one.") + } + invisible(d) + } + df <- read.csv("loss_by_driver.csv", stringsAsFactors = FALSE) + checkFile(df, c("iso", "year", "threshold", "driver", "loss_ha"), "loss_ha", "loss_by_driver.csv") if (subtype == "extent") { ext <- read.csv("extent_2000.csv", stringsAsFactors = FALSE) - checkGFWExtent(ext, df) + checkFile(ext, c("iso", "threshold", "extent_ha"), "extent_ha", "extent_2000.csv") + + if (!identical(unique(ext$threshold), unique(df$threshold))) { + stop("GFWLossByDriver: extent_2000.csv carries canopy threshold ", unique(ext$threshold), + " but loss_by_driver.csv carries ", unique(df$threshold), + "; the share would divide loss at one threshold by extent at another.") + } + + absent <- setdiff(unique(df$iso), ext$iso) + if (length(absent) > 0) { + stop("GFWLossByDriver: ", length(absent), " countries carry loss but have no extent, e.g. ", + toString(head(absent, 5)), ".") + } + # loss pixels are a subset of the 2000 extent at the same threshold, so cumulative loss cannot + # exceed it; this is what a version-mismatched pair of files trips + cum <- stats::aggregate(list(loss = df$loss_ha), by = list(iso = df$iso), FUN = sum) + both <- merge(cum, ext[, c("iso", "extent_ha")], by = "iso") + over <- both$loss > both$extent_ha * (1 + 1e-6) + if (any(over)) { + stop("GFWLossByDriver: cumulative loss exceeds the 2000 extent for ", sum(over), + " countries, e.g. ", toString(head(both$iso[over], 5)), + ". The two files are not from the same version or threshold.") + } + ext$extent <- ext$extent_ha / 1e6 # hectares to Mha ext$year <- 2000L - x <- as.magpie(ext[, c("iso", "year", "extent")], spatial = 1, temporal = 2) - return(magpiesort(x)) + return(magpiesort(as.magpie(ext[, c("iso", "year", "extent")], spatial = 1, temporal = 2))) } - if (subtype != "loss") { - stop("readGFWLossByDriver: unknown subtype '", subtype, "'. Use \"loss\" or \"extent\".") + + observed <- sort(unique(df$driver)) + if (!identical(observed, sort(names(driverClasses)))) { + stop("GFWLossByDriver: the driver classes in the data do not match the class map. ", + "Only in data: ", toString(setdiff(observed, names(driverClasses))), ". ", + "Only in map: ", toString(setdiff(names(driverClasses), observed)), ". ", + "Re-run the preflight and update the class map before trusting any output.") } + # the driver file and the totals file are separate queries of the same table: summed over drivers + # they must agree, which is what catches a partial or mismatched download totals <- read.csv("loss_totals.csv", stringsAsFactors = FALSE) - checkGFWLossByDriver(df, totals) + byDriver <- stats::aggregate(list(drv = df$loss_ha), + by = list(iso = df$iso, year = df$year), FUN = sum) + both <- merge(byDriver, totals[, c("iso", "year", "loss_ha")], by = c("iso", "year"), all = TRUE) + if (anyNA(both$drv) || anyNA(both$loss_ha)) { + stop("GFWLossByDriver: the driver file and the totals file do not cover the same country-years. ", + sum(is.na(both$drv)), " only in totals, ", sum(is.na(both$loss_ha)), + " only in the driver file, e.g. ", + toString(head(paste(both$iso[is.na(both$drv) | is.na(both$loss_ha)], + both$year[is.na(both$drv) | is.na(both$loss_ha)]), 5)), ".") + } + off <- abs(both$drv - both$loss_ha) > 1e-6 * pmax(both$loss_ha, 1) + if (any(off)) { + stop("GFWLossByDriver: driver shares do not sum to the country total for ", sum(off), + " country-years, e.g. ", + toString(head(paste0(both$iso[off], " ", both$year[off], " (", signif(both$drv[off], 6), + " vs ", signif(both$loss_ha[off], 6), " ha)"), 3)), ".") + } - df$driver <- unname(gfwDriverClasses[df$driver]) + df$driver <- unname(driverClasses[df$driver]) df$loss <- df$loss_ha / 1e6 # hectares to Mha - x <- as.magpie(df[, c("iso", "year", "driver", "loss")], spatial = 1, temporal = 2) - - # country-year-driver combinations absent from the file carry no loss - x[is.na(x)] <- 0 + x[is.na(x)] <- 0 # country-year-driver combinations absent from the file carry no loss return(magpiesort(x)) } diff --git a/README.md b/README.md index dc6b6f0d..e604073c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MadRaT land data package -R package **mrland**, version **0.77.1** +R package **mrland**, version **0.77.2** [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3822083.svg)](https://doi.org/10.5281/zenodo.3822083) [![R build status](https://github.com/pik-piam/mrland/workflows/check/badge.svg)](https://github.com/pik-piam/mrland/actions) [![codecov](https://codecov.io/gh/pik-piam/mrland/branch/master/graph/badge.svg)](https://app.codecov.io/gh/pik-piam/mrland) [![r-universe](https://pik-piam.r-universe.dev/badges/mrland)](https://pik-piam.r-universe.dev/builds) @@ -39,7 +39,7 @@ In case of questions / problems please contact Jan Philipp Dietrich , Version: 0.77.1, . +Dietrich J, Mishra A, Weindl I, Bodirsky B, Wang X, Baumstark L, Kreidenweis U, Klein D, Steinmetz N, Chen D, Humpenoeder F, von Jeetze P, Wirth S, Beier F, Hoetten D, Sauer P, Tommey J (2026). "mrland: MadRaT land data package." doi:10.5281/zenodo.3822083 , Version: 0.77.2, . A BibTeX entry for LaTeX users is @@ -48,9 +48,9 @@ A BibTeX entry for LaTeX users is title = {mrland: MadRaT land data package}, author = {Jan Philipp Dietrich and Abhijeet Mishra and Isabelle Weindl and Benjamin Leon Bodirsky and Xiaoxi Wang and Lavinia Baumstark and Ulrich Kreidenweis and David Klein and Nele Steinmetz and David Chen and Florian Humpenoeder and Patrick {von Jeetze} and Stephen Wirth and Felicitas Beier and David Hoetten and Pascal Sauer and Jake Tommey}, doi = {10.5281/zenodo.3822083}, - date = {2026-09-22}, + date = {2026-09-23}, year = {2026}, url = {https://github.com/pik-piam/mrland}, - note = {Version: 0.77.1}, + note = {Version: 0.77.2}, } ``` From 7b85b4308ac6bafb88fd1489e0cc908c659b2297 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Tue, 29 Sep 2026 15:58:22 +0200 Subject: [PATCH 21/25] Address review: Curtis as a madrat source, FRA denominator for GFW - Curtis Table 1 is read from the source folder; the country mapping moves to inst/extdata/regional; downloadForestLossDrivers is removed - GFW shares are divided by FRA 2020 naturally regenerating forest; the extent download is removed - only shifting_cultivation is returned (overall dropped); source -> src Co-Authored-By: Claude Opus 5.5 (1M context) --- R/calcForestLossByDriver.R | 61 +++++++-------- R/calcForestLossShare.R | 43 +++-------- R/convertGFWLossByDriver.R | 6 +- R/downloadForestLossDrivers.R | 60 --------------- R/downloadGFWLossByDriver.R | 31 +++----- R/readForestLossDrivers.R | 54 ++++--------- R/readGFWLossByDriver.R | 77 ++++--------------- .../extdata/ForestLossDrivers/forest_loss.csv | 8 -- .../regionmappingCurtis2018.csv} | 0 man/calcForestLossByDriver.Rd | 15 ++-- man/calcForestLossShare.Rd | 13 ++-- man/convertGFWLossByDriver.Rd | 5 +- man/downloadForestLossDrivers.Rd | 25 ------ man/downloadGFWLossByDriver.Rd | 3 +- man/readForestLossDrivers.Rd | 19 +++-- man/readGFWLossByDriver.Rd | 12 +-- 16 files changed, 115 insertions(+), 317 deletions(-) delete mode 100644 R/downloadForestLossDrivers.R delete mode 100644 inst/extdata/ForestLossDrivers/forest_loss.csv rename inst/extdata/{ForestLossDrivers/mapping.csv => regional/regionmappingCurtis2018.csv} (100%) delete mode 100644 man/downloadForestLossDrivers.Rd diff --git a/R/calcForestLossByDriver.R b/R/calcForestLossByDriver.R index 5590d016..25739c8a 100644 --- a/R/calcForestLossByDriver.R +++ b/R/calcForestLossByDriver.R @@ -1,37 +1,47 @@ #' @title calcForestLossByDriver #' #' @description Forest area lost per year by driver, in Mha, from the Global Forest Watch driver -#' product of Sims et al. (2025) (\code{source = "GFW"}, default) or from Table 1 of Curtis et al. -#' (2018) (\code{source = "Curtis"}), together with the FRA 2020 forest fire area as -#' \code{overall}. +#' product of Sims et al. (2025) (\code{src = "GFW"}, default) or from Table 1 of Curtis et al. +#' (2018) (\code{src = "Curtis"}). Only the driver classes passed on to MAgPIE are returned. #' -#' @param source \code{"GFW"} (default) or \code{"Curtis"} +#' @details Curtis et al. (2018) report loss for seven world regions. It is distributed to +#' countries by FRA 2020 naturally regenerating forest area in 2010. +#' +#' @param src \code{"GFW"} (default) or \code{"Curtis"} #' @param period Years averaged into the annual rate, GFW only; Curtis carries a single 2001-2015 #' mean #' @return MAgPIE object with forest area lost per year by driver, Mha #' @author Abhijeet Mishra, Michael Crawford -#' @importFrom magclass mbind setNames dimSums getYears getItems -#' @importFrom madrat readSource #' @seealso \code{\link{readGFWLossByDriver}}, \code{\link{readForestLossDrivers}} #' @examples #' \dontrun{ #' calcOutput("ForestLossByDriver", aggregate = FALSE) -#' calcOutput("ForestLossByDriver", source = "Curtis", aggregate = FALSE) +#' calcOutput("ForestLossByDriver", src = "Curtis", aggregate = FALSE) #' } -calcForestLossByDriver <- function(source = "GFW", period = 2015:2024) { - - # Only shifting cultivation is passed on to MAgPIE: it leaves the land as forest and MAgPIE does - # not model it itself. Clearing for agriculture, commodities and settlements removes the forest, - # logging is MAgPIE's own harvest, wildfire and other natural disturbances are natural processes. - gfwModelDrivers <- c("shifting_cultivation") +calcForestLossByDriver <- function(src = "GFW", period = 2015:2024) { - fao <- setNames(readSource("FRA2020", subtype = "forest_fire", convert = TRUE), "overall") - fao <- dimSums(fao, dim = 2) / length(getYears(fao)) # mean annual area lost to fire, Mha + # Driver classes passed on to MAgPIE, each needing an element in MAgPIE's driver_source set. + # GFW options: permanent_agriculture, hard_commodities, shifting_cultivation, logging, wildfire, + # settlements_infrastructure, other_natural_disturbances, unknown. Curtis shares only + # shifting_cultivation and wildfire with these names. + # Only shifting cultivation is passed on: it leaves the land as forest and MAgPIE does not model + # it itself. Clearing for agriculture, commodities and settlements removes the forest, logging + # is MAgPIE's own harvest, wildfire and other natural disturbances are natural processes. + modelDrivers <- c("shifting_cultivation") - drivers <- switch( - source, # nolint: undesirable_function_linter. - "Curtis" = readSource("ForestLossDrivers"), + out <- switch( + src, + "Curtis" = { + x <- readSource("ForestLossDrivers", convert = FALSE) + getItems(x, dim = 3)[getItems(x, dim = 3) == "shifting_agriculture"] <- "shifting_cultivation" + mapping <- toolGetMapping("regionmappingCurtis2018.csv", type = "regional", where = "mrland") + # the area and year calcForestLossShare divides by, so every country in a region carries the + # region's rate + weight <- readSource("FRA2020", "forest_area", convert = TRUE)[, "y2010", "naturallyRegeneratingForest"] + toolAggregate(x[, , modelDrivers], rel = mapping, weight = setYears(collapseNames(weight), NULL), + from = "RegionCode", to = "CountryCode") + }, "GFW" = { x <- readSource("GFWLossByDriver", convert = TRUE) wanted <- paste0("y", period) @@ -40,23 +50,14 @@ calcForestLossByDriver <- function(source = "GFW", period = 2015:2024) { stop("calcForestLossByDriver: the GFW record does not cover ", toString(absent), ". It runs ", min(getYears(x, TRUE)), "-", max(getYears(x, TRUE)), ".") } - absent <- setdiff(gfwModelDrivers, getItems(x, 3)) - if (length(absent) > 0) { - stop("calcForestLossByDriver: the GFW source does not carry driver class(es) ", - toString(absent), ".") - } - dimSums(x[, wanted, gfwModelDrivers], dim = 2) / length(wanted) + dimSums(x[, wanted, modelDrivers], dim = 2) / length(wanted) }, - stop("calcForestLossByDriver: unknown source '", source, # nolint: undesirable_function_linter. - "'. Use \"GFW\" or \"Curtis\".") + stop("calcForestLossByDriver: unknown src '", src, "'. Use \"GFW\" or \"Curtis\".") ) - out <- mbind(fao, drivers) - return(list(x = out, weight = NULL, unit = "Mha", - description = paste0("Forest area lost per year by driver (", - source, ")"), # nolint: undesirable_function_linter. + description = paste0("Forest area lost per year by driver (", src, ")"), isocountries = FALSE)) } diff --git a/R/calcForestLossShare.R b/R/calcForestLossShare.R index 30f966c3..08ba70ca 100644 --- a/R/calcForestLossShare.R +++ b/R/calcForestLossShare.R @@ -3,60 +3,41 @@ #' @description Calculates which share of forest area is lost per year to each driver of #' forest loss. #' -#' @param source Driver data to use, \code{"GFW"} (default) or \code{"Curtis"}, see +#' @param src Driver data to use, \code{"GFW"} (default) or \code{"Curtis"}, see #' \code{\link{calcForestLossByDriver}} #' @param period Years averaged into the annual rate, GFW only #' -#' @details The denominator is tree cover extent in 2000 at the same canopy threshold for -#' \code{source = "GFW"} and FRA 2020 naturally regenerating forest area in 2010 for -#' \code{source = "Curtis"}. Shares above 1 are clamped to 1; for a driver column this also -#' warns with the country names. +#' @details The denominator is FRA 2020 naturally regenerating forest area, the natural forest +#' MAgPIE applies the share to: 2020 for \code{src = "GFW"}, 2010 for \code{src = "Curtis"} +#' (loss 2001-2015). Shares above 1 are clamped to 1. #' #' @return MAgPIE object with the share of forest area lost per year by driver #' @author Abhijeet Mishra, Michael Crawford -#' @importFrom magclass setYears setNames getItems -#' @importFrom utils head -#' @importFrom madrat calcOutput readSource #' @seealso \code{\link{calcForestLossByDriver}} #' @examples #' \dontrun{ #' calcOutput("ForestLossShare", aggregate = FALSE) -#' calcOutput("ForestLossShare", source = "Curtis", aggregate = FALSE) +#' calcOutput("ForestLossShare", src = "Curtis", aggregate = FALSE) #' } -calcForestLossShare <- function(source = "GFW", period = 2015:2024) { +calcForestLossShare <- function(src = "GFW", period = 2015:2024) { - lostArea <- calcOutput("ForestLossByDriver", source = source, # nolint: undesirable_function_linter. - period = period, aggregate = FALSE) + lostArea <- calcOutput("ForestLossByDriver", src = src, period = period, aggregate = FALSE) - forestArea <- switch( - source, # nolint: undesirable_function_linter. - "GFW" = readSource("GFWLossByDriver", subtype = "extent", convert = TRUE), - "Curtis" = readSource("FRA2020", subtype = "forest_area", - convert = TRUE)[, "y2010", "naturallyRegeneratingForest"], - stop("calcForestLossShare: unknown source '", source, # nolint: undesirable_function_linter. - "'. Use \"GFW\" or \"Curtis\".") - ) + # FRA year in the loss period: Curtis covers 2001-2015, GFW 2015-2024 by default + year <- if (src == "Curtis") "y2010" else "y2020" + forestArea <- readSource("FRA2020", subtype = "forest_area", + convert = TRUE)[, year, "naturallyRegeneratingForest"] forestArea <- setYears(setNames(forestArea, NULL), NULL) lostShare <- lostArea / forestArea lostShare[is.infinite(lostShare)] <- 0 lostShare[is.na(lostShare)] <- 0 - # overall (FRA fire area) exceeds 1 in some savanna countries and is not read by MAgPIE; a driver - # share above 1 means numerator and denominator disagree - drivers <- setdiff(getItems(lostShare, 3), "overall") - over <- lostShare[, , drivers] > 1 - if (any(over)) { - warning("calcForestLossShare: ", sum(over), " driver share(s) above 1 clamped to 1 (", - source, "): ", # nolint: undesirable_function_linter. - toString(utils::head(getItems(lostShare, 1)[apply(over, 1, any)], 5)), ".") - } lostShare[lostShare > 1] <- 1 return(list(x = lostShare, weight = forestArea, unit = "1", - description = paste0("Share of forest area lost per year by driver (", - source, ")"), # nolint: undesirable_function_linter. + description = paste0("Share of forest area lost per year by driver (", src, ")"), isocountries = FALSE)) } diff --git a/R/convertGFWLossByDriver.R b/R/convertGFWLossByDriver.R index b6ef4dc6..52cd19b0 100644 --- a/R/convertGFWLossByDriver.R +++ b/R/convertGFWLossByDriver.R @@ -6,19 +6,15 @@ #' filled with zero. #' #' @param x magpie object as returned by \code{\link{readGFWLossByDriver}} -#' @param subtype \code{"loss"} or \code{"extent"}, as passed to the read function; the conversion -#' is the same for both #' @return magpie object on madrat's ISO country set, Mha #' @author Michael Crawford -#' @importFrom madrat toolCountryFill -#' @importFrom magclass getItems #' @seealso \code{\link{readGFWLossByDriver}} #' @examples #' \dontrun{ #' a <- readSource("GFWLossByDriver", convert = TRUE) #' } -convertGFWLossByDriver <- function(x, subtype = "loss") { +convertGFWLossByDriver <- function(x) { # GADM codes inside a madrat country; their loss is added to the host rather than dropped gfwIsoHosts <- c(XKO = "SRB", # Kosovo diff --git a/R/downloadForestLossDrivers.R b/R/downloadForestLossDrivers.R deleted file mode 100644 index 9ac3fc7c..00000000 --- a/R/downloadForestLossDrivers.R +++ /dev/null @@ -1,60 +0,0 @@ -#' @title downloadForestLossDrivers -#' -#' @description Stages the ForestLossDrivers source files shipped with the package -#' (\code{inst/extdata/ForestLossDrivers}) into the madrat source folder: Table 1 of Curtis et al. -#' (2018), tree cover loss 2001-2015 by region and dominant driver, and the mapping of countries -#' to the source regions. Table 1 has no machine-readable release and is transcribed by hand. -#' -#' @author Michael Crawford -#' @importFrom utils bibentry person -#' @seealso \code{\link{readForestLossDrivers}} -#' @examples -#' \dontrun{ -#' madrat::downloadSource("ForestLossDrivers") -#' } -downloadForestLossDrivers <- function() { - - extdata <- system.file("extdata", "ForestLossDrivers", package = "mrland") - if (extdata == "") { - stop("ForestLossDrivers package data not found. Is mrland installed correctly?") - } - - staged <- file.copy(list.files(extdata, full.names = TRUE), ".", overwrite = TRUE) - if (!all(staged)) { - stop("Could not stage the ForestLossDrivers source files into the source folder.") - } - - return(list( - title = "Classifying drivers of global forest loss (Table 1)", - description = paste("Tree cover loss 2001-2015 by region and dominant driver,", - "transcribed from Table 1 of Curtis et al. (2018), together", - "with the country to source-region mapping used to", - "disaggregate the regional totals."), - author = c(person(c("Philip", "G."), "Curtis"), - person(c("Christy", "M."), "Slay"), - person(c("Nancy", "L."), "Harris"), - person("Alexandra", "Tyukavina"), - person(c("Matthew", "C."), "Hansen")), - doi = "10.1126/science.aau3445", - url = "https://www.science.org/doi/10.1126/science.aau3445", - license = paste("Table transcribed from the article for use as a model parameter;", - "the underlying gridded product is CC BY 4.0."), - version = "Science 361(6407):1108-1111, Table 1 (2001-2015)", - unit = "Mha (2001-2015 total) and per cent of regional loss", - reference = bibentry( - "Article", - title = "Classifying drivers of global forest loss", - author = c(person(c("Philip", "G."), "Curtis"), - person(c("Christy", "M."), "Slay"), - person(c("Nancy", "L."), "Harris"), - person("Alexandra", "Tyukavina"), - person(c("Matthew", "C."), "Hansen")), - year = "2018", - journal = "Science", - volume = "361", - number = "6407", - pages = "1108-1111", - doi = "10.1126/science.aau3445" - ) - )) -} diff --git a/R/downloadGFWLossByDriver.R b/R/downloadGFWLossByDriver.R index 0b0b581d..5ab9f6c9 100644 --- a/R/downloadGFWLossByDriver.R +++ b/R/downloadGFWLossByDriver.R @@ -2,8 +2,7 @@ #' #' @description Downloads tree cover loss by country, year and dominant driver from the Global #' Forest Watch data API, table \code{gadm__tcl__iso_change}: the UMD/Hansen annual tree cover loss -#' cross-tabulated with the 1 km dominant-driver map of Sims et al. (2025). Tree cover extent in -#' 2000 at the same canopy threshold is taken from \code{gadm__tcl__iso_summary}. +#' cross-tabulated with the 1 km dominant-driver map of Sims et al. (2025). #' #' @author Michael Crawford #' @importFrom utils download.file URLencode read.csv bibentry person @@ -24,8 +23,6 @@ downloadGFWLossByDriver <- function() { # them, so a re-pin has to pick by date rather than by that flag. gfwDataset <- "gadm__tcl__iso_change" # annual loss by country and driver gfwVersion <- "v20260424" - gfwSummaryDataset <- "gadm__tcl__iso_summary" # tree cover extent 2000 by country - gfwSummaryVersion <- "v20260424" # canopy density in 2000 (per cent) defining forest, GFW's convention; the total is sensitive to # it (global loss 2001-2025 is 647 Mha at 0 per cent and 334 Mha at 75) gfwCanopyThreshold <- 30 @@ -35,8 +32,8 @@ downloadGFWLossByDriver <- function() { where <- paste("FROM data WHERE umd_tree_cover_density_2000__threshold =", gfwCanopyThreshold) - # loss by driver, the same loss without the driver split (the read function checks that the two - # agree) and the 2000 extent + # loss by driver and the same loss without the driver split; the read function checks that the + # two agree queries <- list( "loss_by_driver.csv" = paste("SELECT", dims, ", wri_google_tree_cover_loss_drivers__driver AS driver,", @@ -44,18 +41,11 @@ downloadGFWLossByDriver <- function() { "GROUP BY iso, year, threshold, driver"), "loss_totals.csv" = paste("SELECT", dims, ", SUM(umd_tree_cover_loss__ha) AS loss_ha", where, - "GROUP BY iso, year, threshold"), - "extent_2000.csv" = paste("SELECT iso, umd_tree_cover_density_2000__threshold AS threshold,", - "SUM(umd_tree_cover_extent_2000__ha) AS extent_ha", where, - "GROUP BY iso, threshold") + "GROUP BY iso, year, threshold") ) - datasets <- c("loss_by_driver.csv" = paste0(gfwDataset, "/", gfwVersion), - "loss_totals.csv" = paste0(gfwDataset, "/", gfwVersion), - "extent_2000.csv" = paste0(gfwSummaryDataset, "/", gfwSummaryVersion)) headers <- c( "loss_by_driver.csv" = "\"iso\",\"year\",\"threshold\",\"driver\",\"loss_ha\"", - "loss_totals.csv" = "\"iso\",\"year\",\"threshold\",\"loss_ha\"", - "extent_2000.csv" = "\"iso\",\"threshold\",\"extent_ha\"" + "loss_totals.csv" = "\"iso\",\"year\",\"threshold\",\"loss_ha\"" ) # a timeout warning would be an error, downloadSource() runs this with warn = 2 @@ -63,7 +53,7 @@ downloadGFWLossByDriver <- function() { for (f in names(queries)) { # the open download route honours the full SQL; only the query route needs an API key - url <- paste0("https://data-api.globalforestwatch.org/dataset/", datasets[[f]], + url <- paste0("https://data-api.globalforestwatch.org/dataset/", gfwDataset, "/", gfwVersion, "/download/csv?sql=", URLencode(queries[[f]], reserved = TRUE)) download.file(url, f, quiet = TRUE, mode = "wb") @@ -102,10 +92,8 @@ downloadGFWLossByDriver <- function() { description <- paste("Annual UMD/Hansen tree cover loss 2001-2025, in hectares,", "cross-tabulated by country and by the WRI/Google DeepMind 1 km", "dominant-driver class, at a", gfwCanopyThreshold, "per cent canopy", - "density threshold, plus UMD tree cover extent in 2000 at the same", - "threshold (extent_2000.csv). Eight driver classes are present: the seven", - "of", - "Sims et al. (2025) plus 'Unknown'. NOTE: the driver map underlying", + "density threshold. Eight driver classes are present: the seven", + "of Sims et al. (2025) plus 'Unknown'. NOTE: the driver map underlying", "this extract is the annually updated 2001-2025 version", "(wri_google_tree_cover_loss_drivers v1.13), not the 2001-2022", "version published with the paper, so regional driver shares do not", @@ -133,7 +121,6 @@ downloadGFWLossByDriver <- function() { license = "CC BY 4.0", version = paste0(gfwDataset, " ", gfwVersion, " (drivers map ", "wri_google_tree_cover_loss_drivers v1.13, 2001-2025)"), - unit = paste("ha of tree cover loss per country, year and driver;", - "ha of tree cover extent 2000 per country"), + unit = "ha of tree cover loss per country, year and driver", reference = reference)) } diff --git a/R/readForestLossDrivers.R b/R/readForestLossDrivers.R index 8bf8c8d0..0ea305f8 100644 --- a/R/readForestLossDrivers.R +++ b/R/readForestLossDrivers.R @@ -1,46 +1,24 @@ -#' Read ForestLossDrivers +#' @title readForestLossDrivers #' -#' Read-in an Forest loss data (range 2001-2015 but only single annual number her) -#' (Source:DOI: 10.1126/science.aau3445 Table 1). +#' @description Reads Table 1 of Curtis et al. (2018), tree cover loss 2001-2015 by world region +#' and dominant driver, transcribed by hand from the article into \code{forest_loss.csv}. #' -#' -#' @return magpie object of the Curtis et al., 2018 Data -#' @author Abhijeet Mishra -#' @seealso \code{\link[madrat]{readSource}} +#' @return magpie object with the mean annual loss per Curtis region and driver, Mha +#' @author Abhijeet Mishra, Michael Crawford +#' @references Curtis, P. G., Slay, C. M., Harris, N. L., Tyukavina, A. and Hansen, M. C. (2018) +#' Classifying drivers of global forest loss. Science 361, 1108-1111. doi:10.1126/science.aau3445 +#' @seealso \code{\link{calcForestLossByDriver}} #' @examples #' \dontrun{ -#' a <- readSource("ForestLossDrivers") +#' a <- readSource("ForestLossDrivers", convert = FALSE) #' } -#' -#' @importFrom magclass as.magpie -#' @importFrom madrat toolSubtypeSelect -#' @import readxl -#' @import countrycode -#' @importFrom stats complete.cases readForestLossDrivers <- function() { - mapping <- read.csv("mapping.csv", header = TRUE, sep = ",") - isoCountry <- toolGetMapping(type = "regional", name = "regionmappingH12.csv", where = "madrat") - fullMapping <- merge(mapping, isoCountry, by = "CountryCode")[, c(-2, -4)] - colnames(fullMapping) <- c("CountryCode", "RegionCodeSource", "RegionCodeMAgPIE") - - # Table 1 of Curtis et al. (2018): tree cover loss 2001-2015 in Mha, driver shares in per cent. - # Rows are normalised to sum to 100: the printed rows sum to 99-102 (rounding, and cells printed - # as "<1%"), while the drivers below must sum to the observed loss rather than exceed it. So - # Russia/China/South Asia shifting agriculture is 0 here, not the "<1%" the paper prints. - file <- "forest_loss.csv" - df <- read.csv(file = file, header = TRUE, sep = ",") - # per cent to share (/ 100), 2001-2015 total to annual (/ 15) - df[, -c(1:3)] <- (df$treecoverloss_01_15 * (df[, -c(1:3)] / 100)) / 15 - dfMag <- as.magpie(df[, -c(2, 3)], temporal = NULL, spatial = "region") - - # disaggregate to countries, weighted by FRA 2020 naturally regenerating forest area (Mha). - # Same year calcForestLossShare divides by (2010), so every country in a source region carries - # one rate; a different year here rescales each country by its own area ratio instead. - a <- readSource("FRA2020", "forest_area", convert = TRUE)[, , "naturallyRegeneratingForest"] - a <- collapseNames(a) - forestLoss <- toolAggregate(x = dfMag, weight = setYears(a[, "y2010", ], NULL), rel = fullMapping, - from = "RegionCodeSource", to = "CountryCode") - - return(forestLoss) + # loss 2001-2015 in Mha and driver shares in per cent. Rows are normalised to sum to 100: the + # printed rows sum to 99-102 (rounding, and cells printed as "<1%"), while the drivers must sum + # to the observed loss rather than exceed it. + df <- read.csv("forest_loss.csv") + drivers <- c("deforestation", "shifting_agriculture", "forestry", "wildfire", "urbanization") + df[, drivers] <- df$treecoverloss_01_15 * df[, drivers] / 100 / 15 + return(as.magpie(df[, c("region", drivers)], temporal = NULL, spatial = "region")) } diff --git a/R/readGFWLossByDriver.R b/R/readGFWLossByDriver.R index c16da4be..99dfd03c 100644 --- a/R/readGFWLossByDriver.R +++ b/R/readGFWLossByDriver.R @@ -1,15 +1,10 @@ #' @title readGFWLossByDriver #' #' @description Reads the Global Forest Watch extract downloaded by -#' \code{\link{downloadGFWLossByDriver}}: tree cover loss by country, year and driver -#' (\code{subtype = "loss"}, default) or tree cover extent in 2000 at the same canopy threshold -#' (\code{subtype = "extent"}). Both in Mha. +#' \code{\link{downloadGFWLossByDriver}}: tree cover loss by country, year and driver, in Mha. #' -#' @param subtype \code{"loss"} (default) or \code{"extent"} -#' @return magpie object; loss by ISO country x year (2001-2025) x driver class, extent by ISO -#' country for y2000. Mha. +#' @return magpie object, ISO country x year (2001-2025) x driver class, Mha #' @author Michael Crawford -#' @importFrom magclass as.magpie magpiesort #' @importFrom utils read.csv head #' @seealso \code{\link{downloadGFWLossByDriver}}, \code{\link{convertGFWLossByDriver}} #' @examples @@ -17,11 +12,7 @@ #' a <- readSource("GFWLossByDriver") #' } -readGFWLossByDriver <- function(subtype = "loss") { - - if (!subtype %in% c("loss", "extent")) { - stop("readGFWLossByDriver: unknown subtype '", subtype, "'. Use \"loss\" or \"extent\".") - } +readGFWLossByDriver <- function() { # driver classes as named in the data, with GAMS-safe names; the data carries "Unknown" on top of # the seven classes of Sims et al. (2025) @@ -34,57 +25,19 @@ readGFWLossByDriver <- function(subtype = "loss") { "Other natural disturbances" = "other_natural_disturbances", "Unknown" = "unknown") - # required columns present, value column finite and non-negative, exactly one canopy threshold - checkFile <- function(d, cols, value, file) { - missing <- setdiff(cols, names(d)) - if (length(missing) > 0) { - stop("GFWLossByDriver: ", file, " is missing column(s) ", toString(missing), ".") - } - if (anyNA(d[[value]]) || any(d[[value]] < 0)) { - stop("GFWLossByDriver: ", file, " carries ", sum(is.na(d[[value]])), " missing and ", - sum(d[[value]] < 0, na.rm = TRUE), " negative ", value, " values.") - } - # the share MAgPIE consumes is loss / extent, so one threshold per file and the same one in - # both; that the value is the pinned one is checked where it is pinned, in the download - if (length(unique(d$threshold)) != 1L) { - stop("GFWLossByDriver: ", file, " mixes canopy thresholds ", - toString(sort(unique(d$threshold))), "; it must carry exactly one.") - } - invisible(d) - } - df <- read.csv("loss_by_driver.csv", stringsAsFactors = FALSE) - checkFile(df, c("iso", "year", "threshold", "driver", "loss_ha"), "loss_ha", "loss_by_driver.csv") - - if (subtype == "extent") { - ext <- read.csv("extent_2000.csv", stringsAsFactors = FALSE) - checkFile(ext, c("iso", "threshold", "extent_ha"), "extent_ha", "extent_2000.csv") - - if (!identical(unique(ext$threshold), unique(df$threshold))) { - stop("GFWLossByDriver: extent_2000.csv carries canopy threshold ", unique(ext$threshold), - " but loss_by_driver.csv carries ", unique(df$threshold), - "; the share would divide loss at one threshold by extent at another.") - } - - absent <- setdiff(unique(df$iso), ext$iso) - if (length(absent) > 0) { - stop("GFWLossByDriver: ", length(absent), " countries carry loss but have no extent, e.g. ", - toString(head(absent, 5)), ".") - } - # loss pixels are a subset of the 2000 extent at the same threshold, so cumulative loss cannot - # exceed it; this is what a version-mismatched pair of files trips - cum <- stats::aggregate(list(loss = df$loss_ha), by = list(iso = df$iso), FUN = sum) - both <- merge(cum, ext[, c("iso", "extent_ha")], by = "iso") - over <- both$loss > both$extent_ha * (1 + 1e-6) - if (any(over)) { - stop("GFWLossByDriver: cumulative loss exceeds the 2000 extent for ", sum(over), - " countries, e.g. ", toString(head(both$iso[over], 5)), - ". The two files are not from the same version or threshold.") - } - - ext$extent <- ext$extent_ha / 1e6 # hectares to Mha - ext$year <- 2000L - return(magpiesort(as.magpie(ext[, c("iso", "year", "extent")], spatial = 1, temporal = 2))) + missing <- setdiff(c("iso", "year", "threshold", "driver", "loss_ha"), names(df)) + if (length(missing) > 0) { + stop("GFWLossByDriver: loss_by_driver.csv is missing column(s) ", toString(missing), ".") + } + if (anyNA(df$loss_ha) || any(df$loss_ha < 0)) { + stop("GFWLossByDriver: loss_by_driver.csv carries ", sum(is.na(df$loss_ha)), " missing and ", + sum(df$loss_ha < 0, na.rm = TRUE), " negative loss_ha values.") + } + # one canopy threshold, or the same pixel is counted once per threshold + if (length(unique(df$threshold)) != 1L) { + stop("GFWLossByDriver: loss_by_driver.csv mixes canopy thresholds ", + toString(sort(unique(df$threshold))), "; it must carry exactly one.") } observed <- sort(unique(df$driver)) diff --git a/inst/extdata/ForestLossDrivers/forest_loss.csv b/inst/extdata/ForestLossDrivers/forest_loss.csv deleted file mode 100644 index f845215e..00000000 --- a/inst/extdata/ForestLossDrivers/forest_loss.csv +++ /dev/null @@ -1,8 +0,0 @@ -region,treecoverloss_01_15,treecoverloss_pc_01_15,deforestation,shifting_agriculture,forestry,wildfire,urbanization -North America,70,21,1,1,56,40,2 -Latin America,78,25,56,31,12,1,0 -Europe,15,5,0,0,99,1,0 -Africa,39,13,4,92,4,0,0 -Russia China South Asia,64,20,0,0,41,58,1 -Southeast Asia,39,13,78,9,13,0,0 -Australia Oceania,10,3,7,10,29,53,1 diff --git a/inst/extdata/ForestLossDrivers/mapping.csv b/inst/extdata/regional/regionmappingCurtis2018.csv similarity index 100% rename from inst/extdata/ForestLossDrivers/mapping.csv rename to inst/extdata/regional/regionmappingCurtis2018.csv diff --git a/man/calcForestLossByDriver.Rd b/man/calcForestLossByDriver.Rd index dd92c0b0..ac5d2b29 100644 --- a/man/calcForestLossByDriver.Rd +++ b/man/calcForestLossByDriver.Rd @@ -4,10 +4,10 @@ \alias{calcForestLossByDriver} \title{calcForestLossByDriver} \usage{ -calcForestLossByDriver(source = "GFW", period = 2015:2024) +calcForestLossByDriver(src = "GFW", period = 2015:2024) } \arguments{ -\item{source}{\code{"GFW"} (default) or \code{"Curtis"}} +\item{src}{\code{"GFW"} (default) or \code{"Curtis"}} \item{period}{Years averaged into the annual rate, GFW only; Curtis carries a single 2001-2015 mean} @@ -17,14 +17,17 @@ MAgPIE object with forest area lost per year by driver, Mha } \description{ Forest area lost per year by driver, in Mha, from the Global Forest Watch driver -product of Sims et al. (2025) (\code{source = "GFW"}, default) or from Table 1 of Curtis et al. -(2018) (\code{source = "Curtis"}), together with the FRA 2020 forest fire area as -\code{overall}. +product of Sims et al. (2025) (\code{src = "GFW"}, default) or from Table 1 of Curtis et al. +(2018) (\code{src = "Curtis"}). Only the driver classes passed on to MAgPIE are returned. +} +\details{ +Curtis et al. (2018) report loss for seven world regions. It is distributed to +countries by FRA 2020 naturally regenerating forest area in 2010. } \examples{ \dontrun{ calcOutput("ForestLossByDriver", aggregate = FALSE) -calcOutput("ForestLossByDriver", source = "Curtis", aggregate = FALSE) +calcOutput("ForestLossByDriver", src = "Curtis", aggregate = FALSE) } } \seealso{ diff --git a/man/calcForestLossShare.Rd b/man/calcForestLossShare.Rd index 605be436..fb64c09d 100644 --- a/man/calcForestLossShare.Rd +++ b/man/calcForestLossShare.Rd @@ -4,10 +4,10 @@ \alias{calcForestLossShare} \title{calcForestLossShare} \usage{ -calcForestLossShare(source = "GFW", period = 2015:2024) +calcForestLossShare(src = "GFW", period = 2015:2024) } \arguments{ -\item{source}{Driver data to use, \code{"GFW"} (default) or \code{"Curtis"}, see +\item{src}{Driver data to use, \code{"GFW"} (default) or \code{"Curtis"}, see \code{\link{calcForestLossByDriver}}} \item{period}{Years averaged into the annual rate, GFW only} @@ -20,15 +20,14 @@ Calculates which share of forest area is lost per year to each driver of forest loss. } \details{ -The denominator is tree cover extent in 2000 at the same canopy threshold for -\code{source = "GFW"} and FRA 2020 naturally regenerating forest area in 2010 for -\code{source = "Curtis"}. Shares above 1 are clamped to 1; for a driver column this also -warns with the country names. +The denominator is FRA 2020 naturally regenerating forest area, the natural forest +MAgPIE applies the share to: 2020 for \code{src = "GFW"}, 2010 for \code{src = "Curtis"} +(loss 2001-2015). Shares above 1 are clamped to 1. } \examples{ \dontrun{ calcOutput("ForestLossShare", aggregate = FALSE) -calcOutput("ForestLossShare", source = "Curtis", aggregate = FALSE) +calcOutput("ForestLossShare", src = "Curtis", aggregate = FALSE) } } \seealso{ diff --git a/man/convertGFWLossByDriver.Rd b/man/convertGFWLossByDriver.Rd index 9fdef28f..3e4e244a 100644 --- a/man/convertGFWLossByDriver.Rd +++ b/man/convertGFWLossByDriver.Rd @@ -4,13 +4,10 @@ \alias{convertGFWLossByDriver} \title{convertGFWLossByDriver} \usage{ -convertGFWLossByDriver(x, subtype = "loss") +convertGFWLossByDriver(x) } \arguments{ \item{x}{magpie object as returned by \code{\link{readGFWLossByDriver}}} - -\item{subtype}{\code{"loss"} or \code{"extent"}, as passed to the read function; the conversion -is the same for both} } \value{ magpie object on madrat's ISO country set, Mha diff --git a/man/downloadForestLossDrivers.Rd b/man/downloadForestLossDrivers.Rd deleted file mode 100644 index d38321d7..00000000 --- a/man/downloadForestLossDrivers.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/downloadForestLossDrivers.R -\name{downloadForestLossDrivers} -\alias{downloadForestLossDrivers} -\title{downloadForestLossDrivers} -\usage{ -downloadForestLossDrivers() -} -\description{ -Stages the ForestLossDrivers source files shipped with the package -(\code{inst/extdata/ForestLossDrivers}) into the madrat source folder: Table 1 of Curtis et al. -(2018), tree cover loss 2001-2015 by region and dominant driver, and the mapping of countries -to the source regions. Table 1 has no machine-readable release and is transcribed by hand. -} -\examples{ -\dontrun{ -madrat::downloadSource("ForestLossDrivers") -} -} -\seealso{ -\code{\link{readForestLossDrivers}} -} -\author{ -Michael Crawford -} diff --git a/man/downloadGFWLossByDriver.Rd b/man/downloadGFWLossByDriver.Rd index 0c8113c1..c91be4d7 100644 --- a/man/downloadGFWLossByDriver.Rd +++ b/man/downloadGFWLossByDriver.Rd @@ -9,8 +9,7 @@ downloadGFWLossByDriver() \description{ Downloads tree cover loss by country, year and dominant driver from the Global Forest Watch data API, table \code{gadm__tcl__iso_change}: the UMD/Hansen annual tree cover loss -cross-tabulated with the 1 km dominant-driver map of Sims et al. (2025). Tree cover extent in -2000 at the same canopy threshold is taken from \code{gadm__tcl__iso_summary}. +cross-tabulated with the 1 km dominant-driver map of Sims et al. (2025). } \examples{ \dontrun{ diff --git a/man/readForestLossDrivers.Rd b/man/readForestLossDrivers.Rd index 887c71fd..5c5e1f03 100644 --- a/man/readForestLossDrivers.Rd +++ b/man/readForestLossDrivers.Rd @@ -2,26 +2,29 @@ % Please edit documentation in R/readForestLossDrivers.R \name{readForestLossDrivers} \alias{readForestLossDrivers} -\title{Read ForestLossDrivers} +\title{readForestLossDrivers} \usage{ readForestLossDrivers() } \value{ -magpie object of the Curtis et al., 2018 Data +magpie object with the mean annual loss per Curtis region and driver, Mha } \description{ -Read-in an Forest loss data (range 2001-2015 but only single annual number her) -(Source:DOI: 10.1126/science.aau3445 Table 1). +Reads Table 1 of Curtis et al. (2018), tree cover loss 2001-2015 by world region +and dominant driver, transcribed by hand from the article into \code{forest_loss.csv}. } \examples{ \dontrun{ -a <- readSource("ForestLossDrivers") +a <- readSource("ForestLossDrivers", convert = FALSE) } - +} +\references{ +Curtis, P. G., Slay, C. M., Harris, N. L., Tyukavina, A. and Hansen, M. C. (2018) +Classifying drivers of global forest loss. Science 361, 1108-1111. doi:10.1126/science.aau3445 } \seealso{ -\code{\link[madrat]{readSource}} +\code{\link{calcForestLossByDriver}} } \author{ -Abhijeet Mishra +Abhijeet Mishra, Michael Crawford } diff --git a/man/readGFWLossByDriver.Rd b/man/readGFWLossByDriver.Rd index 0af84788..7850dc53 100644 --- a/man/readGFWLossByDriver.Rd +++ b/man/readGFWLossByDriver.Rd @@ -4,20 +4,14 @@ \alias{readGFWLossByDriver} \title{readGFWLossByDriver} \usage{ -readGFWLossByDriver(subtype = "loss") -} -\arguments{ -\item{subtype}{\code{"loss"} (default) or \code{"extent"}} +readGFWLossByDriver() } \value{ -magpie object; loss by ISO country x year (2001-2025) x driver class, extent by ISO -country for y2000. Mha. +magpie object, ISO country x year (2001-2025) x driver class, Mha } \description{ Reads the Global Forest Watch extract downloaded by -\code{\link{downloadGFWLossByDriver}}: tree cover loss by country, year and driver -(\code{subtype = "loss"}, default) or tree cover extent in 2000 at the same canopy threshold -(\code{subtype = "extent"}). Both in Mha. +\code{\link{downloadGFWLossByDriver}}: tree cover loss by country, year and driver, in Mha. } \examples{ \dontrun{ From 6f8b408da6a2d5af7ab864504785a25b4829a3b9 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Tue, 29 Sep 2026 17:02:44 +0200 Subject: [PATCH 22/25] Bump version to 0.78.0 (lucode2::buildLibrary) Co-Authored-By: Claude Opus 5.5 (1M context) --- .buildlibrary | 2 +- CITATION.cff | 4 ++-- DESCRIPTION | 4 ++-- README.md | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.buildlibrary b/.buildlibrary index aa0a896b..98dd6337 100644 --- a/.buildlibrary +++ b/.buildlibrary @@ -1,4 +1,4 @@ -ValidationKey: '15995068' +ValidationKey: '16165500' AcceptedWarnings: - 'Warning: package ''.*'' was built under R version' - 'Warning: namespace ''.*'' is not available and has been replaced' diff --git a/CITATION.cff b/CITATION.cff index 7c35ba75..8532a018 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: If you use this software, please cite it using the metadata from this file. type: software title: 'mrland: MadRaT land data package' -version: 0.77.0 -date-released: '2026-09-23' +version: 0.78.0 +date-released: '2026-09-29' abstract: The package provides land related data via the madrat framework. authors: - family-names: Dietrich diff --git a/DESCRIPTION b/DESCRIPTION index 2656a508..baa951eb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: mrland Title: MadRaT land data package -Version: 0.77.2 -Date: 2026-09-23 +Version: 0.78.0 +Date: 2026-09-29 Authors@R: c( person("Jan Philipp", "Dietrich", , "dietrich@pik-potsdam.de", role = c("aut", "cre")), person("Abhijeet", "Mishra", role = "aut"), diff --git a/README.md b/README.md index a161b0ec..4d4f5203 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MadRaT land data package -R package **mrland**, version **0.77.0** +R package **mrland**, version **0.78.0** [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3822083.svg)](https://doi.org/10.5281/zenodo.3822083) [![R build status](https://github.com/pik-piam/mrland/workflows/check/badge.svg)](https://github.com/pik-piam/mrland/actions) [![codecov](https://codecov.io/gh/pik-piam/mrland/branch/master/graph/badge.svg)](https://app.codecov.io/gh/pik-piam/mrland) [![r-universe](https://pik-piam.r-universe.dev/badges/mrland)](https://pik-piam.r-universe.dev/builds) @@ -39,7 +39,7 @@ In case of questions / problems please contact Jan Philipp Dietrich , Version: 0.77.0, . +Dietrich J, Mishra A, Weindl I, Bodirsky B, Wang X, Baumstark L, Kreidenweis U, Klein D, Steinmetz N, Chen D, Humpenoeder F, von Jeetze P, Wirth S, Beier F, Hoetten D, Sauer P, Tommey J (2026). "mrland: MadRaT land data package." doi:10.5281/zenodo.3822083 , Version: 0.78.0, . A BibTeX entry for LaTeX users is @@ -48,9 +48,9 @@ A BibTeX entry for LaTeX users is title = {mrland: MadRaT land data package}, author = {Jan Philipp Dietrich and Abhijeet Mishra and Isabelle Weindl and Benjamin Leon Bodirsky and Xiaoxi Wang and Lavinia Baumstark and Ulrich Kreidenweis and David Klein and Nele Steinmetz and David Chen and Florian Humpenoeder and Patrick {von Jeetze} and Stephen Wirth and Felicitas Beier and David Hoetten and Pascal Sauer and Jake Tommey}, doi = {10.5281/zenodo.3822083}, - date = {2026-09-23}, + date = {2026-09-29}, year = {2026}, url = {https://github.com/pik-piam/mrland}, - note = {Version: 0.77.0}, + note = {Version: 0.78.0}, } ``` From 1563aa990fa0576fea1ed5b4b58a98b4d46c1b89 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Wed, 30 Sep 2026 11:25:23 +0200 Subject: [PATCH 23/25] Add Michael Crawford as author --- CITATION.cff | 2 ++ DESCRIPTION | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index 8532a018..fbad6cce 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -41,6 +41,8 @@ authors: given-names: Pascal - family-names: Tommey given-names: Jake +- family-names: Crawford + given-names: Michael license: LGPL-3.0 repository-code: https://github.com/pik-piam/mrland doi: 10.5281/zenodo.3822083 diff --git a/DESCRIPTION b/DESCRIPTION index baa951eb..fb561d7b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -20,7 +20,8 @@ Authors@R: c( person("Felicitas", "Beier", role = "aut"), person("David", "Hoetten", role = "aut"), person("Pascal", "Sauer", role = "aut"), - person("Jake", "Tommey", role = "aut") + person("Jake", "Tommey", role = "aut"), + person("Michael", "Crawford", role = "aut") ) Description: The package provides land related data via the madrat framework. From af2e7b79585038a51e17865043667c08aed28d73 Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Wed, 30 Sep 2026 11:39:53 +0200 Subject: [PATCH 24/25] Move Michael Crawford to the middle of the author list --- CITATION.cff | 4 ++-- DESCRIPTION | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index fbad6cce..009039a2 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -23,6 +23,8 @@ authors: given-names: Ulrich - family-names: Klein given-names: David +- family-names: Crawford + given-names: Michael - family-names: Steinmetz given-names: Nele - family-names: Chen @@ -41,8 +43,6 @@ authors: given-names: Pascal - family-names: Tommey given-names: Jake -- family-names: Crawford - given-names: Michael license: LGPL-3.0 repository-code: https://github.com/pik-piam/mrland doi: 10.5281/zenodo.3822083 diff --git a/DESCRIPTION b/DESCRIPTION index fb561d7b..bdd0df80 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -12,6 +12,7 @@ Authors@R: c( person("Lavinia", "Baumstark", role = "aut"), person("Ulrich", "Kreidenweis", role = "aut"), person("David", "Klein", role = "aut"), + person("Michael", "Crawford", role = "aut"), person("Nele", "Steinmetz", role = "aut"), person("David", "Chen", role = "aut"), person("Florian", "Humpenoeder", role = "aut"), @@ -20,8 +21,7 @@ Authors@R: c( person("Felicitas", "Beier", role = "aut"), person("David", "Hoetten", role = "aut"), person("Pascal", "Sauer", role = "aut"), - person("Jake", "Tommey", role = "aut"), - person("Michael", "Crawford", role = "aut") + person("Jake", "Tommey", role = "aut") ) Description: The package provides land related data via the madrat framework. From ce477f33046765f2c940b7e498c2a1ddd296cada Mon Sep 17 00:00:00 2001 From: Michael Crawford Date: Thu, 1 Oct 2026 12:26:26 +0200 Subject: [PATCH 25/25] Read the corrected Curtis table from forest_loss_fixed.csv The 2020 forest_loss.csv stays in the source folder for older mrland versions. Co-Authored-By: Claude Opus 5.5 (1M context) --- R/readForestLossDrivers.R | 4 ++-- man/readForestLossDrivers.Rd | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/R/readForestLossDrivers.R b/R/readForestLossDrivers.R index 0ea305f8..0deb586f 100644 --- a/R/readForestLossDrivers.R +++ b/R/readForestLossDrivers.R @@ -1,7 +1,7 @@ #' @title readForestLossDrivers #' #' @description Reads Table 1 of Curtis et al. (2018), tree cover loss 2001-2015 by world region -#' and dominant driver, transcribed by hand from the article into \code{forest_loss.csv}. +#' and dominant driver, transcribed by hand from the article into \code{forest_loss_fixed.csv}. #' #' @return magpie object with the mean annual loss per Curtis region and driver, Mha #' @author Abhijeet Mishra, Michael Crawford @@ -17,7 +17,7 @@ readForestLossDrivers <- function() { # loss 2001-2015 in Mha and driver shares in per cent. Rows are normalised to sum to 100: the # printed rows sum to 99-102 (rounding, and cells printed as "<1%"), while the drivers must sum # to the observed loss rather than exceed it. - df <- read.csv("forest_loss.csv") + df <- read.csv("forest_loss_fixed.csv") drivers <- c("deforestation", "shifting_agriculture", "forestry", "wildfire", "urbanization") df[, drivers] <- df$treecoverloss_01_15 * df[, drivers] / 100 / 15 return(as.magpie(df[, c("region", drivers)], temporal = NULL, spatial = "region")) diff --git a/man/readForestLossDrivers.Rd b/man/readForestLossDrivers.Rd index 5c5e1f03..9177708c 100644 --- a/man/readForestLossDrivers.Rd +++ b/man/readForestLossDrivers.Rd @@ -11,7 +11,7 @@ magpie object with the mean annual loss per Curtis region and driver, Mha } \description{ Reads Table 1 of Curtis et al. (2018), tree cover loss 2001-2015 by world region -and dominant driver, transcribed by hand from the article into \code{forest_loss.csv}. +and dominant driver, transcribed by hand from the article into \code{forest_loss_fixed.csv}. } \examples{ \dontrun{