4
4
from __future__ import division
5
5
6
6
import numpy as np
7
- from numpy import cos , sin , zeros_like
7
+ from numpy import cos , sin , sqrt , zeros_like
8
8
from .constants import pi , tau
9
9
from .framelib import ecliptic_frame
10
10
from .searchlib import find_discrete
11
11
from .nutationlib import iau2000b_radians
12
12
from .units import Angle
13
13
14
+ _SUN = 10
15
+ _MOON = 301
16
+ _MICROSECOND = 1 / 24.0 / 3600.0 / 1e6
17
+
14
18
# Not only to support historic code but also for future convenience, let
15
19
# folks import the search routine alongside the almanac routines.
16
20
find_discrete
@@ -330,29 +334,50 @@ def _rising_hour_angle(latitude, declination, altitude_radians):
330
334
def _transit_ha (latitude , declination , altitude_radians ):
331
335
return 0.0
332
336
337
+ def _q (a , b , c , sign ):
338
+ discriminant = np .maximum (b * b - 4 * a * c , 0.0 ) # avoid tiny negative results
339
+ return - 2 * c / (b + sign * sqrt (discriminant ))
340
+
341
+ def _intersection (y0 , y1 , v0 , v1 ):
342
+ # Return x at which a curve reaches y=0, given its position and
343
+ # velocity y0,v0 at x=0 and y1,v1 at x=1. For details, see
344
+ # `design/intersect_function.py` in the Skyfield repository.
345
+ sign = 1 - 2 * (y0 > y1 )
346
+ return _q (y1 - y0 - v0 , v0 , y0 , sign )
347
+
333
348
# Per https://aa.usno.navy.mil/faq/RST_defs we estimate 34 arcminutes of
334
349
# atmospheric refraction and 16 arcminutes for the radius of the Sun.
335
350
_sun_horizon_radians = - 50.0 / 21600.0 * tau
336
351
_refraction_radians = - 34.0 / 21600.0 * tau
337
352
_moon_radius_m = 1.7374e6
353
+ _clip_lower = - 1.0
354
+ _clip_upper = + 2.0
355
+
356
+ def build_horizon_function (target ):
357
+ """Build and return a horizon function `h()` for the given `target`.
358
+
359
+ The returned function takes a Distance argument giving the distance
360
+ from the observer to the target, and returns a negative angle in
361
+ radians giving the altitude which, when reached by the target's
362
+ center, places it at the moment of rising.
363
+
364
+ """
365
+ target_id = getattr (target , 'target' , None )
366
+ if target_id == _SUN :
367
+ def h (distance ): return _sun_horizon_radians
368
+ elif target_id == _MOON :
369
+ def h (distance ):
370
+ return _refraction_radians - _moon_radius_m / distance .m
371
+ else :
372
+ def h (distance ): return _refraction_radians
373
+ return h
338
374
339
375
def _find (observer , target , start_time , end_time , horizon_degrees , f ):
340
- # Build a function h() that returns the angle above or below the
341
- # horizon we are aiming for, in radians.
342
376
if horizon_degrees is None :
343
- tt = getattr (target , 'target' , None )
344
- if tt == 10 :
345
- horizon_radians = _sun_horizon_radians
346
- h = lambda distance : horizon_radians
347
- elif tt == 301 :
348
- horizon_radians = _refraction_radians
349
- h = lambda distance : horizon_radians - _moon_radius_m / distance .m
350
- else :
351
- horizon_radians = _refraction_radians
352
- h = lambda distance : horizon_radians
377
+ h = build_horizon_function (target )
353
378
else :
354
379
horizon_radians = horizon_degrees / 360.0 * tau
355
- h = lambda distance : horizon_radians
380
+ def h ( distance ): return horizon_radians
356
381
357
382
geo = observer .vector_functions [- 1 ] # should we check observer.center?
358
383
latitude = geo .latitude
@@ -384,6 +409,10 @@ def _find(observer, target, start_time, end_time, horizon_degrees, f):
384
409
# throw the first few out and keep only the last one.
385
410
i , = np .nonzero (np .diff (difference ) > 0.0 )
386
411
412
+ # Trim a few arrays down to just the matching elements.
413
+ old_ha_radians = ha .radians [i ]
414
+ old_t = t [i ]
415
+
387
416
# When might each rising have actually taken place? Let's
388
417
# interpolate between the two times that bracket each rising.
389
418
a = difference [i ]
@@ -392,24 +421,92 @@ def _find(observer, target, start_time, end_time, horizon_degrees, f):
392
421
interpolated_tt = (b * tt [i ] + a * tt [i + 1 ]) / (a + b )
393
422
t = ts .tt_jd (interpolated_tt )
394
423
395
- ha_per_day = tau # angle the celestrial sphere rotates in 1 day
424
+ def normalize_zero_to_tau (radians ):
425
+ return radians % tau
396
426
397
- # TODO: How many iterations do we need? And can we cut down on that
398
- # number if we use velocity intelligently? For now, we experiment
399
- # using the ./design/test_sunrise_moonrise.py script in the
400
- # repository, that checks both the old Skyfiled routines and this
401
- # new one against the USNO. It suggests that 3 iterations is enough
402
- # for the Moon, the fastest-moving Solar System object, to match.
427
+ def normalize_plus_or_minus_pi (radians ):
428
+ return (radians + pi ) % tau - pi
429
+
430
+ normalize = normalize_zero_to_tau
431
+
432
+ # How did we decide on this many iterations? We played with the
433
+ # script ./design/test_sunrise_moonrise.py in the repository.
403
434
for i in 0 , 1 , 2 :
404
435
_fastify (t )
405
- ha , dec , distance = observer .at (t ).observe (target ).apparent ().hadec ()
436
+
437
+ # Expensive: generate true ha/dec at `t`.
438
+ apparent = observer .at (t ).observe (target ).apparent ()
439
+ ha , dec , distance = apparent .hadec ()
440
+
441
+ # Estimate where the horizon-crossing is.
406
442
desired_ha = f (latitude , dec , h (distance ))
407
443
ha_adjustment = desired_ha - ha .radians
408
444
ha_adjustment = (ha_adjustment + pi ) % tau - pi
445
+
446
+ # Figure out how fast the target's HA is changing.
447
+ ha_diff = normalize (ha .radians - old_ha_radians )
448
+ t_diff = t - old_t
449
+ ha_per_day = ha_diff / t_diff
450
+
451
+ # Remember this iteration's HA and `t` for the next iteration.
452
+ old_ha_radians = ha .radians
453
+ old_t = t
454
+
455
+ # The big moment! Carefully adjust `t` towards intersection.
409
456
timebump = ha_adjustment / ha_per_day
457
+ timebump [timebump == 0.0 ] = _MICROSECOND # avoid divide-by-zero
458
+ previous_t = t
410
459
t = ts .tt_jd (t .whole , t .tt_fraction + timebump )
411
460
412
- is_above_horizon = (desired_ha % pi != 0.0 )
461
+ # Different `normalize` for all but the first iteration.
462
+ normalize = normalize_plus_or_minus_pi
463
+
464
+ if f is _transit_ha :
465
+ return t
466
+
467
+ # In almost all cases, we are now happy. But for rising and setting
468
+ # calculations at high latitudes where the target barely scrapes the
469
+ # horizon, we might be stuck between two solutions, and need to
470
+ # interpolate between them.
471
+
472
+ # Snag the observer's GeographicPosition and learn the target's
473
+ # altitude vs the horizon and how fast it's moving vertically at
474
+ # the second-to-last `t` we computed above.
475
+ v = observer .vector_functions [- 1 ]
476
+ altitude0 , _ , distance0 , rate0 , _ , _ = (
477
+ apparent .frame_latlon_and_rates (v ))
478
+
479
+ # Even faster than _fastify(t) is to just assume that nutation
480
+ # doesn't have much time to move over this short interval.
481
+ t .M = previous_t .M
482
+ t ._nutation_angles_radians = previous_t ._nutation_angles_radians
483
+
484
+ # And again, this time with the very final `t` we computed.
485
+ apparent = observer .at (t ).observe (target ).apparent ()
486
+ altitude1 , _ , distance1 , rate1 , _ , _ = (
487
+ apparent .frame_latlon_and_rates (v ))
488
+
489
+ # Using the target's altitude and altitude-velocity at the final
490
+ # two times we computed, compute where it crosses the horizon.
491
+ tdiff = t - previous_t
492
+ t_scaled_offset = _intersection (
493
+ altitude0 .radians - h (distance0 ),
494
+ altitude1 .radians - h (distance1 ),
495
+ rate0 .radians .per_day * tdiff ,
496
+ rate1 .radians .per_day * tdiff ,
497
+ )
498
+
499
+ # In case the parabola for some reason goes crazy, don't let our
500
+ # solution be thrown too far away from our final two times.
501
+ t_scaled_offset = np .clip (t_scaled_offset , _clip_lower , _clip_upper )
502
+
503
+ t = previous_t + t_scaled_offset * tdiff
504
+
505
+ is_above_horizon = (
506
+ (desired_ha % pi != 0.0 )
507
+ | ((t_scaled_offset > _clip_lower ) & (t_scaled_offset < _clip_upper ))
508
+ )
509
+
413
510
return t , is_above_horizon
414
511
415
512
def find_risings (observer , target , start_time , end_time , horizon_degrees = None ):
@@ -466,5 +563,4 @@ def find_transits(observer, target, start_time, end_time):
466
563
.. versionadded:: 1.47
467
564
468
565
"""
469
- t , _ = _find (observer , target , start_time , end_time , 0.0 , _transit_ha )
470
- return t
566
+ return _find (observer , target , start_time , end_time , 0.0 , _transit_ha )
0 commit comments