Skip to main content

roche/
roche_context.rs

1use crate::errors::RocheError;
2use crate::{Etype, Star, Vec3};
3use crate::{dbrent, pot_min, x_l1, x_l1_1, x_l1_2, x_l2, x_l3};
4use crate::{drpot1, drpot2, rpot_grad, rpot_val, rpot1, rpot2};
5use crate::{set_earth, sphere_eclipse, sphere_eclipse_vector};
6use std::f64::consts::TAU;
7
8///
9/// RocheContext is a basic struct packaging the mass ratio, q=M2/M1,
10/// the star (i.e. the Primary or Secondary), the spin of that star,
11/// and the x-coordinate of the L1 point together with the functions
12/// that accept these values as inputs. x_l1 is calculated on initialisation
13/// with RocheContext::new(q, star, spin) and saves it being recalculated
14/// over and over in loops.
15///
16pub struct RocheContext {
17    pub q: f64,
18    pub star: Star,
19    pub spin: f64,
20    pub x_l1: f64,
21}
22
23impl RocheContext {
24    pub fn new(q: f64, star: Star, spin: f64) -> Result<Self, RocheError> {
25        let x_l1: f64 = match star {
26            Star::Primary => x_l1_1(q, spin)?,
27            Star::Secondary => x_l1_2(q, spin)?,
28        };
29        Ok(Self {
30            q,
31            star,
32            spin,
33            x_l1,
34        })
35    }
36
37    pub fn potential(&self, earth: &Vec3, p: &Vec3, lam: f64) -> Result<f64, RocheError> {
38        rpot_val(self.q, self.star, self.spin, earth, p, lam)
39    }
40
41    pub fn gradient(&self, earth: &Vec3, p: &Vec3, lam: f64) -> Result<(f64, f64), RocheError> {
42        let (dp, dl) = rpot_grad(self.q, self.star, self.spin, earth, p, lam)?;
43        Ok((dp, dl))
44    }
45
46    pub fn potential_grad(
47        &self,
48        earth: &Vec3,
49        p: &Vec3,
50        lam: f64,
51    ) -> Result<(f64, f64, f64), RocheError> {
52        let f = self.potential(earth, p, lam)?;
53        let (dp, dl) = rpot_grad(self.q, self.star, self.spin, earth, p, lam)?;
54        Ok((f, dp, dl))
55    }
56
57    pub fn ref_sphere(&self, ffac: f64) -> Result<(f64, f64), RocheError> {
58        let tref: f64;
59        let rref: f64;
60        let pref: f64;
61        if self.star == Star::Primary {
62            tref = self.x_l1;
63            rref = tref * 1.0_f64.min(1.001 * ffac);
64            pref = rpot1(
65                self.q,
66                self.spin,
67                &Vec3 {
68                    x: ffac * tref,
69                    y: 0.0,
70                    z: 0.0,
71                },
72            )?;
73            Ok((rref, pref))
74        } else if self.star == Star::Secondary {
75            tref = 1.0 - self.x_l1;
76            rref = tref * 1.0_f64.min(1.001 * ffac);
77            pref = rpot2(
78                self.q,
79                self.spin,
80                &Vec3 {
81                    x: 1.0 - ffac * tref,
82                    y: 0.0,
83                    z: 0.0,
84                },
85            )?;
86            Ok((rref, pref))
87        } else {
88            let message = format!("{:?} is not and instance of Star.", self.star);
89            Err(RocheError::ParameterError(message))
90        }
91    }
92
93    pub fn fblink(&self, ffac: f64, acc: f64, earth: &Vec3, p: &Vec3) -> Result<bool, RocheError> {
94        let (rref, pref) = self.ref_sphere(ffac)?;
95
96        let cofm: Vec3 = match self.star {
97            Star::Primary => Vec3::cofm1(),
98            Star::Secondary => Vec3::cofm2(),
99        };
100
101        // First compute the multipliers cutting the reference sphere (if any)
102        let mut lam1 = 0.0;
103        let mut lam2 = 0.0;
104        if !sphere_eclipse_vector(earth, p, &cofm, rref, &mut lam1, &mut lam2) {
105            return Ok(false);
106        }
107        if lam1 == 0.0 {
108            return Ok(true);
109        }
110
111        // Create function objects for 1D minimisation in lambda direction
112        let func = |lam: f64| self.potential(earth, p, lam);
113
114        // Now try to bracket a minimum. We just crudely compute function at regularly spaced intervals filling in the
115        // gaps until the step size between the points drops below the threshold. Take every opportunity to jump out early
116        // either if the potential is below the threshold or if we have bracketed a minimum.
117        let mut nstep: i32 = 1;
118        let mut step: f64 = lam2 - lam1;
119
120        let mut f1: f64 = 0.0;
121        let mut f2: f64 = 0.0;
122        let mut flam: f64 = 1.0;
123        let mut lam: f64 = lam1;
124
125        while step > acc {
126            lam = lam1 + step / 2.0;
127
128            for _ in 0..nstep {
129                flam = func(lam)?;
130                if flam <= pref {
131                    return Ok(true);
132                }
133
134                // Calculate these as late as possible because they may often not be needed
135                if nstep == 1 {
136                    f1 = func(lam1)?;
137                    f2 = func(lam2)?;
138                }
139
140                if flam < f1 && flam < f2 {
141                    break;
142                }
143
144                lam += step;
145            }
146            if flam < f1 && flam < f2 {
147                break;
148            }
149            step /= 2.0;
150            nstep *= 2;
151        }
152
153        if flam < f1 && flam < f2 {
154            // OK, minimum bracketted, so finally pin it down accurately
155            // Possible that multiple minima could cause problems but I have
156            // never seen this in practice.
157            let dfunc = |lam: f64| {
158                let (_dp, dl) = self.gradient(earth, p, lam)?;
159                Ok(dl)
160            };
161
162            let (_xmin, flam) =
163                dbrent(lam1, lam, lam2, func, dfunc, acc, true, pref)?;
164
165            Ok(flam < pref)
166        } else {
167            // Not bracketted even after a detailed search, and we have not jumped
168            // out either, so assume no eclipse
169            Ok(false)
170        }
171    }
172
173    pub fn face(
174        &self,
175        direction: Vec3,
176        rref: f64,
177        pref: f64,
178        acc: f64,
179    ) -> Result<(Vec3, Vec3, f64, f64), RocheError> {
180        let mut pvec: Vec3;
181        let mut r: f64;
182
183        let cofm: Vec3 = match self.star {
184            Star::Primary => Vec3::cofm1(),
185            Star::Secondary => Vec3::cofm2(),
186        };
187
188        let rp: fn(f64, f64, &Vec3) -> Result<f64, RocheError> = match self.star {
189            Star::Primary => rpot1,
190            Star::Secondary => rpot2,
191        };
192
193        let drp: fn(f64, f64, &Vec3) -> Result<Vec3, RocheError> = match self.star {
194            Star::Primary => drpot1,
195            Star::Secondary => drpot2,
196        };
197
198        let mut tref: f64 = rp(self.q, self.spin, &(cofm + rref * direction))?;
199        if tref < pref {
200            let message = format!(
201                "point at reference radius {} appears to be at lower potential {} than the reference potential {}",
202                rref, tref, pref
203            );
204            return Err(RocheError::FaceError(message));
205        }
206
207        let mut r1: f64 = rref / 2.;
208        let mut r2: f64 = rref;
209        tref = pref + 1.;
210
211        const MAXSEARCH: i32 = 30;
212        let mut i: i32 = 0;
213        while i < MAXSEARCH && tref > pref {
214            r1 = r2 / 2.;
215            tref = rp(self.q, self.spin, &(cofm + r1 * direction))?;
216            if tref > pref {
217                r2 = r1;
218            }
219            i += 1;
220        }
221        if tref > pref {
222            let message = "could not find a radius with a potential below the reference potential; probably bad inputs.";
223            return Err(RocheError::FaceError(message.to_string()));
224        }
225
226        const MAXCHOP: i32 = 100;
227        let mut nchop: i32 = 0;
228        while r2 - r1 > acc && nchop < MAXCHOP {
229            r = (r1 + r2) / 2.;
230            pvec = cofm + r * direction;
231            if rp(self.q, self.spin, &pvec)? < pref {
232                r1 = r;
233            } else {
234                r2 = r;
235            }
236            nchop += 1;
237        }
238        if nchop == MAXCHOP {
239            return Err(RocheError::FaceError(
240                "reached maximum number of binary chops".to_string(),
241            ));
242        }
243        r = (r1 + r2) / 2.;
244        pvec = cofm + r * direction;
245        let mut dvec: Vec3 = drp(self.q, self.spin, &pvec)?;
246        let g = dvec.length();
247        dvec /= g;
248        Ok((pvec, dvec, r, g))
249    }
250
251    pub fn ingress_egress(
252        &self,
253        ffac: f64,
254        iangle: f64,
255        delta: f64,
256        r: &Vec3,
257        ingress: &mut f64,
258        egress: &mut f64,
259    ) -> Result<bool, RocheError> {
260        let rref: f64;
261        let pref: f64;
262        (rref, pref) = self.ref_sphere(ffac)?;
263        let ri: f64 = iangle.to_radians();
264        let (sini, cosi) = ri.sin_cos();
265
266        let cofm: Vec3 = match self.star {
267            Star::Primary => Vec3::cofm1(),
268            Star::Secondary => Vec3::cofm2(),
269        };
270
271        let mut phi1: f64 = 0.0;
272        let mut phi2: f64 = 0.0;
273        let mut lam1: f64 = 0.0;
274        let mut lam2: f64 = 0.0;
275        let mut phi: f64 = 0.0;
276        let mut lam: f64 = 0.0;
277
278        if sphere_eclipse(
279            cosi, sini, r, &cofm, rref, &mut phi1, &mut phi2, &mut lam1, &mut lam2,
280        ) {
281            let acc: f64 = 2. * (2. * TAU * (lam2 - lam1) * delta).sqrt();
282
283            if self.pot_min(
284                cosi, sini, r, phi1, phi2, lam1, lam2, rref, pref, acc, &mut phi, &mut lam,
285            )? {
286                let mut pin: f64 = phi;
287                let mut pout: f64 = phi1;
288                let mut pmid: f64;
289
290                while (pin - pout).abs() > delta {
291                    pmid = (pin + pout) / 2.0;
292                    if self
293                        .fblink(ffac, acc, &set_earth(cosi, sini, pmid), r)
294                        .unwrap()
295                    {
296                        pin = pmid;
297                    } else {
298                        pout = pmid;
299                    }
300                }
301                *ingress = (pin + pout) / 2.0;
302                *ingress -= ingress.floor();
303
304                pin = phi;
305                pout = phi2;
306                while (pin - pout).abs() > delta {
307                    pmid = (pin + pout) / 2.;
308                    if self
309                        .fblink(ffac, acc, &set_earth(cosi, sini, pmid), r)
310                        .unwrap()
311                    {
312                        pin = pmid;
313                    } else {
314                        pout = pmid;
315                    }
316                }
317                *egress = (pin + pout) / 2.0;
318                *egress -= egress.floor();
319                if *egress < *ingress {
320                    *egress += 1.0;
321                }
322                Ok(true)
323            } else {
324                Ok(false)
325            }
326        } else {
327            Ok(false)
328        }
329    }
330
331    pub fn star_eclipse(
332        &self,
333        r: f64,
334        ffac: f64,
335        iangle: f64,
336        posn: &Vec3,
337        delta: f64,
338        roche: bool,
339        star: Star,
340        eclipses: &mut Etype,
341    ) -> Result<(), RocheError> {
342        let ri = iangle.to_radians();
343        let (sini, cosi) = ri.sin_cos();
344        let cofm = match star {
345            Star::Primary => Vec3::cofm1(),
346            Star::Secondary => Vec3::cofm2(),
347        };
348        let mut lam1: f64 = 0.0;
349        let mut lam2: f64 = 0.0;
350        let mut ingress: f64 = 0.0;
351        let mut egress: f64 = 0.0;
352        // let mut eclipses = Etype::new();
353        if (roche && self.ingress_egress(ffac, iangle, delta, posn, &mut ingress, &mut egress)?)
354            || (!roche
355                && sphere_eclipse(
356                    cosi,
357                    sini,
358                    posn,
359                    &cofm,
360                    r,
361                    &mut ingress,
362                    &mut egress,
363                    &mut lam1,
364                    &mut lam2,
365                ))
366        {
367            eclipses.push((ingress, egress));
368        }
369        Ok(())
370    }
371
372    pub fn pot_min(
373        &self,
374        cosi: f64,
375        sini: f64,
376        p: &Vec3,
377        phi1: f64,
378        phi2: f64,
379        lam1: f64,
380        lam2: f64,
381        rref: f64,
382        pref: f64,
383        acc: f64,
384        phi: &mut f64,
385        lam: &mut f64,
386    ) -> Result<bool, RocheError> {
387        pot_min(
388            self.q, self.star, self.spin, cosi, sini, p, phi1, phi2, lam1, lam2, rref, pref, acc,
389            phi, lam,
390        )
391    }
392
393    pub fn x_l1(&self) -> Result<f64, RocheError> {
394        x_l1(self.q)
395    }
396
397    pub fn x_l1_asyncronous(&self) -> Result<f64, RocheError> {
398        match self.star {
399            Star::Primary => Ok(x_l1_1(self.q, self.spin)?),
400            Star::Secondary => Ok(x_l1_2(self.q, self.spin)?),
401        }
402    }
403
404    pub fn x_l2(&self) -> Result<f64, RocheError> {
405        x_l2(self.q)
406    }
407
408    pub fn x_l3(&self) -> Result<f64, RocheError> {
409        x_l3(self.q)
410    }
411}