Skip to main content

roche/
phases.rs

1use crate::errors::RocheError;
2use crate::{Star, Vec3, brightspot_position, ingress_egress};
3use crate::{fblink, set_earth_iangle, x_l1};
4use pyo3::prelude::*;
5use std::f64::consts::{FRAC_PI_2, TAU};
6
7
8///
9/// Computes scaled radius of white dwarf, r1 = R1/a given
10/// the mass ratio, orbital inclination and phase width of the
11/// white dwarf's egress (or ingress).
12/// 
13/// Arguments:
14/// 
15/// * `q`: mass ratio M2/M1
16/// * `iangle`: orbital inclination (degrees)
17/// * `dpwd`: phase-width of WD ingress/egress feature
18/// * `ntheta`: number of points on limb of white dwarf when using wdphases during this routine.
19/// * `dr`: tolerance on scaled radius
20/// * `rmax`: maximum scaled radius to assume
21/// 
22/// Returns:
23/// 
24/// * scaled radius of WD
25/// 
26#[pyfunction]
27#[pyo3(signature = (q, iangle, dpwd, ntheta=100, dr=1.0e-5, rmax=0.1))]
28pub fn wdradius(q: f64, iangle: f64, dpwd: f64, ntheta: i32, dr: f64, rmax: f64) -> Result<f64, RocheError> {
29    let mut r1: f64;
30    let mut r1_lo: f64 = 0.0;
31    let mut r1_hi: f64 = rmax;
32    let mut phi3: f64;
33    let mut phi4: f64;
34    let mut dp: f64;
35    while r1_hi - r1_lo > dr {
36        r1 = (r1_lo + r1_hi) / 2.0;
37        (phi3, phi4) = wdphases(q, iangle, r1, -1.0, ntheta)?;
38        dp = phi4 - phi3;
39        if dp > dpwd {r1_hi = r1} else {r1_lo = r1}
40    }
41    Ok((r1_lo + r1_hi) / 2.0)
42}
43
44
45///
46/// phi3, phi4 = wdphases(q, iangle, r1, r2=-1, ntheta=200)
47///
48/// Returns the third and fourth contact phases of the white dwarf.
49///
50/// q      -- mass ratio = M2/M1
51/// iangle -- orbital inclination, degrees
52/// r1     -- scaled white dwarf radius = R1/a
53/// r2     -- scaled secondary radius, < 0 for Roche lobe filling
54/// ntheta -- number of angles to compute at the limb of white dwarf.
55///           (used over quadrants)
56///
57/// The routine searches points equally-spaced at quadrants of the limb
58/// of the white dwarf to determine the contact phases. It will fail if
59/// there is no eclipse at all by raising a RocheError. For partial eclipses
60/// there will be a valid 'fourth' contact (marking the end of eclipse still)
61/// but the third contact will be set = -1.
62///
63#[pyfunction]
64#[pyo3(signature = (q, iangle, r1, r2=-1.0, ntheta=200))]
65pub fn wdphases(
66    q: f64,
67    iangle: f64,
68    r1: f64,
69    r2: f64,
70    ntheta: i32,
71) -> Result<(f64, f64), RocheError> {
72    let ffac: f64 = if r2 <= 0.0 {
73        1.0
74    } else {
75        r2 / (1.0 - x_l1(q)?)
76    };
77    // fourth contact
78    let mut phi4lo: f64 = 0.0;
79    let mut phi4hi: f64 = 0.25;
80    let mut phi4: f64 = 0.0;
81
82    if !eclipsed_4(q, iangle, phi4lo, r1, ffac, ntheta)? {
83        let message = format!("no eclipse at all for q={}, i={}, r1={}", q, iangle, r1);
84        return Err(RocheError::WdphasesError(message));
85    }
86
87    while phi4hi - phi4lo > r1 / (ntheta as f64) / 10.0 {
88        phi4 = (phi4lo + phi4hi) / 2.0;
89        if eclipsed_4(q, iangle, phi4, r1, ffac, ntheta)? {
90            phi4lo = phi4;
91        } else {
92            phi4hi = phi4;
93        }
94    }
95
96    // third contact
97    let mut phi3lo: f64 = 0.0;
98    let mut phi3hi: f64 = 0.25;
99    let mut phi3: f64 = 0.0;
100
101    if uneclipsed_3(q, iangle, phi3lo, r1, ffac, ntheta)? {
102        return Ok((-1.0, phi4));
103    }
104
105    while phi3hi - phi3lo > r1 / (ntheta as f64) / 10.0 {
106        phi3 = (phi3lo + phi3hi) / 2.0;
107        if uneclipsed_3(q, iangle, phi3, r1, ffac, ntheta)? {
108            phi3hi = phi3;
109        } else {
110            phi3lo = phi3;
111        }
112    }
113
114    Ok((phi3, phi4))
115}
116
117///
118/// Returns x, y vectors which define the projected limb of white dwarf
119///when viewed at orbital inclination = iangle and orbital phase = phase
120///
121fn xyv(iangle: f64, r1: f64, phase: f64) -> (Vec3, Vec3) {
122    let (sinp, cosp) = (TAU * phase).sin_cos();
123    let x = Vec3::new(-r1 * sinp, r1 * cosp, 0.0);
124    let iangle_radians = iangle.to_radians();
125    let (sini, cosi) = iangle_radians.sin_cos();
126    let y = Vec3::new(-r1 * cosi * cosp, -r1 * cosi * sinp, r1 * sini);
127    (x, y)
128}
129
130///
131/// Says whether any of the upper-left quadrant of the WD is uneclipsed at
132/// phase = phase 'any' means all of the ntheta points computed uniformly
133/// around the quadrant. This can be used to define the 3rd contact
134///
135fn uneclipsed_3(
136    q: f64,
137    iangle: f64,
138    phase: f64,
139    r1: f64,
140    ffac: f64,
141    ntheta: i32,
142) -> Result<bool, RocheError> {
143    let (x, y) = xyv(iangle, r1, phase);
144    let mut theta: f64;
145    let mut v: Vec3;
146    let mut sint: f64;
147    let mut cost: f64;
148    for i in 0..ntheta {
149        theta = FRAC_PI_2 * (i as f64 / (ntheta as f64 - 1.0));
150        (sint, cost) = theta.sin_cos();
151        v = -x * cost + y * sint;
152        if !fblink(
153            q,
154            Star::Secondary,
155            1.0,
156            ffac,
157            1.0e-5,
158            &set_earth_iangle(iangle, phase),
159            &v,
160        )? {
161            return Ok(true);
162        }
163    }
164
165    Ok(false)
166}
167
168///
169/// Says whether any of lower-right quadrant of the WD is eclipsed at
170/// phase = phase 'Any' means any of ntheta points computed uniformly around the quadrant.
171/// This can be used to define the 4th contact
172///
173fn eclipsed_4(
174    q: f64,
175    iangle: f64,
176    phase: f64,
177    r1: f64,
178    ffac: f64,
179    ntheta: i32,
180) -> Result<bool, RocheError> {
181    let (x, y) = xyv(iangle, r1, phase);
182    let mut theta: f64;
183    let mut v: Vec3;
184    let mut sint: f64;
185    let mut cost: f64;
186    for i in 0..ntheta {
187        theta = FRAC_PI_2 * (i as f64 / (ntheta as f64 - 1.0));
188        (sint, cost) = theta.sin_cos();
189        v = x * cost - y * sint;
190        if fblink(
191            q,
192            Star::Secondary,
193            1.0,
194            ffac,
195            1.0e-5,
196            &set_earth_iangle(iangle, phase),
197            &v,
198        )? {
199            return Ok(true);
200        }
201    }
202
203    Ok(false)
204}
205
206#[pyfunction]
207// #[pyo3(signature = (q, iangle, rbs))]
208pub fn bsphases(q: f64, iangle: f64, rbs: f64) -> Result<(f64, f64), RocheError> {
209    let r = brightspot_position(q, rbs, 1.0e-7, 1.0e-2)?;
210    let mut ingress: f64 = 0.0;
211    let mut egress: f64 = 0.0;
212    let eclipse = ingress_egress(
213        q,
214        Star::Secondary,
215        1.0,
216        1.0,
217        iangle,
218        1.0e-7,
219        &r,
220        &mut ingress,
221        &mut egress,
222    )?;
223    if !eclipse {
224        return Err(RocheError::WdphasesError(
225            "point is not eclipsed".to_string(),
226        ));
227    }
228    Ok((ingress - 1.0, egress - 1.0))
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn wdradius_test() -> Result<(), RocheError> {
237        assert_eq!(wdradius(0.2, 87.0, 0.008, 100, 1.0e-5, 0.1)?, 0.026266479492187505);
238        Ok(())
239    }
240
241    #[test]
242    fn wdphases_test() -> Result<(), RocheError> {
243        // Values from trm.roche.wdphases
244        assert_eq!(
245            wdphases(0.2, 90.0, 0.015, 0.20, 200)?,
246            (0.027637481689453125, 0.032169342041015625)
247        );
248        assert_eq!(
249            wdphases(0.2, 85.0, 0.015, 0.20, 200)?,
250            (0.023677825927734375, 0.029010772705078125)
251        );
252        assert_eq!(
253            wdphases(0.2, 80.0, 0.015, 0.20, 200)?,
254            (-1.0, 0.015842437744140625)
255        );
256        assert!(wdphases(0.2, 60.0, 0.015, 0.20, 200).is_err());
257        Ok(())
258    }
259
260    #[test]
261    fn bsphases_test() -> Result<(), RocheError> {
262        // Values from trm.roche.bsphases
263        let (phi_in, phi_eg) = bsphases(0.2, 90.0, 0.2)?;
264        assert!((phi_in - -0.016291638617189408).abs() < 1.0e-7);
265        assert!((phi_eg - 0.07258765600962591).abs() < 1.0e-7);
266
267        let (phi_in, phi_eg) = bsphases(0.2, 85.0, 0.2)?;
268        assert!((phi_in - -0.013903522665196566).abs() < 1.0e-7);
269        assert!((phi_eg - 0.07018328081120417).abs() < 1.0e-7);
270
271        assert!(bsphases(0.2, 60.0, 0.2).is_err());
272        Ok(())
273    }
274}