solar 0.0.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +30 -5
- data/README.rdoc +2 -2
- data/VERSION +1 -1
- data/lib/solar.rb +7 -462
- data/lib/solar/day_night.rb +65 -0
- data/lib/solar/lambert.rb +68 -0
- data/lib/solar/passages.rb +174 -0
- data/lib/solar/position.rb +51 -0
- data/lib/solar/radiation.rb +258 -0
- data/lib/solar/support.rb +228 -0
- data/solar.gemspec +15 -8
- data/test/test_solar.rb +11 -0
- metadata +29 -38
@@ -0,0 +1,65 @@
|
|
1
|
+
module Solar
|
2
|
+
class <<self
|
3
|
+
# Day-night (or twilight) status at a given position and time.
|
4
|
+
# Returns +:night+, +:day+ or +:twilight+
|
5
|
+
#
|
6
|
+
# Options:
|
7
|
+
#
|
8
|
+
# * +:twilight_zenith+ is the zenith for the sun at dawn
|
9
|
+
# (at the beginning of twilight)
|
10
|
+
# and at dusk (end of twilight). Default value is +:civil+
|
11
|
+
# * +:day_zenith+ is the zenith for the san at sunrise and sun set.
|
12
|
+
# Default value is +:official+ (sun aparently under the horizon,
|
13
|
+
# tangent to it)
|
14
|
+
#
|
15
|
+
# These parameters can be assigned zenith values in degrees or these
|
16
|
+
# symbols: +:official+, +:civil+, +:nautical+ or +:astronomical+.
|
17
|
+
#
|
18
|
+
# A simple day/night result (returning either +:day+ or +:night+)
|
19
|
+
# can be requested by setting the +:simple+ option to true
|
20
|
+
# (which usses the official day definition),
|
21
|
+
# or by setting a +:zenith+ parameter to define the kind of day-night
|
22
|
+
# distinction.
|
23
|
+
#
|
24
|
+
# By setting the +:detailed+ option to true, the result will be one of:
|
25
|
+
# +:night+, +:astronomical_twilight+, +:nautical_twilight+,
|
26
|
+
# +:civil_twilight+, +:day+
|
27
|
+
#
|
28
|
+
def day_or_night(t, longitude, latitude, options = {})
|
29
|
+
h, _ = position(t, longitude, latitude)
|
30
|
+
options = { zenith: :official } if options[:simple]
|
31
|
+
if options[:detailed]
|
32
|
+
if h < ALTITUDES[:astronomical]
|
33
|
+
:night
|
34
|
+
elsif h < ALTITUDES[:nautical]
|
35
|
+
:astronomical_twilight
|
36
|
+
elsif h < ALTITUDES[:civil]
|
37
|
+
:nautical_twilight
|
38
|
+
elsif h < ALTITUDES[:official]
|
39
|
+
:civil_twilight
|
40
|
+
else
|
41
|
+
:day
|
42
|
+
end
|
43
|
+
else
|
44
|
+
# Determined :night / :twilight / :day state;
|
45
|
+
# twilight/day definition can be changed with options :zenith or :twilight_zenith, :day_zenith
|
46
|
+
if options[:zenith]
|
47
|
+
# only :day / :night distinction as defined by :zenith
|
48
|
+
twilight_altitude = day_altitude = altitude_from_options(options)
|
49
|
+
else
|
50
|
+
twilight_altitude = altitude_from_options(
|
51
|
+
zenith: options[:twilight_zenith] || :civil
|
52
|
+
)
|
53
|
+
day_altitude = altitude_from_options(
|
54
|
+
zenith: options[:day_zenith] || :official
|
55
|
+
)
|
56
|
+
end
|
57
|
+
if h > day_altitude
|
58
|
+
:day
|
59
|
+
else
|
60
|
+
h <= twilight_altitude ? :night : :twilight
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Solar
|
2
|
+
class <<self
|
3
|
+
|
4
|
+
# slope, sun_azimuth: degrees (0: horizontal; 90: vertical)
|
5
|
+
# aspect: degrees from North towards East
|
6
|
+
# sun_elevation: degrees (0 on horizon, 90 on zenith)
|
7
|
+
def illumination_factor(sun_azimuth, sun_elevation, slope, aspect)
|
8
|
+
s = sun_vector(sun_azimuth, sun_elevation)
|
9
|
+
nh = vertical_vector
|
10
|
+
n = normal_from_slope_aspect(slope, aspect)
|
11
|
+
# Problem: when sun near horizon...
|
12
|
+
f = dot(s, n) / dot(s, nh)
|
13
|
+
f < 0 ? 0.0 : f
|
14
|
+
end
|
15
|
+
|
16
|
+
def illumination_factor_at(t, longitude, latitude, slope, aspect)
|
17
|
+
sun_elevation, sun_azimuth = Solar.position(t, longitude, latitude)
|
18
|
+
return 0.0 if sun_elevation < 0 # should return 1.0, really
|
19
|
+
illumination_factor(sun_azimuth, sun_elevation, slope, aspect)
|
20
|
+
end
|
21
|
+
|
22
|
+
def mean_illumination_factor_on(date, latitude, slope, aspect, options = {})
|
23
|
+
t1, t2, t3 = Solar.passages(date, 0, latitude, altitude: 24.0)
|
24
|
+
if options[:noon]
|
25
|
+
return factor_at(t2, 0.0, latitude, slope, aspect)
|
26
|
+
end
|
27
|
+
if n = options[:n]
|
28
|
+
dt = (t3 - t1).to_f*24*3600/n
|
29
|
+
else
|
30
|
+
dt = (options[:dt] || 60*30.0).to_f
|
31
|
+
end
|
32
|
+
f = 0.0
|
33
|
+
n = 0
|
34
|
+
# max_f = 0.0
|
35
|
+
(t1..t3).step(dt).each do |t|
|
36
|
+
f += illumination_factor_at(Time.at(t), 0.0, latitude, slope, aspect)
|
37
|
+
n += 1
|
38
|
+
end
|
39
|
+
f / n
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def dot(u, v)
|
45
|
+
u[0]*v[0] + u[1]*v[1] + u[2]*v[2]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Vertical unitary vector (normal to the horizontal plane)
|
49
|
+
def vertical_vector
|
50
|
+
[0, 0, 1]
|
51
|
+
end
|
52
|
+
|
53
|
+
# normal unitary vector in X (horizontal plane to N),
|
54
|
+
# Y (horizontal plane to E), Z (vertical upward) system
|
55
|
+
def normal_from_slope_aspect(slope, aspect)
|
56
|
+
a = to_rad(aspect)
|
57
|
+
b = to_rad(slope)
|
58
|
+
[Math.sin(b)*Math.sin(a), Math.sin(b)*Math.cos(a), Math.cos(b)]
|
59
|
+
end
|
60
|
+
|
61
|
+
# sun vector in X Y Z system
|
62
|
+
def sun_vector(sun_azimuth, sun_elevation)
|
63
|
+
a = to_rad(sun_azimuth)
|
64
|
+
b = to_rad(sun_elevation)
|
65
|
+
[Math.cos(b)*Math.sin(a), Math.cos(b)*Math.cos(a), Math.sin(b)]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Solar
|
2
|
+
class <<self
|
3
|
+
# Sun rise time for a given date (UTC) and position.
|
4
|
+
#
|
5
|
+
# The +:zenith+ or +:altitude+ of the sun can be passed as an argument,
|
6
|
+
# which can be numeric (in degrees) or symbolic:
|
7
|
+
# +:official+, +:civil+, +:nautical+ or +:astronomical+.
|
8
|
+
#
|
9
|
+
# +nil+ is returned if the sun doesn't rise at the date and position.
|
10
|
+
#
|
11
|
+
def rise(date, longitude, latitude, options = {})
|
12
|
+
rising, _, setting = passages(date, longitude, latitude, options)
|
13
|
+
if rising == setting || (setting - rising) == 1
|
14
|
+
nil # rising==setting => no rise; setting-rising == 1 => no set
|
15
|
+
else
|
16
|
+
rising
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Sun set time for a given date (UTC) and position.
|
21
|
+
#
|
22
|
+
# The +:zenith+ or +:altitude+ of the sun can be passed as an argument,
|
23
|
+
# which can be numeric (in degrees) or symbolic:
|
24
|
+
# +:official+, +:civil+, +:nautical+ or +:astronomical+.
|
25
|
+
#
|
26
|
+
# +nil+ is returned if the sun doesn't set at the date and position.
|
27
|
+
#
|
28
|
+
def set(date, longitude, latitude, options = {})
|
29
|
+
rising, _, setting = passages(date, longitude, latitude, options)
|
30
|
+
if rising==setting || (setting-rising)==1
|
31
|
+
nil # rising==setting => no rise; setting-rising == 1 => no set
|
32
|
+
else
|
33
|
+
setting
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Rise and set times as given by rise() and set()
|
38
|
+
def rise_and_set(date, longitude, latitude, options = {})
|
39
|
+
rising, _, setting = passages(date, longitude, latitude, options)
|
40
|
+
if rising==setting || (setting-rising)==1
|
41
|
+
nil # rising==setting => no rise; setting-rising == 1 => no set
|
42
|
+
else
|
43
|
+
[rising, setting]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Solar passages [rising, transit, setting] for a given date (UTC) and position.
|
48
|
+
#
|
49
|
+
# The +:zenith+ or +:altitude+ of the sun can be passed as an argument,
|
50
|
+
# which can be numeric (in degrees) or symbolic:
|
51
|
+
# +:official+, +:civil+, +:nautical+ or +:astronomical+.
|
52
|
+
#
|
53
|
+
# In circumpolar case:
|
54
|
+
# * If Sun never rises, returns 00:00:00 on Date for all passages.
|
55
|
+
# * If Sun never sets, returns 00:00:00 (rising), 12:00:00 (transit),
|
56
|
+
# 24:00:00 (setting) on Date for all passages.
|
57
|
+
#
|
58
|
+
def passages(date, longitude, latitude, options = {})
|
59
|
+
ho = altitude_from_options(options)
|
60
|
+
t = to_jc(jd_r(date.to_datetime))
|
61
|
+
theta0 = (100.46061837 + (36000.770053608 + (0.000387933 - t/38710000)*t)*t) % 360
|
62
|
+
|
63
|
+
# Calculate apparent right ascention and declination for 0 hr Dynamical time for three days (degrees)
|
64
|
+
ra = []
|
65
|
+
decl = []
|
66
|
+
-1.upto(1) do |i|
|
67
|
+
declination, right_ascention = equatorial_position_rad((date+i).to_datetime)
|
68
|
+
ra << to_deg(right_ascention)
|
69
|
+
decl << to_deg(declination)
|
70
|
+
end
|
71
|
+
# tweak right ascention around 180 degrees (autumnal equinox)
|
72
|
+
if ra[0] > ra[1]
|
73
|
+
ra[0] -= 360
|
74
|
+
end
|
75
|
+
if ra[2] < ra[1]
|
76
|
+
ra[2] += 360
|
77
|
+
end
|
78
|
+
|
79
|
+
ho_rad, latitude_rad = [ho, latitude].map{|x| to_rad(x)}
|
80
|
+
decl_rad = decl.map { |x| to_rad(x) }
|
81
|
+
|
82
|
+
# approximate Hour Angle (degrees)
|
83
|
+
ha = Math.sin(ho_rad) /
|
84
|
+
Math.cos(latitude_rad) * Math.cos(decl_rad[1]) -
|
85
|
+
Math.tan(latitude_rad) * Math.tan(decl_rad[1])
|
86
|
+
# handle circumpolar. see note 2 at end of chapter
|
87
|
+
if ha.abs <= 1
|
88
|
+
ha = to_deg(Math.acos(ha))
|
89
|
+
elsif ha > 1 # circumpolar - sun never rises
|
90
|
+
# format sunrise, sunset & solar noon as DateTime
|
91
|
+
sunrise = date.to_datetime
|
92
|
+
transit = date.to_datetime
|
93
|
+
sunset = date.to_datetime
|
94
|
+
return [sunrise, transit, sunset]
|
95
|
+
else # cirumpolar - sun never sets
|
96
|
+
# format sunrise, sunset & solar noon as DateTime
|
97
|
+
sunrise = date.to_datetime
|
98
|
+
transit = date.to_datetime + 0.5
|
99
|
+
sunset = date.to_datetime + 1
|
100
|
+
return [sunrise, transit, sunset]
|
101
|
+
end
|
102
|
+
# approximate m (fraction of 1 day)
|
103
|
+
# store days added or subtracted to add in later
|
104
|
+
m = []
|
105
|
+
days = [0]*3
|
106
|
+
(0..2).each do |i|
|
107
|
+
case i
|
108
|
+
when 0
|
109
|
+
m[i] = (ra[1] - longitude - theta0) / 360 # transit
|
110
|
+
day_offset = +1
|
111
|
+
when 1
|
112
|
+
m[i] = m[0] - ha / 360 # rising
|
113
|
+
day_offset = -1
|
114
|
+
when 2
|
115
|
+
m[i] = m[0] + ha / 360 # setting
|
116
|
+
day_offset = -1
|
117
|
+
end
|
118
|
+
|
119
|
+
until m[i] >= 0
|
120
|
+
m[i] += 1
|
121
|
+
days[i] += day_offset
|
122
|
+
end
|
123
|
+
until m[i] <= 1
|
124
|
+
m[i] -= 1
|
125
|
+
days[i] -= day_offset
|
126
|
+
end
|
127
|
+
end
|
128
|
+
theta = [] # apparent sidereal time (degrees)
|
129
|
+
ra2 = [] # apparent right ascension (degrees)
|
130
|
+
decl2 = [] # apparent declination (degrees)
|
131
|
+
h = [] # local hour angle (degrees)
|
132
|
+
alt = [] # altitude (degrees)
|
133
|
+
delta_m = [1]*3
|
134
|
+
while delta_m[0] >= 0.01 || delta_m[1] >= 0.01 || delta_m[2] >= 0.01
|
135
|
+
0.upto(2) do |i|
|
136
|
+
theta[i] = theta0 + 360.985647 * m[i]
|
137
|
+
n = m[i] + delta_t(date.to_datetime).to_r / 86_400
|
138
|
+
a = ra[1] - ra[0]
|
139
|
+
b = ra[2] - ra[1]
|
140
|
+
c = b - a
|
141
|
+
ra2[i] = ra[1] + n / 2 * (a + b + n * c)
|
142
|
+
|
143
|
+
n = m[i] + delta_t(date.to_datetime).to_r / 86_400
|
144
|
+
a = decl[1] - decl[0]
|
145
|
+
b = decl[2] - decl[1]
|
146
|
+
c = b - a
|
147
|
+
decl2[i] = decl[1] + n / 2 * (a + b + n * c)
|
148
|
+
|
149
|
+
h[i] = theta[i] + longitude - ra2[i]
|
150
|
+
|
151
|
+
alt[i] = to_deg Math.asin(
|
152
|
+
Math.sin(latitude_rad) * Math.sin(to_rad(decl2[i])) +
|
153
|
+
Math.cos(latitude_rad) * Math.cos(to_rad(decl2[i])) *
|
154
|
+
Math.cos(to_rad(h[i]))
|
155
|
+
)
|
156
|
+
end
|
157
|
+
# adjust m
|
158
|
+
delta_m[0] = -h[0] / 360
|
159
|
+
1.upto(2) do |i|
|
160
|
+
delta_m[i] = (alt[i] - ho) /
|
161
|
+
360 * Math.cos(to_rad(decl2[i])) * Math.cos(latitude_rad) * Math.sin(to_rad(h[i]))
|
162
|
+
end
|
163
|
+
0.upto(2) do |i|
|
164
|
+
m[i] += delta_m[i]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
# format sunrise, sunset & solar noon as DateTime
|
168
|
+
sunrise = date.to_datetime + m[1] + days[1]
|
169
|
+
transit = date.to_datetime + m[0] + days[0]
|
170
|
+
sunset = date.to_datetime + m[2] + days[2]
|
171
|
+
[sunrise, transit, sunset]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Solar
|
2
|
+
class <<self
|
3
|
+
# Sun horizontal coordinates (relative position) in degrees:
|
4
|
+
#
|
5
|
+
# * +elevation+ (altitude over horizon) in degrees; positive upwards
|
6
|
+
# * +azimuth+ in degrees measured clockwise (towards East) from North direction
|
7
|
+
#
|
8
|
+
def position(t, longitude, latitude)
|
9
|
+
|
10
|
+
delta_rad, alpha_rad = equatorial_position_rad(t)
|
11
|
+
alpha_deg = to_deg(alpha_rad)
|
12
|
+
# alpha_h += 360 if alpha_h < 0
|
13
|
+
|
14
|
+
# t as Julian centuries of 36525 ephemeris days form the epoch J2000.0
|
15
|
+
if false
|
16
|
+
# Float
|
17
|
+
jd = jd_f(t)
|
18
|
+
else
|
19
|
+
# Rational
|
20
|
+
jd = jd_r(t)
|
21
|
+
end
|
22
|
+
t = to_jc(jd)
|
23
|
+
|
24
|
+
# Sidereal time at Greenwich
|
25
|
+
theta = 280.46061837 +
|
26
|
+
360.98564736629*(jd - 2_451_545) +
|
27
|
+
(0.000387933 - t/38_710_000)*t*t
|
28
|
+
|
29
|
+
# Reduce magnitude to minimize errors
|
30
|
+
theta %= 360
|
31
|
+
|
32
|
+
# Local hour angle
|
33
|
+
h = theta + longitude - alpha_deg
|
34
|
+
h %= 360
|
35
|
+
|
36
|
+
latitude_rad = to_rad(latitude)
|
37
|
+
h_rad = to_rad(h)
|
38
|
+
|
39
|
+
# Local horizontal coordinates : Meeus pg 89
|
40
|
+
altitude_rad = Math.asin(Math.sin(latitude_rad)*Math.sin(delta_rad) +
|
41
|
+
Math.cos(latitude_rad)*Math.cos(delta_rad)*Math.cos(h_rad))
|
42
|
+
azimuth_rad = Math.atan2(
|
43
|
+
Math.sin(h_rad),
|
44
|
+
Math.cos(h_rad) * Math.sin(latitude_rad) -
|
45
|
+
Math.tan(delta_rad) * Math.cos(latitude_rad)
|
46
|
+
)
|
47
|
+
|
48
|
+
[to_deg(altitude_rad), (180 + to_deg(azimuth_rad)) % 360]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
module Solar
|
2
|
+
DEBUG_RADIATION = ENV['DEBUG_SOLAR_RADIATION']
|
3
|
+
class <<self
|
4
|
+
# Radiation in W/m^2 at time t at a position given by longitude, latitud on
|
5
|
+
# terrain of given slope and aspect, given the global radiation on a horizontal plane
|
6
|
+
# and optionally the diffuse radiation on a horizontal plane.
|
7
|
+
# Computes mean radiation in a short period of length givien by :t_step (10 minute by default) centered at t.
|
8
|
+
# Basically the method used is the oned presented in:
|
9
|
+
#
|
10
|
+
# * Solar Engineering of Thermal Processes
|
11
|
+
# 1991 Duffie, Beckman
|
12
|
+
#
|
13
|
+
# Options:
|
14
|
+
#
|
15
|
+
# * +:slope+ slope angle in degrees (0-horizontal to 90-vertical)
|
16
|
+
# * +:aspect+ clockwise from North in degrees
|
17
|
+
# * +:t_step+ time step in hours; if radiation values are given they
|
18
|
+
# are considered as the mean values for the time step.
|
19
|
+
# * +:global_radiation+ Global radation measured on a horizontal plane
|
20
|
+
# in W/m^2 (mean value over the time step)
|
21
|
+
# * +:diffuse_radiation+ Diffuse radation measured on a horizontal plane
|
22
|
+
# in W/m^2 (mean value over the time step)
|
23
|
+
# * +:albedo+ of the terrain, used to compute reflected radiation.
|
24
|
+
#
|
25
|
+
# This can be used with a measured :global_radiation and optional
|
26
|
+
# :diffuse_radiation, both measured on horizontal to compute the
|
27
|
+
# estimated global radiation on a sloped plane.
|
28
|
+
#
|
29
|
+
# It can be also used by giving the :clearness_index to compute
|
30
|
+
# estimated radiation.
|
31
|
+
#
|
32
|
+
def radiation(t, longitude, latitude, options = {})
|
33
|
+
# TODO: parameterize on the .... algorithms
|
34
|
+
# consider method of [Neteler-2008] Appendix A pg. 377-381
|
35
|
+
|
36
|
+
slope = options[:slope] || 0.0
|
37
|
+
aspect = options[:aspect] || 0.0
|
38
|
+
|
39
|
+
# time step in hours
|
40
|
+
t_step = options[:t_step] || 1/6.0
|
41
|
+
# global measured radiation in W/m^2 (mean over time step) (horizontal)
|
42
|
+
g = options[:global_radiation]
|
43
|
+
# optional: diffuse radiation (horizontal)
|
44
|
+
g_d = options[:diffuse_radiation]
|
45
|
+
k_t = options[:clearness_index]
|
46
|
+
|
47
|
+
# ground reflectance (albedo) as %
|
48
|
+
rg = options[:albedo] || 0.2
|
49
|
+
|
50
|
+
# t is assumed at half the time step
|
51
|
+
|
52
|
+
t_utc = t.utc
|
53
|
+
n = t_utc.yday # day of the year (a number between 1 and 365)
|
54
|
+
lambda = to_rad(longitude)
|
55
|
+
phi = to_rad(latitude)
|
56
|
+
|
57
|
+
d = solar_declination(n)
|
58
|
+
|
59
|
+
# utc time in hours
|
60
|
+
tu = t_utc.hour + t_utc.min/60.0 + t_utc.sec/3600.0
|
61
|
+
b = to_rad(360.0*(n-1)/365.0)
|
62
|
+
# equation of time
|
63
|
+
e = 3.82*(0.000075+0.001868*Math.cos(b) - 0.032077*Math.sin(b) - 0.014615*Math.cos(2*b) - 0.04089*Math.sin(2*b))
|
64
|
+
# solar time in hours
|
65
|
+
ts = tu + longitude/15.0 + e
|
66
|
+
|
67
|
+
# hour angle (omega) in radians
|
68
|
+
w = to_rad((ts - 12.0)*15.0)
|
69
|
+
cos_w = Math.cos(w)
|
70
|
+
sin_w = Math.sin(w)
|
71
|
+
|
72
|
+
# extraterrestrial_normal_radiation in W/m^2
|
73
|
+
g_on = ext_radiation(t)
|
74
|
+
|
75
|
+
cos_phi = Math.cos(phi)
|
76
|
+
sin_phi = Math.sin(phi)
|
77
|
+
cos_d = Math.cos(d)
|
78
|
+
sin_d = Math.sin(d)
|
79
|
+
|
80
|
+
# zenith angle in radians
|
81
|
+
# eq. 1.6.5 (pg 15) [1991-Duffie]
|
82
|
+
cos_phi_z = cos_phi*cos_d*cos_w + sin_phi*sin_d
|
83
|
+
|
84
|
+
# extraterrestrial horizontal radiation in W/m^2
|
85
|
+
g_o = g_on*cos_phi_z
|
86
|
+
|
87
|
+
# hour_angle at beginning of time step
|
88
|
+
w1 = to_rad((ts - t_step/2 - 12.0)*15.0)
|
89
|
+
# hour_angle at end of time step
|
90
|
+
w2 = to_rad((ts + t_step/2 - 12.0)*15.0)
|
91
|
+
|
92
|
+
# extraterrestrial horizontal radiation in W/m^2 averaged over the time step
|
93
|
+
# [1991-Duffie pg 37] eq. 1.10.1, 1.10.3
|
94
|
+
# TODO: for long time steps we should average as:
|
95
|
+
# g_o_a = 12/Math::PI * g_on * ( cos_phi*cos_d*(Math.sin(w2)-Math.sin(w1)) + (w2-w1)*sin_phi*sin_d )
|
96
|
+
g_o_a = g_o
|
97
|
+
g_o_a = 0 if g_o_a < 0
|
98
|
+
|
99
|
+
# clearness index
|
100
|
+
k_t ||= g/g_o_a
|
101
|
+
|
102
|
+
# either k_t or g must be defined by the user
|
103
|
+
g ||= (g_o_a == 0 ? 0.0 : k_t*g_o_a)
|
104
|
+
|
105
|
+
# diffuse fraction
|
106
|
+
if k_t.infinite?
|
107
|
+
df = 1.0 # actual df may be around 0.5; for the purpose of computing g_d it is 1
|
108
|
+
else
|
109
|
+
solar_elevation = 90 - to_deg(Math.acos(cos_phi_z))
|
110
|
+
df = diffuse_fraction(k_t, solar_elevation)
|
111
|
+
end
|
112
|
+
|
113
|
+
# diffuse radiation W/m^2
|
114
|
+
g_d ||= df*g
|
115
|
+
|
116
|
+
# beam radiation
|
117
|
+
g_b = g - g_d
|
118
|
+
|
119
|
+
# slope
|
120
|
+
beta = to_rad(slope)
|
121
|
+
# azimuth
|
122
|
+
gamma = to_rad(aspect-180)
|
123
|
+
|
124
|
+
cos_beta = Math.cos(beta)
|
125
|
+
sin_beta = Math.sin(beta)
|
126
|
+
cos_gamma = Math.cos(gamma)
|
127
|
+
sin_gamma = Math.sin(gamma)
|
128
|
+
|
129
|
+
# angle of incidence
|
130
|
+
# eq (1.6.2) pg 14 [1991-Duffie]
|
131
|
+
# eq (3) "Analytical integrated functions for daily solar radiation on slopes" - Allen, Trezza, Tasumi
|
132
|
+
cos_phi = sin_d*sin_phi*cos_beta \
|
133
|
+
- sin_d*cos_phi*sin_beta*cos_gamma \
|
134
|
+
+ cos_d*cos_phi*cos_beta*cos_w \
|
135
|
+
+ cos_d*sin_phi*sin_beta*cos_gamma*cos_w \
|
136
|
+
+ cos_d*sin_beta*sin_gamma*sin_w
|
137
|
+
|
138
|
+
# ratio of beam radiation on tilted surface to beam radiation on horizontal
|
139
|
+
# [1991-Duffie pg 23-24] eq. 1.8.1
|
140
|
+
# rb = illumination_factor_at(t, longitude, latitude, slope, aspect)
|
141
|
+
rb = cos_phi / cos_phi_z
|
142
|
+
rb = 0.0 if rb < 0.0
|
143
|
+
|
144
|
+
# anisotropy index
|
145
|
+
if k_t.infinite? || g_o_a == 0
|
146
|
+
ai = 0.0
|
147
|
+
else
|
148
|
+
ai = g_b / g_o_a
|
149
|
+
end
|
150
|
+
|
151
|
+
# horizontal brightening factor
|
152
|
+
if g != 0
|
153
|
+
f = Math.sqrt(g_b / g)
|
154
|
+
else
|
155
|
+
f = 1.0
|
156
|
+
end
|
157
|
+
|
158
|
+
if DEBUG_RADIATION
|
159
|
+
sun_elevation, sun_azimuth = Solar.position(t_utc, longitude, latitude)
|
160
|
+
rb2 = illumination_factor_at(t_utc, longitude, latitude, slope, aspect)
|
161
|
+
puts ""
|
162
|
+
puts " @#{t_utc} #{sun_elevation} [#{90-sun_elevation}] az: #{sun_azimuth} <#{Math.acos(cos_phi_z)*180/Math::PI}>"
|
163
|
+
puts " kt:#{k_t} df:#{df} g: #{g} gon: #{g_on}"
|
164
|
+
puts " rb: #{rb} (#{rb2}) g_b:#{g_b} g_o_a:#{g_o_a} g_d:#{g_d}"
|
165
|
+
puts " -> #{(g_b + g_d*ai)*rb}+#{g_d*(1-ai)*((1 + cos_beta)/2)*(1 + f*Math.sin(beta/2)**3)}+#{g*rg*(1 - cos_beta)/2}"
|
166
|
+
end
|
167
|
+
|
168
|
+
# global radiation on slope according to HDKR model
|
169
|
+
# eq. 2.16.7 (pg.92) [1991-Duffie]
|
170
|
+
# three terms:
|
171
|
+
# * direct and circumsolar: (g_b + g_d*ai)*rb
|
172
|
+
# * diffuse: g_d*(1-ai)*((1 + cos_beta)/2)*(1 + f*Math.sin(beta/2)**3)
|
173
|
+
# * reflected: g*rg*(1 - cos_beta)/2
|
174
|
+
(g_b + g_d*ai)*rb + g_d*(1-ai)*((1 + cos_beta)/2)*(1 + f*Math.sin(beta/2)**3) + g*rg*(1 - cos_beta)/2
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
G_SC = 1367.0 # solar constant W/m^2
|
180
|
+
|
181
|
+
# Extraterrestrial normal radiation
|
182
|
+
# Solar Engineering of Thermal Processes [1991, Duffie, Beckman]
|
183
|
+
# pg.5-6, 8-9; pg 37.
|
184
|
+
def ext_radiation(t)
|
185
|
+
n = t.utc.yday
|
186
|
+
G_SC*(1.0 + 0.033*Math.cos(to_rad(360.0*n/365.0)))
|
187
|
+
|
188
|
+
# Alternative based on [Neteler-2008 pg378] (A.65)
|
189
|
+
# j = 2*Math::PI*n/365.25
|
190
|
+
# G_SC*(1.0 + 0.03344*Math.cos(j - 0.048869))
|
191
|
+
end
|
192
|
+
|
193
|
+
# Diffuse fraction as a function of the clearness index
|
194
|
+
# Erbs model:
|
195
|
+
#
|
196
|
+
# * Estimation of the diffuse radiation fraction for hourly, daily and monthly-average global radiation
|
197
|
+
# 1982 Erbs, Klein, Duffie
|
198
|
+
# SOLAR ENERGY - 1982 Vol.28 Issue 4
|
199
|
+
#
|
200
|
+
# Other sources of information for this model:
|
201
|
+
#
|
202
|
+
# * Different methods for separating diffuse and direct components of solar radiation and their application in crop growth models
|
203
|
+
# 1992 Bindi, Miglietta, Zipoli
|
204
|
+
# CLIMATE RESEARCH - July 1992
|
205
|
+
# http://www.int-res.com/articles/cr/2/c002p047.pdf (pg 53 Method ER)
|
206
|
+
# * DIVISION OF GLOBAL RADIATION INTO DIRECT RADIATION AND DIFFUSE RADIATION
|
207
|
+
# 2010 Fabienne Lanini
|
208
|
+
# * Solar radiation model
|
209
|
+
# 2001 Wong, Chow
|
210
|
+
# APPLIED ENERGY - August 2001 [pg. 210]
|
211
|
+
# * COMPARISON OF MODELS FOR THE DERIVATION OF DIFFUSE FRACTION OF GLOBAL IRRADIANCE DATA FOR VIENNA, AUSTRIA
|
212
|
+
# 2011 Dervishi, Mahdavi - pg 766
|
213
|
+
#
|
214
|
+
# TODO: use model that uses solar elevation (Skartveit Olseth):
|
215
|
+
#
|
216
|
+
# * AN HOURLY DIFFUSE FRACTION MODEL WITH CORRECTION FOR VARIABILITY AND SURFACE ALBEDO
|
217
|
+
# 1998 Skartveit, Olseth, Tuft
|
218
|
+
# SOLAR ENERY - September 1998
|
219
|
+
# http://www.sciencedirect.com/science/article/pii/S0038092X9800067X
|
220
|
+
#
|
221
|
+
# Precedent model:
|
222
|
+
#
|
223
|
+
# * A model for the diffuse fraction of hourly global radiation
|
224
|
+
# 1986 Skartveit, Olseth
|
225
|
+
# SOLAR ENERGY - JANUARY 1987
|
226
|
+
# http://www.researchgate.net/publication/222958126
|
227
|
+
#
|
228
|
+
# Additional information:
|
229
|
+
#
|
230
|
+
# * Solar radiation model
|
231
|
+
# 2001 Wong, Chow
|
232
|
+
# APPLIED ENERGY - August 2001 [pg. 212]
|
233
|
+
#
|
234
|
+
# * Solar Engineering of Thermal Processes
|
235
|
+
# 1991 Duffie, Beckman [pg.77]
|
236
|
+
#
|
237
|
+
def diffuse_fraction(k_t, solar_elevation)
|
238
|
+
if k_t <= 0.22
|
239
|
+
df = 1.0 - 0.09*k_t
|
240
|
+
elsif k_t <= 0.80
|
241
|
+
df = 0.9511 - 0.1604*k_t + 4.388*k_t**2 - 16.638*k_t**3 + 12.336*k_t**4
|
242
|
+
else
|
243
|
+
df = 0.165
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def solar_declination(day)
|
248
|
+
# solar declination [Duffie-1991 pg 13]
|
249
|
+
ang = to_rad(360*(284.0 + day)/365.0)
|
250
|
+
to_rad(23.45)*Math.sin(ang)
|
251
|
+
|
252
|
+
# Alternative based on [Neteler-2008 pg337] (A.59)
|
253
|
+
# j = 2*Math::PI*day/365.25
|
254
|
+
# Math.asin(0.3978*Math.sin(j - 1.4 + 0.0355*Math.sin(j - 0.0489)))
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|
258
|
+
end
|