Skip to contents

Overview

This guide provides comprehensive instructions for adding a new C++ module to the Fisheries Integrated Modeling System (FIMS). The FIMS architecture is complex, involving C++ implementation, R interface layers, and TMB integration. This guide walks through all necessary steps and files that need to be modified.

Who Should Use This Guide

This guide is intended for:

  • Developers adding new population dynamics modules (e.g., selectivity, recruitment, maturity)
  • Contributors adding new distribution models
  • Anyone extending FIMS with new model components

Prerequisites

Before starting, ensure you have:

  • Familiarity with C++ and template programming
  • Understanding of the R/C++ interface via Rcpp
  • Basic knowledge of TMB (Template Model Builder)
  • Development environment set up as described in CONTRIBUTING.md

FIMS Code Structure

FIMS follows a modular architecture with clear separation between:

  1. C++ Implementation Layer (inst/include/): Core mathematical and computational logic
  2. R Interface Layer (inst/include/interface/rcpp/): Bridges C++ and R
  3. R Wrapper Layer (R/): R functions for initialization and data management
  4. Module Registration (src/): Exposes modules to R via Rcpp

Directory Structure

FIMS/
├── inst/include/
│   ├── population_dynamics/      # Core population dynamics modules
│   │   ├── selectivity/
│   │   │   ├── functors/         # Specific implementations
│   │   │   │   ├── selectivity_base.hpp    # Base class
│   │   │   │   ├── logistic.hpp            # Implementation example
│   │   │   │   └── double_logistic.hpp     # Implementation example
│   │   │   └── selectivity.hpp   # Module header (includes all functors)
│   │   ├── recruitment/
│   │   ├── maturity/
│   │   ├── growth/
│   │   └── fleet/
│   └── interface/rcpp/rcpp_objects/  # R-C++ interface
│       ├── rcpp_selectivity.hpp      # Rcpp interface for selectivity
│       ├── rcpp_recruitment.hpp
│       └── ...
├── src/
│   ├── fims_modules.hpp          # Rcpp module definitions
│   └── init.hpp                  # Module initialization
└── R/
    └── initialize_modules.R      # R initialization functions

Naming Conventions

FIMS follows consistent naming conventions to maintain code readability and organization:

C++ Naming Conventions

Following the Google C++ Style Guide:

  • Classes: PascalCase (e.g., LogisticSelectivity, BevertonHoltRecruitment)
  • Namespaces: snake_case (e.g., fims_popdy, fims_info)
  • Member variables: snake_case with no trailing underscore for public members (e.g., inflection_point, log_devs)
  • Methods/Functions: PascalCase for interface methods, snake_case for internal functions
  • Template parameters: Use typename Type (not class T)
  • File names: snake_case (e.g., logistic.hpp, selectivity_base.hpp)

R Naming Conventions

Following the tidyverse style guide:

  • Functions: snake_case (e.g., initialize_selectivity, m_agecomp)
  • Classes: PascalCase (e.g., LogisticSelectivity)
  • Variables: snake_case (e.g., fleet_name, module_input)

Module-Specific Conventions

  • Base classes: Suffix with Base (e.g., SelectivityBase, RecruitmentBase)
  • Interface classes: Suffix with Interface (e.g., LogisticSelectivityInterface)
  • Header guards: FIMS_[PATH]_[FILENAME]_HPP format
    • Example: FIMS_POPULATION_DYNAMICS_SELECTIVITY_LOGISTIC_HPP
  • Module headers: Named after the module (e.g., selectivity.hpp includes all selectivity functors)

Step-by-Step Guide

This section describes the current workflow for adding a module to FIMS. The examples below use selectivity because it touches the same layers you will update for most work regarding a new module.

Before you start editing FIMS-specific files, it is often helpful to review the broader training materials that cover foundational concepts used by this guide:

  • Intro to C++ explains the C++ syntax and template patterns used throughout inst/include/.
  • Intro to Rcpp explains how Rcpp classes and methods are exposed to R.
  • FIMS User Setup Guide explains how to get a development environment ready for building and testing FIMS.

Documentation expectations

Documentation should start at the same time as implementation, not after the code is finished. As soon as you add a new class or method, plan to document it in the same pull request.

At a minimum, new module work usually needs:

  • Doxygen comments in the C++ headers so the public API and class responsibilities can be discovered.
  • Roxygen comments for any new exported R helpers or user-facing wrappers.
  • Guide updates if the new module changes the recommended development workflow, the files contributors need to touch, or the naming conventions described here.

Step 1: Create the C++ implementation

Class roles in the C++ layer

Before writing code, it helps to know what each file is responsible for:

  • Base class: defines the shared interface and structure that every implementation in the module family must follow.
  • Implementation class: contains the module-specific parameters, math, and reporting behavior.
  • Module (umbrella) header: provides a single include point that exposes all implementations in the module family to the rest of FIMS.

1.1 Create or update the base class

If you are adding a new module in the population dynamics, start with a base class in inst/include/population_dynamics/[category]/functors/[category]_base.hpp.

File: inst/include/population_dynamics/selectivity/functors/selectivity_base.hpp

#ifndef POPULATION_DYNAMICS_SELECTIVITY_BASE_HPP
#define POPULATION_DYNAMICS_SELECTIVITY_BASE_HPP

namespace fims_popdy {

template <typename Type>
struct SelectivityBase {
  uint32_t id;

  SelectivityBase() : id(0) {}
  virtual ~SelectivityBase() {}

  virtual const Type evaluate(const Type &x) = 0;
};

}  // namespace fims_popdy

#endif

Key elements

  • Header guard: prevents duplicate inclusion and should follow the current FIMS naming pattern.
  • typename Type template parameter: keeps the class compatible with the TMB types used during model construction.
  • Pure virtual interface: forces each concrete implementation to provide the required behavior.
  • Shared id field: supports consistent tracking and reporting across module objects.
  • Module namespace: keeps related types grouped with the rest of the population-dynamics code.

1.2 Create or update the implementation class

Add the model-specific class in inst/include/population_dynamics/[category]/functors/[name].hpp. This is where the actual parameter structure, math, and reporting logic live.

File: inst/include/population_dynamics/selectivity/functors/logistic.hpp

#ifndef POPULATION_DYNAMICS_SELECTIVITY_LOGISTIC_HPP
#define POPULATION_DYNAMICS_SELECTIVITY_LOGISTIC_HPP

#include "../../../common/fims_math.hpp"
#include "../../../common/fims_vector.hpp"
#include "selectivity_base.hpp"

namespace fims_popdy {

template <typename Type>
struct LogisticSelectivity : public SelectivityBase<Type> {
  fims::Vector<Type> inflection_point;
  fims::Vector<Type> slope;

  LogisticSelectivity() : SelectivityBase<Type>() {}
  virtual ~LogisticSelectivity() {}

  virtual const Type evaluate(const Type &x) {
    return fims_math::logistic<Type>(inflection_point[0], slope[0], x);
  }

  virtual const Type evaluate(const Type &x, size_t pos) {
    return fims_math::logistic<Type>(inflection_point.get_force_scalar(pos),
                                     slope.get_force_scalar(pos), x);
  }
};

}  // namespace fims_popdy

#endif

Key elements

  • Required includes: pull in the FIMS math helpers, vector type, and the base class.
  • fims::Vector<Type> fields: support both scalar and time-varying parameter values in the C++ layer.
  • evaluate() methods: define the actual model behavior and give you a natural target for unit tests.
  • Optional reporting hooks: many implementations also override report-vector helpers so derived quantities can be surfaced downstream.

See also

1.3 Create or update the umbrella header

Create or update the module header in inst/include/population_dynamics/[category]/[category].hpp so the rest of the framework can include one file instead of several functor headers.

File: inst/include/population_dynamics/selectivity/selectivity.hpp

#ifndef FIMS_POPULATION_DYNAMICS_SELECTIVITY_HPP
#define FIMS_POPULATION_DYNAMICS_SELECTIVITY_HPP

#include "functors/double_logistic.hpp"
#include "functors/logistic.hpp"
#include "functors/selectivity_base.hpp"

#endif /* FIMS_POPULATION_DYNAMICS_SELECTIVITY_HPP */

Key elements

  • Single include point: this is the header other parts of FIMS should include for the whole module family.
  • Explicit functor list: adding a new implementation here is what makes it visible to the rest of the framework.
  • Consistent naming: the umbrella file should match the module family name.

Step 2: Build the Rcpp interface

The Rcpp layer is what connects your C++ class to the R API, the TMB model setup, and the object lifecycle used by FIMS.

2.1 Understand the parameter objects first

Two different vector-like types appear in the module workflow, and they serve different purposes:

  • Parameter in inst/include/interface/rcpp/rcpp_objects/rcpp_interface_base.hpp stores metadata for a single estimable quantity, including:
    • initial_value_m
    • final_value_m
    • estimation_type_m
    • id_m
  • ParameterVector is the Rcpp-facing container that holds Parameter objects and is exposed to R.
  • fims::Vector<Type> is the templated C++ computation container used inside the functor implementation.

This distinction matters because the interface class receives ParameterVector fields from R, then copies only the values into fims::Vector<Type> objects inside add_to_fims_tmb_internal().

A simple R example from the current selectivity tests looks like this:

selectivity <- methods::new(LogisticSelectivity)
selectivity$inflection_point[1]$value <- 10.0
selectivity$inflection_point[1]$estimation_type$set("random_effects")
selectivity$slope[1]$value <- 0.2
selectivity$evaluate(10.0)

See also

2.2 Create or update the interface class

File: inst/include/interface/rcpp/rcpp_objects/rcpp_selectivity.hpp

class SelectivityInterfaceBase : public FIMSRcppInterfaceBase {
 public:
  static uint32_t id_g;
  uint32_t id;
  static std::map<uint32_t, std::shared_ptr<SelectivityInterfaceBase>>
      live_objects;

  SelectivityInterfaceBase() {
    this->id = SelectivityInterfaceBase::id_g++;
  }

  virtual ~SelectivityInterfaceBase() {}
  virtual uint32_t get_id() = 0;
  virtual double evaluate(double x) = 0;
};

uint32_t SelectivityInterfaceBase::id_g = 1;
std::map<uint32_t, std::shared_ptr<SelectivityInterfaceBase>>
    SelectivityInterfaceBase::live_objects;

class LogisticSelectivityInterface : public SelectivityInterfaceBase {
 public:
  ParameterVector inflection_point;
  ParameterVector slope;

  LogisticSelectivityInterface() : SelectivityInterfaceBase() {
    SelectivityInterfaceBase::live_objects[this->id] =
        std::make_shared<LogisticSelectivityInterface>(*this);
    FIMSRcppInterfaceBase::fims_interface_objects.push_back(
        SelectivityInterfaceBase::live_objects[this->id]);
  }

  virtual uint32_t get_id() { return this->id; }

  virtual double evaluate(double x) {
    fims_popdy::LogisticSelectivity<double> logistic_sel;
    logistic_sel.inflection_point.resize(1);
    logistic_sel.inflection_point[0] = this->inflection_point[0].initial_value_m;
    logistic_sel.slope.resize(1);
    logistic_sel.slope[0] = this->slope[0].initial_value_m;
    return logistic_sel.evaluate(x);
  }

  virtual void finalize();
};

Key components

  • Base interface class: manages shared ID behavior and the common interface contract.
  • Concrete interface class: exposes module-specific ParameterVector fields to R.
  • Constructor registration: the object must be stored in both live_objects and fims_interface_objects so it participates in the FIMS lifecycle.
  • evaluate() helper: provides a lightweight way to test the interface from R before building a full TMB model.
  • finalize() method: copies optimized or derived values back out of the Information object after a run.

2.3 Register parameters with TMB in add_to_fims_tmb_internal()

The most important integration work happens in add_to_fims_tmb_internal(). This is the function that copies values out of the R-facing ParameterVector objects, registers estimable parameters, and stores the C++ object in the Information singleton.

template <typename Type>
bool add_to_fims_tmb_internal() {
  std::shared_ptr<fims_info::Information<Type>> info =
      fims_info::Information<Type>::GetInstance();

  std::shared_ptr<fims_popdy::LogisticSelectivity<Type>> selectivity =
      std::make_shared<fims_popdy::LogisticSelectivity<Type>>();

  std::stringstream ss;
  selectivity->id = this->id;
  selectivity->inflection_point.resize(this->inflection_point.size());

  for (size_t i = 0; i < this->inflection_point.size(); i++) {
    selectivity->inflection_point[i] =
        this->inflection_point[i].initial_value_m;

    if (this->inflection_point[i].estimation_type_m.get() ==
        "fixed_effects") {
      ss.str("");
      ss << "Selectivity." << this->id << ".inflection_point."
         << this->inflection_point[i].id_m;
      info->RegisterParameterName(ss.str());
      info->RegisterParameter(selectivity->inflection_point[i]);
    }

    if (this->inflection_point[i].estimation_type_m.get() ==
        "random_effects") {
      ss.str("");
      ss << "Selectivity." << this->id << ".inflection_point."
         << this->inflection_point[i].id_m;
      info->RegisterRandomEffect(selectivity->inflection_point[i]);
      info->RegisterRandomEffectName(ss.str());
    }
  }

  info->variable_map[this->inflection_point.id_m] =
      &(selectivity)->inflection_point;
  info->selectivity_models[selectivity->id] = selectivity;
  return true;
}

Key components

  • Information singleton lookup: gives the interface access to the shared TMB state for the current type.
  • Value copy from ParameterVector to fims::Vector<Type>: moves user-provided parameter values into the computation object.
  • Parameter-name registration: ensures output uses stable, interpretable names.
  • Fixed vs. random effect registration: hooks the parameter into the correct estimation path.
  • variable_map registration: links the Rcpp-side parameter-vector ID to the actual C++ storage; this is easy to miss and is required for correct wiring.
  • Module registration in info->*_models: makes the model object available to the rest of FIMS.

The current workflow instantiates two TMB types here:

virtual bool add_to_fims_tmb() {
  this->add_to_fims_tmb_internal<TMB_FIMS_REAL_TYPE>();
  this->add_to_fims_tmb_internal<TMBAD_FIMS_TYPE>();
  return true;
}

2.4 Extract results in finalize()

The finalize() method is where the interface object pulls values back out of the Information singleton after optimization or reporting.

A good finalize() implementation usually does the following:

  • checks whether the object has already been finalized,
  • looks up the concrete module in the appropriate info->*_models map,
  • copies final_value_m from the C++ object for estimable parameters, and
  • leaves final_value_m equal to initial_value_m for constant parameters.

See also

Step 3: Register the module with the R API

3.1 Update src/fims_modules.hpp

Add the Rcpp interface header and expose the class in RCPP_MODULE(fims).

File: src/fims_modules.hpp

Rcpp::class_<LogisticSelectivityInterface>(
    "LogisticSelectivity",
    "See https://noaa-fims.github.io/doxygen/"
    "classLogisticSelectivityInterface.html.")
    .constructor()
    .field("inflection_point",
           &LogisticSelectivityInterface::inflection_point)
    .field("slope", &LogisticSelectivityInterface::slope)
    .method("get_id", &LogisticSelectivityInterface::get_id)
    .method("evaluate", &LogisticSelectivityInterface::evaluate);

Key points

  • R class name: the first string is the class name users call with methods::new().
  • Documentation string: current dev code often points to the Doxygen page for the interface class.
  • Fields and methods: these are the parts of the interface that become visible in R.

3.2 Update R/FIMS-package.R

Export the class so it is visible from R and rebuild the namespace:

#' @export LogisticSelectivity

Then run devtools::document().

Step 4: Connect the module to the current R workflow

The R wrapper functions use a two-stage workflow before initialize_fims() constructs the TMB-ready objects:

  1. create_default_configurations() creates the configuration tibble that declares which module family and module type should be used.
  2. create_default_parameters() turns those configurations into the parameter tibble that initialize_module() reads.

4.1 Update the configuration and default-parameter path

Relevant files

If you add a new module type that users should be able to request by default, update:

In the current code, top-level default parameter creation is routed through helpers such as:

  • create_default_fleet()
  • create_default_recruitment()
  • create_default_maturity()
  • create_default_Population()

For a new selectivity or data-module option, you will often update an existing helper rather than add a brand new top-level function.

4.2 Update initialize_modules.R

Relevant file: R/initialize_modules.R

initialize_module() currently builds the Rcpp class name by combining module_type and module_name:

module_class_name <- module_input |>
  dplyr::mutate(
    temp_name = paste0(
      dplyr::coalesce(module_type, ""),
      dplyr::coalesce(module_name, "")
    )
  ) |>
  dplyr::pull(temp_name) |>
  unique()

module_class <- get(module_class_name)
module <- methods::new(module_class)

It then populates each field by either:

  • setting scalar/shared values such as n_ages or n_years,
  • filling RealVector fields such as ages or weights, or
  • calling set_param_vector() for ParameterVector fields.

That means a new module type typically needs:

  • a valid module_type value in the configuration/parameter tibble,
  • an exported Rcpp class name that matches paste0(module_type, module_name), and
  • an initialization path inside initialize_fims() if the new module needs explicit linking to other modules.

4.3 Keep the full run path in mind

The full user-facing workflow in current dev code is:

parameters <- data_4_model |>
  create_default_configurations() |>
  create_default_parameters(data = data_4_model)

input <- parameters |>
  initialize_fims(data = data_4_model)

If your new module breaks any part of that pipeline, update the corresponding configuration, parameter, initialization, and registration code together.

Step 5: Add targeted tests

Testing should make it clear both why the module works and where it works.

  • C++ gtests validate the core math, logic, and behavior of the underlying functors.
  • R testthat tests validate the Rcpp interface, object wiring, exported methods, and user-facing behavior.

A good new-module test set usually covers:

  • parameter setup and resizing,
  • a known-value check against an expected result,
  • edge or boundary behavior where the module has meaningful limits,
  • basic lifecycle behavior such as object creation, IDs, and repeated instantiation,
  • integration expectations such as the presence of fields or methods used by initialize_*() helpers.

5.1 C++ Google tests

Use the gtest layer to test the computation object directly. New tests can be initialized and formatted for you by running the R function FIMS::use_gtest_template(). For the standard contributor workflow for building and running the gtest suite, see Standard contributor checks.

Typical targets include:

  • the expected output for a simple parameter/value combination,
  • time-varying behavior if get_force_scalar() is relevant,
  • edge cases that should stay numerically stable.

5.2 R testthat tests

Use testthat to exercise the Rcpp-facing class and the R initialization helpers. New tests can be initialized and formatted for you by running the R function FIMS::use_testthat_template(). For the standard contributor workflow for running testthat, formatting, documentation, and package checks, see Standard contributor checks.

Current selectivity tests illustrate the main patterns:

  • create the object with methods::new(),
  • assign value and estimation_type,
  • verify get_id() and evaluate(),
  • verify repeated object creation behaves as expected.

Current initialize_*() tests also check that the returned object is an S4 object and that it exposes the expected methods in its reference-class definition.

See also

Step 6: Finalize the documentation and build checks

By the time the implementation is working, most of the documentation should already exist. Use this step to make sure the module-specific documentation is complete and then follow Standard contributor checks for the routine contributor tasks such as building Doxygen, running tests, formatting code, spell-checking, and package checks.

6.1 Module-specific documentation

Add or update Doxygen comments in the header files you touched so future developers can discover:

  • the role of the base class,
  • what each implementation computes,
  • what each parameter represents,
  • how the interface methods participate in model setup.

Document any new exported R helpers or user-facing module classes, and update this guide plus CONTRIBUTING.md in the same pull request whenever your work changes the recommended module workflow.

Files to modify: complete checklist

When adding a new module or a new module type, you will usually touch several layers at once.

Required or commonly required files

Validation checklist

For the routine contributor validation steps, use Standard contributor checks. In addition, make sure you update this guide and CONTRIBUTING.md whenever your new module changes the documented workflow.

Common patterns and best practices

Parameter handling

Keep the current distinction clear:

  • use ParameterVector in the Rcpp interface layer,
  • use fims::Vector<Type> in the C++ implementation layer.
fims::Vector<Type> my_parameter;
Type value = my_parameter[0];
Type value_at_time = my_parameter.get_force_scalar(time_index);

ID management and lifecycle

Every interface class needs a stable ID and should participate in the current lifecycle pattern.

static uint32_t id_g;
uint32_t id;

MyInterface() {
  this->id = MyInterface::id_g++;
}

Also make sure the constructor stores the object in both the module-specific live_objects map and FIMSRcppInterfaceBase::fims_interface_objects.

TMB integration

Register parameters by estimation type and remember to add the variable-map entry.

if (this->param[i].estimation_type_m.get() == "fixed_effects") {
  info->RegisterParameter(module_obj->param[i]);
  info->RegisterParameterName(param_name);
}

if (this->param[i].estimation_type_m.get() == "random_effects") {
  info->RegisterRandomEffect(module_obj->param[i]);
  info->RegisterRandomEffectName(param_name);
}

info->variable_map[this->param.id_m] = &(module_obj)->param;

Namespace organization

  • Use fims_popdy for population-dynamics modules.
  • Use fims_info for shared information and TMB integration.
  • Use fims_math for mathematical helper functions.
  • Use fims for common utilities.

Troubleshooting

Common issues

“undefined symbol” errors when loading the package

Cause: the interface was not fully exposed in src/fims_modules.hpp or the corresponding header was not included.

Solution: verify the Rcpp::class_<...>() entry, the header include, and the matching export in R/FIMS-package.R.

Parameters are not being estimated

Cause: add_to_fims_tmb_internal() did not register the parameter name, estimation type, or variable_map entry correctly.

Solution: compare your implementation against the current selectivity or maturity interface classes.

Values are not appearing back in R after a run

Cause: finalize() was not implemented or is not copying values back from the Information object.

Solution: follow the current finalize() pattern in the existing Rcpp interface classes.

The module initializes in R but is missing expected fields

Cause: the configuration/parameter tibble, exported class name, and initialize_module() class lookup are out of sync.

Solution: confirm that paste0(module_type, module_name) resolves to the class you exported in src/fims_modules.hpp.

Getting help

If you encounter issues:

  1. Check the FIMS Discussion Board for similar questions.
  2. Review the closest existing module implementation.
  3. File an Issue with a minimal reproducible example.

Summary

Adding a new module in the current dev branch usually means coordinating updates across:

  1. C++ functors in inst/include/
  2. Rcpp interface and TMB registration in inst/include/interface/rcpp/rcpp_objects/
  3. R exposure in src/fims_modules.hpp and R/FIMS-package.R
  4. R configuration, parameter, and initialization workflow in R/create_default_configurations.R, R/create_default_parameters.R, and R/initialize_modules.R
  5. Tests and documentation that explain and validate the new behavior

Keeping those layers synchronized is the best way to avoid the outdated template problem that motivated this guide in the first place.


Questions or suggestions for improving this guide? Please open an issue or discussion on the FIMS GitHub repository.