solar 0.0.2 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|