diff --git a/__init__.py b/__init__.py index 1d80fad..dfbc93c 100644 --- a/__init__.py +++ b/__init__.py @@ -12,16 +12,17 @@ ***************************************************************************/ /*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * ***************************************************************************/ This script initializes the plugin, making it known to QGIS. """ +# 1. Initialize fallback immediately to shield all downstream imports import site import sys @@ -29,6 +30,25 @@ if user_site not in sys.path: sys.path.append(user_site) + +try: + import torch +except ImportError: + # Create the local mock right here + class MetaMock(type): + def __getattr__(cls, name): + if name == "is_available": + return lambda: False + return cls + + def __call__(cls, *args, **kwargs): + return cls + + class LocalMockTorch(metaclass=MetaMock): + pass + + # Inject it into Python's module registry BEFORE QGIS loads any sub-files + sys.modules["torch"] = LocalMockTorch __author__ = "Fredrik Lindberg" __date__ = "2020-04-02" __copyright__ = "(C) 2020 by Fredrik Lindberg" @@ -41,7 +61,7 @@ def classFactory(iface): # pylint: disable=invalid-name :param iface: A QGIS interface instance. :type iface: QgsInterface """ - # from .processing_umep import ProcessingUMEPPlugin + # Crucial: pass the iface variable QGIS gives you right into the plugin return ProcessingUMEPPlugin() diff --git a/functions/SEBEfiles/Perez_v3_moved.py b/functions/SEBEfiles/Perez_v3_moved.py deleted file mode 100644 index e281a25..0000000 --- a/functions/SEBEfiles/Perez_v3_moved.py +++ /dev/null @@ -1,340 +0,0 @@ -from __future__ import division -from __future__ import print_function -from builtins import range -import numpy as np - - -def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): - """ - This function calculates distribution of luminance on the skyvault based on - Perez luminince distribution model. - Created by: - Fredrik Lindberg 20120527, fredrikl@gvc.gu.se - Gothenburg University, Sweden - Urban Climte Group - - Input parameters: - - zen: Zenith angle of the Sun (in degrees) - - azimuth: Azimuth angle of the Sun (in degrees) - - radD: Horizontal diffuse radiation (W m-2) - - radI: Direct radiation perpendicular to the Sun beam (W m-2) - - jday: Day of year - - Output parameters: - - lv: Relative luminance map (same dimensions as theta. gamma) - - acoeff=[1.353 -0.258 -0.269 -1.437 - -1.222 -0.773 1.415 1.102 - -1.100 -0.252 0.895 0.016 - -0.585 -0.665 -0.267 0.712 - -0.600 -0.347 -2.500 2.323 - -1.016 -0.367 1.008 1.405 - -1.000 0.021 0.503 -0.512 - -1.050 0.029 0.426 0.359]; - - bcoeff=[-0.767 0.001 1.273 -0.123 - -0.205 0.037 -3.913 0.916 - 0.278 -0.181 -4.500 1.177 - 0.723 -0.622 -5.681 2.630 - 0.294 0.049 -5.681 1.842 - 0.288 -0.533 -3.850 3.375 - -0.300 0.192 0.702 -1.632 - -0.325 0.116 0.778 0.003]; - - ccoeff=[2.800 0.600 1.238 1.000 - 6.975 0.177 6.448 -0.124 - 24.22 -13.08 -37.70 34.84 - 33.34 -18.30 -62.25 52.08 - 21.00 -4.766 -21.59 7.249 - 14.00 -0.999 -7.14 7.547 - 19.00 -5.000 1.243 -1.91 - 31.06 -14.50 -46.11 55.37]; - - dcoeff=[1.874 0.630 0.974 0.281 - -1.580 -0.508 -1.781 0.108 - -5.00 1.522 3.923 -2.62 - -3.50 0.002 1.148 0.106 - -3.50 -0.155 1.406 0.399 - -3.40 -0.108 -1.075 1.57 - -4.00 0.025 0.384 0.266 - -7.23 0.405 13.35 0.623]; - - ecoeff=[0.035 -0.125 -0.572 0.994 - 0.262 0.067 -0.219 -0.428 - -0.016 0.160 0.420 -0.556 - 0.466 -0.33 -0.088 -0.033 - 0.003 0.077 -0.066 -0.129 - -0.067 0.402 0.302 -0.484 - 1.047 -0.379 -2.452 1.466 - 1.500 -0.643 1.856 0.564]; - - :param zen: - :param azimuth: - :param radD: - :param radI: - :param jday: - :param patchchoice: - :return: - """ - - m_a1 = np.array( - [1.3525, -1.2219, -1.1000, -0.5484, -0.6000, -1.0156, -1.0000, -1.0500] - ) - m_a2 = np.array( - [-0.2576, -0.7730, -0.2515, -0.6654, -0.3566, -0.3670, 0.0211, 0.0289] - ) - m_a3 = np.array( - [-0.2690, 1.4148, 0.8952, -0.2672, -2.5000, 1.0078, 0.5025, 0.4260] - ) - m_a4 = np.array( - [-1.4366, 1.1016, 0.0156, 0.7117, 2.3250, 1.4051, -0.5119, 0.3590] - ) - m_b1 = np.array( - [-0.7670, -0.2054, 0.2782, 0.7234, 0.2937, 0.2875, -0.3000, -0.3250] - ) - m_b2 = np.array( - [0.0007, 0.0367, -0.1812, -0.6219, 0.0496, -0.5328, 0.1922, 0.1156] - ) - m_b3 = np.array( - [1.2734, -3.9128, -4.5000, -5.6812, -5.6812, -3.8500, 0.7023, 0.7781] - ) - m_b4 = np.array( - [-0.1233, 0.9156, 1.1766, 2.6297, 1.8415, 3.3750, -1.6317, 0.0025] - ) - m_c1 = np.array( - [2.8000, 6.9750, 24.7219, 33.3389, 21.0000, 14.0000, 19.0000, 31.0625] - ) - m_c2 = np.array( - [ - 0.6004, - 0.1774, - -13.0812, - -18.3000, - -4.7656, - -0.9999, - -5.0000, - -14.5000, - ] - ) - m_c3 = np.array( - [ - 1.2375, - 6.4477, - -37.7000, - -62.2500, - -21.5906, - -7.1406, - 1.2438, - -46.1148, - ] - ) - m_c4 = np.array( - [1.0000, -0.1239, 34.8438, 52.0781, 7.2492, 7.5469, -1.9094, 55.3750] - ) - m_d1 = np.array( - [1.8734, -1.5798, -5.0000, -3.5000, -3.5000, -3.4000, -4.0000, -7.2312] - ) - m_d2 = np.array( - [0.6297, -0.5081, 1.5218, 0.0016, -0.1554, -0.1078, 0.0250, 0.4050] - ) - m_d3 = np.array( - [0.9738, -1.7812, 3.9229, 1.1477, 1.4062, -1.0750, 0.3844, 13.3500] - ) - m_d4 = np.array( - [0.2809, 0.1080, -2.6204, 0.1062, 0.3988, 1.5702, 0.2656, 0.6234] - ) - m_e1 = np.array( - [0.0356, 0.2624, -0.0156, 0.4659, 0.0032, -0.0672, 1.0468, 1.5000] - ) - m_e2 = np.array( - [-0.1246, 0.0672, 0.1597, -0.3296, 0.0766, 0.4016, -0.3788, -0.6426] - ) - m_e3 = np.array( - [-0.5718, -0.2190, 0.4199, -0.0876, -0.0656, 0.3017, -2.4517, 1.8564] - ) - m_e4 = np.array( - [0.9938, -0.4285, -0.5562, -0.0329, -0.1294, -0.4844, 1.4656, 0.5636] - ) - - acoeff = np.transpose(np.atleast_2d([m_a1, m_a2, m_a3, m_a4])) - bcoeff = np.transpose(np.atleast_2d([m_b1, m_b2, m_b3, m_b4])) - ccoeff = np.transpose(np.atleast_2d([m_c1, m_c2, m_c3, m_c4])) - dcoeff = np.transpose(np.atleast_2d([m_d1, m_d2, m_d3, m_d4])) - ecoeff = np.transpose(np.atleast_2d([m_e1, m_e2, m_e3, m_e4])) - - deg2rad = np.pi / 180 - rad2deg = 180 / np.pi - altitude = 90 - zen - zen = zen * deg2rad - azimuth = azimuth * deg2rad - altitude = altitude * deg2rad - Idh = radD - # Ibh = radI/sin(altitude) - Ibn = radI - - # Skyclearness - PerezClearness = ((Idh + Ibn) / (Idh + 1.041 * np.power(zen, 3))) / ( - 1 + 1.041 * np.power(zen, 3) - ) - # Extra terrestrial radiation - day_angle = jday * 2 * np.pi / 365 - # I0=1367*(1+0.033*np.cos((2*np.pi*jday)/365)) - I0 = 1367 * ( - 1.00011 - + 0.034221 * np.cos(day_angle) - + 0.00128 * np.sin(day_angle) - + 0.000719 * - # New from robinsson - np.cos(2 * day_angle) - + 0.000077 * np.sin(2 * day_angle) - ) - - # Optical air mass - # m=1/altitude; old - if altitude >= 10 * deg2rad: - AirMass = 1 / np.sin(altitude) - elif altitude < 0: # below equation becomes complex - AirMass = 1 / np.sin(altitude) + 0.50572 * np.power( - 180 * complex(altitude) / np.pi + 6.07995, -1.6364 - ) - else: - AirMass = 1 / np.sin(altitude) + 0.50572 * np.power( - 180 * altitude / np.pi + 6.07995, -1.6364 - ) - - # Skybrightness - # if altitude*rad2deg+6.07995>=0 - PerezBrightness = (AirMass * radD) / I0 - if Idh <= 10: - # m_a=0;m_b=0;m_c=0;m_d=0;m_e=0; - PerezBrightness = 0 - if altitude < 0: - print("Airmass") - print(AirMass) - print(PerezBrightness) - - # sky clearness bins - if PerezClearness < 1.065: - intClearness = 0 - if PerezClearness > 1.065 and PerezClearness < 1.230: - intClearness = 1 - if PerezClearness > 1.230 and PerezClearness < 1.500: - intClearness = 2 - if PerezClearness > 1.500 and PerezClearness < 1.950: - intClearness = 3 - if PerezClearness > 1.950 and PerezClearness < 2.800: - intClearness = 4 - if PerezClearness > 2.800 and PerezClearness < 4.500: - intClearness = 5 - if PerezClearness > 4.500 and PerezClearness < 6.200: - intClearness = 6 - if PerezClearness > 6.200: - intClearness = 7 - - m_a = ( - acoeff[intClearness, 0] - + acoeff[intClearness, 1] * zen - + PerezBrightness - * (acoeff[intClearness, 2] + acoeff[intClearness, 3] * zen) - ) - m_b = ( - bcoeff[intClearness, 0] - + bcoeff[intClearness, 1] * zen - + PerezBrightness - * (bcoeff[intClearness, 2] + bcoeff[intClearness, 3] * zen) - ) - m_e = ( - ecoeff[intClearness, 0] - + ecoeff[intClearness, 1] * zen - + PerezBrightness - * (ecoeff[intClearness, 2] + ecoeff[intClearness, 3] * zen) - ) - - if intClearness > 0: - m_c = ( - ccoeff[intClearness, 0] - + ccoeff[intClearness, 1] * zen - + PerezBrightness - * (ccoeff[intClearness, 2] + ccoeff[intClearness, 3] * zen) - ) - m_d = ( - dcoeff[intClearness, 0] - + dcoeff[intClearness, 1] * zen - + PerezBrightness - * (dcoeff[intClearness, 2] + dcoeff[intClearness, 3] * zen) - ) - else: - # different equations for c & d in clearness bin no. 1, from Robinsson - m_c = ( - np.exp( - np.power( - PerezBrightness - * ( - ccoeff[intClearness, 0] + ccoeff[intClearness, 1] * zen - ), - ccoeff[intClearness, 2], - ) - ) - - 1 - ) - m_d = ( - -np.exp( - PerezBrightness - * (dcoeff[intClearness, 0] + dcoeff[intClearness, 1] * zen) - ) - + dcoeff[intClearness, 2] - + PerezBrightness * dcoeff[intClearness, 3] * PerezBrightness - ) - - skyvaultalt = np.atleast_2d([]) - skyvaultazi = np.atleast_2d([]) - if patchchoice == 2: - # Creating skyvault at one degree intervals - skyvaultalt = np.ones([90, 361]) * 90 - skyvaultazi = np.empty((90, 361)) - for j in range(90): - skyvaultalt[j, :] = 91 - j - skyvaultazi[j, :] = list(range(361)) - - elif patchchoice == 1: - # Creating skyvault of patches of constant radians (Tregeneza and - # Sharples, 1993) - skyvaultaltint = [6, 18, 30, 42, 54, 66, 78] - skyvaultaziint = [12, 12, 15, 15, 20, 30, 60] - for j in range(7): - for k in range(1, int(360 / skyvaultaziint[j]) + 1): - skyvaultalt = np.append(skyvaultalt, skyvaultaltint[j]) - skyvaultazi = np.append(skyvaultazi, k * skyvaultaziint[j]) - - skyvaultalt = np.append(skyvaultalt, 90) - skyvaultazi = np.append(skyvaultazi, 360) - - skyvaultzen = (90 - skyvaultalt) * deg2rad - skyvaultalt = skyvaultalt * deg2rad - skyvaultazi = skyvaultazi * deg2rad - - # Angular distance from the sun from Robinsson - cosSkySunAngle = np.sin(skyvaultalt) * np.sin(altitude) + np.cos( - altitude - ) * np.cos(skyvaultalt) * np.cos(np.abs(skyvaultazi - azimuth)) - - # Main equation - lv = (1 + m_a * np.exp(m_b / np.cos(skyvaultzen))) * ( - ( - 1 - + m_c * np.exp(m_d * np.arccos(cosSkySunAngle)) - + m_e * cosSkySunAngle * cosSkySunAngle - ) - ) - - # Normalisation - lv = lv / np.sum(lv) - - if patchchoice == 1: - # x = np.atleast_2d([]) - # lv = np.transpose(np.append(np.append(np.append(x, skyvaultalt*rad2deg), skyvaultazi*rad2deg), lv)) - x = np.transpose(np.atleast_2d(skyvaultalt * rad2deg)) - y = np.transpose(np.atleast_2d(skyvaultazi * rad2deg)) - z = np.transpose(np.atleast_2d(lv)) - lv = np.append(np.append(x, y, axis=1), z, axis=1) - return lv, PerezClearness, PerezBrightness diff --git a/functions/SEBEfiles/get_ders.py b/functions/SEBEfiles/get_ders.py index dd962f9..36d446c 100644 --- a/functions/SEBEfiles/get_ders.py +++ b/functions/SEBEfiles/get_ders.py @@ -2,17 +2,23 @@ from __future__ import print_function try: - pass -except BaseException: + from osgeo import gdal, gdal_array + from osgeo.gdalconst import ( + GDT_Float64, + GDT_Float32, + GDT_UInt32, + GDT_UInt16, + GDT_Byte, + ) +except: use_gdal = False else: use_gdal = True import numpy -import subprocess -import os +import subprocess, os import tempfile -from math import pi +from math import pi, sin from ..data_io.data_io import read_dem_grid GDALDEM = r"gdaldem" @@ -22,11 +28,11 @@ def get_temp_file(suffix=""): fd, temp_filename = tempfile.mkstemp(suffix=suffix) try: os.close(fd) - except BaseException: + except: pass try: os.remove(temp_filename) - except BaseException: + except: pass # fix_print_with_import print(temp_filename) diff --git a/functions/SEBEfiles/sunmapcreator_2015a.py b/functions/SEBEfiles/sunmapcreator_2015a.py index 236c3dc..5b21cd2 100644 --- a/functions/SEBEfiles/sunmapcreator_2015a.py +++ b/functions/SEBEfiles/sunmapcreator_2015a.py @@ -44,20 +44,6 @@ def sunmapcreator_2015a( azistart, ) = create_patches(patch_option) - # skyvaultaltint = np.array([6, 18, 30, 42, 54, 66, 78, 90]) - # skyvaultaziint = np.array([12, 12, 15, 15, 20, 30, 60, 360]) - # aziinterval = np.array([30, 30, 24, 24, 18, 12, 6, 1]) - # azistart = np.array([0, 4, 2, 5, 8, 0, 10, 0]) - # annulino = np.array([0, 12, 24, 36, 48, 60, 72, 84, 90]) - # skyvaultazi = np.array([]) - # for j in range(8): - # for k in range(1, int(360/skyvaultaziint[j]) + 1): - # # skyvaultalt(index)=skyvaultaltint(j); - # skyvaultazi = np.append(skyvaultazi, k*skyvaultaziint[j] + azistart[j]) - # if skyvaultazi[-1] > 360: - # skyvaultazi[-1] = skyvaultazi[-1] - 360 - # index = index + 1 - iangle2 = np.array([]) Gyear = 0 Dyear = 0 @@ -153,51 +139,12 @@ def sunmapcreator_2015a( radmatR[:, met[i, 1] + 2] = ( radmatR[:, met[i, 1] + 2] + G * (1 / 145) * albedo ) - # Gmonth(met(i,2))=Gmonth(met(i,2))+(G*sin(altitude(i)*(pi/180))); - # Dmonth(met(i,2))=Dmonth(met(i,2))+D; - - # plot(distazi),hold on - # plot(distalt,'r') - # plot(azipos2,15,'r*'),hold off - # pause(0.1); # Adjusting the numbers if multiple years is used - if np.shape(met)[0] > 8760: multiyear = np.shape(met)[0] / 8760 radmatI[:, 2:15] = radmatI[:, 2:15] / multiyear radmatD[:, 2:15] = radmatD[:, 2:15] / multiyear radmatR[:, 2:15] = radmatR[:, 2:15] / multiyear - # Gyear=Gyear/multiyear; - # Dyear=Dyear/multiyear; - # Gmonth=Gmonth/multiyear; - # Dmonth=Gmonth/multiyear; - # # Plotting - # maxrad=max(radmat(:,3)); - # for i=1:length(radmat(:,1)) - # if radmat(i,3)>0 - # pp(radmat(i,2)*(pi/180),(radmat(i,1)*-1)+90,[0 90],'LineStyle','none','Marker','h',... - # 'LineColor',[(1-(radmat(i,3)/maxrad)) (1-(radmat(i,3)/maxrad)) (1-(radmat(i,3)/maxrad))],'NumRings',3) - # hold on - # end - # end - # set(gca,'YDir','reverse','FontSize',8,'View',[270 90],'XTickLabel','') - - # Old skyvault - # noa=19;# No. of anglesteps minus 1 - # step=89/noa; - # iangle=[((step/2):step:89) 90]; - # [iazimuth aziinterval]=svf_angles_100121(iangle); - # iangle2=[]; - # Gyear=0; - # Dyear=0; - # Gmonth=zeros(1,12); - # Dmonth=Gmonth; - # for j=1:length(aziinterval) - # iangle2=[iangle2 iangle(j)*ones(1,aziinterval(j))]; - # end - # - # radmat=[iangle2;iazimuth;zeros(13,length(iangle2))]'; - return radmatI, radmatD, radmatR diff --git a/functions/SOLWEIGpython/COMFA/COMFA_BUDGET.py b/functions/SOLWEIGpython/COMFA/COMFA_BUDGET.py index 20b0047..c2151a0 100644 --- a/functions/SOLWEIGpython/COMFA/COMFA_BUDGET.py +++ b/functions/SOLWEIGpython/COMFA/COMFA_BUDGET.py @@ -16,31 +16,26 @@ def COMFA_Mact(weight, height, sex, age, activity, activity_unit): # activity_time = 30 ### METABOLIC HEAT CALCULATION FOR CHILDREN ### - # Body surface area of child or adult (different for infant according to - # Haylock et al. (1978)) + # Body surface area of child or adult (different for infant according to Haylock et al. (1978)) BSA = weight**0.5378 * height**0.3964 * 0.0242 if activity_unit == "MET": if sex == 2: if (age >= 3) and (age <= 10): - # Resting metabolic rate of a 3-10 year old girl (kJ/day) - # (Cheng & Brown, 2020) + # Resting metabolic rate of a 3-10 year old girl (kJ/day) (Cheng & Brown, 2020) RMR_J = 16.97 * weight + 1.618 * height + 371.2 elif (age > 10) and (age <= 18): - # Resting metabolic rate of a 10-18 year old girl (kJ/day) - # (Cheng & Brown, 2020) + # Resting metabolic rate of a 10-18 year old girl (kJ/day) (Cheng & Brown, 2020) RMR_J = 8.365 * weight + 4.65 * height + 200 else: RMR_J = 8.365 * weight + 4.65 * height + 200 elif sex == 1: if (age >= 3) and (age <= 10): - # Resting metabolic rate of a 3-10 year old boy (kJ/day) (Cheng - # & Brown, 2020) + # Resting metabolic rate of a 3-10 year old boy (kJ/day) (Cheng & Brown, 2020) RMR_J = 19.6 * weight + 1.033 * height + 414.9 elif (age > 10) and (age <= 18): - # Resting metabolic rate of a 10-18 year old boy (kJ/day) - # (Cheng & Brown, 2020) + # Resting metabolic rate of a 10-18 year old boy (kJ/day) (Cheng & Brown, 2020) RMR_J = 16.25 * weight + 1.372 * height + 515.5 else: RMR_J = 8.365 * weight + 4.65 * height + 200 @@ -181,8 +176,7 @@ def COMFA_ra(vw, va): # A[H] = 0.0266 # n[H] = 0.805 - # Written by N. Kenny April 2006. Translated from MATLAB to Python by Nils - # Wallenberg 2022. + # Written by N. Kenny April 2006. Translated from MATLAB to Python by Nils Wallenberg 2022. return ra @@ -227,8 +221,7 @@ def COMFA_rsk(Mact, Ta, RH, age, kid): # 1212 is volumetric heat capacity (or rho.*Cp) rsk = (1212) / (0.13 * Es + 15) - # See Kenny et al. (2009b) pg 434 for derivation of rsk based on Kerslake - # 1972. + # See Kenny et al. (2009b) pg 434 for derivation of rsk based on Kerslake 1972. return rsk @@ -293,8 +286,7 @@ def COMFA_CONV(Mact, Ta, RH, vw, va, rco, weight, height, age, kid): # Body to surface area of kid BSA_k = weight**0.5378 * height**0.3964 * 0.0242 BMI_k = weight / (height / 100) ** 2 - # Body-to-surface area of adult (weight = 65 kg and height = 176 cm, - # according to Cheng & Brown, 2020) + # Body-to-surface area of adult (weight = 65 kg and height = 176 cm, according to Cheng & Brown, 2020) adult_weight = 65 adult_height = 176 BSA_a = adult_weight**0.5378 * adult_height**0.3964 * 0.0242 @@ -525,8 +517,7 @@ def COMFA_EVAP(Mact, Ta, RH, vw, va, rco, rcvo, age, kid): def COMFA_Ts(Mact, Ta, RH, vw, va, rco, age, kid): # Calculates the average surface temperature of a clothed person (degrees C) with inputs Tair (air - # temperature, degrees C), rco (clothing resistance, s/m), vw (air - # velocity, s/m), Mact (Metabolic Activity, W/m2) + # temperature, degrees C), rco (clothing resistance, s/m), vw (air velocity, s/m), Mact (Metabolic Activity, W/m2) rc = COMFA_rc(va, rco) # Resistance of boundary layer (s/m) @@ -573,24 +564,22 @@ def COMFA_BUDGET(Mact, Ta, RH, vw, va, rco, rcvo, weight, height, age, kid): # Wind Velocity (m/s), activity velocity (va, m s-1), static clothing resistance (rco, sm-1), static clothing vapor reistance (rcvo, s m-1). # Note can also make Rabs an input. See below. - # This COFMA model is general for people at or near comfort, and is not - # tested yet on extreme conditions. + ### This COFMA model is general for people at or near comfort, and is not + ### tested yet on extreme conditions. # %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% # %%%% CLOTHING - generally can estimate Icl value (clo) and convert them to # %%%% resistances as follows: - # Rco: rco = Icl * 186.6. Or convert from conductivity based on 1 Clo = - # 0.1555 (m 2K W-1). + # Rco: rco = Icl * 186.6. Or convert from conductivity based on 1 Clo = 0.1555 (m 2K W-1). - # NOTE: Icl values can be found in clo or conductivity values from ISO - # 9920 2007. + # NOTE: Icl values can be found in clo or conductivity values from ISO 9920 2007. # Rcvo: Can convert from a conductivity value of Icl, where Icl is found in tables for specific clothing ensembles from ISO (2007). # 1) convert from Icl to Re,cl (m2 kPa W-1): Re,cl = Icl (m 2K W-1)*0.18 (constant from ISO 9920 pg 12 based on 1 or 2-layer clothing ensembles). # 2) Convert Re,cl to rcvo: rcvo = Re,cl*18,400, where 18400 is a conversion factor from Re,cl (vapour resistance, in m2kPaW-1) # to rcvo, using Lv = 2.5*106 J kg-1, rho = 1.16 kg m-3, and Pa = 98kPa. - # %%%%%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%% + # %%%%%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%% # W m-2 %%%%random value ... Here you need to link to another dataset that calcs Rabs or bring o link to more doce. # Rabs = 400 diff --git a/functions/SOLWEIGpython/COMFA/ComfaPython20201023.py b/functions/SOLWEIGpython/COMFA/ComfaPython20201023.py index ae6048f..8030060 100644 --- a/functions/SOLWEIGpython/COMFA/ComfaPython20201023.py +++ b/functions/SOLWEIGpython/COMFA/ComfaPython20201023.py @@ -46,7 +46,7 @@ else: dayspermonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] -doy = np.sum(dayspermonth[0: month - 1]) + day # day of year (Jan 1 = 1) +doy = np.sum(dayspermonth[0 : month - 1]) + day # day of year (Jan 1 = 1) # Lubbock 33.5779,-101.8552 @@ -61,9 +61,7 @@ L = 0.1 # length of cylinder (cm) D = 0.01 # Diameter of cylinder (cm) -# Effective area of body. 0.78 for standing from Campebll and Normal -# (1998) (0.70 for sitting) -Aeff = 0.78 +Aeff = 0.78 # Effective area of body. 0.78 for standing from Campebll and Normal (1998) (0.70 for sitting) Total_CNRRabs_solweig, zensolweig, Kdsolweig = Rad_Total_solweig( diff --git a/functions/SOLWEIGpython/COMFA/atm_P.py b/functions/SOLWEIGpython/COMFA/atm_P.py index ca64e30..f6f7084 100644 --- a/functions/SOLWEIGpython/COMFA/atm_P.py +++ b/functions/SOLWEIGpython/COMFA/atm_P.py @@ -24,8 +24,6 @@ def atm_P(A=None, *args, **kwargs): # Pa=Po.*(1-.0000225577.*A).^5.25588; # A = altitude, units don't matter because it cancels with Ao (which is 0 at surface). -# Therefore, it will be exp^-1, and Po is surface pressure, which is 1bar, -# or 100kPa. +# Therefore, it will be exp^-1, and Po is surface pressure, which is 1bar, or 100kPa. -# equation from: -# http://www.engineeringtoolbox.com/air-altitude-pressure-d_462.html +# equation from: http://www.engineeringtoolbox.com/air-altitude-pressure-d_462.html diff --git a/functions/SOLWEIGpython/COMFA/radiationfunctionsCOMFA.py b/functions/SOLWEIGpython/COMFA/radiationfunctionsCOMFA.py index 72a312b..d2e2b98 100644 --- a/functions/SOLWEIGpython/COMFA/radiationfunctionsCOMFA.py +++ b/functions/SOLWEIGpython/COMFA/radiationfunctionsCOMFA.py @@ -51,8 +51,7 @@ def CNRRabs_Total( # lat = 33.75; # A = 992; - # Aeff=0.78 # Effective area of body. 0.78 for standing from Campebll and - # Normal (1998) (0.70 for sitting) Moved to main function + # Aeff=0.78 # Effective area of body. 0.78 for standing from Campebll and Normal (1998) (0.70 for sitting) Moved to main function Kin_abs, zen, Kd = CNR_Kinabs_meas(alpha, Kin, L, D, A, lat, d, t, Atr) Kup_abs = CNR_Kup(alpha, Kup, L, D) @@ -82,8 +81,7 @@ def COMFA_RAD_SPATIAL_TC( ) Kb = Kin - Kd - # Not perpendicular but horrisontal surface in COMFA Kb = (Kin - - # Kd)/(np.sin(altitude*deg2rad)) + # Not perpendicular but horrisontal surface in COMFA Kb = (Kin - Kd)/(np.sin(altitude*deg2rad)) Acs = CRT_Acs(L, D) @@ -158,7 +156,7 @@ def Rad_Total_solweig( metload.Solweig_2015a_metdata_noload(metdata, location, utc) ) - if Kd is None: # Kd and Kb is estimated with Reindl et al. 1990 + if Kd == None: # Kd and Kb is estimated with Reindl et al. 1990 Itoa = 1370.0 # Effective solar constant D_ = sun_distance(doy) I0et = Itoa * np.cos(zen) * D_ # extra terrestial solar radiation @@ -167,8 +165,7 @@ def Rad_Total_solweig( Kb = Kin - Kd else: Kb = Kin - Kd - # Not perpendicular but horrisontal surface in COMFA Kb = (Kin - - # Kd)/(np.sin(altitude*deg2rad)) + # Not perpendicular but horrisontal surface in COMFA Kb = (Kin - Kd)/(np.sin(altitude*deg2rad)) Acs = CRT_Acs(L, D) @@ -349,8 +346,7 @@ def CRT_Acs(L, D): def CNR_Kup(alpha, Kup, L, D): # Used to calculate the total reflected solar radiation (K) absorbed by the # a cylinder with inputs alpha (cylinder albedo), Kup (measured reflected solar - # radiation by the CNR net radiometer (W/m2), Acyl (area of the cylinder, - # m2) + # radiation by the CNR net radiometer (W/m2), Acyl (area of the cylinder, m2) Acyl = CRT_Acyl(L, D) Kup_abs = np.dot(np.dot(np.dot((1 - alpha), Kup), 0.5), Acyl) @@ -415,7 +411,7 @@ def day_of_year(yyyy, month, day): else: dayspermonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - doy = np.sum(dayspermonth[0: month - 1]) + day + doy = np.sum(dayspermonth[0 : month - 1]) + day return doy diff --git a/functions/SOLWEIGpython/CirclePlotBar.py b/functions/SOLWEIGpython/CirclePlotBar.py index 347273a..ec07925 100644 --- a/functions/SOLWEIGpython/CirclePlotBar.py +++ b/functions/SOLWEIGpython/CirclePlotBar.py @@ -1,14 +1,12 @@ import numpy as np import matplotlib.cm as cmx -from matplotlib.pyplot import figure +from matplotlib.pyplot import figure, show import matplotlib.pyplot as plt from matplotlib.patches import Circle import matplotlib.colors as colors -# def PolarBarPlot(lv, radD, outfolder, YYYY, DOY, XH, hours, XM, minu, -# iStep, idxStep): - +# def PolarBarPlot(lv, radD, outfolder, YYYY, DOY, XH, hours, XM, minu, iStep, idxStep): def PolarBarPlot( lv, solar_altitude, @@ -19,8 +17,7 @@ def PolarBarPlot( maxrad, plot_type, ): - # def PolarBarPlot(lv, filename_start, outfolder, YYYY, DOY, XH, hours, - # XM, minu, iStep, minrad, maxrad): + # def PolarBarPlot(lv, filename_start, outfolder, YYYY, DOY, XH, hours, XM, minu, iStep, minrad, maxrad): deg2rad = np.pi / 180 @@ -34,8 +31,9 @@ def PolarBarPlot( else: ax.set_theta_direction("anticlockwise") # ax.set_theta_direction('clockwise') - # Unique altitudes in lv, i.e. unique altitude for the patches - skyalt, skyalt_c = np.unique(lv[:, 0], return_counts=True) + skyalt, skyalt_c = np.unique( + lv[:, 0], return_counts=True + ) # Unique altitudes in lv, i.e. unique altitude for the patches # lvSum = np.sum(lv[:,2]) @@ -47,8 +45,9 @@ def PolarBarPlot( if plot_type: jet = cm = plt.get_cmap("jet") - # Watts per square meter Steradian - cNorm = colors.Normalize(vmin=minrad, vmax=maxrad) + cNorm = colors.Normalize( + vmin=minrad, vmax=maxrad + ) # Watts per square meter Steradian scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=jet) else: # patch_category = {1.8:'deepskyblue', 2.5:'forestgreen', 3.3:'yellow', 4.5:'peru', 6.0:'grey'} @@ -69,7 +68,7 @@ def PolarBarPlot( radii_sub = 0 for i in range(skyalt_c.__len__()): clrs = lv_norm[lv_alt == skyalt[i]] - # print(clrs) + if skyalt_c[i] > 1: theta_patch = ( np.arange(0, skyalt_c[skyalt == skyalt[i]][0], 1) @@ -81,11 +80,6 @@ def PolarBarPlot( / skyalt_c[skyalt == skyalt[i]][0] ) - # if plot_type: - # patch_order_range = range(skyalt_c[skyalt == skyalt[i]][0]) - # else: - # patch_order_range = reversed(range(skyalt_c[skyalt == skyalt[i]][0])) - patch_order_range = range(skyalt_c[skyalt == skyalt[i]][0]) for j in patch_order_range: @@ -163,8 +157,9 @@ def PolarBarPlot( sm._A = [] cbaxes = fig.add_axes([0.87, 0.1, 0.03, 0.8]) cb = plt.colorbar(sm, cax=cbaxes) - # Watts per square meter Steradian - cb.ax.set_title(r"$W/m^2$ $sr^{-1}$", fontsize=12, fontweight="bold") + cb.ax.set_title( + r"$W/m^2$ $sr^{-1}$", fontsize=12, fontweight="bold" + ) # Watts per square meter Steradian else: # legend_colors = {'Shaded building wall':'peru', 'Sunlit building wall':'yellow', 'Sky':'deepskyblue', 'Trees':'forestgreen', 'Roof':'black'} # legend_colors = {'Building wall':'peru', 'Building roof':'grey', 'Trees':'forestgreen', 'Sky':'deepskyblue'} diff --git a/functions/SOLWEIGpython/Kside_veg_v2019a.py b/functions/SOLWEIGpython/Kside_veg_v2019a.py index f698412..9a9eeec 100644 --- a/functions/SOLWEIGpython/Kside_veg_v2019a.py +++ b/functions/SOLWEIGpython/Kside_veg_v2019a.py @@ -44,13 +44,13 @@ def Kside_veg_v2019a( KsideD = np.zeros((rows, cols)) ### Direct radiation ### - if cyl == 1: # Kside with cylinder ### + if cyl == 1: ### Kside with cylinder ### KsideI = shadow * radI * np.cos(altitude * deg2rad) KeastI = 0 KsouthI = 0 KwestI = 0 KnorthI = 0 - else: # Kside with weights ### + else: ### Kside with weights ### if azimuth > (360 - t) or azimuth <= (180 - t): KeastI = ( radI @@ -130,15 +130,12 @@ def Kside_veg_v2019a( phiVar[ix] = (aziDel * deg2rad) * ( np.sin((aniAlt[ix] + 6) * deg2rad) - - - # Solid angle / Steradian - np.sin((aniAlt[ix] - 6) * deg2rad) - ) + - np.sin((aniAlt[ix] - 6) * deg2rad) + ) # Solid angle / Steradian - # Radiance fraction normalization radTot = radTot + ( aniLum[ix] * phiVar[ix] * np.sin(aniAlt[ix] * deg2rad) - ) + ) # Radiance fraction normalization lumChi = (aniLum * radD) / radTot # Radiance fraction normalization @@ -146,19 +143,13 @@ def Kside_veg_v2019a( for idx in range(0, 145): anglIncC = np.cos(aniAlt[idx] * deg2rad) * np.cos(0) * np.sin( np.pi / 2 - ) + np.sin( - # Angle of incidence, np.cos(0) because cylinder - always - # perpendicular - aniAlt[idx] - * deg2rad - ) * np.cos( + ) + np.sin(aniAlt[idx] * deg2rad) * np.cos( np.pi / 2 - ) - # Diffuse vertical radiation + ) # Angle of incidence, np.cos(0) because cylinder - always perpendicular KsideD = ( KsideD + diffsh[:, :, idx] * lumChi[idx] * anglIncC * phiVar[idx] - ) + ) # Diffuse vertical radiation Keast = ( albedo * (svfviktbuvegE * (radG * (1 - F_sh) + radD * F_sh)) + KupE diff --git a/functions/SOLWEIGpython/Kside_veg_v2022a_torch.py b/functions/SOLWEIGpython/Kside_veg_v2022a_torch.py new file mode 100644 index 0000000..0d7ffe4 --- /dev/null +++ b/functions/SOLWEIGpython/Kside_veg_v2022a_torch.py @@ -0,0 +1,584 @@ +from __future__ import absolute_import +from .Kvikt_veg import Kvikt_veg + +try: + import torch + +except: + pass + + +def Kside_veg_v2022a( + radI, + radD, + radG, + shadow, + svfS, + svfW, + svfN, + svfE, + svfEveg, + svfSveg, + svfWveg, + svfNveg, + azimuth, + altitude, + psi, + t, + albedo, + F_sh, + KupE, + KupS, + KupW, + KupN, + cyl, + lv, + anisotropic_diffuse, + diffsh, + rows, + cols, + asvf, + shmat, + vegshmat, + vbshvegshmat, +): + device = ( + altitude.device + if isinstance(altitude, torch.Tensor) + else ( + azimuth.device + if isinstance(azimuth, torch.Tensor) + else torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + ) + ) + + vikttot = 4.4897 + aziE = azimuth + t + aziS = azimuth - 90 + t + aziW = azimuth - 180 + t + aziN = azimuth - 270 + t + deg2rad = torch.pi / 180.0 + deg2rad = torch.tensor(deg2rad) + + KsideD = torch.zeros((rows, cols), device=device) + Kref_sun = torch.zeros((rows, cols), device=device) + Kref_sh = torch.zeros((rows, cols), device=device) + Kref_veg = torch.zeros((rows, cols), device=device) + Kside = torch.zeros((rows, cols), device=device) + + Kref_veg_n = torch.zeros((rows, cols), device=device) + Kref_veg_s = torch.zeros((rows, cols), device=device) + Kref_veg_e = torch.zeros((rows, cols), device=device) + Kref_veg_w = torch.zeros((rows, cols), device=device) + + Kref_sh_n = torch.zeros((rows, cols), device=device) + Kref_sh_s = torch.zeros((rows, cols), device=device) + Kref_sh_e = torch.zeros((rows, cols), device=device) + Kref_sh_w = torch.zeros((rows, cols), device=device) + + Kref_sun_n = torch.zeros((rows, cols), device=device) + Kref_sun_s = torch.zeros((rows, cols), device=device) + Kref_sun_e = torch.zeros((rows, cols), device=device) + Kref_sun_w = torch.zeros((rows, cols), device=device) + + KeastRef = torch.zeros((rows, cols), device=device) + KwestRef = torch.zeros((rows, cols), device=device) + KnorthRef = torch.zeros((rows, cols), device=device) + KsouthRef = torch.zeros((rows, cols), device=device) + diffRadE = torch.zeros((rows, cols), device=device) + diffRadS = torch.zeros((rows, cols), device=device) + diffRadW = torch.zeros((rows, cols), device=device) + diffRadN = torch.zeros((rows, cols), device=device) + + if cyl == 1: + KsideI = shadow * radI * torch.cos(altitude * deg2rad) + KeastI = torch.zeros((rows, cols), device=device) + KsouthI = torch.zeros((rows, cols), device=device) + KwestI = torch.zeros((rows, cols), device=device) + KnorthI = torch.zeros((rows, cols), device=device) + else: + KeastI = torch.where( + (azimuth > (360 - t)) | (azimuth <= (180 - t)), + radI + * shadow + * torch.cos(altitude * deg2rad) + * torch.sin(aziE * deg2rad), + torch.zeros((rows, cols), device=device), + ) + KsouthI = torch.where( + (azimuth > (90 - t)) & (azimuth <= (270 - t)), + radI + * shadow + * torch.cos(altitude * deg2rad) + * torch.sin(aziS * deg2rad), + torch.zeros((rows, cols), device=device), + ) + KwestI = torch.where( + (azimuth > (180 - t)) & (azimuth <= (360 - t)), + radI + * shadow + * torch.cos(altitude * deg2rad) + * torch.sin(aziW * deg2rad), + torch.zeros((rows, cols), device=device), + ) + KnorthI = torch.where( + (azimuth <= (90 - t)) | (azimuth > (270 - t)), + radI + * shadow + * torch.cos(altitude * deg2rad) + * torch.sin(aziN * deg2rad), + torch.zeros((rows, cols), device=device), + ) + KsideI = shadow * 0 + + viktveg, viktwall = Kvikt_veg(svfE, svfEveg, vikttot) + svfviktbuvegE = viktwall + (viktveg * (1 - psi)) + + viktveg, viktwall = Kvikt_veg(svfS, svfSveg, vikttot) + svfviktbuvegS = viktwall + (viktveg * (1 - psi)) + + viktveg, viktwall = Kvikt_veg(svfW, svfWveg, vikttot) + svfviktbuvegW = viktwall + (viktveg * (1 - psi)) + + viktveg, viktwall = Kvikt_veg(svfN, svfNveg, vikttot) + svfviktbuvegN = viktwall + (viktveg * (1 - psi)) + + if anisotropic_diffuse == 1: + anisotropic_sky = True + + patch_altitude = lv[:, 0] + patch_azimuth = lv[:, 1] + if anisotropic_sky: + patch_luminance = lv[:, 2] + else: + patch_luminance = ( + torch.ones((patch_altitude.shape[0]), device=device) + / patch_altitude.shape[0] + ) + + skyalt, skyalt_c = torch.unique(patch_altitude, return_counts=True) + radTot = torch.zeros(1, device=device) + steradian = torch.zeros((patch_altitude.shape[0]), device=device) + for i in range(patch_altitude.shape[0]): + if skyalt_c[skyalt == patch_altitude[i]] > 1: + steradian[i] = ( + (360 / skyalt_c[skyalt == patch_altitude[i]]) * deg2rad + ) * ( + torch.sin( + (patch_altitude[i] + patch_altitude[0]) * deg2rad + ) + - torch.sin( + (patch_altitude[i] - patch_altitude[0]) * deg2rad + ) + ) + else: + steradian[i] = ( + (360 / skyalt_c[skyalt == patch_altitude[i]]) * deg2rad + ) * ( + torch.sin((patch_altitude[i]) * deg2rad) + - torch.sin( + (patch_altitude[i - 1] + patch_altitude[0]) * deg2rad + ) + ) + + radTot += ( + patch_luminance[i] + * steradian[i] + * torch.sin(patch_altitude[i] * deg2rad) + ) + + lumChi = (patch_luminance * radD) / radTot + + if cyl == 1: + for idx in range(patch_azimuth.shape[0]): + anglIncC = torch.cos( + patch_altitude[idx] * deg2rad + ) * torch.cos(torch.tensor(0.0)) + KsideD += ( + diffsh[:, :, idx] * lumChi[idx] * anglIncC * steradian[idx] + ) + + sunlit_surface = ( + albedo * (radI * torch.cos(altitude * deg2rad)) + + (radD * 0.5) + ) / torch.pi + shaded_surface = (albedo * radD * 0.5) / torch.pi + + temp_vegsh = (vegshmat[:, :, idx] == 0) | ( + vbshvegshmat[:, :, idx] == 0 + ) + Kref_veg += ( + shaded_surface + * temp_vegsh + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + ) + + temp_vbsh = (1 - shmat[:, :, idx]) * vbshvegshmat[:, :, idx] + temp_sh = temp_vbsh == 1 + + sunlit_patches, shaded_patches = shaded_or_sunlit( + altitude, + azimuth, + patch_altitude[idx], + patch_azimuth[idx], + asvf, + ) + Kref_sun += ( + sunlit_surface + * sunlit_patches + * temp_sh + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + ) + Kref_sh += ( + shaded_surface + * shaded_patches + * temp_sh + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + ) + + Kside = KsideI + KsideD + Kref_sun + Kref_sh + Kref_veg + + Keast = KupE * 0.5 + Kwest = KupW * 0.5 + Knorth = KupN * 0.5 + Ksouth = KupS * 0.5 + + else: + for idx in range(patch_azimuth.shape[0]): + if (patch_azimuth[idx] > 360) or (patch_azimuth[idx] <= 180): + anglIncE = torch.cos( + patch_altitude[idx] * deg2rad + ) * torch.cos((90 - patch_azimuth[idx] + t) * deg2rad) + diffRadE += ( + diffsh[:, :, idx] + * lumChi[idx] + * anglIncE + * steradian[idx] + ) + + if (patch_azimuth[idx] > 90) and (patch_azimuth[idx] <= 270): + anglIncS = torch.cos( + patch_altitude[idx] * deg2rad + ) * torch.cos((180 - patch_azimuth[idx] + t) * deg2rad) + diffRadS += ( + diffsh[:, :, idx] + * lumChi[idx] + * anglIncS + * steradian[idx] + ) + + if (patch_azimuth[idx] > 180) and (patch_azimuth[idx] <= 360): + anglIncW = torch.cos( + patch_altitude[idx] * deg2rad + ) * torch.cos((270 - patch_azimuth[idx] + t) * deg2rad) + diffRadW += ( + diffsh[:, :, idx] + * lumChi[idx] + * anglIncW + * steradian[idx] + ) + + if (patch_azimuth[idx] > 270) or (patch_azimuth[idx] <= 90): + anglIncN = torch.cos( + patch_altitude[idx] * deg2rad + ) * torch.cos((0 - patch_azimuth[idx] + t) * deg2rad) + diffRadN += ( + diffsh[:, :, idx] + * lumChi[idx] + * anglIncN + * steradian[idx] + ) + + sunlit_surface = ( + albedo * (radI * torch.cos(altitude * deg2rad)) + + (radD * 0.5) + ) / torch.pi + shaded_surface = (albedo * radD * 0.5) / torch.pi + + temp_vegsh = (vegshmat[:, :, idx] == 0) | ( + vbshvegshmat[:, :, idx] == 0 + ) + Kref_veg += ( + shaded_surface + * temp_vegsh + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + ) + + if (patch_azimuth[idx] > 360) or (patch_azimuth[idx] < 180): + Kref_veg_e += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_vegsh + * torch.cos((90 - patch_azimuth[idx] + t) * deg2rad) + ) + if (patch_azimuth[idx] > 90) and (patch_azimuth[idx] < 270): + Kref_veg_s += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_vegsh + * torch.cos((180 - patch_azimuth[idx] + t) * deg2rad) + ) + if (patch_azimuth[idx] > 180) and (patch_azimuth[idx] < 360): + Kref_veg_w += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_vegsh + * torch.cos((270 - patch_azimuth[idx] + t) * deg2rad) + ) + if (patch_azimuth[idx] > 270) or (patch_azimuth[idx] < 90): + Kref_veg_n += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_vegsh + * torch.cos((0 - patch_azimuth[idx] + t) * deg2rad) + ) + + temp_vbsh = (1 - shmat[:, :, idx]) * vbshvegshmat[:, :, idx] + temp_sh = temp_vbsh == 1 + azimuth_difference = torch.abs(azimuth - patch_azimuth[idx]) + + if (azimuth_difference > 90) and (azimuth_difference < 270): + sunlit_patches, shaded_patches = shaded_or_sunlit( + altitude, + azimuth, + patch_altitude[idx], + patch_azimuth[idx], + asvf, + ) + Kref_sun += ( + sunlit_surface + * sunlit_patches + * temp_sh + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + ) + Kref_sh += ( + shaded_surface + * shaded_patches + * temp_sh + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + ) + + if (patch_azimuth[idx] > 360) or ( + patch_azimuth[idx] < 180 + ): + Kref_sun_e += ( + sunlit_surface + * sunlit_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (90 - patch_azimuth[idx] + t) * deg2rad + ) + ) + Kref_sh_e += ( + shaded_surface + * shaded_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (90 - patch_azimuth[idx] + t) * deg2rad + ) + ) + if (patch_azimuth[idx] > 90) and ( + patch_azimuth[idx] < 270 + ): + Kref_sun_s += ( + sunlit_surface + * sunlit_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (180 - patch_azimuth[idx] + t) * deg2rad + ) + ) + Kref_sh_s += ( + shaded_surface + * shaded_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (180 - patch_azimuth[idx] + t) * deg2rad + ) + ) + if (patch_azimuth[idx] > 180) and ( + patch_azimuth[idx] < 360 + ): + Kref_sun_w += ( + sunlit_surface + * sunlit_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (270 - patch_azimuth[idx] + t) * deg2rad + ) + ) + Kref_sh_w += ( + shaded_surface + * shaded_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (270 - patch_azimuth[idx] + t) * deg2rad + ) + ) + if (patch_azimuth[idx] > 270) or (patch_azimuth[idx] < 90): + Kref_sun_n += ( + sunlit_surface + * sunlit_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos((0 - patch_azimuth[idx] + t) * deg2rad) + ) + Kref_sh_n += ( + shaded_surface + * shaded_patches + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos((0 - patch_azimuth[idx] + t) * deg2rad) + ) + else: + Kref_sh += ( + shaded_surface + * temp_sh + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + ) + + if (patch_azimuth[idx] > 360) or ( + patch_azimuth[idx] < 180 + ): + Kref_sh_e += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (90 - patch_azimuth[idx] + t) * deg2rad + ) + ) + if (patch_azimuth[idx] > 90) and ( + patch_azimuth[idx] < 270 + ): + Kref_sh_s += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (180 - patch_azimuth[idx] + t) * deg2rad + ) + ) + if (patch_azimuth[idx] > 180) and ( + patch_azimuth[idx] < 360 + ): + Kref_sh_w += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos( + (270 - patch_azimuth[idx] + t) * deg2rad + ) + ) + if (patch_azimuth[idx] > 270) or (patch_azimuth[idx] < 90): + Kref_sh_n += ( + shaded_surface + * steradian[idx] + * torch.cos(patch_altitude[idx] * deg2rad) + * temp_sh + * torch.cos((0 - patch_azimuth[idx] + t) * deg2rad) + ) + + Keast = ( + KeastI + + diffRadE + + Kref_sun_e + + Kref_sh_e + + Kref_veg_e + + KupE * 0.5 + ) + Kwest = ( + KwestI + + diffRadW + + Kref_sun_w + + Kref_sh_w + + Kref_veg_w + + KupW * 0.5 + ) + Knorth = ( + KnorthI + + diffRadN + + Kref_sun_n + + Kref_sh_n + + Kref_veg_n + + KupN * 0.5 + ) + Ksouth = ( + KsouthI + + diffRadS + + Kref_sun_s + + Kref_sh_s + + Kref_veg_s + + KupS * 0.5 + ) + + else: + KeastDG = ( + radD * (1 - svfviktbuvegE) + + albedo * (svfviktbuvegE * (radG * (1 - F_sh) + radD * F_sh)) + + KupE + ) * 0.5 + Keast = KeastI + KeastDG + + KsouthDG = ( + radD * (1 - svfviktbuvegS) + + albedo * (svfviktbuvegS * (radG * (1 - F_sh) + radD * F_sh)) + + KupS + ) * 0.5 + Ksouth = KsouthI + KsouthDG + + KwestDG = ( + radD * (1 - svfviktbuvegW) + + albedo * (svfviktbuvegW * (radG * (1 - F_sh) + radD * F_sh)) + + KupW + ) * 0.5 + Kwest = KwestI + KwestDG + + KnorthDG = ( + radD * (1 - svfviktbuvegN) + + albedo * (svfviktbuvegN * (radG * (1 - F_sh) + radD * F_sh)) + + KupN + ) * 0.5 + Knorth = KnorthI + KnorthDG + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return Keast, Ksouth, Kwest, Knorth, KsideI, KsideD, Kside diff --git a/functions/SOLWEIGpython/Kup_veg_2015a_torch.py b/functions/SOLWEIGpython/Kup_veg_2015a_torch.py new file mode 100644 index 0000000..9bd2af4 --- /dev/null +++ b/functions/SOLWEIGpython/Kup_veg_2015a_torch.py @@ -0,0 +1,52 @@ +try: + import torch +except: + pass + + +def Kup_veg_2015a( + radI, + radD, + radG, + altitude, + svfbuveg, + albedo_b, + F_sh, + gvfalb, + gvfalbE, + gvfalbS, + gvfalbW, + gvfalbN, + gvfalbnosh, + gvfalbnoshE, + gvfalbnoshS, + gvfalbnoshW, + gvfalbnoshN, +): + + Kup = (gvfalb * radI * torch.sin(altitude * (torch.pi / 180.0))) + ( + radD * svfbuveg + + albedo_b * (1 - svfbuveg) * (radG * (1 - F_sh) + radD * F_sh) + ) * gvfalbnosh + + KupE = (gvfalbE * radI * torch.sin(altitude * (torch.pi / 180.0))) + ( + radD * svfbuveg + + albedo_b * (1 - svfbuveg) * (radG * (1 - F_sh) + radD * F_sh) + ) * gvfalbnoshE + + KupS = (gvfalbS * radI * torch.sin(altitude * (torch.pi / 180.0))) + ( + radD * svfbuveg + + albedo_b * (1 - svfbuveg) * (radG * (1 - F_sh) + radD * F_sh) + ) * gvfalbnoshS + + KupW = (gvfalbW * radI * torch.sin(altitude * (torch.pi / 180.0))) + ( + radD * svfbuveg + + albedo_b * (1 - svfbuveg) * (radG * (1 - F_sh) + radD * F_sh) + ) * gvfalbnoshW + + KupN = (gvfalbN * radI * torch.sin(altitude * (torch.pi / 180.0))) + ( + radD * svfbuveg + + albedo_b * (1 - svfbuveg) * (radG * (1 - F_sh) + radD * F_sh) + ) * gvfalbnoshN + + return Kup, KupE, KupS, KupW, KupN diff --git a/functions/SOLWEIGpython/Kvikt_veg.py b/functions/SOLWEIGpython/Kvikt_veg.py index adc6d36..fecdbac 100644 --- a/functions/SOLWEIGpython/Kvikt_veg.py +++ b/functions/SOLWEIGpython/Kvikt_veg.py @@ -1,19 +1,19 @@ -def Kvikt_veg(svf, svfveg, vikttot): +def Kvikt_veg(input_svf, svfveg, vikttot): # Least viktwall = ( vikttot - ( - 63.227 * svf**6 - - 161.51 * svf**5 - + 156.91 * svf**4 - - 70.424 * svf**3 - + 16.773 * svf**2 - - 0.4863 * svf + 63.227 * input_svf**6 + - 161.51 * input_svf**5 + + 156.91 * input_svf**4 + - 70.424 * input_svf**3 + + 16.773 * input_svf**2 + - 0.4863 * input_svf ) ) / vikttot - svfvegbu = svfveg + svf - 1 # Vegetation plus buildings + svfvegbu = svfveg + input_svf - 1 # Vegetation plus buildings viktveg = ( vikttot - ( diff --git a/functions/SOLWEIGpython/Lcyl_v2022a.py b/functions/SOLWEIGpython/Lcyl_v2022a.py index 44c7bc2..7a8e9eb 100644 --- a/functions/SOLWEIGpython/Lcyl_v2022a.py +++ b/functions/SOLWEIGpython/Lcyl_v2022a.py @@ -3,8 +3,8 @@ from . import emissivity_models from . import patch_characteristics -""" This function combines the method to divide the sky vault into patches (Tregenza (1987) and Robinson & Stone (2004)) - and the approach by Unsworth & Monteith or Martin & Berdahl (1984) or Bliss (1961) to calculate emissivities of the +""" This function combines the method to divide the sky vault into patches (Tregenza (1987) and Robinson & Stone (2004)) + and the approach by Unsworth & Monteith or Martin & Berdahl (1984) or Bliss (1961) to calculate emissivities of the different parts of the sky vault. """ @@ -94,12 +94,10 @@ def Lcyl_v2022a( # Anisotropic sky if anisotropic_sky: temp_emissivity = esky_band[skyalt == altitude] - # Isotropic sky but with patches (need to switch anisotropic_sky to - # False) + # Isotropic sky but with patches (need to switch anisotropic_sky to False) else: temp_emissivity = esky - # Estimate longwave radiation on a horizontal surface (Ldown), vertical - # surface (Lside) and perpendicular (Lnormal) + # Estimate longwave radiation on a horizontal surface (Ldown), vertical surface (Lside) and perpendicular (Lnormal) Ldown[patch_altitude == altitude] = ( ((temp_emissivity * SBC * ((Ta + 273.15) ** 4)) / np.pi) * steradian[patch_altitude == altitude] @@ -122,8 +120,7 @@ def Lcyl_v2022a( Lsky_down[:, 2] = Ldown Lsky_side[:, 2] = Lside - # Estimate longwave radiation in each patch based on patch - # characteristics, i.e. sky, vegetation or building (shaded or sunlit) + # Estimate longwave radiation in each patch based on patch characteristics, i.e. sky, vegetation or building (shaded or sunlit) ( Ldown, Lside, @@ -158,13 +155,5 @@ def Lcyl_v2022a( current_step, ) - # print('Lside_sky old = ' + str(Lside_sky.max())) - # print('Lside_veg old = ' + str(Lside_veg.max())) - # print('Lside_sh old = ' + str(Lside_sh.max())) - # print('Lside_sun old = ' + str(Lside_sun.max())) - # print('Lside_ref old = ' + str(Lside_ref.max())) - return Ldown, Lside, Least_, Lwest_, Lnorth_, Lsouth_ - # return Ldown, Lside, Lside_sky, Lside_veg, Lside_sh, Lside_sun, - # Lside_ref, Lsky_normal, Lsky_down, Lsky_side, Least_, Lwest_, Lnorth_, - # Lsouth_ + # return Ldown, Lside, Lside_sky, Lside_veg, Lside_sh, Lside_sun, Lside_ref, Lsky_normal, Lsky_down, Lsky_side, Least_, Lwest_, Lnorth_, Lsouth_ diff --git a/functions/SOLWEIGpython/Lside_veg_torch.py b/functions/SOLWEIGpython/Lside_veg_torch.py new file mode 100644 index 0000000..d32834e --- /dev/null +++ b/functions/SOLWEIGpython/Lside_veg_torch.py @@ -0,0 +1,532 @@ +from __future__ import absolute_import +from .Lvikt_veg_torch import Lvikt_veg + +try: + import torch + +except: + pass + + +def Lside_veg_v2022a( + svfS, + svfW, + svfN, + svfE, + svfEveg, + svfSveg, + svfWveg, + svfNveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + azimuth, + altitude, + Ta, + Tw, + SBC, + ewall, + Ldown, + esky, + t, + F_sh, + CI, + LupE, + LupS, + LupW, + LupN, + anisotropic_longwave, +): + + # This m-file is the current one that estimates L from the four cardinal points 20100414 + device = ( + Ldown.device + if isinstance(Ldown, torch.Tensor) + else ( + Ta.device + if isinstance(Ta, torch.Tensor) + else torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + ) + ) + + # Convert all SVF inputs to the same device + svfS = torch.as_tensor(svfS, device=device) + svfW = torch.as_tensor(svfW, device=device) + svfN = torch.as_tensor(svfN, device=device) + svfE = torch.as_tensor(svfE, device=device) + svfEveg = torch.as_tensor(svfEveg, device=device) + svfSveg = torch.as_tensor(svfSveg, device=device) + svfWveg = torch.as_tensor(svfWveg, device=device) + svfNveg = torch.as_tensor(svfNveg, device=device) + svfEaveg = torch.as_tensor(svfEaveg, device=device) + svfSaveg = torch.as_tensor(svfSaveg, device=device) + svfWaveg = torch.as_tensor(svfWaveg, device=device) + svfNaveg = torch.as_tensor(svfNaveg, device=device) + + # Building height angle from svf + svfalfaE = torch.arcsin(torch.exp((torch.log(1 - svfE)) / 2)) + svfalfaS = torch.arcsin(torch.exp((torch.log(1 - svfS)) / 2)) + svfalfaW = torch.arcsin(torch.exp((torch.log(1 - svfW)) / 2)) + svfalfaN = torch.arcsin(torch.exp((torch.log(1 - svfN)) / 2)) + + vikttot = 4.4897 + aziW = azimuth + t + aziN = azimuth - 90 + t + aziE = azimuth - 180 + t + aziS = azimuth - 270 + t + + F_sh = 2 * F_sh - 1 # (cylindric_wedge scaled 0-1) + + c = 1 - CI + Lsky_allsky = esky * SBC * ((Ta + 273.15) ** 4) * (1 - c) + c * SBC * ( + (Ta + 273.15) ** 4 + ) + + ## Least + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfE, svfEveg, svfEaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaE) + betaB = torch.arctan(torch.tan((svfalfaE) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaE)*(1+F_sh)) #TODO This should be considered in future versions + if (azimuth > (180 - t)) and (azimuth <= (360 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziE * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + # Longwave from ground (see Lcyl_v2022a for remaining fluxes) + if anisotropic_longwave == 1: + Lground = LupE * 0.5 + Least = Lground + else: + Lsky = ((svfE + svfEveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupE * 0.5 + Lrefl = (Ldown + LupE) * (viktrefl) * (1 - ewall) * 0.5 + Least = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl + + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky + + ## Lsouth + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfS, svfSveg, svfSaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaS) + betaB = torch.arctan(torch.tan((svfalfaS) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaS)*(1+F_sh)) + if (azimuth <= (90 - t)) or (azimuth > (270 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziS * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + # Longwave from ground (see Lcyl_v2022a for remaining fluxes) + if anisotropic_longwave == 1: + Lground = LupS * 0.5 + Lsouth = Lground + else: + Lsky = ((svfS + svfSveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupS * 0.5 + Lrefl = (Ldown + LupS) * (viktrefl) * (1 - ewall) * 0.5 + Lsouth = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl + + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky + + ## Lwest + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfW, svfWveg, svfWaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaW) + betaB = torch.arctan(torch.tan((svfalfaW) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaW)*(1+F_sh)) + if (azimuth > (360 - t)) or (azimuth <= (180 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziW * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + # Longwave from ground (see Lcyl_v2022a for remaining fluxes) + if anisotropic_longwave == 1: + Lground = LupW * 0.5 + Lwest = Lground + else: + Lsky = ((svfW + svfWveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupW * 0.5 + Lrefl = (Ldown + LupW) * (viktrefl) * (1 - ewall) * 0.5 + Lwest = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl + + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky + + ## Lnorth + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfN, svfNveg, svfNaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaN) + betaB = torch.arctan(torch.tan((svfalfaN) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaN)*(1+F_sh)) + if (azimuth > (90 - t)) and (azimuth <= (270 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziN * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + # Longwave from ground (see Lcyl_v2022a for remaining fluxes) + if anisotropic_longwave == 1: + Lground = LupN * 0.5 + Lnorth = Lground + else: + Lsky = ((svfN + svfNveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupN * 0.5 + Lrefl = (Ldown + LupN) * (viktrefl) * (1 - ewall) * 0.5 + Lnorth = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return Least, Lsouth, Lwest, Lnorth + + +def Lside_veg_v2026( + svfS, + svfW, + svfN, + svfE, + svfEveg, + svfSveg, + svfWveg, + svfNveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + azimuth, + altitude, + Ta, + Tw, + SBC, + ewall, + Ldown, + esky, + t, + F_sh, + CI, + anisotropic_longwave, +): + + # This m-file is the current one that estimates L from the four cardinal points 20100414 + device = ( + Ldown.device + if isinstance(Ldown, torch.Tensor) + else ( + Ta.device + if isinstance(Ta, torch.Tensor) + else torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + ) + ) + + # Convert all SVF inputs to the same device + svfS = torch.as_tensor(svfS, device=device) + svfW = torch.as_tensor(svfW, device=device) + svfN = torch.as_tensor(svfN, device=device) + svfE = torch.as_tensor(svfE, device=device) + svfEveg = torch.as_tensor(svfEveg, device=device) + svfSveg = torch.as_tensor(svfSveg, device=device) + svfWveg = torch.as_tensor(svfWveg, device=device) + svfNveg = torch.as_tensor(svfNveg, device=device) + svfEaveg = torch.as_tensor(svfEaveg, device=device) + svfSaveg = torch.as_tensor(svfSaveg, device=device) + svfWaveg = torch.as_tensor(svfWaveg, device=device) + svfNaveg = torch.as_tensor(svfNaveg, device=device) + + # Building height angle from svf + svfalfaE = torch.arcsin(torch.exp((torch.log(1 - svfE)) / 2)) + svfalfaS = torch.arcsin(torch.exp((torch.log(1 - svfS)) / 2)) + svfalfaW = torch.arcsin(torch.exp((torch.log(1 - svfW)) / 2)) + svfalfaN = torch.arcsin(torch.exp((torch.log(1 - svfN)) / 2)) + + vikttot = 4.4897 + aziW = azimuth + t + aziN = azimuth - 90 + t + aziE = azimuth - 180 + t + aziS = azimuth - 270 + t + + F_sh = 2 * F_sh - 1 # (cylindric_wedge scaled 0-1) + + c = 1 - CI + Lsky_allsky = esky * SBC * ((Ta + 273.15) ** 4) * (1 - c) + c * SBC * ( + (Ta + 273.15) ** 4 + ) + + ## Least + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfE, svfEveg, svfEaveg, vikttot + ) + + if anisotropic_longwave == 1: + Least = torch.zeros_like(Ldown, device=device) + Lnorth = torch.zeros_like(Ldown, device=device) + Lwest = torch.zeros_like(Ldown, device=device) + Lsouth = torch.zeros_like(Ldown, device=device) + + return Least, Lsouth, Lwest, Lnorth + else: + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaE) + betaB = torch.arctan(torch.tan((svfalfaE) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaE)*(1+F_sh)) #TODO This should be considered in future versions + if (azimuth > (180 - t)) and (azimuth <= (360 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziE * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfE + svfEveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lrefl = Ldown * (viktrefl) * (1 - ewall) * 0.5 + Least = Lsky + Lwallsun + Lwallsh + Lveg + Lrefl + + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky + + ## Lsouth + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfS, svfSveg, svfSaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaS) + betaB = torch.arctan(torch.tan((svfalfaS) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaS)*(1+F_sh)) + if (azimuth <= (90 - t)) or (azimuth > (270 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziS * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfS + svfSveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lrefl = Ldown * (viktrefl) * (1 - ewall) * 0.5 + Lsouth = Lsky + Lwallsun + Lwallsh + Lveg + Lrefl + + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky + + ## Lwest + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfW, svfWveg, svfWaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaW) + betaB = torch.arctan(torch.tan((svfalfaW) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaW)*(1+F_sh)) + if (azimuth > (360 - t)) or (azimuth <= (180 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziW * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfW + svfWveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lrefl = Ldown * (viktrefl) * (1 - ewall) * 0.5 + Lwest = Lsky + Lwallsun + Lwallsh + Lveg + Lrefl + + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky + + ## Lnorth + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfN, svfNveg, svfNaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = torch.arctan(svfalfaN) + betaB = torch.arctan(torch.tan((svfalfaN) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB + # betasun = torch.arctan(0.5*torch.tan(svfalfaN)*(1+F_sh)) + if (azimuth > (90 - t)) and (azimuth <= (270 - t)): + Lwallsun = ( + SBC + * ewall + * ( + (Ta + 273.15 + Tw * torch.sin(aziN * (torch.pi / 180))) + ** 4 + ) + * viktwall + * (1 - F_sh) + * torch.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) + else: + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfN + svfNveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lrefl = Ldown * (viktrefl) * (1 - ewall) * 0.5 + Lnorth = Lsky + Lwallsun + Lwallsh + Lveg + Lrefl + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return Least, Lsouth, Lwest, Lnorth diff --git a/functions/SOLWEIGpython/Lside_veg_v2015a.py b/functions/SOLWEIGpython/Lside_veg_v2015a.py index 3aecfb3..a9604b1 100644 --- a/functions/SOLWEIGpython/Lside_veg_v2015a.py +++ b/functions/SOLWEIGpython/Lside_veg_v2015a.py @@ -2,133 +2,208 @@ import numpy as np from .Lvikt_veg import Lvikt_veg -def Lside_veg_v2015a(svfS,svfW,svfN,svfE,svfEveg,svfSveg,svfWveg,svfNveg,svfEaveg,svfSaveg,svfWaveg,svfNaveg,azimuth,altitude,Ta,Tw,SBC,ewall,Ldown,esky,t,F_sh,CI,LupE,LupS,LupW,LupN): + +def Lside_veg_v2015a( + svfS, + svfW, + svfN, + svfE, + svfEveg, + svfSveg, + svfWveg, + svfNveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + azimuth, + altitude, + Ta, + Tw, + SBC, + ewall, + Ldown, + esky, + t, + F_sh, + CI, + LupE, + LupS, + LupW, + LupN, +): # This m-file is the current one that estimates L from the four cardinal points 20100414 - - #Building height angle from svf - svfalfaE=np.arcsin(np.exp((np.log(1-svfE))/2)) - svfalfaS=np.arcsin(np.exp((np.log(1-svfS))/2)) - svfalfaW=np.arcsin(np.exp((np.log(1-svfW))/2)) - svfalfaN=np.arcsin(np.exp((np.log(1-svfN))/2)) - - vikttot=4.4897 - aziW=azimuth+t - aziN=azimuth-90+t - aziE=azimuth-180+t - aziS=azimuth-270+t - - F_sh = 2*F_sh-1 #(cylindric_wedge scaled 0-1) - - c=1-CI - Lsky_allsky = esky*SBC*((Ta+273.15)**4)*(1-c)+c*SBC*((Ta+273.15)**4) - + + # Building height angle from svf + svfalfaE = np.arcsin(np.exp((np.log(1 - svfE)) / 2)) + svfalfaS = np.arcsin(np.exp((np.log(1 - svfS)) / 2)) + svfalfaW = np.arcsin(np.exp((np.log(1 - svfW)) / 2)) + svfalfaN = np.arcsin(np.exp((np.log(1 - svfN)) / 2)) + + vikttot = 4.4897 + aziW = azimuth + t + aziN = azimuth - 90 + t + aziE = azimuth - 180 + t + aziS = azimuth - 270 + t + + F_sh = 2 * F_sh - 1 # (cylindric_wedge scaled 0-1) + + c = 1 - CI + Lsky_allsky = esky * SBC * ((Ta + 273.15) ** 4) * (1 - c) + c * SBC * ( + (Ta + 273.15) ** 4 + ) + ## Least - [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg(svfE, svfEveg, svfEaveg, vikttot) - + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfE, svfEveg, svfEaveg, vikttot + ) + if altitude > 0: # daytime - alfaB=np.arctan(svfalfaE) - betaB=np.arctan(np.tan((svfalfaE)*F_sh)) - betasun=((alfaB-betaB)/2)+betaB + alfaB = np.arctan(svfalfaE) + betaB = np.arctan(np.tan((svfalfaE) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB # betasun = np.arctan(0.5*np.tan(svfalfaE)*(1+F_sh)) #TODO This should be considered in future versions - if (azimuth > (180-t)) and (azimuth <= (360-t)): - Lwallsun=SBC*ewall*((Ta+273.15+Tw*np.sin(aziE*(np.pi/180)))**4)*\ - viktwall*(1-F_sh)*np.cos(betasun)*0.5 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*F_sh*0.5 + if (azimuth > (180 - t)) and (azimuth <= (360 - t)): + Lwallsun = ( + SBC + * ewall + * ((Ta + 273.15 + Tw * np.sin(aziE * (np.pi / 180))) ** 4) + * viktwall + * (1 - F_sh) + * np.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) else: - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - else: #nighttime - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - - Lsky=((svfE+svfEveg-1)*Lsky_allsky)*viktsky*0.5 - Lveg=SBC*ewall*((Ta+273.15)**4)*viktveg*0.5 - Lground=LupE*0.5 - Lrefl=(Ldown+LupE)*(viktrefl)*(1-ewall)*0.5 - Least=Lsky+Lwallsun+Lwallsh+Lveg+Lground+Lrefl - + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfE + svfEveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupE * 0.5 + Lrefl = (Ldown + LupE) * (viktrefl) * (1 - ewall) * 0.5 + Least = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky - + ## Lsouth - [viktveg,viktwall,viktsky,viktrefl]=Lvikt_veg(svfS,svfSveg,svfSaveg,vikttot) - - if altitude>0: # daytime - alfaB=np.arctan(svfalfaS) - betaB=np.arctan(np.tan((svfalfaS)*F_sh)) - betasun=((alfaB-betaB)/2)+betaB + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfS, svfSveg, svfSaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = np.arctan(svfalfaS) + betaB = np.arctan(np.tan((svfalfaS) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB # betasun = np.arctan(0.5*np.tan(svfalfaS)*(1+F_sh)) - if (azimuth <= (90-t)) or (azimuth > (270-t)): - Lwallsun=SBC*ewall*((Ta+273.15+Tw*np.sin(aziS*(np.pi/180)))**4)*\ - viktwall*(1-F_sh)*np.cos(betasun)*0.5 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*F_sh*0.5 + if (azimuth <= (90 - t)) or (azimuth > (270 - t)): + Lwallsun = ( + SBC + * ewall + * ((Ta + 273.15 + Tw * np.sin(aziS * (np.pi / 180))) ** 4) + * viktwall + * (1 - F_sh) + * np.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) else: - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - else: #nighttime - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - - Lsky=((svfS+svfSveg-1)*Lsky_allsky)*viktsky*0.5 - Lveg=SBC*ewall*((Ta+273.15)**4)*viktveg*0.5 - Lground=LupS*0.5 - Lrefl=(Ldown+LupS)*(viktrefl)*(1-ewall)*0.5 - Lsouth=Lsky+Lwallsun+Lwallsh+Lveg+Lground+Lrefl - + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfS + svfSveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupS * 0.5 + Lrefl = (Ldown + LupS) * (viktrefl) * (1 - ewall) * 0.5 + Lsouth = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky - + ## Lwest - [viktveg,viktwall,viktsky,viktrefl]=Lvikt_veg(svfW,svfWveg,svfWaveg,vikttot) - - if altitude>0: # daytime - alfaB=np.arctan(svfalfaW) - betaB=np.arctan(np.tan((svfalfaW)*F_sh)) - betasun=((alfaB-betaB)/2)+betaB + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfW, svfWveg, svfWaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = np.arctan(svfalfaW) + betaB = np.arctan(np.tan((svfalfaW) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB # betasun = np.arctan(0.5*np.tan(svfalfaW)*(1+F_sh)) - if (azimuth > (360-t)) or (azimuth <= (180-t)): - Lwallsun=SBC*ewall*((Ta+273.15+Tw*np.sin(aziW*(np.pi/180)))**4)*\ - viktwall*(1-F_sh)*np.cos(betasun)*0.5 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*F_sh*0.5 + if (azimuth > (360 - t)) or (azimuth <= (180 - t)): + Lwallsun = ( + SBC + * ewall + * ((Ta + 273.15 + Tw * np.sin(aziW * (np.pi / 180))) ** 4) + * viktwall + * (1 - F_sh) + * np.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) else: - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - else: #nighttime - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - - Lsky=((svfW+svfWveg-1)*Lsky_allsky)*viktsky*0.5 - Lveg=SBC*ewall*((Ta+273.15)**4)*viktveg*0.5 - Lground=LupW*0.5 - Lrefl=(Ldown+LupW)*(viktrefl)*(1-ewall)*0.5 - Lwest=Lsky+Lwallsun+Lwallsh+Lveg+Lground+Lrefl - + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfW + svfWveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupW * 0.5 + Lrefl = (Ldown + LupW) * (viktrefl) * (1 - ewall) * 0.5 + Lwest = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl + # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky - + ## Lnorth - [viktveg,viktwall,viktsky,viktrefl]=Lvikt_veg(svfN,svfNveg,svfNaveg,vikttot) - - if altitude>0: # daytime - alfaB=np.arctan(svfalfaN) - betaB=np.arctan(np.tan((svfalfaN)*F_sh)) - betasun=((alfaB-betaB)/2)+betaB + [viktveg, viktwall, viktsky, viktrefl] = Lvikt_veg( + svfN, svfNveg, svfNaveg, vikttot + ) + + if altitude > 0: # daytime + alfaB = np.arctan(svfalfaN) + betaB = np.arctan(np.tan((svfalfaN) * F_sh)) + betasun = ((alfaB - betaB) / 2) + betaB # betasun = np.arctan(0.5*np.tan(svfalfaN)*(1+F_sh)) - if (azimuth > (90-t)) and (azimuth <= (270-t)): - Lwallsun=SBC*ewall*((Ta+273.15+Tw*np.sin(aziN*(np.pi/180)))**4)*\ - viktwall*(1-F_sh)*np.cos(betasun)*0.5 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*F_sh*0.5 + if (azimuth > (90 - t)) and (azimuth <= (270 - t)): + Lwallsun = ( + SBC + * ewall + * ((Ta + 273.15 + Tw * np.sin(aziN * (np.pi / 180))) ** 4) + * viktwall + * (1 - F_sh) + * np.cos(betasun) + * 0.5 + ) + Lwallsh = ( + SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * F_sh * 0.5 + ) else: - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - else: #nighttime - Lwallsun=0 - Lwallsh=SBC*ewall*((Ta+273.15)**4)*viktwall*0.5 - - Lsky=((svfN+svfNveg-1)*Lsky_allsky)*viktsky*0.5 - Lveg=SBC*ewall*((Ta+273.15)**4)*viktveg*0.5 - Lground=LupN*0.5 - Lrefl=(Ldown+LupN)*(viktrefl)*(1-ewall)*0.5 - Lnorth=Lsky+Lwallsun+Lwallsh+Lveg+Lground+Lrefl + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + else: # nighttime + Lwallsun = 0 + Lwallsh = SBC * ewall * ((Ta + 273.15) ** 4) * viktwall * 0.5 + + Lsky = ((svfN + svfNveg - 1) * Lsky_allsky) * viktsky * 0.5 + Lveg = SBC * ewall * ((Ta + 273.15) ** 4) * viktveg * 0.5 + Lground = LupN * 0.5 + Lrefl = (Ldown + LupN) * (viktrefl) * (1 - ewall) * 0.5 + Lnorth = Lsky + Lwallsun + Lwallsh + Lveg + Lground + Lrefl # clear alfaB betaB betasun Lsky Lwallsh Lwallsun Lveg Lground Lrefl viktveg viktwall viktsky - - return Least,Lsouth,Lwest,Lnorth \ No newline at end of file + + return Least, Lsouth, Lwest, Lnorth diff --git a/functions/SOLWEIGpython/Lvikt_veg_torch.py b/functions/SOLWEIGpython/Lvikt_veg_torch.py new file mode 100644 index 0000000..c7fb1d8 --- /dev/null +++ b/functions/SOLWEIGpython/Lvikt_veg_torch.py @@ -0,0 +1,95 @@ +try: + import torch + +except: + pass + + +def Lvikt_veg(svf, svfveg, svfaveg, vikttot): + device = None + if isinstance(svf, torch.Tensor): + device = svf.device + elif isinstance(svfveg, torch.Tensor): + device = svfveg.device + elif isinstance(svfaveg, torch.Tensor): + device = svfaveg.device + else: + torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + + svf = torch.as_tensor(svf, device=device) + svfveg = torch.as_tensor(svfveg, device=device) + svfaveg = torch.as_tensor(svfaveg, device=device) + + # Least + viktonlywall = ( + vikttot + - ( + 63.227 * svf**6 + - 161.51 * svf**5 + + 156.91 * svf**4 + - 70.424 * svf**3 + + 16.773 * svf**2 + - 0.4863 * svf + ) + ) / vikttot + + viktaveg = ( + vikttot + - ( + 63.227 * svfaveg**6 + - 161.51 * svfaveg**5 + + 156.91 * svfaveg**4 + - 70.424 * svfaveg**3 + + 16.773 * svfaveg**2 + - 0.4863 * svfaveg + ) + ) / vikttot + + viktwall = viktonlywall - viktaveg + + svfvegbu = svfveg + svf - 1 # Vegetation plus buildings + viktsky = ( + 63.227 * svfvegbu**6 + - 161.51 * svfvegbu**5 + + 156.91 * svfvegbu**4 + - 70.424 * svfvegbu**3 + + 16.773 * svfvegbu**2 + - 0.4863 * svfvegbu + ) / vikttot + viktrefl = ( + vikttot + - ( + 63.227 * svfvegbu**6 + - 161.51 * svfvegbu**5 + + 156.91 * svfvegbu**4 + - 70.424 * svfvegbu**3 + + 16.773 * svfvegbu**2 + - 0.4863 * svfvegbu + ) + ) / vikttot + viktveg = ( + vikttot + - ( + 63.227 * svfvegbu**6 + - 161.51 * svfvegbu**5 + + 156.91 * svfvegbu**4 + - 70.424 * svfvegbu**3 + + 16.773 * svfvegbu**2 + - 0.4863 * svfvegbu + ) + ) / vikttot + viktveg = viktveg - viktwall + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return viktveg, viktwall, viktsky, viktrefl diff --git a/functions/SOLWEIGpython/PET_calculations.py b/functions/SOLWEIGpython/PET_calculations.py index 73dd3c1..773b2b5 100644 --- a/functions/SOLWEIGpython/PET_calculations.py +++ b/functions/SOLWEIGpython/PET_calculations.py @@ -40,8 +40,7 @@ def calculate_PET_grid(Ta, RH, Tmrt, va, pet, feedback): pet_index = np.zeros_like(Tmrt) total = 100.0 / (int(pet_index.shape[0] * pet_index.shape[1])) index = 0 - # print(Tmrt.shape) - # print(va.shape) + for y in range(pet_index.shape[0]): if feedback.isCanceled(): feedback.setProgressText("Calculation cancelled") diff --git a/functions/SOLWEIGpython/Solweig_2026a_calc_forprocessing.py b/functions/SOLWEIGpython/Solweig_2026a_calc_forprocessing.py index f30d7e3..61b017c 100644 --- a/functions/SOLWEIGpython/Solweig_2026a_calc_forprocessing.py +++ b/functions/SOLWEIGpython/Solweig_2026a_calc_forprocessing.py @@ -5,7 +5,6 @@ from __future__ import absolute_import import numpy as np -import matplotlib.pyplot as plt from .daylen import daylen from ...util.SEBESOLWEIGCommonFiles.clearnessindex_2013b import ( clearnessindex_2013b, @@ -22,22 +21,15 @@ from .TsWaveDelay_2015a import TsWaveDelay_2015a from .Kup_veg_2015a import Kup_veg_2015a -# from .Lside_veg_v2015a import Lside_veg_v2015a -# from .Kside_veg_v2019a import Kside_veg_v2019a from .Kside_veg_v2022a import Kside_veg_v2022a from ...util.SEBESOLWEIGCommonFiles.Perez_v3 import Perez_v3 from ...util.SEBESOLWEIGCommonFiles.create_patches import create_patches # Anisotropic longwave -from .Lcyl_v2022a import Lcyl_v2022a from .Lside_veg import Lside_veg_v2022a, Lside_veg_v2026 from .anisotropic_sky import anisotropic_sky as ani_sky from .patch_radiation import patch_steradians from copy import deepcopy -import time - -# Wall surface temperature scheme -from .wall_surface_temperature import wall_surface_temperature # Ground surface temperature from .ground_surface import ( diff --git a/functions/SOLWEIGpython/Solweig_2026a_calc_forprocessing_torch.py b/functions/SOLWEIGpython/Solweig_2026a_calc_forprocessing_torch.py new file mode 100644 index 0000000..a7842f2 --- /dev/null +++ b/functions/SOLWEIGpython/Solweig_2026a_calc_forprocessing_torch.py @@ -0,0 +1,1000 @@ +""" +@author Fredrik Lindberg, University of Gothenburg +""" + +from __future__ import absolute_import +from .daylen_torch import daylen +from ...util.SEBESOLWEIGCommonFiles.clearnessindex_2013b_torch import ( + clearnessindex_2013b, +) +from ...util.SEBESOLWEIGCommonFiles.diffusefraction_torch import ( + diffusefraction, +) +from ...util.SEBESOLWEIGCommonFiles.shadowingfunction_wallheight_13_torch import ( + shadowingfunction_wallheight_13, +) +from ...util.SEBESOLWEIGCommonFiles.shadowingfunction_wallheight_23_torch import ( + shadowingfunction_wallheight_23, +) +from .gvf_2018a import gvf_2018a +from .cylindric_wedge_torch import cylindric_wedge +from .TsWaveDelay_2015a import TsWaveDelay_2015a +from .Kup_veg_2015a_torch import Kup_veg_2015a + + +from .Kside_veg_v2022a_torch import Kside_veg_v2022a +from ...util.SEBESOLWEIGCommonFiles.Perez_v3_torch import Perez_v3 +from ...util.SEBESOLWEIGCommonFiles.create_patches_torch import create_patches + +# Anisotropic longwave +from .Lside_veg_torch import Lside_veg_v2022a, Lside_veg_v2026 +from .anisotropic_sky import anisotropic_sky as ani_sky +from .patch_radiation import patch_steradians +from copy import deepcopy + +try: + import torch + +except: + pass + +# Ground surface temperature +from .ground_surface_torch import ( + surfaceTemperature_calc, + outgoingLongwave_calc, +) + + +def Solweig_2026a_calc( + i, + dsm, + scale, + rows, + cols, + svf, + svfN, + svfW, + svfE, + svfS, + svfveg, + svfNveg, + svfEveg, + svfSveg, + svfWveg, + svfaveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + vegdem, + vegdem2, + albedo_b, + absK, + absL, + ewall, + Fside, + Fup, + Fcyl, + altitude, + azimuth, + zen, + jday, + usevegdem, + onlyglobal, + buildings, + location, + psi, + landcover, + lc_grid, + dectime, + altmax, + dirwalls, + walls, + cyl, + elvis, + Ta, + RH, + radG, + radD, + radI, + P, + amaxvalue, + bush, + Twater, + TgK, + Tstart, + alb_grid, + emis_grid, + TgK_wall, + Tstart_wall, + TmaxLST, + TmaxLST_wall, + first, + second, + svfalfa, + svfbuveg, + firstdaytime, + timeadd, + timestepdec, + Tgmap1, + Tgmap1E, + Tgmap1S, + Tgmap1W, + Tgmap1N, + CI, + diffsh, + shmat, + vegshmat, + vbshvegshmat, + anisotropic_sky, + asvf, + patch_option, + voxelMaps, + voxelTable, + ws, + wallScheme, + timeStep, + steradians, + walls_scheme, + dirwalls_scheme, + groundScheme, + outgoingLW, + Tg, + Rn, + Rn_past, + G, + Tm, + cap_grid, + diff_grid, + a1_grid, + a2_grid, + a3_grid, + shadow_past, +): + """ + This is the core function of the SOLWEIG model + 2016-Aug-28 + Fredrik Lindberg, fredrikl@gvc.gu.se + Goteborg Urban Climate Group + Gothenburg University + + :Input variables: + dsm = digital surface model + scale = height to pixel size (2m pixel gives scale = 0.5) + svf,svfN,svfW,svfE,svfS = SVFs for building and ground + svfveg,svfNveg,svfEveg,svfSveg,svfWveg = Veg SVFs blocking sky + svfaveg,svfEaveg,svfSaveg,svfWaveg,svfNaveg = Veg SVFs blocking buildings + vegdem = Vegetation canopy DSM + vegdem2 = Vegetation trunk zone DSM + albedo_b = building wall albedo + absK = human absorption coefficient for shortwave radiation + absL = human absorption coefficient for longwave radiation + ewall = Emissivity of building walls + Fside = The angular factors between a person and the surrounding surfaces + Fup = The angular factors between a person and the surrounding surfaces + Fcyl = The angular factors between a culidric person and the surrounding surfaces + altitude = Sun altitude (degree) + azimuth = Sun azimuth (degree) + zen = Sun zenith angle (radians) + jday = day of year + usevegdem = use vegetation scheme + onlyglobal = calculate dir and diff from global shortwave (Reindl et al. 1990) + buildings = Boolena grid to identify building pixels + location = geographic location + height = height of measurements point (center of gravity of human) + psi = 1 - Transmissivity of shortwave through vegetation + landcover = use landcover scheme !!!NEW IN 2015a!!! + lc_grid = grid with landcoverclasses + lc_class = table with landcover properties + dectime = decimal time + altmax = maximum sun altitude + dirwalls = aspect of walls + walls = one pixel row outside building footprint. height of building walls + cyl = consider man as cylinder instead of cude + elvis = dummy + Ta = air temp + RH + radG = global radiation + radD = diffuse + radI = direct + P = pressure + amaxvalue = max height of buildings + bush = grid representing bushes + Twater = temperature of water (daily) + TgK, Tstart, TgK_wall, Tstart_wall, TmaxLST,TmaxLST_wall, + alb_grid, emis_grid = albedo and emmissivity on ground + first, second = conneted to old Ts model (source area based on Smidt et al.) + svfalfa = SVF recalculated to angle + svfbuveg = complete SVF + firstdaytime, timeadd, timestepdec, Tgmap1, Tgmap1E, Tgmap1S, Tgmap1W, Tgmap1N, + CI = Clearness index + TgOut1 = old Ts model + diffsh, ani = Used in anisotrpic models (Wallenberg et al. 2019, 2022) + """ + + # # # Core program start # # # + # Instrument offset in degrees + t = 0.0 + + # Stefan Bolzmans Constant + SBC = 5.67051e-8 + + # Degrees to radians + deg2rad = torch.pi / 180 + device = ( + Tg.device + if isinstance(Tg, torch.Tensor) + else ( + Ta.device + if isinstance(Ta, torch.Tensor) + else torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + ) + ) + + # Find sunrise decimal hour - new from 2014a + _, _, _, SNUP = daylen(jday, location["latitude"]) + + # Vapor pressure in hPa + ea = 6.107 * 10 ** ((7.5 * Ta) / (237.3 + Ta)) * (RH / 100.0) + + # Determination of clear-sky emissivity from Prata (1996) + msteg = 46.5 * (ea / (Ta + 273.15)) + esky = ( + 1 - (1 + msteg) * torch.exp(-((1.2 + 3.0 * msteg) ** 0.5)) + ) + elvis # -0.04 old error from Jonsson et al.2006 + + if altitude > 0: # # # # # # DAYTIME # # # # # # + # Clearness Index on Earth's surface after Crawford and Dunchon (1999) with a correction + # factor for low sun elevations after Lindberg et al.(2008) + I0, CI, Kt, I0et, CIuncorr = clearnessindex_2013b( + zen, jday, Ta, RH / 100.0, radG, location, P + ) + if (CI > 1) or (CI == torch.inf): + CI = 1 + del I0et, CIuncorr + + # Estimation of radD and radI if not measured after Reindl et al.(1990) + if onlyglobal == 1: + I0, CI, Kt, I0et, CIuncorr = clearnessindex_2013b( + zen, jday, Ta, RH / 100.0, radG, location, P + ) + if (CI > 1) or (CI == torch.inf): + CI = 1 + del I0et, CIuncorr + + radI, radD = diffusefraction(radG, altitude, Kt, Ta, RH) + + # Diffuse Radiation + # Anisotropic Diffuse Radiation after Perez et al. 1993 + if anisotropic_sky == 1: + patchchoice = 1 + zenDeg = zen * (180 / torch.pi) + # Relative luminance + lv, pc_, pb_ = Perez_v3( + zenDeg, + azimuth, + radD, + radI, + jday, + patchchoice, + patch_option, + device, + ) + # Total relative luminance from sky, i.e. from each patch, into each cell + aniLum = torch.zeros((rows, cols), device=device) + for idx in range(lv.shape[0]): + aniLum += diffsh[:, :, idx] * lv[idx, 2] + + dRad = ( + aniLum * radD + ) # Total diffuse radiation from sky into each cell + del aniLum, pc_, pb_ + else: + dRad = radD * svfbuveg + patchchoice = 1 + lv = None + + # Shadow images + if usevegdem == 1: + vegsh, sh, _, wallsh, wallsun, wallshve, _, facesun, wallsh_ = ( + shadowingfunction_wallheight_23( + dsm, + vegdem, + vegdem2, + azimuth, + altitude, + scale, + amaxvalue, + bush, + walls, + dirwalls * torch.pi / 180.0, + walls_scheme, + dirwalls_scheme * torch.pi / 180.0, + ) + ) + shadow = sh - (1 - vegsh) * (1 - psi) + del sh, vegsh, wallsh, wallshve, facesun, wallsh_ + else: + sh, wallsh, wallsun, facesh, facesun, wallsh_ = ( + shadowingfunction_wallheight_13( + dsm, + azimuth, + altitude, + scale, + walls, + dirwalls * torch.pi / 180.0, + walls_scheme, + dirwalls_scheme * torch.pi / 180.0, + ) + ) + shadow = sh + del sh, wallsh, facesun, wallsh_ + + # Building height angle from svf + F_sh = cylindric_wedge( + zen, svfalfa, rows, cols + ) # Fraction shadow on building walls based on sun alt and svf + F_sh[torch.isnan(F_sh)] = 0.5 + + # New estimation of Tgwall with reduction for non-clear situation based on Reindl et al. 1990 + radI0, _ = diffusefraction(I0, altitude, 1.0, Ta, RH) + radG0 = radI0 * (torch.sin(altitude * deg2rad)) + _ + del radI0, _ + corr = ( + 0.1473 * torch.log(90 - (zen / torch.pi * 180)) + 0.3454 + ) # 20070329 correction of lat, Lindberg et al. 2008 + CI_TgG = (radG / radG0) + (1 - corr) + del radG0 + if (CI_TgG > 1) or (CI_TgG == torch.inf): + CI_TgG = 1 + + Tgampwall = TgK_wall * altmax + Tstart_wall + Tgwall = Tgampwall * torch.sin( + ( + ((dectime - torch.floor(dectime)) - SNUP / 24) + / (TmaxLST_wall / 24 - SNUP / 24) + ) + * torch.pi + / 2 + ) # 2015a, based on max sun altitude + if Tgwall < 0: # temporary for removing low Tg during morning 20130205 + Tgwall = 0 + Tgwall = Tgwall * CI_TgG + + # # # # Kdown # # # # + Kdown = ( + radI * shadow * torch.sin(altitude * (torch.pi / 180)) + + dRad + + albedo_b * (1 - svfbuveg) * (radG * (1 - F_sh) + radD * F_sh) + ) # *sin(altitude(i) * (pi / 180)) + + # # # # Ldown # # # # + Ldown = ( + (svf + svfveg - 1) * esky * SBC * ((Ta + 273.15) ** 4) + + (2 - svfveg - svfaveg) * ewall * SBC * ((Ta + 273.15) ** 4) + + (svfaveg - svf) * ewall * SBC * ((Ta + 273.15 + Tgwall) ** 4) + + (2 - svf - svfveg) + * (1 - ewall) + * esky + * SBC + * ((Ta + 273.15) ** 4) + ) # Jonsson et al.(2006) + # Ldown = Ldown - 25 # Shown by Jonsson et al.(2006) and Duarte et al.(2006) + if CI < 0.95: # non - clear conditions + c = 1 - CI + Ldown = Ldown * (1 - c) + c * ( + (svf + svfveg - 1) * SBC * ((Ta + 273.15) ** 4) + + (2 - svfveg - svfaveg) * ewall * SBC * ((Ta + 273.15) ** 4) + + (svfaveg - svf) * ewall * SBC * ((Ta + 273.15 + Tgwall) ** 4) + + (2 - svf - svfveg) * (1 - ewall) * SBC * ((Ta + 273.15) ** 4) + ) # NOT REALLY TESTED!!! BUT MORE CORRECT? + + # Surface temperature parameterization during daytime + if groundScheme == 1: + # calculate the ground surface temperature, and relevant heat fluxes + Tg, Rn, Rn_past, G = surfaceTemperature_calc( + Kdown, + Ldown, + Rn, + Rn_past, + G, + Tg, + Tm, + alb_grid, + emis_grid, + cap_grid, + diff_grid, + lc_grid, + a1_grid, + a2_grid, + a3_grid, + timeStep, + RH, + shadow, + shadow_past, + ) + + else: + # using max sun alt instead of dfm + Tgamp = TgK * altmax + Tstart # Fixed 2021 + Tgdiff = Tgamp * torch.sin( + ( + ((dectime - torch.floor(dectime)) - SNUP / 24) + / (TmaxLST / 24 - SNUP / 24) + ) + * torch.pi + / 2 + ) # 2015 a, based on max sun altitude + + Tgdiff = Tgdiff * CI_TgG # new estimation + + # For Tg output in POIs + TgTemp = Tgdiff * shadow + Ta + _, timeadd, Tg = TsWaveDelay_2015a( + TgTemp, firstdaytime, timeadd, timestepdec, Tg + ) # timeadd only here v2021a + del TgTemp, Tgdiff + + if landcover == 1: + Tg[Tg < 0] = ( + 0 # temporary for removing low Tg during morning 20130205 + ) + + # Calculate the outgoing longwave radiation + if outgoingLW == 1: + + # # # # Lup, daytime # # # # + ( + Lup, + gvfalb, + gvfalbnosh, + LupE, + gvfalbE, + gvfalbnoshE, + LupS, + gvfalbS, + gvfalbnoshS, + LupW, + gvfalbW, + gvfalbnoshW, + LupN, + gvfalbN, + gvfalbnoshN, + gvfLsideW, + gvfLsideS, + gvfLsideE, + gvfLsideN, + ) = outgoingLongwave_calc( + Tg, + Tgwall, + Ta, + Ldown, + emis_grid, + alb_grid, + buildings, + shadow, + wallsun, + walls, + rows, + cols, + 1 / scale, + ) + + else: + ### Ground View Factors + ( + gvfLup, + gvfalb, + gvfalbnosh, + gvfLupE, + gvfalbE, + gvfalbnoshE, + gvfLupS, + gvfalbS, + gvfalbnoshS, + gvfLupW, + gvfalbW, + gvfalbnoshW, + gvfLupN, + gvfalbN, + gvfalbnoshN, + gvfSum, + gvfNorm, + ) = gvf_2018a( + wallsun, + walls, + buildings, + scale, + shadow, + first, + second, + dirwalls, + Tg, + Tgwall, + Ta, + emis_grid, + ewall, + alb_grid, + SBC, + albedo_b, + rows, + cols, + Twater, + lc_grid, + landcover, + ) + + # # # # Lup, daytime # # # # + # Surface temperature wave delay - new as from 2014a + Lup, timeaddnotused, Tgmap1 = TsWaveDelay_2015a( + gvfLup, firstdaytime, timeadd, timestepdec, Tgmap1 + ) + LupE, timeaddnotused, Tgmap1E = TsWaveDelay_2015a( + gvfLupE, firstdaytime, timeadd, timestepdec, Tgmap1E + ) + LupS, timeaddnotused, Tgmap1S = TsWaveDelay_2015a( + gvfLupS, firstdaytime, timeadd, timestepdec, Tgmap1S + ) + LupW, timeaddnotused, Tgmap1W = TsWaveDelay_2015a( + gvfLupW, firstdaytime, timeadd, timestepdec, Tgmap1W + ) + LupN, timeaddnotused, Tgmap1N = TsWaveDelay_2015a( + gvfLupN, firstdaytime, timeadd, timestepdec, Tgmap1N + ) + + # # # # Kup # # # # + Kup, KupE, KupS, KupW, KupN = Kup_veg_2015a( + radI, + radD, + radG, + altitude, + svfbuveg, + albedo_b, + F_sh, + gvfalb, + gvfalbE, + gvfalbS, + gvfalbW, + gvfalbN, + gvfalbnosh, + gvfalbnoshE, + gvfalbnoshS, + gvfalbnoshW, + gvfalbnoshN, + ) + + Keast, Ksouth, Kwest, Knorth, KsideI, KsideD, Kside = Kside_veg_v2022a( + radI, + radD, + radG, + shadow, + svfS, + svfW, + svfN, + svfE, + svfEveg, + svfSveg, + svfWveg, + svfNveg, + azimuth, + altitude, + psi, + t, + albedo_b, + F_sh, + KupE, + KupS, + KupW, + KupN, + cyl, + lv, + anisotropic_sky, + diffsh, + rows, + cols, + asvf, + shmat, + vegshmat, + vbshvegshmat, + ) + + firstdaytime = 0 + + else: # # # # # # # NIGHTTIME # # # # # # # # + # Nocturnal K fluxes set to 0 + Knight = torch.zeros((rows, cols), device=device) + Kdown = torch.zeros((rows, cols), device=device) + Kwest = torch.zeros((rows, cols), device=device) + Kup = torch.zeros((rows, cols), device=device) + Keast = torch.zeros((rows, cols), device=device) + Ksouth = torch.zeros((rows, cols), device=device) + Knorth = torch.zeros((rows, cols), device=device) + KsideI = torch.zeros((rows, cols), device=device) + KsideD = torch.zeros((rows, cols), device=device) + F_sh = torch.zeros((rows, cols), device=device) + shadow = torch.zeros((rows, cols), device=device) + CI_TgG = deepcopy(CI) + dRad = torch.zeros((rows, cols), device=device) + Kside = torch.zeros((rows, cols), device=device) + wallsun = torch.zeros((rows, cols), device=device) + + Tgwall = 0 + + # # # # Ldown # # # # + Ldown = ( + (svf + svfveg - 1) * esky * SBC * ((Ta + 273.15) ** 4) + + (2 - svfveg - svfaveg) * ewall * SBC * ((Ta + 273.15) ** 4) + + (svfaveg - svf) * ewall * SBC * ((Ta + 273.15 + Tgwall) ** 4) + + (2 - svf - svfveg) + * (1 - ewall) + * esky + * SBC + * ((Ta + 273.15) ** 4) + ) # Jonsson et al.(2006) + # Ldown = Ldown - 25 # Shown by Jonsson et al.(2006) and Duarte et al.(2006) + + if CI < 0.95: # non - clear conditions + c = 1 - CI + Ldown = Ldown * (1 - c) + c * ( + (svf + svfveg - 1) * SBC * ((Ta + 273.15) ** 4) + + (2 - svfveg - svfaveg) * ewall * SBC * ((Ta + 273.15) ** 4) + + (svfaveg - svf) * ewall * SBC * ((Ta + 273.15 + Tgwall) ** 4) + + (2 - svf - svfveg) * (1 - ewall) * SBC * ((Ta + 273.15) ** 4) + ) # NOT REALLY TESTED!!! BUT MORE CORRECT? + + # Surface temperature parameterization + if groundScheme == 1: + # calculate the ground surface temperature, and relevant heat fluxes + Tg, Rn, Rn_past, G = surfaceTemperature_calc( + Kdown, + Ldown, + Rn, + Rn_past, + G, + Tg, + Tm, + alb_grid, + emis_grid, + cap_grid, + diff_grid, + lc_grid, + a1_grid, + a2_grid, + a3_grid, + timeStep, + RH, + shadow, + shadow_past, + ) + else: + # In the old scheme the ground surface temperature is equal to the air temperature during nighttime + Tg = torch.ones((rows, cols), device=device) * Ta + + # Calculate the outgoing longwave radiation + if outgoingLW == 1: + # # # # Lup, daytime # # # # + ( + Lup, + gvfalb, + gvfalbnosh, + LupE, + gvfalbE, + gvfalbnoshE, + LupS, + gvfalbS, + gvfalbnoshS, + LupW, + gvfalbW, + gvfalbnoshW, + LupN, + gvfalbN, + gvfalbnoshN, + gvfLsideW, + gvfLsideS, + gvfLsideE, + gvfLsideN, + ) = outgoingLongwave_calc( + Tg, + Tgwall, + Ta, + Ldown, + emis_grid, + alb_grid, + buildings, + shadow, + wallsun, + walls, + rows, + cols, + 1 / scale, + ) + + else: + # # # # Lup, nighttime # # # # + Lup = SBC * emis_grid * ((Knight + Tg + 273.15) ** 4) + LupE = Lup + LupS = Lup + LupW = Lup + LupN = Lup + + I0 = 0 + timeadd = 0 + firstdaytime = 1 + + # # # # Lside # # # # + if groundScheme == 1: + Least = torch.clone(gvfLsideE) + Lsouth = torch.clone(gvfLsideS) + Lwest = torch.clone(gvfLsideW) + Lnorth = torch.clone(gvfLsideN) + Least_, Lsouth_, Lwest_, Lnorth_ = Lside_veg_v2026( + svfS, + svfW, + svfN, + svfE, + svfEveg, + svfSveg, + svfWveg, + svfNveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + azimuth, + altitude, + Ta, + Tgwall, + SBC, + ewall, + Ldown, + esky, + t, + F_sh, + CI, + anisotropic_sky, + ) + else: + Least = torch.zeros_like(Ldown) + Lnorth = torch.zeros_like(Ldown) + Lwest = torch.zeros_like(Ldown) + Lsouth = torch.zeros_like(Ldown) + Least_, Lsouth_, Lwest_, Lnorth_ = Lside_veg_v2022a( + svfS, + svfW, + svfN, + svfE, + svfEveg, + svfSveg, + svfWveg, + svfNveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + azimuth, + altitude, + Ta, + Tgwall, + SBC, + ewall, + Ldown, + esky, + t, + F_sh, + CI, + LupE, + LupS, + LupW, + LupN, + anisotropic_sky, + ) + + Least += Least_ + Lsouth += Lsouth_ + Lwest += Lwest_ + Lnorth += Lnorth_ + Lside = (Lsouth + Lnorth + Least + Lwest) / 4 + + ### Anisotropic sky + if anisotropic_sky == 1: + if "lv" not in locals(): + # Creating skyvault of patches of constant radians (Tregeneza and Sharples, 1993) + skyvaultalt, skyvaultazi, _, _, _, _, _ = create_patches( + patch_option, device + ) + + patch_emissivities = torch.zeros( + skyvaultalt.shape[0], device=device + ) + + x = torch.transpose(torch.atleast_2d(skyvaultalt)) + y = torch.transpose(torch.atleast_2d(skyvaultazi)) + z = torch.transpose(torch.atleast_2d(patch_emissivities)) + + L_patches = torch.append(torch.append(x, y, axis=1), z, axis=1) + del skyvaultalt, skyvaultazi, patch_emissivities, x, y, z + + else: + L_patches = deepcopy(lv) + + # Calculate steradians for patches if it is the first model iteration + if i == 0: + steradians, skyalt, patch_altitude = patch_steradians(L_patches) + del skyalt, patch_altitude + + # Create lv from L_patches if nighttime, i.e. lv does not exist + if altitude < 0: + # CI = deepcopy(CI) + lv = deepcopy(L_patches) + KupE = 0 + KupS = 0 + KupW = 0 + KupN = 0 + + # Adjust sky emissivity under semi-cloudy/hazy/cloudy/overcast conditions, i.e. CI lower than 0.95 + if CI < 0.95: + esky_c = CI * esky + (1 - CI) * 1.0 + esky = esky_c + + ( + Ldown, + Lside_, + Lside_sky, + Lside_veg, + Lside_sh, + Lside_sun, + Lside_ref, + Least_, + Lwest_, + Lnorth_, + Lsouth_, + Keast, + Ksouth, + Kwest, + Knorth, + KsideI, + KsideD, + Kside, + steradians, + skyalt, + ) = ani_sky( + shmat, + vegshmat, + vbshvegshmat, + altitude, + azimuth, + asvf, + cyl, + esky, + L_patches, + wallScheme, + voxelTable, + voxelMaps, + steradians, + Ta, + Tgwall, + ewall, + Lup, + radI, + radD, + radG, + lv, + albedo_b, + 0, + diffsh, + shadow, + KupE, + KupS, + KupW, + KupN, + i, + ) + Lside += Lside_ + else: + Lside_ = torch.zeros((rows, cols), device=device) + L_patches = None + + # Box and anisotropic longwave + if cyl == 0 and anisotropic_sky == 1: + Least += Least_ + Lwest += Lwest_ + Lnorth += Lnorth_ + Lsouth += Lsouth_ + + # Calculation of radiant flux density + # Human body considered as a cylinder with isotropic all-sky diffuse + if cyl == 1 and anisotropic_sky == 0: + Sstr = absK * ( + KsideI * Fcyl + + (Kdown + Kup) * Fup + + (Knorth + Keast + Ksouth + Kwest) * Fside + ) + absL * ( + (Ldown + Lup) * Fup + (Lnorth + Least + Lsouth + Lwest) * Fside + ) + # Human body considered as a cylinder with Perez et al. (1993) (anisotropic sky diffuse) + # and Martin and Berdahl (1984) (anisotropic sky longwave) + elif cyl == 1 and anisotropic_sky == 1: + Sstr = absK * ( + Kside * Fcyl + + (Kdown + Kup) * Fup + + (Knorth + Keast + Ksouth + Kwest) * Fside + ) + absL * ( + (Ldown + Lup) * Fup + + Lside * Fcyl + + (Lnorth + Least + Lsouth + Lwest) * Fside + ) + # Human body considered as a standing cube + else: + Sstr = absK * ( + (Kdown + Kup) * Fup + (Knorth + Keast + Ksouth + Kwest) * Fside + ) + absL * ( + (Ldown + Lup) * Fup + (Lnorth + Least + Lsouth + Lwest) * Fside + ) + + # # # # Tmrt # # # # + Tmrt = torch.sqrt(torch.sqrt((Sstr / (absL * SBC)))) - 273.2 + + # Add longwave to cardinal directions for output in POI + if (cyl == 1) and (anisotropic_sky == 1): + Least += Least_ + Lwest += Lwest_ + Lnorth += Lnorth_ + Lsouth += Lsouth_ + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + return ( + Tmrt, + Kdown, + Kup, + Ldown, + Lup, + Tg, + ea, + esky, + I0, + CI, + shadow, + firstdaytime, + timestepdec, + timeadd, + Tgmap1, + Tgmap1E, + Tgmap1S, + Tgmap1W, + Tgmap1N, + Keast, + Ksouth, + Kwest, + Knorth, + Least, + Lsouth, + Lwest, + Lnorth, + KsideI, + radI, + radD, + Lside, + L_patches, + CI_TgG, + KsideD, + dRad, + Kside, + steradians, + voxelTable, + Rn, + Rn_past, + Tm, + G, + ) diff --git a/functions/SOLWEIGpython/Solweig_run.py b/functions/SOLWEIGpython/Solweig_run.py index be4b108..b85bf01 100644 --- a/functions/SOLWEIGpython/Solweig_run.py +++ b/functions/SOLWEIGpython/Solweig_run.py @@ -58,23 +58,6 @@ pass -# import numpy as np -# from .daylen import daylen -# from ...util.SEBESOLWEIGCommonFiles.clearnessindex_2013b import clearnessindex_2013b -# from ...util.SEBESOLWEIGCommonFiles.diffusefraction import diffusefraction -# from ...util.SEBESOLWEIGCommonFiles.shadowingfunction_wallheight_13 import shadowingfunction_wallheight_13 -# from ...util.SEBESOLWEIGCommonFiles.shadowingfunction_wallheight_23 import shadowingfunction_wallheight_23 -# from .gvf_2018a import gvf_2018a -# from .cylindric_wedge import cylindric_wedge -# from .TsWaveDelay_2015a import TsWaveDelay_2015a -# from .Kup_veg_2015a import Kup_veg_2015a -# # from .Lside_veg_v2015a import Lside_veg_v2015a -# # from .Kside_veg_v2019a import Kside_veg_v2019a -# from .Kside_veg_v2022a import Kside_veg_v2022a -# from ...util.SEBESOLWEIGCommonFiles.Perez_v3 import Perez_v3 -# from ...util.SEBESOLWEIGCommonFiles.create_patches import create_patches - - def solweig_run(configPath, feedback): """ Input: @@ -411,26 +394,8 @@ def solweig_run(configPath, feedback): "I0 CI Shadow SVF_b SVF_bv KsideI PET UTCI CI_TgG KsideD Lside diffDown Kside " ) # poiname = [] - poi_field = configDict[ - "poi_field" - ] # self.parameterAsString(parameters, self.POI_FIELD, context) + poi_field = configDict["poi_field"] if standAlone == 0: - # vlayer = QgsVectorLayer(configDict['poi_file'], 'point', 'ogr') - # idx = vlayer.fields().indexFromName(poi_field) - # numfeat = vlayer.featureCount() - # poisxy = np.zeros((numfeat, 3)) - 999 - # ind = 0 - # for f in vlayer.getFeatures(): # looping through each POI - # y = f.geometry().centroid().asPoint().y() - # x = f.geometry().centroid().asPoint().x() - # poiname.append(f.attributes()[idx]) - # poisxy[ind, 0] = ind - # poisxy[ind, 1] = np.round((x - minx) * scale) - # if miny >= 0: - # poisxy[ind, 2] = np.round((miny + rows * (1. / scale) - y) * scale) - # else: - # poisxy[ind, 2] = np.round((miny + rows * (1. / scale) - y) * scale) - # ind += 1 poi_field = configDict[ "woi_field" @@ -646,9 +611,10 @@ def solweig_run(configPath, feedback): TgK_wall = param["Ts_deg"]["Value"]["Walls"] Tstart_wall = param["Tstart"]["Value"]["Walls"] TmaxLST_wall = param["TmaxLST"]["Value"]["Walls"] - + # Parameterization for the 2026 ground scheme groundSurface = int(configDict["groundmodel"]) + if groundSurface == 1: # Initiate the maps if the surface temperature is available if configDict["input_surf"] != "": @@ -686,10 +652,10 @@ def solweig_run(configPath, feedback): ) else: pass - + # Replace the ground view factors with integration of solid angles outgoingLW = int(configDict["outgoinglongwave"]) - + # Import data for wall temperature parameterization TODO: fix for standalone wallScheme = int(configDict["wallscheme"]) if wallScheme == 1: @@ -790,7 +756,6 @@ def solweig_run(configPath, feedback): voxelMaps = 0 voxelTable = 0 timeStep = 0 - # thermal_effusivity = 0 walls_scheme = np.ones((rows, cols)) * 10.0 dirwalls_scheme = np.ones((rows, cols)) * 10.0 @@ -800,10 +765,6 @@ def solweig_run(configPath, feedback): else: timestepdec = dectime[1] - dectime[0] timeadd = 0.0 - # timeaddE = 0. - # timeaddS = 0. - # timeaddW = 0. - # timeaddN = 0. firstdaytime = 1.0 # Save hemispheric image @@ -867,14 +828,6 @@ def solweig_run(configPath, feedback): else: CI = 1.0 - # Only if Kdir is derived from horizontal global shortwave and horizontal diffuse shortwave - # if altitude[0][i] > 0: - # radI[i] = radI[i]/np.sin(altitude[0][i] * np.pi/180) - # else: - # radG[i] = 0. - # radD[i] = 0. - # radI[i] = 0. - # Timestep of the simulation used in the ground scheme calculation first_timestep = ( pd.to_datetime(YYYY[0][0], format="%Y") diff --git a/functions/SOLWEIGpython/Solweig_run_torch.py b/functions/SOLWEIGpython/Solweig_run_torch.py new file mode 100644 index 0000000..0fa986f --- /dev/null +++ b/functions/SOLWEIGpython/Solweig_run_torch.py @@ -0,0 +1,1619 @@ +# This is the main function of the SOLWEIG model +# 2025-Mar-21 +# Fredrik Lindberg, fredrikl@gvc.gu.se +# Goteborg Urban Climate Group +# Gothenburg University + +# sommon imports + +from __future__ import absolute_import + +try: + import torch +except ImportError: + pass + +import numpy as np +from ...util.umep_solweig_export_component import read_solweig_config +from ...util.SEBESOLWEIGCommonFiles.Solweig_v2015_metdata_noload_torch import ( + Solweig_2015a_metdata_noload, +) +from ...util.SEBESOLWEIGCommonFiles.clearnessindex_2013b_torch import ( + clearnessindex_2013b, +) + +from . import Solweig_2026a_calc_forprocessing_torch as so + +# from ...functions.SOLWEIGpython import WriteMetadataSOLWEIG # Not needed anymore? +from . import PET_calculations as p +from . import UTCI_calculations as utci +from .CirclePlotBar import PolarBarPlot +from .wall_surface_temperature import load_walls +from .wallOfInterest import pointOfInterest +from .patch_characteristics import hemispheric_image +from .wallsAsNetCDF import walls_as_netcdf +from .Tgmaps_v1_torch import Tgmaps_v1 +from .. import wallalgorithms_torch as wa +from .ground_surface_torch import initiate_groundScheme +import json +import zipfile +import pandas as pd +import matplotlib.pylab as plt +from shutil import copyfile + +# imports from osgeo/qgis dependency +try: + from osgeo import gdal + from osgeo.gdalconst import * + from ...util.misc import saveraster, xy2latlon_fromraster + from qgis.core import QgsRasterLayer +except: + pass + +# imports for standalone +try: + from umep import common + from rasterio.transform import xy, rowcol + import pyproj + from tqdm import tqdm + import geopandas as gpd +except: + pass + + +def solweig_run(configPath, feedback): + """ + Input: + configPath : config file including geodata paths and settings. + feedback : To communicate with qgis gui. Set to None if standalone + """ + + # Load config file + configDict = read_solweig_config(configPath) + + # Load parameters settings for SOLWEIG + with open(configDict["para_json_path"], "r") as jsn: + param = json.load(jsn) + + # reading variables from config and parameters that is not yet presented + standAlone = int(configDict["standalone"]) + cyl = int(configDict["cyl"]) + albedo_b = param["Albedo"]["Effective"]["Value"]["Walls"] + ewall = param["Emissivity"]["Value"]["Walls"] + onlyglobal = int(configDict["onlyglobal"]) + elvis = 0.0 + absK = param["Tmrt_params"]["Value"]["absK"] + absL = param["Tmrt_params"]["Value"]["absL"] + + # --- Load on CPU or GPU config + device = torch.device("cpu") + if configDict["calculation_mode"] == "gpu" and torch.cuda.is_available(): + device = torch.device("cuda") + if feedback is not None: + feedback.setProgressText( + "PyTorch and NVIDIA/AMD GPU found. Initiating CUDA mode..." + ) + + elif ( + configDict["calculation_mode"] == "gpu" + and hasattr(torch, "xpu") + and torch.xpu.is_available() + ): + device = torch.device("xpu") + if feedback is not None: + + feedback.setProgressText( + "PyTorch and Intel GPU found. Initiating XPU mode..." + ) + + else: + if feedback is not None: + feedback.setProgressText( + "PyTorch found but compatible GPU not found. Initiating CPU mode..." + ) + standAlone = int(configDict["standalone"]) + + # Load DSM + if standAlone == 1: + dsm, dsm_transf, dsm_crs = common.load_raster( + configDict["filepath_dsm"], bbox=None + ) + scale = 1 / dsm_transf.a + # dsm_height, dsm_width = dsm.shape # y rows by x cols + # y is flipped - so return max for lower row + minx, miny = xy(dsm_transf, dsm.shape[0], 0) + # Define the source and target CRS + source_crs = pyproj.CRS(dsm_crs) + target_crs = pyproj.CRS(4326) # WGS 84 + # Create a transformer object + transformer = pyproj.Transformer.from_crs( + source_crs, target_crs, always_xy=True + ) + # Perform the transformation + lon, lat = transformer.transform(minx, miny) + nd = -9999 # TODO: extract nodatavalue from rasterio + else: + # dsmlayer = QgsRasterLayer(configDict['filepath_dsm']) + dsm_wkt = QgsRasterLayer(configDict["filepath_dsm"]).crs().toWkt() + gdal_dsm = gdal.Open(configDict["filepath_dsm"]) + lat, lon, scale, minx, miny = xy2latlon_fromraster(dsm_wkt, gdal_dsm) + dsm = torch.from_numpy(gdal_dsm.ReadAsArray().astype(float)).to(device) + nd = gdal_dsm.GetRasterBand(1).GetNoDataValue() + + rows = dsm.shape[0] + cols = dsm.shape[1] + + # response to issue #85 + dsm[dsm == nd] = 0.0 + if dsm.min() < 0: + dsmraise = torch.abs(dsm.min()) + dsm = dsm + dsmraise + else: + dsmraise = 0 + + alt = torch.median(dsm) + if alt < 0: + alt = 3 + + # Vegetation + transVeg = param["Tree_settings"]["Value"]["Transmissivity"] + trunkratio = param["Tree_settings"]["Value"]["Trunk_ratio"] + usevegdem = int(configDict["usevegdem"]) + if usevegdem == 1: + if standAlone == 0: + vegdsm = torch.from_numpy( + ( + gdal.Open(configDict["filepath_cdsm"]) + .ReadAsArray() + .astype(float) + ) + ).to(device) + else: + vegdsm, _, _ = common.load_raster( + configDict["filepath_cdsm"], bbox=None + ) + if configDict["filepath_tdsm"] != "": + if standAlone == 0: + vegdsm2 = torch.from_numpy( + ( + gdal.Open(configDict["filepath_tdsm"]) + .ReadAsArray() + .astype(float) + ) + ).to(device) + else: + vegdsm2, _, _ = common.load_raster( + configDict["filepath_tdsm"], bbox=None + ) + else: + vegdsm2 = vegdsm * trunkratio + else: + vegdsm = 0 + vegdsm2 = 0 + + # Clean up intermediate tensors after vegetation loading + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + # Land cover + landcover = int(configDict["landcover"]) + if landcover == 1: + if standAlone == 0: + lcgrid = torch.from_numpy( + ( + gdal.Open(configDict["filepath_lc"]) + .ReadAsArray() + .astype(float) + ) + ).to(device) + else: + lcgrid, _, _ = common.load_raster( + configDict["filepath_lc"], bbox=None + ) + else: + lcgrid = torch.ones_like(dsm, device=device) + + # DEM for buildings #TODO: fix nodata in standalone + demforbuild = int(configDict["demforbuild"]) + if demforbuild == 1: + if standAlone == 0: + gdal_dem = gdal.Open( + configDict["filepath_dem"] + ) # .ReadAsArray().astype(float) + dem = torch.from_numpy(gdal_dem.ReadAsArray().astype(float)).to( + device + ) + nd = gdal_dem.GetRasterBand(1).GetNoDataValue() + else: + dem, _, _ = common.load_raster( + configDict["filepath_dem"], bbox=None + ) + nd = -9999 # TODO: standAlone nd exposure + + # response to issue and #230 + dem[dem == nd] = 0.0 + if dem.min() < 0: + demraise = torch.abs(dem.min()) + dem = dem + demraise + else: + demraise = 0 + + # Clean up DEM intermediate tensors + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + # SVF + zip = zipfile.ZipFile(configDict["input_svf"], "r") + zip.extractall(configDict["working_dir"]) + zip.close() + + if standAlone == 0: + svf = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svf.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfN = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfN.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfS = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfS.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfE = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfE.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfW = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfW.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + else: + svf, _, _ = common.load_raster( + configDict["working_dir"] + "/svf.tif", bbox=None + ) + svfN, _, _ = common.load_raster( + configDict["working_dir"] + "/svfN.tif", bbox=None + ) + svfS, _, _ = common.load_raster( + configDict["working_dir"] + "/svfS.tif", bbox=None + ) + svfE, _, _ = common.load_raster( + configDict["working_dir"] + "/svfE.tif", bbox=None + ) + svfW, _, _ = common.load_raster( + configDict["working_dir"] + "/svfW.tif", bbox=None + ) + + if usevegdem == 1: + if standAlone == 0: + svfveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfNveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfNveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfSveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfSveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfEveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfEveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfWveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfWveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + + svfaveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfaveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfNaveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfNaveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfSaveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfSaveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfEaveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfEaveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + svfWaveg = torch.from_numpy( + ( + gdal.Open(configDict["working_dir"] + "/svfWaveg.tif") + .ReadAsArray() + .astype(float) + ) + ).to(device) + else: + svfveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfveg.tif", bbox=None + ) + svfNveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfNveg.tif", bbox=None + ) + svfSveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfSveg.tif", bbox=None + ) + svfEveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfEveg.tif", bbox=None + ) + svfWveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfWveg.tif", bbox=None + ) + + svfaveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfaveg.tif", bbox=None + ) + svfNaveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfNaveg.tif", bbox=None + ) + svfSaveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfSaveg.tif", bbox=None + ) + svfEaveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfEaveg.tif", bbox=None + ) + svfWaveg, _, _ = common.load_raster( + configDict["working_dir"] + "/svfWaveg.tif", bbox=None + ) + else: + svfveg = torch.ones((rows, cols)).to(device) + svfNveg = torch.ones((rows, cols)).to(device) + svfSveg = torch.ones((rows, cols)).to(device) + svfEveg = torch.ones((rows, cols)).to(device) + svfWveg = torch.ones((rows, cols)).to(device) + svfaveg = torch.ones((rows, cols)).to(device) + svfNaveg = torch.ones((rows, cols)).to(device) + svfSaveg = torch.ones((rows, cols)).to(device) + svfEaveg = torch.ones((rows, cols)).to(device) + svfWaveg = torch.ones((rows, cols)).to(device) + + # Clean up after SVF loading + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + tmp = svf + svfveg - 1.0 + tmp[tmp < 0.0] = 0.0 + # %matlab crazyness around 0 + svfalfa = torch.arcsin(torch.exp((torch.log((1.0 - tmp)) / 2.0))) + + if standAlone == 0: + wallheight = torch.from_numpy( + (gdal.Open(configDict["filepath_wh"]).ReadAsArray().astype(float)) + ).to(device) + wallaspect = torch.from_numpy( + (gdal.Open(configDict["filepath_wa"]).ReadAsArray().astype(float)) + ).to(device) + else: + wallheight, _, _ = common.load_raster( + configDict["filepath_wh"], bbox=None + ) + wallheight = torch.tensor(wallheight, device=device) + wallaspect, _, _ = common.load_raster( + configDict["filepath_wa"], bbox=None + ) + wallaspect = torch.tensor(wallaspect, device=device) + + # Metdata + headernum = 1 + delim = " " + Twater = [] + + metdata = torch.from_numpy( + np.loadtxt( + configDict["input_met"], skiprows=headernum, delimiter=delim + ) + ).to(device) + + location = {"longitude": lon, "latitude": lat, "altitude": alt} + YYYY, altitude, azimuth, zen, jday, leafon, dectime, altmax = ( + Solweig_2015a_metdata_noload(metdata, location, int(configDict["utc"])) + ) + + DOY = metdata[:, 1] + hours = metdata[:, 2] + minu = metdata[:, 3] + Ta = metdata[:, 11] + RH = metdata[:, 10] + radG = metdata[:, 14] + radD = metdata[:, 21] + radI = metdata[:, 22] + P = metdata[:, 12] + Ws = metdata[:, 9] + + # POIs check + if configDict["poi_file"] != "": # usePOI: + header = ( + "yyyy id it imin dectime altitude azimuth kdir kdiff kglobal kdown kup keast ksouth " + "kwest knorth ldown lup least lsouth lwest lnorth Ta Tg RH Esky Tmrt " + "I0 CI Shadow SVF_b SVF_bv KsideI PET UTCI CI_TgG KsideD Lside diffDown Kside " + ) + # poiname = [] + poi_field = configDict[ + "poi_field" + ] # self.parameterAsString(parameters, self.POI_FIELD, context) + if standAlone == 0: + + poi_field = configDict[ + "woi_field" + ] # self.parameterAsStrings(parameters, self.WOI_FIELD, context) + poisxy, poiname = pointOfInterest( + configDict["poi_file"], poi_field, scale, gdal_dsm + ) + + else: + pois_gdf = gpd.read_file(configDict["poi_file"]) + numfeat = pois_gdf.shape[0] + poisxy = torch.zeros((numfeat, 3)).to(device) - 999 + for idx, row in pois_gdf.iterrows(): + y, x = rowcol( + dsm_transf, + row["geometry"].centroid.x, + row["geometry"].centroid.y, + ) # TODO: This produce different result since no standalone round coordinates + poiname.append(row[configDict["poi_field"]]) + poisxy[idx, 0] = idx + poisxy[idx, 1] = x + poisxy[idx, 2] = y + + for k in range(0, poisxy.shape[0]): + poi_save = [] # torch.zeros((1, 33)) + data_out = ( + configDict["output_dir"] + "/POI_" + str(poiname[k]) + ".txt" + ) + np.savetxt( + data_out, + ( + poi_save.cpu().numpy() + if isinstance(poi_save, torch.Tensor) + else poi_save + ), + delimiter=" ", + header=header, + comments="", + ) + + # Num format for POI output + numformat = "%d %d %d %d %.5f " + "%.2f " * 35 + + # Other PET variables + sensorheight = param["Wind_Height"]["Value"]["magl"] + age = param["PET_settings"]["Value"]["Age"] + mbody = param["PET_settings"]["Value"]["Weight"] + ht = param["PET_settings"]["Value"]["Height"] + clo = param["PET_settings"]["Value"]["clo"] + activity = param["PET_settings"]["Value"]["Activity"] + sex = param["PET_settings"]["Value"]["Sex"] + else: + poisxy = None + + # Posture settings + if param["Tmrt_params"]["Value"]["posture"] == "Standing": + Fside = param["Posture"]["Standing"]["Value"]["Fside"] + Fup = param["Posture"]["Standing"]["Value"]["Fup"] + height = torch.tensor( + param["Posture"]["Standing"]["Value"]["height"] + ).to(device) + Fcyl = param["Posture"]["Standing"]["Value"]["Fcyl"] + pos = 1 + else: + Fside = param["Posture"]["Sitting"]["Value"]["Fside"] + Fup = param["Posture"]["Sitting"]["Value"]["Fup"] + height = torch.tensor( + param["Posture"]["Sitting"]["Value"]["height"] + ).to(device) + Fcyl = param["Posture"]["Sitting"]["Value"]["Fcyl"] + pos = 0 + + # Radiative surface influence, Rule of thumb by Schmid et al. (1990). + first = torch.round(height) + if first == 0.0: + first = 1.0 + second = torch.round((height * 20.0)) + + if usevegdem == 1: + # Conifer or deciduous + if configDict["conifer_bool"]: + leafon = torch.ones((1, DOY.shape[0])).to(device) + else: + leafon = torch.zeros((1, DOY.shape[0])).to(device) + if ( + param["Tree_settings"]["Value"]["First_day_leaf"] + > param["Tree_settings"]["Value"]["Last_day_leaf"] + ): + leaf_bool = ( + DOY > param["Tree_settings"]["Value"]["First_day_leaf"] + ) | (DOY < param["Tree_settings"]["Value"]["Last_day_leaf"]) + else: + leaf_bool = ( + DOY > param["Tree_settings"]["Value"]["First_day_leaf"] + ) & (DOY < param["Tree_settings"]["Value"]["Last_day_leaf"]) + leafon[0, leaf_bool] = 1 + + # % Vegetation transmittivity of shortwave radiation + psi = leafon * transVeg + psi[leafon == 0] = 0.5 + # amaxvalue + vegmax = vegdsm.max() + amaxvalue = dsm.max() - dsm.min() + amaxvalue = torch.maximum(amaxvalue, vegmax) + + # Elevation vegdsms if buildingDEM includes ground heights + vegdsm = vegdsm + dsm + vegdsm[vegdsm == dsm] = 0 + vegdsm2 = vegdsm2 + dsm + vegdsm2[vegdsm2 == dsm] = 0 + + # % Bush separation + bush = torch.logical_not((vegdsm2 * vegdsm)) * vegdsm + + svfbuveg = svf - (1.0 - svfveg) * ( + 1.0 - transVeg + ) # % major bug fixed 20141203 + else: + psi = leafon * 0.0 + 1.0 + svfbuveg = svf + bush = torch.zeros([rows, cols]).to(device) + amaxvalue = 0 + + # Clean up vegetation processing tensors + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + # Initialization of maps + Knight = torch.zeros((rows, cols)).to(device) + Tgmap1 = torch.zeros((rows, cols)).to(device) + Tgmap1E = torch.zeros((rows, cols)).to(device) + Tgmap1S = torch.zeros((rows, cols)).to(device) + Tgmap1W = torch.zeros((rows, cols)).to(device) + Tgmap1N = torch.zeros((rows, cols)).to(device) + + # Create building boolean raster from either land cover or height rasters + if demforbuild == 0: + buildings = lcgrid.clone() + buildings[buildings == 7] = 1 + buildings[buildings == 6] = 1 + buildings[buildings == 5] = 1 + buildings[buildings == 4] = 1 + buildings[buildings == 3] = 1 + buildings[buildings == 2] = 0 + else: + buildings = dsm - dem + buildings[buildings < 2.0] = 1.0 + buildings[buildings >= 2.0] = 0.0 + + if int(configDict["savebuild"]) == 1: + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/buildings.tif", + buildings.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/buildings.tif", + buildings.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + + # Import shadow matrices (Anisotropic sky) + anisotropic_sky = int(configDict["aniso"]) + if anisotropic_sky == 1: # UseAniso + data = torch.load( + configDict["input_aniso"], map_location=device, weights_only=True + ) + shmat = data["shadowmat"] + vegshmat = data["vegshadowmat"] + vbshvegshmat = data["vbshmat"] + if usevegdem == 1: + diffsh = torch.zeros((rows, cols, shmat.shape[2])).to(device) + for i in range(0, shmat.shape[2]): + diffsh[:, :, i] = shmat[:, :, i] - (1 - vegshmat[:, :, i]) * ( + 1 - transVeg + ) # changes in psi not implemented yet + else: + diffsh = shmat + + # Estimate number of patches based on shadow matrices + if shmat.shape[2] == 145: + patch_option = 1 # patch_option = 1 # 145 patches + elif shmat.shape[2] == 153: + patch_option = 2 # patch_option = 2 # 153 patches + elif shmat.shape[2] == 306: + patch_option = 3 # patch_option = 3 # 306 patches + elif shmat.shape[2] == 612: + patch_option = 4 # patch_option = 4 # 612 patches + + # asvf to calculate sunlit and shaded patches + asvf = torch.arccos(torch.sqrt(svf)) + + # Empty array for steradians + steradians = torch.zeros((shmat.shape[2])).to(device) + else: + # anisotropic_sky = 0 + diffsh = None + shmat = None + vegshmat = None + vbshvegshmat = None + asvf = None + patch_option = 0 + steradians = 0 + shadow = torch.zeros_like(dsm).to(device) + + # % Ts parameterisation maps + if landcover == 1.0: + # Get land cover properties for Tg wave (land cover scheme based on Bogren et al. 2000, explained in Lindberg et al., 2008 and Lindberg, Onomura & Grimmond, 2016) + [ + TgK, + Tstart, + alb_grid, + emis_grid, + TgK_wall, + Tstart_wall, + TmaxLST, + TmaxLST_wall, + ] = Tgmaps_v1(lcgrid.clone(), param) + else: + TgK = Knight + param["Ts_deg"]["Value"]["Cobble_stone_2014a"] + Tstart = Knight - param["Tstart"]["Value"]["Cobble_stone_2014a"] + TmaxLST = param["TmaxLST"]["Value"]["Cobble_stone_2014a"] + alb_grid = ( + Knight + + param["Albedo"]["Effective"]["Value"]["Cobble_stone_2014a"] + ) + emis_grid = Knight + param["Emissivity"]["Value"]["Cobble_stone_2014a"] + TgK_wall = param["Ts_deg"]["Value"]["Walls"] + Tstart_wall = param["Tstart"]["Value"]["Walls"] + TmaxLST_wall = param["TmaxLST"]["Value"]["Walls"] + + # Parameterization for the 2026 ground scheme + groundSurface = int(configDict["groundmodel"]) + + if groundSurface == 1: + # Initiate the maps if the surface temperature is available + if configDict["input_surf"] != "": + surfData = pd.read_csv(configDict["input_surf"]) + Tg = surfData["Tg"] + Tm = torch.mean(surfData["Tg"]) + ( + _, + _, + Rn, + Rn_past, + G, + cap_grid, + diff_grid, + a1_grid, + a2_grid, + a3_grid, + ) = initiate_groundScheme( + lcgrid.copy(), param, DOY[0], Ta, location, device + ) + else: + ( + Tg, + Tm, + Rn, + Rn_past, + G, + cap_grid, + diff_grid, + a1_grid, + a2_grid, + a3_grid, + ) = initiate_groundScheme( + lcgrid.clone(), param, DOY[0], Ta, location, device + ) + else: + pass + + # Replace the ground view factors with integration of solid angles + outgoingLW = int(configDict["outgoinglongwave"]) + + # Import data for wall temperature parameterization TODO: fix for standalone + wallScheme = int(configDict["wallscheme"]) + if wallScheme == 1: + wallData = torch.load( + configDict["input_wall"], map_location=device, weights_only=True + ) + voxelMaps = wallData["voxelId"] + voxelTable = wallData["voxelTable"] + # Get wall type from standalone + if standAlone == 1: + wall_type_standalone = { + "Brick_wall": "100", + "Concrete_wall": "101", + "Wood_wall": "102", + } + wall_type = wall_type_standalone[configDict["walltype"]] + else: + # Get wall type set in GUI + wall_type = configDict[ + "walltype" + ] # str(100 + int(self.parameterAsString(parameters, self.WALL_TYPE, context))) #TODO + + # Calculate wall height for wall scheme, i.e. include corners (thicker walls) + walls_scheme = wa.findwalls_sp( + dsm, + 2, + device, + torch.tensor([[1, 1, 1], [1, 0, 1], [1, 1, 1]]).to(device), + ) + # Calculate wall aspect for wall scheme, i.e. include corners (thicker walls) + dirwalls_scheme = wa.filter1Goodwin_as_aspect_v3( + walls_scheme.copy(), scale, dsm, feedback, 100.0 / 180.0, device + ) + + # Used in wall temperature parameterization scheme + first_timestep = ( + pd.to_datetime(int(YYYY[0][0].item()), format="%Y") + + pd.to_timedelta(DOY[0].item() - 1, unit="d") + + pd.to_timedelta(hours[0].item(), unit="h") + + pd.to_timedelta(minu[0].item(), unit="m") + ) + second_timestep = ( + pd.to_datetime(int(YYYY[0][1].item()), format="%Y") + + pd.to_timedelta(DOY[1].item() - 1, unit="d") + + pd.to_timedelta(hours[1].item(), unit="h") + + pd.to_timedelta(minu[1].item(), unit="m") + ) + + timeStep = (second_timestep - first_timestep).seconds + + # Load voxelTable as Pandas DataFrame + voxelTable, dirwalls_scheme = load_walls( + voxelTable, + param, + wall_type, + dirwalls_scheme, + Ta[0], + timeStep, + albedo_b, + ewall, + alb_grid, + landcover, + lcgrid, + dsm, + ) + + # Use wall of interest + woi_file = configDict["woi_file"] + if woi_file: + # (dsm_minx, dsm_x_size, dsm_x_rotation, dsm_miny, dsm_y_rotation, dsm_y_size) = gdal_dsm.GetGeoTransform() #TODO: fix for standalone + if standAlone == 0: + woi_field = configDict[ + "woi_field" + ] # self.parameterAsStrings(parameters, self.WOI_FIELD, context) + woisxy, woiname = pointOfInterest( + configDict["woi_file"], woi_field, scale, gdal_dsm + ) + else: + pois_gdf = gpd.read_file(configDict["poi_file"]) + numfeat = pois_gdf.shape[0] + poisxy = torch.zeros((numfeat, 3)).to(device) - 999 + for idx, row in pois_gdf.iterrows(): + y, x = rowcol( + dsm_transf, + row["geometry"].centroid.x, + row["geometry"].centroid.y, + ) # TODO: This produce different result since no standalone round coordinates + poiname.append(row[configDict["poi_field"]]) + poisxy[idx, 0] = idx + poisxy[idx, 1] = x + poisxy[idx, 2] = y + + # Create pandas datetime object to be used when createing an xarray DataSet where wall temperatures/radiation is stored and eventually saved as a NetCDf + if configDict["wallnetcdf"] == 1: + met_for_xarray = ( + pd.to_datetime(YYYY[0][:], format="%Y") + + pd.to_timedelta(DOY - 1, unit="d") + + pd.to_timedelta(hours, unit="h") + + pd.to_timedelta(minu, unit="m") + ) + else: + wallScheme = 0 + voxelMaps = 0 + voxelTable = 0 + timeStep = 0 + # thermal_effusivity = 0 + walls_scheme = torch.ones((rows, cols)).to(device) * 10.0 + dirwalls_scheme = torch.ones((rows, cols)).to(device) * 10.0 + + # Clean up wall scheme setup tensors + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + # Initialisation of time related variables + if Ta.__len__() == 1: + timestepdec = 0 + else: + timestepdec = dectime[1] - dectime[0] + timeadd = 0.0 + # timeaddE = 0. + # timeaddS = 0. + # timeaddW = 0. + # timeaddN = 0. + firstdaytime = 1.0 + + # Save hemispheric image + if anisotropic_sky == 1: + if not poisxy is None: + patch_characteristics = hemispheric_image( + poisxy, shmat, vegshmat, vbshvegshmat, voxelMaps, wallScheme + ) + + # If metfile starts at night + CI = 1.0 + + # Main loop + tmrtplot = torch.zeros((rows, cols)).to(device) + + # Initiate array for I0 values + if torch.unique(DOY).shape[0] > 1: + unique_days = torch.unique(DOY) + first_unique_day = DOY[DOY == unique_days[0]] + I0_array = torch.zeros((first_unique_day.shape[0])).to(device) + else: + first_unique_day = DOY.clone() + I0_array = torch.zeros((DOY.shape[0])).to(device) + + if standAlone == 1: + progress = tqdm(total=Ta.__len__()) + else: + progress = None + + for i in torch.arange(0, Ta.__len__()): + if feedback is not None: + feedback.setProgress( + int(i * (100.0 / Ta.__len__())) + ) # move progressbar forward + if feedback.isCanceled(): + feedback.setProgressText("Calculation cancelled") + break + elif progress is not None: + progress.update(1) + + # Daily water body temperature + if landcover == 1: + if ((dectime[i] - torch.floor(dectime[i]))) == 0 or (i == 0): + Twater = torch.mean(Ta[jday[0] == torch.floor(dectime[i])]) + # Nocturnal cloudfraction from Offerle et al. 2003 + if (dectime[i] - torch.floor(dectime[i])) == 0: + daylines = torch.where(torch.floor(dectime) == dectime[i]) + if daylines.__len__() > 1: + alt = altitude[0][daylines] + alt2 = torch.where(alt > 1) + rise = alt2[0][0] + [_, CI, _, _, _] = clearnessindex_2013b( + zen[0, i + rise + 1], + jday[0, i + rise + 1], + Ta[i + rise + 1], + RH[i + rise + 1] / 100.0, + radG[i + rise + 1], + location, + P[i + rise + 1], + ) + if (CI > 1.0) or (CI == torch.inf): + CI = 1.0 + else: + CI = 1.0 + + # Timestep of the simulation used in the ground scheme calculation + first_timestep = ( + pd.to_datetime(int(YYYY[0][0].item()), format="%Y") + + pd.to_timedelta(DOY[0].item() - 1, unit="d") + + pd.to_timedelta(hours[0].item(), unit="h") + + pd.to_timedelta(minu[0].item(), unit="m") + ) + second_timestep = ( + pd.to_datetime(int(YYYY[0][1].item()), format="%Y") + + pd.to_timedelta(DOY[1].item() - 1, unit="d") + + pd.to_timedelta(hours[1].item(), unit="h") + + pd.to_timedelta(minu[1].item(), unit="m") + ) + + timeStep = (second_timestep - first_timestep).seconds + + ( + Tmrt, + Kdown, + Kup, + Ldown, + Lup, + Tg, + ea, + esky, + I0, + CI, + shadow, + firstdaytime, + timestepdec, + timeadd, + Tgmap1, + Tgmap1E, + Tgmap1S, + Tgmap1W, + Tgmap1N, + Keast, + Ksouth, + Kwest, + Knorth, + Least, + Lsouth, + Lwest, + Lnorth, + KsideI, + radIout, + radDout, + Lside, + Lsky_patch_characteristics, + CI_TgG, + KsideD, + dRad, + Kside, + steradians, + voxelTable, + Rn, + Rn_past, + Tm, + G, + ) = so.Solweig_2026a_calc( + i, + dsm, + scale, + rows, + cols, + svf, + svfN, + svfW, + svfE, + svfS, + svfveg, + svfNveg, + svfEveg, + svfSveg, + svfWveg, + svfaveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + vegdsm, + vegdsm2, + albedo_b, + absK, + absL, + ewall, + Fside, + Fup, + Fcyl, + altitude[0][i], + azimuth[0][i], + zen[0][i], + jday[0][i], + usevegdem, + onlyglobal, + buildings, + location, + psi[0][i], + landcover, + lcgrid, + dectime[i], + altmax[0][i], + wallaspect, + wallheight, + cyl, + elvis, + Ta[i], + RH[i], + radG[i], + radD[i], + radI[i], + P[i], + amaxvalue, + bush, + Twater, + TgK, + Tstart, + alb_grid, + emis_grid, + TgK_wall, + Tstart_wall, + TmaxLST, + TmaxLST_wall, + first, + second, + svfalfa, + svfbuveg, + firstdaytime, + timeadd, + timestepdec, + Tgmap1, + Tgmap1E, + Tgmap1S, + Tgmap1W, + Tgmap1N, + CI, + diffsh, + shmat, + vegshmat, + vbshvegshmat, + anisotropic_sky, + asvf, + patch_option, + voxelMaps, + voxelTable, + Ws[i], + wallScheme, + timeStep, + steradians, + walls_scheme, + dirwalls_scheme, + groundSurface, + outgoingLW, + Tg, + Rn, + Rn_past, + G, + Tm, + cap_grid, + diff_grid, + a1_grid, + a2_grid, + a3_grid, + shadow, + ) + + # Save I0 for I0 vs. Kdown output plot to check if UTC is off + if i < first_unique_day.shape[0]: + I0_array[i] = I0 + elif i == first_unique_day.shape[0]: + # Output I0 vs. Kglobal plot + radG_for_plot = radG[DOY == first_unique_day[0]] + # hours_for_plot = hours[DOY == first_unique_day[0]] + dectime_for_plot = dectime[DOY == first_unique_day[0]] + fig, ax = plt.subplots() + ax.plot(dectime_for_plot, I0_array, label="I0") + ax.plot(dectime_for_plot, radG_for_plot, label="Kglobal") + ax.set_ylabel("Shortwave radiation [$Wm^{-2}$]") + ax.set_xlabel("Decimal time") + ax.set_title("UTC" + str(configDict["utc"])) + ax.legend() + fig.savefig(configDict["output_dir"] + "/metCheck.png", dpi=150) + + tmrtplot = tmrtplot + Tmrt + + if altitude[0][i] > 0: + w = "D" + else: + w = "N" + + # Write to POIs + if not poisxy is None: + for k in range(0, poisxy.shape[0]): + poi_save = torch.zeros((1, 40)).to(device) + poi_save[0, 0] = YYYY[0][i] + poi_save[0, 1] = jday[0][i] + poi_save[0, 2] = hours[i] + poi_save[0, 3] = minu[i] + poi_save[0, 4] = dectime[i] + poi_save[0, 5] = altitude[0][i] + poi_save[0, 6] = azimuth[0][i] + poi_save[0, 7] = radIout + poi_save[0, 8] = radDout + poi_save[0, 9] = radG[i] + poi_save[0, 10] = Kdown[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 11] = Kup[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 12] = Keast[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 13] = Ksouth[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 14] = Kwest[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 15] = Knorth[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 16] = Ldown[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 17] = Lup[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 18] = Least[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 19] = Lsouth[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 20] = Lwest[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 21] = Lnorth[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 22] = Ta[i] + poi_save[0, 23] = Tg[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 24] = RH[i] + poi_save[0, 25] = esky + poi_save[0, 26] = Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 27] = I0 + poi_save[0, 28] = CI + poi_save[0, 29] = shadow[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 30] = svf[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 31] = svfbuveg[ + int(poisxy[k, 2]), int(poisxy[k, 1]) + ] + poi_save[0, 32] = KsideI[int(poisxy[k, 2]), int(poisxy[k, 1])] + # Recalculating wind speed based on powerlaw + WsPET = (1.1 / sensorheight) ** 0.2 * Ws[i] + WsUTCI = (10.0 / sensorheight) ** 0.2 * Ws[i] + resultPET = p._PET( + Ta[i], + RH[i], + Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])], + WsPET, + mbody, + age, + ht, + activity, + clo, + sex, + ) + poi_save[0, 33] = resultPET + resultUTCI = utci.utci_calculator( + Ta[i], + RH[i], + Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])], + WsUTCI, + ) + poi_save[0, 34] = resultUTCI + poi_save[0, 35] = CI_TgG + poi_save[0, 36] = KsideD[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 37] = Lside[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 38] = dRad[int(poisxy[k, 2]), int(poisxy[k, 1])] + poi_save[0, 39] = Kside[int(poisxy[k, 2]), int(poisxy[k, 1])] + data_out = ( + configDict["output_dir"] + + "/POI_" + + str(poiname[k]) + + ".txt" + ) + # f_handle = file(data_out, 'a') + f_handle = open(data_out, "ab") + np.savetxt( + f_handle, + ( + poi_save.cpu().numpy() + if isinstance(poi_save, torch.Tensor) + else poi_save + ), + fmt=numformat, + ) + f_handle.close() + + # Clean up POI tensor after writing + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + # If wall temperature parameterization scheme is in use + if ( + configDict["wallscheme"] == 1 + ): # folderWallScheme: TODO: Fix for standalone + # Store wall data for output + if not woisxy is None: + for k in range(0, woisxy.shape[0]): + temp_wall = voxelTable.loc[ + ( + (voxelTable["ypos"] == woisxy[k, 2]) + & (voxelTable["xpos"] == woisxy[k, 1]) + ), + "wallTemperature", + ].to_numpy() + K_in = voxelTable.loc[ + ( + (voxelTable["ypos"] == woisxy[k, 2]) + & (voxelTable["xpos"] == woisxy[k, 1]) + ), + "K_in", + ].to_numpy() + L_in = voxelTable.loc[ + ( + (voxelTable["ypos"] == woisxy[k, 2]) + & (voxelTable["xpos"] == woisxy[k, 1]) + ), + "L_in", + ].to_numpy() + wallShade = voxelTable.loc[ + ( + (voxelTable["ypos"] == woisxy[k, 2]) + & (voxelTable["xpos"] == woisxy[k, 1]) + ), + "wallShade", + ].to_numpy() + temp_all = torch.concatenate( + [temp_wall, K_in, L_in, wallShade] + ).to(device) + # temp_all = torch.concatenate([temp_wall]) + # wall_data = torch.zeros((1, 7 + temp_wall.shape[0])) + wall_data = torch.zeros((1, 7 + temp_all.shape[0])).to( + device + ) + # Part of file name (wallid), i.e. WOI_wallid.txt + data_out = ( + configDict["output_dir"] + + "/WOI_" + + str(woiname[k]) + + ".txt" + ) + if i == 0: + # Output file header + # header = 'yyyy id it imin dectime Ta SVF Ts' + header = ( + "yyyy id it imin dectime Ta SVF" + + " Ts" * temp_wall.shape[0] + + " Kin" * K_in.shape[0] + + " Lin" * L_in.shape[0] + + " shade" * wallShade.shape[0] + ) + # Part of file name (wallid), i.e. WOI_wallid.txt + # woiname = voxelTable.loc[((voxelTable['ypos'] == woisxy[k, 2]) & (voxelTable['xpos'] == woisxy[k, 1])), 'wallId'].to_numpy()[0] + woi_save = [] # + np.savetxt( + data_out, + woi_save, + delimiter=" ", + header=header, + comments="", + ) + # Fill wall_data with variables + wall_data[0, 0] = YYYY[0][i] + wall_data[0, 1] = jday[0][i] + wall_data[0, 2] = hours[i] + wall_data[0, 3] = minu[i] + wall_data[0, 4] = dectime[i] + wall_data[0, 5] = Ta[i] + wall_data[0, 6] = svf[int(woisxy[k, 2]), int(woisxy[k, 1])] + wall_data[0, 7:] = temp_all + + # Num format for output file data + woi_numformat = ( + "%d %d %d %d %.5f %.2f %.2f" + + " %.2f" * temp_all.shape[0] + ) + # Open file, add data, save + f_handle = open(data_out, "ab") + np.savetxt( + f_handle, + ( + wall_data.cpu().numpy() + if isinstance(wall_data, torch.Tensor) + else wall_data + ), + fmt=woi_numformat, + ) + f_handle.close() + del wall_data, temp_all # Clean up wall data tensors + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + # Save wall temperature/radiation as NetCDF TODO: fix for standAlone? + if configDict["wallnetcdf"] == "1": # wallNetCDF: + netcdf_output = configDict["outputDir"] + "/walls.nc" + walls_as_netcdf( + voxelTable, + rows, + cols, + met_for_xarray, + i, + dsm, + configDict["filepath_dsm"], + netcdf_output, + ) + + if hours[i] < 10: + XH = "0" + else: + XH = "" + if minu[i] < 10: + XM = "0" + else: + XM = "" + + time_code = ( + str(int(YYYY[0, i])) + + "_" + + str(int(DOY[i])) + + "_" + + XH + + str(int(hours[i])) + + XM + + str(int(minu[i])) + + w + ) + + if configDict["outputtmrt"] == "1": + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Tmrt_" + time_code + ".tif", + Tmrt.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Tmrt_" + time_code + ".tif", + Tmrt.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + if configDict["outputkup"] == "1": + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Kup_" + time_code + ".tif", + Kup.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Kup_" + time_code + ".tif", + Kup.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + if configDict["outputkdown"] == "1": + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Kdown_" + time_code + ".tif", + Kdown.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Kdown_" + time_code + ".tif", + Kdown.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + if configDict["outputlup"] == "1": + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Lup_" + time_code + ".tif", + Lup.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Lup_" + time_code + ".tif", + Lup.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + if configDict["outputldown"] == "1": + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Ldown_" + time_code + ".tif", + Ldown.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Ldown_" + time_code + ".tif", + Ldown.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + if configDict["outputsh"] == "1": + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Shadow_" + time_code + ".tif", + shadow.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Shadow_" + time_code + ".tif", + shadow.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + + if configDict["outputkdiff"] == "1": + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Kdiff_" + time_code + ".tif", + dRad.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Kdiff_" + time_code + ".tif", + dRad.detach().cpu().numpy(), + dsm_transf, + dsm_crs, + ) + + # Clean up iteration tensors after output + if device.type == "cuda" or device.type == "xpu": + del ( + Tmrt, + Kdown, + Kup, + Ldown, + Lup, + Keast, + Ksouth, + Kwest, + Knorth, + Least, + Lsouth, + Lwest, + Lnorth, + KsideI, + radIout, + radDout, + Lside, + KsideD, + dRad, + Kside, + ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + # Sky view image of patches + if (anisotropic_sky == 1) & (i == 0) & (not poisxy is None): + for k in range(poisxy.shape[0]): + Lsky_patch_characteristics[:, 2] = patch_characteristics[:, k] + skyviewimage_out = ( + configDict["output_dir"] + + "/POI_" + + str(poiname[k]) + + ".png" + ) + PolarBarPlot( + Lsky_patch_characteristics, + altitude[0][i], + azimuth[0][i], + "Hemisphere partitioning", + skyviewimage_out, + 0, + 5, + 0, + ) + + # Save files for Tree Planter + if configDict["outputtreeplanter"] == "1": # outputTreeplanter: + if feedback is not None: + feedback.setProgressText("Saving files for Tree Planter tool") + # Save DSM + copyfile( + configDict["filepath_dsm"], configDict["output_dir"] + "/DSM.tif" + ) + + # Save CDSM + if usevegdem == 1: + copyfile( + configDict["filepath_cdsm"], + configDict["output_dir"] + "/CDSM.tif", + ) + + albedo_g = param["Albedo"]["Effective"]["Value"]["Cobble_stone_2014a"] + eground = param["Emissivity"]["Value"]["Cobble_stone_2014a"] + + # Saving settings from SOLWEIG for SOLWEIG1D in TreePlanter + settingsHeader = "UTC, posture, onlyglobal, landcover, anisotropic, cylinder, albedo_walls, albedo_ground, emissivity_walls, emissivity_ground, absK, absL, elevation, patch_option" + settingsFmt = ( + "%i", + "%i", + "%i", + "%i", + "%i", + "%i", + "%1.2f", + "%1.2f", + "%1.2f", + "%1.2f", + "%1.2f", + "%1.2f", + "%1.2f", + "%i", + ) + settingsData = torch.tensor( + [ + [ + int(configDict["utc"]), + pos, + onlyglobal, + landcover, + anisotropic_sky, + cyl, + albedo_b, + albedo_g, + ewall, + eground, + absK, + absL, + alt, + patch_option, + ] + ] + ).to(device) + + np.savetxt( + configDict["output_dir"] + "/treeplantersettings.txt", + ( + settingsData.cpu().numpy() + if isinstance(settingsData, torch.Tensor) + else settingsData + ), + fmt=settingsFmt, + header=settingsHeader, + delimiter=" ", + ) + + # Copying met file for SpatialTC + copyfile( + configDict["input_met"], configDict["output_dir"] + "/metforcing.txt" + ) + + tmrtplot = ( + tmrtplot / Ta.__len__() + ) # fix average Tmrt instead of sum, 20191022 + if standAlone == 0: + saveraster( + gdal_dsm, + configDict["output_dir"] + "/Tmrt_average.tif", + tmrtplot.detach().cpu().numpy(), + ) + else: + common.save_raster( + configDict["output_dir"] + "/Tmrt_average.tif", + tmrtplot, + dsm_transf, + dsm_crs, + ) + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() diff --git a/functions/SOLWEIGpython/Tgmaps_v1_torch.py b/functions/SOLWEIGpython/Tgmaps_v1_torch.py new file mode 100644 index 0000000..e75070d --- /dev/null +++ b/functions/SOLWEIGpython/Tgmaps_v1_torch.py @@ -0,0 +1,51 @@ +try: + import torch +except: + pass + + +def Tgmaps_v1(lc_grid, solweig_parameters): + + # Tgmaps_v1 Populates grids with cooeficients for Tg wave + # Detailed explanation goes here + lc_grid[lc_grid >= 100] = 2 + id = torch.unique(lc_grid) + id = lc_grid[lc_grid <= 7].to(int) + TgK = torch.clone(lc_grid) + Tstart = torch.clone(lc_grid) + alb_grid = torch.clone(lc_grid) + emis_grid = torch.clone(lc_grid) + TmaxLST = torch.clone(lc_grid) + + for i in id: + # row = (lc_class[:, 0] == id[i]) + Tstart[Tstart == i] = solweig_parameters["Tstart"]["Value"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ] + alb_grid[alb_grid == i] = solweig_parameters["Albedo"]["Effective"][ + "Value" + ][solweig_parameters["Names"]["Value"][str((int(i.item())))]] + emis_grid[emis_grid == i] = solweig_parameters["Emissivity"]["Value"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ] + TmaxLST[TmaxLST == i] = solweig_parameters["TmaxLST"]["Value"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ] + TgK[TgK == i] = solweig_parameters["Ts_deg"]["Value"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ] + + TgK_wall = solweig_parameters["Ts_deg"]["Value"]["Walls"] + Tstart_wall = solweig_parameters["Tstart"]["Value"]["Walls"] + TmaxLST_wall = solweig_parameters["TmaxLST"]["Value"]["Walls"] + + return ( + TgK, + Tstart, + alb_grid, + emis_grid, + TgK_wall, + Tstart_wall, + TmaxLST, + TmaxLST_wall, + ) diff --git a/functions/SOLWEIGpython/TsWaveDelay_2015a.py b/functions/SOLWEIGpython/TsWaveDelay_2015a.py index d6a85b8..70f5e14 100644 --- a/functions/SOLWEIGpython/TsWaveDelay_2015a.py +++ b/functions/SOLWEIGpython/TsWaveDelay_2015a.py @@ -8,8 +8,9 @@ def TsWaveDelay_2015a(gvfLup, firstdaytime, timeadd, timestepdec, Tgmap1): Tgmap1 = Tgmap0 if timeadd >= (59 / 1440): # more or equal to 59 min - # surface temperature delay function - 1 step - weight1 = np.exp(-33.27 * timeadd) + weight1 = np.exp( + -33.27 * timeadd + ) # surface temperature delay function - 1 step Tgmap1 = Tgmap0 * (1 - weight1) + Tgmap1 * weight1 Lup = Tgmap1 if timestepdec > (59 / 1440): @@ -18,8 +19,9 @@ def TsWaveDelay_2015a(gvfLup, firstdaytime, timeadd, timestepdec, Tgmap1): timeadd = 0 else: timeadd = timeadd + timestepdec - # surface temperature delay function - 1 step - weight1 = np.exp(-33.27 * timeadd) + weight1 = np.exp( + -33.27 * timeadd + ) # surface temperature delay function - 1 step Lup = Tgmap0 * (1 - weight1) + Tgmap1 * weight1 return Lup, timeadd, Tgmap1 diff --git a/functions/SOLWEIGpython/UTCI_calculations.py b/functions/SOLWEIGpython/UTCI_calculations.py index 6954745..a773a19 100644 --- a/functions/SOLWEIGpython/UTCI_calculations.py +++ b/functions/SOLWEIGpython/UTCI_calculations.py @@ -330,8 +330,7 @@ def utci_calculator_grid(Ta, RH, Tmrt, va10m, feedback): # Set UTCI to NaN (-999) if Tmrt or wind speed is NaN if Tmrt[iy, ix] <= -999 or va10m[iy, ix] <= -999: UTCI_approx[iy, ix] = -9999 - # Calculate 6th order polynomial as approximation if wind speed - # is above zero + # Calculate 6th order polynomial as approximation if wind speed is above zero elif va10m[iy, ix] > 0: UTCI_approx[iy, ix] = utci_polynomial( D_Tmrt, Ta, va10m[iy, ix], Pa diff --git a/functions/SOLWEIGpython/WriteMetadataSOLWEIG.py b/functions/SOLWEIGpython/WriteMetadataSOLWEIG.py index 0eb60e4..ee033b7 100644 --- a/functions/SOLWEIGpython/WriteMetadataSOLWEIG.py +++ b/functions/SOLWEIGpython/WriteMetadataSOLWEIG.py @@ -119,11 +119,10 @@ def writeRunInfo( ) file.write("\n") if trunkfile == 1: - # FO# zrunk -> trunk file.write( "Digital vegetation trunk zone model (TDSM): " + filePath_tdsm - ) + ) # FO# zrunk -> trunk file.write("\n") else: file.write("Trunkzone estimated from CDSM") @@ -225,8 +224,9 @@ def writeRunInfo( ) file.write("\n") if cyl == 1: - # FO# '' -> standing - file.write("Human considered as a standing cylinder") + file.write( + "Human considered as a standing cylinder" + ) # FO# '' -> standing else: file.write("Human considered as a standing cube") file.write("\n") diff --git a/functions/SOLWEIGpython/anisotropic_sky.py b/functions/SOLWEIGpython/anisotropic_sky.py index d4b9ee1..93c08c0 100644 --- a/functions/SOLWEIGpython/anisotropic_sky.py +++ b/functions/SOLWEIGpython/anisotropic_sky.py @@ -120,12 +120,10 @@ def anisotropic_sky( # Anisotropic sky if anisotropic_sky_: temp_emissivity = esky_band[skyalt == temp_altitude] - # Isotropic sky but with patches (need to switch anisotropic_sky to - # False) + # Isotropic sky but with patches (need to switch anisotropic_sky to False) else: temp_emissivity = esky - # Estimate longwave radiation on a horizontal surface (Ldown), vertical - # surface (Lside) and perpendicular (Lnormal) + # Estimate longwave radiation on a horizontal surface (Ldown), vertical surface (Lside) and perpendicular (Lnormal) Ldown[patch_altitude == temp_altitude] = ( ((temp_emissivity * SBC * ((Ta + 273.15) ** 4)) / np.pi) * steradians[patch_altitude == temp_altitude] @@ -156,24 +154,21 @@ def anisotropic_sky( radTot = np.zeros(1) # Radiance fraction normalization for i in np.arange(patch_altitude.shape[0]): - # Radiance fraction normalization radTot += ( patch_luminance[i] * steradians[i] * np.sin(patch_altitude[i] * deg2rad) - ) + ) # Radiance fraction normalization lumChi = (patch_luminance * radD) / radTot for i in np.arange(patch_altitude.shape[0]): # Calculations for patches on sky, shmat = 1 = sky is visible temp_sky = (shmat[:, :, i] == 1) & (vegshmat[:, :, i] == 1) - # Calculations for patches that are vegetation, vegshmat = 0 = shade - # from vegetation + # Calculations for patches that are vegetation, vegshmat = 0 = shade from vegetation temp_vegsh = (vegshmat[:, :, i] == 0) | (vbshvegshmat[:, :, i] == 0) - # Calculations for patches that are buildings, shmat = 0 = shade from - # buildings + # Calculations for patches that are buildings, shmat = 0 = shade from buildings temp_vbsh = (1 - shmat[:, :, i]) * vbshvegshmat[:, :, i] temp_sh = temp_vbsh == 1 if wallScheme == 1: @@ -194,8 +189,7 @@ def anisotropic_sky( ) if cyl == 1: - # Angle of incidence, np.cos(0) because cylinder - always - # perpendicular + # Angle of incidence, np.cos(0) because cylinder - always perpendicular angle_of_incidence = np.cos(patch_altitude[i] * deg2rad) * np.cos( 0 ) # * np.sin(np.pi / 2) \ @@ -278,8 +272,6 @@ def anisotropic_sky( else: azimuth_difference = np.abs(solar_azimuth - patch_azimuth[i]) - # print('Building pixels = ' + str(temp_sh.sum())) - # print('Building pixels wall scheme = ' + str(np.sum(temp_sh_w > 0))) ( Lside_sun_temp, Lside_sh_temp, @@ -422,16 +414,14 @@ def anisotropic_sky( Lwest += Lwest_temp Lnorth += Lnorth_temp - # Sum of all Lside components (sky, vegetation, sunlit and shaded - # buildings, reflected) + # Sum of all Lside components (sky, vegetation, sunlit and shaded buildings, reflected) Lside = Lside_sky + Lside_veg + Lside_sh + Lside_sun + Lside_ref - # Sum of all Lside components (sky, vegetation, sunlit and shaded - # buildings, reflected) + # Sum of all Lside components (sky, vegetation, sunlit and shaded buildings, reflected) Ldown = Ldown_sky + Ldown_veg + Ldown_sh + Ldown_sun + Ldown_ref ### Direct radiation ### - if cyl == 1: # Kside with cylinder ### + if cyl == 1: ### Kside with cylinder ### KsideI = shadow * radI * np.cos(solar_altitude * deg2rad) if solar_altitude > 0: diff --git a/functions/SOLWEIGpython/cylindric_wedge_torch.py b/functions/SOLWEIGpython/cylindric_wedge_torch.py new file mode 100644 index 0000000..3e3d799 --- /dev/null +++ b/functions/SOLWEIGpython/cylindric_wedge_torch.py @@ -0,0 +1,134 @@ +try: + import torch +except: + pass + + +def cylindric_wedge(zen, svfalfa, rows, cols): + + # Fraction of sunlit walls based on sun altitude and svf wieghted building angles + # input: + # sun zenith angle "beta" + # svf related angle "alfa" + try: + import torch + except: + Exception("Error, pytorch must be imported.") + + device = ( + svfalfa.device + if isinstance(svfalfa, torch.Tensor) + else torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + ) + beta = zen + # alfa=svfalfa + alfa = torch.zeros((rows, cols), device=device) + svfalfa + # measure the size of the image + # sizex=size(svfalfa,2) + # sizey=size(svfalfa,1) + + xa = 1 - 2.0 / (torch.tan(alfa) * torch.tan(beta)) + ha = 2.0 / (torch.tan(alfa) * torch.tan(beta)) + ba = 1.0 / torch.tan(alfa) + hkil = 2.0 * ba * ha + + qa = torch.zeros((rows, cols), device=device) + # qa(length(svfalfa),length(svfalfa))=0; + qa[xa < 0] = torch.tan(beta) / 2 + + Za = torch.zeros((rows, cols), device=device) + # Za(length(svfalfa),length(svfalfa))=0; + Za[xa < 0] = (((ba[xa < 0] ** 2) - ((qa[xa < 0] ** 2) / 4)) ** 0.5).float() + + phi = torch.zeros((rows, cols), device=device) + # phi(length(svfalfa),length(svfalfa))=0; + phi[xa < 0] = torch.arctan(Za[xa < 0] / qa[xa < 0]) + + A = torch.zeros((rows, cols), device=device) + # A(length(svfalfa),length(svfalfa))=0; + A[xa < 0] = ( + torch.sin(phi[xa < 0]) - phi[xa < 0] * torch.cos(phi[xa < 0]) + ) / (1 - torch.cos(phi[xa < 0])) + + ukil = torch.zeros((rows, cols), device=device) + # ukil(length(svfalfa),length(svfalfa))=0 + ukil[xa < 0] = (2 * ba[xa < 0] * xa[xa < 0] * A[xa < 0]).float() + + Ssurf = hkil + ukil + + F_sh = (2 * torch.pi * ba - Ssurf) / (2 * torch.pi * ba) # Xa + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return F_sh + + +def cylindric_wedge_voxel(zen, svfalfa): + + torch.seterr(divide="ignore", invalid="ignore") + + # Fraction of sunlit walls based on sun altitude and svf wieghted building angles + # input: + # sun zenith angle "beta" + # svf related angle "alfa" + + beta = zen + + xa = 1 - 2.0 / (torch.tan(svfalfa) * torch.tan(beta)) + ha = 2.0 / (torch.tan(svfalfa) * torch.tan(beta)) + ba = 1.0 / torch.tan(svfalfa) + hkil = 2.0 * ba * ha + + device = ( + svfalfa.device + if isinstance(svfalfa, torch.Tensor) + else torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + ) + + qa = torch.zeros((svfalfa.shape[0]), device=device) + # qa(length(svfalfa),length(svfalfa))=0; + qa[xa < 0] = torch.tan(beta) / 2 + + Za = torch.zeros((svfalfa.shape[0]), device=device) + # Za(length(svfalfa),length(svfalfa))=0; + Za[xa < 0] = ((ba[xa < 0] ** 2) - ((qa[xa < 0] ** 2) / 4)) ** 0.5 + + phi = torch.zeros((svfalfa.shape[0]), device=device) + # phi(length(svfalfa),length(svfalfa))=0; + phi[xa < 0] = torch.arctan(Za[xa < 0] / qa[xa < 0]) + + A = torch.zeros((svfalfa.shape[0]), device=device) + # A(length(svfalfa),length(svfalfa))=0; + A[xa < 0] = ( + torch.sin(phi[xa < 0]) - phi[xa < 0] * torch.cos(phi[xa < 0]) + ) / (1 - torch.cos(phi[xa < 0])) + + ukil = torch.zeros((svfalfa.shape[0]), device=device) + # ukil(length(svfalfa),length(svfalfa))=0 + ukil[xa < 0] = 2 * ba[xa < 0] * xa[xa < 0] * A[xa < 0] + + Ssurf = hkil + ukil + + F_sh = (2 * torch.pi * ba - Ssurf) / (2 * torch.pi * ba) # Xa + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return F_sh diff --git a/functions/SOLWEIGpython/daylen_torch.py b/functions/SOLWEIGpython/daylen_torch.py new file mode 100644 index 0000000..3a86f0d --- /dev/null +++ b/functions/SOLWEIGpython/daylen_torch.py @@ -0,0 +1,45 @@ +try: + import torch + +except: + pass + + +def daylen(DOY, XLAT): + # Calculation of declination of sun (Eqn. 16). Amplitude= +/-23.45 + # deg. Minimum = DOY 355 (DEC 21), maximum = DOY 172.5 (JUN 21/22). + # Sun angles. SOC limited for latitudes above polar circles. + # Calculate daylength, sunrise and sunset (Eqn. 17) + + device = ( + DOY.device + if isinstance(DOY, torch.Tensor) + else torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + ) + if not isinstance(XLAT, torch.Tensor): + XLAT = torch.tensor(XLAT, device=device) + + RAD = torch.tensor(torch.pi / 180.0, device=device) + + DEC = -23.45 * torch.cos(2.0 * torch.pi * (DOY + 10.0) / 365.0) + + SOC = torch.tan(RAD * DEC) * torch.tan(RAD * XLAT) + SOC = torch.clamp(SOC, -1.0, 1.0) + # SOC=alt + + DAYL = 12.0 + 24.0 * torch.arcsin(SOC) / torch.pi + SNUP = 12.0 - DAYL / 2.0 + SNDN = 12.0 + DAYL / 2.0 + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return DAYL, DEC, SNDN, SNUP diff --git a/functions/SOLWEIGpython/emissivity_models.py b/functions/SOLWEIGpython/emissivity_models.py index e791a00..1762e50 100644 --- a/functions/SOLWEIGpython/emissivity_models.py +++ b/functions/SOLWEIGpython/emissivity_models.py @@ -38,8 +38,7 @@ def model1(sky_patches, esky, Ta): # Natural log of optical water depth log_owp = np.log(owp) - # Emissivity of each zenith angle, i.e. the zenith angle of each band of - # patches + # Emissivity of each zenith angle, i.e. the zenith angle of each band of patches esky_band = a_c + b_c * log_owp # Altitudes of the Robinson & Stone patches @@ -115,15 +114,13 @@ def model3(sky_patches, esky, Ta): # Unique zeniths for the patches skyzen = 90 - skyalt - # Constant, can (should?) be changed. Model gave unsatisfactory results in - # Nahon et al., 2019 + # Constant, can (should?) be changed. Model gave unsatisfactory results in Nahon et al., 2019 b_c = 1.8 # Estimate emissivites at different altitudes/zenith angles esky_band = 1 - (1 - esky) ** (1 / (b_c * np.cos(skyzen * deg2rad))) - # Estimating longwave radiation for each patch to a horizontal or vertical - # surface + # Estimating longwave radiation for each patch to a horizontal or vertical surface # Altitudes of the Robinson & Stone patches p_alt = sky_patches[:, 0] diff --git a/functions/SOLWEIGpython/ground_surface.py b/functions/SOLWEIGpython/ground_surface.py index c31bb9c..4e9f06d 100644 --- a/functions/SOLWEIGpython/ground_surface.py +++ b/functions/SOLWEIGpython/ground_surface.py @@ -4,7 +4,6 @@ import numpy as np import math -import matplotlib.pyplot as plt # Stefan-Boltzmann s constant SBC = 5.67e-8 @@ -400,7 +399,6 @@ def outgoingLongwave_calc( # Boolean array 1 if the pixel is a wall, 0 if not wallbol = walls > 0 - # The alb grids only take into account the sunlit surfaces in the alb calculation albnosh calculate it for all the surfaces albsunlit = alb * shadow @@ -412,7 +410,6 @@ def outgoingLongwave_calc( # Grid of the outgoing longwave radiation coming from the ground Lup = (SBC * emis * (Tg + 273.15) ** 4 + Ldown * (1 - emis)) * buildings - ### Initialize the ground view factor grids as np.zeros() # Upwelling longwave radiation gvfLup = np.zeros((rows, cols)) @@ -459,10 +456,10 @@ def outgoingLongwave_calc( # Boolean array 1 if the pixel is (or was) a wall, 0 if not pastwalls = np.copy(wallbol) - + # Grid of the longwave radiation emitted by the walls Lwall = SBC * emis_wall * (Tgwall + Ta + 273.15) ** 4 * wallbol - + # Initialisation of the tables # First the ones containing the translated rasters (temporary) building_temp = np.copy(buildings) @@ -586,7 +583,6 @@ def outgoingLongwave_calc( int(x_select_start) : math.ceil(x_select_end), int(y_select_start) : math.ceil(y_select_end), ] - # Change the boolean building grid, if the px was already a building it remains one (px value = 0) building_copy = np.min([building_copy, building_temp], axis=0) @@ -597,7 +593,6 @@ def outgoingLongwave_calc( Lup_sum += Lup_temp * view_factor * building_copy / 20 albsun_sum += albsun_temp * view_factor * building_copy / 20 albtot_sum += albtot_temp * view_factor * building_copy / 20 - # Create a boolean grid to assert that the sunlit walls are not inside a building onlywall_temp = np.logical_and( walls_temp, np.logical_not(pastwalls) @@ -623,7 +618,6 @@ def outgoingLongwave_calc( / zs * np.sqrt((r + sizepx / 2) ** 2 + zs**2) ) - # Then add the radiation incoming from those walls Lup_sum += ( onlywall_temp @@ -642,7 +636,6 @@ def outgoingLongwave_calc( albtot_sum += ( onlywall_temp * alb_wall * viewfactor_wall * building_copy / 20 ) - # Finally add the radiation in Lside dphi = np.arctan((r + step) / zs) - np.arctan(r / zs) dtrigo = zs / np.sqrt(r**2 + zs**2) * r / np.sqrt( @@ -650,7 +643,6 @@ def outgoingLongwave_calc( ) - zs / np.sqrt((r + step) ** 2 + zs**2) * (r + step) / np.sqrt( (r + step) ** 2 + zs**2 ) - # Calculation of the solid angle for each of the cardinal points # plus add the radiation from a potential wall steradiansW, steradiansS, steradiansE, steradiansN = 0, 0, 0, 0 diff --git a/functions/SOLWEIGpython/ground_surface_torch.py b/functions/SOLWEIGpython/ground_surface_torch.py new file mode 100644 index 0000000..3ef5522 --- /dev/null +++ b/functions/SOLWEIGpython/ground_surface_torch.py @@ -0,0 +1,792 @@ +""" +@author: Eliott Bridoux, University of Gothenburg +""" + +import math + +try: + import torch +except: + pass +# Stefan-Boltzmann s constant +SBC = 5.67e-8 + + +def saturated_vp(T): + """ + Used in the calculation of the surface temperature for the water bodies. + + :Parameters: + :T: the temperature just above the water + + :Return: + :qs: the saturated vapor pressure + :slope: the slope of the tangent to the saturated vapor pressure-temperature curve + """ + + L = 2.260e6 # Latent heat of vaporisation + R = 8.314 # Gas constant + + # August-Roche-Magnus approx. in Pa + qs = 6109.4 * torch.exp(17.625 * T / (T + 243.04)) + + # Clausius-Clapeyron equation + slope = L * qs / R / (T + 273.15) ** 2 + + return slope, qs + + +def initiate_groundScheme( + lc_grid, solweig_parameters, day, Ta, location, device +): + """ + Setup the maps used in the ground scheme calculations depending on the landcover + + :Parameters: + :lc_grid: Array of landcover values + :solweig_param: Dict of physical parameters + :day: day of the year (int) + :Ta: air temperature (float) + :location: Dict containing latitude and longitude + + :Return: Array for the surface temperature, radiation flux and physical parameters + """ + + # Get the landcover data from lc_grid array + lc_grid[lc_grid >= 100] = 2 + id = torch.unique(lc_grid) + id = id.int() + + # Physical parameters grids + cap_grid = torch.clone(lc_grid) # Heat capacity + diff_grid = torch.clone(lc_grid) # Thermal diffusivity + a1_grid = torch.clone(lc_grid) + a2_grid = torch.clone(lc_grid) + a3_grid = torch.clone(lc_grid) # The 3 OHM coefficients + + # Initial fluxes and temperature grids + Rn = torch.zeros_like(lc_grid) # Net radiation + Rn_past = torch.zeros_like(lc_grid) # Stored net radiation + G = torch.zeros_like(lc_grid) # Ground heat flux + Tg = torch.clone(lc_grid) # Surface temperature + Tm = torch.clone(lc_grid) # Mean daily surface temperature + + for i in id: + cap_grid[cap_grid == i] = solweig_parameters["Heat capacity"]["Value"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ] + diff_grid[diff_grid == i] = solweig_parameters["Thermal_diffusivity"][ + "Value" + ][solweig_parameters["Names"]["Value"][str((int(i.item())))]] + + # Coefficients of the OHM per land cover + mean_a1 = solweig_parameters["OHM_coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][0] + phi_a1 = solweig_parameters["OHM_coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][1] + a1_grid[a1_grid == i] = mean_a1 * ( + 1 + + 0.33 + * torch.sin(2 * torch.pi / 365.25 * day + phi_a1) + * torch.sign(torch.tensor(location["latitude"], device=device)) + ) + a2_grid[a2_grid == i] = solweig_parameters["OHM_coefficients"][ + "Values" + ][solweig_parameters["Names"]["Value"][str((int(i.item())))]][2] + a3_grid[a3_grid == i] = solweig_parameters["OHM_coefficients"][ + "Values" + ][solweig_parameters["Names"]["Value"][str((int(i.item())))]][3] + + # Initial ground surface temperature parameters + offset_Tg = solweig_parameters["Tg_ini coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][0] + + slope_Tg = solweig_parameters["Tg_ini coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][0] + + ratio_Tg = float( + solweig_parameters["Tg_ini coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][1] + ) + phi_Tg = 1.6 + + # Correct the offset value given the latitude + offset_Tg += slope_Tg * location["latitude"] + + # Mean daily soil temperature parameters + ampl_Tm = solweig_parameters["Tm_ini coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][0] + slope_Tm = solweig_parameters["Tm_ini coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][1] + phi_Tm = 1.7 + offset_Tm = solweig_parameters["Tm_ini coefficients"]["Values"][ + solweig_parameters["Names"]["Value"][str((int(i.item())))] + ][2] + + # Correct the offset value given the latitude + offset_Tm += slope_Tm * location["latitude"] + + if i == 0 or i == 1: + # For paved and asphalt landcover + Tg[Tg == i] = ( + Ta[0] + + offset_Tg + * ( + 1 + + ratio_Tg + * torch.sin(2 * torch.pi / 365.25 * day + phi_Tg) + * torch.sign( + torch.tensor(location["latitude"], device=device) + ) + ) + + 4 + ) + Tm[Tm == i] = ( + torch.mean(Ta) + + ampl_Tm + * torch.sin(2 * torch.pi / 365.25 * day + phi_Tm) + * torch.sign(torch.tensor(location["latitude"], device=device)) + + offset_Tm + + 4 + ) + + elif i == 2: + # For roofs + Tg[Tg == i] = ( + Ta[0] + + offset_Tg + * ( + 1 + + ratio_Tg + * torch.sin(2 * torch.pi / 365.25 * day + phi_Tg) + * torch.sign( + torch.tensor(location["latitude"], device=device) + ) + ) + + 4 + ) + Tm[Tm == i] = torch.mean(Ta) + offset_Tm + + elif i == 5: + # For grass surfaces + Tg[Tg == i] = Ta[0] + offset_Tg * ( + 1 + + ratio_Tg + * torch.sin(2 * torch.pi / 365.25 * day + phi_Tg) + * torch.sign(torch.tensor(location["latitude"], device=device)) + ) + Tm[Tm == i] = ( + torch.mean(Ta) + + ampl_Tm + * torch.sin(2 * torch.pi / 365.25 * day + phi_Tm) + * torch.sign(torch.tensor(location["latitude"], device=device)) + + offset_Tm + ) + + elif i == 6: + # For bare soil landcover + Tg[Tg == i] = ( + Ta[0] + + offset_Tg + * ( + 1 + + ratio_Tg + * torch.sin(2 * torch.pi / 365.25 * day + phi_Tg) + * torch.sign( + torch.tensor(location["latitude"], device=device) + ) + ) + + 2 + ) + Tm[Tm == i] = ( + torch.mean(Ta) + + ampl_Tm + * torch.sin(2 * torch.pi / 365.25 * day + phi_Tm) + * torch.sign(torch.tensor(location["latitude"], device=device)) + + offset_Tm + + 2 + ) + + elif i == 7: + # For water bodies + Tg[Tg == i] = Ta[0] + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return ( + Tg, + Tm, + Rn, + Rn_past, + G, + cap_grid, + diff_grid, + a1_grid, + a2_grid, + a3_grid, + ) + + +def surfaceTemperature_calc( + Kdown, + Ldown, + Rn, + Rn_past, + G, + Tg, + Tm, + alb, + emis, + cap, + diff, + lc_grid, + a1, + a2, + a3, + timestep, + RH, + shadow, + shadow_past, +): + """ + Calculation of the ground surface temperature + + Based on the force restore method with the ground heat flux modeled according to the heat storage flux formulation depicted in the OHM (Grimmond 1991). + A simple model is implemented to assess the surface temperature for water landcover (lc==7) since the OHM fails to model the corresponding ground heat + flux. The temporal integration is done using the Runge-Kutta 2nd order scheme + + :Parameters: + :Kdown: the downwelling shortwave radiation + :Ldown: the downwelling longwave radiation + :Rn_past: net radiation stored to calculate the radiation rate + :G: ground heat flux stored from the previous timestep + :Tg: ground surface temperature grid + :Tm: Temperature of the deep soil + :alb: albedo grid of the ground surface + :emis: emissivity grid of the surface + :cap: heat capacity grid of the material + :diff: thermal diffusivity grid of the material + :lc_grid: landcover grid to identify the water bodies + :a: coefficient grids related to the OHM + :timestep: the timestep between 2 iteration of the simulation in min + + :Return: + :Tg: Surface temperature + :Rn: Net radiation for the current timestep + :Rn_past: Stored net radiation for upcoming radiation rate calc + :G: Ground heat flux for the current timestep + """ + + # Store the past ground surface temperature + Tg_stored = Tg + + # Damping depths of the daily surface temperature wave + D = torch.sqrt((2 * diff) / (2 * math.pi / 86400)) + + ### Runge Kutta method for the surface temperature calc + # First estimate of the surface temperature and of the deep soil temperature given past ground heat flux + k1 = 2 * G / cap / D - 2 * math.pi / 86400 * (Tg - Tm) + Tg_temp = Tg + k1 * timestep + + ### Estimate k2 the surface temperature step based on updated heat fluxes + # The fluxes involved are calculated using the estimated Ts + Lup_temp = SBC * emis * (Tg_temp + 273.15) ** 4 + Ldown * ( + 1 - emis + ) # Temporary outgoing longwave rad (W.m-2) + Rn_temp = Kdown * (1 - alb) + Ldown - Lup_temp # Temporary net rad (W.m-2) + RnStar_temp = (Rn_temp - Rn) / 1 # Temporary radiation rate (W.m-2.h-1) + G_temp = ( + a1 * Rn_temp + a2 * RnStar_temp + a3 + ) # Temporary ground heat flux (W.m-2) + + # Damping of the ground heat flux if it increases (or drops) too quickly + deltaG = abs(G_temp - G) + radCriterion = abs( + a1 * (Rn_temp - Rn_past) + ) # Criterion regarding the radiation step + mask = torch.logical_and( + deltaG > radCriterion, abs(shadow - shadow_past) > 0.5 + ) # Grid of the pixels where the ground heat flux spikes + G_temp[mask] = G[mask] + torch.sign(G_temp - G)[mask] * radCriterion[mask] + + # Correction of the temperature estimates + k2 = 2 * G_temp / cap / D - 2 * math.pi / 86400 * (Tg_temp - Tm) + Tg += (k1 + k2) / 2 * timestep + + ### Finally calculation of the updated heat fluxes + Rn_past = Rn + G_past = G + Lup = SBC * emis * (Tg + 273.15) ** 4 + (1 - emis) * Ldown + Rn = (1 - alb) * Kdown + Ldown - Lup + Rn_star = (Rn - Rn_past) / 1 + G = a1 * Rn + a2 * Rn_star + a3 + + # Damping of the ground heat flux if it increases (or decreases) too quickly + deltaG = abs(G - G_past) + radCriterion = abs( + a1 * (Rn - Rn_past) + ) # Criterion regarding the radiation step + mask = torch.logical_and( + deltaG > radCriterion, abs(shadow - shadow_past) > 0.5 + ) # Grid of the pixels where the ground heat flux spikes + G[mask] = G_past[mask] + torch.sign(G - G_past)[mask] * radCriterion[mask] + + ### Water bodies surface temperature estimate + beta = 0.45 # Amount of shortwave rad absorbed by the water surface layer + thickness = 1 # Depth of the water layer + lamb = 2.260e6 # Latent heat of vaporisation + rho = 1000 # Density of water (kg.m-3) + Rn_water = ( + Kdown + * (1 - alb) + * ( + beta + + (1 - beta) * (1 - torch.exp(torch.tensor(-1, device=Tg.device))) + ) + + Ldown + - Lup + ) # Net radiation for the top water layer beta described the transmitted rad + _, es = saturated_vp(Tg) + E = 0.0858 * (es / 1000) * (1 - RH / 100) / 3600 / 1000 * rho * lamb + deltaTg = torch.clone(lc_grid) + deltaTg = ( + timestep + / cap + / thickness + * (Rn_water - E - diff * cap / thickness * (Tg - Tm)) + ) + Tg[lc_grid == 7] = Tg_stored[lc_grid == 7] + deltaTg[lc_grid == 7] + + return Tg, Rn, Rn_past, G + + +def outgoingLongwave_calc( + Tg, + Tgwall, + Ta, + Ldown, + emis, + alb, + buildings, + shadow, + sunwall, + walls, + rows, + cols, + sizepx, +): + """ + Calculation of the outgoing longwave radiation from the ground, + + :Parameters: + :Tg: ground surface temperature grid + :Tgwall: wall surface temperature grid + :Ta: air temperature grid + :emis: emissivity grid of the surface + :alb: albedo grid of the surface + :emis_wall: emissivity of the wall (for now float = 0.9) + :buildings: boolean grid, 0 if the landcover is roof and 1 if there is no building + :shadow: boolean grid, 0 when the pixel is shadowed and 1 when sunlit + :sunwall: Grid where non zero values indicate a sunlit wall and its height + :walls: grid containing the heights of the walls + :aspect: grid containing the angles of the normal dir to walls + :rows: number of rows in the grids + :cols: number of columns in the grids + :sizepx: size of a pixel in m (1/scale) + + :Return: + """ + + # Assessment of the distance from a pixel at which most of the radiation are received (cf view factor Lambert) + device = Tg.device + factor = torch.tensor( + 0.99, device=device + ) # Percentage of radiation accounted for + zs = 1.1 # in m + r_max = zs * torch.sqrt( + factor / (1 - factor) + ) # in m, maximum radius for the radiation calc + + # Emissivity of the wall + emis_wall = 0.9 + alb_wall = 0.2 + + # Copy of the sunlit wall grid and replacement of the wall height with 1 if sunlit + sunlitwall = sunwall + sunlitwall[sunlitwall > 0] = 1 + + # Boolean array 1 if the pixel is a wall, 0 if not + wallbol = walls > 0 + + # The alb grids only take into account the sunlit surfaces in the alb calculation albnosh calculate it for all the surfaces + albsunlit = alb * shadow + + # Boolean array 1 if the pixel is a wall, 0 if not + wallbol = (walls > 0) * 1 + + # step in meters between every iteration + step = 1 + + # Grid of the outgoing longwave radiation coming from the ground + Lup = (SBC * emis * (Tg + 273.15) ** 4 + Ldown * (1 - emis)) * buildings + + ### Initialize the ground view factor grids as torch.zeros() + # Upwelling longwave radiation + gvfLup = torch.zeros((rows, cols), device=device) + gvfLupE = torch.zeros((rows, cols), device=device) + gvfLupS = torch.zeros((rows, cols), device=device) + gvfLupW = torch.zeros((rows, cols), device=device) + gvfLupN = torch.zeros((rows, cols), device=device) + + # Albedo of the sunlit surfaces + gvfalbsun = torch.zeros((rows, cols), device=device) + gvfalbsunE = torch.zeros((rows, cols), device=device) + gvfalbsunS = torch.zeros((rows, cols), device=device) + gvfalbsunW = torch.zeros((rows, cols), device=device) + gvfalbsunN = torch.zeros((rows, cols), device=device) + + # Albedo complete + gvfalbtot = torch.zeros((rows, cols), device=device) + gvfalbtotE = torch.zeros((rows, cols), device=device) + gvfalbtotS = torch.zeros((rows, cols), device=device) + gvfalbtotW = torch.zeros((rows, cols), device=device) + gvfalbtotN = torch.zeros((rows, cols), device=device) + + # Longwave radiation coming from the side + gvfLsideE = torch.zeros((rows, cols), device=device) + gvfLsideS = torch.zeros((rows, cols), device=device) + gvfLsideW = torch.zeros((rows, cols), device=device) + gvfLsideN = torch.zeros((rows, cols), device=device) + + # Add the radiation from the pixel directly below, only for the total gvf + # Do not take the roofs into account for now + view_factor = (sizepx / 2) ** 2 / ((sizepx / 2) ** 2 + zs**2) + gvfLup = gvfLup + Lup * view_factor + gvfalbsun = gvfalbsun + albsunlit * view_factor * buildings + gvfalbtot = gvfalbtot + alb * view_factor * buildings + + # Division of the 360° field of view in 20 and convert the array in radian + azimuths = torch.linspace(18, 360, steps=20, device=device) + azimuths = azimuths * (torch.pi / 180) + + ### Loop for the number of azimuth values + for azimuth in azimuths: + # Copy of the building grid + building_copy = buildings.clone() + + # Boolean array 1 if the pixel is (or was) a wall, 0 if not + pastwalls = wallbol.clone() + + # Grid of the longwave radiation emitted by the walls + Lwall = SBC * emis_wall * (Tgwall + Ta + 273.15) ** 4 * wallbol + + # Initialisation of the tables + # First the ones containing the translated rasters (temporary) + building_temp = buildings.clone() + Lup_temp = Lup.clone() + Lwall_temp = Lwall.clone() + albsun_temp = albsunlit.clone() + albtot_temp = alb.clone() + walls_temp = torch.zeros((rows, cols), device=device) + sunlitwall_temp = torch.zeros((rows, cols), device=device) + onlywall_temp = torch.zeros((rows, cols), device=device) + + # Then the tables containing the sum of the radiations (or albedo) for this azimuth + Lup_sum = torch.zeros((rows, cols), device=device) + LsideE_sum = torch.zeros((rows, cols), device=device) + LsideN_sum = torch.zeros((rows, cols), device=device) + LsideW_sum = torch.zeros((rows, cols), device=device) + LsideS_sum = torch.zeros((rows, cols), device=device) + albsun_sum = torch.zeros((rows, cols), device=device) + albtot_sum = torch.zeros((rows, cols), device=device) + + ### Shadow casting algorithm + # Translation ranges from 1/2 a pixel to the max radius r_max + for r in torch.arange(sizepx / 2, r_max, step=step): + + # Step of the raster translation + dx = -torch.cos(azimuth) + dy = -torch.sin(azimuth) + + # Scale so that the grid is at least translated from 1px + if abs(dx) > abs(dy): + dx = -r * torch.sign(torch.cos(azimuth)) + dy = ( + -r + * abs(torch.tan(azimuth)) + * torch.sign(torch.sin(azimuth)) + ) + else: + dx = ( + -r + / abs(torch.tan(azimuth)) + * torch.sign(torch.cos(azimuth)) + ) + dy = -r * torch.sign(torch.sin(azimuth)) + + # Select the interested part of the initial raster and the translated one from their four corners and + # translating toward the direction azimuth = 0° for dx > 0 + if dx > 0: + x_select_start = dx + x_select_end = rows + x_transl_start = 0 + x_transl_end = rows - dx + else: + x_select_start = 0 + x_select_end = rows + dx + x_transl_start = -dx + x_transl_end = rows + + # translating toward the direction azimuth = 90° for dy > 0 + if dy > 0: + y_select_start = dy + y_select_end = cols + y_transl_start = 0 + y_transl_end = cols - dy + else: + y_select_start = 0 + y_select_end = cols + dy + y_transl_start = -dy + y_transl_end = cols + + # Copy the initial rasters and input inside translated raster temporary and changing every iteration + # Building grid + building_temp[ + int(x_transl_start) : math.ceil(x_transl_end), + int(y_transl_start) : math.ceil(y_transl_end), + ] = buildings[ + int(x_select_start) : math.ceil(x_select_end), + int(y_select_start) : math.ceil(y_select_end), + ] + + # Ground longwave radiation grid + Lup_temp[ + int(x_transl_start) : math.ceil(x_transl_end), + int(y_transl_start) : math.ceil(y_transl_end), + ] = Lup[ + int(x_select_start) : math.ceil(x_select_end), + int(y_select_start) : math.ceil(y_select_end), + ] + + # Wall longwave radiation grid + Lwall_temp[ + int(x_transl_start) : math.ceil(x_transl_end), + int(y_transl_start) : math.ceil(y_transl_end), + ] = Lwall[ + int(x_select_start) : math.ceil(x_select_end), + int(y_select_start) : math.ceil(y_select_end), + ] + + # Albedo grid for the sunlit area + albsun_temp[ + int(x_transl_start) : math.ceil(x_transl_end), + int(y_transl_start) : math.ceil(y_transl_end), + ] = albsunlit[ + int(x_select_start) : math.ceil(x_select_end), + int(y_select_start) : math.ceil(y_select_end), + ] + + # Albedo grid for all the area + albtot_temp[ + int(x_transl_start) : math.ceil(x_transl_end), + int(y_transl_start) : math.ceil(y_transl_end), + ] = alb[ + int(x_select_start) : math.ceil(x_select_end), + int(y_select_start) : math.ceil(y_select_end), + ] + + # Sunlit wall grid + sunlitwall_temp[ + int(x_transl_start) : math.ceil(x_transl_end), + int(y_transl_start) : math.ceil(y_transl_end), + ] = sunlitwall[ + int(x_select_start) : math.ceil(x_select_end), + int(y_select_start) : math.ceil(y_select_end), + ] + + # All walls grid + walls_temp[ + int(x_transl_start) : math.ceil(x_transl_end), + int(y_transl_start) : math.ceil(y_transl_end), + ] = wallbol[ + int(x_select_start) : math.ceil(x_select_end), + int(y_select_start) : math.ceil(y_select_end), + ] + + # Change the boolean building grid, if the px was already a building it remains one (px value = 0) + building_copy = torch.min(building_copy, building_temp) + + # For each pixel add the translated Lup to the received rad if there where there is no building + view_factor = ((r + step) ** 2 / (zs**2 + (r + step) ** 2)) - ( + r**2 / (zs**2 + r**2) + ) + Lup_sum += Lup_temp * view_factor * building_copy / 20 + albsun_sum += albsun_temp * view_factor * building_copy / 20 + albtot_sum += albtot_temp * view_factor * building_copy / 20 + + # Create a boolean grid to assert that the sunlit walls are not inside a building + onlywall_temp = torch.logical_and( + walls_temp, torch.logical_not(pastwalls) + ) + onlywall_temp = onlywall_temp * building_copy + onlysunwall_temp = sunlitwall_temp * building_copy + + pastwalls = torch.logical_or(pastwalls, walls_temp) + + # Compute the view factor of the wall surface for a given translatio distance + viewfactor_wall = ( + 1 + / 2 ** (1 / 2) + / 3 + * torch.sqrt( + 1 + (r + step) / torch.sqrt((r + step) ** 2 + zs**2) + ) + * ( + 2 + + (r + sizepx / 2) + / torch.sqrt((r + sizepx / 2) ** 2 + zs**2) + ) + * ( + 1 + - (r + sizepx / 2) + / torch.sqrt((r + sizepx / 2) ** 2 + zs**2) + ) + / zs + * torch.sqrt((r + sizepx / 2) ** 2 + zs**2) + ) + + # Then add the radiation incoming from those walls + Lup_sum += ( + onlywall_temp + * Lwall_temp + * viewfactor_wall + * building_copy + / 20 + ) + albsun_sum += ( + onlysunwall_temp + * alb_wall + * viewfactor_wall + * building_copy + / 20 + ) + albtot_sum += ( + onlywall_temp * alb_wall * viewfactor_wall * building_copy / 20 + ) + + # Finally add the radiation in Lside + dphi = torch.arctan((r + step) / zs) - torch.arctan(r / zs) + dtrigo = zs / torch.sqrt(r**2 + zs**2) * r / torch.sqrt( + r**2 + zs**2 + ) - zs / torch.sqrt((r + step) ** 2 + zs**2) * ( + r + step + ) / torch.sqrt( + (r + step) ** 2 + zs**2 + ) + + # Calculation of the solid angle for each of the cardinal points + # plus add the radiation from a potential wall + steradiansW, steradiansS, steradiansE, steradiansN = 0, 0, 0, 0 + if (azimuth >= 0) and (azimuth < torch.pi): + dthetaW = 2 * torch.pi / 20 + steradiansW += dthetaW * (dphi + dtrigo) / 2 + LsideW_sum += onlywall_temp * Lwall_temp * viewfactor_wall / 10 + + if (azimuth >= torch.pi / 2) and (azimuth < 3 * torch.pi / 2): + dthetaS = 2 * torch.pi / 20 + steradiansS += dthetaS * (dphi + dtrigo) / 2 + LsideS_sum += onlywall_temp * Lwall_temp * viewfactor_wall / 10 + + if (azimuth >= torch.pi) and (azimuth < 2 * torch.pi): + dthetaE = 2 * torch.pi / 20 + steradiansE += dthetaE * (dphi + dtrigo) / 2 + LsideE_sum += onlywall_temp * Lwall_temp * viewfactor_wall / 10 + + if (azimuth >= 3 * torch.pi / 2) or (azimuth < torch.pi / 2): + dthetaN = 2 * torch.pi / 20 + steradiansN += dthetaN * (dphi + dtrigo) / 2 + LsideN_sum += onlywall_temp * Lwall_temp * viewfactor_wall / 10 + + LsideW_sum += Lup_temp / torch.pi * steradiansW * building_copy + LsideS_sum += Lup_temp / torch.pi * steradiansS * building_copy + LsideE_sum += Lup_temp / torch.pi * steradiansE * building_copy + LsideN_sum += Lup_temp / torch.pi * steradiansN * building_copy + + ### Add the value for the computed part of the field of view + gvfLup += Lup_sum + gvfalbsun += albsun_sum + gvfalbtot += albtot_sum + + # Add the value if the azimuth correspond to the side of the compass + if (azimuth >= 0) and (azimuth < torch.pi): + gvfLupW += Lup_sum + gvfalbsunW += albsun_sum + gvfalbtotW += albtot_sum + gvfLsideW += LsideW_sum + + if (azimuth >= torch.pi / 2) and (azimuth < 3 * torch.pi / 2): + gvfLupS += Lup_sum + gvfalbsunS += albsun_sum + gvfalbtotS += albtot_sum + gvfLsideS += LsideS_sum + + if (azimuth >= torch.pi) and (azimuth < 2 * torch.pi): + gvfLupE += Lup_sum + gvfalbsunE += albsun_sum + gvfalbtotE += albtot_sum + gvfLsideE += LsideE_sum + + if (azimuth >= 3 * torch.pi / 2) or (azimuth < torch.pi / 2): + gvfLupN += Lup_sum + gvfalbsunN += albsun_sum + gvfalbtotN += albtot_sum + gvfLsideN += LsideN_sum + + # If the px is associated with a roof landcover, for now Lup = 0 + # Here their Lup value is allocated to those px + gvfLup += (SBC * emis * (Tg + 273.15) ** 4) * (buildings * -1 + 1) + gvfLsideE += (SBC * emis * (Tg + 273.15) ** 4) * 0.5 * (buildings * -1 + 1) + gvfLsideN += (SBC * emis * (Tg + 273.15) ** 4) * 0.5 * (buildings * -1 + 1) + gvfLsideW += (SBC * emis * (Tg + 273.15) ** 4) * 0.5 * (buildings * -1 + 1) + gvfLsideS += (SBC * emis * (Tg + 273.15) ** 4) * 0.5 * (buildings * -1 + 1) + gvfalbsun += albsunlit * (buildings * -1 + 1) + gvfalbtot += alb * (buildings * -1 + 1) + gvfalbsunE += albsunlit * 0.5 * (buildings * -1 + 1) + gvfalbsunN += albsunlit * 0.5 * (buildings * -1 + 1) + gvfalbsunW += albsunlit * 0.5 * (buildings * -1 + 1) + gvfalbsunS += albsunlit * 0.5 * (buildings * -1 + 1) + gvfalbtotE += alb * 0.5 * (buildings * -1 + 1) + gvfalbtotN += alb * 0.5 * (buildings * -1 + 1) + gvfalbtotW += alb * 0.5 * (buildings * -1 + 1) + gvfalbtotS += alb * 0.5 * (buildings * -1 + 1) + + return ( + gvfLup, + gvfalbsun, + gvfalbtot, + gvfLupE, + gvfalbsunE, + gvfalbtotE, + gvfLupS, + gvfalbsunS, + gvfalbtotS, + gvfLupW, + gvfalbsunW, + gvfalbtotW, + gvfLupN, + gvfalbsunN, + gvfalbtotN, + gvfLsideW, + gvfLsideS, + gvfLsideE, + gvfLsideN, + ) diff --git a/functions/SOLWEIGpython/gvf_2015a.py b/functions/SOLWEIGpython/gvf_2015a.py index 07dfb64..b73f426 100644 --- a/functions/SOLWEIGpython/gvf_2015a.py +++ b/functions/SOLWEIGpython/gvf_2015a.py @@ -27,8 +27,9 @@ def gvf_2015a( landcover, ): - # Search directions for Ground View Factors (GVF) - azimuthA = np.arange(5, 359, 20) + azimuthA = np.arange( + 5, 359, 20 + ) # Search directions for Ground View Factors (GVF) #### Ground View Factors #### gvfLup = np.zeros((rows, cols)) diff --git a/functions/SOLWEIGpython/gvf_2018a.py b/functions/SOLWEIGpython/gvf_2018a.py index b7e3b15..d3bbfd1 100644 --- a/functions/SOLWEIGpython/gvf_2018a.py +++ b/functions/SOLWEIGpython/gvf_2018a.py @@ -27,8 +27,9 @@ def gvf_2018a( lc_grid, landcover, ): - # Search directions for Ground View Factors (GVF) - azimuthA = np.arange(5, 359, 20) + azimuthA = np.arange( + 5, 359, 20 + ) # Search directions for Ground View Factors (GVF) #### Ground View Factors #### gvfLup = np.zeros((rows, cols)) diff --git a/functions/SOLWEIGpython/patch_characteristics.py b/functions/SOLWEIGpython/patch_characteristics.py index 4ddd816..2a1ce27 100644 --- a/functions/SOLWEIGpython/patch_characteristics.py +++ b/functions/SOLWEIGpython/patch_characteristics.py @@ -1,8 +1,9 @@ import numpy as np +from copy import deepcopy from . import sunlit_shaded_patches -""" This function defines if a patch seen from a pixel is sky, building or vegetation. - It also calculates if a building patch is sunlit or shaded. From this it estimates +""" This function defines if a patch seen from a pixel is sky, building or vegetation. + It also calculates if a building patch is sunlit or shaded. From this it estimates corresponding longwave radiation originating from each surface.""" @@ -53,8 +54,7 @@ def define_patch_characteristics( Lnorth = np.zeros((rows, cols)) Lsouth = np.zeros((rows, cols)) - # Define patch characteristics (sky, vegetation or building, and sunlit or - # shaded if building) + # Define patch characteristics (sky, vegetation or building, and sunlit or shaded if building) for idx in range(patch_altitude.shape[0]): # Calculations for patches on sky, shmat = 1 = sky is visible temp_sky = (shmat[:, :, idx] == 1) & (vegshmat[:, :, idx] == 1) @@ -65,8 +65,7 @@ def define_patch_characteristics( # Longwave radiation from sky to horizontal surface Lside_sky += temp_sky * Lsky_side[idx, 2] - # Calculations for patches that are vegetation, vegshmat = 0 = shade - # from vegetation + # Calculations for patches that are vegetation, vegshmat = 0 = shade from vegetation temp_vegsh = (vegshmat[:, :, idx] == 0) | ( vbshvegshmat[:, :, idx] == 0 ) @@ -89,8 +88,7 @@ def define_patch_characteristics( * temp_vegsh ) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth[idx] > 360) or (patch_azimuth[idx] < 180): Least += ( temp_sky @@ -144,8 +142,7 @@ def define_patch_characteristics( * np.cos((0 - patch_azimuth[idx]) * deg2rad) ) - # Calculations for patches that are buildings, shmat = 0 = shade from - # buildings + # Calculations for patches that are buildings, shmat = 0 = shade from buildings temp_vbsh = (1 - shmat[:, :, idx]) * vbshvegshmat[:, :, idx] temp_sh = temp_vbsh == 1 azimuth_difference = np.abs(solar_azimuth - patch_azimuth[idx]) @@ -159,8 +156,7 @@ def define_patch_characteristics( and (azimuth_difference < 270) and (solar_altitude > 0) ): - # Calculate which patches defined as buildings that are sunlit or - # shaded + # Calculate which patches defined as buildings that are sunlit or shaded sunlit_patches, shaded_patches = ( sunlit_shaded_patches.shaded_or_sunlit( solar_altitude, @@ -171,8 +167,7 @@ def define_patch_characteristics( ) ) - # Calculate longwave radiation from sunlit walls to vertical - # surface + # Calculate longwave radiation from sunlit walls to vertical surface Lside_sun += ( sunlit_surface * sunlit_patches @@ -180,8 +175,7 @@ def define_patch_characteristics( * np.cos(patch_altitude[idx] * deg2rad) * temp_sh ) - # Calculate longwave radiation from shaded walls to vertical - # surface + # Calculate longwave radiation from shaded walls to vertical surface Lside_sh += ( shaded_surface * shaded_patches @@ -190,8 +184,7 @@ def define_patch_characteristics( * temp_sh ) - # Calculate longwave radiation from sunlit walls to horizontal - # surface + # Calculate longwave radiation from sunlit walls to horizontal surface Ldown_sun += ( sunlit_surface * sunlit_patches @@ -199,8 +192,7 @@ def define_patch_characteristics( * np.sin(patch_altitude[idx] * deg2rad) * temp_sh ) - # Calculate longwave radiation from shaded walls to horizontal - # surface + # Calculate longwave radiation from shaded walls to horizontal surface Ldown_sh += ( shaded_surface * shaded_patches @@ -209,8 +201,7 @@ def define_patch_characteristics( * temp_sh ) - # Portion into cardinal directions to be used for standing box or - # POI output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth[idx] > 360) or (patch_azimuth[idx] < 180): Least += ( sunlit_surface @@ -281,8 +272,7 @@ def define_patch_characteristics( ) else: - # Calculate longwave radiation from shaded walls reaching a - # vertical surface + # Calculate longwave radiation from shaded walls reaching a vertical surface Lside_sh += ( shaded_surface * steradian[idx] @@ -290,8 +280,7 @@ def define_patch_characteristics( * temp_sh ) - # Calculate longwave radiation from shaded walls reaching a - # horizontal surface + # Calculate longwave radiation from shaded walls reaching a horizontal surface Ldown_sh += ( shaded_surface * steradian[idx] @@ -299,8 +288,7 @@ def define_patch_characteristics( * temp_sh ) - # Portion into cardinal directions to be used for standing box or - # POI output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth[idx] > 360) or (patch_azimuth[idx] < 180): Least += ( shaded_surface @@ -359,8 +347,7 @@ def define_patch_characteristics( * temp_sh ) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth[idx] > 360) or (patch_azimuth[idx] < 180): Least += ( reflected_on_surfaces @@ -394,12 +381,10 @@ def define_patch_characteristics( * np.cos((0 - patch_azimuth[idx]) * deg2rad) ) - # Sum of all Lside components (sky, vegetation, sunlit and shaded - # buildings, reflected) + # Sum of all Lside components (sky, vegetation, sunlit and shaded buildings, reflected) Lside = Lside_sky + Lside_veg + Lside_sh + Lside_sun + Lside_ref - # Sum of all Lside components (sky, vegetation, sunlit and shaded - # buildings, reflected) + # Sum of all Lside components (sky, vegetation, sunlit and shaded buildings, reflected) Ldown = Ldown_sky + Ldown_veg + Ldown_sh + Ldown_sun + Ldown_ref return ( @@ -426,11 +411,9 @@ def hemispheric_image(poi, sh, vegsh, vbshvegsh, voxelmat, wallScheme): for idy in range(sh.shape[2]): # Calculations for patches on sky, shmat = 1 = sky is visible temp_sky = (sh[:, :, idy] == 1) & (vegsh[:, :, idy] == 1) - # Calculations for patches that are vegetation, vegshmat = 0 = - # shade from vegetation + # Calculations for patches that are vegetation, vegshmat = 0 = shade from vegetation temp_vegsh = (vegsh[:, :, idy] == 0) | (vbshvegsh[:, :, idy] == 0) - # Calculations for patches that are buildings, shmat = 0 = shade - # from buildings + # Calculations for patches that are buildings, shmat = 0 = shade from buildings temp_vbsh = (1 - sh[:, :, idy]) * vbshvegsh[:, :, idy] temp_sh = temp_vbsh == 1 if wallScheme: diff --git a/functions/SOLWEIGpython/patch_radiation.py b/functions/SOLWEIGpython/patch_radiation.py index c1a30a2..651e014 100644 --- a/functions/SOLWEIGpython/patch_radiation.py +++ b/functions/SOLWEIGpython/patch_radiation.py @@ -32,8 +32,7 @@ def longwave_from_sky(sky, Lsky_side, Lsky_down, patch_azimuth): Lwest = np.zeros((sky.shape[0], sky.shape[1])) Lnorth = np.zeros((sky.shape[0], sky.shape[1])) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = sky * Lsky_side * np.cos((90 - patch_azimuth) * deg2rad) if (patch_azimuth > 90) and (patch_azimuth < 270): @@ -87,8 +86,7 @@ def longwave_from_veg( Lwest = np.zeros((vegetation.shape[0], vegetation.shape[1])) Lnorth = np.zeros((vegetation.shape[0], vegetation.shape[1])) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( vegetation_surface @@ -161,8 +159,6 @@ def longwave_from_buildings( and (azimuth_difference < 270) and (solar_altitude > 0) ): - # Calculate which patches defined as buildings that are sunlit or shaded - # sunlit_patches, shaded_patches = sunlit_shaded_patches.shaded_or_sunlit(solar_altitude, solar_azimuth, patch_altitude, patch_azimuth, asvf) # Calculate longwave radiation from sunlit walls to vertical surface Lside_sun = ( @@ -198,8 +194,7 @@ def longwave_from_buildings( * building ) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( sunlit_surface @@ -270,18 +265,15 @@ def longwave_from_buildings( ) else: - # Calculate longwave radiation from shaded walls reaching a vertical - # surface + # Calculate longwave radiation from shaded walls reaching a vertical surface Lside_sh = shaded_surface * steradian * angle_of_incidence * building Lside_sun = np.zeros((Lside_sh.shape[0], Lside_sh.shape[1])) - # Calculate longwave radiation from shaded walls reaching a horizontal - # surface + # Calculate longwave radiation from shaded walls reaching a horizontal surface Ldown_sh = shaded_surface * steradian * angle_of_incidence_h * building Ldown_sun = np.zeros((Lside_sh.shape[0], Lside_sh.shape[1])) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( shaded_surface @@ -349,19 +341,14 @@ def longwave_from_buildings_wallScheme( Lwest = np.zeros((voxelMaps.shape[0], voxelMaps.shape[1])) Lnorth = np.zeros((voxelMaps.shape[0], voxelMaps.shape[1])) - # print(voxelMaps) - # print(voxelTable.head()) l = list(np.unique(voxelMaps)[1:]) - # print(l) a = dict(voxelTable.loc[l, "LongwaveRadiation"]) - # print(a) patch_radiation = np.vectorize(a.get)(voxelMaps).astype(float) patch_radiation[np.isnan(patch_radiation)] = 0 Lside += patch_radiation * steradian * angle_of_incidence Ldown += patch_radiation * steradian * angle_of_incidence_h - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( patch_radiation @@ -441,8 +428,7 @@ def reflected_longwave( (reflecting_surface.shape[0], reflecting_surface.shape[1]) ) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( reflected_radiation diff --git a/functions/SOLWEIGpython/sunonsurface_2018a.py b/functions/SOLWEIGpython/sunonsurface_2018a.py index 8485e20..0acbc6c 100644 --- a/functions/SOLWEIGpython/sunonsurface_2018a.py +++ b/functions/SOLWEIGpython/sunonsurface_2018a.py @@ -93,7 +93,7 @@ def sunonsurface_2018a( signsinazimuth = np.sign(sinazimuth) signcosazimuth = np.sign(cosazimuth) - # The Shadow casting algoritm + ## The Shadow casting algoritm for n in np.arange(0, second): if (pibyfour <= azimuth and azimuth < threetimespibyfour) or ( fivetimespibyfour <= azimuth and azimuth < seventimespibyfour @@ -119,24 +119,20 @@ def sunonsurface_2018a( yp1 = -((dy - absdy) / 2) yp2 = sizey - (dy + absdy) / 2 - tempbu[int(xp1): int(xp2), int(yp1): int(yp2)] = buildings[ - int(xc1): int(xc2), - # moving building - int(yc1): int(yc2), - ] - tempsh[int(xp1): int(xp2), int(yp1): int(yp2)] = shadow[ - int(xc1): int(xc2), int(yc1): int(yc2) + tempbu[int(xp1) : int(xp2), int(yp1) : int(yp2)] = buildings[ + int(xc1) : int(xc2), int(yc1) : int(yc2) + ] # moving building + tempsh[int(xp1) : int(xp2), int(yp1) : int(yp2)] = shadow[ + int(xc1) : int(xc2), int(yc1) : int(yc2) ] # moving shadow - tempLupsh[int(xp1): int(xp2), int(yp1): int(yp2)] = Lup[ - int(xc1): int(xc2), int(yc1): int(yc2) + tempLupsh[int(xp1) : int(xp2), int(yp1) : int(yp2)] = Lup[ + int(xc1) : int(xc2), int(yc1) : int(yc2) ] # moving Lup/shadow - tempalbsh[int(xp1): int(xp2), int(yp1): int(yp2)] = albshadow[ - int(xc1): int(xc2), - # moving Albedo/shadow - int(yc1): int(yc2), - ] - tempalbnosh[int(xp1): int(xp2), int(yp1): int(yp2)] = alb[ - int(xc1): int(xc2), int(yc1): int(yc2) + tempalbsh[int(xp1) : int(xp2), int(yp1) : int(yp2)] = albshadow[ + int(xc1) : int(xc2), int(yc1) : int(yc2) + ] # moving Albedo/shadow + tempalbnosh[int(xp1) : int(xp2), int(yp1) : int(yp2)] = alb[ + int(xc1) : int(xc2), int(yc1) : int(yc2) ] # moving Albedo f = np.min([f, tempbu], axis=0) # utsmetning av buildings @@ -152,11 +148,9 @@ def sunonsurface_2018a( albnosh = tempalbnosh * f weightsumalbnosh = weightsumalbnosh + albnosh - tempwallsun[int(xp1): int(xp2), int(yp1): int(yp2)] = sunwall[ - int(xc1): int(xc2), - # moving buildingwall insun image - int(yc1): int(yc2), - ] + tempwallsun[int(xp1) : int(xp2), int(yp1) : int(yp2)] = sunwall[ + int(xc1) : int(xc2), int(yc1) : int(yc2) + ] # moving buildingwall insun image tempb = tempwallsun * f tempbwall = f * -1 + 1 tempbub = ((tempb + tempbub) > 0) * 1 @@ -201,8 +195,9 @@ def sunonsurface_2018a( ) elif azilow < 0 and azihigh <= 2 * np.pi: # 0 to 90 azilow = azilow + 2 * np.pi - # (SHADOW) # check for the -1 - facesh = np.logical_or(aspect > azilow, aspect <= azihigh) * -1 + 1 + facesh = ( + np.logical_or(aspect > azilow, aspect <= azihigh) * -1 + 1 + ) # (SHADOW) # check for the -1 elif azilow > 0 and azihigh >= 2 * np.pi: # 270 to 360 azihigh = azihigh - 2 * np.pi facesh = ( diff --git a/functions/SOLWEIGpython/wallOfInterest.py b/functions/SOLWEIGpython/wallOfInterest.py index 9d6b92d..fbc4bce 100644 --- a/functions/SOLWEIGpython/wallOfInterest.py +++ b/functions/SOLWEIGpython/wallOfInterest.py @@ -1,5 +1,6 @@ import numpy as np from qgis.core import QgsVectorLayer +from osgeo.gdalconst import * def pointOfInterest(poilyr, poi_field, scale, gdal_dsm): diff --git a/functions/SOLWEIGpython/wall_cover.py b/functions/SOLWEIGpython/wall_cover.py index 992379d..da81e5c 100644 --- a/functions/SOLWEIGpython/wall_cover.py +++ b/functions/SOLWEIGpython/wall_cover.py @@ -26,25 +26,22 @@ def get_wall_cover(voxelTable, lcgrid, dsm, lc_params): for i in range(voxelTable.shape[0]): # Temporary lc_grid based on kernel and wall y and x position temp_lc = ( - lcgrid[ypos[i] - 1: ypos[i] + 2, xpos[i] - 1: xpos[i] + 2] + lcgrid[ypos[i] - 1 : ypos[i] + 2, xpos[i] - 1 : xpos[i] + 2] * domain ) # Temporary dsm based on kernel and wall y and x position temp_dsm = ( - dsm[ypos[i] - 1: ypos[i] + 2, xpos[i] - 1: xpos[i] + 2] * domain + dsm[ypos[i] - 1 : ypos[i] + 2, xpos[i] - 1 : xpos[i] + 2] * domain ) - # Temporary code based on highest pixel in temp_dsm where temp_lc is a - # building + # Temporary code based on highest pixel in temp_dsm where temp_lc is a building temp_code = temp_lc[((temp_lc > 99) & (temp_dsm == temp_dsm.max()))] - # If more than one option in temp_code, use the first one #TODO CHANGE - # TO MOST COMMON + # If more than one option in temp_code, use the first one #TODO CHANGE TO MOST COMMON if len(temp_code) > 1: temp_code = temp_code[0].astype(int) elif len(temp_code) == 1: temp_code = temp_code[0].astype(int) - # If no wall type specified in land cover for specific wall pixel, set - # to concrete + # If no wall type specified in land cover for specific wall pixel, set to concrete elif temp_code.size == 0: temp_code = 101 diff --git a/functions/SOLWEIGpython/wall_surface_temperature.py b/functions/SOLWEIGpython/wall_surface_temperature.py index 47d7b6b..7e2505c 100644 --- a/functions/SOLWEIGpython/wall_surface_temperature.py +++ b/functions/SOLWEIGpython/wall_surface_temperature.py @@ -45,8 +45,9 @@ def load_walls( ) # Initiate/declare new columns used by SOLWEIG and parameterization scheme for wall surface temperatures - # Initial wall temperature is set to air temperature - voxelTable["wallTemperature"] = Ta + voxelTable["wallTemperature"] = ( + Ta # Initial wall temperature is set to air temperature + ) voxelTable["timeStep"] = timeStep # Add columns filled later @@ -165,8 +166,7 @@ def load_walls( voxelTable = voxelTable.set_index("voxelId") # Calculate fractions - # Non-sky fraction = (1 - svf at ground level) - 0.5 (ground surface seen - # from wall) + # Non-sky fraction = (1 - svf at ground level) - 0.5 (ground surface seen from wall) building_fraction = 1 - voxelTable["svfbu_at_ground"].to_numpy() - 0.5 building_fraction[building_fraction < 0] = 0.0 veg_fraction = 1 - voxelTable["svfaveg_at_ground"].to_numpy() - 0.5 @@ -181,12 +181,10 @@ def load_walls( building_fraction + sky_fraction + ground_fraction + veg_fraction ) - # Set thermal effusivity according to wall type (either from GUI or from - # landcover) + # Set thermal effusivity according to wall type (either from GUI or from landcover) if landcover == 1: unique_landcover = np.unique(lcgrid) - # Unique wall codes for unique wall types, e.g. brick, concrete, wood, - # etc. + # Unique wall codes for unique wall types, e.g. brick, concrete, wood, etc. unique_walls = unique_landcover[unique_landcover > 99].astype(int) # Get wall properties for wall surface temperature scheme if unique_walls.size > 1: @@ -284,8 +282,7 @@ def surface_temperature_calc( dT = np.zeros((effusivity.shape[0])) Ts = np.zeros((effusivity.shape[0])) - # Calculate heat flux based on Ts in previous time step and Kin, Lin and - # Ta from current time step + # Calculate heat flux based on Ts in previous time step and Kin, Lin and Ta from current time step Lout_temp = wall_emissivity * SBC * (Ts_previous + 273.15) ** 4 # sensible_temp = 20 * (Ts_previous - Ta) energy_in_temp = Kin + Lin - Lout_temp # - sensible_temp @@ -294,8 +291,7 @@ def surface_temperature_calc( # Surface temperature Ts_current = Ta + dT - # Estimate surface temperature with Ts of current timestep, i.e. not - # Ts_previous + # Estimate surface temperature with Ts of current timestep, i.e. not Ts_previous Lout_temp = wall_emissivity * SBC * (Ts_current + 273.15) ** 4 energy_in_temp = Kin + Lin - Lout_temp # - sensible_temp dT = step_heating(energy_in_temp, effusivity, t) @@ -332,11 +328,10 @@ def wall_surface_temperature( deg2rad = np.pi / 180 # Estimate shadow on wall, i.e. how far up the wall that the shade stretches (or how far down the wall that the sun reaches) - # Starting value is zero, i.e. the entire wall is shaded - voxelTable["wallShade"] = 0 - # If sun is above horizon, get wall shadow height on wall from wallsh - # calculated in shadowingfunction_wallheight_23/13 in - # Solweig_2022a_calc_forprocessing.py + voxelTable["wallShade"] = ( + 0 # Starting value is zero, i.e. the entire wall is shaded + ) + # If sun is above horizon, get wall shadow height on wall from wallsh calculated in shadowingfunction_wallheight_23/13 in Solweig_2022a_calc_forprocessing.py if altitude > 0: # Starting value of wallShadeHeight voxelTable["wallShadeHeight"] = 0 @@ -369,8 +364,7 @@ def wall_surface_temperature( else: voxelTable["wallShadeHeight"] = voxelTable["wallHeight_exact"] - # Ldown and Lup for wall pixels used when estimating the amount of - # longwave received from surrounding surfaces (ground and reflected) + # Ldown and Lup for wall pixels used when estimating the amount of longwave received from surrounding surfaces (ground and reflected) Ldown_array = np.zeros((voxelTable.shape[0])) Lup_array = np.zeros((voxelTable.shape[0])) for idx in np.arange(voxelTable.shape[0]): @@ -383,27 +377,23 @@ def wall_surface_temperature( # If sun above horizon if altitude > 0: # Cylindric wedge based on building height angle from svf and solar zenith angle - # Fraction shadow on building walls based on sun alt and svf F_sh = cylindric_wedge_voxel( (90 - altitude) * deg2rad, voxelTable["svfalfa"].to_numpy() - ) + ) # Fraction shadow on building walls based on sun alt and svf F_sh[np.isnan(F_sh)] = 0.5 - # Only half of hemisphere visible from a wall, therefore the entire - # seen area can be sunlit, compared to an open space where only half - # can be sunlit + # Only half of hemisphere visible from a wall, therefore the entire seen area can be sunlit, compared to an open space where only half can be sunlit F_sh = 2.0 * F_sh - 1.0 # (cylindric_wedge scaled 0-1) - # Fraction of sunlit and shaded building surfaces seen by a building - # wall based on its aspect and the azimuth of the sun + # Fraction of sunlit and shaded building surfaces seen by a building wall based on its aspect and the azimuth of the sun wallSun = np.abs(voxelTable["wallAspect"].to_numpy() - azimuth) wallSun = wallSun / 180.0 wallSun[wallSun > 1.0] = 2 - (wallSun[wallSun > 1.0]) - # Scaling wall shadows to avoid totally shaded and totally sunlit walls - wallSun = 0.2 + wallSun * 0.6 + wallSun = ( + 0.2 + wallSun * 0.6 + ) # Scaling wall shadows to avoid totally shaded and totally sunlit walls - # Temperature in shade and sun based on mean values for all voxels, - # i.e. shade = mean of all shaded and sun = mean of all sunlit + # Temperature in shade and sun based on mean values for all voxels, i.e. shade = mean of all shaded and sun = mean of all sunlit ts_shade = ( voxelTable.loc[voxelTable["wallShade"] == 0, "wallTemperature"] .to_numpy() @@ -415,26 +405,23 @@ def wall_surface_temperature( .mean() ) - # Calculation of longwave radiation received by the voxels based on - # sunlit and shaded surroundings + # Calculation of longwave radiation received by the voxels based on sunlit and shaded surroundings Lwallsun = ( SBC * voxelTable["wallEmissivity"].to_numpy() * ((ts_sun + 273.15) ** 4) - * - # (1 - svf - 0.5) istället för viktwall? - voxelTable["building_fraction"].to_numpy() + * voxelTable["building_fraction"].to_numpy() * (1.0 - F_sh) - ) * wallSun + ) * wallSun # (1 - svf - 0.5) istället för viktwall? Lwallsh = ( SBC * voxelTable["wallEmissivity"].to_numpy() * ((ts_shade + 273.15) ** 4) - * - # (1 - svf - 0.5) istället för viktwall? - voxelTable["building_fraction"].to_numpy() + * voxelTable["building_fraction"].to_numpy() * (1.0 - F_sh) - ) * (1 - wallSun) + ) * ( + 1 - wallSun + ) # (1 - svf - 0.5) istället för viktwall? Lwallsh += ( SBC @@ -460,16 +447,16 @@ def wall_surface_temperature( ) # * 0.5 # (1 - svf - 0.5) istället för viktwall? # Received longwave radiation from vegetation - # * 0.5 # (1 - svf - 0.5) istället för viktwall? Lveg = ( SBC * voxelTable["wallEmissivity"].to_numpy() * (Ta + 273.15) ** 4 * voxelTable["veg_fraction"].to_numpy() - ) + ) # * 0.5 # (1 - svf - 0.5) istället för viktwall? # Received longwave radiation from the sky - # * 0.5 # svf istället för viktwall? - Lsky = SBC * esky * ((Ta + 273.15) ** 4) * voxelTable["SVF_fix"].to_numpy() + Lsky = ( + SBC * esky * ((Ta + 273.15) ** 4) * voxelTable["SVF_fix"].to_numpy() + ) # * 0.5 # svf istället för viktwall? # Received reflected longwave radiation Lrefl = ( (1.0 - voxelTable["wallEmissivity"].to_numpy()) diff --git a/functions/SOLWEIGpython/wallsAsNetCDF.py b/functions/SOLWEIGpython/wallsAsNetCDF.py index c1cd37e..2d2e91e 100644 --- a/functions/SOLWEIGpython/wallsAsNetCDF.py +++ b/functions/SOLWEIGpython/wallsAsNetCDF.py @@ -1,7 +1,7 @@ try: import xarray as xr import rioxarray -except BaseException: +except: pass import numpy as np @@ -14,8 +14,7 @@ def walls_as_netcdf( # rows = number of rows (latitudinal position) # cols = number of columns (longitudinal position) # level = number of voxel levels (elevation position) - # raster_path is used to load an existing .tif layer and create arrays - # with latitudes and longitudes to be used in xarray/NetCDF + # raster_path is used to load an existing .tif layer and create arrays with latitudes and longitudes to be used in xarray/NetCDF # Highest number of voxels used to determine z/height level of NetCDF # levels = voxelTable.loc[voxelTable['voxelHeight'] == voxelTable['voxelHeight'].max(), 'voxelHeight'].to_numpy()[0].astype(int) @@ -33,14 +32,11 @@ def walls_as_netcdf( # Range of height levels height_levels = np.arange(1, levels + 1) - # Create empty numpy array to fill with wall temperatures from current - # time step + # Create empty numpy array to fill with wall temperatures from current time step wallTemperature = np.full((cols, rows, levels), np.nan, dtype=np.float32) # Add current time step wall temperature and longwave radiation to numpy array, which will be used to update the NetCDF. - # for y, x, z, wallTemp in zip(voxelTable['ypos'].astype(int), - # voxelTable['xpos'].astype(int), voxelTable['voxelHeight'].astype(int), - # voxelTable['wallTemperature'].astype(np.float32)): + # for y, x, z, wallTemp in zip(voxelTable['ypos'].astype(int), voxelTable['xpos'].astype(int), voxelTable['voxelHeight'].astype(int), voxelTable['wallTemperature'].astype(np.float32)): for y, x, z, wallTemp in zip( voxelTable["ypos"].astype(int), voxelTable["xpos"].astype(int), @@ -76,8 +72,7 @@ def walls_as_netcdf( attrs={"crs": raster_file.rio.crs.to_string()}, ) - # Update wall temperature and longwave radiation for current timestep - # (iteration) + # Update wall temperature and longwave radiation for current timestep (iteration) data_xr.wall_temperature[:, :, :, iteration] = wallTemperature encodings = {var: comp for var in data_xr.data_vars} diff --git a/functions/TreeGenerator/makevegdems.py b/functions/TreeGenerator/makevegdems.py index ba1bc9a..7bd7fd0 100644 --- a/functions/TreeGenerator/makevegdems.py +++ b/functions/TreeGenerator/makevegdems.py @@ -18,8 +18,7 @@ def vegunitsgeneration( sizey, scale, ): - # This function creates the shape of each vegetation unit and locates it a - # grid. + # This function creates the shape of each vegetation unit and locates it a grid. vegdemtemp = np.zeros([sizey, sizex]) vegdem2temp = np.copy(vegdemtemp) diff --git a/functions/TreePlanter/SOLWEIG1D/Kside1D_veg_v2019a.py b/functions/TreePlanter/SOLWEIG1D/Kside1D_veg_v2019a.py index 4afa97f..c52dbe4 100644 --- a/functions/TreePlanter/SOLWEIG1D/Kside1D_veg_v2019a.py +++ b/functions/TreePlanter/SOLWEIG1D/Kside1D_veg_v2019a.py @@ -133,11 +133,8 @@ def Kside_veg_v2019a( ) # Solid angle / Steradian radTot = radTot + ( - # Radiance fraction normalization - aniLum[ix] - * phiVar[ix] - * np.sin(aniAlt[ix] * deg2rad) - ) + aniLum[ix] * phiVar[ix] * np.sin(aniAlt[ix] * deg2rad) + ) # Radiance fraction normalization lumChi = (aniLum * radD) / radTot # Radiance fraction normalization @@ -146,15 +143,11 @@ def Kside_veg_v2019a( anglIncC = np.cos(aniAlt[idx] * deg2rad) * np.cos(0) * np.sin( np.pi / 2 ) + np.sin(aniAlt[idx] * deg2rad) * np.cos( - # Angle of incidence, np.cos(0) because cylinder - always - # perpendicular - np.pi - / 2 - ) - # Diffuse vertical radiation + np.pi / 2 + ) # Angle of incidence, np.cos(0) because cylinder - always perpendicular KsideD = ( KsideD + diffsh[idx] * lumChi[idx] * anglIncC * phiVar[idx] - ) + ) # Diffuse vertical radiation Keast = ( albedo * (svfviktbuvegE * (radG * (1 - F_sh) + radD * F_sh)) + KupE diff --git a/functions/TreePlanter/SOLWEIG1D/SOLWEIG1D_2023a.py b/functions/TreePlanter/SOLWEIG1D/SOLWEIG1D_2023a.py index efe6499..a91a45b 100644 --- a/functions/TreePlanter/SOLWEIG1D/SOLWEIG1D_2023a.py +++ b/functions/TreePlanter/SOLWEIG1D/SOLWEIG1D_2023a.py @@ -11,6 +11,8 @@ from ..SOLWEIG1D import Solweig1D_2023a_calc as so from ....util.SEBESOLWEIGCommonFiles.create_patches import create_patches +from ...SOLWEIGpython.CirclePlotBar import PolarBarPlot + def tmrt_1d_fun(metfilepath, infolder, tau, lon, lat, dsm, r_range, outputDir): @@ -177,8 +179,7 @@ def tmrt_1d_fun(metfilepath, infolder, tau, lon, lat, dsm, r_range, outputDir): # Always use 153 patches patch_option = 2 - # Creating skyvault of patches of constant radians (Tregeneza and - # Sharples, 1993) + # Creating skyvault of patches of constant radians (Tregeneza and Sharples, 1993) skyvaultalt, skyvaultazi, _, _, _, _, _ = create_patches(patch_option) # Unique altitudes for patches diff --git a/functions/TreePlanter/SOLWEIG1D/SOLWEIG_1D.py b/functions/TreePlanter/SOLWEIG1D/SOLWEIG_1D.py index 9a6506c..99ae37e 100644 --- a/functions/TreePlanter/SOLWEIG1D/SOLWEIG_1D.py +++ b/functions/TreePlanter/SOLWEIG1D/SOLWEIG_1D.py @@ -188,8 +188,7 @@ def tmrt_1d_fun(metfilepath, infolder, tau, lon, lat, dsm, r_range): # diffsh = np.zeros((145)) - # Creating skyvault of patches of constant radians (Tregeneza and - # Sharples, 1993) + # Creating skyvault of patches of constant radians (Tregeneza and Sharples, 1993) skyvaultalt, skyvaultazi, _, _, _, _, _ = create_patches(patch_option) diffsh = np.zeros((skyvaultalt.shape[0])) diff --git a/functions/TreePlanter/SOLWEIG1D/Solweig1D_2019a_calc.py b/functions/TreePlanter/SOLWEIG1D/Solweig1D_2019a_calc.py index 6e71c3c..f58ca0b 100644 --- a/functions/TreePlanter/SOLWEIG1D/Solweig1D_2019a_calc.py +++ b/functions/TreePlanter/SOLWEIG1D/Solweig1D_2019a_calc.py @@ -63,8 +63,7 @@ def Solweig1D_2019a_calc( ): # This is the core function of the SOLWEIG1D model, 2019-Jun-21 - # Fredrik Lindberg, fredrikl@gvc.gu.se, Goteborg Urban Climate Group, - # Gothenburg University, Sweden + # Fredrik Lindberg, fredrikl@gvc.gu.se, Goteborg Urban Climate Group, Gothenburg University, Sweden svfE = svf svfW = svf @@ -100,7 +99,7 @@ def Solweig1D_2019a_calc( 1 - (1 + msteg) * np.exp(-((1.2 + 3.0 * msteg) ** 0.5)) ) + elvis # -0.04 old error from Jonsson et al.2006 - if altitude > 0: # DAYTIME # # # # # # + if altitude > 0: # # # # # # DAYTIME # # # # # # # Clearness Index on Earth's surface after Crawford and Dunchon (1999) with a correction # factor for low sun elevations after Lindberg et al.(2008) I0, CI, Kt, I0et, CIuncorr = clearnessindex_2013b( @@ -130,8 +129,9 @@ def Solweig1D_2019a_calc( aniLum = 0.0 for idx in range(0, 145): - # Total relative luminance from sky into each cell - aniLum = aniLum + diffsh[idx] * lv[0][idx][2] + aniLum = ( + aniLum + diffsh[idx] * lv[0][idx][2] + ) # Total relative luminance from sky into each cell dRad = ( aniLum * radD @@ -150,39 +150,33 @@ def Solweig1D_2019a_calc( * np.sin( ( ((dectime - np.floor(dectime)) - SNUP / 24) - / ( - # 2015 a, based on max sun altitude - TmaxLST / 24 - - SNUP / 24 - ) + / (TmaxLST / 24 - SNUP / 24) ) * np.pi / 2 ) + Tstart - ) + ) # 2015 a, based on max sun altitude Tgwall = Tgampwall * np.sin( ( ((dectime - np.floor(dectime)) - SNUP / 24) - / ( - # 2015a, based on max sun altitude - TmaxLST_wall / 24 - - SNUP / 24 - ) + / (TmaxLST_wall / 24 - SNUP / 24) ) * np.pi / 2 - ) + (Tstart_wall) + ) + ( + Tstart_wall + ) # 2015a, based on max sun altitude if Tgwall < 0: # temporary for removing low Tg during morning 20130205 # Tg = 0 Tgwall = 0 - # New estimation of Tg reduction for non - clear situation based on - # Reindl et al.1990 + # New estimation of Tg reduction for non - clear situation based on Reindl et al.1990 radI0, _ = diffusefraction(I0, altitude, 1.0, Ta, RH) - # 20070329 correction of lat, Lindberg et al. 2008 - corr = 0.1473 * np.log(90 - (zen / np.pi * 180)) + 0.3454 + corr = ( + 0.1473 * np.log(90 - (zen / np.pi * 180)) + 0.3454 + ) # 20070329 correction of lat, Lindberg et al. 2008 CI_Tg = (radI / radI0) + (1 - corr) if (CI_Tg > 1) or (CI_Tg == np.inf): CI_Tg = 1 @@ -200,8 +194,9 @@ def Solweig1D_2019a_calc( LupN = Lup # Building height angle from svf - # Fraction shadow on building walls based on sun alt and svf - F_sh = cylindric_wedge(zen, svfalfa, 1, 1) + F_sh = cylindric_wedge( + zen, svfalfa, 1, 1 + ) # Fraction shadow on building walls based on sun alt and svf F_sh[np.isnan(F_sh)] = 0.5 # # # # # # # Calculation of shortwave daytime radiative fluxes # # # # # # # @@ -221,8 +216,7 @@ def Solweig1D_2019a_calc( ) # Kup, KupE, KupS, KupW, KupN = Kup_veg_2015a(radI, radD, radG, altitude, svf, albedo_b, F_sh, gvfalb, - # gvfalbE, gvfalbS, gvfalbW, gvfalbN, gvfalbnosh, gvfalbnoshE, - # gvfalbnoshS, gvfalbnoshW, gvfalbnoshN) + # gvfalbE, gvfalbS, gvfalbW, gvfalbN, gvfalbnosh, gvfalbnoshE, gvfalbnoshS, gvfalbnoshW, gvfalbnoshN) Keast, Ksouth, Kwest, Knorth, KsideI, KsideD = Kside_veg_v2019a( radI, @@ -279,8 +273,7 @@ def Solweig1D_2019a_calc( # # # # Lup # # # # Lup = SBC * eground * ((Knight + Ta + Tg + 273.15) ** 4) # if landcover == 1: - # Lup[lc_grid == 3] = SBC * 0.98 * (Twater + 273.15) ** 4 # nocturnal - # Water temp + # Lup[lc_grid == 3] = SBC * 0.98 * (Twater + 273.15) ** 4 # nocturnal Water temp LupE = Lup LupS = Lup LupW = Lup @@ -340,8 +333,9 @@ def Solweig1D_2019a_calc( ) # # # # Calculation of radiant flux density and Tmrt # # # # - # Human body considered as a cylinder with Perez et al. (1993) - if cyl == 1 and ani == 1: + if ( + cyl == 1 and ani == 1 + ): # Human body considered as a cylinder with Perez et al. (1993) Sstr = absK * ( (KsideI + KsideD) * Fcyl + (Kdown + Kup) * Fup diff --git a/functions/TreePlanter/SOLWEIG1D/Solweig1D_2023a_calc.py b/functions/TreePlanter/SOLWEIG1D/Solweig1D_2023a_calc.py index 8117b99..bf99a7c 100644 --- a/functions/TreePlanter/SOLWEIG1D/Solweig1D_2023a_calc.py +++ b/functions/TreePlanter/SOLWEIG1D/Solweig1D_2023a_calc.py @@ -10,11 +10,13 @@ # from .TsWaveDelay_2015a import TsWaveDelay_2015a # from .Kup_veg_2015a import Kup_veg_2015a from ...SOLWEIGpython.Lside_veg_v2015a import Lside_veg_v2015a +from ..SOLWEIG1D.Kside1D_veg_v2019a import Kside_veg_v2019a # from ...SOLWEIGpython.Perez_v3_moved import Perez_v3 from ....util.SEBESOLWEIGCommonFiles.Perez_v3 import Perez_v3 from .anisotropic_sky import anisotropic_sky as ani_sky +from copy import deepcopy def Solweig1D_2019a_calc( @@ -70,8 +72,7 @@ def Solweig1D_2019a_calc( ): # This is the core function of the SOLWEIG1D model, 2019-Jun-21 - # Fredrik Lindberg, fredrikl@gvc.gu.se, Goteborg Urban Climate Group, - # Gothenburg University, Sweden + # Fredrik Lindberg, fredrikl@gvc.gu.se, Goteborg Urban Climate Group, Gothenburg University, Sweden svfE = svf svfW = svf @@ -110,7 +111,7 @@ def Solweig1D_2019a_calc( 1 - (1 + msteg) * np.exp(-((1.2 + 3.0 * msteg) ** 0.5)) ) + elvis # -0.04 old error from Jonsson et al.2006 - if altitude > 0: # DAYTIME # # # # # # + if altitude > 0: # # # # # # DAYTIME # # # # # # # Clearness Index on Earth's surface after Crawford and Dunchon (1999) with a correction # factor for low sun elevations after Lindberg et al.(2008) I0, CI, Kt, I0et, CIuncorr = clearnessindex_2013b( @@ -140,8 +141,9 @@ def Solweig1D_2019a_calc( aniLum = 0.0 for idx in range(skyp.shape[0]): - # Total relative luminance from sky into each cell - aniLum = aniLum + skyp[idx] * lv[idx, 2] + aniLum = ( + aniLum + skyp[idx] * lv[idx, 2] + ) # Total relative luminance from sky into each cell dRad = ( aniLum * radD @@ -178,11 +180,11 @@ def Solweig1D_2019a_calc( # Tg = 0 Tgwall = 0 - # New estimation of Tg reduction for non - clear situation based on - # Reindl et al.1990 + # New estimation of Tg reduction for non - clear situation based on Reindl et al.1990 radI0, _ = diffusefraction(I0, altitude, 1.0, Ta, RH) - # 20070329 correction of lat, Lindberg et al. 2008 - corr = 0.1473 * np.log(90 - (zen / np.pi * 180)) + 0.3454 + corr = ( + 0.1473 * np.log(90 - (zen / np.pi * 180)) + 0.3454 + ) # 20070329 correction of lat, Lindberg et al. 2008 CI_Tg = (radG / radI0) + (1 - corr) if (CI_Tg > 1) or (CI_Tg == np.inf): CI_Tg = 1 @@ -197,14 +199,12 @@ def Solweig1D_2019a_calc( Tg = Tg * CI_TgG # new estimation Tgwall = Tgwall * CI_TgG # if landcover == 1: - # Tg[Tg < 0] = 0 # temporary for removing low Tg during morning - # 20130205 + # Tg[Tg < 0] = 0 # temporary for removing low Tg during morning 20130205 # gvf = shadow # Lup = SBC * eground * ((gvf + Ta + Tg + 273.15) ** 4) - # Ground always in shade, i.e. ground longwave radiation based on - # ground emissivity and air temperature + # Ground always in shade, i.e. ground longwave radiation based on ground emissivity and air temperature Lup = SBC * eground * ((Ta + 273.15) ** 4) LupE = Lup * 0.5 LupS = Lup * 0.5 @@ -212,8 +212,9 @@ def Solweig1D_2019a_calc( LupN = Lup * 0.5 # Building height angle from svf - # Fraction shadow on building walls based on sun alt and svf - F_sh = cylindric_wedge(zen, svfalfa, 1, 1) + F_sh = cylindric_wedge( + zen, svfalfa, 1, 1 + ) # Fraction shadow on building walls based on sun alt and svf F_sh[np.isnan(F_sh)] = 0.5 # # # # # # # Calculation of shortwave daytime radiative fluxes # # # # # # # @@ -233,8 +234,7 @@ def Solweig1D_2019a_calc( ) # Kup, KupE, KupS, KupW, KupN = Kup_veg_2015a(radI, radD, radG, altitude, svf, albedo_b, F_sh, gvfalb, - # gvfalbE, gvfalbS, gvfalbW, gvfalbN, gvfalbnosh, gvfalbnoshE, - # gvfalbnoshS, gvfalbnoshW, gvfalbnoshN) + # gvfalbE, gvfalbS, gvfalbW, gvfalbN, gvfalbnosh, gvfalbnoshE, gvfalbnoshS, gvfalbnoshW, gvfalbnoshN) # Keast, Ksouth, Kwest, Knorth, KsideI, KsideD = Kside_veg_v2019a(radI, radD, radG, shadow, svfS, svfW, svfN, svfE, # svfEveg, svfSveg, svfWveg, svfNveg, azimuth, altitude, psi, t, albedo_b, F_sh, Kup, Kup, Kup, @@ -264,8 +264,7 @@ def Solweig1D_2019a_calc( # # # # Lup # # # # Lup = SBC * eground * ((Knight + Ta + Tg + 273.15) ** 4) # if landcover == 1: - # Lup[lc_grid == 3] = SBC * 0.98 * (Twater + 273.15) ** 4 # nocturnal - # Water temp + # Lup[lc_grid == 3] = SBC * 0.98 * (Twater + 273.15) ** 4 # nocturnal Water temp LupE = Lup * 0.5 LupS = Lup * 0.5 LupW = Lup * 0.5 diff --git a/functions/TreePlanter/SOLWEIG1D/anisotropic_sky.py b/functions/TreePlanter/SOLWEIG1D/anisotropic_sky.py index 6c8f3ee..63aa195 100644 --- a/functions/TreePlanter/SOLWEIG1D/anisotropic_sky.py +++ b/functions/TreePlanter/SOLWEIG1D/anisotropic_sky.py @@ -112,12 +112,10 @@ def anisotropic_sky( # Anisotropic sky if anisotropic_sky_: temp_emissivity = esky_band[skyalt == temp_altitude] - # Isotropic sky but with patches (need to switch anisotropic_sky to - # False) + # Isotropic sky but with patches (need to switch anisotropic_sky to False) else: temp_emissivity = esky - # Estimate longwave radiation on a horizontal surface (Ldown), vertical - # surface (Lside) and perpendicular (Lnormal) + # Estimate longwave radiation on a horizontal surface (Ldown), vertical surface (Lside) and perpendicular (Lnormal) Ldown[patch_altitude == temp_altitude] = ( ((temp_emissivity * SBC * ((Ta + 273.15) ** 4)) / np.pi) * steradians[patch_altitude == temp_altitude] @@ -148,12 +146,11 @@ def anisotropic_sky( radTot = np.zeros(1) # Radiance fraction normalization for i in np.arange(patch_altitude.shape[0]): - # Radiance fraction normalization radTot += ( patch_luminance[i] * steradians[i] * np.sin(patch_altitude[i] * deg2rad) - ) + ) # Radiance fraction normalization lumChi = (patch_luminance * radD) / radTot for i in np.arange(patch_altitude.shape[0]): @@ -184,8 +181,7 @@ def anisotropic_sky( ) if cyl == 1: - # Angle of incidence, np.cos(0) because cylinder - always - # perpendicular + # Angle of incidence, np.cos(0) because cylinder - always perpendicular angle_of_incidence = np.cos(patch_altitude[i] * deg2rad) * np.cos( 0 ) # * np.sin(np.pi / 2) \ @@ -273,13 +269,13 @@ def anisotropic_sky( # Lside_sun_temp, Lside_sh_temp, \ # Ldown_sun_temp, Ldown_sh_temp, \ # Least_temp, Lsouth_temp, Lwest_temp, Lnorth_temp = patch_radiation.longwave_from_buildings_wallScheme(temp_sh_w, voxelTable, steradians[i], angle_of_incidence, angle_of_incidence_h, - # patch_azimuth[i]) + # patch_azimuth[i]) # Lside_sun_r_temp, Lside_sh_r_temp, \ # Ldown_sun_r_temp, Ldown_sh_r_temp, \ # Least_r_temp, Lsouth_r_temp, Lwest_r_temp, Lnorth_r_temp = patch_radiation.longwave_from_buildings(temp_sh_roof, steradians[i], angle_of_incidence, angle_of_incidence_h, # patch_azimuth[i], sunlit_patches, shaded_patches, - # azimuth_difference, solar_altitude, ewall, Ta, Tgwall) + # azimuth_difference, solar_altitude, ewall, Ta, Tgwall) # Lside_sun_temp += Lside_sun_r_temp # Lside_sh_temp += Lside_sh_r_temp @@ -434,16 +430,14 @@ def anisotropic_sky( Lwest += Lwest_temp Lnorth += Lnorth_temp - # Sum of all Lside components (sky, vegetation, sunlit and shaded - # buildings, reflected) + # Sum of all Lside components (sky, vegetation, sunlit and shaded buildings, reflected) Lside = Lside_sky + Lside_veg + Lside_sh + Lside_sun + Lside_ref - # Sum of all Lside components (sky, vegetation, sunlit and shaded - # buildings, reflected) + # Sum of all Lside components (sky, vegetation, sunlit and shaded buildings, reflected) Ldown = Ldown_sky + Ldown_veg + Ldown_sh + Ldown_sun + Ldown_ref ### Direct radiation ### - if cyl == 1: # Kside with cylinder ### + if cyl == 1: ### Kside with cylinder ### KsideI = shadow * radI * np.cos(solar_altitude * deg2rad) if solar_altitude > 0: diff --git a/functions/TreePlanter/SOLWEIG1D/emissivity_models.py b/functions/TreePlanter/SOLWEIG1D/emissivity_models.py index e791a00..1762e50 100644 --- a/functions/TreePlanter/SOLWEIG1D/emissivity_models.py +++ b/functions/TreePlanter/SOLWEIG1D/emissivity_models.py @@ -38,8 +38,7 @@ def model1(sky_patches, esky, Ta): # Natural log of optical water depth log_owp = np.log(owp) - # Emissivity of each zenith angle, i.e. the zenith angle of each band of - # patches + # Emissivity of each zenith angle, i.e. the zenith angle of each band of patches esky_band = a_c + b_c * log_owp # Altitudes of the Robinson & Stone patches @@ -115,15 +114,13 @@ def model3(sky_patches, esky, Ta): # Unique zeniths for the patches skyzen = 90 - skyalt - # Constant, can (should?) be changed. Model gave unsatisfactory results in - # Nahon et al., 2019 + # Constant, can (should?) be changed. Model gave unsatisfactory results in Nahon et al., 2019 b_c = 1.8 # Estimate emissivites at different altitudes/zenith angles esky_band = 1 - (1 - esky) ** (1 / (b_c * np.cos(skyzen * deg2rad))) - # Estimating longwave radiation for each patch to a horizontal or vertical - # surface + # Estimating longwave radiation for each patch to a horizontal or vertical surface # Altitudes of the Robinson & Stone patches p_alt = sky_patches[:, 0] diff --git a/functions/TreePlanter/SOLWEIG1D/patch_radiation.py b/functions/TreePlanter/SOLWEIG1D/patch_radiation.py index 24c1cad..c031d86 100644 --- a/functions/TreePlanter/SOLWEIG1D/patch_radiation.py +++ b/functions/TreePlanter/SOLWEIG1D/patch_radiation.py @@ -32,8 +32,7 @@ def longwave_from_sky(sky, Lsky_side, Lsky_down, patch_azimuth): Lwest = 0 Lnorth = 0 - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = sky * Lsky_side * np.cos((90 - patch_azimuth) * deg2rad) if (patch_azimuth > 90) and (patch_azimuth < 270): @@ -87,8 +86,7 @@ def longwave_from_veg( Lwest = 0 Lnorth = 0 - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( vegetation_surface @@ -198,8 +196,7 @@ def longwave_from_buildings( * building ) - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least += ( sunlit_surface @@ -270,18 +267,15 @@ def longwave_from_buildings( ) else: - # Calculate longwave radiation from shaded walls reaching a vertical - # surface + # Calculate longwave radiation from shaded walls reaching a vertical surface Lside_sh = shaded_surface * steradian * angle_of_incidence * building Lside_sun = 0 - # Calculate longwave radiation from shaded walls reaching a horizontal - # surface + # Calculate longwave radiation from shaded walls reaching a horizontal surface Ldown_sh = shaded_surface * steradian * angle_of_incidence_h * building Ldown_sun = 0 - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( shaded_surface @@ -366,8 +360,7 @@ def reflected_longwave( Lwest = 0 Lnorth = 0 - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): Least = ( reflected_radiation @@ -466,8 +459,7 @@ def cardinal_shortwave( Kwest = 0 Knorth = 0 - # Portion into cardinal directions to be used for standing box or POI - # output + # Portion into cardinal directions to be used for standing box or POI output if (patch_azimuth > 360) or (patch_azimuth < 180): angle_of_incidence = np.cos(patch_altitude * deg2rad) * np.cos( (90 - patch_azimuth) * deg2rad diff --git a/functions/TreePlanter/TreeGeneratorTempold/makevegdems.py b/functions/TreePlanter/TreeGeneratorTempold/makevegdems.py index f02ae97..27c3541 100644 --- a/functions/TreePlanter/TreeGeneratorTempold/makevegdems.py +++ b/functions/TreePlanter/TreeGeneratorTempold/makevegdems.py @@ -18,8 +18,7 @@ def vegunitsgeneration( sizey, scale, ): - # This function creates the shape of each vegetation unit and locates it a - # grid. + # This function creates the shape of each vegetation unit and locates it a grid. vegdemtemp = np.zeros([sizey, sizex]) vegdem2temp = np.copy(vegdemtemp) diff --git a/functions/TreePlanter/TreePlanter/GreedyAlgorithm.py b/functions/TreePlanter/TreePlanter/GreedyAlgorithm.py index 0ecc16f..d274224 100644 --- a/functions/TreePlanter/TreePlanter/GreedyAlgorithm.py +++ b/functions/TreePlanter/TreePlanter/GreedyAlgorithm.py @@ -6,16 +6,16 @@ def greedyplanter(treeinput, treedata, treerasters, tmrt_1d, trees, feedback): - # Remove all Tmrt values that are in shade or on top of buildings - treeinput.tmrt_s = treeinput.tmrt_s * treeinput.buildings + treeinput.tmrt_s = ( + treeinput.tmrt_s * treeinput.buildings + ) # Remove all Tmrt values that are in shade or on top of buildings bld_copy = treeinput.buildings.copy() # Creating boolean for where it is possible to plant a tree bd_b = np.int_(np.ceil((treedata.dia / 2) / treeinput.gt[1])) - # Buffer on building raster so that trees can't be planted next to walls. - # Can be planted one radius from walls. + # Buffer on building raster so that trees can't be planted next to walls. Can be planted one radius from walls. for i1 in range(bd_b): walls = np.zeros((treeinput.rows, treeinput.cols)) domain = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]]) @@ -44,14 +44,16 @@ def greedyplanter(treeinput, treedata, treerasters, tmrt_1d, trees, feedback): tmrt_max = 0 # Calculating sum of Tmrt in shade for each possible position in the Tmrt matrix - # Empty matrix for sum of Tmrt in tree shadow - sum_tmrt_tsh = np.zeros((treeinput.rows, treeinput.cols)) - # Empty matrix for sum of Tmrt in sun under tree shadow - sum_tmrt = np.zeros((treeinput.rows, treeinput.cols)) + sum_tmrt_tsh = np.zeros( + (treeinput.rows, treeinput.cols) + ) # Empty matrix for sum of Tmrt in tree shadow + sum_tmrt = np.zeros( + (treeinput.rows, treeinput.cols) + ) # Empty matrix for sum of Tmrt in sun under tree shadow - # Coordinates for where it is possible to plant a tree (buildings, area of - # interest excluded) - res_y, res_x = np.where(bld_copy == 1) + res_y, res_x = np.where( + bld_copy == 1 + ) # Coordinates for where it is possible to plant a tree (buildings, area of interest excluded) for tree in range(trees): if feedback.isCanceled(): @@ -94,8 +96,7 @@ def greedyplanter(treeinput, treedata, treerasters, tmrt_1d, trees, feedback): * tmrt_1d[j, 0] ) - # Adding sum_tmrt and sum_tmrt_tsh to the Treerasters class as well as - # calculating the difference between sunlit and shaded + # Adding sum_tmrt and sum_tmrt_tsh to the Treerasters class as well as calculating the difference between sunlit and shaded treerasters.tmrt(sum_tmrt, sum_tmrt_tsh) if tree == 0: @@ -113,8 +114,7 @@ def greedyplanter(treeinput, treedata, treerasters, tmrt_1d, trees, feedback): best_y[tree] = temp_y[0] best_x[tree] = temp_x[0] - # Add new tree shade and remove Tmrt from Tmrt.SOLWEIG where there is - # shade from new tree + # Add new tree shade and remove Tmrt from Tmrt.SOLWEIG where there is shade from new tree y1 = np.int_(temp_y[0] - treerasters.buffer_y[0]) y2 = np.int_(temp_y[0] + treerasters.buffer_y[1]) x1 = np.int_(temp_x[0] - treerasters.buffer_x[0]) diff --git a/functions/TreePlanter/TreePlanter/HillClimberAlgorithm.py b/functions/TreePlanter/TreePlanter/HillClimberAlgorithm.py index c0cc7f5..47fd3d3 100644 --- a/functions/TreePlanter/TreePlanter/HillClimberAlgorithm.py +++ b/functions/TreePlanter/TreePlanter/HillClimberAlgorithm.py @@ -1,14 +1,14 @@ import numpy as np import time +import timeit +from ..TreePlanter.TreePlanterTreeshade import tsh_gen from ..TreePlanter.TreePlanterTreeshade import tsh_gen_ts from ..TreePlanter.TreePlanterTreeshade import tsh_gen_mt1, tsh_gen_mt2 import itertools -# This function will look for better shading position for a tree by looking one pixel east, west, north, south of it's -# current position. It will compare it's position to the other trees in -# the study area, i.e. other moving trees. - +# This function will look for better shading position for a tree by looking one pixel east, west, north, south of it's +# current position. It will compare it's position to the other trees in the study area, i.e. other moving trees. def topt( y, x, @@ -38,8 +38,7 @@ def topt( p_tmrt[0, 3] = t_y p_tmrt[0, 4] = t_x - # Array with movement one step south, one step north, one step west, one - # step east + # Array with movement one step south, one step north, one step west, one step east k_d = 1 yt = t_y + k_d @@ -96,8 +95,7 @@ def topt( if eucl[i, j] < treerasters.euclidean_d: e_bool_sh[j] = 1 - # Boolean of where it's possible and not to move - e_bool = np.bool_(e_bool) + e_bool = np.bool_(e_bool) # Boolean of where it's possible and not to move t_yx = t_yx[e_bool, :] # Positions where it's possible to move to e_bool_sh = np.bool_(e_bool_sh) @@ -118,8 +116,9 @@ def topt( e_bool_nmt = np.bool_(e_bool_nmt) - # y, x, tmrt shade, tmrt sun, tmrt diff, sum tmrt all trees - tree_tmrt = np.zeros((t_yx.shape[0] + 1, 5)) + tree_tmrt = np.zeros( + (t_yx.shape[0] + 1, 5) + ) # y, x, tmrt shade, tmrt sun, tmrt diff, sum tmrt all trees tree_tmrt[-1, :] = p_tmrt compare_mt = np.zeros((t_yx.shape[0])) @@ -128,12 +127,8 @@ def topt( # Check if tree shadows overlap treesh_ts_bool_pad, treesh_ts_bool_pad_large, compare = tsh_gen_ts( - # Boolean tree shadows for trees that are not moving - y_n, - x_n, - treerasters, - treeinput, - ) + y_n, x_n, treerasters, treeinput + ) # Boolean tree shadows for trees that are not moving if ( compare == 0 @@ -145,16 +140,15 @@ def topt( ((treesh_bool_mt == 1) & (treesh_ts_bool_pad_large == 1)) ): for j in range(t_yx.shape[0]): - # If boolean distance between coming position of the moving - # tree possibly have overlapping shadows with the other - # trees, continue - if e_bool_sh[j] == 1: - # Y-position of the current position of the currently - # moving tree - y_t = np.array([t_yx[j, 0]]) - # X-position of the current position of the currently - # moving tree - x_t = np.array([t_yx[j, 1]]) + if ( + e_bool_sh[j] == 1 + ): # If boolean distance between coming position of the moving tree possibly have overlapping shadows with the other trees, continue + y_t = np.array( + [t_yx[j, 0]] + ) # Y-position of the current position of the currently moving tree + x_t = np.array( + [t_yx[j, 1]] + ) # X-position of the current position of the currently moving tree treesh_bool_mt_pad, treesh_bool_mt_pad_large, _ = ( tsh_gen_ts(y_t, x_t, treerasters, treeinput) ) # Boolean tree shadows for moving tree @@ -164,8 +158,9 @@ def topt( & (treesh_bool_mt_pad_large == 1) ) ): - # Check if timesteps overlap - for ij in range(treesh_bool_mt_pad.shape[2]): + for ij in range( + treesh_bool_mt_pad.shape[2] + ): # Check if timesteps overlap temp_bool = ( ( (treesh_ts_bool_pad[:, :, ij] == 1) @@ -189,10 +184,9 @@ def topt( if e_bool_sh[j] == 1: y_t = np.array([t_yx[j, 0]]) x_t = np.array([t_yx[j, 1]]) - # Boolean tree shadows for moving tree treesh_bool_mt_pad = tsh_gen_mt2( y_t, x_t, treerasters, treeinput - ) + ) # Boolean tree shadows for moving tree for ij in range(treesh_bool_mt_pad.shape[2]): temp_bool = ( ( @@ -214,16 +208,13 @@ def topt( y_t = np.array([t_yx[i, 0]]) x_t = np.array([t_yx[i, 1]]) - # y position of currently moving tree - tree_tmrt[i, 3] = y_t - # x position of currently moving tree - tree_tmrt[i, 4] = x_t + tree_tmrt[i, 3] = y_t # y position of currently moving tree + tree_tmrt[i, 4] = x_t # x position of currently moving tree # Comparison between the currently moving tree and other trees if their shadows overlap # If moving tree is overlapping with any of the other trees - # If any of the none-moving trees are overlapping with each other but - # the moving tree is not overlapping with them + # If any of the none-moving trees are overlapping with each other but the moving tree is not overlapping with them if (compare == 1) & (compare_mt[i] == 0): start_time = time.time() for j in range(treesh_ts_bool_pad.shape[2]): @@ -245,30 +236,30 @@ def topt( start_time = time.time() for j in range(y_n.shape[0]): tree_tmrt[i, 0] += treerasters.tmrt_shade[ - y_n[j], - # Tree shade - x_n[j], - ] + y_n[j], x_n[j] + ] # Tree shade tree_tmrt[i, 1] += treerasters.tmrt_sun[ y_n[j], x_n[j] ] # Sunlit tree_tmrt[i, 2] += treerasters.d_tmrt[ - y_n[j], - # Sunlit - Tree shade - x_n[j], - ] - # Potential decrease in Tmrt from shade due to other trees - tree_tmrt[i, 0] += treerasters.tmrt_shade[y_t, x_t] - # Tmrt in sun for the tree shadow - tree_tmrt[i, 1] += treerasters.tmrt_sun[y_t, x_t] - # Difference in Tmrt between sunlit and shaded - tree_tmrt[i, 2] += treerasters.d_tmrt[y_t, x_t] + y_n[j], x_n[j] + ] # Sunlit - Tree shade + tree_tmrt[i, 0] += treerasters.tmrt_shade[ + y_t, x_t + ] # Potential decrease in Tmrt from shade due to other trees + tree_tmrt[i, 1] += treerasters.tmrt_sun[ + y_t, x_t + ] # Tmrt in sun for the tree shadow + tree_tmrt[i, 2] += treerasters.d_tmrt[ + y_t, x_t + ] # Difference in Tmrt between sunlit and shaded nc = 0 # no change if np.sum(e_bool) > 0: - # Which position has highest tmrt - t_max = np.where(tree_tmrt[:, 2] == np.max(tree_tmrt[:, 2])) + t_max = np.where( + tree_tmrt[:, 2] == np.max(tree_tmrt[:, 2]) + ) # Which position has highest tmrt if t_max[0].__len__() > 1: t_rand = np.zeros((t_max[0].__len__())) for iy in range(t_max[0].__len__()): @@ -285,17 +276,16 @@ def topt( y[ti] = t_out[0, 3] x[ti] = t_out[0, 4] else: - # Position of where tmrt is highest - t_out = tree_tmrt[t_max[0], :] - # If starting position, i.e. input position is best, don't move - if (np.int_(t_out[0, 3]) == t_y) & (np.int_(t_out[0, 4]) == t_x): + t_out = tree_tmrt[t_max[0], :] # Position of where tmrt is highest + if (np.int_(t_out[0, 3]) == t_y) & ( + np.int_(t_out[0, 4]) == t_x + ): # If starting position, i.e. input position is best, don't move nc = 1 else: y[ti] = t_out[0, 3] x[ti] = t_out[0, 4] else: - # If tree can't move because of other trees, no move - t_out = tree_tmrt + t_out = tree_tmrt # If tree can't move because of other trees, no move nc = 1 return t_out, nc, y, x diff --git a/functions/TreePlanter/TreePlanter/StartingPositions.py b/functions/TreePlanter/TreePlanter/StartingPositions.py index 7454786..4148e58 100644 --- a/functions/TreePlanter/TreePlanter/StartingPositions.py +++ b/functions/TreePlanter/TreePlanter/StartingPositions.py @@ -1,9 +1,8 @@ import numpy as np import itertools -# Creating trees at random positions - +# Creating trees at random positions def random_start(pos, trees, tree_pos_all, r_iters): tree_pos = np.random.choice(pos, trees) # Random positions for trees @@ -21,10 +20,7 @@ def random_start(pos, trees, tree_pos_all, r_iters): return tree_pos, tp_c, break_loop -# Creating trees evolutionary from previous starting position. First -# population is random. - - +# Creating trees evolutionary from previous starting position. First population is random. def genetic_start( tree_pos_x, tree_pos_y, @@ -52,15 +48,15 @@ def genetic_start( pos, trees, tree_pos_all, r_iters ) - # Take either x or y position from previous local optimum for each tree, - # make the other coordinate random + # Take either x or y position from previous local optimum for each tree, make the other coordinate random else: distance = 0 tries = np.zeros((trees)) while distance < 1: exists = np.zeros((trees)) - # Iterator to move between trees moving around in the study area - ti = itertools.cycle(range(trees)) + ti = itertools.cycle( + range(trees) + ) # Iterator to move between trees moving around in the study area new_y = np.zeros((trees)) new_x = np.zeros((trees)) tree_pos_c_temp = tree_pos_c.copy() @@ -114,8 +110,7 @@ def genetic_start( new_x[i] = x_temp exists[i] = 1 - # Euclidean distance between random positions so that trees are not - # too close to each other + # Euclidean distance between random positions so that trees are not too close to each other yxp = tuple( itertools.combinations(np.arange(tree_pos.shape[0]), 2) ) @@ -145,49 +140,49 @@ def genetic_start( return tree_pos_y, tree_pos_x, tree_pos, tree_pos_c, tp_c, break_loop -# Creating trees evolutionary from previous starting position. First population is random. -# def geneticstart(tree_pos_x, tree_pos_y, tree_pos_c, positions, trees, tree_pos_all, r_iters, counter): +### Creating trees evolutionary from previous starting position. First population is random. +##def geneticstart(tree_pos_x, tree_pos_y, tree_pos_c, positions, trees, tree_pos_all, r_iters, counter): ## -# pos = positions.pos[:, 0] -# pos_y = positions.pos[:, 2] -# pos_x = positions.pos[:, 1] +## pos = positions.pos[:, 0] +## pos_y = positions.pos[:, 2] +## pos_x = positions.pos[:, 1] ## -# tree_pos = 0 -# tp_c = 0 -# break_loop = 0 +## tree_pos = 0 +## tp_c = 0 +## break_loop = 0 ## -# tree_pos = np.zeros((trees)) +## tree_pos = np.zeros((trees)) ## -# Random for first run -# if counter == 0: -# tree_pos, tp_c, break_loop = randomstart(pos, trees, tree_pos_all, r_iters) +## # Random for first run +## if counter == 0: +## tree_pos, tp_c, break_loop = randomstart(pos, trees, tree_pos_all, r_iters) ## -# Take either x or y position from previous local optimum for each tree, make the other coordinate random -# else: -# for i in range(trees): -# if tree_pos_c[i] < 4: -# Decide which coordinate will be random; 0 = y, 1 = x -# xy_random = np.random.choice([0,1], 1) -# Random y-position -# if xy_random == 0: -# x_temp = tree_pos_x[i] -# pos_y_temp = pos_y[pos_x == x_temp] -# tree_pos_y[i] = np.random.choice(pos_y_temp, 1) -# Random x-position -# else: -# y_temp = tree_pos_y[i] -# pos_x_temp = pos_x[pos_y == y_temp] -# tree_pos_x[i] = np.random.choice(pos_x_temp, 1) -# If the tree have ended up in local optimums with lower Tmrt x times, make both coordinates random -# else: -# tree_pos_temp = np.random.choice(pos, 1) -# tree_pos_y[i] = pos_y[pos == tree_pos_temp] # Random y-position -# tree_pos_x[i] = pos_x[pos == tree_pos_temp] # Random x-position -# tree_pos_c[i] = 0 +## # Take either x or y position from previous local optimum for each tree, make the other coordinate random +## else: +## for i in range(trees): +## if tree_pos_c[i] < 4: +## # Decide which coordinate will be random; 0 = y, 1 = x +## xy_random = np.random.choice([0,1], 1) +## # Random y-position +## if xy_random == 0: +## x_temp = tree_pos_x[i] +## pos_y_temp = pos_y[pos_x == x_temp] +## tree_pos_y[i] = np.random.choice(pos_y_temp, 1) +## # Random x-position +## else: +## y_temp = tree_pos_y[i] +## pos_x_temp = pos_x[pos_y == y_temp] +## tree_pos_x[i] = np.random.choice(pos_x_temp, 1) +## # If the tree have ended up in local optimums with lower Tmrt x times, make both coordinates random +## else: +## tree_pos_temp = np.random.choice(pos, 1) +## tree_pos_y[i] = pos_y[pos == tree_pos_temp] # Random y-position +## tree_pos_x[i] = pos_x[pos == tree_pos_temp] # Random x-position +## tree_pos_c[i] = 0 ## -# tree_pos[i] = positions.pos[((positions.pos[:,1] == tree_pos_x[i]) & (positions.pos[:,2] == tree_pos_y[i])), 0] +## tree_pos[i] = positions.pos[((positions.pos[:,1] == tree_pos_x[i]) & (positions.pos[:,2] == tree_pos_y[i])), 0] ## -# tree_pos_y = np.int_(tree_pos_y) -# tree_pos_x = np.int_(tree_pos_x) +## tree_pos_y = np.int_(tree_pos_y) +## tree_pos_x = np.int_(tree_pos_x) ## -# return tree_pos_y, tree_pos_x, tree_pos, tree_pos_c, tp_c, break_loop +## return tree_pos_y, tree_pos_x, tree_pos, tree_pos_c, tp_c, break_loop diff --git a/functions/TreePlanter/TreePlanter/TreePlanterClasses.py b/functions/TreePlanter/TreePlanter/TreePlanterClasses.py index 84cb0e5..438be4c 100644 --- a/functions/TreePlanter/TreePlanter/TreePlanterClasses.py +++ b/functions/TreePlanter/TreePlanter/TreePlanterClasses.py @@ -11,8 +11,7 @@ def spatialReferenceData(self, feedback): old_cs = osr.SpatialReference() dsm_ref = self.dataSet.GetProjectionRef() - # dsm_ref = QgsRasterLayer(self.dataSet.GetDescription()).crs().toWkt() # - # This method requires lonlat = transform.TransformPoint(minx, miny) + # dsm_ref = QgsRasterLayer(self.dataSet.GetDescription()).crs().toWkt() # This method requires lonlat = transform.TransformPoint(minx, miny) old_cs.ImportFromWkt(dsm_ref) @@ -100,20 +99,27 @@ def __init__( float ) # Building raster self.buildings = self.buildings == 1.0 - # Rows of input rasters from SOLWEIG - self.rows = self.buildings.shape[0] - # Cols of input rasters from SOLWEIG - self.cols = self.buildings.shape[1] - # Canopy digital surface model - self.cdsm = np.zeros((self.rows, self.cols)) - # Canopy digital surface model - self.cdsm_b = np.zeros((self.rows, self.cols)) - # Shadow rasters - self.shadow = np.zeros((self.rows, self.cols, r_range.__len__())) - # Tmrt for each timestep - self.tmrt_ts = np.zeros((self.rows, self.cols, r_range.__len__())) - # Sum of tmrt for all timesteps - self.tmrt_s = np.zeros((self.rows, self.cols)) + self.rows = self.buildings.shape[ + 0 + ] # Rows of input rasters from SOLWEIG + self.cols = self.buildings.shape[ + 1 + ] # Cols of input rasters from SOLWEIG + self.cdsm = np.zeros( + (self.rows, self.cols) + ) # Canopy digital surface model + self.cdsm_b = np.zeros( + (self.rows, self.cols) + ) # Canopy digital surface model + self.shadow = np.zeros( + (self.rows, self.cols, r_range.__len__()) + ) # Shadow rasters + self.tmrt_ts = np.zeros( + (self.rows, self.cols, r_range.__len__()) + ) # Tmrt for each timestep + self.tmrt_s = np.zeros( + (self.rows, self.cols) + ) # Sum of tmrt for all timesteps # Loading DEm, DSM (and CDSM) rasters dataSet = gdal.Open(infolder + "/DSM.tif") @@ -179,7 +185,7 @@ def __init__( del dataSetSel os.remove(infolder + "/selected_area.tif") print("Successfully removed selected_area.tif") - except BaseException: + except: print("Could not remove selected_area.tif") # Buffer zone to remove potential edge effects @@ -237,8 +243,7 @@ def __init__( 1 - treeshade_bool[shy_min:shy_max, shx_min:shx_max, :] ) self.cdsm = cdsm[shy_min:shy_max, shx_min:shx_max] - # y, x = np.where(cdsm_clip == treedata.height) # Position of tree in - # clipped shadow image + # y, x = np.where(cdsm_clip == treedata.height) # Position of tree in clipped shadow image self.buffer_y = np.zeros((2)) self.buffer_x = np.zeros((2)) self.buffer_y[0] = np.int_(y) @@ -261,10 +266,9 @@ def __init__( b[2, :] = np.array( (self.treeshade.shape[1] - 1, 0) ) # Upper right corner - # Lower right corner b[3, :] = np.array( (self.treeshade.shape[0] - 1, self.treeshade.shape[1] - 1) - ) + ) # Lower right corner eucl = np.zeros((b.shape[0], 1)) for i in range(b.shape[0]): eucl[i, 0] = np.linalg.norm(a - b[i, :]) @@ -286,8 +290,7 @@ def tmrt(self, tmrt_sun, tmrt_shade): class Position: # Class containing y and x positions of trees and their corresponding sum of Tmrt in shade and sum of Tmrt in same area as shade but sunlit - # as well as a unique number for each position. Also a matrix with the - # unique number in each y,x position in the matrix. + # as well as a unique number for each position. Also a matrix with the unique number in each y,x position in the matrix. __slots__ = ("pos", "pos_m") def __init__(self, vector, rows, cols): @@ -301,8 +304,7 @@ def __init__(self, vector, rows, cols): class Treedata: - # Class containing data for the tree that is used in Tree planter, i.e. - # the tree that is being "planted" and studied + # Class containing data for the tree that is used in Tree planter, i.e. the tree that is being "planted" and studied __slots__ = ("ttype", "height", "trunk", "dia", "treey", "treex") def __init__(self, ttype, height, trunk, dia, treey, treex): @@ -327,54 +329,54 @@ def __init__(self, range_, shadow_, shadow_ts, tmrt): t_r = range(range_.__len__()) t_l = t_r.__len__() - # Unique values in summation matrix for tree shadows - shade_u = np.unique(shadow_) - # Maximum value of unique values - shade_max = np.max(shade_u) - - # Loop over all unique values - for i in range(1, shade_u.shape[0]): - # Boolean shadow for each timestep i - shade_b = shadow_ == shade_u[i] - # Create regional groups - shade_r = label(shade_b) - # Find out how many regional groups, i.e. unique values - shade_r_u = np.unique(shade_r[0]) - # If more than there groups, i.e. 0, 1, 2, ... , continue - if np.sum(shade_r_u) > 1: - # Loop over the unique values and give all but 1 new values - for j in range(2, shade_r_u.shape[0]): - # Boolean of shadow for each unique value - shade_b2 = shade_r[0] == shade_r_u[j] - # Add +1 to the maximum value of unique values, continues - # (creates new unique values) - shade_max += 1 - # Add these to the building summation matrix - shadow_[shade_b2] = shade_max - - # New unique values of regional groups - shade_u_u = np.unique(shadow_) - # Empty array for storing which timesteps are found in each regional - # group - sh_vec_t = np.zeros((shade_u_u.shape[0], t_l + 1)) - # Adding the unique regional groups to the first column - sh_vec_t[:, 0] = shade_u_u + shade_u = np.unique( + shadow_ + ) # Unique values in summation matrix for tree shadows + shade_max = np.max(shade_u) # Maximum value of unique values + + for i in range(1, shade_u.shape[0]): # Loop over all unique values + shade_b = ( + shadow_ == shade_u[i] + ) # Boolean shadow for each timestep i + shade_r = label(shade_b) # Create regional groups + shade_r_u = np.unique( + shade_r[0] + ) # Find out how many regional groups, i.e. unique values + if ( + np.sum(shade_r_u) > 1 + ): # If more than there groups, i.e. 0, 1, 2, ... , continue + for j in range( + 2, shade_r_u.shape[0] + ): # Loop over the unique values and give all but 1 new values + shade_b2 = ( + shade_r[0] == shade_r_u[j] + ) # Boolean of shadow for each unique value + shade_max += 1 # Add +1 to the maximum value of unique values, continues (creates new unique values) + shadow_[shade_b2] = ( + shade_max # Add these to the building summation matrix + ) + + shade_u_u = np.unique(shadow_) # New unique values of regional groups + sh_vec_t = np.zeros( + (shade_u_u.shape[0], t_l + 1) + ) # Empty array for storing which timesteps are found in each regional group + sh_vec_t[:, 0] = ( + shade_u_u # Adding the unique regional groups to the first column + ) tmrt_t = np.zeros((shade_u_u.shape[0], 2)) tmrt_t[:, 0] = shade_u_u - # Loop over the unique values - for i in range(1, shade_u_u.shape[0]): - # Boolean of each regional group - shade_b = shadow_ == shade_u_u[i] + for i in range(1, shade_u_u.shape[0]): # Loop over the unique values + shade_b = shadow_ == shade_u_u[i] # Boolean of each regional group for j in t_r: # Loop over each timestep - # Boolean of shadow for each timestep - shade_b2 = shadow_ts[:, :, j].copy() == 1 - # Find out where they overlap, i.e. which timesteps are found - # in each regional group - shade_b3 = (shade_b) & (shade_b2) + shade_b2 = ( + shadow_ts[:, :, j].copy() == 1 + ) # Boolean of shadow for each timestep + shade_b3 = (shade_b) & ( + shade_b2 + ) # Find out where they overlap, i.e. which timesteps are found in each regional group if np.sum(shade_b3) > 0: # If they overlap, continue - # Add 1 to timestep column - sh_vec_t[i, 1 + j] = 1 + sh_vec_t[i, 1 + j] = 1 # Add 1 to timestep column tmrt_t[i, 1] += tmrt[j, 0] sh_vec_unique = np.unique(sh_vec_t[:, 1:], axis=0) diff --git a/functions/TreePlanter/TreePlanter/TreePlanterHillClimber.py b/functions/TreePlanter/TreePlanter/TreePlanterHillClimber.py index 34e16fe..3e7de79 100644 --- a/functions/TreePlanter/TreePlanter/TreePlanterHillClimber.py +++ b/functions/TreePlanter/TreePlanter/TreePlanterHillClimber.py @@ -1,5 +1,6 @@ import numpy as np import itertools +from scipy.ndimage import label from ..TreePlanter import HillClimberAlgorithm from ..TreePlanter.adjustments import treenudge from ..TreePlanter import StartingPositions @@ -26,12 +27,15 @@ def treeoptinit( dia = treedata.dia # Diameter of tree canopy - # Empty vector to be filled with Tmrt values for each tree - i_tmrt = np.zeros((r_iters)) - # Empty vector to be filled with corresponding y position of the above - i_y = np.zeros((r_iters, trees)) - # Empty vector to be filled with corresponding x position of the above - i_x = np.zeros((r_iters, trees)) + i_tmrt = np.zeros( + (r_iters) + ) # Empty vector to be filled with Tmrt values for each tree + i_y = np.zeros( + (r_iters, trees) + ) # Empty vector to be filled with corresponding y position of the above + i_x = np.zeros( + (r_iters, trees) + ) # Empty vector to be filled with corresponding x position of the above tree_pos_all = np.zeros((r_iters, trees)) @@ -63,8 +67,7 @@ def treeoptinit( d_tmrt_p = np.zeros((trees)) i_tmrt_max = np.max(i_tmrt) - # Iterate for r_iters number of iterations. Will find optimal positions - # for trees and return the positions and decrease in tmrt + # Iterate for r_iters number of iterations. Will find optimal positions for trees and return the positions and decrease in tmrt for counter in range(r_iters): # Check if plugin is cancelled if feedback.isCanceled(): @@ -86,8 +89,7 @@ def treeoptinit( # Creating starting positions. # If sa = 0, random restart, i - # If sa = 1 evolutionary restart, i.e. y or x is random, the other - # is kept from previous run (previous local optimum) + # If sa = 1 evolutionary restart, i.e. y or x is random, the other is kept from previous run (previous local optimum) if sa == 0: tree_pos, tp_c, break_loop = StartingPositions.random_start( positions.pos[:, 0], trees, tree_pos_all, r_iters @@ -119,10 +121,12 @@ def treeoptinit( break if (counter == 0) | (sa == 0): - # Random y-positions for trees - tree_pos_y = np.zeros((trees), dtype=int) - # Random x-positions for trees - tree_pos_x = np.zeros((trees), dtype=int) + tree_pos_y = np.zeros( + (trees), dtype=int + ) # Random y-positions for trees + tree_pos_x = np.zeros( + (trees), dtype=int + ) # Random x-positions for trees # Y and X positions of starting positions for i2 in range(tree_pos.__len__()): @@ -133,8 +137,7 @@ def treeoptinit( positions.pos[:, 0] == tree_pos[i2], 2 ] - # Euclidean distance between random positions so that trees are not - # too close to each other + # Euclidean distance between random positions so that trees are not too close to each other it_comb = combine(tree_pos, 2) eucl_dist = np.zeros((it_comb.__len__(), 1)) @@ -157,15 +160,16 @@ def treeoptinit( if break_loop == (r_iters + 1): break - # 1 if tree is in the same position as in previous iteration, otherwise - # 0 - tp_nc = np.zeros((trees, 1)) - # 1 if tree is stuck but have been checked for better position and none - # was found, otherwise 0 - tp_nc_a = np.zeros((trees)) + tp_nc = np.zeros( + (trees, 1) + ) # 1 if tree is in the same position as in previous iteration, otherwise 0 + tp_nc_a = np.zeros( + (trees) + ) # 1 if tree is stuck but have been checked for better position and none was found, otherwise 0 - # Iterator to move between trees moving around in the study area - ti = itertools.cycle(range(trees)) + ti = itertools.cycle( + range(trees) + ) # Iterator to move between trees moving around in the study area # Create matrices for tree paths and add starting positions tree_paths_temp = np.zeros((treerasters.rows, treerasters.cols, trees)) @@ -202,8 +206,7 @@ def treeoptinit( tp_nc_a[i] = 0 - # Possibly moving trees that are stuck, where tree shadows - # intersect + # Possibly moving trees that are stuck, where tree shadows intersect elif (tp_nc_a[i] == 0) & (tp_nc[i, 0] == 1): y_out, x_out, tp_nc, tp_nc_a, t1 = treenudge( y_out, @@ -232,8 +235,7 @@ def treeoptinit( i_x[counter, :] = tree_pos_x[:] i_y[counter, :] = tree_pos_y[:] - # Check whether there is a new equal or worse position for each - # individual tree. + # Check whether there is a new equal or worse position for each individual tree. if sa == 1: if i_tmrt[counter] > i_tmrt_max: i_tmrt_max = np.max(i_tmrt) @@ -252,8 +254,9 @@ def treeoptinit( high_p = d_tmrt_temp > d_tmrt_p tree_pos_c[high_p] = 0 - # Path of trees with best positions from starting to ending - if i_tmrt[counter] == np.max(i_tmrt): + if i_tmrt[counter] == np.max( + i_tmrt + ): # Path of trees with best positions from starting to ending tree_paths = tree_paths_temp.copy() if tp_c == 100: @@ -266,8 +269,7 @@ def treeoptinit( i_y_all = i_y.copy() i_x_all = i_x.copy() - # Finding best position from all r_iters iteration, i.e. if r_iters = 1000 - # then best position out of 1000 runs + # Finding best position from all r_iters iteration, i.e. if r_iters = 1000 then best position out of 1000 runs t_max = np.max(i_tmrt) y = np.where(i_tmrt == t_max) diff --git a/functions/TreePlanter/TreePlanter/TreePlanterPrepare.py b/functions/TreePlanter/TreePlanter/TreePlanterPrepare.py index 8ab6840..7153648 100644 --- a/functions/TreePlanter/TreePlanter/TreePlanterPrepare.py +++ b/functions/TreePlanter/TreePlanter/TreePlanterPrepare.py @@ -5,16 +5,16 @@ def treeplanter(treeinput, treedata, treerasters, tmrt_1d): - # Remove all Tmrt values that are in shade or on top of buildings - treeinput.tmrt_s = treeinput.tmrt_s * treeinput.buildings + treeinput.tmrt_s = ( + treeinput.tmrt_s * treeinput.buildings + ) # Remove all Tmrt values that are in shade or on top of buildings bld_copy = treeinput.buildings.copy() # Creating boolean for where it is possible to plant a tree bd_b = np.int_(np.ceil((treedata.dia / 2) / treeinput.gt[1])) - # Buffer on building raster so that trees can't be planted next to walls. - # Can be planted one radius from walls. + # Buffer on building raster so that trees can't be planted next to walls. Can be planted one radius from walls. for i1 in range(bd_b): walls = np.zeros((treeinput.rows, treeinput.cols)) domain = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]]) @@ -32,18 +32,20 @@ def treeplanter(treeinput, treedata, treerasters, tmrt_1d): bld_copy = bld_copy * treeinput.selected_area # Calculating sum of Tmrt in shade for each possible position in the Tmrt matrix - # Empty matrix for sum of Tmrt in tree shadow - sum_tmrt_tsh = np.zeros((treeinput.rows, treeinput.cols)) - # Empty matrix for sum of Tmrt in sun under tree shadow - sum_tmrt = np.zeros((treeinput.rows, treeinput.cols)) + sum_tmrt_tsh = np.zeros( + (treeinput.rows, treeinput.cols) + ) # Empty matrix for sum of Tmrt in tree shadow + sum_tmrt = np.zeros( + (treeinput.rows, treeinput.cols) + ) # Empty matrix for sum of Tmrt in sun under tree shadow - # Coordinates for where it is possible to plant a tree (buildings, area of - # interest excluded) - res_y, res_x = np.where(bld_copy == 1) + res_y, res_x = np.where( + bld_copy == 1 + ) # Coordinates for where it is possible to plant a tree (buildings, area of interest excluded) - # Length of vectors with y and x positions. Will have x and y positions, - # tmrt in shade and in sun and an id for each position - pos_ls = np.zeros((res_y.__len__(), 6)) + pos_ls = np.zeros( + (res_y.__len__(), 6) + ) # Length of vectors with y and x positions. Will have x and y positions, tmrt in shade and in sun and an id for each position index = 0 @@ -82,30 +84,27 @@ def treeplanter(treeinput, treedata, treerasters, tmrt_1d): * tmrt_1d[j, 0] ) - # X position of tree - pos_ls[index, 1] = res_x[i] - # Y position of tree - pos_ls[index, 2] = res_y[i] - # Sum of Tmrt in tree shade - vector - pos_ls[index, 3] = sum_tmrt_tsh[res_y[i], res_x[i]] - # Sum of Tmrt in same area as tree shade but sunlit - vector - pos_ls[index, 4] = sum_tmrt[res_y[i], res_x[i]] + pos_ls[index, 1] = res_x[i] # X position of tree + pos_ls[index, 2] = res_y[i] # Y position of tree + pos_ls[index, 3] = sum_tmrt_tsh[ + res_y[i], res_x[i] + ] # Sum of Tmrt in tree shade - vector + pos_ls[index, 4] = sum_tmrt[ + res_y[i], res_x[i] + ] # Sum of Tmrt in same area as tree shade but sunlit - vector pos_ls[index, 5] = 1 index += 1 pos_bool = pos_ls[:, 3] != 0 pos_ls = pos_ls[pos_bool, :] - # Gives a unique value ranging from 1 to length of pos_ls.shape[0]+1, for - # each position where it is possible to plant a tree + # Gives a unique value ranging from 1 to length of pos_ls.shape[0]+1, for each position where it is possible to plant a tree pos_ls[:, 0] = np.arange(1, pos_ls.shape[0] + 1) - # Adding sum_tmrt and sum_tmrt_tsh to the Treerasters class as well as - # calculating the difference between sunlit and shaded + # Adding sum_tmrt and sum_tmrt_tsh to the Treerasters class as well as calculating the difference between sunlit and shaded treerasters.tmrt(sum_tmrt, sum_tmrt_tsh) - # Adding pos_ls and adding all unique values from pos_ls to their - # respective positions in a 2D matrix and returns a Positions class + # Adding pos_ls and adding all unique values from pos_ls to their respective positions in a 2D matrix and returns a Positions class positions = Position(pos_ls, treeinput.rows, treeinput.cols) return treerasters, positions diff --git a/functions/TreePlanter/TreePlanter/TreePlanterTreeshade.py b/functions/TreePlanter/TreePlanter/TreePlanterTreeshade.py index 6e78bb5..c367db9 100644 --- a/functions/TreePlanter/TreePlanter/TreePlanterTreeshade.py +++ b/functions/TreePlanter/TreePlanter/TreePlanterTreeshade.py @@ -2,10 +2,8 @@ # from ..TreePlanter.adjustments import tree_slice -# This function returns a raster with a boolean shadow and the regional -# group shadow for the tree in position y,x - +# This function returns a raster with a boolean shadow and the regional group shadow for the tree in position y,x def tsh_gen(y, x, treerasters, treeinput): tsh_pos_pad = np.zeros((treeinput.rows, treeinput.cols, y.__len__())) tsh_pos_bool_pad = np.zeros((treeinput.rows, treeinput.cols, y.__len__())) diff --git a/functions/TreePlanter/TreePlanter/adjustments.py b/functions/TreePlanter/TreePlanter/adjustments.py index eaabc5e..830e083 100644 --- a/functions/TreePlanter/TreePlanter/adjustments.py +++ b/functions/TreePlanter/TreePlanter/adjustments.py @@ -21,9 +21,9 @@ def treenudge( tree_adjusted = 0 - # If more than one tree is standing still, check if shadows are next to - # each other - if np.sum(tp_nc[:, 0]) > 1: + if ( + np.sum(tp_nc[:, 0]) > 1 + ): # If more than one tree is standing still, check if shadows are next to each other nc_r = np.where(tp_nc[:, 0] == 1) # Rows of trees standing still nc_y = np.squeeze(y_out[nc_r]) # Y-positions of trees nc_x = np.squeeze(x_out[nc_r]) # X-positions of trees @@ -57,17 +57,16 @@ def treenudge( # Only look at the regional group of the currently moving tree nc_i_r = np.where(nc_label == nc_i) - # Calculate the sum stuck trees in rg nc_i (should be all if - # adjustment should be made) - nc_sum = np.sum(tp_nc[nc_i_r, 0]) + nc_sum = np.sum( + tp_nc[nc_i_r, 0] + ) # Calculate the sum stuck trees in rg nc_i (should be all if adjustment should be made) if (nc_i_r[0].__len__() > 1) & (nc_sum == nc_i_r[0].__len__()): y_dir = np.array([1, -1, 0, 0]) # New temp y-position x_dir = np.array([0, 0, 1, -1]) # New temp x-position dir_bool = np.ones((y_dir.shape[0])) - # Check if it is possible to move in all directions, i.e. are - # these positions possible for trees + # Check if it is possible to move in all directions, i.e. are these positions possible for trees for iy in range(y_dir.shape[0]): y_dir_temp = np.squeeze(y_out[nc_i_r]) + y_dir[iy] x_dir_temp = np.squeeze(x_out[nc_i_r]) + x_dir[iy] @@ -84,17 +83,20 @@ def treenudge( dir_bool[iy] = 0 dir_bool = np.bool_(dir_bool) - # Remove positions where it is not possible to put a tree - y_dir = y_dir[dir_bool] - # Remove positions where it is not possible to put a tree - x_dir = x_dir[dir_bool] + y_dir = y_dir[ + dir_bool + ] # Remove positions where it is not possible to put a tree + x_dir = x_dir[ + dir_bool + ] # Remove positions where it is not possible to put a tree tmrt_nc = np.zeros((y_dir.shape[0], 3)) # New tmrt - # Only go in to if if there are any possible locations for - # trees - if np.any(y_dir.shape[0]): - # Loop for shadows for new positions - for iy in range(y_dir.shape[0]): + if np.any( + y_dir.shape[0] + ): # Only go in to if if there are any possible locations for trees + for iy in range( + y_dir.shape[0] + ): # Loop for shadows for new positions nc_y_c[nc_i_r] = ( np.squeeze(y_out[nc_i_r]) + y_dir[iy] ) # New y-position @@ -112,30 +114,29 @@ def treenudge( * treeinput.shadow[:, :, j] * treeinput.buildings ) - # Tree shade tmrt_nc[iy, 0] += np.sum( tsh_bool_nc_t[:, :, j] * tmrt_1d[j, 0] - ) - # Sunlit + ) # Tree shade tmrt_nc[iy, 1] += np.sum( tsh_bool_nc_t[:, :, j] * treeinput.tmrt_ts[:, :, j] - ) - # New Tmrt (sunlit - tree shade) - tmrt_nc[iy, 2] = tmrt_nc[iy, 1] - tmrt_nc[iy, 0] + ) # Sunlit + tmrt_nc[iy, 2] = ( + tmrt_nc[iy, 1] - tmrt_nc[iy, 0] + ) # New Tmrt (sunlit - tree shade) - # If any new Tmrt decrease is higher, continue if np.around( np.max(tmrt_nc[:, 2]), decimals=1 - ) > np.around(i_tmrt[counter], decimals=1): - # Where is new Tmrt decrease highest + ) > np.around( + i_tmrt[counter], decimals=1 + ): # If any new Tmrt decrease is higher, continue nc_max_r = np.where( tmrt_nc[:, 2] == np.max(tmrt_nc[:, 2]) - ) + ) # Where is new Tmrt decrease highest - # If more than one positions has max tmrt, choose a - # random of the positions - if nc_max_r[0].__len__() > 1: + if ( + nc_max_r[0].__len__() > 1 + ): # If more than one positions has max tmrt, choose a random of the positions nc_max_r = np.random.choice(nc_max_r[0], 1) nc_y_out = ( @@ -153,17 +154,17 @@ def treenudge( t1[0, 3] = y_out[i] t1[0, 4] = x_out[i] - # Reset tp_nc, i.e. trees have moved - tp_nc[nc_i_r, 0] = 0 - # Reset tp_nc_a, i.e. trees have been adjusted - tp_nc_a[nc_i_r] = 1 + tp_nc[nc_i_r, 0] = ( + 0 # Reset tp_nc, i.e. trees have moved + ) + tp_nc_a[nc_i_r] = ( + 1 # Reset tp_nc_a, i.e. trees have been adjusted + ) return y_out, x_out, tp_nc, tp_nc_a, t1 -# Adjusting tree with lowest Tmrt - - +## Adjusting tree with lowest Tmrt def tree_adjust(i_y, i_x, i_tmrt, counter, trees, treerasters, treeinput): a_counter = 0 # Adjustment counter @@ -175,25 +176,34 @@ def tree_adjust(i_y, i_x, i_tmrt, counter, trees, treerasters, treeinput): np.int_(i_y[counter, ix]), np.int_(i_x[counter, ix]) ] - # Bool of position of tree with least decrease in Tmrt - y_min = tmrt_temp[:] == np.min(tmrt_temp[:]) + y_min = tmrt_temp[:] == np.min( + tmrt_temp[:] + ) # Bool of position of tree with least decrease in Tmrt if y_min.shape[0] > 1: - # If more than one index with np.min, return only first - y_min = y_min.cumsum(axis=0).cumsum(axis=0) == 1 - # Bool of positions of the other trees (highest) - y_max = tmrt_temp[:] != np.min(tmrt_temp[:]) - # Current y-positions of all trees for current run - y_te = i_y[counter, :] - # Current x-positions of all trees for current run - x_te = i_x[counter, :] - # y-positions of trees with highest decrease in Tmrt - y_high = np.int_(y_te[y_max]) - # x-positions of trees with highest decrease in Tmrt - x_high = np.int_(x_te[y_max]) - # y-position of tree with lowest decrease in Tmrt - y_low = np.int_(y_te[y_min]) - # x-position of tree with lowest decrease in Tmrt - x_low = np.int_(x_te[y_min]) + y_min = ( + y_min.cumsum(axis=0).cumsum(axis=0) == 1 + ) # If more than one index with np.min, return only first + y_max = tmrt_temp[:] != np.min( + tmrt_temp[:] + ) # Bool of positions of the other trees (highest) + y_te = i_y[ + counter, : + ] # Current y-positions of all trees for current run + x_te = i_x[ + counter, : + ] # Current x-positions of all trees for current run + y_high = np.int_( + y_te[y_max] + ) # y-positions of trees with highest decrease in Tmrt + x_high = np.int_( + x_te[y_max] + ) # x-positions of trees with highest decrease in Tmrt + y_low = np.int_( + y_te[y_min] + ) # y-position of tree with lowest decrease in Tmrt + x_low = np.int_( + x_te[y_min] + ) # x-position of tree with lowest decrease in Tmrt tsh_rg_temp, tsh_bool_temp, comp_ = tsh_gen_ts( y_high, x_high, treerasters, treeinput @@ -214,61 +224,62 @@ def tree_adjust(i_y, i_x, i_tmrt, counter, trees, treerasters, treeinput): tsh_bool_all = tsh_bool_temp == 0 # All pixels that are sunlit - # Remaining pixels, i.e. pixels that are not shaded for sun vs. shade - d_tmrt_pad = treeinput.d_tmrt * tsh_bool_all + d_tmrt_pad = ( + treeinput.d_tmrt * tsh_bool_all + ) # Remaining pixels, i.e. pixels that are not shaded for sun vs. shade tsh_bool_all = np.bool_(1 - tsh_bool_all) # Reverse tsh_bool_all # See if old trees overlap, i.e. the tree that is to be adjusted overlaps with the other trees - # If not, proceed. If they overlap, adjustment for overlap needs to be - # made, etc. - if np.any((tsh_bool_all == 1) & (tsh_bool_min == 1)): + if np.any( + (tsh_bool_all == 1) & (tsh_bool_min == 1) + ): # If not, proceed. If they overlap, adjustment for overlap needs to be made, etc. d_tmrt_vec = ( d_tmrt_pad.flatten() ) # Flatten d_tmrt_pad and create vector - # Sort vector to find pixels with highest difference in Tmrt (sun - # vs. shade) - d_tmrt_vec_s = -np.sort(-d_tmrt_vec) + d_tmrt_vec_s = -np.sort( + -d_tmrt_vec + ) # Sort vector to find pixels with highest difference in Tmrt (sun vs. shade) - # All pixels that have higher difference in Tmrt compared to - # position of tree with lowest difference (decrease) - d_bool = d_tmrt_vec_s > np.min(tmrt_temp[:]) + d_bool = d_tmrt_vec_s > np.min( + tmrt_temp[:] + ) # All pixels that have higher difference in Tmrt compared to position of tree with lowest difference (decrease) d_tmrt_vec_s = d_tmrt_vec_s[d_bool] # Remove all other pixels d_tmrt_vec_s = np.unique(d_tmrt_vec_s) # Only save unique values a_nc = 0 # Adjustment change parameter for ix in range(d_tmrt_vec_s.shape[0]): - # Trying values from d_tmrt_vec_s - tmrt_adjust = d_tmrt_vec_s[ix] - # Find coordinates for tmrt_adjust - y_adjust, x_adjust = np.where(d_tmrt_pad == tmrt_adjust) - # If there are more than one position with tmrt_adjust - for iy in range(y_adjust.shape[0]): - # y-Position of tmrt_adjust - y_temp = np.array([y_adjust[iy] - treerasters.tpy[0]]) - # x-Position of tmrt_adjust - x_temp = np.array([x_adjust[iy] - treerasters.tpx[0]]) + tmrt_adjust = d_tmrt_vec_s[ + ix + ] # Trying values from d_tmrt_vec_s + y_adjust, x_adjust = np.where( + d_tmrt_pad == tmrt_adjust + ) # Find coordinates for tmrt_adjust + for iy in range( + y_adjust.shape[0] + ): # If there are more than one position with tmrt_adjust + y_temp = np.array( + [y_adjust[iy] - treerasters.tpy[0]] + ) # y-Position of tmrt_adjust + x_temp = np.array( + [x_adjust[iy] - treerasters.tpx[0]] + ) # x-Position of tmrt_adjust tsh_rg_adjust, tsh_bool_adjust, comp___ = tsh_gen_ts( - # Create boolean shadow from y_temp, x_temp - y_temp, - x_temp, - treerasters, - treeinput, - ) - # If the shadow of the new position does not overlap with - # the the other trees, proceed + y_temp, x_temp, treerasters, treeinput + ) # Create boolean shadow from y_temp, x_temp if not np.any( (tsh_bool_all == 1) & (tsh_bool_adjust == 1) - ): + ): # If the shadow of the new position does not overlap with the the other trees, proceed y_te[y_min] = y_temp x_te[y_min] = x_temp i_y[counter, :] = y_te # New y-position i_x[counter, :] = x_te # New x-position - # New adjusted Tmrt - i_tmrt[counter] += tmrt_adjust - np.min(tmrt_temp) + i_tmrt[counter] += tmrt_adjust - np.min( + tmrt_temp + ) # New adjusted Tmrt a_nc = 1 print("Adjusted") break diff --git a/functions/URock/CalculatesIndicators.py b/functions/URock/CalculatesIndicators.py index 65c85b1..5b277d6 100644 --- a/functions/URock/CalculatesIndicators.py +++ b/functions/URock/CalculatesIndicators.py @@ -13,6 +13,7 @@ from . import DataUtil from .DataUtil import safe from .GlobalVariables import * +import pandas as pd def obstacleProperties(cursor, obstaclesTable, prefix=PREFIX_NAME): @@ -58,7 +59,7 @@ def obstacleProperties(cursor, obstaclesTable, prefix=PREFIX_NAME): # obstacle area and envelope area query = safe(""" - DROP TABLE IF EXISTS {0}; + DROP TABLE IF EXISTS {0}; CREATE TABLE {0} AS SELECT {1}, {2}, @@ -131,8 +132,7 @@ def zoneProperties(cursor, obstaclePropertiesTable, prefix=PREFIX_NAME): # Name of the output table zoneLengthTable = DataUtil.prefix(outputBaseName, prefix=prefix) - # Create temporary table names (for tables that will be removed at the end - # of the process) + # Create temporary table names (for tables that will be removed at the end of the process) tempoStackedLengthTab = DataUtil.postfix("TEMPO_STACKED_LENGTH_TAB") pointsStackedBlocks = DataUtil.postfix("POINTS_STACKED_BLOCKS") stackedBlocksXExt = DataUtil.postfix("STACKED_BLOCKS_X_EXT") @@ -265,7 +265,7 @@ def zoneProperties(cursor, obstaclePropertiesTable, prefix=PREFIX_NAME): {0}{1} DROP TABLE IF EXISTS {2}; CREATE TABLE {2} - AS SELECT a.{3}, ST_X(a.{4}) AS {5}, + AS SELECT a.{3}, ST_X(a.{4}) AS {5}, ST_AZIMUTH(b.{4}_MIN, a.{4})-PI()/2 AS THETA_LEFT, ST_AZIMUTH(a.{4}, b.{4}_MAX)-PI()/2 AS THETA_RIGHT FROM {6} AS a LEFT JOIN {7} AS b @@ -421,7 +421,7 @@ def studyAreaProperties( ) # nosec B608 cursor.execute( safe(""" - SELECT EXP(1.0 / SUM(OBSTACLE_HEIGHT_TAB.AREA) * + SELECT EXP(1.0 / SUM(OBSTACLE_HEIGHT_TAB.AREA) * SUM(OBSTACLE_HEIGHT_TAB.AREA * LOG(OBSTACLE_HEIGHT_TAB.HEIGHT))) AS H_r, MAX(OBSTACLE_HEIGHT_TAB.HEIGHT) AS H_max FROM (SELECT MAX({0}) AS HEIGHT, diff --git a/functions/URock/DataUtil.py b/functions/URock/DataUtil.py index e92e369..49c46f8 100644 --- a/functions/URock/DataUtil.py +++ b/functions/URock/DataUtil.py @@ -196,11 +196,11 @@ def createIndex(tableName, fieldName, isSpatial): spatialKeyWord = "" if isSpatial: spatialKeyWord = " SPATIAL " - if isinstance(fieldName, type([])): - query = f"""CREATE {spatialKeyWord} INDEX IF NOT EXISTS id_{"_".join(fieldName)}_{tableName} + if type(fieldName) == type([]): + query = f"""CREATE {spatialKeyWord} INDEX IF NOT EXISTS id_{"_".join(fieldName)}_{tableName} ON {tableName}({",".join(fieldName)});""" else: - query = f"""CREATE {spatialKeyWord} INDEX IF NOT EXISTS id_{fieldName}_{tableName} + query = f"""CREATE {spatialKeyWord} INDEX IF NOT EXISTS id_{fieldName}_{tableName} ON {tableName}({fieldName});""" return query @@ -250,7 +250,7 @@ def windDirectionFromXY(windSpeedEast, windSpeedNorth): # Calculate the angle in Radian in a [-pi/2, pi/2] radAngle = np.zeros(windSpeedEast.shape) radAngle[windSpeedEast == 0] = 0 - if isinstance(windSpeedEast, type(pd.Series())): + if type(windSpeedEast) == type(pd.Series()): radAngle[windSpeedEast != 0] = np.arctan( windSpeedNorth[windSpeedEast != 0].divide( windSpeedEast[windSpeedEast != 0] @@ -374,9 +374,7 @@ def getExtremumPoint( return extremumPointTable -# SHOULD BE DELETED SINCE ALREADY IN UMEP !!! - - +####### SHOULD BE DELETED SINCE ALREADY IN UMEP !!! def locate_py(): # get Python version str_ver_qgis = sys.version.split(" ")[0] @@ -394,11 +392,13 @@ def locate_py(): # pre-defined paths for python executable dict_pybin = { "Darwin": path_py / "bin" / "python3", - "Windows": path_py - / ( - "../../bin/pythonw.exe" - if version.parse(str_ver_qgis) >= version.parse("3.9.1") - else "pythonw.exe" + "Windows": ( + path_py + / ( + "../../bin/pythonw.exe" + if version.parse(str_ver_qgis) >= version.parse("3.9.1") + else "pythonw.exe" + ) ), "Linux": path_py, } @@ -426,8 +426,7 @@ def validate_sql_inputs( """ Validate inputs to prevent SQL injection. """ - # Validate field names: must be valid SQL identifiers (alphanumeric + - # underscore, start with letter or underscore) + # Validate field names: must be valid SQL identifiers (alphanumeric + underscore, start with letter or underscore) identifier_pattern = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") if idLines and not identifier_pattern.match(idLines): diff --git a/functions/URock/H2gisConnection.py b/functions/URock/H2gisConnection.py index abb80d0..dfc8d6b 100644 --- a/functions/URock/H2gisConnection.py +++ b/functions/URock/H2gisConnection.py @@ -17,6 +17,7 @@ INSTANCE_PASS, JAVA_PATH_FILENAME, TEMPO_DIRECTORY, + NEW_DB, ) import subprocess import re @@ -145,8 +146,7 @@ def startH2gisInstance( A connection object to the database localH2InstanceDir: String File directory of the database to delete (without extension)""" - # Define where are the jar of the DB and the H2GIS instance (in absolute - # paths) + # Define where are the jar of the DB and the H2GIS instance (in absolute paths) localH2JarDir = dbDirectory + os.sep + H2GIS_UNZIPPED_NAME localH2InstanceDir = dbInstanceDir + os.sep + instanceName + suffix @@ -156,15 +156,13 @@ def startH2gisInstance( print("Connecting to database\n ->%s" % (localH2InstanceDir)) print(localH2JarDir) - # If the DB already exists and if 'newDB' is set to True, delete all the - # DB files + # If the DB already exists and if 'newDB' is set to True, delete all the DB files if isDbExist: os.remove(localH2InstanceDir + DB_EXTENSION) if os.path.exists(localH2InstanceDir + DB_TRACE_EXTENSION): os.remove(localH2InstanceDir + DB_TRACE_EXTENSION) - # get a connection, if a connect cannot be made an exception will be - # raised here + # get a connection, if a connect cannot be made an exception will be raised here conn = jaydebeapi.connect( "org.h2.Driver", "jdbc:h2:" + localH2InstanceDir + ";AUTO_SERVER=TRUE;", @@ -172,8 +170,7 @@ def startH2gisInstance( localH2JarDir, ) - # conn.cursor will return a cursor object, you can use this cursor to - # perform queries + # conn.cursor will return a cursor object, you can use this cursor to perform queries cur = conn.cursor() print("Connected!\n") @@ -254,7 +251,7 @@ def getJavaDir(pluginDirectory): # Java home set. This should be associated to None if javaPath: if javaPath[0] == "%": - javaPath is None + javaPath == None if not javaPath: if os.path.exists(javaPathFile): javaFilePath = open(javaPathFile, "r") @@ -317,8 +314,7 @@ def identifyJavaDir(java_path_os_list): JAVA variable path""" JavaExists = False i = 0 - # Test some common folder paths to check whether a Java installation - # exists and stops once found + # Test some common folder paths to check whether a Java installation exists and stops once found while not (JavaExists or i >= len(java_path_os_list)): javaBaseDir = java_path_os_list[i] JavaExists = os.path.exists(javaBaseDir) @@ -327,7 +323,7 @@ def identifyJavaDir(java_path_os_list): listJavaVersion = os.listdir(javaBaseDir) listSplit = pd.Series() for i, v in enumerate(listJavaVersion): - tempo_split = re.split("\\.|\\-", v) + tempo_split = re.split("\.|\-", v) # Previous command supposed to split version number based on usual characters. # If unusual need the following... if len(tempo_split) != 3: @@ -385,8 +381,7 @@ def getJavaHome(os_type): stdin=subprocess.PIPE, ) output, err = proc.communicate() - # Identify the string corresponding to the java_home in the resulting - # line + # Identify the string corresponding to the java_home in the resulting line javaPath = os.path.abspath( str(output).split("java.home = ")[1].split("\\n")[0] ) @@ -416,7 +411,7 @@ def getJavaHome(os_type): java_key = winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\JavaSoft\JDK" ) - except BaseException: + except: print("Java not found") exit() diff --git a/functions/URock/InitWindField.py b/functions/URock/InitWindField.py index 0d6be3e..3d5fb1c 100644 --- a/functions/URock/InitWindField.py +++ b/functions/URock/InitWindField.py @@ -6,6 +6,11 @@ @author: Jérémy Bernard, University of Gothenburg """ +from . import DataUtil +from .DataUtil import safe +import pandas as pd + +idx = pd.IndexSlice from .GlobalVariables import ( ALONG_WIND_ZONE_EXTEND, CROSS_WIND_ZONE_EXTEND, @@ -108,14 +113,9 @@ IS_UPSTREAM_UPSTREAM_WEIGHTING, CANYON_DELTAH_FIELD, ) -import os -import numpy as np import math -from . import DataUtil -from .DataUtil import safe -import pandas as pd - -idx = pd.IndexSlice +import numpy as np +import os def createGrid( @@ -185,8 +185,8 @@ def createGrid( ST_Y({1}) AS {10} FROM ST_MAKEGRIDPOINTS((SELECT ST_EXPAND(ST_EXTENT({1}), {2}, - {3}) FROM ({5})), - {4}, + {3}) FROM ({5})), + {4}, {4})""").format( gridTable, GEOM_FIELD, @@ -234,7 +234,7 @@ def affectsPointToBuildZone( Vertical lines (not along z but along the grid "north-south") useful for future calculations""" print( - """Affects each grid point to a building Rockle zone and calculates needed + """Affects each grid point to a building Rockle zone and calculates needed variables for 3D wind speed""" ) @@ -322,7 +322,7 @@ def affectsPointToBuildZone( ) elif t == ROOFTOP_CORN_NAME: columnsToKeepQuery = safe( - """b.{0}, a.{1}, a.{2}, b.{3}, a.{4}, a.{5}, a.{6}, a.{7}, + """b.{0}, a.{1}, a.{2}, b.{3}, a.{4}, a.{5}, a.{6}, a.{7}, a.GEOM_CORNER_POINT, b.{8} """ @@ -339,7 +339,7 @@ def affectsPointToBuildZone( ) query.append( - safe(""" + safe(""" {5}; DROP TABLE IF EXISTS {2}; CREATE TABLE {2} @@ -369,11 +369,11 @@ def affectsPointToBuildZone( # For Rockle zones that needs relative point distance, extra calculation is needed # First creates vertical lines - endOfQuery = safe(""" + endOfQuery = safe(""" {6}; {7}; DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} + CREATE TABLE {0} AS SELECT a.{1}, ST_MAKELINE(b.{2}, a.{2}) AS {2}, b.{4}, @@ -406,7 +406,7 @@ def affectsPointToBuildZone( b.{1}, a.{2}, b.{6}, - ST_YMIN(ST_INTERSECTION(a.{3}, + ST_YMIN(ST_INTERSECTION(a.{3}, ST_TOMULTILINE(b.{3})) ) AS {5}, ST_LENGTH(ST_INTERSECTION(a.{3}, b.{3})) AS {4} @@ -423,7 +423,7 @@ def affectsPointToBuildZone( b.{1}, a.{2}, b.{6}, - ST_YMIN(ST_INTERSECTION(a.{3}, + ST_YMIN(ST_INTERSECTION(a.{3}, ST_TOMULTILINE(b.{3})) ) AS {5}, ST_LENGTH(ST_INTERSECTION(a.{3}, b.{3})) AS {4} @@ -439,14 +439,14 @@ def affectsPointToBuildZone( CAVITY_NAME: """b.{0}, b.{1}, a.{2}, - ST_YMAX(ST_INTERSECTION(a.{3}, + ST_YMAX(ST_INTERSECTION(a.{3}, b.{3}) ) AS {5}, a.{6}, - CASE WHEN ST_DIMENSION(ST_INTERSECTION(a.{3}, + CASE WHEN ST_DIMENSION(ST_INTERSECTION(a.{3}, b.{3}))=0 THEN 0 - ELSE ST_LENGTH(ST_MAKELINE(ST_TOMULTIPOINT(ST_INTERSECTION(a.{3}, + ELSE ST_LENGTH(ST_MAKELINE(ST_TOMULTIPOINT(ST_INTERSECTION(a.{3}, b.{3}) ) ) @@ -455,11 +455,11 @@ def affectsPointToBuildZone( b.{7}, CASE WHEN a.{8} > b.{9} THEN b.{10} - ELSE b.{11} + ELSE b.{11} END AS {12}, CASE WHEN a.{8} > b.{9} THEN b.{13} - ELSE b.{14} + ELSE b.{14} END AS {15}, POWER((b.{16}/2-ABS(a.{8}-b.{18}))/(b.{16}/2),0.5) AS {17} """.format( @@ -486,13 +486,13 @@ def affectsPointToBuildZone( WAKE_NAME: """b.{0}, b.{1}, a.{2}, - ST_YMAX(ST_INTERSECTION(a.{3}, + ST_YMAX(ST_INTERSECTION(a.{3}, b.{3}) ) AS {5}, - CASE WHEN ST_DIMENSION(ST_INTERSECTION(a.{3}, + CASE WHEN ST_DIMENSION(ST_INTERSECTION(a.{3}, b.{3}))=0 THEN 0 - ELSE ST_LENGTH(ST_MAKELINE(ST_TOMULTIPOINT(ST_INTERSECTION(a.{3}, + ELSE ST_LENGTH(ST_MAKELINE(ST_TOMULTIPOINT(ST_INTERSECTION(a.{3}, b.{3}) ) ) @@ -508,31 +508,33 @@ def affectsPointToBuildZone( Y_WALL, ID_FIELD_STACKED_BLOCK, ), - STREET_CANYON_NAME: f"""b.{idZone[STREET_CANYON_NAME]}, + STREET_CANYON_NAME: ( + f"""b.{idZone[STREET_CANYON_NAME]}, b.{UPSTREAM_HEIGHT_FIELD}, LEAST(b.{DOWNSTREAM_HEIGHT_FIELD}, b.{UPSTREAM_HEIGHT_FIELD}) AS {MAX_CANYON_HEIGHT_FIELD}, b.{DOWNSTREAM_HEIGHT_FIELD}-b.{UPSTREAM_HEIGHT_FIELD} AS {CANYON_DELTAH_FIELD}, b.{UPWIND_FACADE_ANGLE_FIELD}, a.{ID_POINT_X}, b.{BASE_HEIGHT_FIELD}, - ST_YMAX(ST_INTERSECTION(a.{GEOM_FIELD}, + ST_YMAX(ST_INTERSECTION(a.{GEOM_FIELD}, b.{GEOM_FIELD})) AS {Y_WALL}, ST_LENGTH(ST_MAKELINE(ST_TOMULTIPOINT(ST_INTERSECTION(a.{GEOM_FIELD}, b.{GEOM_FIELD}) ) ) - ) AS {LENGTH_ZONE_FIELD + STREET_CANYON_NAME[0]}, + ) AS {LENGTH_ZONE_FIELD+STREET_CANYON_NAME[0]}, b.{UPWIND_FACADE_FIELD}, b.{ID_UPSTREAM_STACKED_BLOCK}, a.{ID_POINT_Y}, b.{DOWNWIND_FACADE_FIELD} - """, + """ + ), ROOFTOP_PERP_NAME: """b.{0}, b.{1}, a.{2}, b.{3}, b.{4}, - ST_YMAX(ST_INTERSECTION(a.{5}, + ST_YMAX(ST_INTERSECTION(a.{5}, b.{5}) ) AS {6} """.format( @@ -556,7 +558,7 @@ def affectsPointToBuildZone( CREATE TABLE {0} AS SELECT {5} FROM {3} AS a, {2} AS b - WHERE a.{1} && b.{1} AND ST_INTERSECTS(a.{1}, b.{1}) AND + WHERE a.{1} && b.{1} AND ST_INTERSECTS(a.{1}, b.{1}) AND ST_DIMENSION(ST_INTERSECTION(a.{1}, b.{1})) > 0; """).format( dicOfPrefixZoneLim[t], @@ -579,8 +581,7 @@ def affectsPointToBuildZone( # The cavity zone length is needed for the wind speed calculation of # wake zone points and for the upper height limit of the street canyon - # while the wake zone length is needed for cavity zone wind speed - # calculation + # while the wake zone length is needed for cavity zone wind speed calculation cursor.execute( safe(""" {idx1} @@ -588,40 +589,40 @@ def affectsPointToBuildZone( {idx3} {idx4} DROP TABLE IF EXISTS TEMPO_WAKE; - CREATE TABLE TEMPO_WAKE - AS SELECT a.*, - b.{len_cav}, - b.{cos_az}, b.{sin_az}, + CREATE TABLE TEMPO_WAKE + AS SELECT a.*, + b.{len_cav}, + b.{cos_az}, b.{sin_az}, b.{rel_pos} - FROM {wake_table} AS a LEFT JOIN {cavity_table} AS b - ON a.{downwind_f} = b.{downwind_f} + FROM {wake_table} AS a LEFT JOIN {cavity_table} AS b + ON a.{downwind_f} = b.{downwind_f} AND a.{id_px} = b.{id_px}; DROP TABLE IF EXISTS {wake_table}; ALTER TABLE TEMPO_WAKE RENAME TO {wake_table}; - + {idx5} {idx6} {idx7} {idx8} DROP TABLE IF EXISTS TEMPO_CANYON; - CREATE TABLE TEMPO_CANYON - AS SELECT a.*, + CREATE TABLE TEMPO_CANYON + AS SELECT a.*, b.{len_cav} - FROM {canyon_table} AS a LEFT JOIN {cavity_table} AS b + FROM {canyon_table} AS a LEFT JOIN {cavity_table} AS b ON a.{downwind_f} = b.{downwind_f} AND a.{id_px} = b.{id_px}; DROP TABLE IF EXISTS {canyon_table}; ALTER TABLE TEMPO_CANYON RENAME TO {canyon_table}; - + {idx9} {idx10} {idx11} {idx12} DROP TABLE IF EXISTS TEMPO_CAV; - CREATE TABLE TEMPO_CAV - AS SELECT a.*, + CREATE TABLE TEMPO_CAV + AS SELECT a.*, b.{len_wake} - FROM {cavity_table} AS a LEFT JOIN {wake_table} AS b + FROM {cavity_table} AS a LEFT JOIN {wake_table} AS b ON a.{downwind_f} = b.{downwind_f} AND a.{id_px} = b.{id_px}; DROP TABLE IF EXISTS {cavity_table}; @@ -954,8 +955,7 @@ def affectsPointToBuildZone( ) ) - # Special treatment for rooftop corners which have not been calculated - # previously + # Special treatment for rooftop corners which have not been calculated previously cursor.execute( safe("""DROP TABLE IF EXISTS {rooftop_table}; CREATE TABLE {rooftop_table} @@ -1031,8 +1031,10 @@ def affectsPointToVegZone( Dictionary having as key the type of vegetation Rockle zone and as value the name of the table containing points corresponding to the vegetation zone """ - print("""Affects each grid point to a vegetation Rockle zone and calculates - needed variables for 3D wind speed""") + print( + """Affects each grid point to a vegetation Rockle zone and calculates + needed variables for 3D wind speed""" + ) # Name of the output tables dicOfOutputTables = { @@ -1051,9 +1053,9 @@ def affectsPointToVegZone( cursor.execute( safe(";").join( [ - safe(""" + safe(""" {12}; - {13}; + {13}; DROP TABLE IF EXISTS {0}; CREATE TABLE {0} AS SELECT a.{1}, a.{2}, MAX(b.{3}) AS {7} @@ -1167,8 +1169,7 @@ def removeBuildZonePoints( # 1. IDENTIFY BUILDINGS PARTS (X POSITION) HAVING THEIR DOWNSTREAM FACADE INCLUDED WITHIN AN # UPSTREAM CAVITY ZONE - # First identify the coordinate of the upstreamer point for each X - # coordinate to each cavity zone + # First identify the coordinate of the upstreamer point for each X coordinate to each cavity zone cursor.execute( safe(""" {5}{6} @@ -1196,8 +1197,7 @@ def removeBuildZonePoints( ) ) - # Then identify the maximum height of the cavity zone for each of these - # points + # Then identify the maximum height of the cavity zone for each of these points cursor.execute( safe(""" {9}{10}{11}{12}{13}{14} @@ -1297,8 +1297,7 @@ def removeBuildZonePoints( ) ) - # Add all remaining cavity zones (having no other cavity zone contained in - # their cavity zone) + # Add all remaining cavity zones (having no other cavity zone contained in their cavity zone) cursor.execute( safe(""" {7}{8}{9}{10} @@ -1449,8 +1448,7 @@ def removeBuildZonePoints( WAKE_NAME: [ID_FIELD_STACKED_BLOCK, ID_POINT_X], CAVITY_NAME: [ID_UPSTREAM_STACKED_BLOCK, ID_POINT_X], } - # Take all points from a 't' zone which have not been deleted by a cavity - # zone + # Take all points from a 't' zone which have not been deleted by a cavity zone cursor.execute( safe(";").join( [ @@ -1519,8 +1517,7 @@ def removeBuildZonePoints( ) ) - # At the end only one point per position is conserved, the points from the - # more downstream zone + # At the end only one point per position is conserved, the points from the more downstream zone cursor.execute( safe(""" {5}{6}{7}{8} @@ -1567,7 +1564,7 @@ def removeBuildZonePoints( cursor.execute( safe(";").join( [ - safe(""" + safe(""" {8}; {9}; {10}; @@ -1617,7 +1614,7 @@ def removeBuildZonePoints( cursor.execute( safe(";").join( [ - safe(""" + safe(""" {5}; {6}; {7}; @@ -1668,7 +1665,7 @@ def removeBuildZonePoints( cursor.execute( safe(";").join( [ - safe(""" + safe(""" DROP TABLE IF EXISTS {0}; ALTER TABLE {1} RENAME TO {0} """).format( @@ -1795,7 +1792,7 @@ def manageBackwardZones( {8}{9}{10}{11}{12}{13} DROP TABLE IF EXISTS {14}; CREATE TABLE {14} - AS SELECT a.{1}, a.{2}, a.{3}, a.{4}, b.{17}-b.{18} AS {17}, b.{18}, b.{19}, + AS SELECT a.{1}, a.{2}, a.{3}, a.{4}, b.{17}-b.{18} AS {17}, b.{18}, b.{19}, TRUNC(b.{16} / {21}) + 1 AS {22}, b.{20} FROM {0} AS a LEFT JOIN {5} AS b ON a.{1} = b.{1} AND a.{3} = b.{3} AND a.{4} = b.{4} @@ -2111,8 +2108,7 @@ def calculates3dBuildWindFactor( # Temporary tables (and prefix for temporary tables) zValueTable = DataUtil.postfix("Z_VALUES") - # Identify the maximum height where wind speed may be affected by building - # obstacles + # Identify the maximum height where wind speed may be affected by building obstacles maxHeightQuery = { DISPLACEMENT_NAME: "MAX({0}) AS MAX_HEIGHT".format( UPPER_VERTICAL_THRESHOLD @@ -2152,8 +2148,7 @@ def calculates3dBuildWindFactor( ) maxHeight = cursor.fetchall()[0][0] - # Creates the table of z levels impacted by building obstacles (start at - # dz/2) + # Creates the table of z levels impacted by building obstacles (start at dz/2) if maxHeight: if maxHeight > float(dz) / 2: listOfZ = [ @@ -2361,7 +2356,7 @@ def calculates3dBuildWindFactor( UPPER_VERTICAL_THRESHOLD, UPPER_VERTICAL_THRESHOLD + CAVITY_NAME[0], ), - STREET_CANYON_NAME: """b.{0} < a.{1} + STREET_CANYON_NAME: """b.{0} < a.{1} AND b.{0} < a.{2}""".format( Z, UPPER_VERTICAL_THRESHOLD, MAX_CANYON_HEIGHT_FIELD ), @@ -2369,7 +2364,7 @@ def calculates3dBuildWindFactor( AND b.{0} > a.{1}""".format( Z, HEIGHT_FIELD, ROOFTOP_PERP_VAR_HEIGHT ), - ROOFTOP_CORN_NAME: """b.{0} < a.{1}+a.{2} + ROOFTOP_CORN_NAME: """b.{0} < a.{1}+a.{2} AND b.{0} > a.{1}""".format( Z, HEIGHT_FIELD, ROOFTOP_CORNER_VAR_HEIGHT ), @@ -2554,7 +2549,7 @@ def calculates3dVegWindFactor( [ safe(""" {10}; - {11}; + {11}; DROP TABLE IF EXISTS {0}; CREATE TABLE {0} AS SELECT b.{9}, a.{1}, {2} AS {3} @@ -2893,8 +2888,7 @@ def manageSuperimposition( ) ) - # Deal with superimposition when duplicated points between backward cavity - # and wake zones + # Deal with superimposition when duplicated points between backward cavity and wake zones upstreamBackPrioritiesTempoTable = manageUpstreamSuperimposition( cursor, dicAllWeightFactorsTables=dicBackwardWeighted, @@ -2913,7 +2907,7 @@ def manageSuperimposition( {idx2} DROP TABLE IF EXISTS {result_table}; CREATE TABLE {result_table} - AS SELECT a.* + AS SELECT a.* FROM {upstream_table} AS a LEFT JOIN {weighted_table} AS b ON a.{id_p} = b.{id_p} AND a.{id_z} = b.{id_z} WHERE b.{id_p} IS NOT NULL AND b.{id_z} IS NOT NULL @@ -3171,7 +3165,7 @@ def manageUpstreamSuperimposition( CREATE TABLE {10} AS SELECT {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {16} FROM {1} - UNION ALL + UNION ALL SELECT a.{2}, a.{3}, a.{4}, a.{5}, a.{6}, a.{7}, NULL AS {8}, {11} AS {9}, @@ -3298,8 +3292,7 @@ def manageUpstreamSuperimposition( ) ) - # Join the upstream priority weigthted points to the upstream priority - # non-weighted ones + # Join the upstream priority weigthted points to the upstream priority non-weighted ones cursor.execute( safe(""" {10}; @@ -3426,9 +3419,8 @@ def identifyUpstreamer( tempoAllPointsTable = DataUtil.postfix("TEMPO_3D_ALL", suffix=prefix) tempoUniquePointsTable = DataUtil.postfix("TEMPO_3D_UNIQUE", suffix=prefix) - # If priorities should be used, recover list of tables and add columns to - # keep - if isinstance(tablesToConsider, type(pd.DataFrame())): + # If priorities should be used, recover list of tables and add columns to keep + if type(tablesToConsider) == type(pd.DataFrame()): listOfTables = tablesToConsider.index defineCol2Add = "{0} INTEGER, {1} INTEGER, {2} INTEGER,".format( REF_HEIGHT_FIELD, PRIORITY_FIELD, IS_UPSTREAM_FIELD @@ -3444,15 +3436,15 @@ def identifyUpstreamer( for t in listOfTables: selectQueryDownstream[t] = """ SELECT CAST((row_number() over()) as Integer) AS {0}, {1}, {2}, - {3}, {4}, + {3}, {4}, """.format( ID_3D_POINT, ID_POINT, ID_POINT_Z, HEIGHT_FIELD, Y_WALL ) # If priorities should be used, add columns to keep - if isinstance(tablesToConsider, type(pd.DataFrame())): + if type(tablesToConsider) == type(pd.DataFrame()): selectQueryDownstream[t] += """ - {0} AS {2}, + {0} AS {2}, {1} AS {3}, {4} AS {5}, """.format( @@ -3492,7 +3484,7 @@ def identifyUpstreamer( safe(""" DROP TABLE IF EXISTS {0}; CREATE TABLE {0}({1} BIGINT AUTO_INCREMENT, {2} INTEGER, {3} INTEGER, - {4} INTEGER, {5} INTEGER, {6} + {4} INTEGER, {5} INTEGER, {6} {7} DOUBLE, {8} DOUBLE, {9} DOUBLE) AS {10} """).format( @@ -3571,7 +3563,7 @@ def identifyUpstreamer( CREATE TABLE {unique_table} AS SELECT a.* FROM {all_points_table} AS a RIGHT JOIN {unique_points_table} AS b - ON a.{id_3d} = b.{id_3d} AND + ON a.{id_3d} = b.{id_3d} AND a.{id_p} = b.{id_p} """).format( idx1=DataUtil.createIndex( @@ -3902,7 +3894,7 @@ def setInitialWindField( # Get the wind speed at each building height value... cursor.execute(safe(""" SELECT DISTINCT({0}) AS {0} FROM {1} - WHERE {0} IS NOT NULL; + WHERE {0} IS NOT NULL; """).format(HEIGHT_FIELD, initializedWindFactorTable)) buildingHeightList = cursor.fetchall() if len(buildingHeightList) > 0: @@ -3969,16 +3961,16 @@ def setInitialWindField( AS SELECT a.{5}, a.{2}, CASE WHEN a.{3}=1 - THEN (SELECT c.{6} + THEN (SELECT c.{6} FROM {10} AS c WHERE a.{11} = c.{11}) WHEN a.{3} = 2 THEN {7} WHEN a.{3} = 3 - THEN (SELECT b.{6} + THEN (SELECT b.{6} FROM {1} AS b WHERE a.{2} = b.{2}) - ELSE (SELECT 5 * b.{6} + ELSE (SELECT 5 * b.{6} FROM {1} AS b WHERE a.{2} = b.{2}) END AS WIND_SPEED, @@ -4101,8 +4093,7 @@ def setInitialWindField( ) # Renormalize wind speed at each height to make sure there is no offset of - # wind speed between the wind profile and the initialization before the - # balance of wind + # wind speed between the wind profile and the initialization before the balance of wind if REMOVE_INITIALIZATION_OFFSET: max_zi = df_wind0_rockle.index.get_level_values("ID_Z").max() for z_i in verticalWindSpeedProfile.index[1 : max_zi + 1]: @@ -4218,8 +4209,7 @@ def identifyBuildPoints( """).format(HEIGHT_FIELD, stackedBlocksWithBaseHeight)) buildMaxHeight = cursor.fetchall()[0][0] - # Set a list of the level height (and indice) which can intersect with - # buildings + # Set a list of the level height (and indice) which can intersect with buildings if buildMaxHeight: levelHeightList = [ str(j + 1) + "," + str(i) @@ -4251,8 +4241,7 @@ def identifyBuildPoints( CREATE TABLE {0}({1} INTEGER, {2} DOUBLE); """).format(tempoLevelHeightPointTable, ID_POINT_Z, Z)) - # Identify the third dimension of points intersecting buildings and save - # it... + # Identify the third dimension of points intersecting buildings and save it... cursor.execute( safe(""" {9}; @@ -4315,8 +4304,7 @@ def identifyBuildPoints( df_gridBuil.index.levels[1] - 1, level=1 ) - # Consider as buildings points which are surrounded by 3 vertical walls - # (leads to numerical issues... See issue #) + # Consider as buildings points which are surrounded by 3 vertical walls (leads to numerical issues... See issue #) ind2remove = ( df_wall_left.intersection(df_wall_right) .intersection(df_wall_behind) diff --git a/functions/URock/MainCalculation.py b/functions/URock/MainCalculation.py index b05a0ea..1af8fcf 100644 --- a/functions/URock/MainCalculation.py +++ b/functions/URock/MainCalculation.py @@ -22,7 +22,7 @@ import datetime import time import numpy as np -import math +from shutil import rmtree try: from numba import jit @@ -77,12 +77,11 @@ def main( profileType=PROFILE_TYPE, verticalProfileFile=None, ): - # If the function is called within QGIS, a feedback is sent into the QGIS - # interface + # If the function is called within QGIS, a feedback is sent into the QGIS interface if feedback: feedback.setProgressText("Initiating algorithm") - ################################ INIT OUTPUT VARIABLES ################### + ################################ INIT OUTPUT VARIABLES ############################ # Create the temporary directory if not exists tmp_dir_unique = os.path.join( TEMPO_DIRECTORY, @@ -171,9 +170,9 @@ def main( i: os.path.abspath(outputDataRel[i]) for i in outputDataRel } - ########################################################################## - ################################ SCRIPT ################################## - ########################################################################## + ############################################################################ + ################################ SCRIPT #################################### + ############################################################################ # ---------------------------------------------------------------------- # 1. SET H2GIS DATABASE ENVIRONMENT AND LOAD DATA # ---------------------------------------------------------------------- @@ -215,7 +214,7 @@ def main( timeStartCalculation = time.time() # ----------------------------------------------------------------------------------- - # 2. CREATES OBSTACLE GEOMETRIES ----------------------------------------- + # 2. CREATES OBSTACLE GEOMETRIES ---------------------------------------------------- # ----------------------------------------------------------------------------------- if feedback: feedback.setProgressText( @@ -374,7 +373,7 @@ def main( ) # ----------------------------------------------------------------------------------- - # 4. CREATES THE 2D ROCKLE ZONES ----------------------------------------- + # 4. CREATES THE 2D ROCKLE ZONES ---------------------------------------------------- # ----------------------------------------------------------------------------------- if feedback: feedback.setProgressText("Creates the 2D Röckle zones") @@ -611,8 +610,7 @@ def main( prefix=prefix, ) - # Affects each 2D point to a build Rockle zone and calculates needed - # variables for 3D wind speed factors + # Affects each 2D point to a build Rockle zone and calculates needed variables for 3D wind speed factors dicOfInitBuildZoneGridPoint, verticalLineTable = ( InitWindField.affectsPointToBuildZone( cursor=cursor, @@ -637,8 +635,7 @@ def main( prefix=prefix, ) - # Manage backward cavity and wake zones in the leeward zone of tall - # buildings + # Manage backward cavity and wake zones in the leeward zone of tall buildings dicOfBuildZoneGridPoint, facadeWithinCavity = ( InitWindField.manageBackwardZones( cursor=cursor, @@ -654,7 +651,7 @@ def main( ) # ----------------------------------------------------------------------------------- - # 6. INITIALIZE THE 3D WIND FACTORS IN THE ROCKLE ZONES ------------------ + # 6. INITIALIZE THE 3D WIND FACTORS IN THE ROCKLE ZONES ------------------------------- # ----------------------------------------------------------------------------------- if feedback: feedback.setProgressText("Initializes the 3D grid within Röckle zones") @@ -766,8 +763,7 @@ def main( # ---------------------------------------------------------------- # 7. DEALS WITH SUPERIMPOSED ZONES ------------------------------- # ---------------------------------------------------------------- - # Calculates the final weighting factor for each point, dealing with - # duplicates (superimposition) + # Calculates the final weighting factor for each point, dealing with duplicates (superimposition) dicAllWeightFactorsTables = dicOfBuildZone3DWindFactor.copy() dicAllWeightFactorsTables[ALL_VEGETATION_NAME] = ( vegetationWeightFactorTable @@ -866,8 +862,7 @@ def main( cursor.close() feedback.setProgressText("Calculation cancelled by user") return {} - # Set the ground as "building" (understand solid wall) - after getting - # grid size + # Set the ground as "building" (understand solid wall) - after getting grid size nx, ny, nz = nPoints.values() df_gridBuil = df_gridBuil.reindex( df_gridBuil.index.append( diff --git a/functions/URock/Obstacles.py b/functions/URock/Obstacles.py index da15102..613d072 100644 --- a/functions/URock/Obstacles.py +++ b/functions/URock/Obstacles.py @@ -69,8 +69,7 @@ def windRotation( rotationCenterCoordinates = cursor.fetchall()[0] columnNames = {} - # Store the column names (except geometry field) of each table into a - # dictionary + # Store the column names (except geometry field) of each table into a dictionary for i, t in enumerate(dicOfInputTables.values()): columnNames[t] = DataUtil.getColumns(cursor=cursor, tableName=t) columnNames[t].remove(GEOM_FIELD) @@ -133,8 +132,7 @@ def createsBlocks( (only buildings having the same height are merged)""" print("Creates blocks and stacked blocks") - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) correlTable = DataUtil.postfix("correl_table") # Creates final tables @@ -145,9 +143,9 @@ def createsBlocks( # would be much more efficient) cursor.execute( safe(""" - DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} - AS SELECT EXPLOD_ID AS {1}, ST_MAKEVALID(ST_SIMPLIFY(ST_NORMALIZE({2}), {5})) AS {2} + DROP TABLE IF EXISTS {0}; + CREATE TABLE {0} + AS SELECT EXPLOD_ID AS {1}, ST_MAKEVALID(ST_SIMPLIFY(ST_NORMALIZE({2}), {5})) AS {2} FROM ST_EXPLODE ('(SELECT ST_UNION(ST_ACCUM(ST_BUFFER({2},{3},''join=mitre''))) AS {2} FROM {4})'); """).format( @@ -166,7 +164,7 @@ def createsBlocks( {7}; {8}; DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} + CREATE TABLE {0} AS SELECT a.{1}, a.{2}, CAST(a.{3} AS INT) AS {3}, b.{4}, b.{2} AS GEOM_BLOCK FROM {5} AS a, {6} AS b @@ -188,8 +186,7 @@ def createsBlocks( ) ) - # Identify all possible values of height for buildings being in a more - # than 1 building block + # Identify all possible values of height for buildings being in a more than 1 building block cursor.execute( safe(""" {0}; @@ -202,7 +199,7 @@ def createsBlocks( ) ) cursor.execute(safe(""" - SELECT DISTINCT a.{2} + SELECT DISTINCT a.{2} FROM {0} AS a RIGHT JOIN (SELECT {1} FROM {0} GROUP BY {1}) AS b ON a.{1} = b.{1}; """).format(correlTable, ID_FIELD_BLOCK, HEIGHT_FIELD)) @@ -223,20 +220,20 @@ def createsBlocks( {snappingTolerance}) ) AS {GEOM_FIELD}, a.{ID_FIELD_BLOCK} AS {ID_FIELD_BLOCK} - FROM {correlTable} AS a RIGHT JOIN (SELECT {ID_FIELD_BLOCK} + FROM {correlTable} AS a RIGHT JOIN (SELECT {ID_FIELD_BLOCK} FROM {correlTable} WHERE {HEIGHT_FIELD}={height_i}) AS b ON a.{ID_FIELD_BLOCK}=b.{ID_FIELD_BLOCK} WHERE a.{HEIGHT_FIELD}>={height_i} GROUP BY a.{ID_FIELD_BLOCK})') WHERE ST_ISEMPTY({GEOM_FIELD}) IS FALSE - """ + """ # nosec B608 for height_i in df_listOfHeight ] # nosec B608 cursor.execute(f""" DROP TABLE IF EXISTS {stackedBlockTable}; - CREATE TABLE {stackedBlockTable}({ID_FIELD_STACKED_BLOCK} BIGINT AUTO_INCREMENT, - {ID_FIELD_BLOCK} INT, - {GEOM_FIELD} GEOMETRY, + CREATE TABLE {stackedBlockTable}({ID_FIELD_STACKED_BLOCK} BIGINT AUTO_INCREMENT, + {ID_FIELD_BLOCK} INT, + {GEOM_FIELD} GEOMETRY, {HEIGHT_FIELD} INT) AS SELECT CAST((row_number() over()) as Integer) AS {ID_FIELD_STACKED_BLOCK}, {ID_FIELD_BLOCK}, @@ -292,8 +289,7 @@ def identifyBlockAndCavityBase(cursor, stackedBlockTable, prefix=PREFIX_NAME): height and block cavity base height""" print("Identify block base height and block cavity base") - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) tempoAllStacked = DataUtil.postfix("tempo_all_stacked_table") tempoAllBlocks = DataUtil.postfix("tempo_all_blocks_table") tempoCavityStacked = DataUtil.postfix("tempo_cavity_stacked_table") @@ -304,15 +300,14 @@ def identifyBlockAndCavityBase(cursor, stackedBlockTable, prefix=PREFIX_NAME): "stacked_block_prop_table", prefix=prefix ) - # Identify each block base height and ratio of area between the stacked - # and its base block + # Identify each block base height and ratio of area between the stacked and its base block cursor.execute( safe(""" {7}; {8}; {9}; DROP TABLE IF EXISTS {4}; - CREATE TABLE {4} + CREATE TABLE {4} AS SELECT a.{2}, a.{5}, MIN(a.{3}) AS {3}, MAX(b.{3}) AS {6}, MAX((ST_XMAX(a.{1})-ST_XMIN(a.{1}))/ (ST_XMAX(b.{1})-ST_XMIN(b.{1}))) AS AREA_RATIO @@ -352,7 +347,7 @@ def identifyBlockAndCavityBase(cursor, stackedBlockTable, prefix=PREFIX_NAME): {8}; {9}; DROP TABLE IF EXISTS {4}; - CREATE TABLE {4} + CREATE TABLE {4} AS SELECT b.{1}, b.{2}, b.{5}, b.{3}, COALESCE(a.{6}, 0) AS {6}, COALESCE(a.AREA_RATIO, 0) AS AREA_RATIO FROM {0} AS a RIGHT JOIN {7} AS b ON a.{5} = b.{5} @@ -386,7 +381,7 @@ def identifyBlockAndCavityBase(cursor, stackedBlockTable, prefix=PREFIX_NAME): {9}; {10}; DROP TABLE IF EXISTS {4}; - CREATE TABLE {4} + CREATE TABLE {4} AS SELECT a.{1}, a.{2}, a.{5}, MIN(a.{3}) AS {3}, MAX(a.{6}) AS {6}, MAX(a.{6})-a.AREA_RATIO*MIN(a.{6}-b.{6}) AS {7} FROM {0} AS a LEFT JOIN {0} AS b ON a.{2} = b.{2} @@ -418,16 +413,15 @@ def identifyBlockAndCavityBase(cursor, stackedBlockTable, prefix=PREFIX_NAME): ) ) - # Same as previous for stacked buildings being above a ground building - # (not a stacked one...) + # Same as previous for stacked buildings being above a ground building (not a stacked one...) cursor.execute( safe(""" {9}; {10}; DROP TABLE IF EXISTS {4}; - CREATE TABLE {4} + CREATE TABLE {4} AS SELECT a.{1}, a.{2}, a.{5}, a.{3}, a.{6}, - COALESCE(a.{7}, + COALESCE(a.{7}, b.{6}*(1-b.AREA_RATIO)) AS {7} FROM {0} AS a RIGHT JOIN {8} AS b ON a.{5} = b.{5} """).format( @@ -459,7 +453,7 @@ def identifyBlockAndCavityBase(cursor, stackedBlockTable, prefix=PREFIX_NAME): {9}; {10}; DROP TABLE IF EXISTS {3}; - CREATE TABLE {3} + CREATE TABLE {3} AS SELECT a.{1}, a.{4}, a.{5}, a.{8}, COALESCE(b.{6}, 0) AS {6}, COALESCE(b.{7}, 0) AS {7} @@ -538,13 +532,13 @@ def initUpwindFacades(cursor, obstaclesTable, prefix=PREFIX_NAME): cursor.execute( safe(""" DROP TABLE IF EXISTS {0}; - CREATE TABLE {0}({5} BIGINT AUTO_INCREMENT, {1} INTEGER, {2} GEOMETRY, {3} DOUBLE, + CREATE TABLE {0}({5} BIGINT AUTO_INCREMENT, {1} INTEGER, {2} GEOMETRY, {3} DOUBLE, {6} INTEGER, {7} INTEGER, {8} INTEGER, {9} DOUBLE, {10} DOUBLE, {11} DOUBLE, {12} DOUBLE) AS SELECT CAST((row_number() over()) as Integer) AS {5}, {1}, {2} AS {2}, - ST_AZIMUTH(ST_STARTPOINT({2}), + ST_AZIMUTH(ST_STARTPOINT({2}), ST_ENDPOINT({2})) AS {3}, {6}, {7}, @@ -563,7 +557,7 @@ def initUpwindFacades(cursor, obstaclesTable, prefix=PREFIX_NAME): {11}, {12} FROM {4})') - WHERE ST_AZIMUTH(ST_STARTPOINT({2}), + WHERE ST_AZIMUTH(ST_STARTPOINT({2}), ST_ENDPOINT({2})) < PI() """).format( upwindTable, @@ -607,8 +601,7 @@ def updateUpwindFacadeBase(cursor, upwindTable, prefix=PREFIX_NAME): updated base height""" print("Update upwind facades base height") - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) tempoUpwind = DataUtil.postfix("tempo_upwind") # Output base name @@ -625,7 +618,7 @@ def updateUpwindFacadeBase(cursor, upwindTable, prefix=PREFIX_NAME): {12}; {13}; DROP TABLE IF EXISTS {4}; - CREATE TABLE {4} + CREATE TABLE {4} AS SELECT a.{1}, a.{5}, a.{8}, MIN(a.{3}) AS {3}, MIN(b.{6}) AS {6}, MIN(a.{7}) FROM {0} AS a LEFT JOIN {0} AS b ON a.{2} = b.{2} @@ -669,7 +662,7 @@ def updateUpwindFacadeBase(cursor, upwindTable, prefix=PREFIX_NAME): {9}; {10}; DROP TABLE IF EXISTS {3}; - CREATE TABLE {3} + CREATE TABLE {3} AS SELECT a.{1}, a.{4}, a.{5}, a.{7}, a.{8}, a.{11}, a.{12}, a.{13}, a.{14}, a.{15}, COALESCE(b.{6}, a.{6}) AS {6} @@ -740,8 +733,7 @@ def initDownwindFacades(cursor, obstaclesTable, prefix=PREFIX_NAME): # Name of the output table downwindTable = DataUtil.prefix(outputBaseName, prefix=prefix) - # Create temporary table names (for tables that will be removed at the end - # of the process) + # Create temporary table names (for tables that will be removed at the end of the process) tempoDownwindSegments = DataUtil.postfix("TEMPO_DOWNWIND_SEGMENTS") tempoDownwindLines = DataUtil.postfix("TEMPO_DOWNWIND_LINES") @@ -749,7 +741,7 @@ def initDownwindFacades(cursor, obstaclesTable, prefix=PREFIX_NAME): cursor.execute( safe(""" DROP TABLE IF EXISTS {0}; - CREATE TABLE {0}({4} BIGINT AUTO_INCREMENT, {1} INTEGER, {2} GEOMETRY, + CREATE TABLE {0}({4} BIGINT AUTO_INCREMENT, {1} INTEGER, {2} GEOMETRY, {5} INTEGER) AS SELECT CAST((row_number() over()) as Integer) AS {4}, {1}, @@ -759,7 +751,7 @@ def initDownwindFacades(cursor, obstaclesTable, prefix=PREFIX_NAME): {1}, {5} FROM {3})') - WHERE ST_AZIMUTH(ST_STARTPOINT({2}), + WHERE ST_AZIMUTH(ST_STARTPOINT({2}), ST_ENDPOINT({2})) > PI() """).format( tempoDownwindSegments, @@ -771,14 +763,13 @@ def initDownwindFacades(cursor, obstaclesTable, prefix=PREFIX_NAME): ) ) - # Merge the ones that intersect each other and join stacked block - # properties + # Merge the ones that intersect each other and join stacked block properties cursor.execute( safe(""" - DROP TABLE IF EXISTS {0}; - CREATE TABLE {0}({1} BIGINT AUTO_INCREMENT, {2} GEOMETRY, {4} INTEGER) - AS SELECT CAST((row_number() over()) as Integer) AS {1}, ST_NORMALIZE({2}) AS {2}, {4} - FROM ST_EXPLODE ('(SELECT ST_LINEMERGE(ST_UNION(ST_ACCUM({2}))) AS {2}, {4} + DROP TABLE IF EXISTS {0}; + CREATE TABLE {0}({1} BIGINT AUTO_INCREMENT, {2} GEOMETRY, {4} INTEGER) + AS SELECT CAST((row_number() over()) as Integer) AS {1}, ST_NORMALIZE({2}) AS {2}, {4} + FROM ST_EXPLODE ('(SELECT ST_LINEMERGE(ST_UNION(ST_ACCUM({2}))) AS {2}, {4} FROM {3} GROUP BY {4})'); {5};{6}; diff --git a/functions/URock/WindSolver.py b/functions/URock/WindSolver.py index f926983..26beb6f 100644 --- a/functions/URock/WindSolver.py +++ b/functions/URock/WindSolver.py @@ -105,8 +105,7 @@ def solver( v = v0.copy() w = w0.copy() - # Preallocating lambda and lambda + 1 and set values to 0 on sketch - # boundaries + # Preallocating lambda and lambda + 1 and set values to 0 on sketch boundaries lambdaN = np.ones([nx, ny, nz]) lambdaN1 = np.ones([nx, ny, nz]) lambdaN[0, :, :] = 0.0 @@ -347,8 +346,7 @@ def solver( # e[i, j, k] * lambdaN[i - 1, j, k] + f[i, j, k] * lambdaN1[i + 1, j, k] + A * ( # g[i, j, k] * lambdaN[i, j - 1, k] + h[i, j, k] * lambdaN1[i, j + 1, k]) + B * ( # m[i, j, k] * lambdaN[i, j, k - 1] + n[i, j, k] * lambdaN1[i, j, k + 1]))) / ( - # 2. * (o[i, j, k] + A * p[i, j, k] + B * q[i, j, k]))) + (1 - omega) * - # lambdaN1[i, j, k] + # 2. * (o[i, j, k] + A * p[i, j, k] + B * q[i, j, k]))) + (1 - omega) * lambdaN1[i, j, k] # else: # for i, j, k in cells4Solver: diff --git a/functions/URock/Zones.py b/functions/URock/Zones.py index 7ebb86f..cf27092 100644 --- a/functions/URock/Zones.py +++ b/functions/URock/Zones.py @@ -64,43 +64,46 @@ def displacementZones2(cursor, upwindWithPropTable, srid, prefix=PREFIX_NAME): ), } - # Create temporary table names (for tables that will be removed at the end - # of the process) + # Create temporary table names (for tables that will be removed at the end of the process) densifiedLinePoints = DataUtil.postfix("DENSIFIED_LINE_POINTS") ZonePoints = { - DISPLACEMENT_NAME: DISPLACEMENT_NAME - + DataUtil.postfix("_ZONE_POINTS"), - DISPLACEMENT_VORTEX_NAME: DISPLACEMENT_VORTEX_NAME - + DataUtil.postfix("_ZONE_POINTS"), + DISPLACEMENT_NAME: ( + DISPLACEMENT_NAME + DataUtil.postfix("_ZONE_POINTS") + ), + DISPLACEMENT_VORTEX_NAME: ( + DISPLACEMENT_VORTEX_NAME + DataUtil.postfix("_ZONE_POINTS") + ), } ZonePolygons = { - DISPLACEMENT_NAME: DISPLACEMENT_NAME - + DataUtil.postfix("_ZONE_POLYGONS"), - DISPLACEMENT_VORTEX_NAME: DISPLACEMENT_VORTEX_NAME - + DataUtil.postfix("_ZONE_POLYGONS"), + DISPLACEMENT_NAME: ( + DISPLACEMENT_NAME + DataUtil.postfix("_ZONE_POLYGONS") + ), + DISPLACEMENT_VORTEX_NAME: ( + DISPLACEMENT_VORTEX_NAME + DataUtil.postfix("_ZONE_POLYGONS") + ), } # First densify the upwind facades cursor.execute(f""" DROP TABLE IF EXISTS {densifiedLinePoints}; CREATE TABLE {densifiedLinePoints} - AS SELECT EXPLOD_ID, - {GEOM_FIELD}, - X_MED, - HALF_WIDTH, + AS SELECT EXPLOD_ID, + {GEOM_FIELD}, + X_MED, + HALF_WIDTH, {DISPLACEMENT_LENGTH_FIELD}, - {DISPLACEMENT_LENGTH_VORTEX_FIELD}, + {DISPLACEMENT_LENGTH_VORTEX_FIELD}, {UPWIND_FACADE_FIELD} - FROM ST_EXPLODE('(SELECT ST_ACCUM(ST_TOMULTIPOINT(ST_DENSIFY({GEOM_FIELD}, + FROM ST_EXPLODE('(SELECT ST_ACCUM(ST_TOMULTIPOINT(ST_DENSIFY({GEOM_FIELD}, ST_LENGTH({GEOM_FIELD})/{CAV_N_WAKE_FACADE_NPOINTS}))) AS {GEOM_FIELD}, - MAX({DISPLACEMENT_LENGTH_FIELD}) AS {DISPLACEMENT_LENGTH_FIELD}, + MAX({DISPLACEMENT_LENGTH_FIELD}) AS {DISPLACEMENT_LENGTH_FIELD}, MAX({DISPLACEMENT_LENGTH_VORTEX_FIELD}) AS {DISPLACEMENT_LENGTH_VORTEX_FIELD}, {UPWIND_FACADE_FIELD}, - MAX(X_MED) AS X_MED, + MAX(X_MED) AS X_MED, MAX(HALF_WIDTH) AS HALF_WIDTH FROM ST_EXPLODE(''(SELECT ST_TOMULTISEGMENTS({GEOM_FIELD}) AS {GEOM_FIELD}, - {DISPLACEMENT_LENGTH_FIELD}, - {DISPLACEMENT_LENGTH_VORTEX_FIELD}, + {DISPLACEMENT_LENGTH_FIELD}, + {DISPLACEMENT_LENGTH_VORTEX_FIELD}, {UPWIND_FACADE_FIELD}, {STACKED_BLOCK_X_MED} AS X_MED, {STACKED_BLOCK_WIDTH} / 2 AS HALF_WIDTH @@ -108,15 +111,13 @@ def displacementZones2(cursor, upwindWithPropTable, srid, prefix=PREFIX_NAME): GROUP BY {UPWIND_FACADE_FIELD})') """) # nosec B608 # nosec B608 - # Define the names of variables for displacement and displacement vortex - # zones + # Define the names of variables for displacement and displacement vortex zones variablesNames = pd.DataFrame( {"L": [DISPLACEMENT_LENGTH_FIELD, DISPLACEMENT_LENGTH_VORTEX_FIELD]}, index=[DISPLACEMENT_NAME, DISPLACEMENT_VORTEX_NAME], ) - # Create the half ellipse for displacement and displacement vortex zones - # from the densified upwind facade points + # Create the half ellipse for displacement and displacement vortex zones from the densified upwind facade points cursor.execute( safe(";").join( [ @@ -157,16 +158,16 @@ def displacementZones2(cursor, upwindWithPropTable, srid, prefix=PREFIX_NAME): UPWIND_FACADE_ANGLE_FIELD, ELLIPSOID_MIN_LENGTH, ), - DISPLACEMENT_VORTEX_NAME: " b.{0}>RADIANS(90-{1}) AND b.{0}RADIANS(90-{1}) AND b.{0} 0 AND {whereCond[z]}; - """ for z in variablesNames.index]) # nosec B608 # nosec B608 - ) # nosec B608 + """ for z in variablesNames.index])) # nosec B608 # nosec B608 if not DEBUG: # Drop intermediate tables @@ -322,7 +322,7 @@ def displacementZones( -0.5*PI()+ACOS((1-COS(2*PI()/{11}))*R_x /SQRT(POWER((1-COS(2*PI()/{11}))*R_x,2) +POWER(SIN(2*PI()/{11})*R_y,2))) - AND EXPLOD_ID = 2 + AND EXPLOD_ID = 2 OR {4}<0.5*PI() -0.5*PI()+ACOS((1-COS(2*PI()/{11}))*R_x /SQRT(POWER((1-COS(2*PI()/{11}))*R_x,2) @@ -413,8 +413,7 @@ def cavityAndWakeZones( WAKE_NAME: DataUtil.prefix("WAKE_ZONES", prefix=prefix), } - # Create temporary table names (for tables that will be removed at the end - # of the process) + # Create temporary table names (for tables that will be removed at the end of the process) densifiedLinePoints = DataUtil.postfix("DENSIFIED_LINE_POINTS") ZonePoints = { CAVITY_NAME: CAVITY_NAME + DataUtil.postfix("_ZONE_POINTS"), @@ -429,23 +428,23 @@ def cavityAndWakeZones( cursor.execute(f""" DROP TABLE IF EXISTS {densifiedLinePoints}; CREATE TABLE {densifiedLinePoints} - AS SELECT EXPLOD_ID, - {GEOM_FIELD}, - X_MED, - HALF_WIDTH, + AS SELECT EXPLOD_ID, + {GEOM_FIELD}, + X_MED, + HALF_WIDTH, {CAVITY_LENGTH_FIELD}, - {WAKE_LENGTH_FIELD}, + {WAKE_LENGTH_FIELD}, {DOWNWIND_FACADE_FIELD} - FROM ST_EXPLODE('(SELECT ST_ACCUM(ST_TOMULTIPOINT(ST_DENSIFY({GEOM_FIELD}, + FROM ST_EXPLODE('(SELECT ST_ACCUM(ST_TOMULTIPOINT(ST_DENSIFY({GEOM_FIELD}, ST_LENGTH({GEOM_FIELD})/{CAV_N_WAKE_FACADE_NPOINTS}))) AS {GEOM_FIELD}, - MAX({CAVITY_LENGTH_FIELD}) AS {CAVITY_LENGTH_FIELD}, + MAX({CAVITY_LENGTH_FIELD}) AS {CAVITY_LENGTH_FIELD}, MAX({WAKE_LENGTH_FIELD}) AS {WAKE_LENGTH_FIELD}, {DOWNWIND_FACADE_FIELD}, - MAX(X_MED) AS X_MED, + MAX(X_MED) AS X_MED, MAX(HALF_WIDTH) AS HALF_WIDTH FROM ST_EXPLODE(''(SELECT ST_TOMULTISEGMENTS({GEOM_FIELD}) AS {GEOM_FIELD}, - {CAVITY_LENGTH_FIELD}, - {WAKE_LENGTH_FIELD}, + {CAVITY_LENGTH_FIELD}, + {WAKE_LENGTH_FIELD}, {DOWNWIND_FACADE_FIELD}, {STACKED_BLOCK_X_MED} AS X_MED, {STACKED_BLOCK_WIDTH} / 2 AS HALF_WIDTH @@ -459,8 +458,7 @@ def cavityAndWakeZones( index=[CAVITY_NAME, WAKE_NAME], ) - # Create the half ellipse for cavity and wake zones from the densified - # downwind facade points + # Create the half ellipse for cavity and wake zones from the densified downwind facade points cursor.execute( safe(";").join( [ @@ -494,10 +492,9 @@ def cavityAndWakeZones( ) ) - # Create the zone from the half ellipse and the densified line and then - # join missing columns + # Create the zone from the half ellipse and the densified line and then join missing columns cursor.execute(safe(";").join([f""" - {DataUtil.createIndex(tableName=ZonePoints[z], + {DataUtil.createIndex(tableName=ZonePoints[z], fieldName=DOWNWIND_FACADE_FIELD, isSpatial=False)} DROP TABLE IF EXISTS {ZonePolygons[z]}, {outputZoneTableNames[z]}; @@ -506,22 +503,22 @@ def cavityAndWakeZones( {DOWNWIND_FACADE_FIELD} FROM {ZonePoints[z]} GROUP BY {DOWNWIND_FACADE_FIELD}; - {DataUtil.createIndex(tableName=ZonePolygons[z], - fieldName=DOWNWIND_FACADE_FIELD, - isSpatial=False)} - {DataUtil.createIndex(tableName=downwindWithPropTable, - fieldName=DOWNWIND_FACADE_FIELD, - isSpatial=False)} + {DataUtil.createIndex(tableName=ZonePolygons[z], + fieldName=DOWNWIND_FACADE_FIELD, + isSpatial=False)} + {DataUtil.createIndex(tableName=downwindWithPropTable, + fieldName=DOWNWIND_FACADE_FIELD, + isSpatial=False)} CREATE TABLE {outputZoneTableNames[z]} - AS SELECT a.{DOWNWIND_FACADE_FIELD}, + AS SELECT a.{DOWNWIND_FACADE_FIELD}, a.{GEOM_FIELD}, b.{ID_FIELD_STACKED_BLOCK}, b.{HEIGHT_FIELD}, b.{STACKED_BLOCK_X_MED}, b.{STACKED_BLOCK_UPSTREAMEST_X}, - b.{SIN_BLOCK_LEFT_AZIMUTH}, + b.{SIN_BLOCK_LEFT_AZIMUTH}, b.{COS_BLOCK_LEFT_AZIMUTH}, b.{COS_BLOCK_RIGHT_AZIMUTH}, - b.{SIN_BLOCK_RIGHT_AZIMUTH}, + b.{SIN_BLOCK_RIGHT_AZIMUTH}, b.{STACKED_BLOCK_WIDTH}, b.{ID_FIELD_BLOCK} FROM {ZonePolygons[z]} AS a LEFT JOIN {downwindWithPropTable} AS b @@ -599,26 +596,24 @@ def streetCanyonZones( # Name of the output tables streetCanyonZoneTable = DataUtil.prefix(outputBaseName, prefix=prefix) - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) intersectTable = DataUtil.postfix("intersect_table") canyonExtendTable = DataUtil.postfix("canyon_extend_table") - # Identify pieces of upwind facades intersected by cavity zones (only when - # street canyon angle < 45°) + # Identify pieces of upwind facades intersected by cavity zones (only when street canyon angle < 45°) cursor.execute(f""" - {DataUtil.createIndex(tableName=upwindTable, + {DataUtil.createIndex(tableName=upwindTable, fieldName=GEOM_FIELD, isSpatial=True)}; - {DataUtil.createIndex(tableName=cavityZonesTable, - fieldName=GEOM_FIELD, - isSpatial=True)}; - {DataUtil.createIndex(tableName=upwindTable, - fieldName=ID_FIELD_BLOCK, - isSpatial=False)}; - {DataUtil.createIndex(tableName=cavityZonesTable, - fieldName=ID_FIELD_BLOCK, - isSpatial=False)}; + {DataUtil.createIndex(tableName=cavityZonesTable, + fieldName=GEOM_FIELD, + isSpatial=True)}; + {DataUtil.createIndex(tableName=upwindTable, + fieldName=ID_FIELD_BLOCK, + isSpatial=False)}; + {DataUtil.createIndex(tableName=cavityZonesTable, + fieldName=ID_FIELD_BLOCK, + isSpatial=False)}; DROP TABLE IF EXISTS {intersectTable}; CREATE TABLE {intersectTable} AS SELECT b.{ID_FIELD_STACKED_BLOCK} AS {ID_UPSTREAM_STACKED_BLOCK}, @@ -626,7 +621,7 @@ def streetCanyonZones( a.{BASE_HEIGHT_FIELD}, a.{HEIGHT_FIELD}, a.{UPWIND_FACADE_ANGLE_FIELD}, - ST_COLLECTIONEXTRACT(ST_INTERSECTION(a.{GEOM_FIELD}, + ST_COLLECTIONEXTRACT(ST_INTERSECTION(a.{GEOM_FIELD}, b.{GEOM_FIELD}), 2) AS {GEOM_FIELD}, a.{UPWIND_FACADE_FIELD}, @@ -634,7 +629,7 @@ def streetCanyonZones( FROM {upwindTable} AS a, {cavityZonesTable} AS b WHERE a.{GEOM_FIELD} && b.{GEOM_FIELD} AND ST_INTERSECTS(a.{GEOM_FIELD}, b.{GEOM_FIELD}) - AND a.{UPWIND_FACADE_ANGLE_FIELD} >= RADIANS({STREET_CANYON_ANGLE_THRESH}) + AND a.{UPWIND_FACADE_ANGLE_FIELD} >= RADIANS({STREET_CANYON_ANGLE_THRESH}) AND a.{UPWIND_FACADE_ANGLE_FIELD} <= RADIANS(180-{STREET_CANYON_ANGLE_THRESH}) AND a.{ID_FIELD_BLOCK} != b.{ID_FIELD_BLOCK} """) # nosec B608 @@ -652,11 +647,11 @@ def streetCanyonZones( a.{11}, a.{12}, ST_SETSRID(ST_MAKEPOLYGON(ST_MAKELINE(ST_STARTPOINT(a.{4}), - ST_STARTPOINT(ST_TRANSLATE( a.{4}, - 0, + ST_STARTPOINT(ST_TRANSLATE( a.{4}, + 0, ST_YMAX(b.{4})-ST_YMIN(b.{4})+b.{5})), ST_ENDPOINT(ST_TRANSLATE( a.{4}, - 0, + 0, ST_YMAX(b.{4})-ST_YMIN(b.{4})+b.{5})), ST_TOMULTIPOINT(ST_REVERSE(a.{4})))), {16}) AS THE_GEOM, @@ -820,8 +815,7 @@ def rooftopZones(cursor, upwindTable, zonePropertiesTable, prefix=PREFIX_NAME): outputBaseNameroofCorner, prefix=prefix ) - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) temporaryRooftopPerp = DataUtil.postfix("temporary_rooftop_perp") temporaryRooftopCorner = DataUtil.postfix("temporary_rooftop_corner") @@ -889,7 +883,7 @@ def rooftopZones(cursor, upwindTable, zonePropertiesTable, prefix=PREFIX_NAME): -b.{11}), ST_ENDPOINT(a.{3}), ST_STARTPOINT(a.{3}))) AS {3} - FROM {7} AS a LEFT JOIN {8} AS b ON a.{1} = b.{1} + FROM {7} AS a LEFT JOIN {8} AS b ON a.{1} = b.{1} WHERE a.{5} > RADIANS(90-{13}) AND a.{5} < RADIANS(90+{13}) """).format( temporaryRooftopCorner, @@ -926,7 +920,7 @@ def rooftopZones(cursor, upwindTable, zonePropertiesTable, prefix=PREFIX_NAME): "perp": "b.{0}, b.{1},".format( ROOFTOP_PERP_LENGTH, ROOFTOP_PERP_HEIGHT ), - "corner": """a.{0}, a.{1}, a.{2}, b.{3}, + "corner": """a.{0}, a.{1}, a.{2}, b.{3}, a.GEOM_CORNER_POINT,""".format( ROOFTOP_CORNER_LENGTH, ROOFTOP_CORNER_FACADE_LENGTH, @@ -1042,8 +1036,7 @@ def vegetationZones( outputBaseNameBuilt, prefix=prefix ) - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) temporary_built_vegetation = DataUtil.postfix("temporary_built_vegetation") # Identify vegetation zones being in building wake zones @@ -1053,8 +1046,8 @@ def vegetationZones( {11}; DROP TABLE IF EXISTS {7}; CREATE TABLE {7} - AS SELECT ST_COLLECTIONEXTRACT(ST_INTERSECTION(a.{1}, - ST_MAKEVALID(ST_SNAP(b.{1}, + AS SELECT ST_COLLECTIONEXTRACT(ST_INTERSECTION(a.{1}, + ST_MAKEVALID(ST_SNAP(b.{1}, a.{1}, {13}))), 3) AS {1}, @@ -1123,8 +1116,8 @@ def vegetationZones( a.{1})) AND NOT ST_ISEMPTY(b.{1}) GROUP BY b.{6}, a.{1})') WHERE NOT ST_ISEMPTY({1}) - UNION ALL - SELECT CAST((row_number() over()) as Integer) AS {8}, + UNION ALL + SELECT CAST((row_number() over()) as Integer) AS {8}, a.{1}, a.{3}, a.{4}, @@ -1241,8 +1234,7 @@ def identifyImpactingStackedBlocks( ) outputVegetation = DataUtil.prefix("IMPACTING_VEGETATION", prefix=prefix) - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) tabAllBuildZones = DataUtil.postfix("tab_all_build_zones") tabTempStack = DataUtil.postfix("tab_temp_stack") tabTempBlock = DataUtil.postfix("tab_temp_blocks") @@ -1320,14 +1312,13 @@ def identifyImpactingStackedBlocks( ) ) - # Identify which blocks are in the cross-wind extend of blocks and - # impacted zone + # Identify which blocks are in the cross-wind extend of blocks and impacted zone cursor.execute( safe(""" DROP TABLE IF EXISTS {0}; CREATE TABLE {0} AS SELECT ST_EXPAND(ST_EXTENT(a.{1}), {2}, 0) AS {1} - FROM (SELECT {1} FROM {4} + FROM (SELECT {1} FROM {4} UNION ALL SELECT {1} FROM {5}) AS a; {6};{7}; @@ -1338,7 +1329,7 @@ def identifyImpactingStackedBlocks( FROM {10} AS a, {0} AS b WHERE a.{1} && b.{1} AND ST_INTERSECTS(a.{1}, b.{1}) UNION ALL - SELECT {9} + SELECT {9} FROM {11}) """).format( tabCrossExtBox, @@ -1429,8 +1420,7 @@ def identifyImpactingStackedBlocks( # ------------------------------------------------------------------------ # 2. MAKE THE CALCULATION FOR THE VEGETATION ----------------------------- # ------------------------------------------------------------------------ - # Identify which vegetation patches are in the cross-wind extend of blocks - # and impacted zone + # Identify which vegetation patches are in the cross-wind extend of blocks and impacted zone cursor.execute( safe(""" {0};{1}; diff --git a/functions/URock/__init__dep.py b/functions/URock/__init__dep.py index f265c61..46b5e23 100644 --- a/functions/URock/__init__dep.py +++ b/functions/URock/__init__dep.py @@ -26,9 +26,8 @@ __date__ = "2021-10-04" __copyright__ = "(C) 2021 by Jérémy Bernard / University of Gothenburg" -# noinspection PyPep8Naming - +# noinspection PyPep8Naming def classFactory(iface): # pylint: disable=invalid-name """Load URock class from file URock. diff --git a/functions/URock/loadData.py b/functions/URock/loadData.py index 4927815..b4fa3ac 100644 --- a/functions/URock/loadData.py +++ b/functions/URock/loadData.py @@ -63,8 +63,7 @@ def loadData( None""" print("Load input data") - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) buildTablePreSrid = DataUtil.postfix("build_pre_srid") vegTablePreSrid = DataUtil.postfix("veg_pre_srid") @@ -165,7 +164,7 @@ def loadData( # Create an ID FIELD if None. if idFieldBuild is None or idFieldBuild == "": - cursor.execute(safe(""" + cursor.execute(safe(""" ALTER TABLE {0} DROP COLUMN IF EXISTS {1}; ALTER TABLE {0} ADD COLUMN {1} BIGINT AUTO_INCREMENT; """).format(buildTablePreSrid, ID_FIELD_BUILD)) @@ -210,7 +209,7 @@ def loadData( # Create an ID FIELD if None. if idVegetation is None or idVegetation == "": - cursor.execute(safe(""" + cursor.execute(safe(""" ALTER TABLE {0} DROP COLUMN IF EXISTS {1}; ALTER TABLE {0} ADD COLUMN {1} BIGINT AUTO_INCREMENT; """).format(vegTablePreSrid, ID_VEGETATION)) @@ -222,7 +221,7 @@ def loadData( or vegetationAttenuationFactor == "" ): cursor.execute( - safe(""" + safe(""" ALTER TABLE {0} DROP COLUMN IF EXISTS {1}; ALTER TABLE {0} ADD COLUMN {1} DOUBLE DEFAULT {2}; """).format( @@ -236,7 +235,7 @@ def loadData( # of the maximum height if no attribute for base height if vegetationBaseHeight is None or vegetationBaseHeight == "": cursor.execute( - safe(""" + safe(""" ALTER TABLE {0} DROP COLUMN IF EXISTS {1}; ALTER TABLE {0} ADD COLUMN {1} DOUBLE; UPDATE {0} SET {1} = {2} * {3}; @@ -307,8 +306,7 @@ def loadData( cursor.execute(importQuery) # 3. SET VEGETATION AND BUILDING TABLE SRID AND REMOVE SMALL OBSTACLES - # If H2GIS does not identify any SRID for the tables, set the ones - # identied by GDAL + # If H2GIS does not identify any SRID for the tables, set the ones identied by GDAL if h2gisBuildSrid == 0 and h2gisVegSrid == 0: buildSrid = srid vegSrid = srid @@ -386,15 +384,14 @@ def loadFile(cursor, filePath, tableName, srid=None, srid_repro=None): None""" print("Load table '{0}'".format(tableName)) - # Get the input building file extension and the appropriate h2gis read - # function name + # Get the input building file extension and the appropriate h2gis read function name fileExtension = filePath.split(".")[-1] readFunction = DataUtil.readFunction(fileExtension) if readFunction == "CSVREAD": cursor.execute(safe(""" DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} + CREATE TABLE {0} AS SELECT * FROM {2}('{1}'); """).format(tableName, filePath, readFunction)) else: # Import and then copy into a new table to remove all constraints (primary keys...) @@ -475,8 +472,7 @@ def fromShp3dTo2_5( None""" print("From 3D to 2.5D geometries") - # Create temporary table names (for tables that will be removed at the end - # of the IProcess) + # Create temporary table names (for tables that will be removed at the end of the IProcess) trianglesWithId = DataUtil.postfix("triangles_with_id") trees2d = DataUtil.postfix("trees_2d") buildings2d = DataUtil.postfix("buildings_2d") @@ -485,9 +481,9 @@ def fromShp3dTo2_5( # Add ID to the input data and remove vertical polygons... cursor.execute(safe(""" - DROP TABLE IF EXISTS {0}; - CREATE TABLE {0}(ID BIGINT AUTO_INCREMENT, {1} GEOMETRY) - AS (SELECT CAST((row_number() over()) as Integer) AS ID, {1} + DROP TABLE IF EXISTS {0}; + CREATE TABLE {0}(ID BIGINT AUTO_INCREMENT, {1} GEOMETRY) + AS (SELECT CAST((row_number() over()) as Integer) AS ID, {1} FROM ST_EXPLODE('(SELECT * FROM {2} WHERE ST_AREA({1})>0)')) """).format(trianglesWithId, GEOM_FIELD, triangles3d)) @@ -498,8 +494,8 @@ def fromShp3dTo2_5( {6}; {7}; DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} - AS SELECT a.ID, ST_FORCE2D(a.{1}) AS {1}, + CREATE TABLE {0} + AS SELECT a.ID, ST_FORCE2D(a.{1}) AS {1}, CAST(ST_ZMAX(a.{1}) AS INT) AS {2}, CAST(ST_ZMIN(a.{1}) AS INT) AS {3} FROM {4} AS a, {5} AS b @@ -528,7 +524,7 @@ def fromShp3dTo2_5( {5}; {6}; DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} + CREATE TABLE {0} AS SELECT a.ID, ST_FORCE2D(a.{1}) AS {1}, CAST(ST_ZMAX(a.{1}) AS INT) AS {2} FROM {3} AS a LEFT JOIN {4} AS b @@ -557,14 +553,14 @@ def fromShp3dTo2_5( {10}; {11}; DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} + CREATE TABLE {0} AS SELECT b.ID AS ID FROM {3} AS a, {3} AS b - WHERE a.{1} && b.{1} AND + WHERE a.{1} && b.{1} AND (ST_COVERS(a.{1}, b.{1}) AND a.{2} > b.{2} OR ST_EQUALS(a.{1}, b.{1}) AND a.{2} = b.{2} AND a.ID < b.ID) GROUP BY b.ID; - CREATE INDEX IF NOT EXISTS id_ID_{0} ON {0}(ID); + CREATE INDEX IF NOT EXISTS id_ID_{0} ON {0}(ID); DROP TABLE IF EXISTS {4}; CREATE TABLE {4} AS SELECT a.ID AS {5}, a.{1}, a.{2}, 0 AS {6}, {7} AS {8} @@ -600,8 +596,8 @@ def fromShp3dTo2_5( cursor.execute( safe(""" DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} - AS SELECT ID, ST_FORCE2D({1}) AS {1}, + CREATE TABLE {0} + AS SELECT ID, ST_FORCE2D({1}) AS {1}, CAST(ST_ZMAX({1}) AS INT) AS {2} FROM {3} """).format( @@ -617,17 +613,17 @@ def fromShp3dTo2_5( {7}; {8}; DROP TABLE IF EXISTS {0}; - CREATE TABLE {0} + CREATE TABLE {0} AS SELECT b.ID AS ID FROM {3} AS a, {3} AS b - WHERE a.{1} && b.{1} AND + WHERE a.{1} && b.{1} AND (ST_COVERS(a.{1}, b.{1}) AND a.{2} > b.{2} OR ST_EQUALS(a.{1}, b.{1}) AND a.{2} = b.{2} AND a.ID < b.ID) GROUP BY b.ID; - {9}; + {9}; DROP TABLE IF EXISTS {4}; CREATE TABLE {4} - AS SELECT a.ID AS {5}, a.{1}, a.{2} + AS SELECT a.ID AS {5}, a.{1}, a.{2} FROM {3} AS a LEFT JOIN {0} AS b ON a.ID = b.ID WHERE b.ID IS NULL diff --git a/functions/URock/saveData.py b/functions/URock/saveData.py index 6a145dd..57a548f 100644 --- a/functions/URock/saveData.py +++ b/functions/URock/saveData.py @@ -23,6 +23,7 @@ GridOptions, FillNodata, Open, + GA_Update, GetDriverByName, ) from .GlobalVariables import ( @@ -34,6 +35,8 @@ TEMPO_HORIZ_WIND_FILE, VERT_WIND_SPEED, GEOM_FIELD, + OUTPUT_DIRECTORY, + MESH_SIZE, OUTPUT_FILENAME, DELETE_OUTPUT_IF_EXISTS, OUTPUT_RASTER_EXTENSION, @@ -45,6 +48,7 @@ RLAT, LON, LAT, + LEVELS, WINDSPEED_X, WINDSPEED_Y, WINDSPEED_Z, @@ -94,8 +98,8 @@ def saveBasicOutputs( if saveNetcdf: # Get the coordinate in lat/lon of each point # WARNING : for now keep the data in local coordinates) - cursor.execute(safe(""" - SELECT ST_X({0}) AS LON, ST_Y({0}) AS LAT FROM + cursor.execute(safe(""" + SELECT ST_X({0}) AS LON, ST_Y({0}) AS LAT FROM (SELECT ST_TRANSFORM(ST_SETSRID({0},{2}), 4326) AS {0} FROM {1}) """).format(GEOM_FIELD, gridName, srid)) coord = np.array(cursor.fetchall()) @@ -162,11 +166,11 @@ def saveBasicOutputs( { HORIZ_WIND_SPEED: ((ufin**2 + vfin**2) ** 0.5).flatten("F"), WIND_SPEED: ( - (ufin**2 + vfin**2 + wfin**2) ** 0.5 - ).flatten("F"), - HORIZ_WIND_DIRECTION: radToDeg( - windDirectionFromXY(ufin, vfin) - ).flatten("F"), + ((ufin**2 + vfin**2 + wfin**2) ** 0.5).flatten("F") + ), + HORIZ_WIND_DIRECTION: ( + radToDeg(windDirectionFromXY(ufin, vfin)).flatten("F") + ), VERT_WIND_SPEED: wfin.flatten("F"), } ).rename_axis(ID_POINT) @@ -183,7 +187,7 @@ def saveBasicOutputs( {0}{1} DROP TABLE IF EXISTS {2}; CREATE TABLE {2} - AS SELECT a.{3}, {4}, b.{5}, + AS SELECT a.{3}, {4}, b.{5}, b.{6}, b.{7}, b.{11} FROM {8} AS a LEFT JOIN {9} AS b @@ -229,11 +233,10 @@ def saveBasicOutputs( ) # ------------------------------------------------------------------- - # SAVE RASTER ----------------------------------------------------- + # SAVE RASTER ------------------------------------------------------- # ------------------------------------------------------------------- if saveRaster: - # Save the all direction, horizontal and vertical wind speeds - # into a a different raster + # Save the all direction, horizontal and vertical wind speeds into a a different raster for var in [WIND_SPEED, HORIZ_WIND_SPEED, VERT_WIND_SPEED]: saveRasterFile( cursor=cursor, @@ -517,8 +520,7 @@ def saveRasterFile( extension=OUTPUT_RASTER_EXTENSION, ) - # Whether or not a raster output is given as input, the rasterization - # process is slightly different + # Whether or not a raster output is given as input, the rasterization process is slightly different if outputRaster: outputRasterExtent = outputRaster.extent() resX = ( @@ -534,8 +536,7 @@ def saveRasterFile( tmp_file = os.path.join( tmp_dir, f"interp_before_fillna_{var2save}.tif" ) - # If a single output raster cell contains more than 4 points, average - # instead of interpolate + # If a single output raster cell contains more than 4 points, average instead of interpolate if resX * resY > 4 * meshSize**2: Grid( destName=tmp_file, @@ -695,8 +696,7 @@ def interp_vec_to_rast( # grid_values = grid_values[::-1, :] # # Define raster metadata - # transform = from_origin(xmin, ymax, resolution, resolution) # origin is - # top-left + # transform = from_origin(xmin, ymax, resolution, resolution) # origin is top-left # # Save the interpolated grid to a raster file # interp_out = os.path.join(TEMPO_DIRECTORY,"interp_out.tif") @@ -705,8 +705,7 @@ def interp_vec_to_rast( # count=1, dtype=grid_values.dtype, crs=gdf.crs, transform=transform) as dst: # dst.write(grid_values, 1) - # Change the order of the points to make the TIN interpolation faster and - # working for all conditions + # Change the order of the points to make the TIN interpolation faster and working for all conditions order_changed = processing.run( "native:orderbyexpression", { @@ -743,8 +742,7 @@ def interp_vec_to_rast( # If there are buildings in the study area, need to set wind speed = 0 if os.stat(stacked_blocks).st_size > 0: - # Rasterize the stacked blocks keeping the value of each stacked block - # base + # Rasterize the stacked blocks keeping the value of each stacked block base block_base = processing.run( "gdal:rasterize", { @@ -766,8 +764,7 @@ def interp_vec_to_rast( }, )["OUTPUT"] - # Rasterize the stacked blocks keeping the value of each stacked block - # top + # Rasterize the stacked blocks keeping the value of each stacked block top block_top = processing.run( "gdal:rasterize", { @@ -805,15 +802,18 @@ def interp_vec_to_rast( "BAND_E": None, "INPUT_F": None, "BAND_F": None, - "FORMULA": f"((A == -9999) + (A < {z_i})) * ((B == -9999) + (B < {z_i})) * C", + "FORMULA": ( + f"((A == -9999) + (A < {z_i})) * ((B == -9999) + (B < {z_i})) * C" + ), "NO_DATA": None, "EXTENT_OPT": 0, "PROJWIN": None, "RTYPE": 5, "OPTIONS": "", "EXTRA": "", - "OUTPUT": outputFilePathAndNameBaseRaster - + OUTPUT_RASTER_EXTENSION, + "OUTPUT": ( + outputFilePathAndNameBaseRaster + OUTPUT_RASTER_EXTENSION + ), }, )["OUTPUT"] # Else directly save the result of the interpolation diff --git a/functions/URock/urock_analyser_functions.py b/functions/URock/urock_analyser_functions.py index 30425bd..c863691 100644 --- a/functions/URock/urock_analyser_functions.py +++ b/functions/URock/urock_analyser_functions.py @@ -8,7 +8,7 @@ try: import xarray as xr -except BaseException: +except: pass import os import pandas as pd @@ -88,8 +88,7 @@ def plotSectionalViews( else: srid_all = srid_lines - # Create temporary table names (for tables that will be removed at the end - # of the process) + # Create temporary table names (for tables that will be removed at the end of the process) allPointsTab = DataUtil.postfix("ALL_POINTS") linesTab = DataUtil.postfix("LINES") pointsIntersecTab = DataUtil.postfix("POINTS_INTERSECT") @@ -283,8 +282,8 @@ def plotSectionalViews( ) # Get the resolution of the wind speed data cursor.execute(safe(""" - SELECT ST_DISTANCE(a.{0}, b.{0}) AS dist - FROM {1} AS a, {1} AS b + SELECT ST_DISTANCE(a.{0}, b.{0}) AS dist + FROM {1} AS a, {1} AS b WHERE a.{2} = 0 AND a.{3} = 0 AND b.{2} = 1 AND b.{3} = 0 """).format(GEOM_FIELD, allPointsTab, RLON, RLAT)) horiz_res = round(cursor.fetchall()[0][0]) @@ -376,42 +375,46 @@ def plotSectionalViews( ].replace(0, 0 - float(horiz_res) / 2) dic_all = { id_line: { - zval: df_selectedPoints[ - (df_selectedPoints[idLines.upper()] == id_line) - & (df_selectedPoints[Z.upper()] == zval) - ] - .groupby("DIST") - .mean() + zval: ( + df_selectedPoints[ + (df_selectedPoints[idLines.upper()] == id_line) + & (df_selectedPoints[Z.upper()] == zval) + ] + .groupby("DIST") + .mean() + ) for zval in uniques_z } for id_line in pd.unique(df_selectedPoints[idLines.upper()]) } dic_all = { id_line: { - zval: conditional_interpolate( - dic_all[id_line][zval] - .reindex( + zval: ( + conditional_interpolate( + dic_all[id_line][zval] + .reindex( + pd.Index( + np.arange( + 0, + dic_all[id_line][zval].index.max(), + horiz_res, + ) + ).union(dic_all[id_line][zval].index) + ) + .sort_index(), + cols=[ + WINDSPEED_X.upper(), + WINDSPEED_Y.upper(), + WINDSPEED_Z.upper(), + ], + limit=2, + ).reindex( pd.Index( np.arange( 0, dic_all[id_line][zval].index.max(), horiz_res, ) - ).union(dic_all[id_line][zval].index) - ) - .sort_index(), - cols=[ - WINDSPEED_X.upper(), - WINDSPEED_Y.upper(), - WINDSPEED_Z.upper(), - ], - limit=2, - ).reindex( - pd.Index( - np.arange( - 0, - dic_all[id_line][zval].index.max(), - horiz_res, ) ) ) diff --git a/functions/URock/urock_processing_algorithm_dep.py b/functions/URock/urock_processing_algorithm_dep.py index 2c2709e..63dcfd6 100644 --- a/functions/URock/urock_processing_algorithm_dep.py +++ b/functions/URock/urock_processing_algorithm_dep.py @@ -38,6 +38,7 @@ QgsProcessingParameterField, QgsProcessingParameterFeatureSource, QgsProcessingParameterNumber, + QgsProcessingParameterMatrix, QgsProcessingParameterFolderDestination, QgsProcessingParameterString, QgsProcessingParameterRasterLayer, @@ -51,6 +52,7 @@ QgsProcessingException, ) from qgis.PyQt.QtWidgets import QMessageBox +from qgis.utils import iface from pathlib import Path import subprocess import pandas as pd @@ -63,12 +65,14 @@ subprocess.check_call( [str(path_pybin), "-m", "pip", "install", "jaydebeapi"] ) + import jaydebeapi except Exception: QMessageBox.critical( None, "Error", "'jaydebeapi' Python package is missing, cannot connect to H2 Driver", ) + pass from . import MainCalculation from .GlobalVariables import * @@ -517,8 +521,7 @@ def processAlgorithm(self, parameters, context, feedback): outputRaster.extent().yMaximum() - outputRaster.extent().yMinimum() ) / outputRaster.height() - # If there is a raster and no meshSize, take the mean of x and y - # raster resolution + # If there is a raster and no meshSize, take the mean of x and y raster resolution if not meshSize: meshSize = float(xres + yres) / 2 elif not meshSize: diff --git a/functions/dailyshading.py b/functions/dailyshading.py index db9ebc2..cd2f050 100644 --- a/functions/dailyshading.py +++ b/functions/dailyshading.py @@ -190,7 +190,12 @@ def dailyshading( else: if usevegdem == 0: sh = shadow.shadowingfunctionglobalradiation( - dsm, azi[i], alt[i], scale, feedback, 0 + dsm, + azi[i], + alt[i], + scale, + feedback, + 0, ) # shtot = shtot + sh else: @@ -236,7 +241,6 @@ def dailyshading( saveraster(gdal_data, filenamewallshve, wallshve) shadowresult = {"shfinal": shfinal, "time_vector": time_vector} - return shadowresult diff --git a/functions/svf_for_voxels.py b/functions/svf_for_voxels.py index ef7c2c4..a76e34f 100644 --- a/functions/svf_for_voxels.py +++ b/functions/svf_for_voxels.py @@ -251,6 +251,13 @@ def svf_kmeans( feedback, ): + try: + from sklearn.cluster import KMeans + except: + raise ImportError( + "[UMEP-processing Error] pleas install sklearn via pip install scikit-learn or via osgeo4w" + ) + # Calculate where there are buildings and not. Used to elevate dem. ground = dsm - dem # Ground == 1 = ground @@ -451,9 +458,9 @@ def svf_kmeans( # get the calculated svf for the wall pixel to check if it is # higher or lower than 0.5 - temp_svf = svftotal[ - int(voxelTable[temp_y, 5][0]), int(voxelTable[temp_y, 6][0]) + int(voxelTable[temp_y, 5][0]), + int(voxelTable[temp_y, 6][0]), ] if ( temp_svf < 0.5 @@ -510,7 +517,7 @@ def svf_kmeans( return voxelTable, cluster_heights -def interpolate_svf(voxelTable, cluster_heights, kmeans): +def interpolate_svf(voxelTable): unique_wall_pixels = np.unique(voxelTable[:, 4]) unique_wall_pixels = unique_wall_pixels[unique_wall_pixels != 0] diff --git a/functions/svf_for_voxels_torch.py b/functions/svf_for_voxels_torch.py new file mode 100644 index 0000000..236d690 --- /dev/null +++ b/functions/svf_for_voxels_torch.py @@ -0,0 +1,602 @@ +try: + from sklearn.cluster import KMeans +except: + pass +import numpy as np +import torch + + +def _to_tensor(x, device, dtype=torch.float32): + if isinstance(x, torch.Tensor): + return x.to(device) + if x is None: + return None + return torch.tensor(x, dtype=dtype, device=device) + + +from . import svf_functions_torch as svf +from . import wallalgorithms_torch as wa + + +def wallscheme_prepare( + dsm, scale, pixel_resolution, feedback, device=torch.device("cpu") +): + dsm = _to_tensor(dsm, device) + + # Existing UMEP wall and aspect calculations + walls = wa.findwalls_sp( + dsm, + 2, + device, + torch.tensor([[1, 1, 1], [1, 0, 1], [1, 1, 1]], device=dsm.device), + ) + walls_copy = torch.clone(walls) + aspect = wa.filter1Goodwin_as_aspect_v3( + walls_copy, scale, dsm, feedback, 100, device + ) + + walls_exact = walls.clone() + walls_round = torch.ceil(walls).int() + + # 1. Vectorized identification of wall pixels + wall_rows, wall_cols = torch.where(walls_round > 0) + num_walls = wall_rows.shape[0] + + # 2. Assign Wall IDs (1-indexed) vectorially + uniqueWallIDs = torch.zeros((dsm.shape[0], dsm.shape[1]), device=device) + wall_indices = torch.arange(1, num_walls + 1, device=device) + uniqueWallIDs[wall_rows, wall_cols] = wall_indices.float() + + # 3. Calculate number of voxels for every wall pixel at once + # Extract wall attributes only for the valid wall locations + walls_round_extracted = walls_round[wall_rows, wall_cols] + walls_exact_extracted = walls_exact[wall_rows, wall_cols] + + number_of_voxels = (walls_round_extracted / pixel_resolution).int() + + # 4. Repeat wall properties based on their respective voxel counts + # This replaces both the 'i' and 'j' loops entirely + wall2d_id_tensor = torch.repeat_interleave(wall_indices, number_of_voxels) + wall_height_tensor = torch.repeat_interleave( + walls_round_extracted, number_of_voxels + ) + wall_height_exact_tensor = torch.repeat_interleave( + walls_exact_extracted, number_of_voxels + ) + y_position_tensor = torch.repeat_interleave(wall_rows, number_of_voxels) + x_position_tensor = torch.repeat_interleave(wall_cols, number_of_voxels) + + # 5. Generate voxel heights using a cumulative sequence sequence + # This handles the equivalent of `j * pixel_resolution` vectorially + # We create a 1-based local sequence for each repeated segment + # e.g., if number_of_voxels is [2, 3], we generate [1, 2, 1, 2, 3] + v_ids = torch.arange(wall2d_id_tensor.shape[0], device=device) + # Find the starting index of each repeated wall sequence + cum_voxels = torch.cumsum(number_of_voxels, dim=0) + start_indices = torch.cat( + [torch.tensor([0], device=device), cum_voxels[:-1]] + ) + shift = torch.repeat_interleave(start_indices, number_of_voxels) + + local_voxel_index = v_ids - shift + 1 + voxel_height_tensor = local_voxel_index * pixel_resolution + + # 6. Append the final trailing zero-row (mimicking original code logic) + zero_val = torch.tensor([0], device=device) + + wall2d_id_tensor = torch.cat([wall2d_id_tensor, zero_val]) + voxel_height_tensor = torch.cat([voxel_height_tensor, zero_val.float()]) + wall_height_tensor = torch.cat([wall_height_tensor, zero_val]) + wall_height_exact_tensor = torch.cat( + [wall_height_exact_tensor, zero_val.float()] + ) + y_position_tensor = torch.cat([y_position_tensor, zero_val]) + x_position_tensor = torch.cat([x_position_tensor, zero_val]) + + # 7. Construct outputs + voxelId_list = torch.arange( + 1, wall2d_id_tensor.shape[0] + 1, device=device + ) + + voxelTable = torch.column_stack( + [ + voxelId_list.float(), + voxel_height_tensor, + wall_height_tensor.float(), + wall_height_exact_tensor, + wall2d_id_tensor.float(), + y_position_tensor.float(), + x_position_tensor.float(), + ] + ) + + # 8. Construct dictionary mapping (Kept native Python as requested by return type) + # We do this from the extracted wall tensors to avoid looping over the voxel table + wall_dict = dict( + zip(wall_indices.tolist(), walls_exact_extracted.tolist()) + ) + wall_dict[0] = 0.0 + + # Convert specific lists to match original return signature formats if downstream requires it + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return ( + voxelTable, + voxelId_list, + wall_dict, + walls, + aspect, + uniqueWallIDs, + wall2d_id_tensor.tolist(), + voxel_height_tensor.tolist(), + ) + + +def svf_for_voxels( + dsm, + dem, + vegdsm, + vegdsm2, + transVeg, + scale, + usevegdem, + pixel_resolution, + voxelTable, + svf_height, + svf_array, + svfbu_array, + svfveg_array, + svfaveg_array, + svf_height_array, + feedback, + device=torch.device("cpu"), +): + """This function calculates sky view factor at all voxel levels""" + with torch.no_grad(): + dsm = _to_tensor(dsm, device) + dem = _to_tensor(dem, device) + vegdsm = _to_tensor(vegdsm, device) + vegdsm2 = _to_tensor(vegdsm2, device) + voxelTable = _to_tensor(voxelTable, device) + svf_array = _to_tensor(svf_array, device) + svfbu_array = _to_tensor(svfbu_array, device) + svfveg_array = _to_tensor(svfveg_array, device) + svfaveg_array = _to_tensor(svfaveg_array, device) + svf_height_array = _to_tensor(svf_height_array, device) + + # Calculate where there are buildings and not. Used to elevate dem. + ground = dsm - dem + # Ground == 1 = ground + ground[ground < 2] = 1.0 + # Ground == 0 = buildings + ground[ground >= 2] = 0.0 + + # Find maximum wall height, used to estimate how many iterations of svf_calc that are required + maxWallHeight = torch.max(voxelTable[:, 2]) - svf_height + + # Counter to feedback current iteration + counter = 1 + # How many iterations are required to calculate svf for all voxels + loop_range = torch.arange( + svf_height, maxWallHeight + svf_height, svf_height + ) + + # Loop for svf calculations of all voxel heights + for i in loop_range: + + feedback.setProgressText( + "SVF calculation number " + + str(int(counter)) + + " of " + + str(int(loop_range.shape[0])) + ) + + feedback.setProgressText( + "Increasing ground level with " + str(i) + " meters." + ) + + # Elevate ground in dsm + temp_dsm = ((dsm + i) * ground) + (dsm * (1 - ground)) + + if usevegdem == 1: + # Subtract from cdsm + temp_cdsm = vegdsm - i + temp_cdsm[temp_cdsm < 0] = 0 + # Subtract from tdsm + temp_cdsm2 = vegdsm2 - i + temp_cdsm2[temp_cdsm2 < 0] = 0 + else: + temp_cdsm = dsm * 0.0 + temp_cdsm2 = dsm * 0.0 + # Calculate svf. wallScheme set to 0 as only svf is estimated and nothing on the location of voxels, etc. + wallScheme = 0 + ret_ = svf.svfForProcessing153( + temp_dsm, + temp_cdsm, + temp_cdsm2, + scale, + usevegdem, + pixel_resolution, + wallScheme, + dem, + feedback, + device=device, + ) + svfbu = ret_["svf"] + + if usevegdem == 0: + svftotal = svfbu + svfveg = ret_["svfveg"] + svfaveg = ret_["svfaveg"] + else: + svfveg = ret_["svfveg"] + svfaveg = ret_["svfaveg"] + trans = transVeg / 100.0 + svftotal = svfbu - (1 - svfveg) * (1 - trans) + + # Get svf for each voxel + voxel_y = torch.where( + voxelTable[:, 1] == i + svf_height + ) # +svf_height) + for temp_y in voxel_y[0]: + svf_array[temp_y] = svftotal[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] + svfbu_array[temp_y] = svfbu[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ].double() + svfveg_array[temp_y] = svfveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] + svfaveg_array[temp_y] = svfaveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] + svf_height_array[temp_y] = i + svf_height # +svf_height + + if feedback.isCanceled(): + feedback.setProgressText("Calculation cancelled") + break + + counter += 1 + + svf_05 = svf_array.clone() + svf_05[svf_05 > 0.5] = 0.5 + + # Add svf arrays as volumns to voxelTable + voxelTable = torch.column_stack( + [ + voxelTable, + svf_height_array, + svf_array, + svf_05, + svfbu_array, + svfveg_array, + svfaveg_array, + ] + ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return voxelTable + + +def svf_kmeans( + dsm, + dem, + vegdsm, + vegdsm2, + wallHeights, + transVeg, + scale, + usevegdem, + pixel_resolution, + voxelTable, + clusters, + svf_height, + svf_array, + svfbu_array, + svfveg_array, + svfaveg_array, + svf_height_array, + feedback, + device=torch.device("cpu"), +): + try: + from sklearn.cluster import KMeans + except: + raise ImportError( + "[UMEP-processing Error] pleas install sklearn via pip install scikit-learn or via osgeo4w" + ) + + with torch.no_grad(): + + dsm = _to_tensor(dsm, device) + dem = _to_tensor(dem, device) + vegdsm = _to_tensor(vegdsm, device) + vegdsm2 = _to_tensor(vegdsm2, device) + wallHeights = _to_tensor(wallHeights, device) + voxelTable = _to_tensor(voxelTable, device) + svf_array = _to_tensor(svf_array, device) + svfbu_array = _to_tensor(svfbu_array, device) + svfveg_array = _to_tensor(svfveg_array, device) + svfaveg_array = _to_tensor(svfaveg_array, device) + svf_height_array = _to_tensor(svf_height_array, device) + + # Calculate where there are buildings and not. Used to elevate dem. + ground = dsm - dem + # Ground == 1 = ground + ground[ground < 2] = 1.0 + # Ground == 0 = buildings + ground[ground >= 2] = 0.0 + + # building_heights = dsm - dem + + # Reshape data for clustering + # data_reshaped = building_heights.reshape(-1, 1) + data_reshaped = wallHeights.detach().cpu().numpy().reshape(-1, 1) + + # Apply K-means clustering + kmeans = KMeans(n_clusters=clusters, random_state=0) + labels = kmeans.fit_predict(data_reshaped) + + # Reshape the labels back into a torch tensor on the selected device + kmeans_clusters = torch.from_numpy( + labels.reshape(dsm.shape[0], dsm.shape[1]) + ).to(device) + + # Remove cluster representing ground areas, i.e. where dsm - dem = 0 + cluster_range = torch.arange(clusters, device=device) + # cluster_range = cluster_range[cluster_range != torch.unique(kmeans_clusters[ground == 1])] + + # Array to store mean heights of clusters + cluster_heights = torch.zeros((cluster_range.shape[0]), device=device) + + counter = 0 + for i in cluster_range: + # cluster_heights[counter] = torch.round(building_heights[kmeans_clusters == i].mean()) + cluster_heights[counter] = ( + torch.round(wallHeights[kmeans_clusters == i].mean()) + - svf_height + ) # Remove svf_height which is the voxel size to be below the top of the wall + counter += 1 + + # Unique heights based on mean height of clusters, sorted from min to max + cluster_heights = torch.unique(cluster_heights) + cluster_heights = cluster_heights[cluster_heights > 0] + + # Counter to feedback current iteration + counter = 0 + + for i in cluster_heights: + if cluster_heights.shape[0] > 1: + feedback.setProgressText( + "SVF calculation based on K-means. Calculation " + + str(int(counter + 1)) + + " of " + + str(int(cluster_heights.shape[0])) + + " clusters." + ) + feedback.setProgressText( + "Mean wall height of cluster is " + + str(int(i + svf_height)) + + " meters. Increasing ground level with " + + str(int(i)) + + " meters." + ) + + # Elevate ground in dsm + temp_dsm = ((dsm + i) * ground) + (dsm * (1 - ground)) + # temp_dsm = dsm[ground == 1] + temp_mean + + if usevegdem == 1: + # Subtract from cdsm + temp_cdsm = vegdsm - i + temp_cdsm[temp_cdsm < 0] = 0 + # Subtract from tdsm + temp_cdsm2 = vegdsm2 - i + temp_cdsm2[temp_cdsm2 < 0] = 0 + else: + temp_cdsm = dsm * 0.0 + temp_cdsm2 = dsm * 0.0 + + # Calculate svf. wallScheme set to 0 as only svf is estimated and nothing on the location of voxels, etc. + wallScheme = 0 + ret_ = svf.svfForProcessing153( + temp_dsm, + temp_cdsm, + temp_cdsm2, + scale, + usevegdem, + pixel_resolution, + wallScheme, + dem, + feedback, + device=device, + ) + + svfbu = ret_["svf"] + + if usevegdem == 0: + svftotal = svfbu + svfveg = ret_["svfveg"] + svfaveg = ret_["svfaveg"] + else: + svfveg = ret_["svfveg"] + svfaveg = ret_["svfaveg"] + trans = transVeg / 100.0 + svftotal = svfbu - (1 - svfveg) * (1 - trans) + + # Get svf for each voxel + voxel_y = torch.where( + voxelTable[:, 1] == i + svf_height + ) # +svf_height) + for temp_y in voxel_y[0]: + svf_array[temp_y] = svftotal[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] + svfbu_array[temp_y] = svfbu[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ].double() + svfveg_array[temp_y] = svfveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] + svfaveg_array[temp_y] = svfaveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] + svf_height_array[temp_y] = i + svf_height + + if i == cluster_heights[-1]: + temp_data = voxelTable[ + voxelTable[:, 2] > i, : + ] # Get all walls that are taller than the mean of the lowest cluster + unique_walls = torch.unique( + temp_data[:, 4] + ) # Get their unique wall ids for slicing + for ( + unique_wall + ) in ( + unique_walls + ): # Loop over all unique walls lower than lowest cluster + temp_wall = temp_data[temp_data[:, 4] == unique_wall, :][ + :, 1 + ].max() # Max height of highest voxel in unique_wall + temp_y = torch.where( + (voxelTable[:, 4] == unique_wall) + & (voxelTable[:, 1] == temp_wall) + )[ + 0 + ] # Get row of unique_wall and highest voxel in voxelTable + + svf_array[temp_y] = ( + 0.5 # Set svf to 0.5 as these are the highest voxels and nothing or little should obstruct it, i.e. svf = 0.5 + ) + svfbu_array[temp_y] = svfbu[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ].double() # save svfbu for current wall pixel + svfveg_array[temp_y] = svfveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] # save svfveg for current wall pixel + svfaveg_array[temp_y] = svfaveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] # save svfaveg for current wall pixel + svf_height_array[temp_y] = ( + temp_wall # set svf_height to highest voxel for current wall + ) + # Add 0.5 to highest voxel on walls that are lower than cluster with lowest mean height + else: + if counter == 0: + temp_data = voxelTable[ + voxelTable[:, 2] < i, : + ] # Get all walls that are lower than the mean of the lowest cluster + else: + temp_data = voxelTable[ + (voxelTable[:, 2] > cluster_heights[counter - 1]) + & (voxelTable[:, 2] < i), + :, + ] + unique_walls = torch.unique( + temp_data[:, 4] + ) # Get their unique wall ids for slicing + for ( + unique_wall + ) in ( + unique_walls + ): # Loop over all unique walls lower than lowest cluster + temp_wall = temp_data[temp_data[:, 4] == unique_wall, :][ + :, 1 + ].max() # Max height of highest voxel in unique_wall + temp_y = torch.where( + (voxelTable[:, 4] == unique_wall) + & (voxelTable[:, 1] == temp_wall) + )[ + 0 + ] # Get row of unique_wall and highest voxel in voxelTable + temp_svf = svftotal[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] # get the calculated svf for the wall pixel to check if it is higher or lower than 0.5 + if ( + temp_svf < 0.5 + ): # if current wall pixel is lower than 0.5, although it is estimated above the wall, save it at the highest voxel and use for interpolation + svf_array[temp_y] = temp_svf + else: # else, give highest voxel a value of 0.5 + svf_array[temp_y] = 0.5 + svfbu_array[temp_y] = svfbu[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ].double() # save svfbu for current wall pixel + svfveg_array[temp_y] = svfveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] # save svfveg for current wall pixel + svfaveg_array[temp_y] = svfaveg[ + int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) + ] # save svfaveg for current wall pixel + svf_height_array[temp_y] = ( + temp_wall # set svf_height to highest voxel for current wall + ) + + if feedback.isCanceled(): + feedback.setProgressText("Calculation cancelled") + break + + counter += 1 + + svf_05 = svf_array.clone() + svf_05[svf_05 > 0.5] = 0.5 + + # Add svf arrays as volumns to voxelTable + voxelTable = torch.column_stack( + [ + voxelTable, + svf_height_array, + svf_array, + svf_05, + svfbu_array, + svfveg_array, + svfaveg_array, + ] + ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return voxelTable, cluster_heights + + +def interpolate_svf(voxelTable): + with torch.no_grad(): + device = voxelTable.device + voxelTable_np = voxelTable.cpu().numpy() + + unique_wall_pixels = np.unique(voxelTable_np[:, 4]) + unique_wall_pixels = unique_wall_pixels[unique_wall_pixels != 0] + + for unique_wall in unique_wall_pixels: + # Mask for the current wall pixel + wall_mask = voxelTable_np[:, 4] == unique_wall + temp_data = voxelTable_np[ + wall_mask, : + ] # All data for current wall pixel + + # Mask for valid SVF calculation heights + valid_svf_mask = temp_data[:, -1] != 0 + temp_heights = temp_data[valid_svf_mask, 1] # Voxel heights + temp_svf = temp_data[valid_svf_mask, -4] # SVF values + + if len(temp_heights) == 1: + calculated_svf = temp_svf[0] if len(temp_svf) > 0 else 0.0 + new_svf = np.full(temp_data.shape[0], calculated_svf) + + elif len(temp_heights) > 1: # Interpolate + new_svf = np.interp(temp_data[:, 1], temp_heights, temp_svf) + else: + continue + + voxelTable_np[wall_mask, -4] = new_svf + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return torch.from_numpy(voxelTable_np).to(device) diff --git a/functions/svf_functions_torch.py b/functions/svf_functions_torch.py new file mode 100644 index 0000000..b6fc42e --- /dev/null +++ b/functions/svf_functions_torch.py @@ -0,0 +1,723 @@ +try: + import torch +except: + pass + +from ..util import shadowingfunctions_torch as shadow +from ..util.SEBESOLWEIGCommonFiles.create_patches_torch import create_patches + + +def _to_tensor(x, device, dtype=torch.float32): + if isinstance(x, torch.Tensor): + return x.to(device) + if x is None: + return None + return torch.tensor(x, dtype=dtype, device=device) + + +# from ..functions.wallalgorithms import findwalls +from . import wallalgorithms_torch as wa +from . import svf_for_voxels_torch as svfv +from ..util.SEBESOLWEIGCommonFiles import ( + shadowingfunction_wallheight_13_torch as shb, +) +from ..util.SEBESOLWEIGCommonFiles import ( + shadowingfunction_wallheight_23_torch as shbv, +) + +# remove +from ..util.misc import saveraster +from osgeo.gdalconst import * +from osgeo import gdal, osr + + +def annulus_weight(altitude, aziinterval, device): + n = torch.tensor(90.0, device=device) + steprad = (360.0 / aziinterval) * (torch.pi / 180.0) + annulus = 91.0 - altitude + w = ( + (1.0 / (2.0 * torch.pi)) + * torch.sin(torch.pi / (2.0 * n)) + * torch.sin((torch.pi * (2.0 * annulus - 1.0)) / (2.0 * n)) + ) + weight = steprad * w + del n, w + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return weight + + +def svf_angles_100121(device): + azi1 = torch.arange(1.0, 360.0, 360.0 / 16.0) # %22.5 + azi2 = torch.arange(12.0, 360.0, 360.0 / 16.0) # %22.5 + azi3 = torch.arange(5.0, 360.0, 360.0 / 32.0) # %11.25 + azi4 = torch.arange(2.0, 360.0, 360.0 / 32.0) # %11.25 + azi5 = torch.arange(4.0, 360.0, 360.0 / 40.0) # %9 + azi6 = torch.arange(7.0, 360.0, 360.0 / 48.0) # %7.50 + azi7 = torch.arange(6.0, 360.0, 360.0 / 48.0) # %7.50 + azi8 = torch.arange(1.0, 360.0, 360.0 / 48.0) # %7.50 + azi9 = torch.arange(4.0, 359.0, 360.0 / 52.0) # %6.9231 + azi10 = torch.arange(5.0, 360.0, 360.0 / 52.0) # %6.9231 + azi11 = torch.arange(1.0, 360.0, 360.0 / 48.0) # %7.50 + azi12 = torch.arange(0.0, 359.0, 360.0 / 44.0) # %8.1818 + azi13 = torch.arange(3.0, 360.0, 360.0 / 44.0) # %8.1818 + azi14 = torch.arange(2.0, 360.0, 360.0 / 40.0) # %9 + azi15 = torch.arange(7.0, 360.0, 360.0 / 32.0) # %10 + azi16 = torch.arange(3.0, 360.0, 360.0 / 24.0) # %11.25 + azi17 = torch.arange(10.0, 360.0, 360.0 / 16.0) # %15 + azi18 = torch.arange(19.0, 360.0, 360.0 / 12.0) # %22.5 + azi19 = torch.arange(17.0, 360.0, 360.0 / 8.0) # %45 + azi20 = 0.0 # %360 + iazimuth = torch.tensor( + torch.hstack( + ( + azi1, + azi2, + azi3, + azi4, + azi5, + azi6, + azi7, + azi8, + azi9, + azi10, + azi11, + azi12, + azi13, + azi14, + azi15, + azi16, + azi17, + azi18, + azi19, + azi20, + ) + ), + device=device, + ) + aziinterval = torch.tensor( + torch.hstack( + ( + 16.0, + 16.0, + 32.0, + 32.0, + 40.0, + 48.0, + 48.0, + 48.0, + 52.0, + 52.0, + 48.0, + 44.0, + 44.0, + 40.0, + 32.0, + 24.0, + 16.0, + 12.0, + 8.0, + 1.0, + ) + ), + device=device, + ) + angleresult = {"iazimuth": iazimuth, "aziinterval": aziinterval} + del iazimuth, aziinterval + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return angleresult + + +def svfForProcessing153( + dsm, + vegdem, + vegdem2, + scale, + usevegdem, + pixel_resolution, + wallScheme, + demlayer, + feedback, + device=torch.device("cpu"), +): + + with torch.no_grad(): + + dsm = _to_tensor(dsm, device) + vegdem = _to_tensor(vegdem, device) + vegdem2 = _to_tensor(vegdem2, device) + demlayer = _to_tensor(demlayer, device) + + rows = dsm.shape[0] + cols = dsm.shape[1] + svf = torch.zeros([rows, cols], device=device) + svfE = torch.zeros([rows, cols], device=device) + svfS = torch.zeros([rows, cols], device=device) + svfW = torch.zeros([rows, cols], device=device) + svfN = torch.zeros([rows, cols], device=device) + svfveg = torch.zeros((rows, cols), device=device) + svfEveg = torch.zeros((rows, cols), device=device) + svfSveg = torch.zeros((rows, cols), device=device) + svfWveg = torch.zeros((rows, cols), device=device) + svfNveg = torch.zeros((rows, cols), device=device) + svfaveg = torch.zeros((rows, cols), device=device) + svfEaveg = torch.zeros((rows, cols), device=device) + svfSaveg = torch.zeros((rows, cols), device=device) + svfWaveg = torch.zeros((rows, cols), device=device) + svfNaveg = torch.zeros((rows, cols), device=device) + + # % amaxvalue + vegmax = vegdem.max() + amaxvalue = dsm.max() + amaxvalue = torch.maximum(amaxvalue, vegmax) + + # % Elevation vegdems if buildingDSM inclused ground heights + vegdem = vegdem + dsm + vegdem[vegdem == dsm] = 0 + vegdem2 = vegdem2 + dsm + vegdem2[vegdem2 == dsm] = 0 + # % Bush separation + bush = torch.logical_not((vegdem2 * vegdem)) * vegdem + + # index = int(0) + + # patch_option = 1 # 145 patches + patch_option = 2 # 153 patches + # patch_option = 3 # 306 patches + # patch_option = 4 # 612 patches + + # Create patches based on patch_option + ( + skyvaultalt, + skyvaultazi, + annulino, + skyvaultaltint, + aziinterval, + skyvaultaziint, + azistart, + ) = create_patches(patch_option, device) + + skyvaultalt = _to_tensor(skyvaultalt, device) + skyvaultazi = _to_tensor(skyvaultazi, device) + annulino = _to_tensor(annulino, device) + skyvaultaltint = _to_tensor(skyvaultaltint, device) + aziinterval = _to_tensor(aziinterval, device) + skyvaultaziint = _to_tensor(skyvaultaziint, device) + azistart = _to_tensor(azistart, device) + + skyvaultaziint = torch.tensor( + [360 / patches for patches in aziinterval], device=device + ) + iazimuth = torch.zeros( + int(torch.sum(aziinterval).item()), device=device + ) + + shmat = torch.zeros( + (rows, cols, int(torch.sum(aziinterval).item())), device=device + ) + vegshmat = torch.zeros( + (rows, cols, int(torch.sum(aziinterval).item())), device=device + ) + vbshvegshmat = torch.zeros( + (rows, cols, int(torch.sum(aziinterval).item())), device=device + ) + + # Preparations for wall temperature scheme + if wallScheme: + feedback.setProgressText("Estimating view factors for wall voxels") + ( + voxelTable, + voxelId_list, + wall_dict, + walls, + aspect, + uniqueWallIDs, + wall2d_id, + voxel_height, + ) = svfv.wallscheme_prepare( + dsm, scale, pixel_resolution, feedback, device=device + ) + + # Rasters to fill with values in loop + all_buildIDSeen = torch.zeros( + (rows, cols, skyvaultalt.shape[0]), device=device + ) + all_voxelHeight = torch.zeros( + (rows, cols, skyvaultalt.shape[0]), device=device + ) + all_voxelId = torch.zeros( + (rows, cols, skyvaultalt.shape[0]), device=device + ) + else: + voxelTable = 0 + allbuildIDSeen = 0 + allvoxelHeight = 0 + all_voxelId = 0 + walls = 0 + + index = 0 + for j in range(0, skyvaultaltint.shape[0]): + for k in range(0, int(360 / skyvaultaziint[j].item())): + iazimuth[index] = k * skyvaultaziint[j] + azistart[j] + if iazimuth[index] > 360.0: + iazimuth[index] = iazimuth[index] - 360.0 + index = index + 1 + aziintervalaniso = torch.ceil(aziinterval / 2.0) + index = 0 + for i in range(0, skyvaultaltint.shape[0]): + for j in range(0, int(aziinterval[int(i)].item())): + if feedback.isCanceled(): + feedback.setProgressText("Calculation cancelled") + break + altitude = torch.tensor( + float(skyvaultaltint[int(i)].item()), device=device + ) + azimuth = torch.tensor( + float(iazimuth[int(index)].item()), device=device + ) + + # Casting shadow + if wallScheme: + if usevegdem == 1: + ( + vegsh, + sh, + vbshvegsh, + wallsh, + wallsun, + wallshve, + facesh, + facesun, + ) = shbv.shadowingfunction_wallheight_23( + dsm, + vegdem, + vegdem2, + azimuth, + altitude, + scale, + amaxvalue, + bush, + walls, + aspect * torch.pi / 180, + ) + vegshmat[:, :, index] = vegsh + vbshvegshmat[:, :, index] = vbshvegsh + else: + sh, wallsh, wallsun, facesh, facesun = ( + shb.shadowingfunction_wallheight_13( + dsm, + azimuth, + altitude, + scale, + walls, + aspect * torch.pi / 180.0, + ) + ) + vegsh = torch.ones( + (sh.shape[0], sh.shape[1]), + device=device, + dtype=torch.float32, + ) + vbshvegsh = torch.ones( + (sh.shape[0], sh.shape[1]), + device=device, + dtype=torch.float32, + ) + vegshmat[:, :, index] = vegsh + vbshvegshmat[:, :, index] = vbshvegsh + else: + if usevegdem == 1: + shadowresult = shadow.shadowingfunction_20( + dsm, + vegdem, + vegdem2, + azimuth, + altitude, + scale, + amaxvalue, + bush, + feedback, + 1, + device, + ) + + vegsh = torch.tensor( + shadowresult["vegsh"], device=device + ) + vbshvegsh = torch.tensor( + shadowresult["vbshvegsh"], device=device + ) + vegshmat[:, :, index] = vegsh + vbshvegshmat[:, :, index] = vbshvegsh + sh = torch.tensor(shadowresult["sh"], device=device) + else: + sh = shadow.shadowingfunctionglobalradiation( + dsm, azimuth, altitude, scale, feedback, 1, device + ) + + shmat[:, :, index] = sh + + # Wall temperature scheme, i.e. finding out which voxel is seen from each pixel, where direction is patch azimuth and altitude + if wallScheme: + ( + all_buildIDSeen[:, :, index], + all_voxelHeight[:, :, index], + all_voxelId[:, :, index], + ) = shadow.shadowingfunction_findwallID( + dsm, + azimuth, + altitude, + scale, + walls, + uniqueWallIDs, + demlayer, + wall2d_id, + voxel_height, + voxelId_list, + facesh, + wall_dict, + sh, + device, + ) + + # Calculate svfs + for k in range( + int(annulino[int(i)].item()) + 1, + int(annulino[int(i + 1.0)].item()) + 1, + ): + + weight = annulus_weight(k, aziinterval[i], device) * sh + svf = svf + weight + weight = ( + annulus_weight(k, aziintervalaniso[i], device) * sh + ) + if (azimuth >= 0) and (azimuth < 180): + svfE = svfE + weight + if (azimuth >= 90) and (azimuth < 270): + svfS = svfS + weight + if (azimuth >= 180) and (azimuth < 360): + svfW = svfW + weight + if (azimuth >= 270) or (azimuth < 90): + svfN = svfN + weight + + if usevegdem == 1: + for k in torch.arange( + annulino[int(i)] + 1, (annulino[int(i + 1.0)]) + 1 + ): + # % changed to include 90 + weight = annulus_weight(k, aziinterval[i], device) + svfveg = svfveg + weight * vegsh + svfaveg = svfaveg + weight * vbshvegsh + weight = annulus_weight(k, aziintervalaniso[i], device) + if (azimuth >= 0) and (azimuth < 180): + svfEveg = svfEveg + weight * vegsh + svfEaveg = svfEaveg + weight * vbshvegsh + if (azimuth >= 90) and (azimuth < 270): + svfSveg = svfSveg + weight * vegsh + svfSaveg = svfSaveg + weight * vbshvegsh + if (azimuth >= 180) and (azimuth < 360): + svfWveg = svfWveg + weight * vegsh + svfWaveg = svfWaveg + weight * vbshvegsh + if (azimuth >= 270) or (azimuth < 90): + svfNveg = svfNveg + weight * vegsh + svfNaveg = svfNaveg + weight * vbshvegsh + + index += 1 + feedback.setProgress( + int(index * (100.0 / torch.sum(aziinterval))) + ) + + svfS = svfS + 3.0459e-004 + svfW = svfW + 3.0459e-004 + # % Last azimuth is 90. Hence, manual add of last annuli for svfS and SVFW + # %Forcing svf not be greater than 1 (some MATLAB crazyness) + svf[(svf > 1.0)] = 1.0 + svfE[(svfE > 1.0)] = 1.0 + svfS[(svfS > 1.0)] = 1.0 + svfW[(svfW > 1.0)] = 1.0 + svfN[(svfN > 1.0)] = 1.0 + + if usevegdem == 1: + last = torch.zeros((rows, cols), device=device) + last[(vegdem2 == 0.0)] = 3.0459e-004 + svfSveg = svfSveg + last + svfWveg = svfWveg + last + svfSaveg = svfSaveg + last + svfWaveg = svfWaveg + last + # %Forcing svf not be greater than 1 (some MATLAB crazyness) + svfveg[(svfveg > 1.0)] = 1.0 + svfEveg[(svfEveg > 1.0)] = 1.0 + svfSveg[(svfSveg > 1.0)] = 1.0 + svfWveg[(svfWveg > 1.0)] = 1.0 + svfNveg[(svfNveg > 1.0)] = 1.0 + svfaveg[(svfaveg > 1.0)] = 1.0 + svfEaveg[(svfEaveg > 1.0)] = 1.0 + svfSaveg[(svfSaveg > 1.0)] = 1.0 + svfWaveg[(svfWaveg > 1.0)] = 1.0 + svfNaveg[(svfNaveg > 1.0)] = 1.0 + + svfresult = { + "svf": svf, + "svfE": svfE, + "svfS": svfS, + "svfW": svfW, + "svfN": svfN, + "svfveg": svfveg, + "svfEveg": svfEveg, + "svfSveg": svfSveg, + "svfWveg": svfWveg, + "svfNveg": svfNveg, + "svfaveg": svfaveg, + "svfEaveg": svfEaveg, + "svfSaveg": svfSaveg, + "svfWaveg": svfWaveg, + "svfNaveg": svfNaveg, + "shmat": shmat, + "vegshmat": vegshmat, + "vbshvegshmat": vbshvegshmat, + "voxelIds": all_voxelId, + "voxelTable": voxelTable, + "walls": walls, + } + + # Delete input and intermediate tensors not needed outside + del dsm, vegdem, vegdem2, demlayer, bush, vegmax, amaxvalue, last + + # Delete patch configuration tensors + del ( + skyvaultalt, + skyvaultazi, + annulino, + skyvaultaltint, + aziinterval, + skyvaultaziint, + azistart, + iazimuth, + aziintervalaniso, + ) + + # Delete wall scheme temporary variables + del (voxelTable, walls) + + # Delete loop-specific matrices already reference-copied into svfresult + del shmat, vegshmat, vbshvegshmat, all_voxelId + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return svfresult + + +def svfForProcessing655( + dsm, + vegdem, + vegdem2, + scale, + usevegdem, + feedback, + device=torch.device("cpu"), +): + + with torch.no_grad(): + + dsm = _to_tensor(dsm, device) + vegdem = _to_tensor(vegdem, device) + vegdem2 = _to_tensor(vegdem2, device) + + rows = dsm.shape[0] + cols = dsm.shape[1] + svf = torch.zeros([rows, cols], device=device) + svfE = torch.zeros([rows, cols], device=device) + svfS = torch.zeros([rows, cols], device=device) + svfW = torch.zeros([rows, cols], device=device) + svfN = torch.zeros([rows, cols], device=device) + svfveg = torch.zeros((rows, cols), device=device) + svfEveg = torch.zeros((rows, cols), device=device) + svfSveg = torch.zeros((rows, cols), device=device) + svfWveg = torch.zeros((rows, cols), device=device) + svfNveg = torch.zeros((rows, cols), device=device) + svfaveg = torch.zeros((rows, cols), device=device) + svfEaveg = torch.zeros((rows, cols), device=device) + svfSaveg = torch.zeros((rows, cols), device=device) + svfWaveg = torch.zeros((rows, cols), device=device) + svfNaveg = torch.zeros((rows, cols), device=device) + + # % amaxvalue + vegmax = vegdem.max() + amaxvalue = dsm.max() + amaxvalue = torch.maximum(amaxvalue, vegmax) + + # % Elevation vegdems if buildingDSM inclused ground heights + vegdem = vegdem + dsm + vegdem[vegdem == dsm] = 0 + vegdem2 = vegdem2 + dsm + vegdem2[vegdem2 == dsm] = 0 + # % Bush separation + bush = torch.logical_not((vegdem2 * vegdem)) * vegdem + + noa = torch.tensor(19.0, device=device) + # % No. of anglesteps minus 1 + step = 89.0 / noa + iangle = torch.tensor( + torch.hstack((torch.arange(step / 2.0, 89.0, step), 90.0)), + device=device, + ) + annulino = torch.tensor( + torch.hstack((torch.round(torch.arange(0.0, 89.0, step)), 90.0)), + device=device, + ) + angleresult = svf_angles_100121(device) + aziinterval = angleresult["aziinterval"].to(device) + iazimuth = angleresult["iazimuth"].to(device) + aziintervalaniso = torch.ceil((aziinterval / 2.0)) + index = 1 + + for i in range(0, iangle.shape[0] - 1): + for j in range(0, int(aziinterval[int(i)].item())): + if feedback.isCanceled(): + feedback.setProgressText("Calculation cancelled") + break + altitude = float(iangle[int(i)].item()) + azimuth = float(iazimuth[int(index) - 1].item()) + + # Casting shadow + if usevegdem == 1: + shadowresult = shadow.shadowingfunction_20( + dsm, + vegdem, + vegdem2, + azimuth, + altitude, + scale, + amaxvalue, + bush, + feedback, + 1, + device, + ) + + vegsh = torch.tensor(shadowresult["vegsh"], device=device) + vbshvegsh = torch.tensor( + shadowresult["vbshvegsh"], device=device + ) + sh = torch.tensor(shadowresult["sh"], device=device) + else: + sh = shadow.shadowingfunctionglobalradiation( + dsm, azimuth, altitude, scale, feedback, 1, device + ) + + # Calculate svfs + for k in range( + int(annulino[int(i)].item()) + 1, + int(annulino[int(i + 1.0)].item()) + 1, + ): + weight = annulus_weight(k, aziinterval[i], device) * sh + svf = svf + weight + weight = ( + annulus_weight(k, aziintervalaniso[i], device) * sh + ) + if (azimuth >= 0) and (azimuth < 180): + svfE = svfE + weight + if (azimuth >= 90) and (azimuth < 270): + svfS = svfS + weight + if (azimuth >= 180) and (azimuth < 360): + svfW = svfW + weight + if (azimuth >= 270) or (azimuth < 90): + svfN = svfN + weight + + if usevegdem == 1: + for k in torch.arange( + annulino[int(i)] + 1, (annulino[int(i + 1.0)]) + 1 + ): + # % changed to include 90 + weight = annulus_weight(k, aziinterval[i], device) + svfveg = svfveg + weight * vegsh + svfaveg = svfaveg + weight * vbshvegsh + weight = annulus_weight(k, aziintervalaniso[i], device) + if (azimuth >= 0) and (azimuth < 180): + svfEveg = svfEveg + weight * vegsh + svfEaveg = svfEaveg + weight * vbshvegsh + if (azimuth >= 90) and (azimuth < 270): + svfSveg = svfSveg + weight * vegsh + svfSaveg = svfSaveg + weight * vbshvegsh + if (azimuth >= 180) and (azimuth < 360): + svfWveg = svfWveg + weight * vegsh + svfWaveg = svfWaveg + weight * vbshvegsh + if (azimuth >= 270) or (azimuth < 90): + svfNveg = svfNveg + weight * vegsh + svfNaveg = svfNaveg + weight * vbshvegsh + + index += 1 + feedback.setProgress(int(index * (100.0 / 655.0))) + + svfS = svfS + 3.0459e-004 + svfW = svfW + 3.0459e-004 + # % Last azimuth is 90. Hence, manual add of last annuli for svfS and SVFW + # %Forcing svf not be greater than 1 (some MATLAB crazyness) + svf[(svf > 1.0)] = 1.0 + svfE[(svfE > 1.0)] = 1.0 + svfS[(svfS > 1.0)] = 1.0 + svfW[(svfW > 1.0)] = 1.0 + svfN[(svfN > 1.0)] = 1.0 + + if usevegdem == 1: + last = torch.zeros((rows, cols), device=device) + last[(vegdem2 == 0.0)] = 3.0459e-004 + svfSveg = svfSveg + last + svfWveg = svfWveg + last + svfSaveg = svfSaveg + last + svfWaveg = svfWaveg + last + # %Forcing svf not be greater than 1 (some MATLAB crazyness) + svfveg[(svfveg > 1.0)] = 1.0 + svfEveg[(svfEveg > 1.0)] = 1.0 + svfSveg[(svfSveg > 1.0)] = 1.0 + svfWveg[(svfWveg > 1.0)] = 1.0 + svfNveg[(svfNveg > 1.0)] = 1.0 + svfaveg[(svfaveg > 1.0)] = 1.0 + svfEaveg[(svfEaveg > 1.0)] = 1.0 + svfSaveg[(svfSaveg > 1.0)] = 1.0 + svfWaveg[(svfWaveg > 1.0)] = 1.0 + svfNaveg[(svfNaveg > 1.0)] = 1.0 + + svfresult = { + "svf": svf, + "svfE": svfE, + "svfS": svfS, + "svfW": svfW, + "svfN": svfN, + "svfveg": svfveg, + "svfEveg": svfEveg, + "svfSveg": svfSveg, + "svfWveg": svfWveg, + "svfNveg": svfNveg, + "svfaveg": svfaveg, + "svfEaveg": svfEaveg, + "svfSaveg": svfSaveg, + "svfWaveg": svfWaveg, + "svfNaveg": svfNaveg, + } + + # Delete input and intermediate tensors not needed outside + del dsm, vegdem, vegdem2, bush, vegmax, amaxvalue, last + + # Delete angle and patch tensors + del ( + noa, + step, + iangle, + annulino, + angleresult, + aziinterval, + iazimuth, + aziintervalaniso, + ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return svfresult diff --git a/functions/wallalgorithms_torch.py b/functions/wallalgorithms_torch.py new file mode 100644 index 0000000..c0e3742 --- /dev/null +++ b/functions/wallalgorithms_torch.py @@ -0,0 +1,372 @@ +from builtins import range + +# -*- coding: utf-8 -*- +__author__ = "xlinfr" + +import math + +try: + import torch + import torch.nn.functional as F +except: + pass + +# import scipy.misc as sc +import scipy.ndimage as sc +from scipy.ndimage import maximum_filter + + +def findwalls_sp(arr_dsm, walllimit, device, footprint=None): + if isinstance(arr_dsm, torch.Tensor): + dsm_tensor = arr_dsm + else: + dsm_tensor = torch.tensor(arr_dsm, device=device) + + if footprint is None or footprint is False: + footprint = torch.tensor( + [[0, 1, 0], [1, 1, 1], [0, 1, 0]], device=device + ) + else: + footprint = footprint.to(device=device) + + fh, fw = footprint.shape + pad_h, pad_w = fh // 2, fw // 2 + + padded_a = dsm_tensor.unsqueeze(0).unsqueeze(0) + padded_a = F.pad( + padded_a, pad=(pad_w, pad_w, pad_h, pad_h), mode="replicate" + ) + padded_a = padded_a.squeeze(0).squeeze(0) + + max_neighbors = torch.full_like(dsm_tensor, float("-inf")) + + y_indices, x_indices = torch.where(footprint == 1) + + H, W = dsm_tensor.shape + for dy, dx in zip(y_indices, x_indices): + shifted_view = padded_a[dy : dy + H, dx : dx + W] + max_neighbors = torch.maximum(max_neighbors, shifted_view) + + walls = max_neighbors - dsm_tensor + + walls[walls < walllimit] = 0 + + walls[0, :] = 0 + walls[-1, :] = 0 + walls[:, 0] = 0 + walls[:, -1] = 0 + + return walls + + +def filter1Goodwin_as_aspect_v3( + walls_for_dir, scale, a, feedback, total, device, tile_size=2048 +): + """Applies the Goodwin et al. + + (2010) filter processing to calculate wall aspect based on a wall pixel + grid, a DSM (a), and a scale factor. + + Optimized for GPU using 2D Convolutions to eliminate nested pixel loops + and CPU-GPU transfer bottlenecks. + + :param walls_for_dir: Tensor (H, W), Binary grid of walls + :param scale: Float, Scale factor determining filter size + :param a: Tensor (H, W), Digital Surface Model (DSM) + :param feedback: Optional, QGIS/Processing feedback object for progress + :param total: Float, Progress step multiplier + :return: Tensor (H, W), Wall aspect directions + """ + row, col = a.shape + + # 1. Compute kernel footprint based on scale factor + filtersize = torch.floor( + (scale + torch.tensor(0.0000000001, device=device)) + * torch.tensor(9, device=device) + ) + if filtersize <= 2: + filtersize = 3 + elif filtersize != 9 and filtersize % 2 == 0: + filtersize = filtersize + 1 + + filtersize = int(filtersize) + filthalveceil = int(torch.ceil(torch.tensor(filtersize) / 2.0)) + filthalvefloor = int(torch.floor(torch.tensor(filtersize) / 2.0)) + n = filtersize - 1 + + # Initialize base structural elements on CPU for rotation + filtmatrix = torch.zeros((filtersize, filtersize)) + buildfilt = torch.zeros((filtersize, filtersize)) + + filtmatrix[:, filthalveceil - 1] = 1 + buildfilt[filthalveceil - 1, 0:filthalvefloor] = 1 + buildfilt[filthalveceil - 1, filthalveceil:filtersize] = 2 + + filtmatrix_list = [] + buildfilt1_list = [] + buildfilt2_list = [] + + # 2. Pre-calculate all 180 directional filters on CPU or GPU + with torch.no_grad(): + for h in range(180): + filtmatrix1temp = sc.rotate( + filtmatrix.numpy(), h, order=1, reshape=False, mode="nearest" + ) + filtmatrix1 = torch.round(torch.from_numpy(filtmatrix1temp)) + + filtmatrixbuildtemp = sc.rotate( + buildfilt.numpy(), h, order=0, reshape=False, mode="nearest" + ) + filtmatrixbuild = torch.round( + torch.from_numpy(filtmatrixbuildtemp) + ) + + index = 270 - h + if h in (150, 30): + filtmatrixbuild[:, n] = 0 + if index == 225: + filtmatrix1[0, 0] = 1 + filtmatrix1[n, n] = 1 + if index == 135: + filtmatrix1[0, n] = 1 + filtmatrix1[n, 0] = 1 + + filtmatrix_list.append(filtmatrix1.unsqueeze(0).unsqueeze(0)) + buildfilt1_list.append( + (filtmatrixbuild == 1).float().unsqueeze(0).unsqueeze(0) + ) + buildfilt2_list.append( + (filtmatrixbuild == 2).float().unsqueeze(0).unsqueeze(0) + ) + + # Stacking kernels on CPU first - Now perfectly defined! + all_kernels_walls = torch.cat(filtmatrix_list, dim=0) + all_kernels_dsm1 = torch.cat(buildfilt1_list, dim=0) + all_kernels_dsm2 = torch.cat(buildfilt2_list, dim=0) + + # 3. Setup global output allocation arrays + final_y = torch.zeros((row, col), device=device) + final_x = torch.zeros((row, col), device=device) + + walls_binary = (walls_for_dir > 0).float().to(device) + a_device = a.float().to(device) + + # Calculate total tiles for feedback tracking + num_tiles_y = math.ceil(row / tile_size) + num_tiles_x = math.ceil(col / tile_size) + total_tiles = num_tiles_y * num_tiles_x + tile_count = 0 + + # 4. Loop through spatial tiles + for r_start in range(0, row, tile_size): + r_end = min(r_start + tile_size, row) + + for c_start in range(0, col, tile_size): + c_end = min(c_start + tile_size, col) + + if feedback is not None and feedback.isCanceled(): + return final_y + + # Determine padded coordinates (the halo zone) + pad_top = min(r_start, filthalvefloor) + pad_bottom = min(row - r_end, filthalvefloor) + pad_left = min(c_start, filthalvefloor) + pad_right = min(col - c_end, filthalvefloor) + + # Slice tile out with padding included + tile_a = ( + a_device[ + (r_start - pad_top) : (r_end + pad_bottom), + (c_start - pad_left) : (c_end + pad_right), + ] + .unsqueeze(0) + .unsqueeze(0) + ) + + tile_walls = ( + walls_binary[ + (r_start - pad_top) : (r_end + pad_bottom), + (c_start - pad_left) : (c_end + pad_right), + ] + .unsqueeze(0) + .unsqueeze(0) + ) + + # Local allocation sizes for this specific tile (including pads) + tile_rows, tile_cols = tile_a.shape[2], tile_a.shape[3] + + # Running Maximum Setup for this tile + z_max = torch.full((tile_rows, tile_cols), -1.0, device=device) + h_best = torch.zeros( + (tile_rows, tile_cols), dtype=torch.long, device=device + ) + dsm_best1 = torch.zeros((tile_rows, tile_cols), device=device) + dsm_best2 = torch.zeros((tile_rows, tile_cols), device=device) + + # 5. Process angles in small chunks inside this single spatial tile + chunk_size = 10 + for idx in range(0, 180, chunk_size): + end_idx = min(idx + chunk_size, 180) + + k_walls = all_kernels_walls[idx:end_idx].to(device) + k_dsm1 = all_kernels_dsm1[idx:end_idx].to(device) + k_dsm2 = all_kernels_dsm2[idx:end_idx].to(device) + + # Notice: padding=0 because we manually padded our spatial tiles! + walls_conv = F.conv2d(tile_walls, k_walls, padding=0).squeeze( + 0 + ) + dsm_conv1 = F.conv2d(tile_a, k_dsm1, padding=0).squeeze(0) + dsm_conv2 = F.conv2d(tile_a, k_dsm2, padding=0).squeeze(0) + + # Account for spatial shrinkage since F.conv2d with padding=0 trims edges + c_rows, c_cols = walls_conv.shape[1], walls_conv.shape[2] + + # Align running arrays dynamically to conv output window + z_max_crop = z_max[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] + + chunk_max, chunk_h_local = torch.max(walls_conv, dim=0) + is_new_max = chunk_max >= z_max_crop + + if is_new_max.any(): + # Update local trackers + z_max[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] = torch.where(is_new_max, chunk_max, z_max_crop) + + chunk_h_absolute = chunk_h_local + idx + h_best_crop = h_best[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] + h_best[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] = torch.where(is_new_max, chunk_h_absolute, h_best_crop) + + h_local_unsqueeze = chunk_h_local.unsqueeze(0) + chunk_dsm1 = torch.gather( + dsm_conv1, dim=0, index=h_local_unsqueeze + ).squeeze(0) + chunk_dsm2 = torch.gather( + dsm_conv2, dim=0, index=h_local_unsqueeze + ).squeeze(0) + + dsm_best1_crop = dsm_best1[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] + dsm_best2_crop = dsm_best2[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] + + dsm_best1[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] = torch.where(is_new_max, chunk_dsm1, dsm_best1_crop) + dsm_best2[ + filthalvefloor : filthalvefloor + c_rows, + filthalvefloor : filthalvefloor + c_cols, + ] = torch.where(is_new_max, chunk_dsm2, dsm_best2_crop) + + # Un-pad results to extract the pure valid window of this tile + tile_y = 270.0 - h_best.float() + tile_x = torch.where(dsm_best1 > dsm_best2, 1, 2) + + valid_tile_y = tile_y[ + pad_top : tile_rows - pad_bottom, + pad_left : tile_cols - pad_right, + ] + valid_tile_x = tile_x[ + pad_top : tile_rows - pad_bottom, + pad_left : tile_cols - pad_right, + ] + + # Write back cleanly into the global array without overlap seams + final_y[r_start:r_end, c_start:c_end] = valid_tile_y + final_x[r_start:r_end, c_start:c_end] = valid_tile_x + + # Progress handling + tile_count += 1 + if feedback is not None: + feedback.setProgress( + int((tile_count / total_tiles) * total * 0.9) + ) + + # 6. Global Post-processing calculations + border_mask = torch.zeros((row, col), dtype=torch.bool, device=device) + start = filthalveceil - 1 + end_row = row - filthalveceil - 1 + end_col = col - filthalveceil - 1 + border_mask[start:end_row, start:end_col] = True + + valid_mask = (walls_binary == 1) & border_mask + final_y = torch.where(valid_mask, final_y, torch.zeros_like(final_y)) + final_x = torch.where(valid_mask, final_x, torch.zeros_like(final_x)) + + final_y[final_x == 1] = final_y[final_x == 1] - 180 + final_y[final_y < 0] = final_y[final_y < 0] + 360 + + # Incorporate derivative fallback values for flat results + grad, asp = get_ders(a, scale) + asp_device = ( + torch.from_numpy(asp).to(device) + if not isinstance(asp, torch.Tensor) + else asp.to(device) + ) + + final_y = final_y + ((walls_binary == 1) * 1) * ((final_y == 0) * 1) * ( + asp_device / (math.pi / 180.0) + ) + + if feedback is not None: + feedback.setProgress(int(total)) + + del ( + filtmatrix, + buildfilt, + filtmatrix_list, + buildfilt1_list, + buildfilt2_list, + ) + del all_kernels_walls, all_kernels_dsm1, all_kernels_dsm2 + del ( + final_x, + walls_binary, + a_device, + border_mask, + valid_mask, + grad, + asp, + asp_device, + ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return final_y + + +def cart2pol(x, y, units="deg"): + radius = torch.sqrt(x**2 + y**2) + theta = torch.arctan2(y, x) + if units in ["deg", "degs"]: + theta = theta * 180 / torch.pi + return theta, radius + + +def get_ders(dsm, scale): + # dem,_,_=read_dem_grid(dem_file) + dx = 1 / scale + # dx=0.5 + fy, fx = torch.gradient(dsm, spacing=dx) + asp, grad = cart2pol(fy, fx, "rad") + grad = torch.arctan(grad) + asp = asp * -1 + asp = asp + (asp < 0) * (torch.pi * 2) + return grad, asp diff --git a/lc_update.py b/lc_update.py new file mode 100644 index 0000000..9b37b06 --- /dev/null +++ b/lc_update.py @@ -0,0 +1,19 @@ +import re +from PIL import Image +import rasterio +import numpy as np + +# Open the landcover file +lc_filepath = r"/home/lemap/Documents/suede/datasets/gotenburg/cut2/lc_cut.tif" +lc_filepathout = ( + r"/home/lemap/Documents/suede/datasets/gotenburg/cut2/lc_cut_out.tif" +) +landcover = rasterio.open(lc_filepath) +crs = landcover.profile +lc_data = landcover.read() + +lc_data[lc_data == 4] = 2 +lc_data[lc_data == 3] = 2 + +new_lc = rasterio.open(lc_filepathout, "w", **crs) +new_lc.write(lc_data) diff --git a/plugin_upload.py b/plugin_upload.py index 679d2bc..318175c 100644 --- a/plugin_upload.py +++ b/plugin_upload.py @@ -5,6 +5,13 @@ git sha : $TemplateVCSFormat """ +import defusedxml.xmlrpc + +defusedxml.xmlrpc.monkey_patch() # + +import sys +import getpass +import xmlrpc.client # nosec B411 from optparse import OptionParser import xmlrpc.client # nosec B411 import getpass diff --git a/postprocessor/solwieganalyzer_algorithm.py b/postprocessor/solwieganalyzer_algorithm.py index ca1bbfd..d524270 100644 --- a/postprocessor/solwieganalyzer_algorithm.py +++ b/postprocessor/solwieganalyzer_algorithm.py @@ -21,6 +21,7 @@ ) from qgis.PyQt.QtGui import QIcon from osgeo import gdal +from osgeo.gdalconst import * import os import numpy as np import inspect @@ -355,7 +356,7 @@ def processAlgorithm(self, parameters, context, feedback): # daymean[self.build == 0] = -9999 # self.saveraster(gdal_dsm, self.folderPathSave[0] + '/' + self.var + '_' + - # self.dlg.comboBoxSpecificMean.currentText() + '_mean.tif', daymean) + # self.dlg.comboBoxSpecificMean.currentText() + '_mean.tif', daymean) # if self.dlg.checkBoxIntoCanvas.isChecked(): # self.intoCanvas(self.folderPathSave[0] + '/' + self.var +'_' + self.dlg.comboBoxSpecificMean.currentText() + '_mean.tif') @@ -398,7 +399,7 @@ def processAlgorithm(self, parameters, context, feedback): # daymean[self.build == 0] = -9999 # self.saveraster(gdal_dsm, self.folderPathSave[0] + '/' + self.var + '_' + - # self.dlg.comboBoxSpecificMin.currentText() + '_min.tif', daymean) + # self.dlg.comboBoxSpecificMin.currentText() + '_min.tif', daymean) # if self.dlg.checkBoxIntoCanvas.isChecked(): # self.intoCanvas(self.folderPathSave[0] + '/' + self.var + '_' + self.dlg.comboBoxSpecificMin.currentText() + '_min.tif') diff --git a/postprocessor/spatialtc_algorithm.py b/postprocessor/spatialtc_algorithm.py index d6bc77e..0508da6 100644 --- a/postprocessor/spatialtc_algorithm.py +++ b/postprocessor/spatialtc_algorithm.py @@ -22,6 +22,7 @@ ) from qgis.PyQt.QtGui import QIcon from osgeo import gdal, osr +from osgeo.gdalconst import * import os import numpy as np import pandas as pd @@ -44,15 +45,14 @@ def load_grid(filepath, feedback): try: temp_grid = gdal.Open(filepath) # Open raster with GDAL feedback.setProgressText("Successfully loaded " + filepath) - except BaseException: + except: raise QgsProcessingException( "Error: Could not load " + filepath + ". File does not exist. Check path!" ) - # Return gdal raster layer as numpy array, number of rows and columns in - # raster + # Return gdal raster layer as numpy array, number of rows and columns in raster return ( temp_grid.ReadAsArray().astype(float), temp_grid.ReadAsArray().astype(float).shape[0], @@ -317,8 +317,7 @@ def processAlgorithm(self, parameters, context, feedback): solweig_path = os.path.dirname( os.path.abspath(filepath_tmrt) ) # issue #702 - # solweig_path = filepath_tmrt.split('Tmrt')[0] # Path to SOLWEIG - # output folder, i.e. where Tmrt raster is located + # solweig_path = filepath_tmrt.split('Tmrt')[0] # Path to SOLWEIG output folder, i.e. where Tmrt raster is located _, solweigfile = os.path.split(filepath_tmrt) # solweig_path = os.path.dirname(filepath_tmrt) # issue 31 @@ -328,7 +327,7 @@ def processAlgorithm(self, parameters, context, feedback): filepath_build = solweig_path + "/buildings.tif" gdal_buildings = gdal.Open(filepath_build) build = gdal_buildings.ReadAsArray().astype(float) - except BaseException: + except: raise QgsProcessingException( "Error: No building raster found. It should be located in the same folder as the output" " folder specified when SOLWEIG was executed. Make sure you ticked in 'Save necessary rasters for" @@ -343,7 +342,7 @@ def processAlgorithm(self, parameters, context, feedback): metdata = np.loadtxt( solweig_path + "/metforcing.txt", skiprows=1, delimiter=" " ) - except BaseException: + except: raise QgsProcessingException( "Error: Make sure format of meteorological file is correct. You can " "prepare your data by using 'Prepare Existing Data' in " @@ -372,10 +371,12 @@ def processAlgorithm(self, parameters, context, feedback): # Ws = self.metdata[:, 9] # Derive metdata from Trmt raster name - # int(filepath_tmrt[-18:-14]) #issue 571 - yyyyTmrt = int(solweigfile.split("_")[-3]) - # int(filepath_tmrt[-13:-10]) - doyTmrt = int(solweigfile.split("_")[-2]) + yyyyTmrt = int( + solweigfile.split("_")[-3] + ) # int(filepath_tmrt[-18:-14]) #issue 571 + doyTmrt = int( + solweigfile.split("_")[-2] + ) # int(filepath_tmrt[-13:-10]) hoursTmrt = int(filepath_tmrt[-9:-7]) minuTmrt = int(filepath_tmrt[-7:-5]) @@ -499,8 +500,9 @@ def processAlgorithm(self, parameters, context, feedback): activity = self.parameterAsDouble( parameters, self.ACTIVITY, context ) # Activity in watt - # Sex, #TODO CHECK SO SAME FOR PET AND COMFA - sex = self.parameterAsInt(parameters, self.SEX, context) + 1 + sex = ( + self.parameterAsInt(parameters, self.SEX, context) + 1 + ) # Sex, #TODO CHECK SO SAME FOR PET AND COMFA if tcType == 0: feedback.setProgressText( @@ -537,8 +539,7 @@ def processAlgorithm(self, parameters, context, feedback): ) elif tcType == 2: - # If True = COMFA-kid (Cheng & Brown, 2020), if False = regular - # COMFA + # If True = COMFA-kid (Cheng & Brown, 2020), if False = regular COMFA if comfa_kid: feedback.setProgressText( "Calculating energy balance (COMFA-kid (Cheng and Brown, 2020)) for all ground level pixels" @@ -552,16 +553,13 @@ def processAlgorithm(self, parameters, context, feedback): # Atr = 0.7 # atmospheric transmittance alpha = 0.37 # Albedo of the cylinder emis = 0.95 # Emissivity of the cylinder - # Effective area of body. 0.78 for standing from Campbell and - # Normal (1998) (0.70 for sitting) - Aeff = 0.78 + Aeff = 0.78 # Effective area of body. 0.78 for standing from Campbell and Normal (1998) (0.70 for sitting) L = 0.1 # length of cylinder (cm) D = 0.01 # Diameter of cylinder (cm) ht = self.parameterAsDouble( parameters, self.HEIGHT, context ) # Height of person in cm - # Calculate COMFA radiation using SOLWEIG output L, D, Lin, Lup, - # Kin, Kup, emis, alpha, Aeff, Kd, metdata, location, utc + # Calculate COMFA radiation using SOLWEIG output L, D, Lin, Lup, Kin, Kup, emis, alpha, Aeff, Kd, metdata, location, utc Rabs = COMFA_rad( L, D, @@ -583,14 +581,12 @@ def processAlgorithm(self, parameters, context, feedback): Mact, Mact_PET = COMFA_Mact(mbody, ht, sex, age, activity, "W") # Activity speed va = 0.0 - # Rco: rco = Icl * 186.6. Or convert from conductivity based on 1 - # Clo = 0.1555 (m 2K W-1). + # Rco: rco = Icl * 186.6. Or convert from conductivity based on 1 Clo = 0.1555 (m 2K W-1). rco = clo * 186.6 # Rcvo: Can convert from a conductivity value of Icl, where Icl is found in tables for specific clothing ensembles from ISO (2007). # 1) convert from Icl to Re,cl (m2 kPa W-1): Re,cl = Icl (m 2K W-1)*0.18 (constant from ISO 9920 pg 12 based on 1 or 2-layer clothing ensembles). # 2) Convert Re,cl to rcvo: rcvo = Re,cl*18,400, where 18400 is a conversion factor from Re,cl (vapour resistance, in m2kPaW-1) - # to rcvo, using Lv = 2.5*106 J kg-1, rho = 1.16 kg m-3, and Pa = - # 98kPa. + # to rcvo, using Lv = 2.5*106 J kg-1, rho = 1.16 kg m-3, and Pa = 98kPa. rcvo = clo * 0.18 * 18400 MET = np.zeros((Rabs.shape[0], Rabs.shape[1])) diff --git a/postprocessor/suewsanalyzer_algorithm.py b/postprocessor/suewsanalyzer_algorithm.py index baf5525..be98a70 100644 --- a/postprocessor/suewsanalyzer_algorithm.py +++ b/postprocessor/suewsanalyzer_algorithm.py @@ -231,12 +231,13 @@ def processAlgorithm(self, parameters, context, feedback): self.YYYY = str(startday).split("-")[0] if self.YYYY not in years: - raise QgsProcessingException(f"Selected year '{ - self.YYYY}' is not present in the data.\n Availible years are { - str(years)}") + raise QgsProcessingException( + f"Selected year '{self.YYYY}' is not present in the data.\n Availible years are {str(years)}" + ) - # self.dlg.comboBox_SpatialVariable.currentIndex() - 1 - self.id = int(variaIn) + self.id = int( + variaIn + ) # self.dlg.comboBox_SpatialVariable.currentIndex() - 1 poly_field = idField @@ -249,14 +250,14 @@ def processAlgorithm(self, parameters, context, feedback): statvectemp = [0] statresult = [0] idvec = [0] - # QgsVectorLayer(poly.source(), "polygon", "ogr") - vlayer = inputPolygonlayer + vlayer = inputPolygonlayer # QgsVectorLayer(poly.source(), "polygon", "ogr") prov = vlayer.dataProvider() fields = prov.fields() path = vlayer.dataProvider().dataSourceUri() if path.rfind("|") > 0: - # work around. Probably other solution exists - polygonpath = path[: path.rfind("|")] + polygonpath = path[ + : path.rfind("|") + ] # work around. Probably other solution exists else: polygonpath = path diff --git a/postprocessor/targetanalyzer_algorithm.py b/postprocessor/targetanalyzer_algorithm.py index 598a87d..53164c1 100644 --- a/postprocessor/targetanalyzer_algorithm.py +++ b/postprocessor/targetanalyzer_algorithm.py @@ -43,8 +43,9 @@ # from ..util.umep_uwg_export_component import read_uwg_file try: + from target_py import Target from target_py.ui.utils import read_config -except BaseException: +except: pass @@ -299,11 +300,11 @@ def processAlgorithm(self, parameters, context, feedback): # prov = vlayer.dataProvider() path = vlayer.dataProvider().dataSourceUri() - # polygonpath = path [:path.rfind('|')] # work around. Probably other - # solution exists + # polygonpath = path [:path.rfind('|')] # work around. Probably other solution exists if path.rfind("|") > 0: - # work around. Probably other solution exists - polygonpath = path[: path.rfind("|")] + polygonpath = path[ + : path.rfind("|") + ] # work around. Probably other solution exists else: polygonpath = path @@ -318,8 +319,7 @@ def processAlgorithm(self, parameters, context, feedback): startD = int(startDate.strftime("%j")) endD = int(endDate.strftime("%j")) - # for i in range(0, self.idgrid.shape[0]): # loop over vector grid - # instead + # for i in range(0, self.idgrid.shape[0]): # loop over vector grid instead index = 1 nGrids = vlayer.featureCount() for f in vlayer.getFeatures(): @@ -349,21 +349,24 @@ def processAlgorithm(self, parameters, context, feedback): ending = np.max(np.where(datawhole[:, 1] == endD - 1)) else: ending = np.min(np.where(datawhole[:, 1] == endD)) - # + 12 to include whole final night - data1 = datawhole[start : int(ending + 12), :] + data1 = datawhole[ + start : int(ending + 12), : + ] # + 12 to include whole final night # cut ref data if endD > np.max(dataref[:, 1]): ending = np.max(np.where(dataref[:, 1] == endD - 1)) else: ending = np.min(np.where(dataref[:, 1] == endD)) - # + 12 to include whole final night - data2 = dataref[start : int(ending + 12), :] + data2 = dataref[ + start : int(ending + 12), : + ] # + 12 to include whole final night # Select depending of time of day for ref data if dayTypeStr == "1": - # 14 is position for global radiation - data1 = data1[np.where(data1[:, 14] > 1.0), :] + data1 = data1[ + np.where(data1[:, 14] > 1.0), : + ] # 14 is position for global radiation data1 = data1[0][:] data2 = data2[np.where(data2[:, 14] > 1.0), :] data2 = data2[0][:] diff --git a/postprocessor/treeplanter_algorithm.py b/postprocessor/treeplanter_algorithm.py index 7010140..f72f103 100644 --- a/postprocessor/treeplanter_algorithm.py +++ b/postprocessor/treeplanter_algorithm.py @@ -45,11 +45,14 @@ QgsProcessingParameterDefinition, ) from qgis.PyQt.QtGui import QIcon -from osgeo import osr, ogr +from osgeo import gdal, osr, ogr +from osgeo.gdalconst import * import os import numpy as np import inspect from pathlib import Path +import sys +from ..util import misc # from ..util import RoughnessCalcFunctionV2 as rg # from ..util import imageMorphometricParms_v1 as morph @@ -65,24 +68,26 @@ import datetime # import scipy as sp +from scipy.ndimage import label # Import UMEP tools +from ..util.SEBESOLWEIGCommonFiles.Solweig_v2015_metdata_noload import ( + Solweig_2015a_metdata_noload, +) -# from ..util.SEBESOLWEIGCommonFiles import Solweig_v2015_metdata_noload as metload +from ..util.SEBESOLWEIGCommonFiles.clearnessindex_2013b import ( + clearnessindex_2013b, +) from ..functions.TreeGenerator import makevegdems as makevegdems -# from ..functions.TreePlanter.SOLWEIG.shadowingfunction_wallheight_23 import shadowingfunction_wallheight_23 from ..util.SEBESOLWEIGCommonFiles.shadowingfunction_wallheight_23 import ( shadowingfunction_wallheight_23, ) -# from ..functions.TreePlanter.SOLWEIG1D import Solweig1D_2019a_calc as so -# from ..functions.TreePlanter.SOLWEIG.misc import saveraster from ..util.misc import saveraster -# Import functions and classes for Tree planter from ..functions.TreePlanter.TreePlanter import TreePlanterPrepare from ..functions.TreePlanter.TreePlanter import TreePlanterHillClimber from ..functions.TreePlanter.TreePlanter.TreePlanterClasses import ( @@ -94,14 +99,8 @@ ) from ..functions.TreePlanter.TreePlanter import GreedyAlgorithm -# from ..functions.TreePlanter.SOLWEIG1D.SOLWEIG_1D import tmrt_1d_fun from ..functions.TreePlanter.SOLWEIG1D.SOLWEIG1D_2023a import tmrt_1d_fun -# from ..functions.TreePlanter.treeplanterclasses import Treedata -# from ..functions.TreePlanter.treeplanterclasses import Regional_groups -# from ..functions.TreePlanter.treeplanterclasses import ClippedInputdata -# from ..functions.TreePlanter.treeplanterclasses import Treerasters - class ProcessingTreePlanterAlgorithm(QgsProcessingAlgorithm): """ @@ -248,9 +247,7 @@ def initAlgorithm(self, config): # ) # self.addParameter(QgsProcessingParameterRasterDestination(self.OUTPUT_OCCURRENCE, - # self.tr("Occurrence map (Only available when running with random or - # genetic and more than one tree)"), optional=True, - # createByDefault=False)) + # self.tr("Occurrence map (Only available when running with random or genetic and more than one tree)"), optional=True, createByDefault=False)) self.addParameter( QgsProcessingParameterBoolean( @@ -433,7 +430,7 @@ def processAlgorithm(self, parameters, context, feedback): h_start = h_start + "00" h_end = h_end + "00" - # List shadow and tmrt files in infolder + ## List shadow and tmrt files in infolder sh_fl = [f for f in sorted(glob.glob(infolder + "/Shadow_*.tif"))] tmrt_fl = [f for f in sorted(glob.glob(infolder + "/Tmrt_*.tif"))] @@ -488,12 +485,12 @@ def processAlgorithm(self, parameters, context, feedback): tmrt_1d = np.around(tmrt_1d, decimals=1) # Round Tmrt to one decimal # Create tree in empty matrix - # Y-position of tree in empty setting. Y-position is in the middle of - # Y. - treey = math.ceil(tree_input.rows / 2) - # X-position of tree in empty setting. X-position is in the middle of - # X. - treex = math.ceil(tree_input.cols / 2) + treey = math.ceil( + tree_input.rows / 2 + ) # Y-position of tree in empty setting. Y-position is in the middle of Y. + treex = math.ceil( + tree_input.cols / 2 + ) # X-position of tree in empty setting. X-position is in the middle of X. # Create Treedata class object treedata = Treedata(ttype, height, trunk, dia, treey, treex) @@ -506,10 +503,12 @@ def processAlgorithm(self, parameters, context, feedback): cdsm_ = np.zeros((tree_input.rows, tree_input.cols)) # Empty cdsm tdsm_ = np.zeros((tree_input.rows, tree_input.cols)) # Empty tdsm - # Empty dsm raster - dsm_empty = np.ones((tree_input.rows, tree_input.cols)) - # Empty building raster - buildings_empty = np.ones((tree_input.rows, tree_input.cols)) + dsm_empty = np.ones( + (tree_input.rows, tree_input.cols) + ) # Empty dsm raster + buildings_empty = np.ones( + (tree_input.rows, tree_input.cols) + ) # Empty building raster rowcol = tree_input.rows * tree_input.cols @@ -530,26 +529,29 @@ def processAlgorithm(self, parameters, context, feedback): ) # Create shadows for new tree - # Empty tree bush matrix - treebush = np.zeros((tree_input.rows, tree_input.cols)) + treebush = np.zeros( + (tree_input.rows, tree_input.cols) + ) # Empty tree bush matrix - # Empty tree walls matrix - treewalls = np.zeros((tree_input.rows, tree_input.cols)) - # Empty tree walls direction matrix - treewallsdir = np.zeros((tree_input.rows, tree_input.cols)) + treewalls = np.zeros( + (tree_input.rows, tree_input.cols) + ) # Empty tree walls matrix + treewallsdir = np.zeros( + (tree_input.rows, tree_input.cols) + ) # Empty tree walls direction matrix - # Shade for each timestep, shade = 0 treesh_ts1 = np.zeros( (tree_input.rows, tree_input.cols, r_range.__len__()) - ) - # Shade for each timestep, shade = 1 + ) # Shade for each timestep, shade = 0 treesh_ts2 = np.zeros( (tree_input.rows, tree_input.cols, r_range.__len__()) - ) - # Sum of shade for all timesteps - treesh_sum_sh = np.zeros((tree_input.rows, tree_input.cols)) - # Sum of tmrt for all timesteps - treesh_sum_tmrt = np.zeros((tree_input.rows, tree_input.cols)) + ) # Shade for each timestep, shade = 1 + treesh_sum_sh = np.zeros( + (tree_input.rows, tree_input.cols) + ) # Sum of shade for all timesteps + treesh_sum_tmrt = np.zeros( + (tree_input.rows, tree_input.cols) + ) # Sum of tmrt for all timesteps dem_temp = np.ones((tree_input.rows, tree_input.cols)) @@ -579,7 +581,7 @@ def processAlgorithm(self, parameters, context, feedback): ) i_c += 1 - # Regional groups for tree shadows + ## Regional groups for tree shadows shadow_rg = Regional_groups( r_range, treesh_sum_sh, treesh_ts2, tmrt_1d ) @@ -605,8 +607,7 @@ def processAlgorithm(self, parameters, context, feedback): ) else: # Hill climbing algorithm - # Creating matrices with Tmrt for tree shadows at each possible - # position + # Creating matrices with Tmrt for tree shadows at each possible position treerasters, positions = TreePlanterPrepare.treeplanter( cropped_rasters, treedata, treerasters, tmrt_1d ) diff --git a/postprocessor/urock_analyser_algorithm.py b/postprocessor/urock_analyser_algorithm.py index 4cce548..bc7e5ca 100644 --- a/postprocessor/urock_analyser_algorithm.py +++ b/postprocessor/urock_analyser_algorithm.py @@ -108,8 +108,7 @@ def initAlgorithm(self, config): optional=True, ) ) - # Booleans to let the user decide the type of plotting (stream or - # arrows) + # Booleans to let the user decide the type of plotting (stream or arrows) self.addParameter( QgsProcessingParameterBoolean( self.IS_STREAM, diff --git a/postprocessor/uwganalyzer_algorithm.py b/postprocessor/uwganalyzer_algorithm.py index 3223e34..30a45e0 100644 --- a/postprocessor/uwganalyzer_algorithm.py +++ b/postprocessor/uwganalyzer_algorithm.py @@ -35,7 +35,8 @@ import os import numpy as np import inspect -from qgis.PyQt.QtWidgets import QDateEdit +import sys +from qgis.PyQt.QtWidgets import QDateEdit, QTimeEdit, QMessageBox from pathlib import Path import shutil import datetime @@ -266,11 +267,11 @@ def processAlgorithm(self, parameters, context, feedback): # prov = vlayer.dataProvider() path = vlayer.dataProvider().dataSourceUri() - # polygonpath = path [:path.rfind('|')] # work around. Probably other - # solution exists + # polygonpath = path [:path.rfind('|')] # work around. Probably other solution exists if path.rfind("|") > 0: - # work around. Probably other solution exists - polygonpath = path[: path.rfind("|")] + polygonpath = path[ + : path.rfind("|") + ] # work around. Probably other solution exists else: polygonpath = path @@ -285,8 +286,7 @@ def processAlgorithm(self, parameters, context, feedback): startD = int(startDate.strftime("%j")) endD = int(endDate.strftime("%j")) - # for i in range(0, self.idgrid.shape[0]): # loop over vector grid - # instead + # for i in range(0, self.idgrid.shape[0]): # loop over vector grid instead index = 1 nGrids = vlayer.featureCount() for f in vlayer.getFeatures(): @@ -311,11 +311,13 @@ def processAlgorithm(self, parameters, context, feedback): ending = np.max(np.where(datawhole[:, 1] == endD - 1)) else: ending = np.min(np.where(datawhole[:, 1] == endD)) - # + 12 to include whole final night - data1 = datawhole[start : int(ending + 12), :] + data1 = datawhole[ + start : int(ending + 12), : + ] # + 12 to include whole final night - # include only nighttime. 14 is position for global radiation - data1 = data1[np.where(data1[:, 14] < 1.0), :] + data1 = data1[ + np.where(data1[:, 14] < 1.0), : + ] # include only nighttime. 14 is position for global radiation data1 = data1[0][:] # cut ref data @@ -323,11 +325,13 @@ def processAlgorithm(self, parameters, context, feedback): ending = np.max(np.where(dataref[:, 1] == endD - 1)) else: ending = np.min(np.where(dataref[:, 1] == endD)) - # + 12 to include whole final night - data2 = dataref[start : int(ending + 12), :] + data2 = dataref[ + start : int(ending + 12), : + ] # + 12 to include whole final night - # include only nighttime. 14 is position for global radiation - data2 = data2[np.where(data2[:, 14] < 1.0), :] + data2 = data2[ + np.where(data2[:, 14] < 1.0), : + ] # include only nighttime. 14 is position for global radiation data2 = data2[0][:] # if dayTypeStr == '1': diff --git a/preprocessor/copernicusera5_algorithm.py b/preprocessor/copernicusera5_algorithm.py index a13b3ee..09bbffc 100644 --- a/preprocessor/copernicusera5_algorithm.py +++ b/preprocessor/copernicusera5_algorithm.py @@ -48,7 +48,8 @@ # from processing.gui.wrappers import WidgetWrapper from qgis.PyQt.QtGui import QIcon from osgeo import gdal, osr, ogr -from qgis.PyQt.QtWidgets import QDateEdit +from qgis.PyQt.QtWidgets import QDateEdit, QTimeEdit, QMessageBox +from osgeo.gdalconst import * import os import inspect from pathlib import Path @@ -123,7 +124,7 @@ def processAlgorithm(self, parameters, context, feedback): try: import supy as sp from supy import __version__ as ver_supy - except BaseException: + except: raise QgsProcessingException( "This plugin requires the supy package " "to be installed OR upgraded. Please consult the FAQ in the manual " diff --git a/preprocessor/dsm_generator_algorithm.py b/preprocessor/dsm_generator_algorithm.py index 5c8be10..64abf03 100644 --- a/preprocessor/dsm_generator_algorithm.py +++ b/preprocessor/dsm_generator_algorithm.py @@ -61,10 +61,13 @@ from qgis.analysis import QgsZonalStatistics from qgis.PyQt.QtGui import QIcon from osgeo import gdal, osr, ogr +from osgeo.gdalconst import * import os import numpy as np import inspect from pathlib import Path +import sys +from ..util import misc from ..util.misc import saveraster import requests from ..functions.URock.DataUtil import safe @@ -228,8 +231,7 @@ def processAlgorithm(self, parameters, context, feedback): dem_maxx = dem_extent.xMaximum() dem_minx = dem_extent.xMinimum() - # Coordinate systems of input extent (can be map canvas, drawn - # rectangle or layer) and DEM layer. + # Coordinate systems of input extent (can be map canvas, drawn rectangle or layer) and DEM layer. dem_crs = osr.SpatialReference() crs_temp = demlayer.crs().toWkt() dem_crs.ImportFromWkt(crs_temp) @@ -249,34 +251,46 @@ def processAlgorithm(self, parameters, context, feedback): "POINT (" + str(maxx) + " " + str(maxy) + ")" ) maxext.Transform(transformExtent) - # If smaller than zero = warning #changed to gdal 3 - extent_difference_minx = minext.GetX() - dem_minx - # If smaller than zero = warning #changed to gdal 3 - extent_difference_miny = minext.GetY() - dem_miny - # If larger than zero = warning #changed to gdal 3 - extent_difference_maxx = maxext.GetX() - dem_maxx - # If larger than zero = warning #changed to gdal 3 - extent_difference_maxy = maxext.GetY() - dem_maxy + extent_difference_minx = ( + minext.GetX() - dem_minx + ) # If smaller than zero = warning #changed to gdal 3 + extent_difference_miny = ( + minext.GetY() - dem_miny + ) # If smaller than zero = warning #changed to gdal 3 + extent_difference_maxx = ( + maxext.GetX() - dem_maxx + ) # If larger than zero = warning #changed to gdal 3 + extent_difference_maxy = ( + maxext.GetY() - dem_maxy + ) # If larger than zero = warning #changed to gdal 3 else: input_xymin = transformExtent.TransformPoint(minx, miny) input_xymax = transformExtent.TransformPoint(maxx, maxy) - # If smaller than zero = warning #changed to gdal 2 - extent_difference_minx = input_xymin[0] - dem_minx - # If smaller than zero = warning #changed to gdal 2 - extent_difference_miny = input_xymin[1] - dem_miny - # If larger than zero = warning #changed to gdal 2 - extent_difference_maxx = input_xymax[0] - dem_maxx - # If larger than zero = warning #changed to gdal 2 - extent_difference_maxy = input_xymax[1] - dem_maxy + extent_difference_minx = ( + input_xymin[0] - dem_minx + ) # If smaller than zero = warning #changed to gdal 2 + extent_difference_miny = ( + input_xymin[1] - dem_miny + ) # If smaller than zero = warning #changed to gdal 2 + extent_difference_maxx = ( + input_xymax[0] - dem_maxx + ) # If larger than zero = warning #changed to gdal 2 + extent_difference_maxy = ( + input_xymax[1] - dem_maxy + ) # If larger than zero = warning #changed to gdal 2 else: - # If smaller than zero = warning #changed to gdal 3 - extent_difference_minx = minx - dem_minx - # If smaller than zero = warning #changed to gdal 3 - extent_difference_miny = miny - dem_miny - # If larger than zero = warning #changed to gdal 3 - extent_difference_maxx = maxx - dem_maxx - # If larger than zero = warning #changed to gdal 3 - extent_difference_maxy = maxy - dem_maxy + extent_difference_minx = ( + minx - dem_minx + ) # If smaller than zero = warning #changed to gdal 3 + extent_difference_miny = ( + miny - dem_miny + ) # If smaller than zero = warning #changed to gdal 3 + extent_difference_maxx = ( + maxx - dem_maxx + ) # If larger than zero = warning #changed to gdal 3 + extent_difference_maxy = ( + maxy - dem_maxy + ) # If larger than zero = warning #changed to gdal 3 if ( extent_difference_minx < -0.1 @@ -308,7 +322,6 @@ def processAlgorithm(self, parameters, context, feedback): "Meters", "m", "ft", - # Possible units "US survey foot", "feet", "Feet", @@ -316,7 +329,7 @@ def processAlgorithm(self, parameters, context, feedback): "Foot", "ftUS", "International foot", - ] + ] # Possible units if not dem_unit in possible_units: raise QgsProcessingException( "Error! Raster projection is not in meters or feet. Please reproject." @@ -345,8 +358,7 @@ def processAlgorithm(self, parameters, context, feedback): ) if useOsm: - # Coordinate system of input DEM layer used for creation of OSM - # shapefile + # Coordinate system of input DEM layer used for creation of OSM shapefile ras_crs = osr.SpatialReference() dsm_ref = demlayer.crs().toWkt() ras_crs.ImportFromWkt(dsm_ref) @@ -368,8 +380,7 @@ def processAlgorithm(self, parameters, context, feedback): osm_crs = osr.SpatialReference() osm_crs.ImportFromWkt(wgs84_wkt) - # Transform to convert from input extent coordinate system to Open - # Street Map coordinate system + # Transform to convert from input extent coordinate system to Open Street Map coordinate system transform = osr.CoordinateTransformation(input_crs, osm_crs) gdalver = float(gdal.__version__[0]) @@ -388,8 +399,7 @@ def processAlgorithm(self, parameters, context, feedback): latmin = lonlatmin.GetX() latmax = lonlatmax.GetX() # lonlatmin = transform.TransformPoint(miny, minx) #changed to gdal 3 - # lonlatmax = transform.TransformPoint(maxy, maxx) #changed to - # gdal 3 + # lonlatmax = transform.TransformPoint(maxy, maxx) #changed to gdal 3 else: lonlatmin = transform.TransformPoint(minx, miny) lonlatmax = transform.TransformPoint(maxx, maxy) @@ -473,8 +483,9 @@ def processAlgorithm(self, parameters, context, feedback): driver.DeleteDataSource(temp_dir + "points.shp") osmPolygonPath = temp_dir + "multipolygons.shp" - # Reads temp file made from OSM data - vlayer = QgsVectorLayer(osmPolygonPath, "multipolygons", "ogr") + vlayer = QgsVectorLayer( + osmPolygonPath, "multipolygons", "ogr" + ) # Reads temp file made from OSM data fileInfo = QFileInfo(vlayer.source()) polygon_ln = fileInfo.baseName() @@ -518,21 +529,21 @@ def renameField(srcLayer, oldFieldName, newFieldName): feature.fieldNameIndex("bld_height"), float(str(feature["height"])), ) - except BaseException: + except: counterNone += 1 - # Tries with possible building height data (other tag for - # height??) - elif feature["bld_hght"]: + elif feature[ + "bld_hght" + ]: # Tries with possible building height data (other tag for height??) try: feature.setAttribute( feature.fieldNameIndex("bld_height"), float(str(feature["bld_hght"])), ) - except BaseException: + except: counterNone += 1 - # If no height or building height then make building height - # from stories - elif feature["bld_levels"]: + elif feature[ + "bld_levels" + ]: # If no height or building height then make building height from stories try: temp = ( float(str(feature["bld_levels"])) @@ -541,7 +552,7 @@ def renameField(srcLayer, oldFieldName, newFieldName): feature.setAttribute( feature.fieldNameIndex("bld_height"), temp ) - except BaseException: + except: counterNone += 1 else: counterNone += 1 @@ -551,8 +562,7 @@ def renameField(srcLayer, oldFieldName, newFieldName): counterDiff = counter - counterNone else: - # If not OSM data, input polygon layer with building heights should - # be used + # If not OSM data, input polygon layer with building heights should be used vlayer = shapelayer fileInfo = QFileInfo(vlayer.source()) polygon_ln = fileInfo.baseName() @@ -591,8 +601,7 @@ def renameField(srcLayer, oldFieldName, newFieldName): vlayer.commitChanges() - # Sort vlayer ascending to prevent lower buildings from overwriting - # higher buildings in some complexes + # Sort vlayer ascending to prevent lower buildings from overwriting higher buildings in some complexes sortPoly = temp_dir + "sortPoly.shp" if useOsm: diff --git a/preprocessor/imagemorphparms_algorithm.py b/preprocessor/imagemorphparms_algorithm.py index b2faf8d..ab87ada 100644 --- a/preprocessor/imagemorphparms_algorithm.py +++ b/preprocessor/imagemorphparms_algorithm.py @@ -51,7 +51,8 @@ ) from qgis.PyQt.QtGui import QIcon -from osgeo import gdal, ogr +from osgeo import gdal, ogr, osr +from osgeo.gdalconst import * import os import numpy as np import inspect @@ -362,8 +363,12 @@ def processAlgorithm(self, parameters, context, feedback): feature = layer.GetFeature(0) geom = feature.GetGeometryRef() minX, maxX, minY, maxY = geom.GetEnvelope() - # Reorder bbox to use with gdal_translate - bbox = (minX, maxY, maxX, minY) + bbox = ( + minX, + maxY, + maxX, + minY, + ) # Reorder bbox to use with gdal_translate Vector.Destroy() if useDsmBuild: # Only building heights @@ -635,8 +640,7 @@ def processAlgorithm(self, parameters, context, feedback): if z0all == 0.0: z0all = 0.03 - # If pai is larger than 0 and fai is zero, set fai to 0.001. - # Issue # 164 + # If pai is larger than 0 and fai is zero, set fai to 0.001. Issue # 164 if paiall > 0.0: if faiall == 0.0: faiall = 0.001 @@ -647,14 +651,16 @@ def processAlgorithm(self, parameters, context, feedback): numPixels = len(dsm_array[np.where(dsm_array != nd)]) buildDSM = np.copy(dsm_array) - np.copy(dem_array) buildDSM[buildDSM == nd] = 0 - # building should be higher than 2 meter. Changed to 3 meters - # #784 - buildDSM[(buildDSM < 3.0)] = 0 - # 0.5 meter difference in kernel filter identify a wall - walls = wa.findwalls(buildDSM, 0.5, feedback, total) + buildDSM[(buildDSM < 3.0)] = ( + 0 # building should be higher than 2 meter. Changed to 3 meters #784 + ) + walls = wa.findwalls( + buildDSM, 0.5, feedback, total + ) # 0.5 meter difference in kernel filter identify a wall wallarea = np.sum(walls) - # changed to work for irregular grids - gridArea = numPixels * geotransform[1] * abs(geotransform[5]) + gridArea = ( + numPixels * geotransform[1] * abs(geotransform[5]) + ) # changed to work for irregular grids wai = wallarea / gridArea arr2 = np.array( diff --git a/preprocessor/imagemorphparmspoint_algorithm.py b/preprocessor/imagemorphparmspoint_algorithm.py index e900711..123ae3c 100644 --- a/preprocessor/imagemorphparmspoint_algorithm.py +++ b/preprocessor/imagemorphparmspoint_algorithm.py @@ -53,6 +53,7 @@ from qgis.PyQt.QtGui import QIcon from osgeo import gdal +from osgeo.gdalconst import * import os import numpy as np import inspect @@ -99,8 +100,7 @@ def initAlgorithm(self, config): ) ) # self.addParameter(QgsProcessingParameterBoolean(self.USE_POINTLAYER, - # self.tr("Obtain point of interest from point in vector layer"), - # defaultValue=False)) + # self.tr("Obtain point of interest from point in vector layer"), defaultValue=False)) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_POINTLAYER, @@ -140,8 +140,7 @@ def initAlgorithm(self, config): ) ) # self.addParameter(QgsProcessingParameterBoolean(self.USE_DSMBUILD, - # self.tr("Raster DSM (only 3D building or vegetation objects) exist"), - # defaultValue=False)) + # self.tr("Raster DSM (only 3D building or vegetation objects) exist"), defaultValue=False)) self.addParameter( QgsProcessingParameterRasterLayer( self.INPUT_DSM, @@ -177,8 +176,7 @@ def initAlgorithm(self, config): ) ) # self.addParameter(QgsProcessingParameterBoolean(self.SAVE_POINT, - # self.tr("Save point of interest as new vector layer"), - # defaultValue=False)) + # self.tr("Save point of interest as new vector layer"), defaultValue=False)) self.addParameter( QgsProcessingParameterVectorDestination( self.OUTPUT_POINT, @@ -430,8 +428,7 @@ def processAlgorithm(self, parameters, context, feedback): if z0all < 0.03: z0all = 0.03 - # If pai is larger than 0 and fai is zero, set fai to 0.001. Issue # - # 164 + # If pai is larger than 0 and fai is zero, set fai to 0.001. Issue # 164 if paiall > 0.0: if faiall == 0.0: faiall = 0.001 diff --git a/preprocessor/landcoverfraction_algorithm.py b/preprocessor/landcoverfraction_algorithm.py index f29a141..9c63ca5 100644 --- a/preprocessor/landcoverfraction_algorithm.py +++ b/preprocessor/landcoverfraction_algorithm.py @@ -51,6 +51,7 @@ from qgis.PyQt.QtGui import QIcon from osgeo import gdal, ogr +from osgeo.gdalconst import * import os import numpy as np import inspect @@ -313,16 +314,19 @@ def processAlgorithm(self, parameters, context, feedback): if imid == 1: bbox = (x - r, y + r, x + r, y - r) else: - # Remove gdalwarp cuttoline with gdal.Translate. Cut to - # envelope of polygon feature + # Remove gdalwarp cuttoline with gdal.Translate. Cut to envelope of polygon feature VectorDriver = ogr.GetDriverByName("ESRI Shapefile") Vector = VectorDriver.Open(self.dir_poly, 0) # self.dir_poly layer = Vector.GetLayer() feature = layer.GetFeature(0) geom = feature.GetGeometryRef() minX, maxX, minY, maxY = geom.GetEnvelope() - # Reorder bbox to use with gdal_translate - bbox = (minX, maxY, maxX, minY) + bbox = ( + minX, + maxY, + maxX, + minY, + ) # Reorder bbox to use with gdal_translate Vector.Destroy() # Remove gdalwarp with gdal.Translate diff --git a/preprocessor/landcoverfractionpoint_algorithm.py b/preprocessor/landcoverfractionpoint_algorithm.py index 6affe36..5de413b 100644 --- a/preprocessor/landcoverfractionpoint_algorithm.py +++ b/preprocessor/landcoverfractionpoint_algorithm.py @@ -52,6 +52,7 @@ from qgis.PyQt.QtGui import QIcon from osgeo import gdal +from osgeo.gdalconst import * import os import numpy as np import inspect diff --git a/preprocessor/skyviewfactor_algorithm.py b/preprocessor/skyviewfactor_algorithm.py index 3d12e41..e03a54d 100644 --- a/preprocessor/skyviewfactor_algorithm.py +++ b/preprocessor/skyviewfactor_algorithm.py @@ -30,6 +30,8 @@ __revision__ = "$Format:%H$" +import gc + from qgis.PyQt.QtCore import QCoreApplication, QVariant from qgis.core import ( QgsProcessingAlgorithm, @@ -45,13 +47,23 @@ # from processing.gui.wrappers import WidgetWrapper from qgis.PyQt.QtGui import QIcon from osgeo import gdal +from osgeo.gdalconst import * import os import numpy as np import inspect + +try: + import torch +except ImportError: + torch = None + ipex = None + from pathlib import Path import zipfile import sys from ..util import misc +from ..functions import svf_functions_torch as svf_torch +from ..functions import svf_for_voxels_torch as svfv_torch from ..functions import svf_functions as svf from ..functions import svf_for_voxels as svfv @@ -70,6 +82,8 @@ class ProcessingSkyViewFactorAlgorithm(QgsProcessingAlgorithm): INPUT_THEIGHT = "INPUT_THEIGHT" ANISO = "ANISO" KMEANS = "KMEANS" + USE_GPU = "USE_GPU" + CLUSTERS = "CLUSTERS" WALL_SCHEME = "WALL_SCHEME" INPUT_DEM = "INPUT_DEM" @@ -122,6 +136,16 @@ def initAlgorithm(self, config): maxValue=99.9, ) ) + + # Wall parameterization + self.addParameter( + QgsProcessingParameterBoolean( + self.USE_GPU, + self.tr("Use GPU"), + defaultValue=False, + ), + ) + self.addParameter( QgsProcessingParameterBoolean( self.ANISO, @@ -244,6 +268,7 @@ def processAlgorithm(self, parameters, context, feedback): parameters, self.INPUT_THEIGHT, context ) aniso = self.parameterAsBool(parameters, self.ANISO, context) + use_gpu = self.parameterAsBool(parameters, self.USE_GPU, context) # Wall parameterization settings demlayer = self.parameterAsRasterLayer( @@ -253,10 +278,12 @@ def processAlgorithm(self, parameters, context, feedback): parameters, self.INPUT_SVFHEIGHT, context ) - # If K-means will be used or not (true or false) - kmeans = self.parameterAsBool(parameters, self.KMEANS, context) - # + 1 because ground areas will be one cluster when dsm - dem - clusters = self.parameterAsInt(parameters, self.CLUSTERS, context) + 1 + kmeans = self.parameterAsBool( + parameters, self.KMEANS, context + ) # If K-means will be used or not (true or false) + clusters = ( + self.parameterAsInt(parameters, self.CLUSTERS, context) + 1 + ) # + 1 because ground areas will be one cluster when dsm - dem wallScheme = self.parameterAsBool( parameters, self.WALL_SCHEME, context ) @@ -267,6 +294,44 @@ def processAlgorithm(self, parameters, context, feedback): if not (os.path.isdir(outputDir)): os.mkdir(outputDir) + device = None + + if use_gpu: + # Safely identify if we are dealing with the local mock class + if ( + type(torch).__name__ == "MetaMock" + or getattr(torch, "__name__", "") == "LocalMockTorch" + ): + raise QgsProcessingException( + "\n[UMEP Error] PyTorch is required to run GPU mode.\n" + "Please install it using: pip install torch or with osgeo4w.\n" + "Note: setup for intel GPU require a more complex setup :\n" + "pip install torch --index-url https://download.pytorch.org/whl/xpu" + ) + + if torch.cuda.is_available(): + device = torch.device("cuda") + if feedback is not None: + feedback.setProgressText( + "PyTorch and GPU found. Initiating GPU mode..." + ) + elif hasattr(torch, "xpu") and torch.xpu.is_available(): + device = torch.device("xpu") + if feedback is not None: + feedback.setProgressText( + "PyTorch and GPU found. Initiating GPU mode..." + ) + else: + device = torch.device("cpu") + if feedback is not None: + feedback.setProgressText( + "Pytorch found but GPU not found. Initiating CPU mode..." + ) + else: + # Fall back to standard CPU processing + if feedback is not None: + feedback.setProgressText("Running in CPU mode...") + provider = dsmlayer.dataProvider() filepath_dsm = str(provider.dataSourceUri()) gdal_dsm = gdal.Open(filepath_dsm) @@ -285,9 +350,11 @@ def processAlgorithm(self, parameters, context, feedback): pixel_resolution = geotransform[1] scale = 1 / pixel_resolution + if use_gpu: + dsm = torch.from_numpy(dsm).to(device) + if wallScheme: - # Load DEM layer if calculating exact SVFs for wall surface - # temperature scheme + # Load DEM layer if calculating exact SVFs for wall surface temperature scheme if demlayer: provider = demlayer.dataProvider() filepath_dem = str(provider.dataSourceUri()) @@ -333,10 +400,6 @@ def processAlgorithm(self, parameters, context, feedback): ) if vegdsm2: - # vegdsm2 = self.parameterAsRasterLayer(parameters, self.INPUT_TDSM, context) - # if vegdsm2 is None: - # raise QgsProcessingException("Error: No valid Trunk zone DSM selected") - # load raster gdal.AllRegister() provider = vegdsm2.dataProvider() @@ -358,33 +421,88 @@ def processAlgorithm(self, parameters, context, feedback): rows = dsm.shape[0] cols = dsm.shape[1] vegdsm = np.zeros([rows, cols]) + if use_gpu: + vegdsm = torch.from_numpy(vegdsm).to(device) vegdsm2 = 0.0 usevegdem = 0 if aniso == 1: feedback.setProgressText("Calculating SVF using 153 iterations") - ret = svf.svfForProcessing153( - dsm, - vegdsm, - vegdsm2, - scale, - usevegdem, - pixel_resolution, - wallScheme, - dem, - feedback, - ) + + if use_gpu: + print("gpu version") + + ret = svf_torch.svfForProcessing153( + dsm, + vegdsm, + vegdsm2, + scale, + usevegdem, + pixel_resolution, + wallScheme, + dem, + feedback, + device, + ) + else: + print("cpu version") + ret = svf.svfForProcessing153( + dsm, + vegdsm, + vegdsm2, + scale, + usevegdem, + pixel_resolution, + wallScheme, + dem, + feedback, + ) + else: feedback.setProgressText("Calculating SVF using 655 iterations") - ret = svf.svfForProcessing655( - dsm, vegdsm, vegdsm2, scale, usevegdem, feedback - ) + + if use_gpu: + print("gpu version") + + ret = svf_torch.svfForProcessing655( + dsm, + vegdsm, + vegdsm2, + scale, + usevegdem, + feedback, + device=device, + ) + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + else: + print("cpu version") + ret = svf.svfForProcessing655( + dsm, + vegdsm, + vegdsm2, + scale, + usevegdem, + feedback, + ) + + # Clean up large intermediate tensors after SVF calculation + if use_gpu: + del dsm, vegdsm, vegdsm2 + if dem is not None: + del dem + if device.type == "cuda": + torch.cuda.empty_cache() # print('Time to finish first SVF calculation = ' + str(run_time)) if wallScheme == 1: voxelTable = ret["voxelTable"] - # Remove where wall height is zero, i.e. there is no wall... - voxelTable = voxelTable[voxelTable[:, 2] != 0, :] + voxelTable = voxelTable[ + voxelTable[:, 2] != 0, : + ] # Remove where wall height is zero, i.e. there is no wall... wallHeights = ret["walls"] svfbu = ret["svf"] if usevegdem == 0: @@ -402,7 +520,22 @@ def processAlgorithm(self, parameters, context, feedback): svfbu_array = np.zeros((voxelTable.shape[0])) svfveg_array = np.zeros((voxelTable.shape[0])) svfaveg_array = np.zeros((voxelTable.shape[0])) - voxel_y = np.where(voxelTable[:, 1] == svf_height) + + if use_gpu: + svf_array = torch.from_numpy(svf_array).to(device) + svf_height_array = torch.from_numpy(svf_height_array).to( + device + ) + svfbu_array = torch.from_numpy(svfbu_array).to(device) + svfveg_array = torch.from_numpy(svfveg_array).to(device) + svfaveg_array = torch.from_numpy(svfaveg_array).to(device) + + voxel_y = None + if use_gpu: + voxel_y = torch.where(voxelTable[:, 2] != 0) + else: + voxel_y = np.where(voxelTable[:, 2] != 0) + for temp_y in voxel_y[0]: svf_array[temp_y] = svftotal[ int(voxelTable[temp_y, 5]), int(voxelTable[temp_y, 6]) @@ -418,32 +551,82 @@ def processAlgorithm(self, parameters, context, feedback): ] svf_height_array[temp_y] = svf_height + # Clean up large SVF arrays after extraction + if use_gpu: + del svftotal, svfbu, svfveg, svfaveg, voxel_y + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + if kmeans: - voxelTable, cluster_heights = svfv.svf_kmeans( - dsm, - dem, - vegdsm, - vegdsm2, - wallHeights, - transVeg, - scale, - usevegdem, - pixel_resolution, - voxelTable, - clusters, - svf_height, + + if use_gpu: + print("gpu version") + + voxelTable, cluster_heights = svfv_torch.svf_kmeans( + dsm, + dem, + vegdsm, + vegdsm2, + wallHeights, + transVeg, + scale, + usevegdem, + pixel_resolution, + voxelTable, + clusters, + svf_height, + svf_array, + svfbu_array, + svfveg_array, + svfaveg_array, + svf_height_array, + feedback, + device=device, + ) + + # Interpolate for voxels where SVF has not been calculated + voxelTable = svfv_torch.interpolate_svf(voxelTable) + else: + + voxelTable, cluster_heights = svfv.svf_kmeans( + dsm, + dem, + vegdsm, + vegdsm2, + wallHeights, + transVeg, + scale, + usevegdem, + pixel_resolution, + voxelTable, + clusters, + svf_height, + svf_array, + svfbu_array, + svfveg_array, + svfaveg_array, + svf_height_array, + feedback, + ) + + # Interpolate for voxels where SVF has not been calculated + voxelTable = svfv.interpolate_svf(voxelTable) + + # Clean up k-means result tensors + if use_gpu: + del ( svf_array, svfbu_array, svfveg_array, svfaveg_array, svf_height_array, - feedback, - ) - - # Interpolate for voxels where SVF has not been calculated - voxelTable = svfv.interpolate_svf( - voxelTable, cluster_heights, kmeans ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() # Loop for exact SVF at heights (increase DEM) # if demlayer: @@ -451,25 +634,62 @@ def processAlgorithm(self, parameters, context, feedback): feedback.setProgressText( "Calculating SVF for wall surface temperature parameterization" ) - voxelTable = svfv.svf_for_voxels( - dsm, - dem, - vegdsm, - vegdsm2, - transVeg, - scale, - usevegdem, - pixel_resolution, - voxelTable, - svf_height, - svf_array, - svfbu_array, - svfveg_array, - svfaveg_array, - svf_height_array, - feedback, - ) + if use_gpu: + print("gpu version") + voxelTable = svfv_torch.svf_for_voxels( + dsm, + dem, + vegdsm, + vegdsm2, + transVeg, + scale, + usevegdem, + pixel_resolution, + voxelTable, + svf_height, + svf_array, + svfbu_array, + svfveg_array, + svfaveg_array, + svf_height_array, + feedback, + device=device, + ) + else: + print("cpu version") + voxelTable = svfv.svf_for_voxels( + dsm, + dem, + vegdsm, + vegdsm2, + transVeg, + scale, + usevegdem, + pixel_resolution, + voxelTable, + svf_height, + svf_array, + svfbu_array, + svfveg_array, + svfaveg_array, + svf_height_array, + feedback, + ) + + # Clean up voxel processing result tensors + if use_gpu: + del ( + svf_array, + svfbu_array, + svfveg_array, + svfaveg_array, + svf_height_array, + ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() # Remove rows where svfbu, sfveg and svfaveg is zero if usevegdem == 1: voxelTable = voxelTable[ @@ -501,12 +721,68 @@ def processAlgorithm(self, parameters, context, feedback): svfbuW = ret["svfW"] svfbuN = ret["svfN"] - misc.saveraster(gdal_dsm, outputDir + "/" + "svf.tif", svfbu) - misc.saveraster(gdal_dsm, outputDir + "/" + "svfE.tif", svfbuE) - misc.saveraster(gdal_dsm, outputDir + "/" + "svfS.tif", svfbuS) - misc.saveraster(gdal_dsm, outputDir + "/" + "svfW.tif", svfbuW) - misc.saveraster(gdal_dsm, outputDir + "/" + "svfN.tif", svfbuN) + if use_gpu: + + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svf.tif", + svfbu.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfE.tif", + svfbuE.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfS.tif", + svfbuS.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfW.tif", + svfbuW.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfN.tif", + svfbuN.cpu().detach().numpy(), + ) + + else: + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svf.tif", + svfbu, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfE.tif", + svfbuE, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfS.tif", + svfbuS, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfW.tif", + svfbuW, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfN.tif", + svfbuN, + ) + # Clean up main SVF tensors after saving + if use_gpu: + del svfbuE, svfbuS, svfbuW, svfbuN + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() if os.path.isfile(outputDir + "/" + "svfs.zip"): os.remove(outputDir + "/" + "svfs.zip") @@ -539,37 +815,127 @@ def processAlgorithm(self, parameters, context, feedback): svfWaveg = ret["svfWaveg"] svfNaveg = ret["svfNaveg"] - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfveg.tif", svfveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfEveg.tif", svfEveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfSveg.tif", svfSveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfWveg.tif", svfWveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfNveg.tif", svfNveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfaveg.tif", svfaveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfEaveg.tif", svfEaveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfSaveg.tif", svfSaveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfWaveg.tif", svfWaveg - ) - misc.saveraster( - gdal_dsm, outputDir + "/" + "svfNaveg.tif", svfNaveg - ) + if use_gpu: + + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfveg.tif", + svfveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfEveg.tif", + svfEveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfSveg.tif", + svfSveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfWveg.tif", + svfWveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfNveg.tif", + svfNveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfaveg.tif", + svfaveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfEaveg.tif", + svfEaveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfSaveg.tif", + svfSaveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfWaveg.tif", + svfWaveg.cpu().detach().numpy(), + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfNaveg.tif", + svfNaveg.cpu().detach().numpy(), + ) + else: + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfveg.tif", + svfveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfEveg.tif", + svfEveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfSveg.tif", + svfSveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfWveg.tif", + svfWveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfNveg.tif", + svfNveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfaveg.tif", + svfaveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfEaveg.tif", + svfEaveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfSaveg.tif", + svfSaveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfWaveg.tif", + svfWaveg, + ) + misc.saveraster( + gdal_dsm, + outputDir + "/" + "svfNaveg.tif", + svfNaveg, + ) + # Clean up vegetation SVF tensors after saving + if use_gpu: + del ( + svfEveg, + svfSveg, + svfWveg, + svfNveg, + svfaveg, + svfEaveg, + svfSaveg, + svfWaveg, + svfNaveg, + ) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() zippo = zipfile.ZipFile(outputDir + "/" + "svfs.zip", "a") zippo.write(outputDir + "/" + "svfveg.tif", "svfveg.tif") zippo.write(outputDir + "/" + "svfEveg.tif", "svfEveg.tif") @@ -597,36 +963,88 @@ def processAlgorithm(self, parameters, context, feedback): trans = transVeg / 100.0 svftotal = svfbu - (1 - svfveg) * (1 - trans) - misc.saveraster(gdal_dsm, filename, svftotal) + if use_gpu: + misc.saveraster( + gdal_dsm, filename, svftotal.cpu().detach().numpy() + ) + del svftotal, svfbu, svfveg + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + else: + misc.saveraster(gdal_dsm, filename, svftotal) # Save shadow images for SOLWEIG 2019a if aniso == 1: shmat = ret["shmat"] vegshmat = ret["vegshmat"] vbshvegshmat = ret["vbshvegshmat"] - # wallshmat = ret["wallshmat"] - # wallsunmat = ret["wallsunmat"] - # wallshvemat = ret["wallshvemat"] - # facesunmat = ret["facesunmat"] - - np.savez_compressed( - outputDir + "/" + "shadowmats.npz", - shadowmat=shmat, - vegshadowmat=vegshmat, - vbshmat=vbshvegshmat, - ) # , - # vbshvegshmat=vbshvegshmat, wallshmat=wallshmat, wallsunmat=wallsunmat, - # facesunmat=facesunmat, wallshvemat=wallshvemat) + + if use_gpu: + np.savez_compressed( + outputDir + "/" + "shadowmats.npz", + shadowmat=shmat.cpu().detach().numpy(), + vegshadowmat=vegshmat.cpu().detach().numpy(), + vbshmat=vbshvegshmat.cpu().detach().numpy(), + ) # , + del shmat, vegshmat, vbshvegshmat + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + else: + np.savez_compressed( + outputDir + "/" + "shadowmats.npz", + shadowmat=shmat, + vegshadowmat=vegshmat, + vbshmat=vbshvegshmat, + ) # , if wallScheme == 1: voxelId = ret["voxelIds"] voxelTable = ret["voxelTable"] - np.savez_compressed( - outputDir + "/" + "wallScheme.npz", - voxelId=voxelId, - voxelTable=voxelTable, - ) + if use_gpu: + np.savez_compressed( + outputDir + "/" + "wallScheme.npz", + voxelId=voxelId.cpu().detach().numpy(), + voxelTable=voxelTable.cpu().detach().numpy(), + ) + del voxelId, voxelTable + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + else: + np.savez_compressed( + outputDir + "/" + "wallScheme.npz", + voxelId=voxelId, + voxelTable=voxelTable, + ) + + # Final GPU memory cleanup + if use_gpu: + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + # Aggressive GPU memory cleanup + if use_gpu and torch.cuda.is_available(): + feedback.setProgressText("Clearing GPU memory...") + torch.cuda.synchronize() # Ensure all GPU operations are complete + torch.cuda.empty_cache() # Clear unused GPU memory + torch.cuda.reset_peak_memory_stats() # Reset peak memory tracking + torch.cuda.empty_cache() # Clear again to be sure + gc.collect() # Force Python garbage collection + elif use_gpu and torch.xpu.is_available(): + feedback.setProgressText("Clearing GPU memory...") + torch.xpu.synchronize() # Ensure all GPU operations are complete + torch.xpu.empty_cache() # Clear unused GPU memory + torch.xpu.reset_peak_memory_stats() # Reset peak memory tracking + torch.xpu.empty_cache() # Clear again to be sure + gc.collect() # Force Python garbage collection feedback.setProgressText( "Sky View Factor: SVF grid(s) successfully generated" @@ -654,6 +1072,13 @@ def shortHelpString(self): "radiation emitted (or received) by the entire hemispheric environment (Watson and Johnson 1987). " "It is a dimensionless measure between zero and one, representing totally obstructed and free spaces, " "respectively. The methodology that is used to generate SVF here is described in Lindberg and Grimmond (2010).\n" + "\n" + "Sky View Factor supports GPU acceleration via PyTorch for faster calculations. " + "Performance gains are most significant for high-resolution input data or large geographic areas (over a few km²). " + "It requires a compatible NVIDIA or Intel graphics or AMD (with ROCM) card with up-to-date drivers. " + "If out-of-memory errors occur on large datasets, disable acceleration or tile your input data. " + "The system is fully backward compatible and automatically falls back to CPU if hardware or PyTorch is missing.\n" + "\n" "-------------\n" "Lindberg F, Grimmond CSB (2010) Continuous sky view factor maps from high resolution urban digital elevation models. Clim Res 42:177–183\n" "Watson ID, Johnson GT (1987) Graphical estimation of skyview-factors in urban environments. J Climatol 7: 193–197" diff --git a/preprocessor/targetprepare_algorithm.py b/preprocessor/targetprepare_algorithm.py index 70319ed..c410318 100644 --- a/preprocessor/targetprepare_algorithm.py +++ b/preprocessor/targetprepare_algorithm.py @@ -230,7 +230,7 @@ def processAlgorithm(self, parameters, context, feedback): param["res"]["value"] = res # Start loop of polygon grids - # land cover and morphology + ##land cover and morphology index = 0 for feature in vlayer.getFeatures(): feedback.setProgress(int((index * 100) / nGrids)) @@ -250,13 +250,15 @@ def processAlgorithm(self, parameters, context, feedback): conc = split[1] * fracConc road = split[1] - conc irr = split[5] * fracIrr - # bare soil is classed as dry grass - dry = split[6] + split[5] - irr + dry = ( + split[6] + split[5] - irr + ) # bare soil is classed as dry grass else: road = split[1] conc = split[9] - # bare soil is classed as dry grass - dry = split[5] + split[6] + dry = ( + split[5] + split[6] + ) # bare soil is classed as dry grass irr = split[8] roof = split[2] @@ -303,8 +305,7 @@ def processAlgorithm(self, parameters, context, feedback): arrmatsave = arrmat[1 : arrmat.shape[0], :] print(arrmatsave) - # adding zavg (and z0m later) in parameter file. Weighted avg from - # lc-file + # adding zavg (and z0m later) in parameter file. Weighted avg from lc-file zavg = 0 fracsum = sum(arrmatsave[:, 1]) for i in range(0, index): @@ -312,11 +313,13 @@ def processAlgorithm(self, parameters, context, feedback): param["zavg"]["value"] = zavg - # 'C:/temp/targettests/my_site/parameterstest.json' - jsonout = json.dumps(param, indent=4) + jsonout = json.dumps( + param, indent=4 + ) #'C:/temp/targettests/my_site/parameterstest.json' path = os.path.join(outputDir, siteName) - # create directory if it doesn’t exist. Response to #767 - os.makedirs(path, exist_ok=True) + os.makedirs( + path, exist_ok=True + ) # create directory if it doesn’t exist. Response to #767 with open(path + "/parameters.json", "w") as jsn2: jsn2.write(jsonout) diff --git a/preprocessor/treegenerator_algorithm.py b/preprocessor/treegenerator_algorithm.py index d297571..06cfaae 100644 --- a/preprocessor/treegenerator_algorithm.py +++ b/preprocessor/treegenerator_algorithm.py @@ -30,7 +30,7 @@ __revision__ = "$Format:%H$" -from qgis.PyQt.QtCore import QCoreApplication +from qgis.PyQt.QtCore import QCoreApplication, QVariant from qgis.core import ( QgsProcessing, QgsProcessingAlgorithm, @@ -45,13 +45,16 @@ from qgis.PyQt.QtGui import QIcon from osgeo import gdal, osr +from osgeo.gdalconst import * import os import numpy as np import inspect from pathlib import Path +import sys from ..util.misc import saverasternd from ..functions.TreeGenerator import makevegdems +from ..util import misc class ProcessingTreeGeneratorAlgorithm(QgsProcessingAlgorithm): @@ -307,7 +310,6 @@ def processAlgorithm(self, parameters, context, feedback): "Meters", "m", "ft", - # Possible units "US survey foot", "feet", "Feet", @@ -315,7 +317,7 @@ def processAlgorithm(self, parameters, context, feedback): "Foot", "ftUS", "International foot", - ] + ] # Possible units if not temp_unit in possible_units: raise QgsProcessingException( "Error! Raster data is currently in " @@ -411,8 +413,7 @@ def processAlgorithm(self, parameters, context, feedback): # feedback.setProgressText("rowa= " + str(rowa)) # feedback.setProgressText("rows= " + str(rows)) - # Check if there are trees with a tree canopy diameter smaller than - # the pixel resolution of the input raster data + # Check if there are trees with a tree canopy diameter smaller than the pixel resolution of the input raster data if dia < geotransform[1]: raise QgsProcessingException( "Error! You have tree canopy diameters that are smaller than the pixel resolution." diff --git a/preprocessor/urock_prepare_algorithm.py b/preprocessor/urock_prepare_algorithm.py index 55a8eb4..655eacc 100644 --- a/preprocessor/urock_prepare_algorithm.py +++ b/preprocessor/urock_prepare_algorithm.py @@ -197,12 +197,11 @@ def initAlgorithm(self, config): self.addParameter( QgsProcessingParameterVectorDestination( self.OUTPUT_VEGETATION_FILE, - # ,defaultValue = os.path.join(TEMPO_DIRECTORY, f"veg_vector{OUTPUT_VECTOR_EXTENSION}") self.tr("Output vegetation vector file"), optional=True, createByDefault=False, ) - ) + ) # ,defaultValue = os.path.join(TEMPO_DIRECTORY, f"veg_vector{OUTPUT_VECTOR_EXTENSION}") self.addParameter( QgsProcessingParameterString( self.OUTPUT_VEG_HEIGHT_FIELD, @@ -218,8 +217,7 @@ def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. """ - # Get the tmp directory to save some intermediate results with a known - # file name + # Get the tmp directory to save some intermediate results with a known file name tmp_dir = tempfile.gettempdir() # Get building and vegetation layers @@ -268,7 +266,7 @@ def processAlgorithm(self, parameters, context, feedback): parameters, self.OUTPUT_VEG_HEIGHT_FIELD, context ) - # If output not set, create temporary files for building and vegetation + # If output not set, create temporary files for building and vegetation veg_out_basepath = outputVegFilepath.split(".")[0] veg_out_ext = outputVegFilepath.split(".")[-1].lower() build_out_basepath = outputBuildFilepath.split(".")[0] @@ -277,7 +275,11 @@ def processAlgorithm(self, parameters, context, feedback): outputVegFilepath = os.path.join( tmp_dir, f"veg_vector{OUTPUT_VECTOR_EXTENSION}" ) - elif (veg_out_ext != "geojson") and (veg_out_ext != "shp") and (veg_out_ext != "fgb"): + elif ( + (veg_out_ext != "geojson") + and (veg_out_ext != "shp") + and (veg_out_ext != "fgb") + ): outputVegFilepath = veg_out_basepath + OUTPUT_VECTOR_EXTENSION feedback.pushWarning( f".gpkg format is currently not available, output vegetation file extension has been changed to {OUTPUT_VECTOR_EXTENSION}" @@ -286,18 +288,20 @@ def processAlgorithm(self, parameters, context, feedback): outputBuildFilepath = os.path.join( tmp_dir, f"build_vector{OUTPUT_VECTOR_EXTENSION}" ) - elif (build_out_ext != "geojson") and (build_out_ext != "shp") and (build_out_ext != "fgb"): + elif ( + (build_out_ext != "geojson") + and (build_out_ext != "shp") + and (build_out_ext != "fgb") + ): outputBuildFilepath = build_out_basepath + OUTPUT_VECTOR_EXTENSION feedback.pushWarning( f".gpkg format is currently not available, output building file extension has been changed to {OUTPUT_VECTOR_EXTENSION}" ) # BUILDING LAYER CREATION - # Create the building vector layer if at least building footprint and - # building dsm have been provided + # Create the building vector layer if at least building footprint and building dsm have been provided if inputBuildinglayer and build_dsm: - # Reproject the building DSM to the building vector projection if - # needed + # Reproject the building DSM to the building vector projection if needed srid_vbuild = inputBuildinglayer.crs().postgisSrid() srid_dsm_build = build_dsm.crs().postgisSrid() if srid_vbuild != srid_dsm_build: @@ -323,12 +327,10 @@ def processAlgorithm(self, parameters, context, feedback): "OUTPUT": os.path.join(tmp_dir, "build_dsm"), }, )["OUTPUT"] - # Set file name since they are used in raster calculator - # formula later + # Set file name since they are used in raster calculator formula later build_dsm_fieldname = "build_dsm" else: - # Get file name since they are used in raster calculator - # formula later + # Get file name since they are used in raster calculator formula later build_dsm_filename = ( str(build_dsm.dataProvider().dataSourceUri()) .split(os.sep)[-1] @@ -340,8 +342,7 @@ def processAlgorithm(self, parameters, context, feedback): # Create DSM above ground if a DEM is provided if build_dem: - # Reproject the building DEM to the building vector projection - # if needed + # Reproject the building DEM to the building vector projection if needed srid_dem_build = build_dem.crs().postgisSrid() if srid_vbuild != srid_dem_build: build_dem = processing.run( @@ -366,13 +367,11 @@ def processAlgorithm(self, parameters, context, feedback): "OUTPUT": os.path.join(tmp_dir, "build_dem"), }, )["OUTPUT"] - # Set file name since they are used in raster calculator - # formula later + # Set file name since they are used in raster calculator formula later build_dem_fieldname = "build_dem" else: - # Get file name since they are used in raster calculator - # formula later + # Get file name since they are used in raster calculator formula later build_dem_filename = ( str(build_dem.dataProvider().dataSourceUri()) .split(os.sep)[-1] @@ -382,8 +381,7 @@ def processAlgorithm(self, parameters, context, feedback): "[-0123456789]", "", build_dem_filename )[0:11] - # Calculate the difference between DSM and DEM (set negative - # values to 0) + # Calculate the difference between DSM and DEM (set negative values to 0) diff_expression = '("{0}@1">"{1}@1") * ("{0}@1"-"{1}@1") + ("{0}@1" <= "{1}@1") * 0'.format( build_dsm_fieldname, build_dem_fieldname ) @@ -418,8 +416,7 @@ def processAlgorithm(self, parameters, context, feedback): {"INPUT": inputBuildinglayer, "OUTPUT": "TEMPORARY_OUTPUT"}, )["OUTPUT"] - # Calculate the median height of the DSM within each building - # footprint + # Calculate the median height of the DSM within each building footprint tempoBuildinglayer2 = processing.run( "native:zonalstatisticsfb", { @@ -450,8 +447,7 @@ def processAlgorithm(self, parameters, context, feedback): ) # VEGETATION LAYER CREATION - # Create the vegetation vector layer if vegetation DSM has been - # provided + # Create the vegetation vector layer if vegetation DSM has been provided if veg_dsm and inputVeglayer: raise QgsProcessingException( "A single vegetation input should be provided, either DSM or vector" @@ -577,7 +573,7 @@ def processAlgorithm(self, parameters, context, feedback): then 0 when {0} is null and {1} is not null then {1}*{2} - else {0} + else {0} end """.format( radiusVegField, heightVegField, vegetationAspect @@ -607,8 +603,7 @@ def processAlgorithm(self, parameters, context, feedback): "OUTPUT": "TEMPORARY_OUTPUT", }, )["OUTPUT"] - # Calculates tree height when field do not exists or to - # null (using radius field) + # Calculates tree height when field do not exists or to null (using radius field) outputVegFilepath = processing.run( "native:fieldcalculator", { diff --git a/preprocessor/uwgprepare_algorithm.py b/preprocessor/uwgprepare_algorithm.py index 9813a96..48c7c54 100644 --- a/preprocessor/uwgprepare_algorithm.py +++ b/preprocessor/uwgprepare_algorithm.py @@ -46,6 +46,9 @@ ) from qgis.PyQt.QtGui import QIcon +from osgeo.gdalconst import * +from osgeo import gdal, osr, ogr +import numpy as np import os import inspect from pathlib import Path @@ -215,8 +218,9 @@ def processAlgorithm(self, parameters, context, feedback): path = vlayer.dataProvider().dataSourceUri() if path.rfind("|") > 0: - # work around. Probably other solution exists - polygonpath = path[: path.rfind("|")] + polygonpath = path[ + : path.rfind("|") + ] # work around. Probably other solution exists else: polygonpath = path @@ -235,8 +239,9 @@ def processAlgorithm(self, parameters, context, feedback): vlayerBT = polyBT pathBT = vlayerBT.dataProvider().dataSourceUri() if pathBT.rfind("|") > 0: - # work around. Probably other solution exists - polygonpathBT = pathBT[: pathBT.rfind("|")] + polygonpathBT = pathBT[ + : pathBT.rfind("|") + ] # work around. Probably other solution exists else: polygonpathBT = pathBT @@ -285,7 +290,7 @@ def processAlgorithm(self, parameters, context, feedback): time_field = parin["OVERLAY_FIELDS_PREFIX"] + "uwgTime" # Start loop of polygon grids - # land cover and morphology + ##land cover and morphology index = 0 for feature in vlayer.getFeatures(): feedback.setProgress(int((index * 100) / nGrids)) @@ -325,20 +330,23 @@ def processAlgorithm(self, parameters, context, feedback): break # Populate dict from UMEP - # average building height (m) - uwgDict["bldHeight"] = IMP_heights_mean - # urban area building plan density (0-1) - uwgDict["bldDensity"] = LCF_buildings - # urban area vertical to horizontal ratio - uwgDict["verToHor"] = IMP_wai - # Fraction of the urban ground covered in grass/shrubs only (0-1) - uwgDict["grasscover"] = LCF_grass - # Fraction of the urban ground covered in trees (0-1) + uwgDict["bldHeight"] = ( + IMP_heights_mean # average building height (m) + ) + uwgDict["bldDensity"] = ( + LCF_buildings # urban area building plan density (0-1) + ) + uwgDict["verToHor"] = ( + IMP_wai # urban area vertical to horizontal ratio + ) + uwgDict["grasscover"] = ( + LCF_grass # Fraction of the urban ground covered in grass/shrubs only (0-1) + ) uwgDict["treeCover"] = str( float(LCF_decidious) + float(LCF_evergreen) - ) + ) # Fraction of the urban ground covered in trees (0-1) - # urban type fractions + ## urban type fractions if vlayerBT: fracDict = {} totarea = 0.0 @@ -369,8 +377,6 @@ def processAlgorithm(self, parameters, context, feedback): "Pre80", "Pst80", "Pst80", - # this should also come from an unique post for each - # polygon... "Pst80", "Pst80", "Pst80", @@ -379,7 +385,7 @@ def processAlgorithm(self, parameters, context, feedback): "Pst80", "Pst80", "Pst80", - ] + ] # this should also come from an unique post for each polygon... fractions = [ 0.0, 0.0, @@ -402,8 +408,7 @@ def processAlgorithm(self, parameters, context, feedback): fracDict = dict(zip(types, fractions)) timeDict = dict(zip(types, buildtime)) - # populate dict with area for each available urban type within - # grid + # populate dict with area for each available urban type within grid for featureType in vlayertype.getFeatures(): if feat_id == int(featureType.attribute(poly_field[0])): area = featureType.geometry().area() @@ -425,10 +430,11 @@ def processAlgorithm(self, parameters, context, feedback): uwgDict["bld"][1][i] = timeDict[types[i]] uwgDict["bld"][2][i] = fracDict[types[i]] - # Fraction of the rural ground covered by vegetation - uwgDict["rurVegCover"] = rurVegCover + uwgDict["rurVegCover"] = ( + rurVegCover # Fraction of the rural ground covered by vegetation + ) - # generate input files for UWG + ## generate input files for UWG _name = prefix + "_" + str(feat_id) get_uwg_file(uwgDict, outputDir + "/", _name) diff --git a/preprocessor/wall_heightaspect_algorithm.py b/preprocessor/wall_heightaspect_algorithm.py index 2e852d7..c0ee07e 100644 --- a/preprocessor/wall_heightaspect_algorithm.py +++ b/preprocessor/wall_heightaspect_algorithm.py @@ -30,48 +30,47 @@ __revision__ = "$Format:%H$" +import gc + +import gc + from qgis.PyQt.QtCore import QCoreApplication, QVariant from qgis.core import ( QgsProcessingAlgorithm, + QgsProcessingException, + QgsProcessingParameterBoolean, QgsProcessingParameterNumber, QgsProcessingParameterRasterLayer, QgsProcessingParameterRasterDestination, ) from osgeo import gdal -import numpy as np +from osgeo.gdalconst import * + +try: + import torch +except ImportError: + torch = None + ipex = None + import os +from ..functions import wallalgorithms_torch as wa_torch from ..functions import wallalgorithms as wa + from qgis.PyQt.QtGui import QIcon import inspect from pathlib import Path from ..util.misc import saverasternd - -# def saverasternd(gdal_data, filename, raster): -# rows = gdal_data.RasterYSize -# cols = gdal_data.RasterXSize - -# outDs = gdal.GetDriverByName("GTiff").Create(filename, cols, rows, int(1), GDT_Float32) -# outBand = outDs.GetRasterBand(1) - -# # write the data -# outBand.WriteArray(raster, 0, 0) -# # flush data to disk, set the NoData value and calculate stats -# outBand.FlushCache() -# # outBand.SetNoDataValue(-9999) - -# # georeference the image and set the projection -# outDs.SetGeoTransform(gdal_data.GetGeoTransform()) -# outDs.SetProjection(gdal_data.GetProjection()) +import os class ProcessingWallHeightAscpetAlgorithm(QgsProcessingAlgorithm): INPUT_LIMIT = "INPUT_LIMIT" INPUT = "INPUT" + USE_GPU = "USE_GPU" OUTPUT_HEIGHT = "OUTPUT_HEIGHT" OUTPUT_ASPECT = "OUTPUT_ASPECT" - # ASPECT_BOOL = 'ASPECT_BOOL' def initAlgorithm(self, config): @@ -83,9 +82,7 @@ def initAlgorithm(self, config): False, ) ) - # self.addParameter(QgsProcessingParameterBoolean(self.ASPECT_BOOL, - # self.tr("Calculate wall aspect"), - # defaultValue=True)) + self.addParameter( QgsProcessingParameterNumber( self.INPUT_LIMIT, @@ -95,6 +92,15 @@ def initAlgorithm(self, config): minValue=0.0, ) ) + self.addParameter( + QgsProcessingParameterBoolean( + self.USE_GPU, + self.tr("Use GPU if available"), + None, + False, + ) + ) + self.addParameter( QgsProcessingParameterRasterDestination( self.OUTPUT_HEIGHT, @@ -113,6 +119,7 @@ def initAlgorithm(self, config): ) def processAlgorithm(self, parameters, context, feedback): + outputFileHeight = self.parameterAsOutputLayer( parameters, self.OUTPUT_HEIGHT, context ) @@ -124,16 +131,54 @@ def processAlgorithm(self, parameters, context, feedback): walllimit = self.parameterAsDouble( parameters, self.INPUT_LIMIT, context ) + use_gpu = self.parameterAsBool(parameters, self.USE_GPU, context) cmd_folder = Path( os.path.split(inspect.getfile(inspect.currentframe()))[0] ) feedback.setProgressText(str(cmd_folder)) feedback.setProgressText(str(cmd_folder.parent)) + device = None + + if use_gpu: + + # Safely identify if we are dealing with the local mock class + if ( + type(torch).__name__ == "MetaMock" + or getattr(torch, "__name__", "") == "LocalMockTorch" + ): + raise QgsProcessingException( + "\n[UMEP Error] PyTorch is required to run GPU mode.\n" + "Please install it using: pip install torch or with osgeo4w.\n" + "Note: setup for intel GPU require a more complex setup :\n" + "pip install torch --index-url https://download.pytorch.org/whl/xpu" + ) + + if torch.cuda.is_available(): + device = torch.device("cuda") + if feedback is not None: + + feedback.setProgressText( + "PyTorch and GPU found. Initiating GPU mode..." + ) + elif hasattr(torch, "xpu") and torch.xpu.is_available(): + device = torch.device("xpu") + if feedback is not None: + feedback.setProgressText( + "PyTorch and GPU found. Initiating intel GPU mode..." + ) + else: + device = torch.device("cpu") + if feedback is not None: + + feedback.setProgressText( + "Pytorch found but GPU not found. Initiating CPU mode..." + ) - # feedback.setProgressText(str(parameters["INPUT"])) # this prints to the processing log tab - # QgsMessageLog.logMessage("Testing", "umep", level=Qgis.Info) # This - # prints to a umep tab + else: + # Fall back to standard CPU processing + if feedback is not None: + feedback.setProgressText("Running in CPU mode...") provider = dsmin.dataProvider() filepath_dsm = str(provider.dataSourceUri()) @@ -142,24 +187,73 @@ def processAlgorithm(self, parameters, context, feedback): feedback.setProgressText("Calculating wall height") total = 100.0 / (int(dsm.shape[0] * dsm.shape[1])) - # walls = wa.findwalls(dsm, walllimit, feedback, total) - walls = wa.findwalls_sp(dsm, walllimit, False) - wallssave = np.copy(walls) - # feedback.setProgressText(outputFileHeight) + if use_gpu: + walls = wa_torch.findwalls_sp(dsm, walllimit, device, False) + else: + walls = wa.findwalls_sp(dsm, walllimit, False) + + wallssave = walls + + if use_gpu: + wallssave = wallssave.cpu().detach().numpy() saverasternd(gdal_dsm, outputFileHeight, wallssave) if outputFileAspect: total = 100.0 / 180.0 # outputFileAspect = self.parameterAsOutputLayer(parameters, self.OUTPUT_ASPECT, context) feedback.setProgressText("Calculating wall aspect") - dirwalls = wa.filter1Goodwin_as_aspect_v3( - walls, 1, dsm, feedback, total - ) - saverasternd(gdal_dsm, outputFileAspect, dirwalls) + if use_gpu: + dirwalls = wa_torch.filter1Goodwin_as_aspect_v3( + walls, + torch.tensor(1, device=device), + torch.tensor(dsm, device=device), + feedback, + torch.tensor(total, device=device), + device, + ) + + if use_gpu: + dirwalls = dirwalls.cpu().detach().numpy() + saverasternd(gdal_dsm, outputFileAspect, dirwalls) + else: + + if use_gpu: + dirwalls = wa_torch.filter1Goodwin_as_aspect_v3( + walls, + 1, + dsm, + feedback, + total, + ) + else: + dirwalls = wa.filter1Goodwin_as_aspect_v3( + walls, + 1, + dsm, + feedback, + total, + ) + saverasternd(gdal_dsm, outputFileAspect, dirwalls) else: feedback.setProgressText("Wall aspect not calculated") + # Aggressive GPU memory cleanup + if use_gpu and torch.cuda.is_available(): + feedback.setProgressText("Clearing GPU memory...") + torch.cuda.synchronize() # Ensure all GPU operations are complete + torch.cuda.empty_cache() # Clear unused GPU memory + torch.cuda.reset_peak_memory_stats() # Reset peak memory tracking + torch.cuda.empty_cache() # Clear again to be sure + gc.collect() # Force Python garbage collection + elif use_gpu and torch.xpu.is_available(): + feedback.setProgressText("Clearing GPU memory...") + torch.xpu.synchronize() # Ensure all GPU operations are complete + torch.xpu.empty_cache() # Clear unused GPU memory + torch.xpu.reset_peak_memory_stats() # Reset peak memory tracking + torch.xpu.empty_cache() # Clear again to be sure + gc.collect() # Force Python garbage collection + return { self.OUTPUT_HEIGHT: outputFileHeight, self.OUTPUT_ASPECT: outputFileAspect, diff --git a/processor/configsolweig.ini b/processor/configsolweig.ini index 347305f..af24585 100644 --- a/processor/configsolweig.ini +++ b/processor/configsolweig.ini @@ -6,29 +6,29 @@ #--------------------------------------------------------------------------------------------------------- ####### INPUTS ####### # output path -output_dir=C:/temp/test/ +output_dir=/home/lemap/Documents/suede/datasets/gotenburg/solweig_out/ # working dir -working_dir=C:/temp/test/ +working_dir=/home/lemap/Documents/suede/datasets/gotenburg/ # parameters json file -para_json_path=C:/Users/xlinfr/Documents/PythonScripts/processing_umep/processor/parametersforsolweig.json +para_json_path=/home/lemap/Documents/suede/umep_process_execute/UMEP-processing/processor/parametersforsolweig.json # Input ground and building dsm -filepath_dsm=C:/Users/xlinfr/Documents/PythonScripts/SOLWEIG/SOLWEIGdata/DSM_KRbig.tif +filepath_dsm=/home/lemap/Documents/suede/datasets/gotenburg/dsm_cut.tif # Input vegetation dsm -filepath_cdsm=C:/Users/xlinfr/Documents/PythonScripts/SOLWEIG/SOLWEIGdata/CDSM_KRbig.asc +filepath_cdsm=/home/lemap/Documents/suede/datasets/gotenburg/cdsm_cut.tif # Input trunkzone vegetation dsm filepath_tdsm= # Input Digital Elevation Model -filepath_dem= +filepath_dem=/home/lemap/Documents/suede/datasets/gotenburg/dem_cut.tif # Input Land cover dataset -filepath_lc=C:/Users/xlinfr/Documents/PythonScripts/SOLWEIG/SOLWEIGdata/landcover.tif +filepath_lc=/home/lemap/Documents/suede/datasets/gotenburg/lc_cut_corrected.tif # Input wall height raster -filepath_wh=C:\Users\xlinfr\Desktop\SOLWEIGdata\wallheight.tif +filepath_wh=/home/lemap/Documents/suede/datasets/gotenburg/wallheight.tif # Input wall aspect raster -filepath_wa=C:\Users\xlinfr\Desktop\SOLWEIGdata\wallaspect.tif +filepath_wa=/home/lemap/Documents/suede/datasets/gotenburg/wallaspect.tif # Skyview factor files -input_svf=C:\Users\xlinfr\Desktop\SOLWEIGdata\svfs.zip +input_svf=/home/lemap/Documents/suede/datasets/gotenburg/skyview_out/svfs.zip # Input file for anisotrophic sky -input_aniso=C:\Users\xlinfr\Desktop\SOLWEIGdata\shadowmats.npz +input_aniso= #Point of Interest file for ground poi_file= poi_field= @@ -40,7 +40,7 @@ input_surf=C:\Users\xlinfr\Desktop\SOLWEIGdata\surftemp.txt woi_file= woi_field= # input meteorolgical file (i.e. forcing file) -input_met=C:\Users\xlinfr\Documents\UMEP\TutorialData\Gothenburg\Goteborg_SWEREF99_1200\gbg19970606_2015a.txt +input_met=/home/lemap/Documents/suede/datasets/gotenburg/MetFile20100523_Prepared.txt ## input settings ## # option to execute solweig outside of osgeo/qgis environment @@ -58,8 +58,8 @@ lat=57.7 lon=12.0 # utc time given in meteorological forcing file utc=1 -# scale between horisontal and vertical resolution (1 = 1 meter pixel reolution, 0.5 = 2) -scale=1 +# scale between horizontal and vertical resolution (1 = 1 meter pixel resolution, 0.5 = 2) +scale=0.5 # Set to 1 if an EPW file is used as meteorological forcing data useepwfile=0 # land cover scheme activated (1) (Lindberg et al. 2016 UC) @@ -71,11 +71,12 @@ aniso=0 # use OHM for ground surface temperature modeling groundmodel=1 # compute the outgoing longwave rad using solid angles -outgoingLW=1 +outgoinglongwave=1 # use wall surface temperature scheme (Wallenberg et al. 2025, GMD) wallscheme=0 # If building materials is not included in lc, then this is used for all buildings (Wood, Brick or Concrete) walltype=Brick +# use OHM for ground surface temperature modeling # output settings outputtmrt=1 outputkup=1 @@ -88,6 +89,8 @@ outputkdiff=1 outputtreeplanter=1 wallnetcdf=0 +calculation_mode=cpu + #--------------------------------------------------------------------------------------------------------- # dates - used if an EPW-file is used #--------------------------------------------------------------------------------------------------------- diff --git a/processor/parametersforsolweig.json b/processor/parametersforsolweig.json index 270e4c3..e930d2c 100644 --- a/processor/parametersforsolweig.json +++ b/processor/parametersforsolweig.json @@ -1,362 +1,284 @@ { - "Names": { - "Value": { - "0": "Cobble_stone_2014a", - "1": "Dark_asphalt", - "2": "Roofs(buildings)", - "5": "Grass_unmanaged", - "6": "Bare_soil", - "7": "Water", - "99": "Walls", - "100": "Brick_wall", - "101": "Concrete_wall", - "102": "Wood_wall" - }, - "Comment": "Name of each respective land cover class in land cover data." + "Names": { + "Value": { + "0": "Cobble_stone_2014a", + "1": "Dark_asphalt", + "2": "Roofs(buildings)", + "5": "Grass_unmanaged", + "6": "Bare_soil", + "7": "Water", + "99": "Walls", + "100": "Brick_wall", + "101": "Concrete_wall", + "102": "Wood_wall" }, - "Code": { - "Value": { - "Cobble_stone_2014a": 0, - "Dark_asphalt": 1, - "Roofs(buildings)": 2, - "Grass_unmanaged": 5, - "Bare_soil": 6, - "Water": 7, - "Walls": 99, - "Brick_wall": 100, - "Concrete_wall": 101, - "Wood_wall": 102 - }, - "Comment": "Code for each land cover class name." - }, - "Albedo": { - "Effective": { - "Value": { - "Cobble_stone_2014a": 0.2, - "Dark_asphalt": 0.18, - "Roofs(buildings)": 0.18, - "Grass_unmanaged": 0.16, - "Bare_soil": 0.25, - "Water": 0.05, - "Walls": 0.2 - }, - "Comment": "Effective albedos according to Lindberg et al., 2008; 2016." - }, - "Material": { - "Value": { - "Brick_wall": 0.2, - "Concrete_wall": 0.2, - "Wood_wall": 0.2 - }, - "Comment": "Material albedos according to Wallenberg et al., 2025." - } + "Comment": "Name of each respective land cover class in land cover data." + }, + "Code": { + "Value": { + "Cobble_stone_2014a": 0, + "Dark_asphalt": 1, + "Roofs(buildings)": 2, + "Grass_unmanaged": 5, + "Bare_soil": 6, + "Water": 7, + "Walls": 99, + "Brick_wall": 100, + "Concrete_wall": 101, + "Wood_wall": 102 }, - "Emissivity": { - "Value": { - "Cobble_stone_2014a": 0.95, - "Dark_asphalt": 0.95, - "Roofs(buildings)": 0.95, - "Grass_unmanaged": 0.94, - "Bare_soil": 0.94, - "Water": 0.98, - "Walls": 0.9, - "Brick_wall": 0.9, - "Concrete_wall": 0.9, - "Wood_wall": 0.9 - }, - "Comment": "Emissivity of each land cover class." + "Comment": "Code for each land cover class name." + }, + "Albedo": { + "Effective": { + "Value": { + "Cobble_stone_2014a": 0.2, + "Dark_asphalt": 0.18, + "Roofs(buildings)": 0.18, + "Grass_unmanaged": 0.16, + "Bare_soil": 0.25, + "Water": 0.05, + "Walls": 0.2 + }, + "Comment": "Effective albedos according to Lindberg et al., 2008; 2016." }, - "Specific_heat": { - "Value": { - "Cobble_stone_2014a": -9999.0, - "Dark_asphalt": -9999.0, - "Roofs(buildings)": -9999.0, - "Grass_unmanaged": -9999.0, - "Bare_soil": -9999.0, - "Water": -9999.0, - "Walls": -9999.0, - "Brick_wall": 800, - "Concrete_wall": 840, - "Wood_wall": 1880 - }, - "Comment": "Specific heat capacity, in units J kg-1 K-1, used for wall surface temperatures according to Wallenberg et al. 2025." + "Material": { + "Value": { + "Brick_wall": 0.2, + "Concrete_wall": 0.2, + "Wood_wall": 0.2 + }, + "Comment": "Material albedos according to Wallenberg et al., 2025." + } + }, + "Emissivity": { + "Value": { + "Cobble_stone_2014a": 0.95, + "Dark_asphalt": 0.95, + "Roofs(buildings)": 0.95, + "Grass_unmanaged": 0.94, + "Bare_soil": 0.94, + "Water": 0.98, + "Walls": 0.9, + "Brick_wall": 0.9, + "Concrete_wall": 0.9, + "Wood_wall": 0.9 }, - "Heat capacity": { - "Value": { - "Cobble_stone_2014a": 2110000.0, - "Dark_asphalt": 1940000.0, - "Roofs(buildings)": 1250000.0, - "Grass_unmanaged": 1350000.0, - "Bare_soil": 2000000.0, - "Water": 4180000.0, - "Walls": 1250000.0, - "Brick_wall": -9999.0, - "Concrete_wall": -9999.0, - "Wood_wall": -9999.0 - }, - "Comment": "Heat capacity of each land cover class, in J m-3 K-1, equal to density times specific heat and used in the ground surface temperature scheme" + "Comment": "Emissivity of each land cover class." + }, + "Specific_heat": { + "Value": { + "Cobble_stone_2014a": -9999.0, + "Dark_asphalt": -9999.0, + "Roofs(buildings)": -9999.0, + "Grass_unmanaged": -9999.0, + "Bare_soil": -9999.0, + "Water": -9999.0, + "Walls": -9999.0, + "Brick_wall": 800, + "Concrete_wall": 840, + "Wood_wall": 1880 }, - "Thermal_conductivity": { - "Value": { - "Cobble_stone_2014a": 2, - "Dark_asphalt": 0.83, - "Roofs(buildings)": 1, - "Grass_unmanaged": 1.5, - "Bare_soil": 1.5, - "Water": 1, - "Walls": 1, - "Brick_wall": 0.84, - "Concrete_wall": 1.7, - "Wood_wall": 0.17 - }, - "Comment": "Thermal conductivity of each land cover class, in units W m-1 K-1, used for wall surface temperatures according to Wallenberg et al. 2025." + "Comment": "Specific heat capacity, in units J kg-1 K-1, used for wall surface temperatures according to Wallenberg et al. 2025." + }, + "Heat capacity": { + "Value": { + "Cobble_stone_2014a": 2110000.0, + "Dark_asphalt": 1940000.0, + "Roofs(buildings)": 1250000.0, + "Grass_unmanaged": 1350000.0, + "Bare_soil": 2000000.0, + "Water": 4180000.0, + "Walls": 1250000.0, + "Brick_wall": -9999.0, + "Concrete_wall": -9999.0, + "Wood_wall": -9999.0 }, - "Thermal_diffusivity": { - "Value": { - "Cobble_stone_2014a": 0.00000072, - "Dark_asphalt": 0.00000038, - "Roofs(buildings)": 0.00000005, - "Grass_unmanaged": 0.00000021, - "Bare_soil": 0.00000030, - "Water": 0.0000001, - "Walls": 0.00000005, - "Brick_wall": -9999.0, - "Concrete_wall": -9999.0, - "Wood_wall": -9999.0 - }, - "Comment": "Thermal diffusivity of each land cover class, in , equal to thermal conductivity per density per specific heat and used in the ground surface temperature scheme" + "Comment": "Heat capacity of each land cover class, in J m-3 K-1, equal to density times specific heat and used in the ground surface temperature scheme" + }, + "Thermal_conductivity": { + "Value": { + "Cobble_stone_2014a": 2, + "Dark_asphalt": 0.83, + "Roofs(buildings)": 1, + "Grass_unmanaged": 1.5, + "Bare_soil": 1.5, + "Water": 1, + "Walls": 1, + "Brick_wall": 0.84, + "Concrete_wall": 1.7, + "Wood_wall": 0.17 }, - "Density": { - "Value": { - "Cobble_stone_2014a": -9999.0, - "Dark_asphalt": -9999.0, - "Roofs(buildings)": -9999.0, - "Grass_unmanaged": -9999.0, - "Bare_soil": -9999.0, - "Water": -9999.0, - "Walls": -9999.0, - "Brick_wall": 1700, - "Concrete_wall": 2200, - "Wood_wall": 700 - }, - "Comment": "Density of the material in units kg m-3, used for wall surface temperatures according to Wallenberg et al. 2025." - }, - "Wall_thickness": { - "Value": { - "Brick_wall": 0.1, - "Concrete_wall": 0.2, - "Wood_wall": 0.03 - }, - "Comment": "Wall thickness in units meters, used to calculate characteristic time for wall surface temperatures (Wallenberg et al., 2025)." + "Comment": "Thermal conductivity of each land cover class, in units W m-1 K-1, used for wall surface temperatures according to Wallenberg et al. 2025." + }, + "Thermal_diffusivity": { + "Value": { + "Cobble_stone_2014a": 0.00000072, + "Dark_asphalt": 0.00000038, + "Roofs(buildings)": 0.00000005, + "Grass_unmanaged": 0.00000021, + "Bare_soil": 0.0000003, + "Water": 0.0000001, + "Walls": 0.00000005, + "Brick_wall": -9999.0, + "Concrete_wall": -9999.0, + "Wood_wall": -9999.0 }, - "TmaxLST": { - "Value": { - "Cobble_stone_2014a": 15.0, - "Dark_asphalt": 15.0, - "Roofs(buildings)": 15.0, - "Grass_unmanaged": 14.0, - "Bare_soil": 14.0, - "Water": 12.0, - "Walls": 15.0, - "Brick_wall": -999.0, - "Concrete_wall": -999.0, - "Wood_wall": -999.0 - }, - "Comment": "TmaxLST used for ground surface temperatures and wall surface temperatures according to Lindberg et al. 2008; 2016." + "Comment": "Thermal diffusivity of each land cover class, in , equal to thermal conductivity per density per specific heat and used in the ground surface temperature scheme" + }, + "Density": { + "Value": { + "Cobble_stone_2014a": -9999.0, + "Dark_asphalt": -9999.0, + "Roofs(buildings)": -9999.0, + "Grass_unmanaged": -9999.0, + "Bare_soil": -9999.0, + "Water": -9999.0, + "Walls": -9999.0, + "Brick_wall": 1700, + "Concrete_wall": 2200, + "Wood_wall": 700 }, - "Ts_deg": { - "Value": { - "Cobble_stone_2014a": 0.37, - "Dark_asphalt": 0.58, - "Roofs(buildings)": 0.58, - "Grass_unmanaged": 0.21, - "Bare_soil": 0.33, - "Water": 0.0, - "Walls": 0.37, - "Brick_wall": -999.0, - "Concrete_wall": -999.0, - "Wood_wall": -999.0 - }, - "Comment": "Ts_deg used for ground surface temperatures and wall surface temperatures according to Lindberg et al. 2008; 2016." + "Comment": "Density of the material in units kg m-3, used for wall surface temperatures according to Wallenberg et al. 2025." + }, + "Wall_thickness": { + "Value": { + "Brick_wall": 0.1, + "Concrete_wall": 0.2, + "Wood_wall": 0.03 }, - "Tstart": { - "Value": { - "Cobble_stone_2014a": -3.41, - "Dark_asphalt": -9.78, - "Roofs(buildings)": -9.78, - "Grass_unmanaged": -3.38, - "Bare_soil": -3.01, - "Water": 0.0, - "Walls": -3.41, - "Brick_wall": -999.0, - "Concrete_wall": -999.0, - "Wood_wall": -999.0 - }, - "Comment": "Tstart used for ground surface temperatures and wall surface temperatures according to Lindberg et al. 2008; 2016." - }, - "OHM_coefficients": { - "Values": { - "Cobble_stone_2014a": [ - 0.61, - 1.39, - 0.28, - -23.9 - ], - "Dark_asphalt": [ - 0.50, - 1.39, - 0.28, - -31.45 - ], - "Roofs(buildings)": [ - 0.12, - 1.39, - 0.24, - -4.5 - ], - "Grass_unmanaged": [ - 0.27, - 1.39, - 0.33, - -21.75 - ], - "Bare_soil": [ - 0.3, - 1.39, - 0.44, - -24 - ], - "Water": [ - 0.1, - 1.39, - 0, - -10 - ] - }, - "Comment": "Used in the Objective Hysteresis Model leading to the ground surface temperature the mean of coef a1 (-), its phase (rad), a2 (h-1) and a3 (W m-2) " + "Comment": "Wall thickness in units meters, used to calculate characteristic time for wall surface temperatures (Wallenberg et al., 2025)." + }, + "TmaxLST": { + "Value": { + "Cobble_stone_2014a": 15.0, + "Dark_asphalt": 15.0, + "Roofs(buildings)": 15.0, + "Grass_unmanaged": 14.0, + "Bare_soil": 14.0, + "Water": 12.0, + "Walls": 15.0, + "Brick_wall": -999.0, + "Concrete_wall": -999.0, + "Wood_wall": -999.0 }, - "Tg_ini coefficients": { - "Values": { - "Cobble_stone_2014a": [ - -1.0, - -0.04, - 1.5 - ], - "Dark_asphalt": [ - -1.4, - -0.04, - 0.72 - ], - "Roofs(buildings)": [ - -2.9, - 0, - 0.43 - ], - "Grass_unmanaged": [ - -4.0, - -0.02, - 0.38 - ], - "Bare_soil": [ - -1.6, - -0.02, - 0.51 - ], - "Water": [ - 0, - 0, - 0 - ] - }, - "Comment": "Offset in K, Slope of offset vs latitude, Ratio between amplitude and offset and phase used to model the seasonal pattern of the initial ground surface temperature when compared to the air temperature" + "Comment": "TmaxLST used for ground surface temperatures and wall surface temperatures according to Lindberg et al. 2008; 2016." + }, + "Ts_deg": { + "Value": { + "Cobble_stone_2014a": 0.37, + "Dark_asphalt": 0.58, + "Roofs(buildings)": 0.58, + "Grass_unmanaged": 0.21, + "Bare_soil": 0.33, + "Water": 0.0, + "Walls": 0.37, + "Brick_wall": -999.0, + "Concrete_wall": -999.0, + "Wood_wall": -999.0 }, - "Tm_ini coefficients": { - "Values": { - "Cobble_stone_2014a": [ - -4.5, - -0.15, - 9 - ], - "Dark_asphalt": [ - -5.7, - -0.18, - 10 - ], - "Roofs(buildings)": [ - 0, - 0, - 6 - ], - "Grass_unmanaged": [ - -6.2, - -0.2, - 10 - ], - "Bare_soil": [ - -3.4, - -0.11, - 5 - ], - "Water": [ - 0, - 0, - 0 - ] - }, - "Comment": "Amplitude in K, slope of offset vs latitude in K/deg and offset in K used to model the seasonal pattern of the initial ground surface temperature when compared to the air temperature" + "Comment": "Ts_deg used for ground surface temperatures and wall surface temperatures according to Lindberg et al. 2008; 2016." + }, + "Tstart": { + "Value": { + "Cobble_stone_2014a": -3.41, + "Dark_asphalt": -9.78, + "Roofs(buildings)": -9.78, + "Grass_unmanaged": -3.38, + "Bare_soil": -3.01, + "Water": 0.0, + "Walls": -3.41, + "Brick_wall": -999.0, + "Concrete_wall": -999.0, + "Wood_wall": -999.0 }, - "Tmrt_params": { - "Value": { - "absK": 0.70, - "absL": 0.95, - "posture": "Standing" - }, - "Comment": "Absorption coefficients for mean radiant temperature (Tmrt) and posture. Posture is either standing or sitting." + "Comment": "Tstart used for ground surface temperatures and wall surface temperatures according to Lindberg et al. 2008; 2016." + }, + "OHM_coefficients": { + "Values": { + "Cobble_stone_2014a": [0.61, 1.39, 0.28, -23.9], + "Dark_asphalt": [0.5, 1.39, 0.28, -31.45], + "Roofs(buildings)": [0.12, 1.39, 0.24, -4.5], + "Grass_unmanaged": [0.27, 1.39, 0.33, -21.75], + "Bare_soil": [0.3, 1.39, 0.44, -24], + "Water": [0.1, 1.39, 0, -10] }, - "PET_settings": { - "Value": { - "Age": 35, - "Weight": 75.0, - "Height": 180, - "Sex": "Male", - "Activity": 80.0, - "clo": 0.90 - }, - "Comment": "Settings to calculate Physiological Equivalent Temperature (PET). Sex is either Male or Female." - }, - "Wind_Height": { - "Value": { - "magl": 10.0 - }, - "Comment": "Height of wind sensor for PET and UTCI calculations." + "Comment": "Used in the Objective Hysteresis Model leading to the ground surface temperature the mean of coef a1 (-), its phase (rad), a2 (h-1) and a3 (W m-2) " + }, + "Tg_ini coefficients": { + "Values": { + "Cobble_stone_2014a": [-1.0, -0.04, 1.5], + "Dark_asphalt": [-1.4, -0.04, 0.72], + "Roofs(buildings)": [-2.9, 0, 0.43], + "Grass_unmanaged": [-4.0, -0.02, 0.38], + "Bare_soil": [-1.6, -0.02, 0.51], + "Water": [0, 0, 0] }, - "Tree_settings":{ - "Value":{ - "Transmissivity": 0.03, - "Trunk_ratio": 0.25, - "First_day_leaf": 97, - "Last_day_leaf": 300 - }, - "Comment": "Settings for trees. Shortwave trasmissivity in %. Trunkratio as a fraction of total height" + "Comment": "Offset in K, Slope of offset vs latitude, Ratio between amplitude and offset and phase used to model the seasonal pattern of the initial ground surface temperature when compared to the air temperature" + }, + "Tm_ini coefficients": { + "Values": { + "Cobble_stone_2014a": [-4.5, -0.15, 9], + "Dark_asphalt": [-5.7, -0.18, 10], + "Roofs(buildings)": [0, 0, 6], + "Grass_unmanaged": [-6.2, -0.2, 10], + "Bare_soil": [-3.4, -0.11, 5], + "Water": [0, 0, 0] }, - "Posture": { - "Standing" : { - "Value": { - "Fside" : 0.22, - "Fup" : 0.06, - "height" : 1.1, - "Fcyl" : 0.28 - }, - "Comment": "Standing posture of human body. Used in Tmrt calulations" - }, - "Sitting" : { - "Value":{ - "Fside" : 0.166666, - "Fup" : 0.166666, - "height" : 0.75, - "Fcyl" : 0.2 - }, - "Comment": "Sitting posture of human body. Used in Tmrt calulations" - } - } + "Comment": "Amplitude in K, slope of offset vs latitude in K/deg and offset in K used to model the seasonal pattern of the initial ground surface temperature when compared to the air temperature" + }, + "Tmrt_params": { + "Value": { + "absK": 0.7, + "absL": 0.95, + "posture": "Standing" + }, + "Comment": "Absorption coefficients for mean radiant temperature (Tmrt) and posture. Posture is either standing or sitting." + }, + "PET_settings": { + "Value": { + "Age": 35, + "Weight": 75.0, + "Height": 180, + "Sex": "Male", + "Activity": 80.0, + "clo": 0.9 + }, + "Comment": "Settings to calculate Physiological Equivalent Temperature (PET). Sex is either Male or Female." + }, + "Wind_Height": { + "Value": { + "magl": 10.0 + }, + "Comment": "Height of wind sensor for PET and UTCI calculations." + }, + "Tree_settings": { + "Value": { + "Transmissivity": 0.03, + "Trunk_ratio": 0.25, + "First_day_leaf": 97, + "Last_day_leaf": 300 + }, + "Comment": "Settings for trees. Shortwave trasmissivity in %. Trunkratio as a fraction of total height" + }, + "Posture": { + "Standing": { + "Value": { + "Fside": 0.22, + "Fup": 0.06, + "height": 1.1, + "Fcyl": 0.28 + }, + "Comment": "Standing posture of human body. Used in Tmrt calulations" + }, + "Sitting": { + "Value": { + "Fside": 0.166666, + "Fup": 0.166666, + "height": 0.75, + "Fcyl": 0.2 + }, + "Comment": "Sitting posture of human body. Used in Tmrt calulations" + } + } } diff --git a/processor/sebe_algorithm.py b/processor/sebe_algorithm.py index db1bc6c..2b4f702 100644 --- a/processor/sebe_algorithm.py +++ b/processor/sebe_algorithm.py @@ -308,9 +308,6 @@ def processAlgorithm(self, parameters, context, feedback): if vegdsm: usevegdem = 1 feedback.setProgressText("Vegetation scheme activated") - # vegdsm = self.parameterAsRasterLayer(parameters, self.INPUT_CDSM, context) - # if vegdsm is None: - # raise QgsProcessingException("Error: No valid vegetation DSM selected") # load raster gdal.AllRegister() @@ -328,10 +325,6 @@ def processAlgorithm(self, parameters, context, feedback): ) if vegdsm2: - # vegdsm2 = self.parameterAsRasterLayer(parameters, self.INPUT_TDSM, context) - - # if vegdsm2 is None: - # raise QgsProcessingException("Error: No valid Trunk zone DSM selected") # load raster gdal.AllRegister() @@ -360,8 +353,6 @@ def processAlgorithm(self, parameters, context, feedback): filePath_tdsm = None # wall height layer - # if whlayer is None: - # raise QgsProcessingException("Error: No valid wall height raster layer is selected") provider = whlayer.dataProvider() filepath_wh = str(provider.dataSourceUri()) self.gdal_wh = gdal.Open(filepath_wh) @@ -378,8 +369,6 @@ def processAlgorithm(self, parameters, context, feedback): )[1] # wall aspectlayer - # if walayer is None: - # raise QgsProcessingException("Error: No valid wall aspect raster layer is selected") provider = walayer.dataProvider() filepath_wa = str(provider.dataSourceUri()) self.gdal_wa = gdal.Open(filepath_wa) diff --git a/processor/shadow_generator_algorithm.py b/processor/shadow_generator_algorithm.py index c920a80..8f1d1bb 100644 --- a/processor/shadow_generator_algorithm.py +++ b/processor/shadow_generator_algorithm.py @@ -47,6 +47,7 @@ from qgis.PyQt.QtWidgets import QDateEdit, QTimeEdit import numpy as np from osgeo import gdal, osr +from osgeo.gdalconst import * import os from ..functions import dailyshading as dsh from qgis.PyQt.QtGui import QIcon diff --git a/processor/solweig_algorithm.py b/processor/solweig_algorithm.py index de3fc4e..1078341 100644 --- a/processor/solweig_algorithm.py +++ b/processor/solweig_algorithm.py @@ -34,7 +34,6 @@ from qgis.core import ( QgsProcessing, QgsProcessingAlgorithm, - QgsProcessingParameterString, QgsProcessingParameterBoolean, QgsProcessingParameterNumber, QgsProcessingParameterFolderDestination, @@ -51,6 +50,12 @@ # from qgis.PyQt.QtWidgets import QDateEdit, QTimeEdit import numpy as np +try: + import torch +except ImportError: + torch = None + ipex = None + # import pandas as pd from osgeo import gdal from osgeo.gdalconst import * @@ -60,39 +65,17 @@ from pathlib import Path from ..util.misc import xy2latlon_fromraster import zipfile +import gc -# from ..util.SEBESOLWEIGCommonFiles.Solweig_v2015_metdata_noload import Solweig_2015a_metdata_noload -# from ..util.SEBESOLWEIGCommonFiles import Solweig_v2015_metdata_noload as metload from ..util.umep_solweig_export_component import ( read_solweig_config, write_solweig_config, ) - -# from ..util.SEBESOLWEIGCommonFiles.clearnessindex_2013b import clearnessindex_2013b -# from ..functions.SOLWEIGpython.Tgmaps_v1 import Tgmaps_v1 -# from ..functions.SOLWEIGpython import Solweig_2022a_calc_forprocessing as so -# from ..functions.SOLWEIGpython import WriteMetadataSOLWEIG -# from ..functions.SOLWEIGpython import PET_calculations as p -# from ..functions.SOLWEIGpython import UTCI_calculations as utci -# from ..functions.SOLWEIGpython.CirclePlotBar import PolarBarPlot -# from ..functions.SOLWEIGpython.wall_surface_temperature import load_walls - -# from ..functions.SOLWEIGpython.wallOfInterest import wallOfInterest -# from ..functions.SOLWEIGpython.wallsAsNetCDF import walls_as_netcdf - +from ..functions.SOLWEIGpython import Solweig_run_torch as sr_torch from ..functions.SOLWEIGpython import Solweig_run as sr -# import matplotlib.pyplot as plt import json -# For "Save necessary rasters for TreePlanter tool" -# from shutil import copyfile - -# import time - -# -# import datetime - class ProcessingSOLWEIGAlgorithm(QgsProcessingAlgorithm): """ @@ -159,6 +142,7 @@ class ProcessingSOLWEIGAlgorithm(QgsProcessingAlgorithm): POI_FILE = "POI_FILE" POI_FIELD = "POI_FIELD" CYL = "CYL" + USE_GPU = "USE_GPU" # Output OUTPUT_DIR = "OUTPUT_DIR" @@ -301,7 +285,7 @@ def initAlgorithm(self, config): self.INPUT_DEM, self.tr("Digital Elevation Model (DEM)"), "", - optional=True, + optional=False, ) ) self.addParameter( @@ -642,6 +626,17 @@ def initAlgorithm(self, config): ) self.addParameter(shei) + self.addParameter( + QgsProcessingParameterBoolean( + self.USE_GPU, + self.tr( + "Use GPU for SOLWEIG 2026 calculations (if not ticked, CPU will be used)" + ), + defaultValue=False, + optional=False, + ) + ) + # OUTPUT self.addParameter( QgsProcessingParameterBoolean( @@ -685,6 +680,7 @@ def initAlgorithm(self, config): defaultValue=False, ) ) + self.addParameter( QgsProcessingParameterBoolean( self.OUTPUT_TREEPLANTER, @@ -768,7 +764,7 @@ def processAlgorithm(self, parameters, context, feedback): ) useOutgoingLW = self.parameterAsString( parameters, self.USE_OUTGOINGLW, context - ) + ) groundTempFile = self.parameterAsString( parameters, self.INPUT_GROUNDSCHEME, context ) @@ -840,11 +836,13 @@ def processAlgorithm(self, parameters, context, feedback): outputLdown = self.parameterAsBool( parameters, self.OUTPUT_LDOWN, context ) + + gpu_bool = self.parameterAsBool(parameters, self.USE_GPU, context) + outputTreeplanter = self.parameterAsBool( parameters, self.OUTPUT_TREEPLANTER, context ) outputKdiff = False - # outputSstr = False # If "Save necessary rasters for TreePlanter tool" is ticked, save the following raster for TreePlanter or Spatial TC if outputTreeplanter: @@ -856,7 +854,28 @@ def processAlgorithm(self, parameters, context, feedback): outputSh = True saveBuild = True outputKdiff = True - # outputSstr = True + + calculation_mode = "cpu" + + if gpu_bool and (useGroundScheme == "true" or useOutgoingLW == "true"): + # Safely identify if we are dealing with the local mock class + if ( + type(torch).__name__ == "MetaMock" + or getattr(torch, "__name__", "") == "LocalMockTorch" + ): + raise QgsProcessingException( + "\n[UMEP Error] PyTorch is required to run GPU mode.\n" + "Please install it using: pip install torch or with osgeo4w.\n" + "Note: setup for intel GPU require a more complex setup :\n" + "pip install torch --index-url https://download.pytorch.org/whl/xpu" + ) + + # If PyTorch is available, execute the GPU path + calculation_mode = "gpu" + + else: + # Fall back to standard CPU processing + feedback.setProgressText("Running in CPU mode...") if parameters["OUTPUT_DIR"] == "TEMPORARY_OUTPUT": if not (os.path.isdir(outputDir)): @@ -1192,7 +1211,7 @@ def processAlgorithm(self, parameters, context, feedback): else: feedback.setProgressText("Isotropic sky") anisotropic_sky = 0 - + ### Ground cover scheme # Surface temperature parameterization if useGroundScheme: @@ -1315,6 +1334,7 @@ def processAlgorithm(self, parameters, context, feedback): "outgoingLW": outgoingLW, "wallscheme": wallScheme, "walltype": wall_type, #'Brick_wall', #:TODO + "outgoingLW": outgoingLW, "outputtmrt": int(outputTmrt), "outputkup": int(outputKup), "outputkdown": int(outputKdown), @@ -1325,6 +1345,7 @@ def processAlgorithm(self, parameters, context, feedback): "outputkdiff": int(outputKdiff), "outputtreeplanter": int(outputTreeplanter), "wallnetcdf": int(wallNetCDF), + "calculation_mode": calculation_mode, "date1": "2018,5,1,0", # used in standalone "date2": "2018,8,1,18", # used in standalone } @@ -1339,10 +1360,30 @@ def processAlgorithm(self, parameters, context, feedback): # Main function feedback.setProgressText("Executing main model") - sr.solweig_run(outputDir + "/configsolweig.ini", feedback) + if gpu_bool and (useGroundScheme == "true" or useOutgoingLW == "true"): + + sr_torch.solweig_run(outputDir + "/configsolweig.ini", feedback) + else: + sr.solweig_run(outputDir + "/configsolweig.ini", feedback) feedback.setProgressText("SOLWEIG: Model calculation finished.") + # Aggressive GPU memory cleanup + if gpu_bool and torch.cuda.is_available(): + feedback.setProgressText("Clearing GPU memory...") + torch.cuda.synchronize() # Ensure all GPU operations are complete + torch.cuda.empty_cache() # Clear unused GPU memory + torch.cuda.reset_peak_memory_stats() # Reset peak memory tracking + torch.cuda.empty_cache() # Clear again to be sure + gc.collect() # Force Python garbage collection + elif gpu_bool and torch.xpu.is_available(): + feedback.setProgressText("Clearing GPU memory...") + torch.xpu.synchronize() # Ensure all GPU operations are complete + torch.xpu.empty_cache() # Clear unused GPU memory + torch.xpu.reset_peak_memory_stats() # Reset peak memory tracking + torch.xpu.empty_cache() # Clear again to be sure + gc.collect() # Force Python garbage collection + return {self.OUTPUT_DIR: outputDir} def name(self): @@ -1391,6 +1432,12 @@ def shortHelpString(self): "\n" "Tools to generate sky view factors, wall height and aspect etc. is available in the pre-processing past in UMEP\n" "\n" + "SOLWEIG supports GPU acceleration via PyTorch for faster solar radiation calculations. " + "Performance gains are most significant for high-resolution input data or large geographic areas (over a few km²). " + "It requires a compatible NVIDIA or Intel graphics or AMD (with ROCM) card with up-to-date drivers. " + "If out-of-memory errors occur on large datasets, disable acceleration or tile your input data. " + "The system is fully backward compatible and automatically falls back to CPU if hardware or PyTorch is missing.\n" + "\n" "------------\n" "\n" "Full manual available via the Help-button." diff --git a/processor/solweig_algorithm_old.py b/processor/solweig_algorithm_old.py deleted file mode 100644 index acb5102..0000000 --- a/processor/solweig_algorithm_old.py +++ /dev/null @@ -1,2539 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -/*************************************************************************** - ProcessingUMEP - A QGIS plugin - UMEP for processing toolbox - Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ - ------------------- - begin : 2020-04-02 - copyright : (C) 2020 by Fredrik Lindberg - email : fredrikl@gvc.gu.se - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ -""" - -__author__ = "Fredrik Lindberg" -__date__ = "2020-04-02" -__copyright__ = "(C) 2020 by Fredrik Lindberg" - -# This will get replaced with a git SHA1 when you do a git archive - -__revision__ = "$Format:%H$" - -from qgis.PyQt.QtCore import QCoreApplication, QVariant -from qgis.core import ( - QgsProcessing, - QgsProcessingAlgorithm, - QgsProcessingParameterBoolean, - QgsProcessingParameterNumber, - QgsProcessingParameterFolderDestination, - QgsProcessingParameterFile, - QgsProcessingException, - QgsProcessingParameterDefinition, - QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterField, - QgsProcessingParameterRasterLayer, -) -import numpy as np -import pandas as pd -from osgeo import gdal, osr -import os -from qgis.PyQt.QtGui import QIcon -import inspect -from pathlib import Path -from ..util.misc import saveraster -import zipfile -from ..util.SEBESOLWEIGCommonFiles.Solweig_v2015_metdata_noload import ( - Solweig_2015a_metdata_noload, -) -from ..util.SEBESOLWEIGCommonFiles.clearnessindex_2013b import ( - clearnessindex_2013b, -) -from ..functions.SOLWEIGpython.Tgmaps_v1 import Tgmaps_v1 -from ..functions.SOLWEIGpython import Solweig_2022a_calc_forprocessing as so -from ..functions.SOLWEIGpython import WriteMetadataSOLWEIG -from ..functions.SOLWEIGpython import PET_calculations as p -from ..functions.SOLWEIGpython import UTCI_calculations as utci -from ..functions.SOLWEIGpython.CirclePlotBar import PolarBarPlot -from ..functions.SOLWEIGpython.wall_surface_temperature import load_walls -from ..functions import wallalgorithms as wa -from ..functions.SOLWEIGpython.wallOfInterest import wallOfInterest -from ..functions.SOLWEIGpython.wallsAsNetCDF import walls_as_netcdf - -import matplotlib.pyplot as plt -import json - -# For "Save necessary rasters for TreePlanter tool" -from shutil import copyfile - -# - - -class ProcessingSOLWEIGAlgorithm(QgsProcessingAlgorithm): - """ - This algorithm is a processing version of SOLWEIG - """ - - # Constants used to refer to parameters and outputs. They will be - # used when calling the algorithm from another algorithm, or when - # calling from the QGIS console. - - # Spatial data - INPUT_DSM = "INPUT_DSM" - INPUT_SVF = "INPUT_SVF" - INPUT_CDSM = "INPUT_CDSM" - INPUT_TDSM = "INPUT_TDSM" - INPUT_HEIGHT = "INPUT_HEIGHT" - INPUT_ASPECT = "INPUT_ASPECT" - TRANS_VEG = "TRANS_VEG" - LEAF_START = "LEAF_START" - LEAF_END = "LEAF_END" - CONIFER_TREES = "CONIFER_TREES" - INPUT_THEIGHT = "INPUT_THEIGHT" - INPUT_LC = "INPUT_LC" - USE_LC_BUILD = "USE_LC_BUILD" - INPUT_DEM = "INPUT_DEM" - SAVE_BUILD = "SAVE_BUILD" - INPUT_ANISO = "INPUT_ANISO" - INPUT_WALLSCHEME = "INPUT_WALLSCHEME" - WALLTEMP_NETCDF = "WALLTEMP_NETCDF" - - # Enivornmental parameters - ALBEDO_WALLS = "ALBEDO_WALLS" - ALBEDO_GROUND = "ALBEDO_GROUND" - EMIS_WALLS = "EMIS_WALLS" - EMIS_GROUND = "EMIS_GROUND" - WALL_TYPE = "WALL_TYPE" - - # Tmrt parameters - ABS_S = "ABS_S" - ABS_L = "ABS_L" - POSTURE = "POSTURE" - - # Meteorology - INPUT_MET = "INPUTMET" - ONLYGLOBAL = "ONLYGLOBAL" - UTC = "UTC" - - # PET parameters - AGE = "AGE" - ACTIVITY = "ACTIVITY" - CLO = "CLO" - WEIGHT = "WEIGHT" - HEIGHT = "HEIGHT" - SEX = "SEX" - SENSOR_HEIGHT = "SENSOR_HEIGHT" - - # Optional settings - WOI_FILE = "WOI_FILE" - WOI_FIELD = "WOI_FIELD" - # POI = 'POI' - POI_FILE = "POI_FILE" - POI_FIELD = "POI_FIELD" - CYL = "CYL" - - # Output - OUTPUT_DIR = "OUTPUT_DIR" - OUTPUT_TMRT = "OUTPUT_TMRT" - OUTPUT_LUP = "OUTPUT_LUP" - OUTPUT_KUP = "OUTPUT_KUP" - OUTPUT_KDOWN = "OUTPUT_KDOWN" - OUTPUT_LDOWN = "OUTPUT_LDOWN" - OUTPUT_SH = "OUTPUT_SH" - OUTPUT_TREEPLANTER = "OUTPUT_TREEPLANTER" - - def initAlgorithm(self, config): - # spatial - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_DSM, - self.tr("Building and ground Digital Surface Model (DSM)"), - None, - optional=False, - ) - ) - self.addParameter( - QgsProcessingParameterFile( - self.INPUT_SVF, - self.tr("Sky View Factor grids (.zip)"), - extension="zip", - ) - ) - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_HEIGHT, - self.tr("Wall height raster"), - "", - optional=False, - ) - ) - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_ASPECT, - self.tr("Wall aspect raster"), - "", - optional=False, - ) - ) - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_CDSM, - self.tr("Vegetation Canopy DSM"), - "", - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.TRANS_VEG, - self.tr("Transmissivity of light through vegetation (%):"), - QgsProcessingParameterNumber.Type.Integer, - QVariant(3), - True, - minValue=0, - maxValue=100, - ) - ) - - self.addParameter( - QgsProcessingParameterNumber( - self.LEAF_START, - self.tr( - "First day of year with leaves on trees (if deciduous)" - ), - QgsProcessingParameterNumber.Type.Integer, - QVariant(97), - False, - minValue=0, - maxValue=366, - ) - ) - - self.addParameter( - QgsProcessingParameterNumber( - self.LEAF_END, - self.tr( - "Last day of year with leaves on trees (if deciduous)" - ), - QgsProcessingParameterNumber.Type.Integer, - QVariant(300), - False, - minValue=0, - maxValue=366, - ) - ) - - self.addParameter( - QgsProcessingParameterBoolean( - self.CONIFER_TREES, - self.tr("Coniferous trees (deciduous default)"), - defaultValue=False, - ) - ) - - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_TDSM, - self.tr("Vegetation Trunk-zone DSM"), - "", - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.INPUT_THEIGHT, - self.tr( - "Trunk zone height (percent of Canopy Height). Used if no Vegetation Trunk-zone DSM is loaded" - ), - QgsProcessingParameterNumber.Type.Double, - QVariant(25.0), - optional=True, - minValue=0.1, - maxValue=99.9, - ) - ) - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_LC, - self.tr("UMEP land cover grid"), - "", - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.USE_LC_BUILD, - self.tr("Use land cover grid to derive building grid"), - defaultValue=False, - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_DEM, - self.tr("Digital Elevation Model (DEM)"), - "", - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.SAVE_BUILD, - self.tr("Save generated building grid"), - defaultValue=False, - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterFile( - self.INPUT_ANISO, - self.tr( - "Shadow maps used for anisotropic model for sky diffuse and longwave radiation (.npz)" - ), - extension="npz", - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterFile( - self.INPUT_WALLSCHEME, - self.tr( - "Voxel data for wall temperature parameterization (.npz)" - ), - extension="npz", - optional=True, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.WALLTEMP_NETCDF, - self.tr("Save wall temperatures as NetCDF"), - defaultValue=False, - optional=True, - ) - ) - - # Environmental parameters - # self.addParameter(QgsProcessingParameterNumber(self.EFFUS_WALL, - # self.tr('Wall type (only with wall scheme)'), QgsProcessingParameterNumber.Integer, - # QVariant(1065), False, minValue=0, maxValue=20000)) - self.wallType = ( - (self.tr("Brick"), "0"), - (self.tr("Concrete"), "1"), - (self.tr("Wood"), "2"), - ) - self.addParameter( - QgsProcessingParameterEnum( - self.WALL_TYPE, - self.tr("Wall type (only with wall scheme)"), - options=[i[0] for i in self.wallType], - defaultValue=0, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.ALBEDO_WALLS, - self.tr("Albedo (walls)"), - QgsProcessingParameterNumber.Type.Double, - QVariant(0.20), - False, - minValue=0, - maxValue=1, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.ALBEDO_GROUND, - self.tr("Albedo (ground)"), - QgsProcessingParameterNumber.Type.Double, - QVariant(0.15), - False, - minValue=0, - maxValue=1, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.EMIS_WALLS, - self.tr("Emissivity (walls)"), - QgsProcessingParameterNumber.Type.Double, - QVariant(0.90), - False, - minValue=0, - maxValue=1, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.EMIS_GROUND, - self.tr("Emissivity (ground)"), - QgsProcessingParameterNumber.Type.Double, - QVariant(0.95), - False, - minValue=0, - maxValue=1, - ) - ) - - # Tmrt parameters - self.addParameter( - QgsProcessingParameterNumber( - self.ABS_S, - self.tr("Absorption of shortwave radiation of human body"), - QgsProcessingParameterNumber.Type.Double, - QVariant(0.70), - False, - minValue=0, - maxValue=1, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.ABS_L, - self.tr("Absorption of longwave radiation of human body"), - QgsProcessingParameterNumber.Type.Double, - QVariant(0.95), - False, - minValue=0, - maxValue=1, - ) - ) - self.addParameter( - QgsProcessingParameterEnum( - self.POSTURE, - self.tr("Posture of human body"), - ["Standing", "Sitting"], - defaultValue=0, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.CYL, - self.tr("Consider human as cylinder instead of box"), - defaultValue=True, - ) - ) - - # Meteorology - self.addParameter( - QgsProcessingParameterFile( - self.INPUT_MET, - self.tr("Input meteorological file (.txt)"), - extension="txt", - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.ONLYGLOBAL, - self.tr( - "Estimate diffuse and direct shortwave radiation from global radiation" - ), - defaultValue=False, - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.UTC, - self.tr("Coordinated Universal Time (UTC) "), - QgsProcessingParameterNumber.Type.Integer, - QVariant(0), - False, - minValue=-12, - maxValue=12, - ) - ) - - # ADVANCED PARAMETERS - woifile = QgsProcessingParameterFeatureSource( - self.WOI_FILE, - self.tr( - "Vector point file including Wall of Interest(s) for output with wall surface temperatures" - ), - [QgsProcessing.SourceType.TypeVectorPoint], - optional=True, - ) - woifile.setFlags( - woifile.flags() - | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(woifile) - woi_field = QgsProcessingParameterField( - self.WOI_FIELD, - self.tr("Wall ID field"), - "", - self.WOI_FILE, - QgsProcessingParameterField.DataType.Numeric, - optional=True, - ) - woi_field.setFlags( - woi_field.flags() - | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(woi_field) - - # POIs for thermal comfort estimations - # poi = QgsProcessingParameterBoolean(self.POI, - # self.tr("Include Point of Interest(s) for thermal comfort calculations (PET and UTCI)"), defaultValue=False) - # poi.setFlags(poi.flags() | QgsProcessingParameterDefinition.FlagAdvanced) - # self.addParameter(poi) - poifile = QgsProcessingParameterFeatureSource( - self.POI_FILE, - self.tr( - "Vector point file including Point of Interest(s) for thermal comfort calculations (PET and UTCI)" - ), - [QgsProcessing.SourceType.TypeVectorPoint], - optional=True, - ) - poifile.setFlags( - poifile.flags() - | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(poifile) - poi_field = QgsProcessingParameterField( - self.POI_FIELD, - self.tr("ID field"), - "", - self.POI_FILE, - QgsProcessingParameterField.DataType.Numeric, - optional=True, - ) - poi_field.setFlags( - poi_field.flags() - | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(poi_field) - - # PET parameters - age = QgsProcessingParameterNumber( - self.AGE, - self.tr("Age (yy)"), - QgsProcessingParameterNumber.Type.Integer, - QVariant(35), - optional=True, - minValue=0, - maxValue=120, - ) - age.setFlags( - age.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(age) - act = QgsProcessingParameterNumber( - self.ACTIVITY, - self.tr("Activity (W)"), - QgsProcessingParameterNumber.Type.Double, - QVariant(80), - optional=True, - minValue=0, - maxValue=1000, - ) - act.setFlags( - act.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(act) - clo = QgsProcessingParameterNumber( - self.CLO, - self.tr("Clothing (clo)"), - QgsProcessingParameterNumber.Type.Double, - QVariant(0.9), - optional=True, - minValue=0, - maxValue=10, - ) - clo.setFlags( - clo.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(clo) - wei = QgsProcessingParameterNumber( - self.WEIGHT, - self.tr("Weight (kg)"), - QgsProcessingParameterNumber.Type.Integer, - QVariant(75), - optional=True, - minValue=0, - maxValue=500, - ) - wei.setFlags( - wei.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(wei) - hei = QgsProcessingParameterNumber( - self.HEIGHT, - self.tr("Height (cm)"), - QgsProcessingParameterNumber.Type.Integer, - QVariant(180), - optional=True, - minValue=0, - maxValue=250, - ) - hei.setFlags( - hei.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(hei) - sex = QgsProcessingParameterEnum( - self.SEX, - self.tr("Sex"), - ["Male", "Female"], - optional=True, - defaultValue=0, - ) - sex.setFlags( - sex.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(sex) - shei = QgsProcessingParameterNumber( - self.SENSOR_HEIGHT, - self.tr("Height of wind sensor (m agl)"), - QgsProcessingParameterNumber.Type.Double, - QVariant(10), - optional=True, - minValue=0, - maxValue=250, - ) - shei.setFlags( - shei.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced - ) - self.addParameter(shei) - - # OUTPUT - self.addParameter( - QgsProcessingParameterBoolean( - self.OUTPUT_TMRT, - self.tr("Save Mean Radiant Temperature raster(s)"), - defaultValue=True, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.OUTPUT_KDOWN, - self.tr("Save Incoming shortwave radiation raster(s)"), - defaultValue=False, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.OUTPUT_KUP, - self.tr("Save Outgoing shortwave radiation raster(s)"), - defaultValue=False, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.OUTPUT_LDOWN, - self.tr("Save Incoming longwave radiation raster(s)"), - defaultValue=False, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.OUTPUT_LUP, - self.tr("Save Outgoing longwave radiation raster(s)"), - defaultValue=False, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.OUTPUT_SH, - self.tr("Save shadow raster(s)"), - defaultValue=False, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - self.OUTPUT_TREEPLANTER, - self.tr( - "Save necessary raster(s) for the TreePlanter and Spatial TC tools" - ), - defaultValue=False, - ) - ) - self.addParameter( - QgsProcessingParameterFolderDestination( - self.OUTPUT_DIR, "Output folder" - ) - ) - - self.plugin_dir = os.path.dirname(__file__) - self.temp_dir = os.path.dirname(self.plugin_dir) + "/temp" - - def processAlgorithm(self, parameters, context, feedback): - np.seterr(divide="ignore", invalid="ignore") - - # InputParameters - dsmlayer = self.parameterAsRasterLayer( - parameters, self.INPUT_DSM, context - ) - transVeg = ( - self.parameterAsDouble(parameters, self.TRANS_VEG, context) / 100.0 - ) - firstdayleaf = self.parameterAsInt( - parameters, self.LEAF_START, context - ) - lastdayleaf = self.parameterAsInt(parameters, self.LEAF_END, context) - conifer_bool = self.parameterAsBool( - parameters, self.CONIFER_TREES, context - ) - vegdsm = self.parameterAsRasterLayer( - parameters, self.INPUT_CDSM, context - ) - vegdsm2 = self.parameterAsRasterLayer( - parameters, self.INPUT_TDSM, context - ) - lcgrid = self.parameterAsRasterLayer( - parameters, self.INPUT_LC, context - ) - useLcBuild = self.parameterAsBool( - parameters, self.USE_LC_BUILD, context - ) - dem = None - inputSVF = self.parameterAsString(parameters, self.INPUT_SVF, context) - whlayer = self.parameterAsRasterLayer( - parameters, self.INPUT_HEIGHT, context - ) - walayer = self.parameterAsRasterLayer( - parameters, self.INPUT_ASPECT, context - ) - trunkr = self.parameterAsDouble( - parameters, self.INPUT_THEIGHT, context - ) - onlyglobal = self.parameterAsBool(parameters, self.ONLYGLOBAL, context) - utc = self.parameterAsDouble(parameters, self.UTC, context) - inputMet = self.parameterAsString(parameters, self.INPUT_MET, context) - # usePOI = self.parameterAsBool(parameters, self.POI, context) - poilyr = self.parameterAsVectorLayer( - parameters, self.POI_FILE, context - ) - poi_field = None - mbody = None - ht = None - clo = None - age = None - activity = None - sex = None - sensorheight = None - saveBuild = self.parameterAsBool(parameters, self.SAVE_BUILD, context) - demforbuild = 0 - folderPathPerez = self.parameterAsString( - parameters, self.INPUT_ANISO, context - ) - folderWallScheme = self.parameterAsString( - parameters, self.INPUT_WALLSCHEME, context - ) - wallNetCDF = self.parameterAsBool( - parameters, self.WALLTEMP_NETCDF, context - ) - poisxy = None - poiname = None - - # Wall of interest - woilyr = self.parameterAsVectorLayer( - parameters, self.WOI_FILE, context - ) - woi_field = None - woisxy = None - woiname = None - - # Other parameters # - absK = self.parameterAsDouble(parameters, self.ABS_S, context) - absL = self.parameterAsDouble(parameters, self.ABS_L, context) - pos = self.parameterAsInt(parameters, self.POSTURE, context) - - if self.parameterAsBool(parameters, self.CYL, context): - cyl = 1 - else: - cyl = 0 - - if pos == 0: - Fside = 0.22 - Fup = 0.06 - height = 1.1 - Fcyl = 0.28 - else: - Fside = 0.166666 - Fup = 0.166666 - height = 0.75 - Fcyl = 0.2 - - albedo_b = self.parameterAsDouble( - parameters, self.ALBEDO_WALLS, context - ) - albedo_g = self.parameterAsDouble( - parameters, self.ALBEDO_GROUND, context - ) - ewall = self.parameterAsDouble(parameters, self.EMIS_WALLS, context) - eground = self.parameterAsDouble(parameters, self.EMIS_GROUND, context) - elvis = 0 # option removed 20200907 in processing UMEP - # thermal_effusivity = self.parameterAsDouble(parameters, self.EFFUS_WALL, context) - wall_type = None - - outputDir = self.parameterAsString( - parameters, self.OUTPUT_DIR, context - ) - outputTmrt = self.parameterAsBool( - parameters, self.OUTPUT_TMRT, context - ) - outputSh = self.parameterAsBool(parameters, self.OUTPUT_SH, context) - outputKup = self.parameterAsBool(parameters, self.OUTPUT_KUP, context) - outputKdown = self.parameterAsBool( - parameters, self.OUTPUT_KDOWN, context - ) - outputLup = self.parameterAsBool(parameters, self.OUTPUT_LUP, context) - outputLdown = self.parameterAsBool( - parameters, self.OUTPUT_LDOWN, context - ) - outputTreeplanter = self.parameterAsBool( - parameters, self.OUTPUT_TREEPLANTER, context - ) - outputKdiff = False - # outputSstr = False - - # If "Save necessary rasters for TreePlanter tool" is ticked, save the - # following raster for TreePlanter or Spatial TC - if outputTreeplanter: - outputTmrt = True - outputKup = True - outputKdown = True - outputLup = True - outputLdown = True - outputSh = True - saveBuild = True - outputKdiff = True - # outputSstr = True - - if parameters["OUTPUT_DIR"] == "TEMPORARY_OUTPUT": - if not (os.path.isdir(outputDir)): - os.mkdir(outputDir) - - # Load parameters for SOLWEIG for surface temperatures, etc - data_path = self.plugin_dir + "/parametersforsolweig.json" - with open(data_path, "r") as jsn: - solweig_parameters = json.load(jsn) - - # Add GUI Tmrt settings to SOLWEIG parameter file - solweig_parameters["Tmrt_params"]["Value"]["absK"] = absK - solweig_parameters["Tmrt_params"]["Value"]["absL"] = absL - if pos == 0: - solweig_parameters["Tmrt_params"]["Value"]["posture"] = "Standing" - else: - solweig_parameters["Tmrt_params"]["Value"]["posture"] = "Sitting" - - solweig_parameters["Albedo"]["Effective"]["Value"][ - "Cobble_stone_2014a" - ] = albedo_g - solweig_parameters["Albedo"]["Effective"]["Value"]["Walls"] = albedo_b - solweig_parameters["Emissivity"]["Value"][ - "Cobble_stone_2014a" - ] = eground - solweig_parameters["Emissivity"]["Value"]["Walls"] = ewall - - # Code from old plugin - provider = dsmlayer.dataProvider() - filepath_dsm = str(provider.dataSourceUri()) - gdal_dsm = gdal.Open(filepath_dsm) - dsm = gdal_dsm.ReadAsArray().astype(float) - sizex = dsm.shape[0] - sizey = dsm.shape[1] - rows = dsm.shape[0] - cols = dsm.shape[1] - - # response to issue #85 - nd = gdal_dsm.GetRasterBand(1).GetNoDataValue() - dsm[dsm == nd] = 0.0 - # dsmcopy = np.copy(dsm) - if dsm.min() < 0: - dsmraise = np.abs(dsm.min()) - dsm = dsm + dsmraise - feedback.setProgressText( - "Digital Surface Model (DSM) included negative values. DSM raised with " - + str(dsmraise) - + "m." - ) - else: - dsmraise = 0 - - # Get latlon from grid coordinate system - old_cs = osr.SpatialReference() - dsm_ref = dsmlayer.crs().toWkt() - old_cs.ImportFromWkt(dsm_ref) - - wgs84_wkt = """ - GEOGCS["WGS 84", - DATUM["WGS_1984", - SPHEROID["WGS 84",6378137,298.257223563, - AUTHORITY["EPSG","7030"]], - AUTHORITY["EPSG","6326"]], - PRIMEM["Greenwich",0, - AUTHORITY["EPSG","8901"]], - UNIT["degree",0.01745329251994328, - AUTHORITY["EPSG","9122"]], - AUTHORITY["EPSG","4326"]]""" - - new_cs = osr.SpatialReference() - new_cs.ImportFromWkt(wgs84_wkt) - - transform = osr.CoordinateTransformation(old_cs, new_cs) - widthx = gdal_dsm.RasterXSize - heightx = gdal_dsm.RasterYSize - geotransform = gdal_dsm.GetGeoTransform() - minx = geotransform[0] - miny = ( - geotransform[3] - + widthx * geotransform[4] - + heightx * geotransform[5] - ) - - lonlat = transform.TransformPoint(minx, miny) - gdalver = float(gdal.__version__[0]) - if gdalver == 3.0: - lon = lonlat[1] # changed to gdal 3 - lat = lonlat[0] # changed to gdal 3 - else: - lon = lonlat[0] # changed to gdal 2 - lat = lonlat[1] # changed to gdal 2 - scale = 1 / geotransform[1] - - pixel_resolution = geotransform[1] - - alt = np.median(dsm) - if alt < 0: - alt = 3 - feedback.setProgressText("Longitude derived from DSM: " + str(lon)) - feedback.setProgressText("Latitude derived from DSM: " + str(lat)) - - trunkfile = 0 - trunkratio = 0 - # psi = transVeg / 100.0 - - # if useVegdem: - if vegdsm is not None: - usevegdem = 1 - feedback.setProgressText("Vegetation scheme activated") - - # load raster - gdal.AllRegister() - provider = vegdsm.dataProvider() - filePathOld = str(provider.dataSourceUri()) - dataSet = gdal.Open(filePathOld) - vegdsm = dataSet.ReadAsArray().astype(float) - filePath_cdsm = filePathOld - vegsizex = vegdsm.shape[0] - vegsizey = vegdsm.shape[1] - - if not (vegsizex == sizex) & (vegsizey == sizey): - raise QgsProcessingException( - "Error in Vegetation Canopy DSM: All rasters must be of same extent and resolution" - ) - - if vegdsm2 is not None: - gdal.AllRegister() - provider = vegdsm2.dataProvider() - filePathOld = str(provider.dataSourceUri()) - filePath_tdsm = filePathOld - dataSet = gdal.Open(filePathOld) - vegdsm2 = dataSet.ReadAsArray().astype(float) - else: - trunkratio = trunkr / 100.0 - vegdsm2 = vegdsm * trunkratio - filePath_tdsm = None - - vegsizex = vegdsm2.shape[0] - vegsizey = vegdsm2.shape[1] - - if not (vegsizex == sizex) & (vegsizey == sizey): # & - raise QgsProcessingException( - "Error in Trunk Zone DSM: All rasters must be of same extent and resolution" - ) - else: - vegdsm = np.zeros([rows, cols]) - vegdsm2 = np.zeros([rows, cols]) - usevegdem = 0 - filePath_cdsm = None - filePath_tdsm = None - - # Land cover - if lcgrid is not None: - landcover = 1 - feedback.setProgressText("Land cover scheme activated") - - # load raster - gdal.AllRegister() - provider = lcgrid.dataProvider() - filePath_lc = str(provider.dataSourceUri()) - dataSet = gdal.Open(filePath_lc) - lcgrid = dataSet.ReadAsArray().astype(float) - - lcsizex = lcgrid.shape[0] - lcsizey = lcgrid.shape[1] - - if not (lcsizex == sizex) & (lcsizey == sizey): - raise QgsProcessingException( - "Error in land cover grid: All grids must be of same extent and resolution" - ) - - baddataConifer = lcgrid == 3 - baddataDecid = lcgrid == 4 - if baddataConifer.any(): - raise QgsProcessingException( - "Error in land cover grid: Land cover grid includes Confier land cover class. Ground cover information (underneath canopy) is required." - ) - if baddataDecid.any(): - raise QgsProcessingException( - "Error in land cover grid: Land cover grid includes Decidiuous land cover class. Ground cover information (underneath canopy) is required." - ) - if np.isnan(lcgrid).any(): - raise QgsProcessingException( - "Error in land cover grid: Land cover grid includes NaN values. Use the QGIS Fill NoData cells tool to remove NaN values." - ) - else: - filePath_lc = None - landcover = 0 - lcgrid = 0 - - # DEM # - if not useLcBuild: - demforbuild = 1 - dem = self.parameterAsRasterLayer( - parameters, self.INPUT_DEM, context - ) - - if dem is None: - raise QgsProcessingException("Error: No valid DEM selected") - - # load raster - gdal.AllRegister() - provider = dem.dataProvider() - filePathOld = str(provider.dataSourceUri()) - dataSet = gdal.Open(filePathOld) - dem = dataSet.ReadAsArray().astype(float) - - demsizex = dem.shape[0] - demsizey = dem.shape[1] - - if not (demsizex == sizex) & (demsizey == sizey): - raise QgsProcessingException( - "Error in DEM: All grids must be of same extent and resolution" - ) - - # response to issue and #230 - nd = dataSet.GetRasterBand(1).GetNoDataValue() - dem[dem == nd] = 0.0 - if dem.min() < 0: - demraise = np.abs(dem.min()) - dem = dem + demraise - feedback.setProgressText( - "Digital Evevation Model (DEM) included negative values. DEM raised with " - + str(demraise) - + "m." - ) - else: - demraise = 0 - - alt = np.median(dem) - if alt > 0: - alt = 3.0 - - if (dsmraise != demraise) and (dsmraise - demraise > 0.5): - feedback.setProgressText( - "WARNiNG! DEM and DSM was raised unequally (difference > 0.5 m). Check your input data!" - ) - - # SVFs - zip = zipfile.ZipFile(inputSVF, "r") - zip.extractall(self.temp_dir) - zip.close() - - try: - dataSet = gdal.Open(self.temp_dir + "/svf.tif") - svf = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfN.tif") - svfN = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfS.tif") - svfS = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfE.tif") - svfE = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfW.tif") - svfW = dataSet.ReadAsArray().astype(float) - - if usevegdem == 1: - dataSet = gdal.Open(self.temp_dir + "/svfveg.tif") - svfveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfNveg.tif") - svfNveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfSveg.tif") - svfSveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfEveg.tif") - svfEveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfWveg.tif") - svfWveg = dataSet.ReadAsArray().astype(float) - - dataSet = gdal.Open(self.temp_dir + "/svfaveg.tif") - svfaveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfNaveg.tif") - svfNaveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfSaveg.tif") - svfSaveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfEaveg.tif") - svfEaveg = dataSet.ReadAsArray().astype(float) - dataSet = gdal.Open(self.temp_dir + "/svfWaveg.tif") - svfWaveg = dataSet.ReadAsArray().astype(float) - else: - svfveg = np.ones((rows, cols)) - svfNveg = np.ones((rows, cols)) - svfSveg = np.ones((rows, cols)) - svfEveg = np.ones((rows, cols)) - svfWveg = np.ones((rows, cols)) - svfaveg = np.ones((rows, cols)) - svfNaveg = np.ones((rows, cols)) - svfSaveg = np.ones((rows, cols)) - svfEaveg = np.ones((rows, cols)) - svfWaveg = np.ones((rows, cols)) - except BaseException: - raise QgsProcessingException( - "SVF import error: The zipfile including the SVFs seems corrupt. Retry calcualting the SVFs in the Pre-processor or choose another file." - ) - - svfsizex = svf.shape[0] - svfsizey = svf.shape[1] - - if not (svfsizex == sizex) & (svfsizey == sizey): # & - raise QgsProcessingException( - "Error in svf rasters: All grids must be of same extent and resolution" - ) - - tmp = svf + svfveg - 1.0 - tmp[tmp < 0.0] = 0.0 - # %matlab crazyness around 0 - svfalfa = np.arcsin(np.exp((np.log((1.0 - tmp)) / 2.0))) - - feedback.setProgressText("Sky View Factor rasters loaded") - - # wall height layer - if whlayer is None: - raise QgsProcessingException( - "Error: No valid wall height raster layer is selected" - ) - provider = whlayer.dataProvider() - filepath_wh = str(provider.dataSourceUri()) - self.gdal_wh = gdal.Open(filepath_wh) - wallheight = self.gdal_wh.ReadAsArray().astype(float) - vhsizex = wallheight.shape[0] - vhsizey = wallheight.shape[1] - if not (vhsizex == sizex) & (vhsizey == sizey): - raise QgsProcessingException( - "Error in Wall height raster: All rasters must be of same extent and resolution" - ) - - # wall aspectlayer - if walayer is None: - raise QgsProcessingException( - "Error: No valid wall aspect raster layer is selected" - ) - provider = walayer.dataProvider() - filepath_wa = str(provider.dataSourceUri()) - self.gdal_wa = gdal.Open(filepath_wa) - wallaspect = self.gdal_wa.ReadAsArray().astype(float) - vasizex = wallaspect.shape[0] - vasizey = wallaspect.shape[1] - if not (vasizex == sizex) & (vasizey == sizey): - raise QgsProcessingException( - "Error in Wall aspect raster: All rasters must be of same extent and resolution" - ) - - # Metdata - headernum = 1 - delim = " " - Twater = [] - - try: - self.metdata = np.loadtxt( - inputMet, skiprows=headernum, delimiter=delim - ) - metfileexist = 1 - except BaseException: - raise QgsProcessingException( - "Error: Make sure format of meteorological file is correct. You can" - "prepare your data by using 'Prepare Existing Data' in " - "the Pre-processor" - ) - - testwhere = np.where( - (self.metdata[:, 14] < 0.0) | (self.metdata[:, 14] > 1300.0) - ) - if testwhere[0].__len__() > 0: - raise QgsProcessingException( - "Error: Kdown - beyond what is expected at line: " - + str(testwhere[0] + 1) - ) - - if self.metdata.shape[1] == 24: - feedback.setProgressText("Meteorological data successfully loaded") - else: - raise QgsProcessingException( - "Error: Wrong number of columns in meteorological data. You can " - "prepare your data by using 'Prepare Existing Data' in " - "the Pre-processor" - ) - - feedback.setProgressText( - "Calculating sun positions for each time step" - ) - location = {"longitude": lon, "latitude": lat, "altitude": alt} - YYYY, altitude, azimuth, zen, jday, leafon, dectime, altmax = ( - Solweig_2015a_metdata_noload(self.metdata, location, utc) - ) - - # Creating vectors from meteorological input - DOY = self.metdata[:, 1] - hours = self.metdata[:, 2] - minu = self.metdata[:, 3] - Ta = self.metdata[:, 11] - RH = self.metdata[:, 10] - radG = self.metdata[:, 14] - radD = self.metdata[:, 21] - radI = self.metdata[:, 22] - P = self.metdata[:, 12] - Ws = self.metdata[:, 9] - - # Check if diffuse and direct radiation exist - if onlyglobal == 0: - if np.min(radD) == -999: - raise QgsProcessingException( - "Diffuse radiation include NoData values", - 'Tick in the box "Estimate diffuse and direct shortwave..." or aqcuire ' - "observed values from external data sources.", - ) - if np.min(radI) == -999: - raise QgsProcessingException( - "Direct radiation include NoData values", - 'Tick in the box "Estimate diffuse and direct shortwave..." or aqcuire ' - "observed values from external data sources.", - ) - - # POIs check - if poilyr is not None: # usePOI: - # header = 'yyyy id it imin dectime altitude azimuth kdir kdiff kglobal kdown kup keast ksouth ' \ - # 'kwest knorth ldown lup least lsouth lwest lnorth Ta Tg RH Esky Tmrt ' \ - # 'I0 CI Shadow SVF_b SVF_bv KsideI PET UTCI' - - header = ( - "yyyy id it imin dectime altitude azimuth kdir kdiff kglobal kdown kup keast ksouth " - "kwest knorth ldown lup least lsouth lwest lnorth Ta Tg RH Esky Tmrt " - "I0 CI Shadow SVF_b SVF_bv KsideI PET UTCI CI_Tg CI_TgG KsideD Lside diffDown Kside" - ) - - # poilyr = self.parameterAsVectorLayer(parameters, self.POI_FILE, context) - # if poilyr is None: - # raise QgsProcessingException("No valid point layer is selected") - - poi_field = self.parameterAsFields( - parameters, self.POI_FIELD, context - ) - # if poi_field[0] is None: - # raise QgsProcessingException("An attribute field with unique values must be selected when using a POI vector file") - vlayer = poilyr - prov = vlayer.dataProvider() - fields = prov.fields() - idx = vlayer.fields().indexFromName(poi_field[0]) - numfeat = vlayer.featureCount() - poiname = [] - poisxy = np.zeros((numfeat, 3)) - 999 - ind = 0 - for f in vlayer.getFeatures(): # looping through each POI - y = f.geometry().centroid().asPoint().y() - x = f.geometry().centroid().asPoint().x() - - poiname.append(f.attributes()[idx]) - poisxy[ind, 0] = ind - poisxy[ind, 1] = np.round((x - minx) * scale) - if miny >= 0: - poisxy[ind, 2] = np.round( - (miny + rows * (1.0 / scale) - y) * scale - ) - else: - poisxy[ind, 2] = np.round( - (miny + rows * (1.0 / scale) - y) * scale - ) - - ind += 1 - - for k in range(0, poisxy.shape[0]): - poi_save = [] # np.zeros((1, 33)) - data_out = outputDir + "/POI_" + str(poiname[k]) + ".txt" - np.savetxt( - data_out, - poi_save, - delimiter=" ", - header=header, - comments="", - ) - - # Num format for POI output - numformat = "%d %d %d %d %.5f " + "%.2f " * 36 - - # Other PET variables - mbody = self.parameterAsDouble(parameters, self.WEIGHT, context) - ht = ( - self.parameterAsDouble(parameters, self.HEIGHT, context) - / 100.0 - ) - clo = self.parameterAsDouble(parameters, self.CLO, context) - age = self.parameterAsDouble(parameters, self.AGE, context) - activity = self.parameterAsDouble(parameters, self.WEIGHT, context) - sex = self.parameterAsInt(parameters, self.SEX, context) + 1 - sensorheight = self.parameterAsDouble( - parameters, self.SENSOR_HEIGHT, context - ) - - solweig_parameters["PET_settings"]["Value"]["Age"] = age - solweig_parameters["PET_settings"]["Value"]["Weight"] = mbody - solweig_parameters["PET_settings"]["Value"]["Height"] = ht - solweig_parameters["PET_settings"]["Value"]["clo"] = clo - solweig_parameters["PET_settings"]["Value"]["Activity"] = activity - if sex == 1: - solweig_parameters["PET_settings"]["Value"]["Sex"] = "Male" - else: - solweig_parameters["PET_settings"]["Value"]["Sex"] = "Female" - - feedback.setProgressText( - "Point of interest (POI) vector data successfully loaded" - ) - - # %Parameterisarion for Lup - if not height: - height = 1.1 - - # %Radiative surface influence, Rule of thumb by Schmid et al. (1990). - first = np.round(height) - if first == 0.0: - first = 1.0 - second = np.round((height * 20.0)) - - if usevegdem == 1: - # Conifer or deciduous - if conifer_bool: - leafon = np.ones((1, DOY.shape[0])) - else: - leafon = np.zeros((1, DOY.shape[0])) - if firstdayleaf > lastdayleaf: - leaf_bool = (DOY > firstdayleaf) | (DOY < lastdayleaf) - else: - leaf_bool = (DOY > firstdayleaf) & (DOY < lastdayleaf) - leafon[0, leaf_bool] = 1 - - # % Vegetation transmittivity of shortwave radiation - psi = leafon * transVeg - psi[leafon == 0] = 0.5 - # amaxvalue - vegmax = vegdsm.max() - amaxvalue = dsm.max() - dsm.min() - amaxvalue = np.maximum(amaxvalue, vegmax) - - # Elevation vegdsms if buildingDEM includes ground heights - vegdsm = vegdsm + dsm - vegdsm[vegdsm == dsm] = 0 - vegdsm2 = vegdsm2 + dsm - vegdsm2[vegdsm2 == dsm] = 0 - - # % Bush separation - bush = np.logical_not((vegdsm2 * vegdsm)) * vegdsm - - # % major bug fixed 20141203 - svfbuveg = svf - (1.0 - svfveg) * (1.0 - transVeg) - else: - psi = leafon * 0.0 + 1.0 - svfbuveg = svf - bush = np.zeros([rows, cols]) - amaxvalue = 0 - - # %Initialization of maps - Knight = np.zeros((rows, cols)) - Tgmap1 = np.zeros((rows, cols)) - Tgmap1E = np.zeros((rows, cols)) - Tgmap1S = np.zeros((rows, cols)) - Tgmap1W = np.zeros((rows, cols)) - Tgmap1N = np.zeros((rows, cols)) - - # building grid and land cover preparation - # sitein = self.plugin_dir + "/landcoverclasses_2016a.txt" - # f = open(sitein) - # lin = f.readlines() - # lc_class = np.zeros((lin.__len__() - 1, 6)) - # for i in range(1, lin.__len__()): - # lines = lin[i].split() - # for j in np.arange(1, 7): - # lc_class[i - 1, j - 1] = float(lines[j]) - - # lc_df = pd.read_csv(self.plugin_dir + '/landcoverclasses_2023a.txt') - # lc_class = lc_df[['Code', 'Albedo', 'Emissivity', 'Ts_deg', 'Tstart', 'TmaxLST']].to_numpy() - - if demforbuild == 0: - buildings = np.copy(lcgrid) - buildings[buildings == 7] = 1 - buildings[buildings == 6] = 1 - buildings[buildings == 5] = 1 - buildings[buildings == 4] = 1 - buildings[buildings == 3] = 1 - buildings[buildings == 2] = 0 - else: - buildings = dsm - dem - buildings[buildings < 2.0] = 1.0 - buildings[buildings >= 2.0] = 0.0 - - if saveBuild: - saveraster(gdal_dsm, outputDir + "/buildings.tif", buildings) - - # Import shadow matrices (Anisotropic sky) - if folderPathPerez: # UseAniso - anisotropic_sky = 1 - data = np.load(folderPathPerez) - shmat = data["shadowmat"] - vegshmat = data["vegshadowmat"] - vbshvegshmat = data["vbshmat"] - if usevegdem == 1: - diffsh = np.zeros((rows, cols, shmat.shape[2])) - for i in range(0, shmat.shape[2]): - # changes in psi not implemented yet - diffsh[:, :, i] = shmat[:, :, i] - ( - 1 - vegshmat[:, :, i] - ) * (1 - transVeg) - else: - diffsh = shmat - # vegshmat += 1 - # vbshvegshmat += 1 - - # Estimate number of patches based on shadow matrices - if shmat.shape[2] == 145: - patch_option = 1 # patch_option = 1 # 145 patches - elif shmat.shape[2] == 153: - patch_option = 2 # patch_option = 2 # 153 patches - elif shmat.shape[2] == 306: - patch_option = 3 # patch_option = 3 # 306 patches - elif shmat.shape[2] == 612: - patch_option = 4 # patch_option = 4 # 612 patches - - # asvf to calculate sunlit and shaded patches - asvf = np.arccos(np.sqrt(svf)) - - # Empty array for steradians - steradians = np.zeros((shmat.shape[2])) - - anisotropic_feedback = ( - "Sky divided into " - + str(int(shmat.shape[2])) - + " patches\n \ - Anisotropic sky for diffuse shortwave radiation (Perez et al., 1993) and longwave radiation (Martin & Berdahl, 1984)" - ) - feedback.setProgressText(anisotropic_feedback) - else: - feedback.setProgressText("Isotropic sky") - anisotropic_sky = 0 - diffsh = None - shmat = None - vegshmat = None - vbshvegshmat = None - asvf = None - patch_option = 0 - steradians = 0 - - # % Ts parameterisation maps - if landcover == 1.0: - if folderWallScheme: - unique_landcover = np.unique(lcgrid) - unique_landcover = unique_landcover[unique_landcover < 100] - if ( - np.max(unique_landcover) > 7 - or np.min(unique_landcover) < 1 - ): - raise QgsProcessingException( - "The land cover grid includes integer values higher (or lower) than UMEP-formatted " - "land cover grid (should be integer between 1 and 7). If other LC-classes should be included they also need to be included in landcoverclasses_2016a.txt" - ) - else: - if np.max(lcgrid) > 7 or np.min(lcgrid) < 1: - raise QgsProcessingException( - "The land cover grid includes integer values higher (or lower) than UMEP-formatted " - "land cover grid (should be integer between 1 and 7). If other LC-classes should be included they also need to be included in landcoverclasses_2016a.txt" - ) - if np.where(lcgrid) == 3 or np.where(lcgrid) == 4: - raise QgsProcessingException( - "The land cover grid includes values (decidouos and/or conifer) not appropriate for SOLWEIG-formatted land cover grid (should not include 3 or 4)." - ) - - # Get land cover properties for Tg wave (land cover scheme based on - # Bogren et al. 2000, explained in Lindberg et al., 2008 and - # Lindberg, Onomura & Grimmond, 2016) - [ - TgK, - Tstart, - alb_grid, - emis_grid, - TgK_wall, - Tstart_wall, - TmaxLST, - TmaxLST_wall, - ] = Tgmaps_v1(lcgrid.copy(), solweig_parameters) - - else: - TgK = ( - Knight - + solweig_parameters["Ts_deg"]["Value"]["Cobble_stone_2014a"] - ) - Tstart = ( - Knight - - solweig_parameters["Tstart"]["Value"]["Cobble_stone_2014a"] - ) - TmaxLST = solweig_parameters["TmaxLST"]["Value"][ - "Cobble_stone_2014a" - ] - alb_grid = ( - Knight - + solweig_parameters["Albedo"]["Effective"]["Value"][ - "Cobble_stone_2014a" - ] - ) - emis_grid = ( - Knight - + solweig_parameters["Emissivity"]["Value"][ - "Cobble_stone_2014a" - ] - ) - TgK_wall = solweig_parameters["Ts_deg"]["Value"]["Walls"] - Tstart_wall = solweig_parameters["Tstart"]["Value"]["Walls"] - TmaxLST_wall = solweig_parameters["TmaxLST"]["Value"]["Walls"] - - # Import data for wall temperature parameterization - if folderWallScheme: - wallScheme = 1 - wallData = np.load(folderWallScheme) - voxelMaps = wallData["voxelId"] - voxelTable = wallData["voxelTable"] - # Get wall type set in GUI - wall_type = str( - 100 - + int( - self.parameterAsString(parameters, self.WALL_TYPE, context) - ) - ) - - # Calculate wall height for wall scheme, i.e. include corners - # (thicker walls) - walls_scheme = wa.findwalls_sp( - dsm, 2, np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]]) - ) - # Calculate wall aspect for wall scheme, i.e. include corners - # (thicker walls) - dirwalls_scheme = wa.filter1Goodwin_as_aspect_v3( - walls_scheme.copy(), scale, dsm, feedback, 100.0 / 180.0 - ) - - # Used in wall temperature parameterization scheme - first_timestep = ( - pd.to_datetime(YYYY[0][0], format="%Y") - + pd.to_timedelta(DOY[0] - 1, unit="d") - + pd.to_timedelta(hours[0], unit="h") - + pd.to_timedelta(minu[0], unit="m") - ) - second_timestep = ( - pd.to_datetime(YYYY[0][1], format="%Y") - + pd.to_timedelta(DOY[1] - 1, unit="d") - + pd.to_timedelta(hours[1], unit="h") - + pd.to_timedelta(minu[1], unit="m") - ) - - timeStep = (second_timestep - first_timestep).seconds - - # Load voxelTable as Pandas DataFrame - voxelTable, dirwalls_scheme = load_walls( - voxelTable, - solweig_parameters, - wall_type, - dirwalls_scheme, - Ta[0], - timeStep, - albedo_b, - ewall, - alb_grid, - landcover, - lcgrid, - dsm, - ) - # Unique wall types - thermal_effusivity = voxelTable["thermalEffusivity"].unique() - - # Empty wall temperature matrix for parameterization scheme - wallScheme_feedback = ( - "Running with wall parameterization scheme. Walls divided into " - + str(int(voxelTable.shape[0])) - + " unique voxels." - ) - feedback.setProgressText(wallScheme_feedback) - - # Use wall of interest - if woilyr is not None: - ( - dsm_minx, - dsm_x_size, - dsm_x_rotation, - dsm_miny, - dsm_y_rotation, - dsm_y_size, - ) = gdal_dsm.GetGeoTransform() - - woi_field = self.parameterAsStrings( - parameters, self.WOI_FIELD, context - ) - woisxy, woiname = wallOfInterest( - woilyr, - woi_field, - minx, - miny, - scale, - rows, - outputDir, - dsm_minx, - dsm_x_size, - dsm_miny, - dsm_y_size, - ) - - # Create pandas datetime object to be used when createing an xarray - # DataSet where wall temperatures/radiation is stored and - # eventually saved as a NetCDf - if wallNetCDF: - met_for_xarray = ( - pd.to_datetime(YYYY[0][:], format="%Y") - + pd.to_timedelta(DOY - 1, unit="d") - + pd.to_timedelta(hours, unit="h") - + pd.to_timedelta(minu, unit="m") - ) - - else: - wallScheme = 0 - voxelMaps = 0 - voxelTable = 0 - timeStep = 0 - thermal_effusivity = 0 - walls_scheme = np.ones((rows, cols)) * 10.0 - dirwalls_scheme = np.ones((rows, cols)) * 10.0 - - # Initialisation of time related variables - if Ta.__len__() == 1: - timestepdec = 0 - else: - timestepdec = dectime[1] - dectime[0] - timeadd = 0.0 - timeaddE = 0.0 - timeaddS = 0.0 - timeaddW = 0.0 - timeaddN = 0.0 - firstdaytime = 1.0 - - WriteMetadataSOLWEIG.writeRunInfo( - outputDir, - filepath_dsm, - gdal_dsm, - usevegdem, - filePath_cdsm, - trunkfile, - filePath_tdsm, - lat, - lon, - utc, - landcover, - filePath_lc, - metfileexist, - inputMet, - self.metdata, - self.plugin_dir, - absK, - absL, - albedo_b, - albedo_g, - ewall, - eground, - onlyglobal, - trunkratio, - transVeg, - rows, - cols, - pos, - elvis, - cyl, - demforbuild, - anisotropic_sky, - wallScheme, - thermal_effusivity, - ) - - feedback.setProgressText( - "Writing settings for this model run to specified output folder (Filename: RunInfoSOLWEIG_YYYY_DOY_HHMM.txt)" - ) - - # Save svf - if anisotropic_sky: - if not poisxy is None: - patch_characteristics = np.zeros( - (shmat.shape[2], poisxy.shape[0]) - ) - for idx in range(poisxy.shape[0]): - for idy in range(shmat.shape[2]): - # Calculations for patches on sky, shmat = 1 = sky is - # visible - temp_sky = (shmat[:, :, idy] == 1) & ( - vegshmat[:, :, idy] == 1 - ) - # Calculations for patches that are vegetation, - # vegshmat = 0 = shade from vegetation - temp_vegsh = (vegshmat[:, :, idy] == 0) | ( - vbshvegshmat[:, :, idy] == 0 - ) - # Calculations for patches that are buildings, shmat = - # 0 = shade from buildings - temp_vbsh = (1 - shmat[:, :, idy]) * vbshvegshmat[ - :, :, idy - ] - temp_sh = temp_vbsh == 1 - if wallScheme: - temp_sh_w = temp_sh * voxelMaps[:, :, idy] - temp_sh_roof = temp_sh * ( - voxelMaps[:, :, idy] == 0 - ) - else: - temp_sh_w = 0 - temp_sh_roof = 0 - # Sky patch - if temp_sky[int(poisxy[idx, 2]), int(poisxy[idx, 1])]: - patch_characteristics[idy, idx] = 1.8 - # Vegetation patch - elif temp_vegsh[ - int(poisxy[idx, 2]), int(poisxy[idx, 1]) - ]: - patch_characteristics[idy, idx] = 2.5 - # Building patch - elif temp_sh[int(poisxy[idx, 2]), int(poisxy[idx, 1])]: - if wallScheme: - if temp_sh_w[ - int(poisxy[idx, 2]), int(poisxy[idx, 1]) - ]: - patch_characteristics[idy, idx] = 4.5 - elif temp_sh_roof[ - int(poisxy[idx, 2]), int(poisxy[idx, 1]) - ]: - patch_characteristics[idy, idx] = 6.0 - else: - patch_characteristics[idy, idx] = 4.5 - # Roof patch - # elif - - # If metfile starts at night - CI = 1.0 - - # Save solweig_parameters in output folder - with open(outputDir + "/solweig_parameters.json", "w") as f: - json.dump(solweig_parameters, f, indent=2) - - # Main function - feedback.setProgressText("Executing main model") - - tmrtplot = np.zeros((rows, cols)) - TgOut1 = np.zeros((rows, cols)) - - # Initiate array for I0 values - if np.unique(DOY).shape[0] > 1: - unique_days = np.unique(DOY) - first_unique_day = DOY[DOY == unique_days[0]] - I0_array = np.zeros((first_unique_day.shape[0])) - else: - first_unique_day = DOY.copy() - I0_array = np.zeros((DOY.shape[0])) - - # Rotate canyon - rotate_deg = 0 - - for i in np.arange(0, Ta.__len__()): - # move progressbar forward - feedback.setProgress(int(i * (100.0 / Ta.__len__()))) - if feedback.isCanceled(): - feedback.setProgressText("Calculation cancelled") - break - # Daily water body temperature - if landcover == 1: - if ((dectime[i] - np.floor(dectime[i]))) == 0 or (i == 0): - Twater = np.mean(Ta[jday[0] == np.floor(dectime[i])]) - # Nocturnal cloudfraction from Offerle et al. 2003 - if (dectime[i] - np.floor(dectime[i])) == 0: - daylines = np.where(np.floor(dectime) == dectime[i]) - if daylines.__len__() > 1: - alt = altitude[0][daylines] - alt2 = np.where(alt > 1) - rise = alt2[0][0] - [_, CI, _, _, _] = clearnessindex_2013b( - zen[0, i + rise + 1], - jday[0, i + rise + 1], - Ta[i + rise + 1], - RH[i + rise + 1] / 100.0, - radG[i + rise + 1], - location, - # i+rise+1 to match matlab code. correct? - P[i + rise + 1], - ) - if (CI > 1.0) or (CI == np.inf): - CI = 1.0 - else: - CI = 1.0 - - # if altitude[0][i] > 0: - # # # radI[i] = (radG[i] - radD[i])/np.sin(altitude[0][i] * np.pi/180) - # # # onlyglobal = False - # radI[i] = radI[i]/np.sin(altitude[0][i] * np.pi/180) - # else: - # radG[i] = 0. - # radD[i] = 0. - # radI[i] = 0. - - # radI[i] = radI[i]/np.sin(altitude[0][i] * np.pi/180) - - ( - Tmrt, - Kdown, - Kup, - Ldown, - Lup, - Tg, - ea, - esky, - I0, - CI, - shadow, - firstdaytime, - timestepdec, - timeadd, - Tgmap1, - Tgmap1E, - Tgmap1S, - Tgmap1W, - Tgmap1N, - Keast, - Ksouth, - Kwest, - Knorth, - Least, - Lsouth, - Lwest, - Lnorth, - KsideI, - TgOut1, - TgOut, - radIout, - radDout, - Lside, - Lsky_patch_characteristics, - CI_Tg, - CI_TgG, - KsideD, - dRad, - Kside, - steradians, - voxelTable, - ) = so.Solweig_2022a_calc( - i, - dsm, - scale, - rows, - cols, - svf, - svfN, - svfW, - svfE, - svfS, - svfveg, - svfNveg, - svfEveg, - svfSveg, - svfWveg, - svfaveg, - svfEaveg, - svfSaveg, - svfWaveg, - svfNaveg, - vegdsm, - vegdsm2, - albedo_b, - absK, - absL, - ewall, - Fside, - Fup, - Fcyl, - altitude[0][i], - azimuth[0][i] + rotate_deg, - zen[0][i], - jday[0][i], - usevegdem, - onlyglobal, - buildings, - location, - psi[0][i], - landcover, - lcgrid, - dectime[i], - altmax[0][i], - wallaspect, - wallheight, - cyl, - elvis, - Ta[i], - RH[i], - radG[i], - radD[i], - radI[i], - P[i], - amaxvalue, - bush, - Twater, - TgK, - Tstart, - alb_grid, - emis_grid, - TgK_wall, - Tstart_wall, - TmaxLST, - TmaxLST_wall, - first, - second, - svfalfa, - svfbuveg, - firstdaytime, - timeadd, - timestepdec, - Tgmap1, - Tgmap1E, - Tgmap1S, - Tgmap1W, - Tgmap1N, - CI, - TgOut1, - diffsh, - shmat, - vegshmat, - vbshvegshmat, - anisotropic_sky, - asvf, - patch_option, - voxelMaps, - voxelTable, - Ws[i], - wallScheme, - timeStep, - steradians, - walls_scheme, - dirwalls_scheme, - ) - - # Tmrt, Kdown, Kup, Ldown, Lup, Tg, ea, esky, I0, CI, shadow, firstdaytime, timestepdec, timeadd, \ - # Tgmap1, Tgmap1E, Tgmap1S, Tgmap1W, Tgmap1N, Keast, Ksouth, Kwest, Knorth, Least, \ - # Lsouth, Lwest, Lnorth, KsideI, TgOut1, TgOut, radIout, radDout = so.Solweig_2021a_calc( - # i, dsm, scale, rows, cols, svf, svfN, svfW, svfE, svfS, svfveg, - # svfNveg, svfEveg, svfSveg, svfWveg, svfaveg, svfEaveg, svfSaveg, svfWaveg, svfNaveg, - # vegdsm, vegdsm2, albedo_b, absK, absL, ewall, Fside, Fup, Fcyl, altitude[0][i], - # azimuth[0][i], zen[0][i], jday[0][i], usevegdem, onlyglobal, buildings, location, - # psi[0][i], landcover, lcgrid, dectime[i], altmax[0][i], wallaspect, - # wallheight, cyl, elvis, Ta[i], RH[i], radG[i], radD[i], radI[i], P[i], amaxvalue, - # bush, Twater, TgK, Tstart, alb_grid, emis_grid, TgK_wall, Tstart_wall, TmaxLST, - # TmaxLST_wall, first, second, svfalfa, svfbuveg, firstdaytime, timeadd, timestepdec, - # Tgmap1, Tgmap1E, Tgmap1S, Tgmap1W, Tgmap1N, CI, TgOut1, diffsh, - # ani) - - # Tmrt, Kdown, Kup, Ldown, Lup, Tg, ea, esky, I0, CI, shadow, firstdaytime, timestepdec, timeadd, \ - # Tgmap1, timeaddE, Tgmap1E, timeaddS, Tgmap1S, timeaddW, Tgmap1W, timeaddN, Tgmap1N, \ - # Keast, Ksouth, Kwest, Knorth, Least, Lsouth, Lwest, Lnorth, KsideI, TgOut1, TgOut, radIout, radDout \ - # = so.Solweig_2019a_calc(i, dsm, scale, rows, cols, svf, svfN, svfW, svfE, svfS, svfveg, - # svfNveg, svfEveg, svfSveg, svfWveg, svfaveg, svfEaveg, svfSaveg, svfWaveg, svfNaveg, - # vegdsm, vegdsm2, albedo_b, absK, absL, ewall, Fside, Fup, Fcyl, altitude[0][i], - # azimuth[0][i], zen[0][i], jday[0][i], usevegdem, onlyglobal, buildings, location, - # psi[0][i], landcover, lcgrid, dectime[i], altmax[0][i], waspect, - # wheight, cyl, elvis, Ta[i], RH[i], radG[i], radD[i], radI[i], P[i], amaxvalue, - # bush, Twater, TgK, Tstart, alb_grid, emis_grid, TgK_wall, Tstart_wall, TmaxLST, - # TmaxLST_wall, first, second, svfalfa, svfbuveg, firstdaytime, timeadd, timeaddE, timeaddS, - # timeaddW, timeaddN, timestepdec, Tgmap1, Tgmap1E, Tgmap1S, - # Tgmap1W, Tgmap1N, CI, TgOut1, diffsh, ani) - - # Save I0 for I0 vs. Kdown output plot to check if UTC is off - if i < (first_unique_day.shape[0] - 1): - I0_array[i] = I0 - elif i == (first_unique_day.shape[0] - 1): - # Output I0 vs. Kglobal plot - radG_for_plot = radG[DOY == first_unique_day[0]] - # hours_for_plot = hours[DOY == first_unique_day[0]] - dectime_for_plot = dectime[DOY == first_unique_day[0]] - fig, ax = plt.subplots() - ax.plot(dectime_for_plot, I0_array, label="I0") - ax.plot(dectime_for_plot, radG_for_plot, label="Kglobal") - ax.set_ylabel("Shortwave radiation [$Wm^{-2}$]") - ax.set_xlabel("Decimal time") - ax.set_title("UTC" + str(int(utc))) - ax.legend() - fig.savefig(outputDir + "/metCheck.png", dpi=150) - - tmrtplot = tmrtplot + Tmrt - - if altitude[0][i] > 0: - w = "D" - else: - w = "N" - - # # Write to POIs - # if not poisxy is None: - # for k in range(0, poisxy.shape[0]): - # poi_save = np.zeros((1, 35)) - # poi_save[0, 0] = YYYY[0][i] - # poi_save[0, 1] = jday[0][i] - # poi_save[0, 2] = hours[i] - # poi_save[0, 3] = minu[i] - # poi_save[0, 4] = dectime[i] - # poi_save[0, 5] = altitude[0][i] - # poi_save[0, 6] = azimuth[0][i] - # poi_save[0, 7] = radIout - # poi_save[0, 8] = radDout - # poi_save[0, 9] = radG[i] - # poi_save[0, 10] = Kdown[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 11] = Kup[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 12] = Keast[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 13] = Ksouth[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 14] = Kwest[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 15] = Knorth[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 16] = Ldown[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 17] = Lup[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 18] = Least[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 19] = Lsouth[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 20] = Lwest[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 21] = Lnorth[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 22] = Ta[i] - # poi_save[0, 23] = TgOut[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 24] = RH[i] - # poi_save[0, 25] = esky - # poi_save[0, 26] = Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 27] = I0 - # poi_save[0, 28] = CI - # poi_save[0, 29] = shadow[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 30] = svf[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 31] = svfbuveg[int(poisxy[k, 2]), int(poisxy[k, 1])] - # poi_save[0, 32] = KsideI[int(poisxy[k, 2]), int(poisxy[k, 1])] - # # Recalculating wind speed based on powerlaw - # WsPET = (1.1 / sensorheight) ** 0.2 * Ws[i] - # WsUTCI = (10. / sensorheight) ** 0.2 * Ws[i] - # resultPET = p._PET(Ta[i], RH[i], Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])], WsPET, - # mbody, age, ht, activity, clo, sex) - # poi_save[0, 33] = resultPET - # resultUTCI = utci.utci_calculator(Ta[i], RH[i], Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])], - # WsUTCI) - # poi_save[0, 34] = resultUTCI - # data_out = outputDir + '/POI_' + str(poiname[k]) + '.txt' - # # f_handle = file(data_out, 'a') - # f_handle = open(data_out, 'ab') - # np.savetxt(f_handle, poi_save, fmt=numformat) - # f_handle.close() - - # Write to POIs - if not poisxy is None: - for k in range(0, poisxy.shape[0]): - poi_save = np.zeros((1, 41)) - poi_save[0, 0] = YYYY[0][i] - poi_save[0, 1] = jday[0][i] - poi_save[0, 2] = hours[i] - poi_save[0, 3] = minu[i] - poi_save[0, 4] = dectime[i] - poi_save[0, 5] = altitude[0][i] - poi_save[0, 6] = azimuth[0][i] - poi_save[0, 7] = radIout - poi_save[0, 8] = radDout - poi_save[0, 9] = radG[i] - poi_save[0, 10] = Kdown[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 11] = Kup[int(poisxy[k, 2]), int(poisxy[k, 1])] - poi_save[0, 12] = Keast[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 13] = Ksouth[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 14] = Kwest[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 15] = Knorth[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 16] = Ldown[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 17] = Lup[int(poisxy[k, 2]), int(poisxy[k, 1])] - poi_save[0, 18] = Least[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 19] = Lsouth[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 20] = Lwest[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 21] = Lnorth[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 22] = Ta[i] - poi_save[0, 23] = TgOut[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 24] = RH[i] - poi_save[0, 25] = esky - poi_save[0, 26] = Tmrt[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 27] = I0 - poi_save[0, 28] = CI - poi_save[0, 29] = shadow[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 30] = svf[int(poisxy[k, 2]), int(poisxy[k, 1])] - poi_save[0, 31] = svfbuveg[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 32] = KsideI[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - # Recalculating wind speed based on powerlaw - WsPET = (1.1 / sensorheight) ** 0.2 * Ws[i] - WsUTCI = (10.0 / sensorheight) ** 0.2 * Ws[i] - resultPET = p._PET( - Ta[i], - RH[i], - Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])], - WsPET, - mbody, - age, - ht, - activity, - clo, - sex, - ) - poi_save[0, 33] = resultPET - resultUTCI = utci.utci_calculator( - Ta[i], - RH[i], - Tmrt[int(poisxy[k, 2]), int(poisxy[k, 1])], - WsUTCI, - ) - poi_save[0, 34] = resultUTCI - poi_save[0, 35] = CI_Tg - poi_save[0, 36] = CI_TgG - poi_save[0, 37] = KsideD[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 38] = Lside[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 39] = dRad[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - poi_save[0, 40] = Kside[ - int(poisxy[k, 2]), int(poisxy[k, 1]) - ] - data_out = outputDir + "/POI_" + str(poiname[k]) + ".txt" - # f_handle = file(data_out, 'a') - f_handle = open(data_out, "ab") - np.savetxt(f_handle, poi_save, fmt=numformat) - f_handle.close() - - # If wall temperature parameterization scheme is in use - if folderWallScheme: - # Store wall data for output - if not woisxy is None: - for k in range(0, woisxy.shape[0]): - temp_wall = voxelTable.loc[ - ( - (voxelTable["ypos"] == woisxy[k, 2]) - & (voxelTable["xpos"] == woisxy[k, 1]) - ), - "wallTemperature", - ].to_numpy() - K_in = voxelTable.loc[ - ( - (voxelTable["ypos"] == woisxy[k, 2]) - & (voxelTable["xpos"] == woisxy[k, 1]) - ), - "K_in", - ].to_numpy() - L_in = voxelTable.loc[ - ( - (voxelTable["ypos"] == woisxy[k, 2]) - & (voxelTable["xpos"] == woisxy[k, 1]) - ), - "L_in", - ].to_numpy() - wallShade = voxelTable.loc[ - ( - (voxelTable["ypos"] == woisxy[k, 2]) - & (voxelTable["xpos"] == woisxy[k, 1]) - ), - "wallShade", - ].to_numpy() - temp_all = np.concatenate( - [temp_wall, K_in, L_in, wallShade] - ) - # temp_all = np.concatenate([temp_wall]) - # wall_data = np.zeros((1, 7 + temp_wall.shape[0])) - wall_data = np.zeros((1, 7 + temp_all.shape[0])) - # Part of file name (wallid), i.e. WOI_wallid.txt - data_out = ( - outputDir + "/WOI_" + str(woiname[k]) + ".txt" - ) - if i == 0: - # Output file header - # header = 'yyyy id it imin dectime Ta SVF Ts' - header = ( - "yyyy id it imin dectime Ta SVF" - + " Ts" * temp_wall.shape[0] - + " Kin" * K_in.shape[0] - + " Lin" * L_in.shape[0] - + " shade" * wallShade.shape[0] - ) - # Part of file name (wallid), i.e. WOI_wallid.txt - # woiname = voxelTable.loc[((voxelTable['ypos'] == woisxy[k, 2]) & (voxelTable['xpos'] == woisxy[k, 1])), 'wallId'].to_numpy()[0] - woi_save = [] # - np.savetxt( - data_out, - woi_save, - delimiter=" ", - header=header, - comments="", - ) - # Fill wall_data with variables - wall_data[0, 0] = YYYY[0][i] - wall_data[0, 1] = jday[0][i] - wall_data[0, 2] = hours[i] - wall_data[0, 3] = minu[i] - wall_data[0, 4] = dectime[i] - wall_data[0, 5] = Ta[i] - wall_data[0, 6] = svf[ - int(woisxy[k, 2]), int(woisxy[k, 1]) - ] - wall_data[0, 7:] = temp_all - - # Num format for output file data - woi_numformat = ( - "%d %d %d %d %.5f %.2f %.2f" - + " %.2f" * temp_all.shape[0] - ) - # Open file, add data, save - f_handle = open(data_out, "ab") - np.savetxt(f_handle, wall_data, fmt=woi_numformat) - f_handle.close() - - # Save wall temperature/radiation as NetCDF - if wallNetCDF: - netcdf_output = outputDir + "/walls.nc" - walls_as_netcdf( - voxelTable, - rows, - cols, - met_for_xarray, - i, - dsm, - filepath_dsm, - netcdf_output, - ) - - if hours[i] < 10: - XH = "0" - else: - XH = "" - if minu[i] < 10: - XM = "0" - else: - XM = "" - - if outputTmrt: - saveraster( - gdal_dsm, - outputDir - + "/Tmrt_" - + str(int(YYYY[0, i])) - + "_" - + str(int(DOY[i])) - + "_" - + XH - + str(int(hours[i])) - + XM - + str(int(minu[i])) - + w - + ".tif", - Tmrt, - ) - if outputKup: - saveraster( - gdal_dsm, - outputDir - + "/Kup_" - + str(int(YYYY[0, i])) - + "_" - + str(int(DOY[i])) - + "_" - + XH - + str(int(hours[i])) - + XM - + str(int(minu[i])) - + w - + ".tif", - Kup, - ) - if outputKdown: - saveraster( - gdal_dsm, - outputDir - + "/Kdown_" - + str(int(YYYY[0, i])) - + "_" - + str(int(DOY[i])) - + "_" - + XH - + str(int(hours[i])) - + XM - + str(int(minu[i])) - + w - + ".tif", - Kdown, - ) - if outputLup: - saveraster( - gdal_dsm, - outputDir - + "/Lup_" - + str(int(YYYY[0, i])) - + "_" - + str(int(DOY[i])) - + "_" - + XH - + str(int(hours[i])) - + XM - + str(int(minu[i])) - + w - + ".tif", - Lup, - ) - if outputLdown: - saveraster( - gdal_dsm, - outputDir - + "/Ldown_" - + str(int(YYYY[0, i])) - + "_" - + str(int(DOY[i])) - + "_" - + XH - + str(int(hours[i])) - + XM - + str(int(minu[i])) - + w - + ".tif", - Ldown, - ) - if outputSh: - saveraster( - gdal_dsm, - outputDir - + "/Shadow_" - + str(int(YYYY[0, i])) - + "_" - + str(int(DOY[i])) - + "_" - + XH - + str(int(hours[i])) - + XM - + str(int(minu[i])) - + w - + ".tif", - shadow, - ) - - if outputKdiff: - saveraster( - gdal_dsm, - outputDir - + "/Kdiff_" - + str(int(YYYY[0, i])) - + "_" - + str(int(DOY[i])) - + "_" - + XH - + str(int(hours[i])) - + XM - + str(int(minu[i])) - + w - + ".tif", - dRad, - ) - - # Sky view image of patches - if (anisotropic_sky == 1) & (i == 0) & (not poisxy is None): - for k in range(poisxy.shape[0]): - Lsky_patch_characteristics[:, 2] = patch_characteristics[ - :, k - ] - skyviewimage_out = ( - outputDir + "/POI_" + str(poiname[k]) + ".png" - ) - PolarBarPlot( - Lsky_patch_characteristics, - altitude[0][i], - azimuth[0][i], - "Hemisphere partitioning", - skyviewimage_out, - 0, - 5, - 0, - ) - - # Save files for Tree Planter - if outputTreeplanter: - feedback.setProgressText("Saving files for Tree Planter tool") - # Save DSM - copyfile(filepath_dsm, outputDir + "/DSM.tif") - - # Save CDSM - if usevegdem == 1: - copyfile(filePath_cdsm, outputDir + "/CDSM.tif") - - # Saving settings from SOLWEIG for SOLWEIG1D in TreePlanter - settingsHeader = "UTC, posture, onlyglobal, landcover, anisotropic, cylinder, albedo_walls, albedo_ground, emissivity_walls, emissivity_ground, absK, absL, elevation, patch_option" - settingsFmt = ( - "%i", - "%i", - "%i", - "%i", - "%i", - "%i", - "%1.2f", - "%1.2f", - "%1.2f", - "%1.2f", - "%1.2f", - "%1.2f", - "%1.2f", - "%i", - ) - settingsData = np.array( - [ - [ - utc, - pos, - onlyglobal, - landcover, - anisotropic_sky, - cyl, - albedo_b, - albedo_g, - ewall, - eground, - absK, - absL, - alt, - patch_option, - ] - ] - ) - np.savetxt( - outputDir + "/treeplantersettings.txt", - settingsData, - fmt=settingsFmt, - header=settingsHeader, - delimiter=" ", - ) - - # Copying met file for SpatialTC - copyfile(inputMet, outputDir + "/metforcing.txt") - - tmrtplot = ( - tmrtplot / Ta.__len__() - ) # fix average Tmrt instead of sum, 20191022 - saveraster(gdal_dsm, outputDir + "/Tmrt_average.tif", tmrtplot) - feedback.setProgressText("SOLWEIG: Model calculation finished.") - - return {self.OUTPUT_DIR: outputDir} - - def name(self): - """ - Returns the algorithm name, used for identifying the algorithm. This - string should be fixed for the algorithm, and must not be localised. - The name should be unique within each provider. Names should contain - lowercase alphanumeric characters only and no spaces or other - formatting characters. - """ - return "Outdoor Thermal Comfort: SOLWEIG" - - def displayName(self): - """ - Returns the translated algorithm name, which should be used for any - user-visible display of the algorithm name. - """ - return self.tr("Outdoor Thermal Comfort: SOLWEIG v2025a") - - def group(self): - """ - Returns the name of the group this algorithm belongs to. This string - should be localised. - """ - return self.tr(self.groupId()) - - def groupId(self): - """ - Returns the unique ID of the group this algorithm belongs to. This - string should be fixed for the algorithm, and must not be localised. - The group id should be unique within each provider. Group id should - contain lowercase alphanumeric characters only and no spaces or other - formatting characters. - """ - return "Processor" - - def shortHelpString(self): - return self.tr( - "SOLWEIG (v2025a) is a model which can be used to estimate spatial variations of 3D radiation fluxes and " - "mean radiant temperature (Tmrt) in complex urban settings. The SOLWEIG model follows the same " - "approach commonly adopted to observe Tmrt, with shortwave and longwave radiation fluxes from " - "six directions being individually calculated to derive Tmrt. The model requires a limited number " - "of inputs, such as direct, diffuse and global shortwave radiation, air temperature, relative " - "humidity, urban geometry and geographical information (latitude, longitude and elevation). " - "Additional vegetation and ground cover information can also be used to imporove the estimation of Tmrt.\n" - "\n" - "Tools to generate sky view factors, wall height and aspect etc. is available in the pre-processing past in UMEP\n" - "\n" - "------------\n" - "\n" - "Full manual available via the Help-button." - ) - - def helpUrl(self): - url = "https://umep-docs.readthedocs.io/en/latest/processor/Outdoor%20Thermal%20Comfort%20SOLWEIG.html" - return url - - def tr(self, string): - return QCoreApplication.translate("Processing", string) - - def icon(self): - cmd_folder = Path( - os.path.split(inspect.getfile(inspect.currentframe()))[0] - ).parent - icon = QIcon(str(cmd_folder) + "/icons/icon_solweig.png") - return icon - - def createInstance(self): - return ProcessingSOLWEIGAlgorithm() diff --git a/processor/suews_algorithm.py b/processor/suews_algorithm.py index eff0a82..98ef0f4 100644 --- a/processor/suews_algorithm.py +++ b/processor/suews_algorithm.py @@ -49,13 +49,12 @@ from supy.data_model import init_config_from_yaml # import supy as sp # from supy import __version__ as ver_supy -except BaseException: +except: pass from pathlib import Path # from ..util import f90nml -import sys -import os +import sys, os from qgis.PyQt.QtGui import QIcon import inspect from pathlib import Path @@ -459,7 +458,7 @@ def processAlgorithm(self, parameters, context, feedback): try: import supy as sp from supy import __version__ as ver_supy - except BaseException: + except: raise QgsProcessingException( "This plugin requires the supy package " "to be installed OR upgraded. Please consult the FAQ in the manual " @@ -542,7 +541,7 @@ def processAlgorithm(self, parameters, context, feedback): with open(infile, "w") as file: yaml.dump(yaml_dict, file, sort_keys=False) - ####################################################################### + ##################################################################################### # SuPy feedback.setProgressText("Initiating model") @@ -566,10 +565,9 @@ def processAlgorithm(self, parameters, context, feedback): else: chunkDay = 3660 - ####################################################################### + ##################################################################################### # SuPy initialisation - # Path(pathtoplugin + f'/Input/{filecode}_suews_simple.yml') - yaml_path = infile + yaml_path = infile # Path(pathtoplugin + f'/Input/{filecode}_suews_simple.yml') # from supy.data_model import init_config_from_yaml # from supy import SUEWSKernelError @@ -602,7 +600,7 @@ def processAlgorithm(self, parameters, context, feedback): # df_output, # df_state_final, # path_dir_save = yaml_dict['model']['control']['output_file']['path']) - ####################################################################### + ##################################################################################### feedback.setProgressText("Model finished") diff --git a/processor/target_algorithm.py b/processor/target_algorithm.py index 35f618f..b93d3a5 100644 --- a/processor/target_algorithm.py +++ b/processor/target_algorithm.py @@ -6,7 +6,7 @@ __revision__ = "$Format:%H$" -from qgis.PyQt.QtCore import QCoreApplication +from qgis.PyQt.QtCore import QCoreApplication, QVariant from qgis.core import ( QgsProcessing, QgsProcessingAlgorithm, @@ -20,6 +20,7 @@ from qgis.PyQt.QtGui import QIcon from osgeo import osr +from osgeo.gdalconst import * import os import numpy as np import pandas as pd @@ -28,6 +29,10 @@ import datetime from ..util.umep_target_export_component import write_config_file from ..util.misc import xy2latlon +import sys +import shutil +import traceback +import math # from ..functions.target import Target # from target import Target @@ -35,7 +40,7 @@ try: from target_py import Target from target_py.ui.utils import read_config -except BaseException: +except: pass @@ -220,8 +225,8 @@ def processAlgorithm(self, parameters, context, feedback): # Converting UMEP met-file to target met-file try: - metfile = pd.read_csv(inputMet, sep="\\s+") - except BaseException: + metfile = pd.read_csv(inputMet, sep="\s+") + except: raise QgsProcessingException( "Error: Make sure format of meteorological file is correct. You can" "prepare your data by using 'Prepare Existing Data' in " @@ -329,16 +334,18 @@ def processAlgorithm(self, parameters, context, feedback): feedback.setProgressText("Model calculation started.") start = datetime.datetime.now() tar = Target( - # passing the simulation's config file - os.path.join(inputDir, "config.ini"), + os.path.join( + inputDir, "config.ini" + ), # passing the simulation's config file progress=True, # show progress bars in console ) tar.load_config() # run simulation if outputCSV: - # save model results in csv format - tar.run_simulation(save_csv=True) + tar.run_simulation( + save_csv=True + ) # save model results in csv format else: tar.run_simulation(save_csv=False) @@ -371,7 +378,7 @@ def processAlgorithm(self, parameters, context, feedback): dataout = np.load( inputDir + "/output/" + runName + ".npy", allow_pickle=True ) - dfin = pd.read_csv(inputMet, sep="\\s+") + dfin = pd.read_csv(inputMet, sep="\s+") dfin["datetime"] = pd.to_datetime( dfin[["%iy", "id", "it", "imin"]] .astype(str) @@ -388,8 +395,9 @@ def processAlgorithm(self, parameters, context, feedback): ) index = 1 - # looping through each grid saving data - for f in range(0, dataout.shape[1]): + for f in range( + 0, dataout.shape[1] + ): # looping through each grid saving data feedback.setProgress(int((index * 100) / dataout.shape[1])) if feedback.isCanceled(): feedback.setProgressText("Calculation cancelled") diff --git a/processor/urock_processing_algorithm.py b/processor/urock_processing_algorithm.py index 2f481fe..2d4677a 100644 --- a/processor/urock_processing_algorithm.py +++ b/processor/urock_processing_algorithm.py @@ -31,6 +31,7 @@ __revision__ = "$Format:%H$" import os +from qgis.PyQt.QtWidgets import QMessageBox from qgis.PyQt.QtCore import QCoreApplication from qgis.core import ( QgsProcessing, @@ -59,6 +60,7 @@ import inspect import processing import re +import subprocess from ..functions.URock import MainCalculation @@ -69,11 +71,11 @@ saveJavaDir, ) from ..functions.URock import WriteMetadataURock +from ..functions.URock import DataUtil + # All SQL identifiers are validated via saf_id() to prevent injection. # No user input is used directly in SQL construction. - - def saf_id(name): if name: if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name): @@ -350,19 +352,19 @@ def processAlgorithm(self, parameters, context, feedback): """ try: - pass - except BaseException: + import jaydebeapi + except: raise QgsProcessingException( "'jaydebeapi' Python package is missing. Most tools still work. Visit the UMEP manual (Getting Started) for instructions on how to install." ) try: - pass + import numba except Exception: raise QgsProcessingException( "'numba' Python package is missing. Most tools still work. Visit the UMEP manual (Getting Started) for instructions on how to install." ) try: - pass + import xarray except Exception: raise QgsProcessingException( "'xarray' Python package is missing. Most tools still work. Visit the UMEP manual (Getting Started) for instructions on how to install." @@ -573,12 +575,7 @@ def processAlgorithm(self, parameters, context, feedback): outputRaster.extent().yMaximum() - outputRaster.extent().yMinimum() ) / outputRaster.height() - if xres != yres: - raise QgsProcessingException( - "The output raster template should have the same x and y resolution" - ) - # If there is a raster and no meshSize, take the mean of x and y - # raster resolution + # If there is a raster and no meshSize, take the mean of x and y raster resolution if not meshSize: meshSize = float(xres + yres) / 2 elif not meshSize: diff --git a/processor/uwg_algorithm.py b/processor/uwg_algorithm.py index 50aa40f..a46cc08 100644 --- a/processor/uwg_algorithm.py +++ b/processor/uwg_algorithm.py @@ -23,7 +23,9 @@ from qgis.PyQt.QtGui import QIcon # from osgeo import gdal, osr, ogr +from osgeo.gdalconst import * import os +import sys import numpy as np import inspect from pathlib import Path @@ -34,7 +36,7 @@ try: from uwg import UWG -except BaseException: +except: pass @@ -142,8 +144,9 @@ def initAlgorithm(self, config): def processAlgorithm(self, parameters, context, feedback): try: + import uwg + except: pass - except BaseException: raise QgsProcessingException( "uwg python library not found: Instructions on how to install missing python libraries using the pip command: https://umep-docs.readthedocs.io/en/latest/Getting_Started.html" ) @@ -217,7 +220,7 @@ def processAlgorithm(self, parameters, context, feedback): if numtype[0] == 0.0: attr = int(f.attributes()[idx]) - # generate input files for UWG + ## generate input files for UWG uwgDict = read_uwg_file(inputDir, prefix + "_" + str(attr)) uwgDict["Month"] = mm uwgDict["Day"] = dd diff --git a/symbology-style.db b/symbology-style.db new file mode 100644 index 0000000..4222679 Binary files /dev/null and b/symbology-style.db differ diff --git a/util/RoughnessCalcFunctionV2.py b/util/RoughnessCalcFunctionV2.py index d1d91a6..180a016 100644 --- a/util/RoughnessCalcFunctionV2.py +++ b/util/RoughnessCalcFunctionV2.py @@ -1,28 +1,27 @@ #### Function that calculates z0 and zd according to different methods #### -#### INPUTS#### -# Roughness Methods: +####INPUTS#### +##Roughness Methods: # 'RT' - Rule of thumb # 'Rau' - Raupach (1994/95) # 'Bot' - Simplified Bottema (1995) # 'Mac' - MacDonald et al. (1998) # 'Mho' - Millward-Hopkins et al. (2011) [SIMPLIFIED] # 'Kan' - Kanda et al. (2013) -# Building information +##Building information # zH - average building height, # fai - frontal area, # pai - plan area, # zMax - max building height, # zSdev - standard dev of building heights -#### OUTPUTS#### +####OUTPUTS#### # zd = zero-plane displacement height # z0 = roughness length import numpy as np import math -#### FUNCTION#### - +####FUNCTION#### def RoughnessCalcMany(Roughnessmethod, zH, fai, pai, zMax, zSdev): z_d_output = np.zeros((fai.shape[0], 1)) - 999.0 diff --git a/util/SEBESOLWEIGCommonFiles/Perez_v3.py b/util/SEBESOLWEIGCommonFiles/Perez_v3.py index bf0bd27..c9d60ab 100644 --- a/util/SEBESOLWEIGCommonFiles/Perez_v3.py +++ b/util/SEBESOLWEIGCommonFiles/Perez_v3.py @@ -209,10 +209,7 @@ def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice, patch_option): if Idh <= 10: # m_a=0;m_b=0;m_c=0;m_d=0;m_e=0; PerezBrightness = 0 - # if altitude < 0: - # print("Airmass") - # print(AirMass) - # print(PerezBrightness) + # sky clearness bins if PerezClearness < 1.065: intClearness = 0 diff --git a/functions/SOLWEIGpython/Perez_v3_moved.py b/util/SEBESOLWEIGCommonFiles/Perez_v3_torch.py similarity index 55% rename from functions/SOLWEIGpython/Perez_v3_moved.py rename to util/SEBESOLWEIGCommonFiles/Perez_v3_torch.py index 94ee1e3..1b777d7 100644 --- a/functions/SOLWEIGpython/Perez_v3_moved.py +++ b/util/SEBESOLWEIGCommonFiles/Perez_v3_torch.py @@ -1,8 +1,15 @@ from __future__ import division -import numpy as np +from .create_patches_torch import create_patches +try: + import torch +except: + pass -def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): + +def Perez_v3( + zen, azimuth, radD, radI, jday, patchchoice, patch_option, device +): """ This function calculates distribution of luminance on the skyvault based on Perez luminince distribution model. @@ -77,34 +84,52 @@ def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): :return: """ - m_a1 = np.array( - [1.3525, -1.2219, -1.1000, -0.5484, -0.6000, -1.0156, -1.0000, -1.0500] + m_a1 = torch.tensor( + [ + 1.3525, + -1.2219, + -1.1000, + -0.5484, + -0.6000, + -1.0156, + -1.0000, + -1.0500, + ], + device=device, ) - m_a2 = np.array( - [-0.2576, -0.7730, -0.2515, -0.6654, -0.3566, -0.3670, 0.0211, 0.0289] + m_a2 = torch.tensor( + [-0.2576, -0.7730, -0.2515, -0.6654, -0.3566, -0.3670, 0.0211, 0.0289], + device=device, ) - m_a3 = np.array( - [-0.2690, 1.4148, 0.8952, -0.2672, -2.5000, 1.0078, 0.5025, 0.4260] + m_a3 = torch.tensor( + [-0.2690, 1.4148, 0.8952, -0.2672, -2.5000, 1.0078, 0.5025, 0.4260], + device=device, ) - m_a4 = np.array( - [-1.4366, 1.1016, 0.0156, 0.7117, 2.3250, 1.4051, -0.5119, 0.3590] + m_a4 = torch.tensor( + [-1.4366, 1.1016, 0.0156, 0.7117, 2.3250, 1.4051, -0.5119, 0.3590], + device=device, ) - m_b1 = np.array( - [-0.7670, -0.2054, 0.2782, 0.7234, 0.2937, 0.2875, -0.3000, -0.3250] + m_b1 = torch.tensor( + [-0.7670, -0.2054, 0.2782, 0.7234, 0.2937, 0.2875, -0.3000, -0.3250], + device=device, ) - m_b2 = np.array( - [0.0007, 0.0367, -0.1812, -0.6219, 0.0496, -0.5328, 0.1922, 0.1156] + m_b2 = torch.tensor( + [0.0007, 0.0367, -0.1812, -0.6219, 0.0496, -0.5328, 0.1922, 0.1156], + device=device, ) - m_b3 = np.array( - [1.2734, -3.9128, -4.5000, -5.6812, -5.6812, -3.8500, 0.7023, 0.7781] + m_b3 = torch.tensor( + [1.2734, -3.9128, -4.5000, -5.6812, -5.6812, -3.8500, 0.7023, 0.7781], + device=device, ) - m_b4 = np.array( - [-0.1233, 0.9156, 1.1766, 2.6297, 1.8415, 3.3750, -1.6317, 0.0025] + m_b4 = torch.tensor( + [-0.1233, 0.9156, 1.1766, 2.6297, 1.8415, 3.3750, -1.6317, 0.0025], + device=device, ) - m_c1 = np.array( - [2.8000, 6.9750, 24.7219, 33.3389, 21.0000, 14.0000, 19.0000, 31.0625] + m_c1 = torch.tensor( + [2.8000, 6.9750, 24.7219, 33.3389, 21.0000, 14.0000, 19.0000, 31.0625], + device=device, ) - m_c2 = np.array( + m_c2 = torch.tensor( [ 0.6004, 0.1774, @@ -114,9 +139,10 @@ def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): -0.9999, -5.0000, -14.5000, - ] + ], + device=device, ) - m_c3 = np.array( + m_c3 = torch.tensor( [ 1.2375, 6.4477, @@ -126,108 +152,113 @@ def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): -7.1406, 1.2438, -46.1148, - ] + ], + device=device, ) - m_c4 = np.array( - [1.0000, -0.1239, 34.8438, 52.0781, 7.2492, 7.5469, -1.9094, 55.3750] + m_c4 = torch.tensor( + [1.0000, -0.1239, 34.8438, 52.0781, 7.2492, 7.5469, -1.9094, 55.3750], + device=device, ) - m_d1 = np.array( - [1.8734, -1.5798, -5.0000, -3.5000, -3.5000, -3.4000, -4.0000, -7.2312] + m_d1 = torch.tensor( + [ + 1.8734, + -1.5798, + -5.0000, + -3.5000, + -3.5000, + -3.4000, + -4.0000, + -7.2312, + ], + device=device, ) - m_d2 = np.array( - [0.6297, -0.5081, 1.5218, 0.0016, -0.1554, -0.1078, 0.0250, 0.4050] + m_d2 = torch.tensor( + [0.6297, -0.5081, 1.5218, 0.0016, -0.1554, -0.1078, 0.0250, 0.4050], + device=device, ) - m_d3 = np.array( - [0.9738, -1.7812, 3.9229, 1.1477, 1.4062, -1.0750, 0.3844, 13.3500] + m_d3 = torch.tensor( + [0.9738, -1.7812, 3.9229, 1.1477, 1.4062, -1.0750, 0.3844, 13.3500], + device=device, ) - m_d4 = np.array( - [0.2809, 0.1080, -2.6204, 0.1062, 0.3988, 1.5702, 0.2656, 0.6234] + m_d4 = torch.tensor( + [0.2809, 0.1080, -2.6204, 0.1062, 0.3988, 1.5702, 0.2656, 0.6234], + device=device, ) - m_e1 = np.array( - [0.0356, 0.2624, -0.0156, 0.4659, 0.0032, -0.0672, 1.0468, 1.5000] + m_e1 = torch.tensor( + [0.0356, 0.2624, -0.0156, 0.4659, 0.0032, -0.0672, 1.0468, 1.5000], + device=device, ) - m_e2 = np.array( - [-0.1246, 0.0672, 0.1597, -0.3296, 0.0766, 0.4016, -0.3788, -0.6426] + m_e2 = torch.tensor( + [-0.1246, 0.0672, 0.1597, -0.3296, 0.0766, 0.4016, -0.3788, -0.6426], + device=device, ) - m_e3 = np.array( - [-0.5718, -0.2190, 0.4199, -0.0876, -0.0656, 0.3017, -2.4517, 1.8564] + m_e3 = torch.tensor( + [-0.5718, -0.2190, 0.4199, -0.0876, -0.0656, 0.3017, -2.4517, 1.8564], + device=device, ) - m_e4 = np.array( - [0.9938, -0.4285, -0.5562, -0.0329, -0.1294, -0.4844, 1.4656, 0.5636] + m_e4 = torch.tensor( + [0.9938, -0.4285, -0.5562, -0.0329, -0.1294, -0.4844, 1.4656, 0.5636], + device=device, ) - acoeff = np.transpose(np.atleast_2d([m_a1, m_a2, m_a3, m_a4])) - bcoeff = np.transpose(np.atleast_2d([m_b1, m_b2, m_b3, m_b4])) - ccoeff = np.transpose(np.atleast_2d([m_c1, m_c2, m_c3, m_c4])) - dcoeff = np.transpose(np.atleast_2d([m_d1, m_d2, m_d3, m_d4])) - ecoeff = np.transpose(np.atleast_2d([m_e1, m_e2, m_e3, m_e4])) + acoeff = torch.stack([m_a1, m_a2, m_a3, m_a4], dim=1) + bcoeff = torch.stack([m_b1, m_b2, m_b3, m_b4], dim=1) + ccoeff = torch.stack([m_c1, m_c2, m_c3, m_c4], dim=1) + dcoeff = torch.stack([m_d1, m_d2, m_d3, m_d4], dim=1) + ecoeff = torch.stack([m_e1, m_e2, m_e3, m_e4], dim=1) - deg2rad = np.pi / 180 - rad2deg = 180 / np.pi + deg2rad = torch.tensor(torch.pi / 180, device=device).clone().detach() + rad2deg = torch.tensor(180 / torch.pi, device=device).clone().detach() altitude = 90 - zen - zen = zen * deg2rad - azimuth = azimuth * deg2rad - altitude = altitude * deg2rad + zen = torch.tensor(zen, device=device) * deg2rad + azimuth = torch.tensor(azimuth, device=device) * deg2rad + altitude = torch.tensor(altitude, device=device) * deg2rad Idh = radD - # Ibh = radI/sin(altitude) Ibn = radI - # Skyclearness - PerezClearness = ((Idh + Ibn) / (Idh + 1.041 * np.power(zen, 3))) / ( - 1 + 1.041 * np.power(zen, 3) + PerezClearness = ((Idh + Ibn) / (Idh + 1.041 * torch.pow(zen, 3))) / ( + 1 + 1.041 * torch.pow(zen, 3) ) - # Extra terrestrial radiation - day_angle = jday * 2 * np.pi / 365 - # I0=1367*(1+0.033*np.cos((2*np.pi*jday)/365)) + + day_angle = jday * 2 * torch.pi / 365 I0 = 1367 * ( 1.00011 - + 0.034221 * np.cos(day_angle) - + 0.00128 * np.sin(day_angle) - + 0.000719 * - # New from robinsson - np.cos(2 * day_angle) - + 0.000077 * np.sin(2 * day_angle) + + 0.034221 * torch.cos(torch.tensor(day_angle)) + + 0.00128 * torch.sin(torch.tensor(day_angle)) + + 0.000719 * torch.cos(2 * torch.tensor(day_angle)) + + 0.000077 * torch.sin(2 * torch.tensor(day_angle)) ) - # Optical air mass - # m=1/altitude; old if altitude >= 10 * deg2rad: - AirMass = 1 / np.sin(altitude) - elif altitude < 0: # below equation becomes complex - AirMass = 1 / np.sin(altitude) + 0.50572 * np.power( - 180 * complex(altitude) / np.pi + 6.07995, -1.6364 + AirMass = 1 / torch.sin(altitude) + elif altitude < 0: + AirMass = 1 / torch.sin(altitude) + 0.50572 * torch.pow( + 180 * torch.complex(altitude, 0) / torch.pi + 6.07995, -1.6364 ) else: - AirMass = 1 / np.sin(altitude) + 0.50572 * np.power( - 180 * altitude / np.pi + 6.07995, -1.6364 + AirMass = 1 / torch.sin(altitude) + 0.50572 * torch.pow( + 180 * altitude / torch.pi + 6.07995, -1.6364 ) - # Skybrightness - # if altitude*rad2deg+6.07995>=0 - PerezBrightness = (AirMass * radD) / I0 + PerezBrightness = (AirMass * Idh) / I0 if Idh <= 10: - # m_a=0;m_b=0;m_c=0;m_d=0;m_e=0; - PerezBrightness = 0 - # if altitude < 0: - # print("Airmass") - # print(AirMass) - # print(PerezBrightness) - # sky clearness bins + PerezBrightness = torch.tensor(0.0, device=device) + if PerezClearness < 1.065: intClearness = 0 - if PerezClearness > 1.065 and PerezClearness < 1.230: + elif PerezClearness < 1.230: intClearness = 1 - if PerezClearness > 1.230 and PerezClearness < 1.500: + elif PerezClearness < 1.500: intClearness = 2 - if PerezClearness > 1.500 and PerezClearness < 1.950: + elif PerezClearness < 1.950: intClearness = 3 - if PerezClearness > 1.950 and PerezClearness < 2.800: + elif PerezClearness < 2.800: intClearness = 4 - if PerezClearness > 2.800 and PerezClearness < 4.500: + elif PerezClearness < 4.500: intClearness = 5 - if PerezClearness > 4.500 and PerezClearness < 6.200: + elif PerezClearness < 6.200: intClearness = 6 - if PerezClearness > 6.200: + else: intClearness = 7 m_a = ( @@ -263,10 +294,9 @@ def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): * (dcoeff[intClearness, 2] + dcoeff[intClearness, 3] * zen) ) else: - # different equations for c & d in clearness bin no. 1, from Robinsson m_c = ( - np.exp( - np.power( + torch.exp( + torch.pow( PerezBrightness * ( ccoeff[intClearness, 0] + ccoeff[intClearness, 1] * zen @@ -277,7 +307,7 @@ def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): - 1 ) m_d = ( - -np.exp( + -torch.exp( PerezBrightness * (dcoeff[intClearness, 0] + dcoeff[intClearness, 1] * zen) ) @@ -285,70 +315,39 @@ def Perez_v3(zen, azimuth, radD, radI, jday, patchchoice): + PerezBrightness * dcoeff[intClearness, 3] * PerezBrightness ) - # print 'a = ', m_a - # print 'b = ', m_b - # print 'e = ', m_e - # print 'c = ', m_c - # print 'd = ', m_d - - skyvaultalt = np.atleast_2d([]) - skyvaultazi = np.atleast_2d([]) if patchchoice == 2: - # Creating skyvault at one degree intervals - skyvaultalt = np.ones([90, 361]) * 90 - skyvaultazi = np.empty((90, 361)) + skyvaultalt = torch.zeros((90, 361), device=device) + skyvaultazi = torch.zeros((90, 361), device=device) for j in range(90): skyvaultalt[j, :] = 91 - j - skyvaultazi[j, :] = range(361) + skyvaultazi[j, :] = torch.arange(361) elif patchchoice == 1: - # Creating skyvault of patches of constant radians (Tregeneza and - # Sharples, 1993) - skyvaultaltint = [6, 18, 30, 42, 54, 66, 78] - skyvaultaziint = [12, 12, 15, 15, 20, 30, 60] - for j in range(7): - for k in range(1, int(360 / skyvaultaziint[j]) + 1): - skyvaultalt = np.append(skyvaultalt, skyvaultaltint[j]) - skyvaultazi = np.append(skyvaultazi, k * skyvaultaziint[j]) - - skyvaultalt = np.append(skyvaultalt, 90) - skyvaultazi = np.append(skyvaultazi, 360) + skyvaultalt, skyvaultazi, _, _, _, _, _ = create_patches( + patch_option, device + ) skyvaultzen = (90 - skyvaultalt) * deg2rad skyvaultalt = skyvaultalt * deg2rad skyvaultazi = skyvaultazi * deg2rad - # Angular distance from the sun from Robinsson - cosSkySunAngle = np.sin(skyvaultalt) * np.sin(altitude) + np.cos( + cosSkySunAngle = torch.sin(skyvaultalt) * torch.sin(altitude) + torch.cos( altitude - ) * np.cos(skyvaultalt) * np.cos(np.abs(skyvaultazi - azimuth)) + ) * torch.cos(skyvaultalt) * torch.cos(torch.abs(skyvaultazi - azimuth)) - # Main equation - lv = (1 + m_a * np.exp(m_b / np.cos(skyvaultzen))) * ( + lv = (1 + m_a * torch.exp(m_b / torch.cos(skyvaultzen))) * ( ( 1 - + m_c * np.exp(m_d * np.arccos(cosSkySunAngle)) + + m_c * torch.exp(m_d * torch.arccos(cosSkySunAngle)) + m_e * cosSkySunAngle * cosSkySunAngle ) ) - # Normalisation - lv = lv / np.sum(lv) - - # plotting - # axesm('stereo','Origin',[90 180],'MapLatLimit',[0 90],'Aspect','transverse') - # framem off; gridm on; mlabel off; plabel off;axis on; - # setm(gca,'MLabelParallel',-20) - # geoshow(skyvaultalt*rad2deg,skyvaultazi*rad2deg,lv,'DisplayType','texture'); - # colorbar - # set(gcf,'Color',[1 1 1]) - # pause(1) + lv = lv / torch.sum(lv) if patchchoice == 1: - # x = np.atleast_2d([]) - # lv = np.transpose(np.append(np.append(np.append(x, skyvaultalt*rad2deg), skyvaultazi*rad2deg), lv)) - x = np.transpose(np.atleast_2d(skyvaultalt * rad2deg)) - y = np.transpose(np.atleast_2d(skyvaultazi * rad2deg)) - z = np.transpose(np.atleast_2d(lv)) - lv = np.append(np.append(x, y, axis=1), z, axis=1) + x = torch.transpose(torch.unsqueeze(skyvaultalt * rad2deg, 0), 0, 1) + y = torch.transpose(torch.unsqueeze(skyvaultazi * rad2deg, 0), 0, 1) + z = torch.transpose(torch.unsqueeze(lv, 0), 0, 1) + lv = torch.cat((x, y, z), dim=1) return lv, PerezClearness, PerezBrightness diff --git a/util/SEBESOLWEIGCommonFiles/Solweig_v2015_metdata_noload_torch.py b/util/SEBESOLWEIGCommonFiles/Solweig_v2015_metdata_noload_torch.py new file mode 100644 index 0000000..0d563a0 --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/Solweig_v2015_metdata_noload_torch.py @@ -0,0 +1,117 @@ +from __future__ import absolute_import + +# from importdata import importdata +from . import sun_position_torch as sp + +# import sun_position as sp +import datetime +import calendar + +try: + import torch +except: + pass + + +def Solweig_2015a_metdata_noload(inputdata, location, UTC): + """ + This function is used to process the input meteorological file. + It also calculates Sun position based on the time specified in the met-file + + :param inputdata: + :param location: + :param UTC: + :return: + """ + + met = inputdata + device = ( + met.device if isinstance(met, torch.Tensor) else torch.device("cpu") + ) + data_len = len(met[:, 0]) + dectime = torch.tensor( + met[:, 1] + met[:, 2] / 24 + met[:, 3] / (60 * 24.0) + ) + dectimemin = met[:, 3] / (60 * 24.0) + if data_len == 1: + halftimestepdec = 0 + else: + halftimestepdec = (dectime[1] - dectime[0]) / 2.0 + time = dict() + time["sec"] = 0 + time["UTC"] = UTC + sunmaximum = 0.0 + leafon1 = 97 # TODO this should change + leafoff1 = 300 # TODO this should change + + # initialize matrices + altitude = torch.empty(size=(1, data_len), device=device) + azimuth = torch.empty(size=(1, data_len), device=device) + zen = torch.empty(size=(1, data_len), device=device) + jday = torch.empty(size=(1, data_len), device=device) + YYYY = torch.empty(size=(1, data_len), device=device) + leafon = torch.empty(size=(1, data_len), device=device) + altmax = torch.empty(size=(1, data_len), device=device) + + sunmax = dict() + + for i, row in enumerate(met[:, 0]): + if met[i, 1] == 221: + test = 4 + YMD = datetime.datetime(int(met[i, 0]), 1, 1) + datetime.timedelta( + int(met[i, 1]) - 1 + ) + # Finding maximum altitude in 15 min intervals (20141027) + if (i == 0) or ( + torch.remainder(dectime[i], torch.floor(dectime[i])) == 0 + ): + fifteen = 0.0 + sunmaximum = -90.0 + sunmax["zenith"] = 90.0 + while sunmaximum <= 90.0 - sunmax["zenith"]: + sunmaximum = 90.0 - sunmax["zenith"] + fifteen = fifteen + 15.0 / 1440.0 + HM = datetime.timedelta(days=(60 * 10) / 1440.0 + fifteen) + YMDHM = YMD + HM + time["year"] = YMDHM.year + time["month"] = YMDHM.month + time["day"] = YMDHM.day + time["hour"] = YMDHM.hour + time["min"] = YMDHM.minute + sunmax = sp.sun_position(time, location) + altmax[0, i] = sunmaximum + + half = datetime.timedelta(days=float(halftimestepdec)) + H = datetime.timedelta(hours=int(met[i, 2])) + M = datetime.timedelta(minutes=int(met[i, 3])) + YMDHM = YMD + H + M - half + time["year"] = YMDHM.year + time["month"] = YMDHM.month + time["day"] = YMDHM.day + time["hour"] = YMDHM.hour + time["min"] = YMDHM.minute + sun = sp.sun_position(time, location) + sun_zenith = sun["zenith"] + sun_azimuth = sun["azimuth"] + + if (sun_zenith > 89.0) & (sun_zenith <= 90.0): + sun_zenith = 89.0 + + altitude[0, i] = 90.0 - sun_zenith + zen[0, i] = sun_zenith * (torch.pi / 180.0) + azimuth[0, i] = sun_azimuth + + YYYY[0, i] = met[i, 0] + doy = YMD.timetuple().tm_yday + jday[0, i] = doy + if (doy > leafon1) | (doy < leafoff1): + leafon[0, i] = 1 + else: + leafon[0, i] = 0 + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + return YYYY, altitude, azimuth, zen, jday, leafon, dectime, altmax diff --git a/util/SEBESOLWEIGCommonFiles/clearnessindex_2013b_torch.py b/util/SEBESOLWEIGCommonFiles/clearnessindex_2013b_torch.py new file mode 100644 index 0000000..14568e6 --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/clearnessindex_2013b_torch.py @@ -0,0 +1,113 @@ +from __future__ import absolute_import + +author = "xlinfr" + +from . import sun_distance_torch +import math + +try: + import torch +except: + pass + + +def clearnessindex_2013b(zen, jday, Ta, RH, radG, location, P): + """Clearness Index at the Earth's surface calculated from Crawford and Duchon 1999 + + :param zen: zenith angle in radians + :param jday: day of year + :param Ta: air temperature + :param RH: relative humidity + :param radG: global shortwave radiation + :param location: distionary including lat, lon and alt + :param P: pressure + :return: + """ + + if P == -999.0: + p = 1013.0 # Pressure in millibars + else: + p = P * 10.0 # Convert from hPa to millibars + + Itoa = 1370.0 # Effective solar constant + D = sun_distance_torch.sun_distance( + jday + ) # irradiance differences due to Sun-Earth distances + m = ( + 35.0 + * torch.cos(zen) + * ((1224.0 * (torch.cos(zen) ** 2) + 1) ** (-1 / 2.0)) + ) # optical air mass at p=1013 + Trpg = ( + 1.021 - 0.084 * (m * (0.000949 * p + 0.051)) ** 0.5 + ) # Transmission coefficient for Rayliegh scattering and permanent gases + + # empirical constant depending on latitude + if location["latitude"] < 10.0: + G = [3.37, 2.85, 2.80, 2.64] + elif location["latitude"] >= 10.0 and location["latitude"] < 20.0: + G = [2.99, 3.02, 2.70, 2.93] + elif location["latitude"] >= 20.0 and location["latitude"] < 30.0: + G = [3.60, 3.00, 2.98, 2.93] + elif location["latitude"] >= 30.0 and location["latitude"] < 40.0: + G = [3.04, 3.11, 2.92, 2.94] + elif location["latitude"] >= 40.0 and location["latitude"] < 50.0: + G = [2.70, 2.95, 2.77, 2.71] + elif location["latitude"] >= 50.0 and location["latitude"] < 60.0: + G = [2.52, 3.07, 2.67, 2.93] + elif location["latitude"] >= 60.0 and location["latitude"] < 70.0: + G = [1.76, 2.69, 2.61, 2.61] + elif location["latitude"] >= 70.0 and location["latitude"] < 80.0: + G = [1.60, 1.67, 2.24, 2.63] + elif location["latitude"] >= 80.0 and location["latitude"] < 90.0: + G = [1.11, 1.44, 1.94, 2.02] + + if jday > 335 or jday <= 60: + G = G[0] + elif jday > 60 and jday <= 152: + G = G[1] + elif jday > 152 and jday <= 244: + G = G[2] + elif jday > 244 and jday <= 335: + G = G[3] + device = ( + zen.device if isinstance(zen, torch.Tensor) else torch.device("cpu") + ) + G = torch.tensor(G, device=device) + + # dewpoint calculation + a2 = 17.27 + b2 = 237.7 + Td = (b2 * (((a2 * Ta) / (b2 + Ta)) + torch.log(RH))) / ( + a2 - (((a2 * Ta) / (b2 + Ta)) + torch.log(RH)) + ) + Td = (Td * 1.8) + 32 # Dewpoint (F) + u = torch.exp( + 0.1133 - torch.log(G + 1) + 0.0393 * Td + ) # Precipitable water + Tw = 1 - 0.077 * ( + (u * m) ** 0.3 + ) # Transmission coefficient for water vapor + Tar = 0.935**m # Transmission coefficient for aerosols + + I0 = Itoa * torch.cos(zen) * Trpg * Tw * D * Tar + if abs(zen) > torch.pi / 2: + I0 = 0 + # b=I0==abs(zen)>torch.pi/2 + # I0(b==1)=0 + # clear b; + if not (torch.isreal(I0)): + I0 = 0 + + corr = 0.1473 * torch.log(90 - (zen / torch.pi * 180)) + 0.3454 # 20070329 + + CIuncorr = radG / I0 + CI = CIuncorr + (1 - corr) + I0et = Itoa * torch.cos(zen) * D # extra terrestial solar radiation + Kt = radG / I0et + if math.isnan(CI): + CI = float("Inf") + + del p, D, m, Trpg, G, Td, u, Tw, Tar, corr, device + + return I0, CI, Kt, I0et, CIuncorr diff --git a/util/SEBESOLWEIGCommonFiles/create_patches.py b/util/SEBESOLWEIGCommonFiles/create_patches.py index 8f167d7..350d303 100644 --- a/util/SEBESOLWEIGCommonFiles/create_patches.py +++ b/util/SEBESOLWEIGCommonFiles/create_patches.py @@ -80,10 +80,6 @@ def create_patches(patch_option): skyvaultazi, k * skyvaultaziint[j] + azistart[j] ) - # skyvaultzen = (90 - skyvaultalt) * deg2rad - # skyvaultalt = skyvaultalt * deg2rad - # skyvaultazi = skyvaultazi * deg2rad - return ( skyvaultalt, skyvaultazi, diff --git a/util/SEBESOLWEIGCommonFiles/create_patches_torch.py b/util/SEBESOLWEIGCommonFiles/create_patches_torch.py new file mode 100644 index 0000000..fbf9cb9 --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/create_patches_torch.py @@ -0,0 +1,120 @@ +try: + import torch +except: + pass + + +def create_patches(patch_option, device): + + deg2rad = torch.pi / 180 + + # patch_option = 1 = 145 patches (Robinson & Stone, 2004) + # patch_option = 2 = 153 patches (Wallenberg et al., 2022) + # patch_option = 3 = 306 patches -> test + # patch_option = 4 = 612 patches -> test + + skyvaultalt = torch.atleast_2d([]) + skyvaultazi = torch.atleast_2d([]) + + # Creating skyvault of patches of constant radians (Tregeneza and Sharples, 1993) + # Patch option 1, 145 patches, Original Robinson & Stone (2004) after Tregenza (1987)/Tregenza & Sharples (1993) + if patch_option == 1: + annulino = torch.tensor( + [0, 12, 24, 36, 48, 60, 72, 84, 90], device=device + ) + skyvaultaltint = torch.tensor( + [6, 18, 30, 42, 54, 66, 78, 90], device=device + ) # Robinson & Stone (2004) + azistart = torch.tensor( + [0, 4, 2, 5, 8, 0, 10, 0], device=device + ) # Fredrik/Nils + patches_in_band = torch.tensor( + [30, 30, 24, 24, 18, 12, 6, 1], device=device + ) # Robinson & Stone (2004) + # Patch option 2, 153 patches, Wallenberg et al. (2022) + elif patch_option == 2: + annulino = torch.tensor( + [0, 12, 24, 36, 48, 60, 72, 84, 90], device=device + ) + skyvaultaltint = torch.tensor( + [6, 18, 30, 42, 54, 66, 78, 90], device=device + ) # Robinson & Stone (2004) + azistart = torch.tensor( + [0, 4, 2, 5, 8, 0, 10, 0], device=device + ) # Fredrik/Nils + patches_in_band = torch.tensor( + [31, 30, 28, 24, 19, 13, 7, 1], device=device + ) # Nils + # Patch option 3, 306 patches, test + elif patch_option == 3: + annulino = torch.tensor( + [0, 12, 24, 36, 48, 60, 72, 84, 90], device=device + ) + skyvaultaltint = torch.tensor( + [6, 18, 30, 42, 54, 66, 78, 90], device=device + ) # Robinson & Stone (2004) + azistart = torch.tensor( + [0, 4, 2, 5, 8, 0, 10, 0], device=device + ) # Fredrik/Nils + patches_in_band = torch.tensor( + [31 * 2, 30 * 2, 28 * 2, 24 * 2, 19 * 2, 13 * 2, 7 * 2, 1], + device=device, + ) # Nils + # Patch option 4, 612 patches, test + elif patch_option == 4: + annulino = torch.tensor( + [0, 4.5, 9, 15, 21, 27, 33, 39, 45, 51, 57, 63, 69, 75, 81, 90], + device=device, + ) # Nils + skyvaultaltint = torch.tensor( + [3, 9, 15, 21, 27, 33, 39, 45, 51, 57, 63, 69, 75, 81, 90], + device=device, + ) # Nils + patches_in_band = torch.tensor( + [ + 31 * 2, + 31 * 2, + 30 * 2, + 30 * 2, + 28 * 2, + 28 * 2, + 24 * 2, + 24 * 2, + 19 * 2, + 19 * 2, + 13 * 2, + 13 * 2, + 7 * 2, + 7 * 2, + 1, + ], + device=device, + ) # Nils + azistart = torch.tensor( + [0, 0, 4, 4, 2, 2, 5, 5, 8, 8, 0, 0, 10, 10, 0], device=device + ) # Nils + + skyvaultaziint = torch.tensor( + [360 / patches for patches in patches_in_band] + ) + + for j in range(0, skyvaultaltint.shape[0]): + for k in range(0, patches_in_band[j]): + skyvaultalt = skyvaultalt + (skyvaultaltint[j],) + skyvaultazi = skyvaultazi + (k * skyvaultaziint[j] + azistart[j],) + + del deg2rad + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + return ( + skyvaultalt, + skyvaultazi, + annulino, + skyvaultaltint, + patches_in_band, + skyvaultaziint, + azistart, + ) diff --git a/util/SEBESOLWEIGCommonFiles/diffusefraction_torch.py b/util/SEBESOLWEIGCommonFiles/diffusefraction_torch.py new file mode 100644 index 0000000..ad5f503 --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/diffusefraction_torch.py @@ -0,0 +1,69 @@ +from __future__ import division + +try: + import torch +except: + pass + + +def diffusefraction(radG, altitude, Kt, Ta, RH): + """ + This function estimates diffuse and directbeam radiation according to + Reindl et al (1990), Solar Energy 45:1 + + :param radG: + :param altitude: + :param Kt: # radiation at the top of the atmosphere + :param Ta: + :param RH: + :return: + """ + + alfa = altitude * (torch.pi / 180) + + if Ta <= -999.00 or RH <= -999.00 or torch.isnan(Ta) or torch.isnan(RH): + if Kt <= 0.3: + radD = radG * (1.020 - 0.248 * Kt) + elif Kt > 0.3 and Kt < 0.78: + radD = radG * (1.45 - 1.67 * Kt) + else: + radD = radG * 0.147 + else: + RH = RH / 100 + if Kt <= 0.3: + radD = radG * ( + 1 + - 0.232 * Kt + + 0.0239 * torch.sin(alfa) + - 0.000682 * Ta + + 0.0195 * RH + ) + elif Kt > 0.3 and Kt < 0.78: + radD = radG * ( + 1.329 + - 1.716 * Kt + + 0.267 * torch.sin(alfa) + - 0.00357 * Ta + + 0.106 * RH + ) + else: + radD = radG * ( + 0.426 * Kt + - 0.256 * torch.sin(alfa) + + 0.00349 * Ta + + 0.0734 * RH + ) + + radI = (radG - radD) / (torch.sin(alfa)) + + # Corrections for low sun altitudes (20130307) + if radI < 0: + radI = 0 + + if altitude < 1 and radI > radG: + radI = radG + + if radD > radG: + radD = radG + + return radI, radD diff --git a/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_13_torch.py b/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_13_torch.py new file mode 100644 index 0000000..004fcdf --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_13_torch.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +from __future__ import division +from math import radians + +try: + import torch + import torch.nn.functional as F +except: + pass + + +def shade_on_walls(azimuth, aspect, walls, dsm, f, device): + """ + Calculates shadow heights and lighting states on building facades using vector geometry. + + This function determines wall segments that are self-shadowed (facing away from the sun) + using the cosine of the angle difference, tracks the down-ray building shadow volume, + and isolates sections of the walls that remain illuminated. + + Args: + azimuth (float): Sun azimuth angle in radians. + aspect (torch.Tensor): Aspect orientation of the walls in radians. + walls (torch.Tensor): Height profile of the pixels representing building walls. + dsm (torch.Tensor): Digital Surface Model (ground + building heights). + f (torch.Tensor): Maximum building shadow volume tracked during the ray-tracing loop. + device (torch.device): The PyTorch device (CPU/GPU) where computations occur. + + Returns: + tuple: A tuple containing: + - sh (torch.Tensor): Binary ground and roof shadow mask (1 = sun, 0 = shadow). + - wallsh (torch.Tensor): Height of the wall that is in shadow (meters). + - wallsun (torch.Tensor): Height of the wall that is in direct sun (meters). + - facesh (torch.Tensor): Binary mask of walls in self-shadow (1 = shadowed by self). + - facesun (torch.Tensor): Binary mask of walls facing the sun and not occluded. + """ + # If the cosine of the angle difference is <= 0, the facade faces away from the sun (self-shadow) + cos_incidence = torch.cos(aspect - azimuth) + facesh = (cos_incidence <= 0).float() * (walls > 0).float() + + sh = f - dsm # Shadow volume (height of cast shadow) + + # Facades facing the sun (facesh == 0) that are valid walls + facesun = ((facesh == 0) & (walls > 0)).float() + + # Calculate the height of the wall segment receiving direct sunlight + wallsun = walls - sh + wallsun = torch.clamp(wallsun, min=0.0) + wallsun = torch.where( + facesh == 1, torch.tensor(0.0, device=device), wallsun + ) + + # The shadowed wall height is the total wall height minus the sunlit segment + wallsh = walls - wallsun + + # Transform the cast shadow volume into a binary mask (0 = shadow, 1 = sun) + sh = (sh > 0).float() + sh = 1.0 - sh + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return sh, wallsh, wallsun, facesh, facesun + + +def shadowingfunction_wallheight_13( + a, + azimuth, + altitude, + scale, + walls, + aspect, + walls_scheme=False, + aspect_scheme=False, +): + """ + Computes ground/roof shadows and shadow heights on building walls using a DSM. + + This function leverages hardware-accelerated grid sampling (`F.grid_sample`) in PyTorch + to simulate solar ray tracing. It avoids explicit multi-index pixel slicing or nested + loops, making it optimal for high-performance GPU execution. + + Args: + a (torch.Tensor): Digital Surface Model (DSM) matrix. + azimuth (float): Sun azimuth angle in degrees. + altitude (float): Sun altitude angle in degrees. + scale (float): Map scale modifier (e.g., 1 pixel = 1 meter -> 1.0; 2 meters -> 0.5). + walls (torch.Tensor): Extruded standalone wall pixel heights. + aspect (torch.Tensor): Surface normal orientation of the walls in radians. + walls_scheme (torch.Tensor, optional): Alternative building scheme layout. Defaults to False. + aspect_scheme (torch.Tensor, optional): Alternative building scheme wall orientation. Defaults to False. + + Returns: + tuple: Depending on whether `walls_scheme` is provided, returns: + - Standard: (sh, wallsh, wallsun, facesh, facesun) + - Scheme: (sh, wallsh, wallsun, facesh, facesun, shade_on_wall) + """ + # Automatically detect the execution device (CPU or GPU) from the input tensor + device = a.device if isinstance(a, torch.Tensor) else torch.device("cpu") + + # Convert angular sun coordinates to radians + azimuth = radians(azimuth) + altitude = radians(altitude) + + # Extract map grid spatial dimensions + sizex, sizey = a.shape + + # Clone the DSM matrix to track shifting horizon shadows + f = a.clone() + + # --- COORDINATE GRID GENERATION FOR GLOBAL TRANSFORMATION --- + y, x = torch.meshgrid( + torch.linspace(-1, 1, sizex, device=device), + torch.linspace(-1, 1, sizey, device=device), + indexing="ij", + ) + # Reshape grid layout to PyTorch standard: (1, H, W, 2) with (X, Y) ordered at dim=-1 + grille_base = torch.stack((x, y), dim=-1).unsqueeze(0) + + # Prepare 4D-batched representation of the DSM for the sampling engine (1, 1, H, W) + a_4d = a.unsqueeze(0).unsqueeze(0) + + # Extract geometric limits and spatial sun vector components + amaxvalue = torch.max(a) + sinazimuth = torch.sin(azimuth) + cosazimuth = torch.cos(azimuth) + tanaltitudebyscale = torch.tan(altitude) / scale + + # Dynamically compute the maximum loop range needed based on terrain peak height + max_steps = int(amaxvalue / tanaltitudebyscale) + 2 + + # Core Ray-Tracing Loop (Accelerated using structural translation layers) + for index in range(1, max_steps): + # 1. Project physical shadow displacement on the horizontal plane (in pixels) + shift_x = index * sinazimuth / scale + shift_y = index * cosazimuth / scale + dz = index * tanaltitudebyscale + + # Early termination checkpoint if the casting rays clear the map canvas bounds + if abs(shift_x) >= sizey and abs(shift_y) >= sizex: + break + + # 2. Shift the normalized coordinate sheet (-1 to 1 space) + grille_deplacee = grille_base.clone() + grille_deplacee[..., 0] -= (2.0 * shift_x) / (sizey - 1) + grille_deplacee[..., 1] -= (2.0 * shift_y) / (sizex - 1) + + # 3. Retrieve the shifted layer map natively using bilinear interpolation + temp = ( + F.grid_sample( + a_4d, grille_deplacee, mode="bilinear", padding_mode="border" + ).squeeze() + - dz + ) + + # 4. Amalgamate shadows via upper envelope tracking + f = torch.maximum(f, temp) + + # --- FACADE EVALUATION & SHADOW METRICS POST-PROCESSING --- + sh, wallsh, wallsun, facesh, facesun = shade_on_walls( + azimuth, aspect, walls, a, f, device + ) + + if walls_scheme is not False: + _, wallsh_, _, _, _ = shade_on_walls( + azimuth, aspect_scheme, walls_scheme, a, f, device + ) + shade_on_wall = wallsh_.clone() + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return (sh, wallsh, wallsun, facesh, facesun, shade_on_wall) + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return (sh, wallsh, wallsun, facesh, facesun) diff --git a/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_23.py b/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_23.py index a8b102c..fa04a08 100644 --- a/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_23.py +++ b/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_23.py @@ -228,22 +228,21 @@ def shadowingfunction_wallheight_23( shvoveg = (shvoveg - a) * vegsh # Vegetation shadow volume vegsh = 1 - vegsh vbshvegsh = 1 - vbshvegsh - # print(np.max(shvoveg)) + wallsh, wallsun, wallshve, facesh, facesun = shade_on_walls( azimuth, aspect, walls, a, f, shvoveg ) - # print(np.max(wallshve)) + if walls_scheme is not False: wallsh_, wallsun_, wallshve_, facesh_, facesun_ = shade_on_walls( azimuth, aspect_scheme, walls_scheme, a, f, shvoveg ) - # print(np.max(wallshve_)) + shade_on_wall = wallsh_.copy() shade_on_wall[shade_on_wall < wallshve_] = wallshve_[ shade_on_wall < wallshve_ ] - # return vegsh, sh, vbshvegsh, wallsh, wallsun, wallshve, facesh, facesun, # shade_on_wall return ( ( diff --git a/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_23_torch.py b/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_23_torch.py new file mode 100644 index 0000000..672fd1e --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/shadowingfunction_wallheight_23_torch.py @@ -0,0 +1,256 @@ +from __future__ import division + +try: + import torch + import torch.nn.functional as F +except: + pass + + +def shade_on_walls(azimuth, aspect, walls, dsm, f, shvoveg, device): + cos_incidence = torch.cos(aspect - azimuth) + facesh = (cos_incidence <= 0).float() * (walls > 0).float() + + shvo = f - dsm + + facesun = ((facesh == 0) & (walls > 0)).float() + + wallsun = walls - shvo + wallsun = torch.clamp(wallsun, min=0.0) + wallsun = torch.where( + facesh == 1, torch.tensor(0.0, device=device), wallsun + ) + + wallsh = walls - wallsun + + wallshve = shvoveg * (walls > 0).float() + wallshve = torch.clamp(wallshve - wallsh, min=0.0) + + mask_clip = wallshve > walls + wallshve[mask_clip] = walls[mask_clip].to(dtype=wallshve.dtype) + + wallsun = wallsun - wallshve + mask_neg = wallsun < 0 + wallshve[mask_neg] = 0 + wallsun[mask_neg] = 0 + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + return wallsh, wallsun, wallshve, facesh, facesun + + +def shadowingfunction_wallheight_23( + a, + vegdem, + vegdem2, + azimuth, + altitude, + scale, + amaxvalue, + bush, + walls, + aspect, + walls_scheme=False, + aspect_scheme=False, +): + """ + This function calculates shadows on a DSM and shadow height on building + walls including both buildings and vegetion units. + New functionallity to deal with pergolas, August 2021 + + INPUTS: + a = DSM + vegdem = Vegetation canopy DSM (magl) + vegdem2 = Trunkzone DSM (magl) + azimuth and altitude = sun position + scale= scale of DSM (1 meter pixels=1, 2 meter pixels=0.5) + walls= pixel row 'outside' buildings. will be calculated if empty + aspect=normal aspect of walls + + OUTPUT: + sh=ground and roof shadow + + wallsh = height of wall that is in shadow + wallsun = hieght of wall that is in sun + + original Matlab code: + Fredrik Lindberg 2013-08-14 + fredrikl@gvc.gu.se + + :param a: + :param vegdem: + :param vegdem2: + :param azimuth: + :param altitude: + :param scale: + :param amaxvalue: + :param bush: + :param walls: + :param aspect: + :return: + """ + + # automatically get the device to use from the input + device = a.device if isinstance(a, torch.Tensor) else torch.device("cpu") + degrees = torch.pi / 180.0 + azimuth *= degrees + altitude *= degrees + + sizex, sizey = a.shape + bushplant = (bush > 1).float() + + # Tensor initialisation + sh = torch.zeros((sizex, sizey), device=device) + vbshvegsh = torch.zeros((sizex, sizey), device=device) + vegsh = torch.zeros((sizex, sizey), device=device) + bushplant + f = a.clone() + shvoveg = vegdem.clone() + + # ---coordinates drag the layer --- + y, x = torch.meshgrid( + torch.linspace(-1, 1, sizex, device=device), + torch.linspace(-1, 1, sizey, device=device), + indexing="ij", + ) + grille_base = torch.stack((x, y), dim=-1).unsqueeze(0) + + # Preparation of the layers packed for grid_sample + a_4d = a.unsqueeze(0).unsqueeze(0).float() + veg_4d = vegdem.unsqueeze(0).unsqueeze(0).float() + veg2_4d = vegdem2.unsqueeze(0).unsqueeze(0).float() + + # Trigonometric params for the sun's step + sinazimuth = torch.sin(azimuth) + cosazimuth = torch.cos(azimuth) + tanaltitudebyscale = torch.tan(altitude) / scale + + # Calculation of the maximum of steps + # We stop when the shadow exceed the max possible height + max_steps = int(amaxvalue / tanaltitudebyscale) + 2 + + # Facteur d'avancement du rayon (équivalent géométrique de ton ancien 'ds') + # Plus besoin de gros blocs If/Else selon les quadrants du soleil ! + delta_z_par_pas = tanaltitudebyscale + + for index in range(1, max_steps): + + # 1. Calculations of the slide + shift_x = index * sinazimuth / scale + shift_y = index * cosazimuth / scale + dz = index * delta_z_par_pas + + # Stop if the shadow is completely outside of the map + if abs(shift_x) >= sizey and abs(shift_y) >= sizex: + break + + # 2. Applying the offset to our normalized coordinate sheet (-1 to 1) + grille_deplacee = grille_base.clone() + grille_deplacee[..., 0] -= (2.0 * shift_x) / (sizey - 1) + grille_deplacee[..., 1] -= (2.0 * shift_y) / (sizex - 1) + + # 3. Global Material Sampling (Instant Layer Swipe) + temp = ( + F.grid_sample( + a_4d, grille_deplacee, mode="bilinear", padding_mode="border" + ).squeeze() + - dz + ) + tempvegdem = ( + F.grid_sample( + veg_4d, grille_deplacee, mode="bilinear", padding_mode="border" + ).squeeze() + - dz + ) + tempvegdem2 = ( + F.grid_sample( + veg2_4d, + grille_deplacee, + mode="bilinear", + padding_mode="border", + ).squeeze() + - dz + ) + + # 4. Update of cast shadow volumes + f = torch.fmax(f, temp) + shvoveg = torch.fmax(shvoveg, tempvegdem) + + sh = torch.where( + f > a, + torch.tensor(1.0, device=device), + torch.tensor(0.0, device=device), + ) + fabovea = tempvegdem > a + gabovea = tempvegdem2 > a + + # 5. Pergola logic (Overlaying layers with the & operator) + templastfabovea = tempvegdem + delta_z_par_pas + templastgabovea = tempvegdem2 + delta_z_par_pas + + lastfabovea = templastfabovea > a + lastgabovea = templastgabovea > a + + # If all 4 layers are True at the same time, it's a pergola (light passes through). + is_pergola = fabovea & gabovea & lastfabovea & lastgabovea + + # The shadow exists if one of the layers is True, UNLESS it's a pergola. + vegsh2 = (fabovea | gabovea | lastfabovea | lastgabovea) & ( + ~is_pergola + ) + vegsh2 = vegsh2.float() + + # Accumulation of vegetation shadows + vegsh = torch.maximum(vegsh, vegsh2) + vegsh = torch.where( + vegsh * sh > 0, torch.tensor(0.0, device=device), vegsh + ) + vbshvegsh.add_(vegsh) + + sh = 1 - sh + vbshvegsh = torch.where( + vbshvegsh > 0, torch.tensor(1.0, device=device), vbshvegsh + ) + vbshvegsh = vbshvegsh - vegsh + + vegsh = torch.where(vegsh > 0, torch.tensor(1.0, device=device), vegsh) + shvoveg = (shvoveg - a) * vegsh # Vegetation shadow volume + vegsh = 1 - vegsh + vbshvegsh = 1 - vbshvegsh + + wallsh, wallsun, wallshve, facesh, facesun = shade_on_walls( + azimuth, aspect, walls, a, f, shvoveg, device + ) + + if walls_scheme is not False: + wallsh_, wallsun_, wallshve_, facesh_, facesun_ = shade_on_walls( + azimuth, aspect_scheme, walls_scheme, a, f, shvoveg, device + ) + + shade_on_wall = wallsh_.clone() + shade_on_wall[shade_on_wall < wallshve_] = wallshve_[ + shade_on_wall < wallshve_ + ] + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + return ( + ( + vegsh, + sh, + vbshvegsh, + wallsh, + wallsun, + wallshve, + facesh, + facesun, + shade_on_wall, + ) + if walls_scheme is not False + else (vegsh, sh, vbshvegsh, wallsh, wallsun, wallshve, facesh, facesun) + ) diff --git a/util/SEBESOLWEIGCommonFiles/sun_distance_torch.py b/util/SEBESOLWEIGCommonFiles/sun_distance_torch.py new file mode 100644 index 0000000..7a94896 --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/sun_distance_torch.py @@ -0,0 +1,25 @@ +__author__ = "xlinfr" +try: + import torch +except: + pass + + +def sun_distance(jday): + """ + + #% Calculatesrelative earth sun distance + #% with day of year as input. + #% Partridge and Platt, 1975 + """ + b = 2.0 * torch.pi * jday / 365.0 + D = torch.sqrt( + ( + 1.00011 + + 0.034221 * torch.cos(b) + + 0.001280 * torch.sin(b) + + 0.000719 * torch.cos(2.0 * b) + + 0.000077 * torch.sin(2.0 * b) + ) + ) + return D diff --git a/util/SEBESOLWEIGCommonFiles/sun_position.py b/util/SEBESOLWEIGCommonFiles/sun_position.py index 72cfc39..cd24a21 100644 --- a/util/SEBESOLWEIGCommonFiles/sun_position.py +++ b/util/SEBESOLWEIGCommonFiles/sun_position.py @@ -105,7 +105,6 @@ def sun_position(time, location): # 1. Calculate the Julian Day, and Century. Julian Ephemeris day, century # and millenium are calculated using a mean delta_t of 33.184 seconds. julian = julian_calculation(time) - # print(julian) # 2. Calculate the Earth heliocentric longitude, latitude, and radius # vector (L, B, and R) diff --git a/util/SEBESOLWEIGCommonFiles/sun_position_torch.py b/util/SEBESOLWEIGCommonFiles/sun_position_torch.py new file mode 100644 index 0000000..d202cf9 --- /dev/null +++ b/util/SEBESOLWEIGCommonFiles/sun_position_torch.py @@ -0,0 +1,1358 @@ +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function + +try: + import torch + +except: + pass + + +def sun_position(time, location): + """ + % sun = sun_position(time, location) + % + % This function compute the sun position (zenith and azimuth angle at the observer + % location) as a function of the observer local time and position. + % + % It is an implementation of the algorithm presented by Reda et Andreas in: + % Reda, I., Andreas, A. (2003) Solar position algorithm for solar + % radiation application. National Renewable Energy Laboratory (NREL) + % Technical report NREL/TP-560-34302. + % This document is avalaible at www.osti.gov/bridge + % + % This algorithm is based on numerical approximation of the exact equations. + % The authors of the original paper state that this algorithm should be + % precise at +/- 0.0003 degrees. I have compared it to NOAA solar table + % (http://www.srrb.noaa.gov/highlights/sunrise/azel.html) and to USNO solar + % table (http://aa.usno.navy.mil/data/docs/AltAz.html) and found very good + % correspondance (up to the precision of those tables), except for large + % zenith angle, where the refraction by the atmosphere is significant + % (difference of about 1 degree). Note that in this code the correction + % for refraction in the atmosphere as been implemented for a temperature + % of 10C (283 kelvins) and a pressure of 1010 mbar. See the subfunction + % �sun_topocentric_zenith_angle_calculation� for a possible modification + % to explicitely model the effect of temperature and pressure as describe + % in Reda & Andreas (2003). + % + % Input parameters: + % time: a structure that specify the time when the sun position is + % calculated. + % time.year: year. Valid for [-2000, 6000] + % time.month: month [1-12] + % time.day: calendar day [1-31] + % time.hour: local hour [0-23] + % time.min: minute [0-59] + % time.sec: second [0-59] + % time.UTC: offset hour from UTC. Local time = Greenwich time + time.UTC + % This input can also be passed using the Matlab time format ('dd-mmm-yyyy HH:MM:SS'). + % In that case, the time has to be specified as UTC time (time.UTC = 0) + % + % location: a structure that specify the location of the observer + % location.latitude: latitude (in degrees, north of equator is + % positive) + % location.longitude: longitude (in degrees, positive for east of + % Greenwich) + % location.altitude: altitude above mean sea level (in meters) + % + % Output parameters + % sun: a structure with the calculated sun position + % sun.zenith = zenith angle in degrees (angle from the vertical) + % sun.azimuth = azimuth angle in degrees, eastward from the north. + % Only the sun zenith and azimuth angles are returned as output, but a lot + % of other parameters are calculated that could also extracted as output of + % this function. + % + % Exemple of use + % + % location.longitude = -105.1786; + % location.latitude = 39.742476; + % location.altitude = 1830.14; + % time.year = 2005; + % time.month = 10; + % time.day = 17; + % time.hour = 6; + % time.min = 30; + % time.sec = 30; + % time.UTC = -7; + % % + % location.longitude = 11.94; + % location.latitude = 57.70; + % location.altitude = 3.0; + % time.UTC = 1; + % sun = sun_position(time, location); + % + % sun = + % + % zenith: 50.1080438859849 + % azimuth: 194.341174010338 + % + % History + % 09/03/2004 Original creation by Vincent Roy (vincent.roy@drdc-rddc.gc.ca) + % 10/03/2004 Fixed a bug in julian_calculation subfunction (was + % incorrect for year 1582 only), Vincent Roy + % 18/03/2004 Correction to the header (help display) only. No changes to + % the code. (changed the �elevation� field in �location� structure + % information to �altitude�), Vincent Roy + % 13/04/2004 Following a suggestion from Jody Klymak (jklymak@ucsd.edu), + % allowed the 'time' input to be passed as a Matlab time string. + % 22/08/2005 Following a bug report from Bruce Bowler + % (bbowler@bigelow.org), modified the julian_calculation function. Bug + % was 'MATLAB has allowed structure assignment to a non-empty non-structure + % to overwrite the previous value. This behavior will continue in this release, + % but will be an error in a future version of MATLAB. For advice on how to + % write code that will both avoid this warning and work in future versions of + % MATLAB, see R14SP2 Release Notes'. Script should now be + % compliant with futher release of Matlab... + """ + + # 1. Calculate the Julian Day, and Century. Julian Ephemeris day, century + # and millenium are calculated using a mean delta_t of 33.184 seconds. + julian = julian_calculation(time) + # print(julian) + + # 2. Calculate the Earth heliocentric longitude, latitude, and radius + # vector (L, B, and R) + earth_heliocentric_position = earth_heliocentric_position_calculation( + julian + ) + + # 3. Calculate the geocentric longitude and latitude + sun_geocentric_position = sun_geocentric_position_calculation( + earth_heliocentric_position + ) + + # 4. Calculate the nutation in longitude and obliquity (in degrees). + nutation = nutation_calculation(julian) + + # 5. Calculate the true obliquity of the ecliptic (in degrees). + true_obliquity = true_obliquity_calculation(julian, nutation) + + # 6. Calculate the aberration correction (in degrees) + aberration_correction = abberation_correction_calculation( + earth_heliocentric_position + ) + + # 7. Calculate the apparent sun longitude in degrees) + apparent_sun_longitude = apparent_sun_longitude_calculation( + sun_geocentric_position, nutation, aberration_correction + ) + + # 8. Calculate the apparent sideral time at Greenwich (in degrees) + apparent_stime_at_greenwich = apparent_stime_at_greenwich_calculation( + julian, nutation, true_obliquity + ) + + # 9. Calculate the sun rigth ascension (in degrees) + sun_rigth_ascension = sun_rigth_ascension_calculation( + apparent_sun_longitude, true_obliquity, sun_geocentric_position + ) + + # 10. Calculate the geocentric sun declination (in degrees). Positive or + # negative if the sun is north or south of the celestial equator. + sun_geocentric_declination = sun_geocentric_declination_calculation( + apparent_sun_longitude, true_obliquity, sun_geocentric_position + ) + + # 11. Calculate the observer local hour angle (in degrees, westward from south). + observer_local_hour = observer_local_hour_calculation( + apparent_stime_at_greenwich, location, sun_rigth_ascension + ) + + # 12. Calculate the topocentric sun position (rigth ascension, declination and + # rigth ascension parallax in degrees) + topocentric_sun_position = topocentric_sun_position_calculate( + earth_heliocentric_position, + location, + observer_local_hour, + sun_rigth_ascension, + sun_geocentric_declination, + ) + + # 13. Calculate the topocentric local hour angle (in degrees) + topocentric_local_hour = topocentric_local_hour_calculate( + observer_local_hour, topocentric_sun_position + ) + + # 14. Calculate the topocentric zenith and azimuth angle (in degrees) + sun = sun_topocentric_zenith_angle_calculate( + location, topocentric_sun_position, topocentric_local_hour + ) + + return sun + + +def julian_calculation(t_input): + device = torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + """ + % This function compute the julian day and julian century from the local + % time and timezone information. Ephemeris are calculated with a delta_t=0 + % seconds. + + % If time input is a Matlab time string, extract the information from + % this string and create the structure as defined in the main header of + % this script. + """ + if not isinstance(t_input, dict): + # tt = datetime.datetime.strptime(t_input, "%Y-%m-%d %H:%M:%S.%f") # if t_input is a string of this format + # t_input should be a datetime object + time = dict() + time["UTC"] = 0 + time["year"] = t_input.year + time["month"] = t_input.month + time["day"] = t_input.day + time["hour"] = t_input.hour + time["min"] = t_input.minute + time["sec"] = t_input.second + else: + time = t_input + + if time["month"] == 1 or time["month"] == 2: + Y = torch.tensor(time["year"] - 1, device=device) + M = torch.tensor(time["month"] + 12, device=device) + else: + Y = torch.tensor(time["year"], device=device) + M = torch.tensor(time["month"], device=device) + + ut_time = torch.tensor( + ((time["hour"] - time["UTC"]) / 24) + + (time["min"] / (60 * 24)) + + (time["sec"] / (60 * 60 * 24)), + device=device, + ) # time of day in UT time. + D = time["day"] + ut_time + # Day of month in decimal time, ex. 2sd day of month at 12:30:30UT, D=2.521180556 + + # In 1582, the gregorian calendar was adopted + if time["year"] == 1582: + if time["month"] == 10: + if ( + time["day"] <= 4 + ): # The Julian calendar ended on October 4, 1582 + B = torch.tensor(0, device=device) + elif ( + time["day"] >= 15 + ): # The Gregorian calendar started on October 15, 1582 + A = torch.floor(Y / 100) + B = 2 - A + torch.floor(A / 4) + else: + print( + "This date never existed!. Date automatically set to October 4, 1582" + ) + time["month"] = 10 + time["day"] = 4 + B = torch.tensor(0, device=device) + elif time["month"] < 10: # Julian calendar + B = torch.tensor(0, device=device) + else: # Gregorian calendar + A = torch.floor(Y / 100) + B = 2 - A + torch.floor(A / 4) + elif time["year"] < 1582: # Julian calendar + B = torch.tensor(0, device=device) + else: + A = torch.floor(Y / 100) # Gregorian calendar + B = 2 - A + torch.floor(A / 4) + + julian = dict() + julian["day"] = ( + D + + B + + torch.floor(365.25 * (Y + 4716)) + + torch.floor(30.6001 * (M + 1)) + - 1524.5 + ) + + delta_t = 0 # 33.184; + julian["ephemeris_day"] = (julian["day"]) + (delta_t / 86400) + julian["century"] = (julian["day"] - 2451545) / 36525 + julian["ephemeris_century"] = (julian["ephemeris_day"] - 2451545) / 36525 + julian["ephemeris_millenium"] = (julian["ephemeris_century"]) / 10 + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return julian + + +def earth_heliocentric_position_calculation(julian): + device = torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + """ + % This function compute the earth position relative to the sun, using + % tabulated values. + + % Tabulated values for the longitude calculation + % L terms from the original code. + """ + # Tabulated values for the longitude calculation + # L terms from the original code. + L0_terms = torch.tensor( + [ + [175347046.0, 0, 0], + [3341656.0, 4.6692568, 6283.07585], + [34894.0, 4.6261, 12566.1517], + [3497.0, 2.7441, 5753.3849], + [3418.0, 2.8289, 3.5231], + [3136.0, 3.6277, 77713.7715], + [2676.0, 4.4181, 7860.4194], + [2343.0, 6.1352, 3930.2097], + [1324.0, 0.7425, 11506.7698], + [1273.0, 2.0371, 529.691], + [1199.0, 1.1096, 1577.3435], + [990, 5.233, 5884.927], + [902, 2.045, 26.298], + [857, 3.508, 398.149], + [780, 1.179, 5223.694], + [753, 2.533, 5507.553], + [505, 4.583, 18849.228], + [492, 4.205, 775.523], + [357, 2.92, 0.067], + [317, 5.849, 11790.629], + [284, 1.899, 796.298], + [271, 0.315, 10977.079], + [243, 0.345, 5486.778], + [206, 4.806, 2544.314], + [205, 1.869, 5573.143], + [202, 2.4458, 6069.777], + [156, 0.833, 213.299], + [132, 3.411, 2942.463], + [126, 1.083, 20.775], + [115, 0.645, 0.98], + [103, 0.636, 4694.003], + [102, 0.976, 15720.839], + [102, 4.267, 7.114], + [99, 6.21, 2146.17], + [98, 0.68, 155.42], + [86, 5.98, 161000.69], + [85, 1.3, 6275.96], + [85, 3.67, 71430.7], + [80, 1.81, 17260.15], + [79, 3.04, 12036.46], + [71, 1.76, 5088.63], + [74, 3.5, 3154.69], + [74, 4.68, 801.82], + [70, 0.83, 9437.76], + [62, 3.98, 8827.39], + [61, 1.82, 7084.9], + [57, 2.78, 6286.6], + [56, 4.39, 14143.5], + [56, 3.47, 6279.55], + [52, 0.19, 12139.55], + [52, 1.33, 1748.02], + [51, 0.28, 5856.48], + [49, 0.49, 1194.45], + [41, 5.37, 8429.24], + [41, 2.4, 19651.05], + [39, 6.17, 10447.39], + [37, 6.04, 10213.29], + [37, 2.57, 1059.38], + [36, 1.71, 2352.87], + [36, 1.78, 6812.77], + [33, 0.59, 17789.85], + [30, 0.44, 83996.85], + [30, 2.74, 1349.87], + [25, 3.16, 4690.48], + ], + device=device, + ) + + L1_terms = torch.tensor( + [ + [628331966747.0, 0, 0], + [206059.0, 2.678235, 6283.07585], + [4303.0, 2.6351, 12566.1517], + [425.0, 1.59, 3.523], + [119.0, 5.796, 26.298], + [109.0, 2.966, 1577.344], + [93, 2.59, 18849.23], + [72, 1.14, 529.69], + [68, 1.87, 398.15], + [67, 4.41, 5507.55], + [59, 2.89, 5223.69], + [56, 2.17, 155.42], + [45, 0.4, 796.3], + [36, 0.47, 775.52], + [29, 2.65, 7.11], + [21, 5.34, 0.98], + [19, 1.85, 5486.78], + [19, 4.97, 213.3], + [17, 2.99, 6275.96], + [16, 0.03, 2544.31], + [16, 1.43, 2146.17], + [15, 1.21, 10977.08], + [12, 2.83, 1748.02], + [12, 3.26, 5088.63], + [12, 5.27, 1194.45], + [12, 2.08, 4694], + [11, 0.77, 553.57], + [10, 1.3, 3286.6], + [10, 4.24, 1349.87], + [9, 2.7, 242.73], + [9, 5.64, 951.72], + [8, 5.3, 2352.87], + [6, 2.65, 9437.76], + [6, 4.67, 4690.48], + ], + device=device, + ) + + L2_terms = torch.tensor( + [ + [52919.0, 0, 0], + [8720.0, 1.0721, 6283.0758], + [309.0, 0.867, 12566.152], + [27, 0.05, 3.52], + [16, 5.19, 26.3], + [16, 3.68, 155.42], + [10, 0.76, 18849.23], + [9, 2.06, 77713.77], + [7, 0.83, 775.52], + [5, 4.66, 1577.34], + [4, 1.03, 7.11], + [4, 3.44, 5573.14], + [3, 5.14, 796.3], + [3, 6.05, 5507.55], + [3, 1.19, 242.73], + [3, 6.12, 529.69], + [3, 0.31, 398.15], + [3, 2.28, 553.57], + [2, 4.38, 5223.69], + [2, 3.75, 0.98], + ], + device=device, + ) + + L3_terms = torch.tensor( + [ + [289.0, 5.844, 6283.076], + [35, 0, 0], + [17, 5.49, 12566.15], + [3, 5.2, 155.42], + [1, 4.72, 3.52], + [1, 5.3, 18849.23], + [1, 5.97, 242.73], + ], + device=device, + ) + L4_terms = torch.tensor( + [[114.0, 3.142, 0], [8, 4.13, 6283.08], [1, 3.84, 12566.15]], + device=device, + ) + + L5_terms = torch.tensor([1, 3.14, 0], device=device) + L5_terms = torch.atleast_2d( + L5_terms + ) # since L5_terms is 1D, we have to convert it to 2D to avoid indexErrors + + A0 = L0_terms[:, 0] + B0 = L0_terms[:, 1] + C0 = L0_terms[:, 2] + + A1 = L1_terms[:, 0] + B1 = L1_terms[:, 1] + C1 = L1_terms[:, 2] + + A2 = L2_terms[:, 0] + B2 = L2_terms[:, 1] + C2 = L2_terms[:, 2] + + A3 = L3_terms[:, 0] + B3 = L3_terms[:, 1] + C3 = L3_terms[:, 2] + + A4 = L4_terms[:, 0] + B4 = L4_terms[:, 1] + C4 = L4_terms[:, 2] + + A5 = L5_terms[:, 0] + B5 = L5_terms[:, 1] + C5 = L5_terms[:, 2] + + JME = julian["ephemeris_millenium"] + + # Compute the Earth Heliochentric longitude from the tabulated values. + L0 = torch.sum(A0 * torch.cos(B0 + (C0 * JME))) + L1 = torch.sum(A1 * torch.cos(B1 + (C1 * JME))) + L2 = torch.sum(A2 * torch.cos(B2 + (C2 * JME))) + L3 = torch.sum(A3 * torch.cos(B3 + (C3 * JME))) + L4 = torch.sum(A4 * torch.cos(B4 + (C4 * JME))) + L5 = A5 * torch.cos(B5 + (C5 * JME)) + + earth_heliocentric_position = dict() + earth_heliocentric_position["longitude"] = ( + L0 + + (L1 * JME) + + (L2 * torch.pow(JME, 2)) + + (L3 * torch.pow(JME, 3)) + + (L4 * torch.pow(JME, 4)) + + (L5 * torch.pow(JME, 5)) + ) / 1e8 + # Convert the longitude to degrees. + earth_heliocentric_position["longitude"] = ( + earth_heliocentric_position["longitude"] * 180 / torch.pi + ) + + # Limit the range to [0,360] + earth_heliocentric_position["longitude"] = set_to_range( + earth_heliocentric_position["longitude"], 0, 360 + ) + + # Tabulated values for the earth heliocentric latitude. + # B terms from the original code. + B0_terms = torch.tensor( + [ + [280.0, 3.199, 84334.662], + [102.0, 5.422, 5507.553], + [80, 3.88, 5223.69], + [44, 3.7, 2352.87], + [32, 4, 1577.34], + ], + device=device, + ) + + B1_terms = torch.tensor( + [[9, 3.9, 5507.55], [6, 1.73, 5223.69]], device=device + ) + + A0 = B0_terms[:, 0] + B0 = B0_terms[:, 1] + C0 = B0_terms[:, 2] + + A1 = B1_terms[:, 0] + B1 = B1_terms[:, 1] + C1 = B1_terms[:, 2] + + L0 = torch.sum(A0 * torch.cos(B0 + (C0 * JME))) + L1 = torch.sum(A1 * torch.cos(B1 + (C1 * JME))) + + earth_heliocentric_position["latitude"] = (L0 + (L1 * JME)) / 1e8 + + # Convert the latitude to degrees. + earth_heliocentric_position["latitude"] = ( + earth_heliocentric_position["latitude"] * 180 / torch.pi + ) + + # Limit the range to [0,360]; + earth_heliocentric_position["latitude"] = set_to_range( + earth_heliocentric_position["latitude"], 0, 360 + ) + + # Tabulated values for radius vector. + # R terms from the original code + R0_terms = torch.tensor( + [ + [100013989.0, 0, 0], + [1670700.0, 3.0984635, 6283.07585], + [13956.0, 3.05525, 12566.1517], + [3084.0, 5.1985, 77713.7715], + [1628.0, 1.1739, 5753.3849], + [1576.0, 2.8469, 7860.4194], + [925.0, 5.453, 11506.77], + [542.0, 4.564, 3930.21], + [472.0, 3.661, 5884.927], + [346.0, 0.964, 5507.553], + [329.0, 5.9, 5223.694], + [307.0, 0.299, 5573.143], + [243.0, 4.273, 11790.629], + [212.0, 5.847, 1577.344], + [186.0, 5.022, 10977.079], + [175.0, 3.012, 18849.228], + [110.0, 5.055, 5486.778], + [98, 0.89, 6069.78], + [86, 5.69, 15720.84], + [86, 1.27, 161000.69], + [85, 0.27, 17260.15], + [63, 0.92, 529.69], + [57, 2.01, 83996.85], + [56, 5.24, 71430.7], + [49, 3.25, 2544.31], + [47, 2.58, 775.52], + [45, 5.54, 9437.76], + [43, 6.01, 6275.96], + [39, 5.36, 4694], + [38, 2.39, 8827.39], + [37, 0.83, 19651.05], + [37, 4.9, 12139.55], + [36, 1.67, 12036.46], + [35, 1.84, 2942.46], + [33, 0.24, 7084.9], + [32, 0.18, 5088.63], + [32, 1.78, 398.15], + [28, 1.21, 6286.6], + [28, 1.9, 6279.55], + [26, 4.59, 10447.39], + ], + device=device, + ) + + R1_terms = torch.tensor( + [ + [103019.0, 1.10749, 6283.07585], + [1721.0, 1.0644, 12566.1517], + [702.0, 3.142, 0], + [32, 1.02, 18849.23], + [31, 2.84, 5507.55], + [25, 1.32, 5223.69], + [18, 1.42, 1577.34], + [10, 5.91, 10977.08], + [9, 1.42, 6275.96], + [9, 0.27, 5486.78], + ], + device=device, + ) + + R2_terms = torch.tensor( + [ + [4359.0, 5.7846, 6283.0758], + [124.0, 5.579, 12566.152], + [12, 3.14, 0], + [9, 3.63, 77713.77], + [6, 1.87, 5573.14], + [3, 5.47, 18849], + ], + device=device, + ) + + R3_terms = torch.tensor( + [[145.0, 4.273, 6283.076], [7, 3.92, 12566.15]], device=device + ) + + R4_terms = [4, 2.56, 6283.08] + # Force it to be a tensor first, then ensure it's 2D + R4_terms = torch.atleast_2d( + torch.tensor(R4_terms, device=device) + ) # since L5_terms is 1D, we have to convert it to 2D to avoid indexErrors + + A0 = R0_terms[:, 0] + B0 = R0_terms[:, 1] + C0 = R0_terms[:, 2] + + A1 = R1_terms[:, 0] + B1 = R1_terms[:, 1] + C1 = R1_terms[:, 2] + + A2 = R2_terms[:, 0] + B2 = R2_terms[:, 1] + C2 = R2_terms[:, 2] + + A3 = R3_terms[:, 0] + B3 = R3_terms[:, 1] + C3 = R3_terms[:, 2] + + A4 = R4_terms[:, 0] + B4 = R4_terms[:, 1] + C4 = R4_terms[:, 2] + + # Compute the Earth heliocentric radius vector + L0 = torch.sum(A0 * torch.cos(B0 + (C0 * JME))) + L1 = torch.sum(A1 * torch.cos(B1 + (C1 * JME))) + L2 = torch.sum(A2 * torch.cos(B2 + (C2 * JME))) + L3 = torch.sum(A3 * torch.cos(B3 + (C3 * JME))) + L4 = A4 * torch.cos(B4 + (C4 * JME)) + + # Units are in AU + earth_heliocentric_position["radius"] = ( + L0 + + (L1 * JME) + + (L2 * torch.pow(JME, 2)) + + (L3 * torch.pow(JME, 3)) + + (L4 * torch.pow(JME, 4)) + ) / 1e8 + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return earth_heliocentric_position + + +def sun_geocentric_position_calculation(earth_heliocentric_position): + """ + % This function compute the sun position relative to the earth. + """ + sun_geocentric_position = dict() + sun_geocentric_position["longitude"] = ( + earth_heliocentric_position["longitude"] + 180 + ) + # Limit the range to [0,360]; + sun_geocentric_position["longitude"] = set_to_range( + sun_geocentric_position["longitude"], 0, 360 + ) + + sun_geocentric_position["latitude"] = -earth_heliocentric_position[ + "latitude" + ] + # Limit the range to [0,360] + sun_geocentric_position["latitude"] = set_to_range( + sun_geocentric_position["latitude"], 0, 360 + ) + + return sun_geocentric_position + + +def nutation_calculation(julian): + device = torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + """ + % This function compute the nutation in longtitude and in obliquity, in + % degrees. + :param julian: + :return: nutation + """ + + # All Xi are in degrees. + JCE = julian["ephemeris_century"] + + # 1. Mean elongation of the moon from the sun + p = torch.atleast_2d( + torch.tensor( + [(1 / 189474), -0.0019142, 445267.11148, 297.85036], device=device + ) + ) + + # X0 = polyval(p, JCE); + X0 = ( + p[0, 0] * torch.pow(JCE, 3) + + p[0, 1] * torch.pow(JCE, 2) + + p[0, 2] * JCE + + p[0, 3] + ) # This is faster than polyval... + + # 2. Mean anomaly of the sun (earth) + p = torch.atleast_2d( + torch.tensor( + [-(1 / 300000), -0.0001603, 35999.05034, 357.52772], device=device + ) + ) + + # X1 = polyval(p, JCE) + X1 = ( + p[0, 0] * torch.pow(JCE, 3) + + p[0, 1] * torch.pow(JCE, 2) + + p[0, 2] * JCE + + p[0, 3] + ) + + # 3. Mean anomaly of the moon + p = torch.atleast_2d( + torch.tensor( + [(1 / 56250), 0.0086972, 477198.867398, 134.96298], device=device + ) + ) + + # X2 = polyval(p, JCE); + X2 = ( + p[0, 0] * torch.pow(JCE, 3) + + p[0, 1] * torch.pow(JCE, 2) + + p[0, 2] * JCE + + p[0, 3] + ) + + # 4. Moon argument of latitude + p = torch.atleast_2d( + torch.tensor( + [(1 / 327270), -0.0036825, 483202.017538, 93.27191], device=device + ) + ) + + # X3 = polyval(p, JCE) + X3 = ( + p[0, 0] * torch.pow(JCE, 3) + + p[0, 1] * torch.pow(JCE, 2) + + p[0, 2] * JCE + + p[0, 3] + ) + + # 5. Longitude of the ascending node of the moon's mean orbit on the + # ecliptic, measured from the mean equinox of the date + p = torch.atleast_2d( + torch.tensor( + [(1 / 450000), 0.0020708, -1934.136261, 125.04452], device=device + ) + ) + + # X4 = polyval(p, JCE); + X4 = ( + p[0, 0] * torch.pow(JCE, 3) + + p[0, 1] * torch.pow(JCE, 2) + + p[0, 2] * JCE + + p[0, 3] + ) + + # Y tabulated terms from the original code + Y_terms = torch.tensor( + [ + [0, 0, 0, 0, 1], + [-2, 0, 0, 2, 2], + [0, 0, 0, 2, 2], + [0, 0, 0, 0, 2], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [-2, 1, 0, 2, 2], + [0, 0, 0, 2, 1], + [0, 0, 1, 2, 2], + [-2, -1, 0, 2, 2], + [-2, 0, 1, 0, 0], + [-2, 0, 0, 2, 1], + [0, 0, -1, 2, 2], + [2, 0, 0, 0, 0], + [0, 0, 1, 0, 1], + [2, 0, -1, 2, 2], + [0, 0, -1, 0, 1], + [0, 0, 1, 2, 1], + [-2, 0, 2, 0, 0], + [0, 0, -2, 2, 1], + [2, 0, 0, 2, 2], + [0, 0, 2, 2, 2], + [0, 0, 2, 0, 0], + [-2, 0, 1, 2, 2], + [0, 0, 0, 2, 0], + [-2, 0, 0, 2, 0], + [0, 0, -1, 2, 1], + [0, 2, 0, 0, 0], + [2, 0, -1, 0, 1], + [-2, 2, 0, 2, 2], + [0, 1, 0, 0, 1], + [-2, 0, 1, 0, 1], + [0, -1, 0, 0, 1], + [0, 0, 2, -2, 0], + [2, 0, -1, 2, 1], + [2, 0, 1, 2, 2], + [0, 1, 0, 2, 2], + [-2, 1, 1, 0, 0], + [0, -1, 0, 2, 2], + [2, 0, 0, 2, 1], + [2, 0, 1, 0, 0], + [-2, 0, 2, 2, 2], + [-2, 0, 1, 2, 1], + [2, 0, -2, 0, 1], + [2, 0, 0, 0, 1], + [0, -1, 1, 0, 0], + [-2, -1, 0, 2, 1], + [-2, 0, 0, 0, 1], + [0, 0, 2, 2, 1], + [-2, 0, 2, 0, 1], + [-2, 1, 0, 2, 1], + [0, 0, 1, -2, 0], + [-1, 0, 1, 0, 0], + [-2, 1, 0, 0, 0], + [1, 0, 0, 0, 0], + [0, 0, 1, 2, 0], + [0, 0, -2, 2, 2], + [-1, -1, 1, 0, 0], + [0, 1, 1, 0, 0], + [0, -1, 1, 2, 2], + [2, -1, -1, 2, 2], + [0, 0, 3, 2, 2], + [2, -1, 0, 2, 2], + ], + device=device, + ) + + nutation_terms = torch.tensor( + [ + [-171996, -174.2, 92025, 8.9], + [-13187, -1.6, 5736, -3.1], + [-2274, -0.2, 977, -0.5], + [2062, 0.2, -895, 0.5], + [1426, -3.4, 54, -0.1], + [712, 0.1, -7, 0], + [-517, 1.2, 224, -0.6], + [-386, -0.4, 200, 0], + [-301, 0, 129, -0.1], + [217, -0.5, -95, 0.3], + [-158, 0, 0, 0], + [129, 0.1, -70, 0], + [123, 0, -53, 0], + [63, 0, 0, 0], + [63, 0.1, -33, 0], + [-59, 0, 26, 0], + [-58, -0.1, 32, 0], + [-51, 0, 27, 0], + [48, 0, 0, 0], + [46, 0, -24, 0], + [-38, 0, 16, 0], + [-31, 0, 13, 0], + [29, 0, 0, 0], + [29, 0, -12, 0], + [26, 0, 0, 0], + [-22, 0, 0, 0], + [21, 0, -10, 0], + [17, -0.1, 0, 0], + [16, 0, -8, 0], + [-16, 0.1, 7, 0], + [-15, 0, 9, 0], + [-13, 0, 7, 0], + [-12, 0, 6, 0], + [11, 0, 0, 0], + [-10, 0, 5, 0], + [-8, 0, 3, 0], + [7, 0, -3, 0], + [-7, 0, 0, 0], + [-7, 0, 3, 0], + [-7, 0, 3, 0], + [6, 0, 0, 0], + [6, 0, -3, 0], + [6, 0, -3, 0], + [-6, 0, 3, 0], + [-6, 0, 3, 0], + [5, 0, 0, 0], + [-5, 0, 3, 0], + [-5, 0, 3, 0], + [-5, 0, 3, 0], + [4, 0, 0, 0], + [4, 0, 0, 0], + [4, 0, 0, 0], + [-4, 0, 0, 0], + [-4, 0, 0, 0], + [-4, 0, 0, 0], + [3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + ], + device=device, + ) + + # Using the tabulated values, compute the delta_longitude and + # delta_obliquity. + Xi = torch.tensor( + [X0, X1, X2, X3, X4], device=device + ) # a col mat in octave + + tabulated_argument = torch.matmul(Y_terms.float(), Xi) * (torch.pi / 180) + + delta_longitude = ( + nutation_terms[:, 0] + (nutation_terms[:, 1] * JCE) + ) * torch.sin(tabulated_argument) + delta_obliquity = ( + nutation_terms[:, 2] + (nutation_terms[:, 3] * JCE) + ) * torch.cos(tabulated_argument) + + nutation = dict() # init nutation dictionary + # Nutation in longitude + nutation["longitude"] = torch.sum(delta_longitude) / 36000000 + + # Nutation in obliquity + nutation["obliquity"] = torch.sum(delta_obliquity) / 36000000 + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + + return nutation + + +def true_obliquity_calculation(julian, nutation): + device = torch.device( + "cuda" + if torch.cuda.is_available() + else ( + "xpu" + if (hasattr(torch, "xpu") and torch.xpu.is_available()) + else "cpu" + ) + ) + """ + This function compute the true obliquity of the ecliptic. + + :param julian: + :param nutation: + :return: + """ + + p = torch.atleast_2d( + torch.tensor( + [ + 2.45, + 5.79, + 27.87, + 7.12, + -39.05, + -249.67, + -51.38, + 1999.25, + -1.55, + -4680.93, + 84381.448, + ], + device=device, + ) + ) + + # mean_obliquity = polyval(p, julian.ephemeris_millenium/10); + U = julian["ephemeris_millenium"] / 10 + mean_obliquity = ( + p[0, 0] * torch.pow(U, 10) + + p[0, 1] * torch.pow(U, 9) + + p[0, 2] * torch.pow(U, 8) + + p[0, 3] * torch.pow(U, 7) + + p[0, 4] * torch.pow(U, 6) + + p[0, 5] * torch.pow(U, 5) + + p[0, 6] * torch.pow(U, 4) + + p[0, 7] * torch.pow(U, 3) + + p[0, 8] * torch.pow(U, 2) + + p[0, 9] * U + + p[0, 10] + ) + + true_obliquity = (mean_obliquity / 3600) + nutation["obliquity"] + + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "xpu": + torch.xpu.empty_cache() + return true_obliquity + + +def abberation_correction_calculation(earth_heliocentric_position): + """ + This function compute the aberration_correction, as a function of the + earth-sun distance. + + :param earth_heliocentric_position: + :return: + """ + aberration_correction = -20.4898 / ( + 3600 * earth_heliocentric_position["radius"] + ) + return aberration_correction + + +def apparent_sun_longitude_calculation( + sun_geocentric_position, nutation, aberration_correction +): + """ + This function compute the sun apparent longitude + + :param sun_geocentric_position: + :param nutation: + :param aberration_correction: + :return: + """ + apparent_sun_longitude = ( + sun_geocentric_position["longitude"] + + nutation["longitude"] + + aberration_correction + ) + return apparent_sun_longitude + + +def apparent_stime_at_greenwich_calculation(julian, nutation, true_obliquity): + """ + This function compute the apparent sideral time at Greenwich. + + :param julian: + :param nutation: + :param true_obliquity: + :return: + """ + + JD = julian["day"] + JC = julian["century"] + + # Mean sideral time, in degrees + mean_stime = ( + 280.46061837 + + (360.98564736629 * (JD - 2451545)) + + (0.000387933 * torch.pow(JC, 2)) + - (torch.pow(JC, 3) / 38710000) + ) + + # Limit the range to [0-360]; + mean_stime = set_to_range(mean_stime, 0, 360) + + apparent_stime_at_greenwich = mean_stime + ( + nutation["longitude"] * torch.cos(true_obliquity * torch.pi / 180) + ) + return apparent_stime_at_greenwich + + +def sun_rigth_ascension_calculation( + apparent_sun_longitude, true_obliquity, sun_geocentric_position +): + """ + This function compute the sun rigth ascension. + :param apparent_sun_longitude: + :param true_obliquity: + :param sun_geocentric_position: + :return: + """ + + argument_numerator = ( + torch.sin(apparent_sun_longitude * torch.pi / 180) + * torch.cos(true_obliquity * torch.pi / 180) + ) - ( + torch.tan(sun_geocentric_position["latitude"] * torch.pi / 180) + * torch.sin(true_obliquity * torch.pi / 180) + ) + argument_denominator = torch.cos(apparent_sun_longitude * torch.pi / 180) + + sun_rigth_ascension = ( + torch.arctan2(argument_numerator, argument_denominator) + * 180 + / torch.pi + ) + # Limit the range to [0,360]; + sun_rigth_ascension = set_to_range(sun_rigth_ascension, 0, 360) + return sun_rigth_ascension + + +def sun_geocentric_declination_calculation( + apparent_sun_longitude, true_obliquity, sun_geocentric_position +): + """ + + :param apparent_sun_longitude: + :param true_obliquity: + :param sun_geocentric_position: + :return: + """ + + argument = ( + torch.sin(sun_geocentric_position["latitude"] * torch.pi / 180) + * torch.cos(true_obliquity * torch.pi / 180) + ) + ( + torch.cos(sun_geocentric_position["latitude"] * torch.pi / 180) + * torch.sin(true_obliquity * torch.pi / 180) + * torch.sin(apparent_sun_longitude * torch.pi / 180) + ) + + sun_geocentric_declination = torch.arcsin(argument) * 180 / torch.pi + return sun_geocentric_declination + + +def observer_local_hour_calculation( + apparent_stime_at_greenwich, location, sun_rigth_ascension +): + """ + This function computes observer local hour. + + :param apparent_stime_at_greenwich: + :param location: + :param sun_rigth_ascension: + :return: + """ + + observer_local_hour = ( + apparent_stime_at_greenwich + + location["longitude"] + - sun_rigth_ascension + ) + # Set the range to [0-360] + observer_local_hour = set_to_range(observer_local_hour, 0, 360) + return observer_local_hour + + +def topocentric_sun_position_calculate( + earth_heliocentric_position, + location, + observer_local_hour, + sun_rigth_ascension, + sun_geocentric_declination, +): + """ + This function compute the sun position (rigth ascension and declination) + with respect to the observer local position at the Earth surface. + + :param earth_heliocentric_position: + :param location: + :param observer_local_hour: + :param sun_rigth_ascension: + :param sun_geocentric_declination: + :return: + """ + + # Equatorial horizontal parallax of the sun in degrees + eq_horizontal_parallax = 8.794 / ( + 3600 * earth_heliocentric_position["radius"] + ) + + # Term u, used in the following calculations (in radians) + u = torch.arctan( + 0.99664719 + * torch.tan(torch.as_tensor(location["latitude"]) * torch.pi / 180) + ) + + # Term x, used in the following calculations + x = torch.cos(u) + ( + (torch.as_tensor(location["altitude"]) / 6378140) + * torch.cos(torch.as_tensor(location["latitude"]) * torch.pi / 180) + ) + + # Term y, used in the following calculations + y = (0.99664719 * torch.sin(u)) + ( + (torch.as_tensor(location["altitude"]) / 6378140) + * torch.sin(torch.as_tensor(location["latitude"]) * torch.pi / 180) + ) + + # Parallax in the sun rigth ascension (in radians) + nominator = ( + -x + * torch.sin(eq_horizontal_parallax * torch.pi / 180) + * torch.sin(observer_local_hour * torch.pi / 180) + ) + denominator = torch.cos(sun_geocentric_declination * torch.pi / 180) - ( + x + * torch.sin(eq_horizontal_parallax * torch.pi / 180) + * torch.cos(observer_local_hour * torch.pi / 180) + ) + sun_rigth_ascension_parallax = torch.arctan2(nominator, denominator) + # Conversion to degrees. + topocentric_sun_position = dict() + topocentric_sun_position["rigth_ascension_parallax"] = ( + sun_rigth_ascension_parallax * 180 / torch.pi + ) + + # Topocentric sun rigth ascension (in degrees) + topocentric_sun_position["rigth_ascension"] = sun_rigth_ascension + ( + sun_rigth_ascension_parallax * 180 / torch.pi + ) + + # Topocentric sun declination (in degrees) + nominator = ( + torch.sin(sun_geocentric_declination * torch.pi / 180) + - (y * torch.sin(eq_horizontal_parallax * torch.pi / 180)) + ) * torch.cos(sun_rigth_ascension_parallax) + denominator = torch.cos(sun_geocentric_declination * torch.pi / 180) - ( + y * torch.sin(eq_horizontal_parallax * torch.pi / 180) + ) * torch.cos(observer_local_hour * torch.pi / 180) + topocentric_sun_position["declination"] = ( + torch.arctan2(nominator, denominator) * 180 / torch.pi + ) + return topocentric_sun_position + + +def topocentric_local_hour_calculate( + observer_local_hour, topocentric_sun_position +): + """ + This function compute the topocentric local jour angle in degrees + + :param observer_local_hour: + :param topocentric_sun_position: + :return: + """ + + topocentric_local_hour = ( + observer_local_hour + - topocentric_sun_position["rigth_ascension_parallax"] + ) + return topocentric_local_hour + + +def sun_topocentric_zenith_angle_calculate( + location, topocentric_sun_position, topocentric_local_hour +): + """ + This function compute the sun zenith angle, taking into account the + atmospheric refraction. A default temperature of 283K and a + default pressure of 1010 mbar are used. + + :param location: + :param topocentric_sun_position: + :param topocentric_local_hour: + :return: + """ + + # Topocentric elevation, without atmospheric refraction + argument = ( + torch.sin(torch.as_tensor(location["latitude"]) * torch.pi / 180) + * torch.sin( + torch.as_tensor(topocentric_sun_position["declination"]) + * torch.pi + / 180 + ) + ) + ( + torch.cos(torch.as_tensor(location["latitude"]) * torch.pi / 180) + * torch.cos( + torch.as_tensor(topocentric_sun_position["declination"]) + * torch.pi + / 180 + ) + * torch.cos(torch.as_tensor(topocentric_local_hour) * torch.pi / 180) + ) + true_elevation = torch.arcsin(argument) * 180 / torch.pi + + # Atmospheric refraction correction (in degrees) + argument = true_elevation + (10.3 / (true_elevation + 5.11)) + refraction_corr = 1.02 / (60 * torch.tan(argument * torch.pi / 180)) + + # For exact pressure and temperature correction, use this, + # with P the pressure in mbar amd T the temperature in Kelvins: + # refraction_corr = (P/1010) * (283/T) * 1.02 / (60 * tan(argument * pi/180)); + + # Apparent elevation + apparent_elevation = true_elevation + refraction_corr + + sun = dict() + sun["zenith"] = 90 - apparent_elevation + + # Topocentric azimuth angle. The +180 conversion is to pass from astronomer + # notation (westward from south) to navigation notation (eastward from + # north); + nominator = torch.sin( + torch.as_tensor(topocentric_local_hour * torch.pi / 180) + ) + denominator = ( + torch.cos(torch.as_tensor(topocentric_local_hour) * torch.pi / 180) + * torch.sin(torch.as_tensor(location["latitude"]) * torch.pi / 180) + ) - ( + torch.tan( + torch.as_tensor(topocentric_sun_position["declination"]) + * torch.pi + / 180 + ) + * torch.cos(torch.as_tensor(location["latitude"]) * torch.pi / 180) + ) + sun["azimuth"] = ( + torch.arctan2(nominator, denominator) * 180 / torch.pi + ) + 180 + + # Set the range to [0-360] + sun["azimuth"] = set_to_range(sun["azimuth"], 0, 360) + return sun + + +def set_to_range(var, min_interval, max_interval): + """ + Sets a variable in range min_interval and max_interval + + :param var: + :param min_interval: + :param max_interval: + :return: + """ + var = var - max_interval * torch.floor(var / max_interval) + + if var < min_interval: + var = var + max_interval + return var diff --git a/util/__init__.py b/util/__init__.py index e6d64ea..1fcd232 100644 --- a/util/__init__.py +++ b/util/__init__.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- # __author__ = 'xlinfr' -from qgis.core import Qgis, QgsMessageLog -from .umep_installer import locate_py, setup_umep_python -from qgis.PyQt.QtWidgets import QMessageBox import traceback # The .egg packages shipped with QGIS sometimes appear before the user site dir @@ -14,6 +11,9 @@ sys.path.insert(0, site.getusersitepackages()) +from qgis.PyQt.QtWidgets import QMessageBox +from .umep_installer import locate_py, setup_umep_python +from qgis.core import Qgis, QgsMessageLog try: # import timezonefinder @@ -24,7 +24,7 @@ level=Qgis.MessageLevel.Info, ) -except BaseException: +except: if ( QMessageBox.question( None, diff --git a/util/imageMorphometricParms_v2.py b/util/imageMorphometricParms_v2.py index 5014aaa..883853d 100644 --- a/util/imageMorphometricParms_v2.py +++ b/util/imageMorphometricParms_v2.py @@ -26,13 +26,12 @@ import numpy as np import scipy.ndimage.interpolation as sc -# import matplotlib as plt - def imagemorphparam_v2(dsm, dem, scale, mid, dtheta, feedback, imp_point): - # too deal with irregular grids - numPixels = len(dsm[np.where(dsm != -9999)]) + numPixels = len( + dsm[np.where(dsm != -9999)] + ) # too deal with irregular grids build = dsm - dem build[(build < 3.0)] = ( @@ -80,8 +79,7 @@ def imagemorphparam_v2(dsm, dem, scale, mid, dtheta, feedback, imp_point): feedback.setProgress(int(angle / 3.6)) # Rotating buildings - # d = sc.rotate(build, angle, order=0, reshape=False, mode='nearest') - # #old + # d = sc.rotate(build, angle, order=0, reshape=False, mode='nearest') #old a = sc.rotate( build, angle, order=0, reshape=True, mode="constant", cval=-99 ) @@ -92,14 +90,14 @@ def imagemorphparam_v2(dsm, dem, scale, mid, dtheta, feedback, imp_point): n = c.shape[1] imid = np.floor((n / 2.0)) - # filter for fai. Moved inside loop since size change if grid is - # irregular + # filter for fai. Moved inside loop since size change if grid is irregular filt1 = np.ones((n, 1)) * -1.0 filt2 = np.ones((n, 1)) filt = np.array(np.hstack((filt1, filt2))).conj().T buildZero = np.copy(a) - # remove -99 to avoid one 99 meter tall building wall - buildZero[buildZero == -99] = 0 + buildZero[buildZero == -99] = ( + 0 # remove -99 to avoid one 99 meter tall building wall + ) for i in np.arange(1, c.shape[0]): c[int(i) - 1, :] = np.sum( @@ -118,13 +116,13 @@ def imagemorphparam_v2(dsm, dem, scale, mid, dtheta, feedback, imp_point): bld = lineMid[np.where(lineMid > -99)] wall = walltemp[np.where(lineMid > -99)] ly = bld.shape[0] # number of pixels to consider in NtoS - # !TODO should this consider full length (EtoW) of grid and if so, how? - lx = 1 + lx = 1 #!TODO should this consider full length (EtoW) of grid and if so, how? wall = wall[np.where(wall > 2)] # wall vector fai[j] = np.sum(wall) / ((lx * ly) / scale) - # building vector: change from 0 to 2 : 20150906 - bld = bld[np.where(bld > 2)] + bld = bld[ + np.where(bld > 2) + ] # building vector: change from 0 to 2 : 20150906 pai[j] = np.float32(bld.shape[0]) / (lx * ly) deg[j] = angle if np.float32(bld.shape[0]) == 0: diff --git a/util/landCoverFractions_v2.py b/util/landCoverFractions_v2.py index 79a09fb..47e5608 100644 --- a/util/landCoverFractions_v2.py +++ b/util/landCoverFractions_v2.py @@ -25,36 +25,22 @@ def landcover_v2(lc_grid, mid, dtheta, feedback, imp_point, iter): lc_gridvec = lc_grid[np.where(lc_grid == i + 1)] if lc_gridvec.size > 0: # lc_frac_all[0, i] = round((lc_gridvec.size * 1.0) / (lc_grid.size * 1.0), 3) - # ignoring NoData (0) pixels lc_frac_all[0, i] = round( (lc_gridvec.size * 1.0) / (lc_grid.size - (lc_grid == 0).sum()), 3, - ) + ) # ignoring NoData (0) pixels # Anisotropic (Adjusted for irregular grids) lc_frac = np.zeros((int(360.0 / dtheta), iter)) deg = np.zeros((int(360.0 / dtheta), 1)) - # n = lc_grid.shape[0] - # imid = np.floor((n/2.)) - # if mid == 1: - # dY = np.int16(np.arange(np.dot(1, imid))) # the half length of the grid (y) - # else: #moved inside loop as it varies on an irregular grid - # dY = np.int16(np.arange(np.dot(1, n))) # the whole length of the grid - # (y) - - # dX = np.int16(np.arange(imid, imid+1)) - # lx = dX.shape[0] - # ly = dY.shape[0] - j = int(0) for angle in np.arange(0, 360, dtheta): if imp_point == 1: feedback.setProgress(int(angle / 3.6)) - # d = sc.rotate(lc_grid, angle, order=0, reshape=False, mode='nearest') - # #old + # d = sc.rotate(lc_grid, angle, order=0, reshape=False, mode='nearest') #old d = sc.rotate( lc_grid, angle, order=0, reshape=True, mode="constant", cval=-99 ) @@ -69,11 +55,8 @@ def landcover_v2(lc_grid, mid, dtheta, feedback, imp_point, iter): lineMid = d[:, int(imid)] # whole center line bld = lineMid[np.where(lineMid > 0)] # line within grid only - # b = np.round(((lc_grid.max()-lc_grid.min())/d.max())*d+lc_grid.min(), 0) #not needed anymore - # bld = b[dY, dX] # lc array ly = bld.shape[0] # number of pixels to consider in NtoS - # !TODO should this consider full length (EtoW) of grid and if so, how? - lx = 1 + lx = 1 #!TODO should this consider full length (EtoW) of grid and if so, how? for i in range(0, iter): bldtemp = bld[np.where(bld == i + 1)] # lc vector diff --git a/util/misc.py b/util/misc.py index 784803e..6d9783b 100644 --- a/util/misc.py +++ b/util/misc.py @@ -7,9 +7,8 @@ import os import re -# Slope and aspect used in SEBE and Wall aspect - +# Slope and aspect used in SEBE and Wall aspect def get_ders(dsm, scale): # dem,_,_=read_dem_grid(dem_file) dx = 1 / scale diff --git a/util/ncWMSConnector.py b/util/ncWMSConnector.py index 58b38c4..469b9cb 100644 --- a/util/ncWMSConnector.py +++ b/util/ncWMSConnector.py @@ -12,7 +12,7 @@ import netCDF4 as nc4 from requests.auth import HTTPDigestAuth import requests -except BaseException: +except: pass from collections import OrderedDict @@ -40,18 +40,18 @@ def __init__(self): "Snowf", "SWdown", ] - # The first data point available in the dataset on the server - self.start_date = dt(1979, 0o1, 0o1, 00, 00, 00) - # The final data point available in the dataset on the server - self.end_date = dt(2015, 12, 31, 21, 00, 00) + self.start_date = dt( + 1979, 0o1, 0o1, 00, 00, 00 + ) # The first data point available in the dataset on the server + self.end_date = dt( + 2015, 12, 31, 21, 00, 00 + ) # The final data point available in the dataset on the server self.time_res = 3600 * 3 # Time resolution of data in seconds - # Number of days of data to request at a time from server (helps manage - # load on server and produce a progress bar) - self.request_length = 200 + self.request_length = 200 # Number of days of data to request at a time from server (helps manage load on server and produce a progress bar) self.request_params = {} # Holds a dict of request parameters - # Stores a list of downloaded netCDF files for different time subsets, - # with start datetime as key - self.results = OrderedDict() + self.results = ( + OrderedDict() + ) # Stores a list of downloaded netCDF files for different time subsets, with start datetime as key self.killed = False # Aborts execution if needed def kill(self): @@ -190,14 +190,12 @@ def get_data( upperright_lon, ], } - # start_dates = pd.date_range(start_date, end_date, freq='%dD' % - # (self.request_length,)).to_datetime() # to_datetime() not needed... + # start_dates = pd.date_range(start_date, end_date, freq='%dD' % (self.request_length,)).to_datetime() # to_datetime() not needed... start_dates = pd.date_range( start_date, end_date, freq="%dD" % (self.request_length,) ) - # Create queue of retrievals, and safeguard against over-running - # dataset end date + # Create queue of retrievals, and safeguard against over-running dataset end date for s in range(0, len(start_dates)): if self.killed: break @@ -215,18 +213,19 @@ def get_data( final_date = True end_date_candidate = self.end_date - # Get data from start date to next start date minus time resolution self.results[start_dates[s]] = self.retrieve( start_dates[s], end_date_candidate - ) + ) # Get data from start date to next start date minus time resolution if update is not None: update.emit( { "progress": 100 * float(s) / float(len(start_dates)), - "message": " (%s to %s)" - % ( - start_dates[s].strftime("%Y-%m-%d"), - end_date_candidate.strftime("%Y-%m-%d"), + "message": ( + " (%s to %s)" + % ( + start_dates[s].strftime("%Y-%m-%d"), + end_date_candidate.strftime("%Y-%m-%d"), + ) ), } ) @@ -320,13 +319,13 @@ def average_data(self, period, method): time_bins = nc4.num2date(times[:], units=times.units) # Workaround: The server currently has a slightly wobble in time bins, which we know to be precise - # re-do them. The first one tends to be correct, and drift sets in - # later. + # re-do them. The first one tends to be correct, and drift sets in later. time_bins = pd.date_range( time_bins[0], periods=len(time_bins), freq=str(self.time_res) + "s" ) - # Assume all requested variables have the same shape - dim = combined_data.variables[self.vars[0]][:].shape + dim = combined_data.variables[self.vars[0]][ + : + ].shape # Assume all requested variables have the same shape if period is None: new_time_bins = time_bins @@ -339,8 +338,7 @@ def average_data(self, period, method): ).index new_dim = [len(new_time_bins), dim[1], dim[2]] - # Duplicate all non-time dimensions from original file, and add new - # time dimension + # Duplicate all non-time dimensions from original file, and add new time dimension for dname, the_dim in combined_data.dimensions.items(): if dname == "time": t = new_data.createDimension(dname, len(new_time_bins)) @@ -356,17 +354,18 @@ def average_data(self, period, method): ) try: if v_name == "time": - # Replace seconds with hours (if seconds) to save data - outVar.units = varin.units.replace("seconds", "hours") + outVar.units = varin.units.replace( + "seconds", "hours" + ) # Replace seconds with hours (if seconds) to save data else: outVar.units = varin.units - except BaseException: + except: pass try: outVar.setncatts( {k: varin.getncattr(k) for k in varin.ncattrs()} ) - except BaseException: + except: pass # Perform time-averaging and add data to new file @@ -405,8 +404,9 @@ def average_data(self, period, method): new_data.close() combined_data.close() - # Remove individual files as no longer needed - [os.remove(v) for v in list(self.results.values())] + [ + os.remove(v) for v in list(self.results.values()) + ] # Remove individual files as no longer needed return tmp # Return path to combined file def retrieve(self, start_period, end_period): @@ -416,19 +416,23 @@ def retrieve(self, start_period, end_period): """ baseURL = "http://data.urban-climate.net:8080/umep/download" parms = { - "BBOX": "%f,%f,%f,%f" - % ( - float(self.request_params["bbox"][1]), - float(self.request_params["bbox"][0]), - float(self.request_params["bbox"][3]), - float(self.request_params["bbox"][2]), + "BBOX": ( + "%f,%f,%f,%f" + % ( + float(self.request_params["bbox"][1]), + float(self.request_params["bbox"][0]), + float(self.request_params["bbox"][3]), + float(self.request_params["bbox"][2]), + ) ), "DATASET": "watch", "VARIABLES": ",".join(self.request_params["vars"]), - "TIME": "%s/%s" - % ( - start_period.strftime("%Y-%m-%dT%H:%M:%S"), - end_period.strftime("%Y-%m-%dT%H:%M:%S"), + "TIME": ( + "%s/%s" + % ( + start_period.strftime("%Y-%m-%dT%H:%M:%S"), + end_period.strftime("%Y-%m-%dT%H:%M:%S"), + ) ), } try: diff --git a/util/shadowingfunctions.py b/util/shadowingfunctions.py index 7ede385..14732c2 100644 --- a/util/shadowingfunctions.py +++ b/util/shadowingfunctions.py @@ -3,8 +3,6 @@ import numpy as np from math import radians -# from numba import jit - def shadowingfunctionglobalradiation( a, azimuth, altitude, scale, feedback, forsvf @@ -48,11 +46,7 @@ def shadowingfunctionglobalradiation( while amaxvalue >= dz and np.abs(dx) < sizex and np.abs(dy) < sizey: if forsvf == 0: feedback.setProgress(int(index * total)) - # dlg.progressBar.setValue(index) - # while np.logical_and(np.logical_and(amaxvalue >= dz, np.abs(dx) <= sizex), np.abs(dy) <= sizey):(np.logical_and(amaxvalue >= dz, np.abs(dx) <= sizex), np.abs(dy) <= sizey): - # if np.logical_or(np.logical_and(pibyfour <= azimuth, azimuth < - # threetimespibyfour), np.logical_and(fivetimespibyfour <= azimuth, - # azimuth < seventimespibyfour)): + if ( pibyfour <= azimuth and azimuth < threetimespibyfour @@ -83,8 +77,6 @@ def shadowingfunctionglobalradiation( temp[int(xp1) - 1 : int(xp2), int(yp1) - 1 : int(yp2)] = ( a[int(xc1) - 1 : int(xc2), int(yc1) - 1 : int(yc2)] - dz ) - # f = np.maximum(f, temp) # bad performance in python3. Replaced with - # fmax f = np.fmax(f, temp) index += 1.0 @@ -95,9 +87,6 @@ def shadowingfunctionglobalradiation( return sh -# @jit(nopython=True) - - def shadowingfunction_20( a, vegdem, @@ -111,22 +100,6 @@ def shadowingfunction_20( forsvf, ): - # plt.ion() - # fig = plt.figure(figsize=(24, 7)) - # plt.axis('image') - # ax1 = plt.subplot(2, 3, 1) - # ax2 = plt.subplot(2, 3, 2) - # ax3 = plt.subplot(2, 3, 3) - # ax4 = plt.subplot(2, 3, 4) - # ax5 = plt.subplot(2, 3, 5) - # ax6 = plt.subplot(2, 3, 6) - # ax1.title.set_text('fabovea') - # ax2.title.set_text('gabovea') - # ax3.title.set_text('vegsh at ' + str(altitude)) - # ax4.title.set_text('lastfabovea') - # ax5.title.set_text('lastgabovea') - # ax6.title.set_text('vegdem') - # This function casts shadows on buildings and vegetation units. # New capability to deal with pergolas 20210827 @@ -144,8 +117,6 @@ def shadowingfunction_20( barstep = np.max([sizex, sizey]) total = 100.0 / barstep feedback.setProgress(0) - # dlg.progressBar.setRange(0, barstep) - # dlg.progressBar.setValue(0) # initialise parameters dx = 0.0 @@ -250,21 +221,6 @@ def shadowingfunction_20( vegsh[(vegsh * sh > 0.0)] = 0.0 vbshvegsh = vegsh + vbshvegsh # removing shadows 'behind' buildings - # im1 = ax1.imshow(fabovea) - # im2 = ax2.imshow(gabovea) - # im3 = ax3.imshow(vegsh) - # im4 = ax4.imshow(lastfabovea) - # im5 = ax5.imshow(lastgabovea) - # im6 = ax6.imshow(vegshtest) - # im1 = ax1.imshow(tempvegdem) - # im2 = ax2.imshow(tempvegdem2) - # im3 = ax3.imshow(vegsh) - # im4 = ax4.imshow(templastfabovea) - # im5 = ax5.imshow(templastgabovea) - # im6 = ax6.imshow(vegshtest) - # plt.show() - # plt.pause(0.05) - index += 1.0 sh = 1.0 - sh @@ -273,27 +229,6 @@ def shadowingfunction_20( vegsh = 1.0 - vegsh vbshvegsh = 1.0 - vbshvegsh - # plt.close() - # plt.ion() - # fig = plt.figure(figsize=(24, 7)) - # plt.axis('image') - # ax1 = plt.subplot(1, 3, 1) - # im1 = ax1.imshow(vegsh) - # plt.colorbar(im1) - - # ax2 = plt.subplot(1, 3, 2) - # im2 = ax2.imshow(vegdem2) - # plt.colorbar(im2) - # plt.title('TDSM') - - # ax3 = plt.subplot(1, 3, 3) - # im3 = ax3.imshow(vegdem) - # plt.colorbar(im3) - # plt.tight_layout() - # plt.title('CDSM') - # plt.show() - # plt.pause(0.05) - shadowresult = {"sh": sh, "vegsh": vegsh, "vbshvegsh": vbshvegsh} return shadowresult @@ -580,9 +515,6 @@ def shadowingfunction_findwallID( # yet (saved in previous iteration). buildIDSeen = (temp2 > 0) * temp3 * tempwallID + buildIDSeen - # voxelHeight = (temp2 > 0) * temp3 * temp2 + voxelHeight # seen wall - # heights - # voxelHeight = the elevation on a wall that is seen from a pixel with the given altitude and azimuth (only above ground leve, i.e. (temp2 > 0)). # voxelHeight = wall height - descending wall, i.e. temp_wallHeight - # temp2. Only applicable to pixels where there is no value from @@ -590,8 +522,6 @@ def shadowingfunction_findwallID( voxelHeight = (temp2 > 0) * temp3 * ( temp_wallHeight - temp2 ) + voxelHeight - # voxelHeight = (temp2 > 0) * temp3 * (temp_wallHeight - (temp2 * - # (temp2 > 0))) + voxelHeight # seen wall heights # Remember pixels previous iteration that walls have not progressed # into yet. @@ -621,11 +551,7 @@ def shadowingfunction_findwallID( # Find unique values in c d = np.unique(c, axis=0) # Remove rows where both columns are zero - # d = d[((d[:,0] > 0) & (d[:,1] > 0)), :] - # d = d[d[:,:] > 0, :] d = d[~np.all(d == 0, axis=1)] - # d = d[d[:, 0] > 0, :] - # d = d[d[:, 1] > 0, :] not_in_list = 0 in_list = 0 @@ -650,55 +576,9 @@ def shadowingfunction_findwallID( (buildIDSeen == temp_id) & (voxelHeight_ceil == temp_height) ] = 0 - # if ((np.any(buildIDSeen == temp_id) & ~np.all(voxelHeight_ceil == temp_height)) or (~np.all(buildIDSeen == temp_id) & np.any(voxelHeight_ceil == temp_height))): - # print('temp_id = ' + str(temp_id)) - # print('temp_height = ' + str(temp_height)) - - # ax = plt.subplot(1, 2, 1) - # im = ax.imshow(buildIDSeen, vmin=0, vmax=uniqueWallIDs.max()) - # ax1 = plt.subplot(1, 2, 2) - # im = ax1.imshow(voxelHeight, vmin=0, vmax=40) - # plt.pause(0.1) # In interactive mode, need a small delay to get the plot to appear - # plt.draw() - # Correct for shadows, i.e. remove weird pixels on top of buildings etc buildIDSeen = buildIDSeen * (1 - sh) voxelHeight = voxelHeight * (1 - sh) voxelId = voxelId * (1 - sh) return buildIDSeen, voxelHeight, voxelId - - -# temp[xp1:xp2, yp1:yp2] = dsm[xc1:xc2, yc1:yc2] - dz -# f = np.fmax(f, temp) #Moving building shadow - -# if index == 1: #Remove walls on "wrong" side of buildings during first -# iteration - -# firstMove = (f-dsm) > 0 -# if (pibyfour <= azimuth and azimuth < threetimespibyfour) or \ -# (fivetimespibyfour <= azimuth and azimuth < seventimespibyfour): -# dy = signsinazimuth * index -# dx = -1 * signcosazimuth -# else: -# dy = signsinazimuth -# dx = -1 * signcosazimuth * index - -# absdx = np.abs(dx) -# absdy = np.abs(dy) - -# xc1b = int((dx+absdx)/2) -# xc2b = int(rows+(dx-absdx)/2) -# yc1b = int((dy+absdy)/2) -# yc2b = int(cols+(dy-absdy)/2) - -# xp1b = int(-((dx-absdx)/2)) -# xp2b = int(rows-(dx+absdx)/2) -# yp1b = int(-((dy-absdy)/2)) -# yp2b = int(cols-(dy+absdy)/2) - -# temp[xp1b:xp2b, yp1b:yp2b] = dsm[xc1b:xc2b, yc1b:yc2b] - -# wallSeen = temp-dsm -# wallSeen[wallSeen > 0] = 1 -# wallSeen[wallSeen < 0] = 0 diff --git a/util/shadowingfunctions_torch.py b/util/shadowingfunctions_torch.py new file mode 100644 index 0000000..badb699 --- /dev/null +++ b/util/shadowingfunctions_torch.py @@ -0,0 +1,464 @@ +# -*- coding: utf-8 -*- +# Ready for python action! +from math import radians + +try: + import torch + import torch.nn.functional as F +except: + pass + + +def _to_tensor(x, device, dtype=torch.float32): + if isinstance(x, torch.Tensor): + return x.to(device) + if x is None: + return None + return torch.tensor(x, dtype=dtype, device=device) + + +def shadowingfunctionglobalradiation( + a, azimuth, altitude, scale, feedback, forsvf, device=torch.device("cpu") +): + """ + Computes global radiation shadows on a DSM using PyTorch hardware acceleration. + + This function leverages `F.grid_sample` to perform global map translations, + completely removing multi-index array slicing and trigonometric quadrant switches. + It is heavily optimized for GPU matrix operations. + + Args: + a (torch.Tensor or numpy.ndarray): Digital Surface Model (DSM) matrix. + azimuth (float): Sun azimuth angle in degrees. + altitude (float): Sun altitude angle in degrees. + scale (float): Spatial resolution modifier (1 pixel = 1 meter -> 1.0). + feedback (QgsProcessingFeedback or similar): Object used to report algorithm progress. + forsvf (int): Flag to toggle progress reporting (0 = report progress, 1 = skip). + device (torch.device, optional): Device context for execution. Defaults to CPU. + + Returns: + torch.Tensor: Binary shadow mask (1.0 = in sun, 0.0 = in shadow) matching `a.dtype`. + """ + # Ensure inputs are correctly formatted PyTorch tensors on the target device + if not isinstance(a, torch.Tensor): + a = torch.tensor(a, device=device) + else: + a = a.to(device=device) + + # Convert angular sun positions to radians + degrees = torch.pi / 180.0 + azimuth = azimuth * degrees + altitude = altitude * degrees + + sizex, sizey = a.shape + + # Track progress bounds if requested + if forsvf == 0: + barstep = max(sizex, sizey) + total = 100.0 / barstep + + # Clone initial DSM layout to accumulate the upper shadow envelope + f = a.clone() + + # --- COORDINATE GRID GENERATION FOR GLOBAL TRANSFORMATION --- + y, x = torch.meshgrid( + torch.linspace(-1, 1, sizex, device=device), + torch.linspace(-1, 1, sizey, device=device), + indexing="ij", + ) + # Target PyTorch shape: (1, H, W, 2) with (X, Y) layout at dim=-1 + grille_base = torch.stack((x, y), dim=-1).unsqueeze(0) + + # Convert the 2D DSM to a 4D tensor chunk (1, 1, H, W) for grid sampling + a_4d = a.unsqueeze(0).unsqueeze(0) + + # Pre-calculate sun path factors + amaxvalue = torch.max(a).item() + sinazimuth = torch.sin(azimuth) + cosazimuth = torch.cos(azimuth) + tanaltitudebyscale = torch.tan(altitude) / scale + + # Dynamically estimate the safety limit for our vector operations + max_steps = int(amaxvalue / tanaltitudebyscale.item()) + 2 + + # Vectorized loop structure (Eliminates slice variables and block switches) + for step_idx in range(1, max_steps): + index = float(step_idx) + + if forsvf == 0: + feedback.setProgress(int(index * total)) + + # 1. Project ground physical shadow offset (in pixels) + shift_x = index * sinazimuth / scale + shift_y = index * cosazimuth / scale + dz = index * tanaltitudebyscale + + # Safe breakout loop check if rays completely clear the canvas boundaries + if abs(shift_x) >= sizey and abs(shift_y) >= sizex: + break + + # 2. Shift the underlying layout grid space (-1 to 1 space boundaries) + grille_deplacee = grille_base.clone() + grille_deplacee[..., 0] -= (2.0 * shift_x) / (sizey - 1) + grille_deplacee[..., 1] -= (2.0 * shift_y) / (sizex - 1) + + # 3. Retrieve translated elevation layout using bilinear interpolation + temp = ( + F.grid_sample( + a_4d, grille_deplacee, mode="bilinear", padding_mode="border" + ).squeeze() + - dz + ) + + # 4. Amalgamate upper terrain shadow bounds + f = torch.maximum(f, temp) + + # Calculate local shadow gaps + f = f - a + + # Binary conversion: Where f == 0, the ground matches the shadow envelope (it is in sun) + sh = (f == 0).to(dtype=a.dtype) + + return sh + + +# @jit(nopython=True) +def shadowingfunction_20( + a, + vegdem, + vegdem2, + azimuth, + altitude, + scale, + amaxvalue, + bush, + feedback, + forsvf, + device=torch.device("cpu"), +): + + a = _to_tensor(a, device) + vegdem = _to_tensor(vegdem, device) + vegdem2 = _to_tensor(vegdem2, device) + bush = _to_tensor(bush, device) + + degrees = torch.pi / 180.0 + azimuth = azimuth * degrees + altitude = altitude * degrees + + sizex = a.shape[0] + sizey = a.shape[1] + + if forsvf == 0: + barstep = max(sizex, sizey) + total = 100.0 / barstep + feedback.setProgress(0) + + dz = 0.0 + temp = torch.zeros((sizex, sizey), device=device, dtype=a.dtype) + tempvegdem = torch.zeros((sizex, sizey), device=device, dtype=a.dtype) + tempvegdem2 = torch.zeros((sizex, sizey), device=device, dtype=a.dtype) + templastfabovea = torch.zeros((sizex, sizey), device=device, dtype=a.dtype) + templastgabovea = torch.zeros((sizex, sizey), device=device, dtype=a.dtype) + bushplant = bush > 1.0 + sh = torch.zeros((sizex, sizey), device=device, dtype=a.dtype) + vbshvegsh = torch.zeros((sizex, sizey), device=device, dtype=a.dtype) + vegsh = bushplant.to(dtype=a.dtype) + f = a + + shvoveg = vegdem.clone() + y, x = torch.meshgrid( + torch.linspace(-1, 1, sizex, device=device), + torch.linspace(-1, 1, sizey, device=device), + indexing="ij", + ) + grille_base = torch.stack((x, y), dim=-1).unsqueeze(0) + + # Preparation of the layers packed for grid_sample + a_4d = a.unsqueeze(0).unsqueeze(0).float() + veg_4d = vegdem.unsqueeze(0).unsqueeze(0).float() + veg2_4d = vegdem2.unsqueeze(0).unsqueeze(0).float() + + # Trigonometric params for the sun's step + sinazimuth = torch.sin(azimuth) + cosazimuth = torch.cos(azimuth) + tanaltitudebyscale = torch.tan(altitude) / scale + + # Calculation of the maximum of steps + # We stop when the shadow exceed the max possible height + max_steps = int(amaxvalue / tanaltitudebyscale) + 2 + + # Facteur d'avancement du rayon (équivalent géométrique de ton ancien 'ds') + # Plus besoin de gros blocs If/Else selon les quadrants du soleil ! + delta_z_par_pas = tanaltitudebyscale + + for index in range(1, max_steps): + # 1. Calculations of the slide + shift_x = index * sinazimuth / scale + shift_y = index * cosazimuth / scale + dz = index * delta_z_par_pas + + # Stop if the shadow is completely outside of the map + if abs(shift_x) >= sizey and abs(shift_y) >= sizex: + break + + # 2. Applying the offset to our normalized coordinate sheet (-1 to 1) + grille_deplacee = grille_base.clone() + grille_deplacee[..., 0] -= (2.0 * shift_x) / (sizey - 1) + grille_deplacee[..., 1] -= (2.0 * shift_y) / (sizex - 1) + + # 3. Global Material Sampling (Instant Layer Swipe) + temp = ( + F.grid_sample( + a_4d, grille_deplacee, mode="bilinear", padding_mode="border" + ).squeeze() + - dz + ) + tempvegdem = ( + F.grid_sample( + veg_4d, grille_deplacee, mode="bilinear", padding_mode="border" + ).squeeze() + - dz + ) + tempvegdem2 = ( + F.grid_sample( + veg2_4d, + grille_deplacee, + mode="bilinear", + padding_mode="border", + ).squeeze() + - dz + ) + + # 4. Update of cast shadow volumes + f = torch.fmax(f, temp) + shvoveg = torch.fmax(shvoveg, tempvegdem) + + sh = torch.where( + f > a, + torch.tensor(1.0, device=device), + torch.tensor(0.0, device=device), + ) + fabovea = tempvegdem > a + gabovea = tempvegdem2 > a + + # 5. Pergola logic (Overlaying layers with the & operator) + templastfabovea = tempvegdem + delta_z_par_pas + templastgabovea = tempvegdem2 + delta_z_par_pas + + lastfabovea = templastfabovea > a + lastgabovea = templastgabovea > a + + # If all 4 layers are True at the same time, it's a pergola (light passes through). + is_pergola = fabovea & gabovea & lastfabovea & lastgabovea + + # The shadow exists if one of the layers is True, UNLESS it's a pergola. + vegsh2 = (fabovea | gabovea | lastfabovea | lastgabovea) & ( + ~is_pergola + ) + vegsh2 = vegsh2.float() + + # Accumulation of vegetation shadows + vegsh = torch.maximum(vegsh, vegsh2) + vegsh = torch.where( + vegsh * sh > 0, torch.tensor(0.0, device=device), vegsh + ) + vbshvegsh.add_(vegsh) + + sh = 1.0 - sh + vbshvegsh = torch.where( + vbshvegsh > 0.0, torch.tensor(1.0, device=device), vbshvegsh + ) + vbshvegsh = vbshvegsh - vegsh + vegsh = 1.0 - vegsh + vbshvegsh = 1.0 - vbshvegsh + + shadowresult = {"sh": sh, "vegsh": vegsh, "vbshvegsh": vbshvegsh} + return shadowresult + + +def shadowingfunction_findwallID( + dsm, + azimuth, + altitude, + scale, + walls, + uniqueWallIDs, + dem, + wall2d_id, + voxel_height, + voxelId_list, + facesh, + wall_dict, + sh, + device, +): + """ + Identifies which wall ID and voxel height are visible along solar ray vectors from ground pixels. + + This version removes explicit pixel-slice shifting loops by utilizing PyTorch's hardware-accelerated + `F.grid_sample` engine. It implements nearest-neighbor interpolation to prevent spatial distortion + of discrete categorical Wall IDs during matrix transformation layers. + + Args: + dsm (torch.Tensor): Digital Surface Model tensor. + azimuth (float): Sun azimuth angle in degrees. + altitude (float): Sun altitude angle in degrees. + scale (float): Spatial resolution modifier (1 pixel = 1 meter -> 1.0). + walls (torch.Tensor): Height profile of the pixels representing building walls. + uniqueWallIDs (torch.Tensor): Matrix containing distinct structural identifiers for each wall. + dem (torch.Tensor): Digital Elevation Model representing bare earth topography. + wall2d_id (torch.Tensor or list): Reference map containing baseline wall components. + voxel_height (torch.Tensor or list): Reference metric indicating structural slice heights. + voxelId_list (torch.Tensor or list): Linear index tracker for specific 3D voxel spaces. + facesh (torch.Tensor): Binary mask identifying building walls shaded by their own geometry. + wall_dict (dict): Dictionary mapping categorical string/int Wall IDs to their true heights. + sh (torch.Tensor): Baseline ground shadow mask layer (1 = sun, 0 = shadow). + device (torch.device): Execution hardware targeting context (CPU/GPU). + + Returns: + tuple: A tuple containing: + - buildIDSeen (torch.Tensor): Categorical ID of the wall casting a shadow onto the pixel. + - voxelHeight (torch.Tensor): Accumulated vertical shadow volume height profile. + - voxelId (torch.Tensor): Specific structural voxel block identifier seen by the layout. + """ + # 1. PRE-PROCESSING & FORMATTING + dsm = dsm - dem + dsm = torch.where(dsm < 0.5, torch.tensor(0.0, device=device), dsm) + + # Conversion degrees to radians + azimuth_rad = radians(azimuth) + altitude_rad = radians(altitude) + + rows, cols = dsm.shape + + # Initialise tracked arrays + buildIDSeen = torch.zeros((rows, cols), device=device) + voxelHeight = torch.zeros((rows, cols), device=device) + temp3 = torch.ones((rows, cols), device=device, dtype=torch.bool) + + # Mask wall tracking based on facing attributes + uniqueWallIDs_masked = uniqueWallIDs * facesh + + # Build a high-performance native tensor lookup table for wall heights + max_wall_id = int(max(wall_dict.keys())) if wall_dict else 0 + wall_height_lookup = torch.zeros(max_wall_id + 1, device=device) + for k, v in wall_dict.items(): + wall_height_lookup[int(k)] = v + + # --- COORDINATE GRID GENERATION FOR THE SAMPLING TRANSFORMS --- + y, x = torch.meshgrid( + torch.linspace(-1, 1, rows, device=device), + torch.linspace(-1, 1, cols, device=device), + indexing="ij", + ) + grille_base = torch.stack((x, y), dim=-1).unsqueeze(0) + + # Emballer uniqueWallIDs in 4D (1, 1, H, W) for grid_sample + # We keep it as float for grid_sample, and convert back to long for indexing + wall_id_4d = uniqueWallIDs_masked.unsqueeze(0).unsqueeze(0).float() + + # Trig variables + sinazimuth = torch.sin(torch.tensor(azimuth_rad, device=device)) + cosazimuth = torch.cos(torch.tensor(azimuth_rad, device=device)) + tanaltitudebyscale = ( + torch.tan(torch.tensor(altitude_rad, device=device)) / scale + ) + + amaxvalue = torch.max(dsm) + max_steps = int(amaxvalue / tanaltitudebyscale) + 2 + + # 2. CORE VECTORIZED GRID-SHIFTING LOOP + for index in range(1, max_steps): + # Project spatial offsets on pixel dimensions + shift_x = index * sinazimuth / scale + shift_y = index * cosazimuth / scale + dz = index * tanaltitudebyscale + + # Break early if ray projections completely escape spatial boundaries + if abs(shift_x) >= cols and abs(shift_y) >= rows: + break + + # Displace the structural tracking sheet + grille_deplacee = grille_base.clone() + grille_deplacee[..., 0] -= (2.0 * shift_x) / (cols - 1) + grille_deplacee[..., 1] -= (2.0 * shift_y) / (rows - 1) + + # CRITICAL OPTIMIZATION: mode="nearest" keeps Wall IDs integer-pure (no interpolation fuzziness) + tempwallID = F.grid_sample( + wall_id_4d, grille_deplacee, mode="nearest", padding_mode="zeros" + ).squeeze() + + # Batch-map wall ID categories directly to structural heights via tensor indexing + temp_wallHeight = wall_height_lookup[tempwallID.long()] + + # Track wall degradation down ray segments + temp2 = temp_wallHeight - dz + + # Process logical flags globally on the GPU cores + valid_mask = temp2 > 0 + active_pixels_mask = valid_mask & temp3 + + # Amalgamate ray intersection metrics + buildIDSeen = torch.where(active_pixels_mask, tempwallID, buildIDSeen) + voxelHeight = torch.where( + active_pixels_mask, temp_wallHeight - temp2, voxelHeight + ) + + # Update remaining target pixel profiles using fast tensor boolean operations + temp3 = (temp2 <= 0) & (buildIDSeen == 0.0) + + # 3. POST-PROCESSING & VOXEL IDENTIFICATION + voxelHeight_ceil = torch.ceil(voxelHeight) + voxelId = torch.zeros((rows, cols), device=device) + + # Secure mapping arrays are local tensors + if not isinstance(wall2d_id, torch.Tensor): + wall2d_id = torch.tensor(wall2d_id, device=device) + else: + wall2d_id = wall2d_id.to(device) + + if not isinstance(voxel_height, torch.Tensor): + voxel_height = torch.tensor(voxel_height, device=device) + else: + voxel_height = voxel_height.to(device) + + if not isinstance(voxelId_list, torch.Tensor): + voxelId_list = torch.tensor( + voxelId_list, dtype=torch.long, device=device + ) + else: + voxelId_list = voxelId_list.to(device=device, dtype=torch.long) + + # Find unique Wall ID & Voxel Height pairings found across the canvas + stacked_profiles = torch.column_stack( + [buildIDSeen.flatten(), voxelHeight_ceil.flatten()] + ) + unique_combinations = torch.unique(stacked_profiles, dim=0) + unique_combinations = unique_combinations[ + ~torch.all(unique_combinations == 0, dim=1) + ] + + # Map the linear 3D voxel index references + for temp_id, temp_height in unique_combinations: + mask = (wall2d_id == temp_id) & (voxel_height == temp_height) + temp_fill_id = voxelId_list[mask] + + pixel_mask = (buildIDSeen == temp_id) & ( + voxelHeight_ceil == temp_height + ) + + if temp_fill_id.numel() > 0: + voxelId[pixel_mask] = temp_fill_id[0].float() + else: + buildIDSeen[pixel_mask] = 0.0 + voxelHeight_ceil[pixel_mask] = 0.0 + + # Invert original shadow notation layout logic (1 - sh) + # This aligns the mapping accurately to raw cast shadows + shadow_correction = 1.0 - sh + buildIDSeen *= shadow_correction + voxelHeight *= shadow_correction + voxelId *= shadow_correction + + return buildIDSeen, voxelHeight, voxelId diff --git a/util/ssParms.py b/util/ssParms.py index 5422932..92fa444 100644 --- a/util/ssParms.py +++ b/util/ssParms.py @@ -9,13 +9,11 @@ from ..functions import wallalgorithms as wa # from umep_suewsss_export_component import write_GridLayout_file, create_GridLayout_dict -from ..util.umep_suewsss_export_component import ( +from .umep_suewsss_export_component import ( write_GridLayout_file, create_GridLayout_dict, ) -# import matplotlib as plt - def ss_calc(build, cdsm, walls, numPixels, feedback): @@ -108,8 +106,9 @@ def writeGridLayout(ssVect, heightMethod, vertHeights, nlayer, skew): ssDict = create_GridLayout_dict() - # static levels (taken from interface). Last value > max height - if heightMethod == 1: + if ( + heightMethod == 1 + ): # static levels (taken from interface). Last value > max height ssDict["height"] = vertHeights.append(ssVect[:, 0].max()) ssDict["nlayer"] = len(ssDict["height"]) - 1 elif heightMethod == 2: # always nlayers layer based on percentiles @@ -141,34 +140,33 @@ def writeGridLayout(ssVect, heightMethod, vertHeights, nlayer, skew): ssDict["veg_scale"] = [] index = int(0) - # TODO this loop need to be confirmed by Reading - for i in range(1, len(ssDict["height"])): + for i in range( + 1, len(ssDict["height"]) + ): # TODO this loop need to be confirmed by Reading index += 1 startH = int(ssDict["height"][index - 1]) endH = int(ssDict["height"][index]) if index == 1: - # first is plan area index of buildings - ssDict["building_frac"].append(ssVect[0, 1]) - # first is plan area index of trees - ssDict["veg_frac"].append(ssVect[0, 3]) + ssDict["building_frac"].append( + ssVect[0, 1] + ) # first is plan area index of buildings + ssDict["veg_frac"].append( + ssVect[0, 3] + ) # first is plan area index of trees else: - # intergrated pai_build mean in ith vertical layer ssDict["building_frac"].append( np.round(np.mean(ssVect[startH:endH, 1]), 3) - ) - # intergrated pai_veg mean in ith vertical layer + ) # intergrated pai_build mean in ith vertical layer ssDict["veg_frac"].append( np.round(np.mean(ssVect[startH:endH, 3]), 3) - ) + ) # intergrated pai_veg mean in ith vertical layer - # intergrated bscale mean in ith vertical layer ssDict["building_scale"].append( np.round(np.mean(ssVect[startH:endH, 2]), 3) - ) - # intergrated vscale mean in ith vertical layer + ) # intergrated bscale mean in ith vertical layer ssDict["veg_scale"].append( np.round(np.mean(ssVect[startH:endH, 4]), 3) - ) + ) # intergrated vscale mean in ith vertical layer # TODO here we need to add other parameters based on typology diff --git a/util/umep_solweig_export_component.py b/util/umep_solweig_export_component.py index 21fd1e4..12a6581 100644 --- a/util/umep_solweig_export_component.py +++ b/util/umep_solweig_export_component.py @@ -105,9 +105,7 @@ def write_solweig_config(configDict, refdir): "# use anisotrphic sky (Wallenberg et al. XXXX and Wallenberg et al. XXXX)\n" ) f.write("aniso={}\n".format(configDict["aniso"])) - f.write( - "# use the surface temperature parameterization v2026a\n" - ) + f.write("# use the surface temperature parameterization v2026a\n") f.write("groundmodel={}\n".format(configDict["groundmodel"])) f.write( "# use the outgoing longwave radiation computation scheme v2026a\n" @@ -132,6 +130,8 @@ def write_solweig_config(configDict, refdir): f.write("outputkdiff={}\n".format(configDict["outputkdiff"])) f.write("outputtreeplanter={}\n".format(configDict["outputtreeplanter"])) f.write("wallnetcdf={}\n".format(configDict["wallnetcdf"])) + f.write("# calculation mode (cpu or gpu)\n") + f.write("calculation_mode={}\n".format(configDict["calculation_mode"])) f.write("#-------------------------------------------------------\n") f.write("# dates - used if an EPW-file is used\n") f.write("#-------------------------------------------------------\n") diff --git a/util/umep_suewsss_export_component.py b/util/umep_suewsss_export_component.py index cad50b5..343e487 100644 --- a/util/umep_suewsss_export_component.py +++ b/util/umep_suewsss_export_component.py @@ -18,47 +18,96 @@ def create_GridLayout_dict(): ssDict["nlayer"] = 3 # number of vertical layers # geom - # TODO height of top of each layer (start with 0 i.e. one more than - # nlayers) - ssDict["height"] = [0.0, 11.0, 15.0, 22.0] - # TODO fraction of building coverage of each layer; the first one is plan - # area index of buildings - ssDict["building_frac"] = [0.43, 0.38, 0.2] - # TODO fraction of vegetation coverage of each layer - ssDict["veg_frac"] = [0.01, 0.02, 0.01] - # TODO building scale of each layer [m] - ssDict["building_scale"] = [50.0, 50.0, 50] - # TODO vegetation scale of each layer [m] - ssDict["veg_scale"] = [10.0, 10.0, 10] + ssDict["height"] = [ + 0.0, + 11.0, + 15.0, + 22.0, + ] # TODO height of top of each layer (start with 0 i.e. one more than nlayers) + ssDict["building_frac"] = [ + 0.43, + 0.38, + 0.2, + ] # TODO fraction of building coverage of each layer; the first one is plan area index of buildings + ssDict["veg_frac"] = [ + 0.01, + 0.02, + 0.01, + ] # TODO fraction of vegetation coverage of each layer + ssDict["building_scale"] = [ + 50.0, + 50.0, + 50, + ] # TODO building scale of each layer [m] + ssDict["veg_scale"] = [ + 10.0, + 10.0, + 10, + ] # TODO vegetation scale of each layer [m] # roof - # TODO how? fraction of roofs of each layer (sum should be 1) - ssDict["sfr_roof"] = [0.3, 0.3, 0.4] - # TODO? how? initial temperatures of roofs [degC] - ssDict["tin_roof"] = [5, 5, 6] + ssDict["sfr_roof"] = [ + 0.3, + 0.3, + 0.4, + ] # TODO how? fraction of roofs of each layer (sum should be 1) + ssDict["tin_roof"] = [ + 5, + 5, + 6, + ] # TODO? how? initial temperatures of roofs [degC] ssDict["alb_roof"] = [0.5, 0.5, 0.2] # TODO albedo of roofs ssDict["emis_roof"] = [0.95, 0.95, 0.95] # TODO emissivity of roofs - # initial surface water depth state of roofs [mm] - ssDict["state_roof"] = [0.0, 0.0, 0.0] - # surface water depth state limit of roofs [mm] - ssDict["statelimit_roof"] = [5, 5, 5] - # surface water depth threshold of roofs (used in latent heat flux - # calculation) [mm] - ssDict["wetthresh_roof"] = [5, 5, 5] + ssDict["state_roof"] = [ + 0.0, + 0.0, + 0.0, + ] # initial surface water depth state of roofs [mm] + ssDict["statelimit_roof"] = [ + 5, + 5, + 5, + ] # surface water depth state limit of roofs [mm] + ssDict["wetthresh_roof"] = [ + 5, + 5, + 5, + ] # surface water depth threshold of roofs (used in latent heat flux calculation) [mm] ssDict["soilstore_roof"] = [20, 20, 20] # soil water store of roofs [mm] - # soil water store capacity of roofs [mm] - ssDict["soilstorecap_roof"] = [120, 120, 120] - # initial surface water depth state of roofs [mm] - ssDict["roof_albedo_dir_mult_fact"] = [1.0, 1.0, 1.0] + ssDict["soilstorecap_roof"] = [ + 120, + 120, + 120, + ] # soil water store capacity of roofs [mm] + ssDict["roof_albedo_dir_mult_fact"] = [ + 1.0, + 1.0, + 1.0, + ] # initial surface water depth state of roofs [mm] # The following parameters are used to calculate the heat flux from the roof # first roof facet - # TODO thickness of each layer (strictly five lyaers in total) [m] - ssDict["dz_roof1"] = [0.2, 0.1, 0.1, 0.01, 0.01] - # TODO thermal conductivity of each layer [W/m/K] - ssDict["k_roof1"] = [1.2, 1.2, 1.2, 1.2, 1.2] - # TODO specific heat capacity of each layer [J/kg/K] - ssDict["cp_roof1"] = [2e6, 2e6, 2e6, 2e6, 2e6] + ssDict["dz_roof1"] = [ + 0.2, + 0.1, + 0.1, + 0.01, + 0.01, + ] # TODO thickness of each layer (strictly five lyaers in total) [m] + ssDict["k_roof1"] = [ + 1.2, + 1.2, + 1.2, + 1.2, + 1.2, + ] # TODO thermal conductivity of each layer [W/m/K] + ssDict["cp_roof1"] = [ + 2e6, + 2e6, + 2e6, + 2e6, + 2e6, + ] # TODO specific heat capacity of each layer [J/kg/K] ssDict["dz_roof2"] = [0.2, 0.1, 0.1, 0.01, 0.01] # TODO ssDict["k_roof2"] = [2.2, 1.2, 1.2, 1.2, 1.2] # TODO diff --git a/util/umep_target_export_component.py b/util/umep_target_export_component.py index e607ca5..3a47095 100644 --- a/util/umep_target_export_component.py +++ b/util/umep_target_export_component.py @@ -1,9 +1,9 @@ import os """ -input: +input: TargetDict: dictonary with all relevant inputs -refdir = outputfolder +refdir = outputfolder """ diff --git a/util/umep_uwg_export_component.py b/util/umep_uwg_export_component.py index 18ded1c..3596459 100644 --- a/util/umep_uwg_export_component.py +++ b/util/umep_uwg_export_component.py @@ -5,10 +5,10 @@ This module is adjusted from dragonfly_uwg_export_component to transform UMEP pre-processing data to fit uwg -input: +input: uwg_object: dictonary with all relevant inputs refdir = outputfolder -fname = filename +fname = filename """ @@ -20,41 +20,46 @@ def create_uwgdict(): uwgDict["bldHeight"] = 10 # average building height (m) uwgDict["bldDensity"] = 0.5 # urban area building plan density (0-1) uwgDict["verToHor"] = 0.8 # urban area vertical to horizontal ratio - # fraction of building HVAC waste heat set to the street canyon [as - # opposed to the roof] - uwgDict["h_mix"] = 1 - # dimension of a square that encompasses the whole neighborhood [aka. - # characteristic length] (m) - uwgDict["charLength"] = 1000 + uwgDict["h_mix"] = ( + 1 # fraction of building HVAC waste heat set to the street canyon [as opposed to the roof] + ) + uwgDict["charLength"] = ( + 1000 # dimension of a square that encompasses the whole neighborhood [aka. characteristic length] (m) + ) uwgDict["albRoad"] = 0.1 # road albedo (0 - 1) uwgDict["dRoad"] = 0.5 # road pavement thickness (m) uwgDict["kRoad"] = 1 # road pavement conductivity (W/m K) uwgDict["cRoad"] = 1600000 # road volumetric heat capacity (J/m^3 K) - # non-building sensible heat at street level [aka. heat from cars, - # pedestrians, street cooking, etc. ] (W/m^2) - uwgDict["sensAnth"] = 20 + uwgDict["sensAnth"] = ( + 20 # non-building sensible heat at street level [aka. heat from cars, pedestrians, street cooking, etc. ] (W/m^2) + ) # Climate Zone (Eg. City) uwgDict["zone"] = "1A" # Vegetation parameters - # Fraction of the urban ground covered in grass/shrubs only (0-1) - uwgDict["grasscover"] = 0.1 - # Fraction of the urban ground covered in trees (0-1) - uwgDict["treeCover"] = 0.1 - # The month in which vegetation starts to evapotranspire (leaves are out) - uwgDict["vegStart"] = 4 - # The month in which vegetation stops evapotranspiring (leaves fall) - uwgDict["vegEnd"] = 10 + uwgDict["grasscover"] = ( + 0.1 # Fraction of the urban ground covered in grass/shrubs only (0-1) + ) + uwgDict["treeCover"] = ( + 0.1 # Fraction of the urban ground covered in trees (0-1) + ) + uwgDict["vegStart"] = ( + 4 # The month in which vegetation starts to evapotranspire (leaves are out) + ) + uwgDict["vegEnd"] = ( + 10 # The month in which vegetation stops evapotranspiring (leaves fall) + ) uwgDict["albVeg"] = 0.25 # Vegetation albedo - # Fraction of the heat absorbed by grass that is latent (goes to - # evaporating water) - uwgDict["latGrss"] = 0.4 - # Fraction of the heat absorbed by trees that is latent (goes to - # evaporating water) - uwgDict["latTree"] = 0.6 - # Fraction of the rural ground covered by vegetation - uwgDict["rurVegCover"] = 0.9 + uwgDict["latGrss"] = ( + 0.4 # Fraction of the heat absorbed by grass that is latent (goes to evaporating water) + ) + uwgDict["latTree"] = ( + 0.6 # Fraction of the heat absorbed by trees that is latent (goes to evaporating water) + ) + uwgDict["rurVegCover"] = ( + 0.9 # Fraction of the rural ground covered by vegetation + ) # Traffic schedule [1 to 24 hour],# Weekday# Saturday# Sunday uwgDict["SchTraffic"] = [ @@ -189,11 +194,11 @@ def create_uwgdict(): # ================================================= # OPTIONAL URBAN PARAMETERS # ================================================= - # If not provided, optional parameters are taken from corresponding DOE - # Reference building + # If not provided, optional parameters are taken from corresponding DOE Reference building uwgDict["albRoof"] = None # roof albedo (0 - 1) - # Fraction of the roofs covered in grass/shrubs (0 - 1) - uwgDict["vegRoof"] = None + uwgDict["vegRoof"] = ( + None # Fraction of the roofs covered in grass/shrubs (0 - 1) + ) uwgDict["glzR"] = None # Glazing Ratio (0 - 1) uwgDict["SHGC"] = None # Solar Heat Gain Coefficient (0 - 1) uwgDict["albWall"] = None # wall albedo (0 - 1) @@ -211,14 +216,18 @@ def create_uwgdict(): uwgDict["autosize"] = 0 # autosize HVAC (1 for yes; 0 for no) uwgDict["sensOcc"] = 100 # Sensible heat per occupant (W) - # Latent heat fraction from occupant (normally 0.3) - uwgDict["LatFOcc"] = 0.3 - # Radiant heat fraction from occupant (normally 0.2) - uwgDict["RadFOcc"] = 0.2 - # Radiant heat fraction from equipment (normally 0.5) - uwgDict["RadFEquip"] = 0.5 - # Radiant heat fraction from light (normally 0.7) - uwgDict["RadFLight"] = 0.7 + uwgDict["LatFOcc"] = ( + 0.3 # Latent heat fraction from occupant (normally 0.3) + ) + uwgDict["RadFOcc"] = ( + 0.2 # Radiant heat fraction from occupant (normally 0.2) + ) + uwgDict["RadFEquip"] = ( + 0.5 # Radiant heat fraction from equipment (normally 0.5) + ) + uwgDict["RadFLight"] = ( + 0.7 # Radiant heat fraction from light (normally 0.7) + ) # Urban climate parameters uwgDict["h_ubl1"] = 1000 # ubl height - day (m) @@ -226,10 +235,12 @@ def create_uwgdict(): uwgDict["h_ref"] = 150 # inversion height (m) uwgDict["h_temp"] = 2 # temperature height (m) uwgDict["h_wind"] = 10 # wind height (m) - # circulation coefficient (default = 1.2 per Bruno (2012)) - uwgDict["c_circ"] = 1.2 - # exchange coefficient (default = 1; ref Bruno (2014)) - uwgDict["c_exch"] = 1 + uwgDict["c_circ"] = ( + 1.2 # circulation coefficient (default = 1.2 per Bruno (2012)) + ) + uwgDict["c_exch"] = ( + 1 # exchange coefficient (default = 1; ref Bruno (2014)) + ) uwgDict["maxDay"] = 150 # max day threshold (W/m^2) uwgDict["maxNight"] = 20 # max night threshold (W/m^2) uwgDict["windMin"] = 1 # min wind speed (m/s) @@ -251,44 +262,51 @@ def get_uwg_file(uwg_object, refdir, fname): f.write("bldDensity,{},\n".format(uwg_object["bldDensity"])) f.write("verToHor,{},\n".format(uwg_object["verToHor"])) f.write("h_mix,{},\n".format(uwg_object["h_mix"])) - # dimension of a square that encompasses the whole neighborhood [aka. - # characteristic length] (m) - f.write("charLength,{},\n".format(uwg_object["charLength"])) - # road albedo (0 - 1) - f.write("albRoad,{},\n".format(uwg_object["albRoad"])) - # road pavement thickness (m) - f.write("dRoad,{},\n".format(uwg_object["dRoad"])) - # road pavement conductivity (W/m K) - f.write("kRoad,{},\n".format(uwg_object["kRoad"])) - # road volumetric heat capacity (J/m^3 K) - f.write("cRoad,{},\n".format(uwg_object["cRoad"])) - # non-building sensible heat at street level [aka. heat from cars, - # pedestrians, street cooking, etc. ] (W/m^2) - f.write("sensAnth,{},\n".format(uwg_object["sensAnth"])) - # f.write("latAnth,{},\n".format(uwg_object['latAnth'])) # - # non-building latent heat (W/m^2) (currently not used) + f.write( + "charLength,{},\n".format(uwg_object["charLength"]) + ) # dimension of a square that encompasses the whole neighborhood [aka. characteristic length] (m) + f.write( + "albRoad,{},\n".format(uwg_object["albRoad"]) + ) # road albedo (0 - 1) + f.write( + "dRoad,{},\n".format(uwg_object["dRoad"]) + ) # road pavement thickness (m) + f.write( + "kRoad,{},\n".format(uwg_object["kRoad"]) + ) # road pavement conductivity (W/m K) + f.write( + "cRoad,{},\n".format(uwg_object["cRoad"]) + ) # road volumetric heat capacity (J/m^3 K) + f.write( + "sensAnth,{},\n".format(uwg_object["sensAnth"]) + ) # non-building sensible heat at street level [aka. heat from cars, pedestrians, street cooking, etc. ] (W/m^2) + # f.write("latAnth,{},\n".format(uwg_object['latAnth'])) # non-building latent heat (W/m^2) (currently not used) f.write("\n") f.write("zone,{},\n".format(uwg_object["zone"])) f.write("\n") f.write("# Vegetation parameters\n") - # Fraction of the urban ground covered in grass/shrubs only (0-1) - f.write("grasscover,{},\n".format(uwg_object["grasscover"])) - # Fraction of the urban ground covered in trees (0-1) - f.write("treeCover,{},\n".format(uwg_object["treeCover"])) - # The month in which vegetation starts to evapotranspire (leaves are out) - f.write("vegStart,{},\n".format(uwg_object["vegStart"])) - # The month in which vegetation stops evapotranspiring (leaves fall) - f.write("vegEnd,{},\n".format(uwg_object["vegEnd"])) - # Vegetation albedo - f.write("albVeg,{},\n".format(uwg_object["albVeg"])) - # Fraction of the rural ground covered by vegetation - f.write("rurVegCover,{},\n".format(uwg_object["rurVegCover"])) - # Fraction of the heat absorbed by grass that is latent. Used in UWG only - # to calculate sensible heat fraction. - f.write("latGrss,{},\n".format(uwg_object["latGrss"])) - # Fraction of the heat absorbed by trees that is latent. Used in UWG only - # to calculate sensible heat fraction. - f.write("latTree,{},\n".format(uwg_object["latTree"])) + f.write( + "grasscover,{},\n".format(uwg_object["grasscover"]) + ) # Fraction of the urban ground covered in grass/shrubs only (0-1) + f.write( + "treeCover,{},\n".format(uwg_object["treeCover"]) + ) # Fraction of the urban ground covered in trees (0-1) + f.write( + "vegStart,{},\n".format(uwg_object["vegStart"]) + ) # The month in which vegetation starts to evapotranspire (leaves are out) + f.write( + "vegEnd,{},\n".format(uwg_object["vegEnd"]) + ) # The month in which vegetation stops evapotranspiring (leaves fall) + f.write("albVeg,{},\n".format(uwg_object["albVeg"])) # Vegetation albedo + f.write( + "rurVegCover,{},\n".format(uwg_object["rurVegCover"]) + ) # Fraction of the rural ground covered by vegetation + f.write( + "latGrss,{},\n".format(uwg_object["latGrss"]) + ) # Fraction of the heat absorbed by grass that is latent. Used in UWG only to calculate sensible heat fraction. + f.write( + "latTree,{},\n".format(uwg_object["latTree"]) + ) # Fraction of the heat absorbed by trees that is latent. Used in UWG only to calculate sensible heat fraction. f.write("\n") f.write("# Traffic schedule [1 to 24 hour],\n") f.write("SchTraffic,\n") @@ -315,93 +333,98 @@ def get_uwg_file(uwg_object, refdir, fname): ) f.write( "albRoof,{},\n".format( - # roof albedo (0 - 1) - uwg_object["albRoof"] - if uwg_object["albRoof"] - else "" + uwg_object["albRoof"] if uwg_object["albRoof"] else "" ) - ) - # Fraction of the roofs covered in grass/shrubs (0 - 1) + ) # roof albedo (0 - 1) f.write( "vegRoof,{},\n".format( uwg_object["vegRoof"] if uwg_object["vegRoof"] else "" ) - ) - # Glazing Ratio (0 - 1) + ) # Fraction of the roofs covered in grass/shrubs (0 - 1) f.write( "glzR,{},\n".format(uwg_object["glzR"] if uwg_object["glzR"] else "") - ) - # Solar Heat Gain Coefficient (0 - 1) + ) # Glazing Ratio (0 - 1) f.write( "SHGC,{},\n".format(uwg_object["SHGC"] if uwg_object["SHGC"] else "") - ) + ) # Solar Heat Gain Coefficient (0 - 1) f.write( "albWall,{},\n".format( - # wall albedo (0 - 1) - uwg_object["albWall"] - if uwg_object["albWall"] - else "" + uwg_object["albWall"] if uwg_object["albWall"] else "" ) - ) - # average building floor height + ) # wall albedo (0 - 1) f.write( "flr_h,{},\n".format( uwg_object["flr_h"] if uwg_object["flr_h"] else "" ) - ) + ) # average building floor height f.write("\n") f.write("# =================================================\n") f.write("# OPTIONAL PARAMETERS FOR SIMULATION CONTROL,\n") f.write("# =================================================\n") f.write("\n") f.write("# Simulation parameters,\n") - # starting month (1-12) - f.write("Month,{},\n".format(uwg_object["Month"])) - # starting day (1-31) - f.write("Day,{},\n".format(uwg_object["Day"])) - # number of days to run simultion - f.write("nDay,{},\n".format(uwg_object["nDay"])) - # simulation time step (s) - f.write("dtSim,{},\n".format(uwg_object["dtSim"])) - # weather time step (s) - f.write("dtWeather,{},\n".format(uwg_object["dtWeather"])), + f.write("Month,{},\n".format(uwg_object["Month"])) # starting month (1-12) + f.write("Day,{},\n".format(uwg_object["Day"])) # starting day (1-31) + f.write( + "nDay,{},\n".format(uwg_object["nDay"]) + ) # number of days to run simultion + f.write( + "dtSim,{},\n".format(uwg_object["dtSim"]) + ) # simulation time step (s) + f.write( + "dtWeather,{},\n".format(uwg_object["dtWeather"]) + ), # weather time step (s) f.write("\n") f.write("# HVAC system and internal loads\n") - # autosize HVAC (1 for yes; 0 for no) - f.write("autosize,{},\n".format(uwg_object["autosize"])) - # Sensible heat per occupant (W) - f.write("sensOcc,{},\n".format(uwg_object["sensOcc"])) - # Latent heat fraction from occupant (normally 0.3) - f.write("LatFOcc,{},\n".format(uwg_object["LatFOcc"])) - # Radiant heat fraction from occupant (normally 0.2) - f.write("RadFOcc,{},\n".format(uwg_object["RadFOcc"])) - # Radiant heat fraction from equipment (normally 0.5) - f.write("RadFEquip,{},\n".format(uwg_object["RadFEquip"])) - # Radiant heat fraction from light (normally 0.7) - f.write("RadFLight,{},\n".format(uwg_object["RadFLight"])) + f.write( + "autosize,{},\n".format(uwg_object["autosize"]) + ) # autosize HVAC (1 for yes; 0 for no) + f.write( + "sensOcc,{},\n".format(uwg_object["sensOcc"]) + ) # Sensible heat per occupant (W) + f.write( + "LatFOcc,{},\n".format(uwg_object["LatFOcc"]) + ) # Latent heat fraction from occupant (normally 0.3) + f.write( + "RadFOcc,{},\n".format(uwg_object["RadFOcc"]) + ) # Radiant heat fraction from occupant (normally 0.2) + f.write( + "RadFEquip,{},\n".format(uwg_object["RadFEquip"]) + ) # Radiant heat fraction from equipment (normally 0.5) + f.write( + "RadFLight,{},\n".format(uwg_object["RadFLight"]) + ) # Radiant heat fraction from light (normally 0.7) f.write("\n") f.write("#Urban climate parameters\n") - # ubl height - day (m) - f.write("h_ubl1,{},\n".format(uwg_object["h_ubl1"])) - # ubl height - night (m) - f.write("h_ubl2,{},\n".format(uwg_object["h_ubl2"])) - # inversion height (m) - f.write("h_ref,{},\n".format(uwg_object["h_ref"])) - # temperature height (m) - f.write("h_temp,{},\n".format(uwg_object["h_temp"])) + f.write( + "h_ubl1,{},\n".format(uwg_object["h_ubl1"]) + ) # ubl height - day (m) + f.write( + "h_ubl2,{},\n".format(uwg_object["h_ubl2"]) + ) # ubl height - night (m) + f.write("h_ref,{},\n".format(uwg_object["h_ref"])) # inversion height (m) + f.write( + "h_temp,{},\n".format(uwg_object["h_temp"]) + ) # temperature height (m) f.write("h_wind,{},\n".format(uwg_object["h_wind"])) # wind height (m) - # circulation coefficient (default = 1.2 per Bruno (2012)) - f.write("c_circ,{},\n".format(uwg_object["c_circ"])) - # exchange coefficient (default = 1; ref Bruno (2014)) - f.write("c_exch,{},\n".format(uwg_object["c_exch"])) - # max day threshold (W/m^2) - f.write("maxDay,{},\n".format(uwg_object["maxDay"])) - # max night threshold (W/m^2) - f.write("maxNight,{},\n".format(uwg_object["maxNight"])) - # min wind speed (m/s) - f.write("windMin,{},\n".format(uwg_object["windMin"])) - # rural average obstacle height (m) - f.write("h_obs,{},\n".format(uwg_object["h_obs"])) + f.write( + "c_circ,{},\n".format(uwg_object["c_circ"]) + ) # circulation coefficient (default = 1.2 per Bruno (2012)) + f.write( + "c_exch,{},\n".format(uwg_object["c_exch"]) + ) # exchange coefficient (default = 1; ref Bruno (2014)) + f.write( + "maxDay,{},\n".format(uwg_object["maxDay"]) + ) # max day threshold (W/m^2) + f.write( + "maxNight,{},\n".format(uwg_object["maxNight"]) + ) # max night threshold (W/m^2) + f.write( + "windMin,{},\n".format(uwg_object["windMin"]) + ) # min wind speed (m/s) + f.write( + "h_obs,{},\n".format(uwg_object["h_obs"]) + ) # rural average obstacle height (m) f.close()