def get_cie_mesopic_adaptation(Lp, Ls=None, SP=None): """ Get the mesopic adaptation state according to CIE191:2010 Args: :Lp: | float or ndarray with photopic adaptation luminance :Ls: | None, optional | float or ndarray with scotopic adaptation luminance | If None: SP must be supplied. :SP: | None, optional | S/P ratio | If None: Ls must be supplied. Returns: :Lmes: | mesopic adaptation luminance :m: | mesopic adaptation coefficient Reference: 1. `CIE 191:2010 Recommended System for Mesopic Photometry Based on Visual Performance. (ISBN 978-3-901906-88-6 ), <http://cie.co.at/publications/recommended-system-mesopic-photometry-based-visual-performance>`_ """ Lp = np.atleast_1d(Lp) Ls = np.atleast_1d(Ls) SP = np.atleast_1d(SP) if not (None in SP): Ls = Lp * SP elif not (None in Ls): SP = Ls / Lp else: raise Exception( 'Either the S/P ratio or the scotopic luminance Ls must be supplied in addition to the photopic luminance Lp' ) m = np.ones_like(Ls) * np.nan Lmes = m.copy() for i in range(Lp.shape[0]): mi_ = 0.5 fLmes = lambda m, Lp, SP: ( (m * Lp) + (1 - m) * SP * 683 / 1699) / (m + (1 - m) * 683 / 1699) fm = lambda m, Lp, SP: 0.767 + 0.3334 * np.log10(fLmes(m, Lp, SP)) mi = fm(mi_, Lp[i], SP[i]) while True: if np.isclose(mi, mi_): break mi_ = mi mi = fm(mi_, Lp[i], SP[i]) m[i] = mi Lmes[i] = fLmes(mi, Lp[i], SP[i]) return Lmes, m
def positive_arctan(x,y, htype = 'deg'): """ Calculate positive angle (0°-360° or 0 - 2*pi rad.) from x and y. Args: :x: | ndarray of x-coordinates :y: | ndarray of y-coordinates :htype: | 'deg' or 'rad', optional | - 'deg': hue angle between 0° and 360° | - 'rad': hue angle between 0 and 2pi radians Returns: :returns: | ndarray of positive angles. """ if htype == 'deg': r2d = 180.0/np.pi h360 = 360.0 else: r2d = 1.0 h360 = 2.0*np.pi h = np.atleast_1d((np.arctan2(y,x)*r2d)) h[np.where(h<0)] = h[np.where(h<0)] + h360 return h
def __init__(self, *args, argtype='xyz', vtype='xyz', _TINY=1e-15): """ Initialize 3-dimensional vector. Args: :`*args`: | x,y,z coordinates :vtype: | 'xyz', optional | if 'xyz': cartesian coordinate input | if 'tpr': spherical coordinates input (t: theta, p: phi, r: radius) :_TINY: | Set smallest value considered still different from zero. """ self._TINY = _TINY self.vtype = vtype if len(args) == 0: args = [0.0, 0.0, 0.0] args = [np.atleast_1d(args[i]) for i in range(len(args))] # make atleast_1d ndarray if vtype == 'xyz': self.x = args[0] self.y = args[1] self.z = args[2] elif vtype == 'tpr': if len(args) == 2: args.append(np.ones(args[0].shape)) self.set_tpr(*args) self.shape = self.x.shape
def plot_color_data(x,y,z=None, axh=None, show = True, cieobs =_CIEOBS, \ cspace = _CSPACE, formatstr = 'k-', legend_loc = None, **kwargs): """ Plot color data from x,y [,z]. Args: :x: | float or ndarray with x-coordinate data :y: | float or ndarray with y-coordinate data :z: | None or float or ndarray with Z-coordinate data, optional | If None: make 2d plot. :axh: | None or axes handle, optional | Determines axes to plot data in. | None: make new figure. :show: | True or False, optional | Invoke matplotlib.pyplot.show() right after plotting :cieobs: | luxpy._CIEOBS or str, optional | Determines CMF set to calculate spectrum locus or other. :cspace: | luxpy._CSPACE or str or None, optional | Determines color space / chromaticity diagram to plot data in. | Note that data is expected to be in specified :cspace: | If None: don't do any formatting of x,y [z] axes. :formatstr: | 'k-' or str, optional | Format str for plotting (see ?matplotlib.pyplot.plot) :kwargs: | additional keyword arguments for use with matplotlib.pyplot. Returns: :returns: | None (:show: == True) | or | handle to current axes (:show: == False) """ x = np.atleast_1d(x) y = np.atleast_1d(y) if z is not None: z = np.atleast_1d(z) if axh is None: fig = plt.figure() axh = plt.axes(projection='3d') if 'grid' in kwargs.keys(): axh.grid(kwargs['grid']) kwargs.pop('grid') axh.plot3D(x, y, z, formatstr, linewidth=2, **kwargs) axh.set_zlabel(_CSPACE_AXES[cspace][0], kwargs) else: if axh is None: fig = plt.figure() axh = plt.axes() if 'grid' in kwargs.keys(): axh.grid(kwargs['grid']) kwargs.pop('grid') axh.plot(x, y, formatstr, linewidth=2, **kwargs) axh.set_xlabel(_CSPACE_AXES[cspace][1], kwargs) axh.set_ylabel(_CSPACE_AXES[cspace][2], kwargs) if 'label' in kwargs.keys(): axh.legend(loc=legend_loc) if show == True: plt.show() else: return axh
def cie_interp(data, wl_new, kind=None, negative_values_allowed=False, extrap_values=None): """ Interpolate / extrapolate spectral data following standard CIE15-2018. | The kind of interpolation depends on the spectrum type defined in :kind:. | Extrapolation is always done by replicate the closest known values. Args: :data: | ndarray with spectral data | (.shape = (number of spectra + 1, number of original wavelengths)) :wl_new: | ndarray with new wavelengths :kind: | None, optional | - If :kind: is None, return original data. | - If :kind: is a spectrum type (see _INTERP_TYPES), the correct | interpolation type if automatically chosen. | - Or :kind: can be any interpolation type supported by | scipy.interpolate.interp1d (math.interp1d if nan's are present!!) :negative_values_allowed: | False, optional | If False: negative values are clipped to zero. :extrap_values: | None, optional | If None: use CIE recommended 'closest value' approach when extrapolating. | If float or list or ndarray, use those values to fill extrapolated value(s). | If 'ext': use normal extrapolated values by scipy.interpolate.interp1d Returns: :returns: | ndarray of interpolated spectral data. | (.shape = (number of spectra + 1, number of wavelength in wl_new)) """ if (kind is not None): # Wavelength definition: wl_new = getwlr(wl_new) if (not np.array_equal(data[0], wl_new)) | np.isnan(data).any(): extrap_values = np.atleast_1d(extrap_values) # Set interpolation type based on data type: if kind in _INTERP_TYPES['linear']: kind = 'linear' elif kind in _INTERP_TYPES['cubic']: kind = 'cubic' # define wl, S, wl_new: wl = np.array(data[0]) S = data[1:] wl_new = np.array(wl_new) # Interpolate each spectrum in S: N = S.shape[0] nan_indices = np.isnan(S) # Interpolate all (if not all rows have nan): rows_with_nans = np.where(nan_indices.sum(axis=1))[0] if not (rows_with_nans.size == N): #allrows_nans = False if extrap_values[0] is None: fill_value = (0, 0) elif (((type(extrap_values[0]) == np.str_) | (type(extrap_values[0]) == str)) and (extrap_values[0][:3] == 'ext')): fill_value = 'extrapolate' else: fill_value = (extrap_values[0], extrap_values[-1]) Si = sp.interpolate.interp1d(wl, S, kind=kind, bounds_error=False, fill_value=fill_value)(wl_new) #extrapolate by replicating closest known (in source data!) value (conform CIE15-2004 recommendation) if extrap_values[0] is None: Si[:, wl_new < wl[0]] = S[:, :1] Si[:, wl_new > wl[-1]] = S[:, -1:] else: #allrows_nans = True Si = np.zeros([N, wl_new.shape[0]]) Si.fill(np.nan) # Re-interpolate those which have none: if nan_indices.any(): #looping required as some values are NaN's for i in rows_with_nans: nonan_indices = np.logical_not(nan_indices[i]) wl_nonan = wl[nonan_indices] S_i_nonan = S[i][nonan_indices] Si_nonan = math.interp1(wl_nonan, S_i_nonan, wl_new, kind=kind, ext='extrapolate') # Si_nonan = sp.interpolate.interp1d(wl_nonan, S_i_nonan, kind = kind, bounds_error = False, fill_value = 'extrapolate')(wl_new) #extrapolate by replicating closest known (in source data!) value (conform CIE15-2004 recommendation) if extrap_values[0] is None: Si_nonan[wl_new < wl_nonan[0]] = S_i_nonan[0] Si_nonan[wl_new > wl_nonan[-1]] = S_i_nonan[-1] elif (((type(extrap_values[0]) == np.str_) | (type(extrap_values[0]) == str)) and (extrap_values[0][:3] == 'ext')): pass else: Si_nonan[wl_new < wl_nonan[0]] = extrap_values[0] Si_nonan[wl_new > wl_nonan[-1]] = extrap_values[-1] Si[i] = Si_nonan # No negative values allowed for spectra: if negative_values_allowed == False: if np.any(Si): Si[Si < 0.0] = 0.0 # Add wavelengths to data array: return np.vstack((wl_new, Si)) return data