Geometry via SDF¶

Google Collab Book

This notebook introduces signed distance fields in ΦFlow.

In [1]:
# %pip install phiflow
from phi.flow import *

SDF From Existing Geometry¶

Signed distance fields can easily be created from existing geometry. The next cell creates a SDF from a pair of spheres.

In [2]:
spheres = Sphere(vec(x=[1, 2], y=1), radius=.8)
bounds = Box(x=3, y=2)
sdf = geom.sdf_from_geometry(spheres, bounds, x=10, y=10)
plot({"SDF": sdf.values, "Surface": [spheres, sdf]}, overlay='list')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 3
      1 spheres = Sphere(vec(x=[1, 2], y=1), radius=.8)
      2 bounds = Box(x=3, y=2)
----> 3 sdf = geom.sdf_from_geometry(spheres, bounds, x=10, y=10)
      4 plot({"SDF": sdf.values, "Surface": [spheres, sdf]}, overlay='list')

AttributeError: module 'phi.geom' has no attribute 'sdf_from_geometry'

Custom SDF Construction¶

Next, let's construct a SDF from a 2D NumPy array.

In [3]:
bounds = Box(x=(-10, 10), y=(-10, 10))
grid_x, grid_y = np.meshgrid(np.linspace(-10, 10, 100), np.linspace(-10, 10, 100))
sdf_np = np.sqrt(grid_x**2 + grid_y**2) - 5
sdf_tensor = tensor(sdf_np, spatial('x,y'))
sdf = geom.SDFGrid(sdf_tensor, bounds)
plot(sdf_tensor, sdf)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 6
      4 sdf_tensor = tensor(sdf_np, spatial('x,y'))
      5 sdf = geom.SDFGrid(sdf_tensor, bounds)
----> 6 plot(sdf_tensor, sdf)

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_vis.py:291, 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)
    289     min_val = max_val = None
    290 # --- Layout ---
--> 291 subplots = {pos: _space(*fields, ignore_dims=animate, log_dims=log_dims, errs=[err[i] for i in indices[pos]]) for pos, fields in positioning.items()}
    292 subplots = {pos: _insert_value_dim(space, pos, subplots, min_val, max_val) for pos, space in subplots.items()}
    293 if same_scale:

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_vis.py:400, in _space(ignore_dims, log_dims, errs, *values)
    398 all_dims = []
    399 for f, e in zip(values, errs):
--> 400     for dim in get_default_limits(f, None, log_dims, e).vector.item_names:
    401         if dim not in all_dims and dim not in ignore_dims:
    402             all_dims.append(dim)

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_functional.py:1257, in broadcast.<locals>.broadcast_(*args, **kwargs)
   1255 @wraps(function)
   1256 def broadcast_(*args, **kwargs):
-> 1257     return map_(function, *args, dims=dims, range=range, unwrap_scalars=unwrap_scalars, simplify=simplify, map_name=name, **kwargs)

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_functional.py:1389, in map_(function, dims, range, unwrap_scalars, expand_results, simplify, map_name, *args, **kwargs)
   1387     idx_extra_args = list(extra_args)
   1388     idx_all_args = [idx_args.pop(0) if isinstance(v, Shapable) else idx_extra_args.pop(0) for v in args]
-> 1389     f_output = function(*idx_all_args, **idx_kwargs, **extra_kwargs)
   1390     results.append(f_output)
   1391 if isinstance(results[0], tuple):

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/vis/_vis_base.py:546, in get_default_limits(f, all_dims, log_dims, err)
    544     return data_bounds(f) * value_limits
    545 # --- Determine element size ---
--> 546 f_dims = f.geometry.vector.item_names
    547 value_axis = f.spatial_rank <= 1
    548 if value_axis:

AttributeError: 'SDFGrid' object has no attribute 'vector'

Sampling¶

SDFs behave like any other geomoetry. They can be resampled to grids and other fields.

In [4]:
to_grid = CenteredGrid(sdf, 0, bounds, x=40, y=40)
soft = resample(sdf, to_grid, soft=True)
plot(to_grid, soft)
/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[4]:
In [5]:
sdf.lies_inside(vec(x=0, y=0))
Out[5]:
True

Querying the Surface¶

SDFs support querying the closest surface point and normal vector.

In [6]:
loc = geom.UniformGrid(sdf.resolution.with_sizes(10), sdf.bounds).center
sgn_dist, delta, normal, *_ = sdf.approximate_closest_surface(loc)
plot(PointCloud(loc, delta))
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[6], line 2
      1 loc = geom.UniformGrid(sdf.resolution.with_sizes(10), sdf.bounds).center
----> 2 sgn_dist, delta, normal, *_ = sdf.approximate_closest_surface(loc)
      3 plot(PointCloud(loc, delta))

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phi/geom/_sdf_grid.py:166, in SDFGrid.approximate_closest_surface(self, location)
    164     to_surf = math.grid_sample(self._to_surface, float_idx - .5, math.extrapolation.ZERO_GRADIENT)
    165 else:
--> 166     sdf_grad = math.grid_sample(self._grad, float_idx - 1, math.extrapolation.ZERO_GRADIENT)
    167     sdf_grad = math.vec_normalize(sdf_grad)  # theoretically not necessary
    168     to_surf = sgn_dist * -sdf_grad

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/phiml/math/_ops.py:1148, in grid_sample(grid, coordinates, extrap, **kwargs)
   1146 assert channel(coordinates).rank == 1, f"coordinates must have at most one channel dimension but got {channel(coordinates)}"
   1147 coordinates = rename_dims(coordinates, channel, 'vector')
-> 1148 result = broadcast_op(functools.partial(_grid_sample, extrap=extrap, pad_kwargs=kwargs), [grid, coordinates])
   1149 return result

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:1161, in _grid_sample(grid, coordinates, extrap, pad_kwargs)
   1153 """
   1154 Args:
   1155     grid:
   (...)   1158     pad_kwargs:
   1159 """
   1160 dim_names = channel(coordinates).labels[0] or grid.shape.spatial.names
-> 1161 dims = grid.shape.only(dim_names, reorder=True)
   1162 assert len(dims) == len(dim_names), f"all grid dims {dim_names} must be present on grid but got shape {grid.shape}"
   1163 if grid.shape.batch == coordinates.shape.batch or grid.shape.batch.volume == 1 or coordinates.shape.batch.volume == 1:
   1164     # call backend.grid_sample()

AttributeError: 'NoneType' object has no attribute 'shape'