astronoby 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Sun
5
+ # Source:
6
+ # Title: Celestial Calculations
7
+ # Author: J. L. Lawrence
8
+ # Edition: MIT Press
9
+ # Chapter: 6 - The Sun
10
+
11
+ def initialize(epoch:)
12
+ @epoch = epoch
13
+ end
14
+
15
+ def ecliptic_coordinates
16
+ Coordinates::Ecliptic.new(
17
+ latitude: Angle.zero,
18
+ longitude: Angle.as_degrees(
19
+ (true_anomaly + longitude_at_perigee).degrees % 360
20
+ )
21
+ )
22
+ end
23
+
24
+ def horizontal_coordinates(latitude:, longitude:)
25
+ time = Epoch.to_utc(@epoch)
26
+
27
+ ecliptic_coordinates
28
+ .to_equatorial(epoch: @epoch)
29
+ .to_horizontal(time: time, latitude: latitude, longitude: longitude)
30
+ end
31
+
32
+ private
33
+
34
+ def mean_anomaly
35
+ Angle.as_degrees(
36
+ (longitude_at_base_epoch - longitude_at_perigee).degrees % 360
37
+ )
38
+ end
39
+
40
+ def true_anomaly
41
+ eccentric_anomaly = Util::Astrodynamics.eccentric_anomaly_newton_raphson(
42
+ mean_anomaly,
43
+ orbital_eccentricity.degrees,
44
+ 2e-06,
45
+ 10
46
+ )
47
+
48
+ tan = Math.sqrt(
49
+ (1 + orbital_eccentricity.degrees) / (1 - orbital_eccentricity.degrees)
50
+ ) * Math.tan(eccentric_anomaly.radians / 2)
51
+
52
+ Angle.as_degrees((Angle.atan(tan).degrees * 2) % 360)
53
+ end
54
+
55
+ def days_since_epoch
56
+ Epoch::DEFAULT_EPOCH - @epoch
57
+ end
58
+
59
+ def centuries
60
+ @centuries ||= (@epoch - Epoch::J1900) / Epoch::DAYS_PER_JULIAN_CENTURY
61
+ end
62
+
63
+ def longitude_at_base_epoch
64
+ Angle.as_degrees(
65
+ (279.6966778 + 36000.76892 * centuries + 0.0003025 * centuries**2) % 360
66
+ )
67
+ end
68
+
69
+ def longitude_at_perigee
70
+ Angle.as_degrees(
71
+ (281.2208444 + 1.719175 * centuries + 0.000452778 * centuries**2) % 360
72
+ )
73
+ end
74
+
75
+ def orbital_eccentricity
76
+ Angle.as_degrees(
77
+ (0.01675104 - 0.0000418 * centuries - 0.000000126 * centuries**2) % 360
78
+ )
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Body
5
+ def initialize(equatorial_coordinates)
6
+ @equatorial_coordinates = equatorial_coordinates
7
+ end
8
+
9
+ # Source:
10
+ # Title: Celestial Calculations
11
+ # Author: J. L. Lawrence
12
+ # Edition: MIT Press
13
+ # Chapter: 5 - Stars in the Nighttime Sky
14
+ def rising_time(latitude:, longitude:, date:)
15
+ h2_component = h2(latitude: latitude)
16
+ return nil if h2_component.nil?
17
+
18
+ rising_lst = 24 +
19
+ @equatorial_coordinates.right_ascension.hours - h2_component.degrees
20
+ rising_lst -= 24 if rising_lst > 24
21
+
22
+ Util::Time.lst_to_ut(date: date, longitude: longitude, lst: rising_lst)
23
+ end
24
+
25
+ # Source:
26
+ # Title: Celestial Calculations
27
+ # Author: J. L. Lawrence
28
+ # Edition: MIT Press
29
+ # Chapter: 5 - Stars in the Nighttime Sky
30
+ def rising_azimuth(latitude:)
31
+ ar = azimuth_component(latitude: latitude)
32
+ return nil if ar >= 1
33
+
34
+ Angle.acos(ar)
35
+ end
36
+
37
+ # Source:
38
+ # Title: Celestial Calculations
39
+ # Author: J. L. Lawrence
40
+ # Edition: MIT Press
41
+ # Chapter: 5 - Stars in the Nighttime Sky
42
+ def setting_time(latitude:, longitude:, date:)
43
+ h2_component = h2(latitude: latitude)
44
+ return nil if h2_component.nil?
45
+
46
+ setting_lst = @equatorial_coordinates.right_ascension.hours + h2_component.degrees
47
+ setting_lst -= 24 if setting_lst > 24
48
+
49
+ Util::Time.lst_to_ut(date: date, longitude: longitude, lst: setting_lst)
50
+ end
51
+
52
+ # Source:
53
+ # Title: Celestial Calculations
54
+ # Author: J. L. Lawrence
55
+ # Edition: MIT Press
56
+ # Chapter: 5 - Stars in the Nighttime Sky
57
+ def setting_azimuth(latitude:)
58
+ rising_az = rising_azimuth(latitude: latitude)
59
+ return nil if rising_az.nil?
60
+
61
+ Angle.as_degrees(360 - rising_az.degrees)
62
+ end
63
+
64
+ private
65
+
66
+ def azimuth_component(latitude:)
67
+ @equatorial_coordinates.declination.sin / latitude.cos
68
+ end
69
+
70
+ def h2(latitude:)
71
+ ar = azimuth_component(latitude: latitude)
72
+ return nil if ar >= 1
73
+
74
+ h1 = latitude.tan * @equatorial_coordinates.declination.tan
75
+ return nil if h1.abs > 1
76
+
77
+ Angle.as_radians(Math.acos(-h1) / 15.0)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Coordinates
5
+ class Ecliptic
6
+ attr_reader :latitude, :longitude
7
+
8
+ def initialize(latitude:, longitude:)
9
+ @latitude = latitude
10
+ @longitude = longitude
11
+ end
12
+
13
+ # Source:
14
+ # Title: Celestial Calculations
15
+ # Author: J. L. Lawrence
16
+ # Edition: MIT Press
17
+ # Chapter: 4 - Orbits and Coordinate Systems
18
+ def to_equatorial(epoch:)
19
+ mean_obliquity = MeanObliquity.for_epoch(epoch)
20
+
21
+ y = Angle.as_radians(
22
+ @longitude.sin * mean_obliquity.cos -
23
+ @latitude.tan * mean_obliquity.sin
24
+ )
25
+ x = Angle.as_radians(@longitude.cos)
26
+ r = Angle.atan(y.radians / x.radians)
27
+ right_ascension = Util::Trigonometry.adjustement_for_arctangent(y, x, r)
28
+
29
+ declination = Angle.asin(
30
+ @latitude.sin * mean_obliquity.cos +
31
+ @latitude.cos * mean_obliquity.sin * @longitude.sin
32
+ )
33
+
34
+ Equatorial.new(
35
+ right_ascension: right_ascension,
36
+ declination: declination,
37
+ epoch: epoch
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Coordinates
5
+ class Equatorial
6
+ attr_reader :declination, :right_ascension, :hour_angle, :epoch
7
+
8
+ def initialize(
9
+ declination:,
10
+ right_ascension:,
11
+ hour_angle: nil,
12
+ epoch: Epoch::DEFAULT_EPOCH
13
+ )
14
+ @right_ascension = right_ascension
15
+ @declination = declination
16
+ @hour_angle = hour_angle
17
+ @epoch = epoch
18
+ end
19
+
20
+ def compute_hour_angle(time:, longitude:)
21
+ lst = Util::Time.local_sidereal_time(
22
+ time: time,
23
+ longitude: longitude
24
+ )
25
+
26
+ ha = (lst - @right_ascension.hours)
27
+ ha += 24 if ha.negative?
28
+
29
+ Angle.as_hours(ha)
30
+ end
31
+
32
+ def to_horizontal(time:, latitude:, longitude:)
33
+ ha = @hour_angle || compute_hour_angle(time: time, longitude: longitude)
34
+ t0 = @declination.sin * latitude.sin +
35
+ @declination.cos * latitude.cos * ha.cos
36
+ altitude = Angle.asin(t0)
37
+
38
+ t1 = @declination.sin - latitude.sin * altitude.sin
39
+ t2 = t1 / (latitude.cos * altitude.cos)
40
+ azimuth = Angle.acos(t2)
41
+
42
+ if ha.sin.positive?
43
+ azimuth = Angle.as_degrees(BigDecimal("360") - azimuth.degrees)
44
+ end
45
+
46
+ Horizontal.new(
47
+ azimuth: azimuth,
48
+ altitude: altitude,
49
+ latitude: latitude,
50
+ longitude: longitude
51
+ )
52
+ end
53
+
54
+ # Source:
55
+ # Title: Celestial Calculations
56
+ # Author: J. L. Lawrence
57
+ # Edition: MIT Press
58
+ # Chapter: 4 - Orbits and Coordinate Systems
59
+ def to_ecliptic(epoch:)
60
+ mean_obliquity = MeanObliquity.for_epoch(epoch)
61
+
62
+ y = Angle.as_radians(
63
+ @right_ascension.sin * mean_obliquity.cos +
64
+ @declination.tan * mean_obliquity.sin
65
+ )
66
+ x = Angle.as_radians(@right_ascension.cos)
67
+ r = Angle.atan(y.radians / x.radians)
68
+ longitude = Util::Trigonometry.adjustement_for_arctangent(y, x, r)
69
+
70
+ latitude = Angle.asin(
71
+ @declination.sin * mean_obliquity.cos -
72
+ @declination.cos * mean_obliquity.sin * @right_ascension.sin
73
+ )
74
+
75
+ Ecliptic.new(
76
+ latitude: latitude,
77
+ longitude: longitude
78
+ )
79
+ end
80
+
81
+ def to_epoch(epoch)
82
+ Precession.for_equatorial_coordinates(
83
+ coordinates: self,
84
+ epoch: epoch
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Coordinates
5
+ class Horizontal
6
+ attr_reader :azimuth, :altitude, :latitude, :longitude
7
+
8
+ def initialize(
9
+ azimuth:,
10
+ altitude:,
11
+ latitude:,
12
+ longitude:
13
+ )
14
+ @azimuth = azimuth
15
+ @altitude = altitude
16
+ @latitude = latitude
17
+ @longitude = longitude
18
+ end
19
+
20
+ def to_equatorial(time:)
21
+ t0 = @altitude.sin * @latitude.sin +
22
+ @altitude.cos * @latitude.cos * @azimuth.cos
23
+
24
+ declination = Angle.asin(t0)
25
+
26
+ t1 = @altitude.sin - @latitude.sin * declination.sin
27
+
28
+ hour_angle_degrees = Angle
29
+ .acos(t1 / (@latitude.cos * declination.cos))
30
+ .degrees
31
+
32
+ if @azimuth.sin.positive?
33
+ hour_angle_degrees = Angle
34
+ .as_degrees(BigDecimal("360") - hour_angle_degrees)
35
+ .degrees
36
+ end
37
+
38
+ hour_angle_hours = Angle.as_degrees(hour_angle_degrees).hours
39
+ right_ascension_decimal = Util::Time.local_sidereal_time(
40
+ time: time,
41
+ longitude: @longitude
42
+ ) - hour_angle_hours
43
+ right_ascension_decimal += 24 if right_ascension_decimal.negative?
44
+ right_ascension = Angle.as_hours(right_ascension_decimal)
45
+
46
+ Equatorial.new(
47
+ right_ascension: right_ascension,
48
+ declination: declination
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Epoch
5
+ B1900 = 2415020.3135
6
+ J1900 = 2415020.0
7
+ B1950 = 2433282.4235
8
+ J1950 = 2433282.5
9
+ J2000 = 2451545.0
10
+
11
+ DEFAULT_EPOCH = J2000
12
+ DAYS_PER_JULIAN_CENTURY = 36525.0
13
+
14
+ JULIAN_DAY_NUMBER_OFFSET = 0.5
15
+
16
+ def self.from_time(time)
17
+ time.to_datetime.ajd
18
+ end
19
+
20
+ def self.to_utc(epoch)
21
+ DateTime.jd(epoch + JULIAN_DAY_NUMBER_OFFSET).to_time.utc
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module Astronoby
2
+ class IncompatibleArgumentsError < ArgumentError; end
3
+
4
+ class UnsupportedFormatError < ArgumentError; end
5
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Astronoby
6
+ class MeanObliquity
7
+ # Source:
8
+ # IAU resolution in 2006 in favor of the P03 astronomical model
9
+ # The Astronomical Almanac for 2010
10
+
11
+ EPOCH_OF_REFERENCE = Epoch::DEFAULT_EPOCH
12
+ OBLIQUITY_OF_REFERENCE = 23.4392794
13
+
14
+ def self.for_epoch(epoch)
15
+ return obliquity_of_reference if epoch == EPOCH_OF_REFERENCE
16
+
17
+ t = (epoch - EPOCH_OF_REFERENCE) / Epoch::DAYS_PER_JULIAN_CENTURY
18
+
19
+ Angle.as_degrees(
20
+ obliquity_of_reference.degrees - (
21
+ 46.815 * t -
22
+ 0.0006 * t * t +
23
+ 0.00181 * t * t * t
24
+ ) / 3600
25
+ )
26
+ end
27
+
28
+ def self.obliquity_of_reference
29
+ Angle.as_dms(23, 26, 21.45)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Nutation
5
+ # Source:
6
+ # Title: Practical Astronomy with your Calculator or Spreadsheet
7
+ # Authors: Peter Duffett-Smith and Jonathan Zwart
8
+ # Edition: Cambridge University Press
9
+ # Chapter: 35 - Nutation
10
+
11
+ def initialize(epoch)
12
+ @epoch = epoch
13
+ end
14
+
15
+ def self.for_ecliptic_longitude(epoch:)
16
+ new(epoch).for_ecliptic_longitude
17
+ end
18
+
19
+ def self.for_obliquity_of_the_ecliptic(epoch:)
20
+ new(epoch).for_obliquity_of_the_ecliptic
21
+ end
22
+
23
+ def for_ecliptic_longitude
24
+ Angle.as_dms(
25
+ 0,
26
+ 0,
27
+ (
28
+ -17.2 * moon_ascending_node_longitude.sin -
29
+ 1.3 * Math.sin(2 * sun_mean_longitude.radians)
30
+ )
31
+ )
32
+ end
33
+
34
+ def for_obliquity_of_the_ecliptic
35
+ Angle.as_dms(
36
+ 0,
37
+ 0,
38
+ (
39
+ 9.2 * moon_ascending_node_longitude.cos +
40
+ 0.5 * Math.cos(2 * sun_mean_longitude.radians)
41
+ )
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ def julian_centuries
48
+ (@epoch - Epoch::J1900) / Epoch::DAYS_PER_JULIAN_CENTURY
49
+ end
50
+
51
+ def sun_mean_longitude
52
+ Angle.as_degrees(
53
+ (279.6967 + 360.0 * (centuries_a - centuries_a.to_i)) % 360
54
+ )
55
+ end
56
+
57
+ def moon_ascending_node_longitude
58
+ Angle.as_degrees(
59
+ (259.1833 - 360.0 * (centuries_b - centuries_b.to_i)) % 360
60
+ )
61
+ end
62
+
63
+ def centuries_a
64
+ 100.002136 * julian_centuries
65
+ end
66
+
67
+ def centuries_b
68
+ 5.372617 * julian_centuries
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "matrix"
4
+
5
+ module Astronoby
6
+ class Precession
7
+ def self.for_equatorial_coordinates(coordinates:, epoch:)
8
+ new(coordinates, epoch).precess
9
+ end
10
+
11
+ def initialize(coordinates, epoch)
12
+ @coordinates = coordinates
13
+ @epoch = epoch
14
+ end
15
+
16
+ # Source:
17
+ # Title: Practical Astronomy with your Calculator or Spreadsheet
18
+ # Authors: Peter Duffett-Smith and Jonathan Zwart
19
+ # Edition: Cambridge University Press
20
+ # Chapter: 34 - Precession
21
+ def precess
22
+ matrix_a = matrix_for_epoch(@coordinates.epoch)
23
+ matrix_b = matrix_for_epoch(@epoch).transpose
24
+
25
+ vector = Vector[
26
+ @coordinates.right_ascension.cos * @coordinates.declination.cos,
27
+ @coordinates.right_ascension.sin * @coordinates.declination.cos,
28
+ @coordinates.declination.sin
29
+ ]
30
+
31
+ s = matrix_a * vector
32
+ w = matrix_b * s
33
+
34
+ Coordinates::Equatorial.new(
35
+ right_ascension: Util::Trigonometry.adjustement_for_arctangent(
36
+ Angle.as_radians(w[1]),
37
+ Angle.as_radians(w[0]),
38
+ Angle.atan(w[1] / w[0])
39
+ ),
40
+ declination: Angle.asin(w[2]),
41
+ epoch: @epoch
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ def matrix_for_epoch(epoch)
48
+ t = (epoch - Epoch::DEFAULT_EPOCH) / Epoch::DAYS_PER_JULIAN_CENTURY
49
+
50
+ zeta = Angle.as_degrees(
51
+ 0.6406161 * t + 0.0000839 * t * t + 0.000005 * t * t * t
52
+ )
53
+ z = Angle.as_degrees(
54
+ 0.6406161 * t + 0.0003041 * t * t + 0.0000051 * t * t * t
55
+ )
56
+ theta = Angle.as_degrees(
57
+ 0.5567530 * t - 0.0001185 * t * t - 0.0000116 * t * t * t
58
+ )
59
+
60
+ cx = zeta.cos
61
+ sx = zeta.sin
62
+ cz = z.cos
63
+ sz = z.sin
64
+ ct = theta.cos
65
+ st = theta.sin
66
+
67
+ Matrix[
68
+ [
69
+ cx * ct * cz - sx * sz,
70
+ cx * ct * sz + sx * cz,
71
+ cx * st
72
+ ],
73
+ [
74
+ -sx * ct * cz - cx * sz,
75
+ -sx * ct * sz + cx * cz,
76
+ -sx * st
77
+ ],
78
+ [
79
+ -st * cz,
80
+ -st * sz,
81
+ ct
82
+ ]
83
+ ]
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Refraction
5
+ DEFAULT_PRESSURE = 1000
6
+ DEFAULT_TEMPERATURE = 25
7
+
8
+ def self.for_horizontal_coordinates(
9
+ coordinates:,
10
+ pressure: DEFAULT_PRESSURE,
11
+ temperature: DEFAULT_TEMPERATURE
12
+ )
13
+ new(coordinates, pressure, temperature).refract
14
+ end
15
+
16
+ def initialize(coordinates, pressure, temperature)
17
+ @coordinates = coordinates
18
+ @pressure = pressure
19
+ @temperature = temperature
20
+ end
21
+
22
+ # Source:
23
+ # Title: Practical Astronomy with your Calculator or Spreadsheet
24
+ # Authors: Peter Duffett-Smith and Jonathan Zwart
25
+ # Edition: Cambridge University Press
26
+ # Chapter: 37 - Refraction
27
+ def refract
28
+ altitude_in_degrees = @coordinates.altitude.degrees
29
+
30
+ refraction_angle = Angle.as_degrees(
31
+ if altitude_in_degrees > 15
32
+ zenith_angle = Angle.as_degrees(90 - @coordinates.altitude.degrees)
33
+ 0.00452 * @pressure * zenith_angle.tan / (273 + @temperature)
34
+ else
35
+ (
36
+ @pressure *
37
+ (
38
+ 0.1594 +
39
+ 0.0196 * altitude_in_degrees +
40
+ 0.00002 * altitude_in_degrees * altitude_in_degrees
41
+ )
42
+ )./(
43
+ (273 + @temperature) *
44
+ (
45
+ 1 +
46
+ 0.505 * altitude_in_degrees +
47
+ 0.0845 * altitude_in_degrees * altitude_in_degrees
48
+ )
49
+ )
50
+ end
51
+ )
52
+
53
+ Coordinates::Horizontal.new(
54
+ azimuth: @coordinates.azimuth,
55
+ altitude: @coordinates.altitude + refraction_angle,
56
+ latitude: @coordinates.latitude,
57
+ longitude: @coordinates.longitude
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class TrueObliquity
5
+ def self.for_epoch(epoch)
6
+ mean_obliquity = MeanObliquity.for_epoch(epoch)
7
+ nutation = Nutation.for_obliquity_of_the_ecliptic(epoch: epoch)
8
+
9
+ mean_obliquity + nutation
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Util
5
+ module Astrodynamics
6
+ class << self
7
+ # Source:
8
+ # Title: Celestial Calculations
9
+ # Author: J. L. Lawrence
10
+ # Edition: MIT Press
11
+ # Chapter: 4 - Orbits and Coordinate Systems
12
+ def eccentric_anomaly_newton_raphson(
13
+ mean_anomaly,
14
+ orbital_eccentricity,
15
+ precision,
16
+ maximum_iteration_count,
17
+ current_iteration = 0,
18
+ solution_on_previous_interation = nil
19
+ )
20
+ previous_solution = solution_on_previous_interation&.radians
21
+
22
+ solution = if previous_solution.nil?
23
+ if orbital_eccentricity <= 0.75
24
+ mean_anomaly.radians
25
+ else
26
+ Math::PI
27
+ end
28
+ else
29
+ previous_solution - (
30
+ (
31
+ previous_solution -
32
+ orbital_eccentricity * Math.sin(previous_solution) -
33
+ mean_anomaly.radians
34
+ ) / (
35
+ 1 - orbital_eccentricity * Math.cos(previous_solution)
36
+ )
37
+ )
38
+ end
39
+
40
+ if current_iteration >= maximum_iteration_count ||
41
+ (
42
+ previous_solution &&
43
+ (solution - previous_solution).abs <= precision
44
+ )
45
+ return Angle.as_radians(solution)
46
+ end
47
+
48
+ eccentric_anomaly_newton_raphson(
49
+ mean_anomaly,
50
+ orbital_eccentricity,
51
+ precision,
52
+ maximum_iteration_count,
53
+ current_iteration + 1,
54
+ Angle.as_radians(solution)
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end