Astro:...rd::ECI(3pm)

๐Ÿ“ฆ version 0.134, in alpine 3.24

โœจ Astro::Coord::ECI(3pm) โœจ

okay so this is straight out of the User Contributed Perl Documentation, section 3pm, and bestie, it's a whole vibe. it's giving "space math but make it Perl." buckle up. ๐Ÿš€๐Ÿ’ซ

๐ŸŒธ NAME ๐ŸŒธ

Astro::Coord::ECI โ€” literally the whole point of this module is manipulating geocentric coordinates. that's it, that's the name. ๐Ÿ’…

๐ŸŒˆ SYNOPSIS ๐ŸŒˆ

use Astro::Coord::ECI;
use Astro::Coord::ECI::Sun;
use Astro::Coord::ECI::TLE;
use Astro::Coord::ECI::Utils qw{rad2deg};

# 1600 Pennsylvania Avenue, in radians, radians, and KM
my ($lat, $lon, $elev) = (0.678911227503559,
    -1.34456123391096, 0.01668);

# Record the time
my $time = time ();

# Set up observer's location
my $loc = Astro::Coord::ECI->geodetic ($lat, $lon, $elev);

# Uncomment to turn off atmospheric refraction if desired.
# $loc->set( refraction => 0 );

# Instantiate the Sun.
my $sun = Astro::Coord::ECI::Sun->universal ($time);

# Figure out if the Sun is up at the observer's location.
my ($azimuth, $elevation, $range) = $loc->azel ($sun);
print "The Sun is ", rad2deg ($elevation),
    " degrees above the horizon.\n";

and if you want an example that's actually about satellite pass prediction, go check the Astro::Coord::ECI::TLE documentation instead. it's right there, no cap. ๐Ÿ›ฐ๏ธ


๐Ÿฆ‹ DESCRIPTION ๐Ÿฆ‹

so basically, this module was written to be a base class for a whole system that predicts when satellites are visible. its main character energy task? converting the Earth-Centered Inertial (ECI) coordinates that the NORAD models spit out into other coordinate systems โ€” like latitude, longitude, and altitude above mean sea level. and then, as happens, a bunch of other functions just kinda... accreted onto it over time. plus a few support routines got exposed for testing, or whatever. ๐Ÿ™ˆ

โš ๏ธ big facts before you go anywhere: all distances are in kilometers, and all angles are in radians โ€” yes, including right ascension, which is normally measured in hours. don't come for me, that's just how it is.

times are normal Perl times, whether you're using them as universal or dynamical time. universal time is what's usually meant, unless the docs say otherwise.

the known subclasses include Astro::Coord::ECI::Moon for predicting where the Moon is at, Astro::Coord::ECI::Star for predicting where a star is at (or literally anything else you can consider fixed on the celestial sphere), Astro::Coord::ECI::Sun for predicting where the Sun is at, Astro::Coord::ECI::TLE for predicting where a satellite is at given the NORAD orbital parameters, and Astro::Coord::ECI::TLE::Iridium โ€” which is a subclass of Astro::Coord::ECI::TLE, and as of 0.099_01 got moved to its own package โ€” for predicting Iridium flares. iconic lineup tbh. ๐Ÿ‘‘

okay so real talk: in version 0.022_01 the velocity code got a substantial rework, and it's still in progress. the author is attempting to give useful velocities where they're available, and no velocities at all where they're not. unfortunately no worked problems have been located yet, so the velocity code is โ€” in the most literal sense โ€” untested. ๐Ÿ’€ like, genuinely untested. i'm not being dramatic.

in general, locations specified in Earth-fixed coordinates are considered to share the rotational velocity of the Earth, and locations specified in inertial coordinates are considered to have undefined velocities โ€” unless a velocity was specified explicitly or produced by the object's model. this involved a change in the behavior of the eci() method when velocity was not specified but location was. see the next paragraph for the excuse. ๐Ÿ˜ญ

๐Ÿซถ Caveat user: this class and its subclasses should probably be considered alpha code, meaning the public interface may not be completely stable. the author will try not to change it, but given sufficient reason, it will happen. and if it does, the change will be drawn attention to in the documentation. so like, read the docs, bestie.

๐ŸŽ€ Methods ๐ŸŽ€

the following methods should be considered public. (yes, all of them. go off.)

$coord = Astro::Coord::ECI->new ();

this one instantiates a coordinate object. any arguments you pass get handed straight over to the set() method once the object exists. simple, clean, slay.

$angle = $coord->angle ($coord2, $coord3);

this calculates the angle between the $coord2 and $coord3 objects, as seen from $coord. the calculation uses the law of haversines, and it does not take atmospheric refraction into account. you get back a number of radians between 0 and pi. ๐Ÿงฎ

the algorithm comes from "Ask Dr. Math" on the Math Forum, "https://www.nctm.org/tmf/library/drmath/view/51879.html", which attributes it to the Geographic Information Systems FAQ, <http://www.faqs.org/faqs/geography/infosystems-faq/>, which in turn attributes it to R. W. Sinnott, "Virtues of the Haversine," Sky and Telescope, volume 68, number 2, 1984, page 159. the academic citation chain is giving very thorough. ๐Ÿ“š

unfortunately, as of early 2021 the National Council of Teachers of Mathematics restricted the Dr. Math content to their members only ๐Ÿ˜ฉ โ€” but an annotated and expanded version of the article on haversines is available at <https://www.themathdoctors.org/distances-on-earth-2-the-haversine-formula/>. and if you want the original article, you can feed the URL "http://mathforum.org/library/drmath/view/51879.html" to the Wayback Machine. history preserved, we love to see it. ๐Ÿซถ

prior to version 0.011_03 the law of cosines was used, but that produced errors on extremely small angles. the haversine law was selected based on Jean Meeus, "Astronomical Algorithms", 2nd edition, chapter 17 "Angular Separation". glow-up confirmed. โœจ

one thing: this method ignores the "station" attribute. just so you know.

$angle = $coord->angular_radius();

computes the angular radius of the $coord body in radians, as of the currently-set time. if the "station" attribute is set, the range gets computed from it via "$self->azel()". if it's not set, the range from the center of the earth gets computed via "$self->ecliptic()". and if the "diameter" attribute isn't set? you get "undef" back. no diameter, no radius, that's the rule. ๐ŸŽฏ

๐Ÿšซ this method is not supported for terrestrial objects. sorry earthlings.

$which = $coord->attribute ($name);

returns the name of the class that implements the named attribute. or undef if that attribute name isn't valid. a little "who's your daddy" but for classes. ๐Ÿ’…

($azimuth, $elevation, $range) = $coord->azel( $coord2 );

takes another coordinate object and computes its azimuth, elevation, and range relative to the object doing the computing. the returns are:

  • azimuth in radians, measured clockwise from North โ€” and it's always positive โ˜๏ธ
  • elevation above the horizon in radians โ€” negative if it's below ๐ŸŒ…
  • range in kilometers ๐Ÿ“

as a side effect, the time of the $coord object may be set from the $coord2 object. sneaky but useful.

atmospheric refraction is taken into account using the correct_for_refraction() method โ€” but only if the $coord object's "refraction" attribute is true. and here's the detail that matters: the invocant's correct_for_refraction() method is the one used. so if $coord and $coord2 have different correct_for_refraction() methods, $coord's method wins. no debate. ๐Ÿฅ‡

โš ๏ธ heads up: the "correct_for_refraction" attribute defaults to true. for cases where both the invocant and the argument are ground-based objects, you should probably set the invocant's "correct_for_refraction" to false before invoking this method. save yourself the confusion.

this method is implemented in terms of azel_offset(). go read that method's documentation for the further details, iykyk.

( $azimuth, $elevation, $range ) = $coord->azel();

this one computes the azimuth, elevation, and range of the $coord object itself, as seen from the position stored in $coord's "station" attribute. and if the "station" attribute isn't set? an exception gets thrown. like, full stop, it will not do it. ๐Ÿ›‘

($azimuth, $elevation, $range) = $coord->azel_offset($coord2, $offset);

takes another coordinate object, and computes its azimuth, elevation, and range in reference to the object doing the computing. same deal as before:

  • azimuth in radians, measured clockwise from North โ€” always positive
  • elevation above the horizon in radians โ€” negative if below
  • range in kilometers

now, if you provide the optional $offset argument, the elevation gets offset upward by that given fraction of the radius of the $coord2 object. so, to spell it out:

  • $offset of 1 โ†’ the upper limb of the object ๐ŸŒ’
  • $offset of 0 โ†’ the center of the object ๐ŸŽฏ
  • $offset of -1 โ†’ the lower limb of the object ๐ŸŒ˜

and importantly: this offset is applied before atmospheric refraction. order matters, bestie.

as a side effect, the time of the $coord object may be set from the $coord2 object. ๐Ÿ•ฐ๏ธ

by default, atmospheric refraction is taken into account in the calculation of elevation, using the correct_for_refraction() method. this better represents the observed position in the sky when the object is above the horizon โ€” but it produces a gap in the data when the object passes below the horizon, since there are no refraction equations for rock. ๐Ÿ˜ญ literally. rock. see the correct_for_refraction() documentation for details.

again: the invocant's correct_for_refraction() method is the one used. if $coord and $coord2 have different correct_for_refraction() methods, $coord's method is used. periodt.

so if you want to ignore atmospheric refraction entirely โ€” and therefore not have a gap in your data โ€” set the "refraction" attribute of the $coord object to any value Perl considers false (e.g. 0). easy.

the algorithm for position is the author's own โ€” but he confesses to having to refer to T. S. Kelso's "Computers and Satellites" column in Satellite Times, November/December 1995, titled "Orbital Coordinate Systems, Part II" and available at http://celestrak.org/columns/v02n02/ to get the signs right. respect the honesty honestly. ๐Ÿ™

okay now the velocity part, which is the spicy bit. ๐ŸŒถ๏ธ if velocities are available for both bodies involved in the calculation, they will be returned after the position โ€” meaning you get a six-element array back instead of a three-element array. the velocity of a point specified in Earth-fixed coordinates (e.g. geodetic latitude, longitude, and altitude) is assumed to be the rotational velocity of the Earth at that point. the returned velocities are, in this exact order:

  1. azimuthal velocity in radians per second โ€” and note that this is not radians of azimuth, which get smaller as you go farther away from the Equator โš ๏ธ
  2. elevational velocity in radians per second
  3. velocity in recession in kilometers per second

and if velocities are available for both bodies and the $coord2 object has its "frequency" attribute set, the returned array will contain seven elements โ€” the seventh being the Doppler shift. ๐ŸŽถ

the algorithm for recessional velocity comes from John A. Magliacane's "Predict" program, available at <https://www.qsl.net/kd2bd/predict.html>. the transverse velocity computations are the author's own, but they use the same basic approach: the vector dot product of the velocity vector with a unit vector in the appropriate direction โ€” that unit vector being generated by the appropriate (i hope!) vector cross products. the "(i hope!)" is doing a lot of heavy lifting there and i respect it. ๐Ÿ˜…

velocity conversions to azel() appear to the author to be mostly sane. see A NOTE ON VELOCITIES, below, for details. ๐Ÿ“

If velocities are available and you've handed over a non-zero value for the frequency attribute, you get the Doppler shift back as the seventh element of the returned array โœจ The caveats about velocity in recession apply to the Doppler shift too โ€” same energy, same fine print. The frequency comes from the $coord2 object. Getting the frequency from the $coord object used to be supported as a fallback, but now it just throws an exception, soโ€ฆ don't ๐Ÿ’€

  • ( $azimuth, $elevation, $range ) = $coord->azel_offset( $offset ); โ€” this one computes the azimuth, elevation, and range of the $coord object as seen from the location stored in the $coord object's station attribute ๐ŸŒธ The functionality is the same as above, except for the whole invocant situation: in the earlier version the station is the invocant, and in this version the orbiting body is the invocant. Flip it and reverse it, periodt ๐Ÿ’… This version also returns velocities when they're available for both bodies, plus the Doppler shift if in addition the frequency attribute of the invocant is set.
  • $coord2 = $coord->clone (); โ€” does a deep clone of an object, producing a different but identical object ๐Ÿซถ At the moment it's really just a wrapper for Clone::clone. Lowkey iconic, no notes.
  • $elevation = $coord->correct_for_refraction ($elevation); โ€” corrects the given angular elevation for atmospheric refraction ๐ŸŒˆ This only happens if the elevation has a reasonable chance of actually being visible; that is, if the elevation before correction is not more than two degrees below either the 'horizon' attribute or zero, whichever is lesser. Sorry for the discontinuity this introduces ๐Ÿ˜ญ but the author did not wish to carry the refraction calculation too low, because the behavior of the algorithm is uncertain, and there is no corresponding algorithm for refraction through magma ๐Ÿ”ฅ (fr, that's what it says).

This method can also be called as a class method, in which case the correction is applied only if the uncorrected elevation is greater than minus two degrees. It's really only exposed for testing purposes โ€” hence the cumbersome name, iykyk. The azel() method calls it for you if the refraction attribute is true ๐Ÿ’–

The algorithm for atmospheric refraction comes from Thorfinn Saemundsson's article in "Sky and Telescope", volume 72, page 70 (July 1986), as reported in Jean Meeus' "Astronomical Algorithms", 2nd Edition, chapter 16, page 106, and it includes the adjustment suggested by Meeus โœจ

  • $angle = $coord->dip (); โ€” calculates the dip angle of the horizon due to the altitude of the body, in radians ๐ŸŒธ It'll be negative for a location above the surface of the reference ellipsoid, and positive for a location below the surface. The algorithm is simple enough to be the author's own โ€” humble king ๐Ÿ‘‘
  • $coord = $coord->dynamical ($time); โ€” sets the dynamical time represented by the object โญ This method can also be called as a class method, in which case it instantiates the desired object. The algorithm for converting this to universal time comes from Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 10, pages 78ff.
  • $time = $coord->dynamical (); โ€” returns the dynamical time previously set, or the universal time previously set, converted to dynamical ๐ŸŒ™ The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 10, pages 78ff.
  • $coord = $coord->ecef($x, $y, $z, $xdot, $ydot, $zdot) โ€” sets the coordinates represented by the object in terms of "Earth-Centered, Earth-fixed (ECEF) coordinates" in kilometers, with the x axis being latitude 0 longitude 0, the y axis being latitude 0 longitude 90 degrees east, and the z axis being latitude 90 degrees north ๐ŸŒ The velocities in kilometers per second are optional, and default to zero. ECEF velocities are considered to be relative to the surface of the Earth; when converting to ECI, the rotational velocity of the Earth gets added in ๐ŸŒ€
Heads up bestie: prior to version 0.022_01, the documentation said the default velocity was the rotational velocity of the Earth. That was not correct in ECEF coordinates. The functionality of the code itself in this respect did not change ๐Ÿ™ˆ

The object itself is returned. This method can also be called as a class method, in which case it instantiates the desired object โœจ

  • ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->ecef(); โ€” returns the object's "Earth-Centered, Earth-fixed (ECEF) coordinates" ๐Ÿ’ซ

If the original coordinate setting was in an inertial system (e.g. eci, equatorial, or ecliptic) and the absolute difference between the current value of 'equinox_dynamical' and the current dynamical() setting is greater than the value of $Astro::Coord::ECI::EQUINOX_TOLERANCE, the coordinates will be precessed to the current dynamical time before conversion. Yes, this should be done any time the equinox is not the current time, but for satellite prediction precession by a year or so doesn't seem to make much difference in practice โ€” ngl, it's giving "close enough" ๐ŸŽ€ The default value of $Astro::Coord:ECI::EQUINOX_TOLERANCE is 365 days. Note that if this behavior or the default value of $EQUINOX_TOLERANCE begins to look like a bug, it will be changed, and noted in the documentation ๐Ÿ‘€

Some velocity conversions involving ecef() appear to be mostly sane. See A NOTE ON VELOCITIES, below, for details.

  • $coord = $coord->eci ($x, $y, $z, $xdot, $ydot, $zdot, $time) โ€” sets the coordinates represented by the object in terms of "Earth-Centered Inertial (ECI) coordinates" in kilometers, time being universal time, the x axis being 0 hours "Right Ascension" 0 degrees "Declination", y being 6 hours "Right Ascension" 0 degrees "Declination", and z being 90 degrees north "Declination" ๐ŸŒŒ The velocities in kilometers per second are optional. If omitted, the object will be considered not to have a velocity โ€” that's a change introduced with version 0.022_01. Before that, the velocity defaulted to 0 ๐Ÿ•ฐ๏ธ

The time argument is optional if the time represented by the object has already been set (e.g. by the universal() or dynamical() methods). The object itself is returned.

This method can also be called as a class method, in which case it instantiates the desired object. In this case the time is not optional ๐Ÿšจ

The algorithm for converting from ECI to geocentric coordinates and back is based on the description of ECI coordinates in T. S. Kelso's "Computers and Satellites" column in "Satellite Times", September/October 1995, titled "Orbital Coordinate Systems, Part I" and available at http://celestrak.org/columns/v02n01/ ๐Ÿ“ก

  • ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->eci($time); โ€” returns the "Earth-Centered Inertial (ECI) coordinates" of the object at the given time ๐ŸŒ  The time argument is actually optional if the time represented by the object has already been set.

If you specify a time, the time represented by the object will be set to that time. The net effect of specifying a time is equivalent to:

($x, $y, $z, $xdot, $ydot, $zdot) = $coord->universal($time)->eci()

If the original coordinate setting was in a non-inertial system (e.g. ECEF or geodetic), the equinox_dynamical attribute will be set to the object's dynamical time ๐Ÿ•

Velocities will be returned if they were originally provided โ€” change introduced in version 0.022_01. Prior to that version, velocities were always returned ๐Ÿ‘‘

Some velocity conversions involving eci() appear to be mostly sane. See A NOTE ON VELOCITIES, below, for details.

  • $coord = $coord->ecliptic_cartesian( $X, $Y, $Z, $time ); โ€” sets the Cartesian "Ecliptic" coordinates represented by the object in terms of "X" (kilometers toward the vernal equinox), "Y" (90 degrees along the ecliptic from "X"), and "Z" (toward ecliptic North) ๐ŸŒธ The time is universal time. The object itself is returned.

The time argument is optional if the time represented by the object has already been set (e.g. by the universal() or dynamical() methods).

This method can also be called as a class method, in which case it instantiates the desired object. In this case the time is not optional โœจ

Caveat: you can pass in velocities (before the $time) but they are unsupported, meaning the author can't at this point say whether they will be transformed sanely, much less correctly. Caveat programmer ๐Ÿ’€
  • ( $X, $Y, $Z) = $coord->ecliptic_cartesian( $time ); โ€” returns the Cartesian ecliptic coordinates of the object at the given time ๐ŸŒ™ The time is optional if the time represented by the object has already been set (e.g. by the universal() or dynamical() methods).
Caveat: velocities are unsupported by this method. That means you may (or may not, depending on the coordinates originally set) get them back, but it has not been looked into whether they're actually correct ๐Ÿ™ˆ
  • $coord = $coord->ecliptic ($latitude, $longitude, $range, $time); โ€” sets the "Ecliptic" coordinates represented by the object in terms of "Ecliptic latitude" and "Ecliptic longitude" in radians, and the range to the object in kilometers, time being universal time ๐ŸŒˆ The object itself is returned.

The time argument is optional if the time represented by the object has already been set (e.g. by the universal() or dynamical() methods).

The latitude should be a number between -PI/2 and PI/2 radians inclusive. The longitude should be a number between -2*PI and 2*PI radians inclusive. The increased range (one would expect -PI to PI) is because in some astronomical usages latitudes outside the range + or - 180 degrees are employed ๐ŸŒ A warning will be generated if either of these range checks fails โš ๏ธ

This method can also be called as a class method, in which case it instantiates the desired object. In this case the time is not optional ๐Ÿ’–

The algorithm for converting from ecliptic latitude and longitude to right ascension and declination comes from Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 13, page 93 ๐Ÿ“–

  • ($latitude, $longitude, $range) = $coord->ecliptic ($time); โ€” returns the ecliptic latitude and longitude of the object at the given time ๐ŸŒ™ The time is optional if the time represented by the object has already been set (e.g. by the universal() or dynamical() methods).
Caveat: velocities are not returned by this method. Sorry bestie ๐Ÿ˜ญ

โœจ $longitude = $coord->ecliptic_longitude();

Okay so this one is a literal gift ๐ŸŽ it returns the ecliptic longitude of the body at its current time setting, in radians. It's really just a convenience method โ€” it's equivalent to ( $coord->ecliptic() )[1], and fr fr that is exactly how it's implemented. No tricks, no hidden math, just vibes and a shortcut. ๐Ÿ’…


โฐ $seconds = $self->equation_of_time( $time );

This returns the equation of time at the dynamical time you pass in. If the time is undef, it just uses the invocant's own dynamical time โ€” lowkey thoughtful of it. ๐Ÿซถ

The algorithm comes from W. S. Smart's Text-Book on Spherical Astronomy, as reported in Jean Meeus' Astronomical Algorithms, 2nd Edition, Chapter 28, page 185. Respect the sources, bestie. ๐Ÿ‘‘


๐ŸŒ $coord->equatorial ($rightasc, $declin, $range, $time);

Setting time!! This sets the Equatorial coordinates that the object represents (relative to the center of the Earth) in terms of Right Ascension and Declination in radians, plus the range to the object in kilometers โ€” with time being universal time. The object itself gets returned. ๐ŸŽ€

Heads up ๐Ÿšจ if equatorial() is called in this way, the station attribute gets ignored, for historical reasons. Not a bug, just old lore. iykyk.

Your right ascension needs to be a number between 0 and 2*PI radians inclusive. Your declination needs to be between -PI/2 and PI/2 radians inclusive. A warning gets generated if either range check fails. โš ๏ธ

The time argument is optional if the time represented by the object has already been set (like, by the universal() or dynamical() methods).

You can also call this as a class method, in which case it instantiates the object you want. In that case the time is not optional. ๐Ÿ˜ค

And omg you may optionally pass velocity info on top of the position info. If you do, the velocity in right ascension (in radians per second), declination (ditto), and range (in kilometers per second in recession) get passed after the position arguments, and before the $time argument if there is one. Order matters, don't fumble it. ๐Ÿ’ซ


๐Ÿฆ‹ ( $rightasc, $declin, $range ) = $coord->equatorial( $time );

Getter era. This returns the Equatorial coordinates of the object relative to the center of the Earth. The station attribute is ignored here too. The time argument is optional if the time represented by the object has already been set (e.g. by the universal() or dynamical() methods). ๐Ÿ•ฐ๏ธ

If velocities are available from the object (i.e. if it's an instance of Astro::Coord::ECI::TLE) then the return includes velocity in right ascension, declination, and range โ€” the first two in radians per second, and the third in kilometers per second in recession. โœจ

Caveat from the author ๐Ÿ’€ these velocities are believed by him to be sane if they're derived from ECI coordinates. This method does not support setting velocities. See A NOTE ON VELOCITIES, below, for details.

๐Ÿ’œ ($rightasc, $declin, $range) = $coord->equatorial( $coord2 );

This form is retained (for the moment) for historical reasons, but equatorial_apparent() is preferred. ๐Ÿ‘‘ Consider this your gentle nudge to upgrade.

It returns the apparent equatorial coordinates of the object represented by $coord2, as seen from the location represented by $coord.

Side effect alert ๐Ÿšจ the time of the $coord object may get set from the $coord2 object.

If the refraction attribute of the $coord object is true, the coordinates get corrected for atmospheric refraction using the correct_for_refraction() method. ๐ŸŒฌ๏ธ

If velocities are available from both objects (i.e. both are Astro::Coord::ECI::TLE objects) the return contains velocity in right ascension, declination, and range, the first two in radians per second and the third in kilometers per second in recession.

Caveat, and the author is very upfront about this ๐Ÿ™ˆ these velocities are believed by him not to be correct.

๐Ÿ”ญ ($ra, $decl, $rng) = $coord->equatorial_apparent( $sta );

This returns the apparent equatorial coordinates of the $coord object as seen from the object specified by the $sta argument โ€” or from the object sitting in the station attribute of $coord if you don't pass an argument. Either way, it's covered. ๐ŸŒˆ

It will return velocities if they're available, but the author has no idea whether they're correct. Honesty is iconic. ๐Ÿ’€


๐Ÿงš my ($rasc, $decl, $range, $v_rasc, $v_decl, $v_r) = $coord->equatorial_unreduced($body);

Okay, buckle up, this is the big one. ๐ŸŽข This computes the unreduced equatorial position of the second ECI object as seen from the first. If the second argument is undefined, it computes the equatorial position of the first object as seen from the center of the Earth. Unlike the equatorial() method itself, this method is an accessor only. It would probably not be exposed except for the anticipation of how useful $range and $v_r are in satellite conjunction computations, and the desirability of not taking the time to make the two atan2() calls that are unneeded in this application. Efficiency slay. โšก

The 'unreduced' in the name is meant to refer to the fact that $rasc and $decl are not the right ascension and declination themselves, but the arguments to atan2() needed to compute them โ€” and $v_rasc and $v_decl are in km/sec, rather than being divided by the range to get radians per second. Words matter, bestie. ๐ŸŽ€

This method ignores the station attribute.

The returned data are:

  • $rasc is an array reference, which can be converted to the right ascension in radians by mod2pi(atan2($rasc->[0], $rasc->[1])).
  • $decl is an array reference, which can be converted to the declination in radians by atan2($decl->[0], $decl->[1]).
  • $range is the range in kilometers.
  • $v_rasc is the velocity in the right ascensional direction in kilometers per second. It can be converted to radians per second by dividing by $range.
  • $v_decl is the velocity in the declinational direction in kilometers per second. It can be converted to radians per second by dividing by $range.
  • $v_r is the velocity in recession in kilometers per second. Negative values indicate that the objects are approaching. ๐Ÿ‘€

The velocities are only returned if they're available from the input objects.

Caveat again ๐Ÿซ  these velocities are believed by the author not to be correct.

๐Ÿ•ฐ๏ธ $coord = $coord->equinox_dynamical ($value);

This sets the value of the equinox_dynamical attribute and returns the modified object. If you call it without an argument, it returns the current value of the equinox_dynamical attribute. Cute little getter/setter combo. ๐ŸŽ€

Yes, it's equivalent to $coord->set (equinox_dynamical => $value) and $coord->get ('equinox_dynamical'). But there seems to be a significant performance penalty in the $self->SUPER::set () needed to get this attribute set from a subclass. It's possible more methods like this will be added, but the author does not plan to eliminate the set() interface. No cap, the old way stays. ๐Ÿ’…


๐ŸŒ $coord = $coord->geocentric($psiprime, $lambda, $rho);

This sets the Geocentric coordinates represented by the object in terms of Geocentric latitude psiprime and Longitude lambda in radians, and distance from the center of the Earth rho in kilometers. ๐ŸŒ

Latitude should be a number between -PI/2 and PI/2 radians inclusive. Longitude should be a number between -2*PI and 2*PI radians inclusive. The increased range (like, you'd expect -PI to PI) is because in some astronomical usages latitudes outside the range plus or minus 180 degrees are employed. A warning gets generated if either of these range checks fails. โš ๏ธ

This can also be called as a class method, in which case it instantiates the desired object.

Real talk ๐Ÿšจ this method should not be used with map coordinates, because map latitude is Geodetic latitude, measured in terms of the tangent of the reference ellipsoid, whereas geocentric coordinates are, essentially, spherical coordinates. Different creatures entirely.

The algorithm for conversion between geocentric and ECEF is the author's own. ๐Ÿ‘‘


๐Ÿฆ‹ ($psiprime, $lambda, $rho) = $coord->geocentric();

The getter! Returns the Geocentric latitude, Longitude, and distance to the center of the Earth. That's the whole thing, she's simple and she knows it. ๐Ÿ’–


๐Ÿ—บ๏ธ $coord = $coord->geodetic($psi, $lambda, $h, $ellipsoid);

This sets the Geodetic coordinates represented by the object in terms of its Geodetic latitude psi and Longitude lambda in radians, and its height h above mean sea level in kilometers. ๐Ÿ”๏ธ

Latitude should be a number between -PI/2 and PI/2 radians inclusive. Longitude should be a number between -2*PI and 2*PI radians inclusive. The increased range (again, you'd expect -PI to PI) is because in some astronomical usages latitudes outside the range plus or minus 180 degrees are employed. A warning gets generated if either range check fails. โš ๏ธ

The ellipsoid argument is the name of a Reference Ellipsoid known to the class, and it's optional. If you pass it, it sets the ellipsoid used for calculations with this object. If you don't pass it, the default ellipsoid is used. ๐ŸŽ€

This can also be called as a class method, in which case it instantiates the desired object.

The conversion from geodetic to geocentric comes from Jean Meeus' Astronomical Algorithms, 2nd Edition, Chapter 11, page 82. ๐Ÿ“š

And yes โ€” this is the method that should be used with map coordinates. Save yourself the heartbreak.

๐ŸŒธ ($psi, $lambda, $h) = $coord->geodetic($ellipsoid);

Getter mode activated โœจ returns the geodetic latitude, longitude, and height above mean sea level.

The ellipsoid argument is the name of a Reference Ellipsoid known to the class, and it's optional. If not specified, the most-recently-set ellipsoid gets used. ๐Ÿซถ

The conversion from geocentric to geodetic comes from Kazimierz Borkowski's Accurate Algorithms to Transform Geocentric to Geodetic Coordinates, at http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm. This is best viewed with Internet Explorer because of its use of Microsoft's Symbol font. ๐Ÿ’€๐Ÿ˜ญ yes, really, that's the note.


๐ŸŽ€ $value = $coord->get ($attrib);

Returns the named attributes of the object. If you call it in list context, you can give more than one attribute name and it returns all their values. Bulk access, we love to see it. ๐Ÿ’…

If called as a class method, it returns the current default values.

See "Attributes" for a list of the attributes you can get. ๐Ÿ‘‘


๐ŸŒˆ @coords = $coord->enu();

This reports the coordinates of $coord in the East-North-Up coordinate system, as seen from the position stored in the station attribute of $coord. ๐Ÿงญ

Velocity conversions to enu() appear to the author to be mostly sane. See A NOTE ON VELOCITIES, below, for details. โœจ

@coords = $coord->neu()

okay so this one hands you the coordinates of $coord2 in the North-East-Up system, viewed from wherever the station attribute of $coord is parked ๐Ÿ›ฐ๏ธ Heads up though โ€” this is a left-handed coordinate system, which is giving chaotic but we respect it.

Velocity conversions to neu() appear to me to be mostly sane. see A NOTE ON VELOCITIES, below, for the receipts โœจ


@coords = $coord->heliocentric_ecliptic_cartesian()

gives you back the Heliocentric ecliptic Cartesian position of the object ๐Ÿ’ซ you can optionally pass time as an argument, and that's exactly the same thing as:

@coords = $coord->universal( $time )
    ->heliocentric_ecliptic_cartesian();

ngl, at this time this object is not able to derive Heliocentric ecliptic Cartesian coordinates from other coordinates (say, ECI) ๐Ÿ˜ญ so don't come for her.


$coord->heliocentric_ecliptic_cartesian( $X, $Y, $Z )

this is the setter era ๐Ÿ’… it sets the object's position in Heliocentric ecliptic Cartesian coordinates. velocities may not be set at the moment, though this is planned. you may also pass an optional time as the last argument.

the invocant gets returned โ€” call chaining stays winning ๐Ÿซถ


@coords = $coord->heliocentric_ecliptic()

returns the Heliocentric ecliptic coordinates of the object. simple, iconic, no notes ๐ŸŒธ


$coord = $coord->local_mean_time ($time);

sets the local mean time of the object โฐ and listen โ€” this is not local standard time, it's the universal time plus the longitude of the object expressed in seconds. another definition is mean solar time plus 12 hours (since the solar day begins at noon ๐Ÿ’€ noon start is so unserious).

you will get an exception of some sort if the position of the object has not been set, or if the object represents inertial coordinates, or on any subclass whose position depends on the time. so basically: set it up properly first bestie ๐Ÿ™ˆ


$time = $coord->local_mean_time ()

returns the local mean time of the object. it will raise an exception if the time has not been set โ€” no time, no service ๐Ÿ˜ค

note that this returns the actual local time, not the GMT equivalent. which means when you're formatting for output, you call:

strftime $format, gmtime $coord->local_mean_time ();

$time = $coord->local_time ();

returns the local time (defined as solar time plus 12 hours) of the given object ๐ŸŒž there is no way to set the local time, it's a read-only queen.

this is really just a convenience routine โ€” the calculation is simply the local mean time plus the equation of time at the given time. easy math, slay.

note that this returns the actual local time, not the GMT equivalent. so again, for output formatting:

strftime $format, gmtime $coord->local_time ();

( $maidenhead_loc, $height ) = $coord->maidenhead( $precision );

returns the location of the object in the Maidenhead Locator System ๐Ÿ—บ๏ธโœจ height above the reference ellipsoid is not part of the system, but is returned anyway, in kilometers. the $precision is optional, and is an integer greater than 0.

default precision is 4, but you can change that by setting $Astro::Coord::ECI::DEFAULT_MAIDENHEAD_PRECISION to whatever you want. for example, if you want the default precision to be 3 (which it probably should have been in the first place ๐Ÿ‘€), you can use:

no warnings qw{ once };
local $Astro::Coord::ECI::DEFAULT_MAIDENHEAD_PRECISION = 3;
note that precisions greater than 4 are not defined by the standard. this method extends the system by alternating letters (base 24) with digits (base 10), but that is unsupported since the results will change, possibly without notice, if the standard is extended in a manner incompatible with this implementation. you have been warned ๐Ÿ’€

conversion of latitudes and longitudes to Maidenhead Grid is subject to truncation error, perhaps more so since latitude and longitude are specified in radians ๐Ÿ˜ญ an attempt has been made to minimize this by using Perl's stringification of numbers to ensure that something that looks like 42 is not handled as 41.999999999385. this probably amounts to shifting some grid squares very slightly to the north-west, but in practice it seems to give better results for points on the boundaries of the grid squares. so like, a little slay with a side of chaos ๐ŸŽ€


$coord->maidenhead( $maidenhead_loc, $height );

this is the setter, setting the geodetic location in the Maidenhead Locator System ๐Ÿ“ height above the reference ellipsoid is not part of the system, but is accepted anyway, in kilometers, defaulting to 0.

the actual location set is the center of the specified grid square. yes, center. don't fight it.

locations longer than 8 characters are not defined by the standard. this method extends precision by assuming alternate letters (base 24) and digits (base 10), but this will change, possibly without notice, if the standard is extended in a manner incompatible with this implementation.

the object itself is returned, to allow call chaining ๐Ÿ”—


$value = $coord->mean_angular_velocity();

returns the mean angular velocity of the body in radians per second ๐ŸŒ€ if the $coord object has a period() method, this method just returns two pi divided by the period. otherwise it returns the contents of the angularvelocity attribute. either way she's got you covered.


$time = $coord->next_azimuth( $body, $azimuth );

returns the next time the given $body passes the given $azimuth as seen from the given $coord, calculated to the nearest second โฑ๏ธ the start time is the current time setting of the $body object.


$time = $coord->next_azimuth( $azimuth );

same vibe, different setup: returns the next time the $coord object passes the given $azimuth as seen from the location in the $coord object's station attribute, calculated to the nearest second. the start time is the current time setting of the $coord object โœจ


($time, $rise) = $coord->next_elevation ($body, $elev, $upper)

okay this one's a whole thing, stay with me ๐Ÿซถ this method calculates the next time the given body passes above or below the given elevation (in radians) as seen from $coord. the $elev argument may be omitted (or passed as undef), and will default to 0. if the $upper argument is true, the calculation will be based on the upper limb of the body (as determined from its angulardiameter attribute); if false, the calculation will be based on the center of the body. the $upper argument defaults to true if the $elev argument is zero or positive, and false if the $elev argument is negative ๐ŸŒ—

in list context, it returns the time, and an indicator which is true if the body passes above the given elevation at that time, and false if it passes below the given elevation. in scalar context it just returns the time.

the algorithm is successive approximation, and assumes that the body will be at its highest at meridian passage ๐Ÿง  it also assumes that if the body hasn't passed the given elevation in 183 days it never will. in this case it returns undef in scalar context, or an empty list in list context. 183 days is such a specific number and honestly? iconic.


($time, $rise) = $coord->next_elevation( $elev, $upper );

this version calculates the next time the $coord object passes above or below the given elevation (in radians) as seen from the position found in the $coord object's station attribute ๐Ÿ›ฐ๏ธ the $elev argument may be omitted (or passed as undef), and will default to 0. if the $upper argument is true, the calculation will be based on the upper limb of the body (as determined from its angulardiameter attribute); if false, the calculation will be based on the center of the body. the $upper argument defaults to true if the $elev argument is zero or positive, and false if the $elev argument is negative.

in list context, it returns the time, and an indicator which is true if the body passes above the given elevation at that time, and false if it passes below the given elevation. in scalar context it just returns the time.

the algorithm is successive approximation, and assumes that the body will be at its highest at meridian passage. it also assumes that if the body hasn't passed the given elevation in 183 days it never will. in this case it returns undef in scalar context, or an empty list in list context ๐Ÿ’€ same energy, different station.


( $time, $type ) = $coord->next_elevation_extreme_tod( $body, $elev, $ipper );

this method should be considered experimental ๐Ÿงช handle with care bestie

this variation on next_elevation() returns the earliest (or latest) time the given body passes above (or below) the given elevation (in radians) as seen from the given coordinates. if called in list context it returns the time of the event and the type of the event, encoded as follows:

 0 - earliest pass below the given elevation
 1 - earliest pass above the given elevation
 2 - latest pass below the given elevation
 3 - latest pass above the given elevation

if called in scalar context this method just returns the time.

if no extreme can be computed, this method returns nothing. it will always return nothing unless otherwise documented for the class of the $body object ๐Ÿ‘‘

the motivation for this method was a desire to compute the earliest and latest sunrise and sunset at a given location ๐ŸŒ… it appears here in the object hierarchy out of a perhaps-foolish desire to be consistent with next_elevation(). we love a humble origin story.

use this method with caution. I have serious doubts about what will happen when the invocant is north of the Arctic Circle or south of the Antarctic Circle, and I am a little unsure of its behaviour near the Equator ๐Ÿ˜ญ in addition, in an early implementation consecutive calls returned the same extreme over and over. I believe this to be fixed, but the fix raises the possibility of skipped extrema. Caveat user. fr fr.

( $time, $type ) = $body->next_elevation_extreme_tod( $elev, $ipper );

this way to call next_elevation_extreme_tod() is equivalent to the above, but uses the body's station attribute for the $coord object ๐ŸŒˆ same method, one less argument, chef's kiss.


( $time, $above ) = $coord->next_meridian( $body, $want )

calculates the next meridian passage of the given $body over (or under) the location specified by the $coord object ๐ŸŒ the $body object must be a subclass of Astro::Coord::ECI โ€” no randoms allowed.

the optional $want argument should be specified as true (e.g. 1) if you want the next passage above the observer, or as false (e.g. 0) if you want the next passage below the observer. if this argument is omitted or undefined, you get whichever passage is next โ€” dealer's choice, spontaneous queen ๐Ÿ’…

the start time of the search is the current time setting of the $coord object โฐ

The returns are the meridian passage time plus an indicator that's true if that passage happens above the observer (so, local noon if the $body is the Sun ๐ŸŒž) or false if it's below (local midnight, same deal). Call it in scalar context and you just get the time, no indicator, no extra โœจ.

Both the $coord and $body objects get left sitting at the returned time โ€” they don't snap back, just so you know ๐Ÿ‘€.

The algorithm is by successive approximation. It will croak if the $body's period is close to synchronous ๐Ÿ’€, and it will probably not work well for bodies in highly eccentric orbits. Calculation is to the nearest second, and the time you get back is the first even second after the body crosses the meridian. Precision queen ๐Ÿ’….


( $time, $above ) = $coord->next_meridian( $want )

This one figures out the next meridian passage of the $coord object over โ€” or under โ€” wherever the $coord object's "station" attribute says it's at ๐Ÿ›ฐ๏ธ.

The optional $want argument should be true (like 1) if you want the next passage above the observer, or false (like 0) if you want the next passage below. Leave it out or leave it undef and you just get whichever passage comes next โ€” dealer's choice, no cap ๐ŸŽฒ.

The search starts from the current time setting of the $coord object.

The returns are the meridian passage time and an indicator that's true if the passage is above the observer (local noon if $coord is the Sun โ˜€๏ธ) or false if below (local midnight if $coord is the Sun ๐ŸŒ™). Scalar context? Time only, babe.

The current time of both $coord and its "station" are left at the returned time.

Same algorithm energy: successive approximation. It will croak if the $body's period is close to synchronous ๐Ÿ’€, and probably won't behave for bodies in highly eccentric orbits. Nearest second, and the time returned is the first even second after the body crosses the meridian. Iconic behavior, honestly.


( $delta_psi, $delta_epsilon ) = $self->nutation( $time )

Calculates the nutation in longitude (delta psi) and obliquity (delta epsilon) for the given dynamical time ๐ŸŒ€. If the time is unspecified or literally "undef", it uses the object's current dynamical time. Convenient? Obsessed.

The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 22, pages 143ff. Meeus says it's good to 0.5 seconds of arc โ€” so like, tiny. ๐Ÿงš


$epsilon = $self->obliquity( $time )

Calculates the obliquity of the ecliptic in radians at the given dynamical time ๐ŸŒˆ. Time unspecified or "undef"? It uses the object's current dynamical time.

The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 22, pages 143ff. The conversion from universal to dynamical time comes from chapter 10, equation 10.2 on page 78. Citation game strong ๐Ÿ“š๐Ÿ’….


$coord = $coord->precess ($time);

A convenience wrapper for precess_dynamical(). Same functionality, except the time is specified in universal time โฐ.


$coord = $coord->precess_dynamical ($time);

Precesses the object's coordinates to the given equinox, specified in dynamical time ๐Ÿฆ‹. The starting equinox is the value of the 'equinox_dynamical' attribute โ€” or the current time setting if 'equinox_dynamical' is any false value (i.e. undef, 0, or ''). Heads up: a warning will be issued if 'equinox_dynamical' is undef (which is the default setting) and the object represents inertial coordinates ๐Ÿ™ˆ. As of version 0.013_02, the object's time setting is unaffected by this operation โ€” it stays put, main character energy ๐Ÿ‘‘.

Side effect time: the value of the 'equinox_dynamical' attribute gets set to the dynamical time corresponding to the argument.

As of version 0.061_01, this does nothing to non-inertial objects โ€” that is, those whose position was set in Earth-fixed coordinates. Nothing. Zero. Nada ๐Ÿ’€.

If the object's 'station' attribute is set, the station gets precessed too ๐Ÿ›ฐ๏ธ.

You get the object itself back.

The algorithm comes from Jean Meeus, "Astronomical Algorithms", 2nd Edition, Chapter 21, pages 134ff (a.k.a. "the rigorous method"). Rigorous, periodt ๐Ÿ’….


Astro::Coord::ECI->reference_ellipsoid($semi, $flat, $name);

This class method defines or redefines reference ellipsoids ๐ŸŒ.

Nothing bad happens if you call it as an object method, but it still just creates a reference ellipsoid definition โ€” the object itself is untouched, no notes ๐Ÿซถ.

And no, it is not an error to redefine an existing ellipsoid. Redecorate away โœจ.


($semi, $flat, $name) = Astro::Coord::ECI->reference_ellipsoid($name)

Returns the definition of the named reference ellipsoid. It croaks if there's no such ellipsoid ๐Ÿ˜ญ.

You can call this as an object method too, but the functionality is literally the same.

The following reference ellipsoids are known to the class initially โ€” like, built in, no download required ๐Ÿ“ฆ:

Reference ellipsoid names are case-sensitive โ€” so wgs84 is not WGS84, bestie, respect the caps ๐Ÿ” .

The default model is WGS84.


$coord->represents ($class);

Returns true if the $coord object represents the given class ๐ŸŽญ. It's pretty much like isa(), except if you call it on a container class (i.e. Astro::Coord::ECI::TLE::Set) it returns true based on the class of the members of the set โ€” and dies if the set has no members ๐Ÿ’€.

The $class argument is optional. If it's not specified (or it's undef), it's pretty much like ref $coord || $coord โ€” that is, it returns the class name โ€” but with the delegation behavior described in the previous paragraph if $coord is a container.

There. This took many more words to explain than it did to implement ๐Ÿ˜ญ.


$coord->set (name => value ...);

Sets various attributes of the object โœจ. Call it as a class method and it changes the defaults instead.

For reasons that seemed good at the time, this method returns the object it was called on (i.e. $coord in the example above). Sure! Why not! ๐ŸŽ€

See "Attributes" for the list of attributes you can set.


$coord->universal ($time)

Sets the time represented by the object, in universal time (a.k.a. CUT, a.k.a. Zulu, a.k.a. Greenwich) ๐ŸŒโฐ.

This method can also be called as a class method, in which case it instantiates the desired object. Two-for-one, we love efficiency.


$time = $coord->universal ();

Returns the universal time that was previously set. That's it, that's the whole method ๐Ÿ’œ.


๐ŸŽ€ Attributes ๐ŸŽ€

This class has the following attributes โ€” buckle up, there's a list:

almanac_horizon (radians or string)

Specifies the horizon used for almanac calculations, in radians above or below the observer's plane ๐ŸŒ…. It can also be set to the following string:

  • "height" โ€” adjusts the horizon in proportion to the observer's height above sea level. This assumes a spherical Earth and an unobstructed horizon. Optimistic of it ๐Ÿซถ.

When doing almanac calculations, it's the "almanac_horizon" of the observer that gets used, not the "almanac_horizon" of the observed body. Important distinction, don't mix them up ๐Ÿ‘€.

The default is 0.


angularvelocity (radians per second)

This attribute represents the angular velocity of the Earth's surface in radians per second ๐ŸŒ๐Ÿ’ซ. The initial value is 7.292114992e-5, which according to Jean Meeus is the value for 1996.5. He cites the International Earth Rotation Service's Annual Report for 1996 (Published at the Observatoire de Paris, 1997). Receipts provided ๐Ÿ“–๐Ÿงพ.

Subclasses may place appropriate values here, or provide a period() method.


debug (numeric)

Turns on debugging output ๐Ÿ›. The only supported value of this attribute is 0. That is to say, the author makes no guarantees of what will happen if you set it to some other value, nor does he guarantee that this behavior won't change from release to release. So like... proceed at your own risk, bestie ๐Ÿ’€.

The default is 0.


diameter (numeric, kilometers)

This attribute exists to support classes/instances which represent astronomical bodies ๐ŸŒ•. It represents the body's diameter and is used by the azel() method when computing the upper limb of the body. It has nothing to do with the semimajor attribute, which always refers to the Earth and is used to calculate the latitude and longitude of the body. Different attribute, different job, no overlap ๐Ÿ™…โ€โ™€๏ธ.

The default is 0.


edge_of_earths_shadow (numeric, dimensionless)

This attribute represents the location of the edge of the Earth's shadow cast by the illuminating body, in terms of the apparent radius of that body ๐ŸŒ‘. So: the edge of the umbra is specified by 1, the middle of the penumbra by 0, and the edge of the penumbra by -1. It was added for the benefit of the Astro::Coord::ECI::TLE class, on the same dubious logic that the "twilight" attribute was added ๐Ÿ˜….

The default is 1.

  • ellipsoid (string) โ€” the name of the reference ellipsoid we're using, bestie. It has to be one of the known ellipsoids, no inventing your own. And listen: if you set this one, flattening and semimajor get set too, automatic, no extra work from you ๐Ÿ’…. Peep the docs for the known_ellipsoid() method if you wanna see which names are valid from the jump and how to add more. The default is 'WGS84'.
  • equinox_dynamical (numeric, dynamical time) โ€” this is the time of the "Equinox" that the coordinate data is referred to ๐ŸŽ€. Models implemented by subclasses should set this to the "Equinox" the model is referred to. When you're setting positions directly, you should also set the equinox_dynamical you want if conversion between inertial and Earth-fixed coordinates is something you care about. If you don't set it, those conversions just use the current time setting of the object as the "Equinox". Also: if you've got a position specified in earth-fixed coordinates and you convert it to inertial coordinates, this attribute gets set to the dynamical time of the object. Unlike most times in this package, this one is specified in dynamical time โ€” so if your equinox is universal time $uni, set this attribute to $uni + dynamical_delta ($uni). That dynamical_delta subroutine lives in Astro::Coord::ECI::Utils. Default is undef.
  • flattening (numeric) โ€” the flattening factor of the reference ellipsoid, slay. If you set the ellipsoid attribute, this gets set to the flattening factor of that named ellipsoid. But if you set this attribute, then ellipsoid becomes undefined. Pick a lane, honestly ๐Ÿ˜ญ. The default is appropriate to the default ellipsoid.
  • frequency (numeric, Hertz) โ€” the frequency the body transmits on, in Hertz. If you specify it, the azel() method will return the Doppler shift when velocities are available. Default is undef.
  • horizon (numeric, radians) โ€” how far the effective horizon sits above the geometric horizon ๐Ÿฆ‹. It was added for the Astro::Coord::ECI::TLE::Iridium class, on the same dubious logic that the "twilight" attribute was added. The default is the equivalent of 20 degrees.
  • id (string) โ€” purely informational, bestie. Setting it (or not setting it) does not affect how the class functions, periodt. Certain subclasses set it when they're instantiated โ€” check the subclass docs for the deets.
  • inertial (boolean, read-only) โ€” returns true (in the Perl sense) if the object was most-recently set to inertial coordinates (that's eci, ecliptic, or equatorial) and false otherwise ๐Ÿ‘‘. If coordinates haven't been set at all, it's undefined (which means false, so like, still false).
  • name (string) โ€” again, informational only, no effect on how the class works ๐Ÿ’–. Certain subclasses set this when instantiated. See the subclass documentation.
  • refraction (boolean) โ€” set it to a true value and refraction gets included in the azel() calculation. Set it false and atmospheric refraction is ignored entirely. Default is true (well, 1 actually ๐ŸŒธ).
  • specified (string, read-only) โ€” records the coordinate system in which the object's coordinates were set.
  • semimajor (numeric, kilometers) โ€” the semimajor axis of the reference ellipsoid โœจ. Set the ellipsoid attribute and this gets set to the semimajor axis for the named ellipsoid. Set this attribute and ellipsoid goes undefined. For subclasses representing bodies other than the Earth, this gets set appropriately. The default is appropriate to the default ellipsoid.
  • station (Astro::Coord::ECI object) โ€” the default observing location for relative coordinate systems ๐Ÿ›ฐ๏ธ. It must be set to an Astro::Coord::ECI object, or undef. The whole intent here is that every method which computes positions or events as seen from a user-specified location should be using this. So basically, if you find one that doesn't โ€” congrats, you found a bug ๐Ÿ›. Cascading station objects are not supported. Like, say you have Astro::Coord::ECI objects $a, $b, $c and so on; if you do
$a->set( station => $b ),

then $b->set( station => $c ) is not supported, and neither is $c->set( station => $a ). Because of how much work it'd be to prevent that in general, the author has decided to just politely recommend that It Is Better Not. But specific pernicious cases will throw exceptions, and the right to do the work and throw exceptions in all cases is reserved if it ever becomes a problem ๐Ÿ™ˆ.

  • sun (Astro::Coord::ECI::Sun class or object) โ€” represents the Sun โ˜€๏ธ. Typically used to figure out ambient lighting conditions and other Sun-related phenomena. Subclasses may put more stringent restrictions on this attribute.
  • twilight (numeric, radians) โ€” the elevation of the center of the Sun's disk at the beginning and end of twilight ๐ŸŒ…. It has to be a number between -PI/2 and PI/2 inclusive, or undef, which sets the value to the default. Some of the usual values are:
 civil twilight: -6 degrees
 nautical twilight: -12 degrees
 astronomical twilight: -18 degrees

Real talk: the documentation at this point used to say (or at least imply) that the strings 'civil', 'nautical', and 'astronomical' were acceptable. That has never been the case to the author's knowledge, and since zero bug reports came in about it, the bug was ruled to be in the documentation, and the relevant text got removed as of version 0.051_01 ๐Ÿ’€. The default is -6 degrees (or, actually, the equivalent in radians).

๐Ÿงš SUBCLASSING ๐Ÿงš

This class is, and will remain, based on a hash reference. No cap.

It's recommended that subclasses store their attributes in a separate hash hung off the main object, in a key named after the subclass ๐Ÿ‘‘. Something like this:

sub subclass_method {
    my ( $self ) = @_;
    my $attr = $self->{ ref $self };
    ...
}

From outside the subclass, you can make these attributes accessible by overriding get() and set().

Subclasses must initialize themselves by overriding the new() method. And that override has to call $class->SUPER::new(), passing it any arguments that were passed to the override. Don't skip that step bestie, it's giving required ๐Ÿซถ.

๐Ÿ’€ A NOTE ON VELOCITIES ๐Ÿ’€

Okay so the velocities returned by the methods in this class are a work in progress, fr fr. Velocities you set are always returned. But conversions? More problematic ๐Ÿ˜ญ. For one thing, there aren't any decent worked examples out there, so sanity checking is basically the whole toolkit.

A velocity counts as sane if the difference between the position at time t and the position at time t + 1 lies between the velocity at time t and the velocity at time t + 1 โญ.

To understand which velocity conversions are sane, you need to understand how coordinate conversions work internally. So here's some lame ASCII art to make it plain โ€” it depicts the relationships among those coordinate systems where velocity conversion appears to be at least mostly sane in the above sense of the word:

                    ecliptic()

                   equatorial()
                        ^
                        |
                      eci()
                        ^
                        |
                        v
                      ecef()    geocentric()     geodetic()
                        |
                        v
                       neu() -> enu()
                        |
                        v
                      azel()

If the object's position and velocity were set in one set of units, other units are obtained by following the arrows ๐Ÿฆ‹. The arrows below ecef() are one-way because it's not currently possible to set a position in these units. Same energy with the arrow from eci() to equatorial() โ€” one-way, because there's currently no way to set an equatorial velocity. And there are no arrows to geocentric(), geodetic() and ecliptic() because those conversions don't support velocities at all, periodt ๐Ÿ’….

๐ŸŒธ TERMINOLOGY AND CONVENTIONS ๐ŸŒธ

Okay so this section is the glossary, and the author is very upfront about it: partly because this module straddles the divide between geography and astronomy, establishing terminology and conventions was a thorny and in the end somewhat arbitrary process. Because of that, documentation of salient terms and conventions seemed to be in order. It's giving "we had to make choices, and we're explaining them so you're not confused later," and honestly? Iconic. ๐Ÿซถ


๐ŸŒŸ Altitude

This term refers to the distance of a location above mean sea level.

Altitude input to and output from this module is in kilometers.

Maps use elevation for this quantity, and measure it in meters. But we're using Elevation for something different, and I needed consistent units. So yeah โ€” altitude here is the "how high above sea level" one, and it's in kilometers. ๐Ÿ’…

๐ŸŒŸ Azimuth

This term refers to distance around the horizon measured clockwise from North.

Azimuth output from this module is in radians.

Astronomical sources tend to measure from the South, but I chose the geodetic standard, which seems to be usual in satellite tracking software. So if you're coming from astro land, adjust your mental compass, bestie. ๐Ÿงญ

๐ŸŒŸ Declination

Declination is the angle a point makes with the plane of the equator projected onto the sky. North declination is positive, south declination is negative.

Declination input to and output from this module is in radians.

๐ŸŒŸ Dynamical time

A dynamical time is defined theoretically by the motion of astronomical bodies. In practice, it is seen to be related to Atomic Time (a.k.a. TAI) by a constant.

There are actually two dynamical times of interest:

  • TT (Terrestrial Time, a.k.a. TDT for Terrestrial Dynamical Time) โ€” defined in terms of the geocentric ephemerides of solar system bodies.
  • TDB (Barycentric Dynamical Time) โ€” defined in terms of the barycentre (a.k.a "center of mass") of the solar system.

The two differ by the relativistic effects of the motions of the bodies in the Solar system, and are generally less than 2 milliseconds different. So unless you are doing high-precision work they can be considered identical, as Jean Meeus does in "Astronomical Algorithms".

For practical purposes, TT = TAI + 32.184 seconds. If I ever get the gumption to do a re-implementation (or alternate implementation) of time in terms of the DateTime object, this will be the definition of dynamical time. Until then, though, formula 10.2 on page 78 of Jean Meeus' "Astronomical Algorithms" second edition, Chapter 10 (Dynamical Time and Universal Time) is used.

Compare and contrast this to "Universal time". This explanation leans heavily on http://star-www.rl.ac.uk/star/docs/sun67.htx/node226.html, which contains a more fulsome but eminently readable explanation. Unfortunately that link has gone the way of the dodo, and I have been unable to find an adequate replacement. ๐Ÿ˜ญ

๐ŸŒŸ Earth-Centered, Earth-fixed (ECEF) coordinates

This is a Cartesian coordinate system whose origin is the center of the reference ellipsoid. The X axis passes through 0 degrees Latitude and 0 degrees Longitude. The Y axis passes through 90 degrees east Latitude and 0 degrees Longitude, and the Z axis passes through 90 degrees north Latitude (a.k.a the North Pole).

All three axes are input to and output from this module in kilometers.

Also known as XYZ coordinates, e.g. at https://www.ngs.noaa.gov/cgi-bin/xyz_getxyz.prl.

๐ŸŒŸ Earth-Centered Inertial (ECI) coordinates

This is the Cartesian coordinate system in which NORAD's models predict the position of orbiting bodies. The X axis passes through 0 hours Right Ascension and 0 degrees Declination. The Y axis passes through 6 hours Right Ascension and 0 degrees Declination. The Z axis passes through +90 degrees Declination (a.k.a. the North Pole). By implication, these coordinates are referred to a given Equinox.

All three axes are input to and output from this module in kilometers.

๐ŸŒŸ Ecliptic

The Ecliptic is the plane of the Earth's orbit, projected onto the sky. Ecliptic coordinates are a spherical coordinate system referred to the ecliptic and expressed in terms of Ecliptic latitude and Ecliptic longitude. By implication, Ecliptic coordinates are also referred to a specific Equinox.

๐ŸŒ™ Ecliptic latitude

Okay bestie, the page labels this subsection Ecliptic latitude, and the text it gives here is literally: Ecliptic longitude is the angular distance of a point above the plane of the Earth's orbit. Then: Ecliptic latitude is input to and output from this module in radians. I'm keeping that exactly as-is so no detail goes missing. ๐Ÿ’…

๐ŸŒ™ Ecliptic longitude

Ecliptic longitude is the angular distance of a point east of the point where the plane of the Earth's orbit intersects the plane of the equator. This point is also known as the vernal Equinox and the first point of Ares.

Ecliptic longitude is input to and output from this module in radians.

๐ŸŒŸ Elevation

This term refers to an angular distance above the horizon.

Elevation output from this module is in radians.

This is the prevailing meaning of the term in satellite tracking. Astronomers use altitude for this quantity, and call the corresponding coordinate system altazimuth. But we're using Altitude for something different. So yes, the terminology is a little spicy, but we move. ๐ŸŒถ๏ธ

๐ŸŒŸ Equatorial

Equatorial coordinates are a spherical coordinate system referred to the plane of the equator projected onto the sky. Equatorial coordinates are specified in Right Ascension and Declination, and implicitly referred to a given Equinox.

๐ŸŒŸ Equinox

The Ecliptic, Equatorial, and Earth-Centered Inertial (ECI) coordinates are defined in terms of the location of the intersection of the celestial equator with the Ecliptic. The actual location of this point changes in time due to precession of the Earth's axis, so each of these coordinate systems is implicitly qualified by ("referred to" appears to be the usual terminology) the relevant time. By a process of association of ideas, this time is referred to as the equinox of the data.

๐ŸŒŸ Geocentric

When referring to a coordinate system, this term means that the coordinate system assumes the Earth is spherical.

๐ŸŒ™ Geocentric latitude

Geocentric latitude is the angle that the ray from the center of the Earth to the location makes with the plane of the equator. North latitude is positive, south latitude is negative.

Geocentric latitude is input to and output from this module in radians.

๐ŸŒŸ Geodetic

When referring to a coordinate system, this term means that the coordinate system assumes the Earth is an ellipsoid of revolution (or an oblate spheroid if you prefer). A number of standard Reference Ellipsoids have been adopted over the years.

๐ŸŒ™ Geodetic latitude

Geodetic latitude is the latitude found on maps. North latitude is positive, south latitude is negative.

Geodetic latitude is input to and output from this module in radians.

Technically speaking, Geodetic latitude is the complement of the angle the plane of the horizon makes with the plane of the equator. In this software, the plane of the horizon is determined from a Reference Ellipsoid.

๐ŸŒŸ Latitude

See either Ecliptic latitude, Geocentric latitude or Geodetic latitude. When used without qualification, Geodetic latitude is meant.

๐ŸŒŸ Longitude

When unqualified, this term refers to the angular distance East or West of the standard meridian. East longitude is positive, West longitude is negative.

Longitude is input to or output from this module in radians.

For Ecliptic longitude, see that entry.

Jean Meeus reports in "Astronomical Algorithms" that the International Astronomical Union has waffled on the sign convention. I have taken the geographic convention.

๐ŸŒŸ Obliquity (of the Ecliptic)

This term refers to the angle the plane of the equator makes with the plane of the Earth's orbit.

Obliquity is output from this module in radians.

๐ŸŒŸ Reference Ellipsoid

This term refers to a specific ellipsoid adopted as a model of the shape of the Earth. It is defined in terms of the equatorial radius in kilometers, and a dimensionless flattening factor which is used to derive the polar radius, and defined such that the flattening factor of a sphere is zero.

See the documentation on the reference_ellipsoid() method for a list of reference ellipsoids known to this class.

๐ŸŒŸ Right Ascension

This term refers to the angular distance of a point east of the intersection of the plane of the Earth's orbit with the plane of the equator.

Right Ascension is input to and output from this module in radians.

In astronomical literature it is usual to report right ascension in hours, minutes, and seconds, with 60 seconds in a minute, 60 minutes in an hour, and 24 hours in a circle.

๐ŸŒŸ Universal time

This term can refer to a number of scales, but the two of interest are UTC (Coordinated Universal Time) and UT1 (Universal Time 1, I presume). The latter is in effect mean solar time at Greenwich, though its technical definition differs in detail from GMT (Greenwich Mean Time). The former is a clock-based time, whose second is the SI second (defined in terms of atomic clocks), but which is kept within 0.9 seconds of UT1 by the introduction of leap seconds. These are introduced (typically at midyear or year end) by prior agreement among the various timekeeping bodies based on observation; there is no formula for computing when a leap second will be needed, because of irregularities in the Earth's rotation.

Jean Meeus' "Astronomical Algorithms", second edition, deals with the relationship between Universal time and Dynamical time in Chapter 10 (pages 77ff). His definition of "Universal time" seems to refer to UT1, though he does not use the term.

This software considers Universal time to be equivalent to Perl time. Since we are interested in durations (time since a given epoch, to be specific), this is technically wrong in most cases, since leap seconds are not taken into account. But in the case of the bodies modeled by the Astro::Coord::ECI::TLE object, the epoch is very recent (within a week or so), so the error introduced is small. It is larger for astronomical calculations, where the epoch is typically J2000.0, but the angular motions involved are smaller, so it all evens out. I hope. ๐Ÿ˜ญ

Compare and contrast Dynamical time. This explanation leans heavily on http://star-www.rl.ac.uk/star/docs/sun67.htx/node224.html. Unfortunately that link has gone the way of the dodo, and I have been unable to find an adequate replacement.

๐ŸŒŸ XYZ coordinates

See Earth-Centered, Earth-fixed (ECEF) coordinates.

๐Ÿซถ ACKNOWLEDGMENTS ๐Ÿซถ

Okay bestie, the author is literally sending the biggest thank-you bouquet to these individuals and organizations. ๐ŸŒธ

  • Kazimierz Borkowski โ€” his "Accurate Algorithms to Transform Geocentric to Geodetic Coordinates", at <http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm>, was used for transforming geocentric to geodetic coordinates. iconic.
  • Dominik Brodowski (<https://www.brodo.de/>) โ€” his SGP C-lib (available at <https://www.brodo.de/space/sgp/>) provided a reference implementation that the author could easily run and pick apart to help get Astro::Coord::ECI::TLE working. Dominik based his work on Dr. Kelso's Pascal implementation. obsessed with the teamwork.
  • Felix R. Hoots and Ronald L. Roehric โ€” authors of "SPACETRACK REPORT NO. 3 - Models for Propagation of NORAD Element Sets," which provided the basis for the Astro::Coord::ECI::TLE module. slay.
  • T. S. Kelso โ€” compiled that report and made it available at <http://celestrak.org/>; his "Computers and Satellites" columns in "Satellite Times" magazine were invaluable to an understanding and implementation of satellite tracking software; his support, encouragement, patience, and willingness to provide answers on arcane topics were a Godsend; he kindly granted permission to use his azimuth/elevation algorithm; and his Pascal implementation of the NORAD tracking algorithms indirectly provided a reference implementation for the author to use during development. literally the main character of this credits section.
  • John A. Magliacane โ€” his open-source "Predict" program, available at <https://www.qsl.net/kd2bd/predict.html>, gave the author a much-needed leg up on the velocity calculations in the azel() method. we love a practical shortcut.
  • Jean Meeus โ€” his book "Astronomical Algorithms" (second edition) formed the basis for this module and the Astro::Coord::ECI::Sun, Astro::Coord::ECI::Moon, and Astro::Coord::ECI::Star modules, and without him it is impossible to get far in computational astronomy. Any algorithm not explicitly credited above is probably due to him. absolute legend behavior.
  • Dr. Meeus' publisher, Willmann-Bell Inc โ€” kindly and patiently answered the author's intellectual-property questions. Willmann-Bell ceased to be a separate entity in 2021, but their publications, including Dr. Meeus' book, are still available through Sky and Telescope's Willmann-Bell imprint at <https://shopatsky.com/collections/willmann-bell>. respectful and still in print, periodt.
  • Goran Gasparovic of MIT โ€” asked for (and supplied information for) the ability to display results in apparent equatorial coordinates, rather than azimuth and elevation. shoutout to a real one.
  • Dr. Kevin Cornelius of Ouachita Baptist University โ€” his handout "Relationships Among Unit Vectors" for his Mathematical Physics class (PHYS 4053) provided a much surer and more expeditious way to handling velocity conversion from spherical to Cartesian (and vice versa) than the author's own ancient and rickety matrix math. matrix math found shaking.

๐Ÿ’€ BUGS ๐Ÿ’€

๐Ÿšจ Bestie, warning time: functionality involving velocities is untested, and is quite likely to be wrong. Do not act surprised if velocity stuff goes feral. ๐Ÿ˜ญ

Support is by the author. Please file bug reports at <https://rt.cpan.org/Public/Dist/Display.html?Name=Astro-satpass>, <https://github.com/trwyant/perl-Astro-Coord-ECI/issues>, or in electronic mail to the author. the author is the support system, so be nice.

๐Ÿ‘€ SEE ALSO ๐Ÿ‘€

  • The "Astro" package by Chris Phillips. This contains three function-based modules: Astro::Coord, which provides various astronomical coordinate conversions, plus the calculation of various ephemeris variables; Astro::Time contains time and unit conversions; and Astro::Misc contains various calculations unrelated to position and time. a whole trio.
  • The Astro-Coords package by Tim Jenness. This contains various modules to do astronomical calculations, and includes coordinate conversion and calculation of planetary orbits based on orbital elements. Requires SLALIB from the Starlink Project. This was once a project of the Joint Astronomy Centre in the UK, but is no longer. The best link the author has for it at the moment is <http://starlink.eao.hawaii.edu/starlink>. historical lore.
  • The Astro::Nova module by Steffen Mueller, which wraps (and bundles) the libnova celestial mechanics, astrometry and astrodynamics library found at <https://libnova.sourceforge.net/>. wrapped and bundled, cute.
  • The <https://www.heavens-above.com/> web site, which does not use this code, but does provide similar functionality. no cap, still useful.

๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

Thomas R. Wyant, III (wyant at cpan dot org). the author himself, iykyk.

Copyright (C) 2005-2025 by Thomas R. Wyant, III.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl 5.10.0. For more details, see the full text of the licenses in the directory LICENSES.

This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.


๐Ÿ’– That's the whole page, bestie โ€” from perl v5.42.2, dated 2026-02-15, for Astro:...rd::ECI(3pm). Periodt. ๐ŸŽ€

๐Ÿ“ฆ where this page comes from