Mastering Disturbance Matrices in CBM4: an R Guide

Vinicius Manvailer

2026-01-09

The Big Picture: From Forest Events to Carbon Fates

When thinking about disturbance events like a wildfire sweeping through a forest, it may be difficult to imagine what does it actually mean for the carbon stored in that ecosystem. Some carbon goes to the atmosphere as smoke. Trees die but have their tree trunk still standing for a long time. Some leaves and branches are fully consume by fire while some may become litter in the ground and continue to decay over time.

This complex story is told by a Disturbance Matrix (DM). It’s a precise set of proportions—much like a recipe—that dictates how carbon moves from one pool to another during a disturbance. Understanding these matrices and being able to update them is key component in improving forest carbon modeling and understanding carbon dynamics in simulations.

This guide will cover all you need to know to explore how CBM4 uses and structures DMs for disturbance events. We’ll show you how to:

  1. Understand the Concepts: Learn the simple, key ideas behind DMs.
  2. Explore & Visualize: Find and understand the built-in ‘disturbance recipes’.
  3. Create & Customize: Add your own ‘disturbance recipes’ to the model.

Let’s begin by loading our essential tools.

library(rcbm4)
library(data.table)
library(ggalluvial)
library(DT)

Core Concepts: A clearer vocabulary

Before we touch the code, let’s get our terms right. Getting this part makes everything else intuitive.

Disturbance Type vs. Disturbance Matrix

Think of it like this:

Unique vs. Individual Matrices

Because matrices tend to be province and ecozone specific, CBM tends to use one recipe per province x ecozone combination (spatial unit). However, often times the same recipe is used across many special units. When they are reused, they are labeled differently, for example, Chocolate Cake - Alberta Boreal Plains, Chocolate Cake - British Columbia Boreal Cordillera, and etc. It is the same recipe but labeled differently.

So, if you query for all Firewood harvest matrices, you might get back 9 individual matrices, but find they are all based on just 3 unique matrix recipes.

A Practical Workflow

Step 1: Explore what exists with get_disturbance_matrices()

The get_disturbance_matrices() function is your window into the CBM’s recipe book.

Let’s ask for all disturbances with “Firewood” in their name.

firewood_dms <- get_disturbance_matrices(
  disturbance_type_names = "Firewood"
)
#> --- 192 individual DMs returned.
#>  |-->  3 unique DMs (disturbance_matrix_id). 
#> 
#> The same DM may be replicated across the combination of eco_boundary_id, admin_boundary_name, disturbance_type_name and disturbance_matrix_name

# Now, let's apply the vocabulary to the data.
# An individual matrix is a unique application of a matrix recipe to a spatial unit.
names_individual <- unique(firewood_dms[,c("disturbance_matrix_id", "disturbance_matrix_name", "spatial_unit_id", "disturbance_type_name")])
# A unique matrix is the fundamental recipe itself.
names_unique <- unique(firewood_dms[,c("disturbance_matrix_id", "disturbance_matrix_name")])

# The tables below provide an interactive way to explore this data.

# Table 1: A preview of the raw disturbance matrix data.
datatable(
  head(firewood_dms, 10),
  caption = "Table 1: A preview of the raw disturbance matrix data in 'long' format.",
  options = list(scrollX = TRUE, pageLength = 5, searching = FALSE),
  class = 'cell-border stripe'
)

# Table 2: Individual matrix applications found for 'Firewood'.
datatable(
  names_individual,
  caption = "Table 2: Individual matrix applications found for 'Firewood'.",
  options = list(scrollX = TRUE, pageLength = 5),
  class = 'cell-border stripe'
)

# Table 3: Unique matrix recipes found for 'Firewood'.
datatable(
  names_unique,
  caption = "Table 3: Unique matrix recipes found for 'Firewood'.",
  options = list(scrollX = TRUE, pageLength = 5),
  class = 'cell-border stripe'
)

The console should have displayed a message showing that 192 individual DMs were return which are based on 3 unique DMs. The output is a data.table in “long” format, where every row is a single instruction in the recipe: move proportion of carbon from source_pool to sink_pool.

Step 2: See the story with viz_disturbance_matrix()

A list of proportions is hard to interpret. Let’s visualize the story of a disturbance! The viz_disturbance_matrix() function creates a beautiful alluvial diagram showing the flow of carbon.

Let’s visualize one of the unique “Salvage” recipes we found.

# First, pick a unique matrix to visualize. id_title is the best to isolate.
one_unique_matrix_id <- unique(firewood_dms$id_title)[1]

# Filter our data to just one individual application of that unique matrix
dm_to_plot <- firewood_dms[id_title == one_unique_matrix_id]


# Now, visualize it!
viz_disturbance_matrix(
  dm_data = dm_to_plot,
  title = dm_to_plot$id_title[1]
)
#> Visualizing proportions 
#>  - Validating conservation of mass
#>  - Conservation of mass validated successfully. ✅
The carbon flow story for a single salvage disturbance matrix.
The carbon flow story for a single salvage disturbance matrix.

This plot tells you instantly where the carbon goes. The left side is the source, the right is the destination, and the width of the flow shows the proportion. It’s a powerful way to check if a matrix behaves the way you expect. We can see, for example, that most pools stay intact while about a third of the Softwood Foliage, Softwood Other and Softwood Merchantable, are moved to Products or to Aboveground Very Fast DOM - with some impacts to different Roots pools. This is expected as this is a DM that targets SW specifically.

You can also easily compare multiple matrices by faceting the plot. Let’s compare the first three individual salvage matrices.

# Select data three individual matrix in Newfoundland, Boreal Shield East.
three_dms_to_compare <- firewood_dms[id_title %in% unique(firewood_dms$id_title)[c(1, 49, 97)]]

viz_disturbance_matrix(
  dm_data = three_dms_to_compare,
  dm_id = "id_title", # Tell the function to create a separate plot for each "id_title"
  title = "Comparing three harvesting operations"
)
#> Visualizing proportions 
#>  - Validating conservation of mass
#>  - Conservation of mass validated successfully. ✅
Comparing the carbon flow stories of three individual salvage matrices.

Comparing the carbon flow stories of three individual salvage matrices.

We can immediately see that the DMs labeled ‘HW’ draw from Hardwood pools while the ones labeled ‘SW’ draw from Softwood pools. We can also immediately see that The DM in the middle is the one with the highest amount of Products all of which are coming from Snag and Medium DOM pools. This makes intuitive sense as this is a post-logging operation (as shown in the title) which gathers dead standing trees in the Snags pool and dead fallen trees in the Medium DOM pool.


Note: Since DMs can get mixed up, the function will always validate the data before displaying it. The validation consists of sum of proportions by sink pool (and by the column specified in the dm_id argument) to ensure adds up to 1, i.e. no mass is lost or created. If the values are below or above 1 the function will still display the data with warning telling you which pools have failed validation and what was the result of the sum. If all pools are affect and the result is, for example, 2 it means you are trying to display 2 DMs as one and may need to specify the dm_id argument properly.

Step 3: Create your own narrative with add_disturbance_matrices()

This is where you can control of DMs. If you need to model a disturbance that’s not in CBM, you can add (or modify) it with add_disturbance_matrices().

Golden Rule: Never modify the original CBM database. Always make a copy first. The function should warn you and prompt you to confirm if you accidentally try to modify the original database, in which case you can simply say no. If you modify the original, you may need to reinstall the package to restore default CBM database (and behaviors)

Let’s create a new disturbance type for a hypothetical “Low-Impact Harvest”.

3.1: Set Up a Safe Workspace

We’ll create a temporary folder and copy the CBM database there.

skip_if_no_libcbm()

proj_path <- tempfile("dm_vignette_v2")
dir.create(proj_path)

# This is where the default CBM database is located.
libcbm_resources <- reticulate::import("libcbm.resources")
cbm_defaults_path_original <- libcbm_resources$get_cbm_defaults_path()

# Let's create a copy!
cbm_defaults_path_copy <- file.path(proj_path, "cbm_defaults_copy.db")
file.copy(from = cbm_defaults_path_original, to = cbm_defaults_path_copy)
#> [1] TRUE

message("Safe workspace ready. Database copied to: ", cbm_defaults_path_copy)
#> Safe workspace ready. Database copied to: C:\Users\vmanvail\AppData\Local\Temp\RtmpOWAviR\dm_vignette_v26ea87e317651/cbm_defaults_copy.db

3.2: Create the New “Recipe” Data

The easiest way is to modify an existing recipe. We’ll grab a standard harvest DM and adapt it. The new_data you provide must contain these columns: disturbance_matrix_name, spatial_unit_id, source_pool_id, sink_pool_id, and proportion.

skip_if_no_libcbm()

# Get a template DM from our COPY of the database
for_management_dms <- get_disturbance_matrices(
  cbm_defaults_path = cbm_defaults_path_copy, # Must specify the copy here.
  disturbance_type_group = "ForestManagement", 
  provinces = "Ontario"
) # Look at the notes below to learn better ways to find DMs.
#> Your search for disturbance_type_group matched the following groups:
#>   ForestManagement
#> --- 28 individual DMs returned.
#>  |-->  12 unique DMs (disturbance_matrix_id). 
#> 
#> The same DM may be replicated across the combination of eco_boundary_id, admin_boundary_name, disturbance_type_name and disturbance_matrix_name

one_dm <- unique(for_management_dms$id_title)[1]

template_dm <- for_management_dms[id_title == one_dm]

# Create a copy to modify. This is crucial with data.table to avoid changing the original.
new_dm_data <- copy(template_dm)
# Add the new name for our recipe
new_dm_data[, `:=`(disturbance_matrix_name = "Low-Impact Harvest ON-BS-E",
                   disturbance_matrix_description = "Biomass reduced from standard 97% clearcut harvest ON-BS-E")]

# IMPORTANT: Here you would modify the 'proportion' values to reflect your
# new disturbance. For this example, we'll imagine we've reduced the amount
# of biomass removed and left more on site.
# (We will keep the original proportions for simplicity here).
# Remember: For each source pool, the proportions MUST sum to 1!

datatable(new_dm_data)

Pro tip: There are multiple ways to query DMs consult the function documentation ?get_disturbance_matrices to see all the different way you can do this.

3.3: Add the New Recipe to the Database

Now we call add_disturbance_matrices(), pointing to our copied database. We provide our new_data and also give a name to the new Disturbance Type we’re creating.

skip_if_no_libcbm()

add_disturbance_matrices(
  cbm_defaults_path = cbm_defaults_path_copy, # Must specify the copy we are modifying
  new_data = new_dm_data,
  disturbance_type_name = "Harvest - Low Impact",
  disturbance_type_description = "A new disturbance type for low-impact selective harvesting."
)
#> INFO | Will add new disturbance matrices to  C:\Users\vmanvail\AppData\Local\Temp\RtmpOWAviR\dm_vignette_v26ea87e317651/cbm_defaults_copy.db
#> INFO | Input data validation passed. Proceeding to add disturbance matrices...
#> INFO | Checking if a disturbance type exists with the same name ('Harvest - Low Impact')
#> INFO | Adding new disturbance type ' Harvest - Low Impact '
#> INFO | New disturbance type added with id  307
#> INFO | Checking if any of the disturbance matrix names below already exists 
#>  Low-Impact Harvest ON-BS-E
#> INFO | No existing disturbance matrix names found. Proceeding to add new matrices.
#> New disturbance matrices added successfully!
#> Assigned IDs:
#>       disturbance_matrix_name disturbance_matrix_id
#>                        <char>                 <int>
#> 1: Low-Impact Harvest ON-BS-E                   481

What about updating? The CBM database is designed for additions. The best way to “update” a matrix is to add a new, corrected version with a distinct name (e.g., “Wildfire v2”) and associate it with the desired spatial units in your simulation input. This preserves a clear history and prevents accidental modification of shared default matrices.

3.4: Adding Multiple Matrices in Bulk

The add_disturbance_matrices() function is vectorized, meaning you can add recipes for multiple spatial units at once. This is incredibly efficient.

The key is to provide a data.frame or data.table where each new matrix is clearly defined. You must have unique combinations of disturbance_matrix_name, disturbance_matrix_description, and spatial_unit_id for each new matrix you are adding.

Let’s create and add our “Low-Impact Harvest” matrix for all spatial units in Saskatchewan.

skip_if_no_libcbm()

# 1. Get a list of all spatial units for Saskatchewan
sk_spatial_units <- CodesSpatialUnitsDB()[admin_boundary_name == "Saskatchewan"]

# 2. Use the same template DM from our previous step
#    (We are still using the `template_dm` from the "add-dm-create-data" chunk)

# 3. Build a list of data.tables, one for each spatial unit in Saskatchewan
all_new_dms <- lapply(sk_spatial_units$spatial_unit_id, function(spu_id) {
  
  # Get the ecozone name for creating a unique description
  ecozone_name <- sk_spatial_units[spatial_unit_id == spu_id, eco_boundary_name]
  
  # Create the recipe data for this SPU
  new_dm_for_spu <- template_dm[, .(source_pool_id, sink_pool_id, proportion)]
  new_dm_for_spu[, spatial_unit_id := spu_id]
  new_dm_for_spu[, disturbance_matrix_name := paste("Low-Impact Harvest SK", sep = " - ")]
  new_dm_for_spu[, disturbance_matrix_description := paste("Low-Impact harvest for SK")]
  
  return(new_dm_for_spu)
})

# 4. Combine the list into a single large data.table
bulk_new_dm_data <- rbindlist(all_new_dms)

# 5. Add them all in one call!
add_disturbance_matrices(
  cbm_defaults_path = cbm_defaults_path_copy,
  new_data = bulk_new_dm_data,
  disturbance_type_name = "Harvest - Low Impact",
  disturbance_type_description = "A new disturbance type for low-impact selective harvesting."
)
#> INFO | Will add new disturbance matrices to  C:\Users\vmanvail\AppData\Local\Temp\RtmpOWAviR\dm_vignette_v26ea87e317651/cbm_defaults_copy.db
#> INFO | Input data validation passed. Proceeding to add disturbance matrices...
#> INFO | Checking if a disturbance type exists with the same name ('Harvest - Low Impact')
#> INFO | Found existing disturbance type name for ' Harvest - Low Impact ' with id  307
#> INFO | Will connect your DMs to this existing disturbance type and description.
#> INFO | Checking if any of the disturbance matrix names below already exists 
#>  Low-Impact Harvest SK
#> INFO | No existing disturbance matrix names found. Proceeding to add new matrices.
#> New disturbance matrices added successfully!
#> Assigned IDs:
#>    disturbance_matrix_name disturbance_matrix_id
#>                     <char>                 <int>
#> 1:   Low-Impact Harvest SK                   482

message("Successfully added Low-Impact Harvest matrices for all of Saskatchewan!")
#> Successfully added Low-Impact Harvest matrices for all of Saskatchewan!

This simple loop allows you to programmatically generate and deploy custom disturbance matrices across an entire province or any set of spatial units you define, saving immense amounts of time.

3.5: Verify Your Work

Query the database again to see your new creation! Now, when you search for the “Harvest - Low Impact” disturbance type, you should see the original one we created for Ontario, plus all the new ones for Saskatchewan.

skip_if_no_libcbm()

newly_added_dm <- get_disturbance_matrices(
  cbm_defaults_path = cbm_defaults_path_copy,
  disturbance_type_names = "Harvest - Low Impact"
)
#> --- 6 individual DMs returned.
#>  |-->  2 unique DMs (disturbance_matrix_id). 
#> 
#> The same DM may be replicated across the combination of eco_boundary_id, admin_boundary_name, disturbance_type_name and disturbance_matrix_name

unique(newly_added_dm$disturbance_matrix_name)
#> [1] "Low-Impact Harvest ON-BS-E" "Low-Impact Harvest SK"

Conclusion: You’re in Control

Congratulations! You’ve gone from a curious user to a disturbance matrix master. You now have the conceptual framework and the practical skills to explore, understand, and customize the very heart of the CBM. You can now tell your own, more precise stories about how ecosystems respond to change. Happy modeling!