Skip to content

feat(gen5): v26 PPG→HR via normalized ACF peak - #37

Closed
Brackyt wants to merge 3 commits into
OpenStrap:mainfrom
Brackyt:feat/gen5-v26-ppg-hr
Closed

feat(gen5): v26 PPG→HR via normalized ACF peak#37
Brackyt wants to merge 3 commits into
OpenStrap:mainfrom
Brackyt:feat/gen5-v26-ppg-hr

Conversation

@Brackyt

@Brackyt Brackyt commented Aug 4, 2026

Copy link
Copy Markdown

Status: Draft — blocked on hardware validation

Do not merge. Real-hardware validation of deriveHrFromGen5PpgWaveform failed against a WHOOP 5 export (fw 50.40.1.0).

Real-hardware validation failure

Metric Result
Evaluable rolling windows 22 (10–12 s each)
Measured v18 HR 90–100 bpm
Current ACF-derived HR 29–160 bpm

Synthetic sine tests (72/120/45 bpm, noisy, flatline, under-length) pass but are insufficient. No PPG-derived HR should ship until the algorithm is reworked and re-validated on hardware.

What this PR contains (research)

  • deriveHrFromGen5PpgWaveform — pure ACF peak picker on concatenated gen5 v26 PPG int16 samples (24 Hz). Abstains on thin/flat/noisy windows; never fabricates BPM.
  • Minimum window raised to 240 samples (~10 s @ 24 Hz) so resting 40–55 bpm can see ≥4 beats.

Known follow-ups (valid reviewer feedback, deferred)

  • Keep window-duration rule consistent with a configurable sampleHz parameter.
  • Evaluate real ACF neighbors at both BPM boundaries; add monotonic-trend abstention.

Test plan

  • dart test test/gen5_ppg_hr_test.dart (synthetic only — insufficient for ship)
  • Re-validate on hardware after algorithm rework
  • Edge #190 remains blocked until this lands with passing hardware validation

Brackyt added 2 commits August 4, 2026 20:44
Derives BPM from 24 Hz waveform bursts (length≥24, variance gate, 25–230 bpm lag search) and abstains on flatline / no clear peak.
Low resting BPM (40–55) needs ≥4 beats in-window; four 1s bursts
cannot resolve that honestly. Raise min samples to 240 (10s @ 24Hz)
and expand tests: low-HR 12s window, noisy sine, under-length abstain.
Copilot AI lite review requested due to automatic review settings August 4, 2026 20:06
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ed8e2752-81ae-4a86-bf28-643adf481817

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds a Gen5 v26 PPG BPM derivation function, exports it from the public library, and adds regression tests for valid and invalid waveform inputs.

Changes

Gen5 PPG heart-rate derivation

Layer / File(s) Summary
PPG derivation and public export
lib/src/onehz/foundations/gen5_ppg_hr.dart, lib/onehz.dart
Adds validation, mean-centering, normalized autocorrelation from 25–230 BPM, local-peak selection, nullable rounded BPM output, and the public export.
PPG waveform regression tests
test/gen5_ppg_hr_test.dart
Adds configurable synthetic PPG generation and tests for 72, 120, low-rate, noisy, flatline, and insufficient waveforms.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the Gen5 v26 PPG-to-heart-rate derivation added by the pull request.

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.

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 a Gen5 v26 PPG→HR derivation utility to the 1Hz analytics layer, using a normalized autocorrelation (ACF) peak-picking approach that abstains on low-quality input rather than fabricating BPM.

Changes:

  • Introduces deriveHrFromGen5PpgWaveform to estimate HR from concatenated 24 Hz Gen5 PPG bursts via normalized ACF peak selection.
  • Exports the new foundation API via lib/onehz.dart.
  • Adds unit tests covering nominal, low-HR (longer window), noisy, flatline, and too-short input scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
test/gen5_ppg_hr_test.dart Adds regression tests for the new Gen5 PPG→HR derivation behavior and abstention cases.
lib/src/onehz/foundations/gen5_ppg_hr.dart Implements the normalized ACF-based HR derivation function and minimum-window/quality gating.
lib/onehz.dart Exports the new Gen5 PPG HR foundation API from the 1Hz barrel.
Suppressed comments (2)

lib/src/onehz/foundations/gen5_ppg_hr.dart:17

  • The minimum-length check uses a fixed kGen5PpgHrMinSamples (240 @ 24 Hz) but the function also accepts sampleHz. If a caller passes a different sampleHz, the current guard no longer represents a ~10s window as documented (e.g., 48 Hz would allow only 5s). Either drop sampleHz or scale the min-sample requirement by sampleHz.
int? deriveHrFromGen5PpgWaveform(List<int> samples, {double sampleHz = 24.0}) {
  if (samples.length < kGen5PpgHrMinSamples || sampleHz <= 0) return null;

lib/src/onehz/foundations/gen5_ppg_hr.dart:65

  • This comment says ties are broken toward a “physiologically mid-range lag”, but the code below does not implement any mid-range tie-breaker (it only uses c + 0.01 * prominence). Please update the comment to match the actual scoring (or implement the tie-breaker if intended).
    // Prefer higher ACF; break ties toward the physiologically mid-range lag.
    final score = c + 0.01 * prominence;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

///
/// Pure, isolate-safe. Absent / degenerate input returns null — never a
/// fabricated BPM.
library;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not an analyzer issue: library; is a valid unnamed library directive under this repo's SDK constraint (Dart ^3.5). dart analyze on gen5_ppg_hr.dart reports no issues.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/onehz/foundations/gen5_ppg_hr.dart`:
- Around line 37-38: Update the lag-selection logic around minLag, maxLag, and
the real-neighbor checks to evaluate ACF samples immediately outside both
accepted BPM bounds instead of using artificial negative-infinity neighbors.
Interpolate only genuine local maxima, then apply the existing 25–230 BPM gate
to the resulting estimate; monotonic ACF trends must abstain with null rather
than select a boundary lag. Add coverage for the 230 BPM boundary and decreasing
monotonic input.
- Around line 16-17: Update deriveHrFromGen5PpgWaveform to enforce the minimum
10-second window based on sampleHz rather than the fixed kGen5PpgHrMinSamples
threshold. Preserve custom sample rates, reject non-finite or non-positive
sampleHz values in the existing guard, and return null when samples do not cover
the required duration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7cc5d347-50f8-4abc-9c08-5c94e80bea9a

📥 Commits

Reviewing files that changed from the base of the PR and between f0d1153 and b3e7b88.

📒 Files selected for processing (3)
  • lib/onehz.dart
  • lib/src/onehz/foundations/gen5_ppg_hr.dart
  • test/gen5_ppg_hr_test.dart

Comment on lines +16 to +17
int? deriveHrFromGen5PpgWaveform(List<int> samples, {double sampleHz = 24.0}) {
if (samples.length < kGen5PpgHrMinSamples || sampleHz <= 0) return null;

@coderabbitai coderabbitai Bot Aug 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the window-duration rule consistent with sampleHz.

240 samples equal 10 seconds only at 24 Hz. With sampleHz: 48, a five-second 45 BPM sine can return 45, although it contains only 3.75 beats. If custom rates are supported, calculate the minimum from a 10-second duration. Otherwise, remove the override or reject rates other than 24. Reject non-finite rates in the same guard.

Proposed duration gate
+const double _kGen5PpgHrMinWindowSeconds = 10.0;
+
 int? deriveHrFromGen5PpgWaveform(List<int> samples, {double sampleHz = 24.0}) {
-  if (samples.length < kGen5PpgHrMinSamples || sampleHz <= 0) return null;
+  if (!sampleHz.isFinite || sampleHz <= 0) return null;
+  final minSamples = (sampleHz * _kGen5PpgHrMinWindowSeconds).ceil();
+  if (samples.length < minSamples) return null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int? deriveHrFromGen5PpgWaveform(List<int> samples, {double sampleHz = 24.0}) {
if (samples.length < kGen5PpgHrMinSamples || sampleHz <= 0) return null;
const double _kGen5PpgHrMinWindowSeconds = 10.0;
int? deriveHrFromGen5PpgWaveform(List<int> samples, {double sampleHz = 24.0}) {
if (!sampleHz.isFinite || sampleHz <= 0) return null;
final minSamples = (sampleHz * _kGen5PpgHrMinWindowSeconds).ceil();
if (samples.length < minSamples) return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/onehz/foundations/gen5_ppg_hr.dart` around lines 16 - 17, Update
deriveHrFromGen5PpgWaveform to enforce the minimum 10-second window based on
sampleHz rather than the fixed kGen5PpgHrMinSamples threshold. Preserve custom
sample rates, reject non-finite or non-positive sampleHz values in the existing
guard, and return null when samples do not cover the required duration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Brackyt, acknowledged. The finding remains valid, but the hardware-validation mismatch makes deferring the configurable sampleHz work appropriate for this PR.

Would you like me to create a GitHub follow-up issue for the duration gate and ACF validation work?

You are interacting with an AI system.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Valid future work — window duration should stay consistent with a configurable sampleHz rather than hard-coded 24 Hz assumptions. Deferred; PR is Draft until hardware validation passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Brackyt, acknowledged.

The sampleHz-based window-duration gate remains deferred. I will keep the finding unresolved for this PR. Hardware validation is the current prerequisite for further changes to deriveHrFromGen5PpgWaveform.

I can create a GitHub follow-up issue if you want to track the duration gate and ACF hardware-validation work separately.

You are interacting with an AI system.

Comment on lines +37 to +38
final minLag = math.max(1, (60.0 * sampleHz / 230.0).ceil());
final maxLag = math.min(n - 1, (60.0 * sampleHz / 25.0).floor());

@coderabbitai coderabbitai Bot Aug 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Evaluate real ACF neighbors at both BPM boundaries.

At 24 Hz, minLag is 7. Therefore, round(1440 / bestLag) cannot exceed 206, despite the documented 230 BPM maximum. The artificial double.negativeInfinity neighbor also accepts a nonpeak boundary. List<int>.generate(240, (i) => i) has a decreasing ACF, but this code selects lag 7 and returns 206 instead of null.

Calculate ACF values outside both accepted bounds. Locate and interpolate a real local maximum. Then apply the 25–230 BPM gate. Add 230 BPM and monotonic-trend abstention tests.

Also applies to: 42-43, 59-62

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/onehz/foundations/gen5_ppg_hr.dart` around lines 37 - 38, Update the
lag-selection logic around minLag, maxLag, and the real-neighbor checks to
evaluate ACF samples immediately outside both accepted BPM bounds instead of
using artificial negative-infinity neighbors. Interpolate only genuine local
maxima, then apply the existing 25–230 BPM gate to the resulting estimate;
monotonic ACF trends must abstain with null rather than select a boundary lag.
Add coverage for the 230 BPM boundary and decreasing monotonic input.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Brackyt, acknowledged. The finding remains valid and is deferred from this draft.

Would you like me to create a follow-up GitHub issue for boundary peak validation and monotonic-trend abstention?

You are interacting with an AI system.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Valid future work — real ACF neighbor evaluation at BPM boundaries and monotonic-trend abstention are needed before ship. Deferred; PR is Draft pending hardware rework (current ACF: 29–160 bpm vs measured 90–100 bpm on 22 evaluable windows).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Brackyt, acknowledged. The finding remains unresolved and required before release.

The current hardware result shows a material accuracy problem. deriveHrFromGen5PpgWaveform returns 29–160 BPM where measured values are 90–100 BPM across 22 evaluable windows. The draft status is appropriate while the hardware rework continues.


✏️ Learnings added
Learnt from: Brackyt
Repo: OpenStrap/analytics PR: 37
File: lib/src/onehz/foundations/gen5_ppg_hr.dart:37-38
Timestamp: 2026-08-04T22:56:42.187Z
Learning: For `lib/src/onehz/foundations/gen5_ppg_hr.dart`, `deriveHrFromGen5PpgWaveform` must evaluate real autocorrelation neighbors at both BPM boundaries and abstain on monotonic ACF trends before release. During hardware validation, the current ACF returned 29–160 BPM for 22 evaluable windows where measured heart rate was 90–100 BPM. The related pull request remains a draft pending hardware rework.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

The header promises "absent / degenerate input returns null -- never a
fabricated BPM". Measured against this implementation, it did not hold:

    baseline wander (slow sine drift, no cardiac content) -> 206 bpm
    linear ramp (pure DC drift)                           -> 206 bpm
    single step edge                                      -> 206 bpm

206 bpm is the TOP of the 25-230 search range, and it comes out with full
confidence from signals containing no heartbeat at all.

ROOT CAUSE: the peak search considered the BOUNDARIES of its own lag range.
At `lag == minLag` the left neighbour was `double.negativeInfinity`, so the
`c <= left` rejection could never fire; the prominence fallback then
substituted `c - 1`, making prominence exactly 1.0 -- maximal. So the shortest
lag was an unconditionally valid "peak", and every monotonically DECAYING
autocorrelation was accepted there.

That is the opposite of a rare edge case. A smoothly decaying ACF is precisely
what baseline wander, a DC drift and motion artifacts produce -- so the
quieter and smoother the input, the more confident the fabricated tachycardia.

FIX: a periodicity claim needs a real turning point, so only INTERIOR lags are
peak candidates (both neighbours computed, both strictly lower). All three
cases above now abstain. Real cardiac signals are unaffected -- clean sines at
45/60/75/100/140 bpm still resolve to 45/60/76/103/144 (integer-lag
quantization at 24 Hz, unchanged by this).

This matters beyond analytics: edge#190 consumes this to write a DERIVED HR
into `decoded_onehz`. A fabricated 206 bpm would have been persisted as a real
resting heart rate for any second the strap did not measure itself.

PARTLY REFUTING the review note that prompted this: it claimed white noise
yields a confident BPM "~25% of the time". Measured over 400 trials it is
6/400 = 1.5%, both before and after this change -- noise was never the problem.
The smooth-signal boundary case was, and that one is 100% reproducible.

7 tests added, mutation-verified: restoring the boundary-inclusive search
fails exactly the four fabrication tests. Full suite 392 passing.
@abdulsaheel

Copy link
Copy Markdown
Contributor

Reviewed and pushed 51bf6ae.

The "never a fabricated BPM" guarantee didn't hold

The header promises "Absent / degenerate input returns null — never a fabricated BPM." Measured against this implementation:

baseline wander (slow sine drift, no cardiac content) -> 206 bpm
linear ramp (pure DC drift)                           -> 206 bpm
single step edge                                      -> 206 bpm

206 bpm is the top of the 25–230 search range, returned with full confidence from signals containing no heartbeat at all.

Root cause

The peak search considered the boundaries of its own lag range:

final left = lag > minLag ? acf[lag - 1] : double.negativeInfinity;
...
if (c <= left || c <= right) continue;
final prominence = c - math.max(left.isFinite ? left : c - 1, ...);

At lag == minLag the left neighbour is -infinity, so the c <= left rejection can never fire. The prominence fallback then substitutes c - 1, making prominence exactly 1.0 — maximal. So the shortest lag was an unconditionally valid "peak", and every monotonically decaying autocorrelation got accepted there.

That's the opposite of a rare edge case. A smoothly decaying ACF is precisely what baseline wander, DC drift and motion artifacts produce — so the quieter and smoother the input, the more confident the fabricated tachycardia.

Fix

A periodicity claim needs a real turning point, so only interior lags are peak candidates (both neighbours computed, both strictly lower). All three cases above now abstain.

Real signals are unaffected — clean sines still resolve:

input before after
45 bpm 45 45
60 bpm 60 60
75 bpm 76 76
100 bpm 103 103
140 bpm 144 144

(The small offsets are integer-lag quantization at 24 Hz, unchanged by this.)

Why this matters beyond analytics

edge#190 consumes this to write a derived HR into decoded_onehz. A fabricated 206 bpm would have been persisted as a real resting heart rate for any second the strap didn't measure itself — and then fed the whole downstream stack. Worth landing this before that one.

(Separately, on edge#190 I made derived samples unable to evict a measured row, so at least this could never have overwritten real data — but on unmeasured seconds it would have stood as fact.)

Partly refuting the finding that prompted this

The review note claimed white noise yields a confident BPM "~25% of the time". Measured over 400 trials it's 6/400 = 1.5%, both before and after this change — noise was never really the problem. The smooth-signal boundary case was, and that one is 100% reproducible.

Verification

7 tests added, mutation-verified: restoring the boundary-inclusive search fails exactly the four fabrication tests and nothing else. Full analytics suite 392 passing.

@abdulsaheel

Copy link
Copy Markdown
Contributor

Reading back through the thread after pushing 51bf6ae@coderabbitai had already found the boundary bug, and @Brackyt deferred it. Worth closing that loop explicitly:

"At 24 Hz, minLag is 7. Therefore, round(1440 / bestLag) cannot exceed 206, despite the documented 230 BPM maximum. The artificial double.negativeInfinity neighbor also accepts a nonpeak boundary."

That's the same defect, and CodeRabbit got there first — including the detail I'd missed, that the documented 230 bpm ceiling is unreachable at 24 Hz because minLag quantizes to 7. My commit fixes the non-peak-boundary half (interior peaks only, which is what stops the 206 bpm fabrication on wander/ramp/step). The unreachable-230 half is inherent to integer lags at 24 Hz and is really a documentation fix — the honest range is 25–206, not 25–230.

CodeRabbit's other finding — that 240 samples is 10 s only at 24 Hz, so a custom sampleHz breaks the window rule — is also still valid and still unaddressed. I left it alone given the below.

The harder question

@Brackyt's hardware validation is the thing that actually decides this PR:

ACF: 29–160 bpm vs measured 90–100 bpm on 22 evaluable windows

My fix does not close that gap. It removes a fabrication mode (confident 206 bpm from signals with no cardiac content) and it keeps clean sines resolving correctly, but a 29–160 spread against a true 90–100 is a signal-processing problem several layers deeper than peak-boundary handling — most likely bandpass/detrending before the ACF, plus the burst-adjacency issue CodeRabbit flagged on edge#190 (the buffer will concatenate captures with missing intermediate bursts, which would smear the autocorrelation exactly like this).

Combined with @Brackyt's note on edge#190 that the whole v26 PPG line was built on a premise that turned out to be wrong — the strap does give usable v18 data — my recommendation is to close this along with edge#190 rather than keep polishing it. A derived bpm with no RR is strictly worse than measured v18 HR + RR, and it costs real complexity to get right.

If it's kept, the boundary fix is worth having regardless, since the function would otherwise fabricate on any smooth artifact.

Your call — I'm happy either way, and I'd rather flag it than quietly keep improving something that shouldn't ship.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@abdulsaheel I agree with this assessment.

The interior-peak requirement fixes the boundary fabrication defect. It does not validate the derived-HR algorithm for production use.

The hardware result is the deciding evidence. A 29–160 BPM output range against measured 90–100 BPM is not suitable for writing derived HR to decoded_onehz.

Closing this PR and edge#190 is the safer decision if usable v18 measured HR and RR are available. This avoids persisting a lower-confidence value when the device provides measured data.

If the work remains active, keep these items open:

  • Document the effective 24 Hz output range as 25–206 BPM, or change the lag calculation to support the documented upper bound.
  • Derive the minimum sample count from sampleHz so the window remains approximately 10 seconds.
  • Preserve burst timing or reject non-contiguous captures before autocorrelation.
  • Validate against representative hardware data before any derived value is persisted.

🐇


✏️ Learnings added
Learnt from: abdulsaheel
URL: https://github.com/OpenStrap/analytics/pull/37

Timestamp: 2026-08-06T19:40:52.497Z
Learning: For Gen5 PPG in this analytics repository, usable v18 measured HR and RR data is available. Derived HR from v26 PPG has no RR and should not replace the measured v18 HR and RR path without hardware validation that demonstrates adequate accuracy.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@Brackyt

Brackyt commented Aug 6, 2026

Copy link
Copy Markdown
Author

Same conclusion, closing this

@Brackyt Brackyt closed this Aug 6, 2026
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.

3 participants