1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
|
;;; -*- Mode: LISP; Syntax: Common-lisp; Base: 10.; -*-
;;; Sun Oct 21 18:49:59 1990 by Mark Kantrowitz <mkant@GLINDA.OZ.CS.CMU.EDU>
;;; sunset.lisp
;;; ****************************************************************
;;; Sunset and Sunrise Times ***************************************
;;; ****************************************************************
;;;
;;; This program calculates the time of sunrise and sunset given the
;;; date, longitude and latitude.
;;;
;;; It is based upon a pascal program sent to me by Ed Reingold
;;; <reingold@cs.uiuc.edu>. The formula is taken from the
;;; Almanac for Computers 1984, prepared by the Nautical Almanac Office,
;;; United States Naval Observatory, Washington, 1984.
;;;
;;; Portions of lisp code from "Calendrical Calculations" by Nachum
;;; Dershowitz and Edward M. Reingold, Software -- Practice & Experience,
;;; vol. 20, no. 9 (September, 1990), pp. 899-928.
;;;
;;; This code is in the public domain, but any use of it should acknowledge
;;; the source.
;;;
;;; Written by Mark Kantrowitz, mkant@cs.cmu.edu, October 21, 1990.
;;; ****************************************************************
;;; Documentation **************************************************
;;; ****************************************************************
;;;
;;; WARNING: the calculations will be accurate only to +/- 2 min, locations
;;; should be between 65deg north and 65deg south, for dates in the latter
;;; half of the 20th century!
;;;
;;; does not currently handle precession of the axes and refraction effects
;;; of the atmosphere (the latter affects the time at which the sun crosses
;;; the apparent horizon).
;;;
(defpackage :sunset (:export #:time-of-phenomenon))
(in-package :sunset)
;;; RANDOM NOTES:
;;;
;;; Sidereal time = time required for a body to return to the equivalent
;;; position as defined by the stars.
;;;
;;; Correction for refraction, parallax, size of sun = 0.835608 degrees
;;;
;;; Spherical Polar Coordinates:
;;; Reference plane through the equator with origin at center of earth
;;; (a great circle of the celestial sphere).
;;; A great circle passing through a star and the celestial poles is called
;;; an hour circle.
;;;
;;; An observer's local meridian is the great circle passing through the
;;; observer's zenith and the celestial poles. The angle measured westward
;;; from this meridian to the hour circle is called the hour angle (HA) of the
;;; star. The hour angle of the vernal equinox is defined as the local
;;; sidereal time of the observer; therefore,
;;; right ascension + hour angle = sidereal time.
;;; Since the vernal equinox and equator are not fixed, because of precession,
;;; one must specify at what date (epoch) the coordinates were measured.
;;; ********************************
;;; Latitude, Longitude, Time-Zone *
;;; ********************************
;;; Time-zone differences are negative, since we are behind greenwich.
(defvar *location-table*
;; CITY, LATITUDE, LONGITUDE, TIME-ZONE
'((("Boston, MA" "MIT") 42.3 -71.1 -5)
("New York, NY" 40.7 -74.0 -5)
("San Francisco" 37.8 -122.4 -8)
("Los Angeles (LA)" 34.1 -118.3 -8)
("Philadelphia" 39.6 -75.1 -5)
("Chicago" 41.8 -87.7 -6)
(("Pittsburgh" "CMU") 40.4 -80.0 -5)
("Providence, RI" 41.8 -71.4 -5)
("Washington, DC" 38.8 -77.0 -5)
("Seattle, WA" 47.6 -122.3 -8)
("Beaverton, OR" 45.5 -122.8 -8)
("UIUC" 40.1 -88.2 -6)))
(defvar *latitude* 40.4
"Latitude in degrees, + north, - south.")
(defvar *longitude* -80
"Longitude in degrees, + east, - west.")
(defvar *time-zone* -5
"Number of hours difference between local time and universal time.")
(defun setup-latitude-longitude (city)
"This function sets the default *latitude* and *longitude* based on that
of city, if the city name is listed in the location table."
(let ((entry (assoc city *location-table*
:test #'(lambda (city1 city2)
(if (listp city2)
(member city1 city2 :test #'search)
(search city1 city2))))))
(when entry
(setq *latitude* (second entry)
*longitude* (third entry)
*time-zone* (fourth entry)))
(values)))
;;; We're in Pittsburgh, so let's make it the default.
(setup-latitude-longitude "CMU")
;;; ********************************
;;; Trigonometry *******************
;;; ********************************
(defconstant *pi* (if (boundp 'pi) pi 3.141592653589793))
(defun deg-to-rad (deg)
"Converts degrees to radians, since Common Lisp trig functions use radians."
(* deg (/ *pi* 180)))
(defun rad-to-deg (rad)
"Converts radians to degrees."
(* rad (/ 180 *pi*)))
(defun sin-deg (x)
(sin (deg-to-rad x)))
(defun cos-deg (x)
(cos (deg-to-rad x)))
(defun tan-deg (x)
(tan (deg-to-rad x)))
(defun xy->quadrant (x y)
"Determines which quadrant the point (x, y) is in."
(if (plusp x)
(if (plusp y) 1 4)
(if (plusp y) 2 3)))
(defun angle->quadrant (angle)
;; Force angle to lie between 0 and 360.
(setq angle (mod angle 360))
;; Check quadrant based on angle
(1+ (truncate angle 90)))
(defun arctan (x quad)
"Computes the arctangent of a real value. Quad indicates which quadrant
the value was taken from. Since Lisp's standard function has a range of
pi/2 to -pi/2, angles in quadrant 2 or 3 will be returned in quadrants
1 and 4. By adding or subtracting pi, an angle in the correct quadrant
is returned."
(let ((temp (rad-to-deg (atan x))))
(case quad
(2 (+ temp 180))
(3 (+ temp 180))
(4 (+ temp 360))
(1 temp))))
(defun square (x)
(* x x))
(defun arccos (x)
(let ((y (sqrt (- 1 (square x)))))
(arctan (/ y x)
(xy->quadrant x y))))
(defun arcsin (y)
(let ((x (sqrt (- 1 (square y)))))
(arctan (/ y x)
(xy->quadrant x y))))
;;; ********************************
;;; Constants **********************
;;; ********************************
;;; Inclination of earth equator to orbit (epsilon) = ~23.45 degrees
(defvar *earth-inclination* 23.441884
"Inclination of earth's equator to its solar orbit in degrees.")
(defvar *cos-inclination* (cos-deg *earth-inclination*) ; was 0.91746
"Cosine of earth's inclination.")
(defvar *sin-inclination* (sin-deg *earth-inclination*) ; 0.39782
"Sine of earth's inclination.")
;;; Ellipse:
;;; eccentricity (e) = c/a where c^2 = a^2 - b^2, and
;;; x = a Cos(theta), y = b Sin(theta)
;;; For the Earth's orbit, we have:
;;; a = 149457000 and e = 0.016718
;;; aphelion = a+x =1.5207x10^8, perhelion = a-x = 1.4707x10^8
;;; longitude of perhelion = 101.983 degrees.
;;; r(perhelion) = r(pi) = 0.983298
;;; r(aphelion) = r(alpha) = 1.016744
(defvar *earth-orbit-eccentricity* 0.016718
"Eccentricity of orbit of the earth around the sun.")
(defvar *earth-precession* (/ 5025.64 3600.0)
"Precession (P) of the earth's axes in degrees per tropical century.")
;;; Radius of Earth:
;;; average 6371.315 km +/- 0.437
;;; equatorial 6378.533
;;; polar 6356.912
;;; ellipticity 0.003393
;;;
;;; ********************************
;;; Primitives *********************
;;; ********************************
(defun deg-to-hour (deg)
;; 360 degrees is a full day, 24 hours per day,
;; so there are 15 degrees per hour.
(/ deg 15))
(defun hour-to-day (hour)
(/ hour 24))
;;; ********************************
;;; Day Number *********************
;;; ********************************
(defmacro sum (expression index initial condition)
"Sum $expression$ for $index$ = $initial$ and successive integers,
as long as $condition$ holds."
(let* ((temp (gensym)))
`(do ((,temp 0 (+ ,temp ,expression))
(,index ,initial (1+ ,index)))
((not ,condition) ,temp))))
(defun last-day-of-gregorian-month (month year)
"Last day in Gregorian $month$ during $year."
;; If February is a leap year, return 29,
;; otherwise the number of days in the month.
(if (and (= month 2)
(zerop (mod year 4))
(not (member (mod year 400) '(100 200 300))))
29
(nth (1- month)
(list 31 28 31 30 31 30 31 31 30 31 30 31))))
(defun daynumber (month day year)
(+ day
(sum ;; Days in prior months this year.
(last-day-of-gregorian-month m year) m 1 (< m month))))
;;; ********************************
;;; Solar Longitude ****************
;;; ********************************
;;; I believe that the 282... figure is what needs to be modified to
;;; take precession into account. Then, instead of just playing with
;;; the day-number, we'll need to have the year as well. I need to
;;; have the value for a fixed date, say 1900, and then add based
;;; on the precession.
(defun longitude-of-sun (approx)
"Given an approximate time for the phenomenon, in terms of day of the year,
returns the longitude of the sun."
;; Also known as lambda
(let ((mean-anomaly
;; mean anomaly of sun
(- (* 0.9856 approx) 3.289)))
;; longitude of the sun in degrees
(mod (+ mean-anomaly
(* 1.916 (sin-deg mean-anomaly))
(* 0.020 (sin-deg (* 2 mean-anomaly)))
282.634)
360)))
;;; This seems to be slightly more accurate.
;;; Do (defun longitude-of-sun (x) (solar-longitude x)) to use.
(defun solar-longitude (ed)
;; Also known as lambda
(let* ((n (mod (* 360.0 (/ ed 365.2422)) 360.0)) ; degrees
(m (deg-to-rad (mod (+ n (- 278.83354 282.596403)) 360.0)))
(e m)
errt)
(loop
(when (<= (setq errt (- e (+ (* *earth-orbit-eccentricity*
(sin e))
m)))
0.0000001)
(return))
(decf e (/ errt (- 1 (* *earth-orbit-eccentricity* (cos e))))))
(mod (+ (rad-to-deg (* 2 (atan (* 1.0168601 (tan (/ e 2))))))
282.596403)
360)))
;;; ********************************
;;; Declination & Right Ascension **
;;; ********************************
;;; The declination and right ascension are used together to give the
;;; position of a star with reference to the celestial equator and vernal
;;; equinox respectively.
;;;
;;; declination = angular distance between a celestial object and the
;;; celestial equator, measured along an hour circle, with
;;; north positive and south negative. (lowercase greek delta)
;;; right ascension = angle measured from the vernal equinox in the west to
;;; east direction, which is opposite to the apparent rotation
;;; of the celestial sphere, measured in hours, such that
;;; 360 degrees is equivalent to 24 hours. (lowercase alpha)
;;;
(defun right-ascension (lambda)
"Returns the right-ascension (alpha) of the sun,
given its longitude (lambda)."
(deg-to-hour (arctan (* *cos-inclination* (tan-deg lambda))
(angle->quadrant lambda))))
(defun declination (lambda)
"Returns the declination (delta) of the sun,
given its longitude (lambda)."
(arcsin (* *sin-inclination* (sin-deg lambda))))
;;; ********************************
;;; Time of Phenomenon *************
;;; ********************************
(defun time-of-phenomenon (month day year &optional (occurrence :sunset)
(latitude *latitude*) (longitude *longitude*)
(time-zone *time-zone*))
"Calculates the time of either sunrise or sunset for the given time and
location. For daylight savings time, add one hour.
Occurrence may be either :sunset, :sunrise, or :candle-lighting.
(Candle-lighting is 18 minutes before sunset, except in Jerusalem.)"
(let* ((dayofyear (daynumber month day year))
(approx
;; approximate time of phenomenon
(case occurrence
((:sunset :candle-lighting)
(+ dayofyear (hour-to-day (- 18 (deg-to-hour longitude)))))
(:sunrise
(+ dayofyear (hour-to-day (- 6 (deg-to-hour longitude)))))))
(longitude-of-sun (longitude-of-sun approx))
(right-ascension (right-ascension longitude-of-sun))
(declination (declination longitude-of-sun))
(coslocaltime (/ (- (cos-deg (+ 90 (/ 50 60)))
(* (sin-deg declination) (sin-deg latitude)))
(* (cos-deg declination) (cos-deg latitude)))))
(if (> (abs coslocaltime) 1)
(format t "~&No sunset that day.")
(let ((localtime
;; local time of the phenomenon
(arccos coslocaltime))
local-mean-time ; T: Local Mean time of phenomenon
universal-time ; Local time of phenomenon
stdtime ; actual time of phenomenon
)
(case occurrence
(:sunrise (setq localtime (deg-to-hour (- 360 localtime))))
((:sunset :candle-lighting) (setq localtime (deg-to-hour localtime))))
(setq local-mean-time
(mod (- (+ localtime right-ascension)
(+ (* 0.0657098 approx)
6.622))
24))
(setq universal-time (- local-mean-time (deg-to-hour longitude))
;; Since the time-zone lines only approximately follow
;; the 15 degree longitude lines, we can't simply use
;; (floor (deg-to-hour longitude)) for the time-zone.
stdtime (+ universal-time time-zone))
(when (eq occurrence :candle-lighting)
(setq stdtime (- stdtime (/ 18 60))))
(multiple-value-bind (hour minute) (floor stdtime)
(format t "~&~A ~D/~D at ~2,'0D:~2,'0D"
occurrence month day hour (round (* 60 minute))))
(values)))))
;;; ********************************
;;; Examples ***********************
;;; ********************************
#|
* (setup-latitude-longitude "CMU")
* (time-of-phenomenon 10 19 1990 :candle-lighting)
CANDLE-LIGHTING 10/19 at 17:16
* (time-of-phenomenon 10 19 1990 :sunset)
SUNSET 10/19 at 17:34
* (time-of-phenomenon 10 19 1990 :sunrise)
SUNRISE 10/19 at 06:35
* (setup-latitude-longitude "Boston")
* (time-of-phenomenon 10 19 1990 :candle-lighting)
CANDLE-LIGHTING 10/19 at 16:39
* (setup-latitude-longitude "UIUC")
* (time-of-phenomenon 10 19 1990 :candle-lighting)
CANDLE-LIGHTING 10/19 at 16:50
|#
;;; need to make adjustment for daylight davings time. i.e., add 1 hour then.
|