Umbau auf server
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Simple distance, velocity, and angle support for Skyfield.
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
from numpy import abs, copysign, isnan
|
||||
from types import MethodType
|
||||
from .constants import AU_KM, AU_M, C, DAY_S, tau
|
||||
from .descriptorlib import reify
|
||||
from .functions import _to_array, length_of
|
||||
|
||||
_dfmt = '{0}{1:02}deg {2:02}\' {3:02}.{4:0{5}}"'
|
||||
_dsgn = '{0:+>1}{1:02}deg {2:02}\' {3:02}.{4:0{5}}"'
|
||||
_hfmt = '{0}{1:02}h {2:02}m {3:02}.{4:0{5}}s'
|
||||
|
||||
class UnpackingError(Exception):
|
||||
"""You cannot iterate directly over a Skyfield measurement object."""
|
||||
|
||||
class Unit(object):
|
||||
"""A measurement that can be expressed in several choices of unit."""
|
||||
|
||||
def __getitem__(self, *args):
|
||||
"""Tell users to ask for a specific unit before indexing or slicing."""
|
||||
cls = self.__class__
|
||||
name = cls.__name__
|
||||
s = 'to use this {0}, ask for its value in a particular unit:\n\n{1}'
|
||||
attrs = sorted(k for k, v in cls.__dict__.items()
|
||||
if k[0].islower() and isinstance(v, (getset, reify)))
|
||||
examples = [' {0}.{1}'.format(name.lower(), attr) for attr in attrs]
|
||||
raise UnpackingError(s.format(name, '\n'.join(examples)))
|
||||
|
||||
__iter__ = __getitem__ # give advice about both foo[i] and "x,y,z = foo"
|
||||
|
||||
class getset(object):
|
||||
"""Unit name that serves as both a class constructor and instance attribute.
|
||||
|
||||
This supports two use cases:
|
||||
|
||||
* When called as a class method like ``Distance.km(5.0)``, we build
|
||||
and return an instance of ``Distance`` whose ``km`` has been set
|
||||
to 5.0 and whose base unit ``m``, using the appropriate conversion
|
||||
factor, has been set to 5000.0.
|
||||
|
||||
* When invoked like ``d.km`` on a particular ``Distance`` that
|
||||
doesn't yet have a ``km`` attribute (which otherwise Python itself
|
||||
would have returned), we apply the conversion factor to ``d.m``
|
||||
and return the result.
|
||||
|
||||
"""
|
||||
def __init__(self, name, docstring, conversion_factor=None, core_unit=None):
|
||||
self.name = name
|
||||
self.__doc__ = docstring
|
||||
self.conversion_factor = conversion_factor
|
||||
self.core_unit = core_unit
|
||||
|
||||
if conversion_factor is None:
|
||||
def _constructor(cls, value):
|
||||
value = _to_array(value)
|
||||
obj = cls.__new__(cls)
|
||||
setattr(obj, name, value)
|
||||
return obj
|
||||
|
||||
else:
|
||||
def _constructor(cls, value):
|
||||
value = _to_array(value)
|
||||
obj = cls.__new__(cls)
|
||||
setattr(obj, name, value)
|
||||
setattr(obj, core_unit, value / conversion_factor)
|
||||
return obj
|
||||
|
||||
_constructor.__doc__ = self.__doc__
|
||||
self._constructor = _constructor
|
||||
|
||||
def __get__(self, instance, objtype=None):
|
||||
if instance is None: # the class itself has been asked for this name
|
||||
return MethodType(self._constructor, objtype)
|
||||
value = getattr(instance, self.core_unit) * self.conversion_factor
|
||||
instance.__dict__[self.name] = value
|
||||
return value
|
||||
|
||||
class Distance(Unit):
|
||||
"""A distance, stored internally as au and available in other units.
|
||||
|
||||
You can initialize a ``Distance`` by providing a single float or a
|
||||
float array as either an ``au=``, ``km=``, or ``m=`` parameter.
|
||||
|
||||
You can access the magnitude of the distance with its three
|
||||
attributes ``.au``, ``.km``, and ``.m``. By default a distance
|
||||
prints itself in astronomical units (au), but you can take control
|
||||
of the formatting and choice of units yourself using standard Python
|
||||
numeric formatting:
|
||||
|
||||
>>> d = Distance(au=1)
|
||||
>>> print(d)
|
||||
1.0 au
|
||||
>>> print('{:.2f} km'.format(d.km))
|
||||
149597870.70 km
|
||||
|
||||
"""
|
||||
def __init__(self, au=None, km=None, m=None):
|
||||
if au is not None:
|
||||
self.au = _to_array(au)
|
||||
elif km is not None:
|
||||
self.km = km = _to_array(km)
|
||||
self.au = km / AU_KM
|
||||
elif m is not None:
|
||||
self.m = m = _to_array(m)
|
||||
self.au = m / AU_M
|
||||
else:
|
||||
raise ValueError('to construct a Distance provide au, km, or m')
|
||||
|
||||
@classmethod
|
||||
def from_au(cls, au): # deprecated and no longer used internally
|
||||
return cls.au(au)
|
||||
|
||||
au = getset('au', 'Astronomical units'
|
||||
' (the Earth-Sun distance of 149,597,870,700 m).')
|
||||
km = getset('km', 'Kilometers (1,000 meters).', AU_KM, 'au')
|
||||
m = getset('m', 'Meters.', AU_M, 'au')
|
||||
|
||||
def __str__(self):
|
||||
n = self.au
|
||||
return ('{0} au' if getattr(n, 'shape', 0) else '{0:.6} au').format(n)
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} {1}>'.format(type(self).__name__, self)
|
||||
|
||||
def length(self):
|
||||
"""Compute the length when this is an |xyz| vector.
|
||||
|
||||
The Euclidean vector length of this vector is returned as a new
|
||||
:class:`~skyfield.units.Distance` object.
|
||||
|
||||
>>> from skyfield.api import Distance
|
||||
>>> d = Distance(au=[1, 1, 0])
|
||||
>>> d.length()
|
||||
<Distance 1.41421 au>
|
||||
|
||||
"""
|
||||
return Distance(au=length_of(self.au))
|
||||
|
||||
def light_seconds(self):
|
||||
"""Return the length of this vector in light seconds."""
|
||||
return self.m / C
|
||||
|
||||
def to(self, unit):
|
||||
"""Convert this distance to the given AstroPy unit."""
|
||||
from astropy.units import au
|
||||
return (self.au * au).to(unit)
|
||||
|
||||
class Velocity(Unit):
|
||||
"""A velocity, stored internally as au/day and available in other units.
|
||||
|
||||
You can initialize a ``Velocity`` by providing a float or float
|
||||
array to its ``au_per_d=`` parameter.
|
||||
|
||||
"""
|
||||
# TODO: consider reworking this class to return a Rate object.
|
||||
|
||||
def __init__(self, au_per_d=None, km_per_s=None):
|
||||
if km_per_s is not None:
|
||||
self.km_per_s = km_per_s = _to_array(km_per_s)
|
||||
self.au_per_d = km_per_s * DAY_S / AU_KM
|
||||
elif au_per_d is not None:
|
||||
self.au_per_d = _to_array(au_per_d)
|
||||
else:
|
||||
raise ValueError('to construct a Velocity provide'
|
||||
' au_per_d or km_per_s')
|
||||
|
||||
au_per_d = getset('au_per_d', 'Astronomical units per day.')
|
||||
km_per_s = getset('km_per_s', 'Kilometers per second.',
|
||||
AU_KM / DAY_S, 'au_per_d')
|
||||
m_per_s = getset('m_per_s', 'Meters per second.',
|
||||
AU_M / DAY_S, 'au_per_d')
|
||||
|
||||
def __str__(self):
|
||||
n = self.au_per_d
|
||||
fmt = '{0} au/day' if getattr(n, 'shape', 0) else '{0:.6} au/day'
|
||||
return fmt.format(n)
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} {1}>'.format(type(self).__name__, self)
|
||||
|
||||
def to(self, unit):
|
||||
"""Convert this velocity to the given AstroPy unit."""
|
||||
from astropy.units import au, d
|
||||
return (self.au_per_d * au / d).to(unit)
|
||||
|
||||
class AngleRate(object):
|
||||
"""The rate at which an angle is changing."""
|
||||
|
||||
# TODO: design and implement public constructor.
|
||||
|
||||
@classmethod
|
||||
def _from_radians_per_day(cls, radians_per_day):
|
||||
ar = cls()
|
||||
ar._radians_per_day = radians_per_day
|
||||
return ar
|
||||
|
||||
@reify
|
||||
def radians(self):
|
||||
""":class:`Rate` of change in radians."""
|
||||
return Rate._from_per_day(self._radians_per_day)
|
||||
|
||||
@reify
|
||||
def degrees(self):
|
||||
""":class:`Rate` of change in degrees."""
|
||||
return Rate._from_per_day(self._radians_per_day / tau * 360.0)
|
||||
|
||||
@reify
|
||||
def arcminutes(self):
|
||||
""":class:`Rate` of change in arcminutes."""
|
||||
return Rate._from_per_day(self._radians_per_day / tau * 21600.0)
|
||||
|
||||
@reify
|
||||
def arcseconds(self):
|
||||
""":class:`Rate` of change in arcseconds."""
|
||||
return Rate._from_per_day(self._radians_per_day / tau * 1296000.0)
|
||||
|
||||
@reify
|
||||
def mas(self):
|
||||
""":class:`Rate` of change in milliarcseconds."""
|
||||
return Rate._from_per_day(self._radians_per_day / tau * 1.296e9)
|
||||
|
||||
# TODO: str; repr; conversion to AstroPy units
|
||||
|
||||
class Rate(object):
|
||||
"""Measurement whose denominator is time."""
|
||||
|
||||
# TODO: design and implement public constructor.
|
||||
|
||||
@classmethod
|
||||
def _from_per_day(cls, per_day):
|
||||
r = cls()
|
||||
r._per_day = per_day
|
||||
return r
|
||||
|
||||
@reify
|
||||
def per_day(self):
|
||||
"""Units per day of Terrestrial Time."""
|
||||
return self._per_day
|
||||
|
||||
@reify
|
||||
def per_hour(self):
|
||||
"""Units per hour of Terrestrial Time."""
|
||||
return self._per_day / 24.0
|
||||
|
||||
@reify
|
||||
def per_minute(self):
|
||||
"""Units per minute of Terrestrial Time."""
|
||||
return self._per_day / 1440.0
|
||||
|
||||
@reify
|
||||
def per_second(self):
|
||||
"""Units per second of Terrestrial Time."""
|
||||
return self._per_day / 86400.0
|
||||
|
||||
# Angle units.
|
||||
|
||||
_instantiation_instructions = """to instantiate an Angle, try one of:
|
||||
|
||||
Angle(angle=another_angle)
|
||||
Angle(radians=value)
|
||||
Angle(degrees=value)
|
||||
Angle(hours=value)
|
||||
|
||||
where `value` can be either a Python float, a list of Python floats,
|
||||
or a NumPy array of floats"""
|
||||
|
||||
class Angle(Unit):
|
||||
|
||||
def __init__(self, angle=None, radians=None, degrees=None, hours=None,
|
||||
preference=None, signed=False):
|
||||
|
||||
if angle is not None:
|
||||
if not isinstance(angle, Angle):
|
||||
raise ValueError(_instantiation_instructions)
|
||||
self.radians = angle.radians
|
||||
elif radians is not None:
|
||||
self.radians = _to_array(radians)
|
||||
elif degrees is not None:
|
||||
self.degrees = degrees = _to_array(_unsexagesimalize(degrees))
|
||||
self.radians = degrees / 360.0 * tau
|
||||
elif hours is not None:
|
||||
self.hours = hours = _to_array(_unsexagesimalize(hours))
|
||||
self.radians = hours / 24.0 * tau
|
||||
|
||||
self.preference = (preference if preference is not None
|
||||
else 'hours' if hours is not None
|
||||
else 'degrees')
|
||||
self.signed = signed
|
||||
|
||||
@classmethod
|
||||
def from_degrees(cls, degrees, signed=False):
|
||||
degrees = _to_array(_unsexagesimalize(degrees))
|
||||
self = cls.__new__(cls)
|
||||
self.degrees = degrees
|
||||
self.radians = degrees / 360.0 * tau
|
||||
self.preference = 'degrees'
|
||||
self.signed = signed
|
||||
return self
|
||||
|
||||
radians = getset('radians', 'Radians (𝜏 = 2𝜋 in a circle).')
|
||||
|
||||
# Deprecated names, to support legacy code.
|
||||
@reify
|
||||
def _hours(self): return self.hours
|
||||
@reify
|
||||
def _degrees(self): return self.degrees
|
||||
|
||||
@reify
|
||||
def hours(self):
|
||||
r"""Hours (24\ |h| in a circle)."""
|
||||
return self.radians * 24.0 / tau
|
||||
|
||||
@reify
|
||||
def degrees(self):
|
||||
"""Degrees (360° in a circle)."""
|
||||
return self.radians * 360.0 / tau
|
||||
|
||||
def arcminutes(self):
|
||||
"""Return the angle in arcminutes."""
|
||||
return self.degrees * 60.0
|
||||
|
||||
def arcseconds(self):
|
||||
"""Return the angle in arcseconds."""
|
||||
return self.degrees * 3600.0
|
||||
|
||||
def mas(self):
|
||||
"""Return the angle in milliarcseconds."""
|
||||
return self.degrees * 3600000.0
|
||||
|
||||
def __str__(self):
|
||||
size = self.radians.size
|
||||
if size == 0:
|
||||
return 'Angle []'
|
||||
if self.preference == 'degrees':
|
||||
v = self.degrees
|
||||
fmt = _dsgn.format if self.signed else _dfmt.format
|
||||
places = 1
|
||||
else:
|
||||
v = self.hours
|
||||
fmt = _hfmt.format
|
||||
places = 2
|
||||
if size >= 2:
|
||||
return '{0} values from {1} to {2}'.format(
|
||||
len(v), _sfmt(fmt, v[0], places), _sfmt(fmt, v[-1], places))
|
||||
return _sfmt(fmt, v, places)
|
||||
|
||||
def __repr__(self):
|
||||
if self.radians.size == 0:
|
||||
return '<{0} []>'.format(type(self).__name__)
|
||||
else:
|
||||
return '<{0} {1}>'.format(type(self).__name__, self)
|
||||
|
||||
def hms(self, warn=True):
|
||||
"""Convert to a tuple (hours, minutes, seconds).
|
||||
|
||||
All three quantities will have the same sign as the angle itself.
|
||||
|
||||
"""
|
||||
if warn and self.preference != 'hours':
|
||||
raise WrongUnitError('hms')
|
||||
sign, units, minutes, seconds = _sexagesimalize_to_float(self.hours)
|
||||
return sign * units, sign * minutes, sign * seconds
|
||||
|
||||
def signed_hms(self, warn=True):
|
||||
"""Convert to a tuple (sign, hours, minutes, seconds).
|
||||
|
||||
The ``sign`` will be either +1 or -1, and the other quantities
|
||||
will all be positive.
|
||||
|
||||
"""
|
||||
if warn and self.preference != 'hours':
|
||||
raise WrongUnitError('signed_hms')
|
||||
return _sexagesimalize_to_float(self.hours)
|
||||
|
||||
def hstr(self, places=2, warn=True, format=_hfmt):
|
||||
"""Return a string like ``12h 07m 30.00s``; see `Formatting angles`.
|
||||
|
||||
.. versionadded:: 1.39
|
||||
|
||||
Added the ``format=`` parameter.
|
||||
|
||||
"""
|
||||
if warn and self.preference != 'hours':
|
||||
raise WrongUnitError('hstr')
|
||||
hours = self.hours
|
||||
shape = getattr(hours, 'shape', ())
|
||||
fmt = format.format # `format()` method of `format` string
|
||||
if shape:
|
||||
return [_sfmt(fmt, h, places) for h in hours]
|
||||
return _sfmt(fmt, hours, places)
|
||||
|
||||
def dms(self, warn=True):
|
||||
"""Convert to a tuple (degrees, minutes, seconds).
|
||||
|
||||
All three quantities will have the same sign as the angle itself.
|
||||
|
||||
"""
|
||||
if warn and self.preference != 'degrees':
|
||||
raise WrongUnitError('dms')
|
||||
sign, units, minutes, seconds = _sexagesimalize_to_float(self.degrees)
|
||||
return sign * units, sign * minutes, sign * seconds
|
||||
|
||||
def signed_dms(self, warn=True):
|
||||
"""Convert to a tuple (sign, degrees, minutes, seconds).
|
||||
|
||||
The ``sign`` will be either +1 or -1, and the other quantities
|
||||
will all be positive.
|
||||
|
||||
"""
|
||||
if warn and self.preference != 'degrees':
|
||||
raise WrongUnitError('signed_dms')
|
||||
return _sexagesimalize_to_float(self.degrees)
|
||||
|
||||
def dstr(self, places=1, warn=True, format=None):
|
||||
"""Return a string like ``181deg 52' 30.0"``; see `Formatting angles`.
|
||||
|
||||
.. versionadded:: 1.39
|
||||
|
||||
Added the ``format=`` parameter.
|
||||
|
||||
"""
|
||||
if warn and self.preference != 'degrees':
|
||||
raise WrongUnitError('dstr')
|
||||
degrees = self.degrees
|
||||
signed = self.signed
|
||||
if format is None:
|
||||
format = _dsgn if signed else _dfmt
|
||||
fmt = format.format # `format()` method of `format` string
|
||||
shape = getattr(degrees, 'shape', ())
|
||||
if shape:
|
||||
return [_sfmt(fmt, d, places) for d in degrees]
|
||||
return _sfmt(fmt, degrees, places)
|
||||
|
||||
def to(self, unit):
|
||||
"""Convert this angle to the given AstroPy unit."""
|
||||
from astropy.units import rad
|
||||
return (self.radians * rad).to(unit)
|
||||
|
||||
# Or should this do:
|
||||
from astropy.coordinates import Angle
|
||||
from astropy.units import rad
|
||||
return Angle(self.radians, rad).to(unit)
|
||||
|
||||
class WrongUnitError(ValueError):
|
||||
|
||||
def __init__(self, name):
|
||||
unit = 'hours' if (name.startswith('h') or '_h' in name) else 'degrees'
|
||||
usual = 'hours' if (unit == 'degrees') else 'degrees'
|
||||
message = ('this angle is usually expressed in {0}, not {1};'
|
||||
' if you want to use {1} anyway,'.format(usual, unit))
|
||||
if name == unit:
|
||||
message += ' then please use the attribute _{0}'.format(unit)
|
||||
else:
|
||||
message += ' then call {0}() with warn=False'.format(name)
|
||||
self.args = (message,)
|
||||
|
||||
def _sexagesimalize_to_float(value):
|
||||
"""Decompose `value` into units, minutes, and seconds.
|
||||
|
||||
Note that this routine is not appropriate for displaying a value,
|
||||
because rounding to the smallest digit of display is necessary
|
||||
before showing a value to the user. Use `_sexagesimalize_to_int()`
|
||||
for data being displayed to the user.
|
||||
|
||||
This routine simply decomposes the floating point `value` into a
|
||||
sign (+1.0 or -1.0), units, minutes, and seconds, returning the
|
||||
result in a four-element tuple.
|
||||
|
||||
>>> _sexagesimalize_to_float(12.05125)
|
||||
(1.0, 12.0, 3.0, 4.5)
|
||||
>>> _sexagesimalize_to_float(-12.05125)
|
||||
(-1.0, 12.0, 3.0, 4.5)
|
||||
|
||||
"""
|
||||
sign = np.sign(value)
|
||||
n = abs(value)
|
||||
minutes, seconds = divmod(n * 3600.0, 60.0)
|
||||
units, minutes = divmod(minutes, 60.0)
|
||||
return sign, units, minutes, seconds
|
||||
|
||||
def _sexagesimalize_to_int(value, places=0):
|
||||
"""Decompose `value` into units, minutes, seconds, and second fractions.
|
||||
|
||||
This routine prepares a value for sexagesimal display, with its
|
||||
seconds fraction expressed as an integer with `places` digits. The
|
||||
result is a tuple of five integers:
|
||||
|
||||
``(sign [either +1 or -1], units, minutes, seconds, second_fractions)``
|
||||
|
||||
The integers are properly rounded per astronomical convention so
|
||||
that, for example, given ``places=3`` the result tuple ``(1, 11, 22,
|
||||
33, 444)`` means that the input was closer to 11u 22' 33.444" than
|
||||
to either 33.443" or 33.445" in its value.
|
||||
|
||||
"""
|
||||
power = 10 ** places
|
||||
n = int((power * 3600 * value + 0.5) // 1.0)
|
||||
sign = np.sign(n)
|
||||
n, fraction = divmod(abs(n), power)
|
||||
n, seconds = divmod(n, 60)
|
||||
n, minutes = divmod(n, 60)
|
||||
return sign, n, minutes, seconds, fraction
|
||||
|
||||
def _sfmt(fmt, value, places):
|
||||
"""Decompose floating point `value` into sexagesimal, and format."""
|
||||
if isnan(value):
|
||||
return 'nan'
|
||||
sgn, h, m, s, fraction = _sexagesimalize_to_int(value, places)
|
||||
sign = '-' if sgn < 0.0 else ''
|
||||
return fmt(sign, h, m, s, fraction, places)
|
||||
|
||||
def wms(whole, minutes=0.0, seconds=0.0):
|
||||
"""Return a quantity expressed with 1/60 minutes and 1/3600 seconds."""
|
||||
return (whole
|
||||
+ copysign(minutes, whole) / 60.0
|
||||
+ copysign(seconds, whole) / 3600.0)
|
||||
|
||||
def _unsexagesimalize(value):
|
||||
"""Return `value` after interpreting a (units, minutes, seconds) tuple.
|
||||
|
||||
When `value` is not a tuple, it is simply returned.
|
||||
|
||||
>>> _unsexagesimalize(3.25)
|
||||
3.25
|
||||
|
||||
An input tuple is interpreted as units, minutes, and seconds. Note
|
||||
that only the sign of `units` is significant! So all of the
|
||||
following tuples convert into exactly the same value:
|
||||
|
||||
>>> '%f' % _unsexagesimalize((-1, 2, 3))
|
||||
'-1.034167'
|
||||
>>> '%f' % _unsexagesimalize((-1, -2, 3))
|
||||
'-1.034167'
|
||||
>>> '%f' % _unsexagesimalize((-1, -2, -3))
|
||||
'-1.034167'
|
||||
|
||||
"""
|
||||
if isinstance(value, tuple):
|
||||
components = iter(value)
|
||||
value = next(components)
|
||||
factor = 1.0
|
||||
for component in components:
|
||||
factor *= 60.0
|
||||
value += copysign(component, value) / factor
|
||||
return value
|
||||
|
||||
def _interpret_angle(name, angle_object, angle_float, unit='degrees'):
|
||||
"""Return an angle in radians from one of two arguments.
|
||||
|
||||
It is common for Skyfield routines to accept both an argument like
|
||||
`alt` that takes an Angle object as well as an `alt_degrees` that
|
||||
can be given a bare float or a sexagesimal tuple. A pair of such
|
||||
arguments can be passed to this routine for interpretation.
|
||||
|
||||
"""
|
||||
if angle_object is not None:
|
||||
if isinstance(angle_object, Angle):
|
||||
return angle_object.radians
|
||||
elif angle_float is not None:
|
||||
return _unsexagesimalize(angle_float) / 360.0 * tau
|
||||
raise ValueError('you must either provide the {0}= parameter with'
|
||||
' an Angle argument or supply the {0}_{1}= parameter'
|
||||
' with a numeric argument'.format(name, unit))
|
||||
|
||||
def _ltude(value, name, psuffix, nsuffix):
|
||||
# Support for old deprecated Topos argument interpretation.
|
||||
if not isinstance(value, str):
|
||||
return _unsexagesimalize(value)
|
||||
value = value.strip().upper()
|
||||
if value.endswith(psuffix):
|
||||
sign = +1.0
|
||||
elif value.endswith(nsuffix):
|
||||
sign = -1.0
|
||||
else:
|
||||
raise ValueError('your {0} string {1!r} does not end with either {2!r}'
|
||||
' or {3!r}'.format(name, value, psuffix, nsuffix))
|
||||
try:
|
||||
value = float(value[:-1])
|
||||
except ValueError:
|
||||
raise ValueError('your {0} string {1!r} cannot be parsed as a floating'
|
||||
' point number'.format(name, value))
|
||||
return sign * value
|
||||
Reference in New Issue
Block a user