astronoby 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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