Source code for raytraverse.sampler.samplersuns

# -*- 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
from clasp.script_tools import try_mkdir, sglob

from raytools import io, translate
from raytraverse.mapper import MaskedPlanMapper
from raytraverse.sampler import draw
from raytraverse.sampler.samplerarea import SamplerArea
from raytraverse.sampler.basesampler import BaseSampler, filterdict
from raytraverse.sampler.sunsamplerpt import SunSamplerPt
from raytraverse.lightfield import SunsPlaneKD, LightPlaneKD
from raytraverse.evaluate import SamplingMetrics
from raytools.utility import pool_call


[docs] class SamplerSuns(BaseSampler): """wavelet based sun position sampling class Parameters ---------- scene: raytraverse.scene.Scene scene class containing geometry and formatter compatible with engine engine: raytraverse.renderer.Rtrace initialized renderer instance (with scene loaded, no sources) accuracy: float, optional parameter to set threshold at sampling level relative to final level threshold (smaller number will increase sampling, default is 1.0) nlev: int, optional number of levels to sample jitter: bool, optional jitter samples ptkwargs: dict, optional kwargs for raytraveerse.sampler.SunSamplerPt initialization areakwargs: dict, optional kwargs for raytravrse.sampler.SamplerArea initialization metricset: iterable, optional subset of samplerarea.metric set to use for sun detail calculation. """ #: upper bound for drawing from pdf ub = 8 def __init__(self, scene, engine, accuracy=1.0, nlev=3, jitter=True, ptkwargs=None, areakwargs=None, metricset=('avglum', 'loggcr'), t0=.05, t1=.125): super().__init__(scene, engine, accuracy, t0=t0, t1=t1, stype='sunpositions') self._gcrnorm = 8 if areakwargs is None: areakwargs = {} if ptkwargs is None: ptkwargs = {} areakwargs.update(samplerlevel=self._slevel + 1) self._areakwargs = dict(metricset=('avglum', 'loggcr', 'xpeak', 'ypeak'), featurefunc=np.max, edgemode=-self.t0) self._areakwargs.update(areakwargs) self._ptkwargs = ptkwargs self.nlev = nlev self.jitter = jitter self._metidx = [i for i, j in enumerate(self._areakwargs['metricset']) if j in metricset] if len(self._metidx) != len(metricset): raise ValueError(f"bad argument metricset={metricset},all items " f"must be in {self._areakwargs['metricset']}") # initialize runtime variables: #: extra variables since sampler also needs to track each areasampler self._areadraws = None self._areadetail = None self._areaweights = None #: the LightPlaneKD of the sky sourcee used as a specular sampling guide #: in each sunsamplerpt (set in self.run()) self._specguide = None #: raytraverse.mapper.SkyMapper (set in self.run()) self._skymapper = None #: raytraverse.mapper.PlanMapper (set in self.run()) self._areamapper = None self._mask = slice(None) self._candidates = None self.slices = [] self._recovery_data = None
[docs] def sampling_scheme(self, mapper): """calculate sampling scheme""" return np.array([mapper.shape(i) for i in range(self.nlev)])
[docs] def get_existing_run(self, skymapper, areamapper): """check for file conflicts before running/overwriting parameters match call to run Parameters ---------- skymapper: raytraverse.mapper.SkyMapper the mapping for drawing suns areamapper: raytraverse.mapper.PlanMapper the mapping for drawing points Returns ------- conflicts: tuple a tuple of found conflicts (None for each if no conflicts: - suns: np.array of sun positions in vfile - ptfiles: existing point files """ vfile = (f"{self.scene.outdir}/{areamapper.name}/{skymapper.name}_" f"{self.stype}.tsv") try: suns = np.atleast_2d(io.load_txt(vfile)) except FileNotFoundError: suns = None except ValueError: suns = None ptfiles = sglob(f"{self.scene.outdir}/{areamapper.name}/" f"{skymapper.name}_sun*points.tsv") return suns, ptfiles
[docs] def run(self, skymapper, areamapper, specguide=None, recover=True, **kwargs): """adaptively sample sun positions for an area (also adaptively sampled) Parameters ---------- skymapper: raytraverse.mapper.SkyMapper the mapping for drawing suns areamapper: raytraverse.mapper.PlanMapper the mapping for drawing points specguide: Union[raytraverse.lightfield.LightPlaneKD, Bool] sky source lightfield to use as specular guide for sampling recover: continue run on top of existing files, if false, overwrites previous run. kwargs: passed to self.run() Returns ------- raytraverse.lightlplane.LightPlaneKD """ sun_conflict, pt_conflict = self.get_existing_run(skymapper, areamapper) if sun_conflict is not None and recover: self._recovery_data = sun_conflict, pt_conflict else: self._recovery_data = None try_mkdir(f"{self.scene.outdir}/{areamapper.name}") # reset/initialize runtime properties self._skymapper = skymapper self._areamapper = areamapper reflf = f"{self.scene.outdir}/{areamapper.name}/reflection_normals.txt" if specguide is True: self._specguide = reflf else: self._specguide = specguide self._areaweights = None levels = self.sampling_scheme(skymapper) super().run(skymapper, areamapper.name, levels, **kwargs) return SunsPlaneKD(self.scene, self.idxvecs(), self._areamapper, f"{self._skymapper.name}_sun")
def _init4run(self, levels, **kwargs): """(re)initialize object for new run, ensuring properties are cleared prior to executing sampling loop""" leveliter = super()._init4run(levels) self.slices = [] if self._recovery_data is None: return leveliter levels2run = [] suns = self._recovery_data[0] pts = self._recovery_data[1] pt_cnt = len(pts) for i in leveliter: levelsuns = suns[suns[:, 0] == i] if pt_cnt > 0: self._recover_level(levelsuns[:, 2:], i, **kwargs) pt_cnt -= len(levelsuns) else: levels2run.append(i) return levels2run def _recover_level(self, vecs, i, plotp=False, pfish=True): """use existing drawn sun positions to rerun, trying to load LightFields before running""" mapper = self._skymapper name = self._areamapper.name shape = self.levels[i] uv = self._skymapper.xyz2uv(vecs) idx = self._skymapper.uv2idx(uv, shape) candidates = self._skymapper.solar_grid_uv(jitter=self.jitter, level=i, masked=False) candidates[idx] = uv self._candidates = candidates self._lift_weights(i) draws, p = self.draw(i) si, uv = self.sample_to_uv(draws, shape) if si.size > 0: srate = si.shape[1]/np.prod(shape) row = (f"{i + 1} of {self.levels.shape[0]}\t" f"{str(shape): >11}\t{si.shape[1]: >7}\t" f"{srate: >7.02%}") self.scene.log(self, row, True, level=self._slevel) if plotp: self._plot_p(p, i, mapper, name, fisheye=pfish) self._plot_vecs(vecs, i, mapper, name, fisheye=pfish) lum = self.sample(vecs) self._update_weights(si, lum) if plotp: self._plot_weights(i, mapper, name, fisheye=pfish)
[docs] def draw(self, level): """draw on condition of in_solarbounds from skymapper. In this way all solar positions are presented to the area sampler, but the area sampler is initialized with a weighting to sample only where there is variance between sun position. this keeps the subsampling of area and solar position independent. Returns ------- pdraws: np.array index array of flattened samples chosen to sample at next level p: np.array computed probabilities """ dres = self.levels[level] # draw all sun positions within solar bounds pdraws = np.arange(int(np.prod(dres)))[self._mask] p = np.ones(dres).ravel() p[np.logical_not(self._mask)] = 0 if level > 0: # calculate detail over points across sun positions nweights = self._normed_weights() emode = 'reflect' if level < 2: emode = 'constant' det = draw.get_detail(nweights, *filterdict[self.detailfunc], mode=emode) mfunc = self._areakwargs['featurefunc'] det = np.transpose(det.reshape(self._areaweights.shape), (3, 4, 0, 1, 2)) det = mfunc(det, axis=2) self._areadetail = det adetail = det.reshape(-1, *self.lum.shape[-2:])[pdraws] adraws = [] drawmask = [] # draw on each plan and store plan candidates in areadraws for # MaskedPlanMapper construction (in self.sample()) for ad in adetail: adr = draw.from_pdf(ad.ravel(), self._threshold(level), lb=self.lb, ub=self.ub) if len(adr) > 0: auv = self._areamapper.idx2uv(adr, self.lum.shape[-2:], jitter=False) adraws.append(self._areamapper.uv2xyz(auv)) drawmask.append(True) else: drawmask.append(False) self._areadraws = adraws # filter no detail plans pdraws = pdraws[drawmask] else: self._areadraws = [None] * len(pdraws) return pdraws, p
[docs] def sample_to_uv(self, pdraws, shape): """generate samples vectors from flat draw indices Parameters ---------- pdraws: np.array flat index positions of samples to generate shape: tuple shape of level samples Returns ------- si: np.array index array of draws matching samps.shape vecs: np.array sample vectors """ if len(pdraws) == 0: return np.empty(0), np.empty(0) si = np.stack(np.unravel_index(pdraws, shape)) uv = self._candidates[pdraws] self._candidates = None return si, uv
def _sample_sun(self, suni, sunpos, adraws): """this function is for calling with a process pool, by declaring the sun sampler point after the child process is forked the call to engine.load_source happens on an isolated memory instance, allowing for concurrency on different scenes, despite the singleton/ global namespace issues of the cRtrace instance.""" sunsamp = SunSamplerPt(self.scene, self.engine, sunpos, suni, stype=f"{self._skymapper.name}_sun", **self._ptkwargs) areasampler = SamplerArea(self.scene, sunsamp, **self._areakwargs) if adraws is not None: amapper = MaskedPlanMapper(self._areamapper, adraws, 0) else: amapper = self._areamapper needs_sampling = True if self._recovery_data is not None: try: src = f"{self._skymapper.name}_sun_{suni:04d}" pts = (f"{self.scene.outdir}/{self._areamapper.name}/" f"{src}_points.tsv") lf = LightPlaneKD(self.scene, pts, self._areamapper, src) metrics = self._areakwargs['metricset'] try: mc = self._areakwargs['metricclass'] except KeyError: mc = SamplingMetrics areasampler.lum = lf.evaluate(1, metrics=metrics, metricclass=mc, gcrnorm=self._gcrnorm) shp = (*areasampler.sampling_scheme(amapper)[-1], areasampler.features) except (ValueError, OSError): pass else: needs_sampling = False if needs_sampling: lf = areasampler.run(amapper, specguide=self._specguide, log=False) shp = (*areasampler.levels[-1], areasampler.features) # build weights based on final sampling candidates = amapper.point_grid_uv(jitter=False, level=areasampler.nlev - 1, masked=False) mask = amapper.in_view_uv(candidates, False) wvecs = amapper.uv2xyz(candidates) idx, d = lf.query(wvecs) weights = areasampler.lum[idx].reshape(shp) weights = np.moveaxis(weights, 2, 0) weights = weights[self._metidx] for w in weights: w.flat[np.logical_not(mask)] = -1 return weights
[docs] def sample(self, vecs): """call rendering engine to sample rays Parameters ---------- vecs: np.array sample vectors Returns ------- lum: np.array array of shape (N,) to update weights """ self._dump_vecs(vecs) idx = self.slices[-1].indices(self.slices[-1].stop) level_desc = f"Level {len(self.slices)} of {len(self.levels)}" lums = self._run_sample(idx, vecs, level_desc) # initialize areaweights here now that we know the shape if len(self.lum) == 0: self.lum = lums self._areaweights = np.zeros((*self.lum.shape[1:], *self._wshape(0))) else: self.lum = np.concatenate((self.lum, lums), 0) return lums
def _run_sample(self, idx, vecs, level_desc): # if engine is configured for internal multiprocessing run on one # process, else use environment cap. Generally, if -ab > 0 then run # with rtrace -n X for better memory efficiency, else, use processpool # cap if self.engine.nproc is None or self.engine.nproc > 1: cap = 1 else: cap = None lums = pool_call(self._sample_sun, list(zip(range(*idx), vecs, self._areadraws)), desc=level_desc, cap=cap, pbar=self.scene.dolog) return np.array(lums) def _update_weights(self, si, lum): """only need to update areaweights, base weights are only masked""" lum = np.transpose(lum, (1, 2, 3, 0)) update = self._areaweights[..., si[0], si[1]] self._areaweights[..., si[0], si[1]] = np.where(lum > 0, lum, update) def _lift_weights(self, i): if self._candidates is None: self._candidates = self._skymapper.solar_grid_uv(jitter=self.jitter, level=i, masked=False) shape = self._skymapper.shape(i) mask = self._skymapper.in_solarbounds_uv(self._candidates, level=i).reshape(shape) if self.vecs is not None: ij = translate.uv2ij(self._skymapper.xyz2uv(self.vecs), shape[0], 1).T mask[ij[0], ij[1]] = False self._mask = mask.ravel() # at the level of the skysampler, draw all suns, detail happens # at the level of the area sampler self.weights = np.ones(self._wshape(i)) if i > 0: shp = (*self.lum.shape[1:], *self.weights.shape) self._areaweights = translate.resample(self._areaweights, shp) def _normed_weights(self): normi = [i for i, j in enumerate(self._areakwargs['metricset']) if 'lum' in j] nweights = np.copy(self._areaweights) for i in normi: nmin = np.min(nweights[i]) norm = np.max(nweights[i]) - nmin if norm > 0: nweights[i] = (nweights[i] - nmin)/norm return nweights def _dump_vecs(self, vecs): if self.vecs is None: self.vecs = vecs v0 = 0 else: self.vecs = np.concatenate((self.vecs, vecs)) v0 = self.slices[-1].stop self.slices.append(slice(v0, v0 + len(vecs))) vfile = (f"{self.scene.outdir}/{self._areamapper.name}/" f"{self._skymapper.name}_{self.stype}.tsv") # file format: level idx sx sy sz np.savetxt(vfile, self.idxvecs(), ("%d", "%d", "%.4f", "%.4f", "%.4f"))
[docs] def idxvecs(self): idx = np.arange(len(self.vecs))[:, None] level = np.zeros_like(idx, dtype=int) for sl in self.slices[1:]: level[sl.start:] += 1 return np.hstack((level, idx, self.vecs))
@staticmethod def _plot_dist(ps, vm, outf, fisheye=True): outshape = (512*vm.aspect, 512) res = outshape[-1] if fisheye: pixelxyz = vm.pixelrays(res) uv = vm.xyz2uv(pixelxyz.reshape(-1, 3)) pdirs = np.concatenate((pixelxyz[0:res], -pixelxyz[res:]), 0) mask = vm.in_view(pdirs, indices=False).reshape(outshape) ij = translate.uv2ij(uv, ps.shape[-1], aspect=vm.aspect) img = ps[ij[:, 0], ij[:, 1]].reshape(outshape) io.array2hdr(np.where(mask, img, 0), outf) else: detail = translate.resample(ps[-1::-1], outshape, radius=0, gauss=False) if vm.aspect == 2: detail = np.concatenate((detail[res:], detail[0:res]), 0) io.array2hdr(detail, outf) def _plot_p(self, p, level, vm, name, suffix=".hdr", fisheye=True, **kwargs): ps = p.reshape(self.weights.shape) outp = (f"{self.scene.outdir}_{name}_{self.stype}_detail_" f"{level:02d}{suffix}") self._plot_dist(ps, vm, outp, fisheye) if self._areadetail is not None: outp = (f"{self.scene.outdir}_{name}_{self.stype}_areadetail_" f"{level:02d}{suffix}") self._plot_dist(np.max(self._areadetail, axis=(2, 3)), vm, outp, fisheye) def _plot_weights(self, level, vm, name, suffix=".hdr", fisheye=True, **kwargs): if self._areaweights is not None: normw = self._normed_weights() for m, w in zip(self._areakwargs['metricset'], normw): outp = (f"{self.scene.outdir}_{name}_{self.stype}_{m}" f"_{level:02d}{suffix}") self._plot_dist(np.max(w, axis=(0, 1)), vm, outp, fisheye) def _plot_vecs(self, vecs, level, vm, name, suffix=".hdr", fisheye=True, **kwargs): outv = (f"{self.scene.outdir}_{name}_{self.stype}_samples_" f"{level:02d}{suffix}") self._skymapper.plot(vecs, outv, res=512, grow=2, fisheye=fisheye)