class RatpScene(object): """ High level class interface for RATP lighting model """ localisation_db = sample_database() timezone = 0 # consider UTC/GMT time for date inputs idecaly = 0 # regular grid nitrogen = 2 # dummy value (g.m-2) for nitrogen per leaf area def __init__(self, scene=None, grid=None, entities=None, rleaf=0.15, rsoil=0.2, nbinclin=9, orientation=0, localisation='Montpellier', scene_unit='m', mu=None, distinc=None, **grid_kwds): """ Initialise a RatpScene. Arguments: scene: a pyratp.interface.surfacic_point_cloud or a plantgl.Scene or a {shape_id: vertices, faces} scene mesh dict encoding the 3D scene grid: a pyratp.interface.smart_grid instance encoding the voxel grid. If None (default), the grid is adjusted to fit the input scene. entities: a {shape_id: entity} dict defining entities in the scene. If None (default), all shapes are considered as members of the same entity rleaf: leaf reflectance or a {entity: leaf_reflectance} property dict. rsoil: soil reflectance nbinclin: the number of angular classes for leaf angle distribution orientation (float): the angle (deg, positive clockwise) from X+ to North (default: 0) localisation : a string referencing a city of the localisation database (class variable), or a dict{'longitude':longitude, 'latitude':latitude} scene_unit: a string indicating unit used for scene coordinates mu : a list of clumping indices, indexed by density.If None (default) clumping is automatically evaluated using clark-evans method. distinc: a list of list giving the frequency of the nbinclin leaf angles classes (from horizontal to vertical) per entity in the canopy. If None (default), distinc is computed automatically from the scene Other named args to this function are used for controlling grid shape when grid=None """ # scene_box will be used to construct grid, if grid is None scene_box = ((0, 0, 0), (1, 1, 1)) if scene is None: scene = {'plant': unit_square_mesh()} if isinstance(scene, SurfacicPointCloud): self.scene = scene self.scene_mesh = scene.as_scene_mesh() if grid is None: scene_box = scene.bbox() elif is_pgl_scene(scene): self.scene_mesh = pgls.as_scene_mesh(scene) self.scene = SurfacicPointCloud.from_scene_mesh( self.scene_mesh, scene_unit=scene_unit) if grid is None: scene_box = pgls.bbox(scene, scene_unit) else: try: self.scene_mesh = scene self.scene = SurfacicPointCloud.from_scene_mesh( scene, scene_unit=scene_unit) if grid is None: vertices, faces = zip(*scene.values()) vertices = reduce(lambda pts, new: pts + list(new), vertices, []) x, y, z = zip(*vertices) scene_box = ((min(x), min(y), min(z)), (max(x), max(y), max(z))) except Exception as details: print details raise ValueError( "Unrecognised scene format: should be one of pgl.Scene, " "SurfacicPointCloud or {sh_id:(vertices, faces)} dict") if grid is None: self.smart_grid = SmartGrid(scene_box=scene_box, **grid_kwds) elif isinstance(grid, SmartGrid): self.smart_grid = grid else: raise ValueError('Unrecognised grid format: should be None or a ' 'SmartGrid instance') if entities is None: entities = {sh_id: 'default' for sh_id in self.scene_mesh} self.entities = entities # RATP entity code starts at 1 ent = list(set(self.entities.values())) self.entity_map = dict(zip(ent, range(1, len(ent) + 1))) self.entity_code = numpy.array( [self.entity_map[entities[sh]] for sh in self.scene.shape_id]) if not isinstance(rleaf, dict): # if not hasattr(rleaf, '__len__'): # rleaf = [rleaf] rleaf = {'default': rleaf} self.rleaf = {self.entity_map[k]: rleaf[k] for k in self.entity_map} if not hasattr(rsoil, '__len__'): rsoil = [rsoil] self.rsoil = rsoil self.orientation = orientation if not isinstance(localisation, dict): try: localisation = RatpScene.localisation_db[localisation] except KeyError: print 'Warning : localisation', localisation, \ 'not found in database, using default localisation', \ RatpScene.localisation_db.iter().next() localisation = RatpScene.localisation_db.itervalues().next() self.localisation = localisation if mu is None: mu = self.clumping() print(' '.join(['clumping evaluated:'] + [str(m) for m in mu])) self.set_clumping(mu) if distinc is None: distinc = self.inclination_distribution(nbinclin) self.set_inclin(distinc) self.ratp_grid, self.grid_indices = self.grid() def n_entities(self): """ return the number of distinct entities in the canopy""" return max(self.entity_code) def set_clumping(self, mu): nent = self.n_entities() if not isinstance(mu, Iterable): mu = [mu] * nent assert len(mu) == nent self.mu = mu def set_inclin(self, distinc): nent = self.n_entities() assert isinstance(distinc, Iterable) assert len(distinc) == nent nbinclin = len(distinc[0]) assert all(map(lambda x: len(x) == nbinclin, distinc)) self.distinc = distinc self.nbinclin = nbinclin def clumping(self, expand=True): spc = self.scene grid = self.smart_grid x, y, z = spc.x, spc.y, spc.z xv, yv, zv = grid.within_cell_position(x, y, z, normalise=True) jx, jy, jz = grid.grid_index(x, y, z) entity = self.entity_code data = pandas.DataFrame({ 'entity': entity, 'x': xv, 'y': yv, 'z': zv, 's': spc.area, 'n': spc.normals, 'jx': jx, 'jy': jy, 'jz': jz }) mu = [] domain = ((0, 0, 0), (1, 1, 1)) grouped = data.groupby('entity') for e, dfe in grouped: gvox = dfe.groupby(('jx', 'jy', 'jz')) clumps = [] for k, df in gvox: clumping = get_clumping(df['x'], df['y'], df['z'], df['s'], df['n'], domain=domain, expand=expand) min_mu = df['s'].mean() / df['s'].sum( ) # minimal mu in the case of perfect clumping if numpy.isfinite(clumping): clumps.append(max(min_mu, clumping)) mu.append(numpy.mean(clumps)) return mu def grid(self): """ Create and fill a RATP grid """ spc = self.scene grid_pars = { 'latitude': self.localisation['latitude'], 'longitude': self.localisation['longitude'], 'timezone': self.timezone, 'idecaly': self.idecaly, 'orientation': self.orientation, 'rs': self.rsoil, 'nent': self.n_entities() } grid_pars.update(self.smart_grid.ratp_grid_parameters()) ratp_grid = Grid.initialise(**grid_pars) grid_indices = self.smart_grid.grid_index(spc.x, spc.y, spc.z, check=True) jx, jy, jz = self.smart_grid.ratp_grid_index(*grid_indices) entity = self.entity_code - 1 # Grid.fill expect python indices nitrogen = [self.nitrogen] * spc.size ratp_grid, _ = Grid.fill_from_index(entity, jx, jy, jz, spc.area, nitrogen, ratp_grid) # RATPScene grid indices of individual surfacic points jx, jy, jz = grid_indices grid_indices = pandas.DataFrame({ 'point_id': range(len(jx)), 'jx': jx, 'jy': jy, 'jz': jz }) return ratp_grid, grid_indices def inclinations(self): df = self.scene.inclinations() df_ent = pandas.DataFrame({ 'shape_id': self.scene.shape_id, 'entity': self.entity_code }).drop_duplicates() df = df.merge(df_ent) return [ group['inclination'].tolist() for name, group in df.groupby('entity') ] def inclination_distribution(self, nbinclin=9): def _dist(inc): dist = numpy.histogram(inc, nbinclin, (0, 90))[0] return (dist.astype('float') / dist.sum()).tolist() inclinations = self.inclinations() distinc = map(_dist, inclinations) return distinc def voxel_index(self): """Mapping between RATP (filled) VoxelId and RatpScene grid indices""" grid = self.ratp_grid if grid is None: return pandas.DataFrame({}) nveg = grid.nveg if nveg == 0: return pandas.DataFrame({}) index = numpy.arange(1, nveg + 1) # RATP fortan indices numx, numy, numz = grid.numx[:nveg], grid.numy[:nveg], grid.numz[:nveg] # associated smart_grid indices jx, jy, jz = self.smart_grid.decode_ratp_indices( numx - 1, numy - 1, numz - 1) xc, yc, zc = self.smart_grid.voxel_centers(jx, jy, jz) return pandas.DataFrame({ 'VoxelId': index, 'jx': jx, 'jy': jy, 'jz': jz, 'xc': xc, 'yc': yc, 'zc': zc }) def do_irradiation(self, sun_sources=None, sky_sources=None, scattering_indicatrix=None, mu=None, distinc=None): """ Run a simulation of light interception for one wavelength Parameters: sun_sources: elevation (degrees), azimuth (degrees, from North positive clockwise), horizontal irradiance of sources representing the sun. sky_sources: elevation (degrees), azimuth (degrees, from North positive clockwise), horizontal irradiance of sources representing the sky. If both sun_sources and sky_sources are None, a 46 directions, normalised soc sky is used scattering_indicatrix: relative weights associated to sky_sources directions used to compute interception coefficient of scattered light. If None uniform contribution is used if sky_sources is not None or default RATP turtle 46 if sky_sources is None mu: clumping index of vegetation. If None, clumping index is estimated after modified clark evans index distinc: a list of list giving the frequency of the nbinclin leaf angles classes (from horizontal to vertical) per entity in the canopy.If None (default), distinc is computed automatically from the scene """ nent = self.n_entities() if mu is not None: self.set_clumping(mu) if distinc is not None: self.set_inclin(distinc) entities = [{ 'rf': [self.rleaf[i + 1]], 'distinc': self.distinc[i], 'mu': self.mu[i] } for i in range(nent)] vegetation = Vegetation.initialise(entities, nblomin=1) if sun_sources is None and sky_sources is None: sky = Skyvault.initialise() ghi = 1 else: hmoy, azmoy, omega, pc = [], [], [], [] if sun_sources is not None: el, az, irr = sun_sources hmoy.extend(el) azmoy.extend(az) pc.extend(irr) # sun directions are not used to estimate interception coeff of # scattered light 'rka' (mod_Shortwave_Balance.f90, line 142 + # mod_dir_interception.f90, line 112) omega.extend([0.] * len(irr)) if sky_sources is not None: el, az, irr = sky_sources hmoy.extend(el) azmoy.extend(az) pc.extend(irr) if scattering_indicatrix is None: omega.extend([1.] * len(irr)) else: omega.extend(scattering_indicatrix) else: hmoy.extend(elevations46) azmoy.extend(azimuths46) omega.extend(omegas_46) # only use for omega / rediff, not fo lighting pc.extend([0] * len(omegas_46)) # pc is used (mod_Hemi_Interception, line 124) to weight Rdif ghi = sum(pc) if ghi == 0: raise ValueError('Irradiance of sources is null!!!') pc = numpy.array(pc) / ghi # omega are Sr solid angles (mod_dir_interception.f90, line 112) omega = numpy.array(omega) omega /= omega.sum() omega *= 2 * numpy.pi # RATP sources azimuths are from South, positive clockwise # (mod_Shortwave_Balance, line 83) and orientation is not taken into # account for diffuse sources, unike RATP native sun sources # (mod_Shortwave_Balance.f90, line 136) azmoy = numpy.array(azmoy) - 180 + self.orientation sky = Skyvault.initialise(hmoy=hmoy, azmoy=azmoy, omega=omega, pc=pc) met = MicroMeteo.initialise(doy=1, hour=12, Rglob=ghi, Rdif=ghi) res = runRATP.DoIrradiation(self.ratp_grid, vegetation, sky, met) VegetationType, Iteration, day, hour, VoxelId, ShadedPAR, SunlitPAR, \ ShadedArea, SunlitArea = res.T # 'PAR' is expected in Watt.m-2 in RATP input, whereas output is in # micromol => convert back to W.m2 (cf shortwavebalance, line 306) dfvox = pandas.DataFrame({ 'VegetationType': VegetationType, 'Iteration': Iteration, 'day': day, 'hour': hour, 'VoxelId': VoxelId, 'ShadedPAR': ShadedPAR / 4.6, 'SunlitPAR': SunlitPAR / 4.6, 'ShadedArea': ShadedArea, 'SunlitArea': SunlitArea, 'Area': ShadedArea + SunlitArea, 'PAR': (ShadedPAR * ShadedArea + SunlitPAR * SunlitArea) / (ShadedArea + SunlitArea) / 4.6, }) return pandas.merge(dfvox, self.voxel_index()) def scene_lightmap(self, dfvox, spatial='point_id', temporal=True): """ Aggregate light outputs along spatial domain of scene input Args: dfvox: a pandas data frame with ratp outputs spatial: a string indicating the aggregation level: 'point_id' or 'shape_id' . temporal: should iterations be aggregated ? Returns: a pandas dataframe with aggregated outputs """ if spatial == 'point_id': scene_map = self.scene.area_map() else: scene_map = self.scene.shape_map().merge(self.scene.area_map()) grid_map = self.grid_indices return aggregate_light(dfvox, scene_map=scene_map, grid_map=grid_map, temporal=temporal) def xy_lightmap(self, dfvox): """ Aggregate light outputs along x y cells""" grid_map = self.grid_indices area_map = self.scene.area_map() return aggregate_grid(dfvox, grid_map, area_map) def plot(self, dfvox, by='point_id', minval=None, maxval=None): lmap = self.scene_lightmap(dfvox, spatial=by) if by == 'shape_id': df = lmap.loc[:, ('shape_id', 'PAR')].set_index('shape_id') prop = {k: df['PAR'][k] for k in df.index} else: # add shape_id lmap = lmap.merge(self.scene.shape_map()) lmap = lmap.sort_values('point_id') prop = { sh_id: df.to_dict('list')['PAR'] for sh_id, df in lmap.groupby('shape_id') } return display_property(self.scene_mesh, prop, minval=minval, maxval=maxval) def parameters(self): """return a dict of intantiation parameters""" ent = self.entities rl = self.rleaf if self.n_entities() == 1: ent = ent.itervalues().next() rl = rl.itervalues().next() if ent == 'default': ent = None d = { 'grid': self.smart_grid.as_dict(), 'entities': ent, 'rleaf': rl, 'rsoil': self.rsoil, 'nbinclin': self.nbinclin, 'orientation': self.orientation, 'localisation': self.localisation, 'mu': self.mu, 'distinc': self.distinc } return d @staticmethod def read_parameters(file_path): with open(file_path, 'r') as input_file: pars = json.load(input_file) return pars def save_parameters(self, file_path): saved = self.parameters() with open(file_path, 'w') as output_file: json.dump(saved, output_file, sort_keys=True, indent=4, separators=(',', ': ')) @staticmethod def load_ratp_scene(scene, parameters): # copy to avoid altering parameters pars = {k: v for k, v in parameters.iteritems()} grid = SmartGrid.from_dict(pars.pop('grid')) return RatpScene(scene, grid=grid, **pars)
def test_ratp_parameters(): grid = SmartGrid() pars = grid.ratp_grid_parameters() return pars
class RatpScene(object): """ High level class interface for RATP lighting model """ localisation_db = sample_database() timezone = 0 # consider UTC/GMT time for date inputs idecaly = 0 # regular grid nitrogen = 2 # dummy value (g.m-2) for nitrogen per leaf area def __init__(self, scene=None, grid=None, entities=None, rleaf=0.15, rsoil=0.2, orientation=0, localisation='Montpellier', scene_unit='m', **grid_kwds): """ Initialise a RatpScene. Arguments: scene: a pyratp.interface.surfacic_point_cloud or a plantgl.Scene or a {shape_id: vertices, faces} scene mesh dict encoding the 3D scene grid: a pyratp.interface.smart_grid instance encoding the voxel grid. If None (default), the grid is adjusted to fit the input scene. entities: a {shape_id: entity} dict defining entities in the scene. If None (default), all shapes are considered as members of the same entity rleaf: leaf reflectance or a {entity: leaf_reflectance} property dict. rsoil: soil reflectance or a list of soil reflectances in PAR and NIR orientation (float): the angle (deg, positive clockwise) from X+ to North (default: 0) localisation : a string referencing a city of the localisation database (class variable), or a dict{'longitude':longitude, 'latitude':latitude} scene_unit: a string indicating unit used for scene coordinates Other named args to this function are used for controlling grid shape when grid=None """ # scene_box will be used to construct grid, if grid is None scene_box = ((0, 0, 0), (1, 1, 1)) if scene is None: scene = {'plant': unit_square_mesh()} if isinstance(scene, SurfacicPointCloud): self.scene = scene self.scene_mesh = scene.as_scene_mesh() if grid is None: scene_box = scene.bbox() elif is_pgl_scene(scene): self.scene_mesh = pgls.as_scene_mesh(scene) self.scene = SurfacicPointCloud.from_scene_mesh( self.scene_mesh, scene_unit=scene_unit) if grid is None: scene_box = pgls.bbox(scene, scene_unit) else: try: self.scene_mesh = scene self.scene = SurfacicPointCloud.from_scene_mesh( scene, scene_unit=scene_unit) if grid is None: vertices, faces = zip(*scene.values()) vertices = reduce(lambda pts, new: pts + list(new), vertices, []) x, y, z = zip(*vertices) scene_box = ((min(x), min(y), min(z)), (max(x), max(y), max(z))) except Exception as details: print details raise ValueError( "Unrecognised scene format: should be one of pgl.Scene, " "SurfacicPointCloud or {sh_id:(vertices, faces)} dict") if grid is None: self.smart_grid = SmartGrid(scene_box=scene_box, **grid_kwds) elif isinstance(grid, SmartGrid): self.smart_grid = grid else: raise ValueError('Unrecognised grid format: should be None or a ' 'SmartGrid instance') if entities is None: entities = {sh_id: 'default' for sh_id in self.scene_mesh} self.entities = entities # RATP entity code starts at 1 ent = list(set(self.entities.values())) self.entity_map = dict(zip(ent, range(1, len(ent) + 1))) self.entity_code = numpy.array( [self.entity_map[entities[sh]] for sh in self.scene.shape_id]) if not isinstance(rleaf, dict): # if not hasattr(rleaf, '__len__'): # rleaf = [rleaf] rleaf = {'default': rleaf} self.rleaf = {self.entity_map[k]: rleaf[k] for k in self.entity_map} if not hasattr(rsoil, '__len__'): rsoil = [rsoil] self.rsoil = rsoil self.orientation = orientation if not isinstance(localisation, dict): try: self.localisation = RatpScene.localisation_db[localisation] except KeyError: print 'Warning : localisation', localisation, \ 'not found in database, using default localisation', \ RatpScene.localisation_db.iter().next() self.localisation = RatpScene.localisation_db.itervalues( ).next() # self.distinc = None self.nbinclin = 0 self.mu = None self.ratp_grid = None self.grid_indices = None def n_entities(self): """ return the number of distinct entities in the canopy""" return max(self.entity_code) def clumping(self): if self.mu is None: spc = self.scene grid = self.smart_grid x, y, z = spc.x, spc.y, spc.z xv, yv, zv = grid.within_cell_position(x, y, z, normalise=True) jx, jy, jz = grid.grid_index(x, y, z) entity = self.entity_code data = pandas.DataFrame({ 'entity': entity, 'x': xv, 'y': yv, 'z': zv, 's': spc.area, 'n': spc.normals, 'jx': jx, 'jy': jy, 'jz': jz }) mu = [] domain = ((0, 0, 0), (1, 1, 1)) grouped = data.groupby('entity') for e, dfe in grouped: gvox = dfe.groupby(('jx', 'jy', 'jz')) clumps = [] for k, df in gvox: clumping = get_clumping(df['x'], df['y'], df['z'], df['s'], df['n'], domain=domain) min_mu = df['s'].mean() / df['s'].sum( ) # minimal mu in the case of perfect clumping clumps.append(max(min_mu, clumping)) mu.append(numpy.mean(clumps)) self.mu = mu return self.mu def grid(self, rsoil=0.2): """ Create and fill a RATP grid :Parameters: - rsoil : soil reflectances """ if not hasattr(rsoil, '__len__'): rsoil = [rsoil] if self.ratp_grid is None or rsoil != self.rsoil: self.rsoil = rsoil spc = self.scene grid_pars = { 'latitude': self.localisation['latitude'], 'longitude': self.localisation['longitude'], 'timezone': self.timezone, 'idecaly': self.idecaly, 'orientation': self.orientation, 'rs': self.rsoil, 'nent': self.n_entities() } grid_pars.update(self.smart_grid.ratp_grid_parameters()) ratp_grid = Grid.initialise(**grid_pars) grid_indices = self.smart_grid.grid_index(spc.x, spc.y, spc.z, check=True) jx, jy, jz = self.smart_grid.ratp_grid_index(*grid_indices) entity = self.entity_code - 1 # Grid.fill expect python indices nitrogen = [self.nitrogen] * spc.size ratp_grid, _ = Grid.fill_from_index(entity, jx, jy, jz, spc.area, nitrogen, ratp_grid) # RATPScene grid indices of individual surfacic points jx, jy, jz = grid_indices self.grid_indices = pandas.DataFrame({ 'point_id': range(len(jx)), 'jx': jx, 'jy': jy, 'jz': jz }) self.ratp_grid = ratp_grid return self.ratp_grid def inclinations(self): df = self.scene.inclinations() df_ent = pandas.DataFrame({ 'shape_id': self.scene.shape_id, 'entity': self.entity_code }).drop_duplicates() df = df.merge(df_ent) return [ group['inclination'].tolist() for name, group in df.groupby('entity') ] def inclination_distribution(self, nbinclin=9): if self.distinc is None or self.nbinclin != nbinclin: def _dist(inc): dist = numpy.histogram(inc, self.nbinclin, (0, 90))[0] return dist.astype('float') / dist.sum() self.nbinclin = nbinclin inclinations = self.inclinations() self.distinc = map(_dist, inclinations) return self.distinc def voxel_index(self): """Mapping between RATP (filled) VoxelId and RatpScene grid indices""" grid = self.ratp_grid if grid is None: return pandas.DataFrame({}) nveg = grid.nveg if nveg == 0: return pandas.DataFrame({}) index = numpy.arange(1, nveg + 1) # RATP fortan indices numx, numy, numz = grid.numx[:nveg], grid.numy[:nveg], grid.numz[:nveg] # associated smart_grid indices jx, jy, jz = self.smart_grid.decode_ratp_indices( numx - 1, numy - 1, numz - 1) return pandas.DataFrame({ 'VoxelId': index, 'jx': jx, 'jy': jy, 'jz': jz }) def do_irradiation(self, rsoil=0.20, doy=1, hour=12, Rglob=1, Rdif=1, mu=None, sources=None, nbinclin=9): """ Run a simulation of light interception for one wavelength Parameters: - rleaf : list of leaf refectance per entity - rsoil : soil reflectance - doy : [list of] day of year [for the different iterations] - hour : [list of] decimal hour (0-24) [for the different iterations] - Rglob : [list of] global (direct + diffuse) radiation [for the different iterations] (W.m-2) - Rdif : [list of] direct/diffuse radiation ratio [for the different iterations] (0-1) - sources: a list of sequences giving elevation, azimuth, steradians and weights of sky vault. if None, default RATP soc skyvault is used """ nent = self.n_entities() if mu is None: mu = self.clumping() print(' '.join(['clumping evaluated:'] + [str(mu[i]) for i in range(nent)])) else: if not isinstance(mu, Iterable): mu = [mu] * nent inclins = self.inclination_distribution(nbinclin) entities = [{ 'rf': [self.rleaf[i + 1]], 'distinc': inclins[i], 'mu': mu[i] } for i in range(nent)] grid = self.grid(rsoil=rsoil) vegetation = Vegetation.initialise(entities, nblomin=1) if sources == None: sky = Skyvault.initialise() else: el, az, strd, w = sources sky = Skyvault.initialise(hmoy=el, azmoy=az, omega=strd, pc=w) met = MicroMeteo.initialise(doy=doy, hour=hour, Rglob=Rglob, Rdif=Rdif) res = runRATP.DoIrradiation(grid, vegetation, sky, met) VegetationType, Iteration, day, hour, VoxelId, ShadedPAR, SunlitPAR, \ ShadedArea, SunlitArea = res.T # 'PAR' is expected in Watt.m-2 in RATP input, whereas output is in # micromol => convert back to W.m2 (cf shortwavebalance, line 306) dfvox = pandas.DataFrame({ 'VegetationType': VegetationType, 'Iteration': Iteration, 'day': day, 'hour': hour, 'VoxelId': VoxelId, 'ShadedPAR': ShadedPAR / 4.6, 'SunlitPAR': SunlitPAR / 4.6, 'ShadedArea': ShadedArea, 'SunlitArea': SunlitArea, 'Area': ShadedArea + SunlitArea, 'PAR': (ShadedPAR * ShadedArea + SunlitPAR * SunlitArea) / (ShadedArea + SunlitArea) / 4.6, }) return pandas.merge(dfvox, self.voxel_index()) def scene_lightmap(self, dfvox, spatial='point_id', temporal=True): """ Aggregate light outputs along scene inputs Args: dfvox: a pandas data frame with ratp outputs spatial: a string indicating the aggregation level: 'point_id' or 'shape_id' . temporal: should iterations be aggregated ? Returns: a pandas dataframe with aggregated outputs """ dfmap = pandas.merge(self.scene.as_data_frame(), self.grid_indices) aggregated_area = dfmap.loc[:, ( spatial, 'area')].groupby(spatial).agg('sum').reset_index() aggregated_area = aggregated_area.rename(columns={'area': 'agg_area'}) output = pandas.merge(pandas.merge(dfmap, aggregated_area), dfvox) def _process(df): w = df['area'] / df['agg_area'] a_agg = df['agg_area'].values[0] res = pandas.Series({ 'VegetationType': df['VegetationType'].values[0], 'day': df['day'].values[0], 'hour': df['hour'].values[0], 'ShadedPAR': numpy.sum(df['ShadedPAR'] * w), # weighted mean of voxel values (weigth = primitive area) 'SunlitPAR': numpy.sum(df['SunlitPAR'] * w), 'ShadedArea': numpy.sum(df['ShadedArea'] / df['Area'] * w) * a_agg, # weighted mean of shaded fraction times shape_area 'SunlitArea': numpy.sum(df['SunlitArea'] / df['Area'] * w) * a_agg, 'Area': a_agg, 'PAR': numpy.sum(df['PAR'] * w) }) return res grouped = output.groupby(['Iteration', spatial]) res = grouped.apply(_process).reset_index() if temporal and len(set(res['Iteration'])) > 1: grouped = res.groupby(spatial) how = { 'VegetationType': numpy.mean, 'day': numpy.mean, 'hour': numpy.mean, 'ShadedPAR': numpy.sum, 'SunlitPAR': numpy.sum, 'ShadedArea': numpy.mean, 'SunlitArea': numpy.mean, 'Area': numpy.mean, 'PAR': numpy.sum } res = grouped.agg(how).reset_index() if spatial == 'point_id': res = res.merge(self.scene.shape_map()) return res def plot(self, dfvox, by='point_id', minval=None, maxval=None): lmap = self.scene_lightmap(dfvox, spatial=by) if by == 'shape_id': df = lmap.loc[:, ('shape_id', 'PAR')].set_index('shape_id') prop = {k: df['PAR'][k] for k in df.index} else: prop = { sh_id: df.to_dict('list')['PAR'] for sh_id, df in lmap.groupby('shape_id') } return display_property(self.scene_mesh, prop, minval=minval, maxval=maxval)