-
-
Notifications
You must be signed in to change notification settings - Fork 3
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
Conversation
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.
f6748c1
to
12e2fae
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this change! I left a bunch of comments but they are mostly out of scope for this PR. Instead, they look forward and what you could build on top of this new Angle
class. Once you add a few math operators, I think a lot of your other code starts to get much simpler.
@sun_longitude.radians - @coordinates.longitude.radians | ||
) / Math.cos(@coordinates.latitude.radians) / 3600 |
There was a problem hiding this comment.
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
Math.sin(@sun_longitude.radians - @coordinates.longitude.radians) * | ||
Math.sin(@coordinates.latitude.radians) / 3600 |
There was a problem hiding this comment.
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.
@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 |
There was a problem hiding this comment.
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
?
PRECISION = 14 | ||
PI = BigMath.PI(10) | ||
PI = BigMath.PI(PRECISION) | ||
FORMATS = %i[dms hms].freeze |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Freeze all the things! ❄️
lib/astronoby/angle.rb
Outdated
end | ||
|
||
def as_degrees(degrees) | ||
radians = degrees / BigDecimal("180") * PI |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on having a constant like DEGREES_PER_RADIAN = BigDecimal("180") * PI
? (I'm not sure I used the correct name)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose to add a PI_IN_DEGREES = BigDecimal("180")
constant because I wasn't able to add something with PI
as its use is different depending on the conversion direction (multiply or divide).
longitude: Angle.as_degrees( | ||
(true_anomaly.value + longitude_at_perigee.to_degrees.value) % 360 | ||
(true_anomaly.degrees + longitude_at_perigee.degrees) % 360 | ||
) | ||
) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
obliquity_in_radians = mean_obliquity.value.radians | ||
longitude_in_radians = @longitude.radians | ||
latitude_in_radians = @latitude.radians |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you implement math operators on Angle
, I think a lot of the complexity here goes away. You would no longer need to care that these angles are expressed in radians. All you care is that you have a series of Angle
objects and you can do math on them.
Equatorial.new( | ||
right_ascension: right_ascension.to_hours, | ||
declination: declination.to_degrees, | ||
right_ascension: right_ascension, | ||
declination: declination, | ||
epoch: epoch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love that the units are now gone!
-17.2 * Math.sin(moon_ascending_node_longitude.radians) - | ||
1.3 * Math.sin(2 * sun_mean_longitude.radians) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of scope of this PR but there are a bunch of magic numbers that I have not clue what they do. Maybe extract some named constants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot to answer to this one.
Most of the magic numbers in the library are magic numbers from the book I rely on. I don't always have explanation for these numbers, except that they are part of the formula.
I don't really know what to do with these. Magic numbers scream at me, but I can't invent names or use confusing names that might not be accurate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair! I was hoping the book that provided the equation also provided an explanation of what these numbers were. Or at least gave their unit!
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:
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 have 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:
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.