Adding a Tracer

The steps needed to add a new tracer depend greatly on what the tracer is, so this page will not use a single tracer as an example. Also, a significant portion of the code shown in these examples will be cleaned up prior to the MARBL 1.0.0 release (sorry!).

MARBL Code Changes

This is an eight step process.

Step 1. Add to MARBL tracer index type

As mentioned in Step 1. Add to MARBL diagnostic indexing type, the indexing_type is a common structure in MARBL. Due to the many ways to introduce tracers (different modules, living tracers, etc), the tracer indexing type is a little more complex than others.

type, public :: marbl_tracer_index_type
  ! Book-keeping (tracer count and index ranges)
  integer (int_kind) :: total_cnt = 0
  type (marbl_tracer_count_type) :: ecosys_base
  type (marbl_tracer_count_type) :: ciso

  ! General tracers
  integer (int_kind) :: po4_ind         = 0 ! dissolved inorganic phosphate
  .
  .
  .
  ! CISO tracers
  integer (int_kind) :: di13c_ind       = 0 ! dissolved inorganic carbon 13
  .
  .
  .
  ! Living tracers
  type(marbl_living_tracer_index_type), allocatable :: auto_inds(:)
  .
  .
  .
contains
  procedure, public :: add_tracer_index
  procedure, public :: construct => tracer_index_constructor
  procedure, public :: destruct => tracer_index_destructor
end type marbl_tracer_index_type

For this example, assume we are adding a single tracer in the base ecosys module (regretfully referred to as “general tracers” in most comments).

Note

This data type does not conform to MARBL naming conventions and will be renamed marbl_tracer_indexing_type in a future update.

Step 2. Update tracer_index_constructor

If you are adding a tracer that is only active in certain configurations, you would include an if statement around the following code. At this point in time, all the base ecosystem tracers are present in all configurations, so there is no such restriction. For example, here we set in index for the refractory DOC tracer:

subroutine tracer_index_constructor(this, ciso_on, lvariable_PtoC, autotroph_settings, &
           zooplankton_settings, marbl_status_log)
  .
  .
  .
  ! General ecosys tracers
  .
  .
  .
  call this%add_tracer_index('docr', 'ecosys_base', this%docr_ind, marbl_status_log)
  .
  .
  .
end subroutine tracer_index_constructor

Note

There is an issue ticket to refer to objects as self instead of this. Example of an Object-oriented Class has it right.

Step 3. Set tracer metadata

MARBL provides the following metadata to describe each tracer:

type, public :: marbl_tracer_metadata_type
   character(len=char_len) :: short_name
   character(len=char_len) :: long_name
   character(len=char_len) :: units
   character(len=char_len) :: tend_units
   character(len=char_len) :: flux_units
   logical                 :: lfull_depth_tavg
   character(len=char_len) :: tracer_module_name
end type marbl_tracer_metadata_type

There are a few different subroutines in marbl_init_mod.F90 to define the metadata for different classes of tracers. (Metadata for carbon isotope tracers is handled in marbl_ciso_init_mod::marbl_ciso_init_tracer_metadata.)

subroutine marbl_init_tracer_metadata
subroutine marbl_init_non_autotroph_tracer_metadata
subroutine marbl_init_non_autotroph_tracers_metadata
subroutine marbl_init_zooplankton_tracer_metadata
subroutine marbl_init_autotroph_tracer_metadata

The last three subroutines above are called from marbl_init_tracer_metadata(), and marbl_init_non_autotroph_tracer_metadata() is called from marbl_init_non_autotroph_tracers_metadata() Prior to those calls, marbl_init_tracer_metadata() sets two attributes in the metadata type:

marbl_tracer_metadata(:)%lfull_depth_tavg   = .true.
marbl_tracer_metadata(:)%tracer_module_name = 'ecosys'

Metadata for all base ecosystem non-living tracers is set in marbl_init_non_autotroph_tracers_metadata(). For example, here is where the dissolved inorganic phosphate index is set:

subroutine marbl_init_non_autotroph_tracers_metadata(marbl_tracer_metadata, &
           marbl_tracer_indices)
  .
  .
  .
  call marbl_init_non_autotroph_tracer_metadata('PO4', 'Dissolved Inorganic Phosphate', &
             marbl_tracer_metadata(marbl_tracer_indices%po4_ind))

Step 4. Compute surface flux for new tracer (if necessary)

Not all tracers return a surface flux, so this may not be necessary for your tracer. For this example, we will follow the oxygen tracer. Surface fluxes are computed in marbl_surface_flux_mod::marbl_surface_flux_compute:

subroutine marbl_surface_flux_compute( &
  .
  .
  .
  associate(                                                                                      &
    .
    .
    .
    o2_ind            => marbl_tracer_indices%o2_ind,                                      &
    .
    .
    .
    )

  !-----------------------------------------------------------------------
  !  fluxes initially set to 0
  !-----------------------------------------------------------------------

  surface_fluxes(:, :) = c0
  .
  .
  .
  !-----------------------------------------------------------------------
  !  compute CO2 flux, computing disequilibrium one row at a time
  !-----------------------------------------------------------------------

  if (lflux_gas_o2 .or. lflux_gas_co2) then
     .
     .
     .
     if (lflux_gas_o2) then
       .
       .
       .
       pv_o2(:) = xkw_ice(:) * sqrt(660.0_r8 / schmidt_o2(:))
       o2sat(:) = ap_used(:) * o2sat_1atm(:)
       flux_o2_loc(:) = pv_o2(:) * (o2sat(:) - tracers_at_surface(:, o2_ind))
       surface_fluxes(:, o2_ind) = surface_fluxes(:, o2_ind) + flux_o2_loc(:)

Step 5. Compute tracer tendency

The tracer tendencies are computed in a two step process - MARBL computes the tracer tendency terms from a variety of processes and then combines the terms in the end. Given the modular nature of MARBL, the tendencies from each process are computed in their own routine. This is done in marbl_interior_tendency_mod::interior_tendency_compute:

subroutine marbl_interior_tendency_compute( &
  .
  .
  .
  call compute_PAR(domain, interior_tendency_forcings, interior_tendency_forcing_indices, &
                   totalChl_local, PAR)

  call compute_autotroph_elemental_ratios(km, autotroph_local, marbl_tracer_indices, tracer_local, &
       autotroph_derived_terms)

  call compute_function_scaling(temperature, Tfunc)
  .
  .
  .
  do k = 1, km

     call compute_scavenging(k, km, marbl_tracer_indices, tracer_local(:,:), &
          POC, P_CaCO3, P_SiO2, dust, Fefree(:), Fe_scavenge_rate(:), &
          Fe_scavenge(:), Lig_scavenge(:), marbl_status_log)

     if (marbl_status_log%labort_marbl) then
        call marbl_status_log%log_error_trace('compute_scavenging()', subname)
        return
     end if

     call compute_large_detritus_prod(k, domain, marbl_tracer_indices, zooplankton_derived_terms, &
          autotroph_derived_terms, Fe_scavenge(k),                    &
          POC, POP, P_CaCO3, P_CaCO3_ALT_CO2, P_SiO2, dust, P_iron,   &
          dissolved_organic_matter%DOP_loss_P_bal(k), marbl_status_log)

     ! FIXME #28: need to pull particulate share out
     !            of compute_particulate_terms!
     call compute_particulate_terms(k, domain,                   &
          marbl_particulate_share, p_remin_scalef(k),            &
          POC, POP, P_CaCO3, P_CaCO3_ALT_CO2,                    &
          P_SiO2, dust, P_iron, PON_remin(k), PON_sed_loss(k),   &
          QA_dust_def(k),                                        &
          tracer_local(:, k), carbonate, sed_denitrif(k),        &
          other_remin(k), fesedflux(k), marbl_tracer_indices,    &
          glo_avg_fields_interior_tendency, marbl_status_log)

     if (marbl_status_log%labort_marbl) then
        call marbl_status_log%log_error_trace('compute_particulate_terms()', subname)
        return
     end if

     if  (k < km) then
        call update_particulate_terms_from_prior_level(k+1, POC, POP, P_CaCO3, &
             P_CaCO3_ALT_CO2, P_SiO2, dust, P_iron, QA_dust_def(:))
     endif

  end do ! k
  .
  .
  .
  call compute_denitrif(km, marbl_tracer_indices, tracer_local(:, :), &
       dissolved_organic_matter%DOC_remin(:), &
       dissolved_organic_matter%DOCr_remin(:), &
       POC%remin(:), other_remin(:), sed_denitrif(:), denitrif(:))

  call compute_local_tendencies(km, marbl_tracer_indices, autotroph_derived_terms, &
       zooplankton_derived_terms, &
       dissolved_organic_matter, &
       nitrif(:), denitrif(:), sed_denitrif(:), &
       Fe_scavenge(:), Lig_prod(:), Lig_loss(:), &
       P_iron%remin(:), POC%remin(:), POP%remin(:), &
       P_SiO2%remin(:), P_CaCO3%remin(:), P_CaCO3_ALT_CO2%remin(:), &
       other_remin(:), PON_remin(:), &
       tracer_local(:,:), &
       o2_consumption_scalef(:), &
       o2_production(:), o2_consumption(:), &
       interior_tendencies(:,:))

The tendencies are combined in compute_local_tendencies while subroutines like compute_PAR, compute_autotroph_uptake, and compute_denitrif are the per-process computations. So you will need to update compute_local_tendencies to compute the tracer tendency for your new tracer correctly:

subroutine compute_local_tendencies(km, marbl_tracer_indices, autotroph_derived_terms, &
     zooplankton_derived_terms, dissolved_organic_matter, nitrif, denitrif, sed_denitrif, &
     Fe_scavenge, Lig_prod, Lig_loss, P_iron_remin, POC_remin, POP_remin, P_SiO2_remin, &
     P_CaCO3_remin, P_CaCO3_ALT_CO2_remin, other_remin, PON_remin, tracer_local, &
     o2_consumption_scalef, o2_production, o2_consumption, interior_tendencies)
  .
  .
  .
  do k=1, km
    .
    .
    .
    o2_consumption(k) = (O2_loc(k) - parm_o2_min) / parm_o2_min_delta
    o2_consumption(k) = min(max(o2_consumption(k), c0), c1)
    o2_consumption(k) = o2_consumption(k) * ((POC_remin(k) * (c1 - POCremin_refract) + DOC_remin(k) + DOCr_remin(k) &
                                              - (sed_denitrif(k) * denitrif_C_N) - other_remin(k) &
                                              + sum(zoo_loss_dic(:,k)) + sum(zoo_graze_dic(:,k))  &
                                              + sum(auto_loss_dic(:,k)) + sum(auto_graze_dic(:,k))) &
                                             / parm_Remin_D_C_O2 + (c2 * nitrif(k)))
    o2_consumption(k) = o2_consumption_scalef(k) * o2_consumption(k)

    interior_tendencies(o2_ind,k) = o2_production(k) - o2_consumption(k)

  end do

Step 6. Add any necessary diagnostics

By default, MARBL’s diagnostics include the interior restoring tendency for each tracer. Otherwise, it is assumed that the GCM will provide tracer diagnostics itself. MARBL does compute the vertical integral of the conservative terms in the source-sink computation of many tracers. If your tracer affects these integrals, you should update the appropriate subroutine in marbl_diagnostics_mod.F90:

private :: store_diagnostics_carbon_fluxes
private :: store_diagnostics_nitrogen_fluxes
private :: store_diagnostics_phosphorus_fluxes
private :: store_diagnostics_silicon_fluxes
private :: store_diagnostics_iron_fluxes

If you want to provide a specific diagnostic related to your tracer, see Adding a Diagnostic.

Step 7. Update the settings YAML files

The defaults/settings_*.yaml files also contain a list of all defined tracers. On the development branch, make changes to defaults/settings_latest.yaml. Release branches may only offer specific versions of this file, such as defaults/settings_cesm2.1.yaml. The block of code defining the tracers looks like this:

# ABOUT THIS FILE
# ---------------
.
.
.
# Tracer count
_tracer_list :
   # Non-living tracers
   PO4 :
      long_name : Dissolved Inorganic Phosphate
      units : mmol/m^3
   NO3 :
      long_name : Dissolved Inorganic Nitrate
      units : mmol/m^3
.
.
.

This list needed because some parameters (such as tracer_restore_vars(:)) depend on the tracer count. Additionally, it makes it easy for GCMs to see a list of all tracers being returned by MARBL to help configure diagnostic output.

Step 8. Convert the YAML file to JSON

We prefer editing YAML files to editing JSON files because they are much easier to maintain (and allow user comments). Unfortunately, python does not include a YAML parser in the default distributions. Rather than require all users to install pyYAML, we require that of MARBL developers and then ask them to convert the YAML files to JSON. The MARBL_tools/yaml_to_json.py script is provided to do just that:

$ cd MARBL_tools
$ ./yaml_to_json.py

There is not a tracer-specific python script to run, but the MARBL_settings_class has get_tracer_names() and get_tracer_cnt() routines.

GCM Code Changes

The GCM will need to provide initial conditions for this new tracer, and may also need to output additional tracer-specific diagnostics. The MARBL guide is not able to offer guidance on how to do that, as it will vary from GCM to GCM.