Skip to content

Code organization and design philosophy

Marcos Longo edited this page Nov 11, 2025 · 1 revision

ED2 code structure

The ED2 code is mostly written in modern Fortran (i.e., Fortran 90 and later), with very few file handling utilities that are written in C language. New contributions to the code should be all written in standard modern Fortran, because ED2 has a unique data structure which is not easily ported to different languages. Also, make sure to check the good coding practices section (see below) before contributing.

The main ED2 directory (<path_to>/ED2/ED) is divided in the following subfolders:

  • src: The source code directory
  • build: The compilation instructions
  • run: The directory containing a sample ED2IN namelist.
  • Template: A few system-specific utilities for setting multiple simulations.

The src directory

The source code directory, in turn, is divided in several sub-folders:

  • driver: The main ED drivers. This directory contains the main program, and subroutines that organize the model initialization and the time step call.
  • dynamics: Contains most subroutines that drive the ecosystem dynamics at various time steps.
  • include: Contains some header files used by the very few functions written in C;
  • init: Contains subroutines that will initialize various model variables and parameters;
  • io: Contains the functions and subroutines that read and write files;
  • memory: Contains all modules with the model global variables, and some functions that allocate and deallocate the global variables;
  • mpi: Contains all the message-passing interface procedures needed for a parallel run.
  • utils: Contains subroutines that don't really fit into any of the previous directories.
  • preproc: Not part of the main code, it contains some basic tools needed to convert ASCII datasets into HDF5. Check the Drivers section for additional information on how to prepare meteorological drivers for ED2.

The build directory

The build directory has the following sub-folders and the install.sh script:

  • make. This directory contain most compilation instructions:
    • paths.mk: This will set up all the folder structure for the compiler. Unless you are developing the code and has changed the code structure (i.e. added new directories), you don't need to change anything.
    • objects.mk: This file contains the list of all objects to be compiled. This shouldn't be changed unless you are developing the code and added a new fortran file;
    • rules.mk: This file contains the instructions on how to compile each object. Unless you are adding code that is in a new file, this should not be changed.
    • Makefile: This is the main instruction file, which will handle the compilation. Normally this file should not be changed.
    • include.mk.[PLATFORM]: These are system (platform)-dependent files, which set the compiler, the compilation options, and the library paths for MPI and HDF5. You will likely need to edit one of them, or create a new file specific for the computer/cluster where you are going to run ED2.
  • shell. This directory has a few shell and perl scripts used during the compilation. You do not need to run any of the scripts by themselves, they will be called by install.sh.
    • generate_deps.sh: This shell calls sfmakedepend.pl and creates the list of dependencies to set the order in which the Fortran files must be compiled. Normally this file should not be changed.
    • sfmakedepend.pl: The perl script that actually creates the dependency list. Normally this file should not be changed.
    • 2ndcomp.sh: This partially cleans the compilation when checking interfaces. Unless you are adding code that is in a new file, this should not be changed.

The install.sh calls the Makefile as well as any script needed for compilation. Make sure to visit the compilation instructions page before running this script.

Good coding practices

We appreciate new contributions on model development, but we ask you to follow a few good coding practices. Below is a list of things to keep in mind when writing new code:

  • Comments. Make sure to explain the rationale and the steps in the code. We are planning to make all comments in ED-2.2 compatible with Doxygen, so if you add new code in this format, it will help us.
  • Modules. If you create sub-routines or functions that have pointers/targets as arguments, please put them inside a Fortran module. Also, for consistency with the rest of the code, use the same name for the Fortran file and the module name (i.e. module my_new_feature goes to file my_new_feature.f90).
  • Variable declaration. Make sure that all variables are declared (i.e. always use implicit none), and prefer to give intuitive names:
    • Encouraged: leaf_energy for leaf internal energy.
    • Discouraged: e for leaf internal energy).
  • Choose appropriate variable types. Variables that represent quantities should be typically declared as real. Variables that describe multiple options are typically declared as integer. Variables describing yes/no type of tests should be declared as logical
  • Variables from modules. We frequently need to load variables from other modules. In these cases, make sure they are explicitly declared when invoking the use command:
    • Encouraged: use consts_coms, only : t3ple ! ! intent(in)
    • Discouraged: use consts_coms ! This loads all variables from consts_coms
  • Constants. Never set physical constants (e.g. gravity acceleration, Avogrado's number) as undeclared numbers in the code, it is nearly impossible to track them. Instead, define them in ED/src/memory/consts_coms.F90. The constant you are looking for may be already there...
  • Parameters. Unlike physical constants with known and determined values, parameters may be related to traits and phenomenological parameterisations. These should never go into consts_coms.F90 and should not have parameter option when declaring the variable, as they are not constants. Likewise, do not initialise the variable in the module. Instead, insert code in ED/src/init/ed_params.f90 to set the default value, and include code in ED/src/io/ed_xml_config.f90 to allow the variable to be read through xml.
  • f90 or F90. For most files, the extension should be .f90 (lower case). The only cases that you must use .F90 (upper case) is when you include pre-processor instructions in the code. Currently in ED-2.2, this happens in the following cases:
    • Code that must be skipped if not compiled with MPI libraries. For example: ED/src/driver/ed_driver.F90.
    • Code that must be compiled in case of coupled ED-BRAMS code. For example: ED/src/init/ed_init_atm.F90.
    • System-dependent libraries. For example: ED/src/utils/rsys.F90.
  • Debug. Always compile the code with strict compilation settings, and make sure that the code runs for at least a few years under these settings. The code is going to run a lot slower, but it will catch common code mistakes.
  • Clean the clutter. Once you are happy with your new code, make sure to remove any declared variable or module variable that is no longer used. This helps with code readability.
  • Indent. Code inside blocks (e.g., if, do, where, select case) should always be indented. This helps with readability and reduce code mistakes. By default we use 3 spaces to mark indentation, and name such blocks when the block spans are rather long (more than 20 lines or so):
cohort_loop: do ico = 1, cpatch%ncohorts
   # Update stem number density based on accumulated mortality rates.
   cpatch%nplant(ico) = cpatch%nplant(ico) * exp(cpatch%monthly_dlnndt(ico))
   ...
end do cohort_loop

Note that these practices are evolving and being implemented at the code as we continue to develop it, but they may not be fully implemented. If you find code that does not meet these best practices, consider fixing it and making a pull request, thanks!

A few additional suggestions for robustness

Prefer using select case over if when choosing between multiple options from the same variable. This will catch potential bugs due to multiple entries referring to the same option.

! Encouraged construction
select_allometry: select case (iallom)
case (1)
   ...
case (2)
   ...
case (2)
   ! This will cause the compilation to fail, because case (2) is already defined
   ...
case default
   ...
end select select_allometry

! Discouraged construction
pick_allometry: if (iallom == 1) then
   ...
else if (iallom == 2) then
   ...
else if (iallom == 2) then
   ! The code will happily compile but this part will never be called.
   ...
else
   ...
end if pick_allometry

Vectorised loops and constructions (e.g., where or using vector(:) ) are fine and preferred in most cases:

! Where-based approach, preferred
where (pft_param_two(:) == 0.0)
   pft_param_three(:) = 1.0
elsewhere
   pft_param_three(:) = ( pft_param_one(:) - 1.0 ) / pft_param_two(:)
end where

! Do-if construction, acceptable but a bit longer
pft_loop: do ipft=1,n_pft
   if ( pft_param_two(ipft) == 0.0 ) then
      pft_param_three(ipft) = 1.0
   else
      pft_param_three(ipft) = ( pft_param_one(ipft) - 1.0 ) / pft_param_two(ipft)
   end if
end do pft_loop

The exception to the rule above is when the code is looping through cohorts or polygons. In this case, always use the do construction: patches may have zero cohorts (e.g., hyper-arid areas) and grids may have zero polygons (e.g., coupled simulations may have a grid block that is entirely over oceans), and the vectorised option can cause segmentation violation depending on the compiler.

! This will work even if the patches have zero cohorts.
cohort_loop: do ico=1,cpatch%ncohorts
   cpatch%leaf_storage_resp(ico) = 0.0
end do cohort_loop

! This will cause segmentation violation errors when patches are devoid of cohorts.
cpatch%leaf_storage_resp(:) = 0.0

Clone this wiki locally