Skip to content

Add partition spec and index layout infrastructure for sex strata#1581

Open
szu-yun-ko wants to merge 11 commits into
NOAA-FIMS:mainfrom
szu-yun-ko:feat/partition-infra
Open

Add partition spec and index layout infrastructure for sex strata#1581
szu-yun-ko wants to merge 11 commits into
NOAA-FIMS:mainfrom
szu-yun-ko:feat/partition-infra

Conversation

@szu-yun-ko

@szu-yun-ko szu-yun-ko commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What is the feature?

This PR adds the first layer of partition infrastructure for population dynamics. It addresses parts 1–2 of issue #1557 only (representation of sex and indexing layout). Model dynamics, partitioned derived quantities, and likelihood wiring are intentionally deferred to follow-up PRs.

How have you implemented the solution?

  • Flesh out subpopulation.hpp with Axis, GroupSelector, PartitionSpec, IndexLayout, and MakeDefaultSexPartitionSpec() (female/male sex axis).
  • Add partition_spec and index_layout members to Population.
  • Initialize the default sex partition and index layout for each population in CatchAtAge::Initialize().
  • Add gtests for partition/indexing utilities and for partition initialization via CatchAtAge::Initialize().

Design Notes

  • Partition types live in subpopulation.hpp, matching existing documentation that subpopulations represent generic partitions.
  • IndexLayout::i_age_year() preserves the existing pooled (year, age) layout; i_stratum_age_year() adds stratum as the leading dimension for future partitioned containers (n_strata × n_years × n_ages).
  • Sex is the first (and only) axis for now; GroupSelector supports wildcards so the same API can extend to additional axes (e.g., area) later.

Does the PR impact any other area of the project, maybe another repo?

  • No.

Instructions for code reviewer

👋Hello reviewer👋, thank you for taking the time to review this PR!

  • Please use this checklist during your review, checking off items that you have verified are complete but feel free to skip over items that are not relevant!
  • See the GitHub documentation for how to comment on a PR to indicate where you have questions or changes are needed before approving the PR.
  • Please use standard conventional messages for both commit messages and comments
  • PR reviews are a great way to learn so feel free to share your tips and tricks. However, when suggesting changes to the PR that are optional please include nit: (for nitpicking) as the comment type. For example, nit: I prefer using a data.frame() instead of a matrix because ...
  • Engage with the developer. Make it clear when the PR is approved by selecting the approved status, and potentially commenting on the PR with something like This PR is now ready to be merged.

Checklist

  • The code is well-designed
  • The code is designed well for both users and developers
  • Code coverage remains high- [ ] Comments are clear, useful, and explain why instead of what
  • Code is appropriately documented (doxygen and roxygen)

@github-actions

Copy link
Copy Markdown
Contributor

🎨 Chore: code formatting workflow

Our automated workflows cannot run on forks because of permission issues, and thus, we ask that you run the following code locally and push any changes that are created to your feature branch. You will only be reminded of this once per PR. Thank you!

Format C++ code

  1. Install clang-format version 18.0.0
  2. Run the following command from the repository root:
    clang-format -i --style="{BasedOnStyle: Google, SortIncludes: false}" $(find ./inst/include ./src ./tests/gtest -name "*.hpp" -o -name "*.cpp")

Format R code

  1. Install {styler} and {roxygen2}
  2. Run the following commands in R from the repository root:
styler::style_pkg() # Style R code
roxygen2::roxygenise() # Update documentation
styler::style_pkg() # Style R code again
roxygen2::roxygenise() # Update documentation again
usethis::use_tidy_description() # Style DESCRIPTION file

Push changes

  1. Commit the formatting with a commit message of "Chore: format feature branch"
  2. Push to your fork

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.16599% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.69%. Comparing base (397cea9) to head (409511f).

Files with missing lines Patch % Lines
...e/population_dynamics/population/subpopulation.hpp 93.20% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1581      +/-   ##
==========================================
+ Coverage   87.32%   87.69%   +0.36%     
==========================================
  Files         100      103       +3     
  Lines        9060     9307     +247     
  Branches      539      596      +57     
==========================================
+ Hits         7912     8162     +250     
+ Misses       1111     1105       -6     
- Partials       37       40       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@szu-yun-ko szu-yun-ko mentioned this pull request Jun 26, 2026
13 tasks
@szu-yun-ko

Copy link
Copy Markdown
Contributor Author

@awilnoaa @nathanvaughan-NOAA @Andrea-Havron-NOAA This PR should be ready for an initial review. This PR addresses the tasks mentioned in issue #1557, after the review comments are addressed, I will continue tackling tasks in issue #1587 in this same PR.

Partition representation:

  • Axis: a partition dimension (e.g. sex) with a vector of named levels. The size of an Axis is inferred from the length of its level vector.
  • GroupSelector: a partial selection pattern that can be mapped to one stratum or many strata, where each stratum is a combination of a level from each axis.
  • PartitionSpec: the schema of the partition grid, it includes a vector of axes (each having its own name and levels). Member functions include n_strata(), stratum_id(), levels_from_stratum(), and expand_group_to_strata(). These member functions are primarily used for bridging user-facing partition concepts and flat storage indices.
    • A stratum is the atomic storage unit (one level chosen on every axis).
    • A group (GroupSelector) is a looser selection that may match one or many strata.
    • expand_group_to_strata() resolves a group to the strata that should be read or written.
    • stratum_id() and levels_from_stratum() convert between those strata and the integer indices that appear in partitioned vectors.
    • n_strata() sizes those containers (e.g. n_strata × n_years × n_ages for fleet at-age DQs).
  • IndexLayout: provides folded index helpers for pooled (year, age) and partitioned (stratum, year, age) storage.
  • MakeDefaultSexPartitionSpec(): a function for setting up the current (default) partition, which is to include one axis (sex) with two levels (female and male).

Wiring into population:

  • Population gains partition_spec and index_layout members (population.hpp).
  • CatchAtAge::Initialize() sets the default sex partition and populates index_layout from n_years, n_ages, and partition_spec.n_strata() (catch_at_age.hpp).

Partitioned derived-quantity storage:

  • Registers four fleet at-age *_by_partition DQs in rcpp_models.hpp:
    • landings_numbers_at_age_by_partition
    • landings_weight_at_age_by_partition
    • index_numbers_at_age_by_partition
    • index_weight_at_age_by_partition
  • Each is sized n_strata × n_years × n_ages with DimensionInfo {"n_strata", "n_years", "n_ages"}
  • Note that in this PR we implement the storage only. These containers are not filled during Calculate* yet (vectors are allocated and zeroed on Prepare()). This will be addressed later on (ref: issue Partition demand configuration #1587) along with the demand-based calculation routing logic.

Tests added:

GTest

  • test_population_Subpopulation.cpp: default sex specifications, stratum encoding and decoding, stratum group conversion (include wildcard and single-stratum cases), folded indices.
  • test_population_CatchAtAge_InitializePartition.cpp: Initialize() sets partition state on Population.
  • test_fleet_Fleet_InitializePrepare.cpp: Check that for each fleet, the four *_by_partition vectors have size n_strata × n_years × n_ages, while pooled at-age DQs remain n_years × n_ages. Also check that after Prepare(), the same partitioned vectors are all zeros. This confirms storage is allocated and reset even though Calculate* does not fill partitioned values yet.

testthat

  • Column names: expected_colnames now includes stratum_i, the R-side name for the n_strata dimension after reshaping JSON model output.
  • Regenerated because registering partitioned fleet DQs adds rows to get_estimates() on the integration fixtures (deterministic and estimation runs).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the first layer of partition/indexing infrastructure to support sex strata in population dynamics (as groundwork for future partitioned state/derived quantities), and exposes the new stratum dimension through the R output reshaping and related tests.

Changes:

  • Introduces partition primitives (Axis, GroupSelector, PartitionSpec) and indexing helpers (IndexLayout) plus a default sex partition spec.
  • Stores partition_spec and index_layout on Population and initializes default sex partition/index layout in CatchAtAge::Initialize().
  • Adds partitioned fleet derived-quantity containers (storage only), updates R output dimension naming (stratum_i), and updates/extends gtests + testthat snapshots.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/testthat/test-get_estimates.R Updates expected estimate columns to include stratum_i.
tests/testthat/_snaps/get_estimates.md Updates snapshot output for new column and row/col counts.
tests/gtest/test_population_test_fixture.hpp Initializes partitioned fleet age-based derived quantity vectors in fixtures.
tests/gtest/test_population_Subpopulation.cpp Adds unit tests for PartitionSpec and IndexLayout.
tests/gtest/test_population_CatchAtAge_InitializePartition.cpp Adds fixture-based test for default partition/index layout initialization in CatchAtAge::Initialize().
tests/gtest/test_fleet_Fleet_InitializePrepare.cpp Extends fleet DQ size/reset expectations to include partitioned vectors.
tests/gtest/CMakeLists.txt Registers the two new gtest executables.
R/reshape_output.R Maps n_strata dimension header to stratum_i for tidy output.
inst/include/population_dynamics/population/subpopulation.hpp Implements partition spec structures, indexing layout, and default sex partition helper.
inst/include/population_dynamics/population/population.hpp Adds partition_spec and index_layout members to Population.
inst/include/models/functors/catch_at_age.hpp Initializes default sex partition spec and index layout during model initialization.
inst/include/interface/rcpp/rcpp_objects/rcpp_models.hpp Adds partitioned fleet DQ vectors + dimension metadata for R interface output.
Comments suppressed due to low confidence (1)

inst/include/interface/rcpp/rcpp_objects/rcpp_models.hpp:1276

  • index_weight_at_age (and its DimensionInfo) are initialized a second time immediately after index_weight_at_age_by_partition. This duplicate block is redundant and likely accidental; it should be removed to avoid confusion and to ensure future edits don’t update only one copy.
      derived_quantities["index_weight_at_age"] = fims::Vector<Type>(
          fleet_interface->n_years.get() * fleet_interface->n_ages.get());
      derived_quantities_dim_info["index_weight_at_age"] =
          fims_popdy::DimensionInfo(
              "index_weight_at_age",

Comment thread inst/include/population_dynamics/population/subpopulation.hpp
Comment thread inst/include/population_dynamics/population/subpopulation.hpp
Comment thread inst/include/population_dynamics/population/subpopulation.hpp
@szu-yun-ko
szu-yun-ko force-pushed the feat/partition-infra branch 2 times, most recently from 88bc528 to 006edbf Compare July 5, 2026 03:56
@szu-yun-ko
szu-yun-ko force-pushed the feat/partition-infra branch from 364d010 to a7e0ab9 Compare July 9, 2026 00:01
@szu-yun-ko

Copy link
Copy Markdown
Contributor Author

This PR already adds partition structure (PartitionSpec, IndexLayout) and storage (fleet *_by_partition DQs). I've addressed the Copilot comments and added the split math we’ll need when writing into those partitioned containers (but Calculate* does not call any of this yet). The wiring will be addressed after the infrastructure is reviewed so that I can make sure I'm not building on unsolid grounds. The two helpers added in the latest commits include:

SexStratumSplitFactors(spec, proportion_female):

  • For the default sex-only partition only (MakeDefaultSexPartitionSpec(), 2 strata). It returns {p_female, 1 - p_female}. proportion_female comes from Population::proportion_female.get_force_scalar(age) at evaluation time and it is not stored on PartitionSpec. It's not wired at the moment but see below for a sketch of how it will work later on.

PartitionSpec::stratum_split_factor(stratum, split_factors):

  • This is a generic lookup for split_factors[stratum] with validation. split_factors is passed in by the caller and it is not a field on PartitionSpec. The spec defines how many strata exist, the caller supplies how much goes to each.
// Pooled write (unchanged)
const Type pooled = (Fmort * f_multiplier * selectivity) / Z
                    * N_aa * (1 - exp(-Z));
fdq_["landings_numbers_at_age"][i_age_year] += pooled;

// Partitioned write (future; sex-only Model 1)
const auto split_factors = SexStratumSplitFactors(
    population->partition_spec,
    population->proportion_female.get_force_scalar(age));

for (size_t s = 0; s < population->partition_spec.n_strata(); ++s) {
  const size_t i_part =
      population->index_layout.i_stratum_age_year(s, year, age);
  fdq_["landings_numbers_at_age_by_partition"][i_part] +=
      pooled * population->partition_spec.stratum_split_factor(s, split_factors);
}

The changes in this PR so far addresses all tasks mentioned in issue #1557 and I'll proceed to work on #1587 after the current work is reviewed.

}

/**
* @brief Encode one level per axis as a flat stratum index.

@Andrea-Havron-NOAA Andrea-Havron-NOAA Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a more detailed description here using the example of male/female for sex. Do the same for all functions in subpopulations.hpp that are missing a @details section.

&derived_quantities["landings_numbers_at_age"];

// partitioned landings and index at age (storage only; not filled yet)
derived_quantities["landings_numbers_at_age_by_partition"] =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like any of these partitioned derived quantities are being pushed back to info->variable_map. In order to do that I think they will also need to be initialized as VariableVectors in the fleet and population .hpp files. You could look at how their respective non-partitioned quantities are handled to figure out which files are appropriate.

* @details Pooled quantities use i_age_year(). Partitioned quantities add
* stratum as the leading dimension via i_stratum_age_year().
*/
struct IndexLayout {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code here looks to be limited to a single new axis layer with hard coded years/ages axes. Do you have a plan for extending this to nested axes such as sex and area?

@szu-yun-ko szu-yun-ko Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question! Multi-axis nesting (e.g. sex × area) is handled by PartitionSpec, not by nesting more dimensions into IndexLayout. PartitionSpec holds a vector of axes, and n_strata() is the product of each axis’s level count (ref. subpopulation.hpp around line 47). Adding area (or season) increases n_strata and it does not require nested helpers like i_sex_area_year_age. The current wiring pulls n_strata from the spec in CatchAtAge::Initialize() (ref. catch_at_age.hpp around line 160):

      this->populations[p]->partition_spec = MakeDefaultSexPartitionSpec();
      this->populations[p]->index_layout.n_years = this->populations[p]->n_years;
      this->populations[p]->index_layout.n_ages = this->populations[p]->n_ages;
      this->populations[p]->index_layout.n_strata =
          this->populations[p]->partition_spec.n_strata();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants