With `pcg32` and `pcg64`, the streams you select are discarded when you call either `seed()` or `seed(itype)`
With pcg32 and pcg64, the streams you select are discarded when you call either seed() or seed(itype).
When the stream_mixin is specific_stream, functions seed() and seed(itype) have the—perhaps unintended—side effect of resetting the stream to (default_increment<itype>() >> 1). The function seed(itype) is the one which takes a seed of type itype.
This is a consequence of the placement-new technique used by the seeding routines.
Rather than directly updating engine state, which would involve "conditioning" the seed in the same way as the constructor, the PCG seeding routines construct a new engine, on top of the existing one, using "placement-new" syntax. The actual seeding is performed by the engine constructor.
The engine created when the user invokes either seed() or seed(itype) reinstalls the default stream, overwriting any stream that may have been selected by the user.
I view this as a bug, but perhaps it is intentional. Users, however, will be surprised, when they call a function to set the seed, and discover that the stream has changed.
If asked, I would recommend ditching the placement-new allocation, in favor of letting the seeding functions install the new seed "manually." This could be accomplished by isolating the "stirring/conditioning" of the seed in its own function.
Here is the one I am using:
itype constexpr stir(itype const seed) noexcept
{
// This is where seeds are "conditioned." This function is
// called both by ctors and seeding routines.
auto constexpr const three = static_cast<itype>(3u);
return static_cast<itype> (
this->is_mcg
? seed | three
: (seed + increment()) * multiplier() + increment()
);
}
With this function in hand, the seeding routines are a breeze. The default argument below, allows a single function to replace both seed() and seed(itype).
public:
static constexpr const itype default_seed
{
// Every random number engine in the C++ Standard Library
// has a public static constant named default_seed.
static_cast<itype>(0xcafef00dd15ea5e5ULL)
};
void constexpr seed(itype const seed = default_seed) noexcept
{
// This seeding function DOES NOT overwrite any stream that
// may have been selected by the user when the stream_mixin
// is specific_stream. The same function in O'Neill's
// program DOES overwrite the stream, resetting it to the
// value it was given during construction.
state_ = stir(seed);
}
A similar change to the constructor accomplishes the same thing, while making its code more readable.
constexpr engine(itype const seed = default_seed) noexcept
: state_{ stir(seed) }
{}
Here is a short program you can run to verify that this bug does, indeed, exist.
// main.cpp
#include <cassert>
#include <iostream>
#include "pcg_random.hpp"
using state_type = pcg32::state_type;
state_type arbitrary_stream()
{
// Choose an arbitrary stream which is different from
// the stream established by the default constructor.
pcg32 e;
state_type const default_stream{ e.stream() };
state_type const clear_msb{ static_cast<state_type>(-1LL) >> 1 };
state_type const arbitrary_stream{ ~default_stream & clear_msb };
assert(default_stream != arbitrary_stream);
e.set_stream(arbitrary_stream);
assert(e.stream() == arbitrary_stream);
return arbitrary_stream;
}
//----------------------------------------------------------------------
bool seed_default_resets_stream()
{
pcg32 e;
state_type const default_stream{ e.stream() };
state_type const another_stream{ arbitrary_stream() };
assert(another_stream != default_stream);
e.set_stream(another_stream);
assert(e.stream() == another_stream);
e.seed();
assert(e.stream() == default_stream);
return e.stream() == default_stream;
}
//----------------------------------------------------------------------
bool seed_itype_resets_stream()
{
pcg32 e;
state_type const default_stream{ e.stream() };
state_type const another_stream{ arbitrary_stream() };
assert(another_stream != default_stream);
e.set_stream(another_stream);
assert(e.stream() == another_stream);
state_type const arbitrary_seed{ 1u };
e.seed(arbitrary_seed);
assert(e.stream() == default_stream);
return e.stream() == default_stream;
}
//----------------------------------------------------------------------
int main()
{
std::cout << "Test pcg_random issue 94"
"\nhttps://github.com/imneme/pcg-cpp/issues/94"
"\n"
"\nGiven a `pcg32` engine named `e`, does calling `e.seed()` "
"\nreset the stream to the value used by the default constructor? "
"\n"
"\n Answer: " << (seed_default_resets_stream() ? "yes" : "no") <<
"\n"
"\n"
"\nGiven a `pcg32` engine named `e`, and an arbitrary seed "
"\nnamed `s`, does calling `e.seed(s)` reset the stream to "
"\nthe value used by the default constructor? "
"\n"
"\n Answer: " << (seed_itype_resets_stream() ? "yes" : "no") <<
"\n\n";
}
// end file: main.cpp
Here is the output:
Test pcg_random issue 94
https://github.com/imneme/pcg-cpp/issues/94
Given a `pcg32` engine named `e`, does calling `e.seed()`
reset the stream to the value used by the default constructor?
Answer: yes
Given a `pcg32` engine named `e`, and an arbitrary seed
named `s`, does calling `e.seed(s)` reset the stream to
the value used by the default constructor?
Answer: yes
It seems seeding with std::seed_seq doesn't have this issue, the check here fails:
e.stream() == default_stream -- the streams ARE different
pretty easy to seed it, the constructor seems to take in two of state_type so the following should seed it correctly:
std::array<state_type, 2> seed_data;
// put in seed to seed_data
seed_data[0] = ...;
seed_data[1] = ...;
std::seed_seq seq(seed_data.cbegin(), seed_data.cend());
e.seed(seq);
It seems seeding with std::seed_seq doesn't have this issue, ...
Calling seed with a seed sequence does not reset the "default" stream. It does, however, discard the existing stream, and replace it with one that is generated from the seed sequence.
This mirrors the behavior of a constructor that is supplied with a seed sequence as argument.
std::seed_seq seq{ 1, 2, 3 };
pcg32 e(seq); // Generate both seed and stream from the given seed_sequence
The program below demonstrates this behavior by selecting a desired stream, and then noting that it is discarded when function seed(seq) is called.
// main.cpp
#include <cassert>
#include <iostream>
#include <random>
#include "pcg_random.hpp"
using state_type = pcg32::state_type;
bool does_seeding_with_a_seed_seq_discard_the_existing_stream()
{
pcg32 e;
state_type const desired_stream{ 1u };
e.set_stream(desired_stream);
assert(e.stream() == desired_stream);
std::seed_seq seq{ 1, 2, 3 };
e.seed(seq);
assert(e.stream() != desired_stream);
return e.stream() != desired_stream;
}
//----------------------------------------------------------------------
int main()
{
std::cout << "Test pcg_random issue 94"
"\nhttps://github.com/imneme/pcg-cpp/issues/94"
"\n"
"\nGiven a `pcg32` engine named `e`, and an arbitrary seed sequence "
"\nnamed `seq`, does calling `e.seed(seq)` discard the existing stream? "
"\n"
"\n Answer: " << (does_seeding_with_a_seed_seq_discard_the_existing_stream() ? "yes" : "no") <<
"\n\n";
}
// end file: main.cpp
Here is the output:
Test pcg_random issue 94
https://github.com/imneme/pcg-cpp/issues/94
Given a `pcg32` engine named `e`, and an arbitrary seed sequence
named `seq`, does calling `e.seed(seq)` discard the existing stream?
Answer: yes
At present, the obvious work-around/fix for this bug is to use brute force. If possible, set the stream and seed at the same time. Otherwise, save and restore the stream whenever you call function seed.
- Save current stream.
- Seed PCG using any of the three seeding functions.
- Call
set_streamto restore the stream.
A possible exception is the case of a seed sequence. Depending on the application, you may (or may not) wish to have both seed and stream modified when you call function seed with a seed sequence.
Here is some example code:
// main.cpp
#include <random>
#include "pcg_random.hpp"
using state_type = pcg32::state_type;
int main() {
pcg32 e;
state_type const desired_seed{ 42u };
state_type const desired_stream{ 1u };
state_type current_stream{};
// If possible, set the stream and seed at the same time.
e.seed(desired_seed, desired_stream);
// Otherwise, use brute force.
current_stream = e.stream();
e.seed(); // This discards the existing stream.
e.set_stream(current_stream); // This restores it.
// More brute force
current_stream = e.stream();
e.seed(desired_seed); // This discards the existing stream
e.set_stream(current_stream); // This restores it.
// Brute force with a seed sequence
current_stream = e.stream();
std::seed_seq seq{ 1, 2, 3 };
e.seed(seq); // This discards the existing stream
e.set_stream(current_stream); // This restores it.
}
// end file: main.cpp