Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

define show(::MersenneTwister) #25129

Merged
merged 2 commits into from
Oct 5, 2020
Merged

define show(::MersenneTwister) #25129

merged 2 commits into from
Oct 5, 2020

Conversation

rfourquet
Copy link
Member

I have been meaning to do this for a long time. I would say it's mostly useful because srand returns the RNG, so it happens at the REPL to be show a MersenneTwister, with loads of useless information:

julia> srand() # master
MersenneTwister(UInt32[0x0c7ad3d3, 0xa5925687, 0xc6ea340a, 0xabaa8392], Base.dSFMT.DSFMT_state(Int32[-1758623818, 1072978913, 994178365, 1073660380, -791860901, 1073515898, -549958670, 1072772648, 1411463369, 1073306694    -160292708, 1073238798, 320054219, 1073706379, -1551517119, 564778559, 1415871354, 1522268230, 382, 0]), [1.91962, 1.42752, 1.00802, 1.95107, 1.86132, 1.55257, 1.4849, 1.10529, 1.9795, 1.78886    1.13922, 1.99249, 1.29247, 1.76895, 1.25858, 1.77279, 1.29767, 1.65949, 1.73626, 1.38635], 382)

julia> srand() # PR
MersenneTwister RNG with seed 0xd4e8b116a747b4fc7dbf67862e68478a

julia> srand() # PR alternative 2
MersenneTwister(0xd4e8b116a747b4fc7dbf67862e68478a, ...)

julia> srand() # PR alternative 3
MersenneTwister(seed=0xd4e8b116a747b4fc7dbf67862e68478a)

Alternative 2 is a bit ugly, but without the dots, it mistakenly makes you think you can get an equal RNG with MersenneTwister(0xd4e8b116a747b4fc7dbf67862e68478a), (same problem with alternative 3), and with the dots, it makes you think there is a multi-arg constructor, which is not the case.

I like to print the seed as an unsigned number, as it's just a field of bits, and the unsigned can be fed back as a seed later.

Suggestions welcome!

@mschauer
Copy link
Contributor

Can't we just add a method such that MersenneTwister(0xd4e8b116a747b4fc7dbf67862e68478a) works? What am I missing?

@ararslan ararslan added domain:display and printing Aesthetics and correctness of printed representations of objects. domain:randomness Random number generation and the Random stdlib labels Dec 16, 2017
@rfourquet
Copy link
Member Author

Can't we just add a method such that MersenneTwister(0xd4e8b116a747b4fc7dbf67862e68478a) works? What am I missing?

It works in that it creates an RNG with the same seed, but not equal (unless the printed RNG has not generated any values since its seeding), so this is not a way to construct back the RNG. There may be a way by counting how many values have been generated, and using randjump to advance a newly generated RNG, e.g:

julia> m = MersenneTwister(0)
MersenneTwister(seed=0x0, advance=0)

julia> rand(m)
MersenneTwister(seed=0x0, advance=1)

julia> ans == MersenneTwister(seed=0x0, advance=1)
true

But this would require #16906 to jump by arbitrary many steps, and I'm not sure it's worth the trouble (I don't see use case).
My first idea was to print MersenneTwister(seed=0x0, hash=0xdfab154a292e4ec0), where we compute Base.hash of the RNG, to discriminate between 2 unequal RNGs with the same seed, but I find it adds little value, and is still not a way to construct back a new RNG by copy-pasting.

But maybe printing "MersenneTwister(0x0)" is good enough?

@rfourquet
Copy link
Member Author

I forgot to say: this kind of depends on #23546; indeed, feeding back the seed in hexadecimal to an RNG works as long as it fits a UInt128. So I would like that Julia accepts parsing arbitrary long BigInt values in hexadecimal.

@StefanKarpinski
Copy link
Sponsor Member

I prefer the MersenneTwister(0xd4e8b116a747b4fc7dbf67862e68478a, 10) version if counting the advances can be done without a performance impact. I don't think the keywords are really helpful here. We could have a corresponding constructor MersenneTwister(seed, advance=0). Since it doesn't make sense to give and advance count without a seed, positional works here.

@vtjnash
Copy link
Sponsor Member

vtjnash commented Dec 16, 2017

If advance counting works, then I agree with Stefan that MersenneTwister(0xd4e8b116a747b4fc7dbf67862e68478a, +10) seems like a good choice (I added a + too). Otherwise, perhaps use "initial-seed=" or "seeded-from="?

@mschauer
Copy link
Contributor

mschauer commented Dec 16, 2017

MersenneTwister(0xd4e8b116a747b4fc7dbf67862e68478a, +10) reads nicely.

@StefanKarpinski
Copy link
Sponsor Member

I don't care for the +. Can you advance a negative number of steps? That's what it suggests to me.

@rfourquet
Copy link
Member Author

I didn't investigate the overhead of counting, but it should be very low, like incrementeing an integer every few hundreds generated Float. There is this tricky thing that we have a cache for Float64, and soon one for integers, so we may need to specify actually three integers to be fully specific about the advancement state of the RNG.

The trouble is also that the PR implementing the functionality to advance effectively hasn't be updated for a while and there is now way I can do it by the 0.7 feature freeze timeframe. (It should interact very little with other functionalities though, so it would seem safe to introduce later between 0.7 and 1.0. )

So if we want this for 0.7, I like Jameson's suggestion with initial-seed=.... And this can be updated anytime when we have the "advance" functionality (we don't guarranty stability of the printing of a type during the whole 1.x releases, do we?)

@StefanKarpinski
Copy link
Sponsor Member

The advance functionality doesn't need to work now. That will just lead to a situation where we print an RNG as MersenneTwister(seed, advance) but trying to input that syntax is an error, which is fine. We can then introduce that two-argument method in the future.

@rfourquet rfourquet added domain:collections Data structures holding multiple items, e.g. sets and removed domain:collections Data structures holding multiple items, e.g. sets labels Dec 17, 2017
@rfourquet
Copy link
Member Author

I thought a little bit more about this, and the "advance" functionality still sounds tricky but doable. Let's start with a small example, and recall that MT has a cache of Float64 random values, say of size n, together with an index idx counting the remaining values until the cache has to be re-generated (using rand! on the cache array) with fresh new values. This cache is used only to generate scalars (i.e. rand()), as we use directly dSFMT for rand! (this is a simplification of what really happens) Now consider the following two scenarii:

  1. the user asks for rand(), so the cache is generated and one value is popped (returned by rand()); then the user calls rand(n) (which internally uses rand!);
  2. reverse the order of operations

In both cases, 2n values have been generated by dSFMT, and n-1 of them (in the cache) have not yet been consumed. So the numbers are the same, but the next call to rand() will give a different value in each case! So more bookkeeping needs to be done in order to recreate the state of an RNG from the seed and a set of counters. I thinks this is doable efficiently, but if we print MersenneTwister(seed, advance), advance can't be a simple integer: either a tuple of integers (would be confusing), or an unsigned integers which combines the set of counters, from which those counters can be extracted back. In this case printing a MT object would look like:

julia> m = MersenneTwister(3); rand(m, 2^10); m 
MersenneTwister(0x3, 0x0xd4e8b116a747b4fc7dbf67862e68478a)

Isn't it too much? At least it's not worse than the current printing, and we can have a note in the doc about this mysterious 2nd number.

@StefanKarpinski
Copy link
Sponsor Member

StefanKarpinski commented Dec 17, 2017

I'm not actually super concerned about being able to print an output syntax that reproduce the same exact RNG state – being able to de/serialize RNG objects would accomplish the same thing. How about some output format that's clearly not an input syntax then, like MersenneTwister(3)@1024.

@rfourquet
Copy link
Member Author

rfourquet commented Dec 17, 2017

Small update: in addition to the .idx fiels of MT, we have to store 2 BigInt for storing the "advance" state. So while doable, it becomes more complicated to combine those 2 BigInt in 1 hex value. So the current PR now prints MersenneTwistter(seed, counter1, counter2, counter3), e.g. MersenneTwister(0x3, 2144, 1762, 1), which is enough to reconstruct the state of the RNG (with potentially a lot of time, until the randjump PR is merged).

How about some output format that's clearly not an input syntax then, like MersenneTwister(3)@1024.

I would be fine with something like that, but what is 1024 here?

@StefanKarpinski
Copy link
Sponsor Member

StefanKarpinski commented Dec 17, 2017

The 1024 == 2^10 which is the number of steps the RNG has taken. I'm also fine with just printing a bunch of numbers like that to reconstruct state. They're not intelligible, but that's fine, I guess. I would prefer to print the seed as a signed value since that's usually how it's supplied, so my preferred output would be: MersenneTwister(3, 2144, 1762, 1).

@mschauer
Copy link
Contributor

I like MersenneTwister(3, (2144, 1762, 1)). This makes the initial state number a bit more special.

@rfourquet
Copy link
Member Author

So we seem to be converging!
What would a think about a small DWIM tweak: when users call srand, indeed typically it will be a small integer, and it makes sense to print it back as is:

julia> srand(123)
MersenneTwister(0x7b, (0, 0, 0))  # where is my 123 ??

But on the other hand, the default seeding uses 4 random UInt32, so if we print that as signed integer, half of the time it will be a BigInt (when it doesn't fit Int128), which is not a big deal at all, but also, it's meaningless as an integer for MT (unlike the 3 counters which are indeed integers in essence):

julia> srand()
MersenneTwister(282547262182945653700010255641608774292, (0, 0, 0)) 

julia> srand() # versus
MersenneTwister(0xd4909f9036b5e5b14cc424a1f798e294, (0, 0, 0)) 

it's just a bunch of bits used to initialize the states, and I believe hexadecimal conveys this idea. Also, it is consistent with the fact that only positive integers are allowed as the the seed.

So the DIWM could be printing in hex when the seed is too big to fit an Int, or even to record in the rng how it was initiliazed (but that may be too much). I don't love DWIM, so I'm not sure yet myself.

@StefanKarpinski
Copy link
Sponsor Member

I would just do the DWIM thing. Seems like a better experience.

@rfourquet
Copy link
Member Author

I implemented the DWIM. Also, we don't really need two BigInt due to the current implementation of randjump which discards the cache (so we only need to store the jump as BigInt, and offsets from that as Int64, if it makes sense), so we could combine easily the numbers into one giant BigInt, but I'd rather not, as it's much less meaningful.
Small demo:

julia> m = MersenneTwister()
MersenneTwister(0xc5801071048f3cd7a2982d00e152c3ca, (0, -1, 0))

julia> rand(m); m
MersenneTwister(0xc5801071048f3cd7a2982d00e152c3ca, (382, 0, 1))

julia> [rand(m) for i in 1:381]; m
MersenneTwister(0xc5801071048f3cd7a2982d00e152c3ca, (382, 0, 382))

julia> rand(m); m
MersenneTwister(0xc5801071048f3cd7a2982d00e152c3ca, (764, 382, 1))

julia> srand(m, 123)
MersenneTwister(123, (0, -1, 0))

julia> randjump(m, 2)
2-element Array{MersenneTwister,1}:
 MersenneTwister(123, (764, 382, 1))
 MersenneTwister(123, (7766279631452241920, 0, -1, 0))

The last example shows a 4th number, the size of the jump. It would also be meaningful to not print it but to add it to the first 2 numbers of the triple, but it becomes harder to read: MersenneTwister(123, (7766279631452241920, 7766279631452241919, 0)).

I think I'm pretty satisfied with this current version :)

@rfourquet
Copy link
Member Author

So I ended up rebasing #16906 and implemented a MersenneTwister constructor to read back the format above. It's quite neat, as without it we are kind of lying by printing a fake constructor form :)

@rfourquet
Copy link
Member Author

rfourquet commented Sep 21, 2018

I rebased this, with the complication that we now have two caches in MersenneTwister, so the full specification requires 8 integers to be displayed:
(randjump_steps, state_steps, float_cache_1, float_cache_2, int_cache_1, int_cache_2, int_cache_3, int_cache_4).

I tried a compromise where the integer corresponding to the number randjump_steps of steps used in randjump is shown only when randjump has been used, and the 4 numbers corresponding to the integer cache are shown only when rand has been used to generate integers.
So this gives something like this:

julia> m = MersenneTwister(123)
MersenneTwister(123, (0, 0, 0))

julia> rand(m); m
MersenneTwister(123, (1002, 0, 1))

julia> rand(m, Int64); m
MersenneTwister(123, (2002, 0, 255, 1002, 0, 1, 1))

julia> m = Future.randjump(MersenneTwister(123), 10^9)
MersenneTwister(123, (2000000000, 0, 0, 0))

julia> rand(m, Int128); m
MersenneTwister(123, (2000000000, 2002, 1000, 254, 0, 0, 0, 2))

For the first example, we could also simply show MersenneTwister(123) as output, but it doesn't seem very important as an RNG rarely stays in its initial state. The example with randjump shows a number twice as big as the number of specified steps, because it seems more natural to have a number of steps in terms of Float64 rather than in terms of "couples of Float64" (I will propose to have randjump back in Random in a form following this convention).

Feedback welcome!
I have implemented the constructors allowing to re-construct MT objects from this output format so I'm confident this is not too buggy. I can include this constructor part in this PR or open another one.

@rfourquet rfourquet added the needs tests Unit tests are required for this change label Sep 21, 2018
@rfourquet
Copy link
Member Author

rfourquet commented Sep 26, 2018

Bump. Would be nice to find a solution here, to remove the fear of calling Random.seed!(123) without a ; (which prints the RNG which is multiline-garbage).

@mschauer
Copy link
Contributor

Can/do we provide the corresponding constructors such that

julia> m = Future.randjump(MersenneTwister(123), 10^9)
MersenneTwister(123, (2000000000, 0, 0, 0))

julia> MersenneTwister(123, (2000000000, 0, 0, 0))
MersenneTwister(123, (2000000000, 0, 0, 0))

etc.?

@rfourquet
Copy link
Member Author

Can/do we provide the corresponding constructors such that

I have them available locally, and I think it would be good to provide them yes. Just waiting for input on whether we go this route (to finalize the code), and whether I open a separate PR for the constructors.

@mschauer
Copy link
Contributor

Ok, so I like it if this helps :-)

@rfourquet
Copy link
Member Author

Ok, I finally finalized the code implementing the constructors matching show which was dormant since two years. It's a bit tricky to get it right, but I think it's working well now.
See my comment above for how the printing works, except that the number of "jumped" steps is always printed (to limit the complexity), and that right after seeding, only the seed is printed, e.g. string(MersenneTwister(123)) == "MersenneTwister(123)". See also the add tests.

Feed-back welcome!
Currently the constructor fails for seeds bigger than UInt128, this would require #23546.

If no discussion or objection come, I will merge soon...

@rfourquet rfourquet removed the needs tests Unit tests are required for this change label Sep 29, 2020
@rfourquet rfourquet force-pushed the rf/MT-show branch 2 times, most recently from 1439840 to 1e9f32f Compare October 3, 2020 14:22
@rfourquet rfourquet changed the title WIP/RFC: define show(::MersenneTwister) define show(::MersenneTwister) Oct 3, 2020
@rfourquet rfourquet force-pushed the rf/MT-show branch 2 times, most recently from fc43c51 to 7a19359 Compare October 4, 2020 09:32
@rfourquet rfourquet closed this Oct 4, 2020
@rfourquet rfourquet reopened this Oct 4, 2020
E.g.
```
julia> m = MersenneTwister(0); rand(m); m
MersenneTwister(0, (0, 1002, 0, 1))

julia> m == MersenneTwister(0, (0, 1002, 0, 1))
true
```
@rfourquet rfourquet merged commit 6c62d72 into master Oct 5, 2020
@rfourquet rfourquet deleted the rf/MT-show branch October 5, 2020 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:display and printing Aesthetics and correctness of printed representations of objects. domain:randomness Random number generation and the Random stdlib
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants