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.
This guide is intended for:
Before starting, ensure you have:
FIMS follows a modular architecture with clear separation between:
inst/include/): Core mathematical and computational
logicinst/include/interface/rcpp/): Bridges C++ and RR/): R functions for
initialization and data managementsrc/): Exposes
modules to R via RcppFIMS/
├── 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
FIMS follows consistent naming conventions to maintain code readability and organization:
Following the Google C++ Style Guide:
LogisticSelectivity,
BevertonHoltRecruitment)fims_popdy, fims_info)inflection_point,
log_devs)typename Type
(not class T)logistic.hpp, selectivity_base.hpp)Following the tidyverse style guide:
initialize_selectivity, m_agecomp)LogisticSelectivity)fleet_name, module_input)Base (e.g.,
SelectivityBase, RecruitmentBase)Interface (e.g.,
LogisticSelectivityInterface)FIMS_[PATH]_[FILENAME]_HPP format
FIMS_POPULATION_DYNAMICS_SELECTIVITY_LOGISTIC_HPPselectivity.hpp includes all selectivity functors)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.
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:
Before writing code, it helps to know what each file is responsible for:
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
typename Type template parameter:
keeps the class compatible with the TMB types used during model
construction.id field: supports consistent
tracking and reporting across module objects.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
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.See also
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
The Rcpp layer is what connects your C++ class to the R API, the TMB model setup, and the object lifecycle used by FIMS.
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_mfinal_value_mestimation_type_mid_mParameterVector 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
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
ParameterVector fields to R.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.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
ParameterVector to
fims::Vector<Type>: moves user-provided
parameter values into the computation object.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.info->*_models: makes the model object
available to the rest of FIMS.The current workflow instantiates two TMB types here:
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:
info->*_models map,final_value_m from the C++ object for estimable
parameters, andfinal_value_m equal to
initial_value_m for constant parameters.See also
src/fims_modules.hppAdd 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
methods::new().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 that
initialize_module() reads.Relevant files
If you add a new module type that users should be able to request by default, update:
create_default_configurations(), and/orcreate_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.
initialize_modules.RRelevant 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:
n_ages or
n_years,RealVector fields such as ages or
weights, orset_param_vector() for
ParameterVector fields.That means a new module type typically needs:
module_type value in the
configuration/parameter tibble,paste0(module_type, module_name), andinitialize_fims() if the
new module needs explicit linking to other modules.The full user-facing workflow in 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.
Testing should make it clear both why the module works and where it works.
A good new-module test set usually covers:
initialize_*() helpers.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:
get_force_scalar() is
relevant,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:
methods::new(),value and estimation_type,get_id() and evaluate(),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
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.
Add or update Doxygen comments in the header files you touched so future developers can discover:
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.
When adding a new module or a new module type, you will usually touch several layers at once.
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.
Keep the current distinction clear:
ParameterVector in the Rcpp
interface layer,fims::Vector<Type> in the
C++ implementation layer.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.
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;fims_popdy for population-dynamics modules.fims_info for shared information and TMB
integration.fims_math for mathematical helper functions.fims for common utilities.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.
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.
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.
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.
If you encounter issues:
Study these existing modules as templates:
Adding a new module usually means coordinating updates across:
inst/include/inst/include/interface/rcpp/rcpp_objects/src/fims_modules.hpp and
R/FIMS-package.RR/create_default_configurations.R,
R/create_default_parameters.R, and
R/initialize_modules.RKeeping 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.