Heightmaps¶

This notebook introduces surfaces parameterized by heightmaps.

In [1]:
# !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.

In [2]:
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)
Out[2]:

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.

In [3]:
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
Out[3]:

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.

In [4]:
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.

In [5]:
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.

In [6]:
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])
Out[6]:

Vertical Heightmaps¶

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.

In [7]:
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))
Out[7]:

2D Heightmaps¶

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.

In [8]:
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.

In [9]:
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.

Combining Heightmaps¶

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.

In [10]:
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.