Source code for raytraverse.evaluate.fieldmetric

# -*- 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 functools

import numpy as np
from scipy import stats

from raytools import translate
from raytools.evaluate import BaseMetricSet, PositionIndex
from raytools.mapper import ViewMapper


class Ray:

    def __init__(self, xyz):
        self.xyz = np.asarray(xyz).ravel()[0:3]
        self.mag = np.linalg.norm(self.xyz)
        self.unit = self.xyz / self.mag

    def __repr__(self):
        vout = "{:.04f} {:.04f} {:.04f}".format(*self.unit)
        return f"<Ray vec: [{vout}] mag: {self.mag:.06g}>"

    @property
    @functools.lru_cache(1)
    def tp(self):
        """overall vector (with magnitude)"""
        return translate.xyz2tp(self.unit).ravel()

    @property
    @functools.lru_cache(1)
    def aa(self):
        """overall vector (with magnitude)"""
        return translate.xyz2aa(self.unit).ravel()


[docs] class FieldMetric(BaseMetricSet): """calculate metrics on full spherical point clouds rather than view based metrics. Parameters ---------- vec: np.array (N, 3) directions of all rays omega: np.array (N,) solid angle of all rays lum: np.array (N,) luminance of all rays (multiplied by "scale") metricset: list, optional keys of metrics to return, same as property names scale: float, optional scalefactor for luminance npts: int, optional for equatorial metrics, the number of points to interpolate close: bool, optional include npts+1 duplicate to draw closed curve sigma: float, optional scale parameter of gaussian for kernel estimated metrics omega_as_view_area: bool, optional set to true when vectors either represent a whole sphere or a subset that does not match the viewmapper. if False, corrects boundary omega to properly trim to correct size. kwargs: additional arguments that may be required by additional properties """ def __init__(self, vec, omega, lum, vm=None, scale=1., npts=360, close=True, sigma=.05, omega_as_view_area=True, **kwargs): if vm is None: vm = ViewMapper((0, 0, 1)) self.npts = npts self.close = close self.sigma = sigma super().__init__(vec, omega, lum, vm, scale=scale, omega_as_view_area=omega_as_view_area, **kwargs) # -----------------dependencies----------------- @property @functools.lru_cache(1) def tp(self): """vectors in spherical coordinates""" return translate.xyz2tp(self.vec) @property @functools.lru_cache(1) def phi(self): """interpolated output phi values""" nx = np.linspace(0, 2*np.pi, self.npts + 1) if not self.close: nx = nx[:-1] return nx @property @functools.lru_cache(1) def eq_xyz(self): """interpolated output xyz vectors""" theta = np.full(self.phi.shape, np.pi/2) return translate.tp2xyz(np.stack((theta, self.phi)).T) # -------------------------single Ray metrics--------------------------- @property @functools.lru_cache(1) def avg(self): """overall vector (with magnitude)""" return Ray(np.einsum('ij,i,i->j', self.vec, self.lum, self.omega) * self.scale/self.view_area) @property @functools.lru_cache(1) def peak(self): """overall vector (with magnitude)""" i = np.argmax(self.lum) return Ray(self.vec[i] * self.lum[i]) # ----------equatorial metrics (where vm.xyz is the north pole------------- @property @functools.lru_cache(1) def eq_lum(self): """luminance along an interpolated equator with a bandwidth=sigma""" weights = np.sin(self.tp[:, 0]) * self.omega x = (np.mod(self.tp[:, 1], 2*np.pi) - np.pi).reshape(-1) nx = (np.mod(self.phi, 2*np.pi) - np.pi).reshape(-1, 1) n = stats.norm(scale=self.sigma) def polar_kernel(c): ang = np.abs(c - x) ang = np.where(ang > np.pi, 2*np.pi - ang, ang) nweight = n.pdf(ang) return np.average(self.lum, weights=nweight*weights) return np.apply_along_axis(polar_kernel, 1, nx) * self.scale @property @functools.lru_cache(1) def eq_density(self): """ray density along an interpolated equator""" extended = np.concatenate((self.tp[:, 1] - 2*np.pi, self.tp[:, 1], self.tp[:, 1] + 2*np.pi)) k = stats.gaussian_kde(extended, bw_method=self.sigma/3) return k(self.phi) * 3 @property @functools.lru_cache(1) def eq_illum(self): """illuminiance along an interpolated equator""" ctheta = np.maximum(np.einsum("ki,ji->kj", self.eq_xyz, self.vec), 0) return np.einsum('kj,j,j->k', ctheta, self.lum, self.omega)*self.scale @property @functools.lru_cache(1) def eq_gcr(self): """cosine weighted gcr along an interpolated equator""" ctheta = np.maximum(np.einsum("ki,ji->kj", self.eq_xyz, self.vec), 0) a2lum = np.einsum('kj,j,j,j->k', ctheta, self.lum, self.lum, self.omega) return a2lum/np.square(self.eq_illum/self.scale) @property @functools.lru_cache(1) def eq_loggc(self): ev = self.eq_illum slum = self.sources[:, -1] soga = self.sources[:, -2] pos = PositionIndex().positions_vec(self.eq_xyz, self.sources[:, 0:3]) pwsl2 = np.sum(np.square(slum[None])*soga[None] / np.square(pos), axis=1) return np.log10(1 + pwsl2/ev**1.87) @property @functools.lru_cache(1) def eq_dgp(self): ev = self.eq_illum ll = np.where(ev < 1000, np.exp(0.024*ev - 4)/(1 + np.exp(0.024*ev - 4)), 1) t2 = 9.18 * 10**-2 * self.eq_loggc return np.minimum(ll*(5.87 * 10**-5 * ev + t2 + 0.16), 1.0)