Skip to content

Refactor contact_matrix() to use pipeline infrastructure#290

Merged
sbfnk merged 28 commits intomainfrom
pipeline-workflow
Feb 19, 2026
Merged

Refactor contact_matrix() to use pipeline infrastructure#290
sbfnk merged 28 commits intomainfrom
pipeline-workflow

Conversation

@sbfnk
Copy link
Collaborator

@sbfnk sbfnk commented Feb 19, 2026

Summary

Fixes #131

This is a follow-up to #287 that refactors contact_matrix() to use the pipeline infrastructure internally.

Changes

  • contact_matrix() now uses weigh() for all weighting (day-of-week, age, user-defined weights)
  • Shared helpers extracted: warn_multiple_observations(), normalise_weights()
  • compute_matrix() gains weight_threshold parameter to match contact_matrix()
  • Cyclomatic complexity reduced from 54 to 50

Bug fix

Fixed bug where participants with NA dayofweek were incorrectly weighted as weekend days. They now receive an average weight across all days.

Summary by CodeRabbit

  • New Features

    • Added weight_threshold parameter to compute_matrix() to cap extreme weights before normalization.
  • Bug Fixes

    • Participants with missing day-of-week data now weighted by average across all days instead of weekends only.
  • API Changes

    • Renamed copy.contact_survey() to copy_survey() for consistency.

sbfnk and others added 16 commits February 10, 2026 19:37
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
…rg, tighten tolerance

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
…ompute_matrix()

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
…nctions

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

This PR refactors the contact matrix computation pipeline by consolidating weight normalization logic into reusable helpers, replacing the S3 method copy.contact_survey with a public copy_survey function, and unifying age and weight handling through a sequence of dedicated pipeline functions (assign_age_groups, weigh, normalise_weights).

Changes

Cohort / File(s) Summary
Namespace & Exports
NAMESPACE
Removed S3method for copy.contact_survey and data.table import; reflects shift from S3 method to public function.
Helper Functions
R/contact-matrix-utils.R
Added warn_multiple_observations() to warn when survey has multiple rows per participant; introduced normalise_weights() to consolidate weight normalization with optional threshold, replacing previous truncate_renormalise_weights logic.
Copy/Subset Operations
R/subset-contact-survey.R, R/deprecated.R, R/weigh.R
Replaced S3 method copy.contact_survey() with public function copy_survey(survey) for deep-copying survey data; updated all call sites accordingly.
Core Matrix Computation
R/contact_matrix.R, R/compute-matrix.R
Refactored contact_matrix to use centralized warn_multiple_observations(), assign_age_groups, agegroups_to_limits pipeline, and unified weigh function for all weight types (day-of-week, age, user-defined); updated compute_matrix with weight_threshold parameter and normalise_weights integration.
Global Variables & Configuration
R/globals.R
Realigned global variable declarations to reflect shift from is.weekday to dayofweek; restructured weight-related contexts to align with new normalise_weights helper.
Documentation
man/compute_matrix.Rd, man/normalise_weights.Rd, man/warn_multiple_observations.Rd, man/copy_survey.Rd
Added documentation for new/renamed public functions; updated copy_survey signature and keywords.
Tests & Snapshots
tests/testthat/test-matrix.r, tests/testthat/_snaps/matrix.md
Updated test expectations for return.part.weights length (4→6) reflecting new dayofweek weighting structure; updated numeric snapshot values in matrix output.
Release Notes
NEWS.md
Documented new weight_threshold option, refactored contact_matrix weighting approach, bug fix for NA dayofweek handling, and example syntax improvements.

Sequence Diagram(s)

sequenceDiagram
    participant Survey as Survey Data
    participant AgeGroup as assign_age_groups()
    participant Filter as apply_data_filter()
    participant Limits as agegroups_to_limits()
    participant Weigh as weigh()
    participant Normalise as normalise_weights()
    participant Compute as compute_matrix()

    Survey->>AgeGroup: survey with age data
    AgeGroup->>Filter: survey with age.group assigned
    Filter->>Limits: filtered survey
    Limits->>Weigh: age_limits derived
    Weigh->>Weigh: apply day-of-week weights
    Weigh->>Weigh: apply age weights
    Weigh->>Weigh: apply user-defined weights
    Weigh->>Normalise: weighted participants
    Normalise->>Normalise: truncate extreme weights (threshold)
    Normalise->>Normalise: re-normalise by age.group
    Normalise->>Compute: normalised weights
    Compute->>Compute: aggregate contacts by age groups
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 The weights are gathered, thresholds trim the wild,
Normalised and grouped, so orderly and styled,
Pipeline flows from age groups through to matrices clean,
Copy_survey hops where S3 methods have been,
Helpers atomised, refactored with care! 🌿✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: refactoring contact_matrix() to use pipeline infrastructure. This is the primary focus of the changeset.
Linked Issues check ✅ Passed The PR successfully addresses issue #131 by decomposing contact_matrix() into smaller functions (warn_multiple_observations, normalise_weights), centralizing weighting logic via weigh(), and reducing cyclomatic complexity.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives: refactoring contact_matrix to use pipeline infrastructure, extracting helper functions, and addressing the NA dayofweek bug. The replacement of copy() with copy_survey() is directly related to the refactoring scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pipeline-workflow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sbfnk sbfnk force-pushed the pipeline-workflow branch from 8453a30 to 8cab50c Compare February 19, 2026 11:20
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (11)
R/contact-matrix-utils.R (1)

732-755: normalise_weights duplicates logic in truncate_renormalise_weights and participant_weights.

normalise_weights() effectively subsumes the manual normalisation at line 784 and the truncate_renormalise_weights() call at line 787 in participant_weights(). Consider consolidating the legacy path to use normalise_weights(participants, by = "age.group", threshold = weight_threshold) and removing truncate_renormalise_weights.

♻️ Suggested consolidation in participant_weights()
-  # post-stratification weight standardisation: by age.group
-  participants[, weight := weight / sum(weight) * .N, by = age.group]
-
-  # option to truncate overall participant weights (if not NULL or NA)
-  participants <- truncate_renormalise_weights(participants, weight_threshold)
+  # post-stratification weight standardisation (with optional truncation)
+  participants <- normalise_weights(participants, by = "age.group", threshold = weight_threshold)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/contact-matrix-utils.R` around lines 732 - 755, normalise_weights
duplicates the truncation and re-normalisation logic found in
truncate_renormalise_weights and the manual renormalisation in
participant_weights; replace the legacy path by calling
normalise_weights(participants, by = "age.group", threshold = weight_threshold)
from participant_weights (instead of the manual weight clipping and the
truncate_renormalise_weights call) so the same by/threshold behavior is used,
and then remove the now-redundant truncate_renormalise_weights function and the
manual renormalisation block to avoid duplication.
tests/testthat/test-subset-contact-survey.R (1)

43-47: expect_named with exact field list is fragile if the polymod dataset gains new fields.

expect_named(result, c("participants", "contacts", "reference")) asserts an exact match. The test on lines 90–97 already verifies that extra fields are preserved after filtering, which implicitly tests the subsetting contract. A more forward-compatible assertion here would only check that the mandatory fields are present:

-  expect_named(result, c("participants", "contacts", "reference"))
+  expect_true(all(c("participants", "contacts", "reference") %in% names(result)))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/testthat/test-subset-contact-survey.R` around lines 43 - 47, The test
currently asserts an exact set of names with expect_named(result,
c("participants", "contacts", "reference")), which is fragile; change the
assertion to only require the mandatory fields be present after subsetting (for
example assert that "participants", "contacts" and "reference" are all in
names(result) or use a membership-based expectation) while leaving other fields
allowable so the test continues to pass if polymod gains extra columns.
tests/testthat/test-weigh.R (1)

193-204: # nolint end placement is inside the test_that block, which looks like a formatting artefact.

The # nolint start: nonportable_path_linter on line 193 is there to suppress a false-positive on the / in "mismatched target/groups lengths". Ending the suppression on line 195 (inside the { block) is syntactically correct but visually inconsistent. Consider ending it on the same line as the start, or after the closing }:

♻️ Proposed cleanup
-# nolint start: nonportable_path_linter
-test_that("weigh() errors for mismatched target/groups lengths", {
-  # nolint end
+# nolint start: nonportable_path_linter
+test_that("weigh() errors for mismatched target/groups lengths", { # nolint end
   expect_error(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/testthat/test-weigh.R` around lines 193 - 204, The inline nolint block
around the test_that("weigh() errors for mismatched target/groups lengths", {
... }) is split oddly with "# nolint end" inside the test body; move the "#
nolint end" so the suppression is visually consistent — either put it on the
same line as the start comment (after "nonportable_path_linter") or place it
immediately after the closing "}" of the test_that block; update the comment
surrounding the test_that (not the test logic) so the nolint scope still covers
the quoted string but no longer appears inside the test body.
tests/testthat/test-postprocess-matrix.R (1)

106-111: Indirect assertion in per_capita() test — consider verifying the actual formula.

expect_true(all(pc$matrix < result_base$matrix)) only tests a proxy property (per-capita rates should be smaller) rather than the actual transformation. This is fragile: it passes vacuously for any matrix scaled down by a positive factor, and it would give a false positive if the implementation divided by the wrong population column. A stronger test would check the per-capita formula directly, e.g., that pc$matrix equals result_base$matrix divided by the resolved population size element-wise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/testthat/test-postprocess-matrix.R` around lines 106 - 111, Update the
test for per_capita() to assert the actual transformation rather than an
indirect comparison: compute the expected per-capita matrix by dividing
result_base$matrix element-wise by the population values resolved for each cell
(using the same logic per_capita uses to map survey_pop/pop into a matrix of the
same dimensions), then replace expect_true(all(pc$matrix < result_base$matrix))
with an equality check like expect_equal(pc$matrix, expected_matrix, tolerance =
<small_tol>) so the test verifies pc$matrix == result_base$matrix /
resolved_population element-wise (use pc, per_capita, result_base, and pop to
locate relevant variables).
tests/testthat/test-compute-matrix.R (1)

1-91: Missing test coverage for the weight_threshold parameter.

compute_matrix(weight_threshold = ...) is a new parameter with non-trivial normalization semantics (cap + renormalize). There is no test that:

  1. Passes a non-NULL weight_threshold to compute_matrix, and
  2. Verifies the resulting matrix differs from the un-thresholded result in the expected direction.

This gap also means the normalisation inconsistency with contact_matrix noted in R/compute-matrix.R (global vs by = "age.group") would not be caught by the test suite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/testthat/test-compute-matrix.R` around lines 1 - 91, Add a unit test in
tests/testthat/test-compute-matrix.R that calls
compute_matrix(polymod_uk_grouped, weight_threshold = <small positive value>)
and compares it to the baseline compute_matrix(polymod_uk_grouped) to ensure the
thresholding behavior: assert that all entries in the thresholded matrix are
less than or equal to the un-thresholded matrix and that at least one entry is
strictly smaller (i.e. matrices are not identical). Also add an assertion that
the thresholded result differs from contact_matrix(...) when using legacy
weighting settings to catch the global vs by="age.group" normalization
inconsistency; reference compute_matrix, weight_threshold and contact_matrix to
locate the relevant logic.
R/weigh.R (2)

106-140: NA / unmatched participants receive sum(target) / n_total — clarify intent with a comment.

When group_idx is NA (either from NA column values or values not matching any group), the weight factor is sum(target) / n_total (line 134). This means NAs contribute additional total weight beyond the target budget (matched groups sum to sum(target), NAs add more). If the intent is that NAs receive a "neutral average" without inflating total weight, the divisor should arguably be the total number of participants rather than matched-only. Currently it is n_total (all participants), which is reasonable but subtly doesn't preserve the total weight budget.

A brief inline comment explaining the design choice (per the bug fix: "NA dayofweek now receive an average weight across all days") would help future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/weigh.R` around lines 106 - 140, The branch that assigns weight_factor for
NA group_idx values (in the weight_factor calculation using is.na(group_idx) =>
sum(target) / n_total) should be annotated with a concise inline comment
clarifying the design choice: explain that unmatched/NA participants receive the
average weight computed as sum(target) divided by n_total (all participants) so
they get a "neutral average" and this may not preserve the original target
budget exactly; reference the variables group_idx, weight_factor, sum(target),
n_total and the update participants[, weight := weight * weight_factor] so
future readers understand why unmatched NAs are handled this way.

143-167: table() excludes NAs by default — matched_counts will be NA for NA column values.

On line 145, table(col_vals) omits NA entries by default. So when col_vals contains NAs, val_counts["NA"] or val_counts[NA] yields NA, and matched_counts becomes NA for those rows. Combined with matched_target also being NA (since target[NA] = NA), the ifelse on line 159-163 correctly yields NA_real_. The end result is weight * NA = NA.

This is consistent behavior (unmatched → NA weight), but it means participants with NA in the by column silently get NA weight. If that's intended, it's fine; if not, consider table(col_vals, useNA = "ifany") and an explicit handling path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/weigh.R` around lines 143 - 167, In weigh_named, table(col_vals) drops NA
counts which causes matched_counts to be NA for rows where by is NA; update
val_counts to use table(col_vals, useNA = "ifany") and then add explicit
handling for NA keys: compute matched_counts so NA entries get a numeric count
(or treat them separately) and decide whether to assign a default weight or keep
NA, then apply that logic before computing weight_factor and updating
participants[, weight := ...] to avoid silently turning weights into NA for NA
group values.
R/subset-contact-survey.R (3)

96-106: Duplicate error message is fine but could reference a shared helper string.

The error at lines 100-105 is identical to lines 85-89. Both are valid but cover different cross-table scenarios (same column in both tables vs. different columns from each table). This is fine functionally. If you want to DRY it up later, a shared message template would suffice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/subset-contact-survey.R` around lines 96 - 106, The cli::cli_abort call
that emits the cross-table error message is duplicated; extract the shared
message into a single helper string and reuse it in both places to DRY up the
code: create a descriptive variable (e.g. cross_table_error_msg) near where
found_in_part and found_in_cont are checked, build the template once (including
{.val {part_cols}} and {.val {cont_cols}} placeholders), and replace the
duplicated cli::cli_abort(...) invocations to call
cli::cli_abort(cross_table_error_msg) so both branches reference the same
message text.

56-73: Numeric indexing (e.g. survey[1:5]) is silently caught, but symbols in i could still mislead.

all.vars(quote(1:5)) returns character(0), which correctly hits the abort. Good guard.

However, if a user writes survey[n] where n is a variable in the calling scope (intending numeric subsetting), all.vars returns "n", it won't be found in either table, and the function silently returns the unfiltered survey with only a warning about an unknown column. This could be confusing compared to the explicit abort for literal indexing. Consider whether the !found_in_part && !found_in_cont path (lines 108-116) should also abort instead of returning the unfiltered survey, or at least make the warning more prominent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/subset-contact-survey.R` around lines 56 - 73, The subset method
`[.contact_survey` currently treats symbol names in i (derived via ref_vars)
that are not found in participants or contacts by returning the unfiltered
survey with a warning; this can be confusing for cases like survey[n] where n is
a caller variable. Update the logic after computing ref_vars and checking
found_in_part / found_in_cont (the branch that currently issues a warning when
!found_in_part && !found_in_cont) to instead abort with cli::cli_abort giving a
clear error that the referenced symbol is not a column in participants or
contacts, or at minimum escalate the warning to an abort; ensure the message
references the offending symbol(s) and keeps the existing behavior for literal
numeric/logical indexing handled earlier (assemble_survey, copy(x$participants),
copy(x$contacts)).

28-32: copy.contact_survey does not deep-copy the input before mutating it.

The function modifies x directly (x$participants <- copy(…)) and returns it. In R, list modification triggers copy-on-modify so the caller's original is safe. However, data.table::copy generic's documented contract is to return a new deep copy without side-effects on the input. For clarity and to match the documented "deep copy" semantics, consider copying x itself first (analogous to how [.contact_survey and weigh do it):

♻️ Suggested improvement
 copy.contact_survey <- function(x) {
+  result <- x
+  result$participants <- copy(x$participants)
+  result$contacts <- copy(x$contacts)
+  result
-  x$participants <- copy(x$participants)
-  x$contacts <- copy(x$contacts)
-  x
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/subset-contact-survey.R` around lines 28 - 32, The function
copy.contact_survey currently mutates the input by assigning to x$participants
and x$contacts; change it to first make a full deep copy of the input (e.g.,
new_x <- copy(x)) and then modify new_x$participants and new_x$contacts before
returning new_x so the original x is never mutated and the function adheres to
the deep-copy contract of copy().
R/postprocess-matrix.R (1)

69-74: Consider extracting the repeated input validation into a shared helper.

All three exported functions (symmetrise, split_matrix, per_capita) have identical validation blocks checking for x$matrix and x$participants. A small check_compute_matrix_result(x) helper would DRY this up and ensure consistent messaging.

♻️ Example helper
# In an internal utilities file:
check_compute_matrix_result <- function(x, call = rlang::caller_env()) {
  if (!is.list(x) || is.null(x$matrix) || is.null(x$participants)) {
    cli::cli_abort(
      "{.arg x} must be a list with elements {.val matrix} and \\
       {.val participants}, as returned by {.fn compute_matrix}.",
      call = call
    )
  }
}

Also applies to: 131-136, 192-198

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/postprocess-matrix.R` around lines 69 - 74, Extract the duplicated
validation into a single internal helper (e.g. check_compute_matrix_result) and
call it from symmetrise, split_matrix, and per_capita instead of repeating the
block; implement check_compute_matrix_result(x, call = rlang::caller_env()) to
perform the existing is.list/NULL checks and call cli::cli_abort with the same
message (passing through call), put the helper in your internal utilities file
(not exported) and replace the three validation blocks at the locations
currently in symmetrise, split_matrix, and per_capita with a single call to
check_compute_matrix_result(x).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NAMESPACE`:
- Line 6: The package is missing the S3 generic for copy so copy.contact_survey
is never dispatched and R falls back to data.table::copy; add a proper S3
generic definition (e.g., a function named copy that calls UseMethod("copy"))
and export it so calls like compute_matrix's call to copy(survey) dispatch to
copy.contact_survey; ensure the generic's name matches the S3method entry and
that copy.contact_survey remains exported/registered so the deep-copy behavior
for contact_survey is invoked instead of data.table::copy.

In `@NEWS.md`:
- Around line 32-33: Change the American spelling "normalization" to the British
spelling "normalisation" in the NEWS entry that documents compute_matrix()
gaining a weight_threshold parameter; ensure the wording matches existing
British-spelled functions like normalise_weights() and symmetrise(), and update
the sentence mentioning contact_matrix() to use "normalisation" for consistency.

In `@R/compute-matrix.R`:
- Around line 27-31: The defaults for weight_threshold are inconsistent:
compute_matrix uses weight_threshold = NULL while contact_matrix uses
weight_threshold = NA; update contact_matrix to use weight_threshold = NULL so
both APIs match and rely on normalise_weights's guard (normalise_weights checks
if (!is.null(threshold) && !is.na(threshold))). Locate the contact_matrix
function and change its default to NULL, and run tests to ensure no behavior
changes in normalise_weights, compute_matrix, or any callers.

In `@R/contact_matrix.R`:
- Around line 308-327: When weigh_dayofweek is TRUE but survey$participants
lacks a dayofweek column, ensure you still create the is.weekday column so later
code (e.g., return_part_weights logic that uses is.weekday in the by= clause
when weigh_age/return_part_weights conditions are met) doesn't fail; update the
else branch that currently only cli_warns to add survey$participants[,
is.weekday := FALSE] (or another sensible default) so is.weekday always exists
regardless of whether dayofweek was present.

In `@R/postprocess-matrix.R`:
- Around line 192-212: per_capita lacks the NA guard used in symmetrise and
split_matrix which causes matrix_per_capita to propagate NAs; before calling
matrix_per_capita inside per_capita, check x$matrix for missing values (e.g.
anyNA(x$matrix)) and call cli::cli_abort with a clear message if NAs are present
so behavior matches symmetrise/split_matrix. Ensure the check occurs after
validating x and before assigning x$matrix <- matrix_per_capita(...).

---

Nitpick comments:
In `@R/contact-matrix-utils.R`:
- Around line 732-755: normalise_weights duplicates the truncation and
re-normalisation logic found in truncate_renormalise_weights and the manual
renormalisation in participant_weights; replace the legacy path by calling
normalise_weights(participants, by = "age.group", threshold = weight_threshold)
from participant_weights (instead of the manual weight clipping and the
truncate_renormalise_weights call) so the same by/threshold behavior is used,
and then remove the now-redundant truncate_renormalise_weights function and the
manual renormalisation block to avoid duplication.

In `@R/postprocess-matrix.R`:
- Around line 69-74: Extract the duplicated validation into a single internal
helper (e.g. check_compute_matrix_result) and call it from symmetrise,
split_matrix, and per_capita instead of repeating the block; implement
check_compute_matrix_result(x, call = rlang::caller_env()) to perform the
existing is.list/NULL checks and call cli::cli_abort with the same message
(passing through call), put the helper in your internal utilities file (not
exported) and replace the three validation blocks at the locations currently in
symmetrise, split_matrix, and per_capita with a single call to
check_compute_matrix_result(x).

In `@R/subset-contact-survey.R`:
- Around line 96-106: The cli::cli_abort call that emits the cross-table error
message is duplicated; extract the shared message into a single helper string
and reuse it in both places to DRY up the code: create a descriptive variable
(e.g. cross_table_error_msg) near where found_in_part and found_in_cont are
checked, build the template once (including {.val {part_cols}} and {.val
{cont_cols}} placeholders), and replace the duplicated cli::cli_abort(...)
invocations to call cli::cli_abort(cross_table_error_msg) so both branches
reference the same message text.
- Around line 56-73: The subset method `[.contact_survey` currently treats
symbol names in i (derived via ref_vars) that are not found in participants or
contacts by returning the unfiltered survey with a warning; this can be
confusing for cases like survey[n] where n is a caller variable. Update the
logic after computing ref_vars and checking found_in_part / found_in_cont (the
branch that currently issues a warning when !found_in_part && !found_in_cont) to
instead abort with cli::cli_abort giving a clear error that the referenced
symbol is not a column in participants or contacts, or at minimum escalate the
warning to an abort; ensure the message references the offending symbol(s) and
keeps the existing behavior for literal numeric/logical indexing handled earlier
(assemble_survey, copy(x$participants), copy(x$contacts)).
- Around line 28-32: The function copy.contact_survey currently mutates the
input by assigning to x$participants and x$contacts; change it to first make a
full deep copy of the input (e.g., new_x <- copy(x)) and then modify
new_x$participants and new_x$contacts before returning new_x so the original x
is never mutated and the function adheres to the deep-copy contract of copy().

In `@R/weigh.R`:
- Around line 106-140: The branch that assigns weight_factor for NA group_idx
values (in the weight_factor calculation using is.na(group_idx) => sum(target) /
n_total) should be annotated with a concise inline comment clarifying the design
choice: explain that unmatched/NA participants receive the average weight
computed as sum(target) divided by n_total (all participants) so they get a
"neutral average" and this may not preserve the original target budget exactly;
reference the variables group_idx, weight_factor, sum(target), n_total and the
update participants[, weight := weight * weight_factor] so future readers
understand why unmatched NAs are handled this way.
- Around line 143-167: In weigh_named, table(col_vals) drops NA counts which
causes matched_counts to be NA for rows where by is NA; update val_counts to use
table(col_vals, useNA = "ifany") and then add explicit handling for NA keys:
compute matched_counts so NA entries get a numeric count (or treat them
separately) and decide whether to assign a default weight or keep NA, then apply
that logic before computing weight_factor and updating participants[, weight :=
...] to avoid silently turning weights into NA for NA group values.

In `@tests/testthat/test-compute-matrix.R`:
- Around line 1-91: Add a unit test in tests/testthat/test-compute-matrix.R that
calls compute_matrix(polymod_uk_grouped, weight_threshold = <small positive
value>) and compares it to the baseline compute_matrix(polymod_uk_grouped) to
ensure the thresholding behavior: assert that all entries in the thresholded
matrix are less than or equal to the un-thresholded matrix and that at least one
entry is strictly smaller (i.e. matrices are not identical). Also add an
assertion that the thresholded result differs from contact_matrix(...) when
using legacy weighting settings to catch the global vs by="age.group"
normalization inconsistency; reference compute_matrix, weight_threshold and
contact_matrix to locate the relevant logic.

In `@tests/testthat/test-postprocess-matrix.R`:
- Around line 106-111: Update the test for per_capita() to assert the actual
transformation rather than an indirect comparison: compute the expected
per-capita matrix by dividing result_base$matrix element-wise by the population
values resolved for each cell (using the same logic per_capita uses to map
survey_pop/pop into a matrix of the same dimensions), then replace
expect_true(all(pc$matrix < result_base$matrix)) with an equality check like
expect_equal(pc$matrix, expected_matrix, tolerance = <small_tol>) so the test
verifies pc$matrix == result_base$matrix / resolved_population element-wise (use
pc, per_capita, result_base, and pop to locate relevant variables).

In `@tests/testthat/test-subset-contact-survey.R`:
- Around line 43-47: The test currently asserts an exact set of names with
expect_named(result, c("participants", "contacts", "reference")), which is
fragile; change the assertion to only require the mandatory fields be present
after subsetting (for example assert that "participants", "contacts" and
"reference" are all in names(result) or use a membership-based expectation)
while leaving other fields allowable so the test continues to pass if polymod
gains extra columns.

In `@tests/testthat/test-weigh.R`:
- Around line 193-204: The inline nolint block around the test_that("weigh()
errors for mismatched target/groups lengths", { ... }) is split oddly with "#
nolint end" inside the test body; move the "# nolint end" so the suppression is
visually consistent — either put it on the same line as the start comment (after
"nonportable_path_linter") or place it immediately after the closing "}" of the
test_that block; update the comment surrounding the test_that (not the test
logic) so the nolint scope still covers the quoted string but no longer appears
inside the test body.

sbfnk and others added 3 commits February 19, 2026 11:47
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
@sbfnk sbfnk force-pushed the pipeline-workflow branch from 8cab50c to d3345eb Compare February 19, 2026 11:47
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
R/compute-matrix.R (1)

62-62: normalise_weights return value is silently discarded.

normalise_weights modifies survey$participants in-place via :=, so discarding the return value is functionally correct. Consider adding an invisible-return convention invisible(NULL) or updating callers to use survey$participants <- normalise_weights(...) for consistency with how weight_by_age and similar helpers are called elsewhere in the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/compute-matrix.R` at line 62, normalise_weights is mutating
survey$participants in-place but its return is ignored; update normalise_weights
to return the modified data frame invisibly (e.g.,
return(invisible(participants)) so callers can continue to ignore the value but
get a usable result if needed) or change this call to follow the pattern used by
weight_by_age and similar helpers by assigning the result back
(survey$participants <- normalise_weights(survey$participants, threshold =
weight_threshold)); pick one approach and apply consistently across callers and
tests, referencing the normalise_weights function and the survey$participants
usage in compute-matrix.R when making the change.
R/contact-matrix-utils.R (1)

718-755: truncate_renormalise_weights is dead code with no callers — remove it and the hardcoded normalise_weights variant.

truncate_renormalise_weights (lines 748–755) has no external callers and duplicates the threshold-capping-and-renormalize logic already present in normalise_weights (lines 732–745). While normalise_weights offers a more flexible by parameter, truncate_renormalise_weights hardcodes by = age.group and is unused. Remove it. Additionally, participant_weights (line 758) and return_participant_weights (line 1199) also have no external callers and should be reviewed for removal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/contact-matrix-utils.R` around lines 718 - 755, Remove the dead, duplicated
truncation helper and hardcoded variant: delete the truncate_renormalise_weights
function and the hardcoded variant of normalise_weights that uses age.group
only, and consolidate logic into the single flexible
normalise_weights(participants, by = "age.group", threshold = NULL) which
already handles capping and re-normalising; also search for and remove unused
functions participant_weights and return_participant_weights (or confirm callers
and refactor if still needed) so there are no duplicate/unused
weight-normalisation implementations left in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NEWS.md`:
- Around line 20-27: The example uses the native pipe placeholder `_` (polymod
|> _[country == "United Kingdom"] |> ...) which requires R ≥ 4.2.0 and will
parse-fail on older R versions; update the NEWS example by either adding a short
note stating "requires R ≥ 4.2.0" or replace the placeholder pipe expression
with the equivalent non-placeholder form (e.g. use polymod[country == "United
Kingdom"] before piping into assign_age_groups, compute_matrix, and symmetrise
with pop) so users on older R versions can copy-paste without error.

In `@R/compute-matrix.R`:
- Line 33: The call copy(survey) uses data.table::copy (not an S3 generic) so
the registered S3method(copy, contact_survey) never dispatches; either remove
the ineffective S3 registration from NAMESPACE or define a local S3 generic copy
<- function(x, ...) UseMethod("copy") so copy.contact_survey (defined in
R/subset-contact-survey.R) is invoked—update the package NAMESPACE to match the
chosen approach and ensure any calls to copy(...) within the package use the
local generic (or stop registering the S3method if you keep using
data.table::copy).

---

Duplicate comments:
In `@NEWS.md`:
- Around line 32-33: The NEWS.md entry uses American spelling "normalization"
but the codebase uses British spelling; update the line describing
compute_matrix() (mentioning compute_matrix() and weight_threshold) to replace
"normalization" with "normalisation" so it matches other identifiers like
normalise_weights and normalise_weighted_matrix; ensure the rest of the sentence
remains unchanged and run a quick search to confirm no other occurrences in
NEWS.md use the American variant.

In `@R/compute-matrix.R`:
- Around line 27-31: The default for weight_threshold in compute_matrix is
inconsistent with contact_matrix (NULL vs NA); change compute_matrix's signature
to use weight_threshold = NA so both APIs match, and ensure any internal checks
relying on !is.null(threshold) && !is.na(threshold) still work with the new
default; update references to compute_matrix (and its Roxygen/docs/tests if
present) to reflect the NA default, and verify compatibility with
normalise_weights and truncate_renormalise_weights which already handle NULL/NA
guards.

In `@R/contact_matrix.R`:
- Around line 308-327: When weigh_dayofweek is TRUE but survey$participants
lacks a dayofweek column, add an is.weekday logical column to avoid later
data.table grouping errors in return_part_weights; update the else branch that
currently only issues a cli_warn to set survey$participants[, is.weekday :=
FALSE] (or NA if you prefer unknown) so the is.weekday symbol always exists for
subsequent grouping operations.

---

Nitpick comments:
In `@R/compute-matrix.R`:
- Line 62: normalise_weights is mutating survey$participants in-place but its
return is ignored; update normalise_weights to return the modified data frame
invisibly (e.g., return(invisible(participants)) so callers can continue to
ignore the value but get a usable result if needed) or change this call to
follow the pattern used by weight_by_age and similar helpers by assigning the
result back (survey$participants <- normalise_weights(survey$participants,
threshold = weight_threshold)); pick one approach and apply consistently across
callers and tests, referencing the normalise_weights function and the
survey$participants usage in compute-matrix.R when making the change.

In `@R/contact-matrix-utils.R`:
- Around line 718-755: Remove the dead, duplicated truncation helper and
hardcoded variant: delete the truncate_renormalise_weights function and the
hardcoded variant of normalise_weights that uses age.group only, and consolidate
logic into the single flexible normalise_weights(participants, by = "age.group",
threshold = NULL) which already handles capping and re-normalising; also search
for and remove unused functions participant_weights and
return_participant_weights (or confirm callers and refactor if still needed) so
there are no duplicate/unused weight-normalisation implementations left in the
file.

sbfnk and others added 3 commits February 19, 2026 11:58
- Use British spelling 'normalisation' in NEWS.md
- Set weigh_dayofweek to FALSE when dayofweek column is missing
- Add NA guard to per_capita() for consistency with symmetrise/split_matrix

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
- Define copy() S3 generic so copy.contact_survey dispatches correctly
- Add copy.default method that falls back to data.table::copy
- Remove unused participant_weights() and truncate_renormalise_weights()
- Return invisible(participants) from normalise_weights()

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
NAMESPACE (1)

42-42: ⚠️ Potential issue | 🟠 Major

Remove all stale @importFrom data.table copy annotations — not just the one in [.contact_survey.

With export(copy) added (line 15), the local copy generic masks the data.table::copy import, creating a NAMESPACE conflict. However, the problem is broader than the review comment indicates: @importFrom data.table copy appears in five files (not just subset-contact-survey.R):

  • R/subset-contact-survey.R:64 (on [.contact_survey)
  • R/weigh.R:43 (on weigh)
  • R/contact_matrix.R:35 (on contact_matrix)
  • R/compute-matrix.R:24 (on compute_matrix)
  • R/deprecated.R:63 (on .get_survey)

All bare copy() calls in these functions dispatch through the package's generic, which routes to copy.default or copy.contact_survey—both call data.table::copy() explicitly via the :: operator. None of these annotations are needed. Removing only the one from subset-contact-survey.R will not eliminate the NAMESPACE entry; all five must be removed to clear the conflict entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NAMESPACE` at line 42, Remove every stale `@importFrom` data.table copy
annotation from the package sources — not just in subset-contact-survey.R — so
the local export(copy) generic no longer conflicts with an imported symbol;
specifically delete the `@importFrom` data.table copy lines found on the
methods/functions `[.contact_survey`, weigh, contact_matrix, compute_matrix, and
.get_survey (they appear in R/subset-contact-survey.R, R/weigh.R,
R/contact_matrix.R, R/compute-matrix.R, and R/deprecated.R) because these
functions call data.table::copy() explicitly or dispatch through the package
generic and do not need the import.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@NAMESPACE`:
- Line 42: Remove every stale `@importFrom` data.table copy annotation from the
package sources — not just in subset-contact-survey.R — so the local
export(copy) generic no longer conflicts with an imported symbol; specifically
delete the `@importFrom` data.table copy lines found on the methods/functions
`[.contact_survey`, weigh, contact_matrix, compute_matrix, and .get_survey (they
appear in R/subset-contact-survey.R, R/weigh.R, R/contact_matrix.R,
R/compute-matrix.R, and R/deprecated.R) because these functions call
data.table::copy() explicitly or dispatch through the package generic and do not
need the import.

---

Duplicate comments:
In `@NAMESPACE`:
- Around line 7-15: Add the S3 generic export and default method registration
for copy so R dispatch picks up copy.contact_survey: ensure the NAMESPACE
includes export(copy) and S3method(copy,default) alongside the existing
S3method(copy,contact_survey) so calls to copy(...) from compute_matrix invoke
copy.contact_survey rather than falling back to data.table::copy; update
NAMESPACE to include both export(copy) and S3method(copy,default) if not already
present.

These conflicted with the new copy() S3 generic. Functions now dispatch
through the package generic, which calls data.table::copy() explicitly.

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/epiforecasts/socialmixr/issues/comments/3926087006","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nRefactors weighting and validation into reusable helpers (warn_multiple_observations, normalise_weights), adds exported compute_matrix(survey, counts = FALSE, weight_threshold = NULL), and introduces a generic S3 copy API with methods including copy.default and copy.contact_survey.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Compute Matrix** <br> `R/compute-matrix.R`, `man/compute_matrix.Rd`, `NEWS.md`|Add exported `compute_matrix(..., weight_threshold = NULL)` and documentation; integrate normalise_weights for post-stratification and replace in-function multi-observation logic with warn_multiple_observations.|\n|**Weighting & Validation Utilities** <br> `R/contact-matrix-utils.R`, `R/contact_matrix.R`, `R/weigh.R`, `R/globals.R`|Add `normalise_weights()` and `warn_multiple_observations()`; refactor contact_matrix to centralise weighing via `weigh()` (dayofweek, age, user-defined), replace is.weekday with dayofweek, and remove inlined weight renormalisation.|\n|**Generic Copy API** <br> `R/subset-contact-survey.R`, `NAMESPACE`, `man/copy.Rd`, `man/copy.contact_survey.Rd`|Introduce S3 generic `copy(x, ...)`, add `copy.default` delegating to `data.table::copy`, update `copy.contact_survey` implementation and documentation; remove lone man page for old method.|\n|**Post-processing & Guards** <br> `R/postprocess-matrix.R`, `tests/testthat/*`, `tests/testthat/_snaps/matrix.md`|Add NA guard in per_capita to abort on matrices with NA; update tests/snapshots and expected weight/vector lengths to reflect new weighting behavior (dayofweek handling).|\n|**Documentation, NEWS & Metadata** <br> `man/normalise_weights.Rd`, `man/warn_multiple_observations.Rd`, `NEWS.md`, `R/deprecated.R`|Add man pages for new helpers; update NEWS; remove some roxygen import directives for `data.table::copy` from various files and adjust documentation metadata.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\nparticipant User\nparticipant compute_matrix as compute_matrix()\nparticipant Weigh as weigh()\nparticipant Norm as normalise_weights()\nparticipant Merge as merge participants+contacts\nparticipant Matrix as build_matrix()\n\nUser->>compute_matrix: call(survey, weight_threshold)\ncompute_matrix->>Weigh: apply dayofweek/age/user weights\nWeigh-->>compute_matrix: weighted participants\ncompute_matrix->>Norm: normalise_weights(participants, by=\"age.group\", threshold)\nNorm-->>compute_matrix: normalized participants\ncompute_matrix->>Merge: merge participants and contacts\nMerge-->>Matrix: aggregated counts\nMatrix-->>User: return(list(matrix, participants))\n```\n\n## Estimated code review effort\n\n🎯 3 (Moderate) | ⏱️ ~22 minutes\n\n## Possibly related issues\n\n- `#131` — Split up `contact_matrix` function: PR extracts and centralises weighting/validation into helpers and adds compute_matrix, aligning with splitting/refactoring objectives.\n- epiforecasts/socialmixr#288 — Pipeline workflow wiring: PR implements assign_age_groups → weigh → compute_matrix pipeline and related helpers, matching the issue objective.\n\n## Possibly related PRs\n\n- epiforecasts/socialmixr#287 — Modifies copy S3 API/NAMESPACE entries; strong overlap on S3 copy changes.\n- epiforecasts/socialmixr#264 — Related handling of multiple observations per participant; both add/adjust multiple-observation warnings and observation_key usage.\n- epiforecasts/socialmixr#196 — Overlaps on weight normalization and contact-matrix utilities refactor (normalise_weights and weighting flow).\n\n## Poem\n\n> 🐰 Hopped through code with tiny paws and glee,  \n> > Weights now tidy, normalized by degree,  \n> > Copy went generic, surveys kept whole,  \n> > Matrices computed, each part played its role,  \n> > A rabbit's small cheer for a cleaner tree!\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 5</summary>\n\n<details>\n<summary>✅ Passed checks (5 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                                                                                                                          |\n| :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|      Description Check     | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                                                          |\n|         Title check        | ✅ Passed | The title 'Refactor contact_matrix() to use pipeline infrastructure' clearly and concisely summarizes the main change: restructuring contact_matrix to use the package's pipeline infrastructure internally.                         |\n|     Linked Issues check    | ✅ Passed | The PR successfully addresses issue `#131` by decomposing contact_matrix into smaller, reusable functions with extracted helpers (warn_multiple_observations, normalise_weights), centralised weighting logic, and reduced complexity. |\n| Out of Scope Changes check | ✅ Passed | All changes are aligned with the PR objective to refactor contact_matrix to use pipeline infrastructure, extract helpers, and align compute_matrix with contact_matrix via weight_threshold parameter.                               |\n|     Docstring Coverage     | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.                                                                                                                           |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `pipeline-workflow`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=epiforecasts/socialmixr&utm_content=290)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKPR1AGxJcbJAGZoYvh8TBi4gbgA+szUFPAAHgAUAJSQBJDYiCQ88NwkHvDkKBh+VIi4FNhi2BTZiWYATACcAAypkAYAco4ClFzNLZAdAKo2ADJcsLi43IgcAPTzROqw2AIaTMzzJNzwfsGiaOWI84j4DPBoHswJFPPc2B4e8wNDBsNZFFyIAn4YANZvADK+BqDGyAioGAYsC4uzyBXIYAA7sF/n4PPhkZBAEmEMGcpFwkEhmBhXBihWB4VwmS4+DyWA6AGFatQ6OhOJAGi0GgA2MA8sAARia0BaTQ4AFYAMwcFoAFgAWm4EMhbJBagEghRkKFwmJorEEik0vgMlk0rBstxAv80KQAOTIeH5QrZQqlI4VKo02rFGgUDBXDzyTD0BiYrLIJAObJmIXSoUaSBM2CYUjR6EebBKDgGKB6iKGirG1IYLGQJReIhs5DByDIkjwIhTQpESCJWhoWRgfB+FEkEj/AA06FIo8ylDASj8btoqXSjebsBSGnzkAAovEKhF2Yg07V6FaPHkdVxkc4MNFHuJuF5IoJPhJqPB8BhECaw5ByxQYgUspES4trgH7JGuBYsA8NDFnESSpDWhR1g2TbAZEuCwLU+74B49A2lQbABqa6AFEQWDIisChhEWMQlnB4EprIEYsC+DAKMwd4kPE6jyIeVTsqULCQJK8pEZKLT0QAQtg7azvEcLOOIFw2mEyDkehkCdAAgpW3Z9o2Q7IX6vAkBIr6ZCGaSsjQ9BHMhQ5kLQADclokPI5bYrU4KSNkmDoFIVCkMhy5EoEFD4IgdZPDpsiIPRABi2GYsiYDYNwRHqo0AAcADszngmEVD/tIQXAW2kCYssrFfpxO5iMgx6nsg+x8LUk7oBg4ZeM4lD2N61S1GuBgWJAADywiiOIUhNWFzDlYU/zsjG2DSEYYzzYtEXLZA8aJpAgAoBJAABEQJ3uoGRpYWBo0bBkB+Ng0LiG+h1GGNIhiN5XAACKiJB4XZOh2QeAS2SXVE12lig7FeGwVGPVghTpIgf5eBQo7UCwSD8fd71vsg6TwOxYVSIwQNxLgsijjQ5QqPABTk2jHWQBSVGFDTdOyINACSUPwOy4SIP8da0LQmFZPQciWqgth5lAW61UStBIAwtQ0Bq0jcLj8BqHTvPIIkQGthgRCjuD8RsVBL5vqOGvlGAvDnNIiBtqkCNmlkeFsrd2Nw3WjMuoi2TlDssXrky7CFZjJWG+2FXwKxiRtQbKSpGc0W9v2+kjmOJAM/Qk58AbIHoH6aYdV44aa0HYQhvRVhE/AShq5kKheJADWUHrF6BteHi3vej6UM+PspKOP5/pjgEoVMoELmavHgpWqUFAwlsYPRmnC0z1AwmVHsER3HaF2hGHSLA2HzkRtRA9ZXsPa+763cElFO1XRK9GmpmghQ9G+LQfG3zjLATsABePlGabA4lxcmHY2BHBqOyeecNiiMEYpiGicdzZeCgbIWet1Ai03UJ7Km4gjbtXoLUUyJBkTxQSFHXAYBS60ERO2AQ0k9ZaWinpQc/xcEE3tsTJgFBPK4HIBFQa+hjDgCgA5fgfgcAEGIGQZQN9Ngw05Lwfg413pTWJPIJgSgqCqHUFoHQEiTBQDgKgVAvk0B4EIKQcgVAVEsDUVwKg2IHBOBcLohQBiVBqE0NoXQYBDCSNMF0TSABZDcQIrCaSZBuPMh1klDUsJpTmijHFsnoJ4mI3i+yMFLhmIwUAgTSgImfGyws6BcAAAZMG4LIWpj8QhAwipAWpM5bG92aZSWcrdalaWibE+JG5mmJADugYW6h77JHXFuDWFAQrVNoHUhpTTkH9OyIMqJMS4kJPGZMtA0y4ZzKgNzRZuA4ozTVswfAz4PB1L4cEK5M1OzUDQBocIAgvDDnWckXpWAtkdKGXs0Zhy3SVnyCQU5RgSmQEiTCypUylCrI6WUipihEjrOHF0m8ALkGgpGQk+Z8RLk1I6eswFGldnEo3MYU4jg8myFCcNTSvdlH3zxmaAGUKIzOFXsgApnFyX0Cfg8H5GD2AzJWuuRF6FFD2GbEGX0PkVl1IxUirFOK8W9wJX02m2yiX7LGfCuK3t76QBFS8iltTrVLOxfSHB1LgU7OGSa2pq03S6iKRSgA1A0eYwojAbnKATbJvjsgUN5tifwzVOSIsVo4AwyTnr5nCZ0DcAB1IEGhmCrJTSktlGSHHKL3Ey5w8gCkwnTLKqAvg7yBGKhrEMRA3xXEKdoDAZV6TKKfs3QKalYCVngMI4oSguKkJbbIO5tAADaTB7oVHkAAXhXUdYY3ab4AGk2y0BYIdAAusg3lnE0BQ2yKiCg6Ikrr2Fsgcg2IIF4BIDBCG9I4bninlEdCmEz44SIivNKNVahsDoc6OIT90hjyuPAYBq80YkXIPQIdlF9RgyNHBYkJAP6vm/uuYY3AuxU39JQIMHgGxogxFiLgoM31YfcmrAoxU3xRxNM1YiFHC5lXeT2PsA4hxo3HGQ80U4ZxzmSM5EDERkD7m6kefIjUGyXh7n3V9A8KBDy5Z+RmMGiqT2CqBFpTcLSoefdBU2q51xxQSMVNAxJpLySWXHXImAi6oY4V2WQXCDLdW/BWQu7IJY2P8vaS9370DK3CpFCjXnMxB2ObIuyC0Ooh3rTCrtD6qHZyIGFVKiA0L4EiAUG4IF2MWvbezDshR/IWgKSV9QBWCCRDC7l0Esxk6DRKeEtlHKnFcqIrypQ/L+u4yS/am+4q1jLytWEGViB4WdDNBK5e2wyU2voNK6BTtSLUHgapSg2RZ17F5vQSk6FUCKz8H4cRPXLCEeI9kGq7A0j2jhemsAXrRGdqNv6oUgawANBDWGmi7J9FRpMjGq112XlcETfAZNqbuufYMDYeY5mSBgFNhoGwSSi1pJLUopx5avFVrkTW37C31wb1Regb82WJvsj3jCnqR9f2n3PoByCL76OJAcJp1yo5F0qUgOuuKmkxhAg3KONnJ8sIAfXZ0YYYwxjJFzpWc4jh2CLSJOdq0kAbD4HiLIBxbE1FpYNzsIG4IbJbwwNgK41spx4UUq5sIPBwr0PKP12cK8kF6bg6vXqOxD4ReC6QDQbXUqpFQ/ZleUVoPBHHgBQuH5+dSFkAAEhdy55SIFKZy//fQddsu/3n1wWgbgd4q3cDhh29n8vFZGx/lbpt9BeUMEyAQWazAbzwF7AIJ8Qeu7dtIbHVisfGD1kXCp3vvdcj90H4PQViQc9KTc4gUcGmtNvkiAtCmt1aYBkiAgd367Dr+zdIdMC1PaBCC73zfXd076sZ2yq+BImr4Rv3Z3tRQf0iaheBiAuT07Ygfr3z16F6c7M4Bgy4ICtzGRPhlScRIAkIsI4ZoCfwFxWhkTfrHxl4AaoBK4q70SRKFBPzPJLIibNQ0RoE/YZhcCUGvKCTEafLfKtyMZvgWSFARg5jFT3TwAACOy0nQHYxkn85kei9Ip2xEb4RATsjc/yzkxytOvKj6PAAq+8fAX4iBg8j+T2XEYapC9yygUU1G2IiQhQMysG8GSCBShcKAciNwEUbYJslAgUQc3A6upsz8fUSCoGmW/oEQDmRIqGvKqUj29AAethlqnhN+qSkA7KAYgqg2+uw2pMKRwq62Sy7IU2kqrEW2usZqFWWAKhtqGOvO6eguCgS6yAYuEuUuMueBDeReouGkyuquLqhqHSaOGOWOmGOOnqBga032FOpAaKfq/2Qa0oLQwO4goOFcjc0aDOMOSycOdACOzAhaaaEAKOvRb46G/RtEKU4gHgsUuO2xCR6SmSZaOSFa+S5OvqVOUANO7II+qmC+6mS+mmgqXA6h7cfA6E1AymgYqkOBdOVR8gaYyAdyfoYUyIeMpcGQ3awh1oCkueG+MuFEeoNUDuFGp+RcAgRwuRWA2+q8e+rk1BR+lAJ+CMzkKsNQD8xBYwxQpkTsPyHM64nMBUigfEURSesGKe36cwoBpGgYHaAJREAepmwpDYKwlIUeswHYcgMeFE4BlWloBBTkasja4IEGkOoIB2wUYA0pQe4+n+MKjJmYbJWsCB6J6+KkLeupTO0ahplk2MbIkQtQ0pr6qeIma+buUQfp5pEsoQZwBQxGZUrs/mv4gpvpspqGMBB8wWjMLR589JJAdyUgYqfAhQAc9ANBPU78mBZkOoTGEa6QXm6cAm/w8wYWdCrhImaZOEYARJYsasppcM9ED2EaK2GC6RAqPsXAZRKGs+feHED43xO+D8X4ER2S8w3p56bx36ZUz+gCVq62/O/0ZoPpBmwEm+asGIE0ZU9mTsPMs4dAQu4ccZ/JsZ/4Qele9sgQsAXWVxfWKR6QQ2ogGRPs422Rk2fAfZBRc24gdaiRKyACn6HS7xc+amk5Q+w8AZeeB5ZJcMFJq67RKuo4/Sx+BJbRDAiQF+uQro5Ah0o4h01YgQsg1++qWAtS+xVEYgRxsEJxtM5xnqUAPZN8a5UFtSu5qeq+9pgZB5Es5+YWkeeW3AZFmpHOCumFnRyC9F6OBxEQzFCQrFZxgxHYi5bABZNyCMZGHavAeGLSs0X4Z5p0F5OSyqe2tQcywx3q9B/q2U0o0x2UlxH2RgDF6G9GOOeOz0xaNxxOdxpOSWYxYFDa1uTOU4JI0Iw6sFnxA+CFA2I+ZU5pk+YZ2EDcEa8eFGM+3cCVE5qFXK1sRwTsU6QlyFW+U55J++6uOFNJBJTpUVFcS63iDVcQpCGV15BQoCBZ1JFAkQwuJYdmjMp5awWQqJ7utQyIZM25omG5p0FwRIHVZUpk41AusgkQGs3A8wsgJAzg5U+AlUFp187IAJFukVbeTMfeYAnh2cbcYYzC1W7EeACGkAipzscp6kceuMWVkR6ALhpELWpAkQipTUSU2FiUWIQWoYVeIYZUrBh+fWvs9ABikgJ5oNDWRcAks0vKZVyq7I9Z4NzV11MQ9uHankuMfhlqBSYWxWBMjW31w60m65uNgNb+RNgU4NHYrWklTWRW2NiAqQs5RG3+WI74FQB1s0A6JA9EWaU8aVx1GCM1aINSBgQwUABskQXmPmgIjGDy2VJGuh0I2QBSutfgmc2GzU4Wy4bYzkewPAmE7AaM8NusUczNi1y8Z04QFAhIqNKK0YsUmcXmxmECL42s3EDtciKgWQYQo4S0dmIJo+7YX4isiALcxUBsbYa4mtUcIN2QjGk4RpLY/AWALuBdntvKd0UUkJHuDw18lqiENAiWdNUKisfuy5wUkQcgldAJudugkAHwYm/gc44GR19Iass16gNAWA+MyRqsv6oIpdfpX4j5FkBsxmB1MIs2y6o4roNwKqZUX5HdEa2tvd+cOto9yGGh6Etcnud1O44gvuQe0RQetQzpBaedvgWZ7I5NeJuiYA4lipMZ48MRrGX46p5GMljej1ZcOdGtg93JFQvJNuoDcZe508UpApvVEIoYEeIDsepJteEBeVUBOEpB7hTOlVG+ImoMENFYARiEo4rCCsHyt0SU/m2Ixd50AN3GpCX4b9SCnhEGJl6QbAftCDUAZBP4aGYUHgYA5hUy9+5Q5uREsGpEnt6h/DRA8wQjsRJAU1C8adEt3u0t5sb1PspB5BuZ7ELytyigJFRAbimZJhZ2GA+ZOk4QXymdCgjSkMlyg0vWC9A2n5aR35g5A2WRoq/AgF02UqIFRR64S2Tl3Ki1G29w8TrEPFsRNlqqmYPRyljFGGtEgxr5w0kSmAew0gRINmrcmk5GsgoCFAX2xU4VEx0o8oQa8osxSOnlqOSwmIRJmlFxSOgVpawV9g9xZOqT8KP9bj2cEl7Wt0NyFRPhRAQzVwAAas4JcD8tIKTWg6eTCkloXGAF/jfOHO7fdAYjdfPhxGhjVCKaBgs8A5JXQ5QOEJSA4c6UXgfOdpUNCJ6d6TgxPKvX7DQ2EJg4SZiAwILBmZ/S5LNKhvxcKfRNyXQj3VtV5g+H4JPAZDiduKOC89mSgMHdwqHWvRBRbZnMoRBW88s5SKDOc/kBGlkIAmCYdugFXmFMZWyOi1gHRj4YS7gMS63mg0gBoCHd2J7TS9wnS7Tgy6lLIUbAodkHK0OBmQjGFH/EcwQ+83rhCLC4CF+L/eQqy5cwVO7YEIIk3hs5yVACNKFk8GK97j6B/vYd+iy2dfQBs4IB2s+HEJnXWFFu0ms5hk0d3b3WFpG0QFEBfZ8FfbOMhqVc5g6UGcKaOBnRepELjdtZC/nvYOehOfQ/m2m8JaPGW67shdtTSfTYqWK76FeEhW5tCweV+EK5ho89uIgMoUhqpBRKeQgH4CtfpeS0OKHZWbpJbdwiJtq6g3Zvq4y6UScqQ8SMayHHCsE5ymNmE+qxE6Ng/NExtrEzwFk7vfNvClxUzlk4M/6xRoG3s63AOYewU2jn68M+cXmN/a49mVwIdEq1JSs4JOG7RN25oIg+lma3WXfg/mioVa3Fc/wR1Kzl6xc0Fhux2N6EC9BCC3eWC5mxoeW3nm2ynMO3QYnvh0KYZgPelp/f+5K9K9RZ7YdBq/8IdMgkfL3Ti32Pi4CPMGhtRF2yKyHHna8Wiqx9O5nBx1+AB0u6lBx0yypVdMJwcTVLR+BaijByo9ZP+4B4p0AiZKYUWxenbM7e7uZcW8wt6zlWpz2xpyk+oUBZBbTbmWEJQFqO6JmCg7q3QM5FwfIB+wG7s74y+ykX5sdlZYNMAPMOTdU+ULm4aqEiMW076hMV02AO5X07sV5fcJ7vbHqYgGpfEH5ZceM0Tmy9M2FU8fCjTkhFpGAIUAOMFHQMVx9Q7hQO4zwDSUBoQowXIryoUFBFvGB4WIhDSpAA8stO2wIPY5PogHkBcL7kzI7PWWvRN6lUbCw/4AcN1xQDQWVKbLW4Nb194zAKqLNs1HqZaMCaeGAKd3HlcJ3o3axqgOWESKeIWWKlgODNd6hs4eVe2Eh5u+YGku+b+Xu3yj+VE3IozjmWe/kRe6Bc8RpGaOFWk1+SNpkbD/+bkXE4j4UQc6DwilU34DU5AHU9kA01cE05QK0z6rWhMVMQKB5TlwMw4EvvQsy5CaV2MwTkFZV6FdWjV+uFdWgweLQPd44xXI0hsMp1EHXZPhMxgusis0uVev8MOfSx9UTir06okPEKOBoMbyLWNX4xzLqkSJiu3mmArNCjWCRlO94+wd4BwOst2WLc4rL3RnXekBo2RBRLysr1VDy/gM+YwUSIxuEAtMgMb8mLOVGF42wZnRwG706mXZAPENngWwHVn/Q424ycffrpFzIe76L7+y6QaZkMUFjlqjL7IGtZcObwb9n8Rxvqb/Xy3/Qy7ECpgU/AUmodlu75b7fbABeKGIzCKt7Ww87yn2n/47LR7wDTk6xj/lrrDJatVP+Wk4ARNCAcH+b4kVYJzB2DiubxoJb0Lk6nL8U5EJCR3w46S7yuIWZMgFPy5kSKr0wXNEUIa0xiZG5nsDHk3wQTMHiE13Y8pwmWPX8sexyLw9nOhPFHuahfwrtUUayfXob0gBx9aKyNbZGjg55ZAue8vO6jUAzyDFrMJRFFOUWv6W8DeRvE3l0QGT4CJqMKKXsUxIGbVyBUAZAeuRHLoDve8vO/qQMpLAAoAK/DAHQKwEMDNk3RJSgQNYHc9hBHMGwEMRS4M9fsExBoK5WFBA4DAoaeYrZyWKQ4Vi8adYkmi2LZdwk5NZSq9QswDEbABaPnokUJxZIb4uSStNV1rQo86udOdQg4I1y/52AQeYFBxlPS48CylA0DrBGTCfRNcajIMGwEQyXADy4gXAL8nND1lHUtg19JZkhJX9aibRcXJLmlyYtmyxeeSskDVzFwiA6/IuHziUH5CVIsbKYPgVkrzgC+gYSblcC2iJB7M/4UIhRGhRqMfCOhHPlUKUCIBlYuQJBFkKghJ1QYI3G6OzXszGQ8I5aTakn0ozXpzCCdLMDmExqY4QGBNUiGohExQMO0OjZyO92bQP0CujsMqKhkQAzoCIcQLIFm1OglNYIW+QCj10ryEIqhW/KzsVHeThBV82EGdIoFSCX4igseIGleHrb806G3OOwbRHiLE8kiO7B+JDzC4wCceMTPIjNkQFGAls5AYnpU27Rk9ygFPbotTxDDNMugI0TNPTycodNJQQaXpikn6bWD3ePvJQTjkcH45nBAvNwVV2F5eCjA30LwI731z+D/6HaNfn/iQShD9cZSFbgqnr438fKPPc7hDjNYBDahQeF/oaQsjiYb6vkfweHHkCoYwoxuBxA0GJD4BDUFARtDQF2G8E7WfxJciJlgy2QeRggvIWkE8DZBDo30EPKr1+rFMpmm1YcBx1lrGZ7M1vc3gAG9eRm1AAL4G8ARftWoZn15qCcDQddQQG9FwBVCpu+7SYXEDUACMoUYY9PgP31wBi060gKYSQ1Yzvci+wJDvOnx4LZgJhIBFtiLg7by9kArBHxvs1iio8XOGpcHKe2c6aRj+SqXbPk2Lggw0u/nDxvIC7FCJ1Yb4O1vqMVGWpgUlIawWS0f50BXy0jOvmeLRT1InUKY/0UoIzHxAcBtSbkdf1TFkCHBQxBIuSPi61MaRjTekQ5VGJpcuAfqFoEGiFCZQ5i4aZxEYMoSxpVinIMYFiFZ5WDMAejUFtR33L8j/KVxFwbcSmZC9Hi4ogwMgx1Z8QRxcQoIUqKfj2YnO57cQeg30yp5kwcAbIP4OBSmjio1dSgQkLAQ2QCgRwNGBkI8J5MP8qLQzIJTb5NCfEYleTlJQLxak2izJAEWNWzFqNpJ1bTEroiUltDVS6kAGKOjSCyA8gAdS3hOM5hEgFuogE7DxP1wMlOhZYwGv2Jz5J8xxrcEvrDUPKHZTa6uCYVMN6AyAMCWBP4lhIeEJl5SWAHmojEcCpFsgIDEBDnE9rnC8qgLP3LTRx5S0wMq9EKDA1aL7AngMNcWDxExydl74bE8JhWOmGWo8kgsEAkxNsgGVxSHgZMCkxnHo8VxxQd0UFlci7j9xNEiqeiPB6hNIB+7aATD3SZwDZx57IkRQJQFUCbxkk/clpIxKyTRKR0fTnpNgaK4OiOA11N5VUqmwNK7FDsHD0yaI838tlHyMgBtL7NkECowaW+HspqCWRYEhoGyJZ6WCUc1gmCuOUXzJVcYuEsrvzwmaC9mUngynLV3vS+Dss/gx6Rv1YyWjjMvEhaX9PuYAzl8VjHUQEKh7dRES2QJifxKSHCTAxaQ5KbLTRgaT2AesAcUXFsh0z3JLvfzjVTQr74XJqUhQNmGYBYB+JfOSTMjVwoIwXJceM+HHAhAwp9IWAIigiCvwiYKKJAGsAwGopVDHJWAZyYkGZKskkAtpHBP5ObFxBWxWASwptCTqbd2wyIcEhtQzyPUYS/0s2qzIGyngiO2k+OigA0By0TYu3eEoiV8gCFUSLsyIA3C6HZhpAYEFMG0idhWUXJzU6BgjOpBDTqcI0iASARxGTS4eM0gnokxWhQBOgDOcIRdMJEgV5A8cilExPRlwViqY2Y8RhIrmfF4KWMrlPyMg6QAgQ4k2oOeDHIYyvigM98KtPTYoVHZu+dmbtKwqCzGqws9dARRlkkUSA0lBWUrJVn2Vhp4ArEWNLxmvs/y+I/HkXNSFJMXiEFBiYj3jlB5kZHGcQZ3IKr2yG5PxX8jXIwDzA65RVIee+Gbl50rA57K6aqkvlXgEOPcxubjH7kVtNEvc9CipI6LYUBqtJM/IwEIrQi555FSisrJorMj2m70zKOyLQko5iEJwYhECVwDzA7+QYWYLFwGL5o8J5XVwSTghliioZBgdiSOmuypNvwWuOIKxFBAzA8AIc6biegckORDs9AHwuy1/KJBcAqIIAR7FVigwtACk9ducEFhVD/u9sxgPkDOK8MI0vQMRYODnr65OcgjbLC7wnEAAJA6rc396nChx3MoBDaAuCkI9cf0ORXCzrB+grgqcY5Dp3PGTiS+GUlOXqHkYcMKwT8SgGFD4CMJmEVw5bOe3nEn8gl4QjeeFz9AEAqgVoWgOUzAGYiMeUA6HmNlgEAUEeu8y9uuDJGk9yelPRIoBLp7ATUujPMCcJCDTCQsFRgHBfMDwW28WlNTYrhoC+AgyhRYMkUcRNmZck565PZURDibYaAXcUrWUsIvvgF4DCi3G+F4CNjqQCkdM8loZkzwOF2mwHWaCJHSC8hKpqAXfnVBRSI0pOs7P0qhntHE1ERX4aUNFBMlmTQ8E7bsE0XsgdRRwWkHvsrAOqA8QCDhKQNqHKhkA42w6G1sEDtY1xJxQFNbDEzTljZGwfoLyaktB6oLQJkAP1NoKBzfSjAb42Xg4IoWgyKu/SmhSRLoU+D6JcMtGtRMRlApuioy1ufcoP6HR1kBnEAuTRHTCJggHMSALEMCEi4g+uvbJpEIwH0CNAD/Jmtb11A0D/A3SEKOAnfEPjox3U3sWVEpnVDahB5MsfrOqlGzkw3JU+tKM7FP5KBn3JPEhCUC1j/GBSIsRNADpkAty3nQyrFg+QeS7MfoS1dwCl67B+IT8D8ZSVtV1Q0liRZOWvNTkHtseU0vJQgOzko8+VBopBHwINybYCo8gBlRnNVEH9bxjSZpF+ElV19kA2ai3rKpvC5rGYRazUUWEhLUpXxGE93l+LdGqrSEX8/bN+0HpFrJB2Az1HnQrW0DMBXaluRWv9WyBO1DA4pRSNKUASaeQE16WgoxV8hMFegkHIYIhwITocpghFBsURyci2eaOJQMZE7qpLRmgo64n0uoUeDaFxScvnqN5Q2iTcZAAJvY0VjCJvIxmUcS7ydCH92ag3dzi1KwGEghBGwk+Ugh+TyK2pZocQR2k6npAZxNodCEKj4DFksC64k0TSoTmsYCIxydhtYj6yeKmQBxfxUo2qhCIn4YSsqEwywDYw0uF4ycedOc4tq/QnUpQDQDECeKGFV2ORAF3QDXY7VdOO9XaM5XHliYf/WORKUUyUBP1wG++BEqnFAJ25WdLlsisGizr0V4EyCTBIWKRo1Ya6uNLDkgAoTkQjSgZgbF54nqCJkzdwQ8UGXzMn+Dko3Pernr2gjoAAASYLXIWCrql3ubw47s0TNNgSqbbVLpMSw0UUHhusnqGbUe+AYcjCGBYZcKAYfoN7maExC/ZtCWAd/stUfVLJFGNyd9aF1HRCb3QOiwmYakOW6grISdDWLPXEDyidg/C6EIF0ryOEmZvjJLSFGmDPk3i4JbOnYutL4AFoqSycfgAS1HUTqfqgjdhACVPoniXUk2hBy3bpLN52IiNbiKjV498lCTPeWBRSYwqM5W2EuStpSqKbFAdklFT+JKVUiyltI2ni0yqXqDxiYEiCcGiXUGC4Jq6qHLprWKbrzBaE0JOYlmxioY6diYUWDhcTsA3EaADxFV1DKONDEASExMEkMD/bVE6gIObQAKzLFGwtAO/r7SJASIDA/2zKCvAEBiQGAvIIUGTrJ6SghQAgBoD0z8AtBMoDQbKJlAEDShpQvIIkgIGygtAhQ8oEgC0GEiI6CdUiSsOzvlCSgSAAgYWDMV5CSgmg/2eULyAaC0B5QAgZXWgAOpk8/AQoPwNKC7ACAWgOuuRGYlF0kB2d0oFnS0A51+AGA8oHjX4CaDZQiSzOpoH4Eyi8g/AkoFQLQEyia60AzQS3fctN0QBIALQX4GroEAMAhQkobKJKF5C0AWgDAeXe7qaANBfgSgInbTt5CZQk9JAJoOTpaC0BJQwukXaHpR1RAG4GO4wVjsiAyIQ9UAYyNECoZDUrQTinHQpFL1JjEGh0JALYAkjGs6A+G1gOwCsCe46Ah0LgAEDOI5we9WER4LQAH3yLbAk+vBDPuHA96kATrYJQ3CUAYBV90+t4T3sVi0AbA90PlUCBLCqtUwogdjlwG9Cz6hgrHBuGfowDuByZN+uFqvof0b6n9J+1/d9F1VwxP9d+tfUfqf2IgBtnMU2YgEv0UBV9h0AADoYBkDiB3AGgYwPoGsDwASA3QCDmmy9AqBrA5gcwMWBLAO0IUFwBOh0xzolKQQabGaTiDUDKB4g6wbQPDR39XgZg9QbOjKtbxt/Bg7JuYPDRADLYuGMwasmfqCAyWuQiJgl5EQ8IFoKAOjGcJBYma8+gDL0HsAfDZEciaMu7EHKEySisUIgyQdYMxdcD2OxOnoDIo962kuAEA74AcC9xEAq+udIgyGDd6hg3ho6DCFv2dAlyCB0Q4bKQQgHbDPhp/dTBpCuH79lQR/REcOhT9MAq8BAyAfsD/BcgeQegFAHw1KAbA/idQIAEwCeqMuDABeApAFGSzfIFQBkBM6qS8IwkdnRzz/25shoz4cOjBBmwrMDwCAYCNsAEDAUkI/fGeg+G0xv+7w14YSN+G4WfR5o0dE4Orjb9bR7w4dCiOZBv9cR8Y+0aSNH0no/7BhakNbgOhfAWoAgCEHoOYYTQ6QNqPAuKCeg3W/UEgA6BJgHUKAFkIcdCExgWRLNcGeyUdi7ROUXGDx30GVE7ZgdrjFoZ/raDCyfrbjHoMoH4Q/yiangHMZY0/swjYQ8Awxv4o8A8BbGVjTRhA60fxPomfoJQZsPAgP2uL4j7Rzo8sHIy9HAj/7Q43PI8OQAxjbJyY+0emP/BZjCBkYgNsgDQHYwPqJYySaOhrGYj7pGkysZ2MpH9j+udUA4AYCFca67x4WKLGKiJ1toCYIUD4mGy/Q/lYJm6PoeRiUAxWzce6eIIHbqRWaN8C6ofC7mVyX5B5ZadPCqH5QdwRUFDCuTHzK0GA6ueeKDovTYINAaJo6BiezBwxV99uF1myaf2EmWjl4NsOGcOhU1yTNQ2oFSfX3xmjodJ7o4yf6P/tLDwp6bppAiiOw1EIx7wxyZ8NcmVjPJvk/+xGhcKCkQIBpNkFTBeDCkYp3M6sepDrHYjy0cU4kbJRAxdj++/9uygoydS/M/vN4oH0VN2BA1k0eapqAiDjaBDXbCE9aGIqTJ4TXoSoI8f3rbgqAwBC6urn94WMURN0MzBcbA7rVSh5DXCJoRZzfxUzkZrE3sZYVxmIjR0RM0dGJN9n0zs4TM3McP0ymn9+Zhk23t5NMm8zeAEaH4HbO9ouzlOcs1GCRjsBqzQwWsxMb7ONn4LIY84N7jKj4bQspAVM5KY2PDm+zcp6M/+xSbWnH4NzXhauMZ64C0mAA7MJ7B/ykXSETACi3LVbkZGq8iNEi1fvbCCXlAgUHk2GZHOfmGLR0LNHNQCH8X5CKMk1SgJhIxgi+mZIiBnXECIA/Am4/XNJYCj/Rnz8lvswBcOhAW/zHRuIPSauCFm5jrHCS51XkI4X2TiDQ9OMYopehbAwRmqd+cOhQTLdfgWgEKEF1675QTQFU7yBaAC6hQyuhgJlHZ2ZRIrUerQQ0DQCShWdvIXkNKG51KAhQLO6K3lelANBMoaukgMrul0NAGgDRgK+UFsALGEDkoWgLbuyi+7OmDABoDTuKs1WGdw1loNlFt0B7aATuzKJ1fisCAemvwNK8VZaA8gY9iYJPQICFAkA1dzOhndKGat8XJL5FmSyQGQbOrL9bIVfUmLTEGAbrZexvbUGb1+1X0PJgrPXpCT3X5ERWG0JOA73QRqYVBfHUmJau4ArAtiMWJpFwC+AEJQ+lxOoHw1LpJ9LQO6/9tsTNYfrAEKI6+net6AgAA -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"retries":3,"retryAfter":16}}}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
R/contact_matrix.R (1)

307-337: Add a regression test covering the NA dayofweek bug fix.

The stated bug fix ("participants with NA dayofweek were previously treated as weekend; they now receive an average weight across all days") has no corresponding regression test. A test exercising weigh_dayofweek = TRUE with a participant whose dayofweek is NA would confirm the fix holds and guard against regressions.

Based on learnings, regression tests should be added for bug fixes. Would you like me to draft the test case for this scenario?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/contact_matrix.R` around lines 307 - 337, Add a unit/regression test that
ensures participants with NA dayofweek are given the average weight across all
days when weigh_dayofweek = TRUE: create a minimal survey input (participants
data.frame) containing at least one participant with dayofweek = NA and other
participants on weekday and weekend, call the contact_matrix (or the same entry
point that runs the weigh(...) logic) with weigh_dayofweek = TRUE, then assert
that the weight assigned to the NA participant equals the expected mean of the
weekday and weekend group weights (or matches the computed average from the
non-NA groups). Reference the weigh_dayofweek flag, the survey$participants
dayofweek column, and the weigh(...) path that sets is.weekday so the test
exercises the exact code path fixed in contact_matrix.R.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@R/contact_matrix.R`:
- Around line 307-327: The is.weekday assignment currently treats NA dayofweek
as FALSE causing NA participants to be grouped with weekends; change the
assignment in the weigh_dayofweek block so is.weekday is NA whenever dayofweek
is NA (e.g., using a conditional vectorized assignment or data.table::fifelse
checking is.na(dayofweek) before computing dayofweek %in% 1:5) on
survey$participants; keep the rest of the weigh_dayofweek logic (weigh(),
target/groups) intact so return_part_weights can later distinguish true weekends
from NA dayofweek entries.

---

Duplicate comments:
In `@NAMESPACE`:
- Line 7: Ensure the S3 dispatch chain for copy is complete by adding
S3method(copy,default) and export(copy) to the NAMESPACE, remove
importFrom(data.table,copy) (keeping import(data.table)), and confirm
copy.default calls data.table::copy() so copy.contact_survey is dispatched
correctly instead of data.table::copy being invoked directly.

In `@R/compute-matrix.R`:
- Around line 27-31: No action required: the compute_matrix function correctly
adds the weight_threshold parameter (default NULL) and the copy(survey) call
properly dispatches to copy.contact_survey preserving deep-copy behavior; no
code changes needed for compute_matrix, weight_threshold, or
warn_multiple_observations delegation.

---

Nitpick comments:
In `@R/contact_matrix.R`:
- Around line 307-337: Add a unit/regression test that ensures participants with
NA dayofweek are given the average weight across all days when weigh_dayofweek =
TRUE: create a minimal survey input (participants data.frame) containing at
least one participant with dayofweek = NA and other participants on weekday and
weekend, call the contact_matrix (or the same entry point that runs the
weigh(...) logic) with weigh_dayofweek = TRUE, then assert that the weight
assigned to the NA participant equals the expected mean of the weekday and
weekend group weights (or matches the computed average from the non-NA groups).
Reference the weigh_dayofweek flag, the survey$participants dayofweek column,
and the weigh(...) path that sets is.weekday so the test exercises the exact
code path fixed in contact_matrix.R.

@sbfnk
Copy link
Collaborator Author

sbfnk commented Feb 19, 2026

Fixed: Removed all stale @importFrom data.table copy annotations from the 5 files (subset-contact-survey.R, weigh.R, contact_matrix.R, compute-matrix.R, deprecated.R). The NAMESPACE no longer has the conflicting import.

Previously NA dayofweek was incorrectly classified as is.weekday=FALSE
(since NA %in% 1:5 returns FALSE). Now uses fifelse to preserve NA,
allowing these participants to be distinguished from true weekend days.

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NAMESPACE`:
- Line 7: Update the roxygen comments for the exported S3 generic so the ?copy
help page documents that this package exports S3method(copy,default) which may
mask data.table::copy depending on load order; edit the roxygen block in
R/subset-contact-survey.R to add a Note section explaining masking behavior and
that copy.default delegates to data.table::copy for internal calls, and instruct
users to call data.table::copy explicitly if they need the data.table
implementation. Also add a short NEWS entry describing the new exported copy
generic and the load-order caveat so users are informed of the potential
masking.

sbfnk and others added 3 commits February 19, 2026 16:57
Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
NA / population = NA, so NAs can propagate through per_capita()
unlike symmetrise() and split_matrix() which need complete data.

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
Avoids masking data.table::copy() and simplifies the code.
Internal function, not exported.

Co-authored-by: sbfnk-bot <242615673+sbfnk-bot@users.noreply.github.com>
@sbfnk
Copy link
Collaborator Author

sbfnk commented Feb 19, 2026

Refactored: Replaced the copy() S3 generic with a simple internal copy_survey() function. This avoids the data.table::copy() masking issue entirely.

@sbfnk sbfnk added this pull request to the merge queue Feb 19, 2026
Merged via the queue into main with commit e9b1a75 Feb 19, 2026
13 checks passed
@sbfnk sbfnk deleted the pipeline-workflow branch February 19, 2026 22:03
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.

Split up contact_matrix function

1 participant