22import traitlets
33import pythreejs
44import numpy as np
5+ import functools
56import pvlib
67# from IPython.core.display import display
78
1314from pyembree import rtcore_scene as rtcs
1415from pyembree .mesh_construction import TriangleMesh
1516
17+ cached_property = getattr (functools , "cached_property" , property )
18+
1619
1720class Scene (traitlets .HasTraits ):
18-
19- ground = traittypes .Array (np .array ([0.0 , 0.0 , 0.0 ], "f4" )).valid (check_dtype ("f4" ), check_shape (3 ))
21+
22+ ground = traittypes .Array (np .array ([0.0 , 0.0 , 0.0 ], "f4" )).valid (
23+ check_dtype ("f4" ), check_shape (3 ))
2024 up = traittypes .Array (np .array ([0.0 , 0.0 , 1.0 ], "f4" )).valid (
2125 check_dtype ("f4" ), check_shape (3 ))
2226 north = traittypes .Array (np .array ([0.0 , 1.0 , 0.0 ], "f4" )).valid (
@@ -29,9 +33,14 @@ class Scene(traitlets.HasTraits):
2933 # TODO: Add surface for ground so that reflection from ground
3034 # is taken into account
3135
36+ def post_cast (self , output ):
37+ return output
38+
3239 def add_component (self , component ):
33- self .components = self .components + [component ] # Force traitlet update
34- self .meshes .append (TriangleMesh (self .embree_scene , component .triangles ))
40+ # Force traitlet update
41+ self .components = self .components + [component ]
42+ self .meshes .append (
43+ TriangleMesh (self .embree_scene , component .triangles ))
3544
3645 def compute_hit_count (self , blaster ):
3746 output = blaster .compute_count (self )
@@ -43,6 +52,30 @@ def compute_hit_count(self, blaster):
4352 )
4453 return component_counts
4554
55+ @property
56+ def transmittance (self ):
57+ return [c .transmittance for c in self .components ]
58+
59+ @property
60+ def reflectance (self ):
61+ return [c .reflectance for c in self .components ]
62+
63+ @cached_property
64+ def limits (self ):
65+ r"""np.ndarray: Positions of corners of a box containing all
66+ points in the scene."""
67+ mins = []
68+ maxs = []
69+ for c in self .components :
70+ mins .append (np .min (c .vertices , axis = 0 ))
71+ maxs .append (np .max (c .vertices , axis = 0 ))
72+ mins = np .min (np .vstack (mins ), axis = 0 )
73+ maxs = np .max (np .vstack (maxs ), axis = 0 )
74+ limits = np .vstack ([mins , maxs ])
75+ xx , yy , zz = np .meshgrid (limits [:, 0 ], limits [:, 1 ], limits [:, 2 ])
76+ limits = np .vstack ([xx .flatten (), yy .flatten (), zz .flatten ()]).T
77+ return limits
78+
4679 def get_sun_blaster (self , latitude , longitude , date ,
4780 direct_ppfd = 1.0 , diffuse_ppfd = 1.0 , ** kwargs ):
4881 r"""Get a sun blaster that is adjusted for this scene so that
@@ -72,23 +105,14 @@ def get_sun_blaster(self, latitude, longitude, date,
72105 # TODO: Calculate direct/diffuse ppfd from lat/long/date
73106 # using pvi if not provided
74107 max_distance2 = 0.0
75- mins = []
76- maxs = []
77108 for c in self .components :
78- mins .append (np .min (c .vertices , axis = 0 ))
79- maxs .append (np .max (c .vertices , axis = 0 ))
80109 max_distance2 = max (
81110 max_distance2 ,
82111 np .max (np .sum ((c .vertices - self .ground )** 2 , axis = 1 )))
83- mins = np .min (np .vstack (mins ), axis = 0 )
84- maxs = np .max (np .vstack (maxs ), axis = 0 )
85- limits = np .vstack ([mins , maxs ])
86- xx , yy , zz = np .meshgrid (limits [:, 0 ], limits [:, 1 ], limits [:, 2 ])
87- limits = np .vstack ([xx .flatten (), yy .flatten (), zz .flatten ()]).T
88112 max_distance = np .sqrt (max_distance2 )
89113 kwargs .setdefault ('zenith' , self .up * max_distance + self .ground )
90114 kwargs .setdefault ('diffuse_intensity' , diffuse_ppfd )
91- kwargs .setdefault ('scene_limits' , limits )
115+ kwargs .setdefault ('scene_limits' , self . limits )
92116 blaster = SunRayBlaster (latitude = latitude ,
93117 longitude = longitude , date = date ,
94118 ground = self .ground , north = self .north ,
@@ -143,7 +167,7 @@ def update(frame):
143167 o [o <= 0 ] = np .nan
144168 img .set_data (o .reshape ((camera .ny , camera .nx ), order = 'F' ))
145169 return img ,
146-
170+
147171 dates = pd .date_range (t_start , t_stop , periods = n_step )
148172 ani = FuncAnimation (fig , update , frames = list (dates ), blit = False )
149173 if fname is None :
@@ -226,14 +250,15 @@ def _calc_incident_power(self, ray_dir, norm, area, any_direction=True):
226250 if any_direction :
227251 aoi [aoi > np .pi / 2 ] -= np .pi
228252 else :
229- aoi [aoi > np .pi / 2 ] = np .pi # No contribution
253+ aoi [aoi > np .pi / 2 ] = np .pi / 2 # No contribution
230254 else :
231255 if aoi > np .pi / 2 :
232256 if any_direction :
233257 aoi -= np .pi
234258 else :
235- aoi = np .pi
236- return np .cos (aoi ) / area
259+ aoi = np .pi / 2
260+ out = np .cos (aoi ) / area
261+ return out
237262
238263 def _accumulate_hits (self , component_fd , primID , geomID ,
239264 ray_dir , ray_intensity , diffuse_intensity ,
@@ -270,13 +295,17 @@ def _accumulate_hits(self, component_fd, primID, geomID,
270295 tilt = np .arccos (
271296 np .dot (norms , self .up )
272297 / (2.0 * areas * np .linalg .norm (self .up )))
273- component_fd [ ci ] + = pvlib .irradiance .isotropic (
298+ component_diffuse = pvlib .irradiance .isotropic (
274299 np .degrees (tilt ), diffuse_intensity )
300+ component_fd [ci ] += component_diffuse
301+ # assert not any(component_fd[ci] == 0)
275302
276303 def _ipython_display_ (self ):
277- # This needs to actually display, which is not the same as returning a display.
304+ # This needs to actually display, which is not the same as
305+ # returning a display.
278306 cam = pythreejs .PerspectiveCamera (
279- position = [25 , 35 , 100 ], fov = 20 , children = [pythreejs .AmbientLight ()],
307+ position = [25 , 35 , 100 ], fov = 20 ,
308+ children = [pythreejs .AmbientLight ()],
280309 )
281310 children = [cam , pythreejs .AmbientLight (color = "#dddddd" )]
282311 material = pythreejs .MeshBasicMaterial (
@@ -301,3 +330,102 @@ def _ipython_display_(self):
301330 )
302331
303332 return rendererCube
333+
334+
335+ class PeriodicScene (Scene ):
336+
337+ period = traittypes .Array (
338+ np .zeros ((3 ,), "f4" )
339+ ).valid (check_dtype ("f4" ), check_shape (3 ))
340+ direction = traittypes .Array (np .array ([
341+ [1.0 , 0.0 , 0.0 ], [0.0 , 1.0 , 0.0 ], [0.0 , 0.0 , 1.0 ]
342+ ], "f4" )).valid (check_dtype ("f4" ), check_shape (3 , 3 ))
343+ count = traittypes .Array (np .array ([1 , 1 , 1 ], "i4" )).valid (
344+ check_dtype ("i4" ), check_shape (3 ))
345+ buffer_as_primary = traitlets .Bool (False )
346+
347+ def __init__ (self , * args , ** kwargs ):
348+ self ._buffer_meshes = []
349+ self ._buffer_component_map = {}
350+ self ._buffer_component_triangles = []
351+ self ._embree_scene = kwargs .get ('embree_scene' ,
352+ rtcs .EmbreeScene ())
353+ super (PeriodicScene , self ).__init__ (* args , ** kwargs )
354+
355+ @property
356+ def transmittance (self ):
357+ out = super (PeriodicScene , self ).transmittance
358+ self .add_component_buffers ()
359+ for i , v in self ._buffer_component_map .items ():
360+ out += [self .components [i ].transmittance for _ in v ]
361+ return out
362+
363+ @property
364+ def reflectance (self ):
365+ out = super (PeriodicScene , self ).reflectance
366+ self .add_component_buffers ()
367+ for i , v in self ._buffer_component_map .items ():
368+ out += [self .components [i ].reflectance for _ in v ]
369+ return out
370+
371+ @classmethod
372+ def get_periodic_shifts (cls , period , direction , count ):
373+ import itertools
374+ shifts = []
375+ opts = []
376+ for axis in range (3 ):
377+ if period [axis ] == 0 :
378+ opts .append ([0 ])
379+ continue
380+ else :
381+ opts .append (list (range (- count [axis ], count [axis ] + 1 )))
382+ for xbuffer , ybuffer , zbuffer in itertools .product (* opts ):
383+ if xbuffer == 0 and ybuffer == 0 and zbuffer == 0 :
384+ continue
385+
386+ def _shift (ibuffer , axis ):
387+ return ibuffer * period [axis ] * direction [axis , :]
388+
389+ shifts .append (_shift (xbuffer , 0 )
390+ + _shift (ybuffer , 1 )
391+ + _shift (zbuffer , 2 ))
392+ return np .vstack (shifts )
393+
394+ def post_cast (self , output ):
395+ if self .buffer_as_primary :
396+ for ci , v in self ._buffer_component_map .items ():
397+ for ci_per in v :
398+ output ["geomID" ][output ["geomID" ] == ci_per ] = ci
399+ for irange , jrange in self ._buffer_component_triangles [::- 1 ]:
400+ output ["primID" ][output ["primID" ] >= jrange .start ] -= (
401+ jrange .start - irange .start )
402+ return output
403+
404+ def add_component_buffers (self ):
405+ if self ._buffer_meshes :
406+ return
407+ shifts = self .get_periodic_shifts (self .period , self .direction ,
408+ self .count )
409+ j = len (self .components )
410+ icount = 0
411+ jcount = sum ([component .triangles .shape [0 ]
412+ for component in self .components ])
413+ for i , component in enumerate (self .components ):
414+ irange = range (icount , icount + component .triangles .shape [0 ])
415+ self ._buffer_component_map [i ] = []
416+ for shift in shifts :
417+ jrange = range (jcount , jcount + component .triangles .shape [0 ])
418+ self ._buffer_meshes .append (
419+ TriangleMesh (self ._embree_scene ,
420+ component .triangles + shift )
421+ )
422+ self ._buffer_component_map [i ].append (j )
423+ self ._buffer_component_triangles .append ((irange , jrange ))
424+ j += 1
425+ jcount += component .triangles .shape [0 ]
426+ icount += component .triangles .shape [0 ]
427+
428+ @property
429+ def embree_scene (self ):
430+ self .add_component_buffers ()
431+ return self ._embree_scene
0 commit comments