"""For miscellaneous conversions."""
import time
import datetime
import zoneinfo
import numpy as np
import astropy.coordinates as ap_coordinates
import astropy.time as ap_time
import astropy.units as ap_units
import opihiexarata.library as library
import opihiexarata.library.error as error
import opihiexarata.library.hint as hint
[docs]
def degrees_per_second_to_arcsec_per_second(degree_per_second: float) -> float:
"""This converts from degrees per second to arcseconds per second.
Parameters
----------
degree_per_second : float
The value, in degrees per second, which you want to convert from.
Returns
-------
arcsec_per_second : float
The value, in arcseconds per second, which you converted to.
"""
# A simple multiplicative conversion.
arcsec_per_second = degree_per_second * 3600
return arcsec_per_second
[docs]
def degrees_to_sexagesimal_ra_dec(
ra_deg: float, dec_deg: float, precision: int = 2
) -> tuple[str, str]:
"""Convert RA and DEC degree measurements to the more familiar HMSDMS
sexagesimal format.
Parameters
----------
ra_deg : float
The right ascension in degrees.
dec_deg : float
The declination in degrees.
precision : int
The precision of the conversion's seconds term, i.e. how many numbers
are used.
Returns
-------
ra_sex : str
The right ascension in hour:minute:second sexagesimal.
dec_sex : str
The declination in degree:minute:second sexagesimal.
"""
# Levering Astropy for this.
skycoord = ap_coordinates.SkyCoord(ra_deg, dec_deg, frame="icrs", unit="deg")
ra_sex = skycoord.ra.to_string(
ap_units.hour, sep=":", pad=True, precision=precision
)
dec_sex = skycoord.dec.to_string(
ap_units.deg, sep=":", pad=True, precision=precision, alwayssign=True
)
return ra_sex, dec_sex
[docs]
def sexagesimal_ra_dec_to_degrees(ra_sex: str, dec_sex: str) -> tuple[float, float]:
"""Convert RA and DEC measurements from the more familiar HMSDMS
sexagesimal format to degrees.
Parameters
----------
ra_sex : str
The right ascension in hour:minute:second sexagesimal.
dec_sex : str
The declination in degree:minute:second sexagesimal.
Returns
-------
ra_deg : float
The right ascension in degrees.
dec_deg : float
The declination in degrees.
"""
# Levering Astropy for this.
skycoord = ap_coordinates.SkyCoord(
ra_sex, dec_sex, frame="icrs", unit=(ap_units.hourangle, ap_units.deg)
)
ra_deg = float(skycoord.ra.degree)
dec_deg = float(skycoord.dec.degree)
return ra_deg, dec_deg
[docs]
def decimal_day_to_julian_day(year: int, month: int, day: float):
"""A function to convert decimal day time formats to the Julian day.
Parameters
----------
year : int
The year of the time stamp to be converted.
month : int
The month of the time stamp to be converted.
day : float
The day of the time stamp to be converted, may be decimal, the
values are just passed down.
Returns
-------
julian_day : float
The Julian day of the date provided.
"""
# Determining the true year and letting the remainder flow.
int_year = np.array(year, dtype=int)
int_month = np.array(month, dtype=int)
int_day = np.array(day, dtype=int)
float_day = np.array(day, dtype=float)
# Leftover to determine HMS.
leftover_hours = (float_day % 1) * 24
# Hours
int_hour = np.array(leftover_hours, dtype=int)
leftover_minutes = (leftover_hours % 1) * 60
# Minutes
int_minute = np.array(leftover_minutes, dtype=int)
leftover_seconds = (leftover_minutes % 1) * 60
# Seconds.
seconds = leftover_seconds
# Julian time.
julian_day = full_date_to_julian_day(
year=int_year,
month=int_month,
day=int_day,
hour=int_hour,
minute=int_minute,
second=seconds,
)
return julian_day
[docs]
def full_date_to_julian_day(
year: int, month: int, day: int, hour: int, minute: int, second: float
) -> float:
"""A function to convert the a whole date format into the Julian day time.
Parameters
----------
year : int
The year of the time stamp to be converted.
month : int
The month of the time stamp to be converted.
day : int
The day of the time stamp to be converted.
hour : int
The hour of the time stamp to be converted.
minute : int
The minute of the time stamp to be converted.
second : float
The second of the time stamp to be converted.
Returns
-------
julian_day : float
The time input converted into the Julian day.
"""
time_param = {
"year": year,
"month": month,
"day": day,
"hour": hour,
"minute": minute,
"second": second,
}
time_instance = ap_time.Time(time_param, scale="utc", format="ymdhms")
julian_day = time_instance.to_value("jd", subfmt="long")
return julian_day
[docs]
def modified_julian_day_to_julian_day(mjd: float) -> float:
"""A function to convert modified Julian days to Julian days.
Parameters
----------
mjd : float
The modified Julian day to be converted to a Julian day.
Returns
-------
jd : float
The Julian day value after conversion.
"""
time_instance = ap_time.Time(mjd, format="mjd")
jd = float(time_instance.to_value("jd", subfmt="float"))
return jd
[docs]
def julian_day_to_modified_julian_day(jd: float) -> float:
"""A function to convert Julian days to modified Julian days.
Parameters
----------
jd : float
The Julian day to be converted to a modified Julian day.
Returns
-------
mjd : float
The modified Julian day value after conversion.
"""
time_instance = ap_time.Time(jd, format="mjd")
mjd = float(time_instance.to_value("mjd", subfmt="float"))
return mjd
[docs]
def julian_day_to_unix_time(jd: float) -> float:
"""A function to convert between Julian days to UNIX time.
Parameters
----------
jd : float
The Julian day to be converted.
Returns
-------
unix_time : float
The time converted to UNIX time.
"""
# This could eventually be replaced with multiplication and addition, but
# this is a convenient way of doing it.
time_instance = ap_time.Time(jd, format="jd")
unix_time = np.asarray(time_instance.to_value("unix"))
return unix_time
[docs]
def unix_time_to_julian_day(unix_time: float) -> float:
"""A function to convert between julian days to Unix time.
Parameters
----------
unix_time : float
The UNIX time to be converted to a Julian day.
Returns
-------
jd : float
The Julian day value as converted.
"""
# This could eventually be replaced with multiplication and addition, but
# this is a convenient way of doing it.
time_instance = ap_time.Time(unix_time, format="unix")
jd = np.asarray(time_instance.to_value("jd"))
return jd
[docs]
def julian_day_to_decimal_day(jd: float) -> tuple:
"""A function to convert the Julian day time to the decimal day time.
Parameters
----------
jd : float
The Julian day time that is going to be converted.
Returns
-------
year : int
The year of the Julian day time.
month : int
The month of the Julian day time.
day : float
The day of the the Julian day time, the hours, minute, and seconds are all
contained as a decimal.
"""
# Getting the full date and just converting it from there.
year, month, int_day, hour, minute, second = julian_day_to_full_date(jd=jd)
# Calculating the decimal day.
day = int_day + hour / 24 + minute / 1440 + second / 86400
# All done.
return year, month, day
[docs]
def julian_day_to_full_date(jd: float) -> tuple[int, int, int, int, int, float]:
"""A function to convert the Julian day to a full date time.
Parameters
----------
jd : float
The Julian day time that is going to be converted.
Returns
-------
year : int
The year of the Julian day provided.
month : int
The month of the Julian day provided.
day : int
The day of the Julian day provided.
hour : int
The hour of the Julian day provided.
minute : int
The minute of the Julian day provided.
second : float
The second of the Julian day provided.
"""
time_instance = ap_time.Time(jd, format="jd")
# It is faster to not unpack, even though it is less readable.
return tuple(time_instance.to_value("ymdhms"))
[docs]
def current_utc_to_julian_day() -> float:
"""Return the current UTC time when this function is called as a Julian day
time.
Parameters
----------
None
Returns
-------
current_jd : float
The current time in Julian date format.
"""
# We can just derive it from the system UNIX time.
current_unix_time = time.time()
current_jd = unix_time_to_julian_day(unix_time=current_unix_time)
return current_jd
[docs]
def datetime_timezone_1_to_timezone_2(
from_datetime: hint.Union[hint.datetime, str], from_timezone: str, to_timezone: str
) -> hint.datetime:
"""This function converts a date time from one timezone to another timezone.
If the datetime provided is a string of an ISO 8601 time, we convert it,
otherwise, we raise. The timezones provided can be any IANA timezone.
Parameters
----------
from_datetime : datetime or str
The data time, or string representation of one to convert.
from_timezone : str
The timezone which date_time is currently in.
to_timezone : str
The timezone which we are converting to.
Returns
-------
to_datetime : datetime
The datetime after the conversion.
"""
# Check if the datetime is really a datetime or a string representation
# thereof.
if isinstance(from_datetime, datetime.datetime):
# All good.
pass
elif isinstance(from_datetime, str):
# Try and convert.
try:
from_datetime = datetime.datetime.fromisoformat(from_datetime)
except ValueError:
raise error.InputError(
"The string format of the datetime is not valid and cannot be"
" converted. We require it to be in an ISO 8601-like format. The input:"
" {inp}".format(inp=from_datetime)
)
else:
raise error.InputError(
"The datetime input provided is neither a datetime or a datetime string to"
" be converted. The input: {inp}".format(inp=from_datetime)
)
# Adding the important timezone information, using the built in class.
from_timezone = zoneinfo.ZoneInfo(key=from_timezone)
to_timezone = zoneinfo.ZoneInfo(key=to_timezone)
# Adding it to the datetime.
from_datetime_zone_aware = datetime.datetime(
year=from_datetime.year,
month=from_datetime.month,
day=from_datetime.day,
hour=from_datetime.hour,
minute=from_datetime.minute,
second=from_datetime.second,
microsecond=from_datetime.microsecond,
tzinfo=from_timezone,
)
# Converting it to a different timezone.
to_datetime = from_datetime_zone_aware.astimezone(to_timezone)
return to_datetime
[docs]
def string_month_to_number(month_str: str) -> int:
"""A function to convert from the name of a month to the month number.
This is just because it is easy to have here and to add a package import
for something like this is silly.
Parameters
----------
month_str : string
The month name. If it is exactly three characters in length, we assume
it is abbreviated month names and search through that instead.
Returns
-------
month_int : int
The month number integer.
"""
# Capitalization does not matter.
month_str = month_str.casefold()
# There is no real need to import a new package.
month_dict = {
"january": 1,
"february": 2,
"march": 3,
"april": 4,
"may": 5,
"june": 6,
"july": 7,
"august": 8,
"september": 9,
"october": 10,
"november": 11,
"december": 12,
}
# It could also be a 3 letter month abbreviation.
three_letter_month_dict = {
"jan": 1,
"feb": 2,
"mar": 3,
"apr": 4,
"may": 5,
"jun": 6,
"jul": 7,
"aug": 8,
"sep": 9,
"oct": 10,
"nov": 11,
"dec": 12,
}
if len(month_str) == 3:
# It is likely a 3 letter month abbreviation, or May.
month_int = three_letter_month_dict[month_str]
else:
month_int = month_dict[month_str]
return month_int
[docs]
def numpy_type_string_to_instance(numpy_type_string: str) -> hint.numpy_generic:
"""Provided an input string which is supposed to represent a Numpy type,
this function provides the type instance itself.
The main reason for breaking this out into a new function is in the
event that aliases needs to be made.
Parameters
----------
numpy_type_string : str
The Numpy type, provided as a string which is formatted mostly as
numpy.{type_string}. However, should alias exist, they will also be
handled.
Returns
-------
numpy_type_instance : Numpy generic
The data type instance that the string is likely referring to.
"""
# It is likely easiest to do a falling cascade for both readability and
# form. We check all of the types to known aliases or results.
np_t_str = str(numpy_type_string).casefold()
# Staring with integers.
if np_t_str in ("short", "int16", "h"):
numpy_type_instance = np.short
elif np_t_str in ("intc", "int32", "i"):
numpy_type_instance = np.intc
elif np_t_str in ("int_", "int64", "intp", "l"):
numpy_type_instance = np.intc
# Now onto floats.
elif np_t_str in ("half", "float16", "e"):
numpy_type_instance = np.half
elif np_t_str in ("single", "float32", "f"):
numpy_type_instance = np.single
elif np_t_str in ("", "double", "float_", "float64", "d"):
numpy_type_instance = np.double
elif np_t_str in ("longdouble", "longfloat", "float128", "g"):
numpy_type_instance = np.longdouble
# No matching type.
else:
# We default to Numpy's default.
numpy_type_instance = np.float_
# All done.
return numpy_type_instance