Source code for raytools.evaluate.metricset

# -*- coding: utf-8 -*-
# Copyright (c) 2020 Stephen Wasilewski, HSLU and EPFL
# =======================================================================
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# =======================================================================
import numpy as np
import functools

from raytools.evaluate.basemetricset import BaseMetricSet


[docs] class MetricSet(BaseMetricSet): """object for calculating metrics based on a view direction, and rays consisting on direction, solid angle and luminance information by encapsulating these calculations within a class, metrics with redundant calculations can take advantage of cached results, for example dgp does not need to recalculate illuminance when it has been directly requested. all metrics can be accessed as properties (and are calculated just in time) or the object can be called (no arguments) to return a np.array of all metrics defined in "metricset" Parameters ---------- vm: raytools.mapper.ViewMapper the view direction vec: np.array (N, 3) directions of all rays in view omega: np.array (N,) solid angle of all rays in view lum: np.array (N,) luminance of all rays in view (multiplied by "scale") metricset: list, optional keys of metrics to return, same as property names scale: float, optional scalefactor for luminance threshold: float, optional threshold for glaresource/background similar behavior to evalglare '-b' paramenter. if greater than 100 used as a fixed luminance threshold. otherwise used as a factor times the task luminance (defined by 'tradius') guth: bool, optional if True, use Guth for the upper field of view and iwata for the lower if False, use Kim tradius: float, optional radius in degrees for task luminance calculation kwargs: additional arguments that may be required by additional properties """ #: available metrics (and the default return set) defaultmetrics = BaseMetricSet.defaultmetrics + ["ugp", "dgp"] allmetrics = BaseMetricSet.allmetrics + ["ugp", "dgp", "tasklum", "backlum", "dgp_t1", "log_gc", "dgp_t2", "ugr", "threshold", "pwsl2", "view_area", "backlum_true", "srcillum", "srcarea", "maxlum"] safe2sum = BaseMetricSet.safe2sum.union(('pwsl2', 'srcillum')) def __init__(self, vec, omega, lum, vm, metricset=None, scale=179., threshold=2000., guth=True, tradius=30.0, omega_as_view_area=False, lowlight=False, **kwargs): super().__init__(vec, omega, lum, vm, metricset=metricset, scale=scale, omega_as_view_area=omega_as_view_area, guth=guth, **kwargs) self._lowlight = lowlight self._threshold = threshold self.tradius = tradius # -------------------metric dependencies (return array)-------------------- @property @functools.lru_cache(1) def src_mask(self): """boolean mask for filtering source/background rays""" return self.lum * self.scale > self.threshold @property @functools.lru_cache(1) def task_mask(self): return self.vm.degrees(self.vec) < self.tradius @property @functools.lru_cache(1) def sources(self): """vec, omega, lum of rays above threshold""" m = self.src_mask vec = self.vec[m] lum = self.lum[m] oga = self.omega[m] return vec, oga, lum @property @functools.lru_cache(1) def background(self): """vec, omega, lum of rays below threshold""" m = np.logical_not(self.src_mask) vec = self.vec[m] lum = self.lum[m] oga = self.omega[m] return vec, oga, lum @property @functools.lru_cache(1) def source_pos_idx(self): return self.pos_idx[self.src_mask] # -----------------metric functions (return single value)----------------- @property @functools.lru_cache(1) def threshold(self): """threshold for glaresource/background similar behavior to evalglare '-b' paramenter""" if self._threshold > 100: return self._threshold else: return self.tasklum * self._threshold @property @functools.lru_cache(1) def pwsl2(self): """position weighted source luminance squared, used by dgp, ugr, etc sum(Ls^2*omega/Ps^2)""" _, soga, slum = self.sources return np.sum(np.square(slum)*soga*self.scale**2 / np.square(self.source_pos_idx)) @property @functools.lru_cache(1) def srcillum(self): """source illuminance""" svec, soga, slum = self.sources return np.einsum('i,i,i->', self.vm.ctheta(svec), slum, soga) * self.scale @property @functools.lru_cache(1) def srcarea(self): """total source area""" _, soga, _ = self.sources return np.sum(soga) @property @functools.lru_cache(1) def maxlum(self): """peak luminance""" if self.lum.size > 0: return np.max(self.lum)*self.scale else: return 0.0 @property @functools.lru_cache(1) def backlum(self): """average background luminance CIE estimate (official for some metrics)""" return (self.illum - self.srcillum) / np.pi @property @functools.lru_cache(1) def backlum_true(self): """average background luminance mathematical""" bvec, boga, blum = self.background return np.einsum('i,i->', blum, boga)*self.scale/np.sum(boga) @property @functools.lru_cache(1) def tasklum(self): """average task luminance""" lum = self.lum[self.task_mask] oga = self.omega[self.task_mask] return np.einsum('i,i->', lum, oga)*self.scale/np.sum(oga) @property @functools.lru_cache(1) def dgp(self): ll = 1 if self._lowlight and self.illum < 500: ll = np.exp(0.024*self.illum - 4)/(1 + np.exp(0.024*self.illum - 4)) return np.minimum(ll*(self.dgp_t1 + self.dgp_t2 + 0.16), 1.0) @property @functools.lru_cache(1) def dgp_t1(self): return 5.87 * 10**-5 * self.illum @property @functools.lru_cache(1) def log_gc(self): return np.log10(1 + self.pwsl2/self.illum**1.87) @property @functools.lru_cache(1) def dgp_t2(self): return 9.18 * 10**-2 * self.log_gc @property @functools.lru_cache(1) def ugr(self): with np.errstate(divide='ignore'): ug = np.maximum(0, 8 * np.log10(0.25 * self.pwsl2 / self.backlum)) return ug @property @functools.lru_cache(1) def ugp(self): """http://dx.doi.org/10.1016/j.buildenv.2016.08.005""" return (1 + 2/7 * 10**(-(self.ugr + 5)/40))**-10