Skip to content

Commit 3002aaf

Browse files
committed
Added projection ray blaster & options for periodic blaster/scene
1 parent 556361c commit 3002aaf

File tree

2 files changed

+255
-25
lines changed

2 files changed

+255
-25
lines changed

hothouse/blaster.py

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,34 @@ class RayBlaster(traitlets.HasTraits):
3232
intensity = traitlets.CFloat(1.0)
3333
diffuse_intensity = traitlets.CFloat(0.0)
3434
multibounce = traitlets.CBool(False)
35+
period = traittypes.Array(
36+
np.zeros((3,), "f4")
37+
).valid(check_dtype("f4"), check_shape(3))
38+
periodic_direction = traittypes.Array(np.array([
39+
[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]
40+
], "f4")).valid(check_dtype("f4"), check_shape(3, 3))
41+
periodic_count = traittypes.Array(np.array([1, 1, 1], "i4")).valid(
42+
check_dtype("i4"), check_shape(3))
43+
44+
@classmethod
45+
def from_scene(cls, scene, **kwargs):
46+
r"""Create a blaster for a scene by setting missing parameters
47+
that optimize the coverage of the scene.
48+
49+
Args:
50+
scene (hothouse.scene.Scene): Scene to customize the blaster
51+
for.
52+
53+
Returns:
54+
RayBlaster: Created blaster.
55+
56+
"""
57+
raise NotImplementedError
58+
59+
@cached_property
60+
def is_periodic(self):
61+
r"""bool: True if the blaster will be replicated periodically."""
62+
return np.any(self.period > 0)
3563

3664
@property
3765
def ray_intensity(self):
@@ -43,20 +71,38 @@ def cast_once(self, scene, verbose_output=False,
4371
if self.multibounce:
4472
callback_handler = RayCollisionMultiBounce(
4573
self.origins.shape[0], 10,
46-
[c.transmittance for c in scene.components],
47-
[c.reflectance for c in scene.components])
74+
scene.transmittance, scene.reflectance)
4875
else:
4976
callback_handler = None
77+
dists = None
78+
if self.is_periodic:
79+
dists = np.empty(self.origins.shape[0], 'float32')
80+
dists.fill(1e37)
5081
output = scene.embree_scene.run(
5182
self.origins,
5283
self.directions,
84+
dists=dists,
5385
query=query_type._value_,
5486
output=verbose_output,
5587
callback_handler=callback_handler
5688
)
89+
if self.is_periodic:
90+
from hothouse.scene import PeriodicScene
91+
shifts = PeriodicScene.get_periodic_shifts(
92+
self.period, self.periodic_direction,
93+
self.periodic_count)
94+
for shift in shifts:
95+
output = scene.embree_scene.run(
96+
self.origins + shift,
97+
self.directions,
98+
dists=dists,
99+
query=query_type._value_,
100+
output=verbose_output,
101+
callback_handler=callback_handler
102+
)
57103
if self.multibounce and isinstance(output, dict):
58104
output['bounces'] = callback_handler.bounces
59-
return output
105+
return scene.post_cast(output)
60106

61107
def compute_distance(self, scene):
62108
output = self.cast_once(
@@ -159,6 +205,13 @@ class SunRayBlaster(OrthographicRayBlaster):
159205
scene_limits = traittypes.Array(None, allow_none=True).valid(
160206
check_shape(None, 3), check_dtype("f4"))
161207

208+
@classmethod
209+
def get_solar_direction(cls, latitude, longitude, date, up, north):
210+
instance = cls(latitude=latitude, longitude=longitude,
211+
date=date, zenith=(10 * up),
212+
ground=np.zeros((3,), "f4"), north=north)
213+
return instance.forward
214+
162215
@traitlets.default("_solpos_info")
163216
def _solpos_info_default(self):
164217
return pvlib.solarposition.get_solarposition(
@@ -286,7 +339,56 @@ def _up_default(self):
286339

287340

288341
class ProjectionRayBlaster(RayBlaster):
289-
pass
342+
fov_width = traitlets.CFloat(90.0)
343+
fov_height = traitlets.CFloat(90.0)
344+
center = traittypes.Array().valid(check_dtype("f4"), check_shape(3))
345+
forward = traittypes.Array().valid(check_dtype("f4"), check_shape(3))
346+
up = traittypes.Array().valid(check_dtype("f4"), check_shape(3))
347+
east = traittypes.Array().valid(check_dtype("f4"), check_shape(3))
348+
width = traitlets.CFloat(1.0)
349+
height = traitlets.CFloat(1.0)
350+
nx = traitlets.CInt(512)
351+
ny = traitlets.CInt(512)
352+
353+
@cached_property
354+
def camera_distance(self):
355+
r"""float: Distance of the camera from the image plane."""
356+
return (
357+
(self.width / 2.0)
358+
/ np.tan(np.radians(self.fov_width / 2.0))
359+
)
360+
361+
@cached_property
362+
def camera_origin(self):
363+
r"""np.ndarray: Position of the camera."""
364+
return self.center - self.camera_distance * self.forward
365+
366+
@traitlets.default("east")
367+
def _default_east(self):
368+
return np.cross(self.forward, self.up)
369+
370+
@traitlets.default("directions")
371+
def _default_directions(self):
372+
self._directions = self.origins - self.camera_origin
373+
norms = np.linalg.norm(self.origins, axis=1)
374+
for i in range(3):
375+
self._directions[:, i] /= norms
376+
return self._directions.view()
377+
378+
@traitlets.default("origins")
379+
def _default_origins(self):
380+
# here origin is not the center, but the bottom left
381+
self._origins = np.zeros((self.nx, self.ny, 3), dtype="f4")
382+
offset_x, offset_y = np.mgrid[
383+
(-self.width / 2):(self.width / 2):(self.nx * 1j),
384+
(-self.height / 2):(self.height / 2):(self.ny * 1j),
385+
]
386+
self._origins[:] = (
387+
self.center
388+
+ offset_x[..., None] * self.east
389+
+ offset_y[..., None] * self.up
390+
)
391+
return self._origins.view().reshape((self.nx * self.ny, 3))
290392

291393

292394
class SphericalRayBlaster(RayBlaster):

hothouse/scene.py

Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import traitlets
33
import pythreejs
44
import numpy as np
5+
import functools
56
import pvlib
67
# from IPython.core.display import display
78

@@ -13,10 +14,13 @@
1314
from pyembree import rtcore_scene as rtcs
1415
from pyembree.mesh_construction import TriangleMesh
1516

17+
cached_property = getattr(functools, "cached_property", property)
18+
1619

1720
class 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

Comments
 (0)