Skip to content

fix: normalize GaussMarkov secondary seed to 32 bits [#1460] - #1560

Merged
schaubh merged 5 commits into
AVSLab:developfrom
robotrocketscience:feature/bsk-1460--secondary-seed-width
Oct 2, 2026
Merged

schaubh merged 5 commits into
AVSLab:developfrom
robotrocketscience:feature/bsk-1460--secondary-seed-width

Conversation

@robotrocketscience

Copy link
Copy Markdown
Contributor

Description

GaussMarkov::deriveSecondarySeed() (added in #1526, commit 8c0f4ba) XORs the base seed with a 64-bit discriminator:

constexpr uint64_t secondaryStreamDiscriminator = 0x9E3779B97F4A7C15ULL;
const uint64_t candidateSeed = baseSeed ^ secondaryStreamDiscriminator;

SysModel::RNGSeed is uint32_t (sys_model.h:50), so the returned value always carries 0x9E3779B9 in its upper word. Every consumer then narrows it with static_cast<std::minstd_rand::result_type>, and that type is std::uint_fast32_t, whose width is implementation defined. The secondary engine therefore seeds differently depending on the standard library Basilisk was built against:

macOS 25.6 / Apple libc++ Linux (CachyOS) / libstdc++
sizeof(std::minstd_rand::result_type) 4 8
narrowed seed, default RNGSeed 0x0000000064e7b6c4 0x9e3779b964e7b6c4
first draw 128424993 1147871987

That contradicts the function's own contract at gauss_markov.h:54 ("Seed for a distinct, repeatable secondary random stream"). It affects the four derived streams: CSS fault noise (coarseSunSensor.cpp:40), IMU gyro error (imuSensor.cpp:36), and the magnetometer and tempMeasurement spike generators.

The fix masks both seeds to 32 bits so the narrowing casts are no-ops on every platform. The primary stream is untouched — it is seeded directly from the 32-bit RNGSeed.

Scope note

This is not a regression in observable noise values. The consuming distributions are implementation-defined regardless: from an identical engine state, std::normal_distribution yields -0.078154 on libc++ against -0.077279 on libstdc++, so per-platform noise output already differed and still will. What changes is that the engine seed is now derived identically everywhere, which is what the function documents.

A related pre-existing case is left alone deliberately: setRNGSeed() and initializeRNG() narrow a uint64_t seed the same way, so a caller passing a seed above 2^32 directly still gets platform-dependent behavior. That predates #1526 and changing it would alter existing results, so it seemed better raised separately than folded in here.

Verification

Built and run on Linux (GCC 16.2.1, Release):

  • test_gaussMarkov: 7/7 pass, including the new secondarySeedIsPlatformIndependent.
  • Mutation check — reverting only the mask and rebuilding makes the new test fail (6 pass / 1 fail), reporting first draw 1147871987 against the expected 128424993, and flagging every probe seed as wider than 32 bits. The test fails on both platforms without the fix, since the unmasked candidate is returned in full.
  • Seed consumers: 63 tests pass across coarseSunSensor, magnetometer, imuSensor, starTracker and tempMeasurement unit tests.
  • src/utilities/MonteCarlo/_UnitTests: 36 pass.
  • test_saturate, test_discretize: pass.
  • pre-commit on all four touched files: pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_012CrRpn7XEWBLLHagNPzQxP

@schaubh schaubh 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.

The seed-width diagnosis is correct for the current uint32_t sensor callers. Please address the two inline findings: preserve the public utility's distinct-stream guarantee for 64-bit input seeds, and qualify the release note's portability and compatibility claims.

Validation on the PR head: 7 GaussMarkov tests and 63 sensor tests passed. The 64-bit collision was reproduced on macOS with an explicit equivalent 64-bit linear congruential engine, rather than a native Linux build.

Comment thread src/architecture/utilities/gauss_markov.cpp Outdated
Comment thread docs/source/Support/bskReleaseNotesSnippets/1460-gauss-markov-seed-width.rst Outdated

@schaubh schaubh 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.

Good catch. Found small edge case and documentation update that is needed.

@schaubh schaubh self-assigned this Sep 24, 2026
@schaubh schaubh added bug Something isn't working enhancement New feature or request labels Sep 24, 2026
@schaubh schaubh added this to Basilisk Sep 24, 2026
@schaubh
schaubh force-pushed the feature/bsk-1460--secondary-seed-width branch from 8e12415 to 6bc9f45 Compare September 24, 2026 20:10
schaubh added a commit to robotrocketscience/basilisk that referenced this pull request Sep 24, 2026
Check secondary seeds against both 32-bit and 64-bit primary engine
states while preserving primary seeding and secondary seed portability.

Add regression coverage for wide-seed collisions and zero/one
normalization. Update API documentation, known issues, and release notes.

Validation: 8 GaussMarkov tests and 63 sensor tests passed.
schaubh added a commit to robotrocketscience/basilisk that referenced this pull request Sep 24, 2026
Limit the portability claim to raw engine sequences and explain that
sensor outputs can still differ across standard libraries.

Document changes to existing secondary sequences on builds with a
64-bit engine result type. Update the matching known-issue entry.

Validation: Sphinx rendering and pre-commit checks passed.
@schaubh

schaubh commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

@robotrocketscience , I had addressed the two PR issues on this branch. Please let me know if you agree, I can then approve and merge this branch.

@schaubh

schaubh commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@robotrocketscience , are you good with my edits?

robotrocketscience and others added 5 commits October 2, 2026 08:36
deriveSecondarySeed XORs the base seed with a 64-bit discriminator, so the
returned value always carries bits above 32. Every consumer narrows it with
static_cast<std::minstd_rand::result_type>, and that type is std::uint_fast32_t,
whose width is implementation defined -- 32 bit with libc++, 64 bit with
libstdc++ on LP64. The secondary engine therefore seeded differently per
platform, contradicting the function's own contract of a repeatable stream.

Mask both seeds to 32 bits so the narrowing casts are no-ops everywhere.
Asserts the derived seed for the default RNGSeed equals 0x64e7b6c4 and that
its first draw is 128424993, and that no base seed -- including one that
fills all 64 bits -- yields a derived seed wider than 32 bits. The exact-value
checks fail on every platform without the mask, since the unmasked candidate
is returned in full.
Check secondary seeds against both 32-bit and 64-bit primary engine
states while preserving primary seeding and secondary seed portability.

Add regression coverage for wide-seed collisions and zero/one
normalization. Update API documentation, known issues, and release notes.

Validation: 8 GaussMarkov tests and 63 sensor tests passed.
Limit the portability claim to raw engine sequences and explain that
sensor outputs can still differ across standard libraries.

Document changes to existing secondary sequences on builds with a
64-bit engine result type. Update the matching known-issue entry.

Validation: Sphinx rendering and pre-commit checks passed.
@schaubh
schaubh force-pushed the feature/bsk-1460--secondary-seed-width branch from 6bc9f45 to 81d8b08 Compare October 2, 2026 14:37
@schaubh
schaubh self-requested a review October 2, 2026 15:55
@schaubh
schaubh merged commit f6047ca into AVSLab:develop Oct 2, 2026
7 checks passed
schaubh added a commit that referenced this pull request Oct 2, 2026
Check secondary seeds against both 32-bit and 64-bit primary engine
states while preserving primary seeding and secondary seed portability.

Add regression coverage for wide-seed collisions and zero/one
normalization. Update API documentation, known issues, and release notes.

Validation: 8 GaussMarkov tests and 63 sensor tests passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

coarseSunSensor senNoiseStd produces an unbounded random walk by default (walkBounds=-1 disables clamping; comment says the opposite)

2 participants