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

Conversation

rhannequin
Copy link
Owner

@rhannequin rhannequin commented Feb 18, 2024

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:

angle = Astronoby::Angle.as_degrees(180)

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

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:

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.

@rhannequin rhannequin self-assigned this Feb 18, 2024
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.
@rhannequin rhannequin force-pushed the angles-as-a-single-class branch from f6748c1 to 12e2fae Compare February 18, 2024 19:40
Copy link

@JoelQ JoelQ left a 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.

Comment on lines +25 to +26
@sun_longitude.radians - @coordinates.longitude.radians
) / Math.cos(@coordinates.latitude.radians) / 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 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

Comment on lines +31 to +32
Math.sin(@sun_longitude.radians - @coordinates.longitude.radians) *
Math.sin(@coordinates.latitude.radians) / 3600
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.

@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
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 ?

PRECISION = 14
PI = BigMath.PI(10)
PI = BigMath.PI(PRECISION)
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! ❄️

end

def as_degrees(degrees)
radians = degrees / BigDecimal("180") * PI
Copy link

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)

Copy link
Owner Author

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).

Comment on lines 18 to 21
longitude: Angle.as_degrees(
(true_anomaly.value + longitude_at_perigee.to_degrees.value) % 360
(true_anomaly.degrees + longitude_at_perigee.degrees) % 360
)
)
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

Comment on lines +20 to +22
obliquity_in_radians = mean_obliquity.value.radians
longitude_in_radians = @longitude.radians
latitude_in_radians = @latitude.radians
Copy link

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.

Comment on lines 48 to 51
Equatorial.new(
right_ascension: right_ascension.to_hours,
declination: declination.to_degrees,
right_ascension: right_ascension,
declination: declination,
epoch: epoch
Copy link

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!

Comment on lines +28 to +29
-17.2 * Math.sin(moon_ascending_node_longitude.radians) -
1.3 * Math.sin(2 * sun_mean_longitude.radians)
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 of this PR but there are a bunch of magic numbers that I have not clue what they do. Maybe extract some named constants?

Copy link
Owner Author

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.

Copy link

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!

@rhannequin rhannequin merged commit 5449e79 into main Feb 19, 2024
5 checks passed
@rhannequin rhannequin deleted the angles-as-a-single-class branch February 19, 2024 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants