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   26015 use 5.008;
  13         51  
4              
5 13     13   157 use strict;
  13         27  
  13         392  
6 13     13   101 use warnings;
  13         28  
  13         433  
7              
8 13     13   8716 use utf8;
  13         201  
  13         66  
9              
10 13         1282 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   1410 };
  13         20794  
16 13     13   87 use Exporter qw{ import };
  13         31  
  13         361  
17 13     13   82 use Carp;
  13         37  
  13         642  
18 13     13   73 use POSIX qw{ floor };
  13         26  
  13         126  
19 13     13   9911 use Storable qw{ dclone };
  13         44416  
  13         1023  
20              
21 13     13   112 use constant DAYS_PER_JULIAN_MILENNIUM => 365250;
  13         31  
  13         970  
22 13     13   82 use constant CODE_REF => ref sub {};
  13         33  
  13         740  
23 13     13   100 use constant HASH_REF => ref {};
  13         66  
  13         794  
24 13     13   84 use constant SUN_CLASS => __PACKAGE__ . '::Sun';
  13         26  
  13         815  
25              
26             BEGIN {
27 13 100   13   6441 __PACKAGE__->can( 'DEBUG' )
28             or constant->import( DEBUG => 0 );
29             }
30              
31             our $VERSION = '0.006';
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 193284 my ( $self ) = @_;
69 3011         8319 my $attr = $self->__get_attr();
70 3011         8254 my $time = $self->dynamical();
71 3011         70701 my $cutoff = $self->get( 'model_cutoff' );
72 3011         7989 my $cutoff_def = $self->model_cutoff_definition();
73 3011         6848 my $sun = $self->get( 'sun' );
74              
75 3011         5292 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         7929 my $T = jcent2000( $time );
84              
85 3011         20085 my ( $Lb, $Bb, $Rb ) = $self->__model( $time,
86             model_cutoff_definition => $cutoff_def,
87             );
88              
89 3011         6958 my ( $Le, $Be, $Re );
90              
91 3011 50       15217 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         8318 ( $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         5903 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         6096 my ( $lambda, $beta, $Delta, $long_sym );
112 3011 100       7481 if ( $Rb ) { # Not the Sun
113              
114 1719         2812 my $last_tau = 0;
115              
116 1719         3038 while ( 1 ) {
117 3789         6006 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         8094 my $cosBb = cos( $Bb );
129 3789         6410 my $cosBe = cos( $Be );
130 3789         9290 my $x = $Rb * $cosBb * cos( $Lb ) - $Re * $cosBe * cos( $Le );
131 3789         9775 my $y = $Rb * $cosBb * sin( $Lb ) - $Re * $cosBe * sin( $Le );
132 3789         7419 my $z = $Rb * sin( $Bb ) - $Re * sin( $Be );
133              
134 3789         7277 $Delta = sqrt( $x * $x + $y * $y + $z * $z );
135              
136 13     13   109 use constant TAU_FACTOR => 0.005_775_518_3 * SECSPERDAY;
  13         30  
  13         3487  
137 3789         6069 my $tau = TAU_FACTOR * $Delta;
138              
139 3789         5555 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       45072 if ( ( my $check = sprintf '%.1f', $tau ) ne $last_tau ) {
149 2070         4007 $last_tau = $check;
150              
151 2070         3410 DEBUG
152             and printf <<'EOD', julianday( $time - $tau );
153              
154             new JDE = %.5f
155             EOD
156 2070         7258 ( $Lb, $Bb, $Rb ) = $self->__model(
157             $time - $tau,
158             model_cutoff_definition => $cutoff_def,
159             );
160             } else {
161              
162             # Meeus 33.2
163 1719         6938 $lambda = atan2( $y, $x );
164 1719 100       6012 $lambda < 0
165             and $lambda += TWOPI;
166 1719         4227 $beta = atan2( $z, sqrt( $x * $x + $y * $y ) );
167 1719         3205 $long_sym = '𝛌';
168 1719         4195 last;
169             }
170             }
171              
172             DEBUG
173 1719         2516 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   102 20.49552 / 3600 );
  13         27  
195             # Longitude of the Sun
196 1719         4956 my $Ls = mod2pi( $Le + PI );
197             # Eccentricity of the Earth's orbit
198 1719         8867 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         5988 my $pi = deg2rad( ( 0.000_46 * $T + 1.71946 ) * $T + 102.93735 );
202              
203 1719         10336 my $delta_lambda = ( ( $e * cos( $pi - $lambda ) - cos( $Ls -
204             $lambda ) ) / cos $beta ) * CONSTANT_OF_ABERRATION;
205 1719         5144 my $delta_beta = - sin( $beta ) * ( sin( $Ls - $lambda ) - $e *
206             sin( $pi - $lambda ) ) * CONSTANT_OF_ABERRATION;
207 1719         2602 $lambda += $delta_lambda;
208 1719         2879 $beta += $delta_beta;
209              
210 1719         2990 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         3686 ( $lambda, $beta, $Delta ) = ( mod2pi( $Le + PI ), - $Be, $Re );
226 1292         6656 $long_sym = '☉';
227              
228 1292         2074 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         4918 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   3597 use constant DYN_TO_FK5_LINEAR => deg2rad( 1.397 );
  13         43  
  13         56  
  3011         5011  
260 13     13   918 use constant DYN_TO_FK5_QUADRATIC => deg2rad( 0.00031 );
  13         30  
  13         51  
261 13     13   1018 use constant DYN_TO_FK5_DELTA_LON => - deg2rad( 0.09033 / 3600 );
  13         27  
  13         56  
262 13     13   966 use constant DYN_TO_FK5_FACTOR => deg2rad( 0.03916 / 3600 );
  13         23  
  13         46  
263 3011         6764 my $lambda_prime = $lambda - ( DYN_TO_FK5_LINEAR +
264             DYN_TO_FK5_QUADRATIC * $T ) * $T;
265 3011         7232 my $factor = DYN_TO_FK5_FACTOR * ( cos( $lambda_prime ) - sin(
266             $lambda_prime ) );
267 3011         9534 my $delta_lambda = DYN_TO_FK5_DELTA_LON + $factor * tan( $beta );
268 3011         12738 my $delta_beta = $factor;
269              
270 3011         4676 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         4783 $lambda += $delta_lambda;
281 3011         4610 $beta += $delta_beta;
282              
283 3011         4482 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         7577 $attr->{geometric_longitude} = $lambda;
295             }
296              
297             # Nutation
298 3011         10999 my ( $delta_psi, $delta_eps ) = $self->nutation( $time );
299 3011         14852 $lambda += $delta_psi;
300              
301 3011         4502 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         9208 my $epsilon = $self->obliquity( $time );
315              
316             # Meeus 25.10
317              
318 3011 100       7687 unless ( $Rb ) { # The Sun.
319             # Aberration
320 13     13   3781 use constant SOLAR_ABERRATION_FACTOR => - deg2rad( 20.4898 / 3600 );
  13         26  
  13         74  
321 1292         2637 my $delta_lambda = SOLAR_ABERRATION_FACTOR / $Delta;
322 1292         2027 $lambda += $delta_lambda;
323              
324 1292         1963 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         4195 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         6050 my $sin_eps = sin $epsilon;
364 3011         5156 my $cos_eps = cos $epsilon;
365 3011         5214 my $sin_lam = sin $lambda;
366 3011         4520 my $sin_bet = sin $beta;
367 3011         4891 my $cos_bet = cos $beta;
368 3011         14039 my $alpha = mod2pi( atan2(
369             $sin_lam * $cos_eps - $sin_bet / $cos_bet * $sin_eps,
370             cos $lambda ) ); # Meeus 13.3
371 3011         18700 my $delta = asin( $sin_bet * $cos_eps + $cos_bet * $sin_eps *
372             $sin_lam ); # Meeus 13.4
373              
374 3011         13033 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         14964 $self->equatorial( $alpha, $delta, $Delta * AU );
387 3011         527719 $self->equinox_dynamical( $time );
388              
389 3011         18951 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         8462 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   588 my ( $self, $time ) = @_;
413 246 100       630 defined $time
414             or $time = $self->universal();
415 246   66     751 $earth ||= Astro::Coord::ECI->new()->eci( 0, 0, 0 );
416 246         977 $self->universal( $time );
417 246         3061 my $sun = $self->get( 'sun' )->universal( $time );
418 246         2958 $earth->universal( $time );
419 246         8346 my $lon = $self->__longitude_from_sun();
420 246         1494 my $angle = $earth->angle( $self, $sun );
421 246 100       84775 return $lon < 0 ? -$angle : $angle;
422             }
423             }
424              
425             sub geometric_longitude {
426 1     1 1 14 my ( $self ) = @_;
427 1         3 my $attr = $self->__get_attr();
428             defined $attr->{geometric_longitude}
429 1 50       5 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   2907 my ( $self, $time, $offset ) = @_;
435 1248   100     4297 $offset ||= 0;
436              
437 1248 100       2912 if ( defined $time ) {
438 1002         2793 $self->universal( $time );
439             } else {
440 246         555 $time = $self->universal();
441             }
442              
443 1248         17483 my $sun = $self->get( 'sun' )->universal( $time );
444              
445 1248         16476 my ( undef, $lon_b ) = $self->ecliptic();
446 1248         85127 my ( undef, $lon_s ) = $sun->ecliptic();
447              
448 1248         68096 return mod2pi( $lon_b - $lon_s - $offset + PI ) - PI;
449             }
450              
451 0         0 BEGIN {
452 13     13   25501 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         20730 my @memoize_result;
605              
606             sub nutation {
607 8281     8281 1 17050 my ( $self, $time, $cutoff ) = @_;
608              
609 8281 50       17947 defined $time
610             or $time = $self->dynamical();
611              
612 8281   50     33274 $cutoff ||= 0; # milli arc seconds. Meeus is 3
613              
614             {
615 8281 100       11553 ( my $memo = "$time $cutoff" ) eq $memoize_args
  8281         66548  
616             and return @memoize_result;
617 1943         4058 $memoize_args = $memo;
618             }
619              
620 1943         5508 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         13156 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         4972 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         4174 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         4374 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         4110 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         4894 my @arg = map { deg2rad( $_ ) } ( $M_prime, $M, $F, $D, $Omega );
  9715         31810  
647              
648 1943         10775 my ( $delta_psi, $delta_eps ) = ( 0, 0 );
649 1943         4534 foreach my $row ( @model ) {
650 205958         379675 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     537693 if ( $row->[5] && $cutoff <= abs $row->[5] ) {
654 205958         339675 $delta_psi += ( $row->[6] * $T + $row->[5] ) * sin $a;
655             }
656 205958 100 66     488645 if ( $row->[7] && $cutoff <= abs $row->[7] ) {
657 124352         227368 $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         4355 map { deg2rad( $_ / 3600_0000 ) } $delta_psi, $delta_eps );
  3886         16266  
666             }
667              
668             }
669              
670             sub __get_attr {
671 15180     15180   24604 my ( $self ) = @_;
672 15180 50       34227 my $ref = ref $self
673             or confess 'Can not call as static method';
674 15180   100     44936 return $self->{$ref} ||= {
675             model_cutoff_definition => dclone( $self->__model_definition(
676             'default_model_cutoff' ) ),
677             };
678             }
679              
680             sub __default {
681 27     27   80 my ( $class, $arg ) = @_;
682              
683 27         123 my $name = $class->__model_definition( 'body' );
684             defined $arg->{id}
685 27 50       249 or $arg->{id} = $name;
686             defined $arg->{name}
687 27 50       113 or $arg->{name} = $name;
688              
689             defined $arg->{diameter}
690 27 50       241 or $arg->{diameter} = $class->__model_definition( 'diameter' );
691              
692             defined $arg->{model_cutoff}
693 27 100       184 or $arg->{model_cutoff} = 'Meeus';
694              
695             defined $arg->{nutation_cutoff}
696 27 50       92 or $arg->{nutation_cutoff} = 3;
697              
698 27         139 return;
699             }
700              
701             sub __mutate_model_cutoff {
702 28     28   99 my ( $self, $name, $val ) = @_;
703 28 50       92 defined $val
704             or croak "model cutoff must be defined";
705 28 50       164 $self->model_cutoff_definition( $val )
706             or croak "model cutoff '$val' is unknown";
707 28         116 $self->__get_attr()->{$name} = $val;
708 28         113 return $self;
709             }
710              
711             sub model_cutoff_definition {
712 6066     6066 1 13499 my ( $self, $name, @arg ) = @_;
713 6066 100       16741 defined $name
714             or $name = $self->get( 'model_cutoff' );
715 6066         13325 my $attr = $self->__get_attr();
716 6066 100       14062 if ( @arg ) {
717 6 100       20 if ( defined( my $val = $arg[0] ) ) {
718 4 100       16 unless ( ref $val ) {
719 2 50 33     63 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         6 foreach my $series ( @model ) {
727 34         48 my $count = 0;
728 34         48 foreach my $term ( @{ $series->{terms} } ) {
  34         51  
729 308 100       639 last if $term->[0] < $num;
730 274         346 $count++;
731             }
732             $count
733 34 100       76 and $cutoff{$series->{series}} = $count;
734             }
735 2         9 return \%cutoff;
736 2         16 };
737             }
738 4 100       19 if ( CODE_REF eq ref $val ) {
739             $val = $val->(
740 9         19 map { @{ $_ } } @{
  9         34  
741 3         9 $self->__model_definition( 'model' ) }
  3         13  
742             );
743 3         1025 $val->{name} = $name;
744             }
745 4 50       21 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         21 'default_model_cutoff')->{none};
749 4         30 foreach my $name ( keys %{ $val } ) {
  4         36  
750 44 100       84 'name' eq $name
751             and next;
752 40 50       75 exists $terms->{$name}
753             or croak "Series '$name' not in this model";
754 40 50       139 $val->{$name} > $terms->{$name}
755             and croak "Series '$name' has only $terms->{$name} terms";
756             }
757 4         21 $attr->{model_cutoff_definition}{$name} = $val;
758             } else {
759 2 50       10 $self->__model_definition( 'default_model_cutoff' )->{$name}
760             and croak "You may not delete model cutoff definition '$name'";
761 2         19 delete $attr->{model_cutoff_definition}{$name};
762             }
763 6         33 return $self;
764             } else {
765 6060         18194 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   281233 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         10476 ( $arg{model_cutoff_definition}{name} || '' ) :
786             '' ),
787             ;
788              
789 6834         17921 my $jm = jcent2000( $time ) / 10; # Meeus 32.1
790 6834         38913 my @p_vec;
791             my @v_vec;
792 6834         11287 foreach my $coord ( @{ $self->__model_definition( 'model' ) } ) {
  6834         26357  
793 20502         45500 my $dT = my $exponent = 0;
794 20502         29050 my $T = 1;
795 20502         29652 my $pos = my $vel = 0;
796 20502         28462 foreach my $series ( @{ $coord } ) {
  20502         36893  
797             my $limit = $arg{model_cutoff_definition} ?
798             ( $arg{model_cutoff_definition}{$series->{series}} || 0 ) :
799 118569 100 100     384387 @{ $series->{terms} }
  560 100       1578  
800             or next;
801 100172         130223 --$limit;
802 100172         164419 foreach my $inx ( 0 .. $limit ) {
803 1828592         2577590 my $term = $series->{terms}[$inx];
804 1828592         2715088 my $u = $term->[1] + $term->[2] * $jm;
805 1828592         2553702 my $cos_u = cos $u;
806 1828592         2554375 $pos += $term->[0] * $cos_u * $T;
807 1828592         2469931 my $sin_u = sin $u;
808 1828592         3471996 $vel += $dT * $exponent * $term->[0] * $cos_u -
809             $T * $term->[0] * $term->[2] * $sin_u;
810             }
811 100172         133763 $dT = $T;
812 100172         128971 $exponent++;
813 100172         150252 $T *= $jm;
814             }
815 20502         34981 $vel /= DAYS_PER_JULIAN_MILENNIUM; # units/millennium -> units/day
816 20502         37635 push @p_vec, $pos;
817 20502         37239 push @v_vec, $vel;
818             }
819 6834         3846698 $p_vec[0] = mod2pi( $p_vec[0] );
820 6834         86901 return ( @p_vec, @v_vec );
821             }
822              
823             sub __mutate_nutation_cutoff {
824 27     27   99 my ( $self, undef, $val ) = @_;
825 27 50       93 defined $val
826             or croak 'Nutation cutoff must be defined';
827 27 50 33     210 looks_like_number( $val )
828             and $val >= 0
829             or croak 'Nutation cutoff must be a non-negative number';
830 27         101 $self->__get_attr()->{nutation_cutoff} = $val;
831 27         137 return $self;
832             }
833              
834             sub obliquity {
835 5270     5270 1 154609 my ( $self, $time ) = @_;
836              
837 5270 100       14082 defined $time
838             or $time = $self->dynamical();
839             # Obliquity per Meeus 22.3
840 5270         27768 my $U = jcent2000( $time ) / 100;
841 5270         29730 my $epsilon_0 = 0;
842 5270         24063 $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         13102 $epsilon_0 = deg2rad( $epsilon_0 / 3600 );
845 5270         24405 my ( undef, $delta_eps ) = $self->nutation( $time );
846 5270         10855 my $epsilon = $epsilon_0 + $delta_eps;
847              
848 5270         7749 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         10522 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 2503 my ( $self ) = @_;
885 24         108 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 97 my ( $self ) = @_;
893 35         139 return( 1 / abs( 1 / $self->get( 'sun' )->year() - 1 / $self->year() ) );
894             }
895              
896             sub year {
897 70     70 1 243 my ( $self ) = @_;
898 70         309 return $self->__model_definition( 'tropical_period' );
899             }
900              
901             1;
902              
903             __END__