This notebook introduces surfaces parameterized by heightmaps.
# !pip install git+https://github.com/tum-pbs/PhiFlow@3.0
from phi.flow import *
Heightmaps can be used in 2D or 3D. They encode a grid surface made up of lines or planes, respectively. The grid vertices are equally spaced and the displacement (height) of each point is specified via a 1D or 2D tensor.
The following code creates a heightmap in 2D from a list of height values.
height = wrap([.1, .02, 0, 0, 1, .95, .8, .5, 0], spatial('x'))
bounds = Box(x=2, y=1)
heightmap = geom.Heightmap(height, bounds, max_dist=.1)
plot(heightmap)
The heightmap surface separates the inside from the outside.
Whether the inside is below or above the height values can be set in the Heightmap
constructor.
is_inside = CenteredGrid(lambda loc: heightmap.lies_inside(loc), x=100, y=100, bounds=Box(x=2, y=2))
plot(is_inside)
/opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_matplotlib/_matplotlib_plots.py:167: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. plt.tight_layout() # because subplot titles can be added after figure creation
While the inside check is always exact, the distance from the surface is an approximation.
For small distances, the face directly underneath a point will usually be closest but for farther points this is not always true.
To account for this, each face links to one other face that will be queried for larger distances.
Finding these secondary faces is performed during heightmap construction where the parameter max_dist
influences which other faces are the most important to link.
Above, we set max_dist=0.1
. In the following cell, we visualize the distance from the surface.
distance = CenteredGrid(lambda loc: heightmap.approximate_signed_distance(loc), x=100, y=100, bounds=Box(x=2, y=2))
plot(distance, heightmap, overlay='args')
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[4], line 1 ----> 1 distance = CenteredGrid(lambda loc: heightmap.approximate_signed_distance(loc), x=100, y=100, bounds=Box(x=2, y=2)) 2 plot(distance, heightmap, overlay='args') File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/field/_grid.py:75, in CenteredGrid(values, boundary, bounds, resolution, extrapolation, convert, **resolution_) 73 values = sample(values, elements) 74 elif callable(values): ---> 75 values = sample_function(values, elements, 'center', extrapolation) 76 else: 77 if isinstance(values, (tuple, list)) and len(values) == resolution.rank: File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_geom.py:853, in sample_function(f, elements, at, extrapolation) 851 values = math.map_s2b(f)(*pos.vector) 852 else: --> 853 values = math.map_s2b(f)(pos) 854 assert isinstance(values, math.Tensor), f"values function must return a Tensor but returned {type(values)}" 855 return values File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_functional.py:1203, in map_types.<locals>.retyped_f(*args, **kwargs) 1201 retyped_kwarg, input_types = forward_retype(v, input_types) 1202 retyped_kwargs[k] = retyped_kwarg -> 1203 output = f(*retyped_args, **retyped_kwargs) 1204 restored_output = reverse_retype(output, input_types) 1205 return restored_output Cell In[4], line 1, in <lambda>(loc) ----> 1 distance = CenteredGrid(lambda loc: heightmap.approximate_signed_distance(loc), x=100, y=100, bounds=Box(x=2, y=2)) 2 plot(distance, heightmap, overlay='args') File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:218, in Heightmap.approximate_signed_distance(self, location) 217 def approximate_signed_distance(self, location: Tensor) -> Tensor: --> 218 return self.approximate_closest_surface(location)[0] File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:158, in Heightmap.approximate_closest_surface(self, location) 156 # --- use closest face from considered --- 157 delta = math.where(projects_onto_face, proj_delta, delta_highest) --> 158 return math.at_min((distances, delta, normals, offsets, face_idx), key=abs(distances), dim=batch('consider') & instance(self).as_batch()) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:2012, in at_min(value, key, dim) 2010 if not shape(key).only(dim): 2011 return value -> 2012 idx = argmin(key, dim) 2013 return slice_(value, idx) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:2105, in argmin(x, dim, index_dim) 2103 multi_idx_native = choose_backend(idx_native).unravel_index(idx_native[:, 0], dims.sizes) 2104 return reshaped_tensor(multi_idx_native, [keep - broadcast, index_dim.with_size(dims.name_list)]) -> 2105 return broadcast_op(uniform_argmin, [x], broadcast) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:1220, in broadcast_op(operation, tensors, iter_dims, no_return) 1218 iter_dims = broadcast_dims(*tensors) if iter_dims is None else iter_dims 1219 if len(iter_dims) == 0: -> 1220 return operation(*tensors) 1221 else: 1222 if isinstance(iter_dims, SHAPE_TYPES): File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:2103, in argmin.<locals>.uniform_argmin(x) 2101 v_native = x._reshaped_native([keep - broadcast, dims]) 2102 idx_native = x.backend.argmin(v_native, 1, keepdims=True) -> 2103 multi_idx_native = choose_backend(idx_native).unravel_index(idx_native[:, 0], dims.sizes) 2104 return reshaped_tensor(multi_idx_native, [keep - broadcast, index_dim.with_size(dims.name_list)]) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/backend/_numpy_backend.py:360, in NumPyBackend.unravel_index(self, flat_index, shape) 359 def unravel_index(self, flat_index, shape): --> 360 return np.stack(np.unravel_index(flat_index, shape), -1) TypeError: 'NoneType' object cannot be interpreted as an integer
With max_dist=0.1
, the values further than 0.1 away from the cliff get inaccurate distances.
Let's increase `max_dist´ to see the difference.
heightmap = geom.Heightmap(height, bounds, max_dist=1)
distance = CenteredGrid(lambda loc: heightmap.approximate_signed_distance(loc), x=100, y=100, bounds=Box(x=2, y=2))
plot(distance, heightmap, overlay='args')
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[5], line 2 1 heightmap = geom.Heightmap(height, bounds, max_dist=1) ----> 2 distance = CenteredGrid(lambda loc: heightmap.approximate_signed_distance(loc), x=100, y=100, bounds=Box(x=2, y=2)) 3 plot(distance, heightmap, overlay='args') File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/field/_grid.py:75, in CenteredGrid(values, boundary, bounds, resolution, extrapolation, convert, **resolution_) 73 values = sample(values, elements) 74 elif callable(values): ---> 75 values = sample_function(values, elements, 'center', extrapolation) 76 else: 77 if isinstance(values, (tuple, list)) and len(values) == resolution.rank: File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_geom.py:853, in sample_function(f, elements, at, extrapolation) 851 values = math.map_s2b(f)(*pos.vector) 852 else: --> 853 values = math.map_s2b(f)(pos) 854 assert isinstance(values, math.Tensor), f"values function must return a Tensor but returned {type(values)}" 855 return values File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_functional.py:1203, in map_types.<locals>.retyped_f(*args, **kwargs) 1201 retyped_kwarg, input_types = forward_retype(v, input_types) 1202 retyped_kwargs[k] = retyped_kwarg -> 1203 output = f(*retyped_args, **retyped_kwargs) 1204 restored_output = reverse_retype(output, input_types) 1205 return restored_output Cell In[5], line 2, in <lambda>(loc) 1 heightmap = geom.Heightmap(height, bounds, max_dist=1) ----> 2 distance = CenteredGrid(lambda loc: heightmap.approximate_signed_distance(loc), x=100, y=100, bounds=Box(x=2, y=2)) 3 plot(distance, heightmap, overlay='args') File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:218, in Heightmap.approximate_signed_distance(self, location) 217 def approximate_signed_distance(self, location: Tensor) -> Tensor: --> 218 return self.approximate_closest_surface(location)[0] File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:158, in Heightmap.approximate_closest_surface(self, location) 156 # --- use closest face from considered --- 157 delta = math.where(projects_onto_face, proj_delta, delta_highest) --> 158 return math.at_min((distances, delta, normals, offsets, face_idx), key=abs(distances), dim=batch('consider') & instance(self).as_batch()) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:2012, in at_min(value, key, dim) 2010 if not shape(key).only(dim): 2011 return value -> 2012 idx = argmin(key, dim) 2013 return slice_(value, idx) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:2105, in argmin(x, dim, index_dim) 2103 multi_idx_native = choose_backend(idx_native).unravel_index(idx_native[:, 0], dims.sizes) 2104 return reshaped_tensor(multi_idx_native, [keep - broadcast, index_dim.with_size(dims.name_list)]) -> 2105 return broadcast_op(uniform_argmin, [x], broadcast) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:1220, in broadcast_op(operation, tensors, iter_dims, no_return) 1218 iter_dims = broadcast_dims(*tensors) if iter_dims is None else iter_dims 1219 if len(iter_dims) == 0: -> 1220 return operation(*tensors) 1221 else: 1222 if isinstance(iter_dims, SHAPE_TYPES): File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:2103, in argmin.<locals>.uniform_argmin(x) 2101 v_native = x._reshaped_native([keep - broadcast, dims]) 2102 idx_native = x.backend.argmin(v_native, 1, keepdims=True) -> 2103 multi_idx_native = choose_backend(idx_native).unravel_index(idx_native[:, 0], dims.sizes) 2104 return reshaped_tensor(multi_idx_native, [keep - broadcast, index_dim.with_size(dims.name_list)]) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/backend/_numpy_backend.py:360, in NumPyBackend.unravel_index(self, flat_index, shape) 359 def unravel_index(self, flat_index, shape): --> 360 return np.stack(np.unravel_index(flat_index, shape), -1) TypeError: 'NoneType' object cannot be interpreted as an integer
Now larger distances are covered as well. This runs with the same number of computations but trades off accuracy closer to the surface.
We can additionally query the direction to the closest surface point as well as the corresponding normal vector.
Let's plot these vectors for random points inside (orange) and outside (red) of the heightmap.
points = Box(x=3, y=(-1, 1)).sample_uniform(instance(points=200))
sgn_dist, delta, normal, _, face_index = heightmap.approximate_closest_surface(points)
plot({'closest': [heightmap, PointCloud(points, .1 * math.vec_normalize(delta))],
'normal': [heightmap, PointCloud(points, .2 * normal)]}, overlay='list', color=[0, heightmap.lies_inside(points)*2+1])
The grid orientation is determined by which spatial dimension is not part of the grid. E.g. to create a vertical heightmap, the displacement values should be listed along the y direction.
height = wrap([.1, .02, 0, 0, 1, .95, .8, .5, 0], spatial('y'))
bounds = Box(x=1, y=2)
plot(geom.Heightmap(height, bounds, max_dist=.1))
Adding a third dimension z
yields a 2D heightmap in 3D space.
Here, we create height values from a function, but the rest stays the same.
bounds = Box(x=2, y=2, z=1)
height = CenteredGrid(lambda pos: math.exp(-math.vec_squared(pos-1) * 3), 0, bounds['x,y'], x=10, y=10).values
heightmap = geom.Heightmap(height, bounds, max_dist=.1)
show(heightmap)
/tmp/ipykernel_2900/959595594.py:2: DeprecationWarning: phiml.math.vec_squared is deprecated in favor of phiml.math.squared_norm height = CenteredGrid(lambda pos: math.exp(-math.vec_squared(pos-1) * 3), 0, bounds['x,y'], x=10, y=10).values
Again, we can sample directions and distances from the surface.
points = bounds.sample_uniform(instance(points=200))
sgn_dist, delta, normal, _, face_index = heightmap.approximate_closest_surface(points)
show({'closest': [heightmap, PointCloud(points, .1 * math.vec_normalize(delta))],
'normal': [heightmap, PointCloud(points, .2 * normal)]}, overlay='list', color=[0, heightmap.lies_inside(points)*2+1])
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[9], line 3 1 points = bounds.sample_uniform(instance(points=200)) 2 sgn_dist, delta, normal, _, face_index = heightmap.approximate_closest_surface(points) ----> 3 show({'closest': [heightmap, PointCloud(points, .1 * math.vec_normalize(delta))], 4 'normal': [heightmap, PointCloud(points, .2 * normal)]}, overlay='list', color=[0, heightmap.lies_inside(points)*2+1]) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_vis.py:58, in show(lib, row_dims, col_dims, animate, overlay, title, size, same_scale, log_dims, show_color_bar, color, alpha, err, frame_time, repeat, plt_params, max_subfigures, *fields) 56 kwargs = locals() 57 del kwargs['fields'] ---> 58 fig = plot(*fields, **kwargs) 59 plots = get_plots_by_figure(fig) 60 if isinstance(fig, Tensor): File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_vis.py:322, in plot(lib, row_dims, col_dims, animate, overlay, title, size, same_scale, log_dims, show_color_bar, color, alpha, err, frame_time, repeat, plt_params, max_subfigures, *fields) 320 for i, f in enumerate(fields): 321 idx = indices[pos][i] --> 322 plots.plot(f, figure, axes[pos], subplots[pos], min_val, max_val, show_color_bar, color[pos][i][idx], alpha[idx], err[idx]) 323 plots.finalize(figure) 324 LAST_FIGURE[0] = figure File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_vis_base.py:382, in PlottingLibrary.plot(self, data, figure, subplot, space, *args, **kwargs) 380 for recipe in self.recipes: 381 if recipe.can_plot(data, space): --> 382 recipe.plot(data, figure, subplot, space, *args, **kwargs) 383 return 384 raise NotImplementedError(f"No {self.name} recipe found for {data}. Recipes: {self.recipes}") File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_dash/_plotly_plots.py:325, in VectorCloud3D.plot(self, data, figure, subplot, space, min_val, max_val, show_color_bar, color, alpha, err) 323 x, y, z = math.reshaped_numpy(data.points.vector[dims], [vector, data.shape.non_channel]) 324 u, v, w = math.reshaped_numpy(data.values.vector[dims], [vector, extra_channels, data.shape.non_channel]) --> 325 figure.add_cone(x=x.flatten(), y=y.flatten(), z=z.flatten(), u=u.flatten(), v=v.flatten(), w=w.flatten(), 326 colorscale=colorscale, 327 sizemode='raw', anchor='tail', 328 row=row, col=col, opacity=float(alpha)) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/plotly/graph_objs/_figure.py:4507, in Figure.add_cone(self, anchor, autocolorscale, cauto, cmax, cmid, cmin, coloraxis, colorbar, colorscale, customdata, customdatasrc, hoverinfo, hoverinfosrc, hoverlabel, hovertemplate, hovertemplatesrc, hovertext, hovertextsrc, ids, idssrc, legend, legendgroup, legendgrouptitle, legendrank, legendwidth, lighting, lightposition, meta, metasrc, name, opacity, reversescale, scene, showlegend, showscale, sizemode, sizeref, stream, text, textsrc, u, uhoverformat, uid, uirevision, usrc, v, vhoverformat, visible, vsrc, w, whoverformat, wsrc, x, xhoverformat, xsrc, y, yhoverformat, ysrc, z, zhoverformat, zsrc, row, col, **kwargs) 4143 """ 4144 Add a new Cone trace 4145 (...) 4503 Figure 4504 """ 4505 from plotly.graph_objs import Cone -> 4507 new_trace = Cone( 4508 anchor=anchor, 4509 autocolorscale=autocolorscale, 4510 cauto=cauto, 4511 cmax=cmax, 4512 cmid=cmid, 4513 cmin=cmin, 4514 coloraxis=coloraxis, 4515 colorbar=colorbar, 4516 colorscale=colorscale, 4517 customdata=customdata, 4518 customdatasrc=customdatasrc, 4519 hoverinfo=hoverinfo, 4520 hoverinfosrc=hoverinfosrc, 4521 hoverlabel=hoverlabel, 4522 hovertemplate=hovertemplate, 4523 hovertemplatesrc=hovertemplatesrc, 4524 hovertext=hovertext, 4525 hovertextsrc=hovertextsrc, 4526 ids=ids, 4527 idssrc=idssrc, 4528 legend=legend, 4529 legendgroup=legendgroup, 4530 legendgrouptitle=legendgrouptitle, 4531 legendrank=legendrank, 4532 legendwidth=legendwidth, 4533 lighting=lighting, 4534 lightposition=lightposition, 4535 meta=meta, 4536 metasrc=metasrc, 4537 name=name, 4538 opacity=opacity, 4539 reversescale=reversescale, 4540 scene=scene, 4541 showlegend=showlegend, 4542 showscale=showscale, 4543 sizemode=sizemode, 4544 sizeref=sizeref, 4545 stream=stream, 4546 text=text, 4547 textsrc=textsrc, 4548 u=u, 4549 uhoverformat=uhoverformat, 4550 uid=uid, 4551 uirevision=uirevision, 4552 usrc=usrc, 4553 v=v, 4554 vhoverformat=vhoverformat, 4555 visible=visible, 4556 vsrc=vsrc, 4557 w=w, 4558 whoverformat=whoverformat, 4559 wsrc=wsrc, 4560 x=x, 4561 xhoverformat=xhoverformat, 4562 xsrc=xsrc, 4563 y=y, 4564 yhoverformat=yhoverformat, 4565 ysrc=ysrc, 4566 z=z, 4567 zhoverformat=zhoverformat, 4568 zsrc=zsrc, 4569 **kwargs, 4570 ) 4571 return self.add_trace(new_trace, row=row, col=col) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/plotly/graph_objs/_cone.py:2203, in Cone.__init__(self, arg, anchor, autocolorscale, cauto, cmax, cmid, cmin, coloraxis, colorbar, colorscale, customdata, customdatasrc, hoverinfo, hoverinfosrc, hoverlabel, hovertemplate, hovertemplatesrc, hovertext, hovertextsrc, ids, idssrc, legend, legendgroup, legendgrouptitle, legendrank, legendwidth, lighting, lightposition, meta, metasrc, name, opacity, reversescale, scene, showlegend, showscale, sizemode, sizeref, stream, text, textsrc, u, uhoverformat, uid, uirevision, usrc, v, vhoverformat, visible, vsrc, w, whoverformat, wsrc, x, xhoverformat, xsrc, y, yhoverformat, ysrc, z, zhoverformat, zsrc, **kwargs) 2201 self._set_property("coloraxis", arg, coloraxis) 2202 self._set_property("colorbar", arg, colorbar) -> 2203 self._set_property("colorscale", arg, colorscale) 2204 self._set_property("customdata", arg, customdata) 2205 self._set_property("customdatasrc", arg, customdatasrc) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/plotly/basedatatypes.py:4403, in BasePlotlyType._set_property(self, name, arg, provided) 4397 def _set_property(self, name, arg, provided): 4398 """ 4399 Initialize a property of this object using the provided value 4400 or a value popped from the arguments dictionary. If neither 4401 is available, do not set the property. 4402 """ -> 4403 _set_property_provided_value(self, name, arg, provided) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/plotly/basedatatypes.py:398, in _set_property_provided_value(obj, name, arg, provided) 396 val = provided if provided is not None else val 397 if val is not None: --> 398 obj[name] = val File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/plotly/basedatatypes.py:4932, in BasePlotlyType.__setitem__(self, prop, value) 4928 self._set_array_prop(prop, value) 4930 # ### Handle simple property ### 4931 else: -> 4932 self._set_prop(prop, value) 4933 else: 4934 # Make sure properties dict is initialized 4935 self._init_props() File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/plotly/basedatatypes.py:5276, in BasePlotlyType._set_prop(self, prop, val) 5274 return 5275 else: -> 5276 raise err 5278 # val is None 5279 # ----------- 5280 if val is None: 5281 # Check if we should send null update File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/plotly/basedatatypes.py:5271, in BasePlotlyType._set_prop(self, prop, val) 5268 validator = self._get_validator(prop) 5270 try: -> 5271 val = validator.validate_coerce(val) 5272 except ValueError as err: 5273 if self._skip_invalid: File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/_plotly_utils/basevalidators.py:1636, in ColorscaleValidator.validate_coerce(self, v) 1631 v = [ 1632 [e[0], ColorValidator.perform_validate_coerce(e[1])] for e in v 1633 ] 1635 if not v_valid: -> 1636 self.raise_invalid_val(v) 1638 return v File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/_plotly_utils/basevalidators.py:298, in BaseValidator.raise_invalid_val(self, v, inds) 295 for i in inds: 296 name += "[" + str(i) + "]" --> 298 raise ValueError( 299 """ 300 Invalid value of type {typ} received for the '{name}' property of {pname} 301 Received value: {v} 302 303 {valid_clr_desc}""".format( 304 name=name, 305 pname=self.parent_name, 306 typ=type_str(v), 307 v=repr(v), 308 valid_clr_desc=self.description(), 309 ) 310 ) ValueError: Invalid value of type 'builtins.list' received for the 'colorscale' property of cone Received value: [[0, ['rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)']], [1, ['rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(214, 39, 40)', 'rgb(255, 127, 14)', 'rgb(255, 127, 14)']]] The 'colorscale' property is a colorscale and may be specified as: - A list of colors that will be spaced evenly to create the colorscale. Many predefined colorscale lists are included in the sequential, diverging, and cyclical modules in the plotly.colors package. - A list of 2-element lists where the first element is the normalized color level value (starting at 0 and ending at 1), and the second item is a valid color string. (e.g. [[0, 'green'], [0.5, 'red'], [1.0, 'rgb(0, 0, 255)']]) - One of the following named colorscales: ['aggrnyl', 'agsunset', 'algae', 'amp', 'armyrose', 'balance', 'blackbody', 'bluered', 'blues', 'blugrn', 'bluyl', 'brbg', 'brwnyl', 'bugn', 'bupu', 'burg', 'burgyl', 'cividis', 'curl', 'darkmint', 'deep', 'delta', 'dense', 'earth', 'edge', 'electric', 'emrld', 'fall', 'geyser', 'gnbu', 'gray', 'greens', 'greys', 'haline', 'hot', 'hsv', 'ice', 'icefire', 'inferno', 'jet', 'magenta', 'magma', 'matter', 'mint', 'mrybm', 'mygbm', 'oranges', 'orrd', 'oryel', 'oxy', 'peach', 'phase', 'picnic', 'pinkyl', 'piyg', 'plasma', 'plotly3', 'portland', 'prgn', 'pubu', 'pubugn', 'puor', 'purd', 'purp', 'purples', 'purpor', 'rainbow', 'rdbu', 'rdgy', 'rdpu', 'rdylbu', 'rdylgn', 'redor', 'reds', 'solar', 'spectral', 'speed', 'sunset', 'sunsetdark', 'teal', 'tealgrn', 'tealrose', 'tempo', 'temps', 'thermal', 'tropic', 'turbid', 'turbo', 'twilight', 'viridis', 'ylgn', 'ylgnbu', 'ylorbr', 'ylorrd']. Appending '_r' to a named colorscale reverses it.
Like all Geometry
objects, heightmaps can be merged using the union
function.
If we only pass Heightmap
objects with the same grid resolution, ΦFlow will automatically vectorize computations so that the latency does not increase.
We can use this to create complex obstacles with detailed surfaces on all sides or to create cavities.
height = (1 - math.linspace(-1, 1, spatial(x=10)) ** 2) ** .5
upper_heightmap = geom.Heightmap(height, Box(x=(-1, 1), y=None), max_dist=.1, fill_below=False, extrapolation=0)
lower_heightmap = geom.Heightmap(-height, Box(x=(-1, 1), y=None), max_dist=.1, fill_below=True, extrapolation=0)
heightmap = union(lower_heightmap, upper_heightmap)
points = Box(x=(-2, 2), y=(-2, 2)).sample_uniform(instance(points=200))
sgn_dist, delta, normal, _, face_index = heightmap.approximate_closest_surface(points)
show({'closest': [heightmap, PointCloud(points, .1 * math.vec_normalize(delta))],
'normal': [heightmap, PointCloud(points, .2 * normal)]}, overlay='list', color=[0, heightmap.lies_inside(points)*2+1])
--------------------------------------------------------------------------- RecursionError Traceback (most recent call last) Cell In[10], line 4 2 upper_heightmap = geom.Heightmap(height, Box(x=(-1, 1), y=None), max_dist=.1, fill_below=False, extrapolation=0) 3 lower_heightmap = geom.Heightmap(-height, Box(x=(-1, 1), y=None), max_dist=.1, fill_below=True, extrapolation=0) ----> 4 heightmap = union(lower_heightmap, upper_heightmap) 6 points = Box(x=(-2, 2), y=(-2, 2)).sample_uniform(instance(points=200)) 7 sgn_dist, delta, normal, _, face_index = heightmap.approximate_closest_surface(points) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_geom_ops.py:317, in union(dim, *geometries) 315 return geometries[0] 316 elif all(type(g) == type(geometries[0]) and isinstance(g, PhiTreeNode) for g in geometries): --> 317 return stack(tuple(geometries), dim, simplify=True) 318 else: 319 return GeometryStack(layout(geometries, dim)) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_magic_ops.py:268, in stack(values, dim, expand_values, simplify, layout_non_matching, **kwargs) 266 else: 267 new_attrs[a] = stack(a_values, dim, expand_values=expand_values, simplify=simplify, **kwargs) --> 268 return copy_with(values[0], **new_attrs) 269 else: 270 warnings.warn(f"Failed to concat values using value attributes because attributes differ among values {values}") File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_magic_ops.py:984, in replace(obj, **updates) 982 return dataclasses.replace(obj, **updates) 983 else: --> 984 cpy = copy.copy(obj) 985 for attr, value in updates.items(): 986 setattr(cpy, attr, value) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/copy.py:97, in copy(x) 95 if isinstance(rv, str): 96 return x ---> 97 return _reconstruct(x, None, *rv) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/copy.py:260, in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy) 258 if deep: 259 state = deepcopy(state, memo) --> 260 if hasattr(y, '__setstate__'): 261 y.__setstate__(state) 262 else: File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:236, in Heightmap.__getattr__(self, item) 235 def __getattr__(self, item): --> 236 if item in self.shape: 237 return BoundDim(self, item) 238 raise AttributeError(f"{self.__class__.__name__} has no attribute '{item}'") File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:87, in Heightmap.shape(self) 85 @property 86 def shape(self) -> Shape: ---> 87 return (self._height.shape - 1) & channel(self._bounds) File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:236, in Heightmap.__getattr__(self, item) 235 def __getattr__(self, item): --> 236 if item in self.shape: 237 return BoundDim(self, item) 238 raise AttributeError(f"{self.__class__.__name__} has no attribute '{item}'") File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:87, in Heightmap.shape(self) 85 @property 86 def shape(self) -> Shape: ---> 87 return (self._height.shape - 1) & channel(self._bounds) [... skipping similar frames: Heightmap.__getattr__ at line 236 (1483 times), Heightmap.shape at line 87 (1483 times)] File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:236, in Heightmap.__getattr__(self, item) 235 def __getattr__(self, item): --> 236 if item in self.shape: 237 return BoundDim(self, item) 238 raise AttributeError(f"{self.__class__.__name__} has no attribute '{item}'") File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_heightmap.py:87, in Heightmap.shape(self) 85 @property 86 def shape(self) -> Shape: ---> 87 return (self._height.shape - 1) & channel(self._bounds) RecursionError: maximum recursion depth exceeded
Here, we set extrapolation=0
to extend the heightmap beyond its bounds
.
Consequently, points with x < -1 or x > 1 count as inside the mesh and are colored red correspondingly.