给定时间、纬度和经度,求太阳的位置

88

这个问题在三年多前就被问过。曾经有人给出了答案,但我发现了解决方案中的一个小问题。

下面的代码是用R编写的。我已经将它移植到另一种语言中,但为了确保问题不是由于我的移植造成的,我直接在R中测试了原始代码。

sunPosition <- function(year, month, day, hour=12, min=0, sec=0,
                    lat=46.5, long=6.5) {


  twopi <- 2 * pi
  deg2rad <- pi / 180

  # Get day of the year, e.g. Feb 1 = 32, Mar 1 = 61 on leap years
  month.days <- c(0,31,28,31,30,31,30,31,31,30,31,30)
  day <- day + cumsum(month.days)[month]
  leapdays <- year %% 4 == 0 & (year %% 400 == 0 | year %% 100 != 0) & day >= 60
  day[leapdays] <- day[leapdays] + 1

  # Get Julian date - 2400000
  hour <- hour + min / 60 + sec / 3600 # hour plus fraction
  delta <- year - 1949
  leap <- trunc(delta / 4) # former leapyears
  jd <- 32916.5 + delta * 365 + leap + day + hour / 24

  # The input to the Atronomer's almanach is the difference between
  # the Julian date and JD 2451545.0 (noon, 1 January 2000)
  time <- jd - 51545.

  # Ecliptic coordinates

  # Mean longitude
  mnlong <- 280.460 + .9856474 * time
  mnlong <- mnlong %% 360
  mnlong[mnlong < 0] <- mnlong[mnlong < 0] + 360

  # Mean anomaly
  mnanom <- 357.528 + .9856003 * time
  mnanom <- mnanom %% 360
  mnanom[mnanom < 0] <- mnanom[mnanom < 0] + 360
  mnanom <- mnanom * deg2rad

  # Ecliptic longitude and obliquity of ecliptic
  eclong <- mnlong + 1.915 * sin(mnanom) + 0.020 * sin(2 * mnanom)
  eclong <- eclong %% 360
  eclong[eclong < 0] <- eclong[eclong < 0] + 360
  oblqec <- 23.429 - 0.0000004 * time
  eclong <- eclong * deg2rad
  oblqec <- oblqec * deg2rad

  # Celestial coordinates
  # Right ascension and declination
  num <- cos(oblqec) * sin(eclong)
  den <- cos(eclong)
  ra <- atan(num / den)
  ra[den < 0] <- ra[den < 0] + pi
  ra[den >= 0 & num < 0] <- ra[den >= 0 & num < 0] + twopi
  dec <- asin(sin(oblqec) * sin(eclong))

  # Local coordinates
  # Greenwich mean sidereal time
  gmst <- 6.697375 + .0657098242 * time + hour
  gmst <- gmst %% 24
  gmst[gmst < 0] <- gmst[gmst < 0] + 24.

  # Local mean sidereal time
  lmst <- gmst + long / 15.
  lmst <- lmst %% 24.
  lmst[lmst < 0] <- lmst[lmst < 0] + 24.
  lmst <- lmst * 15. * deg2rad

  # Hour angle
  ha <- lmst - ra
  ha[ha < -pi] <- ha[ha < -pi] + twopi
  ha[ha > pi] <- ha[ha > pi] - twopi

  # Latitude to radians
  lat <- lat * deg2rad

  # Azimuth and elevation
  el <- asin(sin(dec) * sin(lat) + cos(dec) * cos(lat) * cos(ha))
  az <- asin(-cos(dec) * sin(ha) / cos(el))
  elc <- asin(sin(dec) / sin(lat))
  az[el >= elc] <- pi - az[el >= elc]
  az[el <= elc & ha > 0] <- az[el <= elc & ha > 0] + twopi

  el <- el / deg2rad
  az <- az / deg2rad
  lat <- lat / deg2rad

  return(list(elevation=el, azimuth=az))
}

我遇到的问题是它返回的方位似乎是错误的。例如,如果我在0ºE和41ºS、3ºS、3ºN和41ºN的位置上在(南半球)夏至12:00运行函数:

> sunPosition(2012,12,22,12,0,0,-41,0)
$elevation
[1] 72.42113

$azimuth
[1] 180.9211

> sunPosition(2012,12,22,12,0,0,-3,0)
$elevation
[1] 69.57493

$azimuth
[1] -0.79713

Warning message:
In asin(sin(dec)/sin(lat)) : NaNs produced
> sunPosition(2012,12,22,12,0,0,3,0)
$elevation
[1] 63.57538

$azimuth
[1] -0.6250971

Warning message:
In asin(sin(dec)/sin(lat)) : NaNs produced
> sunPosition(2012,12,22,12,0,0,41,0)
$elevation
[1] 25.57642

$azimuth
[1] 180.3084

这些数字似乎不太对。我对高度感到满意——前两个应该大致相同,第三个应该稍微低一些,第四个则要低得多。然而,第一个方位角应该大致朝北,但它给出的数字完全相反。剩下的三个应该大致指向正南,但只有最后一个是这样的。中间的两个指向稍微偏北,再次180度错误。

如您所见,在低纬度(接近赤道)的情况下也会触发一些错误。

我认为问题在于这一部分,在第三行触发错误(以elc开头)。

  # Azimuth and elevation
  el <- asin(sin(dec) * sin(lat) + cos(dec) * cos(lat) * cos(ha))
  az <- asin(-cos(dec) * sin(ha) / cos(el))
  elc <- asin(sin(dec) / sin(lat))
  az[el >= elc] <- pi - az[el >= elc]
  az[el <= elc & ha > 0] <- az[el <= elc & ha > 0] + twopi

我在谷歌上搜索并找到了一段类似的C代码,在转换成R代码时,用来计算方位角的那行代码应该是这样的

az <- atan(sin(ha) / (cos(ha) * sin(lat) - tan(dec) * cos(lat)))

这里的输出似乎朝着正确的方向发展,但每次转换回度数时我都无法始终得到正确的答案。

如果能更正代码(怀疑只是上面几行),使其计算出正确的方位角,那就太棒了。


2
你在数学堆栈交换中可能会有更好的运气。 - abcde123483
1
有代码可以在maptools包中完成此操作,请参见?solarpos。 - mdsumner
谢谢@ulvund - 下次可能会去那里试试。 - SpoonNZ
4
好的,我认为你应该直接复制美国国家海洋和大气管理局网站上的JavaScript代码,因为那是很多版本的源。我们编写的代码仅针对海拔高度,并且是为特定应用程序调整的。只需查看http://www.srrb.noaa.gov/highlights/sunrise/azel.html的源代码即可。 - mdsumner
1
你尝试过从我之前的问题中得到的答案吗?ephem甚至可以考虑观察者的海拔高度以及大气层的折射(受温度、压力影响)。 - jfs
显示剩余2条评论
6个回答

117

这个话题似乎很重要,所以我发了一个比通常更长的答案:如果这个算法将来被他人使用,我认为很重要的一点是要附上从中获得的文献参考资料。

简短回答

正如您所指出的,您发布的代码在赤道附近或南半球的位置上无法正常工作。

要修复它,只需在原始代码中替换这些行:

elc <- asin(sin(dec) / sin(lat))
az[el >= elc] <- pi - az[el >= elc]
az[el <= elc & ha > 0] <- az[el <= elc & ha > 0] + twopi

使用这些:

cosAzPos <- (0 <= sin(dec) - sin(el) * sin(lat))
sinAzNeg <- (sin(az) < 0)
az[cosAzPos & sinAzNeg] <- az[cosAzPos & sinAzNeg] + twopi
az[!cosAzPos] <- pi - az[!cosAzPos]

现在它应该适用于全球任何位置。

讨论

您示例中的代码几乎直接从J.J. Michalsky(Solar Energy. 40:227-235)的1988年文章中改编而来。该文章又改进了R. Walraven在1978年文章中提出的算法(太阳能。20:393-397)。Walraven报告说,该方法已经成功地用于精确定位加利福尼亚州戴维斯(38°33'14"N,121°44'17"W)的偏振辐射计。

Michalsky和Walraven的代码都包含重要/致命错误。特别是,虽然Michalsky的算法在美国大部分地区都运行良好,但在赤道附近或南半球的地区(如您所发现的)会失败。 1989年,澳大利亚维多利亚州的J.W. Spencer也注意到此事(太阳能。42(4):353):

亲爱的先生:

Michalsky用于将计算出的方位角分配给正确象限的方法,源自Walraven,当应用于南纬(负值)时,不会给出正确的值。 此外,由于除以零,将无法对零度纬度进行临界高程(elc)的计算。 只需通过考虑cos(azimuth)的符号将方位角分配给正确的象限,就可以避免这两个异议。

对您代码的编辑基于Spencer在该公开评论中提出的更正建议。 我只是稍微更改了它们,以确保R函数sunPosition()保持“向量化”(即在点位置矢量上正常工作,而不需要一次传递一个点)。

函数sunPosition()的准确性

为了测试sunPosition()是否正确工作,我将其结果与美国国家海洋和大气管理局的 Solar Calculator 计算的结果进行了比较。 在两种情况下,都计算了南半球夏至(2012年12月22日)正午(12:00 PM)的太阳位置。 所有结果都在0.02度之内达成一致。

testPts <- data.frame(lat = c(-41,-3,3, 41), 
                      long = c(0, 0, 0, 0))

# Sun's position as returned by the NOAA Solar Calculator,
NOAA <- data.frame(elevNOAA = c(72.44, 69.57, 63.57, 25.6),
                   azNOAA = c(359.09, 180.79, 180.62, 180.3))

# Sun's position as returned by sunPosition()
sunPos <- sunPosition(year = 2012,
                      month = 12,
                      day = 22,
                      hour = 12,
                      min = 0,
                      sec = 0,
                      lat = testPts$lat,
                      long = testPts$long)

cbind(testPts, NOAA, sunPos)
#   lat long elevNOAA azNOAA elevation  azimuth
# 1 -41    0    72.44 359.09  72.43112 359.0787
# 2  -3    0    69.57 180.79  69.56493 180.7965
# 3   3    0    63.57 180.62  63.56539 180.6247
# 4  41    0    25.60 180.30  25.56642 180.3083

代码中的其他错误

发布的代码中至少有两个(相当小的)错误。第一个导致闰年的2月29日和3月1日都被计入年份的第61天。第二个错误源于原始文章中的一个笔误,该笔误在Michalsky在1989年的一篇文章中得到了纠正。(太阳能43(5): 323)。

以下代码块显示了存在问题的行,已注释掉,并紧接着是更正后的版本:

# leapdays <- year %% 4 == 0 & (year %% 400 == 0 | year %% 100 != 0) & day >= 60
  leapdays <- year %% 4 == 0 & (year %% 400 == 0 | year %% 100 != 0) & 
              day >= 60 & !(month==2 & day==60)

# oblqec <- 23.429 - 0.0000004 * time
  oblqec <- 23.439 - 0.0000004 * time

sunPosition()的修正版本

这是已经验证过的修正代码:

sunPosition <- function(year, month, day, hour=12, min=0, sec=0,
                    lat=46.5, long=6.5) {

    twopi <- 2 * pi
    deg2rad <- pi / 180

    # Get day of the year, e.g. Feb 1 = 32, Mar 1 = 61 on leap years
    month.days <- c(0,31,28,31,30,31,30,31,31,30,31,30)
    day <- day + cumsum(month.days)[month]
    leapdays <- year %% 4 == 0 & (year %% 400 == 0 | year %% 100 != 0) & 
                day >= 60 & !(month==2 & day==60)
    day[leapdays] <- day[leapdays] + 1

    # Get Julian date - 2400000
    hour <- hour + min / 60 + sec / 3600 # hour plus fraction
    delta <- year - 1949
    leap <- trunc(delta / 4) # former leapyears
    jd <- 32916.5 + delta * 365 + leap + day + hour / 24

    # The input to the Atronomer's almanach is the difference between
    # the Julian date and JD 2451545.0 (noon, 1 January 2000)
    time <- jd - 51545.

    # Ecliptic coordinates

    # Mean longitude
    mnlong <- 280.460 + .9856474 * time
    mnlong <- mnlong %% 360
    mnlong[mnlong < 0] <- mnlong[mnlong < 0] + 360

    # Mean anomaly
    mnanom <- 357.528 + .9856003 * time
    mnanom <- mnanom %% 360
    mnanom[mnanom < 0] <- mnanom[mnanom < 0] + 360
    mnanom <- mnanom * deg2rad

    # Ecliptic longitude and obliquity of ecliptic
    eclong <- mnlong + 1.915 * sin(mnanom) + 0.020 * sin(2 * mnanom)
    eclong <- eclong %% 360
    eclong[eclong < 0] <- eclong[eclong < 0] + 360
    oblqec <- 23.439 - 0.0000004 * time
    eclong <- eclong * deg2rad
    oblqec <- oblqec * deg2rad

    # Celestial coordinates
    # Right ascension and declination
    num <- cos(oblqec) * sin(eclong)
    den <- cos(eclong)
    ra <- atan(num / den)
    ra[den < 0] <- ra[den < 0] + pi
    ra[den >= 0 & num < 0] <- ra[den >= 0 & num < 0] + twopi
    dec <- asin(sin(oblqec) * sin(eclong))

    # Local coordinates
    # Greenwich mean sidereal time
    gmst <- 6.697375 + .0657098242 * time + hour
    gmst <- gmst %% 24
    gmst[gmst < 0] <- gmst[gmst < 0] + 24.

    # Local mean sidereal time
    lmst <- gmst + long / 15.
    lmst <- lmst %% 24.
    lmst[lmst < 0] <- lmst[lmst < 0] + 24.
    lmst <- lmst * 15. * deg2rad

    # Hour angle
    ha <- lmst - ra
    ha[ha < -pi] <- ha[ha < -pi] + twopi
    ha[ha > pi] <- ha[ha > pi] - twopi

    # Latitude to radians
    lat <- lat * deg2rad

    # Azimuth and elevation
    el <- asin(sin(dec) * sin(lat) + cos(dec) * cos(lat) * cos(ha))
    az <- asin(-cos(dec) * sin(ha) / cos(el))

    # For logic and names, see Spencer, J.W. 1989. Solar Energy. 42(4):353
    cosAzPos <- (0 <= sin(dec) - sin(el) * sin(lat))
    sinAzNeg <- (sin(az) < 0)
    az[cosAzPos & sinAzNeg] <- az[cosAzPos & sinAzNeg] + twopi
    az[!cosAzPos] <- pi - az[!cosAzPos]

    # if (0 < sin(dec) - sin(el) * sin(lat)) {
    #     if(sin(az) < 0) az <- az + twopi
    # } else {
    #     az <- pi - az
    # }


    el <- el / deg2rad
    az <- az / deg2rad
    lat <- lat / deg2rad

    return(list(elevation=el, azimuth=az))
}

参考文献:

Michalsky, J.J. 1988. The Astronomical Almanac's algorithm for approximate solar position (1950-2050). Solar Energy. 40(3):227-235.

Michalsky, J.J. 1989. Errata. Solar Energy. 43(5):323.

Spencer, J.W. 1989. Comments on "The Astronomical Almanac's Algorithm for Approximate Solar Position (1950-2050)". Solar Energy. 42(4):353.

Walraven, R. 1978. Calculating the position of the sun. Solar Energy. 20:393-397.


感谢您的精彩回答!我上周末没来这里,所以错过了。至少到今晚我才有机会尝试一下,但看起来它似乎能解决问题。谢谢! - SpoonNZ
1
@SpoonNZ -- 不用客气。如果你需要任何被引用参考文献的PDF副本,请发送电子邮件给我,我可以将它们发送给你。 - Josh O'Brien
1
@JoshO'Brien:刚刚在另一个答案中添加了一些建议。你可能想要看一下并将它们融入到自己的答案中。 - Richie Cotton
@RichieCotton -- 谢谢您发表建议。我不会在这里添加它们,但是仅仅因为它们是 R 特定的,而且 OP 正在尝试使用 R 代码来调试它,然后再将其移植到另一种语言中。(事实上,我刚刚编辑了我的帖子以纠正原始代码中的日期处理错误,这正是支持使用像你提出的更高级别代码的错误类型。)干杯! - Josh O'Brien
也可以将儒略日组合成以下形式: 时间 = 365 * (年份 - 2000) + floor((年份 - 1949)/4) + 天数 + 小时 - 13.5 - Hawk

19

使用我在上面链接中提到的"NOAA太阳计算",我稍微修改了函数的最后一部分,使用了略微不同的算法,希望没有出错。我已经注释掉了现在无用的代码,并在将纬度转换为弧度后添加了新的算法:

# -----------------------------------------------
# New code
# Solar zenith angle
zenithAngle <- acos(sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(ha))
# Solar azimuth
az <- acos(((sin(lat) * cos(zenithAngle)) - sin(dec)) / (cos(lat) * sin(zenithAngle)))
rm(zenithAngle)
# -----------------------------------------------

# Azimuth and elevation
el <- asin(sin(dec) * sin(lat) + cos(dec) * cos(lat) * cos(ha))
#az <- asin(-cos(dec) * sin(ha) / cos(el))
#elc <- asin(sin(dec) / sin(lat))
#az[el >= elc] <- pi - az[el >= elc]
#az[el <= elc & ha > 0] <- az[el <= elc & ha > 0] + twopi

el <- el / deg2rad
az <- az / deg2rad
lat <- lat / deg2rad

# -----------------------------------------------
# New code
if (ha > 0) az <- az + 180 else az <- 540 - az
az <- az %% 360
# -----------------------------------------------

return(list(elevation=el, azimuth=az))

为了验证你所提到的四种情况下的方位角趋势,让我们将其绘制成时间的函数:

hour <- seq(from = 0, to = 23, by = 0.5)
azimuth <- data.frame(hour = hour)
az41S <- apply(azimuth, 1, function(x) sunPosition(2012,12,22,x,0,0,-41,0)$azimuth)
az03S <- apply(azimuth, 1, function(x) sunPosition(2012,12,22,x,0,0,-03,0)$azimuth)
az03N <- apply(azimuth, 1, function(x) sunPosition(2012,12,22,x,0,0,03,0)$azimuth)
az41N <- apply(azimuth, 1, function(x) sunPosition(2012,12,22,x,0,0,41,0)$azimuth)
azimuth <- cbind(azimuth, az41S, az03S, az41N, az03N)
rm(az41S, az03S, az41N, az03N)
library(ggplot2)
azimuth.plot <- melt(data = azimuth, id.vars = "hour")
ggplot(aes(x = hour, y = value, color = variable), data = azimuth.plot) + 
    geom_line(size = 2) + 
    geom_vline(xintercept = 12) + 
    facet_wrap(~ variable)

附带图片:

在此输入图像描述


@Josh O'Brien:你非常详细的回答是一篇很棒的阅读材料。另外,我们的SunPosition函数产生完全相同的结果。 - mbask
我附上了图片文件,如果你需要的话。 - mdsumner
1
@Charlie - 很棒的回答,图表是一个特别好的补充。在看到它们之前,我没有意识到在“赤道”和更“温带”的地方,夜间太阳方位坐标有多不同。真的很酷。 - Josh O'Brien

12

这里是一个更符合R语言习惯,更易于调试和维护的重写版本。它本质上与Josh的答案相同,但使用Josh和Charlie的算法来计算方位角进行比较。我还包括了我的另一个答案中对日期代码的简化。基本原则是将代码分成许多小函数,这样您可以更轻松地编写单元测试。

astronomersAlmanacTime <- function(x)
{
  # Astronomer's almanach time is the number of 
  # days since (noon, 1 January 2000)
  origin <- as.POSIXct("2000-01-01 12:00:00")
  as.numeric(difftime(x, origin, units = "days"))
}

hourOfDay <- function(x)
{
  x <- as.POSIXlt(x)
  with(x, hour + min / 60 + sec / 3600)
}

degreesToRadians <- function(degrees)
{
  degrees * pi / 180
}

radiansToDegrees <- function(radians)
{
  radians * 180 / pi
}

meanLongitudeDegrees <- function(time)
{
  (280.460 + 0.9856474 * time) %% 360
}

meanAnomalyRadians <- function(time)
{
  degreesToRadians((357.528 + 0.9856003 * time) %% 360)
}

eclipticLongitudeRadians <- function(mnlong, mnanom)
{
  degreesToRadians(
      (mnlong + 1.915 * sin(mnanom) + 0.020 * sin(2 * mnanom)) %% 360
  )
}

eclipticObliquityRadians <- function(time)
{
  degreesToRadians(23.439 - 0.0000004 * time)
}

rightAscensionRadians <- function(oblqec, eclong)
{
  num <- cos(oblqec) * sin(eclong)
  den <- cos(eclong)
  ra <- atan(num / den)
  ra[den < 0] <- ra[den < 0] + pi
  ra[den >= 0 & num < 0] <- ra[den >= 0 & num < 0] + 2 * pi 
  ra
}

rightDeclinationRadians <- function(oblqec, eclong)
{
  asin(sin(oblqec) * sin(eclong))
}

greenwichMeanSiderealTimeHours <- function(time, hour)
{
  (6.697375 + 0.0657098242 * time + hour) %% 24
}

localMeanSiderealTimeRadians <- function(gmst, long)
{
  degreesToRadians(15 * ((gmst + long / 15) %% 24))
}

hourAngleRadians <- function(lmst, ra)
{
  ((lmst - ra + pi) %% (2 * pi)) - pi
}

elevationRadians <- function(lat, dec, ha)
{
  asin(sin(dec) * sin(lat) + cos(dec) * cos(lat) * cos(ha))
}

solarAzimuthRadiansJosh <- function(lat, dec, ha, el)
{
  az <- asin(-cos(dec) * sin(ha) / cos(el))
  cosAzPos <- (0 <= sin(dec) - sin(el) * sin(lat))
  sinAzNeg <- (sin(az) < 0)
  az[cosAzPos & sinAzNeg] <- az[cosAzPos & sinAzNeg] + 2 * pi
  az[!cosAzPos] <- pi - az[!cosAzPos]
  az
}

solarAzimuthRadiansCharlie <- function(lat, dec, ha)
{
  zenithAngle <- acos(sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(ha))
  az <- acos((sin(lat) * cos(zenithAngle) - sin(dec)) / (cos(lat) * sin(zenithAngle)))
  ifelse(ha > 0, az + pi, 3 * pi - az) %% (2 * pi)
}

sunPosition <- function(when = Sys.time(), format, lat = 46.5, long = 6.5) 
{    
  if(is.character(when)) when <- strptime(when, format)
  when <- lubridate::with_tz(when, "UTC")
  time <- astronomersAlmanacTime(when)
  hour <- hourOfDay(when)

  # Ecliptic coordinates  
  mnlong <- meanLongitudeDegrees(time)   
  mnanom <- meanAnomalyRadians(time)  
  eclong <- eclipticLongitudeRadians(mnlong, mnanom)     
  oblqec <- eclipticObliquityRadians(time)

  # Celestial coordinates
  ra <- rightAscensionRadians(oblqec, eclong)
  dec <- rightDeclinationRadians(oblqec, eclong)

  # Local coordinates
  gmst <- greenwichMeanSiderealTimeHours(time, hour)  
  lmst <- localMeanSiderealTimeRadians(gmst, long)

  # Hour angle
  ha <- hourAngleRadians(lmst, ra)

  # Latitude to radians
  lat <- degreesToRadians(lat)

  # Azimuth and elevation
  el <- elevationRadians(lat, dec, ha)
  azJ <- solarAzimuthRadiansJosh(lat, dec, ha, el)
  azC <- solarAzimuthRadiansCharlie(lat, dec, ha)

  data.frame(
      elevation = radiansToDegrees(el), 
      azimuthJ  = radiansToDegrees(azJ),
      azimuthC  = radiansToDegrees(azC)
  )
}

请注意,在测试 NOAA 网站(http://www.esrl.noaa.gov/gmd/grad/solcalc/azel.html)时,NOAA 使用西经度作为正值。而此算法使用西经度作为负值。 - Neon22
当我运行“sunPosition(lat = 43, long = -89)”时,我得到了一个高度为52和方位角为175。但是使用NOAA的Web应用程序http://www.esrl.noaa.gov/gmd/grad/solcalc/,我得到了大约5的高度和272的方位角。我错过了什么吗?NOAA是正确的,但我无法让sunPosition给出准确的结果。 - Tedward
@Tedward sunPosition 默认使用当前时间和日期。这是你想要的吗? - Richie Cotton
是的。我也尝试了一些不同的时间。这是在一天结束时,我今天会重新开始尝试。我相当确定我做错了什么,但不知道是什么。我会继续努力的。 - Tedward
我需要将“when”转换为UTC以获得准确的结果。请参见https://dev59.com/CJrga4cB1Zd3GeqPpJgs#39396807。@aichao提供了转换代码建议。 - Tedward
@Tedward,我在sunPosition中添加了对lubridate::with_tz的调用。请您检查一下现在是否得到了正确的答案。 - Richie Cotton

10
这是对Josh优秀回答的建议性更新。
函数的开头很多都是样板代码,用来计算自2000年1月1日中午以来的天数。使用R现有的日期和时间函数可以更好地处理这个问题。
我认为,与其使用六个不同的变量来指定日期和时间,更容易(并且更符合其他R函数的做法)指定一个现有的日期对象或日期字符串+格式字符串。
下面是两个辅助函数。
astronomers_almanac_time <- function(x)
{
  origin <- as.POSIXct("2000-01-01 12:00:00")
  as.numeric(difftime(x, origin, units = "days"))
}

hour_of_day <- function(x)
{
  x <- as.POSIXlt(x)
  with(x, hour + min / 60 + sec / 3600)
}

现在该函数的开头变得更简单了

sunPosition <- function(when = Sys.time(), format, lat=46.5, long=6.5) {

  twopi <- 2 * pi
  deg2rad <- pi / 180

  if(is.character(when)) when <- strptime(when, format)
  time <- astronomers_almanac_time(when)
  hour <- hour_of_day(when)
  #...
另一个奇怪的地方在于像这样的行:

mnlong[mnlong < 0] <- mnlong[mnlong < 0] + 360

由于已经对 mnlong 的值调用了 %%,因此它们应该已经是非负数了,所以这行代码是多余的。


非常感谢!如前所述,我已将其移植到PHP(可能还会移植到JavaScript - 只是需要决定在哪里处理哪些函数),因此该代码对我没有太大帮助,但应该能够被移植(尽管比原始代码需要更多的思考!)。我需要稍微调整一下处理时区的代码,所以也许可以同时集成这个变化。 - SpoonNZ
2
聪明的更改@Richie Cotton。请注意,赋值hour <- hour_of_day实际上应该是hour <- hour_of_day(when),而变量time应该保存天数的数量,而不是"class" difftime "的对象。函数astronomers_almanac_time的第二行应更改为类似于as.numeric(difftime(x, origin, units = "days"), units = "days")的内容。 - mbask
1
感谢您提供的好建议。如果您有兴趣,可以在您的帖子中包含一个编辑过的 sunPosition() 函数的完整版本,使其在构造上更符合 R 语言的风格。 - Josh O'Brien
@JoshO'Brien:好的,我已经将答案设为社区维基,因为它是我们所有答案的结合体。它给出了与您相同的当前时间和默认(瑞士?)坐标的答案,但需要进行更多的测试。 - Richie Cotton
@RichieCotton -- 多好的想法。一有机会,我会深入地研究你所做的事情。 - Josh O'Brien

8

我在一个Python项目中需要太阳位置。我采用了Josh O'Brien的算法。

感谢Josh。

如果有人觉得有用,这是我的适应。

请注意,我的项目只需要即时太阳位置,因此时间不是参数。

def sun_position(lat=46.5, long=6.5):

    # Latitude [rad]
    lat_rad = math.radians(lat)

    # Get Julian date - 2400000
    day = time.gmtime().tm_yday
    hour = time.gmtime().tm_hour + \
           time.gmtime().tm_min/60.0 + \
           time.gmtime().tm_sec/3600.0
    delta = time.gmtime().tm_year - 1949
    leap = delta / 4
    jd = 32916.5 + delta * 365 + leap + day + hour / 24
 
    # The input to the Atronomer's almanach is the difference between
    # The Julian date and JD 2451545.0 (noon, 1 January 2000)
    t = jd - 51545

    # Ecliptic coordinates

    # Mean longitude
    mnlong_deg = (280.460 + .9856474 * t) % 360

    # Mean anomaly
    mnanom_rad = math.radians((357.528 + .9856003 * t) % 360)

    # Ecliptic longitude and obliquity of ecliptic
    eclong = math.radians((mnlong_deg + 
                           1.915 * math.sin(mnanom_rad) + 
                           0.020 * math.sin(2 * mnanom_rad)
                          ) % 360)
    oblqec_rad = math.radians(23.439 - 0.0000004 * t)

    # Celestial coordinates
    # Right ascension and declination
    num = math.cos(oblqec_rad) * math.sin(eclong)
    den = math.cos(eclong)
    ra_rad = math.atan(num / den)
    if den < 0:
        ra_rad = ra_rad + math.pi
    elif num < 0:
        ra_rad = ra_rad + 2 * math.pi
    dec_rad = math.asin(math.sin(oblqec_rad) * math.sin(eclong))

    # Local coordinates
    # Greenwich mean sidereal time
    gmst = (6.697375 + .0657098242 * t + hour) % 24
    # Local mean sidereal time
    lmst = (gmst + long / 15) % 24
    lmst_rad = math.radians(15 * lmst)

    # Hour angle (rad)
    ha_rad = (lmst_rad - ra_rad) % (2 * math.pi)

    # Elevation
    el_rad = math.asin(
        math.sin(dec_rad) * math.sin(lat_rad) + \
        math.cos(dec_rad) * math.cos(lat_rad) * math.cos(ha_rad))
    
    # Azimuth
    az_rad = math.asin(
        - math.cos(dec_rad) * math.sin(ha_rad) / math.cos(el_rad))
    
    if (math.sin(dec_rad) - math.sin(el_rad) * math.sin(lat_rad) < 0):
        az_rad = math.pi - az_rad
    elif (math.sin(az_rad) < 0):
        az_rad += 2 * math.pi
    
    return el_rad, az_rad

这对我非常有用。谢谢。我所做的一件事是添加了夏令时的调整。如果有用的话,它很简单: 如果(time.localtime().tm_isdst == 1): 小时 += 1 - Mark Ireland

1
我在实现Charlie的代码时,遇到了一个与数据点和Richie Cotton的函数相关的小问题。
longitude= 176.0433687000000020361767383292317390441894531250
latitude= -39.173830619999996827118593500927090644836425781250
event_time = as.POSIXct("2013-10-24 12:00:00", format="%Y-%m-%d %H:%M:%S", tz = "UTC")
sunPosition(when=event_time, lat = latitude, long = longitude)
elevation azimuthJ azimuthC
1 -38.92275      180      NaN
Warning message:
In acos((sin(lat) * cos(zenithAngle) - sin(dec))/(cos(lat) * sin(zenithAngle))) : NaNs produced

因为在solarAzimuthRadiansCharlie函数中,围绕180度角存在浮点数的问题,如下所示:(sin(lat) * cos(zenithAngle) - sin(dec)) / (cos(lat) * sin(zenithAngle)) 稍微超过1的最小值是1.0000000000000004440892098,这会导致输入到acos的NaN值,因为输入值不应该大于1或小于-1。

我怀疑Josh的计算可能也存在类似的边缘情况,在asin步骤的输入值超出-1:1时会导致浮点舍入效应,但在我的特定数据集中没有遇到这种情况。

在我遇到的六个左右的情况中,“真实”(正午或深夜)是发生问题的时候,因此从经验上来看,真实值应该是1/-1。出于这个原因,我会在solarAzimuthRadiansJosh和solarAzimuthRadiansCharlie中应用四舍五入步骤来解决这个问题。我不确定NOAA算法的理论精度是多少(数字精度停止重要的点),但将数据四舍五入到12位小数可以解决我的数据集中的数据问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接