File Coverage

blib/lib/Astro/Coord/ECI/VSOP87D.pm
Criterion Covered Total %
statement 289 295 97.9
branch 62 80 77.5
condition 14 23 60.8
subroutine 38 40 95.0
pod 9 9 100.0
total 412 447 92.1


line stmt bran cond sub pod time code
1             package Astro::Coord::ECI::VSOP87D;
2              
3 13     13   25881 use 5.008;
  13         52  
4              
5 13     13   173 use strict;
  13         35  
  13         393  
6 13     13   97 use warnings;
  13         27  
  13         398  
7              
8 13     13   8938 use utf8;
  13         192  
  13         73  
9              
10 13         1403 use Astro::Coord::ECI::Utils qw{
11             AU PI SECSPERDAY TWOPI
12             asin deg2rad jcent2000 julianday
13             load_module looks_like_number mod2pi
14             rad2deg rad2dms rad2hms tan
15 13     13   1268 };
  13         20404  
16 13     13   104 use Exporter qw{ import };
  13         37  
  13         375  
17 13     13   72 use Carp;
  13         33  
  13         720  
18 13     13   92 use POSIX qw{ floor };
  13         24  
  13         95  
19 13     13   10097 use Storable qw{ dclone };
  13         44176  
  13         1036  
20              
21 13     13   161 use constant DAYS_PER_JULIAN_MILENNIUM => 365250;
  13         30  
  13         939  
22 13     13   81 use constant CODE_REF => ref sub {};
  13         42  
  13         752  
23 13     13   131 use constant HASH_REF => ref {};
  13         51  
  13         856  
24 13     13   86 use constant SUN_CLASS => __PACKAGE__ . '::Sun';
  13         32  
  13         860  
25              
26             BEGIN {
27 13 100   13   6669 __PACKAGE__->can( 'DEBUG' )
28             or constant->import( DEBUG => 0 );
29             }
30              
31             our $VERSION = '0.005_01';
32              
33             my @basic_export = qw{
34             SUN_CLASS
35             model_cutoff_definition
36             nutation obliquity order period time_set year
37             __default
38             __get_attr
39             __mutate_model_cutoff __mutate_nutation_cutoff
40             };
41              
42             our @EXPORT_OK = (
43             @basic_export,
44             qw{
45             geometric_longitude
46             synodic_period
47             __angle_subtended_from_earth
48             __longitude_from_sun
49             __model
50             },
51             );
52             our %EXPORT_TAGS = (
53             mixin => [ @basic_export, qw{
54             synodic_period
55             __angle_subtended_from_earth
56             __longitude_from_sun
57             __model
58             } ],
59             sun => [ @basic_export, qw{ geometric_longitude } ],
60             );
61              
62             # We want to ensure SUN_CLASS is loaded because it is our default Sun
63             # implementation, but we do it at run time to avoid a circular
64             # dependency.
65             load_module( SUN_CLASS );
66              
67             sub time_set {
68 3011     3011 1 210538 my ( $self ) = @_;
69 3011         8478 my $attr = $self->__get_attr();
70 3011         8661 my $time = $self->dynamical();
71 3011         74972 my $cutoff = $self->get( 'model_cutoff' );
72 3011         8939 my $cutoff_def = $self->model_cutoff_definition();
73 3011         7588 my $sun = $self->get( 'sun' );
74              
75 3011         5321 DEBUG
76             and printf <<'EOD', ref $self, ref $sun, julianday( $time );
77              
78             self = %s
79             Sun = %s
80             JDE = %.5f
81             EOD
82              
83 3011         8759 my $T = jcent2000( $time );
84              
85 3011         20819 my ( $Lb, $Bb, $Rb ) = $self->__model( $time,
86             model_cutoff_definition => $cutoff_def,
87             );
88              
89 3011         7395 my ( $Le, $Be, $Re );
90              
91 3011 50       15576 if ( $sun->isa( SUN_CLASS ) ) {
92             # We call __model as a subroutine because the Earth's model
93             # parameters are hung on the Sun, but if we call it as a method
94             # we get the Sun's model, which always returns zeroes.
95 3011         8928 ( $Le, $Be, $Re ) = __model( SUN_CLASS, $time,
96             model_cutoff_definition => $sun->model_cutoff_definition( $cutoff ),
97             );
98             } else {
99 0         0 confess sprintf 'TODO Sun class %s not supported', ref $sun;
100             }
101              
102 3011         6762 DEBUG
103             and printf <<'EOD', rad2deg( $Le ), rad2deg( $Be ), $Re;
104              
105             Heliocentric position of the Earth:
106             L0 = %.5f
107             B0 = %.5f
108             R0 = %.6f
109             EOD
110              
111 3011         6496 my ( $lambda, $beta, $Delta, $long_sym );
112 3011 100       7588 if ( $Rb ) { # Not the Sun
113              
114 1719         2956 my $last_tau = 0;
115              
116 1719         3048 while ( 1 ) {
117 3789         6667 DEBUG
118             and printf <<'EOD', $self->get( 'name' ),
119              
120             Heliocentric position of %s:
121             L = %.5f
122             B = %.5f
123             R = %.6f
124             EOD
125             rad2deg( $Lb ), rad2deg( $Bb ), $Rb;
126              
127             # Meeus 33.1
128 3789         8809 my $cosBb = cos( $Bb );
129 3789         6829 my $cosBe = cos( $Be );
130 3789         9264 my $x = $Rb * $cosBb * cos( $Lb ) - $Re * $cosBe * cos( $Le );
131 3789         10074 my $y = $Rb * $cosBb * sin( $Lb ) - $Re * $cosBe * sin( $Le );
132 3789         7382 my $z = $Rb * sin( $Bb ) - $Re * sin( $Be );
133              
134 3789         8392 $Delta = sqrt( $x * $x + $y * $y + $z * $z );
135              
136 13     13   113 use constant TAU_FACTOR => 0.005_775_518_3 * SECSPERDAY;
  13         27  
  13         3558  
137 3789         6656 my $tau = TAU_FACTOR * $Delta;
138              
139 3789         5606 DEBUG
140             and printf <<'EOD', $x, $y, $z, $Delta, $tau / SECSPERDAY;
141              
142             x = %+.6f
143             y = %+.6f
144             z = %+.6f
145             𝚫 = %.6f
146             𝛕 = %.7f day
147             EOD
148 3789 100       45555 if ( ( my $check = sprintf '%.1f', $tau ) ne $last_tau ) {
149 2070         4339 $last_tau = $check;
150              
151 2070         3276 DEBUG
152             and printf <<'EOD', julianday( $time - $tau );
153              
154             new JDE = %.5f
155             EOD
156 2070         7847 ( $Lb, $Bb, $Rb ) = $self->__model(
157             $time - $tau,
158             model_cutoff_definition => $cutoff_def,
159             );
160             } else {
161              
162             # Meeus 33.2
163 1719         7136 $lambda = atan2( $y, $x );
164 1719 100       6274 $lambda < 0
165             and $lambda += TWOPI;
166 1719         4262 $beta = atan2( $z, sqrt( $x * $x + $y * $y ) );
167 1719         3642 $long_sym = '𝛌';
168 1719         4550 last;
169             }
170             }
171              
172             DEBUG
173 1719         2729 and printf <<'EOD', $long_sym,
174              
175             Geocentric ecliptic position:
176             %s = %.5f
177             = %s
178             𝛃 = %.5f
179             = %s
180             EOD
181             rad2deg( $lambda ), rad2dms( $lambda ),
182             rad2deg( $beta ), rad2dms( $beta );
183              
184             # Meeus corrects for aberration here for a planet, before
185             # conversion to FK5. It makes no real difference to the answer,
186             # since everything done to the latitude and longitude before
187             # conversion to equatorial is just addition and subtraction, and
188             # therefore commutes. But it plays merry Hell with the poor slob
189             # who is trying to verify code versus the worked examples,
190             # because it changes the intermediate results. Sigh.
191              
192             # Aberration per Meeus 23.2
193 13         84 use constant CONSTANT_OF_ABERRATION => deg2rad(
194 13     13   106 20.49552 / 3600 );
  13         29  
195             # Longitude of the Sun
196 1719         5371 my $Ls = mod2pi( $Le + PI );
197             # Eccentricity of the Earth's orbit
198 1719         8767 my $e = ( - 0.000_000_126_7 * $T - 0.000_042_037 ) * $T +
199             0.016_708_634;
200             # Longitude of perihelion of the Earth's orbit
201 1719         5910 my $pi = deg2rad( ( 0.000_46 * $T + 1.71946 ) * $T + 102.93735 );
202              
203 1719         10261 my $delta_lambda = ( ( $e * cos( $pi - $lambda ) - cos( $Ls -
204             $lambda ) ) / cos $beta ) * CONSTANT_OF_ABERRATION;
205 1719         5819 my $delta_beta = - sin( $beta ) * ( sin( $Ls - $lambda ) - $e *
206             sin( $pi - $lambda ) ) * CONSTANT_OF_ABERRATION;
207 1719         2895 $lambda += $delta_lambda;
208 1719         2830 $beta += $delta_beta;
209              
210 1719         3032 DEBUG
211             and printf <<'EOD',
212              
213             Planetary aberration:
214             𝚫𝛌 = %s
215             𝛌 = %.5f
216             = %s
217             𝚫𝛃 = %s
218             𝛃 = %.5f
219             = %s
220             EOD
221             rad2dms( $delta_lambda), rad2deg( $lambda ), rad2dms( $lambda ),
222             rad2dms( $delta_beta), rad2deg( $beta ), rad2dms( $beta );
223              
224             } else { # The Sun
225 1292         4258 ( $lambda, $beta, $Delta ) = ( mod2pi( $Le + PI ), - $Be, $Re );
226 1292         7336 $long_sym = '☉';
227              
228 1292         2057 DEBUG
229             and printf <<'EOD', $long_sym,
230              
231             Geocentric ecliptic position:
232             %s = %.5f
233             = %s
234             𝛃 = %.5f
235             = %s
236             EOD
237             rad2deg( $lambda ), rad2dms( $lambda ),
238             rad2deg( $beta ), rad2dms( $beta );
239             }
240              
241 3011         5092 DEBUG
242             and printf <<'EOD',
243              
244             %s = %.6f
245             𝛃 = %.6f
246             = %s
247             𝚫 = %.6f
248             EOD
249             $long_sym, rad2deg( $lambda ),
250             rad2deg( $beta ), rad2dms( $beta ), $Delta;
251              
252             # Convert to FK5. Meeus says (ch. 25 pg 166) this is necessary for
253             # high accuracy (though not if using his truncated VSOP87D), though
254             # Bretagnon & Francou seem to say it is needed only for VSOP87 and
255             # VSOP87E.
256              
257             # Meeus 32.3
258             {
259 13     13   3883 use constant DYN_TO_FK5_LINEAR => deg2rad( 1.397 );
  13         30  
  13         51  
  3011         5279  
260 13     13   920 use constant DYN_TO_FK5_QUADRATIC => deg2rad( 0.00031 );
  13         30  
  13         68  
261 13     13   1016 use constant DYN_TO_FK5_DELTA_LON => - deg2rad( 0.09033 / 3600 );
  13         28  
  13         91  
262 13     13   990 use constant DYN_TO_FK5_FACTOR => deg2rad( 0.03916 / 3600 );
  13         29  
  13         52  
263 3011         7439 my $lambda_prime = $lambda - ( DYN_TO_FK5_LINEAR +
264             DYN_TO_FK5_QUADRATIC * $T ) * $T;
265 3011         8281 my $factor = DYN_TO_FK5_FACTOR * ( cos( $lambda_prime ) - sin(
266             $lambda_prime ) );
267 3011         10238 my $delta_lambda = DYN_TO_FK5_DELTA_LON + $factor * tan( $beta );
268 3011         14039 my $delta_beta = $factor;
269              
270 3011         4788 DEBUG and printf <<'EOD',
271              
272             Conversion to FK5
273             𝛌' = %.2f
274             𝚫%s = %s
275             𝚫𝛃 = %s
276             EOD
277             rad2deg( $lambda_prime ), $long_sym,
278             rad2dms( $delta_lambda, 5 ), rad2dms( $delta_beta );
279              
280 3011         5106 $lambda += $delta_lambda;
281 3011         5075 $beta += $delta_beta;
282              
283 3011         4632 DEBUG and printf <<'EOD',
284              
285             %s = %.5f
286             = %s
287             𝛃 = %.5f
288             = %s
289             EOD
290             $long_sym, rad2deg( $lambda ), rad2dms( $lambda ),
291             rad2deg( $beta ), rad2dms( $beta ),
292             ;
293              
294 3011         8389 $attr->{geometric_longitude} = $lambda;
295             }
296              
297             # Nutation
298 3011         12603 my ( $delta_psi, $delta_eps ) = $self->nutation( $time );
299 3011         16205 $lambda += $delta_psi;
300              
301 3011         5021 DEBUG
302             and printf <<"EOD",
303              
304             Nutation:
305             𝚫𝛙 = %s
306             𝚫𝛆 = %s
307             𝛌 = %.5f
308             = %s
309             EOD
310             rad2dms( $delta_psi ), rad2dms( $delta_eps ),
311             rad2deg( $lambda ), rad2dms( $lambda );
312              
313             # Obliquity per Meeus 22.3
314 3011         10055 my $epsilon = $self->obliquity( $time );
315              
316             # Meeus 25.10
317              
318 3011 100       7964 unless ( $Rb ) { # The Sun.
319             # Aberration
320 13     13   3897 use constant SOLAR_ABERRATION_FACTOR => - deg2rad( 20.4898 / 3600 );
  13         30  
  13         69  
321 1292         2697 my $delta_lambda = SOLAR_ABERRATION_FACTOR / $Delta;
322 1292         2206 $lambda += $delta_lambda;
323              
324 1292         2179 DEBUG
325             and printf <<'EOD',
326              
327             Solar aberration:
328             𝚫𝛌 = %s
329             𝛌 = %.5f
330             = %s
331             EOD
332             rad2dms( $delta_lambda), rad2deg( $lambda ), rad2dms( $lambda );
333             }
334              
335             DEBUG
336 3011         4829 and printf <<'EOD', $long_sym,
337              
338             Final ecliptic coordinates:
339             %s = %.5f
340             = %s
341             𝛃 = %.5f
342             = %s
343             EOD
344             rad2deg( $lambda ), rad2dms( $lambda ),
345             rad2deg( $beta ), rad2dms( $beta );
346              
347             # NOTE: I could have stored the ecliptic coordinates at this point,
348             # and trusted Astro::Coord::ECI to get the conversion right, but
349             # since I was at this point not confident in that code, I decided to
350             # see Meeus' example all the way to the end.
351             #
352             # Once nutation and obliquity became methods, it turns out that my
353             # conversion between ecliptic and equatorial is equivalent to Meeus'
354             # to his accuracy in the examples. That is, if I store equatorial, I
355             # get back exactuly the same ecliptic coordiantes as I calculated
356             # above. If I store ecliptic, I get back the same equatorial that
357             # Meeus calculated.
358             #
359             # My current intent is to keep the code as-is, simply because for
360             # verification purposes I want it to be as close as possible to
361             # Meeus' example.
362              
363 3011         6342 my $sin_eps = sin $epsilon;
364 3011         5344 my $cos_eps = cos $epsilon;
365 3011         5641 my $sin_lam = sin $lambda;
366 3011         4956 my $sin_bet = sin $beta;
367 3011         4940 my $cos_bet = cos $beta;
368 3011         14873 my $alpha = mod2pi( atan2(
369             $sin_lam * $cos_eps - $sin_bet / $cos_bet * $sin_eps,
370             cos $lambda ) ); # Meeus 13.3
371 3011         19886 my $delta = asin( $sin_bet * $cos_eps + $cos_bet * $sin_eps *
372             $sin_lam ); # Meeus 13.4
373              
374 3011         13834 DEBUG
375             and printf <<'EOD',
376              
377             Equatorial coordinates:
378             𝛂 = %.6f
379             = %s
380             𝛅 = %.6f
381             = %s
382             EOD
383             rad2deg( $alpha ), rad2hms( $alpha ),
384             rad2deg( $delta ), rad2dms( $delta, 2 );
385              
386 3011         16707 $self->equatorial( $alpha, $delta, $Delta * AU );
387 3011         568822 $self->equinox_dynamical( $time );
388              
389 3011         20807 if ( DEBUG ) {
390             my ( $lat, $lon ) = $self->ecliptic();
391             printf <<'EOD', $long_sym,
392              
393             Check -- recovered ecliptic coordinates:
394             %s = %.5f
395             = %s
396             𝛃 = %.5f
397             = %s
398             EOD
399             rad2deg( $lon ), rad2dms( $lon ),
400             rad2deg( $lat ), rad2dms( $lat );
401             }
402              
403 3011         9503 return $self;
404             }
405              
406             {
407             my $earth;
408              
409             # NOTE that the sign of the angle returned is the same as the sign
410             # of the result of __longitude_from_sun().
411             sub __angle_subtended_from_earth {
412 246     246   689 my ( $self, $time ) = @_;
413 246 100       782 defined $time
414             or $time = $self->universal();
415 246   66     835 $earth ||= Astro::Coord::ECI->new()->eci( 0, 0, 0 );
416 246         1000 $self->universal( $time );
417 246         3802 my $sun = $self->get( 'sun' )->universal( $time );
418 246         3381 $earth->universal( $time );
419 246         9656 my $lon = $self->__longitude_from_sun();
420 246         1744 my $angle = $earth->angle( $self, $sun );
421 246 100       91676 return $lon < 0 ? -$angle : $angle;
422             }
423             }
424              
425             sub geometric_longitude {
426 1     1 1 19 my ( $self ) = @_;
427 1         2 my $attr = $self->__get_attr();
428             defined $attr->{geometric_longitude}
429 1 50       11 and return $attr->{geometric_longitude};
430 0         0 croak 'Geometric longitude undefined -- time not set';
431             }
432              
433             sub __longitude_from_sun {
434 1248     1248   3023 my ( $self, $time, $offset ) = @_;
435 1248   100     4683 $offset ||= 0;
436              
437 1248 100       2792 if ( defined $time ) {
438 1002         3193 $self->universal( $time );
439             } else {
440 246         681 $time = $self->universal();
441             }
442              
443 1248         18385 my $sun = $self->get( 'sun' )->universal( $time );
444              
445 1248         16972 my ( undef, $lon_b ) = $self->ecliptic();
446 1248         89440 my ( undef, $lon_s ) = $sun->ecliptic();
447              
448 1248         70878 return mod2pi( $lon_b - $lon_s - $offset + PI ) - PI;
449             }
450              
451 0         0 BEGIN {
452 13     13   25735 my @model = (
453              
454             # The following are from the IAU SOFA module src/nut80.c, from
455             # http://www.iausofa.org/2018_0130_C/sofa_c-20180130.tar.gz
456             # The only edit is the change from curly to square brackets and
457             # the brute-force conversion of C comments to Perl comments. The
458             # columns are:
459             # IAU: nl nlp nf nd nom sp spt ce cet
460             # Meeus: M' M F D 𝛀
461             # /* 1-10 */
462             [ 0, 0, 0, 0, 1, -171996.0, -174.2, 92025.0, 8.9 ],
463             [ 0, 0, 0, 0, 2, 2062.0, 0.2, -895.0, 0.5 ],
464             [ -2, 0, 2, 0, 1, 46.0, 0.0, -24.0, 0.0 ],
465             [ 2, 0, -2, 0, 0, 11.0, 0.0, 0.0, 0.0 ],
466             [ -2, 0, 2, 0, 2, -3.0, 0.0, 1.0, 0.0 ],
467             [ 1, -1, 0, -1, 0, -3.0, 0.0, 0.0, 0.0 ],
468             [ 0, -2, 2, -2, 1, -2.0, 0.0, 1.0, 0.0 ],
469             [ 2, 0, -2, 0, 1, 1.0, 0.0, 0.0, 0.0 ],
470             [ 0, 0, 2, -2, 2, -13187.0, -1.6, 5736.0, -3.1 ],
471             [ 0, 1, 0, 0, 0, 1426.0, -3.4, 54.0, -0.1 ],
472              
473             # /* 11-20 */
474             [ 0, 1, 2, -2, 2, -517.0, 1.2, 224.0, -0.6 ],
475             [ 0, -1, 2, -2, 2, 217.0, -0.5, -95.0, 0.3 ],
476             [ 0, 0, 2, -2, 1, 129.0, 0.1, -70.0, 0.0 ],
477             [ 2, 0, 0, -2, 0, 48.0, 0.0, 1.0, 0.0 ],
478             [ 0, 0, 2, -2, 0, -22.0, 0.0, 0.0, 0.0 ],
479             [ 0, 2, 0, 0, 0, 17.0, -0.1, 0.0, 0.0 ],
480             [ 0, 1, 0, 0, 1, -15.0, 0.0, 9.0, 0.0 ],
481             [ 0, 2, 2, -2, 2, -16.0, 0.1, 7.0, 0.0 ],
482             [ 0, -1, 0, 0, 1, -12.0, 0.0, 6.0, 0.0 ],
483             [ -2, 0, 0, 2, 1, -6.0, 0.0, 3.0, 0.0 ],
484              
485             # /* 21-30 */
486             [ 0, -1, 2, -2, 1, -5.0, 0.0, 3.0, 0.0 ],
487             [ 2, 0, 0, -2, 1, 4.0, 0.0, -2.0, 0.0 ],
488             [ 0, 1, 2, -2, 1, 4.0, 0.0, -2.0, 0.0 ],
489             [ 1, 0, 0, -1, 0, -4.0, 0.0, 0.0, 0.0 ],
490             [ 2, 1, 0, -2, 0, 1.0, 0.0, 0.0, 0.0 ],
491             [ 0, 0, -2, 2, 1, 1.0, 0.0, 0.0, 0.0 ],
492             [ 0, 1, -2, 2, 0, -1.0, 0.0, 0.0, 0.0 ],
493             [ 0, 1, 0, 0, 2, 1.0, 0.0, 0.0, 0.0 ],
494             [ -1, 0, 0, 1, 1, 1.0, 0.0, 0.0, 0.0 ],
495             [ 0, 1, 2, -2, 0, -1.0, 0.0, 0.0, 0.0 ],
496              
497             # /* 31-40 */
498             [ 0, 0, 2, 0, 2, -2274.0, -0.2, 977.0, -0.5 ],
499             [ 1, 0, 0, 0, 0, 712.0, 0.1, -7.0, 0.0 ],
500             [ 0, 0, 2, 0, 1, -386.0, -0.4, 200.0, 0.0 ],
501             [ 1, 0, 2, 0, 2, -301.0, 0.0, 129.0, -0.1 ],
502             [ 1, 0, 0, -2, 0, -158.0, 0.0, -1.0, 0.0 ],
503             [ -1, 0, 2, 0, 2, 123.0, 0.0, -53.0, 0.0 ],
504             [ 0, 0, 0, 2, 0, 63.0, 0.0, -2.0, 0.0 ],
505             [ 1, 0, 0, 0, 1, 63.0, 0.1, -33.0, 0.0 ],
506             [ -1, 0, 0, 0, 1, -58.0, -0.1, 32.0, 0.0 ],
507             [ -1, 0, 2, 2, 2, -59.0, 0.0, 26.0, 0.0 ],
508              
509             # /* 41-50 */
510             [ 1, 0, 2, 0, 1, -51.0, 0.0, 27.0, 0.0 ],
511             [ 0, 0, 2, 2, 2, -38.0, 0.0, 16.0, 0.0 ],
512             [ 2, 0, 0, 0, 0, 29.0, 0.0, -1.0, 0.0 ],
513             [ 1, 0, 2, -2, 2, 29.0, 0.0, -12.0, 0.0 ],
514             [ 2, 0, 2, 0, 2, -31.0, 0.0, 13.0, 0.0 ],
515             [ 0, 0, 2, 0, 0, 26.0, 0.0, -1.0, 0.0 ],
516             [ -1, 0, 2, 0, 1, 21.0, 0.0, -10.0, 0.0 ],
517             [ -1, 0, 0, 2, 1, 16.0, 0.0, -8.0, 0.0 ],
518             [ 1, 0, 0, -2, 1, -13.0, 0.0, 7.0, 0.0 ],
519             [ -1, 0, 2, 2, 1, -10.0, 0.0, 5.0, 0.0 ],
520              
521             # /* 51-60 */
522             [ 1, 1, 0, -2, 0, -7.0, 0.0, 0.0, 0.0 ],
523             [ 0, 1, 2, 0, 2, 7.0, 0.0, -3.0, 0.0 ],
524             [ 0, -1, 2, 0, 2, -7.0, 0.0, 3.0, 0.0 ],
525             [ 1, 0, 2, 2, 2, -8.0, 0.0, 3.0, 0.0 ],
526             [ 1, 0, 0, 2, 0, 6.0, 0.0, 0.0, 0.0 ],
527             [ 2, 0, 2, -2, 2, 6.0, 0.0, -3.0, 0.0 ],
528             [ 0, 0, 0, 2, 1, -6.0, 0.0, 3.0, 0.0 ],
529             [ 0, 0, 2, 2, 1, -7.0, 0.0, 3.0, 0.0 ],
530             [ 1, 0, 2, -2, 1, 6.0, 0.0, -3.0, 0.0 ],
531             [ 0, 0, 0, -2, 1, -5.0, 0.0, 3.0, 0.0 ],
532              
533             # /* 61-70 */
534             [ 1, -1, 0, 0, 0, 5.0, 0.0, 0.0, 0.0 ],
535             [ 2, 0, 2, 0, 1, -5.0, 0.0, 3.0, 0.0 ],
536             [ 0, 1, 0, -2, 0, -4.0, 0.0, 0.0, 0.0 ],
537             [ 1, 0, -2, 0, 0, 4.0, 0.0, 0.0, 0.0 ],
538             [ 0, 0, 0, 1, 0, -4.0, 0.0, 0.0, 0.0 ],
539             [ 1, 1, 0, 0, 0, -3.0, 0.0, 0.0, 0.0 ],
540             [ 1, 0, 2, 0, 0, 3.0, 0.0, 0.0, 0.0 ],
541             [ 1, -1, 2, 0, 2, -3.0, 0.0, 1.0, 0.0 ],
542             [ -1, -1, 2, 2, 2, -3.0, 0.0, 1.0, 0.0 ],
543             [ -2, 0, 0, 0, 1, -2.0, 0.0, 1.0, 0.0 ],
544              
545             # /* 71-80 */
546             [ 3, 0, 2, 0, 2, -3.0, 0.0, 1.0, 0.0 ],
547             [ 0, -1, 2, 2, 2, -3.0, 0.0, 1.0, 0.0 ],
548             [ 1, 1, 2, 0, 2, 2.0, 0.0, -1.0, 0.0 ],
549             [ -1, 0, 2, -2, 1, -2.0, 0.0, 1.0, 0.0 ],
550             [ 2, 0, 0, 0, 1, 2.0, 0.0, -1.0, 0.0 ],
551             [ 1, 0, 0, 0, 2, -2.0, 0.0, 1.0, 0.0 ],
552             [ 3, 0, 0, 0, 0, 2.0, 0.0, 0.0, 0.0 ],
553             [ 0, 0, 2, 1, 2, 2.0, 0.0, -1.0, 0.0 ],
554             [ -1, 0, 0, 0, 2, 1.0, 0.0, -1.0, 0.0 ],
555             [ 1, 0, 0, -4, 0, -1.0, 0.0, 0.0, 0.0 ],
556              
557             # /* 81-90 */
558             [ -2, 0, 2, 2, 2, 1.0, 0.0, -1.0, 0.0 ],
559             [ -1, 0, 2, 4, 2, -2.0, 0.0, 1.0, 0.0 ],
560             [ 2, 0, 0, -4, 0, -1.0, 0.0, 0.0, 0.0 ],
561             [ 1, 1, 2, -2, 2, 1.0, 0.0, -1.0, 0.0 ],
562             [ 1, 0, 2, 2, 1, -1.0, 0.0, 1.0, 0.0 ],
563             [ -2, 0, 2, 4, 2, -1.0, 0.0, 1.0, 0.0 ],
564             [ -1, 0, 4, 0, 2, 1.0, 0.0, 0.0, 0.0 ],
565             [ 1, -1, 0, -2, 0, 1.0, 0.0, 0.0, 0.0 ],
566             [ 2, 0, 2, -2, 1, 1.0, 0.0, -1.0, 0.0 ],
567             [ 2, 0, 2, 2, 2, -1.0, 0.0, 0.0, 0.0 ],
568              
569             # /* 91-100 */
570             [ 1, 0, 0, 2, 1, -1.0, 0.0, 0.0, 0.0 ],
571             [ 0, 0, 4, -2, 2, 1.0, 0.0, 0.0, 0.0 ],
572             [ 3, 0, 2, -2, 2, 1.0, 0.0, 0.0, 0.0 ],
573             [ 1, 0, 2, -2, 0, -1.0, 0.0, 0.0, 0.0 ],
574             [ 0, 1, 2, 0, 1, 1.0, 0.0, 0.0, 0.0 ],
575             [ -1, -1, 0, 2, 1, 1.0, 0.0, 0.0, 0.0 ],
576             [ 0, 0, -2, 0, 1, -1.0, 0.0, 0.0, 0.0 ],
577             [ 0, 0, 2, -1, 2, -1.0, 0.0, 0.0, 0.0 ],
578             [ 0, 1, 0, 2, 0, -1.0, 0.0, 0.0, 0.0 ],
579             [ 1, 0, -2, -2, 0, -1.0, 0.0, 0.0, 0.0 ],
580              
581             # /* 101-106 */
582             [ 0, -1, 2, 0, 1, -1.0, 0.0, 0.0, 0.0 ],
583             [ 1, 1, 0, -2, 1, -1.0, 0.0, 0.0, 0.0 ],
584             [ 1, 0, -2, 2, 0, -1.0, 0.0, 0.0, 0.0 ],
585             [ 2, 0, 0, 2, 0, 1.0, 0.0, 0.0, 0.0 ],
586             [ 0, 0, 2, 4, 2, -1.0, 0.0, 0.0, 0.0 ],
587             [ 0, 1, 0, 1, 0, 1.0, 0.0, 0.0, 0.0 ]
588             );
589              
590             # UNDOCUMENTED AND UNSUPPORTED -- MAY BE CHANGED OR REVOKED WITHOUT
591             # WARNING
592             # Return the IAU 1980 nutation model, as array references. Each
593             # reference contains one term of the model, in the following order
594             # in Meeus' terminology:
595             # M' M F D Omega delta psi( const, T ), delta # epsilon( const, T ).
596             sub __iau1980_nutation_model {
597 0     0   0 return @model;
598             }
599              
600             # This may be a premature optimization, BUT the nutation in
601             # longitude and obliquity are used different places, so I anticipate
602             # this getting called twice by time_set(), with the same time. So:
603 13         60 my $memoize_args = '';
604 13         21514 my @memoize_result;
605              
606             sub nutation {
607 8281     8281 1 18250 my ( $self, $time, $cutoff ) = @_;
608              
609 8281 50       19299 defined $time
610             or $time = $self->dynamical();
611              
612 8281   50     34804 $cutoff ||= 0; # milli arc seconds. Meeus is 3
613              
614             {
615 8281 100       12421 ( my $memo = "$time $cutoff" ) eq $memoize_args
  8281         73188  
616             and return @memoize_result;
617 1943         4299 $memoize_args = $memo;
618             }
619              
620 1943         6267 my $T = jcent2000( $time ); # Meeus 22.1
621              
622             # All this from Meeus Ch 22 pp 143ff, expressed in degrees
623              
624             # Mean elongation of Moon from Sun
625 1943         14595 my $D = ( ( $T / 189_474 - 0.001_914_2 ) * $T + 445_267.111_480
626             ) * $T + 297.850_36;
627              
628             # Mean anomaly of the Sun (Earth)
629 1943         4870 my $M = ( ( - $T / 300_000 - 0.000_160_3 ) * $T + 35_999.050_340
630             ) * $T + 357.527_72;
631              
632             # Mean anomaly of the Moon
633 1943         4539 my $M_prime = ( ( $T / 56_250 + 0.008_697_2 ) * $T + 477_198.867_398
634             ) * $T + 134.962_98;
635              
636             # Moon's argument of latitude
637 1943         4649 my $F = ( ( $T / 327_270 - 0.003_682_5 ) * $T + 483_202.017_538
638             ) * $T + 93.271_91;
639              
640             # Logitude of the ascending node of the Moon's mean orbit on the
641             # ecliptic, measured from the equinox of the date
642 1943         4435 my $Omega = ( ( $T / 450_000 + 0.002_070_8 ) * $T - 1_934.136_261
643             ) * $T + 125.044_52;
644              
645             # Convert to radians, and store in same order as model source
646 1943         5770 my @arg = map { deg2rad( $_ ) } ( $M_prime, $M, $F, $D, $Omega );
  9715         33940  
647              
648 1943         12726 my ( $delta_psi, $delta_eps ) = ( 0, 0 );
649 1943         5382 foreach my $row ( @model ) {
650 205958         407922 my $a = $arg[0] * $row->[0] + $arg[1] * $row->[1] +
651             $arg[2] * $row->[2] + $arg[3] * $row->[3] +
652             $arg[4] * $row->[4];
653 205958 50 33     566146 if ( $row->[5] && $cutoff <= abs $row->[5] ) {
654 205958         367169 $delta_psi += ( $row->[6] * $T + $row->[5] ) * sin $a;
655             }
656 205958 100 66     514199 if ( $row->[7] && $cutoff <= abs $row->[7] ) {
657 124352         245241 $delta_eps += ( $row->[8] * $T + $row->[7] ) * cos $a;
658             }
659              
660             }
661              
662             # The model computes nutations in milli arc seconds, but we
663             # return them in radians
664             return ( @memoize_result =
665 1943         5020 map { deg2rad( $_ / 3600_0000 ) } $delta_psi, $delta_eps );
  3886         17364  
666             }
667              
668             }
669              
670             sub __get_attr {
671 15180     15180   26228 my ( $self ) = @_;
672 15180 50       35672 my $ref = ref $self
673             or confess 'Can not call as static method';
674 15180   100     47803 return $self->{$ref} ||= {
675             model_cutoff_definition => dclone( $self->__model_definition(
676             'default_model_cutoff' ) ),
677             };
678             }
679              
680             sub __default {
681 27     27   78 my ( $class, $arg ) = @_;
682              
683 27         120 my $name = $class->__model_definition( 'body' );
684             defined $arg->{id}
685 27 50       221 or $arg->{id} = $name;
686             defined $arg->{name}
687 27 50       109 or $arg->{name} = $name;
688              
689             defined $arg->{diameter}
690 27 50       224 or $arg->{diameter} = $class->__model_definition( 'diameter' );
691              
692             defined $arg->{model_cutoff}
693 27 100       175 or $arg->{model_cutoff} = 'Meeus';
694              
695             defined $arg->{nutation_cutoff}
696 27 50       97 or $arg->{nutation_cutoff} = 3;
697              
698 27         131 return;
699             }
700              
701             sub __mutate_model_cutoff {
702 28     28   78 my ( $self, $name, $val ) = @_;
703 28 50       84 defined $val
704             or croak "model cutoff must be defined";
705 28 50       138 $self->model_cutoff_definition( $val )
706             or croak "model cutoff '$val' is unknown";
707 28         81 $self->__get_attr()->{$name} = $val;
708 28         104 return $self;
709             }
710              
711             sub model_cutoff_definition {
712 6066     6066 1 14405 my ( $self, $name, @arg ) = @_;
713 6066 100       17903 defined $name
714             or $name = $self->get( 'model_cutoff' );
715 6066         14454 my $attr = $self->__get_attr();
716 6066 100       14880 if ( @arg ) {
717 6 100       15 if ( defined( my $val = $arg[0] ) ) {
718 4 100       13 unless ( ref $val ) {
719 2 50 33     33 looks_like_number( $val )
720             and $val !~ m/ \A Inf (?: inity )? | NaN \z /smx
721             or croak 'Scalar model cutoff definition must be a number';
722 2         6 my $num = $val;
723             $val = sub {
724 2     2   7 my ( @model ) = @_;
725 2         4 my %cutoff;
726 2         5 foreach my $series ( @model ) {
727 34         47 my $count = 0;
728 34         46 foreach my $term ( @{ $series->{terms} } ) {
  34         56  
729 308 100       516 last if $term->[0] < $num;
730 274         368 $count++;
731             }
732             $count
733 34 100       72 and $cutoff{$series->{series}} = $count;
734             }
735 2         6 return \%cutoff;
736 2         12 };
737             }
738 4 100       16 if ( CODE_REF eq ref $val ) {
739             $val = $val->(
740 9         17 map { @{ $_ } } @{
  9         27  
741 3         5 $self->__model_definition( 'model' ) }
  3         10  
742             );
743 3         1009 $val->{name} = $name;
744             }
745 4 50       15 HASH_REF eq ref $val
746             or croak 'The model cutoff definition value must be a hash ref';
747             my $terms = $self->__model_definition(
748 4         13 'default_model_cutoff')->{none};
749 4         20 foreach my $name ( keys %{ $val } ) {
  4         21  
750 44 100       80 'name' eq $name
751             and next;
752 40 50       85 exists $terms->{$name}
753             or croak "Series '$name' not in this model";
754 40 50       127 $val->{$name} > $terms->{$name}
755             and croak "Series '$name' has only $terms->{$name} terms";
756             }
757 4         19 $attr->{model_cutoff_definition}{$name} = $val;
758             } else {
759 2 50       7 $self->__model_definition( 'default_model_cutoff' )->{$name}
760             and croak "You may not delete model cutoff definition '$name'";
761 2         16 delete $attr->{model_cutoff_definition}{$name};
762             }
763 6         23 return $self;
764             } else {
765 6060         20294 return $attr->{model_cutoff_definition}{$name};
766             }
767             }
768              
769             # Static method
770             # Given dynamical time in seconds, return Ecliptic position in Ecliptic
771             # longitude (radians), Ecliptic latitude (radians), Range (AU) and
772             # AU and velocity in AU/day
773             sub __model {
774 6834     6834   275299 my ( $self, $time, %arg ) = @_;
775              
776             DEBUG
777             and printf <<'EOD',
778              
779             __model:
780             invocant: %s
781             model_cutoff: %s
782             EOD
783             $self,
784             ( $arg{model_cutoff_definition} ?
785 6834         11415 ( $arg{model_cutoff_definition}{name} || '' ) :
786             '' ),
787             ;
788              
789 6834         18902 my $jm = jcent2000( $time ) / 10; # Meeus 32.1
790 6834         41906 my @p_vec;
791             my @v_vec;
792 6834         11191 foreach my $coord ( @{ $self->__model_definition( 'model' ) } ) {
  6834         27016  
793 20502         50022 my $dT = my $exponent = 0;
794 20502         32030 my $T = 1;
795 20502         31504 my $pos = my $vel = 0;
796 20502         30821 foreach my $series ( @{ $coord } ) {
  20502         40502  
797             my $limit = $arg{model_cutoff_definition} ?
798             ( $arg{model_cutoff_definition}{$series->{series}} || 0 ) :
799 118569 100 100     407707 @{ $series->{terms} }
  560 100       1468  
800             or next;
801 100172         139264 --$limit;
802 100172         175575 foreach my $inx ( 0 .. $limit ) {
803 1828592         2760278 my $term = $series->{terms}[$inx];
804 1828592         2924082 my $u = $term->[1] + $term->[2] * $jm;
805 1828592         2714696 my $cos_u = cos $u;
806 1828592         2734642 $pos += $term->[0] * $cos_u * $T;
807 1828592         2636552 my $sin_u = sin $u;
808 1828592         3697789 $vel += $dT * $exponent * $term->[0] * $cos_u -
809             $T * $term->[0] * $term->[2] * $sin_u;
810             }
811 100172         144823 $dT = $T;
812 100172         139572 $exponent++;
813 100172         157717 $T *= $jm;
814             }
815 20502         37811 $vel /= DAYS_PER_JULIAN_MILENNIUM; # units/millennium -> units/day
816 20502         41145 push @p_vec, $pos;
817 20502         39159 push @v_vec, $vel;
818             }
819 6834         4278760 $p_vec[0] = mod2pi( $p_vec[0] );
820 6834         94190 return ( @p_vec, @v_vec );
821             }
822              
823             sub __mutate_nutation_cutoff {
824 27     27   79 my ( $self, undef, $val ) = @_;
825 27 50       85 defined $val
826             or croak 'Nutation cutoff must be defined';
827 27 50 33     201 looks_like_number( $val )
828             and $val >= 0
829             or croak 'Nutation cutoff must be a non-negative number';
830 27         104 $self->__get_attr()->{nutation_cutoff} = $val;
831 27         145 return $self;
832             }
833              
834             sub obliquity {
835 5270     5270 1 164876 my ( $self, $time ) = @_;
836              
837 5270 100       15963 defined $time
838             or $time = $self->dynamical();
839             # Obliquity per Meeus 22.3
840 5270         28569 my $U = jcent2000( $time ) / 100;
841 5270         31191 my $epsilon_0 = 0;
842 5270         25847 $epsilon_0 = $epsilon_0 * $U + $_ for qw{ 2.45 5.79 27.87 7.12
843             -39.05 -249.67 -51.38 1999.25 -1.55 -4680.93 84381.448 };
844 5270         13881 $epsilon_0 = deg2rad( $epsilon_0 / 3600 );
845 5270         25172 my ( undef, $delta_eps ) = $self->nutation( $time );
846 5270         12016 my $epsilon = $epsilon_0 + $delta_eps;
847              
848 5270         8261 DEBUG
849             and printf <<"EOD",
850              
851             Obliquity:
852             𝛆₀ = %.7f
853             𝛆 = %.7f
854             = %s
855             EOD
856             rad2deg( $epsilon_0 ), rad2deg( $epsilon ), rad2dms( $epsilon );
857              
858 5270         11449 return $epsilon;
859             }
860              
861             sub order {
862 0     0 1 0 my ( $self ) = @_;
863 0         0 return $self->__model_definition( 'order' );
864             }
865              
866             # Calculate the period of the orbit. The orbital velocity in radians per
867             # Julian centutu is just the inverse of the coefficient of the constant
868             # L1 term -- that is, the A value of the one with B and C both zero,
869             # since that computes as A * cos( B + C * tau ) = A * cos( 0 ) = A
870             #
871             # So the tropical year is just TWOPI divided by this. But we want the
872             # Siderial year, so we have to add in the precession. I found no
873             # reference for this, but to a first approximation the additional angle
874             # to travel is the solar year times the rate of precession, and the
875             # siderial year is the solar year plus the additional distance divided
876             # by the orbital velocity.
877             #
878             # But this calculation is done when the model parameters are built, and
879             # period() just returns the resultant value.
880             #
881             # The rate of pecession is the IAU 2003 value, taken from fapa03.c.
882              
883             sub period {
884 24     24 1 2684 my ( $self ) = @_;
885 24         133 return $self->__model_definition( 'sidereal_period' );
886             }
887              
888             # https://in-the-sky.org/article.php?term=synodic_period
889             # Note that I chose NOT to precalculate this because it depends on the
890             # period of the 'sun' attribute, which may change.
891             sub synodic_period {
892 35     35 1 93 my ( $self ) = @_;
893 35         131 return( 1 / abs( 1 / $self->get( 'sun' )->year() - 1 / $self->year() ) );
894             }
895              
896             sub year {
897 70     70 1 327 my ( $self ) = @_;
898 70         304 return $self->__model_definition( 'tropical_period' );
899             }
900              
901             1;
902              
903             __END__