Mastering Disturbance Matrices in CBM4: an R Guide

Author

Vinicius Manvailer

Published

January 9, 2026

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: Clear 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:

  • A Disturbance Type is a general category of event. Examples: Wildfire, Harvest, Insect attacks. It’s like the name of a dish, say, “Cake”.

  • A Disturbance Matrix is a specific recipe for that event. It contains the exact proportions for carbon transfer. For the “Cake” dish, you could have many recipes: Chocolate Cake, Carrot Cake, etc. Similarly, for the Wildfire disturbance type, you can have different matrices representing different severities (Low, modera or high severity), ecosystem responses (boreal plains, taiga plains) or a combination of both.

Unique vs. individual matrices

Because matrices tend to be province and ecozone specific, CBM use one recipe per province x ecozone combination (spatial unit). However, often times the same recipe is used across many spacial units. When querying disturbance matrices by province and/or ecozone you may get the same DM multiple times, in those cases they will be labeled differently, for example, Wildfire - Alberta Boreal Plains, Wildfire - British Columbia Boreal Plains, and etc. They are labeled diferenttly but might be the same DM (or recipe).

  • A Unique Matrix is the fundamental recipe, identified by disturbance_matrix_id. It’s a distinct set of carbon transfer proportions.

  • An Individual Matrix is the application of a unique matrix to a specific place (a spatial_unit_id). These are identified by a descriptive id_title (e.g., “Firewood harvest in Alberta’s Boreal Plains”).

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 matrices (recipes) for disturbance types (category) that have “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, 26),
  caption = "Table 1: 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 nice alluvial diagram showing the flow of carbon.

Let’s visualize one of the unique “Firewood Harvest” 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 harvest disturbance matrix.

The carbon flow story for a single harvest 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 pools, 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 Softwood specifically (evidenced by the ‘SW’ in the title).

You can also easily compare multiple matrices by faceting the plot. Let’s compare the first three individual harvest 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 harvest matrices.

Comparing the carbon flow stories of three individual harvest 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 easily get mixed up during data processing, the function will always validate the data before displaying it. The validation consists of checking the sum of proportions in each sink pool (and each category in dm_id argument) to ensure they add up to 1, i.e. no mass is lost or created after transfers. 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 get 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_v3")
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\RtmpKGFdkG\dm_vignette_v33ee84daa13e4/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. You will receive an descriptive error message if you something is missing.

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 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,
          caption = "Table 4: New DM data for Low Impact Harevst.",
  options = list(scrollX = TRUE, pageLength = 3),
  class = 'cell-border stripe')

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\RtmpKGFdkG\dm_vignette_v33ee84daa13e4/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 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. However, if you wish to replace an existing DM you can simply pass the new DM with new set of proportions using the existing DM name and description. This will effectively overwrite the existing DM preserving its ID and associations with different spatial_unit_id.

3.4: Adding multiple matrices in bulk

The add_disturbance_matrices() accepts addition of multiple recipes for the different spatial units at once. This is incredibly efficient. You can add, for example, 30 different DMs at once for the “Harvest - Low Impact” disturbance type, that is, one DM for each spatial_unit_id!

The key is to provide a data.frame or data.table where each new matrix is clearly defined. A disturbance matrix is defined as a unique combination of disturbance_matrix_name, disturbance_matrix_description, and set of proportion values.

You can: - Assign one DM to one spatial_unit_id or - Assign the same DM to multiple spatial_unit_id

You cannot: - Have more than one set of DM proportions for a given spatial_unit_id - Have different disturbance_matrix_name and/or disturbance_matrix_description for a given set of DM proportions

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
# This loop will replicate the template_dm and 
# - add a new spatial_unit_id to every iteration
# - create a name and description for each spatial_unit_id version.
all_new_dms <- lapply(sk_spatial_units$spatial_unit_id, function(spu_id) {
  
  # 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 := "Low-Impact Harvest SK"]
  new_dm_for_spu[, disturbance_matrix_description := "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\RtmpKGFdkG\dm_vignette_v33ee84daa13e4/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

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 a lot of time.

Note: Remember you try to add a new DM with the same name as an existing one, the existing DM in the database will be overwritten.

3.5: Verify your work

Query the database again to see your new additions! 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"

Final thoughts

Congratulations! You now have the conceptual framework and the practical skills to explore, understand, and customize disturbance matrices in CBM.

Happy modeling!