rhannequin / astronoby

Ruby library based on astronomy and astrometry books
https://dev.to/rhannequin/series/17782
MIT License
59 stars 2 forks source link

Calculate time for a given azimuth #68

Closed MtnBiker closed 2 months ago

MtnBiker commented 6 months ago

Problem this feature will solve

Not a big problem, but curious when my solar panels are aligned with the sun. In my case they are oriented approx. 190°

Desired solution

For a given azimuth, get time and altitude

Alternatives considered

Trial and error

Thank you Rémy, this is a great resource

rhannequin commented 6 months ago

Hi, thank you for suggesting this feature, this is an interesting one indeed. I have multiple things in mind about this problem.

The first is a small frustration from myself, because I don't know how to calculate this time directly. astronoby implements methods that return times for some events, but I have to be honest and say I'm only converting math formulas from the books I read into Ruby, without actually understanding the math/trigonometry behind. It is something I would like to fix, but I don't know if I'll ultimately be able to. It would maybe be similar to finding the transit time, so I'll try this angle first.

Speaking of transit time, my second point is that, luckily for you, 190° is very close to 180°, which is South and also exactly the azimuth angle when the solar transit happens. The solar transit is when the Sun's the highest in the sky, which happens when it crosses the local meridian (the imaginary plane that goes through the the observer and the North-South axis). Therefore, your exact alignment should be a few minutes later than the solar transit:

observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(38.5816),
  longitude: Astronoby::Angle.from_degrees(-121.4944)
)
sun = Astronoby::Sun.new(time: Time.now)
observation_events = sun.observation_events(observer: observer)

observation_events.transit_time.localtime("-07:00")
# => 2024-05-03 13:02:46 -0700

observation_events.transit_altitude.degrees
# => 67.4190973684892

My third point is that there is still a way to get a pretty precise time, from an (inefficient) iteration method. Basically finding the hour when the Sun will cross a defined azimuth angle, then finding the minute. This is absolutely inefficient, but I believe it would work if efficiency is not a requirement for your program. Here is an (even more inefficient) example:

aligned_azimuth_angle = Astronoby::Angle.from_degrees(190)

hour = (0..23).to_a.map do |hour|
  [
    hour,
    sun
      .apparent_ecliptic_coordinates
      .to_apparent_equatorial(epoch: Astronoby::Epoch.from_time(Time.now))
      .to_horizontal(
        time: Time.new(2024, 5, 3, hour, 0, 0, "-07:00"),
        latitude: observer.latitude,
        longitude: observer.longitude
      )
      .azimuth
    ]
end.to_h.sort_by { _2 }.select { _1.last < aligned_azimuth_angle }.last.first

minute = (0..59).to_a.map do |minute|
  [
    minute,
    sun
      .apparent_ecliptic_coordinates
      .to_apparent_equatorial(epoch: Astronoby::Epoch.from_time(Time.now))
      .to_horizontal(
        time: Time.new(2024, 5, 3, hour, minute, 0, "-07:00"),
        latitude: observer.latitude,
        longitude: observer.longitude
      )
      .azimuth
    ]
end.to_h.sort_by { _2 }.select { _1.last < aligned_azimuth_angle }.last.first

Time.new(2024, 5, 3, hour, minute, 0, "-07:00")
# => 2024-05-03 13:17:00 -0700

# Check Sun's azimuth angle for this time, for good measure
# Also get the altitude
horizontal_coordinates = sun
  .apparent_ecliptic_coordinates
  .to_apparent_equatorial(epoch: Astronoby::Epoch.from_time(Time.now))
  .to_horizontal(time: Time.new(2024, 5, 3, 13, 17, 0, "-07:00"),latitude: observer.latitude,longitude: observer.longitude)

horizontal_coordinates.azimuth.degrees
# => 189.9152435999221
horizontal_coordinates.altitude.degrees
# => 67.01430525975873

As we can see, this is only 15 minutes later than the transit time, but it depends on how accurate you need this time to be. You can also implement the same logic for the second if you want to find the exact instant.

I am not proud of this code, but it can technically answer your problem while a more efficient method is implemented out of the box.

If anyone else reads this issue and has the right trigonometry formulas for this problem, I'll be happy to have a look and eventually implement a new method on Astronoby::Events::ObservationEvent.

MtnBiker commented 6 months ago

Merci. I didn't Ruby well enough to work out what you did for the iteration easily, so I did it manually for one case, but now you gave me a better way.

You're up against one very complicated problem which is how the earth moves. And the other one is time and how Ruby handles it. It's daylight saving time here (which we now know is pretty useless but in the US politically difficult to change) which adds another complication.

puts "#{time} #{time.localtime}" gives a different answer than puts "#{time.localtime} #{time}".

rhannequin commented 6 months ago

I'm happy to help write a better code than the one from my examples, they're not optimised. 😅

We have the same problem in France, we change time zone every 6 months, between CET (UTC+01:00) and CEST (UTC+02:00) which sounds very weird. Indeed in my examples I used a fixed UTC offset but in general we use time zone objects.

Either if we already know it:

require "tzinfo"

tz = TZInfo::Timezone.get("America/Los_Angeles")

time = Time.new(2024, 5, 4, 12, 0, 0, tz)
# => 2024-05-04 12:00:00 -0700
now = tz.now
# => 2024-05-03 08:37:21.876598 -0700

Or from coordinates with gems like wheretz:

require "wheretz"
require "tzinfo"

tz = WhereTZ.get(38.5816, -121.4944)

time = Time.new(2024, 5, 4, 12, 0, 0, tz)
# => 2024-05-04 12:00:00 -0700
now = tz.now
# => 2024-05-03 08:37:21.876598 -0700

In Astronoby, times are always given in UTC so that the conversion is handled by the program using the library, at the end of all calculations. But it accepts any Time object, not necessarily in UTC.

MtnBiker commented 6 months ago

At 9:27 am, PDT

tz = TZInfo::Timezone.get("America/Los_Angeles")

time = Time.new(2024, 5, 4, 12, 0, 0, tz)
# => 2024-05-04 12:00:00 -0700
puts now = tz.now
# => 2024-05-03 08:37:21.876598 -0700
puts Time.now
# => 2024-05-03 09:27:27 -0700

2024-05-03 08:37:21.876598 -0700 which is one of the Ruby confusions.

rhannequin commented 6 months ago

Oh yes I forgot to mention that. Without a time zone argument, Time.new defaults to the machine's timezone. For me:

Time.new
# => 2024-05-03 22:02:56.117456 +0200

With now arguments, Time.new is an alias for Time.now.

I agree this is confusing, that's why we usually only deal with times in UTC, and ultimately convert them to a local time zone at the very end.

rhannequin commented 2 months ago

Closing for inactivity.