defmodule Astrex do
import Math
alias Astrex.Common, as: C
alias Astrex.Types, as: T
alias Astrex.Astro.Transforms, as: Tr
# https://geomag.bgs.ac.uk/data_service/models_compass/wmm_calc.html
# calculates all parameters
@moduledoc """
## Introduction
The Astrex library provides functions that perform astronomical calculations of various kinds.
- az2eq converts from horizontal to equatorial coordinates
- eq2az converts from equatorial to horizontal coordinates
- where_is returns the equatorial coordinates of the specified planet or the moon
- geomag returns the magnetic declination for the current location
All these functions are location dependent (latitude and longitude) and generally are also time
dependent.
The library provides a genserver to host local coordinates, which is responsibility of the
application to start, supervise and initialize with the local coordinates.
All the functions from the main Astrex module will retrieve the local coordinates from the server
and use the system time. This is handy for usage by applications that are geographically specific
and work in real time, e.g. for telescope control.
However, all the functions can be accessed directly from the respective modules by specifying the
desired coordinate and timestamp.
Bonus
- sidereal_speeds, a function to calculate the altitude and azimuth sidereal speeds for a sky point at
any given altitude/azimuth (only available for current system time and current genserver location)
Two calculation methods are provided. Both return consistent results, with some small difference which
does not affect the telescope driving speeds, when used with typical gear ratios.
## Conventions
The following conventions apply throughout the whole library (unless otherwise indicated):
- Longitudes East of Greenwich is POSITIVE: 0° to 180°
- Longitudes West of Greenwich is NEGATIVE: 0° to -180°
- Azimuth North is 0°
- Azimuth East is 90°
- Azimuth South is 180°
- Azimuth West is 270°
## Units
Unless otherwise indicated. all data need to be expressed in degrees, not in deegrees:minutes:seconds nor in radians
## References
The source of the algorithms is indicated in the docs of each function.
## Epoch: the WMM.COF file is updated to 2025 epoc. Unfortunately the WMM does not publish a link to the latest WMM file,
each subsequent epoc needs to be downloaded and updated manually when published. Each epoc file is valid for several years
"""
@mu 7.272e-5 # sidereal rate = earth rotation rate = 7.272 * 10^-5 rad/s
@doc """
Converts from AltAzimth coordinates to Equatorial Celestial coordinates
Does not compensate for the refraction effect. If needed, the compensation can be
applied to the coords argument before calling this function.
See Astrex.Astro.Refraction module.
"""
@spec az2eq(T.altazimuth()) :: T.equatorial()
def az2eq(coords = %{alt: _alt, az: _az}) do
site = Astrex.Server.get_ll()
coords
|> Tr.az2eq(site, C.ndt_now())
end
@doc """
Converts from Equatorial Celestial coordinates to AltAzimth coordinates
Does not compensate for the refraction effect. If needed, the compensation can be
applied to the results of this function.
See Astrex.Astro.Refraction module.
"""
@spec eq2az(T.equatorial()) :: T.altazimuth()
def eq2az(coords = %{ra: _ra, dec: _dec}) do
site = Astrex.Server.get_ll()
coords
|> Tr.eq2az(site, C.ndt_now())
end
@doc """
Returns the current equatorial coordinates of the Moon or planets (including Pluto).
The coordinates are returned in Degrees (declination) and Hours (right ascension)
## Examples
iex> Astrex.where_is(:saturn)
%{dec: -15.285237885762347, ra: 21.665144627176584}
iex> Astrex.where_is(:moon)
%{dec: 15.889732428707166, ra: 2.705974878446635}
"""
@spec where_is(T.solar_system()) :: T.equatorial()
def where_is(ss_object) do
Astrex.Astro.SolarSystem.where_is(ss_object, C.ndt_now())
end
@doc """
Calculates the magnetic deviation from true north, given the local site coordinates
(latitude, longitude) and optionally the height above see level. The height is expressed
in Km and defaults to zero.
Returns a tuple with the following values, in this order:
- magnetic declination
- magnetic inclination
- total magnetic field intensity
- epoch of the current datafile
The magnetic declination value shoud be added to a magnetic compass reading to get the real
orientation (true north) of the device.
"""
@spec mag_declination(number) :: {float(), float(), float(), binary()}
def mag_declination(altitude \\ 0) do
site = Astrex.Server.get_ll()
Astrex.Astro.GeoMag.mag_declination(site, altitude)
end
@doc """
Calculates the sidereal speed, in degrees per second, for any given point
from it's altitude and azimuth coordinates
Source:
"A Mathematical Description of the Control System for the William Herschel
Telescope" R.A.Laing, Royal Greenwich Observatory" pages 2/3
## Examples
iex> Astrex.sidereal_speeds(%{alt: 45, az: 10})
{4.5061593459496844e-4, 7.042059181503614e-4}
"""
@spec sidereal_speeds(T.altazimuth()) :: {float, float}
def sidereal_speeds(coords = %{alt: _alt, az: _az}) do
coords = coords |> C.map2rad()
%{lat: lat, long: _long} = Astrex.Server.get_ll() |> C.map2rad()
z = Math.pi() / 2 - coords.alt
cosLat = Math.cos(lat)
sinLat = Math.sin(lat)
cosAz = Math.cos(coords.az)
sinAz = Math.sin(coords.az)
cosZ = Math.cos(z)
sinZ = Math.sin(z)
# -zRate
altRate = (sinAz * cosLat * @mu) |> rad2deg
azRate = (@mu * (sinLat * sinZ - cosLat * cosZ * cosAz) / sinZ) |> rad2deg
{altRate, azRate}
end
@spec sidereal_speeds(T.equatorial()) :: {float, float}
def sidereal_speeds(coords = %{ra: _ra, dec: _dec}) do
sidereal_speeds(eq2az(coords))
end
# Astrex.Astro.sidereal_speeds2(Astrex.Astro.az2eq(%{alt: 45, az: 10}), 1)
# calculations for secs seconds in future
@doc """
Alternative method to calculate the sidereal speed starting from equatorial coordinates
1) calculates the AltAz coordinates for current time
2) calculates the AltAz coordinates for current time + one second
3) calculates the difference, which are the speeds in degrees per second
The results are very close to the sidereal speeds calculated with the mathematical method.
The difference is not enough to impact on motors rotation Hz
## Examples
iex> Astrex.sidereal_speeds2(%{alt: 45, az: 10}, 1)
{4.5187822290415625e-4, 7.061364708533802e-4}
"""
@spec sidereal_speeds2(T.equatorial(), integer()) :: {float, float}
def sidereal_speeds2(coords = %{ra: _ra, dec: _dec}, secs) do
site = Astrex.Server.get_ll()
now = C.ndt_now()
next = %NaiveDateTime{now | second: now.second + secs}
immediate = coords |> Tr.eq2az(site, now)
next = coords |> Tr.eq2az(site, next)
{next.alt - immediate.alt, next.az - immediate.az}
end
@spec sidereal_speeds2(T.altazimuth(), integer()) :: {float, float}
def sidereal_speeds2(coords = %{alt: _alt, az: _az}, secs) do
sidereal_speeds2(az2eq(coords), secs)
end
end