Skip to main content

roche/
lobes.rs

1use crate::errors::RocheError;
2use crate::potential::{drpot, rpot};
3use crate::x_lagrange::x_l1;
4use crate::{Star, Vec3};
5use pyo3::prelude::*;
6
7// structure to find specific roche potential along a line
8pub struct LineRoche {
9    // mass ratio
10    pub q: f64,
11    // which star are we concerned with? (primary or secondary)
12    pub star: Star,
13    // direction of line in x
14    pub dx: f64,
15    // direction of line in y
16    pub dy: f64,
17    // critical potential to solve for
18    pub cpot: f64,
19}
20
21impl LineRoche {
22    pub fn new(q: f64, star: Star, dx: f64, dy: f64, cpot: f64) -> Self {
23        Self {
24            q,
25            star,
26            dx,
27            dy,
28            cpot,
29        }
30    }
31
32    pub fn cost(&self, lam: f64) -> Result<(f64, f64), RocheError> {
33        let p: Vec3 = match self.star {
34            Star::Primary => Vec3::new(lam * self.dx, lam * self.dy, 0.0),
35            Star::Secondary => Vec3::new(1.0 + lam * self.dx, lam * self.dy, 0.0),
36        };
37        // how far are we from root?
38        let f: f64 = rpot(self.q, &p)? - self.cpot;
39        // gradient of potential at point
40        let dp: Vec3 = drpot(self.q, &p)?;
41        // dot product of gradient with line direction - gives scalar gradient in direction of line
42        let d: f64 = self.dx * dp.x + self.dy * dp.y;
43        Ok((f, d))
44    }
45}
46
47/// 
48/// lobe1 returns arrays x and y for plotting an equatorial section
49/// of the Roche lobe of the primary star in a binary of mass ratio q = M2/M1.
50/// The arrays start and end at the inner Lagrangian point and march around 
51/// uniformly in azimuth looking from the centre of mass of the primary star.
52/// n is the number of points and must be at least 3.
53/// 
54#[pyfunction]
55#[pyo3(signature = (q, n=200))]
56pub fn lobe1(q: f64, n: usize) -> Result<(Vec<f64>, Vec<f64>), RocheError> {
57    // Accuracy of location of surface in terms of binary separation
58    const FRAC: f64 = 1.0e-6;
59
60    // Compute the potential at the inner Lagrange point
61    let rl1: f64 = x_l1(q)?;
62    let p: Vec3 = Vec3::new(rl1, 0.0, 0.0);
63    let cpot: f64 = rpot(q, &p)?;
64
65    let mut xarr: Vec<f64> = Vec::with_capacity(n);
66    let mut yarr: Vec<f64> = Vec::with_capacity(n);
67    for i in 0..n {
68        if i == 0 || i == n - 1 {
69            // special case as derivative is zero at L1
70            xarr.push(rl1);
71            yarr.push(0.0);
72        } else {
73            let theta: f64 = (i as f64) * std::f64::consts::PI * 2.0 / ((n as f64) - 1.0);
74            let dx: f64 = theta.cos();
75            let dy: f64 = theta.sin();
76            let line: LineRoche = LineRoche::new(q, Star::Primary, dx, dy, cpot);
77            let lam: f64 = rtsafe(rl1 / 4.0, rl1, |lam| line.cost(lam), FRAC)?;
78            xarr.push(lam * dx);
79            yarr.push(lam * dy);
80        }
81    }
82    Ok((xarr, yarr))
83}
84
85/// 
86/// lobe2 returns arrays x and y for plotting an equatorial section
87/// of the Roche lobe of the secondary star in a binary of mass ratio q = M2/M1.
88/// The arrays start and end at the inner Lagrangian point and march around 
89/// uniformly in azimuth looking from the centre of mass of the primary star.
90/// n is the number of points and must be at least 3.
91/// 
92#[pyfunction]
93#[pyo3(signature = (q, n=200))]
94pub fn lobe2(q: f64, n: usize) -> Result<(Vec<f64>, Vec<f64>), RocheError> {
95    // Accuracy of location of surface in terms of binary separation
96    const FRAC: f64 = 1.0e-6;
97
98    // Compute the potential at the inner Lagrange point
99    let rl1: f64 = x_l1(q)?;
100    let p: Vec3 = Vec3::new(rl1, 0.0, 0.0);
101    let cpot: f64 = rpot(q, &p)?;
102    let upper: f64 = 1.0 - rl1;
103    let lower: f64 = upper / 4.0;
104    let mut xarr: Vec<f64> = Vec::with_capacity(n);
105    let mut yarr: Vec<f64> = Vec::with_capacity(n);
106    for i in 0..n {
107        if i == 0 || i == n - 1 {
108            // special case as derivative is zero at L1
109            xarr.push(rl1);
110            yarr.push(0.0);
111        } else {
112            let theta: f64 = (i as f64) * std::f64::consts::PI * 2.0 / ((n as f64) - 1.0);
113            let dx: f64 = -theta.cos();
114            let dy: f64 = theta.sin();
115            let line: LineRoche = LineRoche::new(q, Star::Secondary, dx, dy, cpot);
116            let lam: f64 = rtsafe(lower, upper, |lam| line.cost(lam), FRAC)?;
117            xarr.push(1.0 + lam * dx);
118            yarr.push(lam * dy);
119        }
120    }
121    Ok((xarr, yarr))
122}
123
124///
125/// returns arrays vx and vy for plotting an equatorial section
126/// of the Roche lobe of the secondary star in a binary of mass ratio q = M2/M1
127/// in Doppler coordinates. The arrays start and end at the inner Lagrangian 
128/// point and march around uniformly in azimuth looking from the centre of 
129/// mass of the primary star. n is the number of points and must be at least 3. 
130/// 
131#[pyfunction]
132#[pyo3(signature = (q, n=200))]
133pub fn vlobe1(q: f64, n: usize) -> Result<(Vec<f64>, Vec<f64>), RocheError> {
134
135    let mut tvx: f64;
136    let mut tvy: f64;
137
138    let mut vx_arr: Vec<f64> = vec![];
139    let mut vy_arr: Vec<f64> = vec![];
140
141    let (x, y) = lobe1(q, n)?;
142
143    let mu: f64 = q / (1.0 + q);
144    for i in 0..n {
145        tvx = -y[i];
146        tvy = x[i] - mu;
147        vx_arr.push(tvx);
148        vy_arr.push(tvy);
149    }
150    Ok((vx_arr, vy_arr))
151}
152
153///
154/// returns arrays vx and vy for plotting an equatorial section
155/// of the Roche lobe of the secondary star in a binary of mass ratio q = M2/M1
156/// in Doppler coordinates. The arrays start and end at the inner Lagrangian 
157/// point and march around uniformly in azimuth looking from the centre of 
158/// mass of the primary star. n is the number of points and must be at least 3. 
159/// 
160#[pyfunction]
161#[pyo3(signature = (q, n=200))]
162pub fn vlobe2(q: f64, n: usize) -> Result<(Vec<f64>, Vec<f64>), RocheError> {
163
164    let mut tvx: f64;
165    let mut tvy: f64;
166
167    let mut vx_arr: Vec<f64> = vec![];
168    let mut vy_arr: Vec<f64> = vec![];
169
170    let (x, y) = lobe2(q, n)?;
171
172    let mu: f64 = q / (1.0 + q);
173    for i in 0..n {
174        tvx = -y[i];
175        tvy = x[i] - mu;
176        vx_arr.push(tvx);
177        vy_arr.push(tvy);
178    }
179    Ok((vx_arr, vy_arr))
180}
181
182/// rtsafe is a Numerical Recipes-based routine to find roots
183/// of a function using bisection or Newton-Raphson as appropriate.
184/// \param func function object. Returns a tuple of (function value, derivative) at given x.
185/// \param x1 value to the left of the root
186/// \param x2 value to the right of the root
187/// \param xacc minimum accuracy in returned root
188/// \return Returns the x value of the root.
189pub fn rtsafe<F>(x1: f64, x2: f64, func: F, xacc: f64) -> Result<f64, RocheError>
190where
191    F: Fn(f64) -> Result<(f64, f64), RocheError>,
192{
193    let mut xlo = x1;
194    let mut xhi = x2;
195    let mut fl;
196    let mut fh;
197    let mut df;
198    const MAXITER: i32 = 100;
199    (fl, _) = func(xlo)?;
200    (fh, _) = func(xhi)?;
201
202    if (fl > 0.0 && fh > 0.0) || (fl < 0.0 && fh < 0.0) {
203        return Err(RocheError::RtsafeError(
204            "Root must be bracketed in rtsafe".to_string(),
205        ));
206    }
207
208    // return if any of the endpoints is a root
209    if fl == 0.0 {
210        return Ok(xlo);
211    } else if fh == 0.0 {
212        return Ok(xhi);
213    }
214
215    // If fhi < 0.0, set things up so that xlo is below the root and xhi is above
216    if fh < 0.0 {
217        std::mem::swap(&mut xlo, &mut xhi);
218        std::mem::swap(&mut fl, &mut fh);
219    }
220
221    let mut rts = 0.5 * (xlo + xhi);
222    let mut dxold = (xhi - xlo).abs();
223    let mut dx = dxold;
224    let mut f;
225    (f, df) = func(rts)?;
226    let mut iter = 0;
227    while iter < MAXITER {
228        if ((rts - xhi) * df - f) * ((rts - xlo) * df - f) >= 0.0
229            || ((2.0 * f).abs() > (dxold * df).abs())
230        {
231            // Bisect if Newton-Raphson is out of range or not decreasing fast enough
232            dxold = dx;
233            dx = 0.5 * (xhi - xlo);
234            rts = xlo + dx;
235            if xlo == rts {
236                return Ok(rts);
237            }
238        } else {
239            // Newton-Raphson step
240            dxold = dx;
241            dx = f / df;
242            let temp = rts;
243            rts -= dx;
244            if temp == rts {
245                return Ok(rts);
246            }
247        }
248
249        if dx.abs() < xacc {
250            return Ok(rts);
251        }
252
253        (f, df) = func(rts)?;
254        if f < 0.0 {
255            xlo = rts;
256        } else {
257            xhi = rts;
258        }
259        iter += 1;
260    }
261
262    Err(RocheError::RtsafeError(
263        "Maximum number of iterations exceeded in rtsafe".to_string(),
264    ))
265}