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
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
(double l, double w) : length(l), width(w) {}
Rectangle// Member function with its implementation
double calculate_area() {
return length * width;
}
};
int main() {
(5.0, 3.0);
Rectangle rectdouble 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.
The 2D Vicsek model algorithm can be described in these steps:
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.
Neighbor Identification: For each particle \(i\), identify neighbors within radius \(r\).
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) \]
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 \]
Update Position: Move each particle with its updated velocity: \[ \mathbf{r}_i^{\text{new}} = \mathbf{r}_i + \mathbf{v}_i \Delta t \]
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
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:
(); //default constructor
Myclass
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:
();
Systemint particleNumber;
double noiseStrength;
;
Box simulationBoxstd::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!
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
}
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.
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
.
So, for every parameter that we get through the constructor, we can use the this->
construction to store it into a member variable.