Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Butterfly pattern appears in random walk using srand(), why?

About 3 years ago I coded a 2D random walk togheter with a coleague in C++, first it seemed to work properly as we obtained a diferent pattern each time. But whenever we decided to increase the number of steps above some threshold an apparent butterfly pattern appeared, we noticed that with each run of the code the pattern would repeat but starting on a different place of the butterfly. We concluded and reported then that it was due to the pseudorandom generator associated with srand() function, but today I found again this report and there are still some things that I would like to understand. I would like to understand better how the pseudorandom generator works in order to obtain this sort of symmetry and ciclic pattern. The pattern I'm talking about is this (The steps are color coded in a rainbow sequence to apreciate the progression of the walk):

enter image description here

EDIT:

I'm adding the code used to obtain this figure:

#include<iostream>
#include<cmath>
#include<stdlib.h>
#include<time.h>
#include <fstream>
#include <string.h>
#include <string>
#include <iomanip>

using namespace std;

int main ()
{
srand(time(NULL));
int num1,n=250000;



ofstream rnd_coordinates("Random2D.txt");
float x=0,y=0,sumx_f=0,sumy_f=0,sum_d=0,d_m,X,t,d;
float x_m,y_m;

x=0;
y=0;

for(int i=0;i<n;i++){

    t=i;
    num1= rand()%4;

    if(num1==0){
        x++;
    }
    if(num1==1){
        x--;
    }
    if(num1==2){
        y++;
    }
    if(num1==3){
        y--;
    }

    rnd_coordinates<<x<<','<<y<<','<<t<<endl;

}

rnd_coordinates.close();


return 0;
}
like image 902
Isidre Mas Magre Avatar asked May 27 '20 09:05

Isidre Mas Magre


2 Answers

You never hit rand()'s period, but keep in mind you don't actually use the entire rand() range that in its entirety guarantees a 2^32 period.

With that in mind, you have 2 options:

  1. Use all the bits. rand() returns 2 bytes (16 bits), and you need 2 bits (for 4 possible values). Split that 16 bit output into chunks of 2 bits and use them all in sequence.
  2. At the very least if you insist on using the lazy %n way, choose a modulo that's not a divisor of your period. For example choose 5 instead of 4, since 5 is prime, and if you get the 5th value reroll.
like image 125
Blindy Avatar answered Oct 19 '22 20:10

Blindy


The code below constitutes a complete compileable example.

screenshot of the example

Your issue is with dropping bits from the random generator. Lets's see how one could write a source of random bit pairs that doesn't drop bits. It requires that RAND_MAX is of the form 2^n−1, but the idea could be extended to support any RAND_MAX >= 3.

#include <cassert>
#include <cstdint>
#include <cstdlib>

class RandomBitSource {
    int64_t bits = rand();
    int64_t bitMask = RAND_MAX;
    static_assert((int64_t(RAND_MAX + 1) & RAND_MAX) == 0, "No support for RAND_MAX != 2^(n-1)");
public:
    auto get2Bits() {
        if (!bitMask) // got 0 bits
            bits = rand(), bitMask = RAND_MAX;
        else if (bitMask == 1) // got 1 bit
            bits = (bits * (RAND_MAX+1)) | rand(), bitMask = (RAND_MAX+1) | RAND_MAX;

        assert(bitMask & 3);
        bitMask >>= 2;
        int result = bits & 3;
        bits >>= 2;
        return result;
    }
};

Then, the random walk implementation could be as follows. Note that the ' digit separator is a C++14 feature - quite handy.

#include <vector>

using num_t = int;
struct Coord { num_t x, y; };

struct Walk {
    std::vector<Coord> points;
    num_t min_x = {}, max_x = {}, min_y = {}, max_y = {};
    Walk(size_t n) : points(n) {}
};

auto makeWalk(size_t n = 250'000)
{
    Walk walk { n };
    RandomBitSource src;
    num_t x = 0, y = 0;

    for (auto& point : walk.points)
    {
        const int bits = src.get2Bits(), b0 = bits & 1, b1 = bits >> 1;
        x = x + (((~b0 & ~b1) & 1) - ((b0 & ~b1) & 1));
        y = y + (((~b0 & b1) & 1) - ((b0 & b1) & 1));

        if (x < walk.min_x)
            walk.min_x = x;
        else if (x > walk.max_x)
            walk.max_x = x;
        if (y < walk.min_y)
            walk.min_y = y;
        else if (y > walk.max_y)
            walk.max_y = y;

        point = { x, y };
    }
    return walk;
}

With a bit more effort, we can make this into an interactive Qt application. Pressing Return generates a new image.

The image is viewed at the native resolution of the screen it's displayed on, i.e. it maps to physical device pixels. The image is not scaled. Instead, it is rotated when needed to better fit into the screen's orientation (portrait vs landscape). That's for portrait monitor aficionados :)

#include <QtWidgets>

QImage renderWalk(const Walk& walk, Qt::ScreenOrientation orient)
{
    using std::swap;
    auto width = walk.max_x - walk.min_x + 3;
    auto height = walk.max_y - walk.min_y + 3;
    bool const rotated = (width < height) == (orient == Qt::LandscapeOrientation);
    if (rotated) swap(width, height);
    QImage image(width, height, QPixmap(1, 1).toImage().format());
    image.fill(Qt::black);

    QPainter p(&image);
    if (rotated) {
        p.translate(width, 0);
        p.rotate(90);
    }
    p.translate(-walk.min_x, -walk.min_y);

    auto constexpr hueStep = 1.0/720.0;
    qreal hue = 0;
    int const huePeriod = walk.points.size() * hueStep;
    int i = 0;
    for (auto& point : walk.points) {
        if (!i--) {
            p.setPen(QColor::fromHsvF(hue, 1.0, 1.0, 0.5));
            hue += hueStep;
            i = huePeriod;
        }
        p.drawPoint(point.x, point.y);
    }
    return image;
}

#include <ctime>

int main(int argc, char* argv[])
{
    srand(time(NULL));
    QApplication a(argc, argv);
    QLabel view;
    view.setAlignment(Qt::AlignCenter);
    view.setStyleSheet("QLabel {background-color: black;}");
    view.show();

    auto const refresh = [&view] {
        auto *screen = view.screen();
        auto orientation = screen->orientation();
        auto pixmap = QPixmap::fromImage(renderWalk(makeWalk(), orientation));
        pixmap.setDevicePixelRatio(screen->devicePixelRatio());
        view.setPixmap(pixmap);
        view.resize(view.size().expandedTo(pixmap.size()));
    };
    refresh();
    QShortcut enter(Qt::Key_Return, &view);
    enter.setContext(Qt::ApplicationShortcut);
    QObject::connect(&enter, &QShortcut::activated, &view, refresh);
    return a.exec();
}
like image 43
Kuba hasn't forgotten Monica Avatar answered Oct 19 '22 20:10

Kuba hasn't forgotten Monica