diff --git a/.gitignore b/.gitignore
index f93affb683f92fce850d1bb29ec7cf273b915141..37f53ca0a07b155c41630ad29365790285e2be0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,13 +4,17 @@
 
 *.png
 
+# PyCharm project folder
 .idea/
 
 *.csv
 
-
+# STICS related files
 *.sti
 *param_files/*
 *default/*
 *logs/*
-*STICS/*
\ No newline at end of file
+*STICS/*
+
+# Profiling outputs
+*.prof
\ No newline at end of file
diff --git a/INPUTS/CIE_standard_skies.csv b/INPUTS/CIE_standard_skies.csv
new file mode 100644
index 0000000000000000000000000000000000000000..463d374b6483b463cf1d29d2263babd34d19bd03
--- /dev/null
+++ b/INPUTS/CIE_standard_skies.csv
@@ -0,0 +1,16 @@
+Type,Gradation,Indikatrix,a,b,c,d,e,Description of luminance distribution
+1,1,1,4.0 ,-0.70 ,0,-1.0 ,0.00 ,"CIE Standard Overcast Sky, alternative form. Steep luminance gradation towards zenith, azimuthal uniformity"
+2,1,2,4.0 ,-0.70 ,2,-1.5 ,0.15 ,"Overcast, with steep luminance gradation and slight brightening towards the sun"
+3,2,1,1.1 ,-0.8 ,0,-1.0 ,0.00 ,"Overcast, moderately graded with azimuthal uniformity"
+4,2,2,1.1 ,-0.8 ,2,-1.5 ,0.15 ,"Overcast, moderately graded and slight brightening towards the sun"
+5,3,1,0.0 ,-1.0 ,0,-1.0 ,0.00 ,"Sky of uniform luminance"
+6 ,3,2,0.0 ,-1.0 ,2,-1.5 ,0.15 ,"Partly cloudy sky, no gradation towards zenith, slight brightening towards the sun"
+7,3,3,0.0 ,-1.0 ,5,-2.5 ,0.30 ,"Partly cloudy sky, no gradation towards zenith, brighter circumsolar region"
+8,3,4,0.0 ,-1.0 ,10,-3.0 ,0.45 ,"Partly cloudy sky, no gradation towards zenith, distinct solar corona"
+9,4,2,-1.0 ,-0.55 ,2,-1.5 ,0.15 ,"Partly cloudy, with the obscured sun"
+10,4,3,-1.0 ,-0.55 ,5,-2.5 ,0.30 ,"Partly cloudy, with brighter circumsolar region"
+11,4,4,-1.0 ,-0.55 ,10,-3.0 ,0.45 ,"White-blue sky with distinct solar corona"
+12,5,4,-1.0 ,-0.32 ,10,-3.0 ,0.45 ,"CIE Standard Clear Sky, low illuminance turbidity"
+13 ,5,5,-1.0 ,-0.32 ,16,-3.0 ,0.30 ,"CIE Standard Clear Sky, polluted atmosphere"
+14 ,6,5,-1.0 ,-0.15 ,16,-3.0 ,0.30 ,"Cloudless turbid sky with broad solar corona"
+15,6,6,-1.0 ,-0.15 ,24,-2.8 ,0.15 ,"White-blue turbid sky with broad solar corona"
diff --git a/INPUTS/Igawa-5_sky_types_lut.csv b/INPUTS/Igawa-5_sky_types_lut.csv
new file mode 100644
index 0000000000000000000000000000000000000000..7a0f0c925a8d6fed9546f35e22c8dbc72e1afd71
--- /dev/null
+++ b/INPUTS/Igawa-5_sky_types_lut.csv
@@ -0,0 +1,599 @@
+;Kc;Cle;Sky category;CIE Sky Type
+0;0.01;0.0;O;1
+1;0.05;0.0;O;1
+2;0.1;0.0;O;1
+3;0.15;0.0;O;1
+4;0.2;0.0;O;1
+5;0.25;0.0;O;1
+6;0.3;0.0;O;1
+7;0.35;0.0;O;1
+8;0.4;0.0;O;1
+9;0.45;0.0;IO;4
+10;0.5;0.0;IO;4
+11;0.55;0.0;IO;4
+12;0.6;0.0;IO;4
+13;0.65;0.0;IO;4
+14;0.7;0.0;IO;4
+15;0.75;0.0;;5
+16;0.8;0.0;;5
+17;0.85;0.0;;5
+18;0.9;0.0;;5
+19;0.95;0.0;;5
+20;1.0;0.0;;5
+21;1.05;0.0;;5
+22;1.1;0.0;;5
+23;1.15;0.0;;5
+24;1.2;0.0;;5
+25;1.25;0.0;;5
+26;0.01;0.05;O;1
+27;0.05;0.05;O;1
+28;0.1;0.05;O;1
+29;0.15;0.05;O;1
+30;0.2;0.05;IO;4
+31;0.25;0.05;IO;4
+32;0.3;0.05;IO;4
+33;0.35;0.05;IO;4
+34;0.4;0.05;IO;4
+35;0.45;0.05;IO;4
+36;0.5;0.05;IO;4
+37;0.55;0.05;I;7
+38;0.6;0.05;I;7
+39;0.65;0.05;I;7
+40;0.7;0.05;I;7
+41;0.75;0.05;I;7
+42;0.8;0.05;I;7
+43;0.85;0.05;I;7
+44;0.9;0.05;;5
+45;0.95;0.05;;5
+46;1.0;0.05;;5
+47;1.05;0.05;;5
+48;1.1;0.05;;5
+49;1.15;0.05;;5
+50;1.2;0.05;;5
+51;1.25;0.05;;5
+52;0.01;0.1;O;1
+53;0.05;0.1;O;1
+54;0.1;0.1;IO;4
+55;0.15;0.1;IO;4
+56;0.2;0.1;IO;4
+57;0.25;0.1;IO;4
+58;0.3;0.1;IO;4
+59;0.35;0.1;IO;4
+60;0.4;0.1;IO;4
+61;0.45;0.1;I;7
+62;0.5;0.1;I;7
+63;0.55;0.1;I;7
+64;0.6;0.1;I;7
+65;0.65;0.1;I;7
+66;0.7;0.1;I;7
+67;0.75;0.1;I;7
+68;0.8;0.1;I;7
+69;0.85;0.1;I;7
+70;0.9;0.1;I;7
+71;0.95;0.1;;5
+72;1.0;0.1;;5
+73;1.05;0.1;;5
+74;1.1;0.1;;5
+75;1.15;0.1;;5
+76;1.2;0.1;;5
+77;1.25;0.1;;5
+78;0.01;0.15;O;1
+79;0.05;0.15;IO;4
+80;0.1;0.15;IO;4
+81;0.15;0.15;IO;4
+82;0.2;0.15;IO;4
+83;0.25;0.15;IO;4
+84;0.3;0.15;IO;4
+85;0.35;0.15;I;7
+86;0.4;0.15;I;7
+87;0.45;0.15;I;7
+88;0.5;0.15;I;7
+89;0.55;0.15;I;7
+90;0.6;0.15;I;7
+91;0.65;0.15;I;7
+92;0.7;0.15;I;7
+93;0.75;0.15;I;7
+94;0.8;0.15;I;7
+95;0.85;0.15;I;7
+96;0.9;0.15;I;7
+97;0.95;0.15;;5
+98;1.0;0.15;;5
+99;1.05;0.15;;5
+100;1.1;0.15;;5
+101;1.15;0.15;;5
+102;1.2;0.15;;5
+103;1.25;0.15;;5
+104;0.01;0.2;IO;4
+105;0.05;0.2;IO;4
+106;0.1;0.2;IO;4
+107;0.15;0.2;IO;4
+108;0.2;0.2;IO;4
+109;0.25;0.2;IO;4
+110;0.3;0.2;I;7
+111;0.35;0.2;I;7
+112;0.4;0.2;I;7
+113;0.45;0.2;I;7
+114;0.5;0.2;I;7
+115;0.55;0.2;I;7
+116;0.6;0.2;I;7
+117;0.65;0.2;I;7
+118;0.7;0.2;I;7
+119;0.75;0.2;I;7
+120;0.8;0.2;I;7
+121;0.85;0.2;I;7
+122;0.9;0.2;I;7
+123;0.95;0.2;I;7
+124;1.0;0.2;;5
+125;1.05;0.2;;5
+126;1.1;0.2;;5
+127;1.15;0.2;;5
+128;1.2;0.2;;5
+129;1.25;0.2;;5
+130;0.01;0.25;;5
+131;0.05;0.25;;5
+132;0.1;0.25;;5
+133;0.15;0.25;;5
+134;0.2;0.25;;5
+135;0.25;0.25;IO;4
+136;0.3;0.25;I;7
+137;0.35;0.25;I;7
+138;0.4;0.25;I;7
+139;0.45;0.25;I;7
+140;0.5;0.25;I;7
+141;0.55;0.25;I;7
+142;0.6;0.25;I;7
+143;0.65;0.25;I;7
+144;0.7;0.25;I;7
+145;0.75;0.25;I;7
+146;0.8;0.25;I;7
+147;0.85;0.25;I;7
+148;0.9;0.25;I;7
+149;0.95;0.25;I;7
+150;1.0;0.25;I;7
+151;1.05;0.25;I;7
+152;1.1;0.25;I;7
+153;1.15;0.25;;5
+154;1.2;0.25;;5
+155;1.25;0.25;;5
+156;0.01;0.3;;5
+157;0.05;0.3;;5
+158;0.1;0.3;;5
+159;0.15;0.3;;5
+160;0.2;0.3;;5
+161;0.25;0.3;I;7
+162;0.3;0.3;I;7
+163;0.35;0.3;I;7
+164;0.4;0.3;I;7
+165;0.45;0.3;I;7
+166;0.5;0.3;I;7
+167;0.55;0.3;I;7
+168;0.6;0.3;I;7
+169;0.65;0.3;I;7
+170;0.7;0.3;I;7
+171;0.75;0.3;I;7
+172;0.8;0.3;I;7
+173;0.85;0.3;I;7
+174;0.9;0.3;I;7
+175;0.95;0.3;I;7
+176;1.0;0.3;I;7
+177;1.05;0.3;I;7
+178;1.1;0.3;I;7
+179;1.15;0.3;I;7
+180;1.2;0.3;;5
+181;1.25;0.3;;5
+182;0.01;0.35;;5
+183;0.05;0.35;;5
+184;0.1;0.35;;5
+185;0.15;0.35;;5
+186;0.2;0.35;;5
+187;0.25;0.35;;5
+188;0.3;0.35;I;7
+189;0.35;0.35;I;7
+190;0.4;0.35;I;7
+191;0.45;0.35;I;7
+192;0.5;0.35;I;7
+193;0.55;0.35;I;7
+194;0.6;0.35;I;7
+195;0.65;0.35;I;7
+196;0.7;0.35;I;7
+197;0.75;0.35;I;7
+198;0.8;0.35;I;7
+199;0.85;0.35;I;7
+200;0.9;0.35;I;7
+201;0.95;0.35;I;7
+202;1.0;0.35;I;7
+203;1.05;0.35;I;7
+204;1.1;0.35;I;7
+205;1.15;0.35;I;7
+206;1.2;0.35;;5
+207;1.25;0.35;;5
+208;0.01;0.4;;5
+209;0.05;0.4;;5
+210;0.1;0.4;;5
+211;0.15;0.4;;5
+212;0.2;0.4;;5
+213;0.25;0.4;;5
+214;0.3;0.4;I;7
+215;0.35;0.4;I;7
+216;0.4;0.4;I;7
+217;0.45;0.4;I;7
+218;0.5;0.4;I;7
+219;0.55;0.4;I;7
+220;0.6;0.4;I;7
+221;0.65;0.4;I;7
+222;0.7;0.4;I;7
+223;0.75;0.4;I;7
+224;0.8;0.4;I;7
+225;0.85;0.4;I;7
+226;0.9;0.4;I;7
+227;0.95;0.4;I;7
+228;1.0;0.4;I;7
+229;1.05;0.4;I;7
+230;1.1;0.4;I;7
+231;1.15;0.4;I;7
+232;1.2;0.4;I;7
+233;1.25;0.4;;5
+234;0.01;0.45;;5
+235;0.05;0.45;;5
+236;0.1;0.45;;5
+237;0.15;0.45;;5
+238;0.2;0.45;;5
+239;0.25;0.45;;5
+240;0.3;0.45;;5
+241;0.35;0.45;I;7
+242;0.4;0.45;I;7
+243;0.45;0.45;I;7
+244;0.5;0.45;I;7
+245;0.55;0.45;I;7
+246;0.6;0.45;I;7
+247;0.65;0.45;I;7
+248;0.7;0.45;I;7
+249;0.75;0.45;I;7
+250;0.8;0.45;I;7
+251;0.85;0.45;I;7
+252;0.9;0.45;I;7
+253;0.95;0.45;I;7
+254;1.0;0.45;I;7
+255;1.05;0.45;I;7
+256;1.1;0.45;I;7
+257;1.15;0.45;I;7
+258;1.2;0.45;I;7
+259;1.25;0.45;I;7
+260;0.01;0.5;;5
+261;0.05;0.5;;5
+262;0.1;0.5;;5
+263;0.15;0.5;;5
+264;0.2;0.5;;5
+265;0.25;0.5;;5
+266;0.3;0.5;;5
+267;0.35;0.5;;5
+268;0.4;0.5;;5
+269;0.45;0.5;I;7
+270;0.5;0.5;I;7
+271;0.55;0.5;I;7
+272;0.6;0.5;I;7
+273;0.65;0.5;I;7
+274;0.7;0.5;I;7
+275;0.75;0.5;I;7
+276;0.8;0.5;I;7
+277;0.85;0.5;I;7
+278;0.9;0.5;I;7
+279;0.95;0.5;IC;11
+280;1.0;0.5;IC;11
+281;1.05;0.5;IC;11
+282;1.1;0.5;I;7
+283;1.15;0.5;I;7
+284;1.2;0.5;I;7
+285;1.25;0.5;I;7
+286;0.01;0.55;;5
+287;0.05;0.55;;5
+288;0.1;0.55;;5
+289;0.15;0.55;;5
+290;0.2;0.55;;5
+291;0.25;0.55;;5
+292;0.3;0.55;;5
+293;0.35;0.55;;5
+294;0.4;0.55;;5
+295;0.45;0.55;;5
+296;0.5;0.55;I;7
+297;0.55;0.55;I;7
+298;0.6;0.55;I;7
+299;0.65;0.55;I;7
+300;0.7;0.55;I;7
+301;0.75;0.55;I;7
+302;0.8;0.55;I;7
+303;0.85;0.55;IC;11
+304;0.9;0.55;IC;11
+305;0.95;0.55;IC;11
+306;1.0;0.55;IC;11
+307;1.05;0.55;IC;11
+308;1.1;0.55;IC;11
+309;1.15;0.55;IC;11
+310;1.2;0.55;I;7
+311;1.25;0.55;I;7
+312;0.01;0.6;;5
+313;0.05;0.6;;5
+314;0.1;0.6;;5
+315;0.15;0.6;;5
+316;0.2;0.6;;5
+317;0.25;0.6;;5
+318;0.3;0.6;;5
+319;0.35;0.6;;5
+320;0.4;0.6;;5
+321;0.45;0.6;;5
+322;0.5;0.6;I;7
+323;0.55;0.6;I;7
+324;0.6;0.6;I;7
+325;0.65;0.6;I;7
+326;0.7;0.6;I;7
+327;0.75;0.6;I;7
+328;0.8;0.6;I;7
+329;0.85;0.6;IC;11
+330;0.9;0.6;IC;11
+331;0.95;0.6;IC;11
+332;1.0;0.6;IC;11
+333;1.05;0.6;IC;11
+334;1.1;0.6;IC;11
+335;1.15;0.6;IC;11
+336;1.2;0.6;I;7
+337;1.25;0.6;I;7
+338;0.01;0.65;;5
+339;0.05;0.65;;5
+340;0.1;0.65;;5
+341;0.15;0.65;;5
+342;0.2;0.65;;5
+343;0.25;0.65;;5
+344;0.3;0.65;;5
+345;0.35;0.65;;5
+346;0.4;0.65;;5
+347;0.45;0.65;;5
+348;0.5;0.65;;5
+349;0.55;0.65;I;7
+350;0.6;0.65;I;7
+351;0.65;0.65;I;7
+352;0.7;0.65;I;7
+353;0.75;0.65;I;7
+354;0.8;0.65;IC;11
+355;0.85;0.65;IC;11
+356;0.9;0.65;IC;11
+357;0.95;0.65;IC;11
+358;1.0;0.65;IC;11
+359;1.05;0.65;IC;11
+360;1.1;0.65;IC;11
+361;1.15;0.65;IC;11
+362;1.2;0.65;IC;11
+363;1.25;0.65;I;7
+364;0.01;0.7;;5
+365;0.05;0.7;;5
+366;0.1;0.7;;5
+367;0.15;0.7;;5
+368;0.2;0.7;;5
+369;0.25;0.7;;5
+370;0.3;0.7;;5
+371;0.35;0.7;;5
+372;0.4;0.7;;5
+373;0.45;0.7;;5
+374;0.5;0.7;;5
+375;0.55;0.7;I;7
+376;0.6;0.7;I;7
+377;0.65;0.7;I;7
+378;0.7;0.7;I;7
+379;0.75;0.7;IC;11
+380;0.8;0.7;IC;11
+381;0.85;0.7;IC;11
+382;0.9;0.7;IC;11
+383;0.95;0.7;IC;11
+384;1.0;0.7;IC;11
+385;1.05;0.7;IC;11
+386;1.1;0.7;IC;11
+387;1.15;0.7;IC;11
+388;1.2;0.7;IC;11
+389;1.25;0.7;IC;11
+390;0.01;0.75;;5
+391;0.05;0.75;;5
+392;0.1;0.75;;5
+393;0.15;0.75;;5
+394;0.2;0.75;;5
+395;0.25;0.75;;5
+396;0.3;0.75;;5
+397;0.35;0.75;;5
+398;0.4;0.75;;5
+399;0.45;0.75;;5
+400;0.5;0.75;;5
+401;0.55;0.75;;5
+402;0.6;0.75;I;7
+403;0.65;0.75;I;7
+404;0.7;0.75;I;7
+405;0.75;0.75;IC;11
+406;0.8;0.75;IC;11
+407;0.85;0.75;IC;11
+408;0.9;0.75;IC;11
+409;0.95;0.75;C;13
+410;1.0;0.75;C;13
+411;1.05;0.75;C;13
+412;1.1;0.75;IC;11
+413;1.15;0.75;IC;11
+414;1.2;0.75;IC;11
+415;1.25;0.75;IC;11
+416;0.01;0.8;;5
+417;0.05;0.8;;5
+418;0.1;0.8;;5
+419;0.15;0.8;;5
+420;0.2;0.8;;5
+421;0.25;0.8;;5
+422;0.3;0.8;;5
+423;0.35;0.8;;5
+424;0.4;0.8;;5
+425;0.45;0.8;;5
+426;0.5;0.8;;5
+427;0.55;0.8;;5
+428;0.6;0.8;I;7
+429;0.65;0.8;I;7
+430;0.7;0.8;I;7
+431;0.75;0.8;IC;11
+432;0.8;0.8;IC;11
+433;0.85;0.8;IC;11
+434;0.9;0.8;C;13
+435;0.95;0.8;C;13
+436;1.0;0.8;C;13
+437;1.05;0.8;C;13
+438;1.1;0.8;C;13
+439;1.15;0.8;IC;11
+440;1.2;0.8;IC;11
+441;1.25;0.8;IC;11
+442;0.01;0.85;;5
+443;0.05;0.85;;5
+444;0.1;0.85;;5
+445;0.15;0.85;;5
+446;0.2;0.85;;5
+447;0.25;0.85;;5
+448;0.3;0.85;;5
+449;0.35;0.85;;5
+450;0.4;0.85;;5
+451;0.45;0.85;;5
+452;0.5;0.85;;5
+453;0.55;0.85;;5
+454;0.6;0.85;;5
+455;0.65;0.85;I;7
+456;0.7;0.85;I;7
+457;0.75;0.85;IC;11
+458;0.8;0.85;IC;11
+459;0.85;0.85;IC;11
+460;0.9;0.85;C;13
+461;0.95;0.85;C;13
+462;1.0;0.85;C;13
+463;1.05;0.85;C;13
+464;1.1;0.85;C;13
+465;1.15;0.85;IC;11
+466;1.2;0.85;IC;11
+467;1.25;0.85;IC;11
+468;0.01;0.9;;5
+469;0.05;0.9;;5
+470;0.1;0.9;;5
+471;0.15;0.9;;5
+472;0.2;0.9;;5
+473;0.25;0.9;;5
+474;0.3;0.9;;5
+475;0.35;0.9;;5
+476;0.4;0.9;;5
+477;0.45;0.9;;5
+478;0.5;0.9;;5
+479;0.55;0.9;;5
+480;0.6;0.9;;5
+481;0.65;0.9;;5
+482;0.7;0.9;I;7
+483;0.75;0.9;IC;11
+484;0.8;0.9;IC;11
+485;0.85;0.9;IC;11
+486;0.9;0.9;C;13
+487;0.95;0.9;C;13
+488;1.0;0.9;C;13
+489;1.05;0.9;C;13
+490;1.1;0.9;C;13
+491;1.15;0.9;IC;11
+492;1.2;0.9;IC;11
+493;1.25;0.9;IC;11
+494;0.01;0.95;;5
+495;0.05;0.95;;5
+496;0.1;0.95;;5
+497;0.15;0.95;;5
+498;0.2;0.95;;5
+499;0.25;0.95;;5
+500;0.3;0.95;;5
+501;0.35;0.95;;5
+502;0.4;0.95;;5
+503;0.45;0.95;;5
+504;0.5;0.95;;5
+505;0.55;0.95;;5
+506;0.6;0.95;;5
+507;0.65;0.95;;5
+508;0.7;0.95;I;7
+509;0.75;0.95;IC;11
+510;0.8;0.95;IC;11
+511;0.85;0.95;IC;11
+512;0.9;0.95;C;13
+513;0.95;0.95;C;13
+514;1.0;0.95;C;13
+515;1.05;0.95;C;13
+516;1.1;0.95;C;13
+517;1.15;0.95;IC;11
+518;1.2;0.95;IC;11
+519;1.25;0.95;;5
+520;0.01;1.0;;5
+521;0.05;1.0;;5
+522;0.1;1.0;;5
+523;0.15;1.0;;5
+524;0.2;1.0;;5
+525;0.25;1.0;;5
+526;0.3;1.0;;5
+527;0.35;1.0;;5
+528;0.4;1.0;;5
+529;0.45;1.0;;5
+530;0.5;1.0;;5
+531;0.55;1.0;;5
+532;0.6;1.0;;5
+533;0.65;1.0;;5
+534;0.7;1.0;;5
+535;0.75;1.0;IC;11
+536;0.8;1.0;IC;11
+537;0.85;1.0;IC;11
+538;0.9;1.0;C;13
+539;0.95;1.0;C;13
+540;1.0;1.0;C;13
+541;1.05;1.0;C;13
+542;1.1;1.0;C;13
+543;1.15;1.0;IC;11
+544;1.2;1.0;;5
+545;1.25;1.0;;5
+546;0.01;1.05;;5
+547;0.05;1.05;;5
+548;0.1;1.05;;5
+549;0.15;1.05;;5
+550;0.2;1.05;;5
+551;0.25;1.05;;5
+552;0.3;1.05;;5
+553;0.35;1.05;;5
+554;0.4;1.05;;5
+555;0.45;1.05;;5
+556;0.5;1.05;;5
+557;0.55;1.05;;5
+558;0.6;1.05;;5
+559;0.65;1.05;;5
+560;0.7;1.05;;5
+561;0.75;1.05;IC;11
+562;0.8;1.05;IC;11
+563;0.85;1.05;IC;11
+564;0.9;1.05;C;13
+565;0.95;1.05;C;13
+566;1.0;1.05;C;13
+567;1.05;1.05;C;13
+568;1.1;1.05;C;13
+569;1.15;1.05;IC;11
+570;1.2;1.05;;5
+571;1.25;1.05;;5
+572;0.01;1.1;;5
+573;0.05;1.1;;5
+574;0.1;1.1;;5
+575;0.15;1.1;;5
+576;0.2;1.1;;5
+577;0.25;1.1;;5
+578;0.3;1.1;;5
+579;0.35;1.1;;5
+580;0.4;1.1;;5
+581;0.45;1.1;;5
+582;0.5;1.1;;5
+583;0.55;1.1;;5
+584;0.6;1.1;;5
+585;0.65;1.1;;5
+586;0.7;1.1;;5
+587;0.75;1.1;;5
+588;0.8;1.1;IC;11
+589;0.85;1.1;IC;11
+590;0.9;1.1;C;13
+591;0.95;1.1;C;13
+592;1.0;1.1;C;13
+593;1.05;1.1;C;13
+594;1.1;1.1;C;13
+595;1.15;1.1;IC;11
+596;1.2;1.1;;5
+597;1.25;1.1;;5
diff --git a/INPUTS/SCENARIOS/Example1_loc.yaml b/INPUTS/SCENARIOS/Example1_loc.yaml
index 78d08b575d1005336638862985a2d5b4a325b26b..dee079f35d43a2de97c5f7a663fb4bf617849d03 100644
--- a/INPUTS/SCENARIOS/Example1_loc.yaml
+++ b/INPUTS/SCENARIOS/Example1_loc.yaml
@@ -71,6 +71,14 @@ FibonacciSamples:
    Type: integer
    Limit: [1,100000]
    Definition: Samples in the Fibonacci sky subdivision scheme.
+DiffuseSkyType:
+   Value: From weather data
+   Type: string
+   Possibilities: [From weather data, Uniform]
+   Definition: Determine how to compute sky diffuse radiance distribution. 
+               "From weather data" uses a simplified implementation of Igawa (2014) 
+               to derive the sky type from weather data.
+               "Uniform" assumes uniform radiance distribution (faster but less accurate).
 Xmin_InterestZone:
    Value: -2
    Type: float
diff --git a/MODULES/ENVIRONMENT/light.py b/MODULES/ENVIRONMENT/light.py
index c09c7a213c1799f72662ced061200ba47867f0d1..207fdde09ab1dd10acb1d8314c6fa838b116fa29 100644
--- a/MODULES/ENVIRONMENT/light.py
+++ b/MODULES/ENVIRONMENT/light.py
@@ -5,38 +5,23 @@
 #Authors : Roxane Bruhwyler (roxane.bruhwyler@uliege.be or roxane.bruhwyler@hotmail.com) and Nicolas De Cock (nicolas.decock1@gmail.com)
 #This file is part of the PASE software, and is distributed under the MIT license.
 
+from calendar import isleap
 import logging
 import numpy as np
 import pandas as pd
 import pyvista as pyV
 import pvlib.solarposition as pvlibSP
+from pvlib.atmosphere import get_relative_airmass
+from pvlib.irradiance import get_extra_radiation
 import matplotlib.pyplot as plt
 import os
 
 import MODULES.conversion_functions as cf
-from MODULES.ENVIRONMENT.sky_model import ReinhartSky
+from MODULES.ENVIRONMENT.sky_model import ReinhartSky, fibonacci_half_sphere
+from MODULES.ENVIRONMENT.sky_model import CIEStandardSky
 
 logger = logging.getLogger(__name__)
 
-def fibonacci_half_sphere(samples=18):
-    """
-    Function computing a number of direction sampling a virtual upper half sphere with a center at 0,0,0
-    
-    Parameters:
-        samples (int): Number of directions
-
-    Returns:
-       Directions (np.array): Matrix (samples x 3) providing the direction
-    """
-    phi = np.pi * (3. - np.sqrt(5.))
-    i = np.linspace(0,samples-1,num=samples)
-    yp = (1 - i/float(samples-1))
-    radius = np.sqrt(1-yp**2) 
-    theta = phi * i 
-    xp = np.cos(theta) * radius
-    zp = np.sin(theta) * radius
-    return np.column_stack([xp,zp,yp])
-
 
 class Sun_positions:
     
@@ -103,7 +88,12 @@ class Sun_positions:
         return solar_vector
         
     def get_top_of_atm_radiation(self, index, n):
-        
+        """
+
+        :param index: datetime index
+        :param n: seems unused ?
+        :return: irradiance at top of atmosphere on a horizontal surface [W/m²]
+        """
         day_of_year = np.array(index.dayofyear)
         n_days_in_year = day_of_year[len(index)-1]
         solar_declination = self.get_solar_declination(day_of_year)
@@ -137,21 +127,37 @@ class Light:
     def __init__(self, WD, SP, ghi_multiplier=1):
         
         self.data = {}
+        sky_type_lut_path = os.path.join('INPUTS', 'Igawa-5_sky_types_lut.csv')
+        self.sky_type_lut = pd.read_csv(sky_type_lut_path, sep=';')
        
         for year in WD.keys():
             
             GHI = WD[year]['G(h)'].to_numpy() * ghi_multiplier
             
-            if int(year)%4 == 0:                
+            if isleap(int(year)):
                 rad_top_atm = SP.sp_leapY['Top_atm_radiation'].to_numpy()
+                apparent_sun_zenith = SP.sp_leapY['apparent_zenith'].to_numpy()
+                sun_elevation = SP.sp_leapY['elevation'].to_numpy()
             else:
                 rad_top_atm = SP.sp_nonleapY['Top_atm_radiation'].to_numpy()
-                
+                apparent_sun_zenith = SP.sp_nonleapY['apparent_zenith'].to_numpy()
+                sun_elevation = SP.sp_nonleapY['elevation'].to_numpy()
+
             kt = self.get_clearness_sky_index(rad_top_atm, GHI)    
             DHI = self.get_diffuse_horizontal_radiation(kt, GHI)
             BHI = self.get_beam_horizontal_radiation(GHI, DHI)
             Ai = self.get_anisotropy_index(rad_top_atm, BHI)
             f = self.get_modulating_factor(GHI, BHI)
+
+            # Extraterrestrial Normal Irradiance (used for the Kc and Cle below)
+            ENI = get_extra_radiation(WD[year]['DateTime'].dt.dayofyear.values)
+
+            # Meteorological indices Clear Sky Index (Kc) and Cloudless index
+            # (Cle) from Igawa (2014)
+            Kc = self.get_clear_sky_index(ENI, GHI, apparent_sun_zenith)
+            Cle = self.get_cloudless_index(DHI, GHI, sun_elevation)
+
+            cie_sky_type = self.get_sky_type(Kc, Cle)
             
             df = pd.DataFrame({'GHI': GHI.tolist(),
                                'BHI': BHI.tolist(),
@@ -159,9 +165,12 @@ class Light:
                                'rad_top_atm': rad_top_atm.tolist(),
                                'kt': kt.tolist(),
                                'Ai': Ai.tolist(),
-                               'f': f.tolist()}, index=WD[year].index)
+                               'f': f.tolist(),
+                               'Kc': Kc.tolist(),
+                               'Cle': Cle.tolist(),
+                               'CIE Sky Type': cie_sky_type}, index=WD[year].index)
             
-            self.data[year] = df            
+            self.data[year] = df
                     
     def get_clearness_sky_index(self, rad_top_atm, GHI):
             
@@ -214,8 +223,63 @@ class Light:
         f[GHI>0] = np.sqrt(BHI[GHI>0]/GHI[GHI>0])
         
         return f
-    
-        
+
+    def get_clear_sky_index(self, ENI, GHI, apparent_sun_zenith):
+
+        # Get relative airmass from pvlib method
+        # (takes apparent zenith angle in [deg] for Kasten and Young 1989 model)
+        m = get_relative_airmass(apparent_sun_zenith, model='kastenyoung1989')
+
+        GHI_non_zero = GHI != 0
+
+        Kc = np.empty(len(GHI))
+        Kc[:] = np.nan
+
+        Kc = np.divide(GHI, 0.84 * ENI / m * np.exp(-0.054 * m), out=Kc, where=GHI_non_zero)
+
+        return Kc
+
+    def get_cloudless_index(self, DHI, GHI, sun_elevation):
+
+        sun_elevation_rad = np.deg2rad(sun_elevation)
+        GHI_non_zero = GHI != 0
+
+        # Cloud ratio [-]
+        Ce = self.get_cloud_ratio(DHI, GHI, GHI_non_zero)
+
+        # Standard cloud ratio [-]
+        Ces = (0.08302
+               + 0.5358 * np.exp(-17.3 * sun_elevation_rad)
+               + 0.3818 * np.exp(-3.2899 * sun_elevation_rad))
+
+        Cle = np.zeros(len(GHI))
+        Cle = np.divide((1 - Ce), (1 - Ces), out=Cle, where=GHI_non_zero)
+
+        return Cle
+
+    def get_cloud_ratio(self, DHI, GHI, GHI_non_zero):
+
+        Ce = np.zeros((len(GHI),))
+
+        Ce = np.divide(DHI, GHI, out=Ce, where=GHI_non_zero)
+
+        return Ce
+
+    def get_sky_type(self, Kc_target, Cle_target):
+
+        mydf = pd.DataFrame({'Kc': Kc_target, 'Cle': Cle_target})
+        temp_list = []
+        for k, v in mydf.iterrows():
+            if np.isnan(v['Kc']):
+                temp_list.append(np.nan)
+            else:
+                i = ((self.sky_type_lut['Kc']-v['Kc']) *
+                     (self.sky_type_lut['Cle']-v['Cle'])).abs().idxmin()
+                temp_list.append(int(self.sky_type_lut['CIE Sky Type'].iloc[i]))
+
+        return temp_list
+
+
 class Sun_positions_sampled:
     
     def __init__(self, lat, long, precision_lvl, loc_name, freq_deter, TZ):
@@ -371,17 +435,19 @@ class Ray_casting_scene:
     The class is initiate with a mesh containing the source points and a geometry
     '''
     #The class light shade scene init with a geometry (pyvista.polydata) and a mesh instance
-    def __init__(self,mesh,geometry):
+    def __init__(self, mesh, geometry, discrete_sky):
         self.mesh = mesh
         self.sourcepoints = self.mesh.get_sourcepoints()
+
         self.geometry = geometry
         self.n_sourcepoints = self.sourcepoints.shape[0]
         self.sources_flag_dict = mesh.get_sources_flag_dict()
-        
-        
-    def get_light_maps(self, sun_P, scheme='Reinhart', MF=1, n_small_suns=180):
+
+        self.discrete_sky = discrete_sky
+
+    def get_light_maps(self, sun_P):
     
-        if type(self.geometry) == list:
+        if type(self.geometry) == list:  # sun tracking
             sv = np.zeros((1,3))
             self.dir_map = np.zeros((len(self.sourcepoints),len(sun_P[:,0])))
             self.diff_map = np.zeros((len(self.sourcepoints),len(sun_P[:,0])))
@@ -389,19 +455,13 @@ class Ray_casting_scene:
             for time in range(len(sun_P[:,0])):
                 print(time)
                 geometry = self.geometry[time]
-                difff_map = self.diffuse_map(geometry,
-                                             scheme=scheme,
-                                             MF=MF,
-                                             n_small_suns=n_small_suns)
+                difff_map = self.diffuse_map(geometry)
                 sv[0,:] = sun_P[time,:]
                 dirrr_map = self.direct_map(sv, geometry)
                 self.dir_map[:,time] = dirrr_map 
                 self.diff_map[:,time] = difff_map
-        else:
-            self.diff_map = self.diffuse_map(self.geometry,
-                                             scheme=scheme,
-                                             MF=MF,
-                                             n_small_suns=n_small_suns)
+        else:  # no sun tracking
+            self.diff_map = self.diffuse_map(self.geometry)
             self.dir_map = self.direct_map(sun_P, self.geometry)
 
     def self_intercept(self,SourcePoints,intercept_points,id_rays_stopped,tol = 0.01):
@@ -424,45 +484,33 @@ class Ray_casting_scene:
         return np.unique(id_rays_stopped[delta>tol])
         
  
-    def diffuse_map(self, geometry, scheme='Reinhart', MF=1, n_small_suns=180):
+    def diffuse_map(self, geometry):
         """
-        Public method, compute the diffuse light at the point sources defined in the input mesh using
-         the approximation of a isotropic half sphere sky. 
-         The method uses the mesh and the geometry set at the initialization of the instance
-        
-        Parameters:
-            n_small_suns (int): number of sources consider in the sky for the diffuse light computation
-            higher number will provide a better accuracy but heavier computation
+        Public method, compute the diffuse light at the point sources defined
+        in the input mesh (see this class' constructor) under a
+        discrete_sky sky model.
+
+        :param geometry: geometry set at the initialization of the instance
+        :return: diffuse_mask: diffuse map binary mask for each source point and discrete_sky element. Shape: (n_sky_patches, n_source_points)
 
-        Returns:
-           Diffu (np.array 1 x n):  Providing a vector with the fraction ([0-1]) of diffuse light 
-                                   for each of the "n" source points defined in the mesh
         """
-      
-    
+
         #Get direction of ray to reach the small suns and compute the sky view of each point
-        if scheme.lower() == 'reinhart':
-            sky = ReinhartSky(MF=MF)
-            pTarget = np.column_stack([sky.reinhart_patches.x,
-                                       sky.reinhart_patches.y,
-                                       sky.reinhart_patches.z])
-            n_small_suns = len(sky.reinhart_patches)
-        elif scheme.lower() == 'fibonacci':
-            pTarget = fibonacci_half_sphere(n_small_suns)
-        else:
-            raise NotImplementedError('Unrecognized sky discretization scheme')
+        pTarget = np.column_stack([self.discrete_sky.x,
+                                   self.discrete_sky.y,
+                                   self.discrete_sky.z])
+        n_sky_elements = len(self.discrete_sky)
         
-        
-        #Creation of the source points array (Nx3) with N = len(Source) * len(n_small_suns)
+        #Creation of the source points array (Nx3) with N = len(Source) * len(n_sky_elements)
         SourcePoints = np.repeat(np.column_stack((
                                                   self.sourcepoints[:,0],
                                                   self.sourcepoints[:,1],
                                                   self.sourcepoints[:,2]
                                                  )),
-                                      n_small_suns,
+                                      n_sky_elements,
                                       axis=0)
         
-        #Creation of the target points array (Nx3) with N = len(Source) * len(n_small_suns)
+        #Creation of the target points array (Nx3) with N = len(Source) * len(n_sky_elements)
         TargetPoints = np.tile(pTarget,[self.n_sourcepoints,1])
         
         #Computation of the ray interception of the N rays
@@ -480,7 +528,7 @@ class Ray_casting_scene:
         SourceID = np.repeat(np.linspace(0,
                                          self.n_sourcepoints-1,
                                          self.n_sourcepoints),
-                             n_small_suns,
+                             n_sky_elements,
                              axis=0)
         
         #Touched provide a list with the sourceID of the intercept ray
@@ -491,24 +539,17 @@ class Ray_casting_scene:
 
         # Compute the cos(zenith angle) of all the small suns for the normalization
         _, _zenith_angle_all = cf.get_zenith_angle_from_cart(TargetPoints)
-        _cos_zenith_angle_all = np.cos(_zenith_angle_all).reshape(self.n_sourcepoints, n_small_suns)
+        _cos_zenith_angle_all = np.cos(_zenith_angle_all).reshape(self.n_sourcepoints, n_sky_elements)
 
         # Compute the cos(zenith angle) of the small suns that do NOT contribute to the diffuse map
         # (i.e. rays that were intercepted)
 
-        _mask = np.ones(_cos_zenith_angle_all.size, bool)
-        _mask[id_rays_stopped_filtred] = 0
-        _cos_zenith_angle_blocked = _cos_zenith_angle_all.copy()
-        _mask = _mask.reshape(self.n_sourcepoints, n_small_suns)
-        _cos_zenith_angle_blocked[_mask] = 0
-
-        #Creation of the empty matrix of sky view
-        Diffu = np.ones(self.n_sourcepoints, dtype=np.float16)
+        diffuse_mask = np.ones(_cos_zenith_angle_all.size, bool)
+        diffuse_mask[id_rays_stopped_filtred] = 0
 
-        #Computation of the sky view by removing the fraction of intercepted ray at each location
-        Diffu[unique.astype("int")] = 1 - np.sum(_cos_zenith_angle_blocked[unique.astype("int"), :], axis=1)/np.sum(_cos_zenith_angle_all[unique.astype("int"), :], axis=1)
+        diffuse_mask = diffuse_mask.reshape(self.n_sourcepoints, n_sky_elements)
 
-        return Diffu
+        return diffuse_mask
 
     def get_direct_map_by_flag(self,Flags):
         """
@@ -641,9 +682,9 @@ class Ray_casting_scene:
         dfShade = SP_sampled.tz_localize(None).reset_index()
         dfShade['doy'] = dfShade['index'].dt.dayofyear
         dfShade['RefDate'] = pd.to_datetime(dfShade['index'].dt.date)
- #       dfShade['hour'] = dfShade['index'].dt.hour
+        # dfShade['hour'] = dfShade['index'].dt.hour
         dfShade['second'] = pd.to_timedelta(dfShade['index'].dt.time.astype(str)).dt.total_seconds()
- #       dfShade['RefHour'] = dfShade['hour'] 
+        # dfShade['RefHour'] = dfShade['hour']
         dfShade['RefSecond'] = dfShade['second'] 
 
         for year in light_data: 
@@ -657,10 +698,8 @@ class Ray_casting_scene:
                 n = 6
                 
   
-            #dfWeather = light_data[year].tz_localize('Etc/GMT+0').reset_index()
             dfWeather = light_data[year].tz_localize(None).reset_index()
             dfWeather['doy'] = dfWeather['index'].dt.dayofyear
-     #       dfWeather['hour'] = dfWeather['index'].dt.hour
             dfWeather['second'] = pd.to_timedelta(dfWeather['index'].dt.time.astype(str)).dt.total_seconds()
 
             # Jointure entre les données météos et les données d'ombrage en vue de déterminée la date/index liée aux données d'ombrage
@@ -669,34 +708,58 @@ class Ray_casting_scene:
             irradianceMap_direct = {}
             irradianceMap_diffus = {}
             
-            # Boucle sur les n jours de l'annee afin de calculer l'irradiation journaliere
+            # Loop over days of year (doy) to compute daily irradiance.
             doy = dfWeatherMerged['index'].dt.dayofyear.unique()
-            doy.sort()
+            doy.sort()  # doy = [1, 2, 3, ..., 365]
             for day in doy:
                 
-                #Creation de sous dataframe comprenant les donnees du jour numero "doy"
+                #Creation de sous dataframe comprenant les donnees du jour numero "day"
                 sublight_df = dfWeatherMerged.loc[dfWeatherMerged['index'].dt.dayofyear==day,:]
                 df_subShade = dfShade.loc[sublight_df['RefDate'].unique()[0]==dfShade['RefDate']]
                 df_subShade_merged = df_subShade.join(sublight_df[['second','BHI','GHI','DHI']].set_index('second'),on='second',how='left',rsuffix='_',lsuffix="__")
                 
                 #df_subShade_merged = df_subShade.join(sublight_df[['hour','BHI','GHI','DHI']].set_index('hour'),on='hour',how='left',rsuffix='_',lsuffix="__")
                 #Jointure sur l'instant de la journee la plus proche sur base des secondes écoulées depuis le debut de la journee
-                df_subShade_merged = pd.merge_asof(df_subShade,sublight_df[['second','BHI','GHI','DHI','doy']].set_index('second'),on=['second'],direction='nearest',suffixes=('_x','_y')).sort_values('second')
+                df_subShade_merged = pd.merge_asof(df_subShade,sublight_df[['second','BHI','GHI','DHI','doy', 'CIE Sky Type']].set_index('second'),on=['second'],direction='nearest',suffixes=('_x','_y')).sort_values('second')
 
                 #Calcul de l'irradiation directe et diffuse dans ce jour et injection de la donnee dans le dictionnaire lie
                 irradianceMap_direct[day] = np.sum(self.dir_map[:,list(df_subShade.index)] * df_subShade_merged['BHI'].to_numpy(),axis=1)*10**-6*60*60/n
-                if type(self.geometry) != list:
-                    irradianceMap_diffus[day] = np.sum(df_subShade_merged['DHI'].to_numpy())*self.diff_map*10**-6*60*60/n
-                else:
-                    print('day')
+                if type(self.geometry) != list:  # no sun tracking
+                    irradianceMap_diffus[day] = self.compute_daily_diff_irradiation(df_subShade_merged.dropna())
+                else:  # sun tracking -> in this case, the mask changes at each time step
+                    print(f'{day=}')
                     irradianceMap_diffus[day] = np.sum(df_subShade_merged['DHI'].to_numpy()*self.diff_map[:,list(df_subShade.index)], axis=1)*10**-6*60*60/n
-            
+
             #Conversion des dictionnaires en matrice numpy et ajout dans l attribut ad-hoc
-            self.daily_irr_spat[year] = pd.DataFrame.from_dict(irradianceMap_diffus).to_numpy()    + pd.DataFrame.from_dict(irradianceMap_direct).to_numpy() 
-            self.daily_dir_irr_spat[year] = pd.DataFrame.from_dict(irradianceMap_direct).to_numpy()    
+            self.daily_irr_spat[year] = (pd.DataFrame.from_dict(irradianceMap_diffus).to_numpy()
+                                         + pd.DataFrame.from_dict(irradianceMap_direct).to_numpy())
+            self.daily_dir_irr_spat[year] = pd.DataFrame.from_dict(irradianceMap_direct).to_numpy()
             self.daily_diff_irr_spat[year] = pd.DataFrame.from_dict(irradianceMap_diffus).to_numpy()     
 
-    
+    def compute_daily_diff_irradiation(self, df):
+        # loop over each timestep in the day's dataframes
+        for i in range(len(df)):
+            unweighted_radiance = CIEStandardSky(self.discrete_sky,
+                                                 df['azimuth'].iloc[i],
+                                                 df['elevation'].iloc[i],
+                                                 df['CIE Sky Type'].iloc[i]).rel_radiance_distribution
+
+            # sky patch area and cos(z) weighting
+            weighted_radiance = unweighted_radiance * self.discrete_sky['Normalized surf area'] * self.discrete_sky['cos(z)']
+
+            # normalize to relative radiance contributions
+            rel_radiance_contrib = weighted_radiance/np.sum(weighted_radiance)
+
+            # multiply by the mask
+            if type(self.geometry) == list:  # sun tracking
+                raise NotImplementedError('Sun tracking is temporarily broken')
+                # shaded_radiance_contrib = rel_radiance_contrib * self.diff_map[:,time]
+            else:
+                shaded_radiance_contrib = rel_radiance_contrib.values * self.diff_map
+
+            diff_irradiance_map = (shaded_radiance_contrib * df['DHI'].iloc[i]).sum(axis=1)
+
+            return diff_irradiance_map
 
 def show_light_map(light_matrix, msh_grid, PV_central, lim_min, lim_max, lgd_title):
     
diff --git a/MODULES/ENVIRONMENT/sky_model.py b/MODULES/ENVIRONMENT/sky_model.py
index 04da300cc5f3e20fafcc8e316462dcc9d0ec535b..ac9d75f3ba1a0587ae958a601ae82dca12dc05eb 100644
--- a/MODULES/ENVIRONMENT/sky_model.py
+++ b/MODULES/ENVIRONMENT/sky_model.py
@@ -5,10 +5,13 @@ Paper openly available on Researchgate: https://www.google.com/url?sa=t&source=w
 
 """
 
+import cProfile
 import logging
 import math
 import numpy as np
+import os
 import pandas as pd
+from pathlib import Path
 from matplotlib import pyplot as plt
 import matplotlib
 matplotlib.use('TkAgg')
@@ -17,6 +20,27 @@ from MODULES.conversion_functions import sph_to_cart
 
 logger = logging.getLogger(__name__)
 
+
+def fibonacci_half_sphere(samples=18):
+    """
+    Function computing a number of direction sampling a virtual upper half sphere with a center at 0,0,0
+
+    Parameters:
+        samples (int): Number of directions
+
+    Returns:
+       Directions (np.array): Matrix (samples x 3) providing the direction
+    """
+    phi = np.pi * (3. - np.sqrt(5.))
+    i = np.linspace(0, samples - 1, num=samples)
+    yp = (1 - i / float(samples - 1))
+    radius = np.sqrt(1 - yp ** 2)
+    theta = phi * i
+    xp = np.cos(theta) * radius
+    zp = np.sin(theta) * radius
+    return np.column_stack([xp, zp, yp])
+
+
 class ReinhartSky:
 
     def __init__(self, MF=1):
@@ -38,6 +62,9 @@ class ReinhartSky:
         # Compute x, y, z coordinates of unit vectors pointing towards the sky patches
         self.get_patches_xyz()
 
+        # Compute cos(zenith angle), used in irradiance computations
+        self.compute_cos_zenith()
+
     def get_original_tregenza(self):
         original_tregenza_segments = pd.DataFrame()
 
@@ -112,12 +139,14 @@ class ReinhartSky:
         return reinhart_sky, reinhart_num_total
 
     def build_patches(self):
-        self.reinhart_patches = pd.DataFrame()
         self.patch_id = 0
         self.cols = ['row', 'patch_id', 'patch_id_in_row', 'd_el', 'd_az', 'el',
                      'az', 'solid_angle_sr']
+
+        # temporary list that will be appended to (dictionaries) in the loop,
+        # then converted to Dataframe after the loop
+        temp_list = []
         for row in self.reinhart_sky['row']:
-            # print(f'{row=}')
             d_az = 360 / \
                    self.reinhart_sky[self.reinhart_sky['row'] == row][
                        'num_segments'].values[
@@ -125,21 +154,22 @@ class ReinhartSky:
             el = self.alpha * (row + 1 / 2)  # [deg]
 
             for segment in range(self.reinhart_sky['num_segments'].iloc[row]):
-                # print(f'{segment=}')
                 az = d_az * (segment + 1 / 2)  # [deg]
                 solid_angle = self.compute_solid_angle_from_elev_azim(el,
-                                                                      d_az)
+                                                                      d_az)  # [sr]
 
-                df = pd.DataFrame(
-                    [[row, self.patch_id, segment, self.alpha, d_az, el, az,
-                      solid_angle]],
-                    columns=self.cols)
+                # Dictionary that will be appended to temp_list
+                dict = {'row': row, 'patch_id': self.patch_id, 'patch_id_in_row': segment,
+                        'd_el': self.alpha, 'd_az': d_az, 'el': el,
+                     'az': az, 'solid_angle_sr': solid_angle}
 
-                self.reinhart_patches = pd.concat([self.reinhart_patches, df],
-                                             ignore_index=True)
+                temp_list.append(dict)
 
                 self.patch_id += 1
 
+            # Convert the list of dictionaries to DataFrame
+            self.reinhart_patches = pd.DataFrame.from_records(temp_list)
+
     def add_top_patch(self):
         top_row = max(self.reinhart_sky['row']) + 1
         top_patch_solid_angle = self.compute_solid_angle_cone(self.alpha / 2)
@@ -191,9 +221,9 @@ class ReinhartSky:
 
     def compute_surface_areas(self):
         # Compute surf area of each patch
-        self.reinhart_patches['surf area'] = self.reinhart_patches['solid_angle_sr'] / (
+        self.reinhart_patches['Normalized surf area'] = self.reinhart_patches['solid_angle_sr'] / (
                 2 * np.pi)
-        sum_surf_areas = self.reinhart_patches['surf area'].sum()
+        sum_surf_areas = self.reinhart_patches['Normalized surf area'].sum()
 
         logger.debug(f'Sum of surface areas = {sum_surf_areas:.4g}')
 
@@ -202,9 +232,9 @@ class ReinhartSky:
         sum_solid_angles = self.reinhart_patches['solid_angle_sr'].sum()
         sum_solid_angles_div_pi = sum_solid_angles / np.pi
         logger.debug(f'Sum of solid angles = {sum_solid_angles_div_pi:.4g} * pi')
-        if not math.isclose(sum_solid_angles, 2*np.pi):
+        if not math.isclose(sum_solid_angles, 2*np.pi, rel_tol=5e-3):
             logger.warning('Sum of solid angles seems off, check the sky discretization scheme')
-            logger.warning(f'Sum of solid angles = {sum_solid_angles_div_pi:.4g} * pi')
+            logger.warning(f'Sum of solid angles = {sum_solid_angles_div_pi:.6g} * pi')
 
     def get_patches_xyz(self):
         # Compute xyz coords on a unit sphere (actually half sphere since it's the sky dome)
@@ -214,6 +244,9 @@ class ReinhartSky:
                                                  azimut=self.reinhart_patches['az'],
                                                  elev=self.reinhart_patches['el'])
 
+    def compute_cos_zenith(self):
+        self.reinhart_patches['cos(z)'] = np.cos(np.deg2rad(90-self.reinhart_patches['el']))
+
     def test_MFs(self):
         MFs = np.arange(1, 33)
         for MF in MFs:
@@ -262,9 +295,215 @@ class ReinhartSky:
         plt.show()
 
 
+class CIEStandardSky:
+    """
+    Generate a standard radiance distribution among the 15 CIE General Skies.
+    """
+
+    def __init__(self, discrete_sky,
+                 sun_az, sun_el,
+                 sky_type=5):
+
+        self.sky = discrete_sky
+
+        self.az = discrete_sky['az'].values
+        self.el = discrete_sky['el'].values
+
+        self.az_s = sun_az  # [°] Sun azimuth (145° is south-east)
+        self.el_s = sun_el  # [°]  Sun elevation
+
+        self.sky_type = int(sky_type)
+
+        self.rel_radiance_distribution = self.compute_rel_radiance()
+
+
+    def compute_sph_angular_dist(self, Z, Z_s, az, az_s):
+        """
+
+        :param Z: Sky patch zenith angle [deg]
+        :param Z: float
+        :param Z_s: Solar zenith angle [deg]
+        :param Z_s: float
+        :param az: Sky patch azimuth angle from north [deg]
+        :param az: float
+        :param az_s: Solar azimuth angle from north [deg]
+        :param az_s: float
+        :return: chi = Spherical angular distance [rad]
+        """
+
+        # Convert all angles from degrees to radians
+        Z = np.deg2rad(Z)
+        Z_s = np.deg2rad(Z_s)
+        az = np.deg2rad(az)
+        az_s = np.deg2rad(az_s)
+
+        # Absolute difference in azimuth angles
+        Az = np.abs(az - az_s)
+
+        # Angular distance chi
+        chi = np.arccos(
+            np.cos(Z_s) * np.cos(Z) + np.sin(Z_s) * np.sin(Z) * np.cos(Az))
+
+        return chi
+
+    def gradation_fun(self, a, b, Z):
+        """
+        Gradation function. It is not used as is but vectorized in compute_gradation().
+
+        :param a: Horizon-zenith gradient (gradation parameter) [-]
+        :type a: float
+        :param b: Gradient intensity (gradation parameter) [-]
+        :type b: float
+        :param Z: Zenith angle [deg]
+        :type Z: float
+        :return: gradation function value [-]
+        """
+        if math.isclose(Z, np.pi / 2):
+            return 1
+        else:
+            return 1 + a * np.exp(b / np.cos(Z))
+
+    def compute_gradation(self, a, b, Z):
+        """
+        Vectorizes and computes the gradation function
+
+        :param a: Horizon-zenith gradient (gradation parameter) [-]
+        :type a: float
+        :param b: Gradient intensity (gradation parameter) [-]
+        :type b: float
+        :param Z: Zenith angle [deg]
+        :type Z: Nx1 array of float
+        :return: gradation function [-], same shape as Z
+        """
+
+        v_gradation = np.vectorize(self.gradation_fun)
+
+        Z = np.deg2rad(Z)
+
+        return v_gradation(a, b, Z)
+
+    def scattering_indicatrix_fun(self, c, d, e, x):
+        """
+        Compute the scattering indicatrix value.
+
+        :param c: Circumsolar intensity (scattering parameter) [-]
+        :type c: float
+        :param d: Circumsolar radius (scattering parameter) [-]
+        :type d: float
+        :param e: Backscattering effect (scattering parameter) [-]
+        :type e: float
+        :param x: angle (spherical angular distance or zenith, i.e. 0) [rad]
+        :type x: array of float or float
+        :return: Scattering indicatrix value [-], same shape as x
+        """
+        return 1 + c * (np.exp(d * x) - np.exp(d * np.pi / 2)) + e * (
+            np.cos(x)) ** 2
+
+    def get_sky_params(self, standard_sky):
+        """
+
+        :param standard_sky: Dataframe of length 1 with standard sky information
+        :return a: Horizon-zenith gradient (gradation parameter) [-]
+        :return b: Gradient intensity (gradation parameter) [-]
+        :return c: Circumsolar intensity (scattering parameter) [-]
+        :return d: Circumsolar radius (scattering parameter) [-]
+        :return e: Backscattering effect (scattering parameter) [-]
+        """
+
+        a = standard_sky['a'].values[0]
+        b = standard_sky['b'].values[0]
+        c = standard_sky['c'].values[0]
+        d = standard_sky['d'].values[0]
+        e = standard_sky['e'].values[0]
+
+        return a, b, c, d, e
+
+    def relative_radiance_fun(self, sky_type=None, az=None, el=None):
+        """
+
+        :return: relative quantity (radiance or luminance) with respect to
+                 value at the zenith
+        """
+
+        # Read the csv containing the descriptions of the 15 standard skies
+
+        pase_dir_path = Path(__file__).parents[2]
+        standard_skies = pd.read_csv(os.path.join(pase_dir_path,
+                                                  'INPUTS',
+                                                  'CIE_standard_skies.csv'))
+
+        # Extract the parameters of the desired sky type
+        if sky_type is None:
+            standard_sky = standard_skies.loc[standard_skies['Type'] == self.sky_type]
+        else:
+            standard_sky = standard_skies.loc[standard_skies['Type'] == sky_type]
+
+        # Sky model parameters
+        a, b, c, d, e = self.get_sky_params(standard_sky)
+
+        # Elevation to zenith (Sun)
+        Z_s = 90 - self.el_s  # [deg]
+
+        # Elevation to zenith (sky patch)
+        if el is None:
+            Z = 90 - self.el  # [deg]
+        else:
+            Z = 90 - el  # [deg]
+
+
+        # Spherical angular dist
+        if az is None:
+            chi = self.compute_sph_angular_dist(Z, Z_s, self.az, self.az_s)
+        else:
+            chi = self.compute_sph_angular_dist(Z, Z_s, az, self.az_s)
+
+        # Gradation
+        phi = self.compute_gradation(a, b, Z)
+        phi_zenith = self.compute_gradation(a, b, Z=0)  # TODO do not recompute each time !
+
+        # Scattering indicatrix
+        indicatrix = self.scattering_indicatrix_fun(c, d, e, chi)
+        indicatrix_zenith = self.scattering_indicatrix_fun(c, d, e, np.deg2rad(Z_s))  # TODO do not recompute each time !
+
+        # Relative luminance/radiance (called "rel_quantity" for genericity)
+        rel_quantity = phi * indicatrix / (phi_zenith * indicatrix_zenith)
+
+        return rel_quantity
+
+    def compute_rel_radiance(self, sky_type=None, az=None, el=None):
+        v_radiance_fun = np.vectorize(self.relative_radiance_fun)
+        return v_radiance_fun(sky_type, az, el)
+
+
 if __name__ == '__main__':
-    sky = ReinhartSky(MF=1)
-    sky.scatter_2D()
-    sky.scatter_2D(text_id=True)
-    sky.scatter_3D()
-    sky.scatter_3D(text_id=True)
+
+    profile_flag = False
+
+    if profile_flag:
+        MFs = [1, 2]
+        for MF in MFs:
+            profile_output = os.path.join('..', '..', 'OUTPUTS', 'profiling', f'cprofile_output_sky_model_MF{MF}.prof')
+            cProfile.run(f"ReinhartSky(MF={MF})", profile_output, sort='tottime')
+    else:
+        MF = 1
+
+        sky = ReinhartSky(MF=MF)
+        if MF == 1:
+            text_id = True
+        else:
+            text_id = False
+
+        if MF < 8:
+            sky.scatter_2D(text_id=text_id)
+            sky.scatter_3D(text_id=text_id)
+
+    sky.reinhart_patches['Relative radiance distribution'] = CIEStandardSky(sky.reinhart_patches,
+                               sun_az=147.67, sun_el=38.02, sky_type=15).rel_radiance_distribution
+
+    DHI = 100  # [W/m²] arbitrary value for example's sake
+    integral_rel_sky_radiance = (sky.reinhart_patches['Relative radiance distribution']
+                                 * sky.reinhart_patches['solid_angle_sr']).sum()
+    zenith_radiance = DHI/integral_rel_sky_radiance
+    sky.reinhart_patches['Absolute radiance distribution'] = (sky.reinhart_patches['Relative radiance distribution']
+                                                              * zenith_radiance)
+
diff --git a/TEMPORARY_FILES/parse_cprofile_data.py b/TEMPORARY_FILES/parse_cprofile_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ccc77a09d3b377aa4a5421d362b860edabd66a8
--- /dev/null
+++ b/TEMPORARY_FILES/parse_cprofile_data.py
@@ -0,0 +1,18 @@
+import os
+import pstats
+from pstats import SortKey
+
+from MODULES.ENVIRONMENT.sky_model import ReinhartSky
+
+# Profiles of sky_model Reinhart module
+MF = 8
+MFs = [16, 32]
+for MF in MFs:
+    profile_output = os.path.join('..', 'OUTPUTS', 'profiling', f'cprofile_output_sky_model_MF{MF}.prof')
+    p = pstats.Stats(profile_output)
+    p.strip_dirs().sort_stats('tottime').print_stats(10)
+
+# Profile of example.py
+profile_output = os.path.join('..', 'OUTPUTS', 'profiling', f'example.prof')
+p = pstats.Stats(profile_output)
+p.strip_dirs().sort_stats('tottime').print_stats(100)
diff --git a/TEMPORARY_FILES/perez_standard_skies.py b/TEMPORARY_FILES/perez_standard_skies.py
new file mode 100644
index 0000000000000000000000000000000000000000..7937019b714ffdda15c0d696f37f98487da98085
--- /dev/null
+++ b/TEMPORARY_FILES/perez_standard_skies.py
@@ -0,0 +1,348 @@
+import logging
+import math
+import numpy as np
+import os
+import pandas as pd
+import pyvista as pv
+import matplotlib
+matplotlib.use('TkAgg')
+from matplotlib import pyplot as plt
+
+from MODULES.conversion_functions import sph_to_cart
+from MODULES.ENVIRONMENT.sky_model import ReinhartSky
+
+logger = logging.getLogger(__name__)
+logger.setLevel('DEBUG')
+
+def compute_sph_angular_dist(Z, Z_s, az, az_s):
+    """
+
+    Args:
+        Z: Sky patch zenith angle [deg]
+        Z_s: Solar zenith angle [deg]
+        az: Sky patch azimuth angle from north [deg]
+        az_s: Solar azimuth angle from north [deg]
+
+    Returns:
+        chi: Spherical angular distance [rad]
+    """
+
+    # Convert all angles from degrees to radians
+    Z = np.deg2rad(Z)
+    Z_s = np.deg2rad(Z_s)
+    az = np.deg2rad(az)
+    az_s = np.deg2rad(az_s)
+
+    # Absolute difference in azimuth angles
+    Az = np.abs(az-az_s)
+
+    # Angular distance chi
+    chi = np.arccos(np.cos(Z_s) * np.cos(Z) + np.sin(Z_s)*np.sin(Z)*np.cos(Az))
+
+    return chi
+
+def gradation_fun(a, b, Z):
+    """
+    Gradation function defined to be vectorized in compute_gradation().
+    Args:
+        a: Horizon-zenith gradient (gradation parameter) [-]
+        b: Gradient intensity (gradation parameter) [-]
+        Z: zenith angle [rad]
+
+    Returns:
+        Gradation value.
+    """
+    if math.isclose(Z, np.pi/2):
+        return 1
+    else:
+        return 1 + a*np.exp(b/np.cos(Z))
+
+def compute_gradation(a, b, Z):
+    """
+    Computes the gradation function based on vectorized gradation_fun function.
+    Args:
+        a: Horizon-zenith gradient (gradation parameter) [-]
+        b: Gradient intensity (gradation parameter) [-]
+        Z: Zenith angle [deg]. Type : Nx1 array
+
+    Returns:
+        gradation function result
+    """
+    v_gradation = np.vectorize(gradation_fun)
+
+    Z = np.deg2rad(Z)
+
+    return v_gradation(a, b, Z)
+
+def scattering_indicatrix_fun(c, d, e, x):
+    """
+
+    Args:
+        c: Circumsolar intensity (scattering parameter) [-]
+        d: Circumsolar radius (scattering parameter) [-]
+        e: Backscattering effect (scattering parameter) [-]
+        x: angle (sphericla angular distance or zenith, i.e. 0) [rad]
+    Returns:
+        Scattering indicatrix value
+    """
+    return 1 + c * (np.exp(d*x) - np.exp(d*np.pi/2)) + e * (np.cos(x))**2
+
+def get_sky_params(standard_sky):
+    """
+
+    Args:
+        standard_sky: Dataframe of length 1 with standard sky information
+
+    Returns:
+        a: Horizon-zenith gradient (gradation parameter) [-]
+        b: Gradient intensity (gradation parameter) [-]
+        c: Circumsolar intensity (scattering parameter) [-]
+        d: Circumsolar radius (scattering parameter) [-]
+        e: Backscattering effect (scattering parameter) [-]
+    """
+
+    a = standard_sky['a'].values[0]
+    b = standard_sky['b'].values[0]
+    c = standard_sky['c'].values[0]
+    d = standard_sky['d'].values[0]
+    e = standard_sky['e'].values[0]
+
+    return a, b, c, d, e
+
+def luminance_fun(sky_type, patch_az, patch_el, sun_az, sun_el):
+    # Standard sky parameters and description
+    standard_sky = standard_skies.loc[standard_skies['Type'] == sky_type]
+
+    # Sky model parameters
+    a, b, c, d, e = get_sky_params(standard_sky)
+
+    # Elevation to zenith (sky patch)
+    Z = 90 - patch_el  # [deg]
+
+    # Elevation to zenith (Sun)
+    Z_s = 90 - sun_el  # [deg]
+
+    # Spherical angular dist
+    chi = compute_sph_angular_dist(Z, Z_s, patch_az, sun_az)
+
+    # Gradation
+    phi = compute_gradation(a, b, Z)
+    phi_zenith = compute_gradation(a, b, Z=0)
+
+    # Indicatrix
+    indicatrix = scattering_indicatrix_fun(c, d, e, chi)
+    indicatrix_zenith = scattering_indicatrix_fun(c, d, e, np.deg2rad(Z_s))
+
+    # Relative luminance
+    rel_lum = phi * indicatrix / (phi_zenith * indicatrix_zenith)
+
+    return rel_lum
+
+def compute_luminance(sky_type, patch_az, patch_el, sun_az, sun_el):
+    v_luminance_fun = np.vectorize(luminance_fun)
+    return v_luminance_fun(sky_type, patch_az, patch_el, sun_az, sun_el)
+
+def print_n_max_values(vect, n):
+    a = vect.copy()
+    b = np.unique(a)
+    c = np.sort(b)
+    c = c[::-1]
+    print(c[:n])
+
+def plot_luminance_map(az, el, lum, az_s=None, el_s=None, luminance_or_radiance='radiance', polar=True, colormap=None):
+    lum_min = np.min(lum)
+    lum_max = np.max(lum)
+
+    fig = plt.figure()
+    if polar:
+        ax = fig.add_subplot(111, polar=True)
+        c = ax.pcolormesh(np.deg2rad(az), 90-el, lum, vmin=lum_min, vmax=lum_max, cmap=colormap)
+    else:
+        ax = fig.gca()
+
+        c = ax.pcolormesh(az, el, lum, vmin=lum_min, vmax=lum_max, cmap=colormap)
+
+        ax.set_xlabel('Azimuth [deg]')
+        ax.set_ylabel('Zenith angle [deg]')
+
+    cbar = fig.colorbar(c, ax=ax)
+    if luminance_or_radiance == 'luminance':
+        cbar_label = 'Luminance [cd/m²]'
+    elif luminance_or_radiance == 'radiance':
+        cbar_label = 'Radiance [W/(m²⋅sr)]'
+    cbar.set_label(cbar_label)
+
+    if (az_s is not None) and (el_s is not None):
+        if polar:
+            ax.plot(np.deg2rad(az_s), 90-el_s, 'ro', label='Sun position')
+        else:
+            ax.plot(az_s, el_s, 'ro', label='Sun position')
+        ax.legend()
+
+    if polar:
+        # Set N at the top, E to the right
+        ax.set_theta_zero_location('N')
+        ax.set_theta_direction(-1)
+
+    plt.show()
+
+def plot_all_skies(polar=True, luminance_or_radiance='luminance', colormap='jet'):
+    # sky = ReinhartSky().reinhart_patches
+    # az = sky['az']
+    # el = sky['el']
+    az = np.arange(0, 360, step=5)  # [°]
+    el = np.arange(0, 90, step=5)  # [°]
+
+    az_meshgrid, el_meshgrid = np.meshgrid(az, el)
+
+    el_s = 38.02  # [°]
+    az_s = 147.67  # [°] 145° is south-east
+    zenith_luminance = 4404  # cd/m²
+
+    fig, axs = plt.subplots(3, 5,
+                            figsize=(14, 8),
+                            subplot_kw=dict(polar=polar))
+
+    sky_types = np.arange(1, 16)
+    for sky_type in sky_types:
+        lum = compute_luminance(sky_type, az_meshgrid.flatten(), el_meshgrid.flatten(), az_s, el_s)
+
+        lum_min = np.min(lum)
+        lum_max = np.max(lum)
+
+        ax_row = int(np.floor((sky_type-1)/5))
+        ax_col = (sky_type-1) % 5
+        print(f'{sky_type=}', f'{ax_row=}', f'{ax_col=}')
+
+        ax = axs[ax_row, ax_col]
+        if polar:
+            c = ax.pcolormesh(np.deg2rad(az), 90-el, lum.reshape(az_meshgrid.shape), vmin=lum_min, vmax=lum_max, cmap=colormap)
+        else:
+            c = ax.pcolormesh(az, el, lum.reshape(az_meshgrid.shape), vmin=lum_min, vmax=lum_max, cmap=colormap)
+
+            ax.set_xlabel('Azimuth [deg]')
+            ax.set_ylabel('Zenith [deg]')
+
+        ax.title.set_text(f'Sky type {sky_type}')
+
+        # Set N at the top, E to the right
+        ax.set_theta_zero_location('N')
+        ax.set_theta_direction(-1)
+
+        fig.colorbar(c, ax=ax)
+
+        if (az_s is not None) and (el_s is not None):
+            if polar:
+                ax.plot(np.deg2rad(az_s), 90-el_s, 'ro', markeredgecolor='yellow', label='Sun position')
+            else:
+                ax.plot(az_s, 90-el_s, 'ro', markeredgecolor='yellow', label='Sun position')
+
+    if luminance_or_radiance == 'luminance':
+        fig_title = 'Luminance [cd/m²]'
+    elif luminance_or_radiance == 'radiance':
+        fig_title = 'Radiance [W/(m²⋅sr)]'
+    fig.suptitle(fig_title + ' (azimuth-zenith)')
+
+    plt.tight_layout()
+    plt.show()
+
+def plot_3D(sky, rel_lum):
+
+    sky['patch_bound_el_low'] = sky['el'] - sky['d_el'] / 2
+    sky['patch_bound_el_up'] = sky['el'] + sky['d_el'] / 2
+    sky['patch_bound_az_low'] = sky['az'] - sky['d_az'] / 2
+    sky['patch_bound_az_up'] = sky['az'] + sky['d_az'] / 2
+    print(sky['patch_bound_el_up'].iloc[-1])
+    # Fix elevation for top patch
+    sky['patch_bound_el_up'].iloc[-1] = 90
+    print(sky['patch_bound_el_up'].iloc[len(sky) - 1])
+
+    # Vertical levels
+    # in this case a single level slightly above the surface of a sphere
+    RADIUS = 1
+    levels = [RADIUS * 1.01]
+
+    xx_bounds = np.concatenate([sky['patch_bound_az_low'].values,
+                                [sky['patch_bound_az_up'].values[
+                                     -1]]])  # az
+    xx_bounds[-2:-1] += 180
+    yy_bounds = np.concatenate([sky['patch_bound_el_low'].values,
+                                [sky['patch_bound_el_up'].values[
+                                     -1]]])  # el
+
+    grid_scalar = pv.grid_from_sph_coords(xx_bounds, 90 - yy_bounds, levels)
+
+    # And fill its cell arrays with the scalar data
+    grid_scalar.cell_data["Luminance relative to zenith"] = np.array(
+        rel_lum.reshape(az_meshgrid.shape)).swapaxes(-2, -1).ravel("C")
+
+    # Prepare the Sun
+    xyz_sun = sph_to_cart('deg', azimut=az_s, elev=el_s, zenith_angle=None,
+                          dist=RADIUS)
+    sun_mesh = pv.Sphere(radius=RADIUS / 20, center=xyz_sun)
+
+    # Make a plot
+    p = pv.Plotter()
+    p.add_mesh(sun_mesh, color='yellow')
+    p.add_mesh(grid_scalar, opacity=0.8, cmap="jet")
+    p.show()
+
+if __name__ == '__main__':
+
+    standard_skies = pd.read_csv(os.path.join('..', 'INPUTS', 'CIE_standard_skies.csv'))
+
+    # Example from the paper
+    sky_type = 12
+
+    sky_mesh = 'arange'  # {'arange', 'linspace', 'Reinhart'}
+
+    if sky_mesh.lower() == 'reinhart':
+        sky = ReinhartSky(MF=1).reinhart_patches
+        az = sky['az'].values
+        el = sky['el'].values
+        # meshgrid_flag = False
+        meshgrid_flag = True
+        az_meshgrid, el_meshgrid = np.meshgrid(az, el)
+    else:
+        if sky_mesh.lower() == 'arange':
+            step = 5
+            az = np.arange(0, 360+step, step=step)  # [°]
+            el = np.arange(0, 90+step, step=step)  # [°]
+        elif sky_mesh.lower() == 'linspace':
+            az = np.linspace(0, 360, num=145)  # [°]
+            el = np.linspace(0, 90, num=145)  # [°]
+
+        else:
+            raise ValueError('Unrecognized sky discretization')
+
+        meshgrid_flag = True
+        az_meshgrid, el_meshgrid = np.meshgrid(az, el)
+
+    el_s = 38.02  # [°]
+    az_s = 147.67  # [°] 145° is south-east
+    zenith_luminance = 4404  # cd/m²
+
+    if meshgrid_flag:
+        rel_lum = compute_luminance(sky_type, az_meshgrid.flatten(), el_meshgrid.flatten(), az_s, el_s).reshape(az_meshgrid.shape)
+    else:
+        # rel_lum = compute_luminance(sky_type, az, el, az_s, el_s)
+        rel_lum = compute_luminance(sky_type, np.append(az, [360]), np.append(el, [90]), az_s, el_s)
+
+    lum = rel_lum * zenith_luminance
+
+    print_n_max_values(rel_lum, 3)
+
+    # Plot with pcolormesh
+    plot_luminance_map(az, el, rel_lum,
+                       az_s, el_s,
+                       luminance_or_radiance='radiance',
+                       polar=True,
+                       colormap='jet')
+
+    # Plot all 15 standard skies in a (5, 3) subplots figure (takes a couple of minutes)
+    # plot_all_skies()
+
+if sky_mesh.lower() == 'reinhart':
+    # Plot with PyVista (only for Reinhart scheme)
+    plot_3D(sky, rel_lum)
+
diff --git a/example.py b/example.py
index 5c384b1725690892176378cd53f1afbe7b8ab3bb..50796fb74d54f29ccfeddc8d2a05ba608838b3be 100644
--- a/example.py
+++ b/example.py
@@ -17,6 +17,7 @@ from MODULES.PHOTOVOLTAICS.configuration import PV_Configuration_3D
 from MODULES.ENVIRONMENT.light import Sun_positions_sampled, Sun_positions, Light
 from MODULES.ENVIRONMENT.light import show_light_map2, Ray_casting_scene
 from MODULES.ENVIRONMENT.mesh import Mesh
+from MODULES.ENVIRONMENT.sky_model import ReinhartSky
 from MODULES.PHOTOVOLTAICS.production import PV_Production
 from MODULES.CROPS.run_crop_simulations import run_crop_simu
 
@@ -62,7 +63,7 @@ Sun_positions_complete = Sun_positions(Loc_1['Latitude'],
 # Instantiation of the 3D PV central
 PV_1_3Dconfig = PV_Configuration_3D(PV_params_dict,
                                     Sun_positions_samp.solar_vector,
-                                    visualization=True)  # !!!! Problem with rotation angle that are negative
+                                    visualization=False)  # !!!! Problem with rotation angle that are negative
 
 # Initiation of the object containing points of interest to compute light
 M = Mesh()
@@ -76,15 +77,18 @@ M.add_plane_ground_regular_meshes(Loc_1['Xmin_InterestZone'],
                                   Loc_1['dY_InterestZone'],
                                   flag="crop")
 
+# Discrete sky model
+discrete_sky = ReinhartSky(MF=Loc_1['MF']).reinhart_patches
+
 # Computation of sun and light data
 Light_instance = Light(WD.nyears_data, Sun_positions_complete)
 
 # Iniation and run of light ray casting model (direct and diffuse) with points of interest and scene
-L = Ray_casting_scene(mesh=M, geometry=PV_1_3Dconfig.PV_central_PD)
-L.get_light_maps(Sun_positions_samp.solar_vector,
-                 scheme=Loc_1['SkyDiscretizationScheme'],
-                 MF=Loc_1['MF'],
-                 n_small_suns=Loc_1['FibonacciSamples'])
+L = Ray_casting_scene(mesh=M,
+                      geometry=PV_1_3Dconfig.PV_central_PD,
+                      discrete_sky=discrete_sky)
+
+L.get_light_maps(Sun_positions_samp.solar_vector)
 
 # Integration of irradiation along days
 L.get_daily_irradiation_map(Sun_positions_samp.SP,
@@ -100,8 +104,11 @@ show_light_map2(L.sourcepoints[:,:-1],
                 PV_1_3Dconfig.PV_central_PD,
                 "Direct map [-]")
 
+# Display the Sky visibility map (= fraction of sky patches viewed by each control point)
+# It is purely derived from the geometry of the central
+# (i.e. computed once, or for each tracking position if tracking is activated)
 show_light_map2(L.sourcepoints[:,:-1], 
-                np.array(L.diff_map,dtype=np.float32), 
+                np.array(L.diff_map.sum(axis=1)/len(discrete_sky),dtype=np.float32),
                 PV_1_3Dconfig.PV_central_PD,
                 "Sky visibility map [-]")