roche/disc_eclipse.rs
1use crate::errors::RocheError;
2use crate::{Etype, Vec3};
3use std::f64::consts::TAU;
4
5#[derive(Debug, PartialEq, Eq, Clone, Copy)]
6// This enumerates the 5 possible outcomes of the LOSC intersection with a circle.
7pub enum Circle {
8 // Line of sight cone starts at or above the circle of interest
9 Above,
10 // Line of sight circle is everywhere inside circle of interest
11 Inside,
12 // Line of sight circle is everywhere outside circle of interest
13 Outside,
14 // Line of sight circle is separated from the circle of interest
15 Separate,
16 // Line of sight circle cone intersects the circle of interest
17 Crossing,
18}
19
20///
21/// disc_eclipse works out phase ranges during which a cylindrically symmetric, flared disc
22/// running between a pair of radii eclipses a given point.
23///
24/// Arguments:
25///
26/// * `iangle`: the orbital inclination, degrees. 90 = edge on.
27/// * `r`: the position vector of the point in question (units of binary separation)
28/// * `rdisc1`: inner disc radius, units of separation
29/// * `rdisc2`: outer disc radius, units of separation
30/// * `beta`: exponent of flaring, so that the height scales as r**beta. beta should be >= 1
31/// * `height disc`: height at unit radius in disc (even if it does not exist)
32///
33/// Returns:
34///
35/// * a vector of ingress and egress phase pairs during which the point in question is eclipsed.
36/// The ingress phase will always be between 0 and 1 while the egress phase will be larger than this, but
37/// by no more than 1 cycle. If the vector is null, no eclipse takes place.
38///
39pub fn disc_eclipse(
40 iangle: f64,
41 rdisc1: f64,
42 rdisc2: f64,
43 beta: f64,
44 height: f64,
45 r: &Vec3,
46) -> Result<Etype, RocheError> {
47 if beta <= 1.0 {
48 return Err(RocheError::ParameterError(
49 "beta must be >= 1.0".to_string(),
50 ));
51 }
52
53 // Compute and store cosine and sine of inclination if need be.
54 // let mut iangle_old: f64 = -1.0e30;
55 let sini: f64;
56 let cosi: f64;
57 (sini, cosi) = iangle.to_radians().sin_cos();
58 // }
59
60 let mut temp = Etype::new();
61
62 // Compute height of disc at outer boundary
63 let h_out: f64 = height * rdisc2.powf(beta);
64
65 // Deal with points too high ever to be eclipsed whatever the inclination
66 if r.z >= h_out {
67 return Ok(temp);
68 }
69
70 // Special case of exactly edge-on, only curved outer edge matters.
71 if cosi == 0.0 {
72 if r.z.abs() < h_out {
73 let rxy: f64 = (r.x * r.x + r.y * r.y).sqrt();
74 if rxy <= rdisc2 {
75 temp.push((0.0, 1.0));
76 }
77 }
78 return Ok(temp);
79 }
80
81 // Work out distance from axis
82 let rxy: f64 = (r.x * r.x + r.y * r.y).sqrt();
83
84 if rdisc1 < rxy && rxy < rdisc2 && r.z.abs() < height * rxy.powf(beta) {
85 // Point is inside disc and so is eclipsed
86 temp.push((0.0, 1.1));
87 return Ok(temp);
88 }
89
90 let tani: f64 = sini / cosi;
91 let mut result: Circle;
92
93 let mut phase: f64 = 0.0;
94 let mut ingress: f64;
95 let mut egress: f64;
96
97 if rxy < rdisc2 && r.z >= height * rdisc1.max(rxy).powf(beta) {
98 // Point is in approximately conical region above the disc. Just need to check whether
99 // it is not occulted by the edge of the disc
100 result = circle_eclipse(rxy, r.z, h_out, rdisc2, tani, &mut phase);
101
102 if result == Circle::Outside {
103 // point will be occulted by the disc edge at all phases
104 temp.push((0.0, 1.1));
105 } else if result == Circle::Crossing {
106 // point partially occulted by disc edge; work out phases
107 let phi0: f64 = r.y.atan2(r.x) / TAU;
108 ingress = phi0 + phase;
109 ingress -= ingress.floor();
110 egress = ingress + 1.0 - 2.0 * phase;
111 temp.push((ingress, egress));
112 }
113 return Ok(temp);
114 }
115
116 // Compute the radius of circle formed by LOSC in the plane of
117 // the lower outer rim of the disc
118 let rcone_lo: f64 = 0.0_f64.max(tani * (-h_out - r.z));
119
120 // Circle encloses rim, so no intersection
121 if rcone_lo >= rxy + rdisc2 {
122 return Ok(temp);
123 }
124
125 // Compute the radius of circle formed by LOSC in the plane of
126 // the upper outer rim of the disc
127 let rcone_hi: f64 = tani * (h_out - r.z);
128
129 // Circle disjoint from rim, so no intersection
130 if rxy >= rcone_hi + rdisc2 {
131 return Ok(temp);
132 }
133
134 // For the moment we pretend that the disc has no hole at its centre, so
135 // that we are simply interested in the phases over which eclipse occurs.
136 // At this point we are guaranteed that this will happen. All events are
137 // symmetrically located around a phase defined by x and y only which will
138 // be calculated at the end. We therefore just find the half range which
139 // is called 'eclipse_phase' below.
140
141 let eclipse_phase: f64;
142 if rxy + rcone_lo <= rdisc2 {
143 // Cone swept out by line of sight always inside lower face so total eclipse
144 eclipse_phase = 0.5;
145 } else if rxy <= rdisc2 {
146 // Points that project close to the z axis which are only
147 // partially obscured by the disc hovering above them.
148 // this means they must be below -HOUT
149 eclipse_phase = cut_phase(rxy, rcone_lo, rdisc2);
150 } else {
151 // Points further from the z axis than the outer rim of the disc that
152 // will be eclipsed.
153 if rcone_hi * rcone_hi + rdisc2 * rdisc2 >= rxy * rxy
154 && rcone_lo * rcone_lo + rdisc2 * rdisc2 <= rxy * rxy
155 {
156 // In this case it is the curved outer disc rim that sets the limit
157 eclipse_phase = (rdisc2 / rxy).asin() / TAU;
158 } else if rcone_hi * rcone_hi + rdisc2 * rdisc2 < rxy * rxy {
159 // In this case it is upper outer rim that sets the limit
160 eclipse_phase = cut_phase(rxy, rcone_hi, rdisc2);
161 } else {
162 // In this case it is lower outer rim that sets the limit
163 eclipse_phase = cut_phase(rxy, rcone_lo, rdisc2);
164 }
165 }
166
167 // At this point we have covered all cases for the eclipse, whilst ignoring the
168 // possibility of seeing the point through the hole in the middle of the disc.
169 // Now let's calculate the 'appear_phase' if any.
170
171 // First compute height of disc at inner boundary
172 let h_in: f64 = height * rdisc1.powf(beta);
173
174 let mut appear_phase: f64 = -1.0;
175
176 if r.z < -h_out {
177 // In this case the LOSC has to run through 4 circles which are the upper and
178 // lower outer and inner rims.
179
180 // First, the lower outer rim
181 result = circle_eclipse(rxy, r.z, -h_out, rdisc2, tani, &mut phase);
182 if result == Circle::Inside {
183 appear_phase = 0.5;
184 } else if result == Circle::Crossing {
185 appear_phase = appear_phase.min(phase);
186 }
187
188 // Second, the lower inner rim
189 if appear_phase > 0.0 {
190 result = circle_eclipse(rxy, r.z, -h_in, rdisc1, tani, &mut phase);
191 if result == Circle::Crossing {
192 appear_phase = appear_phase.min(phase);
193 } else if result != Circle::Inside {
194 appear_phase = -1.0;
195 }
196 }
197
198 // Fourth, the upper outer rim
199 if appear_phase > 0.0 {
200 result = circle_eclipse(rxy, r.z, h_out, rdisc2, tani, &mut phase);
201 if result == Circle::Crossing {
202 appear_phase = appear_phase.min(phase);
203 } else if result != Circle::Inside {
204 appear_phase = -1.0;
205 }
206 }
207 } else if rxy < rdisc1 {
208 if r.z < -h_in {
209 // Points hovering around underside of disc. Have to consider just three circles
210
211 // First, the lower inner rim
212 result = circle_eclipse(rxy, r.z, -h_in, rdisc1, tani, &mut phase);
213 if result == Circle::Inside {
214 appear_phase = 0.5;
215 } else if result == Circle::Crossing {
216 appear_phase = phase;
217 }
218
219 // Second, the upper inner rim
220 if appear_phase > 0.0 {
221 result = circle_eclipse(rxy, r.z, h_in, rdisc1, tani, &mut phase);
222 if result == Circle::Crossing {
223 appear_phase = appear_phase.min(phase);
224 } else if result != Circle::Inside {
225 appear_phase = -1.0;
226 }
227 }
228
229 // Third, the upper outer rim
230 if appear_phase > 0.0 {
231 result = circle_eclipse(rxy, r.z, h_out, rdisc2, tani, &mut phase);
232 if result == Circle::Crossing {
233 appear_phase = appear_phase.min(phase);
234 } else if result != Circle::Inside {
235 appear_phase = -1.0;
236 }
237 }
238 } else if r.z < h_in {
239 // Points inside hole in middle of disc. Have to consider just two circles
240
241 // First, the upper inner rim
242 result = circle_eclipse(rxy, r.z, h_in, rdisc1, tani, &mut phase);
243 if result == Circle::Inside {
244 appear_phase = 0.0;
245 } else if result == Circle::Crossing {
246 appear_phase = phase;
247 }
248
249 // Second, the upper outer rim
250 if appear_phase > 0.0 {
251 result = circle_eclipse(rxy, r.z, h_out, rdisc2, tani, &mut phase);
252 if result == Circle::Crossing {
253 appear_phase = appear_phase.min(phase);
254 } else if result != Circle::Inside {
255 appear_phase = -1.0;
256 }
257 }
258 }
259 }
260
261 // Here is the central phase
262 let phi0: f64 = r.y.atan2(-r.x / TAU);
263
264 if appear_phase <= 0.0 {
265 ingress = phi0 - eclipse_phase;
266 ingress -= ingress.floor();
267 egress = ingress + 2.0 * eclipse_phase;
268 temp.push((ingress, egress));
269 } else if appear_phase < eclipse_phase {
270 ingress = phi0 - eclipse_phase;
271 ingress -= ingress.floor();
272 egress = ingress + (eclipse_phase - appear_phase);
273 temp.push((ingress, egress));
274 ingress = phi0 + appear_phase;
275 ingress -= ingress.floor();
276 egress = ingress + (eclipse_phase - appear_phase);
277 temp.push((ingress, egress));
278 }
279
280 Ok(temp)
281}
282
283pub fn circle_eclipse(
284 rxy: f64,
285 z: f64,
286 zcirc: f64,
287 radius: f64,
288 tani: f64,
289 phase: &mut f64,
290) -> Circle {
291 // point above circle
292 if z >= zcirc {
293 return Circle::Above;
294 }
295 let rcone: f64 = tani * (zcirc - z);
296
297 // line-of-sight always outside the circle
298 if rcone >= rxy + radius {
299 return Circle::Outside;
300 }
301
302 // line-of-sight circle separate from the circle
303 if rxy >= rcone + radius {
304 return Circle::Separate;
305 }
306
307 // line-of-sight always outside the circle
308 if rxy + rcone <= radius {
309 return Circle::Inside;
310 }
311
312 // crossing case
313 *phase = cut_phase(rxy, rcone, radius);
314
315 Circle::Crossing
316}
317
318pub fn cut_phase(rxy: f64, rcone: f64, radius: f64) -> f64 {
319 // Temporary checks
320 if rxy + rcone <= radius {
321 panic!("rxy + rcone <= radius");
322 }
323 if rxy >= radius + rcone {
324 panic!("rxy >= radius + rcone");
325 }
326 if rcone >= radius + rxy {
327 panic!("rcone >= radius + rxy");
328 }
329
330 ((rxy * rxy + rcone * rcone - radius * radius) / (2.0 * rcone * rxy)).acos() / TAU
331}