Opa_omroep-automatiseren/venv/lib/python3.8/site-packages/pymeeus/Angle.py

1382 lines
40 KiB
Python
Raw Normal View History

2020-12-27 20:00:11 +00:00
# -*- coding: utf-8 -*-
# PyMeeus: Python module implementing astronomical algorithms.
# Copyright (C) 2018 Dagoberto Salazar
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from math import pi, degrees, radians
from pymeeus.base import TOL
"""
.. module:: Angle
:synopsis: Class to handle angles
:license: GNU Lesser General Public License v3 (LGPLv3)
.. moduleauthor:: Dagoberto Salazar
"""
class Angle(object):
"""
Class Angle deals with angles in either decimal format (d.dd) or in
sexagesimal format (d m' s'').
It provides methods to handle an Angle object like it were a simple float,
but adding the functionality associated with an angle.
The constructor takes decimals and sexagesimal input. The sexagesimal
angles can be given as separate degree, minutes, seconds values, or as
tuples or lists. It is also possible to provide another Angle object as
input.
Also, if **radians=True** is passed to the constructor, then the input
value is considered as in radians, and converted to degrees.
"""
def __init__(self, *args, **kwargs):
"""Angle constructor.
It takes decimals and sexagesimal input. The sexagesimal angles can be
given as separate degree, minutes, seconds values, or as tuples or
lists. It is also possible to provide another Angle object as input.
If **radians=True** is passed, then the input value is converted from
radians to degrees.
If **ra=True** is passed, then the input value is converted from Right
Ascension to degrees
:param args: Input angle, in decimal or sexagesimal format, or Angle
:type args: int, float, list, tuple, :py:class:`Angle`
:param radians: If True, input angle is in radians. False by default.
:type radians: bool
:param ra: If True, input angle is in Right Ascension. False by default
:type ra: bool
:returns: Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(-13, 30, 0.0)
>>> print(a)
-13.5
>>> b = Angle(a)
>>> print(b)
-13.5
"""
self._deg = 0.0 # Angle value is stored here in decimal format
self._tol = TOL
self.set(*args, **kwargs) # Let's use 'set()' method to set angle
@staticmethod
def reduce_deg(deg):
"""Takes a degree value in decimal format and converts it to a float
value in the +/-[0:360) range.
:param deg: Input degree angle in decimal format.
:type deg: int, float, :py:class:`Angle`
:returns: Float value of the angle in the +/-[0:360) range.
:rtype: float
>>> a = 386.3
>>> b = Angle.reduce_deg(a)
>>> print(round(b, 1))
26.3
"""
if abs(deg) >= 360.0:
# Extract the sign
sign = 1.0 if deg >= 0 else -1.0
frac = abs(deg) % 1 # Separate the fractional part
deg = int(abs(deg)) % 360 # Reduce to [0:360) range
deg = sign * (deg + frac) # Rebuild the value
return float(deg)
@staticmethod
def reduce_dms(degrees, minutes, seconds=0.0):
"""Takes a degree value in sexagesimal format and converts it to a
value in the +/-[0:360) range (degrees) and [0:60) range (minutes and
seconds). It also takes care of fractional degrees and minutes.
:param degrees: Degrees.
:type degrees: int, float
:param minutes: Minutes.
:type minutes: int, float
:param seconds: Seconds. 0.0 by default.
:type seconds: int, float
:returns: Angle in sexagesimal format, with ranges properly adjusted.
:rtype: tuple
>>> print(Angle.reduce_dms(-743.0, 26.0, 49.6))
(23, 26, 49.6, -1.0)
"""
# If any of the input values is negative, the sign is negative
sign = -1.0 if (degrees < 0) or (minutes < 0) or (seconds < 0) else 1.0
degrees = abs(degrees)
minutes = abs(minutes)
seconds = abs(seconds)
# We need to work first from degrees to seconds
if degrees % 1 > 0.0:
# The degrees value has decimals, push them to minutes
minutes += (degrees % 1) * 60.0
degrees = int(degrees) # Keep the integer part
if minutes % 1 > 0.0:
# The minutes value has decimals, push them to seconds
seconds += (minutes % 1) * 60.0
minutes = int(minutes)
# We now need to work from seconds to degrees, because of overflow
if seconds >= 60.0:
minutes += int(seconds / 60.0) # Push the excess to minutes
seconds = seconds % 60 # Keep the rest
if minutes >= 60.0:
degrees += int(minutes / 60.0) # Push the excess to degrees
minutes = minutes % 60 # Keep the rest
degrees = degrees % 360 # Keep degrees in [0:360) range
return (degrees, minutes, seconds, sign)
@staticmethod
def deg2dms(deg):
"""Converts input from decimal to sexagesimal angle format.
:param deg: Degrees decimal format.
:type deg: int, float
:returns: Angle in sexagesimal format, with ranges adjusted.
:rtype: tuple
.. note:: The output format is (Degrees, Minutes, Seconds, sign)
>>> print(Angle.deg2dms(23.44694444))
(23, 26, 48.999983999997596, 1.0)
"""
deg = Angle.reduce_deg(deg) # Reduce the degrees to the [0:360) range
# Extract the sign
sign = 1.0 if deg >= 0 else -1.0
# We have the sign, now let's work with positive numbers
deg = abs(deg)
mi = (deg % 1) * 60.0 # Get the minutes, with decimals
de = int(deg) # Get the integer part of the degrees
se = (mi % 1) * 60.0 # Get the seconds
mi = int(mi)
return (de, mi, se, sign)
@staticmethod
def dms2deg(degrees, minutes, seconds=0.0):
"""Converts an angle from sexagesimal to decimal format.
:param degrees: Degrees.
:type degrees: int, float
:param minutes: Minutes.
:type minutes: int, float
:param seconds: Seconds. 0.0 by default.
:type seconds: int, float
:returns: Angle in decimal format, within +/-[0:360) range.
:rtype: float
>>> print(Angle.dms2deg(-23, 26, 48.999983999997596))
-23.44694444
"""
(de, mi, se, sign) = Angle.reduce_dms(degrees, minutes, seconds)
deg = sign * (de + mi / 60.0 + se / 3600.0)
return float(deg)
def get_tolerance(self):
"""Gets the internal tolerance value used to compare Angles.
.. note:: The default tolerance value is **base.TOL**.
:returns: Internal tolerance.
:rtype: float
"""
return self._tol
def set_tolerance(self, tol):
"""Changes the internal tolerance value used to compare Angles.
:param tol: New tolerance value.
:type tol: int, float
:returns: None
:rtype: None
"""
self._tol = tol
return
def __call__(self):
"""Method used when object is called only with parenthesis.
:returns: The internal value of the Angle object.
:rtype: int, float
>>> a = Angle(54.6)
>>> print(a())
54.6
"""
return self._deg
def __str__(self):
"""Method used when trying to print the object.
:returns: Angle as string.
:rtype: string
>>> a = Angle(12.5)
>>> print(a)
12.5
"""
return str(self._deg)
def __repr__(self):
"""Method providing the 'official' string representation of the object.
It provides a valid expression that could be used to recreate the
object.
:returns: As string with a valid expression to recreate the object
:rtype: string
>>> a = Angle(12.5)
>>> repr(a)
'Angle(12.5)'
"""
return "{}({})".format(self.__class__.__name__, self._deg)
def set(self, *args, **kwargs):
"""Method used to define the value of the Angle object.
It takes decimals and sexagesimal input. The sexagesimal angles can be
given as separate degree, minutes, seconds values, or as tuples or
lists. It is also possible to provide another Angle object as input.
If **radians=True** is passed, then the input value is converted from
radians to degrees
If **ra=True** is passed, then the input value is converted from Right
Ascension to degrees
:param args: Input angle, in decimal or sexagesimal format, or Angle
:type args: int, float, list, tuple, :py:class:`Angle`
:param radians: If True, input angle is in radians. False by default.
:type radians: bool
:param ra: If True, input angle is in Right Ascension. False by default
:type ra: bool
:returns: None.
:rtype: None
:raises: TypeError if input values are of wrong type.
"""
if "ra" in kwargs:
if kwargs["ra"]:
# Input values are a Right Ascension
self.set_ra(*args)
return
# If no arguments are given, internal angle is set to zero
if len(args) == 0:
self._deg = 0.0
return
# If we have only one argument, it can be a single value, a tuple/list
# or an Angle
elif len(args) == 1:
deg = args[0]
if isinstance(deg, Angle): # Copy constructor
self._deg = deg._deg
self._tol = deg._tol
return
if isinstance(deg, (int, float)):
if "radians" in kwargs:
if kwargs["radians"]:
# Input value is in radians. Convert to degrees
deg = degrees(deg)
# This works for ints, floats and Angles
self._deg = Angle.reduce_deg(deg)
return
elif isinstance(deg, (list, tuple)):
if len(deg) == 0:
raise TypeError("Invalid input value")
elif len(deg) == 1:
# This is a single value
if "radians" in kwargs:
if kwargs["radians"]:
# Input value is in radians. Convert to degrees
deg[0] = degrees(deg[0])
self._deg = Angle.reduce_deg(deg[0])
return
elif len(deg) == 2:
# Seconds value is set to zero
self._deg = Angle.dms2deg(deg[0], deg[1])
return
elif len(deg) == 3:
# The first three values are taken into account
self._deg = Angle.dms2deg(deg[0], deg[1], deg[2])
return
else:
# Only the first four values are taken into account
sign = (
-1.0
if deg[0] < 0 or deg[1] < 0 or deg[2] < 0 or deg[3] < 0
else 1.0
)
# If sign < 0, make all values negative, to be sure
deg0 = sign * abs(deg[0])
deg1 = sign * abs(deg[1])
deg2 = sign * abs(deg[2])
self._deg = Angle.dms2deg(deg0, deg1, deg2)
return
else:
raise TypeError("Invalid input value")
elif len(args) == 2:
# Seconds value is set to zero
self._deg = Angle.dms2deg(args[0], args[1])
return
elif len(args) == 3:
# The first three values are taken into account
self._deg = Angle.dms2deg(args[0], args[1], args[2])
return
else:
# Only the first four values are taken into account
sign = (
-1.0
if args[0] < 0 or args[1] < 0 or args[2] < 0 or args[3] < 0
else 1.0
)
# If sign < 0, make all values negative, to be sure
args0 = sign * abs(args[0])
args1 = sign * abs(args[1])
args2 = sign * abs(args[2])
self._deg = Angle.dms2deg(args0, args1, args2)
return
def set_radians(self, rads):
"""Method to define the value of the Angle object from radians.
:param rads: Input angle, in radians.
:type rads: int, float
:returns: None.
:rtype: None
:raises: TypeError if input value is of wrong type.
>>> a = Angle()
>>> a.set_radians(pi)
>>> print(a)
180.0
"""
self.set(rads, radians=True)
return
def set_ra(self, *args):
"""Define the value of the Angle object from a Right Ascension.
It takes decimals and sexagesimal input. The sexagesimal Right
Ascensions can be given as separate hours, minutes, seconds values, or
as tuples or lists.
:param args: Input Right Ascension, in decimal or sexagesimal format.
:type args: int, float, list, tuple
:returns: None.
:rtype: None
:raises: TypeError if input values are of wrong type.
>>> a = Angle()
>>> a.set_ra(9, 14, 55.8)
>>> print(a)
138.7325
"""
self.set(*args) # Carry out a standard set(), without *kwargs
self._deg *= 15.0 # Multipy Right Ascension by 15.0 to get degrees
return
def dms_str(self, fancy=True, n_dec=-1):
"""Returns the Angle value as a sexagesimal string.
The parameter **fancy** allows to print in "Dd M' S''" format if True,
and in "D:M:S" (easier to parse) if False. On the other hand, the
**n_dec** parameter sets the number of decimals used to print the
seconds. Set to a negative integer to disable (default).
:param fancy: Format of output string. True by default.
:type fancy: bool
:param n_dec: Number of decimals used to print the seconds
:type fancy: int
:returns: Angle value as string in sexagesimal format.
:rtype: string
:raises: TypeError if input value is of wrong type.
>>> a = Angle(42.75)
>>> print(a.dms_str())
42d 45' 0.0''
>>> print(a.dms_str(fancy=False))
42:45:0.0
>>> a = Angle(49, 13, 42.4817)
>>> print(a.dms_str(n_dec=2))
49d 13' 42.48''
"""
if not isinstance(n_dec, int):
raise TypeError("Invalid input value")
d, m, s, sign = Angle.deg2dms(self._deg)
if n_dec >= 0:
s = round(s, n_dec)
if abs(s - 60.0) < TOL:
s = 0.0
m += 1
if abs(m - 60.0) < TOL:
m = 0
d += 1.0
if d >= 360.0:
d -= 360.0
if fancy:
if d != 0:
return "{}d {}' {}''".format(int(sign * d), m, s)
elif m != 0:
return "{}' {}''".format(int(sign * m), s)
elif s != 0.0:
return "{}''".format(sign * s)
else:
return "0d 0' 0.0''"
else:
if d != 0:
return "{}:{}:{}".format(int(sign * d), m, s)
elif m != 0:
return "0:{}:{}".format(int(sign * m), s)
elif s != 0.0:
return "0:0:{}".format(sign * s)
else:
return "0:0:0.0"
def get_ra(self):
"""Returns the Angle value as a Right Ascension in float format
:returns: The internal value of the Angle object as Right Ascension.
:rtype: int, float
>>> a = Angle(138.75)
>>> print(a.get_ra())
9.25
"""
return self._deg / 15.0
def ra_str(self, fancy=True, n_dec=-1):
"""Returns the Angle value as a sexagesimal string in Right Ascension.
The parameter **fancy** allows to print in "Hh M' S''" format if True,
and in "H:M:S" (easier to parse) if False. On the other hand, the
**n_dec** parameter sets the number of decimals used to print the
seconds. Set to a negative integer to disable (default).
:param fancy: Format of output string. True by default.
:type fancy: bool
:param n_dec: Number of decimals used to print the seconds
:type fancy: int
:returns: Angle value as Right Ascension in sexagesimal format.
:rtype: string
:raises: TypeError if input value is of wrong type.
>>> a = Angle(138.75)
>>> print(a.ra_str())
9h 15' 0.0''
>>> print(a.ra_str(fancy=False))
9:15:0.0
>>> a = Angle(2, 44, 11.98581, ra=True)
>>> print(a.ra_str(n_dec=3))
2h 44' 11.986''
"""
a = Angle(self()) / 15.0
s = a.dms_str(fancy, n_dec)
if fancy:
s = s.replace("d", "h")
return s
def rad(self):
"""Returns the Angle value in radians.
:returns: Angle value in radians.
:rtype: float
>>> a = Angle(47.762)
>>> print(round(a.rad(), 8))
0.83360416
"""
return radians(self._deg)
def dms_tuple(self):
"""Returns the Angle as a tuple containing (degrees, minutes, seconds,
sign).
:returns: Angle value as (degrees, minutes, seconds, sign).
:rtype: tuple
"""
return Angle.deg2dms(self())
def ra_tuple(self):
"""Returns the Angle in Right Ascension format as a tuple containing
(hours, minutes, seconds, sign).
:returns: Angle value as RA in (hours, minutes, seconds, sign) format.
:rtype: tuple
"""
return Angle.deg2dms(self() / 15.0)
def to_positive(self):
"""Converts the internal angle value from negative to positive.
:returns: This angle object.
:rtype: :py:class:`Angle`
>>> a = Angle(-87.32)
>>> print(a.to_positive())
272.68
"""
if self._deg < 0:
self._deg = 360.0 - abs(self._deg)
return self
def __eq__(self, b):
"""This method defines the 'is equal' operator between Angles.
.. note:: For the comparison, the internal tolerance value is used.
:returns: A boolean.
:rtype: bool
:raises: TypeError if input values are of wrong type.
>>> a = Angle(172.01)
>>> b = Angle(172.009)
>>> a == b
False
"""
if isinstance(b, (int, float)):
return abs(self._deg - float(b)) < self._tol
elif isinstance(b, Angle):
return abs(self._deg - b._deg) < self._tol
else:
raise TypeError("Wrong operand type")
def __ne__(self, b):
"""This method defines the 'is not equal' operator between Angles.
.. note:: For the comparison, the internal tolerance value is used.
:returns: A boolean.
:rtype: bool
>>> a = Angle(11.200001)
>>> b = Angle(11.200000)
>>> a != b
True
"""
return not self.__eq__(b) # '!=' == 'not(==)'
def __lt__(self, b):
"""This method defines the 'is less than' operator between Angles.
:returns: A boolean.
:rtype: bool
:raises: TypeError if input values are of wrong type.
>>> a = Angle(72.0)
>>> b = Angle(72.0)
>>> a < b
False
"""
if isinstance(b, (int, float)):
return self._deg < float(b)
elif isinstance(b, Angle):
return self._deg < b._deg
else:
raise TypeError("Wrong operand type")
def __ge__(self, b):
"""This method defines 'is equal or greater' operator between Angles.
:returns: A boolean.
:rtype: bool
:raises: TypeError if input values are of wrong type.
>>> a = Angle(172.01)
>>> b = Angle(172.009)
>>> a >= b
True
"""
return not self.__lt__(b) # '>=' == 'not(<)'
def __gt__(self, b):
"""This method defines the 'is greater than' operator between Angles.
:returns: A boolean.
:rtype: bool
:raises: TypeError if input values are of wrong type.
>>> a = Angle(172.01)
>>> b = Angle(172.009)
>>> a > b
True
"""
if isinstance(b, (int, float)):
return self._deg > float(b)
elif isinstance(b, Angle):
return self._deg > b._deg
else:
raise TypeError("Wrong operand type")
def __le__(self, b):
"""This method defines 'is equal or less' operator between Angles.
:returns: A boolean.
:rtype: bool
:raises: TypeError if input values are of wrong type.
>>> a = Angle(72.0)
>>> b = Angle(72.0)
>>> a <= b
True
"""
return not self.__gt__(b) # '<=' == 'not(>)'
def __neg__(self):
"""This method is used to obtain the negative version of this Angle.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
>>> a = Angle(-11.2)
>>> print(-a)
11.2
"""
return Angle(-self._deg)
def __abs__(self):
"""This method is used to obtain the absolute value of this Angle.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
>>> a = Angle(-303.67)
>>> print(abs(a))
303.67
"""
return Angle(abs(self._deg))
def __mod__(self, b):
"""This method is used to obtain the module b of this Angle.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(333.0)
>>> b = Angle(72.0)
>>> print(a % b)
45.0
"""
# Negative values will be treated as if they were positive
sign = 1.0 if self._deg >= 0.0 else -1.0
if isinstance(b, (int, float)):
return Angle(sign * (abs(self._deg) % b))
elif isinstance(b, Angle):
return Angle(sign * (abs(self._deg) % b._deg))
else:
raise TypeError("Wrong operand type")
def __add__(self, b):
"""This method defines the addition between Angles.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(83.1)
>>> b = Angle(18.4)
>>> print(a + b)
101.5
"""
if isinstance(b, (int, float)):
return Angle(self._deg + float(b))
elif isinstance(b, Angle):
return Angle(self._deg + b._deg)
else:
raise TypeError("Wrong operand type")
def __sub__(self, b):
"""This method defines the subtraction between Angles.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(25.4)
>>> b = Angle(10.2)
>>> print(a - b)
15.2
"""
return self.__add__(-b)
def __mul__(self, b):
"""This method defines the multiplication between Angles.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(33.0)
>>> b = Angle(72.0)
>>> print(a * b)
216.0
"""
if isinstance(b, (int, float)):
return Angle(self._deg * float(b))
elif isinstance(b, Angle):
return Angle(self._deg * b._deg)
else:
raise TypeError("Wrong operand type")
def __div__(self, b):
"""This method defines the division between Angles.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: ZeroDivisionError if divisor is zero.
:raises: TypeError if input values are of wrong type.
>>> a = Angle(172.0)
>>> b = Angle(86.0)
>>> print(a/b)
2.0
"""
if b == 0.0:
raise ZeroDivisionError("Division by zero is not allowed")
if isinstance(b, (int, float)):
return Angle(self._deg / float(b))
elif isinstance(b, Angle):
return Angle(self._deg / b._deg)
else:
raise TypeError("Wrong operand type")
def __truediv__(self, b):
"""This method defines the division between Angles (Python 3).
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: ZeroDivisionError if divisor is zero.
:raises: TypeError if input values are of wrong type.
:see: __div__
"""
return self.__div__(b)
def __pow__(self, b):
"""This method defines the power operation for Angles.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(12.5)
>>> b = Angle(4.0)
>>> print(a ** b)
294.0625
"""
if isinstance(b, (int, float)):
return Angle(self._deg ** b)
elif isinstance(b, Angle):
return Angle(self._deg ** b._deg)
else:
raise TypeError("Wrong operand type")
def __imod__(self, b):
"""This method defines the accumulative module b of this Angle.
:returns: This Angle.
:rtype: :py:class:`Angle`
>>> a = Angle(330.0)
>>> b = Angle(45.0)
>>> a %= b
>>> print(a)
15.0
"""
# Negative values will be treated as if they were positive
self = self % b
return self
def __iadd__(self, b):
"""This method defines the accumulative addition to this Angle.
:returns: This Angle.
:rtype: :py:class:`Angle`
>>> a = Angle(172.1)
>>> b = Angle(54.6)
>>> a += b
>>> print(a)
226.7
"""
self = self + b
return self
def __isub__(self, b):
"""This method defines the accumulative subtraction to this Angle.
:returns: This Angle.
:rtype: :py:class:`Angle`
>>> a = Angle(97.0)
>>> b = Angle(39.0)
>>> a -= b
>>> print(a)
58.0
"""
self = self - b
return self
def __imul__(self, b):
"""This method defines the accumulative multiplication to this Angle.
:returns: This Angle.
:rtype: :py:class:`Angle`
>>> a = Angle(30.0)
>>> b = Angle(55.0)
>>> a *= b
>>> print(a)
210.0
"""
self = self * b
return self
def __idiv__(self, b):
"""This method defines the accumulative division to this Angle.
:returns: This Angle.
:rtype: :py:class:`Angle`
:raises: ZeroDivisionError if divisor is zero.
:raises: TypeError if input values are of wrong type.
>>> a = Angle(330.0)
>>> b = Angle(30.0)
>>> a /= b
>>> print(a)
11.0
"""
if b == 0.0:
raise ZeroDivisionError("Division by zero is not allowed")
if not isinstance(b, (int, float, Angle)):
raise TypeError("Wrong operand type")
self = self / b
return self
def __itruediv__(self, b):
"""This method defines accumulative division to this Angle (Python3).
:returns: This Angle.
:rtype: :py:class:`Angle`
:raises: ZeroDivisionError if divisor is zero.
:raises: TypeError if input values are of wrong type.
:see: __idiv__
"""
return self.__idiv__(b)
def __ipow__(self, b):
"""This method defines the accumulative power to this Angle.
:returns: This Angle.
:rtype: :py:class:`Angle`
>>> a = Angle(37.0)
>>> b = Angle(3.0)
>>> a **= b
>>> print(a)
253.0
"""
self = self ** b
return self
def __rmod__(self, b):
"""This method defines module operation between Angles by the right.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
>>> a = Angle(80.0)
>>> print(350 % a)
30.0
"""
if isinstance(b, (int, float)):
b = Angle(b)
# Negative values will be treated as if they were positive
sign = 1.0 if b._deg >= 0.0 else -1.0
return Angle(sign * (abs(b._deg) % self._deg))
def __radd__(self, b):
"""This method defines the addition between Angles by the right
:returns: A new Angle object.
:rtype: :py:class:`Angle`
>>> a = Angle(83.1)
>>> print(8.5 + a)
91.6
"""
return self.__add__(b) # In this case, it is the same as by the left
def __rsub__(self, b):
"""This method defines the subtraction between Angles by the right.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(25.0)
>>> print(24.0 - a)
-1.0
"""
return -self.__sub__(b) # b - a = -(a - b)
def __rmul__(self, b):
"""This method defines multiplication between Angles by the right.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(11.0)
>>> print(250.0 * a)
230.0
"""
return self.__mul__(b) # In this case, it is the same as by the left
def __rdiv__(self, b):
"""This method defines division between Angles by the right.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: ZeroDivisionError if divisor is zero.
:raises: TypeError if input values are of wrong type.
>>> a = Angle(80.0)
>>> print(350 / a)
4.375
"""
if self == 0.0:
raise ZeroDivisionError("Division by zero is not allowed")
if isinstance(b, (int, float)):
return Angle(float(b) / self._deg)
elif isinstance(b, Angle):
return Angle(b._deg / self._deg)
else:
raise TypeError("Wrong operand type")
def __rtruediv__(self, b):
"""This method defines division between Angle by the right (Python3).
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: ZeroDivisionError if divisor is zero.
:raises: TypeError if input values are of wrong type.
:see: __rdiv__
"""
return self.__rdiv__(b)
def __rpow__(self, b):
"""This method defines the power operation for Angles by the right.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
:raises: TypeError if input values are of wrong type.
>>> a = Angle(5.0)
>>> print(24.0 ** a)
144.0
"""
if isinstance(b, (int, float)):
return Angle(b ** self._deg)
elif isinstance(b, Angle):
return Angle(b._deg ** self._deg)
else:
raise TypeError("Wrong operand type")
def __float__(self):
"""This method returns Angle value as a float.
:returns: Internal angle value as a float.
:rtype: float
>>> a = Angle(213.8)
>>> float(a)
213.8
"""
return float(self._deg)
def __int__(self):
"""This method returns Angle value as an int.
:returns: Internal angle value as an int.
:rtype: int
>>> a = Angle(213.8)
>>> int(a)
213
"""
return int(self._deg)
def __round__(self, n=0):
"""This method returns an Angle with content rounded to 'n' decimal.
:returns: A new Angle object.
:rtype: :py:class:`Angle`
>>> a = Angle(11.4361)
>>> print(round(a, 2))
11.44
"""
# NOTE: This method is only called in Python 3
return Angle(round(self._deg, n))
def main():
# Let's define a small helper function
def print_me(msg, val):
print("{}: {}".format(msg, val))
# Let's show some uses of Angle class
print("\n" + 35 * "*")
print("*** Use of Angle class")
print(35 * "*" + "\n")
# Create an Angle object, providing degrees, minutes and seconds
a = Angle(-23.0, 26.0, 48.999983999)
print("a = Angle(-23.0, 26.0, 48.999983999)")
# First we print using the __call__ method (note the extra parentheses)
print_me("The angle 'a()' is", a()) # -23.44694444
# Second we print using the __str__ method (no extra parentheses needed)
print_me("The angle 'a' is", a) # -23.44694444
print("")
# Use the copy constructor
b = Angle(a)
print_me("Angle 'b', which is a copy of 'a', is", b)
print("")
# Use the static 'deg2dms()' method to carry out conversions
d, m, s, sign = Angle.deg2dms(23.44694444)
val = "{}d {}' {}''".format(int(sign * d), m, s)
print_me("{Deg}d {Min}' {Sec}''", val) # 23d 26' 48.999984''
# We can print Angle 'a' directly in sexagesimal format
# In 'fancy' format: # -23d 26' 48.999984''
print_me("{Deg}d {Min}' {Sec}''", a.dms_str(n_dec=6))
# In plain format:
print_me("{Deg}:{Min}:{Sec}", a.dms_str(False, 6)) # -23:26:48.999983999
print("")
# Print directly as a tuple
a = Angle(23.44694444)
print_me("a.dms_tuple()", a.dms_tuple())
print_me("a.ra_tuple()", a.ra_tuple())
print("")
# Redefine Angle 'a' several times
a.set(-0.44694444)
print("a.set(-0.44694444)")
print_me(" a.dms_str()", a.dms_str()) # -26' 48.999984''
a.set(0, 0, -46.31)
print("a.set(0, 0, -46.31)")
print_me(" a.dms_str(False)", a.dms_str(False)) # 0:0:-46.31
print("")
# We can use decimals in degrees/minutes. They are converted automatically
a.set(0, -46.25, 0.0)
print("a.set(0, -46.25, 0.0)")
print_me(" a.dms_str()", a.dms_str()) # -46' 15.0''
a.set(0, 0, 0.0)
print("a.set(0, 0, 0.0)")
print_me(" a.dms_str()", a.dms_str()) # 0d 0' 0.0''
print("")
# We can define the angle as in radians. It will be converted to degrees
b = Angle(pi, radians=True)
print_me("b = Angle(pi, radians=True); print(b)", b) # 180.0
# And we can easily carry out the 'degrees to radians' conversion
print_me("print(b.rad())", b.rad()) # 3.14159265359
print("")
# We can also specify the angle as a Right Ascension
print("Angle can be given as a Right Ascension: Hours, Minutes, Seconds")
a.set_ra(9, 14, 55.8)
print("a.set_ra(9, 14, 55.8)")
print_me(" print(a)", a)
b = Angle(9, 14, 55.8, ra=True)
print("b = Angle(9, 14, 55.8, ra=True)")
print_me(" print(b)", b)
print("")
# We can print the Angle as Right Ascension, as a float and as string
a = Angle(138.75)
print("a = Angle(138.75)")
print_me(" print(a.get_ra())", a.get_ra())
print_me(" print(a.ra_str())", a.ra_str())
print_me(" print(a.ra_str(False))", a.ra_str(False))
print("")
# Use the 'to_positive()' method to get the positive version of an angle
a = Angle(-87.32) # 272.68
print("a = Angle(-87.32)")
print_me(" print(a.to_positive())", a.to_positive())
print("")
# Call the __repr__() method to get a string defining the current object
# This string can then be fed to 'eval()' function to generate the object
print_me("print(b.__repr__())", b.__repr__()) # Angle(138.7325)
c = eval(repr(b))
print_me("c = eval(repr(b)); print(c)", c) # 138.7325
print("")
print_me("c", c) # 138.7325
# Negate an angle
d = Angle(13, 30)
print_me("d", d) # 13.5
e = -d
print_me(" e = -d", e) # -13.5
# Get the absolute value of an angle
e = abs(e)
print_me(" e = abs(e)", e) # 13.5
# Module operation on an angle
d = Angle(17.0)
print_me("d", d) # 17.0
e = c % d
print_me(" e = c % d", e) # 10.0
print("")
# Convert the angle to an integer
d = Angle(13.95)
print_me("d", d) # 13.95
print_me(" int(d)", int(d)) # 13.0
d = Angle(-4.95)
print_me("d", d) # -4.95
print_me(" int(d)", int(d)) # -4.0
# Convert the angle to a float
print_me(" float(d)", float(d)) # -4.95
# Round the angle to a float
e = Angle(-4.951648)
print_me("e", e) # -4.951648
print_me(" round(e)", round(e)) # -5.0
print_me(" round(e, 2)", round(e, 2)) # -4.95
print_me(" round(e, 3)", round(e, 3)) # -4.952
print_me(" round(e, 4)", round(e, 4)) # -4.9516
print("")
# Comparison operators
print_me(" d == e", d == e) # False
print_me(" d != e", d != e) # True
print_me(" d > e", d > e) # True
print_me(" c >= 180.0", c >= 180.0) # False
print_me(" c < 180.0", c < 180.0) # True
print_me(" c <= 180.0", c <= 180.0) # True
print("")
# It is very easy to add Angles to obtain a new Angle
e = c + d
print_me(" c + d", e) # 133.7825
# We can also directly add a decimal angle
e = c + 11.5
print_me(" c + 11.5", e) # 150.2325
print("")
# Types allowed are int, float and Angle
print('e = c + "32.5"')
try:
e = c + "32.5"
except TypeError:
print("TypeError!: Valid types are int, float, and Angle, not string!")
print("")
# Subtraction
e = c - d
print_me(" c - d", e) # 143.6825
# Multiplication
c.set(150.0)
d.set(5.0)
print_me("c", c) # 150.0
print_me("d", d) # 5.0
e = c * d
print_me(" c * d", e) # 30.0
# Division
c.set(150.0)
d.set(6.0)
print_me("d", d) # 6.0
e = c / d
print_me(" c / d", e) # 25.0
print("")
# Division by zero is not allowed
d.set(0.0)
print_me("d", d) # 0.0
print("e = c / d")
try:
e = c / d
except ZeroDivisionError:
print("ZeroDivisionError!: Division by zero is not allowed!")
print("")
# Power
d.set(2.2)
print_me("d", d) # 2.2
e = c ** d
print_me(" c ** d", e) # 91.57336709992524
print("")
# Accumulative module operation
d.set(17.0)
print_me("d", d) # 17.0
e %= d
print_me(" e %= d", e) # 6.573367099925235
# Accumulative addition
c += d
print_me(" c += d", c) # 167.0
# Accumulative subtraction
print_me("b", b) # 138.7325
c -= b
print_me(" c -= b", c) # 28.2675
# Accumulative multiplication
print_me("b", b) # 138.7325
c *= b
print_me(" c *= b", c) # 321.62094375
# Accumulative division
print_me("b", b) # 138.7325
d.set(6.0)
print_me("d", d) # 6.0
b /= d
print_me(" b /= d", b) # 23.1220833333
# Accumulative power
d.set(2.2)
print_me("d", d) # 2.2
c = abs(c)
print_me(" c = abs(c)", c) # 321.62094375
c **= d
print_me(" c **= d", c) # 254.307104203
print("")
# The same operation, but by the right side
e = 3.5 + b
print_me(" e = 3.5 + b", e) # 26.6220833333
e = 3.5 - b
print_me(" e = 3.5 - b", e) # -19.6220833333
e = 3.5 * b
print_me(" e = 3.5 * b", e) # 80.9272916667
e = 3.5 / b
print_me(" e = 3.5 / b", e) # 0.151370443119
e = 3.5 ** b
print_me(" e = 3.5 ** b", e) # 260.783691406
if __name__ == "__main__":
main()