Completing the OOP design of the project

Aims

  • Complete the skeleton of the object-oriented structure of the project

Learning outcomes

  • Constructing combinations of objects
  • Constructing vectors of objects

More classes

Up to now, we only create one class, the System class. We could maybe write all out code inside the system class, but this would largely defeat the point of object-oriented programming.

The idea behind this programming paradigm is to organise the code into smaller units that can be quite autonomous but which can interact with each other.

Earlier we drew the following sketch

classDiagram
 direction LR
    class System{
      %% particle_number
      %% noise_strength
      %%list~Particle~ particles
      %%update_rule()
  
    }

    class Box {
        %%size
        %%boundary_conditions
    }

    class Particle {
        %%position
        %%orientation
    }
  System --> Box : contains
  System "1" --> "N" Particle : contains

In the previous part of this workshop we addressed only one of these objects, the System class.

We now proceed more quickly, using the same ideas, to construct the two remaining objects and see how to integrate them with our original System class.

Here, there will be less detailed code and more high level description of what the classes do: it will be for you to translate these design choices int actual code.

Constructing the Box

The system is simulated in a two-dimensional domain. We decide the domain to be a square box. However, for the sake of generality, let our Box class represent an arbitrary rectangular domain.

The rectangle should have are

  • the side in the x direction
  • the side in the y direction
  • the coordinates of the origin (bottom left corner)
side x side y Origin

In this case we want to make these properties accessible only via the usage of member functions: we want them to be private member variables. This avoids possible accidental changes, making the code more robust.

This means that, when we define the Box class, we will use the following syntax

private:
    double sidex;
    double sidey;
    double x0;
    double y0;

To set the values of these variables or get their values we need dedicated member functions, conventionally called setters and getters. These, obviously, have to be public.

public:
    void setSides(double lx, double ly);
    void setOrigin(double x, double y);
    double getSidex();
    double getSidey();

Multiple constructors

C++ can be initialised i various ways using different constructors. Indeed, in the same class definition, we can have both a default constructor (e.g. without parameters) and various parametrised constructors. A minimal example follows:

public:
    Box(); //default constructor with no parameters
    Box(double lx, double ly, double x0, double y0); //parametrised constructor

Both will require a suitable implementation in the cpp file:


Box::Box(){
    // an empty constructor
}
Box::Box(double lx, double ly, double x0, double y0){
     //this constructor actually assigns values
     this->sidex = lx;
    //  ...
}

Task 1: Construct the Box class

Inspired by what you have done for the System class, create a box.h and a box.cpp file to declare and implement the Box class.

Follow the recommendations above to structure the Box class, with private member variables and public getters and setters.

In particular, provide both a parametrised constructor that takes four parameters:

    Box(double lx, double ly, double x0, double y0);

as well as a default constructor.


Task 2: Use the Box

Go back to your System class and uncomment the parts of the code that pertain to the Box class.

Since we have defined the class and implemented it, we can now use it. In particular we want that, when we construct a System, the Box is also constructed using the correct parameters for its side. We also choose the point (0,0) to always be its lower left corner.

Hence, modify the implementation of the System class so that the member variable simulationBox is an instance of the parametrised constructor of the Box class, using sideLength for both lx and ly parameters and 0,0 for x0 and y0.

To check that your implementation is valid, add the following line to your main.cpp file to print the side of the box:

std::cout<<"The system has a simulation box of side "<<model.simulationBox.getSidex()<<std::endl;

The compilation step is now

g++ main.cpp system.cpp box.cpp -o myvicsek

If you instead have prepared a correct makefile, just type

make

Constructing a Particle

The final object we are interested in possibly the most important: it is the particle, representing the individual agent of the Vicsek model.

The model uses point particles with an associated velocity vector of fixed magnitude but variable angle. This vectors move in space and eventually interact with each other within a radius of interaction \(r\).

Therefore, we simply want a new class that describes objects that contain:

  • an x coordinate
  • a y coordinate
  • an angle theta
  • a velocity magnitude v
  • a radius of interaction r

We shall have all these variables public for simplicity.


We should also decide how these properties get initialised when we create a new particle. We have many options, but the simplest choice could be to assign default values via the default constructor.

Reasonable choices are the following:

  • zero both x, y and theta
  • set r as the unit of length
  • set v to 0.5

This can be directly done in the implementation of the default constructor Particle(), with no parameters.


Task 3: Construct the Particle class

Inspired by what you have done for the System class and the Box class, create a particle.h and a particle.cpp file to declare and implement the particle class.

Use the guidance above to structure your class. It should be sufficient then to uncomment the relevant section of code in system.h to test that the code is valid to proceed with a compilation.

Without a makefile this is

g++ main.cpp system.cpp box.cpp particle.cpp -o myvicsek

If you have not done it already, thi is a good time to create makefile to help yourself with future compilations. A valid makefile can be downloaded here 📝 Makefile. Put it in the same directory of your project and type

make

every time you need to compile your code. For more information, see here.

Creating a vector of particles

C++ has an excellent standard library with multiple data structures that simplify coding in C++. One kwy datastructure is the vector which is accessed via

#include <vector>

As you have seen in the Introduction to C++ part of the course, we can create vectors of any kind:

  • vectors of integers:
std::vector<int> vec; // Creates a 
  • vectors of floating point numbers:
std::vector<double> vec(3, 5.0); // Creates a vector of size 10, all elements initialized to 5.0

In principle, however, we can make vectors out of any type, including the Particle class that we have just defined, using

std::vector<Particle> particles;

We can also just declare a vector and then resize it:

std::vector<Particle> particles; // Creates an empty vector of particles
particles.resize(10);       // Resizes the vector to hold 10 particles, all initialised by their default constructor

Vectors are useful for two reasons:

  • they manage memory allocation and deallocation automatically
  • they can resized dynamically, unlike raw arrays which have fixed sizes.

From our point of view, they are also conceptually very close to Python’s lists. Therefore we are using vectors to simplify or coding.


Task 4: Create and resize the vector of particles.

In system.h you should already have a declaration of a Particle vector. Uncomment it and then move to system.cpp to modify the implementation of the constructor to resize the vector of particles to match the particleNumber member variable.

Random initial state

The last thing that our initialisation of the model needs is a correctly initialise pseudo-random number generator.

Again, the standard library provides us with good tools. A <random> model of the library exists with very good generators.

The paradigm is similar to random number generators in other languages such as Python with numpy: we declare a genertor object and associate it with a particular kind of pseudo-random generator (in this case the Mersenne twister, which has a long period )

#include <random>

int seed = 1234;
std::mt19937 gen = std::mt19937(seed);  

Random numbers from arbitrary distributions are then sampled by first creating a distribution object (for example a uniform distribution between 0 and 1)

std::uniform_real_distribution<double> uniformDist;

and then sampling values from it by pass the random generator in input

double randomValue = uniformDist(gen);

In our code, we need a sampler of the uniform distribution to sample the particle position (and also to perform the noisy dynamics of the Vicsek model). So, it is useful to expand our System class to include a random number generator, initialised in the constructor and a suitable member function to sample uniformly distributed random numbers.


Task 5: Random number generation

Use the advice above to include a member variable in the system.h declaration corresponding to a Mersenne Twister random number generator. Call the generator gen. Also, add a uniform distribution member variable called uniformDist.

Add a suitable int seed parameter to the system’s constructor parameter list. Modify the constructor to initialise the random number generator.

Then, add also a member function

double uniform(double min, double max);

whose implementation is

double System::uniform(double min,double max) {
    return (max-min)*this->uniformDist(gen)+min;

}

To test that this works, modify the main function to print out a random number between 10 and 20.