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:
-
C++ Implementation Layer
(
inst/include/): Core mathematical and computational logic -
R Interface Layer
(
inst/include/interface/rcpp/): Bridges C++ and R -
R Wrapper Layer (
R/): R functions for initialization and data management -
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(notclass 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]_HPPformat- Example:
FIMS_POPULATION_DYNAMICS_SELECTIVITY_LOGISTIC_HPP
- Example:
-
Module headers: Named after the module (e.g.,
selectivity.hppincludes 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.
Related documentation
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
#endifKey elements
- Header guard: prevents duplicate inclusion and should follow the current FIMS naming pattern.
-
typename Typetemplate 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
idfield: 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
#endifKey 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:
-
Parameterininst/include/interface/rcpp/rcpp_objects/rcpp_interface_base.hppstores metadata for a single estimable quantity, including:initial_value_mfinal_value_mestimation_type_mid_m
-
ParameterVectoris the Rcpp-facing container that holdsParameterobjects 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
ParameterVectorfields to R. -
Constructor registration: the object must be stored
in both
live_objectsandfims_interface_objectsso 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 theInformationobject 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
ParameterVectortofims::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_mapregistration: 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:
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->*_modelsmap, - copies
final_value_mfrom the C++ object for estimable parameters, and - leaves
final_value_mequal toinitial_value_mfor 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
devcode 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 LogisticSelectivityThen 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:
-
create_default_configurations()creates the configuration tibble that declares which module family and module type should be used. -
create_default_parameters()turns those configurations into the parameter tibble thatinitialize_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:
- the configuration templates in
create_default_configurations(), and/or - the default-parameter helpers used by
create_default_parameters().
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_agesorn_years, - filling
RealVectorfields such asagesorweights, or - calling
set_param_vector()forParameterVectorfields.
That means a new module type typically needs:
- a valid
module_typevalue 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
valueandestimation_type, - verify
get_id()andevaluate(), - 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.
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
ParameterVectorin the Rcpp interface layer, - use
fims::Vector<Type>in the C++ implementation layer.
ID management and lifecycle
Every interface class needs a stable ID and should participate in the current lifecycle pattern.
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;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.
Getting help
If you encounter issues:
- Check the FIMS Discussion Board for similar questions.
- Review the closest existing module implementation.
- File an Issue with a minimal reproducible example.
Examples and references
Code examples
Study these existing modules as templates:
- Selectivity: inst/include/population_dynamics/selectivity/
- Maturity: inst/include/population_dynamics/maturity/
- Recruitment: inst/include/population_dynamics/recruitment/
- R parameter workflow:
Summary
Adding a new module in the current dev branch usually
means coordinating updates across:
-
C++ functors in
inst/include/ -
Rcpp interface and TMB registration in
inst/include/interface/rcpp/rcpp_objects/ -
R exposure in
src/fims_modules.hppandR/FIMS-package.R -
R configuration, parameter, and initialization
workflow in
R/create_default_configurations.R,R/create_default_parameters.R, andR/initialize_modules.R - 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.
