Help with beginner's project

I recently started self-learning Rust. I went through the Rust book and watched several explanation videos. I am not a professional programmer, but a curious physicist about Rust. As a learning project, I am doing a simple particles simulator, similar to something I did some years ago in C++. I created a struct Particle, which represents a particle, and a struct Interaction which represents an interaction between two particles (i.e. a force). Up to here I think things are working. Now I am creating a new structure which is ParticlesSystem which will hold all the particles and the interactions, and plan to add methods such as calculate_forces and advance_time to perform the simulation, and probably some utilities to save the status to a file, etc. This is my complete code:

use euclid::Vector3D;

/// Defines units of position.
enum Position {}
/// Defines units of velocity.
enum Velocity {}
/// Defines units of acceleration;
enum Acceleration {}
/// Defines units of force.
enum Force {}

/// Represents the concept of a particle in classical mechanics.
#[derive(Debug)]
struct Particle {
    position: Vector3D::<f64, Position>,
    velocity: Vector3D::<f64, Velocity>,
    acceleration: Vector3D::<f64, Acceleration>,
    mass: f64,
}

/// Represents an interaction between two particles, which will lead to a force.
#[derive(Debug)]
struct Interaction<'a> {
	particle_1: &'a Particle,
	particle_2: &'a Particle,
}

impl <'a> Interaction<'a> {
	/// Computes the force acting on `particle_1` due to this interaction.
	fn force_acting_on_particle_1(&self) -> Vector3D<f64,Force> {
		let a = self.particle_1.position;
		let b = self.particle_2.position;
		Vector3D::<f64,Force>::new(b.x-a.x, b.y-a.y, b.z-a.z).normalize()
	}
	/// Computes the force acting on `particle_2` due to this interaction.
	fn force_acting_on_particle_2(&self) -> Vector3D<f64,Force> {
		self.force_acting_on_particle_1() * -1.
	}
}

/// Represents a system of particles, i.e. a collection of particles that interact.
#[derive(Debug)]
struct ParticlesSystem<'a> {
	particles: Vec::<Particle>,
	interactions: Vec::<Interaction<'a>>,
}

impl <'a> ParticlesSystem<'a> {
	/// Creates an empty particles system.  
	fn new() -> Self {
		Self {
			particles: Vec::<Particle>::new(),
			interactions: Vec::<Interaction<'a>>::new(),
		}
	}
	/// Add a particle to the system.
	fn add_particle(&mut self, p: Particle) -> usize {
		&self.particles.push(p);
		self.particles.len()
	}
	/// Add an interaction between two particles of the system.
	fn add_interaction(& mut self, p_id_a: usize, p_id_b: usize) {
		let interaction = Interaction {
			particle_1: &self.particles[p_id_a],
			particle_2: &self.particles[p_id_b],
		};
		&self.interactions.push(interaction);
	}
}

fn main() {
	let mut system = ParticlesSystem::new();
    
    let mut p = Particle {
        position: Vector3D::<f64,Position>::new(-1.,0.,0.),
        velocity: Vector3D::<f64,Velocity>::new(0.,0.,0.),
        acceleration: Vector3D::<f64,Acceleration>::new(0.,0.,0.),
        mass: 1.,
    };
    system.add_particle(p);
    let mut p = Particle {
        position: Vector3D::<f64,Position>::new(1.,0.,0.),
        velocity: Vector3D::<f64,Velocity>::new(0.,0.,0.),
        acceleration: Vector3D::<f64,Acceleration>::new(0.,0.,0.),
        mass: 2.,
    };
    system.add_particle(p);
    let mut p = Particle {
        position: Vector3D::<f64,Position>::new(0.,1.,0.),
        velocity: Vector3D::<f64,Velocity>::new(0.,0.,0.),
        acceleration: Vector3D::<f64,Acceleration>::new(0.,0.,0.),
        mass: 3.,
    };
    system.add_particle(p);
    
    system.add_interaction(0,1);
    system.add_interaction(0,2);
    system.add_interaction(1,2);
	
	dbg!(system);
}

which gives me this when I try to compile it:

error: lifetime may not live long enough
   --> src/main.rs:102:4
    |
83  | impl <'a> ParticlesSystem<'a> {
    |       -- lifetime `'a` defined here
...
97  |     fn add_interaction(& mut self, p_id_a: usize, p_id_b: usize) {
    |                        - let's call the lifetime of this reference `'1`
...
102 |         &self.interactions.push(interaction);
    |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'a`

I think I understand the reason for this, so I modify the definition of add_interaction as fn add_interaction(&'a mut self, p_id_a: usize, p_id_b: usize) to tell the compiler self will live at least as long as the interactions Interaction<'a>. However, after this I get

error[E0499]: cannot borrow `system` as mutable more than once at a time
  --> src/main.rs:97:5
   |
96 |     system.add_interaction(0,1);
   |     ------ first mutable borrow occurs here
97 |     system.add_interaction(0,2);
   |     ^^^^^^
   |     |
   |     second mutable borrow occurs here
   |     first borrow later used here

How can I solve this in a way compatible with Rust?

A general guideline is: when learning Rust, don't store references in structs. References are designed primarily for short lived usage in local variables, parameters, etc. The Interaction struct should store the indexes rather than references to Particle.

For example, see mistake #7. This document has a lot of other common mistakes when learning Rust.


Another thing I noticed is here:

		&self.particles.push(p);

The & should be removed. This would only be used if you wanted to get the reference to the return value of push (() in this case). It isn't needed to call through the self reference.

When storing indexes instead of references in structs, you often need to pass more parameters. For example, here is your code with the references in Particle changed to indexes. I added a &[Particle] param to the Interaction methods, since they need to get a Particle struct from its index. There are other ways to do it, that's just one possibility. Notice that all the lifetimes annotations are gone.

type ParticleIdx = usize;

/// Represents an interaction between two particles, which will lead to a force.
#[derive(Debug)]
struct Interaction {
    particle_1: ParticleIdx,
    particle_2: ParticleIdx,
}

impl Interaction {
    /// Computes the force acting on `particle_1` due to this interaction.
    fn force_acting_on_particle_1(
        &self,
        particles: &[Particle],
    ) -> Vector3D<f64, Force> {
        let a = particles[self.particle_1].position;
        let b = particles[self.particle_2].position;
        Vector3D::<f64, Force>::new(b.x - a.x, b.y - a.y, b.z - a.z)
            .normalize()
    }
    /// Computes the force acting on `particle_2` due to this interaction.
    fn force_acting_on_particle_2(
        &self,
        particles: &[Particle],
    ) -> Vector3D<f64, Force> {
        self.force_acting_on_particle_1(particles) * -1.
    }
}

/// Represents a system of particles, i.e. a collection of particles that interact.
#[derive(Debug)]
struct ParticlesSystem {
    particles: Vec<Particle>,
    interactions: Vec<Interaction>,
}

impl<'a> ParticlesSystem {
    /// Creates an empty particles system.  
    fn new() -> Self {
        Self {
            particles: Vec::<Particle>::new(),
            interactions: Vec::<Interaction>::new(),
        }
    }
    /// Add a particle to the system.
    fn add_particle(&mut self, p: Particle) -> usize {
        self.particles.push(p);
        self.particles.len()
    }
    /// Add an interaction between two particles of the system.
    fn add_interaction(
        &mut self,
        p_id_a: ParticleIdx,
        p_id_b: ParticleIdx,
    ) {
        let interaction =
            Interaction { particle_1: p_id_a, particle_2: p_id_b };
        self.interactions.push(interaction);
    }
}
1 Like

You're trying to create a "self-referencial struct", where the Interaction<'a> within the interactions field contain references to Particles within the particles field. Rust doesn't natively support such constructions in a meaningful way.

You could store indices in Interaction instead.

You could split up your struct into one that owns the Particles, and another that borrows from the owning one, so they're not part of the same struct.

There are other approaches, including using shared ownership and shared mutability (e.g. Rc<RefCell<_>> or such) practically everywhere.

@jumpnbrownweasel thanks, that was really helpful!

I now have my very first version running. The expression for the interaction is hardcoded and there is only one type of interaction. In an OOP language like Python or C++ I would subclass Interaction and define different possible interactions, e.g. elastic force, electromagnetic force, etc. What would be the way of achieving this in Rust? I need different implementations of force_acting_on_particle_1. I am not sure this is than with traits, as they seem to be rather a common functionality for different kinds of objects, not different functionalities for similar kinds of objects.

This is my code now:

use euclid::Vector3D;
use sqlite;

/// Defines units of position.
enum Position {}
/// Defines units of velocity.
enum Velocity {}
/// Defines units of acceleration;
enum Acceleration {}
enum Force {}
/// Defines units of mass.
type Mass = f64;

/// Represents the concept of a particle in classical mechanics.
#[derive(Debug)]
struct Particle {
    position: Vector3D::<f64, Position>,
    velocity: Vector3D::<f64, Velocity>,
    mass: Mass,
}

type ParticleIdx = usize;

/// Represents an interaction between two particles, which will lead to a force.
#[derive(Debug)]
struct Interaction {
    particle_1_idx: ParticleIdx,
    particle_2_idx: ParticleIdx,
}

impl Interaction {
    /// Computes the force acting on `particle_1` due to this interaction.
    fn force_acting_on_particle_1(&self, particles: &[Particle]) -> Vector3D<f64, Force> {
        let a = particles[self.particle_1_idx].position;
        let b = particles[self.particle_2_idx].position;
        Vector3D::<f64, Force>::new(b.x - a.x, b.y - a.y, b.z - a.z).normalize()
    }
    /// Computes the force acting on `particle_2` due to this interaction.
    fn force_acting_on_particle_2(&self, particles: &[Particle]) -> Vector3D<f64, Force> {
        self.force_acting_on_particle_1(particles) * -1.
    }
}

/// Represents a system of particles, i.e. a collection of particles that interact.
#[derive(Debug)]
struct ParticlesSystem {
    particles: Vec<Particle>,
    interactions: Vec<Interaction>,
    time: f64,
    n_time_saved_to_sql: usize,
}

impl ParticlesSystem {
    /// Creates an empty particles system.  
    fn new() -> Self {
        Self {
            particles: Vec::<Particle>::new(),
            interactions: Vec::<Interaction>::new(),
            time: 0.,
            n_time_saved_to_sql: 0,
        }
    }
    /// Add a particle to the system.
    fn add_particle(&mut self, p: Particle) -> usize {
        self.particles.push(p);
        self.particles.len()
    }
    /// Add an interaction between two particles of the system.
    fn add_interaction(&mut self, p_id_a: ParticleIdx, p_id_b: ParticleIdx) {
        let interaction = Interaction { particle_1_idx: p_id_a, particle_2_idx: p_id_b };
        self.interactions.push(interaction);
    }
    /// Advance the time and update the system.
    fn advance_time(&mut self, time_step: f64) {
        // First we compute the acceleration of each particle using the interactions:
        let mut accelerations = vec![Vector3D::<f64,Acceleration>::zero(); self.particles.len()]; // A vector with one acceleration for each particle.
        for interaction in &self.interactions {
            accelerations[interaction.particle_1_idx] += interaction.force_acting_on_particle_1(&self.particles).cast_unit()/self.particles[interaction.particle_1_idx].mass;
            accelerations[interaction.particle_2_idx] += interaction.force_acting_on_particle_2(&self.particles).cast_unit()/self.particles[interaction.particle_2_idx].mass;
        }
        // Now we move the system forward in time:
        for (n_particle,p) in self.particles.iter_mut().enumerate() {
            let a = accelerations[n_particle];
            let dv: Vector3D::<f64,Velocity> = a.cast_unit()*time_step;
            let dr: Vector3D::<f64,Position> = p.velocity.cast_unit()*time_step + dv.cast_unit()*time_step/2.;
            p.position = p.position + dr;
            p.velocity = p.velocity + dv;
        }
        self.time += time_step;
	}
    /// Creates an SQLite file to save the data.
    fn create_sqlite_connection(&self, file_name: &String) -> sqlite::Connection {
        let connection = sqlite::open(file_name).unwrap();
        connection.execute("CREATE TABLE particles_system (n_time INTEGER, n_particle INTEGER, position_x FLOAT, position_y FLOAT, position_z FLOAT, velocity_x FLOAT, velocity_y FLOAT, velocity_z FLOAT, mass FLOAT);").unwrap();
        connection.execute("CREATE TABLE time (n_time INTEGER, time FLOAT);").unwrap();
        connection
    }
    /// Save the state of the system into an SQLite file.
    fn dump_to_sqlite(&mut self, connection: &sqlite::Connection) {
        for (n_particle,p) in self.particles.iter().enumerate() {
            let mut query = String::new();
            query.push_str("INSERT INTO particles_system VALUES (");
            query.push_str(&self.n_time_saved_to_sql.to_string());
            query.push_str(", ");
            query.push_str(&n_particle.to_string());
            query.push_str(", ");
            query.push_str(&p.position.x.to_string());
            query.push_str(", ");
            query.push_str(&p.position.y.to_string());
            query.push_str(", ");
            query.push_str(&p.position.z.to_string());
            query.push_str(", ");
            query.push_str(&p.velocity.x.to_string());
            query.push_str(", ");
            query.push_str(&p.velocity.y.to_string());
            query.push_str(", ");
            query.push_str(&p.velocity.z.to_string());
            query.push_str(", ");
            query.push_str(&p.mass.to_string());
            query.push_str(");");
            connection.execute(query).unwrap();
        }
        let mut query = String::new();
        query.push_str("INSERT INTO time VALUES (");
        query.push_str(&self.n_time_saved_to_sql.to_string());
        query.push_str(", ");
        query.push_str(&self.time.to_string());
        query.push_str(");");
        connection.execute(query).unwrap();

        self.n_time_saved_to_sql += 1;
    }
}

fn main() {
	let mut system = ParticlesSystem::new();
    
    let p = Particle {
        position: Vector3D::<f64,Position>::new(-1.,0.,0.),
        velocity: Vector3D::<f64,Velocity>::new(0.,0.,0.),
        mass: 1.,
    };
    system.add_particle(p);
    let p = Particle {
        position: Vector3D::<f64,Position>::new(1.,0.,0.),
        velocity: Vector3D::<f64,Velocity>::new(0.,0.,0.),
        mass: 2.,
    };
    system.add_particle(p);
    let p = Particle {
        position: Vector3D::<f64,Position>::new(0.,1.,0.),
        velocity: Vector3D::<f64,Velocity>::new(0.,0.,0.),
        mass: 3.,
    };
    system.add_particle(p);
    let p = Particle {
        position: Vector3D::<f64,Position>::new(0.,-1.,0.),
        velocity: Vector3D::<f64,Velocity>::new(0.,0.,0.),
        mass: 4.,
    };
    system.add_particle(p);
    
    system.add_interaction(0,1);
    system.add_interaction(1,2);
    system.add_interaction(2,3);

    let connection = system.create_sqlite_connection(&String::from("/home/me/Desktop/newton.db"));
    system.dump_to_sqlite(&connection); // Save initial state.
    for n_time in 1..999999 {
        system.advance_time(0.00001);
        if n_time % 999 == 0 {
            system.dump_to_sqlite(&connection);
        }
    }
}

The simplest way is probably to define an enum with one variant per possible interaction type, and store it as a field inside Interaction.

3 Likes

To expand a bit, the two options are usually either an enum or a trait object.

A Rust enum is a tagged union, similar to std::variant in C++, so it can carry specific parameters for each type of interaction. This is a great option if you want to support a closed set of interaction types.

If you want someone else to be able to extend the set of interactions, you would typically use a trait that would act similar to an abstract base class in C++. You can then store it as Box<dyn InteractionType>, assuming that's the trait name, to erase the concrete type.

I agree that the enum sounds like a good place to start.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.