Skip to main content

librebound_sys/
wrappers.rs

1//! Safe RAII wrappers around REBOUND C objects.
2
3use crate::ffi;
4use crate::{Error, Result};
5
6// ---------------------------------------------------------------------------
7// IAS15 adaptive-timestep mode (mirrors `enum reb_ias15.adaptive_mode`)
8// ---------------------------------------------------------------------------
9
10/// IAS15 timestep-adaptation rule. REBOUND default since 2024-01 is
11/// [`Self::Prs23`]; older code used [`Self::Global`].
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[repr(i32)]
14pub enum Ias15AdaptiveMode {
15    /// Per-particle fractional error.
16    Individual = 0,
17    /// Single global fractional-error estimate (default before 2024-01).
18    Global = 1,
19    /// Pham, Rein & Spiegel (2023) criterion (default since 2024-01).
20    Prs23 = 2,
21    /// Aarseth (1985) timestep criterion.
22    Aarseth85 = 3,
23}
24
25impl Ias15AdaptiveMode {
26    fn from_raw(raw: i32) -> Self {
27        match raw {
28            0 => Self::Individual,
29            1 => Self::Global,
30            2 => Self::Prs23,
31            3 => Self::Aarseth85,
32            // Forward-compatibility: unknown values reported as the current
33            // REBOUND default. The setter only accepts the four enum values,
34            // so this branch is reachable only if REBOUND adds a new mode.
35            _ => Self::Prs23,
36        }
37    }
38}
39
40// ---------------------------------------------------------------------------
41// IntegratorConfig
42// ---------------------------------------------------------------------------
43
44/// Per-call IAS15 integrator knobs. `None` for any field leaves the REBOUND
45/// default in place.
46///
47/// Applied to a freshly-created [`Simulation`] before any particles are added.
48#[derive(Debug, Clone, Copy, Default)]
49pub struct IntegratorConfig {
50    /// REBOUND `r->dt` (initial timestep). The integrator picks the sign
51    /// automatically once the first step is taken.
52    pub initial_dt: Option<f64>,
53    /// REBOUND `r->ri_ias15.min_dt`. When the adaptive step would shrink
54    /// below this, it clamps. `None` (or 0) = no floor.
55    pub min_dt: Option<f64>,
56    /// REBOUND `r->ri_ias15.epsilon` (precision). REBOUND default 1e-9.
57    pub epsilon: Option<f64>,
58    /// REBOUND `r->ri_ias15.adaptive_mode`. Default `Prs23` since 2024-01.
59    pub adaptive_mode: Option<Ias15AdaptiveMode>,
60}
61
62impl IntegratorConfig {
63    /// Apply each `Some` field to `sim`. Should be called immediately after
64    /// `Simulation::new()` and before adding particles.
65    pub fn apply(&self, sim: &mut Simulation) {
66        if let Some(dt) = self.initial_dt {
67            sim.set_dt(dt);
68        }
69        if let Some(eps) = self.epsilon {
70            sim.set_ias15_epsilon(eps);
71        }
72        if let Some(min_dt) = self.min_dt {
73            sim.set_ias15_min_dt(min_dt);
74        }
75        if let Some(mode) = self.adaptive_mode {
76            sim.set_ias15_adaptive_mode(mode);
77        }
78    }
79}
80
81// ---------------------------------------------------------------------------
82// Simulation
83// ---------------------------------------------------------------------------
84
85/// Owned REBOUND simulation. Freed on drop.
86///
87/// Not `Send`/`Sync` — each thread must create its own simulation.
88pub struct Simulation {
89    ptr: *mut ffi::reb_simulation,
90}
91
92impl Simulation {
93    /// Create a new, empty REBOUND simulation.
94    pub fn new() -> Result<Self> {
95        let ptr = unsafe { ffi::reb_simulation_create() };
96        if ptr.is_null() {
97            return Err(Error::Other("reb_simulation_create returned null".into()));
98        }
99        Ok(Self { ptr })
100    }
101
102    /// Raw const pointer to the underlying REBOUND simulation. Downstream
103    /// `-sys` crates layered on top of REBOUND use this to call ASSIST /
104    /// other C APIs that take `*const reb_simulation`.
105    pub fn as_ptr(&self) -> *const ffi::reb_simulation {
106        self.ptr
107    }
108
109    /// Raw mutable pointer to the underlying REBOUND simulation. Required by
110    /// C APIs that take `*mut reb_simulation` (assist_attach, assist_detach,
111    /// etc.) and by benchmark probes.
112    pub fn as_mut_ptr(&mut self) -> *mut ffi::reb_simulation {
113        self.ptr
114    }
115
116    /// Alias for [`as_mut_ptr`] kept for backward compatibility.
117    #[doc(hidden)]
118    pub fn raw_ptr_mut(&mut self) -> *mut ffi::reb_simulation {
119        self.ptr
120    }
121
122    pub fn t(&self) -> f64 {
123        unsafe { ffi::assist_rs_sim_get_t(self.ptr) }
124    }
125    pub fn set_t(&mut self, t: f64) {
126        unsafe { ffi::assist_rs_sim_set_t(self.ptr, t) }
127    }
128
129    pub fn dt(&self) -> f64 {
130        unsafe { ffi::assist_rs_sim_get_dt(self.ptr) }
131    }
132    pub fn set_dt(&mut self, dt: f64) {
133        unsafe { ffi::assist_rs_sim_set_dt(self.ptr, dt) }
134    }
135
136    pub fn n_particles(&self) -> usize {
137        unsafe { ffi::assist_rs_sim_get_N(self.ptr) as usize }
138    }
139
140    /// Total IAS15 steps (accepted + rejected) since this simulation was
141    /// created. Useful for diagnosing adaptive-timestep behavior.
142    pub fn steps_done(&self) -> u64 {
143        unsafe { ffi::assist_rs_sim_get_steps_done(self.ptr) }
144    }
145
146    pub fn n_var(&self) -> i32 {
147        unsafe { ffi::assist_rs_sim_get_N_var(self.ptr) }
148    }
149
150    pub fn status(&self) -> i32 {
151        unsafe { ffi::assist_rs_sim_get_status(self.ptr) }
152    }
153
154    pub fn set_exact_finish_time(&mut self, v: bool) {
155        unsafe { ffi::assist_rs_sim_set_exact_finish_time(self.ptr, v as i32) }
156    }
157
158    /// IAS15 precision parameter (REBOUND `r->ri_ias15.epsilon`). Default 1e-9.
159    /// Larger values are looser but faster.
160    pub fn ias15_epsilon(&self) -> f64 {
161        unsafe { ffi::assist_rs_sim_get_ias15_epsilon(self.ptr) }
162    }
163    pub fn set_ias15_epsilon(&mut self, eps: f64) {
164        unsafe { ffi::assist_rs_sim_set_ias15_epsilon(self.ptr, eps) }
165    }
166
167    /// IAS15 minimum timestep floor (REBOUND `r->ri_ias15.min_dt`). When the
168    /// adaptive step would shrink below this, it clamps instead of grinding.
169    /// Default 0 = no floor.
170    pub fn ias15_min_dt(&self) -> f64 {
171        unsafe { ffi::assist_rs_sim_get_ias15_min_dt(self.ptr) }
172    }
173    pub fn set_ias15_min_dt(&mut self, min_dt: f64) {
174        unsafe { ffi::assist_rs_sim_set_ias15_min_dt(self.ptr, min_dt) }
175    }
176
177    /// IAS15 adaptive-timestep selector. Default `Prs23` (REBOUND default
178    /// since 2024-01).
179    pub fn ias15_adaptive_mode(&self) -> Ias15AdaptiveMode {
180        let raw = unsafe { ffi::assist_rs_sim_get_ias15_adaptive_mode(self.ptr) };
181        Ias15AdaptiveMode::from_raw(raw)
182    }
183    pub fn set_ias15_adaptive_mode(&mut self, mode: Ias15AdaptiveMode) {
184        unsafe { ffi::assist_rs_sim_set_ias15_adaptive_mode(self.ptr, mode as i32) }
185    }
186
187    /// Diagnostic counter: how many IAS15 steps hit the predictor-corrector
188    /// iteration cap without converging. Monotone-increasing across the
189    /// simulation lifetime; nonzero indicates the integrator was working
190    /// at the edge of convergence (typically a hint to tighten `epsilon` or
191    /// raise `min_dt`).
192    pub fn ias15_iterations_max_exceeded(&self) -> u64 {
193        unsafe { ffi::assist_rs_sim_get_ias15_iterations_max_exceeded(self.ptr) }
194    }
195
196    /// Add a particle to the simulation.
197    pub fn add_particle(&mut self, p: ffi::reb_particle) {
198        unsafe { ffi::reb_simulation_add(self.ptr, p) }
199    }
200
201    /// Add a test particle with given position and velocity (mass = 0).
202    pub fn add_test_particle(&mut self, x: f64, y: f64, z: f64, vx: f64, vy: f64, vz: f64) {
203        let p = ffi::reb_particle {
204            x,
205            y,
206            z,
207            vx,
208            vy,
209            vz,
210            m: 0.0,
211            ..Default::default()
212        };
213        self.add_particle(p);
214    }
215
216    /// Read-only access to the particle array.
217    pub fn particles(&self) -> &[ffi::reb_particle] {
218        let n = self.n_particles();
219        if n == 0 {
220            return &[];
221        }
222        let ptr = unsafe { ffi::assist_rs_sim_get_particles(self.ptr) };
223        if ptr.is_null() {
224            return &[];
225        }
226        unsafe { std::slice::from_raw_parts(ptr, n) }
227    }
228
229    /// Integrate to target time. Returns the status code.
230    pub fn integrate(&mut self, tmax: f64) -> Result<()> {
231        let status = unsafe { ffi::reb_simulation_integrate(self.ptr, tmax) };
232        match status {
233            ffi::REB_STATUS_SUCCESS | ffi::REB_STATUS_RUNNING => Ok(()),
234            ffi::REB_STATUS_NO_PARTICLES => Err(Error::NoParticles),
235            ffi::REB_STATUS_ENCOUNTER => Err(Error::CloseEncounter),
236            ffi::REB_STATUS_ESCAPE => Err(Error::Escape),
237            ffi::REB_STATUS_COLLISION => Err(Error::Collision),
238            other => Err(Error::IntegrationFailed(other)),
239        }
240    }
241
242    /// Add first-order variational particles for a test particle.
243    /// Returns the index of the first variational particle.
244    pub fn add_variation_1st_order(&mut self, testparticle: i32) -> i32 {
245        unsafe { ffi::reb_simulation_add_variation_1st_order(self.ptr, testparticle) }
246    }
247}
248
249impl Drop for Simulation {
250    fn drop(&mut self) {
251        if !self.ptr.is_null() {
252            unsafe { ffi::reb_simulation_free(self.ptr) }
253        }
254    }
255}