# -*- coding: utf-8 -*-
# Copyright (c) 2019 Stephen Wasilewski
# =======================================================================
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# =======================================================================
"""functions for loading sky data and computing sun position"""
import os
import datetime
import re
import numpy as np
from scipy.interpolate import interp1d
from skyfield.api import Topos, utc, load
import raytraverse
from raytools import translate
from raytraverse.formatter import RadianceFormatter
# bsp file made with:
# python -m jplephem excerpt 2020/1/1 2030/1/1 \
# https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de432s.bsp \
# de432_2020s.bsp
planets = load(os.path.dirname(raytraverse.__file__) + '/de432_2020s.bsp')
sun = planets['sun']
earth = planets['earth']
[docs]
def read_epw(epw):
"""read daylight sky data from epw or wea file
Returns
-------
out: np.array
(month, day, hour, dirnorn, difhoriz)
"""
f = open(epw, 'r')
lines = f.readlines()
f.close()
hours = [re.split(r'[ \t,]+', i) for i in lines if re.match(r"\d.*", i)]
data = []
for h in hours:
if len(h) > 23:
dp = [h[1], h[2], h[3], h[14], h[15]]
hoff = .5
else:
dp = [h[0], h[1], h[2], h[3], h[4]]
hoff = 0
data.append([int(i.strip()) for i in dp[0:2]] + [float(dp[2]) - hoff] +
[float(i.strip()) for i in dp[3:]])
return np.array(data)
col_headers = {'year': 0, 'month': 1, 'day': 2, 'hour': 3, 'minute': 4,
'note': 5, 't_drybulb': 6, 't_dewpoint': 7, 'rh': 8, 'asp': 9,
'ext_hor_rad': 10, 'ext_dir_norm_rad': 11,
'hor_infr_rad_int': 12, 'global_hor_rad': 13,
'dir_norm_rad': 14, 'dif_hor_rad': 15, 'global_hor_illum': 16,
'dir_norm_illum': 17, 'diff_hor_illum': 18, 'zenith_lum': 19,
'wind_dir': 20, 'wind_spd': 21, 'sky_cover': 22,
'opaque_sky_cover': 23, 'visibility': 24, 'ceil_height': 25,
'weather_obs': 26, 'weather_codes': 27, 'precip_water': 28,
'aerosol_optical_depth': 29, 'snow_depth': 30,
'days_last_snow': 31, 'albedo': 32, 'liquid_precip_depth': 33,
'liquid_precip_quant': 34}
col_indices = dict(zip(col_headers.values(), col_headers.keys()))
[docs]
def read_epw_full(epw, columns=None):
"""
Parameters
----------
epw
columns: list, optional
integer indices or keys of columns to return
Returns
-------
requested columns from epw as np.array shape (8760, N)
"""
f = open(epw, 'r')
lines = f.readlines()
f.close()
hours = [re.split(r'[ \t,]+', i) for i in lines if re.match(r"\d.*", i)]
data = np.array(hours).T
data[5] = '0'
data = data.astype(float)
# correct hour offset as instantaneous
data[3] -= 0.5
if columns is not None:
c = []
for i in columns:
if i in col_headers:
c.append(col_headers[i])
elif i in col_indices:
c.append(i)
else:
raise ValueError(f"Column {i} is not a valid key: {col_indices}")
columns = c
return data[columns].T
[docs]
def get_loc_epw(epw, name=False):
"""get location from epw or wea header"""
try:
f = open(epw)
hdr = f.readlines()[0:8]
f.close()
if len(hdr[0].split(",")) > 5:
hdr = hdr[0].split(",")
lat = hdr[-4]
lon = hdr[-3]
tz = float(hdr[-2])
lon = str(-float(lon))
tz = str(int(-tz*15))
loc = hdr[1]
elev = hdr[-1].strip()
else:
lat = [i.split()[-1] for i in hdr if re.match(r"latitude.*", i)][0]
lon = [i.split()[-1] for i in hdr if re.match(r"longitude.*", i)][0]
tz = [i.split()[-1] for i in hdr if re.match(r"time_zone.*", i)][0]
loc = [i.split()[-1] for i in hdr if re.match(r"place.*", i)][0]
try:
elev = [i.split()[-1] for i in hdr if
re.match(r"site_elevation.*", i)][0]
except IndexError:
elev = '0.0'
except Exception as e:
raise ValueError("bad epw header", e)
if name:
return float(lat), float(lon), int(tz), loc, elev
else:
return float(lat), float(lon), int(tz)
[docs]
def sunpos_utc(timesteps, lat, lon, builtin=True):
"""Calculate sun position with local time
Calculate sun position (altitude, azimuth) for a particular location
(longitude, latitude) for a specific date and time (time is in UTC)
Parameters
----------
timesteps : np.array(datetime.datetime)
lon : float
longitude in decimals. West is +ve
lat : float
latitude in decimals. North is +ve
builtin: bool
use skyfield builtin timescale
Returns
-------
(skyfield.units.Angle, skyfield.units.Angle)
altitude and azimuth in degrees
"""
dt = np.apply_along_axis(lambda x: x[0].replace(tzinfo=utc), 1,
timesteps.reshape(-1, 1))
# use radiance +west longitude coordinates
loc = earth + Topos(float(lat), float(-lon))
# faster but requires updating periodically
ts = load.timescale(builtin=builtin)
astro = loc.at(ts.utc(dt)).observe(sun)
app = astro.apparent()
return app.altaz('standard')[0:2]
[docs]
def row_2_datetime64(ts, year=2020):
ts = np.asarray(ts).astype(float)
if len(ts.shape) == 1:
if len(ts) < 4:
hm = np.modf(ts[2])
ts = np.concatenate((ts[0:2], [hm[1], hm[0]*60]))
st = ['{}-{:02.00f}-{:02.00f}T{:02.00f}:{:02.00f}'.format(year, *ts), ]
else:
if ts.shape[1] < 4:
hm = np.modf(ts[:, 2:3])
ts = np.hstack((ts[:, 0:2], hm[1], hm[0]*60))
st = ['{}-{:02.00f}-{:02.00f}T{:02.00f}:{:02.00f}'.format(year, *t)
for t in ts]
return np.array(st).astype('datetime64[m]')
[docs]
def datetime64_2_datetime(timesteps, mer=0.):
"""convert datetime representation and offset for timezone
Parameters
----------
timesteps: np.array(np.datetime64)
mer: float
Meridian of the time zone. West is +ve
Returns
-------
np.array(datetime.datetime)
"""
tz = mer/15.
dt = (timesteps + np.timedelta64(int(tz*60), 'm')).astype(datetime.datetime)
return dt
[docs]
def sunpos_degrees(timesteps, lat, lon, mer, builtin=True, ro=0.0):
"""Calculate sun position with local time
Calculate sun position (altitude, azimuth) for a particular location
(longitude, latitude) for a specific date and time (time is in local time)
Parameters
----------
timesteps : np.array(np.datetime64)
lon : float
longitude in decimals. West is +ve
lat : float
latitude in decimals. North is +ve
mer: float
Meridian of the time zone. West is +ve
builtin: bool, optional
use skyfield builtin timescale
ro: float, optional
ccw rotation (project to true north) in degrees
Returns
-------
np.array([float, float])
Sun position as (altitude, azimuth) in degrees
"""
dt = datetime64_2_datetime(timesteps, mer=mer)
alt, az = sunpos_utc(dt, lat, lon, builtin)
# south is az=0
return np.column_stack([alt.degrees, az.degrees - 180 - ro])
[docs]
def sunpos_radians(timesteps, lat, lon, mer, builtin=True, ro=0.0):
"""Calculate sun position with local time
Calculate sun position (altitude, azimuth) for a particular location
(longitude, latitude) for a specific date and time (time is in local time)
Parameters
----------
timesteps : np.array(np.datetime64)
lon : float
longitude in decimals. West is +ve
lat : float
latitude in decimals. North is +ve
mer: float
Meridian of the time zone. West is +ve
builtin: bool
use skyfield builtin timescale
ro: float, optional
ccw rotation (project to true north) in radians
Returns
-------
np.array([float, float])
Sun position as (altitude, azimuth) in radians
"""
dt = datetime64_2_datetime(timesteps, mer=mer)
alt, az = sunpos_utc(dt, lat, lon, builtin)
# south is az=0
return np.column_stack([alt.radians, az.radians - np.pi - ro])
[docs]
def sunpos_xyz(timesteps, lat, lon, mer, builtin=True, ro=0.0):
"""Calculate sun position with local time
Calculate sun position (altitude, azimuth) for a particular location
(longitude, latitude) for a specific date and time (time is in local time)
Parameters
----------
timesteps : np.array(np.datetime64)
lon : float
longitude in decimals. West is +ve
lat : float
latitude in decimals. North is +ve
mer: float
Meridian of the time zone. West is +ve
builtin: bool
use skyfield builtin timescale
ro: float, optional
ccw rotation (project to true north) in degrees
Returns
-------
np.array
Sun position as (x, y, z)
"""
dt = datetime64_2_datetime(timesteps, mer=mer)
alt, az = sunpos_utc(dt, lat, lon, builtin)
az = az.radians - ro*np.pi/180
# translate to spherical rhs before calling translate.tp2xyz
thetaphi = np.column_stack([np.pi/2 - alt.radians, np.pi/2 - az])
return translate.tp2xyz(thetaphi)
[docs]
def generate_wea(ts, wea, interp='linear'):
skydat = read_epw(wea)
wtimes = row_2_datetime64(skydat[:, 0:3]).astype(int)
qtimes = row_2_datetime64(ts).astype(int)
fdir = interp1d(wtimes, skydat[:, 3], kind=interp)
fdif = interp1d(wtimes, skydat[:, 4], kind=interp)
idir = fdir(qtimes)[:, None]
idif = fdif(qtimes)[:, None]
return np.hstack((ts, idir, idif))
# Below is an implementation of perez all weather sky model:
# Perez, R., R. Seals, and J. Michalsky. “All-Weather Model for Sky
# Luminance Distribution—Preliminary Configuration and Validation.”
# Solar Energy 50, no. 3 (March 1, 1993): 235–45.
# https://doi.org/10.1016/0038-092X(93)90017-I.
# Code adapted to and tested against the gendaylit and genskyvec programs
# of Radiance:
# The Radiance Software License, Version 1.0
#
# Copyright (c) 1990 - 2018 The Regents of the University of California,
# through Lawrence Berkeley National Laboratory. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. The end-user documentation included with the redistribution,
# if any, must include the following acknowledgment:
# "This product includes Radiance software
# (http://radsite.lbl.gov/)
# developed by the Lawrence Berkeley National Laboratory
# (http://www.lbl.gov/)."
# Alternately, this acknowledgment may appear in the software itself,
# if and wherever such third-party acknowledgments normally appear.
#
# 4. The names "Radiance," "Lawrence Berkeley National Laboratory"
# and "The Regents of the University of California" must
# not be used to endorse or promote products derived from this
# software without prior written permission. For written
# permission, please contact radiance@radsite.lbl.gov.
#
# 5. Products derived from this software may not be called "Radiance",
# nor may "Radiance" appear in their name, without prior written
# permission of Lawrence Berkeley National Laboratory.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL Lawrence Berkeley National Laboratory OR
# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
# ====================================================================
#
# This software consists of voluntary contributions made by many
# individuals on behalf of Lawrence Berkeley National Laboratory. For more
# information on Lawrence Berkeley National Laboratory, please see
# <http://www.lbl.gov/>.
# gendaylit source copyright:
# Copyright (c) 1994,2006 *Fraunhofer Institut for Solar Energy Systems
# Heidenhofstr. 2, D-79110 Freiburg, Germany
# *Agence de l'Environnement et de la Maitrise de l'Energie
# Centre de Valbonne, 500 route des Lucioles, 06565 Sophia
# Antipolis Cedex, France
# BOUYGUES
# 1 Avenue Eugene Freyssinet, Saint-Quentin-Yvelines, France
# print colored output if activated in command line (-C).
# Based on model from A. Diakite, TU-Berlin. Implemented by J. Wienold,
# August 26 2018
# */
perez_constants = {
'cats': np.array([1, 1.065, 1.23, 1.50, 1.95, 2.80, 4.50, 6.20, 12.01]),
'cp': np.array([1.3525, -0.2576, -0.2690, -1.4366, -0.7670,
0.0007, 1.2734, -0.1233, 2.8000, 0.6004, 1.2375, 1.000,
1.8734, 0.6297, 0.9738, 0.2809, 0.0356, -0.1246, -0.5718,
0.9938, -1.2219, -0.7730, 1.4148, 1.1016, -0.2054, 0.0367,
-3.9128, 0.9156, 6.9750, 0.1774, 6.4477, -0.1239, -1.5798,
-0.5081, -1.7812, 0.1080, 0.2624, 0.0672, -0.2190, -0.4285,
-1.1000, -0.2515, 0.8952, 0.0156, 0.2782, -0.1812, -4.5000,
1.1766, 24.7219, -13.0812, -37.7000, 34.8438, -5.0000,
1.5218, 3.9229, -2.6204, -0.0156, 0.1597, 0.4199, -0.5562,
-0.5484, -0.6654, -0.2672, 0.7117, 0.7234, -0.6219, -5.6812,
2.6297, 33.3389, -18.3000, -62.2500, 52.0781, -3.5000,
0.0016, 1.1477, 0.1062, 0.4659, -0.3296, -0.0876, -0.0329,
-0.6000, -0.3566, -2.5000, 2.3250, 0.2937, 0.0496, -5.6812,
1.8415, 21.000, -4.7656, -21.5906, 7.2492, -3.5000, -0.1554,
1.4062, 0.3988, 0.0032, 0.0766, -0.0656, -0.1294, -1.0156,
-0.3670, 1.0078, 1.4051, 0.2875, -0.5328, -3.8500, 3.3750,
14.0000, -0.9999, -7.1406, 7.5469, -3.4000, -0.1078, -1.075,
1.5702, -0.0672, 0.4016, 0.3017, -0.4844, -1.00, 0.0211,
0.5025, -0.5119, -0.3, 0.1922, 0.7023, -1.6317, 19.0, -5.0,
1.2438, -1.9094, -4.0000, 0.0250, 0.3844, 0.2656, 1.0468,
-0.3788, -2.4517, 1.4656, -1.0500, 0.0289, 0.4260, 0.3590,
-0.325, 0.1156, 0.7781, 0.0025, 31.0625, -14.5, -46.1148,
55.375, -7.2312, 0.405, 13.35, 0.6234, 1.5, -0.6426,
1.8564, 0.5636]).reshape((8, 5, 4)),
'theta': np.array([84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84,
84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84,
84, 84, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72,
72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72,
72, 72, 72, 72, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60,
60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60,
48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48,
48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 36, 36, 36, 36,
36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 12, 12,
12, 12, 12, 12, 0])*np.pi/180,
'phi': np.array([0, 12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156,
168, 180, 192, 204, 216, 228, 240, 252, 264, 276, 288, 300,
312, 324, 336, 348, 0, 12, 24, 36, 48, 60, 72, 84, 96, 108,
120, 132, 144, 156, 168, 180, 192, 204, 216, 228, 240, 252,
264, 276, 288, 300, 312, 324, 336, 348, 0, 15, 30, 45, 60,
75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240,
255, 270, 285, 300, 315, 330, 345, 0, 15, 30, 45, 60, 75,
90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255,
270, 285, 300, 315, 330, 345, 0, 20, 40, 60, 80, 100, 120,
140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 0,
30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 0, 60,
120, 180, 240, 300, 0])*np.pi/180,
'nfc': np.array([[2.766521, 0.547665, -0.369832, 0.009237, 0.059229],
[3.5556, -2.7152, -1.3081, 1.0660, 0.60227]]),
'mdays': np.array([0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334])
}
[docs]
def coeff_lum_perez(sunz, epsilon, delta, catn):
"""matches coeff_lum_perez in gendaylit.c"""
mide = np.logical_and(epsilon > 1.065, epsilon < 2.8)
delta[mide] = np.maximum(delta[mide], 0.2)
x = perez_constants['cp'][catn]
abcde = (x[..., 0] + x[..., 1]*sunz[:, None] + delta[:, None] *
(x[..., 2] + x[..., 3]*sunz[:, None]))
lowe = catn == 0
abcde[lowe, 2] = np.exp(np.power(delta[lowe] * (x[lowe, 2, 0] +
x[lowe, 2, 1] * sunz[lowe]),
x[lowe, 2, 2])) - x[lowe, 2, 3]
abcde[lowe, 3] = (-np.exp(delta[lowe] * (x[lowe, 3, 0] +
x[lowe, 3, 1] * sunz[lowe])) +
x[lowe, 3, 2] + delta[lowe] * x[lowe, 3, 3])
return abcde
[docs]
def perez_apply_coef(coefs, cgamma, dz):
c = coefs[:, None, :]
z = np.maximum(dz, 0.01)
gamma = np.arccos(np.clip(cgamma, -1, 1))
lum = ((1 + c[..., 0]*np.exp(c[..., 1]/z[None, :])) *
(1 + c[..., 2]*np.exp(c[..., 3]*gamma) +
c[..., 4]*np.square(cgamma)))
return lum
[docs]
def perez_lum_raw(tp, dz, sunz, coefs):
"""matches calc_rel_lum_perez in gendaylit.c"""
cg = (np.cos(sunz[:, None])*np.cos(tp[:, 0]) +
np.sin(sunz[:, None])*np.sin(tp[:, 0]) *
np.cos(tp[:, 1]))
return perez_apply_coef(coefs, cg, dz)
[docs]
def perez_lum(xyz, coefs, intersky=True):
"""matches perezlum.cal"""
sxyz = coefs[:, 7:]
cgamma = np.sum(xyz * sxyz[:, None, :], -1)
rawlum = perez_apply_coef(coefs[:, 2:7], cgamma, xyz[:, 2])
c = coefs[:, None, 0:2]
if intersky:
swght = np.power(xyz[:, 2] + 1.01, 10)[None, :]
gwght = np.power(xyz[:, 2] + 1.01, -10)[None, :]
c = (swght * c[..., 0] * rawlum + gwght * c[..., 1]) / (swght + gwght)
else:
c = np.where(xyz[:, 2] >= 0, c[..., 0] * rawlum, c[..., 1])
return c
[docs]
def scale_efficacy(dirdif, sunz, csunz, skybright, catn, td=10.9735311509):
abcdf = np.array([[97.24, 107.22, 104.97, 102.39, 100.71, 106.42, 141.88,
152.23, 0],
[-0.46, 1.15, 2.96, 5.59, 5.94, 3.83, 1.90, 0.35, 0],
[12.0, 0.59, -5.53, -13.95, -22.75, -36.15, -53.24,
-45.27, 0],
[-8.91, -3.95, -8.77, -13.90, -23.74, -28.83, -14.03,
-7.98, 0]]).T
abcdd = np.array([[57.20, 98.99, 109.83, 110.34, 106.36, 107.19, 105.75,
101.18, 0],
[-4.55, -3.46, -4.90, -5.84, -3.97, -1.25, 0.77, 1.58, 0],
[-2.98, -1.21, -1.71, -1.99, -1.75, -1.51, -1.26, -1.10,
0],
[117.12, 12.38, -8.81, -4.56, -6.16, -26.73, -34.44,
-8.29, 0]]).T
precwater = np.broadcast_to(np.exp(0.07*td - .075), dirdif.shape[0])
effimultd = np.stack((np.ones(dirdif.shape[0]), precwater,
np.exp(5.73*sunz - 5), skybright)).T
effimultf = np.stack((np.ones(dirdif.shape[0]), precwater,
csunz, np.log(skybright))).T
ef = np.sum(abcdf[catn]*effimultf, 1)
# this can go negative due to extrapolation, check if this matches
# gendaylit code (which does set these conditions to zero).
ed = np.maximum(np.sum(abcdd[catn]*effimultd, 1), 0)
directi = dirdif[:, 0]*ed
diffusei = dirdif[:, 1]*ef
return directi, diffusei
[docs]
def perez(sxyz, dirdif, md=None, ground_fac=0.2, td=10.9735311509):
"""compute perez coefficients
Notes
-----
to match the results of gendaylit, for a given sun angle without associated
date, the assumed eccentricity is 1.035020
Parameters
----------
sxyz: np.array
(N, 3) dx, dy, dz sun position
dirdif: np.array
(N, 2) direct normal, diffuse horizontal W/m^2
md: np.array, optional
(N, 2) month day of sky calcs (for more precise eccentricity calc)
ground_fac: float
scaling factor (reflectance) for ground brightness
td: np.array float, optional
(N,) dew point temperature in C
Returns
-------
perez: np.array
(N, 10) diffuse normalization, ground brightness, perez coefs, x, y, z
"""
sxyz = np.atleast_2d(sxyz)
dirdif = np.atleast_2d(dirdif)
# match constants from gendaylit.c
sole = 1367
dn = 0
if md is not None:
md = np.atleast_2d(md)
dn = perez_constants['mdays'][md[:, 0] - 1] + md[:, 1]
da = 2*np.pi*(dn - 1)/365
eccentricity = (1.00011 + 0.034221*np.cos(da) + 0.00128*np.sin(da) +
0.000719*np.cos(2*da) + 0.000077*np.sin(2*da))
alt = np.arcsin(sxyz[:, 2])
sunz = np.pi/2 - alt
csunz = np.cos(sunz)
sunz3 = 1.041*np.power(sunz, 3)
airmass = 1/(csunz + 0.15*np.exp(-np.log(93.885 - sunz*180/np.pi)*1.253))
skybright = dirdif[:, 1]*airmass/(sole * eccentricity)
skyclear = ((dirdif[:, 1] + dirdif[:, 0])/dirdif[:, 1] + sunz3)/(1 + sunz3)
# check_parametrization
skyclear = np.minimum(np.maximum(skyclear, 1.0), 12.099)
skybright = np.minimum(np.maximum(skybright, 0.01), 0.6)
catn = np.minimum(np.searchsorted(perez_constants['cats'], skyclear,
side='right') - 1, 7)
directi, diffusei = scale_efficacy(dirdif, sunz, csunz,
skybright, catn, td)
# sunz, epsilon, delta
cperez = coeff_lum_perez(sunz, skyclear, skybright, catn)
tp = np.stack((perez_constants['theta'], perez_constants['phi'])).T
dz = np.cos(tp[:, 0])
normvals = perez_lum_raw(tp, dz, sunz, cperez)
normc = np.sum(normvals * dz, 1)*2*np.pi/145
diffnorm = diffusei/normc/179
# half_sun_angle = 0.2665
solarrad = directi/(2*np.pi*(1 - np.cos(0.2665*np.pi/180)))/179
zenithbr = perez_lum_raw(np.array([0, 0])[None, :], np.array([1]),
sunz, cperez).flatten()
zenithbr *= diffnorm
inter = (skyclear <= 6)
normsc = perez_constants['nfc'][inter.astype(int)]
x = (alt - np.pi/4) / (np.pi/4)
p = np.arange(5)[None, :]
f2 = np.where(inter, (2.739 + .9891*np.sin(.3119 + 2.6*alt)) *
np.exp(-(np.pi/2 - alt)*(.4441 + 1.48*alt)),
0.274*(0.91 + 10*np.exp(-3*(np.pi/2 - alt)) +
0.45*np.square(sxyz[:, 2])))
normfactor = np.sum(np.power(x[:, None], p) * normsc, 1)/f2/np.pi
normfactor[skyclear == 1] = 0.777778
groundbr = zenithbr*normfactor
groundbr[skyclear > 1] += (6.8e-5/np.pi*solarrad*sxyz[:, 2])[skyclear > 1]
groundbr *= ground_fac
coefs = np.hstack((diffnorm[:, None], groundbr[:, None], cperez, sxyz))
return coefs, solarrad
# perez(t, g, a, b, c, d, e) = (1.+a*exp(b/(0.004+abs(cos(t))))) * (1+c*exp(d*g)+e*cos(g)*cos(g));
#
# cosgamma = Dx*A8 + Dy*A9 + Dz*A10;
# gamma = Acos(cosgamma); { angle from sun to this point in sky }
# theta = Acos(Dz); { angle from zenith to this point in sky }
# thetas = Acos(A10); { angle from zenith to sun }
# turb = bound(1.2,arg(11),6.); { clamp the bounds of turbidity }
#
# { zenith brightness, chromaticity }
# yyzTest = (4.0453*turb - 4.971)*tan((0.4444-turb/120.)*(3.1415927-2*thetas)) - 0.2155*turb + 2.4192;
# yyz = if (yyzTest, yyzTest, 0.);
# xz = 0.25886 + 0.00394*turb + thetas*(0.06052 - 0.03202*turb*(1.-0.065272*turb) + thetas*(-0.21196 + 0.06377*turb*(1.-0.058805*turb) + thetas*(0.11693 - 0.02903*turb*(1.-0.057182*turb))));
# yz = 0.26688 + 0.00516*turb + thetas*(0.0667 - 0.04153*turb*(1.-0.07633*turb) + thetas*(-0.26756 + 0.0897*turb*(1.-0.068004*turb) + thetas*(0.15346 - 0.04214*turb*(1.-0.065259*turb))));
#
# { distribution coefficients for luminance, chromaticity; functions of turbidity }
# ax = -0.0193*turb - 0.2593;
# bx = -0.0665*turb + 0.0008;
# cx = -0.0004*turb + 0.2125;
# dx = -0.0641*turb - 0.8989;
# ex = -0.0033*turb + 0.0452;
#
# ay = -0.0167*turb - 0.2608;
# by = -0.095*turb + 0.0092;
# cy = -0.0079*turb + 0.2102;
# dy = -0.0441*turb - 1.6537;
# ey = -0.0109*turb + 0.0529;
#
# { point values for luminance, chromaticity }
# xp = xz * perez(theta, gamma, ax, bx, cx, dx, ex) / perez(0., thetas, ax, bx, cx, dx, ex);
# yp = yz * perez(theta, gamma, ay, by, cy, dy, ey) / perez(0., thetas, ay, by, cy, dy, ey);
#
#
# {perezlum.cal bit}
# wmean(a, x, b, y) = (a*x+b*y)/(a+b);
# intersky = if( (Dz-0.01),
# A1 * (1 + A3*Exp(A4/Dz) ) * ( 1 + A5*Exp(A6*gamma) + A7*cos(gamma)*cos(gamma) ),
# A1 * (1 + A3*Exp(A4/0.01) ) * ( 1 + A5*Exp(A6*gamma) + A7*cos(gamma)*cos(gamma) ) );
# skybr = wmean((Dz+1.01)^10, intersky, (Dz+1.01)^-10, A2 );
# yyp = skybr;
#
#
#
# { output radiance }
#
# { first, tristimulus values (are these CIE XYZ?)}
# x = yyp*xp/yp;
# y = yyp;
# z = yyp*if(1.-xp-yp, 1-xp-yp, 0.)/yp;
#
# { convert using CIE M^-1 matrix from http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html }
# skyr = if(Dz,2.3706743*x - 0.9000405*y - 0.4706338*z,skybr);
# skyg = if(Dz,-0.513885*x + 1.4253036*y + 0.0885814*z,skybr);
# skyb = if(Dz,0.0052982*x - 0.0146949*y + 1.0093968*z,skybr);
[docs]
def sky_mtx(sxyz, dirdif, side, jn=4, intersky=True, color=False, **kwargs):
"""generate sky, ground and sun values from sun position and sky values
Parameters
----------
sxyz: np.array
sun directions (N, 3)
dirdif: np.array
direct normal and diffuse horizontal radiation (W/m^2) (N, 2)
side: int
sky subdivision
jn: int, optional
sky patch subdivision n = jn^2
intersky: bool, optional
include interreflection between ground and sky (mimics perezlum.cal,
not present in gendaymtx
kwargs: dict, optional
passed to perez()
Returns
-------
skymtx: np.array
(N, side*side)
grndval: np.array
(N,)
sunval: np.array
(N, 4) - sun direction and radiance
"""
coefs, solarrad = perez(sxyz, dirdif, **kwargs)
uv = translate.bin2uv(np.arange(side*side), side, offset=0.0)
jitter = translate.bin2uv(np.arange(jn*jn), jn, offset=0.0) + .5/jn
uvj = uv[:, None, :] + jitter/side
xyz = translate.uv2xyz(uvj.reshape(-1, 2), xsign=1).reshape(-1, 3)
lum = perez_lum(xyz, coefs, intersky).reshape(coefs.shape[0], -1, jn*jn)
lum = np.average(lum, -1)
ground = coefs[:, 1:2]
sun = solarrad[:, None]
if color:
lum = lum[..., None] * np.ones((1,1,3))
ground = ground[..., None] * np.ones((1, 1, 3))
sun = sun * np.ones((1, 3))
return lum, ground, np.hstack((sxyz, sun))
[docs]
def radiance_skydef(sunpos, dirdif, loc=None, md=None, ground_fac=0.2,
td=10.9735311509, ro=0.0):
"""similar to gendaylit, returns strings
Parameters
----------
sunpos: Sequence
dx, dy, dz sun position or m,d,h (if loc is not None)
dirdif: Sequence
direct normal, diffuse horizontal W/m^2
loc: tuple, optional
location data given as lat, lon, mer with + west of prime meridian
triggers sunpos treated as timestep
md: tuple, optional
month day of sky calcs (for more precise eccentricity calc with xyz
sunpos)
ground_fac: float
scaling factor (reflectance) for ground brightness
td: np.array float, optional
(N,) dew point temperature in C
ro: float, optional
ignored if sunpos is xyz, else angle in degrees counter-clockwise to
rotate sky (to correct model north, equivalent to clockwise rotation
of scene)
Returns
-------
desc: str
comments with sky info
sund: str
solar material and sun object ("" if no sun)
skyd: str
perezlum brightfunc definition and sky/ground objects
"""
if loc is not None:
desc = (f"# perez sky:\n"
f"# time: {sunpos}\n"
f"# dewpoint: {td}\n"
f"# location: {loc}\n"
f"# sky rotation: {ro}\n"
f"# directnormal, diffuse horiz: {dirdif}\n"
f"# ground reflectance: {ground_fac}\n")
md = (int(sunpos[0]), int(sunpos[1]))
ts = row_2_datetime64(sunpos)
sxyz = sunpos_xyz(ts, *loc, ro=ro).flatten()
else:
desc = (f"# perez sky:\n"
f"# sunposition: {sunpos}\n"
f"# dewpoint: {td}\n"
f"# month/day (for eccentricity): {md}\n"
f"# directnormal, diffuse horiz: {dirdif}\n"
f"# ground reflectance: {ground_fac}\n")
sxyz = sunpos
if sxyz[2] <= 0:
return "# sun below horizon", "", ""
coefs, solarrad = perez(sxyz, dirdif, md, ground_fac, td)
coefs = coefs[0]
srad = solarrad[0]
sund = ""
if srad > 0:
sund = f"\nvoid light solar\n0\n0\n3 {srad:.6f} {srad:.6f} {srad:.6f}\n"
sund += "\nsolar source sun\n0\n0\n4 {:.6f} {:.6f} {:.6f} 0.533\n".format(*sxyz)
skyd = ("\nvoid brightfunc skyfunc\n2 skybright perezlum.cal\n0\n10 "
"{:.6f} {:.6f} {:.6f} {:.6f} {:.6f} {:.6f} {:.6f} {:.6f} {:.6f}"
" {:.6f}\n\n".format(*coefs, *sxyz))
skyd += RadianceFormatter.get_skydef(mod="skyfunc", groundname="groundglow")
return desc, sund, skyd