Hands on objects and classes

Aims

  • Define the essential properties of our simulation and identify them as objects
  • Recall the main syntactical aspects of C++ classes
  • Implement the main structure of the class representing the simulated system

Learning outcomes

  • Familiarise with the rules of Vicsek flocking model
  • Practice object-oriented design choices in C++

Object-oriented programming is about actions between objects

A lot of your programming up to now has been data-centric: you received data, often simply tabulated numerical data, and then you have manipulated it using suitable functions, occasionally custom designed. In this sense, programming boils down to retrieving data, processing it with suitable functions, and produce new data (to be eventually processed again). This procedural programming is very linear, but is often very problem specific: your program finally consists of a well defined pipeline of steps where the data get progressively manipulated. The code becomes easily quite long and complicated.


Object-oriented programming is different: the idea is to look at the problem that you want to solve and identify intelligent ways to break it down into generic, smaller problems.

In particular, the goal is to group variables and functions together into new data types that enjoy new properties and relationships. This allows you to break down a complex code into simpler parts.

From this description it is clear that object-oriented programming (OOP) will be:

  • unsuitable for small projects that can be addressed with standard or existing data types
  • very suitable when considering new problems, with many different parts interacting in various different ways

To make object-oriented interesting, here we are considering an explicit example where reasoning in terms of objects helps us design our code.

Procedural Code

#include <iostream>

double calculate_area(double length, double width) {
    return length * width;
}

int main() {
    double length = 5.0, width = 3.0;
    double area = calculate_area(length, width);

    std::cout << "Area: " << area << std::endl;
    return 0;
}

Object-Oriented Code

#include <iostream>
// Rectangle class to represent a rectangle
class Rectangle {
    public:
        double length;
        double width;
        // Constructor
        Rectangle(double l, double w) : length(l), width(w) {}
        // Member function with its implementation
        double calculate_area() {
            return length * width;
        }
};

int main() {
    Rectangle rect(5.0, 3.0);
    double area = rect.calculate_area();
    std::cout << "Area: " << area << std::endl;
    return 0;
}

Modelling flocking using the Vicsek model

In this mini-project, we are using a minimal model of flocking as our simple application of object-oriented programming to scientific computing.

Wew use the Vicsek model, a celebrated model to describe the behaviour of self-propelled agents subject to alignment interactions.

The problem is two-dimensional. It simulates the collective behavior of self-propelled particles that align their velocities with their neighbors within a certain radius. Each particle moves in a random direction and adjusts its orientation to match the average direction of nearby particles, leading to the emergence of coordinated motion or flocking.


Algorithm

The 2D Vicsek model algorithm can be described in these steps:

  1. Initialize: Place \(N\) particles at random positions \(\mathbf{r}_i\) with random velocities \(\mathbf{v}_i = v (\cos\theta_i, \sin\theta_i)\), where \(\theta_i\) is the orientation angle.

  2. Neighbor Identification: For each particle \(i\), identify neighbors within radius \(r\).

  3. Alignment: Compute the average direction of neighbors, including \(i\): \[ \bar{\theta}_i = \text{atan2}\left(\sum_{\rm j \in neighbours} \sin\theta_j, \sum_{\rm j \in neighbours} \cos\theta_j\right) \]

  4. Noise: Add a random perturbation \(u\) to the orientation as a random variable uniformly distributed in the interval \([-\eta/2,\eta/2]\), where \(\eta\) is teh noise strength

    \[ \theta_i^{\text{new}} = \bar{\theta}_i + u \]

  5. Update Position: Move each particle with its updated velocity: \[ \mathbf{r}_i^{\text{new}} = \mathbf{r}_i + \mathbf{v}_i \Delta t \]

  6. Repeat: Iterate for the desired number of time steps.

From the algorithm above we can extract a few key conceptual entities needed for our simulation

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

Our goal is now to concretely construct C++ objects around these various components. The natural organisation in C++ is to have different files for the different objects, separating their declarations and the implementations. In practice this means having

  • a .h file (a header file) for every object to describe its structure and contain the declarations
  • a .cpp file (a source file) to specify how the object works and contain the the implementation

We will use the example of the System class to recall some essential elements of C++ object orientation.

The System class

Generic class structure

The declaration of a generic C++ class has a standard structure


class MyClass { // UpperCamelCase convention
  public:
    Myclass(); //default constructor

    int memberVariable;  
    float memberFunction();

}; // remember this semicolon!

Objects organise data and functions. The data are stored as member variables (also called attributes) and functions specific to the object are called member functions (also called methods). Member functions and variables can be public or private.

Instantiation

A class declaration is barely the description of the content of an object.

In order to use objects we need to construct them first. The process of constructing objects is called instantiation: we can have indeed many separate instances of the same object.

Every instantiation runs a particular member function of the object, called the constructor. The constructor always has the same name as the class itself and clearly has no type associated with it because what it returns is precisely an object of the type defined by the class.

For example, the class above is named MyClass so it has a member function called MyClass() (notice the brackets). When we want to instantiate an object of such class we just type

MyClass anInstance;

But could as well have multiple instances of the same class

MyClass instance1;
MyClass instance2;
MyClass instance3;

But what does the constructor actually do? In fact, up to now, we did not specify anything. Indeed, we have not yet written any implementation of the class. We will see this very soon in the section below.

At the moment, let us now apply these broad ideas to the specific concept of our System class.

Declaration

The header file for our System class needs precisely to mirror the structure above.

Since a header file can be imported multiple times by various nested objects, it is good practice to confine the code inside so called headr guards, #ifndef/#define statements that make the inclusion of the header optional if the header as already been included.

This means that the basic structure of hour system.h file will be

#ifndef SYSTEM_H
#define SYSTEM_H

class System {
  public:
    System();
};

#endif

This is a bare-bone System: it has nothing specific to our model. We can now sculpt the class the fit what we believe our system should have. For example, here is what we may sketch as a design, where we distinguish member variables from functions

classDiagram
 direction LR
    class System{
      particleNumber
      noiseStrength
      simulationBox
      vector~Particle~ particles
      System()
      updateRule()
      
      }

We have a few member variables:

  • the total number of particles
  • the noise strength in Vicsek model
  • a simulation box
  • a vector containing all the particles

Since C++ is a typed language, we need to think of the types of such variables

  • the total number of particles → it is an integer int
  • the noise strength in Vicsek model → it is a floating point number double
  • a simulation box → it is going to be an instance of a yet-to-be-defined class Box
  • a vector containing all the particles → it is a vector of a yet-to-be-defined class Particle

The System class also contains two member functions:

  • the constructor, which return an instance of the class
  • an update rule to implement the Vicsek dynamics, which just updates the particles, so it is probably of type void

With this in mind we can translate these design ideas into an actual class structure that is specific to our problem

#ifndef SYSTEM_H
#define SYSTEM_H

#include <vector> //to use standard C++ vectors
#include "box.h" //yet to be created!
#include "particle.h" //yet to be created!

class System {
  public:
    System();
    int   particleNumber;
    double noiseStrength;
    Box simulationBox;
    std::vector<Particle> particles;

    void updateRule();
};

#endif

Notice that we added an include statement to use the std::vector for the C++ standard library and construct a vector of particles. Also note that we are also including the definitions of the Box and the Particle classes, which we have not yet written!


Task 1: header file

Create a header file for the System class that mirrors the instructions above.

(Optional but recommended) Add the files to your git repository and commit the changes.

Basic implementation

Now that we have the overall description of the class, we want to implement it. What is missing specifically are the instructions for the default constructor System() and the member function updateRule(). these are to be stored in the source file system.cpp. In it, we include the definitions provided in the header file and then refer to the member functions of a particular class using the :: operator

So, our system.cpp file will start with including the respective header file via

#include "system.h"

and then proceed specifying each member function separately.For example, for the updateRule() member function we will have

void System::updateRule(){
  // the Vicsek update rule...
}

And for the constructor we will have instead

System::System(){
  // whatever we want to do as we instantiate the system
}

Task 2: Simple implementation and instantiation

Create a system.cpp file and implement the constructor and the update rule such that :

  • the constructor prints I am constructing the System!
  • the update rule prints Updating the system...

Remember that you need the <iostream> library to be included in order to be able to print using the standard C++ library (you should know how to do this from Teaching Block 1).

Then, comment out (for now) the Box simulationBox; and the std::vector<Particle> particles; lines from the header file and their associated header files.

Now, in the main.cpp file (that should be in the same folder), inside the main function, instantiate a System object.

Compile your code, either by using a Makefile (see here) or by directly using the compiler with

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

and run it with (or with whatever name you have given tour executable)

./myvicsek

You should see the following output in the command line:

$ ./myvicsek
I am constructing the System!

(Optional but recommended) Add the files to your git repository and commit the changes.


Task 3: Calling a member function

Can you modify the main.cpp file to call the member function updateRule() after instantiating your object? Notice that (differently from the default constructor we wrote above) this uses parenthesis even if it doesn’t take any arguments. Since you use a member function of an instance of a class, you must use the dot operator . as:

instance.memberFunction();

A more meaningful constructor

We have now a working basic class. It does not do much and it does not even store any data, but it should work. Ideally, we would instead like to construct our system so that it can at least store some parameters of our model to be used later on. To do so we need to enrich the constructor, since the default constructor is taking no arguments.

There are several parameters that it may be useful to pass to the constructor. Some may be worth storing in the System object as member variables, some may not, depending on your design choices.

Parameters that are meaningful to pass to the constructor could be:

  • the total number of particles N (mentioned earlier)
  • the noise strength (mentioned earlier)

but we may want to have more, for example:

  • the size of the system (if it is a square box, it would be just its side)
  • the time step of the simulation

To do this, we need to modify the constructor declaration to take these parameters as arguments. We will also need to update the implementation of the constructor to initialize the member variables with these parameters.


Task 4: Improve the constructor declaration

Modify the System class declaration in system.h so that

public:
    System();

is turned into

public:
    System(int particleNumber,double sideLength, double timeStep,double noiseStrength);

Try to compile the code again: can you explain the error messages?

(Optional but recommended) Commit the changes to your repository via git.

Using this

Such changes need then to be mirrored in the implementation, i.e. the .cpp file. Here we need to be able to refer specifically to the member variables of the class in an unambiguous way. The way C++ does this is by using the keyword this.

Important

Technically this is a pointer to the current object (i.e. the memory address of the object) so to access any of its members (variables or ) is done using the -> operator with the syntax

this->memberVariable = ...;

When instead one accesses member functions and variables directly from an object instance one uses the . operator

So, for every parameter that we get through the constructor, we can use the this-> construction to store it into a member variable.


Task 5: Assigning member variables

Edit the system.cpp file so that the constructor now mirrors its declaration in the system.h file and stores the following parameters in the corresponding member variables:

  • the particle number
  • the noise strength

Modify the main.cpp file so that now you call the constructor with suitable parameters. For this it is sufficient to use (...) (with the dots replaced by the required parameters) after the name of the instance. From the main() function, use a cout statement to print out the values of the particle number and noise after the construction of your instance of the class System.

Take the following numerical values:

  • 100 particles
  • side length 20.0
  • timestep of 0.5
  • noise strength 0.1

The units of these quantities will become apparent when we will discuss the details of the model in the next workshop.

Compile and run.

(Optional but recommended) Commit the changes to your git repository.


Task 6: Adjusting the design and the implementation

Now modify both the declaration (system.h) and the implementation (system.cpp) so that you can also store the timestep and the side length.

(Optional but recommended) Commit the changes to your git repository.