# 3. Your first simulation
**Outcome:** run a complete two-particle spring simulation with boundaries and
a thermostat. Complete installation first. All paths below are relative to the
new `pip-simulation` application.
## Complete manifest
Replace `Cargo.toml` with:
```toml
[package]
name = "pip-simulation"
version = "0.1.0"
edition = "2024"
rust-version = "1.97"
[dependencies]
physics_in_parallel = "=4.1.1-alpha"
rayon = "1.12"
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
```
## Complete source
Replace `src/main.rs` with the same implementation as the repository's
[basic particle example](../../examples/basic_particle.rs):
```rust
//! A complete particle loop with an application-owned Rayon pool.
//! Semi-implicit Euler -> reflecting walls -> Langevin thermostat is the
//! explicitly chosen splitting here; it is not a higher-order integrator.
use physics_in_parallel::prelude::basic::{ReflectBox, ResolvedRng, RngMethod, set_max_threads};
use physics_in_parallel::prelude::models::*;
fn simulate() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut particles = create_template(2, 2)?;
particles.set_attribute_vector(ATTR_R, 0, &[0.25, 0.5])?;
particles.set_attribute_vector(ATTR_R, 1, &[0.75, 0.5])?;
set_mass(&mut particles, 0, 2.0)?;
let mut springs = SpringNetwork::new();
springs.insert((0, 1), Spring::new(1.0, 0.4, None)?)?;
let boundary = ReflectBox::new(&[0.0, 0.0], &[1.0, 1.0])?;
let mut thermostat = LangevinThermostat::new(
0.01,
0.1,
ResolvedRng::new(42, RngMethod::IndexedSplitMix64),
ParticleSelection::AliveOnly,
)?;
let mut integrator = SemiImplicitEuler;
for step in 0..20 {
// Force modules accumulate acceleration; each step starts from zero.
particles.attribute_mut::<f64>(ATTR_A)?.fill(0.0);
springs.apply(&mut particles, ParticleSelection::AliveOnly)?;
integrator.apply(&mut particles, 0.01)?;
boundary.apply_to_particles(&mut particles)?;
thermostat.apply(&mut particles, 0.01)?;
if (step + 1) % 10 == 0 {
let summary = kinetic_summary(&particles, ParticleSelection::AliveOnly)?;
println!(
"step {}: KE={}, temperature={}",
step + 1,
summary.energy,
summary.temperature
);
}
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let pool = rayon::ThreadPoolBuilder::new().num_threads(4).build()?;
set_max_threads(Some(4))?;
pool.install(simulate)
}
```
Run `cargo run --release`. Expect two observations, labeled `step 10` and
`step 20`, each reporting kinetic energy and temperature. The seed and operation
order are fixed. These observations are a bounded demonstration, not evidence
that the thermostat has equilibrated after twenty steps.
## Follow one step
1. Clear acceleration; forces accumulate into it.
2. Apply the spring force to selected particles.
3. Advance with semi-implicit Euler at timestep 0.01.
4. Reflect positions and velocities at the box walls.
5. Apply the Langevin thermostat using its retained RNG state.
6. Observe every ten steps without changing the simulation's update order.
This force โ integrator โ boundary โ thermostat ordering is an explicit
scientific splitting. PiP does not infer a higher-order method from the sequence.
The application owns the loop, its timestep, and the observation interval.
## Change one thing at a time
Try changing the spring rest length or timestep, retaining a copy of the original
output. Check physical effects separately from code compilation. Changing mass
must go through `set_mass` so mass and inverse mass remain consistent. Chapters
7 and 8 explain these model contracts.
The model does not save a checkpoint automatically. Chapter 9 covers validated
serialization; an application or workflow runner owns actual persistence.
---
**Previous:** [2. Installation](2-installation.md) ยท **Guide index:** [Documentation](../README.md)
**Next:** [4. Tensors, matrices, and vectors](4-tensors-matrices-and-vectors.md)