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