| 1 |
/*
|
| 2 |
* cmglare.c - routines for calculating glare autonomy.
|
| 3 |
*
|
| 4 |
* N. Jones
|
| 5 |
*/
|
| 6 |
|
| 7 |
/*
|
| 8 |
* Copyright (c) 2017-2019 Nathaniel Jones
|
| 9 |
*
|
| 10 |
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 11 |
* of this software and associated documentation files (the "Software"), to deal
|
| 12 |
* in the Software without restriction, including without limitation the rights
|
| 13 |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 14 |
* copies of the Software, and to permit persons to whom the Software is
|
| 15 |
* furnished to do so, subject to the following conditions:
|
| 16 |
*
|
| 17 |
* The above copyright notice and this permission notice shall be included in all
|
| 18 |
* copies or substantial portions of the Software.
|
| 19 |
*
|
| 20 |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 21 |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 22 |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 23 |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 24 |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 25 |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 26 |
* SOFTWARE.
|
| 27 |
*/
|
| 28 |
|
| 29 |
#include "rtmath.h"
|
| 30 |
#include "cmglare.h"
|
| 31 |
|
| 32 |
#define LUMINOUS_EFFICACY 179 /* lumens per Watt */
|
| 33 |
#define LUMINANCE_THRESHOLD 100 /* minimum threshold that will be interpreted as luminance rather than ratio between Ev and glare source */
|
| 34 |
|
| 35 |
#define ANGLE(u,v) acos(DOT((u),(v))) // TODO need to normalize?
|
| 36 |
|
| 37 |
typedef struct reinhart_sky {
|
| 38 |
//int mf; /* Linear divisions per Tregenza patch. */
|
| 39 |
int rings; /* Number of rings of sky patches. */
|
| 40 |
//int patches; /* Number of sky patches. */
|
| 41 |
double ringElevationAngle; /* Angle in radians between rings. */
|
| 42 |
int *patchesPerRow; /* Sky patches per row. */
|
| 43 |
int *firstPatchIndex; /* Index of first sky patch in each row. */
|
| 44 |
double *solidAngle; /* Solid angle of each patch in each row. */
|
| 45 |
} ReinhartSky;
|
| 46 |
|
| 47 |
static const int tnaz[] = { 30, 30, 24, 24, 18, 12, 6 }; /* Number of patches per row */
|
| 48 |
|
| 49 |
extern char* progname;
|
| 50 |
|
| 51 |
static ReinhartSky* make_sky(const CMATRIX *smx)
|
| 52 |
{
|
| 53 |
int mf, count, i, j;
|
| 54 |
double remaining = PI / 2;
|
| 55 |
|
| 56 |
/* Check sky partitionss */
|
| 57 |
switch (smx->nrows) { // nrows is Reinhart sky subdivisoins plus one for miss
|
| 58 |
case 2: mf = 0; break;
|
| 59 |
case 146: mf = 1; break;
|
| 60 |
case 578: mf = 2; break;
|
| 61 |
case 1298: mf = 3; break;
|
| 62 |
case 2306: mf = 4; break;
|
| 63 |
case 3602: mf = 5; break;
|
| 64 |
case 5186: mf = 6; break;
|
| 65 |
case 7058: mf = 7; break;
|
| 66 |
case 9218: mf = 8; break;
|
| 67 |
case 11666: mf = 9; break;
|
| 68 |
case 14402: mf = 10; break;
|
| 69 |
case 17426: mf = 11; break;
|
| 70 |
case 20738: mf = 12; break;
|
| 71 |
default:
|
| 72 |
fprintf(stderr,
|
| 73 |
"%s: unknown number of sky patches %d\n",
|
| 74 |
progname, smx->nrows);
|
| 75 |
return NULL;
|
| 76 |
}
|
| 77 |
|
| 78 |
/* Allocate sky */
|
| 79 |
ReinhartSky *sky = (ReinhartSky*)malloc(sizeof(ReinhartSky));
|
| 80 |
if (!sky) goto memerr;
|
| 81 |
|
| 82 |
/* Calculate patches per row */
|
| 83 |
sky->rings = 7 * mf + 1;
|
| 84 |
sky->patchesPerRow = (int*)malloc(sky->rings * sizeof(int));
|
| 85 |
sky->firstPatchIndex = (int*)malloc(sky->rings * sizeof(int));
|
| 86 |
if (!sky->patchesPerRow || !sky->firstPatchIndex) goto memerr;
|
| 87 |
count = 1; // The below horizon patch
|
| 88 |
for (i = 0; i < 7; i++)
|
| 89 |
for (j = 0; j < mf; j++) {
|
| 90 |
sky->firstPatchIndex[i * mf + j] = count;
|
| 91 |
count += sky->patchesPerRow[i * mf + j] = tnaz[i] * mf;
|
| 92 |
}
|
| 93 |
sky->firstPatchIndex[7 * mf] = count;
|
| 94 |
sky->patchesPerRow[7 * mf] = 1;
|
| 95 |
//sky->patches = count + 1;
|
| 96 |
|
| 97 |
/* Calculate solid angle of patches in each row */
|
| 98 |
sky->solidAngle = (double*)malloc(sky->rings * sizeof(double));
|
| 99 |
if (!sky->solidAngle) goto memerr;
|
| 100 |
sky->ringElevationAngle = PI / (2 * sky->rings - 1);
|
| 101 |
sky->solidAngle[0] = 2 * PI;
|
| 102 |
for (i = 1; i < sky->rings; i++) {
|
| 103 |
remaining -= sky->ringElevationAngle;
|
| 104 |
sky->solidAngle[i] = 2 * PI * (1 - cos(remaining)); // solid angle of cap
|
| 105 |
sky->solidAngle[i - 1] -= sky->solidAngle[i];
|
| 106 |
sky->solidAngle[i - 1] /= sky->patchesPerRow[i - 1];
|
| 107 |
}
|
| 108 |
|
| 109 |
return sky;
|
| 110 |
|
| 111 |
memerr:
|
| 112 |
fprintf(stderr,
|
| 113 |
"%s: out of memory\n",
|
| 114 |
progname);
|
| 115 |
return NULL;
|
| 116 |
}
|
| 117 |
|
| 118 |
static void free_sky(ReinhartSky *sky)
|
| 119 |
{
|
| 120 |
if (sky) {
|
| 121 |
free(sky->patchesPerRow);
|
| 122 |
free(sky->firstPatchIndex);
|
| 123 |
free(sky->solidAngle);
|
| 124 |
free(sky);
|
| 125 |
}
|
| 126 |
}
|
| 127 |
|
| 128 |
static void get_patch_direction(const ReinhartSky *sky, const int patch, FVECT target)
|
| 129 |
{
|
| 130 |
//if (patch >= sky->patches || patch < 0) throw new RuntimeException("Illegal patch " + patch);
|
| 131 |
if (!patch) {
|
| 132 |
target[0] = target[1] = 0;
|
| 133 |
target[2] = -1;
|
| 134 |
return; // Ignore below horizon?
|
| 135 |
}
|
| 136 |
int row = sky->rings - 1;
|
| 137 |
while (patch < sky->firstPatchIndex[row]) row--;
|
| 138 |
const double alt = PI / 2 - sky->ringElevationAngle * (row - (sky->rings - 1));
|
| 139 |
const double azi = 2 * PI * (patch - sky->firstPatchIndex[row]) / sky->patchesPerRow[row];
|
| 140 |
const double cos_alt = cos(alt);
|
| 141 |
target[0] = cos_alt * -sin(azi);
|
| 142 |
target[1] = cos_alt * -cos(azi);
|
| 143 |
target[2] = sin(alt);
|
| 144 |
}
|
| 145 |
|
| 146 |
static double get_patch_solid_angle(const ReinhartSky *sky, const int patch, const double cos_theta)
|
| 147 |
{
|
| 148 |
//if (patch >= sky->patches || patch < 0) throw new RuntimeException("Illegal patch " + patch);
|
| 149 |
if (!patch) return 2 * (PI - 2 * acos(cos_theta)); // Solid angle overlap between visible hemisphere and ground hemisphere
|
| 150 |
int row = sky->rings - 1;
|
| 151 |
while (patch < sky->firstPatchIndex[row]) row--;
|
| 152 |
return sky->solidAngle[row];
|
| 153 |
}
|
| 154 |
|
| 155 |
static double get_guth(const FVECT dir, const FVECT forward, const FVECT up)
|
| 156 |
{
|
| 157 |
double posindex;
|
| 158 |
FVECT hv, temp;
|
| 159 |
VCROSS(hv, forward, up);
|
| 160 |
normalize(hv);
|
| 161 |
VCROSS(temp, forward, hv);
|
| 162 |
double phi = ANGLE(dir, temp) - PI / 2;
|
| 163 |
|
| 164 |
/* Guth model, equation from IES lighting handbook */
|
| 165 |
if (phi >= 0) {
|
| 166 |
double sigma = ANGLE(dir, forward);
|
| 167 |
VSUM(hv, forward, dir, 1 / DOT(dir, forward));
|
| 168 |
normalize(hv);
|
| 169 |
double tau = ANGLE(hv, up);
|
| 170 |
tau *= 180.0 / PI;
|
| 171 |
sigma *= 180.0 / PI;
|
| 172 |
|
| 173 |
if (sigma <= 0)
|
| 174 |
sigma = -sigma;
|
| 175 |
|
| 176 |
posindex = exp((35.2 - 0.31889 * tau - 1.22 * exp(-2.0 * tau / 9.0)) / 1000.0 * sigma + (21.0 + 0.26667 * tau - 0.002963 * tau * tau) / 100000.0 * sigma * sigma);
|
| 177 |
}
|
| 178 |
/* below line of sight, using Iwata model */
|
| 179 |
else {
|
| 180 |
double teta = PI / 2 - ANGLE(dir, hv);
|
| 181 |
|
| 182 |
if (teta == 0.0)
|
| 183 |
teta = FTINY;
|
| 184 |
|
| 185 |
double fact = 0.8;
|
| 186 |
double d = 1 / tan(phi);
|
| 187 |
double s = tan(teta) / tan(phi);
|
| 188 |
double r = sqrt((1 + s * s) / (d * d));
|
| 189 |
if (r > 0.6)
|
| 190 |
fact = 1.2;
|
| 191 |
if (r > 3)
|
| 192 |
r = 3.0;
|
| 193 |
|
| 194 |
posindex = 1.0 + fact * r;
|
| 195 |
}
|
| 196 |
if (posindex > 16)
|
| 197 |
posindex = 16.0;
|
| 198 |
|
| 199 |
return posindex;
|
| 200 |
}
|
| 201 |
|
| 202 |
float* cm_glare(const CMATRIX *dcmx, const CMATRIX *evmx, const CMATRIX *smx, const int *occupied, const double dgp_limit, const double dgp_threshold, const FVECT *views, const FVECT dir, const FVECT up)
|
| 203 |
{
|
| 204 |
int p, t, c;
|
| 205 |
int hourly_output = dgp_limit < 0;
|
| 206 |
float *dgp_list;
|
| 207 |
ReinhartSky *sky;
|
| 208 |
FVECT vdir;
|
| 209 |
|
| 210 |
/* Check consistancy */
|
| 211 |
if ((dcmx->nrows != evmx->nrows) | (dcmx->ncols != smx->nrows) | (evmx->ncols != smx->ncols)) {
|
| 212 |
fprintf(stderr,
|
| 213 |
"%s: inconsistant matrix dimensions: dc(%d, %d) ev(%d, %d) s(%d, %d)\n",
|
| 214 |
progname, dcmx->nrows, dcmx->ncols, evmx->nrows, evmx->ncols, smx->nrows, smx->ncols);
|
| 215 |
return NULL;
|
| 216 |
}
|
| 217 |
|
| 218 |
/* Create output buffer */
|
| 219 |
dgp_list = (float*)malloc(evmx->nrows * (hourly_output ? evmx->ncols : 1) * sizeof(float));
|
| 220 |
if (!dgp_list) {
|
| 221 |
fprintf(stderr,
|
| 222 |
"%s: out of memory in cm_glare()\n",
|
| 223 |
progname);
|
| 224 |
return NULL;
|
| 225 |
}
|
| 226 |
|
| 227 |
/* Create sky */
|
| 228 |
sky = make_sky(smx);
|
| 229 |
if (!sky) return NULL;
|
| 230 |
|
| 231 |
/* Calculate glare limit */
|
| 232 |
double ev_max = -1;
|
| 233 |
if (!hourly_output) {
|
| 234 |
ev_max = (dgp_limit - 0.16) / 5.87e-5;
|
| 235 |
if (ev_max < 0) ev_max = 0;
|
| 236 |
}
|
| 237 |
|
| 238 |
/* For each position and direction */
|
| 239 |
if (!views) VCOPY(vdir, dir);
|
| 240 |
for (p = 0; p < evmx->nrows; p++) {
|
| 241 |
/* For each time step */
|
| 242 |
int occupied_hours = 0;
|
| 243 |
int glare_hours = 0;
|
| 244 |
if (views) VCOPY(vdir, views[p]);
|
| 245 |
for (t = 0; t < evmx->ncols; t++) {
|
| 246 |
if (!occupied[t]) {
|
| 247 |
/* Not occupied */
|
| 248 |
if (hourly_output) dgp_list[p * evmx->ncols + t] = 0.0f;
|
| 249 |
}
|
| 250 |
else {
|
| 251 |
/* Occupied */
|
| 252 |
double illum = LUMINOUS_EFFICACY * bright(cm_lval(evmx, p, t));
|
| 253 |
occupied_hours++;
|
| 254 |
|
| 255 |
if (illum <= FTINY) {
|
| 256 |
/* No light, so no glare */
|
| 257 |
if (hourly_output) dgp_list[p * evmx->ncols + t] = 0.0f;
|
| 258 |
}
|
| 259 |
else if ((illum >= ev_max) & (!hourly_output)) {
|
| 260 |
/* Guarangeed glare */
|
| 261 |
glare_hours++;
|
| 262 |
}
|
| 263 |
else {
|
| 264 |
/* Calculate enhanced simplified daylight glare probability */
|
| 265 |
double sum = 0.0;
|
| 266 |
FVECT patch_normal;
|
| 267 |
for (c = 0; c < smx->nrows; c++) {
|
| 268 |
const double dc = bright(cm_lval(dcmx, p, c));
|
| 269 |
if (dc > 0) {
|
| 270 |
get_patch_direction(sky, c, patch_normal);
|
| 271 |
if (!c) {
|
| 272 |
/* Direction toward the center of the visible ground */
|
| 273 |
VADD(patch_normal, patch_normal, vdir);
|
| 274 |
if (normalize(patch_normal) == 0) continue;
|
| 275 |
}
|
| 276 |
const double cos_theta = DOT(vdir, patch_normal);
|
| 277 |
if (cos_theta <= FTINY) continue;
|
| 278 |
const double s = LUMINOUS_EFFICACY * bright(cm_lval(smx, c, t));
|
| 279 |
const double omega = get_patch_solid_angle(sky, c, cos_theta);
|
| 280 |
const double patch_luminance = (dc * s) / (omega * cos_theta);
|
| 281 |
|
| 282 |
double min_patch_luminance = dgp_threshold;
|
| 283 |
if (dgp_threshold < LUMINANCE_THRESHOLD)
|
| 284 |
min_patch_luminance *= illum / PI; // TODO should use average luminance, not illuminance
|
| 285 |
if (patch_luminance < min_patch_luminance) continue;
|
| 286 |
const double P = get_guth(patch_normal, vdir, up);
|
| 287 |
sum += (patch_luminance * patch_luminance * omega) / (P * P);
|
| 288 |
}
|
| 289 |
}
|
| 290 |
|
| 291 |
//double dgp = 5.87e-5 * illum + 0.092 * log10(1 + dgp / pow(illum, 1.87)) + 0.159;
|
| 292 |
double eDGPs = 5.87e-5 * illum + 0.0918 * log10(1 + sum / pow(illum, 1.87)) + 0.16;
|
| 293 |
if (illum < 1000) /* low light correction */
|
| 294 |
eDGPs *= exp(0.024 * illum - 4) / (1 + exp(0.024 * illum - 4));
|
| 295 |
//eDGPs /= 1.1 - 0.5 * age / 100.0; /* age correction */
|
| 296 |
if (eDGPs > 1.0) eDGPs = 1.0;
|
| 297 |
|
| 298 |
if (hourly_output)
|
| 299 |
dgp_list[p * evmx->ncols + t] = (float)eDGPs;
|
| 300 |
else if (eDGPs >= dgp_limit)
|
| 301 |
glare_hours++;
|
| 302 |
}
|
| 303 |
}
|
| 304 |
}
|
| 305 |
if (!hourly_output) {
|
| 306 |
/* Save glare autonomy */
|
| 307 |
dgp_list[p] = (float)(occupied_hours - glare_hours) / occupied_hours;
|
| 308 |
}
|
| 309 |
}
|
| 310 |
|
| 311 |
free_sky(sky);
|
| 312 |
|
| 313 |
return dgp_list;
|
| 314 |
}
|
| 315 |
|
| 316 |
static int getvec(FVECT vec, const int dtype, FILE *fp) /* get a vector from fp */
|
| 317 |
{
|
| 318 |
static float vf[3];
|
| 319 |
static double vd[3];
|
| 320 |
char buf[32];
|
| 321 |
int i;
|
| 322 |
|
| 323 |
switch (dtype) {
|
| 324 |
case DTascii:
|
| 325 |
for (i = 0; i < 3; i++) {
|
| 326 |
if (fgetword(buf, sizeof(buf), fp) == NULL ||
|
| 327 |
!isflt(buf))
|
| 328 |
return(-1);
|
| 329 |
vec[i] = atof(buf);
|
| 330 |
}
|
| 331 |
break;
|
| 332 |
case DTfloat:
|
| 333 |
if (getbinary(vf, sizeof(float), 3, fp) != 3)
|
| 334 |
return(-1);
|
| 335 |
VCOPY(vec, vf);
|
| 336 |
break;
|
| 337 |
case DTdouble:
|
| 338 |
if (getbinary(vd, sizeof(double), 3, fp) != 3)
|
| 339 |
return(-1);
|
| 340 |
VCOPY(vec, vd);
|
| 341 |
break;
|
| 342 |
default:
|
| 343 |
fprintf(stderr,
|
| 344 |
"%s: botched input format\n",
|
| 345 |
progname);
|
| 346 |
return(-1);
|
| 347 |
}
|
| 348 |
return(0);
|
| 349 |
}
|
| 350 |
|
| 351 |
int cm_load_schedule(const int count, int* schedule, FILE *fp)
|
| 352 |
{
|
| 353 |
char buf[512];
|
| 354 |
char *cp;
|
| 355 |
char *comma;
|
| 356 |
double val;
|
| 357 |
int i = 0;
|
| 358 |
|
| 359 |
while (fgetline(buf, sizeof(buf), fp) != NULL) {
|
| 360 |
if (buf[0] == '#') continue; // Comment line
|
| 361 |
comma = NULL;
|
| 362 |
for (cp = buf; *cp; cp++) {
|
| 363 |
/* If there are multiple commas, assume the value is after the last comma */
|
| 364 |
if (*cp == ',') {
|
| 365 |
comma = cp; /* Record position of last comma */
|
| 366 |
}
|
| 367 |
}
|
| 368 |
if (comma)
|
| 369 |
val = atof(comma + 1);
|
| 370 |
else
|
| 371 |
val = atof(buf);
|
| 372 |
|
| 373 |
if (i < count) {
|
| 374 |
/* Add the value to the schedule */
|
| 375 |
schedule[i++] = (val > 0);
|
| 376 |
}
|
| 377 |
else {
|
| 378 |
fprintf(stderr,
|
| 379 |
"%s: too many schedule entries\n",
|
| 380 |
progname);
|
| 381 |
return(1);
|
| 382 |
}
|
| 383 |
}
|
| 384 |
fclose(fp);
|
| 385 |
|
| 386 |
if (i < count) {
|
| 387 |
fprintf(stderr,
|
| 388 |
"%s: too few schedule entries\n",
|
| 389 |
progname);
|
| 390 |
return(-1);
|
| 391 |
}
|
| 392 |
|
| 393 |
return 0;
|
| 394 |
}
|
| 395 |
|
| 396 |
FVECT* cm_load_views(const int nrows, const int dtype, FILE *fp)
|
| 397 |
{
|
| 398 |
int i;
|
| 399 |
double d;
|
| 400 |
FVECT orig;
|
| 401 |
|
| 402 |
FVECT *views = (FVECT*)malloc(nrows * sizeof(FVECT));
|
| 403 |
if (!views) {
|
| 404 |
fprintf(stderr,
|
| 405 |
"%s: out of memory in cm_load_views()\n",
|
| 406 |
progname);
|
| 407 |
return NULL;
|
| 408 |
}
|
| 409 |
|
| 410 |
for (i = 0; i < nrows; i++) {
|
| 411 |
if (getvec(orig, dtype, fp) | getvec(views[i], dtype, fp)) {
|
| 412 |
fprintf(stderr,
|
| 413 |
"%s: unexpected end of input, missing %d entries\n",
|
| 414 |
progname, i);
|
| 415 |
return NULL;
|
| 416 |
}
|
| 417 |
|
| 418 |
d = normalize(views[i]);
|
| 419 |
if (d == 0.0) { /* zero ==> flush */
|
| 420 |
fprintf(stderr,
|
| 421 |
"%s: zero length direction detected\n",
|
| 422 |
progname);
|
| 423 |
return NULL;
|
| 424 |
}
|
| 425 |
}
|
| 426 |
|
| 427 |
return views;
|
| 428 |
}
|
| 429 |
|
| 430 |
int cm_write_glare(const float *mp, const int nrows, const int ncols, const int dtype, FILE *fp)
|
| 431 |
{
|
| 432 |
static const char tabEOL[2] = { '\t', '\n' };
|
| 433 |
int r, c;
|
| 434 |
double dc[1];
|
| 435 |
|
| 436 |
switch (dtype) {
|
| 437 |
case DTascii:
|
| 438 |
for (r = 0; r < nrows; r++)
|
| 439 |
for (c = 0; c < ncols; c++, mp++)
|
| 440 |
fprintf(fp, "%.6e%c",
|
| 441 |
mp[0],
|
| 442 |
tabEOL[c >= ncols - 1]);
|
| 443 |
break;
|
| 444 |
case DTfloat:
|
| 445 |
r = ncols*nrows;
|
| 446 |
while (r > 0) {
|
| 447 |
c = putbinary(mp, sizeof(float), r, fp);
|
| 448 |
if (c <= 0)
|
| 449 |
return(0);
|
| 450 |
mp += c;
|
| 451 |
r -= c;
|
| 452 |
}
|
| 453 |
break;
|
| 454 |
case DTdouble:
|
| 455 |
r = ncols*nrows;
|
| 456 |
while (r--) {
|
| 457 |
dc[0] = mp[0];
|
| 458 |
if (putbinary(dc, sizeof(double), 1, fp) != 1)
|
| 459 |
return(0);
|
| 460 |
mp++;
|
| 461 |
}
|
| 462 |
break;
|
| 463 |
default:
|
| 464 |
fputs("Unsupported data type in cm_write_glare()!\n", stderr);
|
| 465 |
return(0);
|
| 466 |
}
|
| 467 |
return(fflush(fp) == 0);
|
| 468 |
} |