Skip to content

Commit

Permalink
Refactor Angle as a single class
Browse files Browse the repository at this point in the history
Previously, there was three subclasses of `Angle`:
- `Degree`
- `Radian`
- `Hour`

This was not a good approach for the universal notion of what an angle
is. The same angle should not be represented by two different objects
depending on their unit.

Now, only `Angle` remains. An angle is always stored with a radian unit,
as it is the unit of angle in the International System of Units.

Converting an angle is not about changing its type anymore, but only
converting its value from one unit to another. The value is nos
accessible through the unit name:

```rb
angle = Astronoby::Angle.as_degrees(180)

angle.degrees # => 0.18e3
angle.radians # => 0.31415926...
```

Because angles in degrees and hours are immediately converted, some
precision is immediately lost. However, the conversion is done with
BigDecimal numbers with a precision of 14. I believe this is more than
enough affordable precision loss for this project.

The reader may notice some test values has changed. Because I took the
opportunity to remove the `ceil` method on the angle initialization,
precision has actually increased. This change is barely noticeable
because it affects far digits of the arc second.
In the future, I will conduct performance tests to figure out if
ceiling angles increase the library overall performance and speed of
calculations.

Angles can still be initialized from different units with the following
class methods:
- `::as_degrees`
- `::as_radians`
- `::as_hours`

String formatting has also changed and can be done immediately on the
angle without the need to convert it to another unit before:

```rb
angle = Astronoby::Angle.as_degrees(180)
angle.str(:dms)
```

I will work on making `Angle` a better value object in the future, to
make its use even more convenient and efficient.

Thanks to @JoelQ for inspiring me into this work.
  • Loading branch information
rhannequin committed Feb 18, 2024
1 parent 6d5fd3a commit f6748c1
Show file tree
Hide file tree
Showing 39 changed files with 326 additions and 585 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ horizontal_coordinates = sun.horizontal_coordinates(
longitude: longitude
)

horizontal_coordinates.altitude.value.to_f
# => 27.50236513017543
horizontal_coordinates.altitude.degrees.to_f
# => 27.502365130176567

horizontal_coordinates.altitude.to_dms.format
horizontal_coordinates.altitude.str(:dms)
# => "+27° 30′ 8.5144″"
```

Expand Down
3 changes: 0 additions & 3 deletions lib/astronoby.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# frozen_string_literal: true

require "astronoby/angle"
require "astronoby/angles/degree"
require "astronoby/angles/dms"
require "astronoby/angles/hms"
require "astronoby/angles/hour"
require "astronoby/angles/radian"
require "astronoby/epoch"
require "astronoby/body"
require "astronoby/bodies/sun"
Expand Down
17 changes: 6 additions & 11 deletions lib/astronoby/aberration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,22 @@ def apply
delta_longitude = Astronoby::Angle.as_degrees(
-20.5 *
Math.cos(
@sun_longitude.to_radians.value - @coordinates.longitude.to_radians.value
) / Math.cos(@coordinates.latitude.to_radians.value) / 3600
@sun_longitude.radians - @coordinates.longitude.radians
) / Math.cos(@coordinates.latitude.radians) / 3600
)

delta_latitude = Astronoby::Angle.as_degrees(
-20.5 *
Math.sin(
@sun_longitude.to_radians.value -
@coordinates.longitude.to_radians.value
) *
Math.sin(@coordinates.latitude.to_radians.value) / 3600
Math.sin(@sun_longitude.radians - @coordinates.longitude.radians) *
Math.sin(@coordinates.latitude.radians) / 3600
)

Astronoby::Coordinates::Ecliptic.new(
latitude: Astronoby::Angle.as_degrees(
@coordinates.latitude.to_degrees.value +
delta_latitude.value
@coordinates.latitude.degrees + delta_latitude.degrees
),
longitude: Astronoby::Angle.as_degrees(
@coordinates.longitude.to_degrees.value +
delta_longitude.value
@coordinates.longitude.degrees + delta_longitude.degrees
)
)
end
Expand Down
102 changes: 71 additions & 31 deletions lib/astronoby/angle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,97 @@

module Astronoby
class Angle
UNITS = [
DEGREES = :degrees,
HOURS = :hours,
RADIANS = :radians
].freeze

UNIT_CLASS_NAMES = {
DEGREES => "Astronoby::Degree",
HOURS => "Astronoby::Hour",
RADIANS => "Astronoby::Radian"
}

PRECISION = 14
PI = BigMath.PI(10)
PI = BigMath.PI(PRECISION)
FORMATS = %i[dms hms].freeze

class << self
UNIT_CLASS_NAMES.each do |unit, class_name|
define_method("as_#{unit}") do |angle|
Kernel.const_get(class_name).new(angle)
end
def as_radians(radians)
new(radians)
end

def as_degrees(degrees)
radians = degrees / BigDecimal("180") * PI
new(radians)
end

def as_hours(hours)
radians = hours * (PI / BigDecimal("12"))
new(radians)
end

def as_hms(hour, minute, second)
angle = hour + minute / 60.0 + second / 3600.0
Kernel.const_get(UNIT_CLASS_NAMES[HOURS]).new(angle)
hours = hour + minute / 60.0 + second / 3600.0
as_hours(hours)
end

def as_dms(degree, minute, second)
sign = degree.negative? ? -1 : 1
angle = degree.abs + minute / 60.0 + second / 3600.0
Kernel.const_get(UNIT_CLASS_NAMES[DEGREES]).new(sign * angle)
degrees = degree.abs + minute / 60.0 + second / 3600.0
as_degrees(sign * degrees)
end
end

UNITS.each do |unit|
define_method("to_#{unit}") do
raise NotImplementedError, "#{self.class} must implement #to_#{unit} method."
def radians
@angle
end

def degrees
@angle * BigDecimal("180") / PI
end

def hours
@angle / (PI / BigDecimal("12"))
end

def initialize(angle)
@angle = if angle.is_a?(Integer) || angle.is_a?(BigDecimal)
BigDecimal(angle)
else
BigDecimal(angle, PRECISION)
end
end

def initialize(angle, unit:)
@angle = BigDecimal(angle, PRECISION).ceil(PRECISION)
@unit = unit
def str(format)
case format
when :dms then to_dms(degrees).format
when :hms then to_hms(hours).format
else
raise UnsupportedFormatError.new(
"Expected a format between #{FORMATS.join(", ")}, got #{format}"
)
end
end

def value
@angle
def to_dms(deg)
sign = deg.negative? ? "-" : "+"
absolute_degrees = deg.abs
degrees = absolute_degrees.floor
decimal_minutes = BigDecimal("60") * (absolute_degrees - degrees)
absolute_decimal_minutes = (
BigDecimal("60") * (absolute_degrees - degrees)
).abs
minutes = decimal_minutes.floor
seconds = BigDecimal("60") * (
absolute_decimal_minutes - absolute_decimal_minutes.floor
)

Dms.new(sign, degrees, minutes, seconds.to_f.floor(4))
end

def ==(other)
value == other.value && self.class == other.class
def to_hms(hrs)
absolute_hours = hrs.abs
hours = absolute_hours.floor
decimal_minutes = BigDecimal("60") * (absolute_hours - hours)
absolute_decimal_minutes = (
BigDecimal("60") * (absolute_hours - hours)
).abs
minutes = decimal_minutes.floor
seconds = BigDecimal("60") * (
absolute_decimal_minutes - absolute_decimal_minutes.floor
)

Hms.new(hours, minutes, seconds.to_f.floor(4))
end
end
end
52 changes: 0 additions & 52 deletions lib/astronoby/angles/degree.rb

This file was deleted.

36 changes: 0 additions & 36 deletions lib/astronoby/angles/hour.rb

This file was deleted.

25 changes: 0 additions & 25 deletions lib/astronoby/angles/radian.rb

This file was deleted.

17 changes: 6 additions & 11 deletions lib/astronoby/bodies/sun.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def ecliptic_coordinates
Coordinates::Ecliptic.new(
latitude: Angle.as_degrees(0),
longitude: Angle.as_degrees(
(true_anomaly.value + longitude_at_perigee.to_degrees.value) % 360
(true_anomaly.degrees + longitude_at_perigee.degrees) % 360
)
)
end
Expand All @@ -33,29 +33,24 @@ def horizontal_coordinates(latitude:, longitude:)

def mean_anomaly
Angle.as_degrees(
(
longitude_at_base_epoch.to_degrees.value -
longitude_at_perigee.to_degrees.value
) % 360
(longitude_at_base_epoch.degrees - longitude_at_perigee.degrees) % 360
)
end

def true_anomaly
eccentric_anomaly = Astronoby::Util::Astrodynamics.eccentric_anomaly_newton_raphson(
mean_anomaly,
orbital_eccentricity.to_degrees.value,
orbital_eccentricity.degrees,
2e-06,
10
)

tan = Math.sqrt(
(1 + orbital_eccentricity.to_degrees.value)./(
1 - orbital_eccentricity.to_degrees.value
)
) * Math.tan(eccentric_anomaly.to_radians.value / 2)
(1 + orbital_eccentricity.degrees) / (1 - orbital_eccentricity.degrees)
) * Math.tan(eccentric_anomaly.radians / 2)

Astronoby::Angle.as_degrees(
(Astronoby::Angle.as_radians(Math.atan(tan)).to_degrees.value * 2) % 360
(Astronoby::Angle.as_radians(Math.atan(tan)).degrees * 2) % 360
)
end

Expand Down
16 changes: 7 additions & 9 deletions lib/astronoby/body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ def rising_time(latitude:, longitude:, date:)
return nil if h2_component.nil?

rising_lst = 24 +
@equatorial_coordinates.right_ascension.to_hours.value -
h2_component.to_degrees.value
@equatorial_coordinates.right_ascension.hours - h2_component.degrees
rising_lst -= 24 if rising_lst > 24

Astronoby::Util::Time.lst_to_ut(
Expand Down Expand Up @@ -48,8 +47,7 @@ def setting_time(latitude:, longitude:, date:)
h2_component = h2(latitude: latitude)
return nil if h2_component.nil?

setting_lst = @equatorial_coordinates.right_ascension.to_hours.value +
h2_component.to_degrees.value
setting_lst = @equatorial_coordinates.right_ascension.hours + h2_component.degrees
setting_lst -= 24 if setting_lst > 24

Astronoby::Util::Time.lst_to_ut(
Expand All @@ -68,23 +66,23 @@ def setting_azimuth(latitude:)
rising_az = rising_azimuth(latitude: latitude)
return nil if rising_az.nil?

Astronoby::Angle.as_degrees(360 - rising_az.to_degrees.value)
Astronoby::Angle.as_degrees(360 - rising_az.degrees)
end

private

def azimuth_component(latitude:)
Math.sin(@equatorial_coordinates.declination.to_radians.value)./(
Math.cos(latitude.to_radians.value)
Math.sin(@equatorial_coordinates.declination.radians)./(
Math.cos(latitude.radians)
)
end

def h2(latitude:)
ar = azimuth_component(latitude: latitude)
return nil if ar >= 1

h1 = Math.tan(latitude.to_radians.value) *
Math.tan(@equatorial_coordinates.declination.to_radians.value)
h1 = Math.tan(latitude.radians) *
Math.tan(@equatorial_coordinates.declination.radians)
return nil if h1.abs > 1

Astronoby::Angle.as_radians(Math.acos(-h1) / 15.0)
Expand Down
Loading

0 comments on commit f6748c1

Please sign in to comment.