Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Angle as a single class #13

Merged
merged 3 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ jobs:
strategy:
matrix:
ruby:
- 3.1.4
- 3.2.0
- 3.2.1
- 3.2.2
- 3.2.3
- 3.3.0
steps:
Expand Down
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
2 changes: 1 addition & 1 deletion astronoby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
spec.description = "Ruby version of the calculations from various books like Celestial Calculations by J. L. Lawrence, Practical Astronomy with your Calculator or Spreadsheet by Peter Duffett-Smith and Jonathan Zwart, or Astronomical Algorithms by Jean Meeus"
spec.homepage = "https://github.com/rhannequin/astronoby"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.1.4"
spec.required_ruby_version = ">= 3.2.0"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
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
Comment on lines +25 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR but for the future, I wonder if it would be useful to implement some math operators directly on Angle? Then you don't need to unwrap the angle values. Ideally you get to treat Angle instances like numbers, not just wrappers of numbers.

With Angle#cos and Angle#- this method would look like:

(@sun_longitude - @coordinates.longitude).cos / @coordinates.lattitude.cos / 3600

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR but I'm wondering with the 3600 number comes from? I'd love to see this as a named constant. Given that I associate the number 360 with degrees in a circle, I'm wondering if this might be a number that needs to be owned by Angle ?

)

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
Comment on lines +31 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here, if you had Angle#sin and Angle#- implemented this method would look like:

(@sun_longitude - @coordinates.logitude).sin * @coordinates.lattitude.sin / 3600

Note that you don't need to implement Angle#* or Angle#/ because Angle#sin would return a float, not an angle.

)

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
114 changes: 83 additions & 31 deletions lib/astronoby/angle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,109 @@

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)
PI_IN_DEGREES = BigDecimal("180")

RADIAN_PER_HOUR = PI / BigDecimal("12")
MINUTES_PER_DEGREE = BigDecimal("60")
MINUTES_PER_HOUR = BigDecimal("60")
SECONDS_PER_MINUTE = BigDecimal("60")
SECONDS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE

FORMATS = %i[dms hms].freeze
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Freeze all the things! ❄️


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 zero
new(0)
end

def as_radians(radians)
new(radians)
end

def as_degrees(degrees)
radians = degrees / PI_IN_DEGREES * PI
new(radians)
end

def as_hours(hours)
radians = hours * RADIAN_PER_HOUR
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 / MINUTES_PER_HOUR + second / SECONDS_PER_HOUR
as_hours(hours)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's clever! Nice reuse

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 / MINUTES_PER_HOUR + second / SECONDS_PER_HOUR
as_degrees(sign * degrees)
end
end

UNITS.each do |unit|
define_method("to_#{unit}") do
raise NotImplementedError, "#{self.class} must implement #to_#{unit} method."
Comment on lines -41 to -43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay to cleaning up some metaprogramming!

def radians
@angle
end

def degrees
@angle * PI_IN_DEGREES / PI
end

def hours
@angle / RADIAN_PER_HOUR
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 = MINUTES_PER_DEGREE * (absolute_degrees - degrees)
absolute_decimal_minutes = (
MINUTES_PER_DEGREE * (absolute_degrees - degrees)
).abs
minutes = decimal_minutes.floor
seconds = SECONDS_PER_MINUTE * (
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 = MINUTES_PER_HOUR * (absolute_hours - hours)
absolute_decimal_minutes = (
MINUTES_PER_HOUR * (absolute_hours - hours)
).abs
minutes = decimal_minutes.floor
seconds = SECONDS_PER_MINUTE * (
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.

19 changes: 7 additions & 12 deletions lib/astronoby/bodies/sun.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ def initialize(epoch:)

def ecliptic_coordinates
Coordinates::Ecliptic.new(
latitude: Angle.as_degrees(0),
latitude: Angle.zero,
longitude: Angle.as_degrees(
(true_anomaly.value + longitude_at_perigee.to_degrees.value) % 360
(true_anomaly.degrees + longitude_at_perigee.degrees) % 360
)
)
Comment on lines 18 to 21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this first PR but a future refactor you could do would be to implement some math operators on Angle. Then you don't need to do the unwrap/do math/rewrap dance here. For example, if you implemented Angle#+ and Angle#% this would become

longitude: (true_anomaly + logitude_at_perigee) % 360

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same goes for a lot of the math in this file. By implementing a few arithmetic operators on Angle, you get to work with angle values almost as if they were integers

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need to pay attention to the types when implementing math operations though. Keep in mind:

  • Some operations return angles, some return raw integers
  • Some operations work on two angles, some operations work on an angle and a number

For example Angle#cos would take an angle and return a float. Angle#* is effectively scaling your angle. It takes an angle and a number and returns a new angle.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to see a future world where this file doesn't do any unwrapping or re-wrapping of angles

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
Loading
Loading