Showing posts with label random. Show all posts
Showing posts with label random. Show all posts

Sunday, 4 December 2016

A Case Of Non-Randomness - Part 2

The Problem

I was just starting to set up the procedures needed for rows of buildings when I hit a problem.
Bad randomness showing up in row of buildings
These are supposed to be random heights!  I've had a nagging feeling that my random number generation operators weren't working properly and had noticed a few artefacts before, but dismissed them as me being overly sensitive to patterns.  This horrible patterning however tipped me over the edge and I had to investigate.

Visualisation

A great way to sanity check random numbers is to visualise a sequence as a grid of intensity values (see Runes blog post again).  So I put together a couple of nested procedures to build a 2D grid of tiles, each coloured black-to-white according to the random function under scrutiny.
Random number sequence visualisation using geometry
This instantly revealed unpleasant patterns in the number sequences.  Trying different seeds showed that most of them seemed to have regular repeating pattern or clearly be restricted to lower or upper value ranges.
Bad patterning in the random number generation

Investigation

My first avenue of investigation was to reproduce the algorithm explicitly in the test procedure using the same sequence of operations as the random generation library code.
Explicitly implemented random number generator
I hoped that this would allow me to play with the algorithm to find the bug.  However, this version produced very nice random values, with no discernible patterns.  Carefully picking through the implementation I couldn't see any functional difference.
Next I added numerical display of the seed and value for each tile and zooming in on the start of the first row I noticed that they did agree for a few values, before diverging.  This seemed to coincide with the seed (handled as a signed integer) going negative.  I wasn't sure how this would affect the algorithm though so I switched to debug build and stepped through the for this case.
After some debugging runs and breaking open the algorithms a bit I discovered what was going on.

Cause

My entropy extraction macro (see Hash part of Random Value Generation Process in the previous article) uses right shift operations >> to re-align the overlapping bits, but I had forgotten that this will perform a sign extended shift if the operand is a signed value.  That is, if the most significant bit is 1, indicating a negative number, then 1's are shifted in instead of 0's.  This situation caused the extraction algorithm to XOR chunks of 1's into the seed values.  XOR has the interesting (and normally useful) property that successive applications of 1's causes toggling.  This fact combined with shifts used set up a repeating oscillation in some chunks of bits.  It was completely believable that this could produce the heavily striped and range limited results I was seeing.

Fix

The fix here was relatively straight forward as the behaviour of >> on unsigned integers always shifts in zeros into the upper bits, as was actually required.  Interestingly, the reason we didn't see this problem on the explicit implementation was that I had made sure the shift operation always used the unsigned shift.
Casting the operands to unsigned integer prior to each shift fixed this and the two implementations now agreed, AND also produced the randomness they were designed to generate.
Much better random sequences

Slight Red Herring

Going back to the building height problem I discovered that this hadn't fixed it, the result was exactly the same!  So, I had managed to find and fix a real problem, just not the one that was causing my issue.  Oops!

The Real Culprit

Checking the building height calculation I realised that I was relying on the variant parameterisation of the Random Twist operator to generate random values from the building index along the row.  The reason it was designed to work like this is that I needed a way to generate unique seeds for each building from the single seed provided to the row as a whole.  The only distinguishing information the buildings have at this point where they are in the row, i.e. their index.  The twist operation seemed to be perfect here as the variant input was intended for exactly this sort of operation.
Using twist variant input to derive per plot variation in a row of houses
Unfortunately this didn't seem to produce very good results for a sequence of successive variant values.  Adding a visualisation of the twist operation with the tile index driving the variation input again showed up this lack of randomness.
Patterning from the Random Twist operator
In hindsight this is one of the issues Rune talks about in his article; close or sequential seeds don't necessarily produce sequences that are very random relative to each other.  He suggests a decent hash of the input to better seed the sequences.

A Better Twist

This time the patterning wasn't as strong, but it was certainly there.  Looking into the Twist implementation I realised the mixing method of the variant value was far too simplistic and I was relying on small changes in variant input to produce significant changes to the output seed and hence number sequence.  This was obviously not the case and I needed a better mixer.
// jump to another point in the
// random stream (parameterised)
int Random::Twist( int input, int variant ) 
{ 
  return Random::Algorithm::Next_LCG_ANSIC( input^variant ); 
}
After some research, I realised that the property I was after is referred to as 'avalanche', i.e. where small input changes cause large output changes, and was a common desirable feature of hashing algorithms.  Again, rather than investigate and implement my own solution I chose a publicly available integer hashing function that claimed very good avalanche characteristics.
Replacing the mixer generation with this in the Twist operator proved very successful and finally achieved a good random variants again.
Much improved twist variant output

Fixed Buildings

Going back to my row of buildings again, with the above changes produced much, much more satisfactory results.
That's much better
I expect to see improvements throughout my projects as a result of these fixes, some will be more subtle than others.  I'm glad I took the time to investigate this as it's resolved a niggle that I couldn't quite put my finger on.


Epilogue

Procedural generation is typically required to be completely deterministic so that, for example, every time you re-visit a game the world is familiar to you, or that every player of a game sees the same world.  This is an important feature of a game such as No Man's Sky, a vast, hugely varied procedural universe that is the same for every player.
Fixing the bugs in my random number generation operators means I have changed the behaviour of something fundamental to all the generation processes across my projects.  This in turn means that all the content they generate will now be different, the content they generated before has been lost.  Forever.
The deterministic nature of the processes involved in procedural generation, combined with their complexity means they are hugely susceptible to the butterfly effect, that small changes to any part (all parts are effectively inputs) can have huge effects on the output.
I have often wondered how many times Hello Games has made changes to No Man's Sky that have completely invalidated the current universe it is set in.  How many versions of universe have they gone through to get to the one we play?  I dread to think how much of a problem this is for ongoing work where they can't change or even fix bugs in so much of the game for fear of breaking the universe.

“There is a theory which states that if ever anyone discovers exactly what the universe is for and why it is here, it will instantly disappear and be replaced by something even more bizarre and inexplicable."
"There is another theory which states that this has already happened.”
- Douglas Adams, The Restaurant at the End of the Universe

Monday, 28 November 2016

A Case Of Non-Randomness - Part 1

In preparation for a post about a randomness bug I found, let's look at some of what's going on behind the scenes in Apparance with random number generation, propagation, and manipulation.

Premise

The random number generation in Apparance is based on two main principals:
  1. Generating a repeatable sequence of random values.
  2. Branching of random number sequences.
These are embodied in two operators you can build into your procedures, the "Random Value" operator (with Float and Integer variants), and the "Random Twist" operator.
The two Random Value operators and the Random Twist operator
Both take a seed* value as input and produce a new seed value as an output.  The Value operators also take Range inputs and produce a Value output.  Seeds are signed (32 bit) integers and so are compatible with the normal integer connection type.
The Value operators are fairly self-explanatory but the need for the Twist operation will be explained further down.
* I've adopted the convention of using the # symbol at the end of an input or output name to indicate a seed value. 

Random Sequences

When a procedure needs some variation, it will usually be parameterised by a seed value such that it can be recreated in exactly the same way by providing the same seed, or different versions of it created by providing different seed values to each instance.
Seeding a procedure.  Same seed; same output, different seed; different output.
Within the procedure, where multiple values may be needed, these can be obtained by calculating a sequence of random numbers from the given starting seed.  Each Random Value operator takes its seed from the seed output of the previous one.  This way each operation is provided with a different seed from which to generate its output value.
Generating multiple values within a procedure

Multiple Seeds

If you want to create a collection of similar, but varying, objects you would need to instance one procedure multiple times, each with a different seed.  So, how do you generate these seeds?
A simple approach would be to just use the same random generation sequence the Random Value operator uses to get each new seed (discarding the values they also generates).  However, there is a big problem with doing this; if internally your procedure uses several random values from the random sequence, the set for each instance can overlap to some extent.  
The problem with using a single random number generator; a rolling pattern of values
If the values in the procedure are used to drive a similar aspect of the output, e.g. the heights of several sub-elements, then you will see similarities between instances where there should be none.  This may be hidden by the exact nature of a scenario, but there are plenty of cases where this will be horribly noticeable.
Wiring up an instancing example using the procedural symbols from my Proc-jam game and using the normal random operator to generate the seeds we can investigate this effect.
Investigating simple seed sequence generation problems
As the set of symbols within the instance procedure is also created using from the normal random generation operator, the symbols displayed are a window into the LCG sequence, each one shifted along by one value.
Symbol sequence with sequential seeds from same generator showing the 'windowing' effect
The solution here needs some consideration of how random sequences are generated and what a seed is.

Generation Algorithms

The sequence of values produced by a random number generator come from the random number generation algorithm it employs.  The seed is used to set up the internal state of the generator, and each time a random number is generated, the internal state changes ready to provide the next one.  The algorithm is responsible for ensuring that the output is perceived as random.  There are a huge variety of algorithms people have devised, each with different characteristics of randomness, speed, size (internal state), and cryptographic suitability (a common use case).
For procedural generation the requirements are usually just going to be around speed, and maybe storage size.  Randomness isn't normally a problem as there are many good algorithms that are fast and small.  For Apparance, one of the most basic classes of generator is all that is needed; the Linear Congruential Generator (LCG).
LCGs are particularly attractive for procedural generation use since the state can be the same size as the seed, allowing us to effectively pass the state around in the same form as we do the seeds, in fact they are treated as one-and-the-same.  This is important because for a fully deterministic procedural system this state has to be propagated around the system with all the other parameters, and this is indeed what happens in Apparance.

Growing Seeds

A seed is the starting point for a random sequence, start with a different seed and you get a different sequence.  In fact though, what you are getting is just a different part of some larger overall sequence.  For any random number generation algorithm the sequence it generates will repeat itself eventually, but the length of this sequence is dependent on the number of possible configurations of its internal state.  The more state, the longer it can generate values without repeating itself.  For a LCG using a 32 bit integer seed there are 2^32 possible internal states and hence it would have a theoretical repeat period of over four billion values.  However, this theoretical maximum isn't guaranteed with every LCG as the mathematics used is very simple and, consequently, choosing the internal constants used is very important.  For Apparance I chose to use constants from publicly documented implementations to save the investigation and analysis time needed to derive good ones.

Multiple Algorithms

Given that we can choose between different LCG variants, each with its own sequence of states/seeds/values, we could choose to solve our issue by using one algorithm for the inner sequence and a different one for the outer sequence.
Using two different random number algorithms to avoid sequence overlap
Since the sequence of values needed by a procedure is going to be far, far smaller than the total sequence size, what we have here is multiple procedures running along different parts of the same overall sequence. For this to be successful all we needed was to change the seed to one that will not produce the next, expected, value in the sequence.  That is exactly what the other algorithm is doing, it effectively skips the needle from one part of the record to another.

Scalability Issues

This approach has two significant downsides though:
  1. This requires either more than one Random Value operator, or the operator needs to be parameterised by the algorithm used.
  2. The outer procedure needs to make sure it's not using the same algorithm as the inner procedure.
The first issue here adds to the complexity of the available operators and their use.  The second introduces coupling between procedures; knowledge of the inner one is needed to use it in the outer one.  Both of these also suffer from scalability issues; it's very easy to imagine scenarios where this problem is nested more than two procedures deep, i.e. we may need more than two algorithms, or that two procedures need to be used side-by-side that are already using both algorithms.  A better solution is needed.

Twist Variants

This 'branching' of the random streams is particularly effective, but it turns out that to solve our dependency problem it is necessary to be able to jump multiple ways from a given point in the sequence.
This is exactly what the Random Twist operator does.  It produces a new seed value using a different LCG, and also has a Variant input allowing parameterisation of this process.  This is implemented as a simple bit mangle of the variant value with the new seed value so as to change it parametrically, much like the twist operation itself, and jumping tracks again.
Better use of twist operator to generate sub-seeds

Fundamentals

It's worth summarising the random number generation processes at play here before we move on to the main story next time.
The random generation used here can be broken down into these fundamental operations:
  • Generate a New Seed from a Seed
  • Generate a Value from a Seed
The first is the function of the random generation algorithm (LCG in our case), and the second is actually a form of hash function.  Hash functions map values of one type or size to values of another type or size.  Commonly used to generate check-sums of large sets of data or used in encryption, they are also useful here.  The random number generation in Apparance uses a set of hash operations to map random seeds to floating point and integer values, with configurable ranges for use in the procedural generation process.
The Random Value operators are composed thus:
The Random Value generation process
The Random Twist operator is composed thus:
The Random Twist seed generation process
This should give you enough of a grounding in Apparances randomness operators.  Next time we'll look at the implementation of these a bit more and the quirky bug I found disrupting them.

See Also

If you are interested in reading up in more depth on random number sequences, in particular with respect to procedural generation, I highly recommend Rune Skovbo Johansen's great primer on the subject.
It covers some of the sequencing and hashing issues with deriving numbers for use in procedural generation scenarios.  There is also a fairly detailed analysis of some of the more practical generation algorithms.